script/dom/html/
htmlcollection.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;
6
7use dom_struct::dom_struct;
8use html5ever::{LocalName, QualName, local_name, namespace_url, ns};
9use style::str::split_html_space_chars;
10use stylo_atoms::Atom;
11
12use crate::dom::bindings::codegen::Bindings::HTMLCollectionBinding::HTMLCollectionMethods;
13use crate::dom::bindings::domname::namespace_from_domstring;
14use crate::dom::bindings::inheritance::Castable;
15use crate::dom::bindings::reflector::{Reflector, reflect_dom_object_with_cx};
16use crate::dom::bindings::root::{Dom, DomRoot, MutNullableDom};
17use crate::dom::bindings::str::DOMString;
18use crate::dom::bindings::trace::JSTraceable;
19use crate::dom::element::Element;
20use crate::dom::node::{Node, NodeTraits};
21use crate::dom::window::Window;
22
23pub(crate) trait CollectionFilter: JSTraceable {
24    fn filter<'a>(&self, elem: &'a Element, root: &'a Node) -> bool;
25}
26
27/// Alternative to [`CollectionFilter`] that provides elements directly via
28/// a custom iterator, rather than filtering a tree traversal. This is more
29/// efficient when the collection's elements can be enumerated directly
30/// (e.g. `selectedOptions` iterating only the select's list of options).
31pub(crate) trait CollectionSource: JSTraceable {
32    fn iter<'a>(&'a self, root: &'a Node) -> Box<dyn Iterator<Item = DomRoot<Element>> + 'a>;
33}
34
35/// How a collection enumerates its elements.
36#[derive(JSTraceable)]
37enum CollectionKind {
38    /// Filter elements from a subtree traversal of the root node.
39    Filter(Box<dyn CollectionFilter + 'static>),
40    /// Provide elements directly via a custom iterator.
41    Source(Box<dyn CollectionSource + 'static>),
42}
43
44/// An optional `u32`, using `u32::MAX` to represent None.  It would be nicer
45/// just to use `Option<u32>` for this, but that would produce word alignment
46/// issues since `Option<u32>` uses 33 bits.
47#[derive(Clone, Copy, JSTraceable, MallocSizeOf)]
48struct OptionU32 {
49    bits: u32,
50}
51
52impl OptionU32 {
53    fn to_option(self) -> Option<u32> {
54        if self.bits == u32::MAX {
55            None
56        } else {
57            Some(self.bits)
58        }
59    }
60
61    fn some(bits: u32) -> OptionU32 {
62        assert_ne!(bits, u32::MAX);
63        OptionU32 { bits }
64    }
65
66    fn none() -> OptionU32 {
67        OptionU32 { bits: u32::MAX }
68    }
69}
70
71#[dom_struct]
72pub(crate) struct HTMLCollection {
73    reflector_: Reflector,
74    root: Dom<Node>,
75    #[ignore_malloc_size_of = "Trait objects cannot be sized"]
76    kind: CollectionKind,
77    // We cache the version of the root node and all its decendents,
78    // the length of the collection, and a cursor into the collection.
79    // FIXME: make the cached cursor element a weak pointer
80    cached_version: Cell<u64>,
81    cached_cursor_element: MutNullableDom<Element>,
82    cached_cursor_index: Cell<OptionU32>,
83    cached_length: Cell<OptionU32>,
84}
85
86impl HTMLCollection {
87    fn new_inherited_with_kind(root: &Node, kind: CollectionKind) -> HTMLCollection {
88        HTMLCollection {
89            reflector_: Reflector::new(),
90            root: Dom::from_ref(root),
91            kind,
92            // Default values for the cache
93            cached_version: Cell::new(root.inclusive_descendants_version()),
94            cached_cursor_element: MutNullableDom::new(None),
95            cached_cursor_index: Cell::new(OptionU32::none()),
96            cached_length: Cell::new(OptionU32::none()),
97        }
98    }
99
100    pub(crate) fn new_inherited(
101        root: &Node,
102        filter: Box<dyn CollectionFilter + 'static>,
103    ) -> HTMLCollection {
104        Self::new_inherited_with_kind(root, CollectionKind::Filter(filter))
105    }
106
107    pub(crate) fn new_inherited_with_source(
108        root: &Node,
109        source: Box<dyn CollectionSource + 'static>,
110    ) -> HTMLCollection {
111        Self::new_inherited_with_kind(root, CollectionKind::Source(source))
112    }
113
114    /// Returns a collection which is always empty.
115    pub(crate) fn always_empty(
116        cx: &mut js::context::JSContext,
117        window: &Window,
118        root: &Node,
119    ) -> DomRoot<Self> {
120        #[derive(JSTraceable)]
121        struct NoFilter;
122        impl CollectionFilter for NoFilter {
123            fn filter<'a>(&self, _: &'a Element, _: &'a Node) -> bool {
124                false
125            }
126        }
127
128        Self::new(cx, window, root, Box::new(NoFilter))
129    }
130
131    pub(crate) fn new(
132        cx: &mut js::context::JSContext,
133        window: &Window,
134        root: &Node,
135        filter: Box<dyn CollectionFilter + 'static>,
136    ) -> DomRoot<Self> {
137        reflect_dom_object_with_cx(Box::new(Self::new_inherited(root, filter)), window, cx)
138    }
139
140    /// Create a new  [`HTMLCollection`] that just filters element using a static function.
141    pub(crate) fn new_with_filter_fn(
142        cx: &mut js::context::JSContext,
143        window: &Window,
144        root: &Node,
145        filter_function: fn(&Element, &Node) -> bool,
146    ) -> DomRoot<Self> {
147        // The function *must* be static so that it never holds references to DOM objects, which
148        // would cause issues with garbage collection -- since it isn't traced.
149        #[derive(JSTraceable, MallocSizeOf)]
150        pub(crate) struct StaticFunctionFilter(
151            #[no_trace]
152            #[ignore_malloc_size_of = "Static function pointer"]
153            fn(&Element, &Node) -> bool,
154        );
155        impl CollectionFilter for StaticFunctionFilter {
156            fn filter(&self, element: &Element, root: &Node) -> bool {
157                (self.0)(element, root)
158            }
159        }
160        Self::new(
161            cx,
162            window,
163            root,
164            Box::new(StaticFunctionFilter(filter_function)),
165        )
166    }
167
168    pub(crate) fn create(
169        cx: &mut js::context::JSContext,
170        window: &Window,
171        root: &Node,
172        filter: Box<dyn CollectionFilter + 'static>,
173    ) -> DomRoot<Self> {
174        Self::new(cx, window, root, filter)
175    }
176
177    /// Create a new [`HTMLCollection`] backed by a custom element source.
178    pub(crate) fn new_with_source(
179        cx: &mut js::context::JSContext,
180        window: &Window,
181        root: &Node,
182        source: Box<dyn CollectionSource + 'static>,
183    ) -> DomRoot<Self> {
184        reflect_dom_object_with_cx(
185            Box::new(Self::new_inherited_with_source(root, source)),
186            window,
187            cx,
188        )
189    }
190
191    fn validate_cache(&self) {
192        // Clear the cache if the root version is different from our cached version
193        let cached_version = self.cached_version.get();
194        let curr_version = self.root.inclusive_descendants_version();
195        if curr_version != cached_version {
196            // Default values for the cache
197            self.cached_version.set(curr_version);
198            self.cached_cursor_element.set(None);
199            self.cached_length.set(OptionU32::none());
200            self.cached_cursor_index.set(OptionU32::none());
201        }
202    }
203
204    fn set_cached_cursor(
205        &self,
206        index: u32,
207        element: Option<DomRoot<Element>>,
208    ) -> Option<DomRoot<Element>> {
209        if let Some(element) = element {
210            self.cached_cursor_index.set(OptionU32::some(index));
211            self.cached_cursor_element.set(Some(&element));
212            Some(element)
213        } else {
214            None
215        }
216    }
217
218    /// <https://dom.spec.whatwg.org/#concept-getelementsbytagname>
219    pub(crate) fn by_qualified_name(
220        cx: &mut js::context::JSContext,
221        window: &Window,
222        root: &Node,
223        qualified_name: LocalName,
224    ) -> DomRoot<HTMLCollection> {
225        // case 1
226        if qualified_name == local_name!("*") {
227            #[derive(JSTraceable, MallocSizeOf)]
228            struct AllFilter;
229            impl CollectionFilter for AllFilter {
230                fn filter(&self, _elem: &Element, _root: &Node) -> bool {
231                    true
232                }
233            }
234            return HTMLCollection::create(cx, window, root, Box::new(AllFilter));
235        }
236
237        #[derive(JSTraceable, MallocSizeOf)]
238        struct HtmlDocumentFilter {
239            #[no_trace]
240            qualified_name: LocalName,
241            #[no_trace]
242            ascii_lower_qualified_name: LocalName,
243        }
244        impl CollectionFilter for HtmlDocumentFilter {
245            fn filter(&self, elem: &Element, root: &Node) -> bool {
246                if root.is_in_html_doc() && elem.namespace() == &ns!(html) {
247                    // case 2
248                    HTMLCollection::match_element(elem, &self.ascii_lower_qualified_name)
249                } else {
250                    // case 2 and 3
251                    HTMLCollection::match_element(elem, &self.qualified_name)
252                }
253            }
254        }
255
256        let filter = HtmlDocumentFilter {
257            ascii_lower_qualified_name: qualified_name.to_ascii_lowercase(),
258            qualified_name,
259        };
260        HTMLCollection::create(cx, window, root, Box::new(filter))
261    }
262
263    fn match_element(elem: &Element, qualified_name: &LocalName) -> bool {
264        match elem.prefix().as_ref() {
265            None => elem.local_name() == qualified_name,
266            Some(prefix) => {
267                qualified_name.starts_with(&**prefix) &&
268                    qualified_name.find(':') == Some(prefix.len()) &&
269                    qualified_name.ends_with(&**elem.local_name())
270            },
271        }
272    }
273
274    pub(crate) fn by_tag_name_ns(
275        cx: &mut js::context::JSContext,
276        window: &Window,
277        root: &Node,
278        tag: DOMString,
279        maybe_ns: Option<DOMString>,
280    ) -> DomRoot<HTMLCollection> {
281        let local = LocalName::from(tag);
282        let ns = namespace_from_domstring(maybe_ns);
283        let qname = QualName::new(None, ns, local);
284        HTMLCollection::by_qual_tag_name(cx, window, root, qname)
285    }
286
287    pub(crate) fn by_qual_tag_name(
288        cx: &mut js::context::JSContext,
289        window: &Window,
290        root: &Node,
291        qname: QualName,
292    ) -> DomRoot<HTMLCollection> {
293        #[derive(JSTraceable, MallocSizeOf)]
294        struct TagNameNSFilter {
295            #[no_trace]
296            qname: QualName,
297        }
298        impl CollectionFilter for TagNameNSFilter {
299            fn filter(&self, elem: &Element, _root: &Node) -> bool {
300                ((self.qname.ns == namespace_url!("*")) || (self.qname.ns == *elem.namespace())) &&
301                    ((self.qname.local == local_name!("*")) ||
302                        (self.qname.local == *elem.local_name()))
303            }
304        }
305        let filter = TagNameNSFilter { qname };
306        HTMLCollection::create(cx, window, root, Box::new(filter))
307    }
308
309    pub(crate) fn by_class_name(
310        cx: &mut js::context::JSContext,
311        window: &Window,
312        root: &Node,
313        classes: DOMString,
314    ) -> DomRoot<HTMLCollection> {
315        let class_atoms = split_html_space_chars(&classes.str())
316            .map(Atom::from)
317            .collect();
318        HTMLCollection::by_atomic_class_name(cx, window, root, class_atoms)
319    }
320
321    pub(crate) fn by_atomic_class_name(
322        cx: &mut js::context::JSContext,
323        window: &Window,
324        root: &Node,
325        classes: Vec<Atom>,
326    ) -> DomRoot<HTMLCollection> {
327        #[derive(JSTraceable, MallocSizeOf)]
328        struct ClassNameFilter {
329            #[no_trace]
330            classes: Vec<Atom>,
331        }
332        impl CollectionFilter for ClassNameFilter {
333            fn filter(&self, elem: &Element, _root: &Node) -> bool {
334                let case_sensitivity = elem
335                    .owner_document()
336                    .quirks_mode()
337                    .classes_and_ids_case_sensitivity();
338
339                self.classes
340                    .iter()
341                    .all(|class| elem.has_class(class, case_sensitivity))
342            }
343        }
344
345        if classes.is_empty() {
346            return HTMLCollection::always_empty(cx, window, root);
347        }
348
349        let filter = ClassNameFilter { classes };
350        HTMLCollection::create(cx, window, root, Box::new(filter))
351    }
352
353    pub(crate) fn children(
354        cx: &mut js::context::JSContext,
355        window: &Window,
356        root: &Node,
357    ) -> DomRoot<HTMLCollection> {
358        HTMLCollection::new_with_filter_fn(cx, window, root, |element, root| {
359            root.is_parent_of(element.upcast())
360        })
361    }
362
363    /// Iterate forwards from a node, filtering by a [`CollectionFilter`].
364    /// Only usable with filter-based collections for cursor optimization.
365    fn filter_iter_after<'a>(
366        &'a self,
367        after: &'a Node,
368        filter: &'a (dyn CollectionFilter + 'static),
369    ) -> impl Iterator<Item = DomRoot<Element>> + 'a {
370        after
371            .following_nodes(&self.root)
372            .filter_map(DomRoot::downcast)
373            .filter(move |element| filter.filter(element, &self.root))
374    }
375
376    /// Iterate backwards from a node, filtering by a [`CollectionFilter`].
377    /// Only usable with filter-based collections for cursor optimization.
378    fn filter_iter_before<'a>(
379        &'a self,
380        before: &'a Node,
381        filter: &'a (dyn CollectionFilter + 'static),
382    ) -> impl Iterator<Item = DomRoot<Element>> + 'a {
383        before
384            .preceding_nodes(&self.root)
385            .filter_map(DomRoot::downcast)
386            .filter(move |element| filter.filter(element, &self.root))
387    }
388
389    pub(crate) fn elements_iter(&self) -> Box<dyn Iterator<Item = DomRoot<Element>> + '_> {
390        match &self.kind {
391            CollectionKind::Filter(filter) => {
392                Box::new(self.filter_iter_after(&self.root, filter.as_ref()))
393            },
394            CollectionKind::Source(source) => source.iter(&self.root),
395        }
396    }
397
398    pub(crate) fn root_node(&self) -> DomRoot<Node> {
399        DomRoot::from_ref(&self.root)
400    }
401}
402
403impl HTMLCollectionMethods<crate::DomTypeHolder> for HTMLCollection {
404    /// <https://dom.spec.whatwg.org/#dom-htmlcollection-length>
405    fn Length(&self) -> u32 {
406        self.validate_cache();
407
408        if let Some(cached_length) = self.cached_length.get().to_option() {
409            // Cache hit
410            cached_length
411        } else {
412            // Cache miss, calculate the length
413            let length = self.elements_iter().count() as u32;
414            self.cached_length.set(OptionU32::some(length));
415            length
416        }
417    }
418
419    /// <https://dom.spec.whatwg.org/#dom-htmlcollection-item>
420    fn Item(&self, index: u32) -> Option<DomRoot<Element>> {
421        self.validate_cache();
422
423        if let Some(element) = self.cached_cursor_element.get() {
424            // Cache hit, the cursor element is set
425            if let Some(cached_index) = self.cached_cursor_index.get().to_option() {
426                if cached_index == index {
427                    // The cursor is the element we're looking for.
428                    return Some(element);
429                }
430
431                // Cursor-relative traversal is only possible for filter-based
432                // collections, where elements follow tree order.
433                if let CollectionKind::Filter(ref filter) = self.kind {
434                    let node: DomRoot<Node> = DomRoot::upcast(element);
435                    return if cached_index < index {
436                        // Iterate forwards from the cursor.
437                        let offset = index - (cached_index + 1);
438                        self.set_cached_cursor(
439                            index,
440                            self.filter_iter_after(&node, filter.as_ref())
441                                .nth(offset as usize),
442                        )
443                    } else {
444                        // Iterate backwards from the cursor.
445                        let offset = cached_index - (index + 1);
446                        self.set_cached_cursor(
447                            index,
448                            self.filter_iter_before(&node, filter.as_ref())
449                                .nth(offset as usize),
450                        )
451                    };
452                }
453            }
454        }
455
456        // Cache miss or source-based collection: iterate from the beginning.
457        self.set_cached_cursor(index, self.elements_iter().nth(index as usize))
458    }
459
460    /// <https://dom.spec.whatwg.org/#dom-htmlcollection-nameditem>
461    fn NamedItem(&self, key: DOMString) -> Option<DomRoot<Element>> {
462        // Step 1.
463        if key.is_empty() {
464            return None;
465        }
466
467        let key = Atom::from(key);
468
469        // Step 2.
470        self.elements_iter().find(|elem| {
471            elem.get_id().is_some_and(|id| id == key) ||
472                (elem.namespace() == &ns!(html) && elem.get_name().is_some_and(|id| id == key))
473        })
474    }
475
476    /// <https://dom.spec.whatwg.org/#dom-htmlcollection-item>
477    fn IndexedGetter(&self, index: u32) -> Option<DomRoot<Element>> {
478        self.Item(index)
479    }
480
481    // check-tidy: no specs after this line
482    fn NamedGetter(&self, name: DOMString) -> Option<DomRoot<Element>> {
483        self.NamedItem(name)
484    }
485
486    /// <https://dom.spec.whatwg.org/#interface-htmlcollection>
487    fn SupportedPropertyNames(&self) -> Vec<DOMString> {
488        // Step 1
489        let mut result = vec![];
490
491        // Step 2
492        for elem in self.elements_iter() {
493            // Step 2.1
494            if let Some(id_atom) = elem.get_id() {
495                let id_str = DOMString::from(&*id_atom);
496                if !result.contains(&id_str) {
497                    result.push(id_str);
498                }
499            }
500            // Step 2.2
501            if *elem.namespace() == ns!(html) {
502                if let Some(name_atom) = elem.get_name() {
503                    let name_str = DOMString::from(&*name_atom);
504                    if !result.contains(&name_str) {
505                        result.push(name_str)
506                    }
507                }
508            }
509        }
510
511        // Step 3
512        result
513    }
514}