/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #ifndef WSRunScanner_h #define WSRunScanner_h #include "EditorBase.h" #include "EditorForwards.h" #include "EditorDOMPoint.h" // for EditorDOMPoint #include "EditorLineBreak.h" // for EditorLineBreakBase #include "HTMLEditor.h" #include "HTMLEditUtils.h" #include "mozilla/Assertions.h" #include "mozilla/Maybe.h" #include "mozilla/Result.h" #include "mozilla/dom/Element.h" #include "mozilla/dom/HTMLBRElement.h" #include "mozilla/dom/Text.h" #include "nsCOMPtr.h" #include "nsIContent.h" namespace mozilla { /** * WSScanResult is result of ScanNextVisibleNodeOrBlockBoundaryFrom(), * ScanPreviousVisibleNodeOrBlockBoundaryFrom(), and their static wrapper * methods. This will have information of found visible content (and its * position) or reached block element or topmost editable content at the * start of scanner. */ class MOZ_STACK_CLASS WSScanResult final { private: using Element = dom::Element; using HTMLBRElement = dom::HTMLBRElement; using Text = dom::Text; enum class WSType : uint8_t { NotInitialized, // Could be the DOM tree is broken as like crash tests. UnexpectedError, // The scanner cannot work in uncomposed tree, but tried to scan in it. InUncomposedDoc, // The run is maybe collapsible white-spaces at start of a hard line. LeadingWhiteSpaces, // The run is maybe collapsible white-spaces at end of a hard line. TrailingWhiteSpaces, // Collapsible, but visible white-spaces. CollapsibleWhiteSpaces, // Visible characters except collapsible white-spaces. NonCollapsibleCharacters, // Empty inline container elemnet such as ``. Note that it may // be visible if its border/padding is not 0 for example. // NOTE: This won't be used if it's the inline editing host at the scan // start point. EmptyInlineContainerElement, // Special content such as ``, etc. SpecialContent, //
element. BRElement, // A linefeed which is preformatted. PreformattedLineBreak, // Other block's boundary (child block of current block, maybe). OtherBlockBoundary, // Current block's boundary. CurrentBlockBoundary, // Inline editing host boundary. InlineEditingHostBoundary, }; friend std::ostream& operator<<(std::ostream& aStream, const WSType& aType) { switch (aType) { case WSType::NotInitialized: return aStream << "WSType::NotInitialized"; case WSType::UnexpectedError: return aStream << "WSType::UnexpectedError"; case WSType::InUncomposedDoc: return aStream << "WSType::InUncomposedDoc"; case WSType::LeadingWhiteSpaces: return aStream << "WSType::LeadingWhiteSpaces"; case WSType::TrailingWhiteSpaces: return aStream << "WSType::TrailingWhiteSpaces"; case WSType::CollapsibleWhiteSpaces: return aStream << "WSType::CollapsibleWhiteSpaces"; case WSType::NonCollapsibleCharacters: return aStream << "WSType::NonCollapsibleCharacters"; case WSType::EmptyInlineContainerElement: return aStream << "WSType::EmptyInlineContainerElement"; case WSType::SpecialContent: return aStream << "WSType::SpecialContent"; case WSType::BRElement: return aStream << "WSType::BRElement"; case WSType::PreformattedLineBreak: return aStream << "WSType::PreformattedLineBreak"; case WSType::OtherBlockBoundary: return aStream << "WSType::OtherBlockBoundary"; case WSType::CurrentBlockBoundary: return aStream << "WSType::CurrentBlockBoundary"; case WSType::InlineEditingHostBoundary: return aStream << "WSType::InlineEditingHostBoundary"; } return aStream << ""; } friend class WSRunScanner; // Because of WSType. explicit WSScanResult(WSType aReason) : mReason(aReason) { MOZ_ASSERT(mReason == WSType::UnexpectedError || mReason == WSType::NotInitialized); } public: WSScanResult() = delete; enum class ScanDirection : bool { Backward, Forward }; WSScanResult(const WSRunScanner& aScanner, ScanDirection aScanDirection, nsIContent& aContent, WSType aReason) : mContent(&aContent), mReason(aReason), mDirection(aScanDirection) { MOZ_ASSERT(aReason != WSType::CollapsibleWhiteSpaces && aReason != WSType::NonCollapsibleCharacters && aReason != WSType::PreformattedLineBreak); AssertIfInvalidData(aScanner); MaybeSetEditingHost(aScanner); } WSScanResult(const WSRunScanner& aScanner, ScanDirection aScanDirection, const EditorDOMPoint& aPoint, WSType aReason) : mContent(aPoint.GetContainerAs()), mOffset(Some(aPoint.Offset())), mReason(aReason), mDirection(aScanDirection) { AssertIfInvalidData(aScanner); MaybeSetEditingHost(aScanner); } WSScanResult(WSScanResult&& aResult, EditorLineBreak&& aIgnoredLineBreak, const Element& aEditingHost) : WSScanResult(std::forward(aResult)) { MOZ_ASSERT(ReachedBlockBoundary()); mIgnoredLineBreak.emplace(std::forward(aIgnoredLineBreak)); // If an editing host is specified and the reached content is outside the // editing host, we should store the editing host to make the method // returning the reached point to return a point in the editing host. Element* const contentEditingHost = mContent->GetEditingHost(); if (contentEditingHost && contentEditingHost != &aEditingHost) { mEditingHost = const_cast(&aEditingHost); } } static WSScanResult Error() { return WSScanResult(WSType::UnexpectedError); } void AssertIfInvalidData(const WSRunScanner& aScanner) const; bool Failed() const { return mReason == WSType::NotInitialized || mReason == WSType::UnexpectedError; } /** * GetContent() returns found visible and editable content/element. * See MOZ_ASSERT_IF()s in AssertIfInvalidData() for the detail. */ nsIContent* GetContent() const { return mContent; } [[nodiscard]] bool ContentIsElement() const { return mContent && mContent->IsElement(); } [[nodiscard]] bool ContentIsText() const { return mContent && mContent->IsText(); } /** * The following accessors makes it easier to understand each callers. */ MOZ_NEVER_INLINE_DEBUG Element* ElementPtr() const { MOZ_DIAGNOSTIC_ASSERT(mContent->IsElement()); return mContent->AsElement(); } MOZ_NEVER_INLINE_DEBUG HTMLBRElement* BRElementPtr() const { MOZ_DIAGNOSTIC_ASSERT(mContent->IsHTMLElement(nsGkAtoms::br)); return static_cast(mContent.get()); } MOZ_NEVER_INLINE_DEBUG Text* TextPtr() const { MOZ_DIAGNOSTIC_ASSERT(mContent->IsText()); return mContent->AsText(); } template MOZ_NEVER_INLINE_DEBUG EditorLineBreakType CreateEditorLineBreak() const { if (ReachedBRElement()) { return EditorLineBreakType(*BRElementPtr()); } if (ReachedPreformattedLineBreak()) { MOZ_ASSERT_IF(mDirection == ScanDirection::Backward, *mOffset > 0); return EditorLineBreakType(*TextPtr(), mDirection == ScanDirection::Forward ? mOffset.valueOr(0) : std::max(mOffset.valueOr(1), 1u) - 1); } MOZ_CRASH("Didn't reach a line break"); return EditorLineBreakType(*BRElementPtr()); } /** * Return true if found or reached content is editable. */ [[nodiscard]] bool ContentIsEditable() const { return mContent && HTMLEditUtils::IsSimplyEditableNode(*mContent); } /** * Return true if found or reached content is removable node. */ [[nodiscard]] bool ContentIsRemovable() const { return mContent && HTMLEditUtils::IsRemovableNode(*mContent); } [[nodiscard]] bool ContentIsEditableRoot() const { return ContentIsElement() && HTMLEditUtils::ElementIsEditableRoot(*ElementPtr()); } [[nodiscard]] bool ReachedOutsideEditingHost() const { return !!mEditingHost; } /** * Offset_Deprecated() returns meaningful value only when * InVisibleOrCollapsibleCharacters() returns true or the scanner reached to * start or end of its scanning range and that is same as start or end * container which are specified when the scanner is initialized. If it's * result of scanning backward, this offset means the point of the found * point. Otherwise, i.e., scanning forward, this offset means next point * of the found point. E.g., if it reaches a collapsible white-space, this * offset is at the first non-collapsible character after it. */ MOZ_NEVER_INLINE_DEBUG uint32_t Offset_Deprecated() const { NS_ASSERTION(mOffset.isSome(), "Retrieved non-meaningful offset"); return mOffset.valueOr(0); } /** * Point_Deprecated() returns the position in found visible node or reached * block boundary. So, this returns meaningful point only when * Offset_Deprecated() returns meaningful value. */ template EditorDOMPointType Point_Deprecated() const { NS_ASSERTION(mOffset.isSome(), "Retrieved non-meaningful point"); return EditorDOMPointType(mContent, mOffset.valueOr(0)); } /** * PointAtReachedContent() returns the position of found visible content or * reached block element. Note that the returned point may be outside the * editing host if WSRunScanner::Option::OnlyEditableNodes is not specified * and the ancestor limiter is not specified or specified outside the editing * host. */ template EditorDOMPointType PointAtReachedContent() const { MOZ_ASSERT(mContent); switch (mReason) { case WSType::CollapsibleWhiteSpaces: case WSType::NonCollapsibleCharacters: case WSType::PreformattedLineBreak: MOZ_DIAGNOSTIC_ASSERT(mOffset.isSome()); return mDirection == ScanDirection::Forward ? EditorDOMPointType(mContent, mOffset.valueOr(0)) : EditorDOMPointType(mContent, std::max(mOffset.valueOr(1), 1u) - 1); default: MOZ_ASSERT_IF(mContent == mEditingHost, !ReachedCurrentBlockBoundary()); MOZ_ASSERT_IF(mContent == mEditingHost, !ReachedInlineEditingHostBoundary()); return EditorDOMPointType(mContent); } } /** * Similar to PointAtReachedContent(), but return a boundary point of the * editing host if the reached content is outside of the editing host. */ template EditorDOMPointType PointAtReachedContentOrEditingHostBoundary() const { if (mEditingHost) { MOZ_ASSERT(mContent); MOZ_ASSERT_IF(mContent == mEditingHost, !ReachedCurrentBlockBoundary()); MOZ_ASSERT_IF(mContent == mEditingHost, !ReachedInlineEditingHostBoundary()); return mDirection == ScanDirection::Forward ? EditorDOMPointType::AtEndOf(*mEditingHost) : EditorDOMPointType(mEditingHost, 0u); } return PointAtReachedContent(); } /** * PointAfterReachedContent() returns the next position of found visible * content or reached block element. Note that the returned point may be * outside the editing host if WSRunScanner::Option::OnlyEditableNodes is not * specified and the ancestor limiter is not specified or specified outside * the editing host. */ template EditorDOMPointType PointAfterReachedContent() const { MOZ_ASSERT(mContent); MOZ_ASSERT_IF(mContent == mEditingHost, !ReachedCurrentBlockBoundary()); MOZ_ASSERT_IF(mContent == mEditingHost, !ReachedInlineEditingHostBoundary()); return PointAtReachedContent() .template NextPointOrAfterContainer(); } /** * Similar to PointAfterReachedContent(), but return a boundary point of the * editing host if the reached content is outside of the editing host. */ template EditorDOMPointType PointAfterReachedContentOrEditingHostBoundary() const { if (mEditingHost) { MOZ_ASSERT(mContent); MOZ_ASSERT_IF(mContent == mEditingHost, !ReachedCurrentBlockBoundary()); MOZ_ASSERT_IF(mContent == mEditingHost, !ReachedInlineEditingHostBoundary()); return mDirection == ScanDirection::Forward ? EditorDOMPointType::AtEndOf(*mEditingHost) : EditorDOMPointType(mEditingHost, 0u); } return PointAfterReachedContent(); } /** * Return the next position of found visible content node. So, this should * not be used if it reached a visible character middle of a `Text`. Note that * the returned point may be outside the editing host if * WSRunScanner::Option::OnlyEditableNodes is not specified and the ancestor * limiter is not specified or specified outside the editing host. */ template EditorDOMPointType PointAfterReachedContentNode() const { MOZ_ASSERT(mContent); MOZ_ASSERT_IF(mContent == mEditingHost, !ReachedCurrentBlockBoundary()); MOZ_ASSERT_IF(mContent == mEditingHost, !ReachedInlineEditingHostBoundary()); return EditorDOMPointType::After(*mContent); } /** * Similar to PointAfterReachedContentNode(), but return a boundary point of * the editing host if the reached content is outside of the editing host. */ template EditorDOMPointType PointAfterReachedContentNodeOrEditingHostBoundary() const { MOZ_ASSERT(mContent); if (mEditingHost) { MOZ_ASSERT_IF(mContent == mEditingHost, !ReachedCurrentBlockBoundary()); MOZ_ASSERT_IF(mContent == mEditingHost, !ReachedInlineEditingHostBoundary()); return mDirection == ScanDirection::Forward ? EditorDOMPointType::AtEndOf(*mEditingHost) : EditorDOMPointType(mEditingHost, 0u); } return PointAfterReachedContentNode(); } /** * Return the point of the reached block boundary. If reached current block * boundary, return its inner boundary. Otherwise, its outer boundary. */ template EditorDOMPointType PointAtReachedBlockBoundary() const { MOZ_ASSERT(ReachedBlockBoundary()); if (mDirection == ScanDirection::Forward) { return ReachedCurrentBlockBoundary() ? EditorDOMPointType::AtEndOf(*mContent) : EditorDOMPointType(mContent); } return ReachedCurrentBlockBoundary() ? EditorDOMPointType(mContent, 0u) : EditorDOMPointType::After(*mContent); } /** * Similar to PointAtReachedBlockBoundary() but the block is outside the * editing host, return the point at the editing host boundary. */ template EditorDOMPointType PointAtReachedBlockBoundaryOrEditingHostBoundary() const { MOZ_ASSERT(ReachedBlockBoundary()); if (mEditingHost) { return mDirection == ScanDirection::Forward ? EditorDOMPointType::AtEndOf(*mEditingHost) : EditorDOMPointType(mEditingHost, 0u); } return PointAtReachedBlockBoundary(); } /** * Return the point of the reached line boundary. I.e., start or end of the * line content, i.e., if scanned forward and reached a line break, return the * point of the line break, not after the line break. */ template EditorDOMPointType PointAtReachedLineBoundary() const { MOZ_ASSERT(ReachedLineBoundary()); if (ReachedBlockBoundary()) { return PointAtReachedBlockBoundary(); } if (mDirection == ScanDirection::Forward) { return PointAtReachedContent(); } return PointAfterReachedContent(); } /** * Similar to PointAtReachedLineBoundary() but if the line boundary is outside * the editing host, return the editing host boundary. */ template EditorDOMPointType PointAtReachedLineBoundaryOrEditingHostBoundary() const { MOZ_ASSERT(ReachedLineBoundary()); if (ReachedBlockBoundary()) { return PointAtReachedBlockBoundaryOrEditingHostBoundary< EditorDOMPointType>(); } if (mDirection == ScanDirection::Forward) { return PointAtReachedContentOrEditingHostBoundary(); } return PointAfterReachedContentOrEditingHostBoundary(); } /** * The scanner reached an empty inline container element such as * . Note that the element may be visible, e.g., may have * non-zero border/padding. */ [[nodiscard]] constexpr bool ReachedEmptyInlineContainerElement() const { return mReason == WSType::EmptyInlineContainerElement; } /** * The scanner reached an editable empty inline container element such as * . Note that the element may be visible, e.g., may have * non-zero border/padding. */ [[nodiscard]] bool ReachedEditableEmptyInlineContainerElement() const { return ReachedEmptyInlineContainerElement() && ContentIsEditable(); } /** * The scanner reached a removable empty inline container element such as * . Note that the element may be visible, e.g., may have * non-zero border/padding. */ [[nodiscard]] bool ReachedRemovableEmptyInlineContainerElement() const { return ReachedEmptyInlineContainerElement() && ContentIsRemovable(); } /** * The scanner reached an empty inline container element which is visible. */ [[nodiscard]] bool ReachedVisibleEmptyInlineContainerElement( const nsIContent* aAncestorLimiterToCheckDisplayNone = nullptr) const { return ReachedEmptyInlineContainerElement() && HTMLEditUtils::IsVisibleElementEvenIfLeafNode(*ElementPtr()) && !HTMLEditUtils::IsInclusiveAncestorCSSDisplayNone( *ElementPtr(), aAncestorLimiterToCheckDisplayNone); } /** * The scanner reached an editable empty inline container element which is * visible. */ [[nodiscard]] bool ReachedEditableEmptyInlineContainerElement( const nsIContent* aAncestorLimiterToCheckDisplayNone = nullptr) const { return ReachedVisibleEmptyInlineContainerElement( aAncestorLimiterToCheckDisplayNone) && ContentIsEditable(); } /** * The scanner reached a removable empty inline container element which is * visible. */ [[nodiscard]] bool ReachedRemovableEmptyInlineContainerElement( const nsIContent* aAncestorLimiterToCheckDisplayNone = nullptr) const { return ReachedVisibleEmptyInlineContainerElement( aAncestorLimiterToCheckDisplayNone) && ContentIsRemovable(); } /** * The scanner reached an empty inline container element which is invisible. */ [[nodiscard]] bool ReachedInvisibleEmptyInlineContainerElement( const nsIContent* aAncestorLimiterToCheckDisplayNone = nullptr) const { return ReachedEmptyInlineContainerElement() && (!HTMLEditUtils::IsVisibleElementEvenIfLeafNode(*ElementPtr()) || HTMLEditUtils::IsInclusiveAncestorCSSDisplayNone( *ElementPtr(), aAncestorLimiterToCheckDisplayNone)); } /** * The scanner reached an editable empty inline container element which is * invisible. */ [[nodiscard]] bool ReachedEditableInvisibleEmptyInlineContainerElement( const nsIContent* aAncestorLimiterToCheckDisplayNone = nullptr) const { return ReachedInvisibleEmptyInlineContainerElement( aAncestorLimiterToCheckDisplayNone) && ContentIsEditable(); } /** * The scanner reached a removable empty inline container element which is * invisible. */ [[nodiscard]] bool ReachedRemovableInvisibleEmptyInlineContainerElement( const nsIContent* aAncestorLimiterToCheckDisplayNone = nullptr) const { return ReachedEditableInvisibleEmptyInlineContainerElement( aAncestorLimiterToCheckDisplayNone) && ContentIsRemovable(); } /** * The scanner reached or something which is inline and is not a * container. */ [[nodiscard]] constexpr bool ReachedSpecialContent() const { return mReason == WSType::SpecialContent; } /** * The point is in visible characters or collapsible white-spaces. */ bool InVisibleOrCollapsibleCharacters() const { return mReason == WSType::CollapsibleWhiteSpaces || mReason == WSType::NonCollapsibleCharacters; } /** * The point is in collapsible white-spaces. */ bool InCollapsibleWhiteSpaces() const { return mReason == WSType::CollapsibleWhiteSpaces; } /** * The point is in visible non-collapsible characters. */ bool InNonCollapsibleCharacters() const { return mReason == WSType::NonCollapsibleCharacters; } /** * The scanner reached a
element. */ bool ReachedBRElement() const { return mReason == WSType::BRElement; } bool ReachedBRElementNotFollowedByBlockBoundary() const { return ReachedBRElement() && !HTMLEditUtils::IsBRElementFollowedByBlockBoundary(*BRElementPtr()); } bool ReachedBRElementFollowedByBlockBoundary() const { return ReachedBRElement() && HTMLEditUtils::IsBRElementFollowedByBlockBoundary(*BRElementPtr()); } bool ReachedPreformattedLineBreak() const { return mReason == WSType::PreformattedLineBreak; } /** * Return true if reached a
element or a preformatted line break. * Return false when reached a block boundary. Use ReachedLineBoundary() if * you want it to return true in the case too. */ [[nodiscard]] bool ReachedLineBreak() const { return ReachedBRElement() || ReachedPreformattedLineBreak(); } /** * The scanner reached a
element. */ bool ReachedHRElement() const { return mContent && mContent->IsHTMLElement(nsGkAtoms::hr); } /** * The scanner reached current block boundary or other block element. */ bool ReachedBlockBoundary() const { return mReason == WSType::CurrentBlockBoundary || mReason == WSType::OtherBlockBoundary; } /** * The scanner reached current block element boundary. */ bool ReachedCurrentBlockBoundary() const { return mReason == WSType::CurrentBlockBoundary; } /** * The scanner reached other block element. */ bool ReachedOtherBlockElement() const { return mReason == WSType::OtherBlockBoundary; } /** * The scanner reached other block element that isn't editable */ bool ReachedNonEditableOtherBlockElement() const { return ReachedOtherBlockElement() && !GetContent()->IsEditable(); } /** * The scanner reached inline editing host boundary. */ [[nodiscard]] bool ReachedInlineEditingHostBoundary() const { return mReason == WSType::InlineEditingHostBoundary; } /** * The scanner reached something non-text node. */ bool ReachedSomethingNonTextContent() const { return !InVisibleOrCollapsibleCharacters(); } [[nodiscard]] bool ReachedLineBoundary() const { switch (mReason) { case WSType::CurrentBlockBoundary: case WSType::OtherBlockBoundary: case WSType::BRElement: case WSType::PreformattedLineBreak: return true; default: return ReachedHRElement(); } } /** * Return a reference to editor line break which was ignored at scanning. * If there is, it means that the line break is unnecessary. Note that the * invisible line break may be outside the editing host. */ [[nodiscard]] const Maybe& MaybeIgnoredLineBreak() const { return mIgnoredLineBreak; } friend std::ostream& operator<<(std::ostream& aStream, const ScanDirection& aDirection) { return aStream << (aDirection == ScanDirection::Backward ? "ScanDirection::Backward" : "ScanDirection::Forward"); } friend std::ostream& operator<<(std::ostream& aStream, const WSScanResult& aResult) { aStream << "{ mReason: " << aResult.mReason; if (aResult.mReason == WSType::NotInitialized || aResult.mReason == WSType::InUncomposedDoc) { return aStream << " }"; } return aStream << ", mContent: " << aResult.mContent << ", mEditingHost: " << aResult.mEditingHost << "< mIgnoredLineBreak: " << aResult.mIgnoredLineBreak << ", mOffset: " << aResult.mOffset << ", mDirection: " << aResult.mDirection << " }"; } private: void MaybeSetEditingHost(const WSRunScanner& aScanner); nsCOMPtr mContent; RefPtr mEditingHost; Maybe mIgnoredLineBreak; Maybe mOffset; WSType mReason = WSType::NotInitialized; ScanDirection mDirection = ScanDirection::Backward; }; class MOZ_STACK_CLASS WSRunScanner final { private: using Element = dom::Element; using HTMLBRElement = dom::HTMLBRElement; using Text = dom::Text; public: using WSType = WSScanResult::WSType; enum class IgnoreNonEditableNodes : bool { No, Yes }; enum class StopAtNonEditableNode : bool { No, Yes }; enum class ReferHTMLDefaultStyle : bool { No, Yes }; enum class Option { // If set, return only editable content or return non-editable content as a // special content in the closest editing host if the scan start point is // editable. OnlyEditableNodes, // If set, use the HTML default style to consider whether the found one is a // block or an inline. ReferHTMLDefaultStyle, // If set, stop scanning the DOM when it reaches a `Comment` node. StopAtComment, // If set, stop at any empty inline containers, even when it's visible. StopAtAnyEmptyInlineContainers, // If set, stop ignoring visible empty inline containers such as // or // . // XXX Currently, this does not work well if the inline container has only // `::before` and/or `::after` content and the frame is dirty. StopAtVisibleEmptyInlineContainers, }; using Options = EnumSet