Skip to main content

script/dom/
selection.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
5use std::cell::Cell;
6use std::cmp::Ordering;
7
8use dom_struct::dom_struct;
9use js::context::{JSContext, NoGC};
10use script_bindings::reflector::{Reflector, reflect_dom_object_with_cx};
11
12use crate::dom::abstractrange::bp_position;
13use crate::dom::bindings::codegen::Bindings::NodeBinding::{GetRootNodeOptions, NodeMethods};
14use crate::dom::bindings::codegen::Bindings::RangeBinding::RangeMethods;
15use crate::dom::bindings::codegen::Bindings::SelectionBinding::SelectionMethods;
16use crate::dom::bindings::error::{Error, ErrorResult, Fallible};
17use crate::dom::bindings::inheritance::Castable;
18use crate::dom::bindings::refcounted::Trusted;
19use crate::dom::bindings::reflector::DomGlobal;
20use crate::dom::bindings::root::{Dom, DomRoot, MutNullableDom};
21use crate::dom::bindings::str::DOMString;
22use crate::dom::document::Document;
23use crate::dom::eventtarget::EventTarget;
24use crate::dom::node::{Node, NodeTraits};
25use crate::dom::range::Range;
26
27#[derive(Clone, Copy, JSTraceable, MallocSizeOf)]
28enum Direction {
29    Forwards,
30    Backwards,
31    Directionless,
32}
33
34#[dom_struct]
35pub(crate) struct Selection {
36    reflector_: Reflector,
37    document: Dom<Document>,
38    range: MutNullableDom<Range>,
39    direction: Cell<Direction>,
40    /// <https://w3c.github.io/selection-api/#dfn-has-scheduled-selectionchange-event>
41    has_scheduled_selectionchange_event: Cell<bool>,
42}
43
44impl Selection {
45    fn new_inherited(document: &Document) -> Selection {
46        Selection {
47            reflector_: Reflector::new(),
48            document: Dom::from_ref(document),
49            range: MutNullableDom::new(None),
50            direction: Cell::new(Direction::Directionless),
51            has_scheduled_selectionchange_event: Cell::new(false),
52        }
53    }
54
55    pub(crate) fn new(cx: &mut JSContext, document: &Document) -> DomRoot<Selection> {
56        reflect_dom_object_with_cx(
57            Box::new(Selection::new_inherited(document)),
58            &*document.global(),
59            cx,
60        )
61    }
62
63    fn set_range(&self, range: &Range) {
64        // If we are setting to literally the same Range object
65        // (not just the same positions), then there's nothing changing
66        // and no task to queue.
67        if let Some(existing) = self.range.get() &&
68            &*existing == range
69        {
70            return;
71        }
72        self.range.set(Some(range));
73        range.associate_selection(self);
74        self.queue_selectionchange_task();
75    }
76
77    fn clear_range(&self) {
78        // If we already don't have a a Range object, then there's
79        // nothing changing and no task to queue.
80        if let Some(range) = self.range.get() {
81            range.disassociate_selection(self);
82            self.range.set(None);
83            self.queue_selectionchange_task();
84        }
85    }
86
87    /// <https://w3c.github.io/selection-api/#dfn-schedule-a-selectionchange-event>
88    pub(crate) fn queue_selectionchange_task(&self) {
89        // https://w3c.github.io/editing/docs/execCommand/#state-override
90        // https://w3c.github.io/editing/docs/execCommand/#value-override
91        // > Whenever the number of ranges in the selection changes to something
92        // > different, and whenever a boundary point of the range at a given index in the
93        // > selection changes to something different, the state override and value
94        // > override must be unset for every command.
95        self.document.clear_command_overrides();
96
97        // Step 1. If target's has scheduled selectionchange event is true, abort these steps.
98        if self.has_scheduled_selectionchange_event.get() {
99            return;
100        }
101        // Step 2. Set target's has scheduled selectionchange event to true.
102        self.has_scheduled_selectionchange_event.set(true);
103        // Step 3. Queue a task on the user interaction task source to fire a
104        // selectionchange event on target.
105        let this = Trusted::new(self);
106        self.document
107            .owner_global()
108            .task_manager()
109            .user_interaction_task_source() // w3c/selection-api#117
110            .queue(
111                // https://w3c.github.io/selection-api/#firing-selectionchange-event
112                task!(selectionchange_task_steps: move |cx| {
113                    let this = this.root();
114                    // Step 1. Set target's has scheduled selectionchange event to false.
115                    this.has_scheduled_selectionchange_event.set(false);
116                    // Step 2. If target is an element, fire an event named
117                    // selectionchange, which bubbles and not cancelable, at target.
118                    //
119                    // n/a
120
121                    // Step 3. Otherwise, if target is a document, fire an event named
122                    // selectionchange, which does not bubble and not cancelable, at
123                    // target.
124                    this.document.upcast::<EventTarget>().fire_event(cx, atom!("selectionchange"));
125                }),
126            );
127    }
128
129    fn is_in_document_of_range(&self, node: &Node) -> bool {
130        // TODO(mrobinson): This should eventually allow nodes in the same composed tree (and
131        // not just the same tree), but this requires more work to allow `Selection` to cross
132        // shadow tree boundaries.
133        &*node.GetRootNode(&GetRootNodeOptions { composed: false }) ==
134            self.document.upcast::<Node>()
135    }
136
137    /// <https://w3c.github.io/editing/docs/execCommand/#active-range>
138    pub(crate) fn active_range(&self) -> Option<DomRoot<Range>> {
139        // > The active range is the range of the selection given by calling
140        // > getSelection() on the context object. (Thus the active range may be null.)
141        self.range.get()
142    }
143
144    pub(crate) fn collapse_current_range(&self, node: &Node, offset: u32) {
145        let range = self.range.get().expect("Must always have a range");
146        range.set_start(node, offset);
147        range.set_end(node, offset);
148    }
149
150    pub(crate) fn extend_current_range(&self, node: &Node, offset: u32) {
151        let range = self.range.get().expect("Must always have a range");
152        assert!(range.collapsed(), "Must only extend after collapsing");
153
154        let anchor_node = range.start_container();
155        if (*anchor_node == *node && range.start_offset() < offset) || anchor_node.is_before(node) {
156            range.set_end(node, offset);
157            self.direction.set(Direction::Forwards);
158        } else {
159            range.set_start(node, offset);
160            self.direction.set(Direction::Backwards);
161        }
162    }
163
164    /// <https://w3c.github.io/selection-api/#dfn-anchor>
165    pub(crate) fn anchor_node(&self) -> Option<DomRoot<Node>> {
166        self.range.get().map(|range| match self.direction.get() {
167            Direction::Forwards => range.start_container(),
168            _ => range.end_container(),
169        })
170    }
171
172    /// <https://w3c.github.io/selection-api/#dfn-anchor>
173    pub(crate) fn anchor_offset(&self) -> u32 {
174        self.range
175            .get()
176            .map(|range| match self.direction.get() {
177                Direction::Forwards => range.start_offset(),
178                _ => range.end_offset(),
179            })
180            .unwrap_or(0)
181    }
182
183    /// <https://w3c.github.io/selection-api/#dfn-focus>
184    pub(crate) fn focus_node(&self) -> Option<DomRoot<Node>> {
185        self.range.get().map(|range| match self.direction.get() {
186            Direction::Forwards => range.end_container(),
187            _ => range.start_container(),
188        })
189    }
190
191    /// <https://w3c.github.io/selection-api/#dfn-focus>
192    pub(crate) fn focus_offset(&self) -> u32 {
193        self.range
194            .get()
195            .map(|range| match self.direction.get() {
196                Direction::Forwards => range.end_offset(),
197                _ => range.start_offset(),
198            })
199            .unwrap_or(0)
200    }
201}
202
203impl SelectionMethods<crate::DomTypeHolder> for Selection {
204    /// <https://w3c.github.io/selection-api/#dom-selection-anchornode>
205    fn GetAnchorNode(&self) -> Option<DomRoot<Node>> {
206        // > The attribute must return the anchor node of this, or null if the anchor is
207        // > null or anchor is not in the document tree.
208        let anchor_node = self.anchor_node()?;
209        if !anchor_node.is_in_a_document_tree() {
210            return None;
211        }
212        Some(anchor_node)
213    }
214
215    /// <https://w3c.github.io/selection-api/#dom-selection-anchoroffset>
216    fn AnchorOffset(&self) -> u32 {
217        // > The attribute must return the anchor offset of this, or 0 if the anchor is null
218        // > or anchor is not in the document tree.
219        if self
220            .anchor_node()
221            .is_none_or(|anchor_node| !anchor_node.is_in_a_document_tree())
222        {
223            return 0;
224        }
225        self.anchor_offset()
226    }
227
228    /// <https://w3c.github.io/selection-api/#dom-selection-focusnode>
229    fn GetFocusNode(&self) -> Option<DomRoot<Node>> {
230        // > The attribute must return the focus node of this, or null if the focus is
231        // > null or focus is not in the document tree.
232        let focus_node = self.focus_node()?;
233        if !focus_node.is_in_a_document_tree() {
234            return None;
235        }
236        Some(focus_node)
237    }
238
239    /// <https://w3c.github.io/selection-api/#dom-selection-focusoffset>
240    fn FocusOffset(&self) -> u32 {
241        // > The attribute must return the focus offset of this, or 0 if the focus is null
242        // > or focus is not in the document tree.
243        if self
244            .focus_node()
245            .is_none_or(|focus_node| !focus_node.is_in_a_document_tree())
246        {
247            return 0;
248        }
249        self.focus_offset()
250    }
251
252    /// <https://w3c.github.io/selection-api/#dom-selection-iscollapsed>
253    fn IsCollapsed(&self) -> bool {
254        // > The attribute must return true if and only if the anchor and focus are the
255        // > same (including if both are null). Otherwise it must return false.
256        self.range.get().is_none_or(|range| range.collapsed())
257    }
258
259    /// <https://w3c.github.io/selection-api/#dom-selection-rangecount>
260    fn RangeCount(&self) -> u32 {
261        // > The attribute must return 0 if this is empty or either focus or anchor is not
262        // > in the document tree, and must return 1 otherwise.
263        let Some(range) = self.range.get() else {
264            return 0;
265        };
266        if !range.start_and_end_are_in_document_tree() {
267            return 0;
268        }
269        1
270    }
271
272    /// <https://w3c.github.io/selection-api/#dom-selection-type>
273    fn Type(&self) -> DOMString {
274        // > The attribute must return "None" if this is empty or either focus or anchor
275        // > is not in the document tree, "Caret" if this's range is collapsed, and "Range"
276        // > otherwise.
277        let Some(range) = self.range.get() else {
278            return DOMString::from("None");
279        };
280        if !range.start_and_end_are_in_document_tree() {
281            return DOMString::from("None");
282        }
283
284        if range.collapsed() {
285            DOMString::from("Caret")
286        } else {
287            DOMString::from("Range")
288        }
289    }
290
291    /// <https://w3c.github.io/selection-api/#dom-selection-getrangeat>
292    fn GetRangeAt(&self, index: u32) -> Fallible<DomRoot<Range>> {
293        // > The method must throw an IndexSizeError exception if index is not 0, or if this
294        // > is empty or either focus or anchor is not in the document tree. Otherwise, it
295        // > must return a reference to (not a copy of) this's range.
296        if index != 0 {
297            return Err(Error::IndexSize(None));
298        }
299
300        let Some(range) = self.range.get() else {
301            return Err(Error::IndexSize(None));
302        };
303
304        if !range.start_and_end_are_in_document_tree() {
305            return Err(Error::IndexSize(None));
306        }
307
308        Ok(DomRoot::from_ref(&range))
309    }
310
311    /// <https://w3c.github.io/selection-api/#dom-selection-addrange>
312    fn AddRange(&self, range: &Range) {
313        // Step 1. If the root of the range's boundary points are not the document
314        // associated with this, abort these steps.
315        if !self.is_in_document_of_range(&range.start_container()) {
316            return;
317        }
318
319        // Step 2. If rangeCount is not 0, abort these steps.
320        if self.RangeCount() != 0 {
321            return;
322        }
323
324        // Step 3. Set this's range to range by a strong reference (not by making a copy).
325        self.set_range(range);
326
327        // Are we supposed to set Direction here? w3c/selection-api#116
328        self.direction.set(Direction::Forwards);
329    }
330
331    /// <https://w3c.github.io/selection-api/#dom-selection-removerange>
332    fn RemoveRange(&self, range: &Range) -> ErrorResult {
333        // > The method must make this empty by disassociating its range if this's range
334        // > is range. Otherwise, it must throw a NotFoundError.
335        if let Some(own_range) = self.range.get() &&
336            &*own_range == range
337        {
338            self.clear_range();
339            return Ok(());
340        }
341        Err(Error::NotFound(None))
342    }
343
344    /// <https://w3c.github.io/selection-api/#dom-selection-removeallranges>
345    fn RemoveAllRanges(&self) {
346        // > The method must make this empty by disassociating its range if this has an
347        // > associated range.
348        self.clear_range();
349    }
350
351    /// <https://w3c.github.io/selection-api/#dom-selection-empty>
352    fn Empty(&self) {
353        // > The method must be an alias, and behave identically, to removeAllRanges().
354        self.clear_range();
355    }
356
357    /// <https://w3c.github.io/selection-api/#dom-selection-collapse>
358    fn Collapse(&self, cx: &mut JSContext, node: Option<&Node>, offset: u32) -> ErrorResult {
359        // Step 1. If node is null, this method must behave identically as
360        // removeAllRanges() and abort these steps.
361        let Some(node) = node else {
362            self.clear_range();
363            return Ok(());
364        };
365
366        // Step 2. If node is a DocumentType, throw an InvalidNodeTypeError exception and
367        // abort these steps.
368        if node.is_doctype() {
369            return Err(Error::InvalidNodeType(None));
370        }
371
372        // Step 3. The method must throw an IndexSizeError exception if offset is longer
373        // than node's length and abort these steps.
374        if offset > node.len() {
375            return Err(Error::IndexSize(None));
376        }
377
378        // Step 4. If document associated with this is not a shadow-including inclusive
379        // ancestor of node, abort these steps.
380        //
381        // TODO(mrobinson): This should eventually allow nodes in the same composed tree (and
382        // not just the same tree), but this requires more work to allow `Selection` to cross
383        // shadow tree boundaries.
384        if &*node.GetRootNode(&GetRootNodeOptions { composed: false }) !=
385            self.document.upcast::<Node>()
386        {
387            return Ok(());
388        }
389
390        // Step 5. Otherwise, let newRange be a new range.
391        // Step 6. Set the start the start and the end of newRange to (node, offset).
392        let new_range = Range::new(cx, &self.document, node, offset, node, offset);
393
394        // Step 7. Set this's range to newRange.
395        self.set_range(&new_range);
396
397        // Are we supposed to set Direction here? w3c/selection-api#116
398        self.direction.set(Direction::Forwards);
399
400        Ok(())
401    }
402
403    /// <https://w3c.github.io/selection-api/#dom-selection-setposition>
404    fn SetPosition(&self, cx: &mut JSContext, node: Option<&Node>, offset: u32) -> ErrorResult {
405        // > The method must be an alias, and behave identically, to collapse().
406        self.Collapse(cx, node, offset)
407    }
408
409    /// <https://w3c.github.io/selection-api/#dom-selection-collapsetostart>
410    fn CollapseToStart(&self, cx: &mut JSContext) -> ErrorResult {
411        // > The method must throw InvalidStateError exception if the this is empty.
412        // > Otherwise, it must create a new range, set the start both its start and end to
413        // > the start of this's range, and then set this's range to the newly-created
414        // > range.
415        if let Some(range) = self.range.get() {
416            self.Collapse(cx, Some(&*range.start_container()), range.start_offset())
417        } else {
418            Err(Error::InvalidState(None))
419        }
420    }
421
422    /// <https://w3c.github.io/selection-api/#dom-selection-collapsetoend>
423    fn CollapseToEnd(&self, cx: &mut JSContext) -> ErrorResult {
424        // > The method must throw InvalidStateError exception if the this is empty.
425        // > Otherwise, it must create a new range, set the start both its start and end to
426        // > the end of this's range, and then set this's range to the newly-created range.
427        if let Some(range) = self.range.get() {
428            self.Collapse(cx, Some(&*range.end_container()), range.end_offset())
429        } else {
430            Err(Error::InvalidState(None))
431        }
432    }
433
434    /// <https://w3c.github.io/selection-api/#dom-selection-extend>
435    fn Extend(&self, cx: &mut JSContext, node: &Node, offset: u32) -> ErrorResult {
436        // Step 1. If the document associated with this is not a shadow-including
437        // inclusive ancestor of node, abort these steps.
438        //
439        // TODO(mrobinson): This should eventually allow nodes in the same composed tree (and
440        // not just the same tree), but this requires more work to allow `Selection` to cross
441        // shadow tree boundaries.
442        if &*node.GetRootNode(&GetRootNodeOptions { composed: false }) !=
443            self.document.upcast::<Node>()
444        {
445            return Ok(());
446        }
447
448        // Step 2. If this is empty, throw an InvalidStateError exception and abort these steps.
449        let Some(range) = self.range.get() else {
450            return Err(Error::InvalidState(None));
451        };
452
453        // This isn't specified, but it appears to be implementation behavior of other
454        // browsers. See w3c/selection-api#118.
455        if node.is_doctype() {
456            return Err(Error::InvalidNodeType(None));
457        }
458
459        // As with is_doctype, this is not explicit in the selection specification steps
460        // here but implied by which exceptions are thrown in WPT tests.
461        if offset > node.len() {
462            return Err(Error::IndexSize(None));
463        }
464
465        // Step 3. Let oldAnchor and oldFocus be the this's anchor and focus, and let
466        // newFocus be the boundary point (node, offset).
467        //
468        // Note: oldFocus is unused, so we do not set it here.
469        let old_anchor_node = &*self
470            .anchor_node()
471            .expect("has range, therefore has anchor node");
472        let old_anchor_offset = self.anchor_offset();
473
474        // Step 4. Let newRange be a new range.
475        let new_range;
476        let direction;
477
478        // Step 5. If node's root is not the same as the this's range's root, set the
479        // start newRange's start and end to newFocus.
480        if !self.is_in_document_of_range(&range.start_container()) {
481            new_range = Range::new(cx, &self.document, node, offset, node, offset);
482            direction = Direction::Forwards;
483        } else {
484            let is_old_anchor_before_or_equal = matches!(
485                bp_position(old_anchor_node, old_anchor_offset, node, offset),
486                Ordering::Less | Ordering::Equal
487            );
488            if is_old_anchor_before_or_equal {
489                // Step 6. Otherwise, if oldAnchor is before or equal to newFocus, set the start
490                // newRange's start to oldAnchor, then set its end to newFocus.
491                new_range = Range::new(
492                    cx,
493                    &self.document,
494                    old_anchor_node,
495                    old_anchor_offset,
496                    node,
497                    offset,
498                );
499                direction = Direction::Forwards;
500            } else {
501                // Step 7. Otherwise, set the start newRange's start to newFocus, then set
502                // its end to oldAnchor.
503                new_range = Range::new(
504                    cx,
505                    &self.document,
506                    node,
507                    offset,
508                    old_anchor_node,
509                    old_anchor_offset,
510                );
511                direction = Direction::Backwards;
512            }
513        }
514
515        // Step 8. Set this's range to newRange.
516        self.set_range(&new_range);
517
518        // Step 9. If newFocus is before oldAnchor, set this's direction to backwards.
519        // Otherwise, set it to forwards.
520        self.direction.set(direction);
521
522        Ok(())
523    }
524
525    /// <https://w3c.github.io/selection-api/#dom-selection-setbaseandextent>
526    fn SetBaseAndExtent(
527        &self,
528        cx: &mut JSContext,
529        anchor_node: &Node,
530        anchor_offset: u32,
531        focus_node: &Node,
532        focus_offset: u32,
533    ) -> ErrorResult {
534        // This isn't specified, but it appears to be implementation behavior of other
535        // browsers. See w3c/selection-api#118.
536        if anchor_node.is_doctype() || focus_node.is_doctype() {
537            return Err(Error::InvalidNodeType(None));
538        }
539
540        // Step 1. If anchorOffset is longer than anchorNode's length or if focusOffset is
541        // longer than focusNode's length, throw an IndexSizeError exception and abort
542        // these steps.
543        if anchor_offset > anchor_node.len() || focus_offset > focus_node.len() {
544            return Err(Error::IndexSize(None));
545        }
546
547        // Step 2. If document associated with this is not a shadow-including inclusive
548        // ancestor of anchorNode or focusNode, abort these steps.
549        //
550        // TODO(mrobinson): This should eventually allow nodes in the same composed tree (and
551        // not just the same tree), but this requires more work to allow `Selection` to cross
552        // shadow tree boundaries.
553        if &*anchor_node.GetRootNode(&GetRootNodeOptions { composed: false }) !=
554            self.document.upcast::<Node>()
555        {
556            return Ok(());
557        }
558        if &*focus_node.GetRootNode(&GetRootNodeOptions { composed: false }) !=
559            self.document.upcast::<Node>()
560        {
561            return Ok(());
562        }
563
564        // Step 3. Let anchor be the boundary point (anchorNode, anchorOffset) and let
565        // focus be the boundary point (focusNode, focusOffset).
566        //
567        // Note: We do not model the boundary point in this way.
568
569        // Step 4. Let newRange be a new range.
570        let new_range;
571        let direction;
572
573        // Step 5. If anchor is before focus, set the start the newRange's start to anchor
574        // and its end to focus. Otherwise, set the start them to focus and anchor
575        // respectively.
576        let is_anchor_before_focus =
577            bp_position(anchor_node, anchor_offset, focus_node, focus_offset) == Ordering::Less;
578        if is_anchor_before_focus {
579            new_range = Range::new(
580                cx,
581                &self.document,
582                anchor_node,
583                anchor_offset,
584                focus_node,
585                focus_offset,
586            );
587            direction = Direction::Forwards;
588        } else {
589            new_range = Range::new(
590                cx,
591                &self.document,
592                focus_node,
593                focus_offset,
594                anchor_node,
595                anchor_offset,
596            );
597            direction = Direction::Backwards;
598        }
599
600        // Step 6. Set this's range to newRange.
601        self.set_range(&new_range);
602
603        // Step 7. If focus is before anchor, set this's direction to backwards.
604        // Otherwise, set it to forwards
605        self.direction.set(direction);
606
607        Ok(())
608    }
609
610    /// <https://w3c.github.io/selection-api/#dom-selection-selectallchildren>
611    fn SelectAllChildren(&self, cx: &mut JSContext, node: &Node) -> ErrorResult {
612        // Step 1. If node is a DocumentType, throw an InvalidNodeTypeError exception and
613        // abort these steps.
614        if node.is_doctype() {
615            return Err(Error::InvalidNodeType(None));
616        }
617
618        // Step 2. If node's root is not the document associated with this, abort these
619        // steps.
620        if !self.is_in_document_of_range(node) {
621            return Ok(());
622        }
623
624        // Let newRange be a new range and childCount be the number of children of node.
625        let child_count = node.children_count();
626
627        // Step 4. Set newRange's start to (node, 0).
628        // Step 5. Set newRange's end to (node, childCount).
629        let new_range = Range::new(cx, &self.document, node, 0, node, child_count);
630
631        // Step 6. Set this's range to newRange.
632        self.set_range(&new_range);
633
634        // Step 7. Set this's direction to forwards.
635        self.direction.set(Direction::Forwards);
636
637        Ok(())
638    }
639
640    /// <https://w3c.github.io/selection-api/#dom-selection-deletecontents>
641    fn DeleteFromDocument(&self, cx: &mut JSContext) -> ErrorResult {
642        // > The method must invoke deleteContents() on this's range if this is not empty
643        // > and both focus and anchor are in the document tree. Otherwise the method must
644        // > do nothing.
645        let Some(range) = self.range.get() else {
646            return Ok(());
647        };
648        if !range.start_and_end_are_in_document_tree() {
649            return Ok(());
650        }
651
652        range.DeleteContents(cx)
653    }
654
655    /// <https://w3c.github.io/selection-api/#dom-selection-containsnode>
656    fn ContainsNode(&self, node: &Node, allow_partial_containment: bool) -> bool {
657        // > The method must return false if this is empty or if node's root is not the document
658        // > associated with this.
659        // >
660        // > Otherwise, if allowPartialContainment is false, the method must return true if and only
661        // > if start of its range is before or visually equivalent to the first boundary point in
662        // > the node *and* end of its range is after or visually equivalent to the last boundary
663        // > point in the node.
664        // >
665        // > If allowPartialContainment is true, the method must return true if and only if start of
666        // > its range is before or visually equivalent to the last boundary point in the node *and*
667        // > end of its range is after or visually equivalent to the first boundary point in the
668        // > node.
669
670        if !self.is_in_document_of_range(node) {
671            return false;
672        }
673        let Some(range) = self.range.get() else {
674            return false;
675        };
676        let start_node = &*range.start_container();
677        if !self.is_in_document_of_range(start_node) {
678            return false;
679        }
680        let end_node = &*range.end_container();
681
682        let first_offset = 0;
683        let last_offset = node.len();
684        let (compare_start_to, compare_end_to) = if allow_partial_containment {
685            (last_offset, first_offset)
686        } else {
687            (first_offset, last_offset)
688        };
689
690        // TODO: find out what "visually equivalent" means for boundary points and implement it.
691        // https://github.com/w3c/selection-api/issues/6
692        // For now it is simplified to "position is equal".
693        matches!(
694            bp_position(start_node, range.start_offset(), node, compare_start_to),
695            Ordering::Less | Ordering::Equal
696        ) && matches!(
697            bp_position(end_node, range.end_offset(), node, compare_end_to),
698            Ordering::Greater | Ordering::Equal
699        )
700    }
701
702    /// <https://w3c.github.io/selection-api/#dom-selection-stringifier>
703    fn Stringifier(&self, no_gc: &NoGC) -> DOMString {
704        // > The stringification must return the string, which is the concatenation of the
705        // > rendered text if there is a range associated with this.
706        // >
707        // > If the selection is within a textarea or input element, it must return the
708        // > selected substring in its value.
709        //
710        // TODO: This implementation should be examined in depth. Does rendered text take
711        // into account `display: none`. The case for textarea and input elements is
712        // completely unhandled here.
713        if let Some(range) = self.range.get() {
714            range.Stringifier(no_gc)
715        } else {
716            DOMString::from("")
717        }
718    }
719}