WSRunScanner.h (61169B)
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 WSRunScanner_h 7 #define WSRunScanner_h 8 9 #include "EditorBase.h" 10 #include "EditorForwards.h" 11 #include "EditorDOMPoint.h" // for EditorDOMPoint 12 #include "EditorLineBreak.h" // for EditorLineBreakBase 13 #include "HTMLEditor.h" 14 #include "HTMLEditUtils.h" 15 16 #include "mozilla/Assertions.h" 17 #include "mozilla/Maybe.h" 18 #include "mozilla/Result.h" 19 #include "mozilla/dom/Element.h" 20 #include "mozilla/dom/HTMLBRElement.h" 21 #include "mozilla/dom/Text.h" 22 #include "nsCOMPtr.h" 23 #include "nsIContent.h" 24 25 namespace mozilla { 26 27 /** 28 * WSScanResult is result of ScanNextVisibleNodeOrBlockBoundaryFrom(), 29 * ScanPreviousVisibleNodeOrBlockBoundaryFrom(), and their static wrapper 30 * methods. This will have information of found visible content (and its 31 * position) or reached block element or topmost editable content at the 32 * start of scanner. 33 */ 34 class MOZ_STACK_CLASS WSScanResult final { 35 private: 36 using Element = dom::Element; 37 using HTMLBRElement = dom::HTMLBRElement; 38 using Text = dom::Text; 39 40 enum class WSType : uint8_t { 41 NotInitialized, 42 // Could be the DOM tree is broken as like crash tests. 43 UnexpectedError, 44 // The scanner cannot work in uncomposed tree, but tried to scan in it. 45 InUncomposedDoc, 46 // The run is maybe collapsible white-spaces at start of a hard line. 47 LeadingWhiteSpaces, 48 // The run is maybe collapsible white-spaces at end of a hard line. 49 TrailingWhiteSpaces, 50 // Collapsible, but visible white-spaces. 51 CollapsibleWhiteSpaces, 52 // Visible characters except collapsible white-spaces. 53 NonCollapsibleCharacters, 54 // Special content such as `<img>`, etc. 55 SpecialContent, 56 // <br> element. 57 BRElement, 58 // A linefeed which is preformatted. 59 PreformattedLineBreak, 60 // Other block's boundary (child block of current block, maybe). 61 OtherBlockBoundary, 62 // Current block's boundary. 63 CurrentBlockBoundary, 64 // Inline editing host boundary. 65 InlineEditingHostBoundary, 66 }; 67 68 friend std::ostream& operator<<(std::ostream& aStream, const WSType& aType) { 69 switch (aType) { 70 case WSType::NotInitialized: 71 return aStream << "WSType::NotInitialized"; 72 case WSType::UnexpectedError: 73 return aStream << "WSType::UnexpectedError"; 74 case WSType::InUncomposedDoc: 75 return aStream << "WSType::InUncomposedDoc"; 76 case WSType::LeadingWhiteSpaces: 77 return aStream << "WSType::LeadingWhiteSpaces"; 78 case WSType::TrailingWhiteSpaces: 79 return aStream << "WSType::TrailingWhiteSpaces"; 80 case WSType::CollapsibleWhiteSpaces: 81 return aStream << "WSType::CollapsibleWhiteSpaces"; 82 case WSType::NonCollapsibleCharacters: 83 return aStream << "WSType::NonCollapsibleCharacters"; 84 case WSType::SpecialContent: 85 return aStream << "WSType::SpecialContent"; 86 case WSType::BRElement: 87 return aStream << "WSType::BRElement"; 88 case WSType::PreformattedLineBreak: 89 return aStream << "WSType::PreformattedLineBreak"; 90 case WSType::OtherBlockBoundary: 91 return aStream << "WSType::OtherBlockBoundary"; 92 case WSType::CurrentBlockBoundary: 93 return aStream << "WSType::CurrentBlockBoundary"; 94 case WSType::InlineEditingHostBoundary: 95 return aStream << "WSType::InlineEditingHostBoundary"; 96 } 97 return aStream << "<Illegal value>"; 98 } 99 100 friend class WSRunScanner; // Because of WSType. 101 102 explicit WSScanResult(WSType aReason) : mReason(aReason) { 103 MOZ_ASSERT(mReason == WSType::UnexpectedError || 104 mReason == WSType::NotInitialized); 105 } 106 107 public: 108 WSScanResult() = delete; 109 enum class ScanDirection : bool { Backward, Forward }; 110 WSScanResult(const WSRunScanner& aScanner, ScanDirection aScanDirection, 111 nsIContent& aContent, WSType aReason) 112 : mContent(&aContent), mReason(aReason), mDirection(aScanDirection) { 113 MOZ_ASSERT(aReason != WSType::CollapsibleWhiteSpaces && 114 aReason != WSType::NonCollapsibleCharacters && 115 aReason != WSType::PreformattedLineBreak); 116 AssertIfInvalidData(aScanner); 117 } 118 WSScanResult(const WSRunScanner& aScanner, ScanDirection aScanDirection, 119 const EditorDOMPoint& aPoint, WSType aReason) 120 : mContent(aPoint.GetContainerAs<nsIContent>()), 121 mOffset(Some(aPoint.Offset())), 122 mReason(aReason), 123 mDirection(aScanDirection) { 124 AssertIfInvalidData(aScanner); 125 } 126 127 static WSScanResult Error() { return WSScanResult(WSType::UnexpectedError); } 128 129 void AssertIfInvalidData(const WSRunScanner& aScanner) const; 130 131 bool Failed() const { 132 return mReason == WSType::NotInitialized || 133 mReason == WSType::UnexpectedError; 134 } 135 136 /** 137 * GetContent() returns found visible and editable content/element. 138 * See MOZ_ASSERT_IF()s in AssertIfInvalidData() for the detail. 139 */ 140 nsIContent* GetContent() const { return mContent; } 141 142 [[nodiscard]] bool ContentIsElement() const { 143 return mContent && mContent->IsElement(); 144 } 145 146 [[nodiscard]] bool ContentIsText() const { 147 return mContent && mContent->IsText(); 148 } 149 150 /** 151 * The following accessors makes it easier to understand each callers. 152 */ 153 MOZ_NEVER_INLINE_DEBUG Element* ElementPtr() const { 154 MOZ_DIAGNOSTIC_ASSERT(mContent->IsElement()); 155 return mContent->AsElement(); 156 } 157 MOZ_NEVER_INLINE_DEBUG HTMLBRElement* BRElementPtr() const { 158 MOZ_DIAGNOSTIC_ASSERT(mContent->IsHTMLElement(nsGkAtoms::br)); 159 return static_cast<HTMLBRElement*>(mContent.get()); 160 } 161 MOZ_NEVER_INLINE_DEBUG Text* TextPtr() const { 162 MOZ_DIAGNOSTIC_ASSERT(mContent->IsText()); 163 return mContent->AsText(); 164 } 165 166 template <typename EditorLineBreakType> 167 MOZ_NEVER_INLINE_DEBUG EditorLineBreakType CreateEditorLineBreak() const { 168 if (ReachedBRElement()) { 169 return EditorLineBreakType(*BRElementPtr()); 170 } 171 if (ReachedPreformattedLineBreak()) { 172 MOZ_ASSERT_IF(mDirection == ScanDirection::Backward, *mOffset > 0); 173 return EditorLineBreakType(*TextPtr(), 174 mDirection == ScanDirection::Forward 175 ? mOffset.valueOr(0) 176 : std::max(mOffset.valueOr(1), 1u) - 1); 177 } 178 MOZ_CRASH("Didn't reach a line break"); 179 return EditorLineBreakType(*BRElementPtr()); 180 } 181 182 /** 183 * Returns true if found or reached content is editable. 184 */ 185 bool IsContentEditable() const { return mContent && mContent->IsEditable(); } 186 187 [[nodiscard]] bool IsContentEditableRoot() const { 188 return mContent && mContent->IsElement() && 189 HTMLEditUtils::ElementIsEditableRoot(*mContent->AsElement()); 190 } 191 192 /** 193 * Offset_Deprecated() returns meaningful value only when 194 * InVisibleOrCollapsibleCharacters() returns true or the scanner reached to 195 * start or end of its scanning range and that is same as start or end 196 * container which are specified when the scanner is initialized. If it's 197 * result of scanning backward, this offset means the point of the found 198 * point. Otherwise, i.e., scanning forward, this offset means next point 199 * of the found point. E.g., if it reaches a collapsible white-space, this 200 * offset is at the first non-collapsible character after it. 201 */ 202 MOZ_NEVER_INLINE_DEBUG uint32_t Offset_Deprecated() const { 203 NS_ASSERTION(mOffset.isSome(), "Retrieved non-meaningful offset"); 204 return mOffset.valueOr(0); 205 } 206 207 /** 208 * Point_Deprecated() returns the position in found visible node or reached 209 * block boundary. So, this returns meaningful point only when 210 * Offset_Deprecated() returns meaningful value. 211 */ 212 template <typename EditorDOMPointType> 213 EditorDOMPointType Point_Deprecated() const { 214 NS_ASSERTION(mOffset.isSome(), "Retrieved non-meaningful point"); 215 return EditorDOMPointType(mContent, mOffset.valueOr(0)); 216 } 217 218 /** 219 * PointAtReachedContent() returns the position of found visible content or 220 * reached block element. 221 */ 222 template <typename EditorDOMPointType> 223 EditorDOMPointType PointAtReachedContent() const { 224 MOZ_ASSERT(mContent); 225 switch (mReason) { 226 case WSType::CollapsibleWhiteSpaces: 227 case WSType::NonCollapsibleCharacters: 228 case WSType::PreformattedLineBreak: 229 MOZ_DIAGNOSTIC_ASSERT(mOffset.isSome()); 230 return mDirection == ScanDirection::Forward 231 ? EditorDOMPointType(mContent, mOffset.valueOr(0)) 232 : EditorDOMPointType(mContent, 233 std::max(mOffset.valueOr(1), 1u) - 1); 234 default: 235 return EditorDOMPointType(mContent); 236 } 237 } 238 239 /** 240 * PointAfterReachedContent() returns the next position of found visible 241 * content or reached block element. 242 */ 243 template <typename EditorDOMPointType> 244 EditorDOMPointType PointAfterReachedContent() const { 245 MOZ_ASSERT(mContent); 246 return PointAtReachedContent<EditorDOMPointType>() 247 .template NextPointOrAfterContainer<EditorDOMPointType>(); 248 } 249 250 /** 251 * Return the next position of found visible content node. So, this should 252 * not be used if it reached a visible character middle of a `Text`. 253 */ 254 template <typename EditorDOMPointType> 255 EditorDOMPointType PointAfterReachedContentNode() const { 256 MOZ_ASSERT(mContent); 257 return EditorDOMPointType::After(*mContent); 258 } 259 260 /** 261 * The scanner reached <img> or something which is inline and is not a 262 * container. 263 */ 264 bool ReachedSpecialContent() const { 265 return mReason == WSType::SpecialContent; 266 } 267 268 /** 269 * The point is in visible characters or collapsible white-spaces. 270 */ 271 bool InVisibleOrCollapsibleCharacters() const { 272 return mReason == WSType::CollapsibleWhiteSpaces || 273 mReason == WSType::NonCollapsibleCharacters; 274 } 275 276 /** 277 * The point is in collapsible white-spaces. 278 */ 279 bool InCollapsibleWhiteSpaces() const { 280 return mReason == WSType::CollapsibleWhiteSpaces; 281 } 282 283 /** 284 * The point is in visible non-collapsible characters. 285 */ 286 bool InNonCollapsibleCharacters() const { 287 return mReason == WSType::NonCollapsibleCharacters; 288 } 289 290 /** 291 * The scanner reached a <br> element. 292 */ 293 bool ReachedBRElement() const { return mReason == WSType::BRElement; } 294 bool ReachedVisibleBRElement() const { 295 return ReachedBRElement() && 296 HTMLEditUtils::IsVisibleBRElement(*BRElementPtr()); 297 } 298 bool ReachedInvisibleBRElement() const { 299 return ReachedBRElement() && 300 HTMLEditUtils::IsInvisibleBRElement(*BRElementPtr()); 301 } 302 303 bool ReachedPreformattedLineBreak() const { 304 return mReason == WSType::PreformattedLineBreak; 305 } 306 307 /** 308 * Return true if reached a <br> element or a preformatted line break. 309 * Return false when reached a block boundary. Use ReachedLineBoundary() if 310 * you want it to return true in the case too. 311 */ 312 [[nodiscard]] bool ReachedLineBreak() const { 313 return ReachedBRElement() || ReachedPreformattedLineBreak(); 314 } 315 316 /** 317 * The scanner reached a <hr> element. 318 */ 319 bool ReachedHRElement() const { 320 return mContent && mContent->IsHTMLElement(nsGkAtoms::hr); 321 } 322 323 /** 324 * The scanner reached current block boundary or other block element. 325 */ 326 bool ReachedBlockBoundary() const { 327 return mReason == WSType::CurrentBlockBoundary || 328 mReason == WSType::OtherBlockBoundary; 329 } 330 331 /** 332 * The scanner reached current block element boundary. 333 */ 334 bool ReachedCurrentBlockBoundary() const { 335 return mReason == WSType::CurrentBlockBoundary; 336 } 337 338 /** 339 * The scanner reached other block element. 340 */ 341 bool ReachedOtherBlockElement() const { 342 return mReason == WSType::OtherBlockBoundary; 343 } 344 345 /** 346 * The scanner reached other block element that isn't editable 347 */ 348 bool ReachedNonEditableOtherBlockElement() const { 349 return ReachedOtherBlockElement() && !GetContent()->IsEditable(); 350 } 351 352 /** 353 * The scanner reached inline editing host boundary. 354 */ 355 [[nodiscard]] bool ReachedInlineEditingHostBoundary() const { 356 return mReason == WSType::InlineEditingHostBoundary; 357 } 358 359 /** 360 * The scanner reached something non-text node. 361 */ 362 bool ReachedSomethingNonTextContent() const { 363 return !InVisibleOrCollapsibleCharacters(); 364 } 365 366 [[nodiscard]] bool ReachedLineBoundary() const { 367 switch (mReason) { 368 case WSType::CurrentBlockBoundary: 369 case WSType::OtherBlockBoundary: 370 case WSType::BRElement: 371 case WSType::PreformattedLineBreak: 372 return true; 373 default: 374 return ReachedHRElement(); 375 } 376 } 377 378 friend std::ostream& operator<<(std::ostream& aStream, 379 const ScanDirection& aDirection) { 380 return aStream << (aDirection == ScanDirection::Backward 381 ? "ScanDirection::Backward" 382 : "ScanDirection::Forward"); 383 } 384 385 friend std::ostream& operator<<(std::ostream& aStream, 386 const WSScanResult& aResult) { 387 aStream << "{ mReason: " << aResult.mReason; 388 if (aResult.mReason == WSType::NotInitialized || 389 aResult.mReason == WSType::InUncomposedDoc) { 390 return aStream << " }"; 391 } 392 return aStream << ", mContent: " << aResult.mContent 393 << ", mOffset: " << aResult.mOffset 394 << ", mDirection: " << aResult.mDirection << " }"; 395 } 396 397 private: 398 nsCOMPtr<nsIContent> mContent; 399 Maybe<uint32_t> mOffset; 400 WSType mReason; 401 ScanDirection mDirection = ScanDirection::Backward; 402 }; 403 404 class MOZ_STACK_CLASS WSRunScanner final { 405 private: 406 using Element = dom::Element; 407 using HTMLBRElement = dom::HTMLBRElement; 408 using Text = dom::Text; 409 410 public: 411 using WSType = WSScanResult::WSType; 412 413 enum class IgnoreNonEditableNodes : bool { No, Yes }; 414 enum class StopAtNonEditableNode : bool { No, Yes }; 415 enum class ReferHTMLDefaultStyle : bool { No, Yes }; 416 enum class Option { 417 // If set, return only editable content or return non-editable content as a 418 // special content in the closest editing host if the scan start point is 419 // editable. 420 OnlyEditableNodes, 421 // If set, use the HTML default style to consider whether the found one is a 422 // block or an inline. 423 ReferHTMLDefaultStyle, 424 // If set, stop scanning the DOM when it reaches a `Comment` node. 425 StopAtComment, 426 }; 427 using Options = EnumSet<Option>; 428 429 [[nodiscard]] constexpr static IgnoreNonEditableNodes 430 ShouldIgnoreNonEditableSiblingsOrDescendants( 431 Options aOptions // NOLINT(performance-unnecessary-value-param) 432 ) { 433 return static_cast<IgnoreNonEditableNodes>( 434 aOptions.contains(Option::OnlyEditableNodes)); 435 } 436 [[nodiscard]] constexpr static StopAtNonEditableNode 437 ShouldStopAtNonEditableNode( 438 Options aOptions // NOLINT(performance-unnecessary-value-param) 439 ) { 440 return static_cast<StopAtNonEditableNode>( 441 aOptions.contains(Option::OnlyEditableNodes)); 442 } 443 444 [[nodiscard]] constexpr static ReferHTMLDefaultStyle 445 ShouldReferHTMLDefaultStyle( 446 Options aOptions // NOLINT(performance-unnecessary-value-param) 447 ) { 448 return static_cast<ReferHTMLDefaultStyle>( 449 aOptions.contains(Option::ReferHTMLDefaultStyle)); 450 } 451 452 template <typename EditorDOMPointType> 453 WSRunScanner(Options aOptions, // NOLINT(performance-unnecessary-value-param) 454 const EditorDOMPointType& aScanStartPoint, 455 const Element* aAncestorLimiter = nullptr) 456 : mScanStartPoint(aScanStartPoint.template To<EditorDOMPoint>()), 457 mTextFragmentDataAtStart(aOptions, mScanStartPoint, aAncestorLimiter) {} 458 459 // ScanInclusiveNextVisibleNodeOrBlockBoundaryFrom() returns the first visible 460 // node at or after aPoint. If there is no visible nodes after aPoint, 461 // returns topmost editable inline ancestor at end of current block. See 462 // comments around WSScanResult for the detail. When you reach a character, 463 // this returns WSScanResult both whose Point_Deprecated() and 464 // PointAtReachedContent() return the found character position. 465 template <typename PT, typename CT> 466 WSScanResult ScanInclusiveNextVisibleNodeOrBlockBoundaryFrom( 467 const EditorDOMPointBase<PT, CT>& aPoint) const; 468 template <typename PT, typename CT> 469 static WSScanResult ScanInclusiveNextVisibleNodeOrBlockBoundary( 470 Options aOptions, // NOLINT(performance-unnecessary-value-param) 471 const EditorDOMPointBase<PT, CT>& aPoint, 472 const Element* aAncestorLimiter = nullptr) { 473 return WSRunScanner(aOptions, aPoint, aAncestorLimiter) 474 .ScanInclusiveNextVisibleNodeOrBlockBoundaryFrom(aPoint); 475 } 476 477 // ScanPreviousVisibleNodeOrBlockBoundaryFrom() returns the first visible node 478 // before aPoint. If there is no visible nodes before aPoint, returns topmost 479 // editable inline ancestor at start of current block. See comments around 480 // WSScanResult for the detail. When you reach a character, this returns 481 // WSScanResult whose Point_Deprecated() returns next point of the found 482 // character and PointAtReachedContent() returns the point at found character. 483 template <typename PT, typename CT> 484 WSScanResult ScanPreviousVisibleNodeOrBlockBoundaryFrom( 485 const EditorDOMPointBase<PT, CT>& aPoint) const; 486 template <typename PT, typename CT> 487 static WSScanResult ScanPreviousVisibleNodeOrBlockBoundary( 488 Options aOptions, // NOLINT(performance-unnecessary-value-param) 489 const EditorDOMPointBase<PT, CT>& aPoint, 490 const Element* aAncestorLimiter = nullptr) { 491 return WSRunScanner(aOptions, aPoint, aAncestorLimiter) 492 .ScanPreviousVisibleNodeOrBlockBoundaryFrom(aPoint); 493 } 494 495 /** 496 * Return a point in a `Text` node which is at current character or next 497 * character if aPoint does not points a character or end of a `Text` node. 498 */ 499 template <typename EditorDOMPointType, typename PT, typename CT> 500 static EditorDOMPointType GetInclusiveNextCharPoint( 501 Options aOptions, // NOLINT(performance-unnecessary-value-param) 502 const EditorDOMPointBase<PT, CT>& aPoint, 503 const Element* aAncestorLimiter = nullptr) { 504 if (aPoint.IsInTextNode() && !aPoint.IsEndOfContainer() && 505 (!aOptions.contains(Option::OnlyEditableNodes) || 506 HTMLEditUtils::IsSimplyEditableNode( 507 *aPoint.template ContainerAs<Text>()))) { 508 return EditorDOMPointType(aPoint.template ContainerAs<Text>(), 509 aPoint.Offset()); 510 } 511 return WSRunScanner(aOptions, aPoint, aAncestorLimiter) 512 .GetInclusiveNextCharPoint<EditorDOMPointType>(aPoint); 513 } 514 515 /** 516 * Return a point in a `Text` node which is before aPoint. 517 */ 518 template <typename EditorDOMPointType, typename PT, typename CT> 519 static EditorDOMPointType GetPreviousCharPoint( 520 Options aOptions, // NOLINT(performance-unnecessary-value-param) 521 const EditorDOMPointBase<PT, CT>& aPoint, 522 const Element* aAncestorLimiter = nullptr) { 523 if (aPoint.IsInTextNode() && !aPoint.IsStartOfContainer() && 524 (!aOptions.contains(Option::OnlyEditableNodes) || 525 HTMLEditUtils::IsSimplyEditableNode( 526 *aPoint.template ContainerAs<Text>()))) { 527 return EditorDOMPointType(aPoint.template ContainerAs<Text>(), 528 aPoint.Offset() - 1); 529 } 530 return WSRunScanner(aOptions, aPoint, aAncestorLimiter) 531 .GetPreviousCharPoint<EditorDOMPointType>(aPoint); 532 } 533 534 /** 535 * Scan aTextNode from end or start to find last or first visible things. 536 * I.e., this returns a point immediately before or after invisible 537 * white-spaces of aTextNode if aTextNode ends or begins with some invisible 538 * white-spaces. 539 * Note that the result may not be in different text node if aTextNode has 540 * only invisible white-spaces and there is previous or next text node. 541 */ 542 template <typename EditorDOMPointType> 543 static EditorDOMPointType GetAfterLastVisiblePoint( 544 Options aOptions, // NOLINT(performance-unnecessary-value-param) 545 Text& aTextNode, const Element* aAncestorLimiter = nullptr); 546 template <typename EditorDOMPointType> 547 static EditorDOMPointType GetFirstVisiblePoint( 548 Options aOptions, // NOLINT(performance-unnecessary-value-param) 549 Text& aTextNode, const Element* aAncestorLimiter = nullptr); 550 551 /** 552 * GetRangeInTextNodesToForwardDeleteFrom() returns the range to remove 553 * text when caret is at aPoint. 554 */ 555 static Result<EditorDOMRangeInTexts, nsresult> 556 GetRangeInTextNodesToForwardDeleteFrom( 557 Options aOptions, // NOLINT(performance-unnecessary-value-param) 558 const EditorDOMPoint& aPoint, const Element* aAncestorLimiter = nullptr); 559 560 /** 561 * GetRangeInTextNodesToBackspaceFrom() returns the range to remove text 562 * when caret is at aPoint. 563 */ 564 static Result<EditorDOMRangeInTexts, nsresult> 565 GetRangeInTextNodesToBackspaceFrom( 566 Options aOptions, // NOLINT(performance-unnecessary-value-param) 567 const EditorDOMPoint& aPoint, const Element* aAncestorLimiter = nullptr); 568 569 /** 570 * GetRangesForDeletingAtomicContent() returns the range to delete 571 * aAtomicContent. If it's followed by invisible white-spaces, they will 572 * be included into the range. 573 */ 574 static EditorDOMRange GetRangesForDeletingAtomicContent( 575 Options aOptions, // NOLINT(performance-unnecessary-value-param) 576 const nsIContent& aAtomicContent, 577 const Element* aAncestorLimiter = nullptr); 578 579 /** 580 * GetRangeForDeleteBlockElementBoundaries() returns a range starting from end 581 * of aLeftBlockElement to start of aRightBlockElement and extend invisible 582 * white-spaces around them. 583 * 584 * @param aLeftBlockElement The block element which will be joined with 585 * aRightBlockElement. 586 * @param aRightBlockElement The block element which will be joined with 587 * aLeftBlockElement. This must be an element 588 * after aLeftBlockElement. 589 * @param aPointContainingTheOtherBlock 590 * When aRightBlockElement is an ancestor of 591 * aLeftBlockElement, this must be set and the 592 * container must be aRightBlockElement. 593 * When aLeftBlockElement is an ancestor of 594 * aRightBlockElement, this must be set and the 595 * container must be aLeftBlockElement. 596 * Otherwise, must not be set. 597 */ 598 static EditorDOMRange GetRangeForDeletingBlockElementBoundaries( 599 Options aOptions, // NOLINT(performance-unnecessary-value-param) 600 const Element& aLeftBlockElement, const Element& aRightBlockElement, 601 const EditorDOMPoint& aPointContainingTheOtherBlock, 602 const Element* aAncestorLimiter = nullptr); 603 604 /** 605 * ShrinkRangeIfStartsFromOrEndsAfterAtomicContent() may shrink aRange if it 606 * starts and/or ends with an atomic content, but the range boundary 607 * is in adjacent text nodes. Returns true if this modifies the range. 608 */ 609 static Result<bool, nsresult> ShrinkRangeIfStartsFromOrEndsAfterAtomicContent( 610 Options aOptions, // NOLINT(performance-unnecessary-value-param) 611 nsRange& aRange, const Element* aAncestorLimiter = nullptr); 612 613 /** 614 * GetRangeContainingInvisibleWhiteSpacesAtRangeBoundaries() returns 615 * extended range if range boundaries of aRange are in invisible white-spaces. 616 */ 617 static EditorDOMRange GetRangeContainingInvisibleWhiteSpacesAtRangeBoundaries( 618 Options aOptions, // NOLINT(performance-unnecessary-value-param) 619 const EditorDOMRange& aRange, const Element* aAncestorLimiter = nullptr); 620 621 /** 622 * GetPrecedingBRElementUnlessVisibleContentFound() scans a `<br>` element 623 * backward, but stops scanning it if the scanner finds visible character 624 * or something. In other words, this method ignores only invisible 625 * white-spaces between `<br>` element and aPoint. 626 */ 627 template <typename EditorDOMPointType> 628 MOZ_NEVER_INLINE_DEBUG static HTMLBRElement* 629 GetPrecedingBRElementUnlessVisibleContentFound( 630 Options aOptions, // NOLINT(performance-unnecessary-value-param) 631 const EditorDOMPointType& aPoint, 632 const Element* aAncestorLimiter = nullptr) { 633 MOZ_ASSERT(aPoint.IsSetAndValid()); 634 // XXX This method behaves differently even in similar point. 635 // If aPoint is in a text node following `<br>` element, reaches the 636 // `<br>` element when all characters between the `<br>` and 637 // aPoint are ASCII whitespaces. 638 // But if aPoint is not in a text node, e.g., at start of an inline 639 // element which is immediately after a `<br>` element, returns the 640 // `<br>` element even if there is no invisible white-spaces. 641 if (aPoint.IsStartOfContainer()) { 642 return nullptr; 643 } 644 // TODO: Scan for end boundary is redundant in this case, we should optimize 645 // it. 646 TextFragmentData textFragmentData(aOptions, aPoint, aAncestorLimiter); 647 return textFragmentData.StartsFromBRElement() 648 ? textFragmentData.StartReasonBRElementPtr() 649 : nullptr; 650 } 651 652 [[nodiscard]] constexpr Options ScanOptions() const { 653 return mTextFragmentDataAtStart.ScanOptions(); 654 } 655 [[nodiscard]] bool ReferredHTMLDefaultStyle() const { 656 return mTextFragmentDataAtStart.ReferredHTMLDefaultStyle(); 657 } 658 659 const EditorDOMPoint& ScanStartRef() const { return mScanStartPoint; } 660 661 protected: 662 using EditorType = EditorBase::EditorType; 663 664 class TextFragmentData; 665 666 // VisibleWhiteSpacesData represents 0 or more visible white-spaces. 667 class MOZ_STACK_CLASS VisibleWhiteSpacesData final { 668 public: 669 bool IsInitialized() const { 670 return mLeftWSType != WSType::NotInitialized || 671 mRightWSType != WSType::NotInitialized; 672 } 673 674 EditorDOMPoint StartRef() const { return mStartPoint; } 675 EditorDOMPoint EndRef() const { return mEndPoint; } 676 677 /** 678 * Information why the white-spaces start from (i.e., this indicates the 679 * previous content type of the fragment). 680 */ 681 bool StartsFromNonCollapsibleCharacters() const { 682 return mLeftWSType == WSType::NonCollapsibleCharacters; 683 } 684 bool StartsFromSpecialContent() const { 685 return mLeftWSType == WSType::SpecialContent; 686 } 687 bool StartsFromPreformattedLineBreak() const { 688 return mLeftWSType == WSType::PreformattedLineBreak; 689 } 690 691 /** 692 * Information why the white-spaces end by (i.e., this indicates the 693 * next content type of the fragment). 694 */ 695 bool EndsByNonCollapsibleCharacters() const { 696 return mRightWSType == WSType::NonCollapsibleCharacters; 697 } 698 bool EndsByTrailingWhiteSpaces() const { 699 return mRightWSType == WSType::TrailingWhiteSpaces; 700 } 701 bool EndsBySpecialContent() const { 702 return mRightWSType == WSType::SpecialContent; 703 } 704 bool EndsByBRElement() const { return mRightWSType == WSType::BRElement; } 705 bool EndsByPreformattedLineBreak() const { 706 return mRightWSType == WSType::PreformattedLineBreak; 707 } 708 bool EndsByBlockBoundary() const { 709 return mRightWSType == WSType::CurrentBlockBoundary || 710 mRightWSType == WSType::OtherBlockBoundary; 711 } 712 bool EndsByInlineEditingHostBoundary() const { 713 return mRightWSType == WSType::InlineEditingHostBoundary; 714 } 715 716 /** 717 * ComparePoint() compares aPoint with the white-spaces. 718 */ 719 enum class PointPosition { 720 BeforeStartOfFragment, 721 StartOfFragment, 722 MiddleOfFragment, 723 EndOfFragment, 724 AfterEndOfFragment, 725 NotInSameDOMTree, 726 }; 727 template <typename EditorDOMPointType> 728 PointPosition ComparePoint(const EditorDOMPointType& aPoint) const { 729 MOZ_ASSERT(aPoint.IsSetAndValid()); 730 if (StartRef() == aPoint) { 731 return PointPosition::StartOfFragment; 732 } 733 if (EndRef() == aPoint) { 734 return PointPosition::EndOfFragment; 735 } 736 const bool startIsBeforePoint = StartRef().IsBefore(aPoint); 737 const bool pointIsBeforeEnd = aPoint.IsBefore(EndRef()); 738 if (startIsBeforePoint && pointIsBeforeEnd) { 739 return PointPosition::MiddleOfFragment; 740 } 741 if (startIsBeforePoint) { 742 return PointPosition::AfterEndOfFragment; 743 } 744 if (pointIsBeforeEnd) { 745 return PointPosition::BeforeStartOfFragment; 746 } 747 return PointPosition::NotInSameDOMTree; 748 } 749 750 private: 751 // Initializers should be accessible only from `TextFragmentData`. 752 friend class WSRunScanner::TextFragmentData; 753 VisibleWhiteSpacesData() 754 : mLeftWSType(WSType::NotInitialized), 755 mRightWSType(WSType::NotInitialized) {} 756 757 template <typename EditorDOMPointType> 758 void SetStartPoint(const EditorDOMPointType& aStartPoint) { 759 mStartPoint = aStartPoint; 760 } 761 template <typename EditorDOMPointType> 762 void SetEndPoint(const EditorDOMPointType& aEndPoint) { 763 mEndPoint = aEndPoint; 764 } 765 void SetStartFrom(WSType aLeftWSType) { mLeftWSType = aLeftWSType; } 766 void SetStartFromLeadingWhiteSpaces() { 767 mLeftWSType = WSType::LeadingWhiteSpaces; 768 } 769 void SetEndBy(WSType aRightWSType) { mRightWSType = aRightWSType; } 770 void SetEndByTrailingWhiteSpaces() { 771 mRightWSType = WSType::TrailingWhiteSpaces; 772 } 773 774 EditorDOMPoint mStartPoint; 775 EditorDOMPoint mEndPoint; 776 WSType mLeftWSType, mRightWSType; 777 }; 778 779 using PointPosition = VisibleWhiteSpacesData::PointPosition; 780 781 /** 782 * Return aPoint if it points a character in a `Text` node, or start of next 783 * `Text` node otherwise. 784 * FYI: For the performance, this does not check whether given container is 785 * not after mStart.mReasonContent or not. 786 */ 787 template <typename EditorDOMPointType, typename PT, typename CT> 788 EditorDOMPointType GetInclusiveNextCharPoint( 789 const EditorDOMPointBase<PT, CT>& aPoint) const { 790 return TextFragmentDataAtStartRef() 791 .GetInclusiveNextCharPoint<EditorDOMPointType>( 792 aPoint, ShouldIgnoreNonEditableSiblingsOrDescendants( 793 mTextFragmentDataAtStart.ScanOptions())); 794 } 795 796 /** 797 * Return the previous editable point in a `Text` node. Note that this 798 * returns the last character point when it meets non-empty text node, 799 * otherwise, returns a point in an empty text node. 800 * FYI: For the performance, this does not check whether given container is 801 * not before mEnd.mReasonContent or not. 802 */ 803 template <typename EditorDOMPointType, typename PT, typename CT> 804 EditorDOMPointType GetPreviousCharPoint( 805 const EditorDOMPointBase<PT, CT>& aPoint) const { 806 return TextFragmentDataAtStartRef() 807 .GetPreviousCharPoint<EditorDOMPointType>( 808 aPoint, ShouldIgnoreNonEditableSiblingsOrDescendants( 809 mTextFragmentDataAtStart.ScanOptions())); 810 } 811 812 /** 813 * GetEndOfCollapsibleASCIIWhiteSpaces() returns the next visible char 814 * (meaning a character except ASCII white-spaces) point or end of last text 815 * node scanning from aPointAtASCIIWhiteSpace. 816 * Note that this may return different text node from the container of 817 * aPointAtASCIIWhiteSpace. 818 */ 819 template <typename EditorDOMPointType> 820 EditorDOMPointType GetEndOfCollapsibleASCIIWhiteSpaces( 821 const EditorDOMPointInText& aPointAtASCIIWhiteSpace, 822 nsIEditor::EDirection aDirectionToDelete) const { 823 MOZ_ASSERT(aDirectionToDelete == nsIEditor::eNone || 824 aDirectionToDelete == nsIEditor::eNext || 825 aDirectionToDelete == nsIEditor::ePrevious); 826 return TextFragmentDataAtStartRef() 827 .GetEndOfCollapsibleASCIIWhiteSpaces<EditorDOMPointType>( 828 aPointAtASCIIWhiteSpace, aDirectionToDelete); 829 } 830 831 /** 832 * GetFirstASCIIWhiteSpacePointCollapsedTo() returns the first ASCII 833 * white-space which aPointAtASCIIWhiteSpace belongs to. In other words, 834 * the white-space at aPointAtASCIIWhiteSpace should be collapsed into 835 * the result. 836 * Note that this may return different text node from the container of 837 * aPointAtASCIIWhiteSpace. 838 */ 839 template <typename EditorDOMPointType> 840 EditorDOMPointType GetFirstASCIIWhiteSpacePointCollapsedTo( 841 const EditorDOMPointInText& aPointAtASCIIWhiteSpace, 842 nsIEditor::EDirection aDirectionToDelete) const { 843 MOZ_ASSERT(aDirectionToDelete == nsIEditor::eNone || 844 aDirectionToDelete == nsIEditor::eNext || 845 aDirectionToDelete == nsIEditor::ePrevious); 846 return TextFragmentDataAtStartRef() 847 .GetFirstASCIIWhiteSpacePointCollapsedTo<EditorDOMPointType>( 848 aPointAtASCIIWhiteSpace, aDirectionToDelete); 849 } 850 851 /** 852 * TextFragmentData stores the information of white-space sequence which 853 * contains `aPoint` of the constructor. 854 */ 855 class MOZ_STACK_CLASS TextFragmentData final { 856 private: 857 class NoBreakingSpaceData; 858 class MOZ_STACK_CLASS BoundaryData final { 859 public: 860 using NoBreakingSpaceData = 861 WSRunScanner::TextFragmentData::NoBreakingSpaceData; 862 863 /** 864 * ScanCollapsibleWhiteSpaceStartFrom() returns start boundary data of 865 * white-spaces containing aPoint. When aPoint is in a text node and 866 * points a non-white-space character or the text node is preformatted, 867 * this returns the data at aPoint. 868 * 869 * @param aPoint Scan start point. 870 * @param aNBSPData Optional. If set, this recodes first and last 871 * NBSP positions. 872 */ 873 template <typename EditorDOMPointType> 874 static BoundaryData ScanCollapsibleWhiteSpaceStartFrom( 875 Options aOptions, // NOLINT(performance-unnecessary-value-param) 876 const EditorDOMPointType& aPoint, NoBreakingSpaceData* aNBSPData, 877 const Element& aAncestorLimiter); 878 879 /** 880 * ScanCollapsibleWhiteSpaceEndFrom() returns end boundary data of 881 * white-spaces containing aPoint. When aPoint is in a text node and 882 * points a non-white-space character or the text node is preformatted, 883 * this returns the data at aPoint. 884 * 885 * @param aPoint Scan start point. 886 * @param aNBSPData Optional. If set, this recodes first and last 887 * NBSP positions. 888 */ 889 template <typename EditorDOMPointType> 890 static BoundaryData ScanCollapsibleWhiteSpaceEndFrom( 891 Options aOptions, // NOLINT(performance-unnecessary-value-param) 892 const EditorDOMPointType& aPoint, NoBreakingSpaceData* aNBSPData, 893 const Element& aAncestorLimiter); 894 895 BoundaryData() = default; 896 template <typename EditorDOMPointType> 897 BoundaryData(const EditorDOMPointType& aPoint, nsIContent& aReasonContent, 898 WSType aReason) 899 : mReasonContent(&aReasonContent), 900 mPoint(aPoint.template To<EditorDOMPoint>()), 901 mReason(aReason) {} 902 bool Initialized() const { return mReasonContent && mPoint.IsSet(); } 903 904 nsIContent* GetReasonContent() const { return mReasonContent; } 905 const EditorDOMPoint& PointRef() const { return mPoint; } 906 WSType RawReason() const { return mReason; } 907 908 bool IsNonCollapsibleCharacters() const { 909 return mReason == WSType::NonCollapsibleCharacters; 910 } 911 bool IsSpecialContent() const { 912 return mReason == WSType::SpecialContent; 913 } 914 bool IsBRElement() const { return mReason == WSType::BRElement; } 915 bool IsPreformattedLineBreak() const { 916 return mReason == WSType::PreformattedLineBreak; 917 } 918 bool IsCurrentBlockBoundary() const { 919 return mReason == WSType::CurrentBlockBoundary; 920 } 921 bool IsOtherBlockBoundary() const { 922 return mReason == WSType::OtherBlockBoundary; 923 } 924 bool IsBlockBoundary() const { 925 return mReason == WSType::CurrentBlockBoundary || 926 mReason == WSType::OtherBlockBoundary; 927 } 928 bool IsInlineEditingHostBoundary() const { 929 return mReason == WSType::InlineEditingHostBoundary; 930 } 931 bool IsHardLineBreak() const { 932 return mReason == WSType::CurrentBlockBoundary || 933 mReason == WSType::OtherBlockBoundary || 934 mReason == WSType::BRElement || 935 mReason == WSType::PreformattedLineBreak; 936 } 937 MOZ_NEVER_INLINE_DEBUG Element* OtherBlockElementPtr() const { 938 MOZ_DIAGNOSTIC_ASSERT(mReasonContent->IsElement()); 939 return mReasonContent->AsElement(); 940 } 941 MOZ_NEVER_INLINE_DEBUG HTMLBRElement* BRElementPtr() const { 942 MOZ_DIAGNOSTIC_ASSERT(mReasonContent->IsHTMLElement(nsGkAtoms::br)); 943 return static_cast<HTMLBRElement*>(mReasonContent.get()); 944 } 945 946 private: 947 /** 948 * Helper methods of ScanCollapsibleWhiteSpaceStartFrom() and 949 * ScanCollapsibleWhiteSpaceEndFrom() when they need to scan in a text 950 * node. 951 */ 952 template <typename EditorDOMPointType> 953 static Maybe<BoundaryData> ScanCollapsibleWhiteSpaceStartInTextNode( 954 const EditorDOMPointType& aPoint, NoBreakingSpaceData* aNBSPData); 955 template <typename EditorDOMPointType> 956 static Maybe<BoundaryData> ScanCollapsibleWhiteSpaceEndInTextNode( 957 const EditorDOMPointType& aPoint, NoBreakingSpaceData* aNBSPData); 958 959 nsCOMPtr<nsIContent> mReasonContent; 960 EditorDOMPoint mPoint; 961 // Must be one of WSType::NotInitialized, 962 // WSType::NonCollapsibleCharacters, WSType::SpecialContent, 963 // WSType::BRElement, WSType::CurrentBlockBoundary, 964 // WSType::OtherBlockBoundary or WSType::InlineEditingHostBoundary. 965 WSType mReason = WSType::NotInitialized; 966 }; 967 968 class MOZ_STACK_CLASS NoBreakingSpaceData final { 969 public: 970 enum class Scanning { Forward, Backward }; 971 void NotifyNBSP(const EditorDOMPointInText& aPoint, 972 Scanning aScanningDirection) { 973 MOZ_ASSERT(aPoint.IsSetAndValid()); 974 MOZ_ASSERT(aPoint.IsCharNBSP()); 975 if (!mFirst.IsSet() || aScanningDirection == Scanning::Backward) { 976 mFirst = aPoint; 977 } 978 if (!mLast.IsSet() || aScanningDirection == Scanning::Forward) { 979 mLast = aPoint; 980 } 981 } 982 983 const EditorDOMPointInText& FirstPointRef() const { return mFirst; } 984 const EditorDOMPointInText& LastPointRef() const { return mLast; } 985 986 bool FoundNBSP() const { 987 MOZ_ASSERT(mFirst.IsSet() == mLast.IsSet()); 988 return mFirst.IsSet(); 989 } 990 991 private: 992 EditorDOMPointInText mFirst; 993 EditorDOMPointInText mLast; 994 }; 995 996 public: 997 TextFragmentData() = delete; 998 999 /** 1000 * If aScanMode is Scan::EditableNodes and aPoint is in an editable node, 1001 * this scans only in the editing host. Therefore, it's same as that 1002 * aAncestorLimiter is specified to the editing host. 1003 */ 1004 template <typename EditorDOMPointType> 1005 TextFragmentData( 1006 Options aOptions, // NOLINT(performance-unnecessary-value-param) 1007 const EditorDOMPointType& aPoint, 1008 const Element* aAncestorLimiter = nullptr); 1009 1010 bool IsInitialized() const { 1011 return mStart.Initialized() && mEnd.Initialized(); 1012 } 1013 1014 [[nodiscard]] constexpr Options ScanOptions() const { return mOptions; } 1015 [[nodiscard]] bool ReferredHTMLDefaultStyle() const { 1016 return mOptions.contains(Option::ReferHTMLDefaultStyle); 1017 } 1018 1019 const Element* GetAncestorLimiter() const { return mAncestorLimiter; } 1020 1021 nsIContent* GetStartReasonContent() const { 1022 return mStart.GetReasonContent(); 1023 } 1024 nsIContent* GetEndReasonContent() const { return mEnd.GetReasonContent(); } 1025 1026 bool StartsFromNonCollapsibleCharacters() const { 1027 return mStart.IsNonCollapsibleCharacters(); 1028 } 1029 bool StartsFromSpecialContent() const { return mStart.IsSpecialContent(); } 1030 bool StartsFromBRElement() const { return mStart.IsBRElement(); } 1031 bool StartsFromVisibleBRElement() const { 1032 return StartsFromBRElement() && 1033 HTMLEditUtils::IsVisibleBRElement(*GetStartReasonContent()); 1034 } 1035 bool StartsFromInvisibleBRElement() const { 1036 return StartsFromBRElement() && 1037 HTMLEditUtils::IsInvisibleBRElement(*GetStartReasonContent()); 1038 } 1039 bool StartsFromPreformattedLineBreak() const { 1040 return mStart.IsPreformattedLineBreak(); 1041 } 1042 bool StartsFromCurrentBlockBoundary() const { 1043 return mStart.IsCurrentBlockBoundary(); 1044 } 1045 bool StartsFromOtherBlockElement() const { 1046 return mStart.IsOtherBlockBoundary(); 1047 } 1048 bool StartsFromBlockBoundary() const { return mStart.IsBlockBoundary(); } 1049 bool StartsFromInlineEditingHostBoundary() const { 1050 return mStart.IsInlineEditingHostBoundary(); 1051 } 1052 bool StartsFromHardLineBreak() const { return mStart.IsHardLineBreak(); } 1053 bool EndsByNonCollapsibleCharacters() const { 1054 return mEnd.IsNonCollapsibleCharacters(); 1055 } 1056 bool EndsBySpecialContent() const { return mEnd.IsSpecialContent(); } 1057 bool EndsByBRElement() const { return mEnd.IsBRElement(); } 1058 bool EndsByVisibleBRElement() const { 1059 return EndsByBRElement() && 1060 HTMLEditUtils::IsVisibleBRElement(*GetEndReasonContent()); 1061 } 1062 bool EndsByInvisibleBRElement() const { 1063 return EndsByBRElement() && 1064 HTMLEditUtils::IsInvisibleBRElement(*GetEndReasonContent()); 1065 } 1066 bool EndsByPreformattedLineBreak() const { 1067 return mEnd.IsPreformattedLineBreak(); 1068 } 1069 bool EndsByInvisiblePreformattedLineBreak() const { 1070 return mEnd.IsPreformattedLineBreak() && 1071 HTMLEditUtils::IsInvisiblePreformattedNewLine(mEnd.PointRef()); 1072 } 1073 bool EndsByCurrentBlockBoundary() const { 1074 return mEnd.IsCurrentBlockBoundary(); 1075 } 1076 bool EndsByOtherBlockElement() const { return mEnd.IsOtherBlockBoundary(); } 1077 bool EndsByBlockBoundary() const { return mEnd.IsBlockBoundary(); } 1078 bool EndsByInlineEditingHostBoundary() const { 1079 return mEnd.IsInlineEditingHostBoundary(); 1080 } 1081 1082 WSType StartRawReason() const { return mStart.RawReason(); } 1083 WSType EndRawReason() const { return mEnd.RawReason(); } 1084 1085 MOZ_NEVER_INLINE_DEBUG Element* StartReasonOtherBlockElementPtr() const { 1086 return mStart.OtherBlockElementPtr(); 1087 } 1088 MOZ_NEVER_INLINE_DEBUG HTMLBRElement* StartReasonBRElementPtr() const { 1089 return mStart.BRElementPtr(); 1090 } 1091 MOZ_NEVER_INLINE_DEBUG Element* EndReasonOtherBlockElementPtr() const { 1092 return mEnd.OtherBlockElementPtr(); 1093 } 1094 MOZ_NEVER_INLINE_DEBUG HTMLBRElement* EndReasonBRElementPtr() const { 1095 return mEnd.BRElementPtr(); 1096 } 1097 1098 const EditorDOMPoint& StartRef() const { return mStart.PointRef(); } 1099 const EditorDOMPoint& EndRef() const { return mEnd.PointRef(); } 1100 1101 const EditorDOMPoint& ScanStartRef() const { return mScanStartPoint; } 1102 1103 bool FoundNoBreakingWhiteSpaces() const { return mNBSPData.FoundNBSP(); } 1104 const EditorDOMPointInText& FirstNBSPPointRef() const { 1105 return mNBSPData.FirstPointRef(); 1106 } 1107 const EditorDOMPointInText& LastNBSPPointRef() const { 1108 return mNBSPData.LastPointRef(); 1109 } 1110 1111 /** 1112 * Return inclusive next point in inclusive next `Text` node from aPoint. 1113 * So, it may be in a collapsed white-space or invisible white-spaces. 1114 * NOTE: Option::OnlyEditableNodes is ignored because it's treated as "stop 1115 * at non-editable content" in the other places, but this "ignores" them. 1116 */ 1117 template <typename EditorDOMPointType, typename PT, typename CT> 1118 [[nodiscard]] static EditorDOMPointType GetInclusiveNextCharPoint( 1119 const EditorDOMPointBase<PT, CT>& aPoint, 1120 Options aOptions, // NOLINT(performance-unnecessary-value-param) 1121 IgnoreNonEditableNodes aIgnoreNonEditableNodes, 1122 const nsIContent* aFollowingLimiterContent = nullptr); 1123 1124 template <typename EditorDOMPointType, typename PT, typename CT> 1125 [[nodiscard]] EditorDOMPointType GetInclusiveNextCharPoint( 1126 const EditorDOMPointBase<PT, CT>& aPoint, 1127 IgnoreNonEditableNodes aIgnoreNonEditableNodes) const { 1128 return GetInclusiveNextCharPoint<EditorDOMPointType>( 1129 aPoint, mOptions, aIgnoreNonEditableNodes, GetEndReasonContent()); 1130 } 1131 1132 /** 1133 * Return previous point in inclusive previous `Text` node from aPoint. 1134 * So, it may be in a collapsed white-space or invisible white-spaces. 1135 * NOTE: Option::OnlyEditableNodes is ignored because it's treated as "stop 1136 * at non-editable content" in the other places, but this "ignores" them. 1137 */ 1138 template <typename EditorDOMPointType, typename PT, typename CT> 1139 [[nodiscard]] static EditorDOMPointType GetPreviousCharPoint( 1140 const EditorDOMPointBase<PT, CT>& aPoint, 1141 Options aOptions, // NOLINT(performance-unnecessary-value-param) 1142 IgnoreNonEditableNodes aIgnoreNonEditableNodes, 1143 const nsIContent* aPrecedingLimiterContent = nullptr); 1144 1145 template <typename EditorDOMPointType, typename PT, typename CT> 1146 [[nodiscard]] EditorDOMPointType GetPreviousCharPoint( 1147 const EditorDOMPointBase<PT, CT>& aPoint, 1148 IgnoreNonEditableNodes aIgnoreNonEditableNodes) const { 1149 return GetPreviousCharPoint<EditorDOMPointType>( 1150 aPoint, mOptions, aIgnoreNonEditableNodes, GetStartReasonContent()); 1151 } 1152 1153 /** 1154 * Return end of current collapsible ASCII white-spaces. 1155 * NOTE: Option::OnlyEditableNodes is ignored because it's treated as "stop 1156 * at non-editable content" in the other places, but this "ignores" them. 1157 * 1158 * @param aPointAtASCIIWhiteSpace Must be in a sequence of collapsible 1159 * ASCII white-spaces. 1160 * @param aDirectionToDelete The direction to delete. 1161 */ 1162 template <typename EditorDOMPointType> 1163 [[nodiscard]] static EditorDOMPointType GetEndOfCollapsibleASCIIWhiteSpaces( 1164 const EditorDOMPointInText& aPointAtASCIIWhiteSpace, 1165 nsIEditor::EDirection aDirectionToDelete, 1166 Options aOptions, // NOLINT(performance-unnecessary-value-param) 1167 IgnoreNonEditableNodes aIgnoreNonEditableNodes, 1168 const nsIContent* aFollowingLimiterContent = nullptr); 1169 1170 template <typename EditorDOMPointType> 1171 [[nodiscard]] EditorDOMPointType GetEndOfCollapsibleASCIIWhiteSpaces( 1172 const EditorDOMPointInText& aPointAtASCIIWhiteSpace, 1173 nsIEditor::EDirection aDirectionToDelete, 1174 IgnoreNonEditableNodes aIgnoreNonEditableNodes) const { 1175 return GetEndOfCollapsibleASCIIWhiteSpaces<EditorDOMPointType>( 1176 aPointAtASCIIWhiteSpace, aDirectionToDelete, mOptions, 1177 aIgnoreNonEditableNodes, GetEndReasonContent()); 1178 } 1179 1180 /** 1181 * Return start of current collapsible ASCII white-spaces. 1182 * NOTE: Option::OnlyEditableNodes is ignored because it's treated as "stop 1183 * at non-editable content" in the other places, but this "ignores" them. 1184 * 1185 * @param aPointAtASCIIWhiteSpace Must be in a sequence of collapsible 1186 * ASCII white-spaces. 1187 * @param aDirectionToDelete The direction to delete. 1188 */ 1189 template <typename EditorDOMPointType> 1190 [[nodiscard]] static EditorDOMPointType 1191 GetFirstASCIIWhiteSpacePointCollapsedTo( 1192 const EditorDOMPointInText& aPointAtASCIIWhiteSpace, 1193 nsIEditor::EDirection aDirectionToDelete, 1194 Options aOptions, // NOLINT(performance-unnecessary-value-param) 1195 IgnoreNonEditableNodes aIgnoreNonEditableNodes, 1196 const nsIContent* aPrecedingLimiterContent = nullptr); 1197 1198 template <typename EditorDOMPointType> 1199 [[nodiscard]] EditorDOMPointType GetFirstASCIIWhiteSpacePointCollapsedTo( 1200 const EditorDOMPointInText& aPointAtASCIIWhiteSpace, 1201 nsIEditor::EDirection aDirectionToDelete, 1202 IgnoreNonEditableNodes aIgnoreNonEditableNodes) const { 1203 return GetFirstASCIIWhiteSpacePointCollapsedTo<EditorDOMPointType>( 1204 aPointAtASCIIWhiteSpace, aDirectionToDelete, mOptions, 1205 aIgnoreNonEditableNodes, GetStartReasonContent()); 1206 } 1207 1208 /** 1209 * GetNonCollapsedRangeInTexts() returns non-empty range in texts which 1210 * is the largest range in aRange if there is some text nodes. 1211 */ 1212 EditorDOMRangeInTexts GetNonCollapsedRangeInTexts( 1213 const EditorDOMRange& aRange) const; 1214 1215 /** 1216 * InvisibleLeadingWhiteSpaceRangeRef() retruns reference to two DOM points, 1217 * start of the line and first visible point or end of the hard line. When 1218 * this returns non-positioned range or positioned but collapsed range, 1219 * there is no invisible leading white-spaces. 1220 * Note that if there are only invisible white-spaces in a hard line, 1221 * this returns all of the white-spaces. 1222 */ 1223 const EditorDOMRange& InvisibleLeadingWhiteSpaceRangeRef() const; 1224 1225 /** 1226 * InvisibleTrailingWhiteSpaceRangeRef() returns reference to two DOM 1227 * points, first invisible white-space and end of the hard line. When this 1228 * returns non-positioned range or positioned but collapsed range, 1229 * there is no invisible trailing white-spaces. 1230 * Note that if there are only invisible white-spaces in a hard line, 1231 * this returns all of the white-spaces. 1232 */ 1233 const EditorDOMRange& InvisibleTrailingWhiteSpaceRangeRef() const; 1234 1235 /** 1236 * GetNewInvisibleLeadingWhiteSpaceRangeIfSplittingAt() returns new 1237 * invisible leading white-space range which should be removed if 1238 * splitting invisible white-space sequence at aPointToSplit creates 1239 * new invisible leading white-spaces in the new line. 1240 * Note that the result may be collapsed range if the point is around 1241 * invisible white-spaces. 1242 */ 1243 template <typename EditorDOMPointType> 1244 EditorDOMRange GetNewInvisibleLeadingWhiteSpaceRangeIfSplittingAt( 1245 const EditorDOMPointType& aPointToSplit) const { 1246 // If there are invisible trailing white-spaces and some or all of them 1247 // become invisible leading white-spaces in the new line, although we 1248 // don't need to delete them, but for aesthetically and backward 1249 // compatibility, we should remove them. 1250 const EditorDOMRange& trailingWhiteSpaceRange = 1251 InvisibleTrailingWhiteSpaceRangeRef(); 1252 // XXX Why don't we check leading white-spaces too? 1253 if (!trailingWhiteSpaceRange.IsPositioned()) { 1254 return trailingWhiteSpaceRange; 1255 } 1256 // If the point is before the trailing white-spaces, the new line won't 1257 // start with leading white-spaces. 1258 if (aPointToSplit.IsBefore(trailingWhiteSpaceRange.StartRef())) { 1259 return EditorDOMRange(); 1260 } 1261 // If the point is in the trailing white-spaces, the new line may 1262 // start with some leading white-spaces. Returning collapsed range 1263 // is intentional because the caller may want to know whether the 1264 // point is in trailing white-spaces or not. 1265 if (aPointToSplit.EqualsOrIsBefore(trailingWhiteSpaceRange.EndRef())) { 1266 return EditorDOMRange(trailingWhiteSpaceRange.StartRef(), 1267 aPointToSplit); 1268 } 1269 // Otherwise, if the point is after the trailing white-spaces, it may 1270 // be just outside of the text node. E.g., end of parent element. 1271 // This is possible case but the validation cost is not worthwhile 1272 // due to the runtime cost in the worst case. Therefore, we should just 1273 // return collapsed range at the end of trailing white-spaces. Then, 1274 // callers can know the point is immediately after the trailing 1275 // white-spaces. 1276 return EditorDOMRange(trailingWhiteSpaceRange.EndRef()); 1277 } 1278 1279 /** 1280 * GetNewInvisibleTrailingWhiteSpaceRangeIfSplittingAt() returns new 1281 * invisible trailing white-space range which should be removed if 1282 * splitting invisible white-space sequence at aPointToSplit creates 1283 * new invisible trailing white-spaces in the new line. 1284 * Note that the result may be collapsed range if the point is around 1285 * invisible white-spaces. 1286 */ 1287 template <typename EditorDOMPointType> 1288 EditorDOMRange GetNewInvisibleTrailingWhiteSpaceRangeIfSplittingAt( 1289 const EditorDOMPointType& aPointToSplit) const { 1290 // If there are invisible leading white-spaces and some or all of them 1291 // become end of current line, they will become visible. Therefore, we 1292 // need to delete the invisible leading white-spaces before insertion 1293 // point. 1294 const EditorDOMRange& leadingWhiteSpaceRange = 1295 InvisibleLeadingWhiteSpaceRangeRef(); 1296 if (!leadingWhiteSpaceRange.IsPositioned()) { 1297 return leadingWhiteSpaceRange; 1298 } 1299 // If the point equals or is after the leading white-spaces, the line 1300 // will end without trailing white-spaces. 1301 if (leadingWhiteSpaceRange.EndRef().IsBefore(aPointToSplit)) { 1302 return EditorDOMRange(); 1303 } 1304 // If the point is in the leading white-spaces, the line may 1305 // end with some trailing white-spaces. Returning collapsed range 1306 // is intentional because the caller may want to know whether the 1307 // point is in leading white-spaces or not. 1308 if (leadingWhiteSpaceRange.StartRef().EqualsOrIsBefore(aPointToSplit)) { 1309 return EditorDOMRange(aPointToSplit, leadingWhiteSpaceRange.EndRef()); 1310 } 1311 // Otherwise, if the point is before the leading white-spaces, it may 1312 // be just outside of the text node. E.g., start of parent element. 1313 // This is possible case but the validation cost is not worthwhile 1314 // due to the runtime cost in the worst case. Therefore, we should 1315 // just return collapsed range at start of the leading white-spaces. 1316 // Then, callers can know the point is immediately before the leading 1317 // white-spaces. 1318 return EditorDOMRange(leadingWhiteSpaceRange.StartRef()); 1319 } 1320 1321 /** 1322 * FollowingContentMayBecomeFirstVisibleContent() returns true if some 1323 * content may be first visible content after removing content after aPoint. 1324 * Note that it's completely broken what this does. Don't use this method 1325 * with new code. 1326 */ 1327 template <typename EditorDOMPointType> 1328 bool FollowingContentMayBecomeFirstVisibleContent( 1329 const EditorDOMPointType& aPoint) const { 1330 MOZ_ASSERT(aPoint.IsSetAndValid()); 1331 if (!mStart.IsHardLineBreak() && !mStart.IsInlineEditingHostBoundary()) { 1332 return false; 1333 } 1334 // If the point is before start of text fragment, that means that the 1335 // point may be at the block boundary or inline element boundary. 1336 if (aPoint.EqualsOrIsBefore(mStart.PointRef())) { 1337 return true; 1338 } 1339 // VisibleWhiteSpacesData is marked as start of line only when it 1340 // represents leading white-spaces. 1341 const EditorDOMRange& leadingWhiteSpaceRange = 1342 InvisibleLeadingWhiteSpaceRangeRef(); 1343 if (!leadingWhiteSpaceRange.StartRef().IsSet()) { 1344 return false; 1345 } 1346 if (aPoint.EqualsOrIsBefore(leadingWhiteSpaceRange.StartRef())) { 1347 return true; 1348 } 1349 if (!leadingWhiteSpaceRange.EndRef().IsSet()) { 1350 return false; 1351 } 1352 return aPoint.EqualsOrIsBefore(leadingWhiteSpaceRange.EndRef()); 1353 } 1354 1355 /** 1356 * PrecedingContentMayBecomeInvisible() returns true if end of preceding 1357 * content is collapsed (when ends with an ASCII white-space). 1358 * Note that it's completely broken what this does. Don't use this method 1359 * with new code. 1360 */ 1361 template <typename EditorDOMPointType> 1362 bool PrecedingContentMayBecomeInvisible( 1363 const EditorDOMPointType& aPoint) const { 1364 MOZ_ASSERT(aPoint.IsSetAndValid()); 1365 // If this fragment is ends by block boundary, always the caller needs 1366 // additional check. 1367 if (mEnd.IsBlockBoundary() || mEnd.IsInlineEditingHostBoundary()) { 1368 return true; 1369 } 1370 1371 // If the point is in visible white-spaces and ends with an ASCII 1372 // white-space, it may be collapsed even if it won't be end of line. 1373 const VisibleWhiteSpacesData& visibleWhiteSpaces = 1374 VisibleWhiteSpacesDataRef(); 1375 if (!visibleWhiteSpaces.IsInitialized()) { 1376 return false; 1377 } 1378 // XXX Odd case, but keep traditional behavior of `FindNearestRun()`. 1379 if (!visibleWhiteSpaces.StartRef().IsSet()) { 1380 return true; 1381 } 1382 if (!visibleWhiteSpaces.StartRef().EqualsOrIsBefore(aPoint)) { 1383 return false; 1384 } 1385 // XXX Odd case, but keep traditional behavior of `FindNearestRun()`. 1386 if (visibleWhiteSpaces.EndsByTrailingWhiteSpaces()) { 1387 return true; 1388 } 1389 // XXX Must be a bug. This claims that the caller needs additional 1390 // check even when there is no white-spaces. 1391 if (visibleWhiteSpaces.StartRef() == visibleWhiteSpaces.EndRef()) { 1392 return true; 1393 } 1394 return aPoint.IsBefore(visibleWhiteSpaces.EndRef()); 1395 } 1396 1397 /** 1398 * GetPreviousNBSPPointIfNeedToReplaceWithASCIIWhiteSpace() may return an 1399 * NBSP point which should be replaced with an ASCII white-space when we're 1400 * inserting text into aPointToInsert. Note that this is a helper method for 1401 * the traditional white-space normalizer. Don't use this with the new 1402 * white-space normalizer. 1403 * Must be called only when VisibleWhiteSpacesDataRef() returns initialized 1404 * instance and previous character of aPointToInsert is in the range. 1405 */ 1406 EditorDOMPointInText GetPreviousNBSPPointIfNeedToReplaceWithASCIIWhiteSpace( 1407 const EditorDOMPoint& aPointToInsert) const; 1408 1409 /** 1410 * GetInclusiveNextNBSPPointIfNeedToReplaceWithASCIIWhiteSpace() may return 1411 * an NBSP point which should be replaced with an ASCII white-space when 1412 * the caller inserts text into aPointToInsert. 1413 * Note that this is a helper method for the traditional white-space 1414 * normalizer. Don't use this with the new white-space normalizer. 1415 * Must be called only when VisibleWhiteSpacesDataRef() returns initialized 1416 * instance, and inclusive next char of aPointToInsert is in the range. 1417 */ 1418 EditorDOMPointInText 1419 GetInclusiveNextNBSPPointIfNeedToReplaceWithASCIIWhiteSpace( 1420 const EditorDOMPoint& aPointToInsert) const; 1421 1422 /** 1423 * GetReplaceRangeDataAtEndOfDeletionRange() and 1424 * GetReplaceRangeDataAtStartOfDeletionRange() return delete range if 1425 * end or start of deleting range splits invisible trailing/leading 1426 * white-spaces and it may become visible, or return replace range if 1427 * end or start of deleting range splits visible white-spaces and it 1428 * causes some ASCII white-spaces become invisible unless replacing 1429 * with an NBSP. 1430 */ 1431 ReplaceRangeData GetReplaceRangeDataAtEndOfDeletionRange( 1432 const TextFragmentData& aTextFragmentDataAtStartToDelete) const; 1433 ReplaceRangeData GetReplaceRangeDataAtStartOfDeletionRange( 1434 const TextFragmentData& aTextFragmentDataAtEndToDelete) const; 1435 1436 /** 1437 * VisibleWhiteSpacesDataRef() returns reference to visible white-spaces 1438 * data. That is zero or more white-spaces which are visible. 1439 * Note that when there is no visible content, it's not initialized. 1440 * Otherwise, even if there is no white-spaces, it's initialized and 1441 * the range is collapsed in such case. 1442 */ 1443 const VisibleWhiteSpacesData& VisibleWhiteSpacesDataRef() const; 1444 1445 private: 1446 EditorDOMPoint mScanStartPoint; 1447 RefPtr<const Element> mAncestorLimiter; 1448 BoundaryData mStart; 1449 BoundaryData mEnd; 1450 NoBreakingSpaceData mNBSPData; 1451 mutable Maybe<EditorDOMRange> mLeadingWhiteSpaceRange; 1452 mutable Maybe<EditorDOMRange> mTrailingWhiteSpaceRange; 1453 mutable Maybe<VisibleWhiteSpacesData> mVisibleWhiteSpacesData; 1454 const Options mOptions; 1455 }; 1456 1457 const TextFragmentData& TextFragmentDataAtStartRef() const { 1458 return mTextFragmentDataAtStart; 1459 } 1460 1461 // The node passed to our constructor. 1462 EditorDOMPoint mScanStartPoint; 1463 // Together, the above represent the point at which we are building up ws 1464 // info. 1465 1466 private: 1467 /** 1468 * ComputeRangeInTextNodesContainingInvisibleWhiteSpaces() returns range 1469 * containing invisible white-spaces if deleting between aStart and aEnd 1470 * causes them become visible. 1471 * 1472 * @param aStart TextFragmentData at start of deleting range. 1473 * This must be initialized with DOM point in a text node. 1474 * @param aEnd TextFragmentData at end of deleting range. 1475 * This must be initialized with DOM point in a text node. 1476 */ 1477 static EditorDOMRangeInTexts 1478 ComputeRangeInTextNodesContainingInvisibleWhiteSpaces( 1479 const TextFragmentData& aStart, const TextFragmentData& aEnd); 1480 1481 TextFragmentData mTextFragmentDataAtStart; 1482 1483 friend class WhiteSpaceVisibilityKeeper; 1484 friend class WSScanResult; 1485 }; 1486 1487 } // namespace mozilla 1488 1489 #endif // #ifndef WSRunScanner_h