1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 /* vim: set ts=8 sts=2 et sw=2 tw=80: */
3 /* This Source Code Form is subject to the terms of the Mozilla Public
4 * License, v. 2.0. If a copy of the MPL was not distributed with this
5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
7 #ifndef mozilla_RangeBoundary_h
8 #define mozilla_RangeBoundary_h
11 #include "nsIContent.h"
12 #include "mozilla/dom/ShadowRoot.h"
13 #include "mozilla/Assertions.h"
14 #include "mozilla/Maybe.h"
20 template <typename T
, typename U
>
21 class EditorDOMPointBase
;
23 // This class will maintain a reference to the child immediately
24 // before the boundary's offset. We try to avoid computing the
25 // offset as much as possible and just ensure mRef points to the
30 // [child0] [child1] [child2]
34 // If mOffset == 0, mRef is null.
35 // For text nodes, mRef will always be null and the offset will
36 // be kept up-to-date.
38 template <typename ParentType
, typename RefType
>
39 class RangeBoundaryBase
;
41 typedef RangeBoundaryBase
<nsCOMPtr
<nsINode
>, nsCOMPtr
<nsIContent
>>
43 typedef RangeBoundaryBase
<nsINode
*, nsIContent
*> RawRangeBoundary
;
46 * There are two ways of ensuring that `mRef` points to the correct node.
47 * In most cases, the `RangeBoundary` is used by an object that is a
48 * `MutationObserver` (i.e. `nsRange`) and replaces its `RangeBoundary`
49 * objects when its parent chain changes.
50 * However, there are Ranges which are not `MutationObserver`s (i.e.
51 * `StaticRange`). `mRef` may become invalid when a DOM mutation happens.
52 * Therefore, it needs to be recomputed using `mOffset` before it is being
54 * Because recomputing / validating of `mRef` could be an expensive operation,
55 * it should be ensured that `Ref()` is called as few times as possible, i.e.
56 * only once per method of `RangeBoundaryBase`.
58 * Furthermore, there are special implications when the `RangeBoundary` is not
59 * used by an `MutationObserver`:
60 * After a DOM mutation, the Boundary may point to something that is not valid
61 * anymore, i.e. the `mOffset` is larger than `Container()->Length()`. In this
62 * case, `Ref()` and `Get*ChildAtOffset()` return `nullptr` as an indication
63 * that this RangeBoundary is not valid anymore. Also, `IsSetAndValid()`
64 * returns false. However, `IsSet()` will still return true.
67 enum class RangeBoundaryIsMutationObserved
{ No
= 0, Yes
= 1 };
69 // This class has two specializations, one using reference counting
70 // pointers and one using raw pointers. This helps us avoid unnecessary
71 // AddRef/Release calls.
72 template <typename ParentType
, typename RefType
>
73 class RangeBoundaryBase
{
74 template <typename T
, typename U
>
75 friend class RangeBoundaryBase
;
76 template <typename T
, typename U
>
77 friend class EditorDOMPointBase
;
81 friend void ImplCycleCollectionTraverse(nsCycleCollectionTraversalCallback
&,
82 RangeBoundary
&, const char*,
84 friend void ImplCycleCollectionUnlink(RangeBoundary
&);
86 static const uint32_t kFallbackOffset
= 0;
89 RangeBoundaryBase(nsINode
* aContainer
, nsIContent
* aRef
)
90 : mParent(aContainer
), mRef(aRef
), mIsMutationObserved(true) {
92 NS_WARNING_ASSERTION(mRef
->GetParentNode() == mParent
,
93 "Initializing RangeBoundary with invalid value");
99 RangeBoundaryBase(nsINode
* aContainer
, uint32_t aOffset
,
100 RangeBoundaryIsMutationObserved aRangeIsMutationObserver
=
101 RangeBoundaryIsMutationObserved::Yes
)
102 : mParent(aContainer
),
104 mOffset(mozilla::Some(aOffset
)),
105 mIsMutationObserved(bool(aRangeIsMutationObserver
)) {
106 if (mIsMutationObserved
&& mParent
&& mParent
->IsContainerNode()) {
107 // Find a reference node
108 if (aOffset
== mParent
->GetChildCount()) {
109 mRef
= mParent
->GetLastChild();
110 } else if (aOffset
> 0) {
111 mRef
= mParent
->GetChildAt_Deprecated(aOffset
- 1);
113 NS_WARNING_ASSERTION(mRef
|| aOffset
== 0,
114 "Constructing RangeBoundary with invalid value");
116 NS_WARNING_ASSERTION(!mRef
|| mRef
->GetParentNode() == mParent
,
117 "Constructing RangeBoundary with invalid value");
121 : mParent(nullptr), mRef(nullptr), mIsMutationObserved(true) {}
123 // Needed for initializing RawRangeBoundary from an existing RangeBoundary.
124 template <typename PT
, typename RT
>
125 RangeBoundaryBase(const RangeBoundaryBase
<PT
, RT
>& aOther
,
126 RangeBoundaryIsMutationObserved aIsMutationObserved
)
127 : mParent(aOther
.mParent
),
129 mOffset(aOther
.mOffset
),
130 mIsMutationObserved(bool(aIsMutationObserved
)) {}
133 * This method may return `nullptr` in two cases:
134 * 1. `mIsMutationObserved` is true and the boundary points to the first
135 * child of `mParent`.
136 * 2. `mIsMutationObserved` is false and `mOffset` is out of bounds for
137 * `mParent`s child list.
138 * If `mIsMutationObserved` is false, this method may do some significant
139 * computation. Therefore it is advised to call it as seldom as possible.
140 * Code inside of this class should call this method exactly one time and
141 * afterwards refer to `mRef` directly.
143 nsIContent
* Ref() const {
144 if (mIsMutationObserved
) {
150 // `mRef` may have become invalid due to some DOM mutation,
151 // which is not monitored here. Therefore, we need to validate `mRef`
153 if (*mOffset
> Container()->Length()) {
154 // offset > child count means that the range boundary has become invalid
155 // due to a DOM mutation.
157 } else if (*mOffset
== Container()->Length()) {
158 mRef
= mParent
->GetLastChild();
159 } else if (*mOffset
) {
160 // validate and update `mRef`.
161 // If `ComputeIndexOf()` returns `Nothing`, then `mRef` is not a child of
162 // `mParent` anymore.
163 // If the returned index for `mRef` does not match to `mOffset`, `mRef`
164 // needs to be updated.
165 auto indexOfRefObject
= mParent
->ComputeIndexOf(mRef
);
166 if (indexOfRefObject
.isNothing() || *mOffset
!= *indexOfRefObject
+ 1) {
167 mRef
= mParent
->GetChildAt_Deprecated(*mOffset
- 1);
175 nsINode
* Container() const { return mParent
; }
178 * This method may return `nullptr` if `mIsMutationObserved` is false and
179 * `mOffset` is out of bounds.
181 nsIContent
* GetChildAtOffset() const {
182 if (!mParent
|| !mParent
->IsContainerNode()) {
185 nsIContent
* const ref
= Ref();
187 if (!mIsMutationObserved
&& *mOffset
!= 0) {
188 // This means that this boundary is invalid.
189 // `mOffset` is out of bounds.
192 MOZ_ASSERT(*Offset(OffsetFilter::kValidOrInvalidOffsets
) == 0,
193 "invalid RangeBoundary");
194 return mParent
->GetFirstChild();
196 MOZ_ASSERT(mParent
->GetChildAt_Deprecated(
197 *Offset(OffsetFilter::kValidOrInvalidOffsets
)) ==
198 ref
->GetNextSibling());
199 return ref
->GetNextSibling();
203 * GetNextSiblingOfChildOffset() returns next sibling of a child at offset.
204 * If this refers after the last child or the container cannot have children,
205 * this returns nullptr with warning.
207 nsIContent
* GetNextSiblingOfChildAtOffset() const {
208 if (NS_WARN_IF(!mParent
) || NS_WARN_IF(!mParent
->IsContainerNode())) {
211 nsIContent
* const ref
= Ref();
213 if (!mIsMutationObserved
&& *mOffset
!= 0) {
214 // This means that this boundary is invalid.
215 // `mOffset` is out of bounds.
218 MOZ_ASSERT(*Offset(OffsetFilter::kValidOffsets
) == 0,
219 "invalid RangeBoundary");
220 nsIContent
* firstChild
= mParent
->GetFirstChild();
221 if (NS_WARN_IF(!firstChild
)) {
222 // Already referring the end of the container.
225 return firstChild
->GetNextSibling();
227 if (NS_WARN_IF(!ref
->GetNextSibling())) {
228 // Already referring the end of the container.
231 return ref
->GetNextSibling()->GetNextSibling();
235 * GetPreviousSiblingOfChildAtOffset() returns previous sibling of a child
236 * at offset. If this refers the first child or the container cannot have
237 * children, this returns nullptr with warning.
239 nsIContent
* GetPreviousSiblingOfChildAtOffset() const {
240 if (NS_WARN_IF(!mParent
) || NS_WARN_IF(!mParent
->IsContainerNode())) {
243 nsIContent
* const ref
= Ref();
244 if (NS_WARN_IF(!ref
)) {
245 // Already referring the start of the container.
251 enum class OffsetFilter
{ kValidOffsets
, kValidOrInvalidOffsets
};
254 * @return maybe an offset, depending on aOffsetFilter. If it is:
255 * kValidOffsets: if the offset is valid, it, Nothing{} otherwise.
256 * kValidOrInvalidOffsets: the internally stored offset, even if
257 * invalid, or if not available, a defined
258 * default value. That is, always some value.
260 Maybe
<uint32_t> Offset(const OffsetFilter aOffsetFilter
) const {
261 switch (aOffsetFilter
) {
262 case OffsetFilter::kValidOffsets
: {
263 if (IsSetAndValid()) {
264 MOZ_ASSERT_IF(!mIsMutationObserved
, mOffset
);
265 if (!mOffset
&& mIsMutationObserved
) {
266 DetermineOffsetFromReference();
269 return !mIsMutationObserved
&& *mOffset
> Container()->Length()
273 case OffsetFilter::kValidOrInvalidOffsets
: {
274 MOZ_ASSERT_IF(!mIsMutationObserved
, mOffset
.isSome());
275 if (mOffset
.isSome()) {
278 if (mParent
&& mIsMutationObserved
) {
279 DetermineOffsetFromReference();
280 if (mOffset
.isSome()) {
285 return Some(kFallbackOffset
);
289 // Needed to calm the compiler. There was deliberately no default case added
290 // to the above switch-statement, because it would prevent build-errors when
291 // not all enumerators are handled.
292 MOZ_ASSERT_UNREACHABLE();
293 return Some(kFallbackOffset
);
296 friend std::ostream
& operator<<(
297 std::ostream
& aStream
,
298 const RangeBoundaryBase
<ParentType
, RefType
>& aRangeBoundary
) {
299 aStream
<< "{ mParent=" << aRangeBoundary
.Container();
300 if (aRangeBoundary
.Container()) {
301 aStream
<< " (" << *aRangeBoundary
.Container()
302 << ", Length()=" << aRangeBoundary
.Container()->Length() << ")";
304 if (aRangeBoundary
.mIsMutationObserved
) {
305 aStream
<< ", mRef=" << aRangeBoundary
.mRef
;
306 if (aRangeBoundary
.mRef
) {
307 aStream
<< " (" << *aRangeBoundary
.mRef
<< ")";
311 aStream
<< ", mOffset=" << aRangeBoundary
.mOffset
;
312 aStream
<< ", mIsMutationObserved="
313 << (aRangeBoundary
.mIsMutationObserved
? "true" : "false") << " }";
318 void DetermineOffsetFromReference() const {
321 MOZ_ASSERT(mRef
->GetParentNode() == mParent
);
322 MOZ_ASSERT(mIsMutationObserved
);
323 MOZ_ASSERT(mOffset
.isNothing());
325 if (mRef
->IsBeingRemoved()) {
326 // ComputeIndexOf would return nothing because mRef has already been
327 // removed from the child node chain of mParent.
331 const Maybe
<uint32_t> index
= mParent
->ComputeIndexOf(mRef
);
332 MOZ_ASSERT(*index
!= UINT32_MAX
);
333 mOffset
.emplace(MOZ_LIKELY(index
.isSome()) ? *index
+ 1u : 0u);
336 void InvalidateOffset() {
338 MOZ_ASSERT(mParent
->IsContainerNode(),
339 "Range is positioned on a text node!");
340 if (!mIsMutationObserved
) {
341 // RangeBoundaries that are not used in the context of a
342 // `MutationObserver` use the offset as main source of truth to compute
343 // `mRef`. Therefore, it must not be updated or invalidated.
347 MOZ_ASSERT(mOffset
.isSome() && mOffset
.value() == 0,
348 "Invalidating offset of invalid RangeBoundary?");
355 void NotifyParentBecomesShadowHost() {
357 MOZ_ASSERT(mParent
->IsContainerNode(),
358 "Range is positioned on a text node!");
359 if (!StaticPrefs::dom_shadowdom_selection_across_boundary_enabled()) {
363 if (!mIsMutationObserved
) {
364 // RangeBoundaries that are not used in the context of a
365 // `MutationObserver` use the offset as main source of truth to compute
366 // `mRef`. Therefore, it must not be updated or invalidated.
371 MOZ_ASSERT(mOffset
.isSome() && mOffset
.value() == 0,
372 "Invalidating offset of invalid RangeBoundary?");
376 if (dom::ShadowRoot
* shadowRoot
= mParent
->GetShadowRootForSelection()) {
377 mParent
= shadowRoot
;
383 bool IsSet() const { return mParent
&& (mRef
|| mOffset
.isSome()); }
385 bool IsSetAndValid() const {
390 if (mIsMutationObserved
&& Ref()) {
391 // XXX mRef refers previous sibling of pointing child. Therefore, it
392 // seems odd that this becomes invalid due to its removal. Should we
393 // change RangeBoundaryBase to refer child at offset directly?
394 return Ref()->GetParentNode() == Container() && !Ref()->IsBeingRemoved();
397 MOZ_ASSERT(mOffset
.isSome());
398 return *mOffset
<= Container()->Length();
401 bool IsStartOfContainer() const {
402 // We're at the first point in the container if we don't have a reference,
403 // and our offset is 0. If we don't have a Ref, we should already have an
404 // offset, so we can just directly fetch it.
405 return mIsMutationObserved
? !Ref() && mOffset
.value() == 0
406 : mOffset
.value() == 0;
409 bool IsEndOfContainer() const {
410 // We're at the last point in the container if Ref is a pointer to the last
411 // child in Container(), or our Offset() is the same as the length of our
412 // container. If we don't have a Ref, then we should already have an offset,
413 // so we can just directly fetch it.
414 return mIsMutationObserved
&& Ref()
415 ? !Ref()->GetNextSibling()
416 : mOffset
.value() == Container()->Length();
419 // Convenience methods for switching between the two types
421 RangeBoundaryBase
<nsINode
*, nsIContent
*> AsRaw() const {
422 return RangeBoundaryBase
<nsINode
*, nsIContent
*>(
423 *this, RangeBoundaryIsMutationObserved(mIsMutationObserved
));
426 template <typename A
, typename B
>
427 RangeBoundaryBase
& operator=(const RangeBoundaryBase
<A
, B
>& aOther
) = delete;
429 template <typename A
, typename B
>
430 RangeBoundaryBase
& CopyFrom(
431 const RangeBoundaryBase
<A
, B
>& aOther
,
432 RangeBoundaryIsMutationObserved aIsMutationObserved
) {
433 // mParent and mRef can be strong pointers, so better to try to avoid any
434 // extra AddRef/Release calls.
435 if (mParent
!= aOther
.mParent
) {
436 mParent
= aOther
.mParent
;
438 if (mRef
!= aOther
.mRef
) {
441 mOffset
= aOther
.mOffset
;
442 mIsMutationObserved
= bool(aIsMutationObserved
);
446 bool Equals(const nsINode
* aNode
, uint32_t aOffset
) const {
447 if (mParent
!= aNode
) {
451 const Maybe
<uint32_t> offset
= Offset(OffsetFilter::kValidOffsets
);
452 return offset
&& (*offset
== aOffset
);
455 template <typename A
, typename B
>
456 bool operator==(const RangeBoundaryBase
<A
, B
>& aOther
) const {
457 return mParent
== aOther
.mParent
&&
458 (mIsMutationObserved
&& aOther
.mIsMutationObserved
&& mRef
459 ? mRef
== aOther
.mRef
460 : Offset(OffsetFilter::kValidOrInvalidOffsets
) ==
463 A
, B
>::OffsetFilter::kValidOrInvalidOffsets
));
466 template <typename A
, typename B
>
467 bool operator!=(const RangeBoundaryBase
<A
, B
>& aOther
) const {
468 return !(*this == aOther
);
473 mutable RefType mRef
;
475 mutable mozilla::Maybe
<uint32_t> mOffset
;
476 bool mIsMutationObserved
;
479 template <typename ParentType
, typename RefType
>
480 const uint32_t RangeBoundaryBase
<ParentType
, RefType
>::kFallbackOffset
;
482 inline void ImplCycleCollectionUnlink(RangeBoundary
& aField
) {
483 ImplCycleCollectionUnlink(aField
.mParent
);
484 ImplCycleCollectionUnlink(aField
.mRef
);
487 inline void ImplCycleCollectionTraverse(
488 nsCycleCollectionTraversalCallback
& aCallback
, RangeBoundary
& aField
,
489 const char* aName
, uint32_t aFlags
) {
490 ImplCycleCollectionTraverse(aCallback
, aField
.mParent
, "mParent", 0);
491 ImplCycleCollectionTraverse(aCallback
, aField
.mRef
, "mRef", 0);
494 } // namespace mozilla
496 #endif // defined(mozilla_RangeBoundary_h)