/* -*- 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/CharacterDataBuffer.h" #include "mozilla/dom/Text.h" namespace mozilla { struct LimitersAndCaretData; // Declared in nsFrameSelection.h namespace dom { class HTMLBRElement; }; /***************************************************************************** * 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; }; /** * Handle "insertParagraph" command. */ class MOZ_STACK_CLASS HTMLEditor::AutoInsertParagraphHandler final { public: AutoInsertParagraphHandler() = delete; AutoInsertParagraphHandler(const AutoInsertParagraphHandler&) = delete; AutoInsertParagraphHandler(AutoInsertParagraphHandler&&) = delete; MOZ_CAN_RUN_SCRIPT explicit AutoInsertParagraphHandler( HTMLEditor& aHTMLEditor, const Element& aEditingHost) : mHTMLEditor(aHTMLEditor), mEditingHost(aEditingHost), mDefaultParagraphSeparatorTagName( aHTMLEditor.DefaultParagraphSeparatorTagName()), mDefaultParagraphSeparator(aHTMLEditor.GetDefaultParagraphSeparator()) { } [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result Run(); private: /** * Insert
element. * * @param aPointToInsert The position where the new
should be * inserted. * @param aBlockElementWhichShouldHaveCaret * [optional] If set, this collapse selection into * the element with * CollapseSelectionToPointOrIntoBlockWhichShouldHaveCaret(). */ [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result HandleInsertBRElement( const EditorDOMPoint& aPointToInsert, const Element* aBlockElementWhichShouldHaveCaret = nullptr); /** * Insert a linefeed. */ [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result HandleInsertLinefeed(const EditorDOMPoint& aPointToInsert); /** * SplitParagraphWithTransaction() splits the parent block, aParentDivOrP, at * aPointToSplit. * * @param aBlockElementToSplit The current paragraph which should be split. * @param aPointToSplit The point to split aBlockElementToSplit. */ [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result SplitParagraphWithTransaction(Element& aBlockElementToSplit, const EditorDOMPoint& aPointToSplit); /** * Delete preceding invisible line break before aPointToSplit if and only if * there is. * * @return New point to split aBlockElementToSplit */ [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result EnsureNoInvisibleLineBreakBeforePointToSplit( const Element& aBlockElementToSplit, const EditorDOMPoint& aPointToSplit); /** * Maybe insert a
element if it's required to keep the inline container * visible after splitting aBlockElementToSplit at aPointToSplit. * * @return New point to split aBlockElementToSplit */ [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result MaybeInsertFollowingBRElementToPreserveRightBlock( const Element& aBlockElementToSplit, const EditorDOMPoint& aPointToSplit); /** * Return true if the HTMLEditor is in the mode which `insertParagraph` should * always create a new paragraph or in the cases that we create a new * paragraph in the legacy mode. */ [[nodiscard]] bool ShouldCreateNewParagraph( Element& aParentDivOrP, const EditorDOMPoint& aPointToSplit) const; /** * Return true if aBRElement is nullptr or an invisible
or a padding
* for making the last empty line visible. */ [[nodiscard]] static bool IsNullOrInvisibleBRElementOrPaddingOneForEmptyLastLine( const dom::HTMLBRElement* aBRElement); /** * Handle insertParagraph command (i.e., handling Enter key press) in a * heading element. This splits aHeadingElement element at aPointToSplit. * Then, if right heading element is empty, it'll be removed and new paragraph * is created (its type is decided with default paragraph separator). * * @param aHeadingElement The heading element to be split. * @param aPointToSplit The point to split aHeadingElement. * @return New paragraph element, meaning right heading * element if aHeadingElement is split, or newly * created or existing paragraph element. */ [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result HandleInHeadingElement(Element& aHeadingElement, const EditorDOMPoint& aPointToSplit); /** * Handle insertParagraph command at end of a heading element. */ [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result HandleAtEndOfHeadingElement(Element& aHeadingElement); /** * Handle insertParagraph command (i.e., handling Enter key press) in a list * item element. * * @param aListItemElement The list item which has the following point. * @param aPointToSplit The point to split aListItemElement. * @return New paragraph element, meaning right list item * element if aListItemElement is split, or newly * created paragraph element. */ [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result HandleInListItemElement(Element& aListItemElement, const EditorDOMPoint& aPointToSplit); /** * Split aMailCiteElement at aPointToSplit. * * @param aMailCiteElement The mail-cite element which should be split. * @param aPointToSplit The point to split. * @return Candidate caret position where is at inserted *
element into the split point. */ [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result HandleInMailCiteElement(Element& aMailCiteElement, const EditorDOMPoint& aPointToSplit); /** * Insert a
element into aPointToBreak. * This may split container elements at the point and/or may move following *
element to immediately after the new
element if necessary. * * @param aPointToBreak The point where new
element will be * inserted before. * @return If succeeded, returns new
element and * candidate caret point. */ [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result InsertBRElement(const EditorDOMPoint& aPointToBreak); /** * Return true if we should insert a line break instead of a paragraph. */ [[nodiscard]] MOZ_CAN_RUN_SCRIPT bool ShouldInsertLineBreakInstead( const Element* aEditableBlockElement, const EditorDOMPoint& aCandidatePointToSplit); enum class InsertBRElementIntoEmptyBlock : bool { Start, End }; /** * Make sure that aMaybeBlockElement is visible with putting a
element if * and only if it's an empty block element. */ [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result InsertBRElementIfEmptyBlockElement( Element& aMaybeBlockElement, InsertBRElementIntoEmptyBlock aInsertBRElementIntoEmptyBlock, BlockInlineCheck aBlockInlineCheck); /** * Split aMailCiteElement at aPointToSplit. This deletes all inclusive * ancestors of aPointToSplit in aMailCiteElement too. */ [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result SplitMailCiteElement(const EditorDOMPoint& aPointToSplit, Element& aMailCiteElement); /** * aMailCiteElement may be a element which is styled as block. If it's * followed by a block boundary, it requires a padding
element when it's * serialized. This method may insert a
element if it's required. */ [[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult MaybeInsertPaddingBRElementToInlineMailCiteElement( const EditorDOMPoint& aPointToInsertBRElement, Element& aMailCiteElement); /** * Return the deepest inline container element which is the first leaf or the * first leaf container of aBlockElement. */ [[nodiscard]] static Element* GetDeepestFirstChildInlineContainerElement( Element& aBlockElement); /** * Collapse `Selection` to aCandidatePointToPutCaret or into * aBlockElementShouldHaveCaret. If aBlockElementShouldHaveCaret is specified * and aCandidatePointToPutCaret is outside it, this ignores * aCandidatePointToPutCaret and collapse `Selection` into * aBlockElementShouldHaveCaret. */ [[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult CollapseSelectionToPointOrIntoBlockWhichShouldHaveCaret( const EditorDOMPoint& aCandidatePointToPutCaret, const Element* aBlockElementShouldHaveCaret, const SuggestCaretOptions& aOptions); /** * Return a better point to split the paragraph to avoid to keep a typing in a * link or a paragraph in list item in the new paragraph. */ [[nodiscard]] EditorDOMPoint GetBetterPointToSplitParagraph( const Element& aBlockElementToSplit, const EditorDOMPoint& aCandidatePointToSplit); enum class IgnoreBlockBoundaries : bool { No, Yes }; /** * Return true if splitting aBlockElementToSplit at aPointToSplit will create * empty left element. * * @param aBlockElementToSplit The paragraph element which we want to * split. * @param aPointToSplit The split position in aBlockElementToSplit. * @param aIgnoreBlockBoundaries If No, return true only when aPointToSplit * is immediately after a block boundary of * aBlockElementToSplit. In other words, * may return true only when aPointToSplit * is not in a child block of * aBlockElementToSplit. * If Yes, return true even when aPointToSplit * is immediately after any current block * boundary which is followed by the block * boundary of aBlockElementToSplit. In other * words, return true when aPointToSplit is in * a child block which is start of any ancestor * block elements. */ [[nodiscard]] static bool SplitPointIsStartOfSplittingBlock( const Element& aBlockElementToSplit, const EditorDOMPoint& aPointToSplit, IgnoreBlockBoundaries aIgnoreBlockBoundaries); /** * Return true if splitting aBlockElementToSplit at aPointToSplit will create * empty right element. * * @param aBlockElementToSplit The paragraph element which we want to * split. * @param aPointToSplit The split position in aBlockElementToSplit. * @param aIgnoreBlockBoundaries If No, return true only when aPointToSplit * is immediately before a block boundary of * aBlockElementToSplit. In other words, * may return true only when aPointToSplit * is not in a child block of * aBlockElementToSplit. * If Yes, return true even when aPointToSplit * is immediately before any current block * boundary which is followed by the block * boundary of aBlockElementToSplit. In other * words, return true when aPointToSplit is in * a child block which is end of any ancestor * block elements. */ [[nodiscard]] static bool SplitPointIsEndOfSplittingBlock( const Element& aBlockElementToSplit, const EditorDOMPoint& aPointToSplit, IgnoreBlockBoundaries aIgnoreBlockBoundaries); MOZ_KNOWN_LIVE HTMLEditor& mHTMLEditor; MOZ_KNOWN_LIVE const Element& mEditingHost; MOZ_KNOWN_LIVE nsStaticAtom& mDefaultParagraphSeparatorTagName; const ParagraphSeparator mDefaultParagraphSeparator; }; /** * Handle "insertLineBreak" command. */ class MOZ_STACK_CLASS HTMLEditor::AutoInsertLineBreakHandler final { public: AutoInsertLineBreakHandler() = delete; AutoInsertLineBreakHandler(const AutoInsertLineBreakHandler&) = delete; AutoInsertLineBreakHandler(AutoInsertLineBreakHandler&&) = delete; MOZ_CAN_RUN_SCRIPT explicit AutoInsertLineBreakHandler( HTMLEditor& aHTMLEditor, const Element& aEditingHost) : mHTMLEditor(aHTMLEditor), mEditingHost(aEditingHost) {} [[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult Run(); private: /** * Insert a linefeed character into aPointToBreak. * * @param aPointToBreak The point where new linefeed character will be * inserted before. * @param aEditingHost Current active editing host. * @return A suggest point to put caret. */ [[nodiscard]] static MOZ_CAN_RUN_SCRIPT Result InsertLinefeed(HTMLEditor& aHTMLEditor, const EditorDOMPoint& aPointToBreak, const Element& aEditingHost); /** * Insert
element at `Selection`. */ [[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult HandleInsertBRElement(); /** * Insert a linefeed at `Selection`. */ [[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult HandleInsertLinefeed(); friend class AutoInsertParagraphHandler; MOZ_KNOWN_LIVE HTMLEditor& mHTMLEditor; MOZ_KNOWN_LIVE const Element& mEditingHost; }; /** * Handle delete multiple ranges, typically they are the selection ranges. */ class MOZ_STACK_CLASS HTMLEditor::AutoDeleteRangesHandler final { public: explicit AutoDeleteRangesHandler( const AutoDeleteRangesHandler* aParent = nullptr) : mParent(aParent), mOriginalDirectionAndAmount(nsIEditor::eNone), mOriginalStripWrappers(nsIEditor::eNoStrip) {} /** * ComputeRangesToDelete() computes actual deletion ranges. */ [[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult ComputeRangesToDelete( const HTMLEditor& aHTMLEditor, nsIEditor::EDirection aDirectionAndAmount, AutoClonedSelectionRangeArray& aRangesToDelete, const Element& aEditingHost); /** * Deletes content in or around aRangesToDelete. * NOTE: This method creates SelectionBatcher. Therefore, each caller * needs to check if the editor is still available even if this returns * NS_OK. */ [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result Run( HTMLEditor& aHTMLEditor, nsIEditor::EDirection aDirectionAndAmount, nsIEditor::EStripWrappers aStripWrappers, AutoClonedSelectionRangeArray& aRangesToDelete, const Element& aEditingHost); private: [[nodiscard]] bool IsHandlingRecursively() const { return mParent != nullptr; } [[nodiscard]] bool CanFallbackToDeleteRangeWithTransaction( const nsRange& aRangeToDelete) const; [[nodiscard]] bool CanFallbackToDeleteRangesWithTransaction( const AutoClonedSelectionRangeArray& aRangesToDelete) const; /** * HandleDeleteAroundCollapsedRanges() handles deletion with collapsed * ranges. Callers must guarantee that this is called only when * aRangesToDelete.IsCollapsed() returns true. * * @param aDirectionAndAmount Direction of the deletion. * @param aStripWrappers Must be eStrip or eNoStrip. * @param aRangesToDelete Ranges to delete. This `IsCollapsed()` must * return true. * @param aWSRunScannerAtCaret Scanner instance which scanned from * caret point. * @param aScanFromCaretPointResult Scan result of aWSRunScannerAtCaret * toward aDirectionAndAmount. * @param aEditingHost The editing host. */ [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result HandleDeleteAroundCollapsedRanges( HTMLEditor& aHTMLEditor, nsIEditor::EDirection aDirectionAndAmount, nsIEditor::EStripWrappers aStripWrappers, AutoClonedSelectionRangeArray& aRangesToDelete, const WSRunScanner& aWSRunScannerAtCaret, const WSScanResult& aScanFromCaretPointResult, const Element& aEditingHost); nsresult ComputeRangesToDeleteAroundCollapsedRanges( const HTMLEditor& aHTMLEditor, nsIEditor::EDirection aDirectionAndAmount, AutoClonedSelectionRangeArray& aRangesToDelete, const WSRunScanner& aWSRunScannerAtCaret, const WSScanResult& aScanFromCaretPointResult, const Element& aEditingHost) const; /** * HandleDeleteNonCollapsedRanges() handles deletion with non-collapsed * ranges. Callers must guarantee that this is called only when * aRangesToDelete.IsCollapsed() returns false. * * @param aDirectionAndAmount Direction of the deletion. * @param aStripWrappers Must be eStrip or eNoStrip. * @param aRangesToDelete The ranges to delete. * @param aSelectionWasCollapsed If the caller extended `Selection` * from collapsed, set this to `Yes`. * Otherwise, i.e., `Selection` is not * collapsed from the beginning, set * this to `No`. * @param aEditingHost The editing host. */ enum class SelectionWasCollapsed { Yes, No }; [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result HandleDeleteNonCollapsedRanges(HTMLEditor& aHTMLEditor, nsIEditor::EDirection aDirectionAndAmount, nsIEditor::EStripWrappers aStripWrappers, AutoClonedSelectionRangeArray& aRangesToDelete, SelectionWasCollapsed aSelectionWasCollapsed, const Element& aEditingHost); nsresult ComputeRangesToDeleteNonCollapsedRanges( const HTMLEditor& aHTMLEditor, nsIEditor::EDirection aDirectionAndAmount, AutoClonedSelectionRangeArray& aRangesToDelete, SelectionWasCollapsed aSelectionWasCollapsed, const Element& aEditingHost) const; /** * Handle deletion of collapsed ranges in a text node. * * @param aDirectionAndAmount Must be eNext or ePrevious. * @param aCaretPosition The position where caret is. This container * must be a text node. * @param aEditingHost The editing host. */ [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result HandleDeleteTextAroundCollapsedRanges( HTMLEditor& aHTMLEditor, nsIEditor::EDirection aDirectionAndAmount, AutoClonedSelectionRangeArray& aRangesToDelete, const Element& aEditingHost); nsresult ComputeRangesToDeleteTextAroundCollapsedRanges( nsIEditor::EDirection aDirectionAndAmount, AutoClonedSelectionRangeArray& aRangesToDelete) const; /** * Handle deletion of atomic elements like
,


, , , etc and * data nodes except text node (e.g., comment node). Note that don't call this * directly with `
` element. * * @param aAtomicContent The atomic content to be deleted. * @param aCaretPoint The caret point (i.e., selection start or * end). * @param aWSRunScannerAtCaret WSRunScanner instance which was initialized * with the caret point. * @param aEditingHost The editing host. */ [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result HandleDeleteAtomicContent(HTMLEditor& aHTMLEditor, nsIContent& aAtomicContent, const EditorDOMPoint& aCaretPoint, const WSRunScanner& aWSRunScannerAtCaret, const Element& aEditingHost); nsresult ComputeRangesToDeleteAtomicContent( const nsIContent& aAtomicContent, AutoClonedSelectionRangeArray& aRangesToDelete) const; /** * GetAtomicContnetToDelete() returns better content that is deletion of * atomic element. If aScanFromCaretPointResult is special, since this * point may not be editable, we look for better point to remove atomic * content. * * @param aDirectionAndAmount Direction of the deletion. * @param aWSRunScannerAtCaret WSRunScanner instance which was * initialized with the caret point. * @param aScanFromCaretPointResult Scan result of aWSRunScannerAtCaret * toward aDirectionAndAmount. */ [[nodiscard]] static nsIContent* GetAtomicContentToDelete( nsIEditor::EDirection aDirectionAndAmount, const WSRunScanner& aWSRunScannerAtCaret, const WSScanResult& aScanFromCaretPointResult) MOZ_NONNULL_RETURN; /** * HandleDeleteAtOtherBlockBoundary() handles deletion at other block boundary * (i.e., immediately before or after a block). If this does not join blocks, * `Run()` may be called recursively with creating another instance. * * @param aDirectionAndAmount Direction of the deletion. * @param aStripWrappers Must be eStrip or eNoStrip. * @param aOtherBlockElement The block element which follows the caret or * is followed by caret. * @param aCaretPoint The caret point (i.e., selection start or * end). * @param aWSRunScannerAtCaret WSRunScanner instance which was initialized * with the caret point. * @param aRangesToDelete Ranges to delete of the caller. This should * be collapsed and the point should match with * aCaretPoint. * @param aEditingHost The editing host. */ [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result HandleDeleteAtOtherBlockBoundary( HTMLEditor& aHTMLEditor, nsIEditor::EDirection aDirectionAndAmount, nsIEditor::EStripWrappers aStripWrappers, Element& aOtherBlockElement, const EditorDOMPoint& aCaretPoint, WSRunScanner& aWSRunScannerAtCaret, AutoClonedSelectionRangeArray& aRangesToDelete, const Element& aEditingHost); /** * ExtendOrShrinkRangeToDelete() extends aRangeToDelete if there are * an invisible
element and/or some parent empty elements. * * @param aLimitersAndCaretData The frame selection data. * @param aRangeToDelete The range to be extended for deletion. This * must not be collapsed, must be positioned. */ template [[nodiscard]] Result ExtendOrShrinkRangeToDelete( const HTMLEditor& aHTMLEditor, const LimitersAndCaretData& aLimitersAndCaretData, const EditorDOMRangeType& aRangeToDelete) const; /** * Extend the start boundary of aRangeToDelete to contain ancestor inline * elements which will be empty once the content in aRangeToDelete is removed * from the tree. * * NOTE: This is designed for deleting inline elements which become empty if * aRangeToDelete which crosses a block boundary of right block child. * Therefore, you may need to improve this method if you want to use this in * the other cases. * * @param aRangeToDelete [in/out] The range to delete. This start * boundary may be modified. * @param aEditingHost The editing host. * @return true if aRangeToDelete is modified. * false if aRangeToDelete is not modified. * error if aRangeToDelete gets unexpected * situation. */ [[nodiscard]] static Result ExtendRangeToContainAncestorInlineElementsAtStart( nsRange& aRangeToDelete, const Element& aEditingHost); /** * A helper method for ExtendOrShrinkRangeToDelete(). This returns shrunken * range if aRangeToDelete selects all over list elements which have some list * item elements to avoid to delete all list items from the list element. */ [[nodiscard]] static EditorRawDOMRange GetRangeToAvoidDeletingAllListItemsIfSelectingAllOverListElements( const EditorRawDOMRange& aRangeToDelete); /** * DeleteUnnecessaryNodes() removes unnecessary nodes around aRange. */ [[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult DeleteUnnecessaryNodes(HTMLEditor& aHTMLEditor, const EditorDOMRange& aRange, const Element& aEditingHost); /** * If aContent is a text node that contains only collapsed white-space or * empty and editable. */ [[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult DeleteNodeIfInvisibleAndEditableTextNode(HTMLEditor& aHTMLEditor, nsIContent& aContent); /** * DeleteParentBlocksIfEmpty() removes parent block elements if they * don't have visible contents. Note that due performance issue of * WhiteSpaceVisibilityKeeper, this call may be expensive. And also note that * this removes a empty block with a transaction. So, please make sure that * you've already created `AutoPlaceholderBatch`. * * @param aPoint The point whether this method climbing up the DOM * tree to remove empty parent blocks. * @return NS_OK if one or more empty block parents are deleted. * NS_SUCCESS_EDITOR_ELEMENT_NOT_FOUND if the point is * not in empty block. * Or NS_ERROR_* if something unexpected occurs. */ [[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult DeleteParentBlocksWithTransactionIfEmpty(HTMLEditor& aHTMLEditor, const EditorDOMPoint& aPoint, const Element& aEditingHost); [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result FallbackToDeleteRangeWithTransaction(HTMLEditor& aHTMLEditor, nsRange& aRangeToDelete) const { MOZ_ASSERT(aHTMLEditor.IsEditActionDataAvailable()); MOZ_ASSERT(CanFallbackToDeleteRangeWithTransaction(aRangeToDelete)); Result caretPointOrError = aHTMLEditor.DeleteRangeWithTransaction(mOriginalDirectionAndAmount, mOriginalStripWrappers, aRangeToDelete); NS_WARNING_ASSERTION(caretPointOrError.isOk(), "EditorBase::DeleteRangeWithTransaction() failed"); return caretPointOrError; } [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result FallbackToDeleteRangesWithTransaction( HTMLEditor& aHTMLEditor, AutoClonedSelectionRangeArray& aRangesToDelete, const Element& aEditingHost) const; /** * Compute target range(s) which will be called by * `EditorBase::DeleteRangeWithTransaction()` or * `HTMLEditor::DeleteRangesWithTransaction()`. * TODO: We should not use it for consistency with each deletion handler * in this and nested classes. */ nsresult ComputeRangeToDeleteRangeWithTransaction( const HTMLEditor& aHTMLEditor, nsIEditor::EDirection aDirectionAndAmount, nsRange& aRange, const Element& aEditingHost) const; nsresult ComputeRangesToDeleteRangesWithTransaction( const HTMLEditor& aHTMLEditor, nsIEditor::EDirection aDirectionAndAmount, AutoClonedSelectionRangeArray& aRangesToDelete, const Element& aEditingHost) const; nsresult FallbackToComputeRangeToDeleteRangeWithTransaction( const HTMLEditor& aHTMLEditor, nsRange& aRangeToDelete, const Element& aEditingHost) const { MOZ_ASSERT(aHTMLEditor.IsEditActionDataAvailable()); MOZ_ASSERT(CanFallbackToDeleteRangeWithTransaction(aRangeToDelete)); nsresult rv = ComputeRangeToDeleteRangeWithTransaction( aHTMLEditor, mOriginalDirectionAndAmount, aRangeToDelete, aEditingHost); NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "AutoDeleteRangesHandler::" "ComputeRangeToDeleteRangeWithTransaction() failed"); return rv; } nsresult FallbackToComputeRangesToDeleteRangesWithTransaction( const HTMLEditor& aHTMLEditor, AutoClonedSelectionRangeArray& aRangesToDelete, const Element& aEditingHost) const { MOZ_ASSERT(aHTMLEditor.IsEditActionDataAvailable()); MOZ_ASSERT(CanFallbackToDeleteRangesWithTransaction(aRangesToDelete)); nsresult rv = ComputeRangesToDeleteRangesWithTransaction( aHTMLEditor, mOriginalDirectionAndAmount, aRangesToDelete, aEditingHost); NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "AutoDeleteRangesHandler::" "ComputeRangesToDeleteRangesWithTransaction() failed"); return rv; } class MOZ_STACK_CLASS AutoBlockElementsJoiner; class MOZ_STACK_CLASS AutoEmptyBlockAncestorDeleter; const AutoDeleteRangesHandler* const mParent; nsIEditor::EDirection mOriginalDirectionAndAmount; nsIEditor::EStripWrappers mOriginalStripWrappers; }; /** * Handle join block elements. Despite the name, this may just move first line * of a block into another block or just deleted the range with keeping table * structure. */ class MOZ_STACK_CLASS HTMLEditor::AutoDeleteRangesHandler::AutoBlockElementsJoiner final { public: AutoBlockElementsJoiner() = delete; explicit AutoBlockElementsJoiner( AutoDeleteRangesHandler& aDeleteRangesHandler) : mDeleteRangesHandler(&aDeleteRangesHandler), mDeleteRangesHandlerConst(aDeleteRangesHandler) {} explicit AutoBlockElementsJoiner( const AutoDeleteRangesHandler& aDeleteRangesHandler) : mDeleteRangesHandler(nullptr), mDeleteRangesHandlerConst(aDeleteRangesHandler) {} /** * PrepareToDeleteAtCurrentBlockBoundary() considers left content and right * content which are joined for handling deletion at current block boundary * (i.e., at start or end of the current block). * * @param aHTMLEditor The HTML editor. * @param aDirectionAndAmount Direction of the deletion. * @param aCurrentBlockElement The current block element. * @param aCaretPoint The caret point (i.e., selection start * or end). * @param aEditingHost The editing host. * @return true if can continue to handle the * deletion. */ [[nodiscard]] bool PrepareToDeleteAtCurrentBlockBoundary( const HTMLEditor& aHTMLEditor, nsIEditor::EDirection aDirectionAndAmount, Element& aCurrentBlockElement, const EditorDOMPoint& aCaretPoint, const Element& aEditingHost); /** * PrepareToDeleteAtOtherBlockBoundary() considers left content and right * content which are joined for handling deletion at other block boundary * (i.e., immediately before or after a block). * * @param aHTMLEditor The HTML editor. * @param aDirectionAndAmount Direction of the deletion. * @param aOtherBlockElement The block element which follows the * caret or is followed by caret. * @param aCaretPoint The caret point (i.e., selection start * or end). * @param aWSRunScannerAtCaret WSRunScanner instance which was * initialized with the caret point. * @return true if can continue to handle the * deletion. */ [[nodiscard]] bool PrepareToDeleteAtOtherBlockBoundary( const HTMLEditor& aHTMLEditor, nsIEditor::EDirection aDirectionAndAmount, Element& aOtherBlockElement, const EditorDOMPoint& aCaretPoint, const WSRunScanner& aWSRunScannerAtCaret); /** * PrepareToDeleteNonCollapsedRange() considers left block element and * right block element which are inclusive ancestor block element of * start and end container of aRangeToDelete * * @param aHTMLEditor The HTML editor. * @param aRangeToDelete The range to delete. Must not be * collapsed. * @param aEditingHost The editing host. * @return true if can continue to handle the * deletion. */ [[nodiscard]] bool PrepareToDeleteNonCollapsedRange( const HTMLEditor& aHTMLEditor, const nsRange& aRangeToDelete, const Element& aEditingHost); /** * Run() executes the joining. * * @param aHTMLEditor The HTML editor. * @param aDirectionAndAmount Direction of the deletion. * @param aStripWrappers Must be eStrip or eNoStrip. * @param aCaretPoint The caret point (i.e., selection start * or end). * @param aRangeToDelete The range to delete. This should be * collapsed and match with aCaretPoint. */ [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result Run( HTMLEditor& aHTMLEditor, nsIEditor::EDirection aDirectionAndAmount, nsIEditor::EStripWrappers aStripWrappers, const EditorDOMPoint& aCaretPoint, nsRange& aRangeToDelete, const Element& aEditingHost); nsresult ComputeRangeToDelete(const HTMLEditor& aHTMLEditor, nsIEditor::EDirection aDirectionAndAmount, const EditorDOMPoint& aCaretPoint, nsRange& aRangeToDelete, const Element& aEditingHost) const; /** * Run() executes the joining. * * @param aHTMLEditor The HTML editor. * @param aLimitersAndCaretData The data copied from nsFrameSelection. * @param aDirectionAndAmount Direction of the deletion. * @param aStripWrappers Whether delete or keep new empty * ancestor elements. * @param aRangeToDelete The range to delete. Must not be * collapsed. * @param aSelectionWasCollapsed Whether selection was or was not * collapsed when starting to handle * deletion. */ [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result Run( HTMLEditor& aHTMLEditor, const LimitersAndCaretData& aLimitersAndCaretData, nsIEditor::EDirection aDirectionAndAmount, nsIEditor::EStripWrappers aStripWrappers, nsRange& aRangeToDelete, AutoDeleteRangesHandler::SelectionWasCollapsed aSelectionWasCollapsed, const Element& aEditingHost); nsresult ComputeRangeToDelete( const HTMLEditor& aHTMLEditor, const AutoClonedSelectionRangeArray& aRangesToDelete, nsIEditor::EDirection aDirectionAndAmount, nsRange& aRangeToDelete, AutoDeleteRangesHandler::SelectionWasCollapsed aSelectionWasCollapsed, const Element& aEditingHost) const; [[nodiscard]] nsIContent* GetLeafContentInOtherBlockElement() const { MOZ_ASSERT(mMode == Mode::JoinOtherBlock); return mLeafContentInOtherBlock; } private: [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result HandleDeleteAtCurrentBlockBoundary(HTMLEditor& aHTMLEditor, nsIEditor::EDirection aDirectionAndAmount, const EditorDOMPoint& aCaretPoint, const Element& aEditingHost); nsresult ComputeRangeToDeleteAtCurrentBlockBoundary( const HTMLEditor& aHTMLEditor, const EditorDOMPoint& aCaretPoint, nsRange& aRangeToDelete, const Element& aEditingHost) const; [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result HandleDeleteAtOtherBlockBoundary(HTMLEditor& aHTMLEditor, nsIEditor::EDirection aDirectionAndAmount, nsIEditor::EStripWrappers aStripWrappers, const EditorDOMPoint& aCaretPoint, nsRange& aRangeToDelete, const Element& aEditingHost); // FYI: This method may modify selection, but it won't cause running // script because of `AutoHideSelectionChanges` which blocks // selection change listeners and the selection change event // dispatcher. MOZ_CAN_RUN_SCRIPT_BOUNDARY nsresult ComputeRangeToDeleteAtOtherBlockBoundary( const HTMLEditor& aHTMLEditor, nsIEditor::EDirection aDirectionAndAmount, const EditorDOMPoint& aCaretPoint, nsRange& aRangeToDelete, const Element& aEditingHost) const; [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result JoinBlockElementsInSameParent( HTMLEditor& aHTMLEditor, const LimitersAndCaretData& aLimitersAndCaretData, nsIEditor::EDirection aDirectionAndAmount, nsIEditor::EStripWrappers aStripWrappers, nsRange& aRangeToDelete, AutoDeleteRangesHandler::SelectionWasCollapsed aSelectionWasCollapsed, const Element& aEditingHost); nsresult ComputeRangeToJoinBlockElementsInSameParent( const HTMLEditor& aHTMLEditor, nsIEditor::EDirection aDirectionAndAmount, nsRange& aRangeToDelete, const Element& aEditingHost) const; [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result HandleDeleteLineBreak(HTMLEditor& aHTMLEditor, nsIEditor::EDirection aDirectionAndAmount, const EditorDOMPoint& aCaretPoint, const Element& aEditingHost); enum class ComputeRangeFor : bool { GetTargetRanges, ToDeleteTheRange }; nsresult ComputeRangeToDeleteLineBreak( const HTMLEditor& aHTMLEditor, nsRange& aRangeToDelete, const Element& aEditingHost, ComputeRangeFor aComputeRangeFor) const; [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result DeleteContentInRange(HTMLEditor& aHTMLEditor, const LimitersAndCaretData& aLimitersAndCaretData, nsIEditor::EDirection aDirectionAndAmount, nsIEditor::EStripWrappers aStripWrappers, nsRange& aRangeToDelete, const Element& aEditingHost); nsresult ComputeRangeToDeleteContentInRange( const HTMLEditor& aHTMLEditor, nsIEditor::EDirection aDirectionAndAmount, nsRange& aRange, const Element& aEditingHost) const; [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result HandleDeleteNonCollapsedRange( HTMLEditor& aHTMLEditor, nsIEditor::EDirection aDirectionAndAmount, nsIEditor::EStripWrappers aStripWrappers, nsRange& aRangeToDelete, AutoDeleteRangesHandler::SelectionWasCollapsed aSelectionWasCollapsed, const Element& aEditingHost); nsresult ComputeRangeToDeleteNonCollapsedRange( const HTMLEditor& aHTMLEditor, nsIEditor::EDirection aDirectionAndAmount, nsRange& aRangeToDelete, AutoDeleteRangesHandler::SelectionWasCollapsed aSelectionWasCollapsed, const Element& aEditingHost) const; /** * JoinNodesDeepWithTransaction() joins aLeftNode and aRightNode "deeply". * First, they are joined simply, then, new right node is assumed as the * child at length of the left node before joined and new left node is * assumed as its previous sibling. Then, they will be joined again. * And then, these steps are repeated. * * @param aLeftContent The node which will be removed form the tree. * @param aRightContent The node which will be inserted the contents of * aRightContent. * @return The point of the first child of the last right * node. The result is always set if this succeeded. */ [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result JoinNodesDeepWithTransaction(HTMLEditor& aHTMLEditor, nsIContent& aLeftContent, nsIContent& aRightContent); enum class PutCaretTo : bool { StartOfRange, EndOfRange }; /** * DeleteNodesEntirelyInRangeButKeepTableStructure() removes each node in * aArrayOfContent. However, if some nodes are part of a table, removes all * children of them instead. I.e., this does not make damage to table * structure at the range, but may remove table entirely if it's in the * range. */ [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result DeleteNodesEntirelyInRangeButKeepTableStructure( HTMLEditor& aHTMLEditor, const nsTArray>& aArrayOfContent, PutCaretTo aPutCaretTo); [[nodiscard]] bool NeedsToJoinNodesAfterDeleteNodesEntirelyInRangeButKeepTableStructure( const HTMLEditor& aHTMLEditor, const nsTArray>& aArrayOfContents, AutoDeleteRangesHandler::SelectionWasCollapsed aSelectionWasCollapsed) const; Result ComputeRangeToDeleteNodesEntirelyInRangeButKeepTableStructure( const HTMLEditor& aHTMLEditor, nsRange& aRange, AutoDeleteRangesHandler::SelectionWasCollapsed aSelectionWasCollapsed) const; /** * DeleteContentButKeepTableStructure() removes aContent if it's an element * which is part of a table structure. If it's a part of table structure, * removes its all children recursively. I.e., this may delete all of a * table, but won't break table structure partially. * * @param aContent The content which or whose all children should * be removed. */ [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result DeleteContentButKeepTableStructure(HTMLEditor& aHTMLEditor, nsIContent& aContent); /** * DeleteTextAtStartAndEndOfRange() removes text if start and/or end of * aRange is in a text node. */ [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result DeleteTextAtStartAndEndOfRange(HTMLEditor& aHTMLEditor, nsRange& aRange, PutCaretTo aPutCaretTo); /** * Return a block element which is an inclusive ancestor of the container of * aPoint if aPoint is start of ancestor blocks. For example, if `
abc
[]def
`, return * #div2. */ template [[nodiscard]] static Result GetMostDistantBlockAncestorIfPointIsStartAtBlock( const EditorDOMPointType& aPoint, const Element& aEditingHost, const Element* aAncestorLimiter = nullptr); /** * Extend aRangeToDelete to contain new empty inline ancestors and contain * an invisible
element before right child block which causes an empty * line but the range starts after it. */ void ExtendRangeToDeleteNonCollapsedRange( const HTMLEditor& aHTMLEditor, nsRange& aRangeToDelete, const Element& aEditingHost, ComputeRangeFor aComputeRangeFor) const; /** * Compute mLeafContentInOtherBlock from the DOM. */ [[nodiscard]] nsIContent* ComputeLeafContentInOtherBlockElement( nsIEditor::EDirection aDirectionAndAmount) const; class MOZ_STACK_CLASS AutoInclusiveAncestorBlockElementsJoiner; enum class Mode { NotInitialized, JoinCurrentBlock, JoinOtherBlock, JoinBlocksInSameParent, DeleteBRElement, // The instance will handle only the
element immediately before a // block. DeletePrecedingBRElementOfBlock, // The instance will handle only the preceding preformatted line break // before a block. DeletePrecedingPreformattedLineBreak, DeleteContentInRange, DeleteNonCollapsedRange, // The instance will handle preceding lines of the right block and content // in the range in the right block. DeletePrecedingLinesAndContentInRange, }; AutoDeleteRangesHandler* mDeleteRangesHandler; const AutoDeleteRangesHandler& mDeleteRangesHandlerConst; nsCOMPtr mLeftContent; nsCOMPtr mRightContent; nsCOMPtr mLeafContentInOtherBlock; RefPtr mOtherBlockElement; // mSkippedInvisibleContents stores all content nodes which are skipped at // scanning mLeftContent and mRightContent. The content nodes should be // removed at deletion. AutoTArray, 8> mSkippedInvisibleContents; RefPtr mBRElement; EditorDOMPointInText mPreformattedLineBreak; Mode mMode = Mode::NotInitialized; }; /** * Actually handle joining inclusive ancestor block elements. */ class MOZ_STACK_CLASS HTMLEditor::AutoDeleteRangesHandler:: AutoBlockElementsJoiner::AutoInclusiveAncestorBlockElementsJoiner final { public: AutoInclusiveAncestorBlockElementsJoiner() = delete; AutoInclusiveAncestorBlockElementsJoiner( nsIContent& aInclusiveDescendantOfLeftBlockElement, nsIContent& aInclusiveDescendantOfRightBlockElement) : mInclusiveDescendantOfLeftBlockElement( aInclusiveDescendantOfLeftBlockElement), mInclusiveDescendantOfRightBlockElement( aInclusiveDescendantOfRightBlockElement), mCanJoinBlocks(false), mFallbackToDeleteLeafContent(false) {} [[nodiscard]] bool IsSet() const { return mLeftBlockElement && mRightBlockElement; } [[nodiscard]] bool IsSameBlockElement() const { return mLeftBlockElement && mLeftBlockElement == mRightBlockElement; } /** * Prepare for joining inclusive ancestor block elements. When this * returns false, the deletion should be canceled. */ [[nodiscard]] Result Prepare(const HTMLEditor& aHTMLEditor, const Element& aEditingHost); /** * When this returns true, this can join the blocks with `Run()`. */ [[nodiscard]] bool CanJoinBlocks() const { return mCanJoinBlocks; } /** * When this returns true, `Run()` must return "ignored" so that * caller can skip calling `Run()`. This is available only when * `CanJoinBlocks()` returns `true`. * TODO: This should be merged into `CanJoinBlocks()` in the future. */ [[nodiscard]] bool ShouldDeleteLeafContentInstead() const { MOZ_ASSERT(CanJoinBlocks()); return mFallbackToDeleteLeafContent; } /** * ComputeRangesToDelete() extends aRangeToDelete includes the element * boundaries between joining blocks. If they won't be joined, this * collapses the range to aCaretPoint. */ [[nodiscard]] nsresult ComputeRangeToDelete(const HTMLEditor& aHTMLEditor, const EditorDOMPoint& aCaretPoint, nsRange& aRangeToDelete) const; /** * Join inclusive ancestor block elements which are found by preceding * Prepare() call. * The right element is always joined to the left element. * If the elements are the same type and not nested within each other, * JoinEditableNodesWithTransaction() is called (example, joining two * list items together into one). * If the elements are not the same type, or one is a descendant of the * other, we instead destroy the right block placing its children into * left block. */ [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result Run( HTMLEditor& aHTMLEditor, const Element& aEditingHost); private: /** * This method returns true when * `MergeFirstLineOfRightBlockElementIntoDescendantLeftBlockElement()`, * `MergeFirstLineOfRightBlockElementIntoAncestorLeftBlockElement()` and * `MergeFirstLineOfRightBlockElementIntoLeftBlockElement()` handle it * with the `if` block of the main lambda of them. */ [[nodiscard]] bool CanMergeLeftAndRightBlockElements() const { if (!IsSet()) { return false; } // `MergeFirstLineOfRightBlockElementIntoDescendantLeftBlockElement()` if (mPointContainingTheOtherBlockElement.GetContainer() == mRightBlockElement) { return mNewListElementTagNameOfRightListElement.isSome(); } // `MergeFirstLineOfRightBlockElementIntoAncestorLeftBlockElement()` if (mPointContainingTheOtherBlockElement.GetContainer() == mLeftBlockElement) { return mNewListElementTagNameOfRightListElement.isSome() && mRightBlockElement->GetChildCount(); } MOZ_ASSERT(!mPointContainingTheOtherBlockElement.IsSet()); // `MergeFirstLineOfRightBlockElementIntoLeftBlockElement()` return mNewListElementTagNameOfRightListElement.isSome() || (mLeftBlockElement->NodeInfo()->NameAtom() == mRightBlockElement->NodeInfo()->NameAtom() && EditorUtils::GetComputedWhiteSpaceStyles(*mLeftBlockElement) == EditorUtils::GetComputedWhiteSpaceStyles(*mRightBlockElement)); } OwningNonNull mInclusiveDescendantOfLeftBlockElement; OwningNonNull mInclusiveDescendantOfRightBlockElement; RefPtr mLeftBlockElement; RefPtr mRightBlockElement; Maybe mNewListElementTagNameOfRightListElement; EditorDOMPoint mPointContainingTheOtherBlockElement; RefPtr mPrecedingInvisibleBRElement; bool mCanJoinBlocks; bool mFallbackToDeleteLeafContent; }; /** * Handle deleting empty block ancestors. */ class MOZ_STACK_CLASS HTMLEditor::AutoDeleteRangesHandler::AutoEmptyBlockAncestorDeleter final { public: /** * ScanEmptyBlockInclusiveAncestor() scans an inclusive ancestor element * which is empty and a block element. Then, stores the result and * returns the found empty block element. * * @param aHTMLEditor The HTMLEditor. * @param aStartContent Start content to look for empty ancestors. */ [[nodiscard]] Element* ScanEmptyBlockInclusiveAncestor( const HTMLEditor& aHTMLEditor, nsIContent& aStartContent); /** * ComputeTargetRanges() computes "target ranges" for deleting * `mEmptyInclusiveAncestorBlockElement`. */ nsresult ComputeTargetRanges( const HTMLEditor& aHTMLEditor, nsIEditor::EDirection aDirectionAndAmount, const Element& aEditingHost, AutoClonedSelectionRangeArray& aRangesToDelete) const; /** * Deletes found empty block element by `ScanEmptyBlockInclusiveAncestor()`. * If found one is a list item element, calls * `MaybeInsertBRElementBeforeEmptyListItemElement()` before deleting * the list item element. * If found empty ancestor is not a list item element, * `GetNewCaretPosition()` will be called to determine new caret position. * Finally, removes the empty block ancestor. * * @param aHTMLEditor The HTMLEditor. * @param aDirectionAndAmount If found empty ancestor block is a list item * element, this is ignored. Otherwise: * - If eNext, eNextWord or eToEndOfLine, * collapse Selection to after found empty * ancestor. * - If ePrevious, ePreviousWord or * eToBeginningOfLine, collapse Selection to * end of previous editable node. * - Otherwise, eNone is allowed but does * nothing. * @param aEditingHost The editing host. */ [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result Run( HTMLEditor& aHTMLEditor, nsIEditor::EDirection aDirectionAndAmount, const Element& aEditingHost); private: /** * MaybeReplaceSubListWithNewListItem() replaces * mEmptyInclusiveAncestorBlockElement with new list item element * (containing
) if: * - mEmptyInclusiveAncestorBlockElement is a list element * - The parent of mEmptyInclusiveAncestorBlockElement is a list element * - The parent becomes empty after deletion * If this does not perform the replacement, returns "ignored". */ [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result MaybeReplaceSubListWithNewListItem(HTMLEditor& aHTMLEditor); /** * MaybeInsertBRElementBeforeEmptyListItemElement() inserts a `
` element * if `mEmptyInclusiveAncestorBlockElement` is a list item element which * is first editable element in its parent, and its grand parent is not a * list element, inserts a `
` element before the empty list item. */ [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result MaybeInsertBRElementBeforeEmptyListItemElement(HTMLEditor& aHTMLEditor); /** * GetNewCaretPosition() returns new caret position after deleting * `mEmptyInclusiveAncestorBlockElement`. */ [[nodiscard]] Result GetNewCaretPosition( const HTMLEditor& aHTMLEditor, nsIEditor::EDirection aDirectionAndAmount) const; RefPtr mEmptyInclusiveAncestorBlockElement; }; /****************************************************************************** * 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 dom::CharacterDataBuffer& characterDataBuffer = aText.DataBuffer(); const uint32_t minimizedReplaceStart = [&]() { const auto firstDiffCharOffset = mNewLengthBefore ? characterDataBuffer.FindFirstDifferentCharOffset( PrecedingWhiteSpaces(), mReplaceStartOffset) : dom::CharacterDataBuffer::kNotFound; if (firstDiffCharOffset == dom::CharacterDataBuffer::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 ? characterDataBuffer.RFindFirstDifferentCharOffset( FollowingWhiteSpaces(), mReplaceEndOffset) : dom::CharacterDataBuffer::kNotFound; if (lastDiffCharOffset == dom::CharacterDataBuffer::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 dom::CharacterDataBuffer& characterDataBuffer = aText.DataBuffer(); const auto minimizedReplaceStart = [&]() -> uint32_t { if (mNormalizedString.IsEmpty()) { return mReplaceStartOffset; } const uint32_t firstDiffCharOffset = characterDataBuffer.FindFirstDifferentCharOffset(mNormalizedString, mReplaceStartOffset); if (firstDiffCharOffset == dom::CharacterDataBuffer::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 = characterDataBuffer.RFindFirstDifferentCharOffset(mNormalizedString, mReplaceEndOffset); MOZ_ASSERT(lastDiffCharOffset != dom::CharacterDataBuffer::kNotFound); return lastDiffCharOffset == dom::CharacterDataBuffer::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; }; /****************************************************************************** * A runnable to run HTMLEditor::OnModifiedDocument when it's safe. ******************************************************************************/ class HTMLEditor::DocumentModifiedEvent final : public Runnable { public: explicit DocumentModifiedEvent(HTMLEditor& aHTMLEditor) : Runnable("DocumentModifiedEvent"), mHTMLEditor(aHTMLEditor) {} MOZ_CAN_RUN_SCRIPT_BOUNDARY NS_IMETHOD Run() { (void)MOZ_KnownLive(mHTMLEditor)->OnModifyDocument(*this); return NS_OK; } const nsTArray& NewInvisibleWhiteSpacesRef() const { return mNewInvisibleWhiteSpaces; } private: ~DocumentModifiedEvent() = default; const OwningNonNull mHTMLEditor; nsTArray mNewInvisibleWhiteSpaces; }; } // namespace mozilla #endif // #ifndef HTMLEditorNestedClasses_h