Backed out changeset 0a5b4d7ec127 (bug 1920857) for causing failures at browser_ext_t...
[gecko.git] / editor / libeditor / HTMLEditUtils.h
blob65801598eb1524eacde4d10e3eaad1ac2dd85fa8
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
9 /**
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"
34 #include "nsCRT.h"
35 #include "nsGkAtoms.h"
36 #include "nsHTMLTags.h"
37 #include "nsTArray.h"
39 class nsAtom;
40 class nsPresContext;
42 namespace mozilla {
44 enum class CollectChildrenOption {
45 // Ignore non-editable nodes
46 IgnoreNonEditableChildren,
47 // Ignore invisible text nodes
48 IgnoreInvisibleTextNodes,
49 // Collect list children too.
50 CollectListChildren,
51 // Collect table children too.
52 CollectTableChildren,
55 class HTMLEditUtils final {
56 using AbstractRange = dom::AbstractRange;
57 using Element = dom::Element;
58 using Selection = dom::Selection;
59 using Text = dom::Text;
61 public:
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 = '>';
69 /**
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();
78 /**
79 * Return true if inclusive flat tree ancestor has `inert` state.
81 static bool ContentIsInert(const nsIContent& aContent);
83 /**
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));
98 /**
99 * IsNonEditableReplacedContent() returns true when aContent is an inclusive
100 * descendant of a replaced element whose content shouldn't be editable by
101 * user's operation.
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)) {
107 return true;
110 return false;
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
151 * joined.
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
179 * as inline.
181 * @param aBlockInlineCheck UseComputedDisplayOutsideStyle and
182 * UseComputedDisplayStyle return same result for
183 * any elements.
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
218 * styles.
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) {
228 return
229 // clang-format off
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;
252 // clang-format on
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()) {
263 return false;
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) {
275 return
276 // clang-format off
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;
289 // clang-format on
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()) {
299 return false;
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(),
329 aChild);
331 return false;
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(),
340 aChildNodeName);
342 return false;
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());
356 return false;
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;
369 } else {
370 childTagEnum =
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)) {
385 return true;
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)) {
394 return true;
397 // XXX Otherwise, Chromium checks the CSS box is a block, but we don't do it
398 // for now.
399 return false;
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) {
436 nsHTMLTag tagEnum;
437 // XXX Should this handle #cdata-section too?
438 if (aContent.IsText()) {
439 tagEnum = eHTMLTag_text;
440 } else {
441 // XXX Why don't we use nsHTMLTags::AtomTagToId? Are there some
442 // difference?
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)) {
455 return false;
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,
491 const Text& aText);
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);
503 return false;
505 static bool IsVisibleBRElement(const dom::HTMLBRElement& aBRElement) {
506 // If followed by a block boundary without visible content, it's invisible
507 // <br> element.
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);
516 return false;
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()) {
545 return false;
547 // If there are some other characters in the text node, it's a visible
548 // linefeed.
549 if (!aPoint.IsAtLastContent()) {
550 if (EditorUtils::IsWhiteSpacePreformatted(
551 *aPoint.template ContainerAs<Text>())) {
552 return true;
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
566 // linefeed.
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;
584 return false;
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,
613 SafeToAskLayout,
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)) {
659 if (foundListItem) {
660 return false; // 2 list items found.
662 if (!IsEmptyNode(*child, {})) {
663 return false; // found non-empty list item.
665 foundListItem = true;
666 continue;
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.
676 return true;
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) {
691 return false;
693 continue;
695 if (child->IsHTMLElement(nsGkAtoms::li)) {
696 if (MOZ_UNLIKELY(!aListElement.IsAnyOfHTMLElements(nsGkAtoms::ol,
697 nsGkAtoms::ul))) {
698 return false;
700 continue;
702 if (child->IsAnyOfHTMLElements(nsGkAtoms::dt, nsGkAtoms::dd)) {
703 if (MOZ_UNLIKELY(!aListElement.IsAnyOfHTMLElements(nsGkAtoms::dl))) {
704 return false;
706 continue;
708 if (MOZ_UNLIKELY(child->IsElement())) {
709 return false;
711 if (MOZ_LIKELY(child->IsText())) {
712 if (MOZ_UNLIKELY(HTMLEditUtils::IsVisibleTextNode(*child->AsText()))) {
713 return false;
717 return true;
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())) {
728 return true;
731 bool brElementHasFound = false;
732 for (OwningNonNull<nsIContent>& content : aArrayOfContents) {
733 if (!EditorUtils::IsEditableContent(content,
734 EditorUtils::EditorType::HTML)) {
735 continue;
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) {
741 return false;
743 brElementHasFound = true;
744 continue;
746 if (!HTMLEditUtils::IsEmptyInlineContainer(
747 content,
748 {EmptyCheckOption::TreatSingleBRElementAsVisible,
749 EmptyCheckOption::TreatNonEditableContentAsInvisible},
750 aBlockInlineCheck)) {
751 return false;
754 return true;
758 * IsPointAtEdgeOfLink() returns true if aPoint is at start or end of a
759 * link.
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()) {
768 return false;
770 if (!aPoint.IsStartOfContainer() && !aPoint.IsEndOfContainer()) {
771 return false;
773 // XXX Assuming it's not in an empty text node because it's unrealistic edge
774 // case.
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) {
783 *aFoundLinkElement =
784 do_AddRef(point.template ContainerAs<Element>()).take();
786 return true;
789 return false;
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
796 * element.
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();
808 return true;
811 return false;
815 * IsRangeEntirelyInLink() returns true if aRange is entirely in a link
816 * element.
817 * Note that this returns true even if editing host of the range is in a link
818 * element.
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()) {
831 return false;
833 return IsContentInclusiveDescendantOfLink(*commonAncestorNode->AsContent(),
834 aFoundLinkElement);
838 * Get adjacent content node of aNode if there is (even if one is in different
839 * parent element).
841 * @param aNode The node from which we start to walk the DOM
842 * tree.
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
848 * the editing host.
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 ||
862 (aAncestorLimiter &&
863 !aNode.IsInclusiveDescendantOf(aAncestorLimiter))) {
864 return nullptr;
866 return HTMLEditUtils::GetAdjacentContent(aNode, WalkTreeDirection::Backward,
867 aOptions, aBlockInlineCheck,
868 aAncestorLimiter);
870 static nsIContent* GetNextContent(const nsINode& aNode,
871 const WalkTreeOptions& aOptions,
872 BlockInlineCheck aBlockInlineCheck,
873 const Element* aAncestorLimiter = nullptr) {
874 if (&aNode == aAncestorLimiter ||
875 (aAncestorLimiter &&
876 !aNode.IsInclusiveDescendantOf(aAncestorLimiter))) {
877 return nullptr;
879 return HTMLEditUtils::GetAdjacentContent(aNode, WalkTreeDirection::Forward,
880 aOptions, aBlockInlineCheck,
881 aAncestorLimiter);
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})) {
902 * // Do something...
903 * point.Set(content);
906 * Following code must be you expected:
908 * while (nsIContent* content =
909 * GetNextContent(point, {WalkTreeOption::IgnoreNonEditableNode}) {
910 * // Do something...
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
924 * with aOption.
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)) {
935 continue;
937 if (aOptions.contains(WalkTreeOption::StopAtBlockBoundary) &&
938 HTMLEditUtils::IsBlockElement(*sibling, aBlockInlineCheck)) {
939 return nullptr;
941 return sibling;
943 return nullptr;
947 * GetNextSibling() return the following sibling of aContent which matches
948 * with aOption.
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)) {
959 continue;
961 if (aOptions.contains(WalkTreeOption::StopAtBlockBoundary) &&
962 HTMLEditUtils::IsBlockElement(*sibling, aBlockInlineCheck)) {
963 return nullptr;
965 return sibling;
967 return nullptr;
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)) {
982 continue;
984 if (aOptions.contains(WalkTreeOption::StopAtBlockBoundary) &&
985 HTMLEditUtils::IsBlockElement(*child, aBlockInlineCheck)) {
986 return nullptr;
988 return child;
990 return nullptr;
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)) {
1005 continue;
1007 if (aOptions.contains(WalkTreeOption::StopAtBlockBoundary) &&
1008 HTMLEditUtils::IsBlockElement(*child, aBlockInlineCheck)) {
1009 return nullptr;
1011 return child;
1013 return nullptr;
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();
1027 if (!parentNode) {
1028 return false;
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();
1045 if (!parentNode) {
1046 return false;
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
1055 * deleting.
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.
1077 } else {
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.
1084 return nullptr;
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)) {
1100 return nullptr;
1102 } else {
1103 editableContent = HTMLEditUtils::GetNextContent(
1104 *editableContent, {WalkTreeOption::IgnoreNonEditableNode},
1105 BlockInlineCheck::UseComputedDisplayStyle, &aEditingHost);
1106 if (NS_WARN_IF(!editableContent)) {
1107 return nullptr;
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>()))) {
1119 return nullptr;
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.
1128 OnlyLeafNode,
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
1143 * nodes.
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) {
1152 MOZ_ASSERT_IF(
1153 aLeafNodeTypes.contains(LeafNodeType::OnlyEditableLeafNode),
1154 !aLeafNodeTypes.contains(LeafNodeType::LeafNodeOrNonEditableNode));
1155 // editor shouldn't touch child nodes which are replaced with native
1156 // anonymous nodes.
1157 if (aNode.IsElement() &&
1158 HTMLEditUtils::IsNeverElementContentsEditableByUser(
1159 *aNode.AsElement())) {
1160 return nullptr;
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);
1169 continue;
1171 if (aLeafNodeTypes.contains(LeafNodeType::LeafNodeOrChildBlock) &&
1172 HTMLEditUtils::IsBlockElement(*content, aBlockInlineCheck)) {
1173 return content;
1175 if (!content->HasChildren() ||
1176 HTMLEditUtils::IsNeverElementContentsEditableByUser(*content)) {
1177 return content;
1179 if (aLeafNodeTypes.contains(LeafNodeType::LeafNodeOrNonEditableNode) &&
1180 aNode.IsEditable() && !content->IsEditable()) {
1181 return content;
1183 content = content->GetLastChild();
1185 return nullptr;
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
1191 * leaf.
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) {
1200 MOZ_ASSERT_IF(
1201 aLeafNodeTypes.contains(LeafNodeType::OnlyEditableLeafNode),
1202 !aLeafNodeTypes.contains(LeafNodeType::LeafNodeOrNonEditableNode));
1203 // editor shouldn't touch child nodes which are replaced with native
1204 // anonymous nodes.
1205 if (aNode.IsElement() &&
1206 HTMLEditUtils::IsNeverElementContentsEditableByUser(
1207 *aNode.AsElement())) {
1208 return nullptr;
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);
1217 continue;
1219 if (aLeafNodeTypes.contains(LeafNodeType::LeafNodeOrChildBlock) &&
1220 HTMLEditUtils::IsBlockElement(*content, aBlockInlineCheck)) {
1221 return content;
1223 if (!content->HasChildren() ||
1224 HTMLEditUtils::IsNeverElementContentsEditableByUser(*content)) {
1225 return content;
1227 if (aLeafNodeTypes.contains(LeafNodeType::LeafNodeOrNonEditableNode) &&
1228 aNode.IsEditable() && !content->IsEditable()) {
1229 return content;
1231 content = content->GetFirstChild();
1233 return nullptr;
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) {
1255 MOZ_ASSERT_IF(
1256 aLeafNodeTypes.contains(LeafNodeType::OnlyEditableLeafNode),
1257 !aLeafNodeTypes.contains(LeafNodeType::LeafNodeOrNonEditableNode));
1259 if (&aStartContent == aAncestorLimiter) {
1260 return nullptr;
1263 nsIContent* nextContent = aStartContent.GetNextSibling();
1264 if (!nextContent) {
1265 if (!aStartContent.GetParentElement()) {
1266 NS_WARNING("Reached orphan node while climbing up the DOM tree");
1267 return nullptr;
1269 for (Element* parentElement : aStartContent.AncestorsOfType<Element>()) {
1270 if (parentElement == &aCurrentBlock) {
1271 return nullptr;
1273 if (parentElement == aAncestorLimiter) {
1274 NS_WARNING("Reached editing host while climbing up the DOM tree");
1275 return nullptr;
1277 nextContent = parentElement->GetNextSibling();
1278 if (nextContent) {
1279 break;
1281 if (!parentElement->GetParentElement()) {
1282 NS_WARNING("Reached orphan node while climbing up the DOM tree");
1283 return nullptr;
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)) {
1292 return nextContent;
1294 if (aLeafNodeTypes.contains(LeafNodeType::LeafNodeOrNonEditableNode) &&
1295 aStartContent.IsEditable() && !nextContent->IsEditable()) {
1296 return nextContent;
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)) {
1302 return child;
1305 // Else return the next content itself.
1306 return nextContent;
1310 * Similar to the above method, but take a DOM point to specify scan start
1311 * point.
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());
1320 MOZ_ASSERT_IF(
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()) {
1327 return nullptr;
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();
1342 if (!nextContent) {
1343 if (aStartPoint.GetContainer() == &aCurrentBlock) {
1344 // We are at end of the block.
1345 return nullptr;
1348 // We are at end of non-block container
1349 return HTMLEditUtils::GetNextLeafContentOrNextBlockElement(
1350 *aStartPoint.template ContainerAs<nsIContent>(), aCurrentBlock,
1351 aLeafNodeTypes, IgnoreInsideBlockBoundary(aBlockInlineCheck),
1352 aAncestorLimiter);
1355 // We have a next node. If it's a block, return it.
1356 if (HTMLEditUtils::IsBlockElement(*nextContent, aBlockInlineCheck)) {
1357 return nextContent;
1359 if (aLeafNodeTypes.contains(LeafNodeType::LeafNodeOrNonEditableNode) &&
1360 aStartPoint.GetContainer()->IsEditable() &&
1361 !nextContent->IsEditable()) {
1362 return nextContent;
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))) {
1369 return child;
1372 // Else return the node itself
1373 return nextContent;
1377 * GetPreviousLeafContentOrPreviousBlockElement() returns previous leaf
1378 * content or previous block element of aStartContent inside
1379 * aAncestorLimiter.
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) {
1396 MOZ_ASSERT_IF(
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) {
1403 return nullptr;
1406 nsIContent* previousContent = aStartContent.GetPreviousSibling();
1407 if (!previousContent) {
1408 if (!aStartContent.GetParentElement()) {
1409 NS_WARNING("Reached orphan node while climbing up the DOM tree");
1410 return nullptr;
1412 for (Element* parentElement : aStartContent.AncestorsOfType<Element>()) {
1413 if (parentElement == &aCurrentBlock) {
1414 return nullptr;
1416 if (parentElement == aAncestorLimiter) {
1417 NS_WARNING("Reached editing host while climbing up the DOM tree");
1418 return nullptr;
1420 previousContent = parentElement->GetPreviousSibling();
1421 if (previousContent) {
1422 break;
1424 if (!parentElement->GetParentElement()) {
1425 NS_WARNING("Reached orphan node while climbing up the DOM tree");
1426 return nullptr;
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)) {
1445 return child;
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
1454 * point.
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());
1463 MOZ_ASSERT_IF(
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()) {
1470 return nullptr;
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.
1487 return nullptr;
1490 // We are at start of non-block container
1491 return HTMLEditUtils::GetPreviousLeafContentOrPreviousBlockElement(
1492 *aStartPoint.template ContainerAs<nsIContent>(), aCurrentBlock,
1493 aLeafNodeTypes, IgnoreInsideBlockBoundary(aBlockInlineCheck),
1494 aAncestorLimiter);
1497 nsCOMPtr<nsIContent> previousContent =
1498 aStartPoint.GetPreviousSiblingOfChild();
1499 if (NS_WARN_IF(!previousContent)) {
1500 return nullptr;
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))) {
1517 return child;
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
1538 // them.
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
1541 // white-spaces.
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
1562 * aAncestorTypes.
1564 enum class AncestorType {
1565 ClosestBlockElement,
1566 MostDistantInlineElementInBlock,
1567 EditableElement,
1568 IgnoreHRElement, // Ignore ancestor <hr> element since invalid structure
1569 ButtonElement,
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()) {
1606 return nullptr;
1608 for (Element* element : aContent.InclusiveAncestorsOfType<Element>()) {
1609 if (HTMLEditUtils::IsTable(element)) {
1610 return element;
1613 return nullptr;
1616 static Element* GetInclusiveAncestorAnyTableElement(
1617 const nsIContent& aContent) {
1618 for (Element* parent : aContent.InclusiveAncestorsOfType<Element>()) {
1619 if (HTMLEditUtils::IsAnyTableElement(parent)) {
1620 return parent;
1623 return nullptr;
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)) {
1647 return nullptr;
1649 if (HTMLEditUtils::IsListItem(parentElement)) {
1650 return parentElement;
1652 if (parentElement == aAncestorLimit) {
1653 return nullptr;
1656 return nullptr;
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();
1689 maybeFirstListItem;
1690 maybeFirstListItem = maybeFirstListItem->GetNextNode(&aListElement)) {
1691 if (HTMLEditUtils::IsListItem(maybeFirstListItem)) {
1692 return maybeFirstListItem->AsElement();
1695 return nullptr;
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();
1712 continue;
1714 if (maybeLastListItem->GetPreviousSibling()) {
1715 maybeLastListItem = maybeLastListItem->GetPreviousSibling();
1716 continue;
1718 for (Element* parent = maybeLastListItem->GetParentElement(); parent;
1719 parent = parent->GetParentElement()) {
1720 maybeLastListItem = nullptr;
1721 if (parent == &aListElement) {
1722 return nullptr;
1724 if (parent->GetPreviousSibling()) {
1725 maybeLastListItem = parent->GetPreviousSibling();
1726 break;
1730 return nullptr;
1734 * GetFirstTableCellElementChild() and GetLastTableCellElementChild()
1735 * return the first/last element child of <tr> element if it's a table
1736 * cell element.
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)
1743 ? firstElementChild
1744 : nullptr;
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)
1751 ? lastElementChild
1752 : nullptr;
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
1769 : nullptr;
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
1778 : nullptr;
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)) {
1794 return nullptr;
1797 // If aNode is the editing host itself, there is no modifiable inline
1798 // parent.
1799 if (&aContent == aEditingHost || &aContent == aAncestorLimiter) {
1800 return nullptr;
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)) {
1808 return nullptr;
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)) {
1820 break;
1822 topMostInlineContent = element;
1824 return topMostInlineContent;
1828 * GetMostDistantAncestorEditableEmptyInlineElement() returns most distant
1829 * ancestor which only has aEmptyContent or its ancestor, editable and
1830 * inline element.
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) {
1837 return nullptr;
1839 nsIContent* lastEmptyContent = const_cast<nsIContent*>(&aEmptyContent);
1840 for (Element* element : aEmptyContent.AncestorsOfType<Element>()) {
1841 if (element == aEditingHost || element == aAncestorLimiter) {
1842 break;
1844 if (!HTMLEditUtils::IsInlineContent(*element, aBlockInlineCheck) ||
1845 !HTMLEditUtils::IsSimplyEditableNode(*element)) {
1846 break;
1848 if (element->GetChildCount() > 1) {
1849 for (const nsIContent* child = element->GetFirstChild(); child;
1850 child = child->GetNextSibling()) {
1851 if (child == lastEmptyContent || child->IsComment()) {
1852 continue;
1854 return lastEmptyContent != &aEmptyContent
1855 ? Element::FromNode(lastEmptyContent)
1856 : nullptr;
1859 lastEmptyContent = element;
1861 return lastEmptyContent != &aEmptyContent
1862 ? Element::FromNode(lastEmptyContent)
1863 : nullptr;
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()) {
1877 return nullptr;
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()) {
1884 return nullptr;
1886 nsIContent* childAtStart = start.GetChild();
1887 if (!childAtStart || !childAtStart->IsElement()) {
1888 return nullptr;
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()
1895 : nullptr;
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()) {
1916 return nullptr;
1918 const nsRange* firstRange = aSelection.GetRangeAt(0);
1919 if (NS_WARN_IF(!firstRange) || NS_WARN_IF(!firstRange->IsPositioned())) {
1920 return nullptr;
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
1935 * descendants?
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()) {
1943 return nullptr;
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) !=
1953 1) {
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(
1972 *content,
1973 {WalkTreeOption::IgnoreDataNodeExceptText,
1974 WalkTreeOption::IgnoreWhiteSpaceOnlyText},
1975 BlockInlineCheck::Unused, &aElement)) {
1976 if (auto* brElement = dom::HTMLBRElement::FromNode(*content)) {
1977 return brElement;
1980 return nullptr;
1984 * Return last <br> element or last text node ending with a preserved line
1985 * break of/before aBlockElement.
1987 enum ScanLineBreak {
1988 AtEndOfBlock,
1989 BeforeBlock,
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
2000 * true.
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,
2011 bool aToSetStyle);
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) {
2050 return Some(i - 1);
2052 break;
2053 case HTMLEditUtils::kNewLine:
2054 if (!isNewLineCollapsible) {
2055 return Some(i - 1);
2057 break;
2058 case HTMLEditUtils::kNBSP:
2059 if (!isNBSPCollapsible) {
2060 return Some(i - 1);
2062 break;
2063 default:
2064 MOZ_ASSERT(!nsCRT::IsAsciiSpace(textFragment.CharAt(i - 1)));
2065 return Some(i - 1);
2068 return Nothing();
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,
2086 aWalkTextOptions);
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) {
2121 return Some(i);
2123 break;
2124 case HTMLEditUtils::kNewLine:
2125 if (!isNewLineCollapsible) {
2126 return Some(i);
2128 break;
2129 case HTMLEditUtils::kNBSP:
2130 if (!isNBSPCollapsible) {
2131 return Some(i);
2133 break;
2134 default:
2135 MOZ_ASSERT(!nsCRT::IsAsciiSpace(textFragment.CharAt(i)));
2136 return Some(i);
2139 return Nothing();
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
2146 * white-space.
2148 static uint32_t GetFirstWhiteSpaceOffsetCollapsedWith(
2149 const EditorDOMPointInText& aPoint,
2150 const WalkTextOptions& aWalkTextOptions = {}) {
2151 MOZ_ASSERT(aPoint.IsSetAndValid());
2152 MOZ_ASSERT(!aPoint.IsEndOfContainer());
2153 MOZ_ASSERT_IF(
2154 aWalkTextOptions.contains(WalkTextOption::TreatNBSPsCollapsible),
2155 aPoint.IsCharCollapsibleASCIISpaceOrNBSP());
2156 MOZ_ASSERT_IF(
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());
2166 MOZ_ASSERT_IF(
2167 aWalkTextOptions.contains(WalkTextOption::TreatNBSPsCollapsible),
2168 EditorRawDOMPoint(&aTextNode, aOffset)
2169 .IsCharCollapsibleASCIISpaceOrNBSP());
2170 MOZ_ASSERT_IF(
2171 !aWalkTextOptions.contains(WalkTextOption::TreatNBSPsCollapsible),
2172 EditorRawDOMPoint(&aTextNode, aOffset).IsCharCollapsibleASCIISpace());
2173 if (!aOffset) {
2174 return 0;
2176 Maybe<uint32_t> previousVisibleCharOffset =
2177 GetPreviousNonCollapsibleCharOffset(aTextNode, aOffset,
2178 aWalkTextOptions);
2179 return previousVisibleCharOffset.isSome()
2180 ? previousVisibleCharOffset.value() + 1
2181 : 0;
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();
2226 ++offset) {
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
2239 * around.
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");
2273 return ret;
2276 // Otherwise, we should put caret at the invisible `<br>` element.
2277 return EditorDOMPointType(&aContent);
2281 * GetBetterInsertionPointFor() returns better insertion point to insert
2282 * aContentToInsert.
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>
2333 * effects aContent.
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
2359 * into this array.
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,
2370 aOptions);
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
2383 * elements.
2384 * @param aOutArrayOfContents [out] This method will append found descendants
2385 * into this array.
2386 * @param aOptions The option which element should be treated as
2387 * empty.
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,
2408 *nsGkAtoms::empty);
2410 [[nodiscard]] static bool ElementHasAttributeExcept(
2411 const Element& aElement, const nsAtom& aAttribute1,
2412 const nsAtom& aAttribute2) {
2413 return ElementHasAttributeExcept(aElement, aAttribute1, aAttribute2,
2414 *nsGkAtoms::empty);
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);
2435 while (true) {
2436 nsIContent* firstChild = result.GetContainer()->GetFirstChild();
2437 if (!firstChild) {
2438 break;
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)
2453 .isSome()) {
2454 firstChild = nextSibling;
2455 break;
2459 if ((!firstChild->IsText() &&
2460 !HTMLEditUtils::IsContainerNode(*firstChild)) ||
2461 !EditorUtils::IsEditableContent(*firstChild,
2462 EditorBase::EditorType::HTML)) {
2463 break;
2465 if (aInvisibleText == InvisibleText::Skip && firstChild->IsText()) {
2466 result.Set(firstChild,
2467 HTMLEditUtils::GetInclusiveNextNonCollapsibleCharOffset(
2468 *firstChild->AsText(), 0u)
2469 .valueOr(0u));
2470 break;
2472 result.Set(firstChild, 0u);
2474 return result;
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);
2485 while (true) {
2486 nsIContent* lastChild = result.GetContainer()->GetLastChild();
2487 if (!lastChild) {
2488 break;
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())
2503 .isSome()) {
2504 lastChild = nextSibling;
2505 break;
2509 if ((!lastChild->IsText() &&
2510 !HTMLEditUtils::IsContainerNode(*lastChild)) ||
2511 !EditorUtils::IsEditableContent(*lastChild,
2512 EditorBase::EditorType::HTML)) {
2513 break;
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);
2521 break;
2523 result.Set(lastChild, visibleCharOffset.value() + 1u);
2524 break;
2526 result = EditorDOMPointType::AtEndOf(*lastChild);
2528 return result;
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
2548 * keywords of CSS.
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);
2604 private:
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)) {
2623 return true;
2625 if (aOptions.contains(WalkTreeOption::IgnoreDataNodeExceptText) &&
2626 !EditorUtils::IsElementOrText(aContent)) {
2627 return true;
2629 if (aOptions.contains(WalkTreeOption::IgnoreWhiteSpaceOnlyText) &&
2630 aContent.IsText() &&
2631 const_cast<Text*>(aContent.AsText())->TextIsOnlyWhitespace()) {
2632 return true;
2634 return false;
2637 static uint32_t CountChildren(const nsINode& aNode,
2638 const WalkTreeOptions& aOptions,
2639 BlockInlineCheck aBlockInlineCheck) {
2640 uint32_t count = 0;
2641 for (nsIContent* child = aNode.GetFirstChild(); child;
2642 child = child->GetNextSibling()) {
2643 if (HTMLEditUtils::IsContentIgnored(*child, aOptions)) {
2644 continue;
2646 if (aOptions.contains(WalkTreeOption::StopAtBlockBoundary) &&
2647 HTMLEditUtils::IsBlockElement(*child, aBlockInlineCheck)) {
2648 break;
2650 ++count;
2652 return count;
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
2673 * a block boundary.
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;
2686 public:
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)) {
2693 mDTFound = true;
2694 if (mDDFound) {
2695 break;
2697 continue;
2699 if (child->IsHTMLElement(nsGkAtoms::dd)) {
2700 mDDFound = true;
2701 if (mDTFound) {
2702 break;
2704 continue;
2709 bool DTElementFound() const { return mDTFound; }
2710 bool DDElementFound() const { return mDDFound; }
2712 private:
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;
2726 public:
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());
2770 mIndex = 0;
2771 return !mSelectedCellElements.IsEmpty() ? mSelectedCellElements[0].get()
2772 : nullptr;
2774 Element* GetNextElement() const {
2775 MOZ_ASSERT(mIndex < mSelectedCellElements.Length());
2776 return ++mIndex < mSelectedCellElements.Length()
2777 ? mSelectedCellElements[mIndex].get()
2778 : nullptr;
2781 private:
2782 AutoTArray<OwningNonNull<Element>, 16> mSelectedCellElements;
2783 mutable size_t mIndex = 0;
2786 } // namespace mozilla
2788 #endif // #ifndef HTMLEditUtils_h