diff options
Diffstat (limited to '')
-rw-r--r-- | editor/libeditor/EditorUtils.h | 1197 |
1 files changed, 1197 insertions, 0 deletions
diff --git a/editor/libeditor/EditorUtils.h b/editor/libeditor/EditorUtils.h new file mode 100644 index 0000000000..ced7c29871 --- /dev/null +++ b/editor/libeditor/EditorUtils.h @@ -0,0 +1,1197 @@ +/* -*- 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 mozilla_EditorUtils_h +#define mozilla_EditorUtils_h + +#include "mozilla/ContentIterator.h" +#include "mozilla/EditAction.h" +#include "mozilla/EditorBase.h" +#include "mozilla/EditorDOMPoint.h" +#include "mozilla/RangeBoundary.h" +#include "mozilla/Result.h" +#include "mozilla/dom/HTMLBRElement.h" +#include "mozilla/dom/Selection.h" +#include "mozilla/dom/StaticRange.h" +#include "nsAtom.h" +#include "nsCOMPtr.h" +#include "nsContentUtils.h" +#include "nscore.h" +#include "nsDebug.h" +#include "nsDirection.h" +#include "nsRange.h" +#include "nsString.h" + +class nsISimpleEnumerator; +class nsITransferable; + +namespace mozilla { +class MoveNodeResult; +template <class T> +class OwningNonNull; + +namespace dom { +class Element; +class Text; +} // namespace dom + +/*************************************************************************** + * EditResult returns nsresult and preferred point where selection should be + * collapsed or the range where selection should select. + * + * NOTE: If we stop modifying selection at every DOM tree change, perhaps, + * the following classes need to inherit this class. + */ +class MOZ_STACK_CLASS EditResult final { + public: + bool Succeeded() const { return NS_SUCCEEDED(mRv); } + bool Failed() const { return NS_FAILED(mRv); } + nsresult Rv() const { return mRv; } + bool EditorDestroyed() const { return mRv == NS_ERROR_EDITOR_DESTROYED; } + const EditorDOMPoint& PointRefToCollapseSelection() const { + MOZ_DIAGNOSTIC_ASSERT(mStartPoint.IsSet()); + MOZ_DIAGNOSTIC_ASSERT(mStartPoint == mEndPoint); + return mStartPoint; + } + const EditorDOMPoint& StartPointRef() const { return mStartPoint; } + const EditorDOMPoint& EndPointRef() const { return mEndPoint; } + already_AddRefed<dom::StaticRange> CreateStaticRange() const { + return dom::StaticRange::Create(mStartPoint.ToRawRangeBoundary(), + mEndPoint.ToRawRangeBoundary(), + IgnoreErrors()); + } + already_AddRefed<nsRange> CreateRange() const { + return nsRange::Create(mStartPoint.ToRawRangeBoundary(), + mEndPoint.ToRawRangeBoundary(), IgnoreErrors()); + } + + EditResult() = delete; + explicit EditResult(nsresult aRv) : mRv(aRv) { + MOZ_DIAGNOSTIC_ASSERT(NS_FAILED(mRv)); + } + template <typename PT, typename CT> + explicit EditResult(const EditorDOMPointBase<PT, CT>& aPointToPutCaret) + : mRv(aPointToPutCaret.IsSet() ? NS_OK : NS_ERROR_FAILURE), + mStartPoint(aPointToPutCaret), + mEndPoint(aPointToPutCaret) {} + + template <typename SPT, typename SCT, typename EPT, typename ECT> + EditResult(const EditorDOMPointBase<SPT, SCT>& aStartPoint, + const EditorDOMPointBase<EPT, ECT>& aEndPoint) + : mRv(aStartPoint.IsSet() && aEndPoint.IsSet() ? NS_OK + : NS_ERROR_FAILURE), + mStartPoint(aStartPoint), + mEndPoint(aEndPoint) {} + + EditResult(const EditResult& aOther) = delete; + EditResult& operator=(const EditResult& aOther) = delete; + EditResult(EditResult&& aOther) = default; + EditResult& operator=(EditResult&& aOther) = default; + + private: + nsresult mRv; + EditorDOMPoint mStartPoint; + EditorDOMPoint mEndPoint; +}; + +/*************************************************************************** + * EditActionResult is useful to return multiple results of an editor + * action handler without out params. + * Note that when you return an anonymous instance from a method, you should + * use EditActionIgnored(), EditActionHandled() or EditActionCanceled() for + * easier to read. In other words, EditActionResult should be used when + * declaring return type of a method, being an argument or defined as a local + * variable. + */ +class MOZ_STACK_CLASS EditActionResult final { + public: + bool Succeeded() const { return NS_SUCCEEDED(mRv); } + bool Failed() const { return NS_FAILED(mRv); } + nsresult Rv() const { return mRv; } + bool Canceled() const { return mCanceled; } + bool Handled() const { return mHandled; } + bool Ignored() const { return !mCanceled && !mHandled; } + bool EditorDestroyed() const { return mRv == NS_ERROR_EDITOR_DESTROYED; } + + EditActionResult SetResult(nsresult aRv) { + mRv = aRv; + return *this; + } + EditActionResult MarkAsCanceled() { + mCanceled = true; + return *this; + } + EditActionResult MarkAsHandled() { + mHandled = true; + return *this; + } + + explicit EditActionResult(nsresult aRv) + : mRv(aRv), mCanceled(false), mHandled(false) {} + + EditActionResult& operator|=(const EditActionResult& aOther) { + mCanceled |= aOther.mCanceled; + mHandled |= aOther.mHandled; + // When both result are same, keep the result. + if (mRv == aOther.mRv) { + return *this; + } + // If one of the result is NS_ERROR_EDITOR_DESTROYED, use it since it's + // the most important error code for editor. + if (EditorDestroyed() || aOther.EditorDestroyed()) { + mRv = NS_ERROR_EDITOR_DESTROYED; + } + // If one of the results is error, use NS_ERROR_FAILURE. + else if (Failed() || aOther.Failed()) { + mRv = NS_ERROR_FAILURE; + } else { + // Otherwise, use generic success code, NS_OK. + mRv = NS_OK; + } + return *this; + } + + EditActionResult& operator|=(const MoveNodeResult& aMoveNodeResult); + + private: + nsresult mRv; + bool mCanceled; + bool mHandled; + + EditActionResult(nsresult aRv, bool aCanceled, bool aHandled) + : mRv(aRv), mCanceled(aCanceled), mHandled(aHandled) {} + + EditActionResult() + : mRv(NS_ERROR_NOT_INITIALIZED), mCanceled(false), mHandled(false) {} + + friend EditActionResult EditActionIgnored(nsresult aRv); + friend EditActionResult EditActionHandled(nsresult aRv); + friend EditActionResult EditActionCanceled(nsresult aRv); +}; + +/*************************************************************************** + * When an edit action handler (or its helper) does nothing, + * EditActionIgnored should be returned. + */ +inline EditActionResult EditActionIgnored(nsresult aRv = NS_OK) { + return EditActionResult(aRv, false, false); +} + +/*************************************************************************** + * When an edit action handler (or its helper) handled and not canceled, + * EditActionHandled should be returned. + */ +inline EditActionResult EditActionHandled(nsresult aRv = NS_OK) { + return EditActionResult(aRv, false, true); +} + +/*************************************************************************** + * When an edit action handler (or its helper) handled and canceled, + * EditActionHandled should be returned. + */ +inline EditActionResult EditActionCanceled(nsresult aRv = NS_OK) { + return EditActionResult(aRv, true, true); +} + +/*************************************************************************** + * CreateNodeResultBase is a simple class for CreateSomething() methods + * which want to return new node. + */ +template <typename NodeType> +class CreateNodeResultBase; + +typedef CreateNodeResultBase<dom::Element> CreateElementResult; + +template <typename NodeType> +class MOZ_STACK_CLASS CreateNodeResultBase final { + typedef CreateNodeResultBase<NodeType> SelfType; + + public: + bool Succeeded() const { return NS_SUCCEEDED(mRv); } + bool Failed() const { return NS_FAILED(mRv); } + nsresult Rv() const { return mRv; } + bool EditorDestroyed() const { return mRv == NS_ERROR_EDITOR_DESTROYED; } + NodeType* GetNewNode() const { return mNode; } + + CreateNodeResultBase() = delete; + + explicit CreateNodeResultBase(nsresult aRv) : mRv(aRv) { + MOZ_DIAGNOSTIC_ASSERT(NS_FAILED(mRv)); + } + + explicit CreateNodeResultBase(NodeType* aNode) + : mNode(aNode), mRv(aNode ? NS_OK : NS_ERROR_FAILURE) {} + + explicit CreateNodeResultBase(RefPtr<NodeType>&& aNode) + : mNode(std::move(aNode)), mRv(mNode.get() ? NS_OK : NS_ERROR_FAILURE) {} + + CreateNodeResultBase(const SelfType& aOther) = delete; + SelfType& operator=(const SelfType& aOther) = delete; + CreateNodeResultBase(SelfType&& aOther) = default; + SelfType& operator=(SelfType&& aOther) = default; + + already_AddRefed<NodeType> forget() { + mRv = NS_ERROR_NOT_INITIALIZED; + return mNode.forget(); + } + + private: + RefPtr<NodeType> mNode; + nsresult mRv; +}; + +/*************************************************************************** + * MoveNodeResult is a simple class for MoveSomething() methods. + * This holds error code and next insertion point if moving contents succeeded. + */ +class MOZ_STACK_CLASS MoveNodeResult final { + public: + bool Succeeded() const { return NS_SUCCEEDED(mRv); } + bool Failed() const { return NS_FAILED(mRv); } + bool Handled() const { return mHandled; } + bool Ignored() const { return !mHandled; } + nsresult Rv() const { return mRv; } + bool EditorDestroyed() const { return mRv == NS_ERROR_EDITOR_DESTROYED; } + const EditorDOMPoint& NextInsertionPointRef() const { + return mNextInsertionPoint; + } + EditorDOMPoint NextInsertionPoint() const { return mNextInsertionPoint; } + + void MarkAsHandled() { mHandled = true; } + + MoveNodeResult() : mRv(NS_ERROR_NOT_INITIALIZED), mHandled(false) {} + + explicit MoveNodeResult(nsresult aRv) : mRv(aRv), mHandled(false) { + MOZ_DIAGNOSTIC_ASSERT(NS_FAILED(mRv)); + } + + MoveNodeResult(const MoveNodeResult& aOther) = delete; + MoveNodeResult& operator=(const MoveNodeResult& aOther) = delete; + MoveNodeResult(MoveNodeResult&& aOther) = default; + MoveNodeResult& operator=(MoveNodeResult&& aOther) = default; + + MoveNodeResult& operator|=(const MoveNodeResult& aOther) { + mHandled |= aOther.mHandled; + // When both result are same, keep the result but use newer point. + if (mRv == aOther.mRv) { + mNextInsertionPoint = aOther.mNextInsertionPoint; + return *this; + } + // If one of the result is NS_ERROR_EDITOR_DESTROYED, use it since it's + // the most important error code for editor. + if (EditorDestroyed() || aOther.EditorDestroyed()) { + mRv = NS_ERROR_EDITOR_DESTROYED; + mNextInsertionPoint.Clear(); + return *this; + } + // If the other one has not been set explicit nsresult, keep current + // value. + if (aOther.mRv == NS_ERROR_NOT_INITIALIZED) { + return *this; + } + // If this one has not been set explicit nsresult, copy the other one's. + if (mRv == NS_ERROR_NOT_INITIALIZED) { + mRv = aOther.mRv; + mNextInsertionPoint = aOther.mNextInsertionPoint; + return *this; + } + // If one of the results is error, use NS_ERROR_FAILURE. + if (Failed() || aOther.Failed()) { + mRv = NS_ERROR_FAILURE; + mNextInsertionPoint.Clear(); + return *this; + } + // Otherwise, use generic success code, NS_OK, and use newer point. + mRv = NS_OK; + mNextInsertionPoint = aOther.mNextInsertionPoint; + return *this; + } + + private: + template <typename PT, typename CT> + explicit MoveNodeResult(const EditorDOMPointBase<PT, CT>& aNextInsertionPoint, + bool aHandled) + : mNextInsertionPoint(aNextInsertionPoint), + mRv(aNextInsertionPoint.IsSet() ? NS_OK : NS_ERROR_FAILURE), + mHandled(aHandled && aNextInsertionPoint.IsSet()) { + if (mNextInsertionPoint.IsSet()) { + AutoEditorDOMPointChildInvalidator computeOffsetAndForgetChild( + mNextInsertionPoint); + } + } + + MoveNodeResult(nsINode* aParentNode, uint32_t aOffsetOfNextInsertionPoint, + bool aHandled) { + if (!aParentNode) { + mRv = NS_ERROR_FAILURE; + mHandled = false; + return; + } + aOffsetOfNextInsertionPoint = + std::min(aOffsetOfNextInsertionPoint, aParentNode->Length()); + mNextInsertionPoint.Set(aParentNode, aOffsetOfNextInsertionPoint); + mRv = mNextInsertionPoint.IsSet() ? NS_OK : NS_ERROR_FAILURE; + mHandled = aHandled && mNextInsertionPoint.IsSet(); + } + + EditorDOMPoint mNextInsertionPoint; + nsresult mRv; + bool mHandled; + + friend MoveNodeResult MoveNodeIgnored(nsINode* aParentNode, + uint32_t aOffsetOfNextInsertionPoint); + friend MoveNodeResult MoveNodeHandled(nsINode* aParentNode, + uint32_t aOffsetOfNextInsertionPoint); + template <typename PT, typename CT> + friend MoveNodeResult MoveNodeIgnored( + const EditorDOMPointBase<PT, CT>& aNextInsertionPoint); + template <typename PT, typename CT> + friend MoveNodeResult MoveNodeHandled( + const EditorDOMPointBase<PT, CT>& aNextInsertionPoint); +}; + +/*************************************************************************** + * When a move node handler (or its helper) does nothing, + * MoveNodeIgnored should be returned. + */ +inline MoveNodeResult MoveNodeIgnored(nsINode* aParentNode, + uint32_t aOffsetOfNextInsertionPoint) { + return MoveNodeResult(aParentNode, aOffsetOfNextInsertionPoint, false); +} + +template <typename PT, typename CT> +inline MoveNodeResult MoveNodeIgnored( + const EditorDOMPointBase<PT, CT>& aNextInsertionPoint) { + return MoveNodeResult(aNextInsertionPoint, false); +} + +/*************************************************************************** + * When a move node handler (or its helper) handled and not canceled, + * MoveNodeHandled should be returned. + */ +inline MoveNodeResult MoveNodeHandled(nsINode* aParentNode, + uint32_t aOffsetOfNextInsertionPoint) { + return MoveNodeResult(aParentNode, aOffsetOfNextInsertionPoint, true); +} + +template <typename PT, typename CT> +inline MoveNodeResult MoveNodeHandled( + const EditorDOMPointBase<PT, CT>& aNextInsertionPoint) { + return MoveNodeResult(aNextInsertionPoint, true); +} + +/*************************************************************************** + * SplitNodeResult is a simple class for + * HTMLEditor::SplitNodeDeepWithTransaction(). + * This makes the callers' code easier to read. + */ +class MOZ_STACK_CLASS SplitNodeResult final { + public: + bool Succeeded() const { return NS_SUCCEEDED(mRv); } + bool Failed() const { return NS_FAILED(mRv); } + nsresult Rv() const { return mRv; } + bool Handled() const { return mPreviousNode || mNextNode; } + bool EditorDestroyed() const { return mRv == NS_ERROR_EDITOR_DESTROYED; } + + /** + * DidSplit() returns true if a node was actually split. + */ + bool DidSplit() const { return mPreviousNode && mNextNode; } + + /** + * GetLeftNode() simply returns the left node which was created at splitting. + * This returns nullptr if the node wasn't split. + */ + nsIContent* GetLeftNode() const { + return mPreviousNode && mNextNode ? mPreviousNode.get() : nullptr; + } + + /** + * GetRightNode() simply returns the right node which was split. + * This won't return nullptr unless failed to split due to invalid arguments. + */ + nsIContent* GetRightNode() const { + if (mGivenSplitPoint.IsSet()) { + return mGivenSplitPoint.GetChild(); + } + return mPreviousNode && !mNextNode ? mPreviousNode : mNextNode; + } + + /** + * GetPreviousNode() returns previous node at the split point. + */ + nsIContent* GetPreviousNode() const { + if (mGivenSplitPoint.IsSet()) { + return mGivenSplitPoint.IsEndOfContainer() ? mGivenSplitPoint.GetChild() + : nullptr; + } + return mPreviousNode; + } + + /** + * GetNextNode() returns next node at the split point. + */ + nsIContent* GetNextNode() const { + if (mGivenSplitPoint.IsSet()) { + return !mGivenSplitPoint.IsEndOfContainer() ? mGivenSplitPoint.GetChild() + : nullptr; + } + return mNextNode; + } + + /** + * SplitPoint() returns the split point in the container. + * This is useful when callers insert an element at split point with + * EditorBase::CreateNodeWithTransaction() or something similar methods. + * + * Note that the result is EditorRawDOMPoint but the nodes are grabbed + * by this instance. Therefore, the life time of both container node + * and child node are guaranteed while using the result temporarily. + */ + EditorDOMPoint SplitPoint() const { + if (Failed()) { + return EditorDOMPoint(); + } + if (mGivenSplitPoint.IsSet()) { + return EditorDOMPoint(mGivenSplitPoint); + } + if (!mPreviousNode) { + return EditorDOMPoint(mNextNode); + } + EditorDOMPoint point(mPreviousNode); + DebugOnly<bool> advanced = point.AdvanceOffset(); + NS_WARNING_ASSERTION(advanced, + "Failed to advance offset to after previous node"); + return point; + } + + /** + * This constructor shouldn't be used by anybody except methods which + * use this as result when it succeeds. + * + * @param aPreviousNodeOfSplitPoint Previous node immediately before + * split point. + * @param aNextNodeOfSplitPoint Next node immediately after split + * point. + */ + SplitNodeResult(nsIContent* aPreviousNodeOfSplitPoint, + nsIContent* aNextNodeOfSplitPoint) + : mPreviousNode(aPreviousNodeOfSplitPoint), + mNextNode(aNextNodeOfSplitPoint), + mRv(NS_OK) { + MOZ_DIAGNOSTIC_ASSERT(mPreviousNode || mNextNode); + } + + /** + * This constructor should be used when the method didn't split any nodes + * but want to return given split point as right point. + */ + explicit SplitNodeResult(const EditorRawDOMPoint& aGivenSplitPoint) + : mGivenSplitPoint(aGivenSplitPoint), mRv(NS_OK) { + MOZ_DIAGNOSTIC_ASSERT(mGivenSplitPoint.IsSet()); + } + + /** + * This constructor shouldn't be used by anybody except methods which + * use this as error result when it fails. + */ + explicit SplitNodeResult(nsresult aRv) : mRv(aRv) { + MOZ_DIAGNOSTIC_ASSERT(NS_FAILED(mRv)); + } + + private: + // When methods which return this class split some nodes actually, they + // need to set a set of left node and right node to this class. However, + // one or both of them may be moved or removed by mutation observer. + // In such case, we cannot represent the point with EditorDOMPoint since + // it requires current container node. Therefore, we need to use + // nsCOMPtr<nsIContent> here instead. + nsCOMPtr<nsIContent> mPreviousNode; + nsCOMPtr<nsIContent> mNextNode; + + // Methods which return this class may not split any nodes actually. Then, + // they may want to return given split point as is since such behavior makes + // their callers simpler. In this case, the point may be in a text node + // which cannot be represented as a node. Therefore, we need EditorDOMPoint + // for representing the point. + EditorDOMPoint mGivenSplitPoint; + + nsresult mRv; + + SplitNodeResult() = delete; +}; + +/*************************************************************************** + * SplitRangeOffFromNodeResult class is a simple class for methods which split a + * node at 2 points for making part of the node split off from the node. + */ +class MOZ_STACK_CLASS SplitRangeOffFromNodeResult final { + public: + bool Succeeded() const { return NS_SUCCEEDED(mRv); } + bool Failed() const { return NS_FAILED(mRv); } + nsresult Rv() const { return mRv; } + bool EditorDestroyed() const { return mRv == NS_ERROR_EDITOR_DESTROYED; } + + /** + * GetLeftContent() returns new created node before the part of quarried out. + * This may return nullptr if the method didn't split at start edge of + * the node. + */ + nsIContent* GetLeftContent() const { return mLeftContent; } + dom::Element* GetLeftContentAsElement() const { + return dom::Element::FromNodeOrNull(mLeftContent); + } + + /** + * GetMiddleContent() returns new created node between left node and right + * node. I.e., this is quarried out from the node. This may return nullptr + * if the method unwrapped the middle node. + */ + nsIContent* GetMiddleContent() const { return mMiddleContent; } + dom::Element* GetMiddleContentAsElement() const { + return dom::Element::FromNodeOrNull(mMiddleContent); + } + + /** + * GetRightContent() returns the right node after the part of quarried out. + * This may return nullptr it the method didn't split at end edge of the + * node. + */ + nsIContent* GetRightContent() const { return mRightContent; } + dom::Element* GetRightContentAsElement() const { + return dom::Element::FromNodeOrNull(mRightContent); + } + + SplitRangeOffFromNodeResult(nsIContent* aLeftContent, + nsIContent* aMiddleContent, + nsIContent* aRightContent) + : mLeftContent(aLeftContent), + mMiddleContent(aMiddleContent), + mRightContent(aRightContent), + mRv(NS_OK) {} + + SplitRangeOffFromNodeResult(SplitNodeResult& aSplitResultAtLeftOfMiddleNode, + SplitNodeResult& aSplitResultAtRightOfMiddleNode) + : mRv(NS_OK) { + if (aSplitResultAtLeftOfMiddleNode.Succeeded()) { + mLeftContent = aSplitResultAtLeftOfMiddleNode.GetPreviousNode(); + } + if (aSplitResultAtRightOfMiddleNode.Succeeded()) { + mRightContent = aSplitResultAtRightOfMiddleNode.GetNextNode(); + mMiddleContent = aSplitResultAtRightOfMiddleNode.GetPreviousNode(); + } + if (!mMiddleContent && aSplitResultAtLeftOfMiddleNode.Succeeded()) { + mMiddleContent = aSplitResultAtLeftOfMiddleNode.GetNextNode(); + } + } + + explicit SplitRangeOffFromNodeResult(nsresult aRv) : mRv(aRv) { + MOZ_DIAGNOSTIC_ASSERT(NS_FAILED(mRv)); + } + + SplitRangeOffFromNodeResult(const SplitRangeOffFromNodeResult& aOther) = + delete; + SplitRangeOffFromNodeResult& operator=( + const SplitRangeOffFromNodeResult& aOther) = delete; + SplitRangeOffFromNodeResult(SplitRangeOffFromNodeResult&& aOther) = default; + SplitRangeOffFromNodeResult& operator=(SplitRangeOffFromNodeResult&& aOther) = + default; + + private: + nsCOMPtr<nsIContent> mLeftContent; + nsCOMPtr<nsIContent> mMiddleContent; + nsCOMPtr<nsIContent> mRightContent; + + nsresult mRv; + + SplitRangeOffFromNodeResult() = delete; +}; + +/*************************************************************************** + * SplitRangeOffResult class is a simple class for methods which splits + * specific ancestor elements at 2 DOM points. + */ +class MOZ_STACK_CLASS SplitRangeOffResult final { + public: + bool Succeeded() const { return NS_SUCCEEDED(mRv); } + bool Failed() const { return NS_FAILED(mRv); } + nsresult Rv() const { return mRv; } + bool Handled() const { return mHandled; } + bool EditorDestroyed() const { return mRv == NS_ERROR_EDITOR_DESTROYED; } + + /** + * This is at right node of split at start point. + */ + const EditorDOMPoint& SplitPointAtStart() const { return mSplitPointAtStart; } + /** + * This is at right node of split at end point. I.e., not in the range. + * This is after the range. + */ + const EditorDOMPoint& SplitPointAtEnd() const { return mSplitPointAtEnd; } + + SplitRangeOffResult() = delete; + + /** + * Constructor for success case. + * + * @param aTrackedRangeStart This should be at topmost right node + * child at start point if actually split + * there, or at start point to be tried + * to split. Note that if the method + * allows to run script after splitting + * at start point, the point should be + * tracked with AutoTrackDOMPoint. + * @param aSplitNodeResultAtStart Raw split node result at start point. + * @param aTrackedRangeEnd This should be at topmost right node + * child at end point if actually split + * here, or at end point to be tried to + * split. As same as aTrackedRangeStart, + * this value should be tracked while + * running some script. + * @param aSplitNodeResultAtEnd Raw split node result at start point. + */ + SplitRangeOffResult(const EditorDOMPoint& aTrackedRangeStart, + const SplitNodeResult& aSplitNodeResultAtStart, + const EditorDOMPoint& aTrackedRangeEnd, + const SplitNodeResult& aSplitNodeResultAtEnd) + : mSplitPointAtStart(aTrackedRangeStart), + mSplitPointAtEnd(aTrackedRangeEnd), + mRv(NS_OK), + mHandled(aSplitNodeResultAtStart.Handled() || + aSplitNodeResultAtEnd.Handled()) { + MOZ_ASSERT(mSplitPointAtStart.IsSet()); + MOZ_ASSERT(mSplitPointAtEnd.IsSet()); + MOZ_ASSERT(aSplitNodeResultAtStart.Succeeded()); + MOZ_ASSERT(aSplitNodeResultAtEnd.Succeeded()); + } + + explicit SplitRangeOffResult(nsresult aRv) : mRv(aRv), mHandled(false) { + MOZ_DIAGNOSTIC_ASSERT(NS_FAILED(mRv)); + } + + SplitRangeOffResult(const SplitRangeOffResult& aOther) = delete; + SplitRangeOffResult& operator=(const SplitRangeOffResult& aOther) = delete; + SplitRangeOffResult(SplitRangeOffResult&& aOther) = default; + SplitRangeOffResult& operator=(SplitRangeOffResult&& aOther) = default; + + private: + EditorDOMPoint mSplitPointAtStart; + EditorDOMPoint mSplitPointAtEnd; + + // If you need to store previous and/or next node at start/end point, + // you might be able to use `SplitNodeResult::GetPreviousNode()` etc in the + // constructor only when `SplitNodeResult::Handled()` returns true. But + // the node might have gone with another DOM tree mutation. So, be careful + // if you do it. + + nsresult mRv; + + bool mHandled; +}; + +/*************************************************************************** + * stack based helper class for calling EditorBase::EndTransaction() after + * EditorBase::BeginTransaction(). This shouldn't be used in editor classes + * or helper classes while an edit action is being handled. Use + * AutoTransactionBatch in such cases since it uses non-virtual internal + * methods. + ***************************************************************************/ +class MOZ_RAII AutoTransactionBatchExternal final { + public: + MOZ_CAN_RUN_SCRIPT explicit AutoTransactionBatchExternal( + EditorBase& aEditorBase) + : mEditorBase(aEditorBase) { + MOZ_KnownLive(mEditorBase).BeginTransaction(); + } + + MOZ_CAN_RUN_SCRIPT ~AutoTransactionBatchExternal() { + MOZ_KnownLive(mEditorBase).EndTransaction(); + } + + private: + EditorBase& mEditorBase; +}; + +/****************************************************************************** + * AutoSelectionRangeArray stores all ranges in `aSelection`. + * Note that modifying the ranges means modifing the selection ranges. + *****************************************************************************/ +class MOZ_STACK_CLASS AutoSelectionRangeArray final { + public: + explicit AutoSelectionRangeArray(dom::Selection* aSelection) { + if (!aSelection) { + return; + } + uint32_t rangeCount = aSelection->RangeCount(); + for (uint32_t i = 0; i < rangeCount; i++) { + mRanges.AppendElement(*aSelection->GetRangeAt(i)); + } + } + + AutoTArray<mozilla::OwningNonNull<nsRange>, 8> mRanges; +}; + +/****************************************************************************** + * AutoRangeArray stores ranges which do no belong any `Selection`. + * So, different from `AutoSelectionRangeArray`, this can be used for + * ranges which may need to be modified before touching the DOM tree, + * but does not want to modify `Selection` for the performance. + *****************************************************************************/ +class MOZ_STACK_CLASS AutoRangeArray final { + public: + explicit AutoRangeArray(const dom::Selection& aSelection) { + Initialize(aSelection); + } + + void Initialize(const dom::Selection& aSelection) { + mDirection = aSelection.GetDirection(); + mRanges.Clear(); + for (uint32_t i = 0; i < aSelection.RangeCount(); i++) { + mRanges.AppendElement(aSelection.GetRangeAt(i)->CloneRange()); + if (aSelection.GetRangeAt(i) == aSelection.GetAnchorFocusRange()) { + mAnchorFocusRange = mRanges.LastElement(); + } + } + } + + auto& Ranges() { return mRanges; } + const auto& Ranges() const { return mRanges; } + auto& FirstRangeRef() { return mRanges[0]; } + const auto& FirstRangeRef() const { return mRanges[0]; } + + template <template <typename> typename StrongPtrType> + AutoTArray<StrongPtrType<nsRange>, 8> CloneRanges() const { + AutoTArray<StrongPtrType<nsRange>, 8> ranges; + for (const auto& range : mRanges) { + ranges.AppendElement(range->CloneRange()); + } + return ranges; + } + + EditorDOMPoint GetStartPointOfFirstRange() const { + if (mRanges.IsEmpty() || !mRanges[0]->IsPositioned()) { + return EditorDOMPoint(); + } + return EditorDOMPoint(mRanges[0]->StartRef()); + } + EditorDOMPoint GetEndPointOfFirstRange() const { + if (mRanges.IsEmpty() || !mRanges[0]->IsPositioned()) { + return EditorDOMPoint(); + } + return EditorDOMPoint(mRanges[0]->EndRef()); + } + + nsresult SelectNode(nsINode& aNode) { + mRanges.Clear(); + if (!mAnchorFocusRange) { + mAnchorFocusRange = nsRange::Create(&aNode); + if (!mAnchorFocusRange) { + return NS_ERROR_FAILURE; + } + } + ErrorResult error; + mAnchorFocusRange->SelectNode(aNode, error); + if (error.Failed()) { + mAnchorFocusRange = nullptr; + return error.StealNSResult(); + } + mRanges.AppendElement(*mAnchorFocusRange); + return NS_OK; + } + + /** + * ExtendAnchorFocusRangeFor() extends the anchor-focus range for deleting + * content for aDirectionAndAmount. The range won't be extended to outer of + * selection limiter. Note that if a range is extened, the range is + * recreated. Therefore, caller cannot cache pointer of any ranges before + * calling this. + */ + [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<nsIEditor::EDirection, nsresult> + ExtendAnchorFocusRangeFor(const EditorBase& aEditorBase, + nsIEditor::EDirection aDirectionAndAmount); + + /** + * For compatiblity with the other browsers, we should shrink ranges to + * start from an atomic content and/or end after one instead of start + * from end of a preceding text node and end by start of a follwing text + * node. Returns true if this modifies a range. + */ + enum class IfSelectingOnlyOneAtomicContent { + Collapse, // Collapse to the range selecting only one atomic content to + // start or after of it. Whether to collapse start or after + // it depends on aDirectionAndAmount. This is ignored if + // there are multiple ranges. + KeepSelecting, // Won't collapse the range. + }; + Result<bool, nsresult> ShrinkRangesIfStartFromOrEndAfterAtomicContent( + const HTMLEditor& aHTMLEditor, nsIEditor::EDirection aDirectionAndAmount, + IfSelectingOnlyOneAtomicContent aIfSelectingOnlyOneAtomicContent, + const dom::Element* aEditingHost); + + /** + * The following methods are same as `Selection`'s methods. + */ + bool IsCollapsed() const { + return mRanges.IsEmpty() || + (mRanges.Length() == 1 && mRanges[0]->Collapsed()); + } + template <typename PT, typename CT> + nsresult Collapse(const EditorDOMPointBase<PT, CT>& aPoint) { + mRanges.Clear(); + if (!mAnchorFocusRange) { + ErrorResult error; + mAnchorFocusRange = nsRange::Create(aPoint.ToRawRangeBoundary(), + aPoint.ToRawRangeBoundary(), error); + if (error.Failed()) { + mAnchorFocusRange = nullptr; + return error.StealNSResult(); + } + } else { + nsresult rv = mAnchorFocusRange->CollapseTo(aPoint.ToRawRangeBoundary()); + if (NS_FAILED(rv)) { + mAnchorFocusRange = nullptr; + return rv; + } + } + mRanges.AppendElement(*mAnchorFocusRange); + return NS_OK; + } + template <typename SPT, typename SCT, typename EPT, typename ECT> + nsresult SetStartAndEnd(const EditorDOMPointBase<SPT, SCT>& aStart, + const EditorDOMPointBase<EPT, ECT>& aEnd) { + mRanges.Clear(); + if (!mAnchorFocusRange) { + ErrorResult error; + mAnchorFocusRange = nsRange::Create(aStart.ToRawRangeBoundary(), + aEnd.ToRawRangeBoundary(), error); + if (error.Failed()) { + mAnchorFocusRange = nullptr; + return error.StealNSResult(); + } + } else { + nsresult rv = mAnchorFocusRange->SetStartAndEnd( + aStart.ToRawRangeBoundary(), aEnd.ToRawRangeBoundary()); + if (NS_FAILED(rv)) { + mAnchorFocusRange = nullptr; + return rv; + } + } + mRanges.AppendElement(*mAnchorFocusRange); + return NS_OK; + } + const nsRange* GetAnchorFocusRange() const { return mAnchorFocusRange; } + nsDirection GetDirection() const { return mDirection; } + + const RangeBoundary& AnchorRef() const { + if (!mAnchorFocusRange) { + static RangeBoundary sEmptyRangeBoundary; + return sEmptyRangeBoundary; + } + return mDirection == nsDirection::eDirNext ? mAnchorFocusRange->StartRef() + : mAnchorFocusRange->EndRef(); + } + nsINode* GetAnchorNode() const { + return AnchorRef().IsSet() ? AnchorRef().Container() : nullptr; + } + uint32_t GetAnchorOffset() const { + return AnchorRef().IsSet() + ? AnchorRef() + .Offset(RangeBoundary::OffsetFilter::kValidOffsets) + .valueOr(0) + : 0; + } + nsIContent* GetChildAtAnchorOffset() const { + return AnchorRef().IsSet() ? AnchorRef().GetChildAtOffset() : nullptr; + } + + const RangeBoundary& FocusRef() const { + if (!mAnchorFocusRange) { + static RangeBoundary sEmptyRangeBoundary; + return sEmptyRangeBoundary; + } + return mDirection == nsDirection::eDirNext ? mAnchorFocusRange->EndRef() + : mAnchorFocusRange->StartRef(); + } + nsINode* GetFocusNode() const { + return FocusRef().IsSet() ? FocusRef().Container() : nullptr; + } + uint32_t FocusOffset() const { + return FocusRef().IsSet() + ? FocusRef() + .Offset(RangeBoundary::OffsetFilter::kValidOffsets) + .valueOr(0) + : 0; + } + nsIContent* GetChildAtFocusOffset() const { + return FocusRef().IsSet() ? FocusRef().GetChildAtOffset() : nullptr; + } + + private: + AutoTArray<mozilla::OwningNonNull<nsRange>, 8> mRanges; + RefPtr<nsRange> mAnchorFocusRange; + nsDirection mDirection = nsDirection::eDirNext; +}; + +/****************************************************************************** + * some helper classes for iterating the dom tree + *****************************************************************************/ + +class MOZ_RAII DOMIterator { + public: + explicit DOMIterator(); + explicit DOMIterator(nsINode& aNode); + virtual ~DOMIterator() = default; + + nsresult Init(nsRange& aRange); + nsresult Init(const RawRangeBoundary& aStartRef, + const RawRangeBoundary& aEndRef); + + template <class NodeClass> + void AppendAllNodesToArray( + nsTArray<OwningNonNull<NodeClass>>& aArrayOfNodes) const; + + /** + * AppendNodesToArray() calls aFunctor before appending found node to + * aArrayOfNodes. If aFunctor returns false, the node will be ignored. + * You can use aClosure instead of capturing something with lambda. + * Note that aNode is guaranteed that it's an instance of NodeClass + * or its sub-class. + * XXX If we can make type of aNode templated without std::function, + * it'd be better, though. + */ + typedef bool (*BoolFunctor)(nsINode& aNode, void* aClosure); + template <class NodeClass> + void AppendNodesToArray(BoolFunctor aFunctor, + nsTArray<OwningNonNull<NodeClass>>& aArrayOfNodes, + void* aClosure = nullptr) const; + + protected: + ContentIteratorBase* mIter; + PostContentIterator mPostOrderIter; +}; + +class MOZ_RAII DOMSubtreeIterator final : public DOMIterator { + public: + explicit DOMSubtreeIterator(); + virtual ~DOMSubtreeIterator() = default; + + nsresult Init(nsRange& aRange); + + private: + ContentSubtreeIterator mSubtreeIter; + explicit DOMSubtreeIterator(nsINode& aNode) = delete; +}; + +/** + * ReplaceRangeDataBase() represents range to be replaced and replacing string. + */ +template <typename EditorDOMPointType> +class MOZ_STACK_CLASS ReplaceRangeDataBase final { + public: + ReplaceRangeDataBase() = default; + template <typename OtherEditorDOMRangeType> + ReplaceRangeDataBase(const OtherEditorDOMRangeType& aRange, + const nsAString& aReplaceString) + : mRange(aRange), mReplaceString(aReplaceString) {} + template <typename StartPointType, typename EndPointType> + ReplaceRangeDataBase(const StartPointType& aStart, const EndPointType& aEnd, + const nsAString& aReplaceString) + : mRange(aStart, aEnd), mReplaceString(aReplaceString) {} + + bool IsSet() const { return mRange.IsPositioned(); } + bool IsSetAndValid() const { return mRange.IsPositionedAndValid(); } + bool Collapsed() const { return mRange.Collapsed(); } + bool HasReplaceString() const { return !mReplaceString.IsEmpty(); } + const EditorDOMPointType& StartRef() const { return mRange.StartRef(); } + const EditorDOMPointType& EndRef() const { return mRange.EndRef(); } + const EditorDOMRangeBase<EditorDOMPointType>& RangeRef() const { + return mRange; + } + const nsString& ReplaceStringRef() const { return mReplaceString; } + + template <typename PointType> + MOZ_NEVER_INLINE_DEBUG void SetStart(const PointType& aStart) { + mRange.SetStart(aStart); + } + template <typename PointType> + MOZ_NEVER_INLINE_DEBUG void SetEnd(const PointType& aEnd) { + mRange.SetEnd(aEnd); + } + template <typename StartPointType, typename EndPointType> + MOZ_NEVER_INLINE_DEBUG void SetStartAndEnd(const StartPointType& aStart, + const EndPointType& aEnd) { + mRange.SetRange(aStart, aEnd); + } + template <typename OtherEditorDOMRangeType> + MOZ_NEVER_INLINE_DEBUG void SetRange(const OtherEditorDOMRangeType& aRange) { + mRange = aRange; + } + void SetReplaceString(const nsAString& aReplaceString) { + mReplaceString = aReplaceString; + } + template <typename StartPointType, typename EndPointType> + MOZ_NEVER_INLINE_DEBUG void SetStartAndEnd(const StartPointType& aStart, + const EndPointType& aEnd, + const nsAString& aReplaceString) { + SetStartAndEnd(aStart, aEnd); + SetReplaceString(aReplaceString); + } + template <typename OtherEditorDOMRangeType> + MOZ_NEVER_INLINE_DEBUG void Set(const OtherEditorDOMRangeType& aRange, + const nsAString& aReplaceString) { + SetRange(aRange); + SetReplaceString(aReplaceString); + } + + private: + EditorDOMRangeBase<EditorDOMPointType> mRange; + // This string may be used with ReplaceTextTransaction. Therefore, for + // avoiding memory copy, we should store it with nsString rather than + // nsAutoString. + nsString mReplaceString; +}; + +using ReplaceRangeData = ReplaceRangeDataBase<EditorDOMPoint>; +using ReplaceRangeInTextsData = ReplaceRangeDataBase<EditorDOMPointInText>; + +class EditorUtils final { + public: + using EditorType = EditorBase::EditorType; + using Selection = dom::Selection; + + /** + * IsDescendantOf() checks if aNode is a child or a descendant of aParent. + * aOutPoint is set to the child of aParent. + * + * @return true if aNode is a child or a descendant of aParent. + */ + static bool IsDescendantOf(const nsINode& aNode, const nsINode& aParent, + EditorRawDOMPoint* aOutPoint = nullptr); + static bool IsDescendantOf(const nsINode& aNode, const nsINode& aParent, + EditorDOMPoint* aOutPoint); + + /** + * Returns true if aContent is a <br> element and it's marked as padding for + * empty editor. + */ + static bool IsPaddingBRElementForEmptyEditor(const nsIContent& aContent) { + const dom::HTMLBRElement* brElement = + dom::HTMLBRElement::FromNode(&aContent); + return brElement && brElement->IsPaddingForEmptyEditor(); + } + + /** + * Returns true if aContent is a <br> element and it's marked as padding for + * empty last line. + */ + static bool IsPaddingBRElementForEmptyLastLine(const nsIContent& aContent) { + const dom::HTMLBRElement* brElement = + dom::HTMLBRElement::FromNode(&aContent); + return brElement && brElement->IsPaddingForEmptyLastLine(); + } + + /** + * IsEditableContent() returns true if aContent's data or children is ediable + * for the given editor type. Be aware, returning true does NOT mean the + * node can be removed from its parent node, and returning false does NOT + * mean the node cannot be removed from the parent node. + * XXX May be the anonymous nodes in TextEditor not editable? If it's not + * so, we can get rid of aEditorType. + */ + static bool IsEditableContent(const nsIContent& aContent, + EditorType aEditorType) { + if ((aEditorType == EditorType::HTML && !aContent.IsEditable()) || + EditorUtils::IsPaddingBRElementForEmptyEditor(aContent)) { + return false; + } + + // In HTML editors, if we're dealing with an element, then ask it + // whether it's editable. + if (aContent.IsElement()) { + return aEditorType == EditorType::HTML ? aContent.IsEditable() : true; + } + // Text nodes are considered to be editable by both typed of editors. + return aContent.IsText(); + } + + /** + * Returns true if aContent is a usual element node (not padding <br> element + * for empty editor) or a text node. In other words, returns true if + * aContent is a usual element node or visible data node. + */ + static bool IsElementOrText(const nsIContent& aContent) { + if (aContent.IsText()) { + return true; + } + return aContent.IsElement() && + !EditorUtils::IsPaddingBRElementForEmptyEditor(aContent); + } + + /** + * IsContentPreformatted() checks the style info for the node for the + * preformatted text style. This does NOT flush layout. + */ + static bool IsContentPreformatted(nsIContent& aContent); + + /** + * Helper method for `AppendString()` and `AppendSubString()`. This should + * be called only when `aText` is in a password field. This method masks + * A part of or all of `aText` (`aStartOffsetInText` and later) should've + * been copied (apppended) to `aString`. `aStartOffsetInString` is where + * the password was appended into `aString`. + */ + static void MaskString(nsString& aString, dom::Text* aText, + uint32_t aStartOffsetInString, + uint32_t aStartOffsetInText); + + static nsStaticAtom* GetTagNameAtom(const nsAString& aTagName) { + if (aTagName.IsEmpty()) { + return nullptr; + } + nsAutoString lowerTagName; + nsContentUtils::ASCIIToLower(aTagName, lowerTagName); + return NS_GetStaticAtom(lowerTagName); + } + + static nsStaticAtom* GetAttributeAtom(const nsAString& aAttribute) { + if (aAttribute.IsEmpty()) { + return nullptr; // Don't use nsGkAtoms::_empty for attribute. + } + return NS_GetStaticAtom(aAttribute); + } + + /** + * Helper method for deletion. When this returns true, Selection will be + * computed with nsFrameSelection that also requires flushed layout + * information. + */ + template <typename SelectionOrAutoRangeArray> + static bool IsFrameSelectionRequiredToExtendSelection( + nsIEditor::EDirection aDirectionAndAmount, + SelectionOrAutoRangeArray& aSelectionOrAutoRangeArray) { + switch (aDirectionAndAmount) { + case nsIEditor::eNextWord: + case nsIEditor::ePreviousWord: + case nsIEditor::eToBeginningOfLine: + case nsIEditor::eToEndOfLine: + return true; + case nsIEditor::ePrevious: + case nsIEditor::eNext: + return aSelectionOrAutoRangeArray.IsCollapsed(); + default: + return false; + } + } + + /** + * Returns true if aSelection includes the point in aParentContent. + */ + static bool IsPointInSelection(const Selection& aSelection, + const nsINode& aParentNode, int32_t aOffset); +}; + +} // namespace mozilla + +#endif // #ifndef mozilla_EditorUtils_h |