HTMLEditUtils.h (139832B)
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/. */ 5 6 #ifndef HTMLEditUtils_h 7 #define HTMLEditUtils_h 8 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. 13 */ 14 15 #include "EditorBase.h" 16 #include "EditorDOMPoint.h" 17 #include "EditorForwards.h" 18 #include "EditorLineBreak.h" 19 #include "EditorUtils.h" 20 #include "HTMLEditHelpers.h" 21 22 #include "mozilla/Attributes.h" 23 #include "mozilla/EnumSet.h" 24 #include "mozilla/IntegerRange.h" 25 #include "mozilla/Maybe.h" 26 #include "mozilla/Result.h" 27 #include "mozilla/dom/AbstractRange.h" 28 #include "mozilla/dom/AncestorIterator.h" 29 #include "mozilla/dom/CharacterDataBuffer.h" 30 #include "mozilla/dom/Element.h" 31 #include "mozilla/dom/HTMLBRElement.h" 32 #include "mozilla/dom/Selection.h" 33 #include "mozilla/dom/Text.h" 34 35 #include "nsContentUtils.h" 36 #include "nsCRT.h" 37 #include "nsGkAtoms.h" 38 #include "nsHTMLTags.h" 39 #include "nsTArray.h" 40 41 class nsAtom; 42 class nsPresContext; 43 44 namespace mozilla { 45 46 enum class CollectChildrenOption { 47 // Ignore non-editable nodes 48 IgnoreNonEditableChildren, 49 // Ignore invisible text nodes 50 IgnoreInvisibleTextNodes, 51 // Collect list children too. 52 CollectListChildren, 53 // Collect table children too. 54 CollectTableChildren, 55 }; 56 57 class HTMLEditUtils final { 58 using AbstractRange = dom::AbstractRange; 59 using Element = dom::Element; 60 using Selection = dom::Selection; 61 using Text = dom::Text; 62 using WhitespaceOption = dom::CharacterDataBuffer::WhitespaceOption; 63 using WhitespaceOptions = dom::CharacterDataBuffer::WhitespaceOptions; 64 65 public: 66 static constexpr char16_t kNewLine = '\n'; 67 static constexpr char16_t kCarriageReturn = '\r'; 68 static constexpr char16_t kTab = '\t'; 69 static constexpr char16_t kSpace = ' '; 70 static constexpr char16_t kNBSP = 0x00A0; 71 static constexpr char16_t kGreaterThan = '>'; 72 73 /** 74 * IsSimplyEditableNode() returns true when aNode is simply editable. 75 * This does NOT means that aNode can be removed from current parent nor 76 * aNode's data is editable. 77 */ 78 static bool IsSimplyEditableNode(const nsINode& aNode) { 79 return aNode.IsEditable(); 80 } 81 82 /** 83 * Return true if aNode is editable or not in a composed doc. This is useful 84 * if the caller may modify document fragment before inserting it into a 85 * Document. 86 */ 87 static bool NodeIsEditableOrNotInComposedDoc(const nsINode& aNode) { 88 return MOZ_UNLIKELY(!aNode.IsInComposedDoc()) || aNode.IsEditable(); 89 } 90 91 /** 92 * Return true if aElement is an editing host which is either: 93 * - the root element 94 * - parent is not editable 95 * - the <body> element of the document 96 */ 97 [[nodiscard]] static bool ElementIsEditableRoot(const Element& aElement); 98 99 /** 100 * Return true if inclusive flat tree ancestor has `inert` state. 101 */ 102 static bool ContentIsInert(const nsIContent& aContent); 103 104 /** 105 * IsNeverContentEditableElementByUser() returns true if the element's content 106 * is never editable by user. E.g., the content is always replaced by 107 * native anonymous node or something. 108 */ 109 static bool IsNeverElementContentsEditableByUser(const nsIContent& aContent) { 110 return aContent.IsElement() && 111 // XXX I think we should not treat <button> contents as editable 112 !aContent.IsHTMLElement(nsGkAtoms::button) && 113 (!HTMLEditUtils::IsContainerNode(aContent) || 114 HTMLEditUtils::IsReplacedElement(*aContent.AsElement()) || 115 aContent.IsAnyOfHTMLElements(nsGkAtoms::applet, nsGkAtoms::colgroup, 116 nsGkAtoms::frameset, nsGkAtoms::head, 117 nsGkAtoms::html)); 118 } 119 120 enum class ReplaceOrVoidElementOption { 121 LookForOnlyVoidElement, 122 LookForOnlyReplaceElement, 123 LookForOnlyNonVoidReplacedElement, 124 LookForReplacedOrVoidElement, 125 }; 126 127 /** 128 * Return an inclusive ancestor replaced element or void element of aContent. 129 * I.e., if this returns non-nullptr, aContent is in a replaced element or a 130 * void element. 131 */ 132 [[nodiscard]] static Element* GetInclusiveAncestorReplacedOrVoidElement( 133 const nsIContent& aContent, ReplaceOrVoidElementOption aOption) { 134 const bool lookForAnyReplaceElement = 135 aOption == ReplaceOrVoidElementOption::LookForOnlyReplaceElement || 136 aOption == ReplaceOrVoidElementOption::LookForReplacedOrVoidElement; 137 const bool lookForNonVoidReplacedElement = 138 aOption == 139 ReplaceOrVoidElementOption::LookForOnlyNonVoidReplacedElement; 140 const bool lookForVoidElement = 141 aOption == ReplaceOrVoidElementOption::LookForOnlyVoidElement || 142 aOption == ReplaceOrVoidElementOption::LookForReplacedOrVoidElement; 143 Element* lastReplacedOrVoidElement = nullptr; 144 for (Element* const element : 145 aContent.InclusiveAncestorsOfType<Element>()) { 146 // XXX I think we should not treat <button> contents as editable 147 if (lookForAnyReplaceElement && 148 !element->IsHTMLElement(nsGkAtoms::button) && 149 HTMLEditUtils::IsReplacedElement(*element)) { 150 lastReplacedOrVoidElement = element; 151 } else if (lookForNonVoidReplacedElement && 152 !element->IsHTMLElement(nsGkAtoms::button) && 153 HTMLEditUtils::IsNonVoidReplacedElement(*element)) { 154 lastReplacedOrVoidElement = element; 155 } else if (lookForVoidElement && 156 !HTMLEditUtils::IsContainerNode(*element)) { 157 lastReplacedOrVoidElement = element; 158 } 159 } 160 return lastReplacedOrVoidElement; 161 } 162 163 /* 164 * IsRemovalNode() returns true when parent of aContent is editable even 165 * if aContent isn't editable. 166 * This is a valid method to check it if you find the content from point 167 * of view of siblings or parents of aContent. 168 * Note that padding `<br>` element for empty editor and manual native 169 * anonymous content should be deletable even after `HTMLEditor` is destroyed 170 * because they are owned/managed by `HTMLEditor`. 171 */ 172 static bool IsRemovableNode(const nsIContent& aContent) { 173 return EditorUtils::IsPaddingBRElementForEmptyEditor(aContent) || 174 aContent.IsRootOfNativeAnonymousSubtree() || 175 (aContent.GetParentNode() && 176 aContent.GetParentNode()->IsEditable() && 177 &aContent != aContent.OwnerDoc()->GetBody() && 178 &aContent != aContent.OwnerDoc()->GetDocumentElement()); 179 } 180 181 /** 182 * IsRemovableFromParentNode() returns true when aContent is editable, has a 183 * parent node and the parent node is also editable. 184 * This is a valid method to check it if you find the content from point 185 * of view of descendants of aContent. 186 * Note that padding `<br>` element for empty editor and manual native 187 * anonymous content should be deletable even after `HTMLEditor` is destroyed 188 * because they are owned/managed by `HTMLEditor`. 189 */ 190 static bool IsRemovableFromParentNode(const nsIContent& aContent) { 191 return EditorUtils::IsPaddingBRElementForEmptyEditor(aContent) || 192 aContent.IsRootOfNativeAnonymousSubtree() || 193 (aContent.IsEditable() && aContent.GetParentNode() && 194 aContent.GetParentNode()->IsEditable() && 195 &aContent != aContent.OwnerDoc()->GetBody() && 196 &aContent != aContent.OwnerDoc()->GetDocumentElement()); 197 } 198 199 /** 200 * CanContentsBeJoined() returns true if aLeftContent and aRightContent can be 201 * joined. 202 */ 203 static bool CanContentsBeJoined(const nsIContent& aLeftContent, 204 const nsIContent& aRightContent); 205 206 /** 207 * Returns true if aContent is an element and it should be treated as a block. 208 * 209 * @param aBlockInlineCheck 210 * - If UseHTMLDefaultStyle, this returns true only for HTML elements which 211 * are defined as a block by the default style. I.e., non-HTML elements are 212 * always treated as inline. 213 * - If UseComputedDisplayOutsideStyle, this returns true for element nodes 214 * whose display-outside is not inline nor ruby. This is useful to get 215 * inclusive ancestor block element. 216 * - If UseComputedDisplayStyle, this returns true for element nodes whose 217 * display-outside is not inline or whose display-inside is flow-root and they 218 * do not appear as a form control. This is useful to check whether 219 * collapsible white-spaces at the element edges are visible or invisible or 220 * whether <br> element at end of the element is visible or invisible. 221 */ 222 [[nodiscard]] static bool IsBlockElement(const nsIContent& aContent, 223 BlockInlineCheck aBlockInlineCheck); 224 225 /** 226 * This is designed to check elements or non-element nodes which are laid out 227 * as inline. Therefore, inline-block etc and ruby are treated as inline. 228 * Note that invisible non-element nodes like comment nodes are also treated 229 * as inline. 230 * 231 * @param aBlockInlineCheck UseComputedDisplayOutsideStyle and 232 * UseComputedDisplayStyle return same result for 233 * any elements. 234 */ 235 [[nodiscard]] static bool IsInlineContent(const nsIContent& aContent, 236 BlockInlineCheck aBlockInlineCheck); 237 238 /** 239 * IsVisibleElementEvenIfLeafNode() returns true if aContent is an empty block 240 * element, a visible replaced element such as a form control. This does not 241 * check the layout information. 242 */ 243 static bool IsVisibleElementEvenIfLeafNode(const nsIContent& aContent); 244 245 /** 246 * Return true if aContent is an inline element which formats the content 247 * without giving any meanings. E.g., <b>, <big>, <sub>, <sup>, etc, but 248 * not <em>, <strong>, etc. 249 */ 250 [[nodiscard]] static bool IsInlineStyleElement(const nsIContent& aContent); 251 252 /** 253 * IsDisplayOutsideInline() returns true if display-outside value is 254 * "inside". This does NOT flush the layout. 255 */ 256 [[nodiscard]] static bool IsDisplayOutsideInline(const Element& aElement); 257 258 /** 259 * IsDisplayInsideFlowRoot() returns true if display-inline value of aElement 260 * is "flow-root". This does NOT flush the layout. 261 */ 262 [[nodiscard]] static bool IsDisplayInsideFlowRoot(const Element& aElement); 263 264 /** 265 * Return true if aContent is a flex item or a grid item. Note that if 266 * aContent is the `Text` node in the following case, this returns `true`. 267 * <div style="display:flex"><span style="display:contents">text</span></div> 268 */ 269 [[nodiscard]] static bool IsFlexOrGridItem(const nsIContent& aContent); 270 271 /** 272 * IsRemovableInlineStyleElement() returns true if aElement is an inline 273 * element and can be removed or split to in order to modifying inline 274 * styles. 275 */ 276 static bool IsRemovableInlineStyleElement(Element& aElement); 277 278 /** 279 * Return true if aTagName is one of the format element name of 280 * Document.execCommand("formatBlock"). 281 */ 282 [[nodiscard]] static bool IsFormatTagForFormatBlockCommand( 283 const nsStaticAtom& aTagName) { 284 return 285 // clang-format off 286 &aTagName == nsGkAtoms::address || 287 &aTagName == nsGkAtoms::article || 288 &aTagName == nsGkAtoms::aside || 289 &aTagName == nsGkAtoms::blockquote || 290 &aTagName == nsGkAtoms::dd || 291 &aTagName == nsGkAtoms::div || 292 &aTagName == nsGkAtoms::dl || 293 &aTagName == nsGkAtoms::dt || 294 &aTagName == nsGkAtoms::footer || 295 &aTagName == nsGkAtoms::h1 || 296 &aTagName == nsGkAtoms::h2 || 297 &aTagName == nsGkAtoms::h3 || 298 &aTagName == nsGkAtoms::h4 || 299 &aTagName == nsGkAtoms::h5 || 300 &aTagName == nsGkAtoms::h6 || 301 &aTagName == nsGkAtoms::header || 302 &aTagName == nsGkAtoms::hgroup || 303 &aTagName == nsGkAtoms::main || 304 &aTagName == nsGkAtoms::nav || 305 &aTagName == nsGkAtoms::p || 306 &aTagName == nsGkAtoms::pre || 307 &aTagName == nsGkAtoms::section; 308 // clang-format on 309 } 310 311 /** 312 * Return true if aContent is a format element of 313 * Document.execCommand("formatBlock"). 314 */ 315 [[nodiscard]] static bool IsFormatElementForFormatBlockCommand( 316 const nsIContent& aContent) { 317 if (!aContent.IsHTMLElement() || 318 !aContent.NodeInfo()->NameAtom()->IsStatic()) { 319 return false; 320 } 321 const nsStaticAtom* tagName = aContent.NodeInfo()->NameAtom()->AsStatic(); 322 return IsFormatTagForFormatBlockCommand(*tagName); 323 } 324 325 /** 326 * Return true if aTagName is one of the format element name of 327 * cmd_paragraphState. 328 */ 329 [[nodiscard]] static bool IsFormatTagForParagraphStateCommand( 330 const nsStaticAtom& aTagName) { 331 return 332 // clang-format off 333 &aTagName == nsGkAtoms::address || 334 &aTagName == nsGkAtoms::dd || 335 &aTagName == nsGkAtoms::dl || 336 &aTagName == nsGkAtoms::dt || 337 &aTagName == nsGkAtoms::h1 || 338 &aTagName == nsGkAtoms::h2 || 339 &aTagName == nsGkAtoms::h3 || 340 &aTagName == nsGkAtoms::h4 || 341 &aTagName == nsGkAtoms::h5 || 342 &aTagName == nsGkAtoms::h6 || 343 &aTagName == nsGkAtoms::p || 344 &aTagName == nsGkAtoms::pre; 345 // clang-format on 346 } 347 348 /** 349 * Return true if aContent is a format element of cmd_paragraphState. 350 */ 351 [[nodiscard]] static bool IsFormatElementForParagraphStateCommand( 352 const nsIContent& aContent) { 353 if (!aContent.IsHTMLElement() || 354 !aContent.NodeInfo()->NameAtom()->IsStatic()) { 355 return false; 356 } 357 const nsStaticAtom* tagName = aContent.NodeInfo()->NameAtom()->AsStatic(); 358 return IsFormatTagForParagraphStateCommand(*tagName); 359 } 360 361 /** 362 * Return true if aContent is an element which can be outdented such as 363 * a list element, a list-item element or a <blockquote>. 364 */ 365 [[nodiscard]] static bool IsOutdentable(const nsIContent& aContent); 366 367 /** 368 * Return true if aContent is one of <h1>, <h2>, <h3>, <h4>, <h5> or <h6>. 369 */ 370 [[nodiscard]] static bool IsHeadingElement(const nsIContent& aContent); 371 372 /** 373 * Return true if aContent is a list item element such as <li>, <dt> or <dd>. 374 */ 375 [[nodiscard]] static bool IsListItemElement(const nsIContent& aContent); 376 [[nodiscard]] static bool IsListItemElement(const nsIContent* aContent) { 377 return aContent && IsListItemElement(*aContent); 378 } 379 380 /** 381 * Return true if aContent is a <tr>. 382 */ 383 [[nodiscard]] static bool IsTableRowElement(const nsIContent& aContent); 384 [[nodiscard]] static bool IsTableRowElement(const nsIContent* aContent) { 385 return aContent && IsTableRowElement(*aContent); 386 } 387 388 /** 389 * Return true if aContent is an element which makes a table and is not a 390 * <col> nor a <colgroup>. So, <table>, <caption>, <tbody>, <tr>, <td>, etc. 391 */ 392 [[nodiscard]] static bool IsAnyTableElementExceptColumnElement( 393 const nsIContent& aContent); 394 395 /** 396 * Return true if aContent is an element which makes a table and nis not a 397 * <col>, <colgroup> nor <table>. 398 */ 399 [[nodiscard]] static bool IsAnyTableElementExceptTableElementAndColumElement( 400 const nsIContent& aContent); 401 402 /** 403 * Return true if aContent is a table cell such as <td> or <th>. 404 */ 405 [[nodiscard]] static bool IsTableCellElement(const nsIContent& aContent); 406 [[nodiscard]] static bool IsTableCellElement(const nsIContent* aContent) { 407 return aContent && IsTableCellElement(*aContent); 408 } 409 410 /** 411 * Return true if aContent is a table cell or a caption, i.e., that may 412 * contain visible content. 413 */ 414 [[nodiscard]] static bool IsTableCellOrCaptionElement( 415 const nsIContent& aContent); 416 417 /** 418 * Return true if aContent is a list element such as <ul>, <ol> or <dl>. 419 */ 420 [[nodiscard]] static bool IsListElement(const nsIContent& aContent); 421 [[nodiscard]] static bool IsListElement(const nsIContent* aContent) { 422 return aContent && IsListElement(*aContent); 423 } 424 425 /** 426 * Return true if aContent is an <img>. 427 * XXX Should this return true for other elements which is replaced with an 428 * image like <object>? 429 */ 430 [[nodiscard]] static bool IsImageElement(const nsIContent& aContent); 431 432 /** 433 * Return true if aContent is an <a> which has non-empty `href` attribute 434 * value. 435 */ 436 [[nodiscard]] static bool IsHyperlinkElement(const nsIContent& aContent); 437 438 /** 439 * Return true if aContent is an <a> which has non-empty `name` attribute 440 * value. 441 */ 442 [[nodiscard]] static bool IsNamedAnchorElement(const nsIContent& aContent); 443 444 /** 445 * Return true if aContent is a <div type="_moz">. 446 */ 447 [[nodiscard]] static bool IsMozDivElement(const nsIContent& aContent); 448 449 /** 450 * Return true if aElement is a mailcite element in the mail editor. 451 */ 452 [[nodiscard]] static bool IsMailCiteElement(const Element& aElement); 453 454 /** 455 * Return true if aElement is a replaced element. 456 */ 457 [[nodiscard]] static bool IsReplacedElement(const Element& aElement); 458 459 /** 460 * Return true if aElement is a non-void replaced element such as <iframe>, 461 * <embed>, <audio>, <video>, <select>, etc. 462 */ 463 [[nodiscard]] static bool IsNonVoidReplacedElement(const Element& aElement) { 464 return IsReplacedElement(aElement) && IsContainerNode(aElement); 465 } 466 467 /** 468 * Return true if aElement is a form widget, i.e., a replaced element for the 469 * <form>. 470 */ 471 [[nodiscard]] static bool IsFormWidgetElement(const nsIContent& aContent); 472 473 /** 474 * Return true if aContent is an element which can ahve `align` attribute. 475 */ 476 [[nodiscard]] static bool IsAlignAttrSupported(const nsIContent& aContent); 477 478 static bool CanNodeContain(const nsINode& aParent, const nsIContent& aChild) { 479 switch (aParent.NodeType()) { 480 case nsINode::ELEMENT_NODE: 481 case nsINode::DOCUMENT_FRAGMENT_NODE: 482 return HTMLEditUtils::CanNodeContain(*aParent.NodeInfo()->NameAtom(), 483 aChild); 484 } 485 return false; 486 } 487 488 static bool CanNodeContain(const nsINode& aParent, 489 const nsAtom& aChildNodeName) { 490 switch (aParent.NodeType()) { 491 case nsINode::ELEMENT_NODE: 492 case nsINode::DOCUMENT_FRAGMENT_NODE: 493 return HTMLEditUtils::CanNodeContain(*aParent.NodeInfo()->NameAtom(), 494 aChildNodeName); 495 } 496 return false; 497 } 498 499 static bool CanNodeContain(const nsAtom& aParentNodeName, 500 const nsIContent& aChild) { 501 switch (aChild.NodeType()) { 502 case nsINode::TEXT_NODE: 503 case nsINode::COMMENT_NODE: 504 case nsINode::CDATA_SECTION_NODE: 505 case nsINode::ELEMENT_NODE: 506 case nsINode::DOCUMENT_FRAGMENT_NODE: 507 return HTMLEditUtils::CanNodeContain(aParentNodeName, 508 *aChild.NodeInfo()->NameAtom()); 509 } 510 return false; 511 } 512 513 // XXX Only this overload does not check the node type. Therefore, only this 514 // handle Document and ProcessingInstructionTagName. 515 static bool CanNodeContain(const nsAtom& aParentNodeName, 516 const nsAtom& aChildNodeName) { 517 nsHTMLTag childTagEnum; 518 if (&aChildNodeName == nsGkAtoms::textTagName) { 519 childTagEnum = eHTMLTag_text; 520 } else if (&aChildNodeName == nsGkAtoms::commentTagName || 521 &aChildNodeName == nsGkAtoms::cdataTagName) { 522 childTagEnum = eHTMLTag_comment; 523 } else { 524 childTagEnum = 525 nsHTMLTags::AtomTagToId(const_cast<nsAtom*>(&aChildNodeName)); 526 } 527 528 nsHTMLTag parentTagEnum = 529 nsHTMLTags::AtomTagToId(const_cast<nsAtom*>(&aParentNodeName)); 530 return HTMLEditUtils::CanNodeContain(parentTagEnum, childTagEnum); 531 } 532 533 /** 534 * Return a point where can insert a node whose name is aInsertNodeName. 535 * Note that if the container of aPointToInsert is not an element, this check 536 * whether aInsertNodeName can be inserted into the element. Therefore, the 537 * caller may need to split the container when actually inserting a node. 538 */ 539 [[nodiscard]] static EditorDOMPoint GetPossiblePointToInsert( 540 const EditorDOMPoint& aPointToInsert, const nsAtom& aInsertNodeName, 541 const Element& aEditingHost) { 542 if (MOZ_UNLIKELY(!aPointToInsert.IsInContentNode())) { 543 return EditorDOMPoint(); 544 } 545 EditorDOMPoint pointToInsert(aPointToInsert); 546 // We shouldn't modify the subtree in a replaced element so that we need to 547 // test whether aInsertNodeName is inserted with inclusive ancestors 548 // starting from the most distant replaced element ancestor. 549 if (Element* const replacedOrVoidElement = 550 HTMLEditUtils::GetInclusiveAncestorReplacedOrVoidElement( 551 *aPointToInsert.GetContainer()->AsContent(), 552 ReplaceOrVoidElementOption::LookForReplacedOrVoidElement)) { 553 if (MOZ_UNLIKELY(replacedOrVoidElement == &aEditingHost) || 554 MOZ_UNLIKELY( 555 !replacedOrVoidElement->IsInclusiveDescendantOf(&aEditingHost))) { 556 return EditorDOMPoint(); 557 } 558 pointToInsert.Set(replacedOrVoidElement); 559 } 560 if ((pointToInsert.IsInTextNode() && 561 &aInsertNodeName == nsGkAtoms::textTagName) || 562 HTMLEditUtils::CanNodeContain(*pointToInsert.GetContainer(), 563 aInsertNodeName)) { 564 return pointToInsert; 565 } 566 if (pointToInsert.IsInTextNode()) { 567 Element* const parentElement = 568 pointToInsert.GetContainerParentAs<Element>(); 569 if (NS_WARN_IF(!parentElement)) { 570 return EditorDOMPoint(); 571 } 572 if (HTMLEditUtils::CanNodeContain(*parentElement, aInsertNodeName)) { 573 // Okay, the insertion point should be fine even though the caller needs 574 // to split the `Text`. 575 return pointToInsert; 576 } 577 } 578 nsIContent* lastContent = pointToInsert.GetContainer()->AsContent(); 579 for (Element* const element : lastContent->AncestorsOfType<Element>()) { 580 if (HTMLEditUtils::CanNodeContain(*element, aInsertNodeName)) { 581 return EditorDOMPoint(lastContent); 582 } 583 if (MOZ_UNLIKELY(element == &aEditingHost)) { 584 return EditorDOMPoint(); 585 } 586 lastContent = element; 587 } 588 return pointToInsert; 589 } 590 591 /** 592 * CanElementContainParagraph() returns true if aElement can have a <p> 593 * element as its child or its descendant. 594 */ 595 static bool CanElementContainParagraph(const Element& aElement) { 596 if (HTMLEditUtils::CanNodeContain(aElement, *nsGkAtoms::p)) { 597 return true; 598 } 599 600 // Even if the element cannot have a <p> element as a child, it can contain 601 // <p> element as a descendant if it's one of the following elements. 602 if (aElement.IsAnyOfHTMLElements(nsGkAtoms::ol, nsGkAtoms::ul, 603 nsGkAtoms::dl, nsGkAtoms::table, 604 nsGkAtoms::thead, nsGkAtoms::tbody, 605 nsGkAtoms::tfoot, nsGkAtoms::tr)) { 606 return true; 607 } 608 609 // XXX Otherwise, Chromium checks the CSS box is a block, but we don't do it 610 // for now. 611 return false; 612 } 613 614 /** 615 * Return a point which can insert a node whose name is aTagName scanning 616 * from aPoint to its ancestor points. 617 */ 618 template <typename EditorDOMPointType> 619 static EditorDOMPoint GetInsertionPointInInclusiveAncestor( 620 const nsAtom& aTagName, const EditorDOMPointType& aPoint, 621 const Element* aAncestorLimit = nullptr) { 622 if (MOZ_UNLIKELY(!aPoint.IsInContentNode())) { 623 return EditorDOMPoint(); 624 } 625 Element* lastChild = nullptr; 626 for (Element* containerElement : 627 aPoint.template ContainerAs<nsIContent>() 628 ->template InclusiveAncestorsOfType<Element>()) { 629 if (!HTMLEditUtils::IsSimplyEditableNode(*containerElement)) { 630 return EditorDOMPoint(); 631 } 632 if (HTMLEditUtils::CanNodeContain(*containerElement, aTagName)) { 633 return lastChild ? EditorDOMPoint(lastChild) 634 : aPoint.template To<EditorDOMPoint>(); 635 } 636 if (containerElement == aAncestorLimit) { 637 return EditorDOMPoint(); 638 } 639 lastChild = containerElement; 640 } 641 return EditorDOMPoint(); 642 } 643 644 /** 645 * IsContainerNode() returns true if aContent is a container node. 646 */ 647 [[nodiscard]] static bool IsContainerNode(const nsIContent& aContent) { 648 if (aContent.IsCharacterData()) { 649 return false; 650 } 651 return HTMLEditUtils::IsContainerNode( 652 // XXX Why don't we use nsHTMLTags::AtomTagToId? Are there some 653 // difference? 654 nsHTMLTags::StringTagToId(aContent.NodeName())); 655 } 656 657 /** 658 * IsSplittableNode() returns true if aContent can split. 659 */ 660 static bool IsSplittableNode(const nsIContent& aContent) { 661 if (!EditorUtils::IsEditableContent(aContent, 662 EditorUtils::EditorType::HTML) || 663 !HTMLEditUtils::IsRemovableFromParentNode(aContent)) { 664 return false; 665 } 666 if (aContent.IsElement()) { 667 // XXX Perhaps, instead of using container, we should have "splittable" 668 // information in the DB. E.g., `<template>`, `<script>` elements 669 // can have children, but shouldn't be split. 670 return HTMLEditUtils::IsContainerNode(aContent) && 671 !aContent.IsAnyOfHTMLElements(nsGkAtoms::body, nsGkAtoms::button, 672 nsGkAtoms::caption, nsGkAtoms::table, 673 nsGkAtoms::tbody, nsGkAtoms::tfoot, 674 nsGkAtoms::thead, nsGkAtoms::tr) && 675 !HTMLEditUtils::IsNeverElementContentsEditableByUser(aContent) && 676 !HTMLEditUtils::GetInclusiveAncestorReplacedOrVoidElement( 677 aContent, 678 ReplaceOrVoidElementOption::LookForReplacedOrVoidElement); 679 } 680 return aContent.IsText() && aContent.Length() > 0; 681 } 682 683 /** 684 * See execCommand spec: 685 * https://w3c.github.io/editing/execCommand.html#non-list-single-line-container 686 * https://w3c.github.io/editing/execCommand.html#single-line-container 687 */ 688 [[nodiscard]] static bool IsNonListSingleLineContainer( 689 const nsIContent& aContent); 690 [[nodiscard]] static bool IsSingleLineContainer(const nsIContent& aContent); 691 692 /** 693 * Return true if aText has only a linefeed and it's preformatted. 694 */ 695 [[nodiscard]] static bool TextHasOnlyOnePreformattedLinefeed( 696 const Text& aText) { 697 return aText.TextDataLength() == 1u && 698 aText.DataBuffer().CharAt(0u) == kNewLine && 699 EditorUtils::IsNewLinePreformatted(aText); 700 } 701 702 /** 703 * IsVisibleTextNode() returns true if aText has visible text. If it has 704 * only white-spaces and they are collapsed, returns false. 705 */ 706 [[nodiscard]] static bool IsVisibleTextNode(const Text& aText); 707 708 /** 709 * IsInVisibleTextFrames() returns true if any text in aText is in visible 710 * text frames. Callers have to guarantee that there is no pending reflow. 711 */ 712 static bool IsInVisibleTextFrames(nsPresContext* aPresContext, 713 const Text& aText); 714 715 /** 716 * IsVisibleBRElement() and IsInvisibleBRElement() return true if aContent is 717 * a visible HTML <br> element, i.e., not a padding <br> element for making 718 * last line in a block element visible, or an invisible <br> element. 719 */ 720 static bool IsVisibleBRElement(const nsIContent& aContent) { 721 if (const dom::HTMLBRElement* brElement = 722 dom::HTMLBRElement::FromNode(&aContent)) { 723 return IsVisibleBRElement(*brElement); 724 } 725 return false; 726 } 727 static bool IsVisibleBRElement(const dom::HTMLBRElement& aBRElement) { 728 // If followed by a block boundary without visible content, it's invisible 729 // <br> element. 730 return !HTMLEditUtils::GetElementOfImmediateBlockBoundary( 731 aBRElement, WalkTreeDirection::Forward); 732 } 733 static bool IsInvisibleBRElement(const nsIContent& aContent) { 734 if (const dom::HTMLBRElement* brElement = 735 dom::HTMLBRElement::FromNode(&aContent)) { 736 return IsInvisibleBRElement(*brElement); 737 } 738 return false; 739 } 740 static bool IsInvisibleBRElement(const dom::HTMLBRElement& aBRElement) { 741 return !HTMLEditUtils::IsVisibleBRElement(aBRElement); 742 } 743 744 enum class IgnoreInvisibleLineBreak { No, Yes }; 745 746 /** 747 * Return true if aPoint is immediately before current block boundary. If 748 * aIgnoreInvisibleLineBreak is "Yes", this returns true if aPoint is before 749 * invisible line break before a block boundary. 750 */ 751 template <typename PT, typename CT> 752 [[nodiscard]] static bool PointIsImmediatelyBeforeCurrentBlockBoundary( 753 const EditorDOMPointBase<PT, CT>& aPoint, 754 IgnoreInvisibleLineBreak aIgnoreInvisibleLineBreak); 755 756 /** 757 * Return true if aRange crosses the inclusive ancestor block element at 758 * start boundary, in other words, if aRange ends outside of the inclusive 759 * ancestor block of the start boundary. 760 */ 761 template <typename EditorDOMPointType> 762 [[nodiscard]] static bool RangeIsAcrossStartBlockBoundary( 763 const EditorDOMRangeBase<EditorDOMPointType>& aRange, 764 BlockInlineCheck aBlockInlineCheck) { 765 MOZ_ASSERT(aRange.IsPositionedAndValid()); 766 if (MOZ_UNLIKELY(!aRange.StartRef().IsInContentNode())) { 767 return false; 768 } 769 const Element* const startBlockElement = 770 HTMLEditUtils::GetInclusiveAncestorElement( 771 *aRange.StartRef().template ContainerAs<nsIContent>(), 772 ClosestBlockElement, 773 UseComputedDisplayStyleIfAuto(aBlockInlineCheck)); 774 if (MOZ_UNLIKELY(!startBlockElement)) { 775 return false; 776 } 777 return EditorRawDOMPoint::After(*startBlockElement) 778 .EqualsOrIsBefore(aRange.EndRef()); 779 } 780 781 /** 782 * Return true if `display` of inclusive ancestor of aContent is `none`. 783 */ 784 static bool IsInclusiveAncestorCSSDisplayNone(const nsIContent& aContent); 785 786 /** 787 * IsVisiblePreformattedNewLine() and IsInvisiblePreformattedNewLine() return 788 * true if the point is preformatted linefeed and it's visible or invisible. 789 * If linefeed is immediately before a block boundary, it's invisible. 790 * 791 * @param aFollowingBlockElement [out] If the node is followed by a block 792 * boundary, this is set to the element 793 * creating the block boundary. 794 */ 795 template <typename EditorDOMPointType> 796 [[nodiscard]] static bool IsVisiblePreformattedNewLine( 797 const EditorDOMPointType& aPoint, 798 Element** aFollowingBlockElement = nullptr) { 799 if (aFollowingBlockElement) { 800 *aFollowingBlockElement = nullptr; 801 } 802 if (!aPoint.IsInTextNode() || aPoint.IsEndOfContainer() || 803 !aPoint.IsCharPreformattedNewLine()) { 804 return false; 805 } 806 // If there are some other characters in the text node, it's a visible 807 // linefeed. 808 if (!aPoint.IsAtLastContent()) { 809 if (EditorUtils::IsWhiteSpacePreformatted( 810 *aPoint.template ContainerAs<Text>())) { 811 return true; 812 } 813 const dom::CharacterDataBuffer& characterDataBuffer = 814 aPoint.template ContainerAs<Text>()->DataBuffer(); 815 const uint32_t nextVisibleCharOffset = 816 characterDataBuffer.FindNonWhitespaceChar( 817 EditorUtils::IsNewLinePreformatted( 818 *aPoint.template ContainerAs<Text>()) 819 ? WhitespaceOptions{WhitespaceOption::FormFeedIsSignificant, 820 WhitespaceOption::NewLineIsSignificant} 821 : WhitespaceOptions{WhitespaceOption::FormFeedIsSignificant}, 822 aPoint.Offset() + 1); 823 if (nextVisibleCharOffset != dom::CharacterDataBuffer::kNotFound) { 824 return true; // There is a visible character after the point. 825 } 826 } 827 // If followed by a block boundary without visible content, it's invisible 828 // linefeed. 829 Element* followingBlockElement = 830 HTMLEditUtils::GetElementOfImmediateBlockBoundary( 831 *aPoint.template ContainerAs<Text>(), WalkTreeDirection::Forward); 832 if (aFollowingBlockElement) { 833 *aFollowingBlockElement = followingBlockElement; 834 } 835 return !followingBlockElement; 836 } 837 template <typename EditorDOMPointType> 838 static bool IsInvisiblePreformattedNewLine( 839 const EditorDOMPointType& aPoint, 840 Element** aFollowingBlockElement = nullptr) { 841 if (!aPoint.IsInTextNode() || aPoint.IsEndOfContainer() || 842 !aPoint.IsCharPreformattedNewLine()) { 843 if (aFollowingBlockElement) { 844 *aFollowingBlockElement = nullptr; 845 } 846 return false; 847 } 848 return !IsVisiblePreformattedNewLine(aPoint, aFollowingBlockElement); 849 } 850 851 /** 852 * Return a point to insert a padding line break if aPoint is following a 853 * block boundary and the line containing aPoint requires a following padding 854 * line break to make the line visible. 855 */ 856 template <typename PT, typename CT> 857 static EditorDOMPoint LineRequiresPaddingLineBreakToBeVisible( 858 const EditorDOMPointBase<PT, CT>& aPoint, const Element& aEditingHost); 859 860 /** 861 * ShouldInsertLinefeedCharacter() returns true if the caller should insert 862 * a linefeed character instead of <br> element. 863 */ 864 static bool ShouldInsertLinefeedCharacter( 865 const EditorDOMPoint& aPointToInsert, const Element& aEditingHost); 866 867 enum class EmptyCheckOption { 868 TreatSingleBRElementAsVisible, 869 TreatBlockAsVisible, 870 TreatListItemAsVisible, 871 TreatTableCellAsVisible, 872 TreatNonEditableContentAsInvisible, 873 TreatCommentAsVisible, 874 SafeToAskLayout, 875 }; 876 using EmptyCheckOptions = EnumSet<EmptyCheckOption, uint32_t>; 877 878 friend std::ostream& operator<<(std::ostream& aStream, 879 const EmptyCheckOption& aOption); 880 friend std::ostream& operator<<(std::ostream& aStream, 881 const EmptyCheckOptions& aOptions); 882 883 /** 884 * Return false if aNode has some visible content nodes, list elements or 885 * table elements. 886 * 887 * @param aPresContext Must not be nullptr if 888 * EmptyCheckOption::SafeToAskLayout is set. 889 * @param aNode The node to check whether it's empty. 890 * @param aOptions You can specify which type of elements are visible 891 * and/or whether this can access layout information. 892 * @param aSeenBR [Out] Set to true if this meets an <br> element 893 * before meeting visible things. 894 */ 895 static bool IsEmptyNode(nsPresContext* aPresContext, const nsINode& aNode, 896 const EmptyCheckOptions& aOptions = {}, 897 bool* aSeenBR = nullptr); 898 899 /** 900 * Return false if aNode has some visible content nodes, list elements or 901 * table elements. 902 * 903 * @param aNode The node to check whether it's empty. 904 * @param aOptions You can specify which type of elements are visible 905 * and/or whether this can access layout information. 906 * Must not contain EmptyCheckOption::SafeToAskLayout. 907 * @param aSeenBR [Out] Set to true if this meets an <br> element 908 * before meeting visible things. 909 */ 910 static bool IsEmptyNode(const nsINode& aNode, 911 const EmptyCheckOptions& aOptions = {}, 912 bool* aSeenBR = nullptr) { 913 MOZ_ASSERT(!aOptions.contains(EmptyCheckOption::SafeToAskLayout)); 914 return IsEmptyNode(nullptr, aNode, aOptions, aSeenBR); 915 } 916 917 /** 918 * IsEmptyInlineContainer() returns true if aContent is an inline element 919 * which can have children and does not have meaningful content. 920 */ 921 static bool IsEmptyInlineContainer(const nsIContent& aContent, 922 const EmptyCheckOptions& aOptions, 923 BlockInlineCheck aBlockInlineCheck) { 924 return HTMLEditUtils::IsInlineContent(aContent, aBlockInlineCheck) && 925 HTMLEditUtils::IsContainerNode(aContent) && 926 HTMLEditUtils::IsEmptyNode(aContent, aOptions); 927 } 928 929 /** 930 * IsEmptyBlockElement() returns true if aElement is a block level element 931 * and it doesn't have any visible content. 932 */ 933 static bool IsEmptyBlockElement(const Element& aElement, 934 const EmptyCheckOptions& aOptions, 935 BlockInlineCheck aBlockInlineCheck) { 936 return HTMLEditUtils::IsBlockElement(aElement, aBlockInlineCheck) && 937 HTMLEditUtils::IsEmptyNode(aElement, aOptions); 938 } 939 940 /** 941 * Return true if aListElement is completely empty or it has only one list 942 * item element which is empty. 943 */ 944 [[nodiscard]] static bool IsEmptyAnyListElement(const Element& aListElement) { 945 MOZ_ASSERT(HTMLEditUtils::IsListElement(aListElement)); 946 bool foundListItem = false; 947 for (nsIContent* child = aListElement.GetFirstChild(); child; 948 child = child->GetNextSibling()) { 949 if (HTMLEditUtils::IsListItemElement(*child)) { 950 if (foundListItem) { 951 return false; // 2 list items found. 952 } 953 if (!IsEmptyNode(*child, {})) { 954 return false; // found non-empty list item. 955 } 956 foundListItem = true; 957 continue; 958 } 959 if (child->IsElement()) { 960 return false; // found sublist or illegal child. 961 } 962 if (child->IsText() && 963 HTMLEditUtils::IsVisibleTextNode(*child->AsText())) { 964 return false; // found illegal visible text node. 965 } 966 } 967 return true; 968 } 969 970 /** 971 * Return true if aListElement does not have invalid child. 972 */ 973 enum class TreatSubListElementAs { Invalid, Valid }; 974 [[nodiscard]] static bool IsValidListElement( 975 const Element& aListElement, 976 TreatSubListElementAs aTreatSubListElementAs) { 977 MOZ_ASSERT(HTMLEditUtils::IsListElement(aListElement)); 978 for (nsIContent* child = aListElement.GetFirstChild(); child; 979 child = child->GetNextSibling()) { 980 if (HTMLEditUtils::IsListElement(*child)) { 981 if (aTreatSubListElementAs == TreatSubListElementAs::Invalid) { 982 return false; 983 } 984 continue; 985 } 986 if (child->IsHTMLElement(nsGkAtoms::li)) { 987 if (MOZ_UNLIKELY(!aListElement.IsAnyOfHTMLElements(nsGkAtoms::ol, 988 nsGkAtoms::ul))) { 989 return false; 990 } 991 continue; 992 } 993 if (child->IsAnyOfHTMLElements(nsGkAtoms::dt, nsGkAtoms::dd)) { 994 if (MOZ_UNLIKELY(!aListElement.IsAnyOfHTMLElements(nsGkAtoms::dl))) { 995 return false; 996 } 997 continue; 998 } 999 if (MOZ_UNLIKELY(child->IsElement())) { 1000 return false; 1001 } 1002 if (MOZ_LIKELY(child->IsText())) { 1003 if (MOZ_UNLIKELY(HTMLEditUtils::IsVisibleTextNode(*child->AsText()))) { 1004 return false; 1005 } 1006 } 1007 } 1008 return true; 1009 } 1010 1011 /** 1012 * IsEmptyOneHardLine() returns true if aArrayOfContents does not represent 1013 * 2 or more lines and have meaningful content. 1014 */ 1015 static bool IsEmptyOneHardLine( 1016 nsTArray<OwningNonNull<nsIContent>>& aArrayOfContents, 1017 BlockInlineCheck aBlockInlineCheck) { 1018 if (NS_WARN_IF(aArrayOfContents.IsEmpty())) { 1019 return true; 1020 } 1021 1022 bool brElementHasFound = false; 1023 for (OwningNonNull<nsIContent>& content : aArrayOfContents) { 1024 if (!EditorUtils::IsEditableContent(content, 1025 EditorUtils::EditorType::HTML)) { 1026 continue; 1027 } 1028 if (content->IsHTMLElement(nsGkAtoms::br)) { 1029 // If there are 2 or more `<br>` elements, it's not empty line since 1030 // there may be only one `<br>` element in a hard line. 1031 if (brElementHasFound) { 1032 return false; 1033 } 1034 brElementHasFound = true; 1035 continue; 1036 } 1037 if (!HTMLEditUtils::IsEmptyInlineContainer( 1038 content, 1039 {EmptyCheckOption::TreatSingleBRElementAsVisible, 1040 EmptyCheckOption::TreatNonEditableContentAsInvisible}, 1041 aBlockInlineCheck)) { 1042 return false; 1043 } 1044 } 1045 return true; 1046 } 1047 1048 /** 1049 * IsPointAtEdgeOfLink() returns true if aPoint is at start or end of a 1050 * link. 1051 */ 1052 template <typename PT, typename CT> 1053 static bool IsPointAtEdgeOfLink(const EditorDOMPointBase<PT, CT>& aPoint, 1054 Element** aFoundLinkElement = nullptr) { 1055 if (aFoundLinkElement) { 1056 *aFoundLinkElement = nullptr; 1057 } 1058 if (!aPoint.IsInContentNode()) { 1059 return false; 1060 } 1061 if (!aPoint.IsStartOfContainer() && !aPoint.IsEndOfContainer()) { 1062 return false; 1063 } 1064 // XXX Assuming it's not in an empty text node because it's unrealistic edge 1065 // case. 1066 bool maybeStartOfAnchor = aPoint.IsStartOfContainer(); 1067 for (EditorRawDOMPoint point(aPoint.template ContainerAs<nsIContent>()); 1068 point.IsInContentNode() && 1069 (maybeStartOfAnchor ? point.IsStartOfContainer() 1070 : point.IsAtLastContent()); 1071 point = point.ParentPoint()) { 1072 if (HTMLEditUtils::IsHyperlinkElement(*point.ContainerAs<nsIContent>())) { 1073 // Now, we're at start or end of <a href>. 1074 if (aFoundLinkElement) { 1075 *aFoundLinkElement = 1076 do_AddRef(point.template ContainerAs<Element>()).take(); 1077 } 1078 return true; 1079 } 1080 } 1081 return false; 1082 } 1083 1084 /** 1085 * IsContentInclusiveDescendantOfLink() returns true if aContent is a 1086 * descendant of a link element. 1087 * Note that this returns true even if editing host of aContent is in a link 1088 * element. 1089 */ 1090 static bool IsContentInclusiveDescendantOfLink( 1091 nsIContent& aContent, Element** aFoundLinkElement = nullptr) { 1092 if (aFoundLinkElement) { 1093 *aFoundLinkElement = nullptr; 1094 } 1095 for (Element* element : aContent.InclusiveAncestorsOfType<Element>()) { 1096 if (HTMLEditUtils::IsHyperlinkElement(*element)) { 1097 if (aFoundLinkElement) { 1098 *aFoundLinkElement = do_AddRef(element).take(); 1099 } 1100 return true; 1101 } 1102 } 1103 return false; 1104 } 1105 1106 /** 1107 * IsRangeEntirelyInLink() returns true if aRange is entirely in a link 1108 * element. 1109 * Note that this returns true even if editing host of the range is in a link 1110 * element. 1111 */ 1112 template <typename EditorDOMRangeType> 1113 static bool IsRangeEntirelyInLink(const EditorDOMRangeType& aRange, 1114 Element** aFoundLinkElement = nullptr) { 1115 MOZ_ASSERT(aRange.IsPositionedAndValid()); 1116 if (aFoundLinkElement) { 1117 *aFoundLinkElement = nullptr; 1118 } 1119 nsINode* commonAncestorNode = 1120 nsContentUtils::GetClosestCommonInclusiveAncestor( 1121 aRange.StartRef().GetContainer(), aRange.EndRef().GetContainer()); 1122 if (NS_WARN_IF(!commonAncestorNode) || !commonAncestorNode->IsContent()) { 1123 return false; 1124 } 1125 return IsContentInclusiveDescendantOfLink(*commonAncestorNode->AsContent(), 1126 aFoundLinkElement); 1127 } 1128 1129 /** 1130 * Get adjacent content node of aNode if there is (even if one is in different 1131 * parent element). 1132 * 1133 * @param aNode The node from which we start to walk the DOM 1134 * tree. 1135 * @param aOptions See WalkTreeOption for the detail. 1136 * @param aBlockInlineCheck Whether considering block vs. inline with the 1137 * computed style or the HTML default style. 1138 * @param aAncestorLimiter Ancestor limiter element which these methods 1139 * never cross its boundary. This is typically 1140 * the editing host. 1141 */ 1142 enum class WalkTreeOption { 1143 IgnoreNonEditableNode, // Ignore non-editable nodes and their children. 1144 IgnoreDataNodeExceptText, // Ignore data nodes which are not text node. 1145 IgnoreWhiteSpaceOnlyText, // Ignore text nodes having only white-spaces. 1146 StopAtBlockBoundary, // Stop waking the tree at a block boundary. 1147 }; 1148 using WalkTreeOptions = EnumSet<WalkTreeOption>; 1149 static nsIContent* GetPreviousContent( 1150 const nsINode& aNode, const WalkTreeOptions& aOptions, 1151 BlockInlineCheck aBlockInlineCheck, 1152 const Element* aAncestorLimiter = nullptr) { 1153 if (&aNode == aAncestorLimiter || 1154 (aAncestorLimiter && 1155 !aNode.IsInclusiveDescendantOf(aAncestorLimiter))) { 1156 return nullptr; 1157 } 1158 return HTMLEditUtils::GetAdjacentContent(aNode, WalkTreeDirection::Backward, 1159 aOptions, aBlockInlineCheck, 1160 aAncestorLimiter); 1161 } 1162 static nsIContent* GetNextContent(const nsINode& aNode, 1163 const WalkTreeOptions& aOptions, 1164 BlockInlineCheck aBlockInlineCheck, 1165 const Element* aAncestorLimiter = nullptr) { 1166 if (&aNode == aAncestorLimiter || 1167 (aAncestorLimiter && 1168 !aNode.IsInclusiveDescendantOf(aAncestorLimiter))) { 1169 return nullptr; 1170 } 1171 return HTMLEditUtils::GetAdjacentContent(aNode, WalkTreeDirection::Forward, 1172 aOptions, aBlockInlineCheck, 1173 aAncestorLimiter); 1174 } 1175 1176 /** 1177 * And another version that takes a point in DOM tree rather than a node. 1178 */ 1179 template <typename PT, typename CT> 1180 static nsIContent* GetPreviousContent( 1181 const EditorDOMPointBase<PT, CT>& aPoint, const WalkTreeOptions& aOptions, 1182 BlockInlineCheck aBlockInlineCheck, 1183 const Element* aAncestorLimiter = nullptr); 1184 1185 /** 1186 * And another version that takes a point in DOM tree rather than a node. 1187 * 1188 * Note that this may return the child at the offset. E.g., following code 1189 * causes infinite loop. 1190 * 1191 * EditorRawDOMPoint point(aEditableNode); 1192 * while (nsIContent* content = 1193 * GetNextContent(point, {WalkTreeOption::IgnoreNonEditableNode})) { 1194 * // Do something... 1195 * point.Set(content); 1196 * } 1197 * 1198 * Following code must be you expected: 1199 * 1200 * while (nsIContent* content = 1201 * GetNextContent(point, {WalkTreeOption::IgnoreNonEditableNode}) { 1202 * // Do something... 1203 * DebugOnly<bool> advanced = point.Advanced(); 1204 * MOZ_ASSERT(advanced); 1205 * point.Set(point.GetChild()); 1206 * } 1207 */ 1208 template <typename PT, typename CT> 1209 static nsIContent* GetNextContent(const EditorDOMPointBase<PT, CT>& aPoint, 1210 const WalkTreeOptions& aOptions, 1211 BlockInlineCheck aBlockInlineCheck, 1212 const Element* aAncestorLimiter = nullptr); 1213 1214 /** 1215 * GetPreviousSibling() return the preceding sibling of aContent which matches 1216 * with aOption. 1217 * 1218 * @param aBlockInlineCheck Can be Unused if aOptions does not contain 1219 * StopAtBlockBoundary. 1220 */ 1221 static nsIContent* GetPreviousSibling( 1222 const nsIContent& aContent, const WalkTreeOptions& aOptions, 1223 BlockInlineCheck aBlockInlineCheck = BlockInlineCheck::Unused) { 1224 aBlockInlineCheck = UseComputedDisplayOutsideStyleIfAuto(aBlockInlineCheck); 1225 for (nsIContent* sibling = aContent.GetPreviousSibling(); sibling; 1226 sibling = sibling->GetPreviousSibling()) { 1227 if (HTMLEditUtils::IsContentIgnored(*sibling, aOptions)) { 1228 continue; 1229 } 1230 if (aOptions.contains(WalkTreeOption::StopAtBlockBoundary) && 1231 HTMLEditUtils::IsBlockElement(*sibling, aBlockInlineCheck)) { 1232 return nullptr; 1233 } 1234 return sibling; 1235 } 1236 return nullptr; 1237 } 1238 1239 /** 1240 * GetNextSibling() return the following sibling of aContent which matches 1241 * with aOption. 1242 * 1243 * @param aBlockInlineCheck Can be Unused if aOptions does not contain 1244 * StopAtBlockBoundary. 1245 */ 1246 static nsIContent* GetNextSibling( 1247 const nsIContent& aContent, const WalkTreeOptions& aOptions, 1248 BlockInlineCheck aBlockInlineCheck = BlockInlineCheck::Unused) { 1249 aBlockInlineCheck = UseComputedDisplayOutsideStyleIfAuto(aBlockInlineCheck); 1250 for (nsIContent* sibling = aContent.GetNextSibling(); sibling; 1251 sibling = sibling->GetNextSibling()) { 1252 if (HTMLEditUtils::IsContentIgnored(*sibling, aOptions)) { 1253 continue; 1254 } 1255 if (aOptions.contains(WalkTreeOption::StopAtBlockBoundary) && 1256 HTMLEditUtils::IsBlockElement(*sibling, aBlockInlineCheck)) { 1257 return nullptr; 1258 } 1259 return sibling; 1260 } 1261 return nullptr; 1262 } 1263 1264 /** 1265 * Return the last child of aNode which matches with aOption. 1266 * 1267 * @param aBlockInlineCheck Can be unused if aOptions does not contain 1268 * StopAtBlockBoundary. 1269 */ 1270 static nsIContent* GetLastChild( 1271 const nsINode& aNode, const WalkTreeOptions& aOptions, 1272 BlockInlineCheck aBlockInlineCheck = BlockInlineCheck::Unused) { 1273 aBlockInlineCheck = UseComputedDisplayOutsideStyleIfAuto(aBlockInlineCheck); 1274 for (nsIContent* child = aNode.GetLastChild(); child; 1275 child = child->GetPreviousSibling()) { 1276 if (HTMLEditUtils::IsContentIgnored(*child, aOptions)) { 1277 continue; 1278 } 1279 if (aOptions.contains(WalkTreeOption::StopAtBlockBoundary) && 1280 HTMLEditUtils::IsBlockElement(*child, aBlockInlineCheck)) { 1281 return nullptr; 1282 } 1283 return child; 1284 } 1285 return nullptr; 1286 } 1287 1288 /** 1289 * Return the first child of aNode which matches with aOption. 1290 * 1291 * @param aBlockInlineCheck Can be unused if aOptions does not contain 1292 * StopAtBlockBoundary. 1293 */ 1294 static nsIContent* GetFirstChild( 1295 const nsINode& aNode, const WalkTreeOptions& aOptions, 1296 BlockInlineCheck aBlockInlineCheck = BlockInlineCheck::Unused) { 1297 aBlockInlineCheck = UseComputedDisplayOutsideStyleIfAuto(aBlockInlineCheck); 1298 for (nsIContent* child = aNode.GetFirstChild(); child; 1299 child = child->GetNextSibling()) { 1300 if (HTMLEditUtils::IsContentIgnored(*child, aOptions)) { 1301 continue; 1302 } 1303 if (aOptions.contains(WalkTreeOption::StopAtBlockBoundary) && 1304 HTMLEditUtils::IsBlockElement(*child, aBlockInlineCheck)) { 1305 return nullptr; 1306 } 1307 return child; 1308 } 1309 return nullptr; 1310 } 1311 1312 /** 1313 * Return true if aContent is the last child of aNode with ignoring all 1314 * children which do not match with aOption. 1315 * 1316 * @param aBlockInlineCheck Can be unused if aOptions does not contain 1317 * StopAtBlockBoundary. 1318 */ 1319 static bool IsLastChild( 1320 const nsIContent& aContent, const WalkTreeOptions& aOptions, 1321 BlockInlineCheck aBlockInlineCheck = BlockInlineCheck::Unused) { 1322 nsINode* parentNode = aContent.GetParentNode(); 1323 if (!parentNode) { 1324 return false; 1325 } 1326 return HTMLEditUtils::GetLastChild(*parentNode, aOptions, 1327 aBlockInlineCheck) == &aContent; 1328 } 1329 1330 /** 1331 * Return true if aContent is the first child of aNode with ignoring all 1332 * children which do not match with aOption. 1333 * 1334 * @param aBlockInlineCheck Can be unused if aOptions does not contain 1335 * StopAtBlockBoundary. 1336 */ 1337 static bool IsFirstChild( 1338 const nsIContent& aContent, const WalkTreeOptions& aOptions, 1339 BlockInlineCheck aBlockInlineCheck = BlockInlineCheck::Unused) { 1340 nsINode* parentNode = aContent.GetParentNode(); 1341 if (!parentNode) { 1342 return false; 1343 } 1344 return HTMLEditUtils::GetFirstChild(*parentNode, aOptions, 1345 aBlockInlineCheck) == &aContent; 1346 } 1347 1348 /** 1349 * GetAdjacentContentToPutCaret() walks the DOM tree to find an editable node 1350 * near aPoint where may be a good point to put caret and keep typing or 1351 * deleting. 1352 * 1353 * @param aPoint The DOM point where to start to search from. 1354 * @return If found, returns non-nullptr. Otherwise, nullptr. 1355 * Note that if found node is in different table structure 1356 * element, this returns nullptr. 1357 */ 1358 enum class WalkTreeDirection { Forward, Backward }; 1359 template <typename PT, typename CT> 1360 static nsIContent* GetAdjacentContentToPutCaret( 1361 const EditorDOMPointBase<PT, CT>& aPoint, 1362 WalkTreeDirection aWalkTreeDirection, const Element& aEditingHost) { 1363 MOZ_ASSERT(aPoint.IsSetAndValid()); 1364 1365 nsIContent* editableContent = nullptr; 1366 if (aWalkTreeDirection == WalkTreeDirection::Backward) { 1367 editableContent = HTMLEditUtils::GetPreviousContent( 1368 aPoint, {WalkTreeOption::IgnoreNonEditableNode}, 1369 BlockInlineCheck::Auto, &aEditingHost); 1370 if (!editableContent) { 1371 return nullptr; // Not illegal. 1372 } 1373 } else { 1374 editableContent = HTMLEditUtils::GetNextContent( 1375 aPoint, {WalkTreeOption::IgnoreNonEditableNode}, 1376 BlockInlineCheck::Auto, &aEditingHost); 1377 if (NS_WARN_IF(!editableContent)) { 1378 // Perhaps, illegal because the node pointed by aPoint isn't editable 1379 // and nobody of previous nodes is editable. 1380 return nullptr; 1381 } 1382 } 1383 1384 // scan in the right direction until we find an eligible text node, 1385 // but don't cross any breaks, images, or table elements. 1386 // XXX This comment sounds odd. editableContent may have already crossed 1387 // breaks and/or images if they are non-editable. 1388 while (editableContent && !editableContent->IsText() && 1389 !editableContent->IsHTMLElement(nsGkAtoms::br) && 1390 !HTMLEditUtils::IsImageElement(*editableContent)) { 1391 if (aWalkTreeDirection == WalkTreeDirection::Backward) { 1392 editableContent = HTMLEditUtils::GetPreviousContent( 1393 *editableContent, {WalkTreeOption::IgnoreNonEditableNode}, 1394 BlockInlineCheck::Auto, &aEditingHost); 1395 if (NS_WARN_IF(!editableContent)) { 1396 return nullptr; 1397 } 1398 } else { 1399 editableContent = HTMLEditUtils::GetNextContent( 1400 *editableContent, {WalkTreeOption::IgnoreNonEditableNode}, 1401 BlockInlineCheck::Auto, &aEditingHost); 1402 if (NS_WARN_IF(!editableContent)) { 1403 return nullptr; 1404 } 1405 } 1406 } 1407 1408 // don't cross any table elements 1409 if ((!aPoint.IsInContentNode() && 1410 !!HTMLEditUtils::GetInclusiveAncestorAnyTableElement( 1411 *editableContent)) || 1412 (HTMLEditUtils::GetInclusiveAncestorAnyTableElement(*editableContent) != 1413 HTMLEditUtils::GetInclusiveAncestorAnyTableElement( 1414 *aPoint.template ContainerAs<nsIContent>()))) { 1415 return nullptr; 1416 } 1417 1418 // otherwise, ok, we have found a good spot to put the selection 1419 return editableContent; 1420 } 1421 1422 enum class LeafNodeType { 1423 // Even if there is a child block, keep scanning a leaf content in it. 1424 OnlyLeafNode, 1425 // If there is a child block, return it too. Note that this does not 1426 // mean that block siblings are not treated as leaf nodes. 1427 LeafNodeOrChildBlock, 1428 // If there is a non-editable element if and only if scanning from editable 1429 // node, return it too. 1430 LeafNodeOrNonEditableNode, 1431 // Ignore non-editable content at walking the tree. 1432 OnlyEditableLeafNode, 1433 // Treat `Comment` nodes are empty leaf nodes. 1434 TreatCommentAsLeafNode, 1435 }; 1436 using LeafNodeTypes = EnumSet<LeafNodeType>; 1437 1438 friend std::ostream& operator<<(std::ostream& aStream, 1439 const LeafNodeType& aLeafNodeType); 1440 friend std::ostream& operator<<(std::ostream& aStream, 1441 const LeafNodeTypes& aLeafNodeTypes); 1442 1443 /** 1444 * GetLastLeafContent() returns rightmost leaf content in aNode. It depends 1445 * on aLeafNodeTypes whether this which types of nodes are treated as leaf 1446 * nodes. 1447 * 1448 * @param aBlockInlineCheck Can be Unused if aLeafNodeTypes does not contain 1449 * LeafNodeOrCHildBlock. 1450 */ 1451 static nsIContent* GetLastLeafContent( 1452 const nsINode& aNode, const LeafNodeTypes& aLeafNodeTypes, 1453 BlockInlineCheck aBlockInlineCheck = BlockInlineCheck::Unused, 1454 const Element* aAncestorLimiter = nullptr) { 1455 MOZ_ASSERT_IF( 1456 aLeafNodeTypes.contains(LeafNodeType::OnlyEditableLeafNode), 1457 !aLeafNodeTypes.contains(LeafNodeType::LeafNodeOrNonEditableNode)); 1458 // editor shouldn't touch child nodes which are replaced with native 1459 // anonymous nodes. 1460 if (aNode.IsElement() && 1461 HTMLEditUtils::IsNeverElementContentsEditableByUser( 1462 *aNode.AsElement())) { 1463 return nullptr; 1464 } 1465 for (nsIContent* content = aNode.GetLastChild(); content;) { 1466 if (aLeafNodeTypes.contains(LeafNodeType::OnlyEditableLeafNode) && 1467 !EditorUtils::IsEditableContent(*content, 1468 EditorUtils::EditorType::HTML)) { 1469 content = HTMLEditUtils::GetPreviousContent( 1470 *content, {WalkTreeOption::IgnoreNonEditableNode}, 1471 aBlockInlineCheck, aAncestorLimiter); 1472 continue; 1473 } 1474 if (!aLeafNodeTypes.contains(LeafNodeType::TreatCommentAsLeafNode) && 1475 content->IsComment()) { 1476 content = content->GetPreviousSibling(); 1477 continue; 1478 } 1479 if (aLeafNodeTypes.contains(LeafNodeType::LeafNodeOrChildBlock) && 1480 HTMLEditUtils::IsBlockElement( 1481 *content, 1482 UseComputedDisplayOutsideStyleIfAuto(aBlockInlineCheck))) { 1483 return content; 1484 } 1485 if (!content->HasChildren() || 1486 HTMLEditUtils::IsNeverElementContentsEditableByUser(*content)) { 1487 return content; 1488 } 1489 if (aLeafNodeTypes.contains(LeafNodeType::LeafNodeOrNonEditableNode) && 1490 !HTMLEditUtils::IsSimplyEditableNode(*content)) { 1491 return content; 1492 } 1493 content = content->GetLastChild(); 1494 } 1495 return nullptr; 1496 } 1497 1498 /** 1499 * GetFirstLeafContent() returns leftmost leaf content in aNode. It depends 1500 * on aLeafNodeTypes whether this scans into a block child or treat block as a 1501 * leaf. 1502 * 1503 * @param aBlockInlineCheck Can be Unused if aLeafNodeTypes does not contain 1504 * LeafNodeOrCHildBlock. 1505 */ 1506 static nsIContent* GetFirstLeafContent( 1507 const nsINode& aNode, const LeafNodeTypes& aLeafNodeTypes, 1508 BlockInlineCheck aBlockInlineCheck = BlockInlineCheck::Unused, 1509 const Element* aAncestorLimiter = nullptr) { 1510 MOZ_ASSERT_IF( 1511 aLeafNodeTypes.contains(LeafNodeType::OnlyEditableLeafNode), 1512 !aLeafNodeTypes.contains(LeafNodeType::LeafNodeOrNonEditableNode)); 1513 // editor shouldn't touch child nodes which are replaced with native 1514 // anonymous nodes. 1515 if (aNode.IsElement() && 1516 HTMLEditUtils::IsNeverElementContentsEditableByUser( 1517 *aNode.AsElement())) { 1518 return nullptr; 1519 } 1520 for (nsIContent* content = aNode.GetFirstChild(); content;) { 1521 if (aLeafNodeTypes.contains(LeafNodeType::OnlyEditableLeafNode) && 1522 !EditorUtils::IsEditableContent(*content, 1523 EditorUtils::EditorType::HTML)) { 1524 content = HTMLEditUtils::GetNextContent( 1525 *content, {WalkTreeOption::IgnoreNonEditableNode}, 1526 aBlockInlineCheck, aAncestorLimiter); 1527 continue; 1528 } 1529 if (!aLeafNodeTypes.contains(LeafNodeType::TreatCommentAsLeafNode) && 1530 content->IsComment()) { 1531 content = content->GetNextSibling(); 1532 continue; 1533 } 1534 if (aLeafNodeTypes.contains(LeafNodeType::LeafNodeOrChildBlock) && 1535 HTMLEditUtils::IsBlockElement( 1536 *content, 1537 UseComputedDisplayOutsideStyleIfAuto(aBlockInlineCheck))) { 1538 return content; 1539 } 1540 if (!content->HasChildren() || 1541 HTMLEditUtils::IsNeverElementContentsEditableByUser(*content)) { 1542 return content; 1543 } 1544 if (aLeafNodeTypes.contains(LeafNodeType::LeafNodeOrNonEditableNode) && 1545 !HTMLEditUtils::IsSimplyEditableNode(*content)) { 1546 return content; 1547 } 1548 content = content->GetFirstChild(); 1549 } 1550 return nullptr; 1551 } 1552 1553 /** 1554 * GetNextLeafContentOrNextBlockElement() returns next leaf content or 1555 * next block element of aStartContent inside aAncestorLimiter. 1556 * 1557 * @param aStartContent The start content to scan next content. 1558 * @param aLeafNodeTypes See LeafNodeType. 1559 * @param aAncestorLimiter Optional, if you set this, it must be an 1560 * inclusive ancestor of aStartContent. 1561 */ 1562 static nsIContent* GetNextLeafContentOrNextBlockElement( 1563 const nsIContent& aStartContent, const LeafNodeTypes& aLeafNodeTypes, 1564 BlockInlineCheck aBlockInlineCheck, 1565 const Element* aAncestorLimiter = nullptr) { 1566 MOZ_ASSERT_IF( 1567 aLeafNodeTypes.contains(LeafNodeType::OnlyEditableLeafNode), 1568 !aLeafNodeTypes.contains(LeafNodeType::LeafNodeOrNonEditableNode)); 1569 1570 if (&aStartContent == aAncestorLimiter) { 1571 return nullptr; 1572 } 1573 1574 Element* container = aStartContent.GetParentElement(); 1575 for (nsIContent* nextContent = aStartContent.GetNextSibling();;) { 1576 if (!nextContent) { 1577 if (!container) { 1578 NS_WARNING("Reached orphan node while climbing up the DOM tree"); 1579 return nullptr; 1580 } 1581 for (Element* parentElement : 1582 container->InclusiveAncestorsOfType<Element>()) { 1583 if (parentElement == aAncestorLimiter || 1584 HTMLEditUtils::IsBlockElement( 1585 *parentElement, 1586 UseComputedDisplayStyleIfAuto(aBlockInlineCheck))) { 1587 return nullptr; 1588 } 1589 if (aLeafNodeTypes.contains( 1590 LeafNodeType::LeafNodeOrNonEditableNode) && 1591 !parentElement->IsEditable()) { 1592 return nullptr; 1593 } 1594 nextContent = parentElement->GetNextSibling(); 1595 if (nextContent) { 1596 container = nextContent->GetParentElement(); 1597 break; 1598 } 1599 if (!parentElement->GetParentElement()) { 1600 NS_WARNING("Reached orphan node while climbing up the DOM tree"); 1601 return nullptr; 1602 } 1603 } 1604 MOZ_ASSERT(nextContent); 1605 } 1606 1607 if (!aLeafNodeTypes.contains(LeafNodeType::TreatCommentAsLeafNode) && 1608 nextContent->IsComment()) { 1609 nextContent = nextContent->GetNextSibling(); 1610 continue; 1611 } 1612 1613 // We have a next content. If it's a block, return it. 1614 if (HTMLEditUtils::IsBlockElement( 1615 *nextContent, 1616 PreferDisplayOutsideIfUsingDisplay( 1617 UseComputedDisplayStyleIfAuto(aBlockInlineCheck)))) { 1618 return nextContent; 1619 } 1620 if (aLeafNodeTypes.contains(LeafNodeType::LeafNodeOrNonEditableNode) && 1621 !nextContent->IsEditable()) { 1622 return nextContent; 1623 } 1624 if (HTMLEditUtils::IsContainerNode(*nextContent)) { 1625 // Else if it's a container, get deep leftmost child 1626 if (nsIContent* child = HTMLEditUtils::GetFirstLeafContent( 1627 *nextContent, aLeafNodeTypes, aBlockInlineCheck)) { 1628 return child; 1629 } 1630 } 1631 // Else return the next content itself. 1632 return nextContent; 1633 } 1634 MOZ_MAKE_COMPILER_ASSUME_IS_UNREACHABLE( 1635 "Must return from the preceding for-loop"); 1636 } 1637 1638 /** 1639 * Similar to the above method, but take a DOM point to specify scan start 1640 * point. 1641 */ 1642 template <typename PT, typename CT> 1643 static nsIContent* GetNextLeafContentOrNextBlockElement( 1644 const EditorDOMPointBase<PT, CT>& aStartPoint, 1645 const LeafNodeTypes& aLeafNodeTypes, BlockInlineCheck aBlockInlineCheck, 1646 const Element* aAncestorLimiter = nullptr) { 1647 MOZ_ASSERT(aStartPoint.IsSet()); 1648 MOZ_ASSERT_IF( 1649 aLeafNodeTypes.contains(LeafNodeType::OnlyEditableLeafNode), 1650 !aLeafNodeTypes.contains(LeafNodeType::LeafNodeOrNonEditableNode)); 1651 NS_ASSERTION(!aLeafNodeTypes.contains(LeafNodeType::OnlyEditableLeafNode), 1652 "Not implemented yet"); 1653 1654 if (!aStartPoint.IsInContentNode()) { 1655 return nullptr; 1656 } 1657 if (aStartPoint.IsInTextNode()) { 1658 return HTMLEditUtils::GetNextLeafContentOrNextBlockElement( 1659 *aStartPoint.template ContainerAs<Text>(), aLeafNodeTypes, 1660 aBlockInlineCheck, aAncestorLimiter); 1661 } 1662 if (!HTMLEditUtils::IsContainerNode( 1663 *aStartPoint.template ContainerAs<nsIContent>())) { 1664 return HTMLEditUtils::GetNextLeafContentOrNextBlockElement( 1665 *aStartPoint.template ContainerAs<nsIContent>(), aLeafNodeTypes, 1666 aBlockInlineCheck, aAncestorLimiter); 1667 } 1668 1669 for (nsIContent* nextContent = aStartPoint.GetChild();;) { 1670 if (!nextContent) { 1671 if (aStartPoint.GetContainer() == aAncestorLimiter || 1672 HTMLEditUtils::IsBlockElement( 1673 *aStartPoint.template ContainerAs<nsIContent>(), 1674 UseComputedDisplayStyleIfAuto(aBlockInlineCheck))) { 1675 // We are at end of the block. 1676 return nullptr; 1677 } 1678 1679 // We are at end of non-block container 1680 return HTMLEditUtils::GetNextLeafContentOrNextBlockElement( 1681 *aStartPoint.template ContainerAs<nsIContent>(), aLeafNodeTypes, 1682 PreferDisplayOutsideIfUsingDisplay(aBlockInlineCheck), 1683 aAncestorLimiter); 1684 } 1685 1686 if (!aLeafNodeTypes.contains(LeafNodeType::TreatCommentAsLeafNode) && 1687 nextContent->IsComment()) { 1688 nextContent = nextContent->GetNextSibling(); 1689 continue; 1690 } 1691 1692 // We have a next node. If it's a block, return it. 1693 if (HTMLEditUtils::IsBlockElement( 1694 *nextContent, 1695 UseComputedDisplayOutsideStyleIfAuto(aBlockInlineCheck))) { 1696 return nextContent; 1697 } 1698 if (aLeafNodeTypes.contains(LeafNodeType::LeafNodeOrNonEditableNode) && 1699 !HTMLEditUtils::IsSimplyEditableNode(*nextContent)) { 1700 return nextContent; 1701 } 1702 if (HTMLEditUtils::IsContainerNode(*nextContent)) { 1703 // else if it's a container, get deep leftmost child 1704 if (nsIContent* child = HTMLEditUtils::GetFirstLeafContent( 1705 *nextContent, aLeafNodeTypes, 1706 PreferDisplayOutsideIfUsingDisplay(aBlockInlineCheck))) { 1707 return child; 1708 } 1709 } 1710 // Else return the node itself 1711 return nextContent; 1712 } 1713 MOZ_MAKE_COMPILER_ASSUME_IS_UNREACHABLE( 1714 "Must return from the preceding for-loop"); 1715 } 1716 1717 /** 1718 * GetPreviousLeafContentOrPreviousBlockElement() returns previous leaf 1719 * content or previous block element of aStartContent inside 1720 * aAncestorLimiter. 1721 * 1722 * @param aStartContent The start content to scan previous content. 1723 * @param aLeafNodeTypes See LeafNodeType. 1724 * @param aAncestorLimiter Optional, if you set this, it must be an 1725 * inclusive ancestor of aStartContent. 1726 */ 1727 static nsIContent* GetPreviousLeafContentOrPreviousBlockElement( 1728 const nsIContent& aStartContent, const LeafNodeTypes& aLeafNodeTypes, 1729 BlockInlineCheck aBlockInlineCheck, 1730 const Element* aAncestorLimiter = nullptr) { 1731 MOZ_ASSERT_IF( 1732 aLeafNodeTypes.contains(LeafNodeType::OnlyEditableLeafNode), 1733 !aLeafNodeTypes.contains(LeafNodeType::LeafNodeOrNonEditableNode)); 1734 NS_ASSERTION(!aLeafNodeTypes.contains(LeafNodeType::OnlyEditableLeafNode), 1735 "Not implemented yet"); 1736 1737 if (&aStartContent == aAncestorLimiter) { 1738 return nullptr; 1739 } 1740 1741 Element* container = aStartContent.GetParentElement(); 1742 for (nsIContent* previousContent = aStartContent.GetPreviousSibling();;) { 1743 if (!previousContent) { 1744 if (!container) { 1745 NS_WARNING("Reached orphan node while climbing up the DOM tree"); 1746 return nullptr; 1747 } 1748 for (Element* parentElement : 1749 container->InclusiveAncestorsOfType<Element>()) { 1750 if (parentElement == aAncestorLimiter || 1751 HTMLEditUtils::IsBlockElement( 1752 *parentElement, 1753 UseComputedDisplayStyleIfAuto(aBlockInlineCheck))) { 1754 return nullptr; 1755 } 1756 if (aLeafNodeTypes.contains( 1757 LeafNodeType::LeafNodeOrNonEditableNode) && 1758 !parentElement->IsEditable()) { 1759 return nullptr; 1760 } 1761 previousContent = parentElement->GetPreviousSibling(); 1762 if (previousContent) { 1763 container = previousContent->GetParentElement(); 1764 break; 1765 } 1766 if (!parentElement->GetParentElement()) { 1767 NS_WARNING("Reached orphan node while climbing up the DOM tree"); 1768 return nullptr; 1769 } 1770 } 1771 MOZ_ASSERT(previousContent); 1772 } 1773 1774 if (!aLeafNodeTypes.contains(LeafNodeType::TreatCommentAsLeafNode) && 1775 previousContent->IsComment()) { 1776 previousContent = previousContent->GetPreviousSibling(); 1777 continue; 1778 } 1779 1780 // We have a next content. If it's a block, return it. 1781 if (HTMLEditUtils::IsBlockElement( 1782 *previousContent, 1783 PreferDisplayOutsideIfUsingDisplay( 1784 UseComputedDisplayOutsideStyleIfAuto(aBlockInlineCheck)))) { 1785 return previousContent; 1786 } 1787 if (aLeafNodeTypes.contains(LeafNodeType::LeafNodeOrNonEditableNode) && 1788 !HTMLEditUtils::IsSimplyEditableNode(*previousContent)) { 1789 return previousContent; 1790 } 1791 if (HTMLEditUtils::IsContainerNode(*previousContent)) { 1792 // Else if it's a container, get deep rightmost child 1793 if (nsIContent* child = HTMLEditUtils::GetLastLeafContent( 1794 *previousContent, aLeafNodeTypes, 1795 PreferDisplayOutsideIfUsingDisplay(aBlockInlineCheck))) { 1796 return child; 1797 } 1798 } 1799 // Else return the next content itself. 1800 return previousContent; 1801 } 1802 MOZ_MAKE_COMPILER_ASSUME_IS_UNREACHABLE( 1803 "Must return from the preceding for-loop"); 1804 } 1805 1806 /** 1807 * Similar to the above method, but take a DOM point to specify scan start 1808 * point. 1809 */ 1810 template <typename PT, typename CT> 1811 static nsIContent* GetPreviousLeafContentOrPreviousBlockElement( 1812 const EditorDOMPointBase<PT, CT>& aStartPoint, 1813 const LeafNodeTypes& aLeafNodeTypes, BlockInlineCheck aBlockInlineCheck, 1814 const Element* aAncestorLimiter = nullptr) { 1815 MOZ_ASSERT(aStartPoint.IsSet()); 1816 MOZ_ASSERT_IF( 1817 aLeafNodeTypes.contains(LeafNodeType::OnlyEditableLeafNode), 1818 !aLeafNodeTypes.contains(LeafNodeType::LeafNodeOrNonEditableNode)); 1819 NS_ASSERTION(!aLeafNodeTypes.contains(LeafNodeType::OnlyEditableLeafNode), 1820 "Not implemented yet"); 1821 1822 if (!aStartPoint.IsInContentNode()) { 1823 return nullptr; 1824 } 1825 if (aStartPoint.IsInTextNode()) { 1826 return HTMLEditUtils::GetPreviousLeafContentOrPreviousBlockElement( 1827 *aStartPoint.template ContainerAs<Text>(), aLeafNodeTypes, 1828 aBlockInlineCheck, aAncestorLimiter); 1829 } 1830 if (!HTMLEditUtils::IsContainerNode( 1831 *aStartPoint.template ContainerAs<nsIContent>())) { 1832 return HTMLEditUtils::GetPreviousLeafContentOrPreviousBlockElement( 1833 *aStartPoint.template ContainerAs<nsIContent>(), aLeafNodeTypes, 1834 aBlockInlineCheck, aAncestorLimiter); 1835 } 1836 1837 if (aStartPoint.IsStartOfContainer()) { 1838 if (aStartPoint.GetContainer() == aAncestorLimiter || 1839 HTMLEditUtils::IsBlockElement( 1840 *aStartPoint.template ContainerAs<nsIContent>(), 1841 UseComputedDisplayStyleIfAuto(aBlockInlineCheck))) { 1842 // We are at start of the block. 1843 return nullptr; 1844 } 1845 1846 // We are at start of non-block container 1847 return HTMLEditUtils::GetPreviousLeafContentOrPreviousBlockElement( 1848 *aStartPoint.template ContainerAs<nsIContent>(), aLeafNodeTypes, 1849 PreferDisplayOutsideIfUsingDisplay(aBlockInlineCheck), 1850 aAncestorLimiter); 1851 } 1852 1853 for (nsIContent* previousContent = aStartPoint.GetPreviousSiblingOfChild(); 1854 previousContent && 1855 (aLeafNodeTypes.contains(LeafNodeType::TreatCommentAsLeafNode) || 1856 !previousContent->IsComment()); 1857 previousContent = previousContent->GetPreviousSibling()) { 1858 // We have a prior node. If it's a block, return it. 1859 if (HTMLEditUtils::IsBlockElement( 1860 *previousContent, 1861 UseComputedDisplayOutsideStyleIfAuto(aBlockInlineCheck))) { 1862 return previousContent; 1863 } 1864 if (aLeafNodeTypes.contains(LeafNodeType::LeafNodeOrNonEditableNode) && 1865 !HTMLEditUtils::IsSimplyEditableNode(*previousContent)) { 1866 return previousContent; 1867 } 1868 if (HTMLEditUtils::IsContainerNode(*previousContent)) { 1869 // Else if it's a container, get deep rightmost child 1870 if (nsIContent* child = HTMLEditUtils::GetLastLeafContent( 1871 *previousContent, aLeafNodeTypes, 1872 PreferDisplayOutsideIfUsingDisplay(aBlockInlineCheck))) { 1873 return child; 1874 } 1875 } 1876 // Else return the node itself 1877 return previousContent; 1878 } 1879 return nullptr; 1880 } 1881 1882 /** 1883 * Return previous non-empty leaf content or child block or non-editable 1884 * content (depending on aLeafNodeTypes). This ignores invisible inline leaf 1885 * element like `<b></b>` and empty `Text` nodes. So, this may return 1886 * invisible `Text` node, but it may be useful to consider whether we need to 1887 * insert a padding <br> element. 1888 */ 1889 [[nodiscard]] static nsIContent* 1890 GetPreviousNonEmptyLeafContentOrPreviousBlockElement( 1891 const nsIContent& aContent, const LeafNodeTypes& aLeafNodeTypes, 1892 BlockInlineCheck aBlockInlineCheck, 1893 const Element* aAncestorLimiter = nullptr) { 1894 for (nsIContent* previousContent = 1895 HTMLEditUtils::GetPreviousLeafContentOrPreviousBlockElement( 1896 aContent, aLeafNodeTypes, aBlockInlineCheck, aAncestorLimiter); 1897 previousContent; 1898 previousContent = 1899 HTMLEditUtils::GetPreviousLeafContentOrPreviousBlockElement( 1900 *previousContent, aLeafNodeTypes, aBlockInlineCheck, 1901 aAncestorLimiter)) { 1902 if (aLeafNodeTypes.contains(LeafNodeType::LeafNodeOrChildBlock) && 1903 HTMLEditUtils::IsBlockElement( 1904 *previousContent, 1905 PreferDisplayOutsideIfUsingDisplay( 1906 UseComputedDisplayOutsideStyleIfAuto(aBlockInlineCheck)))) { 1907 return previousContent; // Reached block element 1908 } 1909 if (aLeafNodeTypes.contains(LeafNodeType::LeafNodeOrNonEditableNode) && 1910 HTMLEditUtils::IsSimplyEditableNode(*previousContent)) { 1911 return previousContent; // Reached non-editable content 1912 } 1913 Text* const previousText = Text::FromNode(previousContent); 1914 if (!previousText) { 1915 if (!HTMLEditUtils::IsVisibleElementEvenIfLeafNode(*previousContent)) { 1916 continue; // Ignore invisible inline elements 1917 } 1918 return previousContent; // Reached visible inline element 1919 } 1920 if (!previousText->TextDataLength()) { 1921 continue; // Ignore empty Text nodes. 1922 } 1923 return previousText; // Reached non-empty text 1924 } 1925 return nullptr; 1926 } 1927 1928 /** 1929 * Return previous visible leaf content or child block or non-editable content 1930 * (depending on aLeafNodeTypes). This ignores invisible inline leaf element 1931 * like `<b></b>` and empty `Text` nodes. So, this may return invisible 1932 * `Text` node, but it may be useful to consider whether we need to insert a 1933 * padding <br> element. 1934 */ 1935 template <typename PT, typename CT> 1936 [[nodiscard]] static nsIContent* 1937 GetPreviousNonEmptyLeafContentOrPreviousBlockElement( 1938 const EditorDOMPointBase<PT, CT>& aPoint, 1939 const LeafNodeTypes& aLeafNodeTypes, BlockInlineCheck aBlockInlineCheck, 1940 const Element* aAncestorLimiter = nullptr) { 1941 for (nsIContent* previousContent = 1942 HTMLEditUtils::GetPreviousLeafContentOrPreviousBlockElement( 1943 aPoint, aLeafNodeTypes, aBlockInlineCheck, aAncestorLimiter); 1944 previousContent; 1945 previousContent = 1946 HTMLEditUtils::GetPreviousLeafContentOrPreviousBlockElement( 1947 *previousContent, aLeafNodeTypes, aBlockInlineCheck, 1948 aAncestorLimiter)) { 1949 if (aLeafNodeTypes.contains(LeafNodeType::LeafNodeOrChildBlock) && 1950 HTMLEditUtils::IsBlockElement( 1951 *previousContent, 1952 PreferDisplayOutsideIfUsingDisplay( 1953 UseComputedDisplayOutsideStyleIfAuto(aBlockInlineCheck)))) { 1954 return previousContent; // Reached block element 1955 } 1956 if (aLeafNodeTypes.contains(LeafNodeType::LeafNodeOrNonEditableNode) && 1957 HTMLEditUtils::IsSimplyEditableNode(*previousContent)) { 1958 return previousContent; // Reached non-editable content 1959 } 1960 Text* const previousText = Text::FromNode(previousContent); 1961 if (!previousText) { 1962 if (!HTMLEditUtils::IsVisibleElementEvenIfLeafNode(*previousContent)) { 1963 continue; // Ignore invisible inline elements 1964 } 1965 return previousContent; // Reached visible inline element 1966 } 1967 if (!previousText->TextDataLength()) { 1968 continue; // Ignore empty Text nodes. 1969 } 1970 return previousText; // Reached non-empty text 1971 } 1972 return nullptr; 1973 } 1974 /** 1975 * Returns a content node whose inline styles should be preserved after 1976 * deleting content in a range. Typically, you should set aPoint to start 1977 * boundary of the range to delete. 1978 */ 1979 template <typename EditorDOMPointType> 1980 static nsIContent* GetContentToPreserveInlineStyles( 1981 const EditorDOMPointType& aPoint, const Element& aEditingHost); 1982 1983 /** 1984 * Get previous/next editable point from start or end of aContent. 1985 */ 1986 enum class InvisibleWhiteSpaces { 1987 Ignore, // Ignore invisible white-spaces, i.e., don't return middle of 1988 // them. 1989 Preserve, // Preserve invisible white-spaces, i.e., result may be start or 1990 // end of a text node even if it begins or ends with invisible 1991 // white-spaces. 1992 }; 1993 enum class TableBoundary { 1994 Ignore, // May cross any table element boundary. 1995 NoCrossTableElement, // Won't cross `<table>` element boundary. 1996 NoCrossAnyTableElement, // Won't cross any table element boundary. 1997 }; 1998 template <typename EditorDOMPointType> 1999 static EditorDOMPointType GetPreviousEditablePoint( 2000 nsIContent& aContent, const Element* aAncestorLimiter, 2001 InvisibleWhiteSpaces aInvisibleWhiteSpaces, 2002 TableBoundary aHowToTreatTableBoundary); 2003 template <typename EditorDOMPointType> 2004 static EditorDOMPointType GetNextEditablePoint( 2005 nsIContent& aContent, const Element* aAncestorLimiter, 2006 InvisibleWhiteSpaces aInvisibleWhiteSpaces, 2007 TableBoundary aHowToTreatTableBoundary); 2008 2009 /** 2010 * GetAncestorElement() and GetInclusiveAncestorElement() return 2011 * (inclusive) block ancestor element of aContent whose time matches 2012 * aAncestorTypes. 2013 */ 2014 enum class AncestorType { 2015 // Return the closest block element. 2016 ClosestBlockElement, 2017 // Return the closest container element, either block or inline. I.e., this 2018 // ignores void elements like <br>, <hr>, etc. 2019 ClosestContainerElement, 2020 // Return a child element of the closest block element. 2021 // NOTE: If the scanning start node is a block element, the methods return 2022 // nullptr or the ancestor limiter if AllowRootOrAncestorLimiterElement is 2023 // set. 2024 // NOTE: If ClosestButtonElement is set, the methods may return a <button>. 2025 // NOTE: If StopAtClosestButtonElement is set, the methods return a child of 2026 // <button>. 2027 MostDistantInlineElementInBlock, 2028 // Ignore ancestor <hr> elements to check whether a block. 2029 IgnoreHRElement, 2030 // Return the closest button element. 2031 ClosestButtonElement, 2032 // Stop scanning at <button> element. The methods do not return the 2033 // <button> element if MostDistantInlineElementInBlock is set. 2034 StopAtClosestButtonElement, 2035 // When there is no ancestor which matches with the other flags and reached 2036 // the ancestor limiter (or an editing host, the editable <body> or the 2037 // editable root document element if and only if EditableElement is 2038 // specified), return the ancestor limiter, editable <body> or editable root 2039 // document element instead. 2040 ReturnAncestorLimiterIfNoProperAncestor, 2041 2042 // Limit to editable elements. If it reaches an non-editable element, 2043 // return the last ancestor element which matches with the other types. 2044 EditableElement, 2045 }; 2046 using AncestorTypes = EnumSet<AncestorType>; 2047 2048 friend std::ostream& operator<<(std::ostream& aStream, 2049 const AncestorType& aType); 2050 friend std::ostream& operator<<(std::ostream& aStream, 2051 const AncestorTypes& aTypes); 2052 2053 constexpr static AncestorTypes 2054 ClosestEditableBlockElementOrInlineEditingHost = { 2055 AncestorType::ClosestBlockElement, AncestorType::EditableElement, 2056 AncestorType::ReturnAncestorLimiterIfNoProperAncestor}; 2057 constexpr static AncestorTypes ClosestBlockElement = { 2058 AncestorType::ClosestBlockElement}; 2059 constexpr static AncestorTypes ClosestEditableBlockElement = { 2060 AncestorType::ClosestBlockElement, AncestorType::EditableElement}; 2061 constexpr static AncestorTypes ClosestBlockElementExceptHRElement = { 2062 AncestorType::ClosestBlockElement, AncestorType::IgnoreHRElement}; 2063 constexpr static AncestorTypes ClosestEditableBlockElementExceptHRElement = { 2064 AncestorType::ClosestBlockElement, AncestorType::IgnoreHRElement, 2065 AncestorType::EditableElement}; 2066 constexpr static AncestorTypes ClosestEditableBlockElementOrButtonElement = { 2067 AncestorType::ClosestBlockElement, AncestorType::EditableElement, 2068 AncestorType::ClosestButtonElement}; 2069 // Return the most distant ancestor element in current block or <button>. 2070 constexpr static AncestorTypes 2071 MostDistantEditableInlineElementInBlockOrButton = { 2072 AncestorType::MostDistantInlineElementInBlock, 2073 AncestorType::StopAtClosestButtonElement}; 2074 // Return the most distant ancestor element in current block or the closest 2075 // <button> if starting to scan within a <button>. If you want a child in the 2076 // <button> in the latter case, use 2077 // MostDistantEditableInlineElementInBlockOrButton. 2078 constexpr static AncestorTypes 2079 MostDistantEditableInlineElementInBlockOrClosestButton = { 2080 AncestorType::MostDistantInlineElementInBlock, 2081 AncestorType::ClosestButtonElement}; 2082 constexpr static AncestorTypes ClosestContainerElementOrVoidAncestorLimiter = 2083 {AncestorType::ClosestContainerElement, 2084 AncestorType::ReturnAncestorLimiterIfNoProperAncestor}; 2085 static Element* GetAncestorElement(const nsIContent& aContent, 2086 const AncestorTypes& aAncestorTypes, 2087 BlockInlineCheck aBlockInlineCheck, 2088 const Element* aAncestorLimiter = nullptr); 2089 static Element* GetInclusiveAncestorElement( 2090 const nsIContent& aContent, const AncestorTypes& aAncestorTypes, 2091 BlockInlineCheck aBlockInlineCheck, 2092 const Element* aAncestorLimiter = nullptr); 2093 2094 /** 2095 * GetClosestAncestorTableElement() returns the nearest inclusive ancestor 2096 * <table> element of aContent. 2097 */ 2098 static Element* GetClosestAncestorTableElement(const nsIContent& aContent) { 2099 // TODO: the method name and its documentation clash with the 2100 // implementation. Split this method into 2101 // `GetClosestAncestorTableElement` and 2102 // `GetClosestInclusiveAncestorTableElement`. 2103 if (!aContent.GetParent()) { 2104 return nullptr; 2105 } 2106 for (Element* element : aContent.InclusiveAncestorsOfType<Element>()) { 2107 if (element->IsHTMLElement(nsGkAtoms::table)) { 2108 return element; 2109 } 2110 } 2111 return nullptr; 2112 } 2113 2114 static Element* GetInclusiveAncestorAnyTableElement( 2115 const nsIContent& aContent) { 2116 for (Element* parent : aContent.InclusiveAncestorsOfType<Element>()) { 2117 if (HTMLEditUtils::IsAnyTableElementExceptColumnElement(*parent)) { 2118 return parent; 2119 } 2120 } 2121 return nullptr; 2122 } 2123 2124 [[nodiscard]] static Element* GetClosestAncestorAnyListElement( 2125 const nsIContent& aContent); 2126 [[nodiscard]] static Element* GetClosestInclusiveAncestorAnyListElement( 2127 const nsIContent& aContent); 2128 2129 /** 2130 * Return a list item element if aContent or its ancestor in editing host is 2131 * one. However, this won't cross table related element. 2132 */ 2133 static Element* GetClosestInclusiveAncestorListItemElement( 2134 const nsIContent& aContent, const Element* aAncestorLimit = nullptr) { 2135 MOZ_ASSERT_IF(aAncestorLimit, 2136 aContent.IsInclusiveDescendantOf(aAncestorLimit)); 2137 2138 if (HTMLEditUtils::IsListItemElement(aContent)) { 2139 return const_cast<Element*>(aContent.AsElement()); 2140 } 2141 2142 for (Element* parentElement : aContent.AncestorsOfType<Element>()) { 2143 if (HTMLEditUtils::IsAnyTableElementExceptColumnElement(*parentElement)) { 2144 return nullptr; 2145 } 2146 if (HTMLEditUtils::IsListItemElement(*parentElement)) { 2147 return parentElement; 2148 } 2149 if (parentElement == aAncestorLimit) { 2150 return nullptr; 2151 } 2152 } 2153 return nullptr; 2154 } 2155 2156 /** 2157 * GetRangeSelectingAllContentInAllListItems() returns a range which selects 2158 * from start of the first list item to end of the last list item of 2159 * aListElement. Note that the result may be in different list element if 2160 * aListElement has child list element(s) directly. 2161 */ 2162 template <typename EditorDOMRangeType> 2163 static EditorDOMRangeType GetRangeSelectingAllContentInAllListItems( 2164 const Element& aListElement) { 2165 MOZ_ASSERT(HTMLEditUtils::IsListElement(aListElement)); 2166 Element* firstListItem = 2167 HTMLEditUtils::GetFirstListItemElement(aListElement); 2168 Element* lastListItem = HTMLEditUtils::GetLastListItemElement(aListElement); 2169 MOZ_ASSERT_IF(firstListItem, lastListItem); 2170 MOZ_ASSERT_IF(!firstListItem, !lastListItem); 2171 if (!firstListItem || !lastListItem) { 2172 return EditorDOMRangeType(); 2173 } 2174 return EditorDOMRangeType( 2175 typename EditorDOMRangeType::PointType(firstListItem, 0u), 2176 EditorDOMRangeType::PointType::AtEndOf(*lastListItem)); 2177 } 2178 2179 /** 2180 * GetFirstListItemElement() returns the first list item element in the 2181 * pre-order tree traversal of the DOM. 2182 */ 2183 static Element* GetFirstListItemElement(const Element& aListElement) { 2184 MOZ_ASSERT(HTMLEditUtils::IsListElement(aListElement)); 2185 for (nsIContent* maybeFirstListItem = aListElement.GetFirstChild(); 2186 maybeFirstListItem; 2187 maybeFirstListItem = maybeFirstListItem->GetNextNode(&aListElement)) { 2188 if (HTMLEditUtils::IsListItemElement(*maybeFirstListItem)) { 2189 return maybeFirstListItem->AsElement(); 2190 } 2191 } 2192 return nullptr; 2193 } 2194 2195 /** 2196 * GetLastListItemElement() returns the last list item element in the 2197 * post-order tree traversal of the DOM. I.e., returns the last list 2198 * element whose close tag appears at last. 2199 */ 2200 static Element* GetLastListItemElement(const Element& aListElement) { 2201 MOZ_ASSERT(HTMLEditUtils::IsListElement(aListElement)); 2202 for (nsIContent* maybeLastListItem = aListElement.GetLastChild(); 2203 maybeLastListItem;) { 2204 if (HTMLEditUtils::IsListItemElement(*maybeLastListItem)) { 2205 return maybeLastListItem->AsElement(); 2206 } 2207 if (maybeLastListItem->HasChildren()) { 2208 maybeLastListItem = maybeLastListItem->GetLastChild(); 2209 continue; 2210 } 2211 if (maybeLastListItem->GetPreviousSibling()) { 2212 maybeLastListItem = maybeLastListItem->GetPreviousSibling(); 2213 continue; 2214 } 2215 for (Element* parent = maybeLastListItem->GetParentElement(); parent; 2216 parent = parent->GetParentElement()) { 2217 maybeLastListItem = nullptr; 2218 if (parent == &aListElement) { 2219 return nullptr; 2220 } 2221 if (parent->GetPreviousSibling()) { 2222 maybeLastListItem = parent->GetPreviousSibling(); 2223 break; 2224 } 2225 } 2226 } 2227 return nullptr; 2228 } 2229 2230 /** 2231 * GetFirstTableCellElementChild() and GetLastTableCellElementChild() 2232 * return the first/last element child of <tr> element if it's a table 2233 * cell element. 2234 */ 2235 static Element* GetFirstTableCellElementChild( 2236 const Element& aTableRowElement) { 2237 MOZ_ASSERT(aTableRowElement.IsHTMLElement(nsGkAtoms::tr)); 2238 Element* const firstElementChild = aTableRowElement.GetFirstElementChild(); 2239 return HTMLEditUtils::IsTableCellElement(firstElementChild) 2240 ? firstElementChild 2241 : nullptr; 2242 } 2243 static Element* GetLastTableCellElementChild( 2244 const Element& aTableRowElement) { 2245 MOZ_ASSERT(aTableRowElement.IsHTMLElement(nsGkAtoms::tr)); 2246 Element* const lastElementChild = aTableRowElement.GetLastElementChild(); 2247 return HTMLEditUtils::IsTableCellElement(lastElementChild) 2248 ? lastElementChild 2249 : nullptr; 2250 } 2251 2252 /** 2253 * GetPreviousTableCellElementSibling() and GetNextTableCellElementSibling() 2254 * return a table cell element of previous/next element sibling of given 2255 * content node if and only if the element sibling is a table cell element. 2256 */ 2257 static Element* GetPreviousTableCellElementSibling( 2258 const nsIContent& aChildOfTableRow) { 2259 MOZ_ASSERT(aChildOfTableRow.GetParentNode()); 2260 MOZ_ASSERT(aChildOfTableRow.GetParentNode()->IsHTMLElement(nsGkAtoms::tr)); 2261 Element* const previousElementSibling = 2262 aChildOfTableRow.GetPreviousElementSibling(); 2263 return HTMLEditUtils::IsTableCellElement(previousElementSibling) 2264 ? previousElementSibling 2265 : nullptr; 2266 } 2267 static Element* GetNextTableCellElementSibling( 2268 const nsIContent& aChildOfTableRow) { 2269 MOZ_ASSERT(aChildOfTableRow.GetParentNode()); 2270 MOZ_ASSERT(aChildOfTableRow.GetParentNode()->IsHTMLElement(nsGkAtoms::tr)); 2271 Element* const nextElementSibling = 2272 aChildOfTableRow.GetNextElementSibling(); 2273 return HTMLEditUtils::IsTableCellElement(nextElementSibling) 2274 ? nextElementSibling 2275 : nullptr; 2276 } 2277 2278 /** 2279 * GetMostDistantAncestorInlineElement() returns the most distant ancestor 2280 * inline element between aContent and the aEditingHost. Even if aEditingHost 2281 * is an inline element, this method never returns aEditingHost as the result. 2282 * Optionally, you can specify ancestor limiter content node. This guarantees 2283 * that the result is a descendant of aAncestorLimiter if aContent is a 2284 * descendant of aAncestorLimiter. 2285 */ 2286 static nsIContent* GetMostDistantAncestorInlineElement( 2287 const nsIContent& aContent, BlockInlineCheck aBlockInlineCheck, 2288 const Element* aEditingHost = nullptr, 2289 const nsIContent* aAncestorLimiter = nullptr) { 2290 aBlockInlineCheck = UseComputedDisplayStyleIfAuto(aBlockInlineCheck); 2291 if (HTMLEditUtils::IsBlockElement(aContent, aBlockInlineCheck)) { 2292 return nullptr; 2293 } 2294 2295 // If aNode is the editing host itself, there is no modifiable inline 2296 // parent. 2297 if (&aContent == aEditingHost || &aContent == aAncestorLimiter) { 2298 return nullptr; 2299 } 2300 2301 // If aNode is outside of the <body> element, we don't support to edit 2302 // such elements for now. 2303 // XXX This should be MOZ_ASSERT after fixing bug 1413131 for avoiding 2304 // calling this expensive method. 2305 if (aEditingHost && !aContent.IsInclusiveDescendantOf(aEditingHost)) { 2306 return nullptr; 2307 } 2308 2309 if (!aContent.GetParent()) { 2310 return const_cast<nsIContent*>(&aContent); 2311 } 2312 2313 // Looks for the highest inline parent in the editing host. 2314 nsIContent* topMostInlineContent = const_cast<nsIContent*>(&aContent); 2315 for (Element* element : aContent.AncestorsOfType<Element>()) { 2316 if (element == aEditingHost || element == aAncestorLimiter || 2317 HTMLEditUtils::IsBlockElement(*element, aBlockInlineCheck)) { 2318 break; 2319 } 2320 topMostInlineContent = element; 2321 } 2322 return topMostInlineContent; 2323 } 2324 2325 /** 2326 * GetMostDistantAncestorEditableEmptyInlineElement() returns most distant 2327 * ancestor which only has aEmptyContent or its ancestor, editable and 2328 * inline element. 2329 */ 2330 static Element* GetMostDistantAncestorEditableEmptyInlineElement( 2331 const nsIContent& aEmptyContent, BlockInlineCheck aBlockInlineCheck, 2332 const Element* aEditingHost = nullptr, 2333 const nsIContent* aAncestorLimiter = nullptr) { 2334 if (&aEmptyContent == aEditingHost || &aEmptyContent == aAncestorLimiter) { 2335 return nullptr; 2336 } 2337 aBlockInlineCheck = UseComputedDisplayStyleIfAuto(aBlockInlineCheck); 2338 nsIContent* lastEmptyContent = const_cast<nsIContent*>(&aEmptyContent); 2339 for (Element* element : aEmptyContent.AncestorsOfType<Element>()) { 2340 if (element == aEditingHost || element == aAncestorLimiter) { 2341 break; 2342 } 2343 if (!HTMLEditUtils::IsInlineContent(*element, aBlockInlineCheck) || 2344 !HTMLEditUtils::IsSimplyEditableNode(*element)) { 2345 break; 2346 } 2347 if (element->GetChildCount() > 1) { 2348 for (const nsIContent* child = element->GetFirstChild(); child; 2349 child = child->GetNextSibling()) { 2350 if (child == lastEmptyContent || child->IsComment()) { 2351 continue; 2352 } 2353 return lastEmptyContent != &aEmptyContent 2354 ? Element::FromNode(lastEmptyContent) 2355 : nullptr; 2356 } 2357 } 2358 lastEmptyContent = element; 2359 } 2360 return lastEmptyContent != &aEmptyContent 2361 ? Element::FromNode(lastEmptyContent) 2362 : nullptr; 2363 } 2364 2365 /** 2366 * GetElementIfOnlyOneSelected() returns an element if aRange selects only 2367 * the element node (and its descendants). 2368 */ 2369 static Element* GetElementIfOnlyOneSelected(const AbstractRange& aRange) { 2370 return GetElementIfOnlyOneSelected(EditorRawDOMRange(aRange)); 2371 } 2372 template <typename EditorDOMPointType> 2373 static Element* GetElementIfOnlyOneSelected( 2374 const EditorDOMRangeBase<EditorDOMPointType>& aRange) { 2375 if (!aRange.IsPositioned() || aRange.Collapsed()) { 2376 return nullptr; 2377 } 2378 const auto& start = aRange.StartRef(); 2379 const auto& end = aRange.EndRef(); 2380 if (NS_WARN_IF(!start.IsSetAndValid()) || 2381 NS_WARN_IF(!end.IsSetAndValid()) || 2382 start.GetContainer() != end.GetContainer()) { 2383 return nullptr; 2384 } 2385 nsIContent* childAtStart = start.GetChild(); 2386 if (!childAtStart || !childAtStart->IsElement()) { 2387 return nullptr; 2388 } 2389 // If start child is not the last sibling and only if end child is its 2390 // next sibling, the start child is selected. 2391 if (childAtStart->GetNextSibling()) { 2392 return childAtStart->GetNextSibling() == end.GetChild() 2393 ? childAtStart->AsElement() 2394 : nullptr; 2395 } 2396 // If start child is the last sibling and only if no child at the end, 2397 // the start child is selected. 2398 return !end.GetChild() ? childAtStart->AsElement() : nullptr; 2399 } 2400 2401 static Element* GetTableCellElementIfOnlyOneSelected( 2402 const AbstractRange& aRange) { 2403 Element* element = HTMLEditUtils::GetElementIfOnlyOneSelected(aRange); 2404 return HTMLEditUtils::IsTableCellElement(element) ? element : nullptr; 2405 } 2406 2407 /** 2408 * GetFirstSelectedTableCellElement() returns a table cell element (i.e., 2409 * `<td>` or `<th>` if and only if first selection range selects only a 2410 * table cell element. 2411 */ 2412 static Element* GetFirstSelectedTableCellElement( 2413 const Selection& aSelection) { 2414 if (!aSelection.RangeCount()) { 2415 return nullptr; 2416 } 2417 const nsRange* firstRange = aSelection.GetRangeAt(0); 2418 if (NS_WARN_IF(!firstRange) || NS_WARN_IF(!firstRange->IsPositioned())) { 2419 return nullptr; 2420 } 2421 return GetTableCellElementIfOnlyOneSelected(*firstRange); 2422 } 2423 2424 /** 2425 * GetInclusiveFirstChildWhichHasOneChild() returns the deepest element whose 2426 * tag name is one of `aFirstElementName` and `aOtherElementNames...` if and 2427 * only if the elements have only one child node. In other words, when 2428 * this method meets an element which does not matches any of the tag name 2429 * or it has no children or 2+ children. 2430 * 2431 * XXX This method must be implemented without treating edge cases. So, the 2432 * behavior is odd. E.g., why can we ignore non-editable node at counting 2433 * each children? Why do we dig non-editable aNode or first child of its 2434 * descendants? 2435 */ 2436 template <typename FirstElementName, typename... OtherElementNames> 2437 static Element* GetInclusiveDeepestFirstChildWhichHasOneChild( 2438 const nsINode& aNode, const WalkTreeOptions& aOptions, 2439 BlockInlineCheck aBlockInlineCheck, FirstElementName aFirstElementName, 2440 OtherElementNames... aOtherElementNames) { 2441 if (!aNode.IsElement()) { 2442 return nullptr; 2443 } 2444 Element* parentElement = nullptr; 2445 for (nsIContent* content = const_cast<nsIContent*>(aNode.AsContent()); 2446 content && content->IsElement() && 2447 content->IsAnyOfHTMLElements(aFirstElementName, aOtherElementNames...); 2448 // XXX Why do we scan only the first child of every element? If it's 2449 // not editable, why do we ignore it when aOptions specifies so. 2450 content = content->GetFirstChild()) { 2451 if (HTMLEditUtils::CountChildren(*content, aOptions, aBlockInlineCheck) != 2452 1) { 2453 return content->AsElement(); 2454 } 2455 parentElement = content->AsElement(); 2456 } 2457 return parentElement; 2458 } 2459 2460 /** 2461 * Get the first line break in aElement. This scans only leaf nodes so 2462 * if a <br> element has children illegally, it'll be ignored. 2463 * 2464 * @param aElement The element which may have a <br> element or a 2465 * preformatted linefeed. 2466 */ 2467 template <typename EditorLineBreakType> 2468 static Maybe<EditorLineBreakType> GetFirstLineBreak( 2469 const dom::Element& aElement) { 2470 for (nsIContent* content = HTMLEditUtils::GetFirstLeafContent( 2471 aElement, {LeafNodeType::OnlyLeafNode}); 2472 content; content = HTMLEditUtils::GetNextContent( 2473 *content, 2474 {WalkTreeOption::IgnoreDataNodeExceptText, 2475 WalkTreeOption::IgnoreWhiteSpaceOnlyText}, 2476 BlockInlineCheck::Unused, &aElement)) { 2477 if (auto* brElement = dom::HTMLBRElement::FromNode(*content)) { 2478 return Some(EditorLineBreakType(*brElement)); 2479 } 2480 if (auto* textNode = Text::FromNode(*content)) { 2481 if (EditorUtils::IsNewLinePreformatted(*textNode)) { 2482 uint32_t offset = textNode->DataBuffer().FindChar(kNewLine); 2483 if (offset != dom::CharacterDataBuffer::kNotFound) { 2484 return Some(EditorLineBreakType(*textNode, offset)); 2485 } 2486 } 2487 } 2488 } 2489 return Nothing(); 2490 } 2491 2492 enum class ScanLineBreak { 2493 AtEndOfBlock, 2494 BeforeBlock, 2495 }; 2496 /** 2497 * Return last <br> element or last text node ending with a preserved line 2498 * break of/before aBlockElement. 2499 * Note that the result may be non-editable and/or non-removable. 2500 */ 2501 template <typename EditorLineBreakType> 2502 static Maybe<EditorLineBreakType> GetUnnecessaryLineBreak( 2503 const Element& aBlockElement, ScanLineBreak aScanLineBreak); 2504 2505 /** 2506 * Return following <br> element from aPoint if and only if it's immediately 2507 * before a block boundary but it's not necessary to make the preceding 2508 * empty line of the block boundary visible anymore. 2509 * Note that the result may be non-editable and/or non-removable linebreak. 2510 */ 2511 template <typename EditorLineBreakType, typename EditorDOMPointType> 2512 [[nodiscard]] static Maybe<EditorLineBreakType> 2513 GetFollowingUnnecessaryLineBreak(const EditorDOMPointType& aPoint); 2514 2515 /** 2516 * IsInTableCellSelectionMode() returns true when Gecko's editor thinks that 2517 * selection is in a table cell selection mode. 2518 * Note that Gecko's editor traditionally treats selection as in table cell 2519 * selection mode when first range selects a table cell element. I.e., even 2520 * if `nsFrameSelection` is not in table cell selection mode, this may return 2521 * true. 2522 */ 2523 static bool IsInTableCellSelectionMode(const Selection& aSelection) { 2524 return GetFirstSelectedTableCellElement(aSelection) != nullptr; 2525 } 2526 2527 static EditAction GetEditActionForInsert(const nsAtom& aTagName); 2528 static EditAction GetEditActionForRemoveList(const nsAtom& aTagName); 2529 static EditAction GetEditActionForInsert(const Element& aElement); 2530 static EditAction GetEditActionForFormatText(const nsAtom& aProperty, 2531 const nsAtom* aAttribute, 2532 bool aToSetStyle); 2533 static EditAction GetEditActionForAlignment(const nsAString& aAlignType); 2534 2535 /** 2536 * GetPreviousNonCollapsibleCharOffset() returns offset of previous 2537 * character which is not collapsible white-space characters. 2538 */ 2539 enum class WalkTextOption { 2540 TreatNBSPsCollapsible, 2541 }; 2542 using WalkTextOptions = EnumSet<WalkTextOption>; 2543 static Maybe<uint32_t> GetPreviousNonCollapsibleCharOffset( 2544 const EditorDOMPointInText& aPoint, 2545 const WalkTextOptions& aWalkTextOptions = {}) { 2546 MOZ_ASSERT(aPoint.IsSetAndValid()); 2547 return GetPreviousNonCollapsibleCharOffset( 2548 *aPoint.ContainerAs<Text>(), aPoint.Offset(), aWalkTextOptions); 2549 } 2550 static Maybe<uint32_t> GetPreviousNonCollapsibleCharOffset( 2551 const Text& aTextNode, uint32_t aOffset, 2552 const WalkTextOptions& aWalkTextOptions = {}) { 2553 if (MOZ_UNLIKELY(!aOffset)) { 2554 return Nothing{}; 2555 } 2556 MOZ_ASSERT(aOffset <= aTextNode.TextDataLength()); 2557 if (EditorUtils::IsWhiteSpacePreformatted(aTextNode)) { 2558 return Some(aOffset - 1); 2559 } 2560 WhitespaceOptions whitespaceOptions{ 2561 WhitespaceOption::FormFeedIsSignificant}; 2562 if (EditorUtils::IsNewLinePreformatted(aTextNode)) { 2563 whitespaceOptions += WhitespaceOption::NewLineIsSignificant; 2564 } 2565 if (aWalkTextOptions.contains(WalkTextOption::TreatNBSPsCollapsible)) { 2566 whitespaceOptions += WhitespaceOption::TreatNBSPAsCollapsible; 2567 } 2568 const uint32_t prevVisibleCharOffset = 2569 aTextNode.DataBuffer().RFindNonWhitespaceChar(whitespaceOptions, 2570 aOffset - 1); 2571 return prevVisibleCharOffset != dom::CharacterDataBuffer::kNotFound 2572 ? Some(prevVisibleCharOffset) 2573 : Nothing(); 2574 } 2575 2576 /** 2577 * GetNextNonCollapsibleCharOffset() returns offset of next character which is 2578 * not collapsible white-space characters. 2579 */ 2580 static Maybe<uint32_t> GetNextNonCollapsibleCharOffset( 2581 const EditorDOMPointInText& aPoint, 2582 const WalkTextOptions& aWalkTextOptions = {}) { 2583 MOZ_ASSERT(aPoint.IsSetAndValid()); 2584 return GetNextNonCollapsibleCharOffset(*aPoint.ContainerAs<Text>(), 2585 aPoint.Offset(), aWalkTextOptions); 2586 } 2587 static Maybe<uint32_t> GetNextNonCollapsibleCharOffset( 2588 const Text& aTextNode, uint32_t aOffset, 2589 const WalkTextOptions& aWalkTextOptions = {}) { 2590 return GetInclusiveNextNonCollapsibleCharOffset(aTextNode, aOffset + 1, 2591 aWalkTextOptions); 2592 } 2593 2594 /** 2595 * GetInclusiveNextNonCollapsibleCharOffset() returns offset of inclusive next 2596 * character which is not collapsible white-space characters. 2597 */ 2598 template <typename PT, typename CT> 2599 static Maybe<uint32_t> GetInclusiveNextNonCollapsibleCharOffset( 2600 const EditorDOMPointBase<PT, CT>& aPoint, 2601 const WalkTextOptions& aWalkTextOptions = {}) { 2602 static_assert(std::is_same<PT, RefPtr<Text>>::value || 2603 std::is_same<PT, Text*>::value); 2604 MOZ_ASSERT(aPoint.IsSetAndValid()); 2605 return GetInclusiveNextNonCollapsibleCharOffset( 2606 *aPoint.template ContainerAs<Text>(), aPoint.Offset(), 2607 aWalkTextOptions); 2608 } 2609 static Maybe<uint32_t> GetInclusiveNextNonCollapsibleCharOffset( 2610 const Text& aTextNode, uint32_t aOffset, 2611 const WalkTextOptions& aWalkTextOptions = {}) { 2612 if (MOZ_UNLIKELY(aOffset >= aTextNode.TextDataLength())) { 2613 return Nothing(); 2614 } 2615 MOZ_ASSERT(aOffset <= aTextNode.TextDataLength()); 2616 if (EditorUtils::IsWhiteSpacePreformatted(aTextNode)) { 2617 return Some(aOffset); 2618 } 2619 WhitespaceOptions whitespaceOptions{ 2620 WhitespaceOption::FormFeedIsSignificant}; 2621 if (EditorUtils::IsNewLinePreformatted(aTextNode)) { 2622 whitespaceOptions += WhitespaceOption::NewLineIsSignificant; 2623 } 2624 if (aWalkTextOptions.contains(WalkTextOption::TreatNBSPsCollapsible)) { 2625 whitespaceOptions += WhitespaceOption::TreatNBSPAsCollapsible; 2626 } 2627 const uint32_t inclusiveNextVisibleCharOffset = 2628 aTextNode.DataBuffer().FindNonWhitespaceChar(whitespaceOptions, 2629 aOffset); 2630 if (inclusiveNextVisibleCharOffset != dom::CharacterDataBuffer::kNotFound) { 2631 return Some(inclusiveNextVisibleCharOffset); 2632 } 2633 return Nothing(); 2634 } 2635 2636 /** 2637 * GetFirstWhiteSpaceOffsetCollapsedWith() returns first collapsible 2638 * white-space offset which is collapsed with a white-space at the given 2639 * position. I.e., the character at the position must be a collapsible 2640 * white-space. 2641 */ 2642 template <typename PT, typename CT> 2643 static uint32_t GetFirstWhiteSpaceOffsetCollapsedWith( 2644 const EditorDOMPointBase<PT, CT>& aPoint, 2645 const WalkTextOptions& aWalkTextOptions = {}) { 2646 static_assert(std::is_same<PT, RefPtr<Text>>::value || 2647 std::is_same<PT, Text*>::value); 2648 MOZ_ASSERT(aPoint.IsSetAndValid()); 2649 MOZ_ASSERT(!aPoint.IsEndOfContainer()); 2650 MOZ_ASSERT_IF( 2651 aWalkTextOptions.contains(WalkTextOption::TreatNBSPsCollapsible), 2652 aPoint.IsCharCollapsibleASCIISpaceOrNBSP()); 2653 MOZ_ASSERT_IF( 2654 !aWalkTextOptions.contains(WalkTextOption::TreatNBSPsCollapsible), 2655 aPoint.IsCharCollapsibleASCIISpace()); 2656 return GetFirstWhiteSpaceOffsetCollapsedWith( 2657 *aPoint.template ContainerAs<Text>(), aPoint.Offset(), 2658 aWalkTextOptions); 2659 } 2660 static uint32_t GetFirstWhiteSpaceOffsetCollapsedWith( 2661 const Text& aTextNode, uint32_t aOffset, 2662 const WalkTextOptions& aWalkTextOptions = {}) { 2663 MOZ_ASSERT(aOffset < aTextNode.TextLength()); 2664 MOZ_ASSERT_IF( 2665 aWalkTextOptions.contains(WalkTextOption::TreatNBSPsCollapsible), 2666 EditorRawDOMPoint(&aTextNode, aOffset) 2667 .IsCharCollapsibleASCIISpaceOrNBSP()); 2668 MOZ_ASSERT_IF( 2669 !aWalkTextOptions.contains(WalkTextOption::TreatNBSPsCollapsible), 2670 EditorRawDOMPoint(&aTextNode, aOffset).IsCharCollapsibleASCIISpace()); 2671 if (!aOffset) { 2672 return 0; 2673 } 2674 Maybe<uint32_t> previousVisibleCharOffset = 2675 GetPreviousNonCollapsibleCharOffset(aTextNode, aOffset, 2676 aWalkTextOptions); 2677 return previousVisibleCharOffset.isSome() 2678 ? previousVisibleCharOffset.value() + 1 2679 : 0; 2680 } 2681 2682 /** 2683 * GetPreviousPreformattedNewLineInTextNode() returns a point which points 2684 * previous preformatted linefeed if there is and aPoint is in a text node. 2685 * If the node's linefeed characters are not preformatted or aPoint is not 2686 * in a text node, this returns unset DOM point. 2687 */ 2688 template <typename EditorDOMPointType, typename ArgEditorDOMPointType> 2689 static EditorDOMPointType GetPreviousPreformattedNewLineInTextNode( 2690 const ArgEditorDOMPointType& aPoint) { 2691 if (!aPoint.IsInTextNode() || aPoint.IsStartOfContainer() || 2692 !EditorUtils::IsNewLinePreformatted( 2693 *aPoint.template ContainerAs<Text>())) { 2694 return EditorDOMPointType(); 2695 } 2696 const Text& textNode = *aPoint.template ContainerAs<Text>(); 2697 MOZ_ASSERT(aPoint.Offset() <= textNode.DataBuffer().GetLength()); 2698 const uint32_t previousLineBreakOffset = 2699 textNode.DataBuffer().RFindChar('\n', aPoint.Offset() - 1u); 2700 return previousLineBreakOffset != dom::CharacterDataBuffer::kNotFound 2701 ? EditorDOMPointType(&textNode, previousLineBreakOffset) 2702 : EditorDOMPointType(); 2703 } 2704 2705 /** 2706 * GetInclusiveNextPreformattedNewLineInTextNode() returns a point which 2707 * points inclusive next preformatted linefeed if there is and aPoint is in a 2708 * text node. If the node's linefeed characters are not preformatted or aPoint 2709 * is not in a text node, this returns unset DOM point. 2710 */ 2711 template <typename EditorDOMPointType, typename ArgEditorDOMPointType> 2712 static EditorDOMPointType GetInclusiveNextPreformattedNewLineInTextNode( 2713 const ArgEditorDOMPointType& aPoint) { 2714 if (!aPoint.IsInTextNode() || aPoint.IsEndOfContainer() || 2715 !EditorUtils::IsNewLinePreformatted( 2716 *aPoint.template ContainerAs<Text>())) { 2717 return EditorDOMPointType(); 2718 } 2719 const Text& textNode = *aPoint.template ContainerAs<Text>(); 2720 MOZ_ASSERT(aPoint.Offset() <= textNode.DataBuffer().GetLength()); 2721 const uint32_t inclusiveNextVisibleCharOffset = 2722 textNode.DataBuffer().FindChar('\n', aPoint.Offset()); 2723 return inclusiveNextVisibleCharOffset != dom::CharacterDataBuffer::kNotFound 2724 ? EditorDOMPointType(&textNode, inclusiveNextVisibleCharOffset) 2725 : EditorDOMPointType(); 2726 } 2727 2728 /** 2729 * Get the first visible char offset in aText. I.e., this returns invisible 2730 * white-space length at start of aText. If there is no visible char in 2731 * aText, this returns the text data length. 2732 * Note that WSRunScanner::GetFirstVisiblePoint() may return different `Text` 2733 * node point, but this does not scan following `Text` nodes even if aText 2734 * is completely invisible. 2735 */ 2736 [[nodiscard]] static uint32_t GetFirstVisibleCharOffset(const Text& aText); 2737 2738 /** 2739 * Get next offset of the last visible char in aText. I.e., this returns 2740 * the first offset of invisible trailing white-spaces. If there is no 2741 * invisible trailing white-spaces in aText, this returns 0. 2742 * Note that WSRunScanner::GetAfterLastVisiblePoint() may return different 2743 * `Text` node point, but this does not scan preceding `Text` nodes even if 2744 * aText is completely invisible. 2745 */ 2746 [[nodiscard]] static uint32_t GetOffsetAfterLastVisibleChar( 2747 const Text& aText); 2748 2749 /** 2750 * Get the number of invisible white-spaces in the white-space sequence. Note 2751 * that some invisible white-spaces may be after the first visible character. 2752 * E.g., "SP SP NBSP SP SP NBSP". If this Text follows a block boundary, the 2753 * first SPs are the leading invisible white-spaces, and the first NBSP is the 2754 * first visible character. However, following 2 SPs are collapsed to one. 2755 * Therefore, one of them is counted as an invisible white-space. 2756 * 2757 * Note that this assumes that all white-spaces starting from aOffset and 2758 * ending by aOffset + aLength are collapsible white-spaces including NBSPs. 2759 */ 2760 [[nodiscard]] static uint32_t GetInvisibleWhiteSpaceCount( 2761 const Text& aText, uint32_t aOffset = 0u, uint32_t aLength = UINT32_MAX); 2762 2763 /** 2764 * GetGoodCaretPointFor() returns a good point to collapse `Selection` 2765 * after handling edit action with aDirectionAndAmount. 2766 * 2767 * @param aContent The content where you want to put caret 2768 * around. 2769 * @param aDirectionAndAmount Muse be one of eNext, eNextWord, eToEndOfLine, 2770 * ePrevious, ePreviousWord and eToBeggingOfLine. 2771 * Set the direction of handled edit action. 2772 */ 2773 template <typename EditorDOMPointType> 2774 static EditorDOMPointType GetGoodCaretPointFor( 2775 nsIContent& aContent, nsIEditor::EDirection aDirectionAndAmount) { 2776 MOZ_ASSERT(nsIEditor::EDirectionIsValidExceptNone(aDirectionAndAmount)); 2777 2778 // XXX Why don't we check whether the candidate position is enable or not? 2779 // When the result is not editable point, caret will be enclosed in 2780 // the non-editable content. 2781 2782 // If we can put caret in aContent, return start or end in it. 2783 if (aContent.IsText() || HTMLEditUtils::IsContainerNode(aContent) || 2784 NS_WARN_IF(!aContent.GetParentNode())) { 2785 return EditorDOMPointType( 2786 &aContent, nsIEditor::DirectionIsDelete(aDirectionAndAmount) 2787 ? 0 2788 : aContent.Length()); 2789 } 2790 2791 // If we are going forward, put caret at aContent itself. 2792 if (nsIEditor::DirectionIsDelete(aDirectionAndAmount)) { 2793 return EditorDOMPointType(&aContent); 2794 } 2795 2796 // If we are going backward, put caret to next node unless aContent is an 2797 // invisible `<br>` element. 2798 // XXX Shouldn't we put caret to first leaf of the next node? 2799 if (!HTMLEditUtils::IsInvisibleBRElement(aContent)) { 2800 EditorDOMPointType ret(EditorDOMPointType::After(aContent)); 2801 NS_WARNING_ASSERTION(ret.IsSet(), "Failed to set after aContent"); 2802 return ret; 2803 } 2804 2805 // Otherwise, we should put caret at the invisible `<br>` element. 2806 return EditorDOMPointType(&aContent); 2807 } 2808 2809 /** 2810 * GetBetterInsertionPointFor() returns better insertion point to insert 2811 * aContentToInsert. 2812 * 2813 * @param aContentToInsert The content to insert. 2814 * @param aPointToInsert A candidate point to insert the node. 2815 * @return Better insertion point if next visible node 2816 * is a <br> element and previous visible node 2817 * is neither none, another <br> element nor 2818 * different block level element. 2819 */ 2820 template <typename EditorDOMPointType, typename EditorDOMPointTypeInput> 2821 static EditorDOMPointType GetBetterInsertionPointFor( 2822 const nsIContent& aContentToInsert, 2823 const EditorDOMPointTypeInput& aPointToInsert); 2824 2825 /** 2826 * GetBetterCaretPositionToInsertText() returns better point to put caret 2827 * if aPoint is near a text node or in non-container node. 2828 */ 2829 template <typename EditorDOMPointType, typename EditorDOMPointTypeInput> 2830 static EditorDOMPointType GetBetterCaretPositionToInsertText( 2831 const EditorDOMPointTypeInput& aPoint); 2832 2833 /** 2834 * ComputePointToPutCaretInElementIfOutside() returns a good point in aElement 2835 * to put caret if aCurrentPoint is outside of aElement. 2836 * 2837 * @param aElement The result is a point in aElement. 2838 * @param aCurrentPoint The current (candidate) caret point. Only if this 2839 * is outside aElement, returns a point in aElement. 2840 */ 2841 template <typename EditorDOMPointType, typename EditorDOMPointTypeInput> 2842 static Result<EditorDOMPointType, nsresult> 2843 ComputePointToPutCaretInElementIfOutside( 2844 const Element& aElement, const EditorDOMPointTypeInput& aCurrentPoint); 2845 2846 /** 2847 * Return a line break if aPoint is after a line break which is immediately 2848 * before a block boundary. 2849 */ 2850 template <typename EditorLineBreakType, typename EditorDOMPointType> 2851 static Maybe<EditorLineBreakType> 2852 GetLineBreakBeforeBlockBoundaryIfPointIsBetweenThem( 2853 const EditorDOMPointType& aPoint, const Element& aEditingHost); 2854 2855 /** 2856 * Content-based query returns true if 2857 * <mHTMLProperty mAttribute=mAttributeValue> effects aContent. If there is 2858 * such a element, but another element whose attribute value does not match 2859 * with mAttributeValue is closer ancestor of aContent, then the distant 2860 * ancestor does not effect aContent. 2861 * 2862 * @param aContent The target of the query 2863 * @param aStyle The style which queries a representing element. 2864 * @param aValue Optional, the value of aStyle.mAttribute, example: blue 2865 * in <font color="blue"> May be null. Ignored if 2866 * aStyle.mAttribute is null. 2867 * @param aOutValue [OUT] the value of the attribute, if returns true 2868 * @return true if <mHTMLProperty mAttribute=mAttributeValue> 2869 * effects aContent. 2870 */ 2871 [[nodiscard]] static bool IsInlineStyleSetByElement( 2872 const nsIContent& aContent, const EditorInlineStyle& aStyle, 2873 const nsAString* aValue, nsAString* aOutValue = nullptr); 2874 2875 /** 2876 * CollectAllChildren() collects all child nodes of aParentNode. 2877 */ 2878 static void CollectAllChildren( 2879 const nsINode& aParentNode, 2880 nsTArray<OwningNonNull<nsIContent>>& aOutArrayOfContents) { 2881 MOZ_ASSERT(aOutArrayOfContents.IsEmpty()); 2882 aOutArrayOfContents.SetCapacity(aParentNode.GetChildCount()); 2883 for (nsIContent* childContent = aParentNode.GetFirstChild(); childContent; 2884 childContent = childContent->GetNextSibling()) { 2885 aOutArrayOfContents.AppendElement(*childContent); 2886 } 2887 } 2888 2889 /** 2890 * CollectChildren() collects child nodes of aNode (starting from 2891 * first editable child, but may return non-editable children after it). 2892 * 2893 * @param aNode Parent node of retrieving children. 2894 * @param aOutArrayOfContents [out] This method will inserts found children 2895 * into this array. 2896 * @param aIndexToInsertChildren Starting from this index, found 2897 * children will be inserted to the array. 2898 * @param aOptions Options to scan the children. 2899 * @return Number of found children. 2900 */ 2901 static size_t CollectChildren( 2902 const nsINode& aNode, 2903 nsTArray<OwningNonNull<nsIContent>>& aOutArrayOfContents, 2904 const CollectChildrenOptions& aOptions) { 2905 return HTMLEditUtils::CollectChildren(aNode, aOutArrayOfContents, 0u, 2906 aOptions); 2907 } 2908 static size_t CollectChildren( 2909 const nsINode& aNode, 2910 nsTArray<OwningNonNull<nsIContent>>& aOutArrayOfContents, 2911 size_t aIndexToInsertChildren, const CollectChildrenOptions& aOptions); 2912 2913 /** 2914 * CollectEmptyInlineContainerDescendants() appends empty inline elements in 2915 * aNode to aOutArrayOfContents. Although it's array of nsIContent, the 2916 * instance will be elements. 2917 * 2918 * @param aNode The node whose descendants may have empty inline 2919 * elements. 2920 * @param aOutArrayOfContents [out] This method will append found descendants 2921 * into this array. 2922 * @param aOptions The option which element should be treated as 2923 * empty. 2924 * @param aBlockInlineCheck Whether use computed style or HTML default style 2925 * when consider block vs. inline. 2926 * @return Number of found elements. 2927 */ 2928 static size_t CollectEmptyInlineContainerDescendants( 2929 const nsINode& aNode, 2930 nsTArray<OwningNonNull<nsIContent>>& aOutArrayOfContents, 2931 const EmptyCheckOptions& aOptions, BlockInlineCheck aBlockInlineCheck); 2932 2933 /** 2934 * Check whether aElement has attributes except the name aAttribute and 2935 * "_moz_*" attributes. 2936 */ 2937 [[nodiscard]] static bool ElementHasAttribute(const Element& aElement) { 2938 return ElementHasAttributeExcept(aElement, *nsGkAtoms::_empty, 2939 *nsGkAtoms::empty, *nsGkAtoms::_empty); 2940 } 2941 [[nodiscard]] static bool ElementHasAttributeExcept( 2942 const Element& aElement, const nsAtom& aAttribute) { 2943 return ElementHasAttributeExcept(aElement, aAttribute, *nsGkAtoms::_empty, 2944 *nsGkAtoms::empty); 2945 } 2946 [[nodiscard]] static bool ElementHasAttributeExcept( 2947 const Element& aElement, const nsAtom& aAttribute1, 2948 const nsAtom& aAttribute2) { 2949 return ElementHasAttributeExcept(aElement, aAttribute1, aAttribute2, 2950 *nsGkAtoms::empty); 2951 } 2952 [[nodiscard]] static bool ElementHasAttributeExcept( 2953 const Element& aElement, const nsAtom& aAttribute1, 2954 const nsAtom& aAttribute2, const nsAtom& aAttribute3); 2955 2956 enum class EditablePointOption { 2957 // Do not ignore invisible collapsible white-spaces which are next to a 2958 // block boundary. 2959 RecognizeInvisibleWhiteSpaces, 2960 // Stop at Comment node. 2961 StopAtComment, 2962 // Stop at List element. 2963 StopAtListElement, 2964 // Stop at ListItem element. 2965 StopAtListItemElement, 2966 // Stop at Table element. 2967 StopAtTableElement, 2968 // Stop at any table element. 2969 StopAtAnyTableElement, 2970 }; 2971 using EditablePointOptions = EnumSet<EditablePointOption>; 2972 2973 friend std::ostream& operator<<(std::ostream& aStream, 2974 const EditablePointOption& aOption); 2975 friend std::ostream& operator<<(std::ostream& aStream, 2976 const EditablePointOptions& aOptions); 2977 2978 private: 2979 class MOZ_STACK_CLASS AutoEditablePointChecker final { 2980 public: 2981 explicit AutoEditablePointChecker(const EditablePointOptions& aOptions) 2982 : mIgnoreInvisibleText(!aOptions.contains( 2983 EditablePointOption::RecognizeInvisibleWhiteSpaces)), 2984 mIgnoreComment( 2985 !aOptions.contains(EditablePointOption::StopAtComment)), 2986 mStopAtListElement( 2987 aOptions.contains(EditablePointOption::StopAtListElement)), 2988 mStopAtListItemElement( 2989 aOptions.contains(EditablePointOption::StopAtListItemElement)), 2990 mStopAtTableElement( 2991 aOptions.contains(EditablePointOption::StopAtTableElement)), 2992 mStopAtAnyTableElement( 2993 aOptions.contains(EditablePointOption::StopAtAnyTableElement)) {} 2994 2995 [[nodiscard]] bool IgnoreInvisibleWhiteSpaces() const { 2996 return mIgnoreInvisibleText; 2997 } 2998 2999 [[nodiscard]] bool NodeShouldBeIgnored(const nsIContent& aContent) const { 3000 if (mIgnoreInvisibleText && aContent.IsText() && 3001 HTMLEditUtils::IsSimplyEditableNode(aContent) && 3002 !HTMLEditUtils::IsVisibleTextNode(*aContent.AsText())) { 3003 return true; 3004 } 3005 if (mIgnoreComment && aContent.IsComment()) { 3006 return true; 3007 } 3008 return false; 3009 } 3010 3011 [[nodiscard]] bool ShouldStopScanningAt(const nsIContent& aContent) const { 3012 if (HTMLEditUtils::IsListElement(aContent)) { 3013 return mStopAtListElement; 3014 } 3015 if (HTMLEditUtils::IsListItemElement(aContent)) { 3016 return mStopAtListItemElement; 3017 } 3018 if (HTMLEditUtils::IsAnyTableElementExceptColumnElement(aContent)) { 3019 return mStopAtAnyTableElement || 3020 (mStopAtTableElement && 3021 aContent.IsHTMLElement(nsGkAtoms::table)); 3022 } 3023 return false; 3024 } 3025 3026 private: 3027 const bool mIgnoreInvisibleText; 3028 const bool mIgnoreComment; 3029 const bool mStopAtListElement; 3030 const bool mStopAtListItemElement; 3031 const bool mStopAtTableElement; 3032 const bool mStopAtAnyTableElement; 3033 }; 3034 3035 public: 3036 /** 3037 * Return a point which points deepest editable start point of aContent. This 3038 * walks the DOM tree in aContent to search meaningful first descendant. If 3039 * EditablePointOption::IgnoreInvisibleText is specified, this returns first 3040 * visible char offset if this reaches a visible `Text` first. If there is an 3041 * empty inline element such as <span>, this returns start of the inline 3042 * element. If this reaches non-editable element or non-container element 3043 * like <img>, this returns the position. 3044 */ 3045 template <typename EditorDOMPointType> 3046 [[nodiscard]] static EditorDOMPointType GetDeepestEditableStartPointOf( 3047 const nsIContent& aContent, const EditablePointOptions& aOptions) { 3048 if (NS_WARN_IF(!EditorUtils::IsEditableContent( 3049 aContent, EditorBase::EditorType::HTML))) { 3050 return EditorDOMPointType(); 3051 } 3052 const AutoEditablePointChecker checker(aOptions); 3053 EditorRawDOMPoint result(&aContent, 0u); 3054 while (true) { 3055 nsIContent* firstChild = result.GetContainer()->GetFirstChild(); 3056 if (!firstChild) { 3057 break; 3058 } 3059 // If the caller wants to skip invisible white-spaces, we should skip 3060 // invisible text nodes. 3061 nsIContent* meaningfulFirstChild = nullptr; 3062 if (checker.NodeShouldBeIgnored(*firstChild)) { 3063 // If we ignored a non-empty `Text`, it means that we're next to a block 3064 // boundary. 3065 for (nsIContent* nextSibling = firstChild->GetNextSibling(); 3066 nextSibling; nextSibling = nextSibling->GetNextSibling()) { 3067 if (!checker.NodeShouldBeIgnored(*nextSibling) || 3068 checker.ShouldStopScanningAt(*nextSibling)) { 3069 meaningfulFirstChild = nextSibling; 3070 break; 3071 } 3072 } 3073 if (!meaningfulFirstChild) { 3074 break; 3075 } 3076 } else { 3077 meaningfulFirstChild = firstChild; 3078 } 3079 if (meaningfulFirstChild->IsText()) { 3080 if (checker.IgnoreInvisibleWhiteSpaces()) { 3081 result.Set(meaningfulFirstChild, 3082 HTMLEditUtils::GetInclusiveNextNonCollapsibleCharOffset( 3083 *meaningfulFirstChild->AsText(), 0u) 3084 .valueOr(0u)); 3085 } else { 3086 result.Set(meaningfulFirstChild, 0u); 3087 } 3088 break; 3089 } 3090 if (checker.ShouldStopScanningAt(*meaningfulFirstChild) || 3091 !HTMLEditUtils::IsContainerNode(*meaningfulFirstChild) || 3092 !EditorUtils::IsEditableContent(*meaningfulFirstChild, 3093 EditorBase::EditorType::HTML)) { 3094 // FIXME: If the node is at middle of invisible white-spaces, we should 3095 // ignore the node. 3096 result.Set(meaningfulFirstChild); 3097 break; 3098 } 3099 result.Set(meaningfulFirstChild, 0u); 3100 } 3101 return result.To<EditorDOMPointType>(); 3102 } 3103 3104 /** 3105 * Return a point which points deepest editable last point of aContent. This 3106 * walks the DOM tree in aContent to search meaningful last descendant. If 3107 * EditablePointOption::IgnoreInvisibleText is specified, this returns next 3108 * offset of the last visible char if this reaches a visible `Text` first. If 3109 * there is an empty inline element such as <span>, this returns end of the 3110 * inline element. If this reaches non-editable element or non-container 3111 * element like <img>, this returns the position after that. 3112 */ 3113 template <typename EditorDOMPointType> 3114 [[nodiscard]] static EditorDOMPointType GetDeepestEditableEndPointOf( 3115 const nsIContent& aContent, const EditablePointOptions& aOptions) { 3116 if (NS_WARN_IF(!EditorUtils::IsEditableContent( 3117 aContent, EditorBase::EditorType::HTML))) { 3118 return EditorDOMPointType(); 3119 } 3120 const AutoEditablePointChecker checker(aOptions); 3121 auto result = EditorRawDOMPoint::AtEndOf(aContent); 3122 while (true) { 3123 nsIContent* lastChild = result.GetContainer()->GetLastChild(); 3124 if (!lastChild) { 3125 break; 3126 } 3127 // If the caller wants to skip invisible white-spaces, we should skip 3128 // invisible text nodes. 3129 nsIContent* meaningfulLastChild = nullptr; 3130 // XXX Should we skip the lastChild if it's an invisible line break? 3131 if (checker.NodeShouldBeIgnored(*lastChild)) { 3132 for (nsIContent* nextSibling = lastChild->GetPreviousSibling(); 3133 nextSibling; nextSibling = nextSibling->GetPreviousSibling()) { 3134 if (!checker.NodeShouldBeIgnored(*nextSibling) || 3135 checker.ShouldStopScanningAt(*nextSibling)) { 3136 meaningfulLastChild = nextSibling; 3137 break; 3138 } 3139 } 3140 if (!meaningfulLastChild) { 3141 break; 3142 } 3143 } else { 3144 meaningfulLastChild = lastChild; 3145 } 3146 if (meaningfulLastChild->IsText()) { 3147 if (checker.IgnoreInvisibleWhiteSpaces()) { 3148 const Maybe<uint32_t> visibleCharOffset = 3149 HTMLEditUtils::GetPreviousNonCollapsibleCharOffset( 3150 *meaningfulLastChild->AsText(), 3151 meaningfulLastChild->AsText()->TextDataLength()); 3152 if (visibleCharOffset.isNothing()) { 3153 result = EditorRawDOMPoint::AtEndOf(*meaningfulLastChild); 3154 } else { 3155 result.Set(meaningfulLastChild, visibleCharOffset.value() + 1u); 3156 } 3157 } else { 3158 result = EditorRawDOMPoint::AtEndOf(*meaningfulLastChild); 3159 } 3160 break; 3161 } 3162 if (checker.ShouldStopScanningAt(*meaningfulLastChild) || 3163 !HTMLEditUtils::IsContainerNode(*meaningfulLastChild) || 3164 !EditorUtils::IsEditableContent(*meaningfulLastChild, 3165 EditorBase::EditorType::HTML)) { 3166 // FIXME: If the node is at middle of invisible white-spaces, we should 3167 // ignore the node. 3168 result.SetAfter(meaningfulLastChild); 3169 break; 3170 } 3171 result = EditorRawDOMPoint::AtEndOf(*lastChild); 3172 } 3173 return result.To<EditorDOMPointType>(); 3174 } 3175 3176 /** 3177 * Get `#[0-9a-f]{6}` style HTML color value if aColorValue is valid value 3178 * for color-specifying attribute. The result is useful to set attributes 3179 * of HTML elements which take a color value. 3180 * 3181 * @param aColorValue [in] Should be one of `#[0-9a-fA-Z]{3}`, 3182 * `#[0-9a-fA-Z]{3}` or a color name. 3183 * @param aNormalizedValue [out] Set to `#[0-9a-f]{6}` style color code 3184 * if this returns true. Otherwise, returns 3185 * aColorValue as-is. 3186 * @return true if aColorValue is valid. Otherwise, false. 3187 */ 3188 static bool GetNormalizedHTMLColorValue(const nsAString& aColorValue, 3189 nsAString& aNormalizedValue); 3190 3191 /** 3192 * Return true if aColorValue may be a CSS specific color value or general 3193 * keywords of CSS. 3194 */ 3195 [[nodiscard]] static bool MaybeCSSSpecificColorValue( 3196 const nsAString& aColorValue); 3197 3198 /** 3199 * Return true if aColorValue can be specified to `color` value of <font>. 3200 */ 3201 [[nodiscard]] static bool CanConvertToHTMLColorValue( 3202 const nsAString& aColorValue); 3203 3204 /** 3205 * Convert aColorValue to `#[0-9a-f]{6}` style HTML color value. 3206 */ 3207 static bool ConvertToNormalizedHTMLColorValue(const nsAString& aColorValue, 3208 nsAString& aNormalizedValue); 3209 3210 /** 3211 * Get serialized color value (`rgb(...)` or `rgba(...)`) or "currentcolor" 3212 * if aColorValue is valid. The result is useful to set CSS color property. 3213 * 3214 * @param aColorValue [in] Should be valid CSS color value. 3215 * @param aZeroAlphaColor [in] If TransparentKeyword, aNormalizedValue is 3216 * set to "transparent" if the alpha value is 0. 3217 * Otherwise, `rgba(...)` value is set. 3218 * @param aNormalizedValue [out] Serialized color value or "currentcolor". 3219 * @return true if aColorValue is valid. Otherwise, false. 3220 */ 3221 enum class ZeroAlphaColor { RGBAValue, TransparentKeyword }; 3222 static bool GetNormalizedCSSColorValue(const nsAString& aColorValue, 3223 ZeroAlphaColor aZeroAlphaColor, 3224 nsAString& aNormalizedValue); 3225 3226 /** 3227 * Check whether aColorA and aColorB are same color. 3228 * 3229 * @param aTransparentKeyword Whether allow to treat "transparent" keyword 3230 * as a valid value or an invalid value. 3231 * @return If aColorA and aColorB are valid values and 3232 * mean same color, returns true. 3233 */ 3234 enum class TransparentKeyword { Invalid, Allowed }; 3235 static bool IsSameHTMLColorValue(const nsAString& aColorA, 3236 const nsAString& aColorB, 3237 TransparentKeyword aTransparentKeyword); 3238 3239 /** 3240 * Check whether aColorA and aColorB are same color. 3241 * 3242 * @return If aColorA and aColorB are valid values and 3243 * mean same color, returns true. 3244 */ 3245 template <typename CharType> 3246 static bool IsSameCSSColorValue(const nsTSubstring<CharType>& aColorA, 3247 const nsTSubstring<CharType>& aColorB); 3248 3249 /** 3250 * Return true if aColor is completely transparent. 3251 */ 3252 [[nodiscard]] static bool IsTransparentCSSColor(const nsAString& aColor); 3253 3254 private: 3255 static bool CanNodeContain(nsHTMLTag aParentTagId, nsHTMLTag aChildTagId); 3256 static bool IsContainerNode(nsHTMLTag aTagId); 3257 3258 static bool CanCrossContentBoundary(nsIContent& aContent, 3259 TableBoundary aHowToTreatTableBoundary) { 3260 const bool cannotCrossBoundary = 3261 (aHowToTreatTableBoundary == TableBoundary::NoCrossAnyTableElement && 3262 HTMLEditUtils::IsAnyTableElementExceptColumnElement(aContent)) || 3263 (aHowToTreatTableBoundary == TableBoundary::NoCrossTableElement && 3264 aContent.IsHTMLElement(nsGkAtoms::table)); 3265 return !cannotCrossBoundary; 3266 } 3267 3268 static bool IsContentIgnored(const nsIContent& aContent, 3269 const WalkTreeOptions& aOptions) { 3270 if (aOptions.contains(WalkTreeOption::IgnoreNonEditableNode) && 3271 !EditorUtils::IsEditableContent(aContent, 3272 EditorUtils::EditorType::HTML)) { 3273 return true; 3274 } 3275 if (aOptions.contains(WalkTreeOption::IgnoreDataNodeExceptText) && 3276 !EditorUtils::IsElementOrText(aContent)) { 3277 return true; 3278 } 3279 if (aOptions.contains(WalkTreeOption::IgnoreWhiteSpaceOnlyText) && 3280 aContent.IsText() && 3281 const_cast<Text*>(aContent.AsText())->TextIsOnlyWhitespace()) { 3282 return true; 3283 } 3284 return false; 3285 } 3286 3287 static uint32_t CountChildren(const nsINode& aNode, 3288 const WalkTreeOptions& aOptions, 3289 BlockInlineCheck aBlockInlineCheck) { 3290 uint32_t count = 0; 3291 for (nsIContent* child = aNode.GetFirstChild(); child; 3292 child = child->GetNextSibling()) { 3293 if (HTMLEditUtils::IsContentIgnored(*child, aOptions)) { 3294 continue; 3295 } 3296 if (aOptions.contains(WalkTreeOption::StopAtBlockBoundary) && 3297 HTMLEditUtils::IsBlockElement( 3298 *child, 3299 UseComputedDisplayOutsideStyleIfAuto(aBlockInlineCheck))) { 3300 break; 3301 } 3302 ++count; 3303 } 3304 return count; 3305 } 3306 3307 /** 3308 * Helper for GetPreviousContent() and GetNextContent(). 3309 */ 3310 static nsIContent* GetAdjacentLeafContent( 3311 const nsINode& aNode, WalkTreeDirection aWalkTreeDirection, 3312 const WalkTreeOptions& aOptions, BlockInlineCheck aBlockInlineCheck, 3313 const Element* aAncestorLimiter = nullptr); 3314 static nsIContent* GetAdjacentContent( 3315 const nsINode& aNode, WalkTreeDirection aWalkTreeDirection, 3316 const WalkTreeOptions& aOptions, BlockInlineCheck aBlockInlineCheck, 3317 const Element* aAncestorLimiter = nullptr); 3318 3319 /** 3320 * GetElementOfImmediateBlockBoundary() returns a block element if its 3321 * block boundary and aContent may be first visible thing before/after the 3322 * boundary. And it may return a <br> element only when aContent is a 3323 * text node and follows a <br> element because only in this case, the 3324 * start white-spaces are invisible. So the <br> element works same as 3325 * a block boundary. 3326 */ 3327 static Element* GetElementOfImmediateBlockBoundary( 3328 const nsIContent& aContent, const WalkTreeDirection aDirection); 3329 3330 /** 3331 * Return true if parent element is a grid or flex container. 3332 * Note that even if the parent is a grid/flex container, the 3333 * <display-outside> of aMaybeFlexOrGridItemContent may be "inline" if the 3334 * parent is also a grid/flex item but has `display:contents`. 3335 */ 3336 [[nodiscard]] static bool ParentElementIsGridOrFlexContainer( 3337 const nsIContent& aMaybeFlexOrGridItemContent); 3338 }; 3339 3340 /** 3341 * DefinitionListItemScanner() scans given `<dl>` element's children. 3342 * Then, you can check whether `<dt>` and/or `<dd>` elements are in it. 3343 */ 3344 class MOZ_STACK_CLASS DefinitionListItemScanner final { 3345 using Element = dom::Element; 3346 3347 public: 3348 DefinitionListItemScanner() = delete; 3349 explicit DefinitionListItemScanner(Element& aDLElement) { 3350 MOZ_ASSERT(aDLElement.IsHTMLElement(nsGkAtoms::dl)); 3351 for (nsIContent* child = aDLElement.GetFirstChild(); child; 3352 child = child->GetNextSibling()) { 3353 if (child->IsHTMLElement(nsGkAtoms::dt)) { 3354 mDTFound = true; 3355 if (mDDFound) { 3356 break; 3357 } 3358 continue; 3359 } 3360 if (child->IsHTMLElement(nsGkAtoms::dd)) { 3361 mDDFound = true; 3362 if (mDTFound) { 3363 break; 3364 } 3365 continue; 3366 } 3367 } 3368 } 3369 3370 bool DTElementFound() const { return mDTFound; } 3371 bool DDElementFound() const { return mDDFound; } 3372 3373 private: 3374 bool mDTFound = false; 3375 bool mDDFound = false; 3376 }; 3377 3378 /** 3379 * SelectedTableCellScanner() scans all table cell elements which are selected 3380 * by each selection range. Note that if 2nd or later ranges do not select 3381 * only one table cell element, the ranges are just ignored. 3382 */ 3383 class MOZ_STACK_CLASS SelectedTableCellScanner final { 3384 using Element = dom::Element; 3385 using Selection = dom::Selection; 3386 3387 public: 3388 SelectedTableCellScanner() = delete; 3389 explicit SelectedTableCellScanner(const Selection& aSelection) { 3390 Element* firstSelectedCellElement = 3391 HTMLEditUtils::GetFirstSelectedTableCellElement(aSelection); 3392 if (!firstSelectedCellElement) { 3393 return; // We're not in table cell selection mode. 3394 } 3395 mSelectedCellElements.SetCapacity(aSelection.RangeCount()); 3396 mSelectedCellElements.AppendElement(*firstSelectedCellElement); 3397 const uint32_t rangeCount = aSelection.RangeCount(); 3398 for (const uint32_t i : IntegerRange(1u, rangeCount)) { 3399 MOZ_ASSERT(aSelection.RangeCount() == rangeCount); 3400 nsRange* range = aSelection.GetRangeAt(i); 3401 if (MOZ_UNLIKELY(NS_WARN_IF(!range)) || 3402 MOZ_UNLIKELY(NS_WARN_IF(!range->IsPositioned()))) { 3403 continue; // Shouldn't occur in normal conditions. 3404 } 3405 // Just ignore selection ranges which do not select only one table 3406 // cell element. This is possible case if web apps sets multiple 3407 // selections and first range selects a table cell element. 3408 if (Element* selectedCellElement = 3409 HTMLEditUtils::GetTableCellElementIfOnlyOneSelected(*range)) { 3410 mSelectedCellElements.AppendElement(*selectedCellElement); 3411 } 3412 } 3413 } 3414 3415 explicit SelectedTableCellScanner(const AutoClonedRangeArray& aRanges); 3416 3417 bool IsInTableCellSelectionMode() const { 3418 return !mSelectedCellElements.IsEmpty(); 3419 } 3420 3421 const nsTArray<OwningNonNull<Element>>& ElementsRef() const { 3422 return mSelectedCellElements; 3423 } 3424 3425 /** 3426 * GetFirstElement() and GetNextElement() are stateful iterator methods. 3427 * This is useful to port legacy code which used old `nsITableEditor` API. 3428 */ 3429 Element* GetFirstElement() const { 3430 MOZ_ASSERT(!mSelectedCellElements.IsEmpty()); 3431 mIndex = 0; 3432 return !mSelectedCellElements.IsEmpty() ? mSelectedCellElements[0].get() 3433 : nullptr; 3434 } 3435 Element* GetNextElement() const { 3436 MOZ_ASSERT(mIndex < mSelectedCellElements.Length()); 3437 return ++mIndex < mSelectedCellElements.Length() 3438 ? mSelectedCellElements[mIndex].get() 3439 : nullptr; 3440 } 3441 3442 private: 3443 AutoTArray<OwningNonNull<Element>, 16> mSelectedCellElements; 3444 mutable size_t mIndex = 0; 3445 }; 3446 3447 } // namespace mozilla 3448 3449 #endif // #ifndef HTMLEditUtils_h