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/Assertions.h"
13 #include "mozilla/Maybe.h"
19 template <typename T
, typename U
>
20 class EditorDOMPointBase
;
22 // This class will maintain a reference to the child immediately
23 // before the boundary's offset. We try to avoid computing the
24 // offset as much as possible and just ensure mRef points to the
29 // [child0] [child1] [child2]
33 // If mOffset == 0, mRef is null.
34 // For text nodes, mRef will always be null and the offset will
35 // be kept up-to-date.
37 template <typename ParentType
, typename RefType
>
38 class RangeBoundaryBase
;
40 typedef RangeBoundaryBase
<nsCOMPtr
<nsINode
>, nsCOMPtr
<nsIContent
>>
42 typedef RangeBoundaryBase
<nsINode
*, nsIContent
*> RawRangeBoundary
;
45 * There are two ways of ensuring that `mRef` points to the correct node.
46 * In most cases, the `RangeBoundary` is used by an object that is a
47 * `MutationObserver` (i.e. `nsRange`) and replaces its `RangeBoundary`
48 * objects when its parent chain changes.
49 * However, there are Ranges which are not `MutationObserver`s (i.e.
50 * `StaticRange`). `mRef` may become invalid when a DOM mutation happens.
51 * Therefore, it needs to be recomputed using `mOffset` before it is being
53 * Because recomputing / validating of `mRef` could be an expensive operation,
54 * it should be ensured that `Ref()` is called as few times as possible, i.e.
55 * only once per method of `RangeBoundaryBase`.
57 * Furthermore, there are special implications when the `RangeBoundary` is not
58 * used by an `MutationObserver`:
59 * After a DOM mutation, the Boundary may point to something that is not valid
60 * anymore, i.e. the `mOffset` is larger than `Container()->Length()`. In this
61 * case, `Ref()` and `Get*ChildAtOffset()` return `nullptr` as an indication
62 * that this RangeBoundary is not valid anymore. Also, `IsSetAndValid()`
63 * returns false. However, `IsSet()` will still return true.
66 enum class RangeBoundaryIsMutationObserved
{ No
= 0, Yes
= 1 };
68 // This class has two specializations, one using reference counting
69 // pointers and one using raw pointers. This helps us avoid unnecessary
70 // AddRef/Release calls.
71 template <typename ParentType
, typename RefType
>
72 class RangeBoundaryBase
{
73 template <typename T
, typename U
>
74 friend class RangeBoundaryBase
;
75 template <typename T
, typename U
>
76 friend class EditorDOMPointBase
;
80 friend void ImplCycleCollectionTraverse(nsCycleCollectionTraversalCallback
&,
81 RangeBoundary
&, const char*,
83 friend void ImplCycleCollectionUnlink(RangeBoundary
&);
85 static const uint32_t kFallbackOffset
= 0;
88 RangeBoundaryBase(nsINode
* aContainer
, nsIContent
* aRef
)
89 : mParent(aContainer
), mRef(aRef
), mIsMutationObserved(true) {
91 NS_WARNING_ASSERTION(mRef
->GetParentNode() == mParent
,
92 "Initializing RangeBoundary with invalid value");
98 RangeBoundaryBase(nsINode
* aContainer
, uint32_t aOffset
,
99 RangeBoundaryIsMutationObserved aRangeIsMutationObserver
=
100 RangeBoundaryIsMutationObserved::Yes
)
101 : mParent(aContainer
),
103 mOffset(mozilla::Some(aOffset
)),
104 mIsMutationObserved(bool(aRangeIsMutationObserver
)) {
105 if (mIsMutationObserved
&& mParent
&& mParent
->IsContainerNode()) {
106 // Find a reference node
107 if (aOffset
== mParent
->GetChildCount()) {
108 mRef
= mParent
->GetLastChild();
109 } else if (aOffset
> 0) {
110 mRef
= mParent
->GetChildAt_Deprecated(aOffset
- 1);
112 NS_WARNING_ASSERTION(mRef
|| aOffset
== 0,
113 "Constructing RangeBoundary with invalid value");
115 NS_WARNING_ASSERTION(!mRef
|| mRef
->GetParentNode() == mParent
,
116 "Constructing RangeBoundary with invalid value");
120 : mParent(nullptr), mRef(nullptr), mIsMutationObserved(true) {}
122 // Needed for initializing RawRangeBoundary from an existing RangeBoundary.
123 template <typename PT
, typename RT
>
124 RangeBoundaryBase(const RangeBoundaryBase
<PT
, RT
>& aOther
,
125 RangeBoundaryIsMutationObserved aIsMutationObserved
)
126 : mParent(aOther
.mParent
),
128 mOffset(aOther
.mOffset
),
129 mIsMutationObserved(bool(aIsMutationObserved
)) {}
132 * This method may return `nullptr` in two cases:
133 * 1. `mIsMutationObserved` is true and the boundary points to the first
134 * child of `mParent`.
135 * 2. `mIsMutationObserved` is false and `mOffset` is out of bounds for
136 * `mParent`s child list.
137 * If `mIsMutationObserved` is false, this method may do some significant
138 * computation. Therefore it is advised to call it as seldom as possible.
139 * Code inside of this class should call this method exactly one time and
140 * afterwards refer to `mRef` directly.
142 nsIContent
* Ref() const {
143 if (mIsMutationObserved
) {
149 // `mRef` may have become invalid due to some DOM mutation,
150 // which is not monitored here. Therefore, we need to validate `mRef`
152 if (*mOffset
> Container()->Length()) {
153 // offset > child count means that the range boundary has become invalid
154 // due to a DOM mutation.
156 } else if (*mOffset
== Container()->Length()) {
157 mRef
= mParent
->GetLastChild();
158 } else if (*mOffset
) {
159 // validate and update `mRef`.
160 // If `ComputeIndexOf()` returns `Nothing`, then `mRef` is not a child of
161 // `mParent` anymore.
162 // If the returned index for `mRef` does not match to `mOffset`, `mRef`
163 // needs to be updated.
164 auto indexOfRefObject
= mParent
->ComputeIndexOf(mRef
);
165 if (indexOfRefObject
.isNothing() || *mOffset
!= *indexOfRefObject
+ 1) {
166 mRef
= mParent
->GetChildAt_Deprecated(*mOffset
- 1);
174 nsINode
* Container() const { return mParent
; }
177 * This method may return `nullptr` if `mIsMutationObserved` is false and
178 * `mOffset` is out of bounds.
180 nsIContent
* GetChildAtOffset() const {
181 if (!mParent
|| !mParent
->IsContainerNode()) {
184 nsIContent
* const ref
= Ref();
186 if (!mIsMutationObserved
&& *mOffset
!= 0) {
187 // This means that this boundary is invalid.
188 // `mOffset` is out of bounds.
191 MOZ_ASSERT(*Offset(OffsetFilter::kValidOrInvalidOffsets
) == 0,
192 "invalid RangeBoundary");
193 return mParent
->GetFirstChild();
195 MOZ_ASSERT(mParent
->GetChildAt_Deprecated(
196 *Offset(OffsetFilter::kValidOrInvalidOffsets
)) ==
197 ref
->GetNextSibling());
198 return ref
->GetNextSibling();
202 * GetNextSiblingOfChildOffset() returns next sibling of a child at offset.
203 * If this refers after the last child or the container cannot have children,
204 * this returns nullptr with warning.
206 nsIContent
* GetNextSiblingOfChildAtOffset() const {
207 if (NS_WARN_IF(!mParent
) || NS_WARN_IF(!mParent
->IsContainerNode())) {
210 nsIContent
* const ref
= Ref();
212 if (!mIsMutationObserved
&& *mOffset
!= 0) {
213 // This means that this boundary is invalid.
214 // `mOffset` is out of bounds.
217 MOZ_ASSERT(*Offset(OffsetFilter::kValidOffsets
) == 0,
218 "invalid RangeBoundary");
219 nsIContent
* firstChild
= mParent
->GetFirstChild();
220 if (NS_WARN_IF(!firstChild
)) {
221 // Already referring the end of the container.
224 return firstChild
->GetNextSibling();
226 if (NS_WARN_IF(!ref
->GetNextSibling())) {
227 // Already referring the end of the container.
230 return ref
->GetNextSibling()->GetNextSibling();
234 * GetPreviousSiblingOfChildAtOffset() returns previous sibling of a child
235 * at offset. If this refers the first child or the container cannot have
236 * children, this returns nullptr with warning.
238 nsIContent
* GetPreviousSiblingOfChildAtOffset() const {
239 if (NS_WARN_IF(!mParent
) || NS_WARN_IF(!mParent
->IsContainerNode())) {
242 nsIContent
* const ref
= Ref();
243 if (NS_WARN_IF(!ref
)) {
244 // Already referring the start of the container.
250 enum class OffsetFilter
{ kValidOffsets
, kValidOrInvalidOffsets
};
253 * @return maybe an offset, depending on aOffsetFilter. If it is:
254 * kValidOffsets: if the offset is valid, it, Nothing{} otherwise.
255 * kValidOrInvalidOffsets: the internally stored offset, even if
256 * invalid, or if not available, a defined
257 * default value. That is, always some value.
259 Maybe
<uint32_t> Offset(const OffsetFilter aOffsetFilter
) const {
260 switch (aOffsetFilter
) {
261 case OffsetFilter::kValidOffsets
: {
262 if (IsSetAndValid()) {
263 MOZ_ASSERT_IF(!mIsMutationObserved
, mOffset
);
264 if (!mOffset
&& mIsMutationObserved
) {
265 DetermineOffsetFromReference();
268 return !mIsMutationObserved
&& *mOffset
> Container()->Length()
272 case OffsetFilter::kValidOrInvalidOffsets
: {
273 MOZ_ASSERT_IF(!mIsMutationObserved
, mOffset
.isSome());
274 if (mOffset
.isSome()) {
277 if (mParent
&& mIsMutationObserved
) {
278 DetermineOffsetFromReference();
279 if (mOffset
.isSome()) {
284 return Some(kFallbackOffset
);
288 // Needed to calm the compiler. There was deliberately no default case added
289 // to the above switch-statement, because it would prevent build-errors when
290 // not all enumerators are handled.
291 MOZ_ASSERT_UNREACHABLE();
292 return Some(kFallbackOffset
);
295 friend std::ostream
& operator<<(
296 std::ostream
& aStream
,
297 const RangeBoundaryBase
<ParentType
, RefType
>& aRangeBoundary
) {
298 aStream
<< "{ mParent=" << aRangeBoundary
.Container();
299 if (aRangeBoundary
.Container()) {
300 aStream
<< " (" << *aRangeBoundary
.Container()
301 << ", Length()=" << aRangeBoundary
.Container()->Length() << ")";
303 if (aRangeBoundary
.mIsMutationObserved
) {
304 aStream
<< ", mRef=" << aRangeBoundary
.mRef
;
305 if (aRangeBoundary
.mRef
) {
306 aStream
<< " (" << *aRangeBoundary
.mRef
<< ")";
310 aStream
<< ", mOffset=" << aRangeBoundary
.mOffset
;
311 aStream
<< ", mIsMutationObserved="
312 << (aRangeBoundary
.mIsMutationObserved
? "true" : "false") << " }";
317 void DetermineOffsetFromReference() const {
320 MOZ_ASSERT(mRef
->GetParentNode() == mParent
);
321 MOZ_ASSERT(mIsMutationObserved
);
322 MOZ_ASSERT(mOffset
.isNothing());
324 if (mRef
->IsBeingRemoved()) {
325 // ComputeIndexOf would return nothing because mRef has already been
326 // removed from the child node chain of mParent.
330 const Maybe
<uint32_t> index
= mParent
->ComputeIndexOf(mRef
);
331 MOZ_ASSERT(*index
!= UINT32_MAX
);
332 mOffset
.emplace(MOZ_LIKELY(index
.isSome()) ? *index
+ 1u : 0u);
335 void InvalidateOffset() {
337 MOZ_ASSERT(mParent
->IsContainerNode(),
338 "Range is positioned on a text node!");
339 if (!mIsMutationObserved
) {
340 // RangeBoundaries that are not used in the context of a
341 // `MutationObserver` use the offset as main source of truth to compute
342 // `mRef`. Therefore, it must not be updated or invalidated.
346 MOZ_ASSERT(mOffset
.isSome() && mOffset
.value() == 0,
347 "Invalidating offset of invalid RangeBoundary?");
354 bool IsSet() const { return mParent
&& (mRef
|| mOffset
.isSome()); }
356 bool IsSetAndValid() const {
361 if (mIsMutationObserved
&& Ref()) {
362 // XXX mRef refers previous sibling of pointing child. Therefore, it
363 // seems odd that this becomes invalid due to its removal. Should we
364 // change RangeBoundaryBase to refer child at offset directly?
365 return Ref()->GetParentNode() == Container() && !Ref()->IsBeingRemoved();
368 MOZ_ASSERT(mOffset
.isSome());
369 return *mOffset
<= Container()->Length();
372 bool IsStartOfContainer() const {
373 // We're at the first point in the container if we don't have a reference,
374 // and our offset is 0. If we don't have a Ref, we should already have an
375 // offset, so we can just directly fetch it.
376 return mIsMutationObserved
? !Ref() && mOffset
.value() == 0
377 : mOffset
.value() == 0;
380 bool IsEndOfContainer() const {
381 // We're at the last point in the container if Ref is a pointer to the last
382 // child in Container(), or our Offset() is the same as the length of our
383 // container. If we don't have a Ref, then we should already have an offset,
384 // so we can just directly fetch it.
385 return mIsMutationObserved
&& Ref()
386 ? !Ref()->GetNextSibling()
387 : mOffset
.value() == Container()->Length();
390 // Convenience methods for switching between the two types
392 RangeBoundaryBase
<nsINode
*, nsIContent
*> AsRaw() const {
393 return RangeBoundaryBase
<nsINode
*, nsIContent
*>(
394 *this, RangeBoundaryIsMutationObserved(mIsMutationObserved
));
397 template <typename A
, typename B
>
398 RangeBoundaryBase
& operator=(const RangeBoundaryBase
<A
, B
>& aOther
) = delete;
400 template <typename A
, typename B
>
401 RangeBoundaryBase
& CopyFrom(
402 const RangeBoundaryBase
<A
, B
>& aOther
,
403 RangeBoundaryIsMutationObserved aIsMutationObserved
) {
404 // mParent and mRef can be strong pointers, so better to try to avoid any
405 // extra AddRef/Release calls.
406 if (mParent
!= aOther
.mParent
) {
407 mParent
= aOther
.mParent
;
409 if (mRef
!= aOther
.mRef
) {
412 mOffset
= aOther
.mOffset
;
413 mIsMutationObserved
= bool(aIsMutationObserved
);
417 bool Equals(const nsINode
* aNode
, uint32_t aOffset
) const {
418 if (mParent
!= aNode
) {
422 const Maybe
<uint32_t> offset
= Offset(OffsetFilter::kValidOffsets
);
423 return offset
&& (*offset
== aOffset
);
426 template <typename A
, typename B
>
427 bool operator==(const RangeBoundaryBase
<A
, B
>& aOther
) const {
428 return mParent
== aOther
.mParent
&&
429 (mIsMutationObserved
&& aOther
.mIsMutationObserved
&& mRef
430 ? mRef
== aOther
.mRef
431 : Offset(OffsetFilter::kValidOrInvalidOffsets
) ==
434 A
, B
>::OffsetFilter::kValidOrInvalidOffsets
));
437 template <typename A
, typename B
>
438 bool operator!=(const RangeBoundaryBase
<A
, B
>& aOther
) const {
439 return !(*this == aOther
);
444 mutable RefType mRef
;
446 mutable mozilla::Maybe
<uint32_t> mOffset
;
447 bool mIsMutationObserved
;
450 template <typename ParentType
, typename RefType
>
451 const uint32_t RangeBoundaryBase
<ParentType
, RefType
>::kFallbackOffset
;
453 inline void ImplCycleCollectionUnlink(RangeBoundary
& aField
) {
454 ImplCycleCollectionUnlink(aField
.mParent
);
455 ImplCycleCollectionUnlink(aField
.mRef
);
458 inline void ImplCycleCollectionTraverse(
459 nsCycleCollectionTraversalCallback
& aCallback
, RangeBoundary
& aField
,
460 const char* aName
, uint32_t aFlags
) {
461 ImplCycleCollectionTraverse(aCallback
, aField
.mParent
, "mParent", 0);
462 ImplCycleCollectionTraverse(aCallback
, aField
.mRef
, "mRef", 0);
465 } // namespace mozilla
467 #endif // defined(mozilla_RangeBoundary_h)