Skip to main content

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