/* 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_RangeBoundary_h #define mozilla_RangeBoundary_h #include "mozilla/Assertions.h" #include "mozilla/Maybe.h" #include "mozilla/StaticPrefs_dom.h" #include "mozilla/ToString.h" #include "mozilla/dom/ChildIterator.h" #include "mozilla/dom/HTMLSlotElement.h" #include "mozilla/dom/ShadowRoot.h" #include "nsCOMPtr.h" #include "nsFmtString.h" #include "nsIContent.h" namespace mozilla { namespace dom { class CrossShadowBoundaryRange; class Range; } // namespace dom template class EditorDOMPointBase; // This class will maintain a reference to the child immediately // before the boundary's offset. We try to avoid computing the // offset as much as possible and just ensure mRef points to the // correct child. // // mParent // | // [child0] [child1] [child2] // / | // mRef mOffset=2 // // If mOffset == 0, mRef is null. // For text nodes, mRef will always be null and the offset will // be kept up-to-date. // // One special case is when mTreeKind is TreeKind::FlatForSelection and // mParent is slot and slot has assigned nodes, it'll use // the assigned nodes to determine the reference with the // same idea as above. // // Users of RangeBoundary should be extra careful about comparing // range boundaries with different kinds, as it tends to lead to // unexpected results. template class RangeBoundaryBase; using RangeBoundary = RangeBoundaryBase, nsCOMPtr>; using RawRangeBoundary = RangeBoundaryBase; using ConstRawRangeBoundary = RangeBoundaryBase; /** * Whether the range boundary is for the start or end boundary of a range. */ enum class RangeBoundarySide : bool { Start, End }; inline auto format_as(const RangeBoundarySide aSide) { return aSide == RangeBoundarySide::Start ? "Start" : "End"; } inline std::ostream& operator<<(std::ostream& aStream, const RangeBoundarySide aSide) { return aStream << format_as(aSide); } /** * There are two ways of ensuring that `mRef` points to the correct node. * In most cases, the `RangeBoundary` is used by an object that is a * `MutationObserver` (i.e. `dom::Range`) and replaces its `RangeBoundary` * objects when its parent chain changes. * However, there are Ranges which are not `MutationObserver`s (i.e. * `StaticRange`). `mRef` may become invalid when a DOM mutation happens. * Therefore, it needs to be recomputed using `mOffset` before it is being * accessed. * Because recomputing / validating of `mRef` could be an expensive operation, * it should be ensured that `Ref()` is called as few times as possible, i.e. * only once per method of `RangeBoundaryBase`. * * Furthermore, there are special implications when the `RangeBoundary` is not * used by an `MutationObserver`: * After a DOM mutation, the Boundary may point to something that is not valid * anymore, i.e. the `mOffset` is larger than `Container()->Length()`. In this * case, `Ref()` and `Get*ChildAtOffset()` return `nullptr` as an indication * that this RangeBoundary is not valid anymore. Also, `IsSetAndValid()` * returns false. However, `IsSet()` will still return true. * */ enum class RangeBoundarySetBy : bool { Offset = false, Ref = true }; enum class RangeBoundaryFor { // Use Start if the boundary is start of a non-collapsed range. Start, // Use End if the boundary is end of a non-collapsed range. End, // Use Collapsed if the boundary is for collapsed range start/end. Collapsed, }; inline std::ostream& operator<<(std::ostream& aStream, RangeBoundaryFor aFor) { constexpr static const char* sNames[] = { "Start", "End", "Collapsed", }; return aStream << sNames[static_cast(aFor)]; } inline auto format_as(RangeBoundaryFor aFor) { return ToString(aFor); } // This class has two types of specializations, one using reference counting // pointers and one using raw pointers (both non-const and const versions). The // latter help us avoid unnecessary AddRef/Release calls. template class RangeBoundaryBase { template friend class RangeBoundaryBase; template friend class EditorDOMPointBase; friend dom::Range; friend class mozilla::dom::CrossShadowBoundaryRange; friend void ImplCycleCollectionTraverse(nsCycleCollectionTraversalCallback&, RangeBoundary&, const char*, uint32_t); friend void ImplCycleCollectionUnlink(RangeBoundary&); static const uint32_t kFallbackOffset = 0; template struct GetNodeType; template struct GetNodeType>> { using type = std::remove_pointer_t; }; template struct GetNodeType>> { using type = typename T::element_type; }; public: using RawParentType = typename GetNodeType::type; static_assert(std::is_same_v || std::is_same_v); using RawRefType = typename GetNodeType::type; static_assert(std::is_same_v || std::is_same_v); private: /** * Make an instance from "ref" which is the previous sibling of what the * instance will point to. */ [[nodiscard]] static RangeBoundaryBase FromRef( RawRefType& aRef, TreeKind aTreeKind = TreeKind::DOM) { nsINode* const parentNode = ComputeParentNode(&aRef, aTreeKind); if (MOZ_UNLIKELY(!parentNode)) { return RangeBoundaryBase(aTreeKind); } return RangeBoundaryBase(parentNode, &aRef, aTreeKind); } public: /** * Make an instance from a child node which the instance will point to. */ [[nodiscard]] static RangeBoundaryBase FromChild( RawRefType& aChild, TreeKind aTreeKind = TreeKind::DOM) { nsINode* const parentNode = ComputeParentNode(&aChild, aTreeKind); if (MOZ_UNLIKELY(!parentNode)) { return RangeBoundaryBase(aTreeKind); } nsIContent* const ref = ComputeRef(parentNode, &aChild, aTreeKind); return RangeBoundaryBase(parentNode, ref, aTreeKind); } /** * Make an instance pointing after aChild. */ [[nodiscard]] static RangeBoundaryBase After( RawRefType& aChild, TreeKind aTreeKind = TreeKind::DOM) { // If the caller wants a point after aChild, we can use aChild as the ref. return FromRef(aChild, aTreeKind); } /** * Make an instance which points to the start of aParent. */ [[nodiscard]] static RangeBoundaryBase StartOfParent( RawParentType& aParent, RangeBoundarySetBy aPointTo = RangeBoundarySetBy::Ref, TreeKind aTreeKind = TreeKind::DOM) { if (MOZ_UNLIKELY(aParent.NodeType() == nsINode::DOCUMENT_TYPE_NODE)) { return RangeBoundaryBase(aTreeKind); } return RangeBoundaryBase(&aParent, nullptr, 0, aPointTo, aTreeKind); } /** * Make an instance which points to the end of aParent. */ [[nodiscard]] static RangeBoundaryBase EndOfParent( RawParentType& aParent, RangeBoundarySetBy aSetBy = RangeBoundarySetBy::Ref, TreeKind aTreeKind = TreeKind::DOM) { if (MOZ_UNLIKELY(aParent.NodeType() == nsINode::DOCUMENT_TYPE_NODE)) { return RangeBoundaryBase(aTreeKind); } if (aSetBy == RangeBoundarySetBy::Ref && aParent.IsContainerNode()) { MOZ_ASSERT(!aParent.IsCharacterData()); nsIContent* const lastChild = ComputeLastChild(&aParent, aTreeKind); return RangeBoundaryBase(&aParent, lastChild, aTreeKind); } const uint32_t length = ComputeLength(&aParent, aTreeKind); return RangeBoundaryBase(&aParent, length, aSetBy, aTreeKind); } RangeBoundaryBase(RawParentType* aContainer, RawRefType* aRef, TreeKind aTreeKind = TreeKind::DOM) : mParent(aContainer), mRef(aRef), mSetBy(RangeBoundarySetBy::Ref), mTreeKind(aTreeKind) { MOZ_ASSERT( aTreeKind == TreeKind::DOM || aTreeKind == TreeKind::FlatForSelection, "Only TreeKind::DOM and TreeKind::FlatForSelection are valid at the " "moment."); if (mRef) { NS_WARNING_ASSERTION( IsValidParent(mParent, mRef), nsFmtCString( FMT_STRING( "Constructing RangeBoundary with invalid value:\nthis={}"), *this) .get()); MOZ_ASSERT(IsValidParent(mParent, mRef), "Initializing RangeBoundary with invalid value"); } else { mOffset.emplace(0); } } [[nodiscard]] static RangeBoundaryBase MakeIfValidRef( RawParentType* aContainer, RawRefType* aRef, TreeKind aTreeKind = TreeKind::DOM) { if (MOZ_UNLIKELY(!aContainer || aContainer->NodeType() == nsINode::DOCUMENT_TYPE_NODE || (aRef && !IsValidParent(aContainer, aRef, aTreeKind)))) { return RangeBoundaryBase(aTreeKind); } return RangeBoundaryBase(aContainer, aRef, aTreeKind); } RangeBoundaryBase(RawParentType* aContainer, uint32_t aOffset, RangeBoundarySetBy aSetBy = RangeBoundarySetBy::Ref, TreeKind aTreeKind = TreeKind::DOM) : mParent(aContainer), mRef(nullptr), mOffset(mozilla::Some(aOffset)), mSetBy(aSetBy), mTreeKind(aTreeKind) { MOZ_ASSERT( aTreeKind == TreeKind::DOM || aTreeKind == TreeKind::FlatForSelection, "Only TreeKind::DOM and TreeKind::FlatForSelection are valid at the " "moment."); if (IsSetByOffset()) { // If aPointTo is "Offset", this may be created for a StaticRange so that // we may not need to warn invalid boundary. return; } if (mParent && mParent->IsContainerNode()) { // Find a reference node if (aOffset == GetLength(mParent)) { mRef = GetLastChild(mParent); } else if (aOffset > 0) { mRef = GetChildAt(mParent, aOffset - 1); } NS_WARNING_ASSERTION( mRef || aOffset == 0, nsFmtCString( FMT_STRING( "Constructing RangeBoundary with invalid value:\nthis={}"), *this) .get()); MOZ_ASSERT(mRef || aOffset == 0); return; } NS_WARNING_ASSERTION( !mRef || IsValidParent(mParent, mRef), nsFmtCString( FMT_STRING( "Constructing RangeBoundary with invalid value:\nthis={}"), *this) .get()); MOZ_ASSERT(!mRef || IsValidParent(mParent, mRef)); } [[nodiscard]] static RangeBoundaryBase MakeIfValidOffset( RawParentType* aContainer, uint32_t aOffset, RangeBoundarySetBy aSetBy = RangeBoundarySetBy::Ref, TreeKind aTreeKind = TreeKind::DOM) { if (MOZ_UNLIKELY(!aContainer || aContainer->NodeType() == nsINode::DOCUMENT_TYPE_NODE || aOffset > ComputeLength(aContainer, aTreeKind))) { return RangeBoundaryBase(aTreeKind); } return RangeBoundaryBase(aContainer, aOffset, aSetBy, aTreeKind); } [[nodiscard]] TreeKind GetTreeKind() const { return mTreeKind; } /** * Return a RangeBoundaryBase whose TreeKind is FlatForSelection. * * Note that the result may not be a part of the flattened tree, e.g., the * container may be an inclusive descendant of a non-flattened node. However, * to compute composed range, the boundary point is required. * * If you need a range boundary whose container is in the flattened tree, use * GetRangeBoundaryInFlatTree() instead. */ RangeBoundaryBase AsRangeBoundaryInFlatTreeOrNonFlattenedNode( RangeBoundaryFor aFor) const { if (mTreeKind == TreeKind::FlatForSelection) { return *this; } MOZ_ASSERT(IsSet()); if (!mParent->IsContainerNode()) { MOZ_ASSERT(mOffset); return RangeBoundaryBase(mParent, *mOffset, mSetBy, TreeKind::FlatForSelection); } enum class ChildKind : bool { ChildAtOffset, Ref }; // The child node which we're pointing may have different parent node in the // flat tree. E.g., the child node is slotted into a , the child node // is a child of ShadowRoot. Therefore, we need to compute proper container // from the pointing child. const auto ComputeRangeBoundaryInFlatTreeFromChildNode = [&](RawRefType* aChild, ChildKind aChildKind) { RangeBoundaryBase ret = aChildKind == ChildKind::ChildAtOffset ? FromChild(*aChild, TreeKind::FlatForSelection) : FromRef(*aChild, TreeKind::FlatForSelection); if (MOZ_LIKELY(ret.IsSet())) { return ret; } // If we're pointing a node which won't appear in the flat tree, the // parent must be a shadow host or a . Let's return start or // end of the parent. dom::ShadowRoot* const shadowRoot = mParent->GetShadowRootForSelection(); RawParentType* const slot = mParent->GetAsHTMLSlotElementIfFilledForSelection(); MOZ_ASSERT(shadowRoot || slot); MOZ_ASSERT_IF(shadowRoot, aChild->GetContainingShadow() != shadowRoot); // We treat the unassigned children of a shadow root and the fallback // content of filled are collapsed at end of the container. If // we're pointing start of the parent, we should use the start of the // container. Otherwise, we should use the end of the container. return IsStartOfContainer() ? StartOfParent(shadowRoot ? *shadowRoot : *slot, mSetBy, TreeKind::FlatForSelection) : EndOfParent(shadowRoot ? *shadowRoot : *slot, mSetBy, TreeKind::FlatForSelection); }; // Each RangeBoundaryBase instance may have implicit "direction". There are // following scenarios: // 1. Created for start boundary of a non-collapsed range // 2. Created for end boundary of a non-collapsed range // 3. Created for a collapsed range // 4. Created for the other purposes to point a child node or an offset. // In #1, the child node at the offset is what the setter focuses. // On the other hand, in #2 and #3, the previous sibling of the child node // at the offset is what the setter focus. Unfortunately, a child node // and its previous sibling may keep the relation in the flat tree. // Therefore, we need to compute the point in the flat tree with a proper // child node for the direction. // In #4, the caller should consider how this instance should be treated as. if (aFor == RangeBoundaryFor::Start) { // If we're pointing a child node, return the point in the flat tree // parent of the child node at the offset because the setter wanted the // point at the offset. if (RawRefType* const child = GetChildAtOffset()) { return ComputeRangeBoundaryInFlatTreeFromChildNode( child, ChildKind::ChildAtOffset); } // Otherwise, we're pointing the end of the container in the DOM tree. // If there is a ref, we should use it because we can guess that the // setter wanted to point AFTER the last child rather than the end of the // container. if (RawRefType* const lastChild = Ref()) { return ComputeRangeBoundaryInFlatTreeFromChildNode(lastChild, ChildKind::Ref); } } else { MOZ_ASSERT(aFor == RangeBoundaryFor::End || aFor == RangeBoundaryFor::Collapsed); // If we're for an end boundary of a range (or the boundary is for a // collapsed range) and we are not pointing the start of the container, // return the point in the flat tree parent of the ref because the setter // must have wanted the point AFTER the ref. if (RawRefType* const ref = Ref()) { return ComputeRangeBoundaryInFlatTreeFromChildNode(ref, ChildKind::Ref); } // Otherwise, we're pointing the start of the container in the DOM tree. // If there is the first child node, return the point in the flat tree // parent of the first child node because we can guess that the setter // wanted to point BEFORE the first child rather than the start of the // container. if (RawRefType* const child = GetChildAtOffset()) { return ComputeRangeBoundaryInFlatTreeFromChildNode( child, ChildKind::ChildAtOffset); } } // Otherwise, return the point in the empty parent. NS_ASSERTION( !mParent->HasChildNodes(), fmt::format("Called with invalid offset?\nthis={}", *this).c_str()); return EndOfParent(*mParent, mSetBy, TreeKind::FlatForSelection); } /** * Return a RangeBoundaryBase whose TreeKind is FlatForSelection and the * container is a part of the flattened tree, i.e., the container is never a * non-flattened node. */ RangeBoundaryBase GetRangeBoundaryInFlatTree(RangeBoundaryFor aFor) const { MOZ_ASSERT(IsSet()); RangeBoundaryBase inFlatTree = AsRangeBoundaryInFlatTreeOrNonFlattenedNode(aFor); if (NS_WARN_IF(!inFlatTree.IsSet())) { MOZ_ASSERT(inFlatTree.mTreeKind == TreeKind::FlatForSelection); return inFlatTree; } dom::Element* const shadowHostOrSlotElementNotFlatingTheParent = inFlatTree.mParent ->template GetFlatTreeAncestorElementForNonFlatTreeNode< TreeKind::FlatForSelection>(); if (!shadowHostOrSlotElementNotFlatingTheParent) [[likely]] { return inFlatTree; } // If mParent is a non-flattened node because mParent is an inclusive // descendant of // - an unslotted child of the shadow host element // - a fallback content node of the element // Then, we should treat the boundary is at end of the shadow root or the // . nsIContent* const shadowRoot = shadowHostOrSlotElementNotFlatingTheParent->GetShadowRootForSelection(); MOZ_ASSERT_IF(!shadowRoot, shadowHostOrSlotElementNotFlatingTheParent ->GetAsHTMLSlotElementIfFilledForSelection()); return EndOfParent( shadowRoot ? *shadowRoot : *shadowHostOrSlotElementNotFlatingTheParent, mSetBy, TreeKind::FlatForSelection); } RangeBoundaryBase AsRangeBoundaryInDOMTree() const { if (mTreeKind == TreeKind::DOM) { return *this; } MOZ_ASSERT(IsSet()); if (!mParent->IsContainerNode()) { MOZ_ASSERT(mOffset); return RangeBoundaryBase(mParent, *mOffset, mSetBy, TreeKind::DOM); } // If we're pointing a child node, let's recompute the point in the non-flat // tree from the child node. if (RawRefType* const child = GetChildAtOffset()) { return FromChild(*child, TreeKind::DOM); } // Otherwise, we're pointing the end of the container in the flat tree. // If the last children are assigned to a slot, let's return after the last // assigned node. if (nsIContent* const lastChild = GetLastChild(mParent)) { return FromRef(*lastChild, TreeKind::DOM); } // Okay, there is no child in mParent, let's return end of it. return EndOfParent(*mParent, mSetBy, TreeKind::DOM); } /** * Special constructor to create RangeBoundaryBase which stores both mRef * and mOffset. This can make the instance provide both mRef and mOffset * without computation, but the creator needs to guarantee that this is * valid at least at construction. */ RangeBoundaryBase(RawParentType* aContainer, RawRefType* aRef, uint32_t aOffset, RangeBoundarySetBy aSetBy = RangeBoundarySetBy::Ref, TreeKind aTreeKind = TreeKind::DOM) : mParent(const_cast(aContainer)), mRef(const_cast(aRef)), mOffset(mozilla::Some(aOffset)), mSetBy(aSetBy), mTreeKind(aTreeKind) { MOZ_ASSERT(IsSetAndValid()); } explicit RangeBoundaryBase(TreeKind aTreeKind = TreeKind::DOM) : mParent(nullptr), mRef(nullptr), mSetBy(RangeBoundarySetBy::Ref), mTreeKind(aTreeKind) {} // Convert from another RangeBoundaryBase type. template || // It's possible to convert to non-const RangeBoundary only from // non-const types. !std::is_const_v>> RangeBoundaryBase(const RangeBoundaryBase& aOther, RangeBoundarySetBy aSetBy) : mParent(aOther.mParent), mRef(aOther.mRef), mOffset(aOther.mOffset), mSetBy(aSetBy), mTreeKind(aOther.mTreeKind) {} /** * This method may return `nullptr` in two cases: * 1. `mPointingToChildNode` is true and the boundary points to the first * child of `mParent`. * 2. `mPointingToChildNode` is false and `mOffset` is out of bounds for * `mParent`s child list. * If `mPointingToChildNode` is false, this method may do some significant * computation. Therefore it is advised to call it as seldom as possible. * Code inside of this class should call this method exactly one time and * afterwards refer to `mRef` directly. */ RawRefType* Ref() const { if (IsSetByRef()) { return mRef; } MOZ_ASSERT(mParent); MOZ_ASSERT(mOffset); // `mRef` may have become invalid due to some DOM mutation, // which is not monitored here. Therefore, we need to validate `mRef` // manually. const uint32_t parentLength = GetLength(mParent); if (*mOffset > parentLength) { // offset > child count means that the range boundary has become invalid // due to a DOM mutation. mRef = nullptr; } else if (*mOffset == parentLength) { mRef = GetLastChild(mParent); } else if (*mOffset) { // validate and update `mRef`. // If `ComputeIndexOf()` returns `Nothing`, then `mRef` is not a child of // `mParent` anymore. // If the returned index for `mRef` does not match to `mOffset`, `mRef` // needs to be updated. const Maybe indexOfRefObject = mRef ? ComputeIndexOf(mParent, mRef, mTreeKind) : Nothing(); if (indexOfRefObject.isNothing() || *mOffset != *indexOfRefObject + 1) { mRef = GetChildAt(mParent, *mOffset - 1); } } else { mRef = nullptr; } return mRef; } RawParentType* GetContainer() const { return mParent; } dom::Document* GetComposedDoc() const { return mParent ? mParent->GetComposedDoc() : nullptr; } /** * This method may return `nullptr` if `mPointingToChildNode` is false and * `mOffset` is out of bounds. */ RawRefType* GetChildAtOffset() const { if (!mParent || !mParent->IsContainerNode()) { return nullptr; } RawRefType* const ref = Ref(); if (!ref) { if (!MaybeMutationObserved() && *mOffset != 0) { // This means that this boundary is invalid. // `mOffset` is out of bounds. return nullptr; } MOZ_ASSERT_IF(mTreeKind == TreeKind::DOM, *Offset(OffsetFilter::kValidOrInvalidOffsets) == 0); NS_ASSERTION( *Offset(OffsetFilter::kValidOrInvalidOffsets) == 0, nsFmtCString(FMT_STRING("Invalid range boundary:\nthis=%{}"), *this) .get()); return GetFirstChild(mParent); } NS_ASSERTION( GetChildAt(mParent, *Offset(OffsetFilter::kValidOrInvalidOffsets)) == GetNextSibling(ref), nsFmtCString( "Invalid range " "boundary:\nthis={}\nGetChildAt()={}\nGetNextSibling(ref)={}\n", *this, ToString( RefPtr{GetChildAt( mParent, *Offset(OffsetFilter::kValidOrInvalidOffsets))}) .c_str(), ToString(RefPtr{GetNextSibling(ref)}).c_str()) .get()); return GetNextSibling(ref); } /** * GetNextSiblingOfChildOffset() returns next sibling of a child at offset. * If this refers after the last child or the container cannot have children, * this returns nullptr with warning. */ RawRefType* GetNextSiblingOfChildAtOffset() const { if (NS_WARN_IF(!mParent) || NS_WARN_IF(!mParent->IsContainerNode())) { return nullptr; } RawRefType* const ref = Ref(); if (!ref) { if (!MaybeMutationObserved() && *mOffset != 0) { // This means that this boundary is invalid. // `mOffset` is out of bounds. return nullptr; } MOZ_ASSERT(*Offset(OffsetFilter::kValidOffsets) == 0, "invalid RangeBoundary"); nsIContent* firstChild = GetFirstChild(mParent); if (!firstChild) { // Already referring the end of the container. return nullptr; } return GetNextSibling(firstChild); } if (!GetNextSibling(ref)) { // Already referring the end of the container. return nullptr; } return GetNextSibling(GetNextSibling(ref)); } /** * GetPreviousSiblingOfChildAtOffset() returns previous sibling of a child * at offset. If this refers the first child or the container cannot have * children, this returns nullptr with warning. */ RawRefType* GetPreviousSiblingOfChildAtOffset() const { if (NS_WARN_IF(!mParent) || NS_WARN_IF(!mParent->IsContainerNode())) { return nullptr; } RawRefType* const ref = Ref(); if (!ref) { // Already referring the start of the container. return nullptr; } return ref; } /** * Return true if this has already computed/set offset. */ [[nodiscard]] bool HasOffset() const { return mOffset.isSome(); } enum class OffsetFilter { kValidOffsets, kValidOrInvalidOffsets }; /** * @return maybe an offset, depending on aOffsetFilter. If it is: * kValidOffsets: if the offset is valid, it, Nothing{} otherwise. * kValidOrInvalidOffsets: the internally stored offset, even if * invalid, or if not available, a defined * default value. That is, always some value. */ Maybe Offset(const OffsetFilter aOffsetFilter) const { switch (aOffsetFilter) { case OffsetFilter::kValidOffsets: { if (IsSetAndValid()) { MOZ_ASSERT_IF(IsSetByOffset(), mOffset); if (!mOffset && IsSetByRef()) { DetermineOffsetFromReference(); } } return IsSetByOffset() && *mOffset > GetLength(mParent) ? Nothing{} : mOffset; } case OffsetFilter::kValidOrInvalidOffsets: { MOZ_ASSERT_IF(IsSetByOffset(), mOffset.isSome()); if (mOffset.isSome()) { return mOffset; } if (mParent && IsSetByRef()) { DetermineOffsetFromReference(); if (mOffset.isSome()) { return mOffset; } } return Some(kFallbackOffset); } } // Needed to calm the compiler. There was deliberately no default case added // to the above switch-statement, because it would prevent build-errors when // not all enumerators are handled. MOZ_ASSERT_UNREACHABLE(); return Some(kFallbackOffset); } [[nodiscard]] static Maybe ComputeIndexOf(const nsINode* aParent, const nsIContent* aChild, TreeKind aKind) { MOZ_ASSERT(aParent); MOZ_ASSERT(aChild); return aKind == TreeKind::DOM ? aParent->ComputeIndexOf(aChild) : dom::FlattenedChildIteratorForSelection::GetIndexOf(aParent, aChild); } friend std::ostream& operator<<( std::ostream& aStream, const RangeBoundaryBase& aRangeBoundary) { aStream << "{ mParent=" << aRangeBoundary.GetContainer(); if (aRangeBoundary.GetContainer()) { aStream << " (" << *aRangeBoundary.GetContainer() << ", Length=" << aRangeBoundary.GetLength(aRangeBoundary.GetContainer()) << ")"; } if (aRangeBoundary.IsSetByRef()) { aStream << ", mRef=" << aRangeBoundary.mRef; if (aRangeBoundary.mRef) { aStream << " (" << *aRangeBoundary.mRef << ")"; } } aStream << ", mOffset=" << aRangeBoundary.mOffset; aStream << ", mSetBy=" << (aRangeBoundary.IsSetByRef() ? "Ref" : "Offset"); aStream << ", mTreeKind=" << aRangeBoundary.mTreeKind; aStream << " }"; return aStream; } friend auto format_as( const RangeBoundaryBase& aRangeBoundary) { return ToString(aRangeBoundary); } private: void DetermineOffsetFromReference() const { MOZ_ASSERT(mParent); MOZ_ASSERT(mRef); MOZ_ASSERT(IsValidParent(mParent, mRef)); MOZ_ASSERT(IsSetByRef()); MOZ_ASSERT(mOffset.isNothing()); if (mRef->IsBeingRemoved()) { // ComputeIndexOf would return nothing because mRef has already been // removed from the child node chain of mParent. return; } const Maybe index = ComputeIndexOf(mParent, mRef, mTreeKind); NS_WARNING_ASSERTION( index.isSome(), nsFmtCString( FMT_STRING("mRef is not a child of mParent:\nthis={}\nmRef is in " "shadow tree={}\n"), *this, YesOrNo(mRef && mRef->IsInShadowTree())) .get()); MOZ_ASSERT(*index != UINT32_MAX); mOffset.emplace(MOZ_LIKELY(index.isSome()) ? *index + 1u : 0u); } RawRefType* GetNextSibling(const nsIContent* aCurrentNode) const { MOZ_ASSERT(mParent); MOZ_ASSERT(aCurrentNode); return mTreeKind == TreeKind::DOM ? aCurrentNode->GetNextSibling() : dom::FlattenedChildIteratorForSelection::GetNextChild( aCurrentNode); } [[nodiscard]] static nsIContent* ComputeRef(const nsINode* aParent, const nsIContent* aChild, TreeKind aKind) { MOZ_ASSERT(aParent); MOZ_ASSERT(aChild); MOZ_ASSERT(aParent == ComputeParentNode(aChild, aKind)); return aKind == TreeKind::DOM ? aChild->GetPreviousSibling() : dom::FlattenedChildIteratorForSelection::GetPreviousChild( aChild); } RawRefType* GetFirstChild(const nsINode* aNode) const { MOZ_ASSERT(aNode); return mTreeKind == TreeKind::DOM ? aNode->GetFirstChild() : dom::FlattenedChildIteratorForSelection::GetFirstChild(aNode); } [[nodiscard]] static nsINode* ComputeParentNode(const nsIContent* aChild, TreeKind aKind) { MOZ_ASSERT(aChild); if (aKind == TreeKind::DOM) { return aChild->GetParentNode(); } if (dom::HTMLSlotElement* const slot = aChild->GetAssignedSlotForSelection()) { return slot; } nsINode* const parentNode = aChild->GetParentNode(); if (!parentNode) { return nullptr; } if (parentNode->GetAsHTMLSlotElementIfFilledForSelection()) { // If the parent node is a filled , aChild is a fallback content // node which is not in the flattened tree. return nullptr; } const dom::ShadowRoot* const shadowRoot = parentNode->GetShadowRoot(); if (!shadowRoot) { // Don't use shadow host as parent node if aChild is a child of a // ShadowRoot even though we allow the relation in IsValidParent(). // If we return the host, `Selection` will make wrong composed range. return parentNode; } // If the parent node has a shadow root which is a content shadow root, // aChild cannot be a range boundary in the flat tree because all children // of the parent node is replaced with the shadow root in the flat tree // unless assigned to a slot. if (!shadowRoot->IsUAWidget()) { return nullptr; } // If aChild is a child of a ShadowRoot which is for a UA widget, we want to // put it into the host because the ShadowRoot is a native one, not visible // from the web. return shadowRoot->GetHost(); } [[nodiscard]] static bool IsValidParent(const nsINode* aParent, const nsIContent* aChild, TreeKind aKind) { MOZ_ASSERT(aParent); MOZ_ASSERT(aChild); if (aParent == ComputeParentNode(aChild, aKind)) { return true; } if (aKind == TreeKind::FlatForSelection) { // It's okay if aParent is a host of a shadow tree and aChild is a child // of the ShadowRoot. if (aParent->GetShadowRootForSelection() == aChild->GetParentNode()) { return true; } } return false; } [[nodiscard]] bool IsValidParent(const nsINode* aParent, const nsIContent* aChild) const { return IsValidParent(aParent, aChild, mTreeKind); } [[nodiscard]] static uint32_t ComputeLength(const nsINode* aNode, TreeKind aKind) { MOZ_ASSERT(aNode); return aKind == TreeKind::DOM ? aNode->Length() : dom::FlattenedChildIteratorForSelection::GetLength(aNode); } [[nodiscard]] uint32_t GetLength(const nsINode* aNode) const { return ComputeLength(aNode, mTreeKind); } RawRefType* GetChildAt(const nsINode* aParent, uint32_t aOffset) const { MOZ_ASSERT(aParent); return mTreeKind == TreeKind::DOM ? aParent->GetChildAt_Deprecated(aOffset) : dom::FlattenedChildIteratorForSelection::GetChildAt(aParent, aOffset); } [[nodiscard]] static nsIContent* ComputeLastChild(const nsINode* aParent, TreeKind aKind) { MOZ_ASSERT(aParent); return aKind == TreeKind::DOM ? aParent->GetLastChild() : dom::FlattenedChildIteratorForSelection::GetLastChild(aParent); } [[nodiscard]] nsIContent* GetLastChild(const nsINode* aParent) const { return ComputeLastChild(aParent, mTreeKind); } void InvalidateOffset() { MOZ_ASSERT(mParent); MOZ_ASSERT(mParent->IsContainerNode(), "Range is positioned on a text node!"); if (IsSetByOffset()) { // RangeBoundaries that are not used in the context of a // `MutationObserver` use the offset as main source of truth to compute // `mRef`. Therefore, it must not be updated or invalidated. return; } if (!mRef) { MOZ_ASSERT(mOffset.isSome() && mOffset.value() == 0, "Invalidating offset of invalid RangeBoundary?"); return; } mOffset.reset(); } public: void NotifyParentBecomesShadowHost() { MOZ_ASSERT(mParent); MOZ_ASSERT(mParent->IsContainerNode(), "Range is positioned on a text node!"); if (!MaybeMutationObserved()) { // RangeBoundaries that are not used in the context of a // `MutationObserver` use the offset as main source of truth to compute // `mRef`. Therefore, it must not be updated or invalidated. return; } if (!mRef) { MOZ_ASSERT(mOffset.isSome() && mOffset.value() == 0, "Invalidating offset of invalid RangeBoundary?"); return; } if (dom::ShadowRoot* shadowRoot = mParent->GetShadowRootForSelection()) { mParent = shadowRoot; } mOffset = Some(0); } bool IsSet() const { return mParent && (mRef || mOffset.isSome()); } [[nodiscard]] bool IsSetAndInComposedDoc() const { return IsSet() && mParent->IsInComposedDoc(); } bool IsSetAndValid() const { if (!IsSet() || MOZ_UNLIKELY(mParent->NodeType() == nsINode::DOCUMENT_TYPE_NODE)) { return false; } if (IsSetByRef() && Ref()) { // XXX mRef refers previous sibling of pointing child. Therefore, it // seems odd that this becomes invalid due to its removal. Should we // change RangeBoundaryBase to refer child at offset directly? return IsValidParent(GetContainer(), Ref()) && !Ref()->IsBeingRemoved(); } MOZ_ASSERT(mOffset.isSome()); return *mOffset <= ComputeLength(mParent, mTreeKind); } bool IsStartOfContainer() const { // We're at the first point in the container if we don't have a reference, // and our offset is 0. If we don't have a Ref, we should already have an // offset, so we can just directly fetch it. return IsSetByRef() ? !Ref() && mOffset.value() == 0 : mOffset.value() == 0; } bool IsEndOfContainer() const { // We're at the last point in the container if Ref is a pointer to the last // child in GetContainer(), or our Offset() is the same as the length of our // container. If we don't have a Ref, then we should already have an offset, // so we can just directly fetch it. return IsSetByRef() && Ref() ? !GetNextSibling(Ref()) : mOffset.value() == GetLength(mParent); } // Convenience methods for switching between the two types // of RangeBoundary. template >> RawRangeBoundary AsRaw() const { return RawRangeBoundary(*this, mSetBy); } ConstRawRangeBoundary AsConstRaw() const { return ConstRawRangeBoundary(*this, mSetBy); } RangeBoundaryBase& operator=(const RangeBoundaryBase& aOther) { MOZ_ASSERT(mTreeKind == aOther.mTreeKind); if (this != &aOther) { mParent = aOther.mParent; mRef = aOther.mRef; mOffset = aOther.mOffset; mSetBy = aOther.mSetBy; } return *this; } template < typename PT, typename RT, typename RPT = RawParentType, typename = std::enable_if_t || std::is_const_v>> RangeBoundaryBase& CopyFrom(const RangeBoundaryBase& aOther, RangeBoundarySetBy aSetBy) { MOZ_ASSERT(mTreeKind == aOther.mTreeKind); // mParent and mRef can be strong pointers, so better to try to avoid any // extra AddRef/Release calls. if (mParent != aOther.mParent) { mParent = aOther.mParent; } if (mRef != aOther.mRef) { mRef = aOther.mRef; } mSetBy = aSetBy; if (IsSetByOffset() && aOther.mOffset.isNothing()) { // "Fix" the offset from mRef if and only if we won't be updated for // further mutations and aOther has not computed the offset of its mRef. // XXX What should we do if aOther is not updated for mutations and // mOffset has already been invalid? mOffset = aOther.Offset( RangeBoundaryBase::OffsetFilter::kValidOrInvalidOffsets); MOZ_DIAGNOSTIC_ASSERT(mOffset.isSome()); } else { mOffset = aOther.mOffset; } // If the mutation will be observed but the other does not have proper // mRef for its mOffset, we need to compute mRef like the constructor // which takes aOffset. if (IsSetByRef() && !mRef && mParent && mOffset.isSome() && *mOffset) { if (*mOffset == mParent->GetChildCount()) { mRef = GetLastChild(mParent); } else { mRef = GetChildAt(mParent, *mOffset - 1); } } return *this; } bool Equals(const RawParentType* aNode, uint32_t aOffset) const { if (mParent != aNode) { return false; } const Maybe offset = Offset(OffsetFilter::kValidOffsets); return offset && (*offset == aOffset); } template [[nodiscard]] bool operator==(const RangeBoundaryBase& aOther) const { if (!mParent && !aOther.mParent) { return true; } if (mParent != aOther.mParent) { return false; } if (RefIsFixed() && aOther.RefIsFixed()) { return mRef == aOther.mRef; } if (mTreeKind != aOther.mTreeKind) { return false; } return Offset(OffsetFilter::kValidOrInvalidOffsets) == aOther.Offset( RangeBoundaryBase::OffsetFilter::kValidOrInvalidOffsets); } template bool operator!=(const RangeBoundaryBase& aOther) const { return !(*this == aOther); } private: [[nodiscard]] bool RefIsFixed() const { return mParent && ( // If mutation is observed, mRef is the base of mOffset unless // it's not a container node like `Text` node. (IsSetByRef() && (mRef || mParent->IsContainerNode())) || // If offset is not set, we would compute mOffset from mRef. // So, mRef is "fixed" for now. mOffset.isNothing()); } [[nodiscard]] bool IsSetByOffset() const { return !static_cast(mSetBy); } [[nodiscard]] bool IsSetByRef() const { return static_cast(mSetBy); } /** * If dom::Range stores mutations for RangeBoundary instances, mSetBy is * always "Ref". However, it's the default behavior of RangeBoundaryBase so * that even if this returns true, the mutations may not be observed actually. */ [[nodiscard]] bool MaybeMutationObserved() const { return IsSetByRef(); } ParentType mParent; mutable RefType mRef; mutable mozilla::Maybe mOffset; RangeBoundarySetBy mSetBy; const TreeKind mTreeKind; }; template const uint32_t RangeBoundaryBase::kFallbackOffset; inline void ImplCycleCollectionUnlink(RangeBoundary& aField) { ImplCycleCollectionUnlink(aField.mParent); ImplCycleCollectionUnlink(aField.mRef); } inline void ImplCycleCollectionTraverse( nsCycleCollectionTraversalCallback& aCallback, RangeBoundary& aField, const char* aName, uint32_t aFlags) { ImplCycleCollectionTraverse(aCallback, aField.mParent, "mParent", 0); ImplCycleCollectionTraverse(aCallback, aField.mRef, "mRef", 0); } } // namespace mozilla #endif // defined(mozilla_RangeBoundary_h)