1 /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 /* This Source Code Form is subject to the terms of the Mozilla Public
3 * License, v. 2.0. If a copy of the MPL was not distributed with this
4 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6 #ifndef HTMLEditUtils_h
7 #define HTMLEditUtils_h
10 * This header declares/defines static helper methods as members of
11 * HTMLEditUtils. If you want to create or look for helper trivial classes for
12 * HTMLEditor, see HTMLEditHelpers.h.
15 #include "EditorBase.h"
16 #include "EditorDOMPoint.h"
17 #include "EditorForwards.h"
18 #include "EditorUtils.h"
19 #include "HTMLEditHelpers.h"
21 #include "mozilla/Attributes.h"
22 #include "mozilla/EnumSet.h"
23 #include "mozilla/IntegerRange.h"
24 #include "mozilla/Maybe.h"
25 #include "mozilla/Result.h"
26 #include "mozilla/dom/AbstractRange.h"
27 #include "mozilla/dom/AncestorIterator.h"
28 #include "mozilla/dom/Element.h"
29 #include "mozilla/dom/HTMLBRElement.h"
30 #include "mozilla/dom/Selection.h"
31 #include "mozilla/dom/Text.h"
33 #include "nsContentUtils.h"
35 #include "nsGkAtoms.h"
36 #include "nsHTMLTags.h"
44 enum class CollectChildrenOption
{
45 // Ignore non-editable nodes
46 IgnoreNonEditableChildren
,
47 // Ignore invisible text nodes
48 IgnoreInvisibleTextNodes
,
49 // Collect list children too.
51 // Collect table children too.
55 class HTMLEditUtils final
{
56 using AbstractRange
= dom::AbstractRange
;
57 using Element
= dom::Element
;
58 using Selection
= dom::Selection
;
59 using Text
= dom::Text
;
62 static constexpr char16_t kNewLine
= '\n';
63 static constexpr char16_t kCarriageReturn
= '\r';
64 static constexpr char16_t kTab
= '\t';
65 static constexpr char16_t kSpace
= ' ';
66 static constexpr char16_t kNBSP
= 0x00A0;
67 static constexpr char16_t kGreaterThan
= '>';
70 * IsSimplyEditableNode() returns true when aNode is simply editable.
71 * This does NOT means that aNode can be removed from current parent nor
72 * aNode's data is editable.
74 static bool IsSimplyEditableNode(const nsINode
& aNode
) {
75 return aNode
.IsEditable();
79 * Return true if inclusive flat tree ancestor has `inert` state.
81 static bool ContentIsInert(const nsIContent
& aContent
);
84 * IsNeverContentEditableElementByUser() returns true if the element's content
85 * is never editable by user. E.g., the content is always replaced by
86 * native anonymous node or something.
88 static bool IsNeverElementContentsEditableByUser(const nsIContent
& aContent
) {
89 return aContent
.IsElement() &&
90 (!HTMLEditUtils::IsContainerNode(aContent
) ||
91 aContent
.IsAnyOfHTMLElements(
92 nsGkAtoms::applet
, nsGkAtoms::colgroup
, nsGkAtoms::frameset
,
93 nsGkAtoms::head
, nsGkAtoms::html
, nsGkAtoms::iframe
,
94 nsGkAtoms::meter
, nsGkAtoms::progress
, nsGkAtoms::select
,
95 nsGkAtoms::textarea
));
99 * IsNonEditableReplacedContent() returns true when aContent is an inclusive
100 * descendant of a replaced element whose content shouldn't be editable by
103 static bool IsNonEditableReplacedContent(const nsIContent
& aContent
) {
104 for (Element
* element
: aContent
.InclusiveAncestorsOfType
<Element
>()) {
105 if (element
->IsAnyOfHTMLElements(nsGkAtoms::select
, nsGkAtoms::option
,
106 nsGkAtoms::optgroup
)) {
114 * IsRemovalNode() returns true when parent of aContent is editable even
115 * if aContent isn't editable.
116 * This is a valid method to check it if you find the content from point
117 * of view of siblings or parents of aContent.
118 * Note that padding `<br>` element for empty editor and manual native
119 * anonymous content should be deletable even after `HTMLEditor` is destroyed
120 * because they are owned/managed by `HTMLEditor`.
122 static bool IsRemovableNode(const nsIContent
& aContent
) {
123 return EditorUtils::IsPaddingBRElementForEmptyEditor(aContent
) ||
124 aContent
.IsRootOfNativeAnonymousSubtree() ||
125 (aContent
.GetParentNode() &&
126 aContent
.GetParentNode()->IsEditable() &&
127 &aContent
!= aContent
.OwnerDoc()->GetBody() &&
128 &aContent
!= aContent
.OwnerDoc()->GetDocumentElement());
132 * IsRemovableFromParentNode() returns true when aContent is editable, has a
133 * parent node and the parent node is also editable.
134 * This is a valid method to check it if you find the content from point
135 * of view of descendants of aContent.
136 * Note that padding `<br>` element for empty editor and manual native
137 * anonymous content should be deletable even after `HTMLEditor` is destroyed
138 * because they are owned/managed by `HTMLEditor`.
140 static bool IsRemovableFromParentNode(const nsIContent
& aContent
) {
141 return EditorUtils::IsPaddingBRElementForEmptyEditor(aContent
) ||
142 aContent
.IsRootOfNativeAnonymousSubtree() ||
143 (aContent
.IsEditable() && aContent
.GetParentNode() &&
144 aContent
.GetParentNode()->IsEditable() &&
145 &aContent
!= aContent
.OwnerDoc()->GetBody() &&
146 &aContent
!= aContent
.OwnerDoc()->GetDocumentElement());
150 * CanContentsBeJoined() returns true if aLeftContent and aRightContent can be
153 static bool CanContentsBeJoined(const nsIContent
& aLeftContent
,
154 const nsIContent
& aRightContent
);
157 * Returns true if aContent is an element and it should be treated as a block.
159 * @param aBlockInlineCheck
160 * - If UseHTMLDefaultStyle, this returns true only for HTML elements which
161 * are defined as a block by the default style. I.e., non-HTML elements are
162 * always treated as inline.
163 * - If UseComputedDisplayOutsideStyle, this returns true for element nodes
164 * whose display-outside is not inline nor ruby. This is useful to get
165 * inclusive ancestor block element.
166 * - If UseComputedDisplayStyle, this returns true for element nodes whose
167 * display-outside is not inline or whose display-inside is flow-root and they
168 * do not appear as a form control. This is useful to check whether
169 * collapsible white-spaces at the element edges are visible or invisible or
170 * whether <br> element at end of the element is visible or invisible.
172 [[nodiscard
]] static bool IsBlockElement(const nsIContent
& aContent
,
173 BlockInlineCheck aBlockInlineCheck
);
176 * This is designed to check elements or non-element nodes which are layed out
177 * as inline. Therefore, inline-block etc and ruby are treated as inline.
178 * Note that invisible non-element nodes like comment nodes are also treated
181 * @param aBlockInlineCheck UseComputedDisplayOutsideStyle and
182 * UseComputedDisplayStyle return same result for
185 [[nodiscard
]] static bool IsInlineContent(const nsIContent
& aContent
,
186 BlockInlineCheck aBlockInlineCheck
);
189 * IsVisibleElementEvenIfLeafNode() returns true if aContent is an empty block
190 * element, a visible replaced element such as a form control. This does not
191 * check the layout information.
193 static bool IsVisibleElementEvenIfLeafNode(const nsIContent
& aContent
);
195 static bool IsInlineStyle(nsINode
* aNode
);
198 * IsDisplayOutsideInline() returns true if display-outside value is
199 * "inside". This does NOT flush the layout.
201 [[nodiscard
]] static bool IsDisplayOutsideInline(const Element
& aElement
);
204 * IsDisplayInsideFlowRoot() returns true if display-inline value of aElement
205 * is "flow-root". This does NOT flush the layout.
207 [[nodiscard
]] static bool IsDisplayInsideFlowRoot(const Element
& aElement
);
210 * Return true if aElement is a flex item or a grid item. This works only
211 * when aElement has a primary frame.
213 [[nodiscard
]] static bool IsFlexOrGridItem(const Element
& aElement
);
216 * IsRemovableInlineStyleElement() returns true if aElement is an inline
217 * element and can be removed or split to in order to modifying inline
220 static bool IsRemovableInlineStyleElement(Element
& aElement
);
223 * Return true if aTagName is one of the format element name of
224 * Document.execCommand("formatBlock").
226 [[nodiscard
]] static bool IsFormatTagForFormatBlockCommand(
227 const nsStaticAtom
& aTagName
) {
230 &aTagName
== nsGkAtoms::address
||
231 &aTagName
== nsGkAtoms::article
||
232 &aTagName
== nsGkAtoms::aside
||
233 &aTagName
== nsGkAtoms::blockquote
||
234 &aTagName
== nsGkAtoms::dd
||
235 &aTagName
== nsGkAtoms::div
||
236 &aTagName
== nsGkAtoms::dl
||
237 &aTagName
== nsGkAtoms::dt
||
238 &aTagName
== nsGkAtoms::footer
||
239 &aTagName
== nsGkAtoms::h1
||
240 &aTagName
== nsGkAtoms::h2
||
241 &aTagName
== nsGkAtoms::h3
||
242 &aTagName
== nsGkAtoms::h4
||
243 &aTagName
== nsGkAtoms::h5
||
244 &aTagName
== nsGkAtoms::h6
||
245 &aTagName
== nsGkAtoms::header
||
246 &aTagName
== nsGkAtoms::hgroup
||
247 &aTagName
== nsGkAtoms::main
||
248 &aTagName
== nsGkAtoms::nav
||
249 &aTagName
== nsGkAtoms::p
||
250 &aTagName
== nsGkAtoms::pre
||
251 &aTagName
== nsGkAtoms::section
;
256 * Return true if aContent is a format element of
257 * Document.execCommand("formatBlock").
259 [[nodiscard
]] static bool IsFormatElementForFormatBlockCommand(
260 const nsIContent
& aContent
) {
261 if (!aContent
.IsHTMLElement() ||
262 !aContent
.NodeInfo()->NameAtom()->IsStatic()) {
265 const nsStaticAtom
* tagName
= aContent
.NodeInfo()->NameAtom()->AsStatic();
266 return IsFormatTagForFormatBlockCommand(*tagName
);
270 * Return true if aTagName is one of the format element name of
271 * cmd_paragraphState.
273 [[nodiscard
]] static bool IsFormatTagForParagraphStateCommand(
274 const nsStaticAtom
& aTagName
) {
277 &aTagName
== nsGkAtoms::address
||
278 &aTagName
== nsGkAtoms::dd
||
279 &aTagName
== nsGkAtoms::dl
||
280 &aTagName
== nsGkAtoms::dt
||
281 &aTagName
== nsGkAtoms::h1
||
282 &aTagName
== nsGkAtoms::h2
||
283 &aTagName
== nsGkAtoms::h3
||
284 &aTagName
== nsGkAtoms::h4
||
285 &aTagName
== nsGkAtoms::h5
||
286 &aTagName
== nsGkAtoms::h6
||
287 &aTagName
== nsGkAtoms::p
||
288 &aTagName
== nsGkAtoms::pre
;
293 * Return true if aContent is a format element of cmd_paragraphState.
295 [[nodiscard
]] static bool IsFormatElementForParagraphStateCommand(
296 const nsIContent
& aContent
) {
297 if (!aContent
.IsHTMLElement() ||
298 !aContent
.NodeInfo()->NameAtom()->IsStatic()) {
301 const nsStaticAtom
* tagName
= aContent
.NodeInfo()->NameAtom()->AsStatic();
302 return IsFormatTagForParagraphStateCommand(*tagName
);
305 static bool IsNodeThatCanOutdent(nsINode
* aNode
);
306 static bool IsHeader(nsINode
& aNode
);
307 static bool IsListItem(const nsINode
* aNode
);
308 static bool IsTable(nsINode
* aNode
);
309 static bool IsTableRow(nsINode
* aNode
);
310 static bool IsAnyTableElement(const nsINode
* aNode
);
311 static bool IsAnyTableElementButNotTable(nsINode
* aNode
);
312 static bool IsTableCell(const nsINode
* aNode
);
313 static bool IsTableCellOrCaption(nsINode
& aNode
);
314 static bool IsAnyListElement(const nsINode
* aNode
);
315 static bool IsPre(const nsINode
* aNode
);
316 static bool IsImage(nsINode
* aNode
);
317 static bool IsLink(const nsINode
* aNode
);
318 static bool IsNamedAnchor(const nsINode
* aNode
);
319 static bool IsMozDiv(nsINode
* aNode
);
320 static bool IsMailCite(const Element
& aElement
);
321 static bool IsFormWidget(const nsINode
* aNode
);
322 static bool SupportsAlignAttr(nsINode
& aNode
);
324 static bool CanNodeContain(const nsINode
& aParent
, const nsIContent
& aChild
) {
325 switch (aParent
.NodeType()) {
326 case nsINode::ELEMENT_NODE
:
327 case nsINode::DOCUMENT_FRAGMENT_NODE
:
328 return HTMLEditUtils::CanNodeContain(*aParent
.NodeInfo()->NameAtom(),
334 static bool CanNodeContain(const nsINode
& aParent
,
335 const nsAtom
& aChildNodeName
) {
336 switch (aParent
.NodeType()) {
337 case nsINode::ELEMENT_NODE
:
338 case nsINode::DOCUMENT_FRAGMENT_NODE
:
339 return HTMLEditUtils::CanNodeContain(*aParent
.NodeInfo()->NameAtom(),
345 static bool CanNodeContain(const nsAtom
& aParentNodeName
,
346 const nsIContent
& aChild
) {
347 switch (aChild
.NodeType()) {
348 case nsINode::TEXT_NODE
:
349 case nsINode::COMMENT_NODE
:
350 case nsINode::CDATA_SECTION_NODE
:
351 case nsINode::ELEMENT_NODE
:
352 case nsINode::DOCUMENT_FRAGMENT_NODE
:
353 return HTMLEditUtils::CanNodeContain(aParentNodeName
,
354 *aChild
.NodeInfo()->NameAtom());
359 // XXX Only this overload does not check the node type. Therefore, only this
360 // handle Document and ProcessingInstructionTagName.
361 static bool CanNodeContain(const nsAtom
& aParentNodeName
,
362 const nsAtom
& aChildNodeName
) {
363 nsHTMLTag childTagEnum
;
364 if (&aChildNodeName
== nsGkAtoms::textTagName
) {
365 childTagEnum
= eHTMLTag_text
;
366 } else if (&aChildNodeName
== nsGkAtoms::commentTagName
||
367 &aChildNodeName
== nsGkAtoms::cdataTagName
) {
368 childTagEnum
= eHTMLTag_comment
;
371 nsHTMLTags::AtomTagToId(const_cast<nsAtom
*>(&aChildNodeName
));
374 nsHTMLTag parentTagEnum
=
375 nsHTMLTags::AtomTagToId(const_cast<nsAtom
*>(&aParentNodeName
));
376 return HTMLEditUtils::CanNodeContain(parentTagEnum
, childTagEnum
);
380 * CanElementContainParagraph() returns true if aElement can have a <p>
381 * element as its child or its descendant.
383 static bool CanElementContainParagraph(const Element
& aElement
) {
384 if (HTMLEditUtils::CanNodeContain(aElement
, *nsGkAtoms::p
)) {
388 // Even if the element cannot have a <p> element as a child, it can contain
389 // <p> element as a descendant if it's one of the following elements.
390 if (aElement
.IsAnyOfHTMLElements(nsGkAtoms::ol
, nsGkAtoms::ul
,
391 nsGkAtoms::dl
, nsGkAtoms::table
,
392 nsGkAtoms::thead
, nsGkAtoms::tbody
,
393 nsGkAtoms::tfoot
, nsGkAtoms::tr
)) {
397 // XXX Otherwise, Chromium checks the CSS box is a block, but we don't do it
403 * Return a point which can insert a node whose name is aTagName scanning
404 * from aPoint to its ancestor points.
406 template <typename EditorDOMPointType
>
407 static EditorDOMPoint
GetInsertionPointInInclusiveAncestor(
408 const nsAtom
& aTagName
, const EditorDOMPointType
& aPoint
,
409 const Element
* aAncestorLimit
= nullptr) {
410 if (MOZ_UNLIKELY(!aPoint
.IsInContentNode())) {
411 return EditorDOMPoint();
413 Element
* lastChild
= nullptr;
414 for (Element
* containerElement
:
415 aPoint
.template ContainerAs
<nsIContent
>()
416 ->template InclusiveAncestorsOfType
<Element
>()) {
417 if (!HTMLEditUtils::IsSimplyEditableNode(*containerElement
)) {
418 return EditorDOMPoint();
420 if (HTMLEditUtils::CanNodeContain(*containerElement
, aTagName
)) {
421 return lastChild
? EditorDOMPoint(lastChild
)
422 : aPoint
.template To
<EditorDOMPoint
>();
424 if (containerElement
== aAncestorLimit
) {
425 return EditorDOMPoint();
427 lastChild
= containerElement
;
429 return EditorDOMPoint();
433 * IsContainerNode() returns true if aContent is a container node.
435 static bool IsContainerNode(const nsIContent
& aContent
) {
437 // XXX Should this handle #cdata-section too?
438 if (aContent
.IsText()) {
439 tagEnum
= eHTMLTag_text
;
441 // XXX Why don't we use nsHTMLTags::AtomTagToId? Are there some
443 tagEnum
= nsHTMLTags::StringTagToId(aContent
.NodeName());
445 return HTMLEditUtils::IsContainerNode(tagEnum
);
449 * IsSplittableNode() returns true if aContent can split.
451 static bool IsSplittableNode(const nsIContent
& aContent
) {
452 if (!EditorUtils::IsEditableContent(aContent
,
453 EditorUtils::EditorType::HTML
) ||
454 !HTMLEditUtils::IsRemovableFromParentNode(aContent
)) {
457 if (aContent
.IsElement()) {
458 // XXX Perhaps, instead of using container, we should have "splittable"
459 // information in the DB. E.g., `<template>`, `<script>` elements
460 // can have children, but shouldn't be split.
461 return HTMLEditUtils::IsContainerNode(aContent
) &&
462 !aContent
.IsAnyOfHTMLElements(nsGkAtoms::body
, nsGkAtoms::button
,
463 nsGkAtoms::caption
, nsGkAtoms::table
,
464 nsGkAtoms::tbody
, nsGkAtoms::tfoot
,
465 nsGkAtoms::thead
, nsGkAtoms::tr
) &&
466 !HTMLEditUtils::IsNeverElementContentsEditableByUser(aContent
) &&
467 !HTMLEditUtils::IsNonEditableReplacedContent(aContent
);
469 return aContent
.IsText() && aContent
.Length() > 0;
473 * See execCommand spec:
474 * https://w3c.github.io/editing/execCommand.html#non-list-single-line-container
475 * https://w3c.github.io/editing/execCommand.html#single-line-container
477 static bool IsNonListSingleLineContainer(const nsINode
& aNode
);
478 static bool IsSingleLineContainer(const nsINode
& aNode
);
481 * IsVisibleTextNode() returns true if aText has visible text. If it has
482 * only white-spaces and they are collapsed, returns false.
484 [[nodiscard
]] static bool IsVisibleTextNode(const Text
& aText
);
487 * IsInVisibleTextFrames() returns true if any text in aText is in visible
488 * text frames. Callers have to guarantee that there is no pending reflow.
490 static bool IsInVisibleTextFrames(nsPresContext
* aPresContext
,
494 * IsVisibleBRElement() and IsInvisibleBRElement() return true if aContent is
495 * a visible HTML <br> element, i.e., not a padding <br> element for making
496 * last line in a block element visible, or an invisible <br> element.
498 static bool IsVisibleBRElement(const nsIContent
& aContent
) {
499 if (const dom::HTMLBRElement
* brElement
=
500 dom::HTMLBRElement::FromNode(&aContent
)) {
501 return IsVisibleBRElement(*brElement
);
505 static bool IsVisibleBRElement(const dom::HTMLBRElement
& aBRElement
) {
506 // If followed by a block boundary without visible content, it's invisible
508 return !HTMLEditUtils::GetElementOfImmediateBlockBoundary(
509 aBRElement
, WalkTreeDirection::Forward
);
511 static bool IsInvisibleBRElement(const nsIContent
& aContent
) {
512 if (const dom::HTMLBRElement
* brElement
=
513 dom::HTMLBRElement::FromNode(&aContent
)) {
514 return IsInvisibleBRElement(*brElement
);
518 static bool IsInvisibleBRElement(const dom::HTMLBRElement
& aBRElement
) {
519 return !HTMLEditUtils::IsVisibleBRElement(aBRElement
);
523 * Return true if `display` of inclusive ancestor of aContent is `none`.
525 static bool IsInclusiveAncestorCSSDisplayNone(const nsIContent
& aContent
);
528 * IsVisiblePreformattedNewLine() and IsInvisiblePreformattedNewLine() return
529 * true if the point is preformatted linefeed and it's visible or invisible.
530 * If linefeed is immediately before a block boundary, it's invisible.
532 * @param aFollowingBlockElement [out] If the node is followed by a block
533 * boundary, this is set to the element
534 * creating the block boundary.
536 template <typename EditorDOMPointType
>
537 static bool IsVisiblePreformattedNewLine(
538 const EditorDOMPointType
& aPoint
,
539 Element
** aFollowingBlockElement
= nullptr) {
540 if (aFollowingBlockElement
) {
541 *aFollowingBlockElement
= nullptr;
543 if (!aPoint
.IsInTextNode() || aPoint
.IsEndOfContainer() ||
544 !aPoint
.IsCharPreformattedNewLine()) {
547 // If there are some other characters in the text node, it's a visible
549 if (!aPoint
.IsAtLastContent()) {
550 if (EditorUtils::IsWhiteSpacePreformatted(
551 *aPoint
.template ContainerAs
<Text
>())) {
554 const nsTextFragment
& textFragment
=
555 aPoint
.template ContainerAs
<Text
>()->TextFragment();
556 for (uint32_t offset
= aPoint
.Offset() + 1;
557 offset
< textFragment
.GetLength(); ++offset
) {
558 char16_t ch
= textFragment
.CharAt(AssertedCast
<int32_t>(offset
));
559 if (nsCRT::IsAsciiSpace(ch
) && ch
!= HTMLEditUtils::kNewLine
) {
560 continue; // ASCII white-space which is collapsed into the linefeed.
562 return true; // There is a visible character after it.
565 // If followed by a block boundary without visible content, it's invisible
567 Element
* followingBlockElement
=
568 HTMLEditUtils::GetElementOfImmediateBlockBoundary(
569 *aPoint
.template ContainerAs
<Text
>(), WalkTreeDirection::Forward
);
570 if (aFollowingBlockElement
) {
571 *aFollowingBlockElement
= followingBlockElement
;
573 return !followingBlockElement
;
575 template <typename EditorDOMPointType
>
576 static bool IsInvisiblePreformattedNewLine(
577 const EditorDOMPointType
& aPoint
,
578 Element
** aFollowingBlockElement
= nullptr) {
579 if (!aPoint
.IsInTextNode() || aPoint
.IsEndOfContainer() ||
580 !aPoint
.IsCharPreformattedNewLine()) {
581 if (aFollowingBlockElement
) {
582 *aFollowingBlockElement
= nullptr;
586 return !IsVisiblePreformattedNewLine(aPoint
, aFollowingBlockElement
);
590 * ShouldInsertLinefeedCharacter() returns true if the caller should insert
591 * a linefeed character instead of <br> element.
593 static bool ShouldInsertLinefeedCharacter(
594 const EditorDOMPoint
& aPointToInsert
, const Element
& aEditingHost
);
597 * IsEmptyNode() returns false if aNode has some visible content nodes,
598 * list elements or table elements.
600 * @param aPresContext Must not be nullptr if
601 * EmptyCheckOption::SafeToAskLayout is set.
602 * @param aNode The node to check whether it's empty.
603 * @param aOptions You can specify which type of elements are visible
604 * and/or whether this can access layout information.
605 * @param aSeenBR [Out] Set to true if this meets an <br> element
606 * before meeting visible things.
608 enum class EmptyCheckOption
{
609 TreatSingleBRElementAsVisible
,
610 TreatListItemAsVisible
,
611 TreatTableCellAsVisible
,
612 TreatNonEditableContentAsInvisible
,
615 using EmptyCheckOptions
= EnumSet
<EmptyCheckOption
, uint32_t>;
616 static bool IsEmptyNode(nsPresContext
* aPresContext
, const nsINode
& aNode
,
617 const EmptyCheckOptions
& aOptions
= {},
618 bool* aSeenBR
= nullptr);
619 static bool IsEmptyNode(const nsINode
& aNode
,
620 const EmptyCheckOptions
& aOptions
= {},
621 bool* aSeenBR
= nullptr) {
622 MOZ_ASSERT(!aOptions
.contains(EmptyCheckOption::SafeToAskLayout
));
623 return IsEmptyNode(nullptr, aNode
, aOptions
, aSeenBR
);
627 * IsEmptyInlineContainer() returns true if aContent is an inline element
628 * which can have children and does not have meaningful content.
630 static bool IsEmptyInlineContainer(const nsIContent
& aContent
,
631 const EmptyCheckOptions
& aOptions
,
632 BlockInlineCheck aBlockInlineCheck
) {
633 return HTMLEditUtils::IsInlineContent(aContent
, aBlockInlineCheck
) &&
634 HTMLEditUtils::IsContainerNode(aContent
) &&
635 HTMLEditUtils::IsEmptyNode(aContent
, aOptions
);
639 * IsEmptyBlockElement() returns true if aElement is a block level element
640 * and it doesn't have any visible content.
642 static bool IsEmptyBlockElement(const Element
& aElement
,
643 const EmptyCheckOptions
& aOptions
,
644 BlockInlineCheck aBlockInlineCheck
) {
645 return HTMLEditUtils::IsBlockElement(aElement
, aBlockInlineCheck
) &&
646 HTMLEditUtils::IsEmptyNode(aElement
, aOptions
);
650 * Return true if aListElement is completely empty or it has only one list
651 * item element which is empty.
653 [[nodiscard
]] static bool IsEmptyAnyListElement(const Element
& aListElement
) {
654 MOZ_ASSERT(HTMLEditUtils::IsAnyListElement(&aListElement
));
655 bool foundListItem
= false;
656 for (nsIContent
* child
= aListElement
.GetFirstChild(); child
;
657 child
= child
->GetNextSibling()) {
658 if (HTMLEditUtils::IsListItem(child
)) {
660 return false; // 2 list items found.
662 if (!IsEmptyNode(*child
, {})) {
663 return false; // found non-empty list item.
665 foundListItem
= true;
668 if (child
->IsElement()) {
669 return false; // found sublist or illegal child.
671 if (child
->IsText() &&
672 HTMLEditUtils::IsVisibleTextNode(*child
->AsText())) {
673 return false; // found illegal visible text node.
680 * Return true if aListElement does not have invalid child.
682 enum class TreatSubListElementAs
{ Invalid
, Valid
};
683 [[nodiscard
]] static bool IsValidListElement(
684 const Element
& aListElement
,
685 TreatSubListElementAs aTreatSubListElementAs
) {
686 MOZ_ASSERT(HTMLEditUtils::IsAnyListElement(&aListElement
));
687 for (nsIContent
* child
= aListElement
.GetFirstChild(); child
;
688 child
= child
->GetNextSibling()) {
689 if (HTMLEditUtils::IsAnyListElement(child
)) {
690 if (aTreatSubListElementAs
== TreatSubListElementAs::Invalid
) {
695 if (child
->IsHTMLElement(nsGkAtoms::li
)) {
696 if (MOZ_UNLIKELY(!aListElement
.IsAnyOfHTMLElements(nsGkAtoms::ol
,
702 if (child
->IsAnyOfHTMLElements(nsGkAtoms::dt
, nsGkAtoms::dd
)) {
703 if (MOZ_UNLIKELY(!aListElement
.IsAnyOfHTMLElements(nsGkAtoms::dl
))) {
708 if (MOZ_UNLIKELY(child
->IsElement())) {
711 if (MOZ_LIKELY(child
->IsText())) {
712 if (MOZ_UNLIKELY(HTMLEditUtils::IsVisibleTextNode(*child
->AsText()))) {
721 * IsEmptyOneHardLine() returns true if aArrayOfContents does not represent
722 * 2 or more lines and have meaningful content.
724 static bool IsEmptyOneHardLine(
725 nsTArray
<OwningNonNull
<nsIContent
>>& aArrayOfContents
,
726 BlockInlineCheck aBlockInlineCheck
) {
727 if (NS_WARN_IF(aArrayOfContents
.IsEmpty())) {
731 bool brElementHasFound
= false;
732 for (OwningNonNull
<nsIContent
>& content
: aArrayOfContents
) {
733 if (!EditorUtils::IsEditableContent(content
,
734 EditorUtils::EditorType::HTML
)) {
737 if (content
->IsHTMLElement(nsGkAtoms::br
)) {
738 // If there are 2 or more `<br>` elements, it's not empty line since
739 // there may be only one `<br>` element in a hard line.
740 if (brElementHasFound
) {
743 brElementHasFound
= true;
746 if (!HTMLEditUtils::IsEmptyInlineContainer(
748 {EmptyCheckOption::TreatSingleBRElementAsVisible
,
749 EmptyCheckOption::TreatNonEditableContentAsInvisible
},
750 aBlockInlineCheck
)) {
758 * IsPointAtEdgeOfLink() returns true if aPoint is at start or end of a
761 template <typename PT
, typename CT
>
762 static bool IsPointAtEdgeOfLink(const EditorDOMPointBase
<PT
, CT
>& aPoint
,
763 Element
** aFoundLinkElement
= nullptr) {
764 if (aFoundLinkElement
) {
765 *aFoundLinkElement
= nullptr;
767 if (!aPoint
.IsInContentNode()) {
770 if (!aPoint
.IsStartOfContainer() && !aPoint
.IsEndOfContainer()) {
773 // XXX Assuming it's not in an empty text node because it's unrealistic edge
775 bool maybeStartOfAnchor
= aPoint
.IsStartOfContainer();
776 for (EditorRawDOMPoint
point(aPoint
.template ContainerAs
<nsIContent
>());
777 point
.IsSet() && (maybeStartOfAnchor
? point
.IsStartOfContainer()
778 : point
.IsAtLastContent());
779 point
= point
.ParentPoint()) {
780 if (HTMLEditUtils::IsLink(point
.GetContainer())) {
781 // Now, we're at start or end of <a href>.
782 if (aFoundLinkElement
) {
784 do_AddRef(point
.template ContainerAs
<Element
>()).take();
793 * IsContentInclusiveDescendantOfLink() returns true if aContent is a
794 * descendant of a link element.
795 * Note that this returns true even if editing host of aContent is in a link
798 static bool IsContentInclusiveDescendantOfLink(
799 nsIContent
& aContent
, Element
** aFoundLinkElement
= nullptr) {
800 if (aFoundLinkElement
) {
801 *aFoundLinkElement
= nullptr;
803 for (Element
* element
: aContent
.InclusiveAncestorsOfType
<Element
>()) {
804 if (HTMLEditUtils::IsLink(element
)) {
805 if (aFoundLinkElement
) {
806 *aFoundLinkElement
= do_AddRef(element
).take();
815 * IsRangeEntirelyInLink() returns true if aRange is entirely in a link
817 * Note that this returns true even if editing host of the range is in a link
820 template <typename EditorDOMRangeType
>
821 static bool IsRangeEntirelyInLink(const EditorDOMRangeType
& aRange
,
822 Element
** aFoundLinkElement
= nullptr) {
823 MOZ_ASSERT(aRange
.IsPositionedAndValid());
824 if (aFoundLinkElement
) {
825 *aFoundLinkElement
= nullptr;
827 nsINode
* commonAncestorNode
=
828 nsContentUtils::GetClosestCommonInclusiveAncestor(
829 aRange
.StartRef().GetContainer(), aRange
.EndRef().GetContainer());
830 if (NS_WARN_IF(!commonAncestorNode
) || !commonAncestorNode
->IsContent()) {
833 return IsContentInclusiveDescendantOfLink(*commonAncestorNode
->AsContent(),
838 * Get adjacent content node of aNode if there is (even if one is in different
841 * @param aNode The node from which we start to walk the DOM
843 * @param aOptions See WalkTreeOption for the detail.
844 * @param aBlockInlineCheck Whether considering block vs. inline with the
845 * computed style or the HTML default style.
846 * @param aAncestorLimiter Ancestor limiter element which these methods
847 * never cross its boundary. This is typically
850 enum class WalkTreeOption
{
851 IgnoreNonEditableNode
, // Ignore non-editable nodes and their children.
852 IgnoreDataNodeExceptText
, // Ignore data nodes which are not text node.
853 IgnoreWhiteSpaceOnlyText
, // Ignore text nodes having only white-spaces.
854 StopAtBlockBoundary
, // Stop waking the tree at a block boundary.
856 using WalkTreeOptions
= EnumSet
<WalkTreeOption
>;
857 static nsIContent
* GetPreviousContent(
858 const nsINode
& aNode
, const WalkTreeOptions
& aOptions
,
859 BlockInlineCheck aBlockInlineCheck
,
860 const Element
* aAncestorLimiter
= nullptr) {
861 if (&aNode
== aAncestorLimiter
||
863 !aNode
.IsInclusiveDescendantOf(aAncestorLimiter
))) {
866 return HTMLEditUtils::GetAdjacentContent(aNode
, WalkTreeDirection::Backward
,
867 aOptions
, aBlockInlineCheck
,
870 static nsIContent
* GetNextContent(const nsINode
& aNode
,
871 const WalkTreeOptions
& aOptions
,
872 BlockInlineCheck aBlockInlineCheck
,
873 const Element
* aAncestorLimiter
= nullptr) {
874 if (&aNode
== aAncestorLimiter
||
876 !aNode
.IsInclusiveDescendantOf(aAncestorLimiter
))) {
879 return HTMLEditUtils::GetAdjacentContent(aNode
, WalkTreeDirection::Forward
,
880 aOptions
, aBlockInlineCheck
,
885 * And another version that takes a point in DOM tree rather than a node.
887 template <typename PT
, typename CT
>
888 static nsIContent
* GetPreviousContent(
889 const EditorDOMPointBase
<PT
, CT
>& aPoint
, const WalkTreeOptions
& aOptions
,
890 BlockInlineCheck aBlockInlineCheck
,
891 const Element
* aAncestorLimiter
= nullptr);
894 * And another version that takes a point in DOM tree rather than a node.
896 * Note that this may return the child at the offset. E.g., following code
897 * causes infinite loop.
899 * EditorRawDOMPoint point(aEditableNode);
900 * while (nsIContent* content =
901 * GetNextContent(point, {WalkTreeOption::IgnoreNonEditableNode})) {
903 * point.Set(content);
906 * Following code must be you expected:
908 * while (nsIContent* content =
909 * GetNextContent(point, {WalkTreeOption::IgnoreNonEditableNode}) {
911 * DebugOnly<bool> advanced = point.Advanced();
912 * MOZ_ASSERT(advanced);
913 * point.Set(point.GetChild());
916 template <typename PT
, typename CT
>
917 static nsIContent
* GetNextContent(const EditorDOMPointBase
<PT
, CT
>& aPoint
,
918 const WalkTreeOptions
& aOptions
,
919 BlockInlineCheck aBlockInlineCheck
,
920 const Element
* aAncestorLimiter
= nullptr);
923 * GetPreviousSibling() return the preceding sibling of aContent which matches
926 * @param aBlockInlineCheck Can be Unused if aOptions does not contain
927 * StopAtBlockBoundary.
929 static nsIContent
* GetPreviousSibling(
930 const nsIContent
& aContent
, const WalkTreeOptions
& aOptions
,
931 BlockInlineCheck aBlockInlineCheck
= BlockInlineCheck::Unused
) {
932 for (nsIContent
* sibling
= aContent
.GetPreviousSibling(); sibling
;
933 sibling
= sibling
->GetPreviousSibling()) {
934 if (HTMLEditUtils::IsContentIgnored(*sibling
, aOptions
)) {
937 if (aOptions
.contains(WalkTreeOption::StopAtBlockBoundary
) &&
938 HTMLEditUtils::IsBlockElement(*sibling
, aBlockInlineCheck
)) {
947 * GetNextSibling() return the following sibling of aContent which matches
950 * @param aBlockInlineCheck Can be Unused if aOptions does not contain
951 * StopAtBlockBoundary.
953 static nsIContent
* GetNextSibling(
954 const nsIContent
& aContent
, const WalkTreeOptions
& aOptions
,
955 BlockInlineCheck aBlockInlineCheck
= BlockInlineCheck::Unused
) {
956 for (nsIContent
* sibling
= aContent
.GetNextSibling(); sibling
;
957 sibling
= sibling
->GetNextSibling()) {
958 if (HTMLEditUtils::IsContentIgnored(*sibling
, aOptions
)) {
961 if (aOptions
.contains(WalkTreeOption::StopAtBlockBoundary
) &&
962 HTMLEditUtils::IsBlockElement(*sibling
, aBlockInlineCheck
)) {
971 * Return the last child of aNode which matches with aOption.
973 * @param aBlockInlineCheck Can be unused if aOptions does not contain
974 * StopAtBlockBoundary.
976 static nsIContent
* GetLastChild(
977 const nsINode
& aNode
, const WalkTreeOptions
& aOptions
,
978 BlockInlineCheck aBlockInlineCheck
= BlockInlineCheck::Unused
) {
979 for (nsIContent
* child
= aNode
.GetLastChild(); child
;
980 child
= child
->GetPreviousSibling()) {
981 if (HTMLEditUtils::IsContentIgnored(*child
, aOptions
)) {
984 if (aOptions
.contains(WalkTreeOption::StopAtBlockBoundary
) &&
985 HTMLEditUtils::IsBlockElement(*child
, aBlockInlineCheck
)) {
994 * Return the first child of aNode which matches with aOption.
996 * @param aBlockInlineCheck Can be unused if aOptions does not contain
997 * StopAtBlockBoundary.
999 static nsIContent
* GetFirstChild(
1000 const nsINode
& aNode
, const WalkTreeOptions
& aOptions
,
1001 BlockInlineCheck aBlockInlineCheck
= BlockInlineCheck::Unused
) {
1002 for (nsIContent
* child
= aNode
.GetFirstChild(); child
;
1003 child
= child
->GetNextSibling()) {
1004 if (HTMLEditUtils::IsContentIgnored(*child
, aOptions
)) {
1007 if (aOptions
.contains(WalkTreeOption::StopAtBlockBoundary
) &&
1008 HTMLEditUtils::IsBlockElement(*child
, aBlockInlineCheck
)) {
1017 * Return true if aContent is the last child of aNode with ignoring all
1018 * children which do not match with aOption.
1020 * @param aBlockInlineCheck Can be unused if aOptions does not contain
1021 * StopAtBlockBoundary.
1023 static bool IsLastChild(
1024 const nsIContent
& aContent
, const WalkTreeOptions
& aOptions
,
1025 BlockInlineCheck aBlockInlineCheck
= BlockInlineCheck::Unused
) {
1026 nsINode
* parentNode
= aContent
.GetParentNode();
1030 return HTMLEditUtils::GetLastChild(*parentNode
, aOptions
,
1031 aBlockInlineCheck
) == &aContent
;
1035 * Return true if aContent is the first child of aNode with ignoring all
1036 * children which do not match with aOption.
1038 * @param aBlockInlineCheck Can be unused if aOptions does not contain
1039 * StopAtBlockBoundary.
1041 static bool IsFirstChild(
1042 const nsIContent
& aContent
, const WalkTreeOptions
& aOptions
,
1043 BlockInlineCheck aBlockInlineCheck
= BlockInlineCheck::Unused
) {
1044 nsINode
* parentNode
= aContent
.GetParentNode();
1048 return HTMLEditUtils::GetFirstChild(*parentNode
, aOptions
,
1049 aBlockInlineCheck
) == &aContent
;
1053 * GetAdjacentContentToPutCaret() walks the DOM tree to find an editable node
1054 * near aPoint where may be a good point to put caret and keep typing or
1057 * @param aPoint The DOM point where to start to search from.
1058 * @return If found, returns non-nullptr. Otherwise, nullptr.
1059 * Note that if found node is in different table structure
1060 * element, this returns nullptr.
1062 enum class WalkTreeDirection
{ Forward
, Backward
};
1063 template <typename PT
, typename CT
>
1064 static nsIContent
* GetAdjacentContentToPutCaret(
1065 const EditorDOMPointBase
<PT
, CT
>& aPoint
,
1066 WalkTreeDirection aWalkTreeDirection
, const Element
& aEditingHost
) {
1067 MOZ_ASSERT(aPoint
.IsSetAndValid());
1069 nsIContent
* editableContent
= nullptr;
1070 if (aWalkTreeDirection
== WalkTreeDirection::Backward
) {
1071 editableContent
= HTMLEditUtils::GetPreviousContent(
1072 aPoint
, {WalkTreeOption::IgnoreNonEditableNode
},
1073 BlockInlineCheck::UseComputedDisplayStyle
, &aEditingHost
);
1074 if (!editableContent
) {
1075 return nullptr; // Not illegal.
1078 editableContent
= HTMLEditUtils::GetNextContent(
1079 aPoint
, {WalkTreeOption::IgnoreNonEditableNode
},
1080 BlockInlineCheck::UseComputedDisplayStyle
, &aEditingHost
);
1081 if (NS_WARN_IF(!editableContent
)) {
1082 // Perhaps, illegal because the node pointed by aPoint isn't editable
1083 // and nobody of previous nodes is editable.
1088 // scan in the right direction until we find an eligible text node,
1089 // but don't cross any breaks, images, or table elements.
1090 // XXX This comment sounds odd. editableContent may have already crossed
1091 // breaks and/or images if they are non-editable.
1092 while (editableContent
&& !editableContent
->IsText() &&
1093 !editableContent
->IsHTMLElement(nsGkAtoms::br
) &&
1094 !HTMLEditUtils::IsImage(editableContent
)) {
1095 if (aWalkTreeDirection
== WalkTreeDirection::Backward
) {
1096 editableContent
= HTMLEditUtils::GetPreviousContent(
1097 *editableContent
, {WalkTreeOption::IgnoreNonEditableNode
},
1098 BlockInlineCheck::UseComputedDisplayStyle
, &aEditingHost
);
1099 if (NS_WARN_IF(!editableContent
)) {
1103 editableContent
= HTMLEditUtils::GetNextContent(
1104 *editableContent
, {WalkTreeOption::IgnoreNonEditableNode
},
1105 BlockInlineCheck::UseComputedDisplayStyle
, &aEditingHost
);
1106 if (NS_WARN_IF(!editableContent
)) {
1112 // don't cross any table elements
1113 if ((!aPoint
.IsInContentNode() &&
1114 !!HTMLEditUtils::GetInclusiveAncestorAnyTableElement(
1115 *editableContent
)) ||
1116 (HTMLEditUtils::GetInclusiveAncestorAnyTableElement(*editableContent
) !=
1117 HTMLEditUtils::GetInclusiveAncestorAnyTableElement(
1118 *aPoint
.template ContainerAs
<nsIContent
>()))) {
1122 // otherwise, ok, we have found a good spot to put the selection
1123 return editableContent
;
1126 enum class LeafNodeType
{
1127 // Even if there is a child block, keep scanning a leaf content in it.
1129 // If there is a child block, return it too. Note that this does not
1130 // mean that block siblings are not treated as leaf nodes.
1131 LeafNodeOrChildBlock
,
1132 // If there is a non-editable element if and only if scanning from editable
1133 // node, return it too.
1134 LeafNodeOrNonEditableNode
,
1135 // Ignore non-editable content at walking the tree.
1136 OnlyEditableLeafNode
,
1138 using LeafNodeTypes
= EnumSet
<LeafNodeType
>;
1141 * GetLastLeafContent() returns rightmost leaf content in aNode. It depends
1142 * on aLeafNodeTypes whether this which types of nodes are treated as leaf
1145 * @param aBlockInlineCheck Can be Unused if aLeafNodeTypes does not contain
1146 * LeafNodeOrCHildBlock.
1148 static nsIContent
* GetLastLeafContent(
1149 const nsINode
& aNode
, const LeafNodeTypes
& aLeafNodeTypes
,
1150 BlockInlineCheck aBlockInlineCheck
= BlockInlineCheck::Unused
,
1151 const Element
* aAncestorLimiter
= nullptr) {
1153 aLeafNodeTypes
.contains(LeafNodeType::OnlyEditableLeafNode
),
1154 !aLeafNodeTypes
.contains(LeafNodeType::LeafNodeOrNonEditableNode
));
1155 // editor shouldn't touch child nodes which are replaced with native
1157 if (aNode
.IsElement() &&
1158 HTMLEditUtils::IsNeverElementContentsEditableByUser(
1159 *aNode
.AsElement())) {
1162 for (nsIContent
* content
= aNode
.GetLastChild(); content
;) {
1163 if (aLeafNodeTypes
.contains(LeafNodeType::OnlyEditableLeafNode
) &&
1164 !EditorUtils::IsEditableContent(*content
,
1165 EditorUtils::EditorType::HTML
)) {
1166 content
= HTMLEditUtils::GetPreviousContent(
1167 *content
, {WalkTreeOption::IgnoreNonEditableNode
},
1168 aBlockInlineCheck
, aAncestorLimiter
);
1171 if (aLeafNodeTypes
.contains(LeafNodeType::LeafNodeOrChildBlock
) &&
1172 HTMLEditUtils::IsBlockElement(*content
, aBlockInlineCheck
)) {
1175 if (!content
->HasChildren() ||
1176 HTMLEditUtils::IsNeverElementContentsEditableByUser(*content
)) {
1179 if (aLeafNodeTypes
.contains(LeafNodeType::LeafNodeOrNonEditableNode
) &&
1180 aNode
.IsEditable() && !content
->IsEditable()) {
1183 content
= content
->GetLastChild();
1189 * GetFirstLeafContent() returns leftmost leaf content in aNode. It depends
1190 * on aLeafNodeTypes whether this scans into a block child or treat block as a
1193 * @param aBlockInlineCheck Can be Unused if aLeafNodeTypes does not contain
1194 * LeafNodeOrCHildBlock.
1196 static nsIContent
* GetFirstLeafContent(
1197 const nsINode
& aNode
, const LeafNodeTypes
& aLeafNodeTypes
,
1198 BlockInlineCheck aBlockInlineCheck
= BlockInlineCheck::Unused
,
1199 const Element
* aAncestorLimiter
= nullptr) {
1201 aLeafNodeTypes
.contains(LeafNodeType::OnlyEditableLeafNode
),
1202 !aLeafNodeTypes
.contains(LeafNodeType::LeafNodeOrNonEditableNode
));
1203 // editor shouldn't touch child nodes which are replaced with native
1205 if (aNode
.IsElement() &&
1206 HTMLEditUtils::IsNeverElementContentsEditableByUser(
1207 *aNode
.AsElement())) {
1210 for (nsIContent
* content
= aNode
.GetFirstChild(); content
;) {
1211 if (aLeafNodeTypes
.contains(LeafNodeType::OnlyEditableLeafNode
) &&
1212 !EditorUtils::IsEditableContent(*content
,
1213 EditorUtils::EditorType::HTML
)) {
1214 content
= HTMLEditUtils::GetNextContent(
1215 *content
, {WalkTreeOption::IgnoreNonEditableNode
},
1216 aBlockInlineCheck
, aAncestorLimiter
);
1219 if (aLeafNodeTypes
.contains(LeafNodeType::LeafNodeOrChildBlock
) &&
1220 HTMLEditUtils::IsBlockElement(*content
, aBlockInlineCheck
)) {
1223 if (!content
->HasChildren() ||
1224 HTMLEditUtils::IsNeverElementContentsEditableByUser(*content
)) {
1227 if (aLeafNodeTypes
.contains(LeafNodeType::LeafNodeOrNonEditableNode
) &&
1228 aNode
.IsEditable() && !content
->IsEditable()) {
1231 content
= content
->GetFirstChild();
1237 * GetNextLeafContentOrNextBlockElement() returns next leaf content or
1238 * next block element of aStartContent inside aAncestorLimiter.
1239 * Note that the result may be a contet outside aCurrentBlock if
1240 * aStartContent equals aCurrentBlock.
1242 * @param aStartContent The start content to scan next content.
1243 * @param aCurrentBlock Must be ancestor of aStartContent. Dispite
1244 * the name, inline content is allowed if
1245 * aStartContent is in an inline editing host.
1246 * @param aLeafNodeTypes See LeafNodeType.
1247 * @param aAncestorLimiter Optional, setting this guarantees the
1248 * result is in aAncestorLimiter unless
1249 * aStartContent is not a descendant of this.
1251 static nsIContent
* GetNextLeafContentOrNextBlockElement(
1252 const nsIContent
& aStartContent
, const nsIContent
& aCurrentBlock
,
1253 const LeafNodeTypes
& aLeafNodeTypes
, BlockInlineCheck aBlockInlineCheck
,
1254 const Element
* aAncestorLimiter
= nullptr) {
1256 aLeafNodeTypes
.contains(LeafNodeType::OnlyEditableLeafNode
),
1257 !aLeafNodeTypes
.contains(LeafNodeType::LeafNodeOrNonEditableNode
));
1259 if (&aStartContent
== aAncestorLimiter
) {
1263 nsIContent
* nextContent
= aStartContent
.GetNextSibling();
1265 if (!aStartContent
.GetParentElement()) {
1266 NS_WARNING("Reached orphan node while climbing up the DOM tree");
1269 for (Element
* parentElement
: aStartContent
.AncestorsOfType
<Element
>()) {
1270 if (parentElement
== &aCurrentBlock
) {
1273 if (parentElement
== aAncestorLimiter
) {
1274 NS_WARNING("Reached editing host while climbing up the DOM tree");
1277 nextContent
= parentElement
->GetNextSibling();
1281 if (!parentElement
->GetParentElement()) {
1282 NS_WARNING("Reached orphan node while climbing up the DOM tree");
1286 MOZ_ASSERT(nextContent
);
1287 aBlockInlineCheck
= IgnoreInsideBlockBoundary(aBlockInlineCheck
);
1290 // We have a next content. If it's a block, return it.
1291 if (HTMLEditUtils::IsBlockElement(*nextContent
, aBlockInlineCheck
)) {
1294 if (aLeafNodeTypes
.contains(LeafNodeType::LeafNodeOrNonEditableNode
) &&
1295 aStartContent
.IsEditable() && !nextContent
->IsEditable()) {
1298 if (HTMLEditUtils::IsContainerNode(*nextContent
)) {
1299 // Else if it's a container, get deep leftmost child
1300 if (nsIContent
* child
= HTMLEditUtils::GetFirstLeafContent(
1301 *nextContent
, aLeafNodeTypes
, aBlockInlineCheck
)) {
1305 // Else return the next content itself.
1310 * Similar to the above method, but take a DOM point to specify scan start
1313 template <typename PT
, typename CT
>
1314 static nsIContent
* GetNextLeafContentOrNextBlockElement(
1315 const EditorDOMPointBase
<PT
, CT
>& aStartPoint
,
1316 const nsIContent
& aCurrentBlock
, const LeafNodeTypes
& aLeafNodeTypes
,
1317 BlockInlineCheck aBlockInlineCheck
,
1318 const Element
* aAncestorLimiter
= nullptr) {
1319 MOZ_ASSERT(aStartPoint
.IsSet());
1321 aLeafNodeTypes
.contains(LeafNodeType::OnlyEditableLeafNode
),
1322 !aLeafNodeTypes
.contains(LeafNodeType::LeafNodeOrNonEditableNode
));
1323 NS_ASSERTION(!aLeafNodeTypes
.contains(LeafNodeType::OnlyEditableLeafNode
),
1324 "Not implemented yet");
1326 if (!aStartPoint
.IsInContentNode()) {
1329 if (aStartPoint
.IsInTextNode()) {
1330 return HTMLEditUtils::GetNextLeafContentOrNextBlockElement(
1331 *aStartPoint
.template ContainerAs
<Text
>(), aCurrentBlock
,
1332 aLeafNodeTypes
, aBlockInlineCheck
, aAncestorLimiter
);
1334 if (!HTMLEditUtils::IsContainerNode(
1335 *aStartPoint
.template ContainerAs
<nsIContent
>())) {
1336 return HTMLEditUtils::GetNextLeafContentOrNextBlockElement(
1337 *aStartPoint
.template ContainerAs
<nsIContent
>(), aCurrentBlock
,
1338 aLeafNodeTypes
, aBlockInlineCheck
, aAncestorLimiter
);
1341 nsCOMPtr
<nsIContent
> nextContent
= aStartPoint
.GetChild();
1343 if (aStartPoint
.GetContainer() == &aCurrentBlock
) {
1344 // We are at end of the block.
1348 // We are at end of non-block container
1349 return HTMLEditUtils::GetNextLeafContentOrNextBlockElement(
1350 *aStartPoint
.template ContainerAs
<nsIContent
>(), aCurrentBlock
,
1351 aLeafNodeTypes
, IgnoreInsideBlockBoundary(aBlockInlineCheck
),
1355 // We have a next node. If it's a block, return it.
1356 if (HTMLEditUtils::IsBlockElement(*nextContent
, aBlockInlineCheck
)) {
1359 if (aLeafNodeTypes
.contains(LeafNodeType::LeafNodeOrNonEditableNode
) &&
1360 aStartPoint
.GetContainer()->IsEditable() &&
1361 !nextContent
->IsEditable()) {
1364 if (HTMLEditUtils::IsContainerNode(*nextContent
)) {
1365 // else if it's a container, get deep leftmost child
1366 if (nsIContent
* child
= HTMLEditUtils::GetFirstLeafContent(
1367 *nextContent
, aLeafNodeTypes
,
1368 IgnoreInsideBlockBoundary(aBlockInlineCheck
))) {
1372 // Else return the node itself
1377 * GetPreviousLeafContentOrPreviousBlockElement() returns previous leaf
1378 * content or previous block element of aStartContent inside
1380 * Note that the result may be a content outside aCurrentBlock if
1381 * aStartContent equals aCurrentBlock.
1383 * @param aStartContent The start content to scan previous content.
1384 * @param aCurrentBlock Must be ancestor of aStartContent. Despite
1385 * the name, inline content is allowed if
1386 * aStartContent is in an inline editing host.
1387 * @param aLeafNodeTypes See LeafNodeType.
1388 * @param aAncestorLimiter Optional, setting this guarantees the
1389 * result is in aAncestorLimiter unless
1390 * aStartContent is not a descendant of this.
1392 static nsIContent
* GetPreviousLeafContentOrPreviousBlockElement(
1393 const nsIContent
& aStartContent
, const nsIContent
& aCurrentBlock
,
1394 const LeafNodeTypes
& aLeafNodeTypes
, BlockInlineCheck aBlockInlineCheck
,
1395 const Element
* aAncestorLimiter
= nullptr) {
1397 aLeafNodeTypes
.contains(LeafNodeType::OnlyEditableLeafNode
),
1398 !aLeafNodeTypes
.contains(LeafNodeType::LeafNodeOrNonEditableNode
));
1399 NS_ASSERTION(!aLeafNodeTypes
.contains(LeafNodeType::OnlyEditableLeafNode
),
1400 "Not implemented yet");
1402 if (&aStartContent
== aAncestorLimiter
) {
1406 nsIContent
* previousContent
= aStartContent
.GetPreviousSibling();
1407 if (!previousContent
) {
1408 if (!aStartContent
.GetParentElement()) {
1409 NS_WARNING("Reached orphan node while climbing up the DOM tree");
1412 for (Element
* parentElement
: aStartContent
.AncestorsOfType
<Element
>()) {
1413 if (parentElement
== &aCurrentBlock
) {
1416 if (parentElement
== aAncestorLimiter
) {
1417 NS_WARNING("Reached editing host while climbing up the DOM tree");
1420 previousContent
= parentElement
->GetPreviousSibling();
1421 if (previousContent
) {
1424 if (!parentElement
->GetParentElement()) {
1425 NS_WARNING("Reached orphan node while climbing up the DOM tree");
1429 MOZ_ASSERT(previousContent
);
1430 aBlockInlineCheck
= IgnoreInsideBlockBoundary(aBlockInlineCheck
);
1433 // We have a next content. If it's a block, return it.
1434 if (HTMLEditUtils::IsBlockElement(*previousContent
, aBlockInlineCheck
)) {
1435 return previousContent
;
1437 if (aLeafNodeTypes
.contains(LeafNodeType::LeafNodeOrNonEditableNode
) &&
1438 aStartContent
.IsEditable() && !previousContent
->IsEditable()) {
1439 return previousContent
;
1441 if (HTMLEditUtils::IsContainerNode(*previousContent
)) {
1442 // Else if it's a container, get deep rightmost child
1443 if (nsIContent
* child
= HTMLEditUtils::GetLastLeafContent(
1444 *previousContent
, aLeafNodeTypes
, aBlockInlineCheck
)) {
1448 // Else return the next content itself.
1449 return previousContent
;
1453 * Similar to the above method, but take a DOM point to specify scan start
1456 template <typename PT
, typename CT
>
1457 static nsIContent
* GetPreviousLeafContentOrPreviousBlockElement(
1458 const EditorDOMPointBase
<PT
, CT
>& aStartPoint
,
1459 const nsIContent
& aCurrentBlock
, const LeafNodeTypes
& aLeafNodeTypes
,
1460 BlockInlineCheck aBlockInlineCheck
,
1461 const Element
* aAncestorLimiter
= nullptr) {
1462 MOZ_ASSERT(aStartPoint
.IsSet());
1464 aLeafNodeTypes
.contains(LeafNodeType::OnlyEditableLeafNode
),
1465 !aLeafNodeTypes
.contains(LeafNodeType::LeafNodeOrNonEditableNode
));
1466 NS_ASSERTION(!aLeafNodeTypes
.contains(LeafNodeType::OnlyEditableLeafNode
),
1467 "Not implemented yet");
1469 if (!aStartPoint
.IsInContentNode()) {
1472 if (aStartPoint
.IsInTextNode()) {
1473 return HTMLEditUtils::GetPreviousLeafContentOrPreviousBlockElement(
1474 *aStartPoint
.template ContainerAs
<Text
>(), aCurrentBlock
,
1475 aLeafNodeTypes
, aBlockInlineCheck
, aAncestorLimiter
);
1477 if (!HTMLEditUtils::IsContainerNode(
1478 *aStartPoint
.template ContainerAs
<nsIContent
>())) {
1479 return HTMLEditUtils::GetPreviousLeafContentOrPreviousBlockElement(
1480 *aStartPoint
.template ContainerAs
<nsIContent
>(), aCurrentBlock
,
1481 aLeafNodeTypes
, aBlockInlineCheck
, aAncestorLimiter
);
1484 if (aStartPoint
.IsStartOfContainer()) {
1485 if (aStartPoint
.GetContainer() == &aCurrentBlock
) {
1486 // We are at start of the block.
1490 // We are at start of non-block container
1491 return HTMLEditUtils::GetPreviousLeafContentOrPreviousBlockElement(
1492 *aStartPoint
.template ContainerAs
<nsIContent
>(), aCurrentBlock
,
1493 aLeafNodeTypes
, IgnoreInsideBlockBoundary(aBlockInlineCheck
),
1497 nsCOMPtr
<nsIContent
> previousContent
=
1498 aStartPoint
.GetPreviousSiblingOfChild();
1499 if (NS_WARN_IF(!previousContent
)) {
1503 // We have a prior node. If it's a block, return it.
1504 if (HTMLEditUtils::IsBlockElement(*previousContent
, aBlockInlineCheck
)) {
1505 return previousContent
;
1507 if (aLeafNodeTypes
.contains(LeafNodeType::LeafNodeOrNonEditableNode
) &&
1508 aStartPoint
.GetContainer()->IsEditable() &&
1509 !previousContent
->IsEditable()) {
1510 return previousContent
;
1512 if (HTMLEditUtils::IsContainerNode(*previousContent
)) {
1513 // Else if it's a container, get deep rightmost child
1514 if (nsIContent
* child
= HTMLEditUtils::GetLastLeafContent(
1515 *previousContent
, aLeafNodeTypes
,
1516 IgnoreInsideBlockBoundary(aBlockInlineCheck
))) {
1520 // Else return the node itself
1521 return previousContent
;
1525 * Returns a content node whose inline styles should be preserved after
1526 * deleting content in a range. Typically, you should set aPoint to start
1527 * boundary of the range to delete.
1529 template <typename EditorDOMPointType
>
1530 static nsIContent
* GetContentToPreserveInlineStyles(
1531 const EditorDOMPointType
& aPoint
, const Element
& aEditingHost
);
1534 * Get previous/next editable point from start or end of aContent.
1536 enum class InvisibleWhiteSpaces
{
1537 Ignore
, // Ignore invisible white-spaces, i.e., don't return middle of
1539 Preserve
, // Preserve invisible white-spaces, i.e., result may be start or
1540 // end of a text node even if it begins or ends with invisible
1543 enum class TableBoundary
{
1544 Ignore
, // May cross any table element boundary.
1545 NoCrossTableElement
, // Won't cross `<table>` element boundary.
1546 NoCrossAnyTableElement
, // Won't cross any table element boundary.
1548 template <typename EditorDOMPointType
>
1549 static EditorDOMPointType
GetPreviousEditablePoint(
1550 nsIContent
& aContent
, const Element
* aAncestorLimiter
,
1551 InvisibleWhiteSpaces aInvisibleWhiteSpaces
,
1552 TableBoundary aHowToTreatTableBoundary
);
1553 template <typename EditorDOMPointType
>
1554 static EditorDOMPointType
GetNextEditablePoint(
1555 nsIContent
& aContent
, const Element
* aAncestorLimiter
,
1556 InvisibleWhiteSpaces aInvisibleWhiteSpaces
,
1557 TableBoundary aHowToTreatTableBoundary
);
1560 * GetAncestorElement() and GetInclusiveAncestorElement() return
1561 * (inclusive) block ancestor element of aContent whose time matches
1564 enum class AncestorType
{
1565 ClosestBlockElement
,
1566 MostDistantInlineElementInBlock
,
1568 IgnoreHRElement
, // Ignore ancestor <hr> element since invalid structure
1571 using AncestorTypes
= EnumSet
<AncestorType
>;
1572 constexpr static AncestorTypes
1573 ClosestEditableBlockElementOrInlineEditingHost
= {
1574 AncestorType::ClosestBlockElement
,
1575 AncestorType::MostDistantInlineElementInBlock
,
1576 AncestorType::EditableElement
};
1577 constexpr static AncestorTypes ClosestBlockElement
= {
1578 AncestorType::ClosestBlockElement
};
1579 constexpr static AncestorTypes ClosestEditableBlockElement
= {
1580 AncestorType::ClosestBlockElement
, AncestorType::EditableElement
};
1581 constexpr static AncestorTypes ClosestEditableBlockElementExceptHRElement
= {
1582 AncestorType::ClosestBlockElement
, AncestorType::IgnoreHRElement
,
1583 AncestorType::EditableElement
};
1584 constexpr static AncestorTypes ClosestEditableBlockElementOrButtonElement
= {
1585 AncestorType::ClosestBlockElement
, AncestorType::EditableElement
,
1586 AncestorType::ButtonElement
};
1587 static Element
* GetAncestorElement(const nsIContent
& aContent
,
1588 const AncestorTypes
& aAncestorTypes
,
1589 BlockInlineCheck aBlockInlineCheck
,
1590 const Element
* aAncestorLimiter
= nullptr);
1591 static Element
* GetInclusiveAncestorElement(
1592 const nsIContent
& aContent
, const AncestorTypes
& aAncestorTypes
,
1593 BlockInlineCheck aBlockInlineCheck
,
1594 const Element
* aAncestorLimiter
= nullptr);
1597 * GetClosestAncestorTableElement() returns the nearest inclusive ancestor
1598 * <table> element of aContent.
1600 static Element
* GetClosestAncestorTableElement(const nsIContent
& aContent
) {
1601 // TODO: the method name and its documentation clash with the
1602 // implementation. Split this method into
1603 // `GetClosestAncestorTableElement` and
1604 // `GetClosestInclusiveAncestorTableElement`.
1605 if (!aContent
.GetParent()) {
1608 for (Element
* element
: aContent
.InclusiveAncestorsOfType
<Element
>()) {
1609 if (HTMLEditUtils::IsTable(element
)) {
1616 static Element
* GetInclusiveAncestorAnyTableElement(
1617 const nsIContent
& aContent
) {
1618 for (Element
* parent
: aContent
.InclusiveAncestorsOfType
<Element
>()) {
1619 if (HTMLEditUtils::IsAnyTableElement(parent
)) {
1626 [[nodiscard
]] static Element
* GetClosestAncestorAnyListElement(
1627 const nsIContent
& aContent
);
1628 [[nodiscard
]] static Element
* GetClosestInclusiveAncestorAnyListElement(
1629 const nsIContent
& aContent
);
1632 * GetClosestAncestorListItemElement() returns a list item element if
1633 * aContent or its ancestor in editing host is one. However, this won't
1634 * cross table related element.
1636 static Element
* GetClosestAncestorListItemElement(
1637 const nsIContent
& aContent
, const Element
* aAncestorLimit
= nullptr) {
1638 MOZ_ASSERT_IF(aAncestorLimit
,
1639 aContent
.IsInclusiveDescendantOf(aAncestorLimit
));
1641 if (HTMLEditUtils::IsListItem(&aContent
)) {
1642 return const_cast<Element
*>(aContent
.AsElement());
1645 for (Element
* parentElement
: aContent
.AncestorsOfType
<Element
>()) {
1646 if (HTMLEditUtils::IsAnyTableElement(parentElement
)) {
1649 if (HTMLEditUtils::IsListItem(parentElement
)) {
1650 return parentElement
;
1652 if (parentElement
== aAncestorLimit
) {
1660 * GetRangeSelectingAllContentInAllListItems() returns a range which selects
1661 * from start of the first list item to end of the last list item of
1662 * aListElement. Note that the result may be in different list element if
1663 * aListElement has child list element(s) directly.
1665 template <typename EditorDOMRangeType
>
1666 static EditorDOMRangeType
GetRangeSelectingAllContentInAllListItems(
1667 const Element
& aListElement
) {
1668 MOZ_ASSERT(HTMLEditUtils::IsAnyListElement(&aListElement
));
1669 Element
* firstListItem
=
1670 HTMLEditUtils::GetFirstListItemElement(aListElement
);
1671 Element
* lastListItem
= HTMLEditUtils::GetLastListItemElement(aListElement
);
1672 MOZ_ASSERT_IF(firstListItem
, lastListItem
);
1673 MOZ_ASSERT_IF(!firstListItem
, !lastListItem
);
1674 if (!firstListItem
|| !lastListItem
) {
1675 return EditorDOMRangeType();
1677 return EditorDOMRangeType(
1678 typename
EditorDOMRangeType::PointType(firstListItem
, 0u),
1679 EditorDOMRangeType::PointType::AtEndOf(*lastListItem
));
1683 * GetFirstListItemElement() returns the first list item element in the
1684 * pre-order tree traversal of the DOM.
1686 static Element
* GetFirstListItemElement(const Element
& aListElement
) {
1687 MOZ_ASSERT(HTMLEditUtils::IsAnyListElement(&aListElement
));
1688 for (nsIContent
* maybeFirstListItem
= aListElement
.GetFirstChild();
1690 maybeFirstListItem
= maybeFirstListItem
->GetNextNode(&aListElement
)) {
1691 if (HTMLEditUtils::IsListItem(maybeFirstListItem
)) {
1692 return maybeFirstListItem
->AsElement();
1699 * GetLastListItemElement() returns the last list item element in the
1700 * post-order tree traversal of the DOM. I.e., returns the last list
1701 * element whose close tag appears at last.
1703 static Element
* GetLastListItemElement(const Element
& aListElement
) {
1704 MOZ_ASSERT(HTMLEditUtils::IsAnyListElement(&aListElement
));
1705 for (nsIContent
* maybeLastListItem
= aListElement
.GetLastChild();
1706 maybeLastListItem
;) {
1707 if (HTMLEditUtils::IsListItem(maybeLastListItem
)) {
1708 return maybeLastListItem
->AsElement();
1710 if (maybeLastListItem
->HasChildren()) {
1711 maybeLastListItem
= maybeLastListItem
->GetLastChild();
1714 if (maybeLastListItem
->GetPreviousSibling()) {
1715 maybeLastListItem
= maybeLastListItem
->GetPreviousSibling();
1718 for (Element
* parent
= maybeLastListItem
->GetParentElement(); parent
;
1719 parent
= parent
->GetParentElement()) {
1720 maybeLastListItem
= nullptr;
1721 if (parent
== &aListElement
) {
1724 if (parent
->GetPreviousSibling()) {
1725 maybeLastListItem
= parent
->GetPreviousSibling();
1734 * GetFirstTableCellElementChild() and GetLastTableCellElementChild()
1735 * return the first/last element child of <tr> element if it's a table
1738 static Element
* GetFirstTableCellElementChild(
1739 const Element
& aTableRowElement
) {
1740 MOZ_ASSERT(aTableRowElement
.IsHTMLElement(nsGkAtoms::tr
));
1741 Element
* firstElementChild
= aTableRowElement
.GetFirstElementChild();
1742 return firstElementChild
&& HTMLEditUtils::IsTableCell(firstElementChild
)
1746 static Element
* GetLastTableCellElementChild(
1747 const Element
& aTableRowElement
) {
1748 MOZ_ASSERT(aTableRowElement
.IsHTMLElement(nsGkAtoms::tr
));
1749 Element
* lastElementChild
= aTableRowElement
.GetLastElementChild();
1750 return lastElementChild
&& HTMLEditUtils::IsTableCell(lastElementChild
)
1756 * GetPreviousTableCellElementSibling() and GetNextTableCellElementSibling()
1757 * return a table cell element of previous/next element sibling of given
1758 * content node if and only if the element sibling is a table cell element.
1760 static Element
* GetPreviousTableCellElementSibling(
1761 const nsIContent
& aChildOfTableRow
) {
1762 MOZ_ASSERT(aChildOfTableRow
.GetParentNode());
1763 MOZ_ASSERT(aChildOfTableRow
.GetParentNode()->IsHTMLElement(nsGkAtoms::tr
));
1764 Element
* previousElementSibling
=
1765 aChildOfTableRow
.GetPreviousElementSibling();
1766 return previousElementSibling
&&
1767 HTMLEditUtils::IsTableCell(previousElementSibling
)
1768 ? previousElementSibling
1771 static Element
* GetNextTableCellElementSibling(
1772 const nsIContent
& aChildOfTableRow
) {
1773 MOZ_ASSERT(aChildOfTableRow
.GetParentNode());
1774 MOZ_ASSERT(aChildOfTableRow
.GetParentNode()->IsHTMLElement(nsGkAtoms::tr
));
1775 Element
* nextElementSibling
= aChildOfTableRow
.GetNextElementSibling();
1776 return nextElementSibling
&& HTMLEditUtils::IsTableCell(nextElementSibling
)
1777 ? nextElementSibling
1782 * GetMostDistantAncestorInlineElement() returns the most distant ancestor
1783 * inline element between aContent and the aEditingHost. Even if aEditingHost
1784 * is an inline element, this method never returns aEditingHost as the result.
1785 * Optionally, you can specify ancestor limiter content node. This guarantees
1786 * that the result is a descendant of aAncestorLimiter if aContent is a
1787 * descendant of aAncestorLimiter.
1789 static nsIContent
* GetMostDistantAncestorInlineElement(
1790 const nsIContent
& aContent
, BlockInlineCheck aBlockInlineCheck
,
1791 const Element
* aEditingHost
= nullptr,
1792 const nsIContent
* aAncestorLimiter
= nullptr) {
1793 if (HTMLEditUtils::IsBlockElement(aContent
, aBlockInlineCheck
)) {
1797 // If aNode is the editing host itself, there is no modifiable inline
1799 if (&aContent
== aEditingHost
|| &aContent
== aAncestorLimiter
) {
1803 // If aNode is outside of the <body> element, we don't support to edit
1804 // such elements for now.
1805 // XXX This should be MOZ_ASSERT after fixing bug 1413131 for avoiding
1806 // calling this expensive method.
1807 if (aEditingHost
&& !aContent
.IsInclusiveDescendantOf(aEditingHost
)) {
1811 if (!aContent
.GetParent()) {
1812 return const_cast<nsIContent
*>(&aContent
);
1815 // Looks for the highest inline parent in the editing host.
1816 nsIContent
* topMostInlineContent
= const_cast<nsIContent
*>(&aContent
);
1817 for (Element
* element
: aContent
.AncestorsOfType
<Element
>()) {
1818 if (element
== aEditingHost
|| element
== aAncestorLimiter
||
1819 HTMLEditUtils::IsBlockElement(*element
, aBlockInlineCheck
)) {
1822 topMostInlineContent
= element
;
1824 return topMostInlineContent
;
1828 * GetMostDistantAncestorEditableEmptyInlineElement() returns most distant
1829 * ancestor which only has aEmptyContent or its ancestor, editable and
1832 static Element
* GetMostDistantAncestorEditableEmptyInlineElement(
1833 const nsIContent
& aEmptyContent
, BlockInlineCheck aBlockInlineCheck
,
1834 const Element
* aEditingHost
= nullptr,
1835 const nsIContent
* aAncestorLimiter
= nullptr) {
1836 if (&aEmptyContent
== aEditingHost
|| &aEmptyContent
== aAncestorLimiter
) {
1839 nsIContent
* lastEmptyContent
= const_cast<nsIContent
*>(&aEmptyContent
);
1840 for (Element
* element
: aEmptyContent
.AncestorsOfType
<Element
>()) {
1841 if (element
== aEditingHost
|| element
== aAncestorLimiter
) {
1844 if (!HTMLEditUtils::IsInlineContent(*element
, aBlockInlineCheck
) ||
1845 !HTMLEditUtils::IsSimplyEditableNode(*element
)) {
1848 if (element
->GetChildCount() > 1) {
1849 for (const nsIContent
* child
= element
->GetFirstChild(); child
;
1850 child
= child
->GetNextSibling()) {
1851 if (child
== lastEmptyContent
|| child
->IsComment()) {
1854 return lastEmptyContent
!= &aEmptyContent
1855 ? Element::FromNode(lastEmptyContent
)
1859 lastEmptyContent
= element
;
1861 return lastEmptyContent
!= &aEmptyContent
1862 ? Element::FromNode(lastEmptyContent
)
1867 * GetElementIfOnlyOneSelected() returns an element if aRange selects only
1868 * the element node (and its descendants).
1870 static Element
* GetElementIfOnlyOneSelected(const AbstractRange
& aRange
) {
1871 return GetElementIfOnlyOneSelected(EditorRawDOMRange(aRange
));
1873 template <typename EditorDOMPointType
>
1874 static Element
* GetElementIfOnlyOneSelected(
1875 const EditorDOMRangeBase
<EditorDOMPointType
>& aRange
) {
1876 if (!aRange
.IsPositioned() || aRange
.Collapsed()) {
1879 const auto& start
= aRange
.StartRef();
1880 const auto& end
= aRange
.EndRef();
1881 if (NS_WARN_IF(!start
.IsSetAndValid()) ||
1882 NS_WARN_IF(!end
.IsSetAndValid()) ||
1883 start
.GetContainer() != end
.GetContainer()) {
1886 nsIContent
* childAtStart
= start
.GetChild();
1887 if (!childAtStart
|| !childAtStart
->IsElement()) {
1890 // If start child is not the last sibling and only if end child is its
1891 // next sibling, the start child is selected.
1892 if (childAtStart
->GetNextSibling()) {
1893 return childAtStart
->GetNextSibling() == end
.GetChild()
1894 ? childAtStart
->AsElement()
1897 // If start child is the last sibling and only if no child at the end,
1898 // the start child is selected.
1899 return !end
.GetChild() ? childAtStart
->AsElement() : nullptr;
1902 static Element
* GetTableCellElementIfOnlyOneSelected(
1903 const AbstractRange
& aRange
) {
1904 Element
* element
= HTMLEditUtils::GetElementIfOnlyOneSelected(aRange
);
1905 return element
&& HTMLEditUtils::IsTableCell(element
) ? element
: nullptr;
1909 * GetFirstSelectedTableCellElement() returns a table cell element (i.e.,
1910 * `<td>` or `<th>` if and only if first selection range selects only a
1911 * table cell element.
1913 static Element
* GetFirstSelectedTableCellElement(
1914 const Selection
& aSelection
) {
1915 if (!aSelection
.RangeCount()) {
1918 const nsRange
* firstRange
= aSelection
.GetRangeAt(0);
1919 if (NS_WARN_IF(!firstRange
) || NS_WARN_IF(!firstRange
->IsPositioned())) {
1922 return GetTableCellElementIfOnlyOneSelected(*firstRange
);
1926 * GetInclusiveFirstChildWhichHasOneChild() returns the deepest element whose
1927 * tag name is one of `aFirstElementName` and `aOtherElementNames...` if and
1928 * only if the elements have only one child node. In other words, when
1929 * this method meets an element which does not matches any of the tag name
1930 * or it has no children or 2+ children.
1932 * XXX This method must be implemented without treating edge cases. So, the
1933 * behavior is odd. E.g., why can we ignore non-editable node at counting
1934 * each children? Why do we dig non-editable aNode or first child of its
1937 template <typename FirstElementName
, typename
... OtherElementNames
>
1938 static Element
* GetInclusiveDeepestFirstChildWhichHasOneChild(
1939 const nsINode
& aNode
, const WalkTreeOptions
& aOptions
,
1940 BlockInlineCheck aBlockInlineCheck
, FirstElementName aFirstElementName
,
1941 OtherElementNames
... aOtherElementNames
) {
1942 if (!aNode
.IsElement()) {
1945 Element
* parentElement
= nullptr;
1946 for (nsIContent
* content
= const_cast<nsIContent
*>(aNode
.AsContent());
1947 content
&& content
->IsElement() &&
1948 content
->IsAnyOfHTMLElements(aFirstElementName
, aOtherElementNames
...);
1949 // XXX Why do we scan only the first child of every element? If it's
1950 // not editable, why do we ignore it when aOptions specifies so.
1951 content
= content
->GetFirstChild()) {
1952 if (HTMLEditUtils::CountChildren(*content
, aOptions
, aBlockInlineCheck
) !=
1954 return content
->AsElement();
1956 parentElement
= content
->AsElement();
1958 return parentElement
;
1962 * Get the first <br> element in aElement. This scans only leaf nodes so
1963 * if a <br> element has children illegally, it'll be ignored.
1965 * @param aElement The element which may have a <br> element.
1966 * @return First <br> element node in aElement if there is.
1968 static dom::HTMLBRElement
* GetFirstBRElement(const dom::Element
& aElement
) {
1969 for (nsIContent
* content
= HTMLEditUtils::GetFirstLeafContent(
1970 aElement
, {LeafNodeType::OnlyLeafNode
});
1971 content
; content
= HTMLEditUtils::GetNextContent(
1973 {WalkTreeOption::IgnoreDataNodeExceptText
,
1974 WalkTreeOption::IgnoreWhiteSpaceOnlyText
},
1975 BlockInlineCheck::Unused
, &aElement
)) {
1976 if (auto* brElement
= dom::HTMLBRElement::FromNode(*content
)) {
1984 * Return last <br> element or last text node ending with a preserved line
1985 * break of/before aBlockElement.
1987 enum ScanLineBreak
{
1991 static nsIContent
* GetUnnecessaryLineBreakContent(
1992 const Element
& aBlockElement
, ScanLineBreak aScanLineBreak
);
1995 * IsInTableCellSelectionMode() returns true when Gecko's editor thinks that
1996 * selection is in a table cell selection mode.
1997 * Note that Gecko's editor traditionally treats selection as in table cell
1998 * selection mode when first range selects a table cell element. I.e., even
1999 * if `nsFrameSelection` is not in table cell selection mode, this may return
2002 static bool IsInTableCellSelectionMode(const Selection
& aSelection
) {
2003 return GetFirstSelectedTableCellElement(aSelection
) != nullptr;
2006 static EditAction
GetEditActionForInsert(const nsAtom
& aTagName
);
2007 static EditAction
GetEditActionForRemoveList(const nsAtom
& aTagName
);
2008 static EditAction
GetEditActionForInsert(const Element
& aElement
);
2009 static EditAction
GetEditActionForFormatText(const nsAtom
& aProperty
,
2010 const nsAtom
* aAttribute
,
2012 static EditAction
GetEditActionForAlignment(const nsAString
& aAlignType
);
2015 * GetPreviousNonCollapsibleCharOffset() returns offset of previous
2016 * character which is not collapsible white-space characters.
2018 enum class WalkTextOption
{
2019 TreatNBSPsCollapsible
,
2021 using WalkTextOptions
= EnumSet
<WalkTextOption
>;
2022 static Maybe
<uint32_t> GetPreviousNonCollapsibleCharOffset(
2023 const EditorDOMPointInText
& aPoint
,
2024 const WalkTextOptions
& aWalkTextOptions
= {}) {
2025 MOZ_ASSERT(aPoint
.IsSetAndValid());
2026 return GetPreviousNonCollapsibleCharOffset(
2027 *aPoint
.ContainerAs
<Text
>(), aPoint
.Offset(), aWalkTextOptions
);
2029 static Maybe
<uint32_t> GetPreviousNonCollapsibleCharOffset(
2030 const Text
& aTextNode
, uint32_t aOffset
,
2031 const WalkTextOptions
& aWalkTextOptions
= {}) {
2032 const bool isWhiteSpaceCollapsible
=
2033 !EditorUtils::IsWhiteSpacePreformatted(aTextNode
);
2034 const bool isNewLineCollapsible
=
2035 !EditorUtils::IsNewLinePreformatted(aTextNode
);
2036 const bool isNBSPCollapsible
=
2037 isWhiteSpaceCollapsible
&&
2038 aWalkTextOptions
.contains(WalkTextOption::TreatNBSPsCollapsible
);
2039 const nsTextFragment
& textFragment
= aTextNode
.TextFragment();
2040 MOZ_ASSERT(aOffset
<= textFragment
.GetLength());
2041 for (uint32_t i
= aOffset
; i
; i
--) {
2042 // TODO: Perhaps, nsTextFragment should have scanner methods because
2043 // the text may be in per-one-byte storage or per-two-byte storage,
2044 // and `CharAt` needs to check it everytime.
2045 switch (textFragment
.CharAt(i
- 1)) {
2046 case HTMLEditUtils::kSpace
:
2047 case HTMLEditUtils::kCarriageReturn
:
2048 case HTMLEditUtils::kTab
:
2049 if (!isWhiteSpaceCollapsible
) {
2053 case HTMLEditUtils::kNewLine
:
2054 if (!isNewLineCollapsible
) {
2058 case HTMLEditUtils::kNBSP
:
2059 if (!isNBSPCollapsible
) {
2064 MOZ_ASSERT(!nsCRT::IsAsciiSpace(textFragment
.CharAt(i
- 1)));
2072 * GetNextNonCollapsibleCharOffset() returns offset of next character which is
2073 * not collapsible white-space characters.
2075 static Maybe
<uint32_t> GetNextNonCollapsibleCharOffset(
2076 const EditorDOMPointInText
& aPoint
,
2077 const WalkTextOptions
& aWalkTextOptions
= {}) {
2078 MOZ_ASSERT(aPoint
.IsSetAndValid());
2079 return GetNextNonCollapsibleCharOffset(*aPoint
.ContainerAs
<Text
>(),
2080 aPoint
.Offset(), aWalkTextOptions
);
2082 static Maybe
<uint32_t> GetNextNonCollapsibleCharOffset(
2083 const Text
& aTextNode
, uint32_t aOffset
,
2084 const WalkTextOptions
& aWalkTextOptions
= {}) {
2085 return GetInclusiveNextNonCollapsibleCharOffset(aTextNode
, aOffset
+ 1,
2090 * GetInclusiveNextNonCollapsibleCharOffset() returns offset of inclusive next
2091 * character which is not collapsible white-space characters.
2093 static Maybe
<uint32_t> GetInclusiveNextNonCollapsibleCharOffset(
2094 const EditorDOMPointInText
& aPoint
,
2095 const WalkTextOptions
& aWalkTextOptions
= {}) {
2096 MOZ_ASSERT(aPoint
.IsSetAndValid());
2097 return GetInclusiveNextNonCollapsibleCharOffset(
2098 *aPoint
.ContainerAs
<Text
>(), aPoint
.Offset(), aWalkTextOptions
);
2100 static Maybe
<uint32_t> GetInclusiveNextNonCollapsibleCharOffset(
2101 const Text
& aTextNode
, uint32_t aOffset
,
2102 const WalkTextOptions
& aWalkTextOptions
= {}) {
2103 const bool isWhiteSpaceCollapsible
=
2104 !EditorUtils::IsWhiteSpacePreformatted(aTextNode
);
2105 const bool isNewLineCollapsible
=
2106 !EditorUtils::IsNewLinePreformatted(aTextNode
);
2107 const bool isNBSPCollapsible
=
2108 isWhiteSpaceCollapsible
&&
2109 aWalkTextOptions
.contains(WalkTextOption::TreatNBSPsCollapsible
);
2110 const nsTextFragment
& textFragment
= aTextNode
.TextFragment();
2111 MOZ_ASSERT(aOffset
<= textFragment
.GetLength());
2112 for (uint32_t i
= aOffset
; i
< textFragment
.GetLength(); i
++) {
2113 // TODO: Perhaps, nsTextFragment should have scanner methods because
2114 // the text may be in per-one-byte storage or per-two-byte storage,
2115 // and `CharAt` needs to check it everytime.
2116 switch (textFragment
.CharAt(i
)) {
2117 case HTMLEditUtils::kSpace
:
2118 case HTMLEditUtils::kCarriageReturn
:
2119 case HTMLEditUtils::kTab
:
2120 if (!isWhiteSpaceCollapsible
) {
2124 case HTMLEditUtils::kNewLine
:
2125 if (!isNewLineCollapsible
) {
2129 case HTMLEditUtils::kNBSP
:
2130 if (!isNBSPCollapsible
) {
2135 MOZ_ASSERT(!nsCRT::IsAsciiSpace(textFragment
.CharAt(i
)));
2143 * GetFirstWhiteSpaceOffsetCollapsedWith() returns first collapsible
2144 * white-space offset which is collapsed with a white-space at the given
2145 * position. I.e., the character at the position must be a collapsible
2148 static uint32_t GetFirstWhiteSpaceOffsetCollapsedWith(
2149 const EditorDOMPointInText
& aPoint
,
2150 const WalkTextOptions
& aWalkTextOptions
= {}) {
2151 MOZ_ASSERT(aPoint
.IsSetAndValid());
2152 MOZ_ASSERT(!aPoint
.IsEndOfContainer());
2154 aWalkTextOptions
.contains(WalkTextOption::TreatNBSPsCollapsible
),
2155 aPoint
.IsCharCollapsibleASCIISpaceOrNBSP());
2157 !aWalkTextOptions
.contains(WalkTextOption::TreatNBSPsCollapsible
),
2158 aPoint
.IsCharCollapsibleASCIISpace());
2159 return GetFirstWhiteSpaceOffsetCollapsedWith(
2160 *aPoint
.ContainerAs
<Text
>(), aPoint
.Offset(), aWalkTextOptions
);
2162 static uint32_t GetFirstWhiteSpaceOffsetCollapsedWith(
2163 const Text
& aTextNode
, uint32_t aOffset
,
2164 const WalkTextOptions
& aWalkTextOptions
= {}) {
2165 MOZ_ASSERT(aOffset
< aTextNode
.TextLength());
2167 aWalkTextOptions
.contains(WalkTextOption::TreatNBSPsCollapsible
),
2168 EditorRawDOMPoint(&aTextNode
, aOffset
)
2169 .IsCharCollapsibleASCIISpaceOrNBSP());
2171 !aWalkTextOptions
.contains(WalkTextOption::TreatNBSPsCollapsible
),
2172 EditorRawDOMPoint(&aTextNode
, aOffset
).IsCharCollapsibleASCIISpace());
2176 Maybe
<uint32_t> previousVisibleCharOffset
=
2177 GetPreviousNonCollapsibleCharOffset(aTextNode
, aOffset
,
2179 return previousVisibleCharOffset
.isSome()
2180 ? previousVisibleCharOffset
.value() + 1
2185 * GetPreviousPreformattedNewLineInTextNode() returns a point which points
2186 * previous preformatted linefeed if there is and aPoint is in a text node.
2187 * If the node's linefeed characters are not preformatted or aPoint is not
2188 * in a text node, this returns unset DOM point.
2190 template <typename EditorDOMPointType
, typename ArgEditorDOMPointType
>
2191 static EditorDOMPointType
GetPreviousPreformattedNewLineInTextNode(
2192 const ArgEditorDOMPointType
& aPoint
) {
2193 if (!aPoint
.IsInTextNode() || aPoint
.IsStartOfContainer() ||
2194 !EditorUtils::IsNewLinePreformatted(
2195 *aPoint
.template ContainerAs
<Text
>())) {
2196 return EditorDOMPointType();
2198 Text
* textNode
= aPoint
.template ContainerAs
<Text
>();
2199 const nsTextFragment
& textFragment
= textNode
->TextFragment();
2200 MOZ_ASSERT(aPoint
.Offset() <= textFragment
.GetLength());
2201 for (uint32_t offset
= aPoint
.Offset(); offset
; --offset
) {
2202 if (textFragment
.CharAt(offset
- 1) == HTMLEditUtils::kNewLine
) {
2203 return EditorDOMPointType(textNode
, offset
- 1);
2206 return EditorDOMPointType();
2210 * GetInclusiveNextPreformattedNewLineInTextNode() returns a point which
2211 * points inclusive next preformatted linefeed if there is and aPoint is in a
2212 * text node. If the node's linefeed characters are not preformatted or aPoint
2213 * is not in a text node, this returns unset DOM point.
2215 template <typename EditorDOMPointType
, typename ArgEditorDOMPointType
>
2216 static EditorDOMPointType
GetInclusiveNextPreformattedNewLineInTextNode(
2217 const ArgEditorDOMPointType
& aPoint
) {
2218 if (!aPoint
.IsInTextNode() || aPoint
.IsEndOfContainer() ||
2219 !EditorUtils::IsNewLinePreformatted(
2220 *aPoint
.template ContainerAs
<Text
>())) {
2221 return EditorDOMPointType();
2223 Text
* textNode
= aPoint
.template ContainerAs
<Text
>();
2224 const nsTextFragment
& textFragment
= textNode
->TextFragment();
2225 for (uint32_t offset
= aPoint
.Offset(); offset
< textFragment
.GetLength();
2227 if (textFragment
.CharAt(offset
) == HTMLEditUtils::kNewLine
) {
2228 return EditorDOMPointType(textNode
, offset
);
2231 return EditorDOMPointType();
2235 * GetGoodCaretPointFor() returns a good point to collapse `Selection`
2236 * after handling edit action with aDirectionAndAmount.
2238 * @param aContent The content where you want to put caret
2240 * @param aDirectionAndAmount Muse be one of eNext, eNextWord, eToEndOfLine,
2241 * ePrevious, ePreviousWord and eToBeggingOfLine.
2242 * Set the direction of handled edit action.
2244 template <typename EditorDOMPointType
>
2245 static EditorDOMPointType
GetGoodCaretPointFor(
2246 nsIContent
& aContent
, nsIEditor::EDirection aDirectionAndAmount
) {
2247 MOZ_ASSERT(nsIEditor::EDirectionIsValidExceptNone(aDirectionAndAmount
));
2249 // XXX Why don't we check whether the candidate position is enable or not?
2250 // When the result is not editable point, caret will be enclosed in
2251 // the non-editable content.
2253 // If we can put caret in aContent, return start or end in it.
2254 if (aContent
.IsText() || HTMLEditUtils::IsContainerNode(aContent
) ||
2255 NS_WARN_IF(!aContent
.GetParentNode())) {
2256 return EditorDOMPointType(
2257 &aContent
, nsIEditor::DirectionIsDelete(aDirectionAndAmount
)
2259 : aContent
.Length());
2262 // If we are going forward, put caret at aContent itself.
2263 if (nsIEditor::DirectionIsDelete(aDirectionAndAmount
)) {
2264 return EditorDOMPointType(&aContent
);
2267 // If we are going backward, put caret to next node unless aContent is an
2268 // invisible `<br>` element.
2269 // XXX Shouldn't we put caret to first leaf of the next node?
2270 if (!HTMLEditUtils::IsInvisibleBRElement(aContent
)) {
2271 EditorDOMPointType
ret(EditorDOMPointType::After(aContent
));
2272 NS_WARNING_ASSERTION(ret
.IsSet(), "Failed to set after aContent");
2276 // Otherwise, we should put caret at the invisible `<br>` element.
2277 return EditorDOMPointType(&aContent
);
2281 * GetBetterInsertionPointFor() returns better insertion point to insert
2284 * @param aContentToInsert The content to insert.
2285 * @param aPointToInsert A candidate point to insert the node.
2286 * @param aEditingHost The editing host containing aPointToInsert.
2287 * @return Better insertion point if next visible node
2288 * is a <br> element and previous visible node
2289 * is neither none, another <br> element nor
2290 * different block level element.
2292 template <typename EditorDOMPointType
, typename EditorDOMPointTypeInput
>
2293 static EditorDOMPointType
GetBetterInsertionPointFor(
2294 const nsIContent
& aContentToInsert
,
2295 const EditorDOMPointTypeInput
& aPointToInsert
,
2296 const Element
& aEditingHost
);
2299 * GetBetterCaretPositionToInsertText() returns better point to put caret
2300 * if aPoint is near a text node or in non-container node.
2302 template <typename EditorDOMPointType
, typename EditorDOMPointTypeInput
>
2303 static EditorDOMPointType
GetBetterCaretPositionToInsertText(
2304 const EditorDOMPointTypeInput
& aPoint
, const Element
& aEditingHost
);
2307 * ComputePointToPutCaretInElementIfOutside() returns a good point in aElement
2308 * to put caret if aCurrentPoint is outside of aElement.
2310 * @param aElement The result is a point in aElement.
2311 * @param aCurrentPoint The current (candidate) caret point. Only if this
2312 * is outside aElement, returns a point in aElement.
2314 template <typename EditorDOMPointType
, typename EditorDOMPointTypeInput
>
2315 static Result
<EditorDOMPointType
, nsresult
>
2316 ComputePointToPutCaretInElementIfOutside(
2317 const Element
& aElement
, const EditorDOMPointTypeInput
& aCurrentPoint
);
2320 * Content-based query returns true if
2321 * <mHTMLProperty mAttribute=mAttributeValue> effects aContent. If there is
2322 * such a element, but another element whose attribute value does not match
2323 * with mAttributeValue is closer ancestor of aContent, then the distant
2324 * ancestor does not effect aContent.
2326 * @param aContent The target of the query
2327 * @param aStyle The style which queries a representing element.
2328 * @param aValue Optional, the value of aStyle.mAttribute, example: blue
2329 * in <font color="blue"> May be null. Ignored if
2330 * aStyle.mAttribute is null.
2331 * @param aOutValue [OUT] the value of the attribute, if returns true
2332 * @return true if <mHTMLProperty mAttribute=mAttributeValue>
2335 [[nodiscard
]] static bool IsInlineStyleSetByElement(
2336 const nsIContent
& aContent
, const EditorInlineStyle
& aStyle
,
2337 const nsAString
* aValue
, nsAString
* aOutValue
= nullptr);
2340 * CollectAllChildren() collects all child nodes of aParentNode.
2342 static void CollectAllChildren(
2343 const nsINode
& aParentNode
,
2344 nsTArray
<OwningNonNull
<nsIContent
>>& aOutArrayOfContents
) {
2345 MOZ_ASSERT(aOutArrayOfContents
.IsEmpty());
2346 aOutArrayOfContents
.SetCapacity(aParentNode
.GetChildCount());
2347 for (nsIContent
* childContent
= aParentNode
.GetFirstChild(); childContent
;
2348 childContent
= childContent
->GetNextSibling()) {
2349 aOutArrayOfContents
.AppendElement(*childContent
);
2354 * CollectChildren() collects child nodes of aNode (starting from
2355 * first editable child, but may return non-editable children after it).
2357 * @param aNode Parent node of retrieving children.
2358 * @param aOutArrayOfContents [out] This method will inserts found children
2360 * @param aIndexToInsertChildren Starting from this index, found
2361 * children will be inserted to the array.
2362 * @param aOptions Options to scan the children.
2363 * @return Number of found children.
2365 static size_t CollectChildren(
2366 const nsINode
& aNode
,
2367 nsTArray
<OwningNonNull
<nsIContent
>>& aOutArrayOfContents
,
2368 const CollectChildrenOptions
& aOptions
) {
2369 return HTMLEditUtils::CollectChildren(aNode
, aOutArrayOfContents
, 0u,
2372 static size_t CollectChildren(
2373 const nsINode
& aNode
,
2374 nsTArray
<OwningNonNull
<nsIContent
>>& aOutArrayOfContents
,
2375 size_t aIndexToInsertChildren
, const CollectChildrenOptions
& aOptions
);
2378 * CollectEmptyInlineContainerDescendants() appends empty inline elements in
2379 * aNode to aOutArrayOfContents. Although it's array of nsIContent, the
2380 * instance will be elements.
2382 * @param aNode The node whose descendants may have empty inline
2384 * @param aOutArrayOfContents [out] This method will append found descendants
2386 * @param aOptions The option which element should be treated as
2388 * @param aBlockInlineCheck Whether use computed style or HTML default style
2389 * when consider block vs. inline.
2390 * @return Number of found elements.
2392 static size_t CollectEmptyInlineContainerDescendants(
2393 const nsINode
& aNode
,
2394 nsTArray
<OwningNonNull
<nsIContent
>>& aOutArrayOfContents
,
2395 const EmptyCheckOptions
& aOptions
, BlockInlineCheck aBlockInlineCheck
);
2398 * Check whether aElement has attributes except the name aAttribute and
2399 * "_moz_*" attributes.
2401 [[nodiscard
]] static bool ElementHasAttribute(const Element
& aElement
) {
2402 return ElementHasAttributeExcept(aElement
, *nsGkAtoms::_empty
,
2403 *nsGkAtoms::empty
, *nsGkAtoms::_empty
);
2405 [[nodiscard
]] static bool ElementHasAttributeExcept(
2406 const Element
& aElement
, const nsAtom
& aAttribute
) {
2407 return ElementHasAttributeExcept(aElement
, aAttribute
, *nsGkAtoms::_empty
,
2410 [[nodiscard
]] static bool ElementHasAttributeExcept(
2411 const Element
& aElement
, const nsAtom
& aAttribute1
,
2412 const nsAtom
& aAttribute2
) {
2413 return ElementHasAttributeExcept(aElement
, aAttribute1
, aAttribute2
,
2416 [[nodiscard
]] static bool ElementHasAttributeExcept(
2417 const Element
& aElement
, const nsAtom
& aAttribute1
,
2418 const nsAtom
& aAttribute2
, const nsAtom
& aAttribute3
);
2421 * Returns EditorDOMPoint which points deepest editable start/end point of
2422 * aNode. If a node is a container node and first/last child is editable,
2423 * returns the child's start or last point recursively.
2425 enum class InvisibleText
{ Recognize
, Skip
};
2426 template <typename EditorDOMPointType
>
2427 [[nodiscard
]] static EditorDOMPointType
GetDeepestEditableStartPointOf(
2428 const nsIContent
& aContent
,
2429 InvisibleText aInvisibleText
= InvisibleText::Recognize
) {
2430 if (NS_WARN_IF(!EditorUtils::IsEditableContent(
2431 aContent
, EditorBase::EditorType::HTML
))) {
2432 return EditorDOMPointType();
2434 EditorDOMPointType
result(&aContent
, 0u);
2436 nsIContent
* firstChild
= result
.GetContainer()->GetFirstChild();
2440 // If the caller wants to skip invisible white-spaces, we should skip
2441 // invisible text nodes.
2442 if (aInvisibleText
== InvisibleText::Skip
&& firstChild
->IsText() &&
2443 EditorUtils::IsEditableContent(*firstChild
,
2444 EditorBase::EditorType::HTML
) &&
2445 !HTMLEditUtils::IsVisibleTextNode(*firstChild
->AsText())) {
2446 for (nsIContent
* nextSibling
= firstChild
->GetNextSibling();
2447 nextSibling
; nextSibling
= nextSibling
->GetNextSibling()) {
2448 if (!nextSibling
->IsText() ||
2449 // We know its previous sibling is very start of a block.
2450 // Therefore, we only need to scan the text here.
2451 HTMLEditUtils::GetInclusiveNextNonCollapsibleCharOffset(
2452 *firstChild
->AsText(), 0u)
2454 firstChild
= nextSibling
;
2459 if ((!firstChild
->IsText() &&
2460 !HTMLEditUtils::IsContainerNode(*firstChild
)) ||
2461 !EditorUtils::IsEditableContent(*firstChild
,
2462 EditorBase::EditorType::HTML
)) {
2465 if (aInvisibleText
== InvisibleText::Skip
&& firstChild
->IsText()) {
2466 result
.Set(firstChild
,
2467 HTMLEditUtils::GetInclusiveNextNonCollapsibleCharOffset(
2468 *firstChild
->AsText(), 0u)
2472 result
.Set(firstChild
, 0u);
2476 template <typename EditorDOMPointType
>
2477 [[nodiscard
]] static EditorDOMPointType
GetDeepestEditableEndPointOf(
2478 const nsIContent
& aContent
,
2479 InvisibleText aInvisibleText
= InvisibleText::Recognize
) {
2480 if (NS_WARN_IF(!EditorUtils::IsEditableContent(
2481 aContent
, EditorBase::EditorType::HTML
))) {
2482 return EditorDOMPointType();
2484 auto result
= EditorDOMPointType::AtEndOf(aContent
);
2486 nsIContent
* lastChild
= result
.GetContainer()->GetLastChild();
2490 // If the caller wants to skip invisible white-spaces, we should skip
2491 // invisible text nodes.
2492 if (aInvisibleText
== InvisibleText::Skip
&& lastChild
->IsText() &&
2493 EditorUtils::IsEditableContent(*lastChild
,
2494 EditorBase::EditorType::HTML
) &&
2495 !HTMLEditUtils::IsVisibleTextNode(*lastChild
->AsText())) {
2496 for (nsIContent
* nextSibling
= lastChild
->GetPreviousSibling();
2497 nextSibling
; nextSibling
= nextSibling
->GetPreviousSibling()) {
2498 if (!nextSibling
->IsText() ||
2499 // We know its previous sibling is very start of a block.
2500 // Therefore, we only need to scan the text here.
2501 HTMLEditUtils::GetPreviousNonCollapsibleCharOffset(
2502 *lastChild
->AsText(), lastChild
->AsText()->TextDataLength())
2504 lastChild
= nextSibling
;
2509 if ((!lastChild
->IsText() &&
2510 !HTMLEditUtils::IsContainerNode(*lastChild
)) ||
2511 !EditorUtils::IsEditableContent(*lastChild
,
2512 EditorBase::EditorType::HTML
)) {
2515 if (aInvisibleText
== InvisibleText::Skip
&& lastChild
->IsText()) {
2516 Maybe
<uint32_t> visibleCharOffset
=
2517 HTMLEditUtils::GetPreviousNonCollapsibleCharOffset(
2518 *lastChild
->AsText(), lastChild
->AsText()->TextDataLength());
2519 if (visibleCharOffset
.isNothing()) {
2520 result
= EditorDOMPointType::AtEndOf(*lastChild
);
2523 result
.Set(lastChild
, visibleCharOffset
.value() + 1u);
2526 result
= EditorDOMPointType::AtEndOf(*lastChild
);
2532 * Get `#[0-9a-f]{6}` style HTML color value if aColorValue is valid value
2533 * for color-specifying attribute. The result is useful to set attributes
2534 * of HTML elements which take a color value.
2536 * @param aColorValue [in] Should be one of `#[0-9a-fA-Z]{3}`,
2537 * `#[0-9a-fA-Z]{3}` or a color name.
2538 * @param aNormalizedValue [out] Set to `#[0-9a-f]{6}` style color code
2539 * if this returns true. Otherwise, returns
2540 * aColorValue as-is.
2541 * @return true if aColorValue is valid. Otherwise, false.
2543 static bool GetNormalizedHTMLColorValue(const nsAString
& aColorValue
,
2544 nsAString
& aNormalizedValue
);
2547 * Return true if aColorValue may be a CSS specific color value or general
2550 [[nodiscard
]] static bool MaybeCSSSpecificColorValue(
2551 const nsAString
& aColorValue
);
2554 * Return true if aColorValue can be specified to `color` value of <font>.
2556 [[nodiscard
]] static bool CanConvertToHTMLColorValue(
2557 const nsAString
& aColorValue
);
2560 * Convert aColorValue to `#[0-9a-f]{6}` style HTML color value.
2562 static bool ConvertToNormalizedHTMLColorValue(const nsAString
& aColorValue
,
2563 nsAString
& aNormalizedValue
);
2566 * Get serialized color value (`rgb(...)` or `rgba(...)`) or "currentcolor"
2567 * if aColorValue is valid. The result is useful to set CSS color property.
2569 * @param aColorValue [in] Should be valid CSS color value.
2570 * @param aZeroAlphaColor [in] If TransparentKeyword, aNormalizedValue is
2571 * set to "transparent" if the alpha value is 0.
2572 * Otherwise, `rgba(...)` value is set.
2573 * @param aNormalizedValue [out] Serialized color value or "currentcolor".
2574 * @return true if aColorValue is valid. Otherwise, false.
2576 enum class ZeroAlphaColor
{ RGBAValue
, TransparentKeyword
};
2577 static bool GetNormalizedCSSColorValue(const nsAString
& aColorValue
,
2578 ZeroAlphaColor aZeroAlphaColor
,
2579 nsAString
& aNormalizedValue
);
2582 * Check whether aColorA and aColorB are same color.
2584 * @param aTransparentKeyword Whether allow to treat "transparent" keyword
2585 * as a valid value or an invalid value.
2586 * @return If aColorA and aColorB are valid values and
2587 * mean same color, returns true.
2589 enum class TransparentKeyword
{ Invalid
, Allowed
};
2590 static bool IsSameHTMLColorValue(const nsAString
& aColorA
,
2591 const nsAString
& aColorB
,
2592 TransparentKeyword aTransparentKeyword
);
2595 * Check whether aColorA and aColorB are same color.
2597 * @return If aColorA and aColorB are valid values and
2598 * mean same color, returns true.
2600 template <typename CharType
>
2601 static bool IsSameCSSColorValue(const nsTSubstring
<CharType
>& aColorA
,
2602 const nsTSubstring
<CharType
>& aColorB
);
2605 static bool CanNodeContain(nsHTMLTag aParentTagId
, nsHTMLTag aChildTagId
);
2606 static bool IsContainerNode(nsHTMLTag aTagId
);
2608 static bool CanCrossContentBoundary(nsIContent
& aContent
,
2609 TableBoundary aHowToTreatTableBoundary
) {
2610 const bool cannotCrossBoundary
=
2611 (aHowToTreatTableBoundary
== TableBoundary::NoCrossAnyTableElement
&&
2612 HTMLEditUtils::IsAnyTableElement(&aContent
)) ||
2613 (aHowToTreatTableBoundary
== TableBoundary::NoCrossTableElement
&&
2614 aContent
.IsHTMLElement(nsGkAtoms::table
));
2615 return !cannotCrossBoundary
;
2618 static bool IsContentIgnored(const nsIContent
& aContent
,
2619 const WalkTreeOptions
& aOptions
) {
2620 if (aOptions
.contains(WalkTreeOption::IgnoreNonEditableNode
) &&
2621 !EditorUtils::IsEditableContent(aContent
,
2622 EditorUtils::EditorType::HTML
)) {
2625 if (aOptions
.contains(WalkTreeOption::IgnoreDataNodeExceptText
) &&
2626 !EditorUtils::IsElementOrText(aContent
)) {
2629 if (aOptions
.contains(WalkTreeOption::IgnoreWhiteSpaceOnlyText
) &&
2630 aContent
.IsText() &&
2631 const_cast<Text
*>(aContent
.AsText())->TextIsOnlyWhitespace()) {
2637 static uint32_t CountChildren(const nsINode
& aNode
,
2638 const WalkTreeOptions
& aOptions
,
2639 BlockInlineCheck aBlockInlineCheck
) {
2641 for (nsIContent
* child
= aNode
.GetFirstChild(); child
;
2642 child
= child
->GetNextSibling()) {
2643 if (HTMLEditUtils::IsContentIgnored(*child
, aOptions
)) {
2646 if (aOptions
.contains(WalkTreeOption::StopAtBlockBoundary
) &&
2647 HTMLEditUtils::IsBlockElement(*child
, aBlockInlineCheck
)) {
2656 * Helper for GetPreviousContent() and GetNextContent().
2658 static nsIContent
* GetAdjacentLeafContent(
2659 const nsINode
& aNode
, WalkTreeDirection aWalkTreeDirection
,
2660 const WalkTreeOptions
& aOptions
, BlockInlineCheck aBlockInlineCheck
,
2661 const Element
* aAncestorLimiter
= nullptr);
2662 static nsIContent
* GetAdjacentContent(
2663 const nsINode
& aNode
, WalkTreeDirection aWalkTreeDirection
,
2664 const WalkTreeOptions
& aOptions
, BlockInlineCheck aBlockInlineCheck
,
2665 const Element
* aAncestorLimiter
= nullptr);
2668 * GetElementOfImmediateBlockBoundary() returns a block element if its
2669 * block boundary and aContent may be first visible thing before/after the
2670 * boundary. And it may return a <br> element only when aContent is a
2671 * text node and follows a <br> element because only in this case, the
2672 * start white-spaces are invisible. So the <br> element works same as
2675 static Element
* GetElementOfImmediateBlockBoundary(
2676 const nsIContent
& aContent
, const WalkTreeDirection aDirection
);
2680 * DefinitionListItemScanner() scans given `<dl>` element's children.
2681 * Then, you can check whether `<dt>` and/or `<dd>` elements are in it.
2683 class MOZ_STACK_CLASS DefinitionListItemScanner final
{
2684 using Element
= dom::Element
;
2687 DefinitionListItemScanner() = delete;
2688 explicit DefinitionListItemScanner(Element
& aDLElement
) {
2689 MOZ_ASSERT(aDLElement
.IsHTMLElement(nsGkAtoms::dl
));
2690 for (nsIContent
* child
= aDLElement
.GetFirstChild(); child
;
2691 child
= child
->GetNextSibling()) {
2692 if (child
->IsHTMLElement(nsGkAtoms::dt
)) {
2699 if (child
->IsHTMLElement(nsGkAtoms::dd
)) {
2709 bool DTElementFound() const { return mDTFound
; }
2710 bool DDElementFound() const { return mDDFound
; }
2713 bool mDTFound
= false;
2714 bool mDDFound
= false;
2718 * SelectedTableCellScanner() scans all table cell elements which are selected
2719 * by each selection range. Note that if 2nd or later ranges do not select
2720 * only one table cell element, the ranges are just ignored.
2722 class MOZ_STACK_CLASS SelectedTableCellScanner final
{
2723 using Element
= dom::Element
;
2724 using Selection
= dom::Selection
;
2727 SelectedTableCellScanner() = delete;
2728 explicit SelectedTableCellScanner(const Selection
& aSelection
) {
2729 Element
* firstSelectedCellElement
=
2730 HTMLEditUtils::GetFirstSelectedTableCellElement(aSelection
);
2731 if (!firstSelectedCellElement
) {
2732 return; // We're not in table cell selection mode.
2734 mSelectedCellElements
.SetCapacity(aSelection
.RangeCount());
2735 mSelectedCellElements
.AppendElement(*firstSelectedCellElement
);
2736 const uint32_t rangeCount
= aSelection
.RangeCount();
2737 for (const uint32_t i
: IntegerRange(1u, rangeCount
)) {
2738 MOZ_ASSERT(aSelection
.RangeCount() == rangeCount
);
2739 nsRange
* range
= aSelection
.GetRangeAt(i
);
2740 if (MOZ_UNLIKELY(NS_WARN_IF(!range
)) ||
2741 MOZ_UNLIKELY(NS_WARN_IF(!range
->IsPositioned()))) {
2742 continue; // Shouldn't occur in normal conditions.
2744 // Just ignore selection ranges which do not select only one table
2745 // cell element. This is possible case if web apps sets multiple
2746 // selections and first range selects a table cell element.
2747 if (Element
* selectedCellElement
=
2748 HTMLEditUtils::GetTableCellElementIfOnlyOneSelected(*range
)) {
2749 mSelectedCellElements
.AppendElement(*selectedCellElement
);
2754 explicit SelectedTableCellScanner(const AutoRangeArray
& aRanges
);
2756 bool IsInTableCellSelectionMode() const {
2757 return !mSelectedCellElements
.IsEmpty();
2760 const nsTArray
<OwningNonNull
<Element
>>& ElementsRef() const {
2761 return mSelectedCellElements
;
2765 * GetFirstElement() and GetNextElement() are stateful iterator methods.
2766 * This is useful to port legacy code which used old `nsITableEditor` API.
2768 Element
* GetFirstElement() const {
2769 MOZ_ASSERT(!mSelectedCellElements
.IsEmpty());
2771 return !mSelectedCellElements
.IsEmpty() ? mSelectedCellElements
[0].get()
2774 Element
* GetNextElement() const {
2775 MOZ_ASSERT(mIndex
< mSelectedCellElements
.Length());
2776 return ++mIndex
< mSelectedCellElements
.Length()
2777 ? mSelectedCellElements
[mIndex
].get()
2782 AutoTArray
<OwningNonNull
<Element
>, 16> mSelectedCellElements
;
2783 mutable size_t mIndex
= 0;
2786 } // namespace mozilla
2788 #endif // #ifndef HTMLEditUtils_h