/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* 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 HTMLEditorNestedClasses_h #define HTMLEditorNestedClasses_h #include "EditorDOMPoint.h" #include "EditorForwards.h" #include "HTMLEditor.h" // for HTMLEditor #include "HTMLEditHelpers.h" // for EditorInlineStyleAndValue #include "HTMLEditUtils.h" // for HTMLEditUtils::IsContainerNode #include "mozilla/Attributes.h" #include "mozilla/OwningNonNull.h" #include "mozilla/Result.h" #include "mozilla/dom/Text.h" #include "nsTextFragment.h" namespace mozilla { /***************************************************************************** * AutoInlineStyleSetter is a temporary class to set an inline style to * specific nodes. ****************************************************************************/ class MOZ_STACK_CLASS HTMLEditor::AutoInlineStyleSetter final : private EditorInlineStyleAndValue { using Element = dom::Element; using Text = dom::Text; public: explicit AutoInlineStyleSetter( const EditorInlineStyleAndValue& aStyleAndValue) : EditorInlineStyleAndValue(aStyleAndValue) {} void Reset() { mFirstHandledPoint.Clear(); mLastHandledPoint.Clear(); } const EditorDOMPoint& FirstHandledPointRef() const { return mFirstHandledPoint; } const EditorDOMPoint& LastHandledPointRef() const { return mLastHandledPoint; } /** * Split aText at aStartOffset and aEndOffset (except when they are start or * end of its data) and wrap the middle text node in an element to apply the * style. */ [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result SplitTextNodeAndApplyStyleToMiddleNode(HTMLEditor& aHTMLEditor, Text& aText, uint32_t aStartOffset, uint32_t aEndOffset); /** * Remove same style from children and apply the style entire (except * non-editable nodes) aContent. */ [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result ApplyStyleToNodeOrChildrenAndRemoveNestedSameStyle(HTMLEditor& aHTMLEditor, nsIContent& aContent); /** * Invert the style with creating new element or something. This should * be called only when IsInvertibleWithCSS() returns true. */ [[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult InvertStyleIfApplied(HTMLEditor& aHTMLEditor, Element& aElement); [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result InvertStyleIfApplied(HTMLEditor& aHTMLEditor, Text& aTextNode, uint32_t aStartOffset, uint32_t aEndOffset); /** * Extend or shrink aRange for applying the style to the range. * See comments in the definition what this does. */ Result ExtendOrShrinkRangeToApplyTheStyle( const HTMLEditor& aHTMLEditor, const EditorDOMRange& aRange) const; /** * Returns next/previous sibling of aContent or an ancestor of it if it's * editable and does not cross block boundary. */ [[nodiscard]] static nsIContent* GetNextEditableInlineContent( const nsIContent& aContent, const nsINode* aLimiter = nullptr); [[nodiscard]] static nsIContent* GetPreviousEditableInlineContent( const nsIContent& aContent, const nsINode* aLimiter = nullptr); /** * GetEmptyTextNodeToApplyNewStyle creates new empty text node to insert * a new element which will contain newly inserted text or returns existing * empty text node if aCandidatePointToInsert is around it. * * NOTE: Unfortunately, editor does not want to insert text into empty inline * element in some places (e.g., automatically adjusting caret position to * nearest text node). Therefore, we need to create new empty text node to * prepare new styles for inserting text. This method is designed for the * preparation. * * @param aHTMLEditor The editor. * @param aCandidatePointToInsert The point where the caller wants to * insert new text. * @return If this creates new empty text node returns it. * If this couldn't create new empty text node due to * the point or aEditingHost cannot have text node, * returns nullptr. * Otherwise, returns error. */ [[nodiscard]] MOZ_CAN_RUN_SCRIPT static Result, nsresult> GetEmptyTextNodeToApplyNewStyle( HTMLEditor& aHTMLEditor, const EditorDOMPoint& aCandidatePointToInsert); private: [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result ApplyStyle( HTMLEditor& aHTMLEditor, nsIContent& aContent); [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result ApplyCSSTextDecoration(HTMLEditor& aHTMLEditor, nsIContent& aContent); /** * Returns true if aStyledElement is a good element to set `style` attribute. */ [[nodiscard]] bool ElementIsGoodContainerToSetStyle( nsStyledElement& aStyledElement) const; /** * ElementIsGoodContainerForTheStyle() returns true if aElement is a * good container for applying the style to a node. I.e., if this returns * true, moving nodes into aElement is enough to apply the style to them. * Otherwise, you need to create new element for the style. */ [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result ElementIsGoodContainerForTheStyle(HTMLEditor& aHTMLEditor, Element& aElement) const; /** * Return true if the node is an element node and it represents the style or * sets the style (including when setting different value) with `style` * attribute. */ [[nodiscard]] bool ContentIsElementSettingTheStyle( const HTMLEditor& aHTMLEditor, nsIContent& aContent) const; /** * Helper methods to shrink range to apply the style. */ [[nodiscard]] EditorRawDOMPoint GetShrunkenRangeStart( const HTMLEditor& aHTMLEditor, const EditorDOMRange& aRange, const nsINode& aCommonAncestorOfRange, const nsIContent* aFirstEntirelySelectedContentNodeInRange) const; [[nodiscard]] EditorRawDOMPoint GetShrunkenRangeEnd( const HTMLEditor& aHTMLEditor, const EditorDOMRange& aRange, const nsINode& aCommonAncestorOfRange, const nsIContent* aLastEntirelySelectedContentNodeInRange) const; /** * Helper methods to extend the range to apply the style. */ [[nodiscard]] EditorRawDOMPoint GetExtendedRangeStartToWrapAncestorApplyingSameStyle( const HTMLEditor& aHTMLEditor, const EditorRawDOMPoint& aStartPoint) const; [[nodiscard]] EditorRawDOMPoint GetExtendedRangeEndToWrapAncestorApplyingSameStyle( const HTMLEditor& aHTMLEditor, const EditorRawDOMPoint& aEndPoint) const; [[nodiscard]] EditorRawDOMRange GetExtendedRangeToMinimizeTheNumberOfNewElements( const HTMLEditor& aHTMLEditor, const nsINode& aCommonAncestor, EditorRawDOMPoint&& aStartPoint, EditorRawDOMPoint&& aEndPoint) const; /** * OnHandled() are called when this class creates new element to apply the * style, applies new style to existing element or ignores to apply the style * due to already set. */ void OnHandled(const EditorDOMPoint& aStartPoint, const EditorDOMPoint& aEndPoint) { if (!mFirstHandledPoint.IsSet()) { mFirstHandledPoint = aStartPoint; } mLastHandledPoint = aEndPoint; } void OnHandled(nsIContent& aContent) { if (aContent.IsElement() && !HTMLEditUtils::IsContainerNode(aContent)) { if (!mFirstHandledPoint.IsSet()) { mFirstHandledPoint.Set(&aContent); } mLastHandledPoint.SetAfter(&aContent); return; } if (!mFirstHandledPoint.IsSet()) { mFirstHandledPoint.Set(&aContent, 0u); } mLastHandledPoint = EditorDOMPoint::AtEndOf(aContent); } // mFirstHandledPoint and mLastHandledPoint store the first and last points // which are newly created or apply the new style, or just ignored at trying // to split a text node. EditorDOMPoint mFirstHandledPoint; EditorDOMPoint mLastHandledPoint; }; /** * AutoMoveOneLineHandler moves the content in a line (between line breaks/block * boundaries) to specific point or end of a container element. */ class MOZ_STACK_CLASS HTMLEditor::AutoMoveOneLineHandler final { public: /** * Use this constructor when you want a line to move specific point. */ explicit AutoMoveOneLineHandler(const EditorDOMPoint& aPointToInsert) : mPointToInsert(aPointToInsert), mMoveToEndOfContainer(MoveToEndOfContainer::No) { MOZ_ASSERT(mPointToInsert.IsSetAndValid()); MOZ_ASSERT(mPointToInsert.IsInContentNode()); } /** * Use this constructor when you want a line to move end of * aNewContainerElement. */ explicit AutoMoveOneLineHandler(Element& aNewContainerElement) : mPointToInsert(&aNewContainerElement, 0), mMoveToEndOfContainer(MoveToEndOfContainer::Yes) { MOZ_ASSERT(mPointToInsert.IsSetAndValid()); } /** * Must be called before calling Run(). * * @param aHTMLEditor The HTML editor. * @param aPointInHardLine A point in a line which you want to move. * @param aEditingHost The editing host. */ [[nodiscard]] nsresult Prepare(HTMLEditor& aHTMLEditor, const EditorDOMPoint& aPointInHardLine, const Element& aEditingHost); /** * Must be called if Prepare() returned NS_OK. * * @param aHTMLEditor The HTML editor. * @param aEditingHost The editing host. */ [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result Run( HTMLEditor& aHTMLEditor, const Element& aEditingHost); /** * Returns true if there are some content nodes which can be moved to another * place or deleted in the line containing aPointInHardLine. Note that if * there is only a padding
element in an empty block element, this * returns false even though it may be deleted. */ static Result CanMoveOrDeleteSomethingInLine( const EditorDOMPoint& aPointInHardLine, const Element& aEditingHost); AutoMoveOneLineHandler(const AutoMoveOneLineHandler& aOther) = delete; AutoMoveOneLineHandler(AutoMoveOneLineHandler&& aOther) = delete; private: [[nodiscard]] bool ForceMoveToEndOfContainer() const { return mMoveToEndOfContainer == MoveToEndOfContainer::Yes; } [[nodiscard]] EditorDOMPoint& NextInsertionPointRef() { if (ForceMoveToEndOfContainer()) { mPointToInsert.SetToEndOf(mPointToInsert.GetContainer()); } return mPointToInsert; } /** * Consider whether Run() should preserve or does not preserve white-space * style of moving content. * * @param aContentInLine Specify a content node in the moving line. * Typically, container of aPointInHardLine of * Prepare(). * @param aInclusiveAncestorBlockOfInsertionPoint * Inclusive ancestor block element of insertion * point. Typically, computed * mDestInclusiveAncestorBlock. */ [[nodiscard]] static PreserveWhiteSpaceStyle ConsiderWhetherPreserveWhiteSpaceStyle( const nsIContent* aContentInLine, const Element* aInclusiveAncestorBlockOfInsertionPoint); /** * Look for inclusive ancestor block element of aBlockElement and a descendant * of aAncestorElement. If aBlockElement and aAncestorElement are same one, * this returns nullptr. * * @param aBlockElement A block element which is a descendant of * aAncestorElement. * @param aAncestorElement An inclusive ancestor block element of * aBlockElement. */ [[nodiscard]] static Element* GetMostDistantInclusiveAncestorBlockInSpecificAncestorElement( Element& aBlockElement, const Element& aAncestorElement); /** * Split ancestors at the line range boundaries and collect array of contents * in the line to aOutArrayOfContents. Specify aNewContainer to the container * of insertion point to avoid splitting the destination. */ [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result SplitToMakeTheLineIsolated( HTMLEditor& aHTMLEditor, const nsIContent& aNewContainer, const Element& aEditingHost, nsTArray>& aOutArrayOfContents) const; /** * Delete unnecessary trailing line break in aMovedContentRange if there is. */ [[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult DeleteUnnecessaryTrailingLineBreakInMovedLineEnd( HTMLEditor& aHTMLEditor, const EditorDOMRange& aMovedContentRange, const Element& aEditingHost) const; // Range of selected line. EditorDOMRange mLineRange; // Next insertion point. If mMoveToEndOfContainer is `Yes`, this is // recomputed with its container in NextInsertionPointRef. Therefore, this // should not be referred directly. EditorDOMPoint mPointToInsert; // An inclusive ancestor block element of the moving line. RefPtr mSrcInclusiveAncestorBlock; // An inclusive ancestor block element of the insertion point. RefPtr mDestInclusiveAncestorBlock; // nullptr if mMovingToParentBlock is false. // Must be non-nullptr if mMovingToParentBlock is true. The topmost ancestor // block element which contains mSrcInclusiveAncestorBlock and a descendant of // mDestInclusiveAncestorBlock. I.e., this may be same as // mSrcInclusiveAncestorBlock, but never same as mDestInclusiveAncestorBlock. RefPtr mTopmostSrcAncestorBlockInDestBlock; enum class MoveToEndOfContainer { No, Yes }; MoveToEndOfContainer mMoveToEndOfContainer; PreserveWhiteSpaceStyle mPreserveWhiteSpaceStyle = PreserveWhiteSpaceStyle::No; // true if mDestInclusiveAncestorBlock is an ancestor of // mSrcInclusiveAncestorBlock. bool mMovingToParentBlock = false; }; /** * Convert contents around aRanges of Run() to specified list element. If there * are some different type of list elements, this method converts them to * specified list items too. Basically, each line will be wrapped in a list * item element. However, only when

element is selected, its child
* elements won't be treated as line separators. Perhaps, this is a bug. */ class MOZ_STACK_CLASS HTMLEditor::AutoListElementCreator final { public: /** * @param aListElementTagName The new list element tag name. * @param aListItemElementTagName The new list item element tag name. * @param aBulletType If this is not empty string, it's set * to `type` attribute of new list item * elements. Otherwise, existing `type` * attributes will be removed. */ AutoListElementCreator(const nsStaticAtom& aListElementTagName, const nsStaticAtom& aListItemElementTagName, const nsAString& aBulletType) // Needs const_cast hack here because the struct users may want // non-const nsStaticAtom pointer due to bug 1794954 : mListTagName(const_cast(aListElementTagName)), mListItemTagName(const_cast(aListItemElementTagName)), mBulletType(aBulletType) { MOZ_ASSERT(&mListTagName == nsGkAtoms::ul || &mListTagName == nsGkAtoms::ol || &mListTagName == nsGkAtoms::dl); MOZ_ASSERT_IF( &mListTagName == nsGkAtoms::ul || &mListTagName == nsGkAtoms::ol, &mListItemTagName == nsGkAtoms::li); MOZ_ASSERT_IF(&mListTagName == nsGkAtoms::dl, &mListItemTagName == nsGkAtoms::dt || &mListItemTagName == nsGkAtoms::dd); } /** * @param aHTMLEditor The HTML editor. * @param aRanges [in/out] The ranges which will be converted to list. * The instance must not have saved ranges because it'll * be used in this method. * If succeeded, this will have selection ranges which * should be applied to `Selection`. * If failed, this keeps storing original selection * ranges. * @param aSelectAllOfCurrentList Yes if this should treat all of * ancestor list element at selection. * @param aEditingHost The editing host. */ [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result Run( HTMLEditor& aHTMLEditor, AutoClonedSelectionRangeArray& aRanges, HTMLEditor::SelectAllOfCurrentList aSelectAllOfCurrentList, const Element& aEditingHost) const; private: using ContentNodeArray = nsTArray>; using AutoContentNodeArray = AutoTArray, 64>; /** * If aSelectAllOfCurrentList is "Yes" and aRanges is in a list element, * returns the list element. * Otherwise, extend aRanges to select start and end lines selected by it and * correct all topmost content nodes in the extended ranges with splitting * ancestors at range edges. */ [[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult SplitAtRangeEdgesAndCollectContentNodesToMoveIntoList( HTMLEditor& aHTMLEditor, AutoClonedRangeArray& aRanges, SelectAllOfCurrentList aSelectAllOfCurrentList, const Element& aEditingHost, ContentNodeArray& aOutArrayOfContents) const; /** * Return true if aArrayOfContents has only
elements or empty inline * container elements. I.e., it means that aArrayOfContents represents * only empty line(s) if this returns true. */ [[nodiscard]] static bool IsEmptyOrContainsOnlyBRElementsOrEmptyInlineElements( const ContentNodeArray& aArrayOfContents); /** * Delete all content nodes ina ArrayOfContents, and if we can put new list * element at start of the first range of aRanges, insert new list element * there. * * @return The empty list item element in new list element. */ [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result, nsresult> ReplaceContentNodesWithEmptyNewList( HTMLEditor& aHTMLEditor, const AutoClonedRangeArray& aRanges, const AutoContentNodeArray& aArrayOfContents, const Element& aEditingHost) const; /** * Creat new list elements or use existing list elements and move * aArrayOfContents into list item elements. * * @return A list or list item element which should have caret. */ [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result, nsresult> WrapContentNodesIntoNewListElements(HTMLEditor& aHTMLEditor, AutoClonedRangeArray& aRanges, AutoContentNodeArray& aArrayOfContents, const Element& aEditingHost) const; struct MOZ_STACK_CLASS AutoHandlingState final { // Current list element which is a good container to create new list item // element. RefPtr mCurrentListElement; // Previously handled list item element. RefPtr mPreviousListItemElement; // List or list item element which should have caret after handling all // contents. RefPtr mListOrListItemElementToPutCaret; // Replacing block element. This is typically already removed from the DOM // tree. RefPtr mReplacingBlockElement; // Once id attribute of mReplacingBlockElement copied, the id attribute // shouldn't be copied again. bool mMaybeCopiedReplacingBlockElementId = false; }; /** * Helper methods of WrapContentNodesIntoNewListElements. They are called for * handling one content node of aArrayOfContents. It's set to aHandling*. */ [[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult HandleChildContent( HTMLEditor& aHTMLEditor, nsIContent& aHandlingContent, AutoHandlingState& aState, const Element& aEditingHost) const; [[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult HandleChildListElement(HTMLEditor& aHTMLEditor, Element& aHandlingListElement, AutoHandlingState& aState) const; [[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult HandleChildListItemElement( HTMLEditor& aHTMLEditor, Element& aHandlingListItemElement, AutoHandlingState& aState) const; [[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult HandleChildListItemInDifferentTypeList(HTMLEditor& aHTMLEditor, Element& aHandlingListItemElement, AutoHandlingState& aState) const; [[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult HandleChildListItemInSameTypeList( HTMLEditor& aHTMLEditor, Element& aHandlingListItemElement, AutoHandlingState& aState) const; [[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult HandleChildDivOrParagraphElement( HTMLEditor& aHTMLEditor, Element& aHandlingDivOrParagraphElement, AutoHandlingState& aState, const Element& aEditingHost) const; enum class EmptyListItem { NotCreate, Create }; [[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult CreateAndUpdateCurrentListElement( HTMLEditor& aHTMLEditor, const EditorDOMPoint& aPointToInsert, EmptyListItem aEmptyListItem, AutoHandlingState& aState, const Element& aEditingHost) const; [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result AppendListItemElement(HTMLEditor& aHTMLEditor, const Element& aListElement, AutoHandlingState& aState) const; [[nodiscard]] MOZ_CAN_RUN_SCRIPT static nsresult MaybeCloneAttributesToNewListItem(HTMLEditor& aHTMLEditor, Element& aListItemElement, AutoHandlingState& aState); [[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult HandleChildInlineContent( HTMLEditor& aHTMLEditor, nsIContent& aHandlingInlineContent, AutoHandlingState& aState) const; [[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult WrapContentIntoNewListItemElement( HTMLEditor& aHTMLEditor, nsIContent& aHandlingContent, AutoHandlingState& aState) const; /** * If aRanges is collapsed outside aListItemOrListToPutCaret, this collapse * aRanges in aListItemOrListToPutCaret again. */ nsresult EnsureCollapsedRangeIsInListItemOrListElement( Element& aListItemOrListToPutCaret, AutoClonedRangeArray& aRanges) const; MOZ_KNOWN_LIVE nsStaticAtom& mListTagName; MOZ_KNOWN_LIVE nsStaticAtom& mListItemTagName; const nsAutoString mBulletType; }; /****************************************************************************** * NormalizedStringToInsertText stores normalized insertion string with * normalized surrounding white-spaces if the insertion point is surrounded by * collapsible white-spaces. For deleting invisible (collapsed) white-spaces, * this also stores the replace range and new white-space length before and * after the inserting text. ******************************************************************************/ struct MOZ_STACK_CLASS HTMLEditor::NormalizedStringToInsertText final { NormalizedStringToInsertText( const nsAString& aStringToInsertWithoutSurroundingWhiteSpaces, const EditorDOMPoint& aPointToInsert) : mNormalizedString(aStringToInsertWithoutSurroundingWhiteSpaces), mReplaceStartOffset( aPointToInsert.IsInTextNode() ? aPointToInsert.Offset() : 0u), mReplaceEndOffset(mReplaceStartOffset) { MOZ_ASSERT(aStringToInsertWithoutSurroundingWhiteSpaces.Length() == InsertingTextLength()); } NormalizedStringToInsertText( const nsAString& aStringToInsertWithSurroundingWhiteSpaces, uint32_t aInsertOffset, uint32_t aReplaceStartOffset, uint32_t aReplaceLength, uint32_t aNewPrecedingWhiteSpaceLengthBeforeInsertionString, uint32_t aNewFollowingWhiteSpaceLengthAfterInsertionString) : mNormalizedString(aStringToInsertWithSurroundingWhiteSpaces), mReplaceStartOffset(aReplaceStartOffset), mReplaceEndOffset(mReplaceStartOffset + aReplaceLength), mReplaceLengthBefore(aInsertOffset - mReplaceStartOffset), mReplaceLengthAfter(aReplaceLength - mReplaceLengthBefore), mNewLengthBefore(aNewPrecedingWhiteSpaceLengthBeforeInsertionString), mNewLengthAfter(aNewFollowingWhiteSpaceLengthAfterInsertionString) { MOZ_ASSERT(aReplaceStartOffset <= aInsertOffset); MOZ_ASSERT(aReplaceStartOffset + aReplaceLength >= aInsertOffset); MOZ_ASSERT(aNewPrecedingWhiteSpaceLengthBeforeInsertionString + aNewFollowingWhiteSpaceLengthAfterInsertionString < mNormalizedString.Length()); MOZ_ASSERT(mReplaceLengthBefore + mReplaceLengthAfter == ReplaceLength()); MOZ_ASSERT(mReplaceLengthBefore >= mNewLengthBefore); MOZ_ASSERT(mReplaceLengthAfter >= mNewLengthAfter); } NormalizedStringToInsertText GetMinimizedData(const Text& aText) const { if (mNormalizedString.IsEmpty() || !ReplaceLength()) { return *this; } const nsTextFragment& textFragment = aText.TextFragment(); const uint32_t minimizedReplaceStart = [&]() { const auto firstDiffCharOffset = mNewLengthBefore ? textFragment.FindFirstDifferentCharOffset( PrecedingWhiteSpaces(), mReplaceStartOffset) : nsTextFragment::kNotFound; if (firstDiffCharOffset == nsTextFragment::kNotFound) { return // We don't need to insert new normalized white-spaces before the // inserting string, (mReplaceStartOffset + mReplaceLengthBefore) // but keep extending the replacing range for deleting invisible // white-spaces. - DeletingPrecedingInvisibleWhiteSpaces(); } return firstDiffCharOffset; }(); const uint32_t minimizedReplaceEnd = [&]() { const auto lastDiffCharOffset = mNewLengthAfter ? textFragment.RFindFirstDifferentCharOffset( FollowingWhiteSpaces(), mReplaceEndOffset) : nsTextFragment::kNotFound; if (lastDiffCharOffset == nsTextFragment::kNotFound) { return // We don't need to insert new normalized white-spaces after the // inserting string, (mReplaceEndOffset - mReplaceLengthAfter) // but keep extending the replacing range for deleting invisible // white-spaces. + DeletingFollowingInvisibleWhiteSpaces(); } return lastDiffCharOffset + 1u; }(); if (minimizedReplaceStart == mReplaceStartOffset && minimizedReplaceEnd == mReplaceEndOffset) { return *this; } const uint32_t newPrecedingWhiteSpaceLength = mNewLengthBefore - (minimizedReplaceStart - mReplaceStartOffset); const uint32_t newFollowingWhiteSpaceLength = mNewLengthAfter - (mReplaceEndOffset - minimizedReplaceEnd); return NormalizedStringToInsertText( Substring(mNormalizedString, mNewLengthBefore - newPrecedingWhiteSpaceLength, mNormalizedString.Length() - (mNewLengthBefore - newPrecedingWhiteSpaceLength) - (mNewLengthAfter - newFollowingWhiteSpaceLength)), OffsetToInsertText(), minimizedReplaceStart, minimizedReplaceEnd - minimizedReplaceStart, newPrecedingWhiteSpaceLength, newFollowingWhiteSpaceLength); } /** * Return offset to insert the given text. */ [[nodiscard]] uint32_t OffsetToInsertText() const { return mReplaceStartOffset + mReplaceLengthBefore; } /** * Return inserting text length not containing the surrounding white-spaces. */ [[nodiscard]] uint32_t InsertingTextLength() const { return mNormalizedString.Length() - mNewLengthBefore - mNewLengthAfter; } /** * Return end offset of inserted string after replacing the text with * mNormalizedString. */ [[nodiscard]] uint32_t EndOffsetOfInsertedText() const { return OffsetToInsertText() + InsertingTextLength(); } /** * Return the length to replace with mNormalizedString. The result means that * it's the length of surrounding white-spaces at the insertion point. */ [[nodiscard]] uint32_t ReplaceLength() const { return mReplaceEndOffset - mReplaceStartOffset; } [[nodiscard]] uint32_t DeletingPrecedingInvisibleWhiteSpaces() const { return mReplaceLengthBefore - mNewLengthBefore; } [[nodiscard]] uint32_t DeletingFollowingInvisibleWhiteSpaces() const { return mReplaceLengthAfter - mNewLengthAfter; } [[nodiscard]] nsDependentSubstring PrecedingWhiteSpaces() const { return Substring(mNormalizedString, 0u, mNewLengthBefore); } [[nodiscard]] nsDependentSubstring FollowingWhiteSpaces() const { return Substring(mNormalizedString, mNormalizedString.Length() - mNewLengthAfter); } // Normalizes string which should be inserted. nsAutoString mNormalizedString; // Start offset in the `Text` to replace. const uint32_t mReplaceStartOffset; // End offset in the `Text` to replace. const uint32_t mReplaceEndOffset; // If it needs to replace preceding and/or following white-spaces, these // members store the length of white-spaces which should be replaced // before/after the insertion point. const uint32_t mReplaceLengthBefore = 0u; const uint32_t mReplaceLengthAfter = 0u; // If it needs to replace preceding and/or following white-spaces, these // members store the new length of white-spaces before/after the insertion // string. const uint32_t mNewLengthBefore = 0u; const uint32_t mNewLengthAfter = 0u; }; /****************************************************************************** * ReplaceWhiteSpacesData stores normalized string to replace white-spaces in * a `Text`. If ReplaceLength() returns 0, this user needs to do nothing. ******************************************************************************/ struct MOZ_STACK_CLASS HTMLEditor::ReplaceWhiteSpacesData final { ReplaceWhiteSpacesData() = default; /** * @param aWhiteSpaces The new white-spaces which we will replace the * range with. * @param aStartOffset Replace start offset in the text node. * @param aReplaceLength Replace length in the text node. * @param aOffsetAfterReplacing * [optional] If the caller may want to put caret * middle of the white-spaces, the offset may be * changed by deleting some invisible white-spaces. * Therefore, this may be set for the purpose. */ ReplaceWhiteSpacesData(const nsAString& aWhiteSpaces, uint32_t aStartOffset, uint32_t aReplaceLength, uint32_t aOffsetAfterReplacing = UINT32_MAX) : mNormalizedString(aWhiteSpaces), mReplaceStartOffset(aStartOffset), mReplaceEndOffset(aStartOffset + aReplaceLength), mNewOffsetAfterReplace(aOffsetAfterReplacing) { MOZ_ASSERT(ReplaceLength() >= mNormalizedString.Length()); MOZ_ASSERT_IF(mNewOffsetAfterReplace != UINT32_MAX, mNewOffsetAfterReplace <= mReplaceStartOffset + mNormalizedString.Length()); } /** * @param aWhiteSpaces The new white-spaces which we will replace the * range with. * @param aStartOffset Replace start offset in the text node. * @param aReplaceLength Replace length in the text node. * @param aOffsetAfterReplacing * [optional] If the caller may want to put caret * middle of the white-spaces, the offset may be * changed by deleting some invisible white-spaces. * Therefore, this may be set for the purpose. */ ReplaceWhiteSpacesData(nsAutoString&& aWhiteSpaces, uint32_t aStartOffset, uint32_t aReplaceLength, uint32_t aOffsetAfterReplacing = UINT32_MAX) : mNormalizedString(std::forward(aWhiteSpaces)), mReplaceStartOffset(aStartOffset), mReplaceEndOffset(aStartOffset + aReplaceLength), mNewOffsetAfterReplace(aOffsetAfterReplacing) { MOZ_ASSERT(ReplaceLength() >= mNormalizedString.Length()); MOZ_ASSERT_IF(mNewOffsetAfterReplace != UINT32_MAX, mNewOffsetAfterReplace <= mReplaceStartOffset + mNormalizedString.Length()); } ReplaceWhiteSpacesData GetMinimizedData(const Text& aText) const { if (!ReplaceLength()) { return *this; } const nsTextFragment& textFragment = aText.TextFragment(); const auto minimizedReplaceStart = [&]() -> uint32_t { if (mNormalizedString.IsEmpty()) { return mReplaceStartOffset; } const uint32_t firstDiffCharOffset = textFragment.FindFirstDifferentCharOffset(mNormalizedString, mReplaceStartOffset); if (firstDiffCharOffset == nsTextFragment::kNotFound) { // We don't need to insert new white-spaces, return mReplaceStartOffset + mNormalizedString.Length(); } return firstDiffCharOffset; }(); const auto minimizedReplaceEnd = [&]() -> uint32_t { if (mNormalizedString.IsEmpty()) { return mReplaceEndOffset; } if (minimizedReplaceStart == mReplaceStartOffset + mNormalizedString.Length()) { // Note that here may be invisible white-spaces before // mReplaceEndOffset. Then, this value may be larger than // minimizedReplaceStart. MOZ_ASSERT(mReplaceEndOffset >= minimizedReplaceStart); return mReplaceEndOffset; } if (ReplaceLength() != mNormalizedString.Length()) { // If we're deleting some invisible white-spaces, don't shrink the end // of the replacing range because it may shrink mNormalizedString too // much. return mReplaceEndOffset; } const auto lastDiffCharOffset = textFragment.RFindFirstDifferentCharOffset(mNormalizedString, mReplaceEndOffset); MOZ_ASSERT(lastDiffCharOffset != nsTextFragment::kNotFound); return lastDiffCharOffset == nsTextFragment::kNotFound ? mReplaceEndOffset : lastDiffCharOffset + 1u; }(); if (minimizedReplaceStart == mReplaceStartOffset && minimizedReplaceEnd == mReplaceEndOffset) { return *this; } const uint32_t precedingUnnecessaryLength = minimizedReplaceStart - mReplaceStartOffset; const uint32_t followingUnnecessaryLength = mReplaceEndOffset - minimizedReplaceEnd; return ReplaceWhiteSpacesData( Substring(mNormalizedString, precedingUnnecessaryLength, mNormalizedString.Length() - (precedingUnnecessaryLength + followingUnnecessaryLength)), minimizedReplaceStart, minimizedReplaceEnd - minimizedReplaceStart, mNewOffsetAfterReplace); } /** * Return the normalized string before mNewOffsetAfterReplace. So, * mNewOffsetAfterReplace must not be UINT32_MAX and in the replaced range * when this is called. * * @param aReplaceEndOffset Specify the offset in the Text node of * mNewOffsetAfterReplace before replacing with the * data. * @return The substring before mNewOffsetAfterReplace which is typically set * for new caret position in the Text node or collapsed deleting range * surrounded by the white-spaces. */ [[nodiscard]] ReplaceWhiteSpacesData PreviousDataOfNewOffset( uint32_t aReplaceEndOffset) const { MOZ_ASSERT(mNewOffsetAfterReplace != UINT32_MAX); MOZ_ASSERT(mReplaceStartOffset <= mNewOffsetAfterReplace); MOZ_ASSERT(mReplaceEndOffset >= mNewOffsetAfterReplace); MOZ_ASSERT(mReplaceStartOffset <= aReplaceEndOffset); MOZ_ASSERT(mReplaceEndOffset >= aReplaceEndOffset); if (!ReplaceLength() || aReplaceEndOffset == mReplaceStartOffset) { return ReplaceWhiteSpacesData(); } return ReplaceWhiteSpacesData( Substring(mNormalizedString, 0u, mNewOffsetAfterReplace - mReplaceStartOffset), mReplaceStartOffset, aReplaceEndOffset - mReplaceStartOffset); } /** * Return the normalized string after mNewOffsetAfterReplace. So, * mNewOffsetAfterReplace must not be UINT32_MAX and in the replaced range * when this is called. * * @param aReplaceStartOffset Specify the replace start offset with the * normalized white-spaces. * @return The substring after mNewOffsetAfterReplace which is typically set * for new caret position in the Text node or collapsed deleting range * surrounded by the white-spaces. */ [[nodiscard]] ReplaceWhiteSpacesData NextDataOfNewOffset( uint32_t aReplaceStartOffset) const { MOZ_ASSERT(mNewOffsetAfterReplace != UINT32_MAX); MOZ_ASSERT(mReplaceStartOffset <= mNewOffsetAfterReplace); MOZ_ASSERT(mReplaceEndOffset >= mNewOffsetAfterReplace); MOZ_ASSERT(mReplaceStartOffset <= aReplaceStartOffset); MOZ_ASSERT(mReplaceEndOffset >= aReplaceStartOffset); if (!ReplaceLength() || aReplaceStartOffset == mReplaceEndOffset) { return ReplaceWhiteSpacesData(); } return ReplaceWhiteSpacesData( Substring(mNormalizedString, mNewOffsetAfterReplace - mReplaceStartOffset), aReplaceStartOffset, mReplaceEndOffset - aReplaceStartOffset); } [[nodiscard]] uint32_t ReplaceLength() const { return mReplaceEndOffset - mReplaceStartOffset; } [[nodiscard]] uint32_t DeletingInvisibleWhiteSpaces() const { return ReplaceLength() - mNormalizedString.Length(); } [[nodiscard]] ReplaceWhiteSpacesData operator+( const ReplaceWhiteSpacesData& aOther) const { if (!ReplaceLength()) { return aOther; } if (!aOther.ReplaceLength()) { return *this; } MOZ_ASSERT(mReplaceEndOffset == aOther.mReplaceStartOffset); MOZ_ASSERT_IF( aOther.mNewOffsetAfterReplace != UINT32_MAX, aOther.mNewOffsetAfterReplace >= DeletingInvisibleWhiteSpaces()); return ReplaceWhiteSpacesData( nsAutoString(mNormalizedString + aOther.mNormalizedString), mReplaceStartOffset, aOther.mReplaceEndOffset, aOther.mNewOffsetAfterReplace != UINT32_MAX ? aOther.mNewOffsetAfterReplace - DeletingInvisibleWhiteSpaces() : mNewOffsetAfterReplace); } nsAutoString mNormalizedString; const uint32_t mReplaceStartOffset = 0u; const uint32_t mReplaceEndOffset = 0u; // If the caller specifies a point in a white-space sequence, some invisible // white-spaces will be deleted with replacing them with normalized string. // Then, they may want to keep the position for putting caret or something. // So, this may store a specific offset in the text node after replacing. const uint32_t mNewOffsetAfterReplace = UINT32_MAX; }; } // namespace mozilla #endif // #ifndef HTMLEditorNestedClasses_h