diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 17:32:43 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 17:32:43 +0000 |
commit | 6bf0a5cb5034a7e684dcc3500e841785237ce2dd (patch) | |
tree | a68f146d7fa01f0134297619fbe7e33db084e0aa /editor/libeditor/HTMLEditorNestedClasses.h | |
parent | Initial commit. (diff) | |
download | thunderbird-6bf0a5cb5034a7e684dcc3500e841785237ce2dd.tar.xz thunderbird-6bf0a5cb5034a7e684dcc3500e841785237ce2dd.zip |
Adding upstream version 1:115.7.0.upstream/1%115.7.0upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
-rw-r--r-- | editor/libeditor/HTMLEditorNestedClasses.h | 522 |
1 files changed, 522 insertions, 0 deletions
diff --git a/editor/libeditor/HTMLEditorNestedClasses.h b/editor/libeditor/HTMLEditorNestedClasses.h new file mode 100644 index 0000000000..1d5ac2113f --- /dev/null +++ b/editor/libeditor/HTMLEditorNestedClasses.h @@ -0,0 +1,522 @@ +/* -*- 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 "mozilla/Attributes.h" +#include "mozilla/OwningNonNull.h" +#include "mozilla/Result.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<SplitRangeOffFromNodeResult, nsresult> + 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<CaretPoint, nsresult> + 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<SplitRangeOffFromNodeResult, nsresult> + 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<EditorRawDOMRange, nsresult> ExtendOrShrinkRangeToApplyTheStyle( + const HTMLEditor& aHTMLEditor, const EditorDOMRange& aRange, + const Element& aEditingHost) 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. + * @param aEditingHost The editing host. + * @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<RefPtr<Text>, nsresult> + GetEmptyTextNodeToApplyNewStyle(HTMLEditor& aHTMLEditor, + const EditorDOMPoint& aCandidatePointToInsert, + const Element& aEditingHost); + + private: + [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<CaretPoint, nsresult> ApplyStyle( + HTMLEditor& aHTMLEditor, nsIContent& aContent); + + [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<CaretPoint, nsresult> + 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<bool, nsresult> + 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 (!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<MoveNodeResult, nsresult> 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 <br> element in an empty block element, this + * returns false even though it may be deleted. + */ + static Result<bool, nsresult> 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<CaretPoint, nsresult> + SplitToMakeTheLineIsolated( + HTMLEditor& aHTMLEditor, const nsIContent& aNewContainer, + const Element& aEditingHost, + nsTArray<OwningNonNull<nsIContent>>& 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<Element> mSrcInclusiveAncestorBlock; + // An inclusive ancestor block element of the insertion point. + RefPtr<Element> 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<Element> 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 <p> element is selected, its child <br> + * 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<nsStaticAtom&>(aListElementTagName)), + mListItemTagName(const_cast<nsStaticAtom&>(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<EditActionResult, nsresult> Run( + HTMLEditor& aHTMLEditor, AutoRangeArray& aRanges, + HTMLEditor::SelectAllOfCurrentList aSelectAllOfCurrentList, + const Element& aEditingHost) const; + + private: + using ContentNodeArray = nsTArray<OwningNonNull<nsIContent>>; + using AutoContentNodeArray = AutoTArray<OwningNonNull<nsIContent>, 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, AutoRangeArray& aRanges, + SelectAllOfCurrentList aSelectAllOfCurrentList, + const Element& aEditingHost, ContentNodeArray& aOutArrayOfContents) const; + + /** + * Return true if aArrayOfContents has only <br> 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<RefPtr<Element>, nsresult> + ReplaceContentNodesWithEmptyNewList( + HTMLEditor& aHTMLEditor, const AutoRangeArray& 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<RefPtr<Element>, nsresult> + WrapContentNodesIntoNewListElements(HTMLEditor& aHTMLEditor, + AutoRangeArray& 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<Element> mCurrentListElement; + // Previously handled list item element. + RefPtr<Element> mPreviousListItemElement; + // List or list item element which should have caret after handling all + // contents. + RefPtr<Element> mListOrListItemElementToPutCaret; + // Replacing block element. This is typically already removed from the DOM + // tree. + RefPtr<Element> 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<CreateElementResult, nsresult> + 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, AutoRangeArray& aRanges) const; + + MOZ_KNOWN_LIVE nsStaticAtom& mListTagName; + MOZ_KNOWN_LIVE nsStaticAtom& mListItemTagName; + const nsAutoString mBulletType; +}; + +} // namespace mozilla + +#endif // #ifndef HTMLEditorNestedClasses_h |