/* 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 EditorLineBreak_h #define EditorLineBreak_h #include "EditorDOMPoint.h" #include "EditorForwards.h" #include "EditorUtils.h" #include "mozilla/Maybe.h" #include "mozilla/ToString.h" #include "mozilla/dom/CharacterDataBuffer.h" #include "mozilla/dom/Element.h" #include "mozilla/dom/HTMLBRElement.h" #include "mozilla/dom/Text.h" #include "nsCOMPtr.h" #include "nsDebug.h" #include "nsGkAtoms.h" #include "nsIContent.h" namespace mozilla { class AutoTrackLineBreak; enum class PaddingForEmptyBlock { // If the line break is padding for empty block, treat it as not // significant. This is useful to select unecessary things at deletion. Unnecessary, // Treat the padding for empty block as significant since it makes the block // have one-line height. Significant, }; inline std::string format_as(const PaddingForEmptyBlock& aValue) { return aValue == PaddingForEmptyBlock::Significant ? "PaddingForEmptyBlock::Significant" : "PaddingForEmptyBlock::Unnecessary"; } inline std::ostream& operator<<(std::ostream& aStream, const PaddingForEmptyBlock& aValue) { return aStream << format_as(aValue); } /****************************************************************************** * EditorLineBreakBase stores
or a preformatted line break position. * This cannot represent no line break. Therefore, if a method may not return * a line break, they need to use Maybe. ******************************************************************************/ template class EditorLineBreakBase { using SelfType = EditorLineBreakBase; public: using CharacterDataBuffer = dom::CharacterDataBuffer; using HTMLBRElement = dom::HTMLBRElement; using Text = dom::Text; explicit EditorLineBreakBase(const HTMLBRElement& aBRElement) : mContent(const_cast(&aBRElement)) {} explicit EditorLineBreakBase(RefPtr&& aBRElement); explicit EditorLineBreakBase(RefPtr&& aBRElement); explicit EditorLineBreakBase(nsCOMPtr&& aBRElement); EditorLineBreakBase(const Text& aText, uint32_t aOffset) : mContent(const_cast(&aText)), mOffsetInText(Some(aOffset)) {} EditorLineBreakBase(RefPtr&& aText, uint32_t aOffset); EditorLineBreakBase(nsCOMPtr&& aText, uint32_t aOffset); template [[nodiscard]] static Maybe MaybeFrom( const EditorDOMPointType& aPoint) { if (HTMLBRElement* const childAsBRElement = HTMLBRElement::FromNodeOrNull(aPoint.GetChild())) { return Some(EditorLineBreakBase(*childAsBRElement)); } if (Text* const containerAsText = Text::FromNodeOrNull(aPoint.GetContainer())) { if (EditorUtils::IsNewLinePreformatted(*containerAsText) && aPoint.Offset() < containerAsText->TextDataLength() && aPoint.IsCharNewLine()) { return Some(EditorLineBreakBase(*containerAsText, aPoint.Offset())); } } return Nothing{}; } /** * Return a preformatted line break if and only if aText contains only one * preformatted line break and there is no visible text around it. E.g., * the preformatted line break may be surrounded by collapsible * white-spaces. */ [[nodiscard]] static Maybe CreateIfTextHasOnlyOneAndNoOtherVisibleCharacters(const Text& aText) { if (!aText.TextDataLength() || !EditorUtils::IsNewLinePreformatted(aText)) { return Nothing(); } if (aText.TextDataLength() == 1) { return aText.DataBuffer().CharAt(0) == '\n' ? Some(EditorLineBreakBase(aText, 0u)) : Nothing(); } if (EditorUtils::IsWhiteSpacePreformatted(aText) && aText.TextDataLength() > 1) { return Nothing(); // Contains at least 2 visible characters. } using WhitespaceOption = CharacterDataBuffer::WhitespaceOption; const CharacterDataBuffer::WhitespaceOptions whitespaceOptions{ WhitespaceOption::FormFeedIsSignificant, WhitespaceOption::NewLineIsSignificant}; const uint32_t firstVisibleCharOffset = aText.DataBuffer().FindNonWhitespaceChar(whitespaceOptions, 0u); if (firstVisibleCharOffset == CharacterDataBuffer::kNotFound) { return Nothing(); // No visible characters. } if (aText.DataBuffer().CharAt(firstVisibleCharOffset) != '\n') { return Nothing(); // The first visible character is not a linefeed. } if (firstVisibleCharOffset + 1 == aText.TextDataLength()) { // The first visible character is a preformatted linefeed following some // collapsible white-spaces and it's the last character. return Some(EditorLineBreakBase(aText, firstVisibleCharOffset)); } const uint32_t secondVisibleCharOffset = aText.DataBuffer().FindNonWhitespaceChar(whitespaceOptions, firstVisibleCharOffset + 1); if (secondVisibleCharOffset != CharacterDataBuffer::kNotFound) { // There is another visible character after the preformatted linefeed. return Nothing(); } // aText contains only one preformatted linefeed and it's surrounded by // collapsible white-spaces. return Some(EditorLineBreakBase(aText, firstVisibleCharOffset)); } [[nodiscard]] static SelfType AtLastChar(const Text& aText) { MOZ_RELEASE_ASSERT(aText.TextDataLength()); return SelfType(aText, aText.TextDataLength() - 1u); } [[nodiscard]] static SelfType AtLastChar(RefPtr&& aText) { MOZ_RELEASE_ASSERT(aText); MOZ_RELEASE_ASSERT(aText->TextDataLength()); const uint32_t lastCharIndex = aText->TextDataLength() - 1u; return SelfType(std::forward>(aText), lastCharIndex); } [[nodiscard]] static SelfType AtLastChar(nsCOMPtr&& aText) { MOZ_RELEASE_ASSERT(aText); MOZ_RELEASE_ASSERT(aText->IsText()); MOZ_RELEASE_ASSERT(aText->AsText()->TextDataLength()); const uint32_t lastCharIndex = aText->AsText()->TextDataLength() - 1u; return SelfType(std::forward>(aText), lastCharIndex); } [[nodiscard]] bool IsInComposedDoc() const { return mContent->IsInComposedDoc(); } template [[nodiscard]] EditorDOMPointType To() const { if constexpr (std::is_same_v || std::is_same_v) { return mOffsetInText ? EditorDOMPointType(mContent, *mOffsetInText) : EditorDOMPointType(mContent); } else if constexpr (std::is_same_v || std::is_same_v) { MOZ_ASSERT(IsPreformattedLineBreak()); return IsPreformattedLineBreak() ? EditorDOMPointType(&TextRef(), *mOffsetInText) : EditorDOMPointType(); } else { MOZ_MAKE_COMPILER_ASSUME_IS_UNREACHABLE( "Handle the new EditorDOMPointType!"); return EditorDOMPointType(); } } template [[nodiscard]] EditorDOMPointType After() const { if (IsHTMLBRElement()) { return EditorDOMPointType::After(BRElementRef()); } if (mOffsetInText.value() + 1 < TextRef().TextDataLength()) { return EditorDOMPointType(&TextRef(), mOffsetInText.value() + 1); } // If the line break is end of a Text node and it's followed by another // Text, we should return start of the following Text. if (Text* const followingText = Text::FromNodeOrNull(TextRef().GetNextSibling())) { return EditorDOMPointType(followingText, 0); } return EditorDOMPointType::After(TextRef()); } template [[nodiscard]] EditorDOMPointType Before() const { if (IsHTMLBRElement()) { return EditorDOMPointType(&BRElementRef(), dom::Selection::InterlinePosition::EndOfLine); } return To(); } [[nodiscard]] bool IsHTMLBRElement() const { MOZ_ASSERT_IF(!mOffsetInText, mContent->IsHTMLElement(nsGkAtoms::br)); return mOffsetInText.isNothing(); } [[nodiscard]] bool IsPreformattedLineBreak() const { MOZ_ASSERT_IF(mOffsetInText, mContent->IsText()); return mOffsetInText.isSome(); } [[nodiscard]] bool TextIsOnlyPreformattedLineBreak() const { return IsPreformattedLineBreak() && !Offset() && TextRef().TextDataLength() == 1u; } [[nodiscard]] bool IsPreformattedLineBreakAtStartOfText() const { MOZ_ASSERT_IF(mOffsetInText, mContent->IsText()); return mOffsetInText.isSome() && !mOffsetInText.value(); } [[nodiscard]] nsIContent& ContentRef() const { return *mContent; } [[nodiscard]] bool IsInclusiveDescendantOf(const nsINode& aNode) const { return mContent->IsInclusiveDescendantOf(&aNode); } [[nodiscard]] HTMLBRElement& BRElementRef() const { MOZ_DIAGNOSTIC_ASSERT(IsHTMLBRElement()); MOZ_DIAGNOSTIC_ASSERT(GetBRElement()); return *GetBRElement(); } [[nodiscard]] HTMLBRElement* GetBRElement() const { return HTMLBRElement::FromNode(mContent); } [[nodiscard]] Text& TextRef() const { MOZ_DIAGNOSTIC_ASSERT(IsPreformattedLineBreak()); MOZ_DIAGNOSTIC_ASSERT(GetText()); return *GetText(); } [[nodiscard]] Text* GetText() const { return Text::FromNode(mContent); } [[nodiscard]] uint32_t Offset() const { MOZ_ASSERT(IsPreformattedLineBreak()); return mOffsetInText.value(); } [[nodiscard]] bool CharAtOffsetIsLineBreak() const { MOZ_DIAGNOSTIC_ASSERT(IsPreformattedLineBreak()); return *mOffsetInText < TextRef().TextDataLength() && TextRef().DataBuffer().CharAt(*mOffsetInText) == '\n'; } [[nodiscard]] bool IsDeletableFromComposedDoc() const { if (IsPreformattedLineBreak()) { return TextRef().IsEditable(); } const nsIContent* const parent = BRElementRef().GetParent(); return parent && parent->IsEditable(); } [[nodiscard]] bool IsFollowedByBlockBoundary( const dom::Element* aAncestorLimiter = nullptr) const; [[nodiscard]] bool IsFollowedByCurrentBlockBoundary( const dom::Element* aAncestorLimiter = nullptr) const; [[nodiscard]] bool IsFollowingCurrentBlockBoundary( const dom::Element* aAncestorLimiter = nullptr) const; [[nodiscard]] bool IsFollowedByLineBoundary( const dom::Element* aAncestorLimiter = nullptr) const; [[nodiscard]] bool IsFollowingLineBoundary( const dom::Element* aAncestorLimiter = nullptr) const; [[nodiscard]] bool IsFollowingAnotherLineBreak( const dom::Element* aAncestorLimiter = nullptr) const; [[nodiscard]] bool IsPaddingForEmptyBlock( const dom::Element* aAncestorLimiter = nullptr) const; [[nodiscard]] bool IsUnnecessary( PaddingForEmptyBlock aPaddingForEmptyBlock, const dom::Element* aAncestorLimiter = nullptr) const; [[nodiscard]] bool IsSignificant( PaddingForEmptyBlock aPaddingForEmptyBlock, const dom::Element* aAncestorLimiter = nullptr) const; [[nodiscard]] dom::Element* GetBlockElementIfFollowedByBlockBoundary( const dom::Element* aAncestorLimiter = nullptr) const; [[nodiscard]] dom::Element* GetBlockElementIfFollowedByCurrentBlockBoundary( const dom::Element* aAncestorLimiter = nullptr) const; [[nodiscard]] dom::Element* GetBlockElementIfFollowedByOtherBlockBoundary( const dom::Element* aAncestorLimiter = nullptr) const; friend inline std::ostream& operator<<( std::ostream& aStream, const EditorLineBreakBase& aLineBreak) { return aStream << fmt::format("{{ mContent={}, mOffsetInText={} }}", nsCOMPtr(aLineBreak.mContent), // XXX Oddly, cannot make Maybe // formattable. Maybe a bug of {fmt}. ToString(aLineBreak.mOffsetInText)); } private: ContentType mContent; Maybe mOffsetInText; friend class AutoTrackLineBreak; }; using EditorLineBreak = EditorLineBreakBase>; using EditorRawLineBreak = EditorLineBreakBase; #define NS_INSTANTIATE_EDITOR_LINE_BREAK_CONST_METHOD(aResultType, \ aMethodName, ...) \ template aResultType EditorLineBreak::aMethodName(__VA_ARGS__) const; \ template aResultType EditorRawLineBreak::aMethodName(__VA_ARGS__) const; template <> inline EditorLineBreakBase>::EditorLineBreakBase( RefPtr&& aBRElement) : mContent(aBRElement.forget()) { MOZ_RELEASE_ASSERT(mContent); } template <> inline EditorLineBreakBase>::EditorLineBreakBase( RefPtr&& aBRElement) : mContent(aBRElement.forget()) { MOZ_RELEASE_ASSERT(mContent); MOZ_RELEASE_ASSERT(mContent->IsHTMLElement(nsGkAtoms::br)); } template <> inline EditorLineBreakBase>::EditorLineBreakBase( nsCOMPtr&& aBRElement) : mContent(aBRElement.forget()) { MOZ_RELEASE_ASSERT(mContent); MOZ_RELEASE_ASSERT(mContent->IsHTMLElement(nsGkAtoms::br)); } template <> inline EditorLineBreakBase>::EditorLineBreakBase( RefPtr&& aText, uint32_t aOffset) : mContent(std::move(aText)), mOffsetInText(Some(aOffset)) { MOZ_RELEASE_ASSERT(mContent); MOZ_ASSERT(EditorUtils::IsNewLinePreformatted(*mContent)); MOZ_RELEASE_ASSERT(GetText()->TextDataLength() > aOffset); MOZ_RELEASE_ASSERT(CharAtOffsetIsLineBreak()); } template <> inline EditorLineBreakBase>::EditorLineBreakBase( nsCOMPtr&& aText, uint32_t aOffset) : mContent(aText.forget()), mOffsetInText(Some(aOffset)) { MOZ_RELEASE_ASSERT(mContent); MOZ_RELEASE_ASSERT(mContent->IsText()); MOZ_ASSERT(EditorUtils::IsNewLinePreformatted(*mContent)); MOZ_RELEASE_ASSERT(TextRef().TextDataLength() > aOffset); MOZ_ASSERT(CharAtOffsetIsLineBreak()); } template <> inline EditorLineBreakBase::EditorLineBreakBase( const HTMLBRElement& aBRElement) : mContent(const_cast(&aBRElement)) {} template <> inline EditorLineBreakBase::EditorLineBreakBase( RefPtr&& aBRElement) : mContent(aBRElement) { MOZ_RELEASE_ASSERT(mContent); aBRElement = nullptr; } template <> inline EditorLineBreakBase::EditorLineBreakBase( RefPtr&& aBRElement) : mContent(aBRElement) { MOZ_RELEASE_ASSERT(mContent); MOZ_RELEASE_ASSERT(mContent->IsHTMLElement(nsGkAtoms::br)); aBRElement = nullptr; } template <> inline EditorLineBreakBase::EditorLineBreakBase( nsCOMPtr&& aBRElement) : mContent(aBRElement) { MOZ_RELEASE_ASSERT(mContent); MOZ_RELEASE_ASSERT(mContent->IsHTMLElement(nsGkAtoms::br)); aBRElement = nullptr; } template <> inline EditorLineBreakBase::EditorLineBreakBase( RefPtr&& aText, uint32_t aOffset) : mContent(aText), mOffsetInText(Some(aOffset)) { MOZ_RELEASE_ASSERT(mContent); MOZ_ASSERT(EditorUtils::IsNewLinePreformatted(*mContent)); MOZ_RELEASE_ASSERT(GetText()->TextDataLength() > aOffset); MOZ_RELEASE_ASSERT(CharAtOffsetIsLineBreak()); aText = nullptr; } template <> inline EditorLineBreakBase::EditorLineBreakBase( nsCOMPtr&& aText, uint32_t aOffset) : mContent(aText), mOffsetInText(Some(aOffset)) { MOZ_RELEASE_ASSERT(mContent); MOZ_RELEASE_ASSERT(mContent->IsText()); MOZ_ASSERT(EditorUtils::IsNewLinePreformatted(*mContent)); MOZ_RELEASE_ASSERT(GetText()->TextDataLength() > aOffset); MOZ_ASSERT(CharAtOffsetIsLineBreak()); aText = nullptr; } class CreateLineBreakResult final : public CaretPoint { public: CreateLineBreakResult(const EditorLineBreak& aLineBreak, const EditorDOMPoint& aCaretPoint) : CaretPoint(aCaretPoint), mLineBreak(Some(aLineBreak)) {} CreateLineBreakResult(EditorLineBreak&& aLineBreak, const EditorDOMPoint& aCaretPoint) : CaretPoint(aCaretPoint), mLineBreak(Some(std::move(aLineBreak))) {} CreateLineBreakResult(const EditorLineBreak& aLineBreak, EditorDOMPoint&& aCaretPoint) : CaretPoint(aCaretPoint), mLineBreak(Some(aLineBreak)) {} CreateLineBreakResult(EditorLineBreak&& aLineBreak, EditorDOMPoint&& aCaretPoint) : CaretPoint(std::move(aCaretPoint)), mLineBreak(Some(std::move(aLineBreak))) {} explicit CreateLineBreakResult(CreateElementResult&& aCreateElementResult) : CaretPoint(aCreateElementResult.UnwrapCaretPoint()), mLineBreak(Some(aCreateElementResult.UnwrapNewNode())) {} explicit CreateLineBreakResult(CreateElementResult&& aCreateElementResult, EditorDOMPoint&& aCaretPoint) : CaretPoint(std::forward(aCaretPoint)), mLineBreak(Some(aCreateElementResult.UnwrapNewNode())) { aCreateElementResult.IgnoreCaretPointSuggestion(); } [[nodiscard]] static CreateLineBreakResult NotHandled() { return CreateLineBreakResult(); } [[nodiscard]] constexpr bool Handled() const { return mLineBreak.isSome(); } [[nodiscard]] constexpr const EditorLineBreak& LineBreakRef() const { MOZ_ASSERT(Handled()); return mLineBreak.ref(); } [[nodiscard]] constexpr const EditorLineBreak* operator->() const { return &LineBreakRef(); } // Shortcut for unclear methods of EditorLineBreak if `->` operator is used. template [[nodiscard]] EditorDOMPointType AtLineBreak() const { return LineBreakRef().To(); } template [[nodiscard]] EditorDOMPointType BeforeLineBreak() const { return LineBreakRef().Before(); } template [[nodiscard]] EditorDOMPointType AfterLineBreak() const { return LineBreakRef().After(); } [[nodiscard]] nsIContent& LineBreakContentRef() const { return LineBreakRef().ContentRef(); } [[nodiscard]] bool LineBreakIsInComposedDoc() const { return LineBreakRef().IsInComposedDoc(); } private: CreateLineBreakResult() : CaretPoint(EditorDOMPoint()) {} Maybe mLineBreak; }; } // namespace mozilla template struct fmt::formatter> : fmt::ostream_formatter {}; #endif // #ifndef EditorLineBreak_h