1use std::cell::Cell;
6use std::cmp::Ordering;
7
8use dom_struct::dom_struct;
9use html5ever::{LocalName, QualName, local_name, namespace_url, ns};
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::reflector::{Reflector, reflect_dom_object};
17use crate::dom::bindings::root::{Dom, DomRoot, MutNullableDom};
18use crate::dom::bindings::str::DOMString;
19use crate::dom::bindings::trace::JSTraceable;
20use crate::dom::element::Element;
21use crate::dom::node::{Node, NodeTraits};
22use crate::dom::window::Window;
23use crate::script_runtime::CanGc;
24
25pub(crate) trait CollectionFilter: JSTraceable {
26 fn filter<'a>(&self, elem: &'a Element, root: &'a Node) -> bool;
27}
28
29#[derive(Clone, Copy, JSTraceable, MallocSizeOf)]
33struct OptionU32 {
34 bits: u32,
35}
36
37impl OptionU32 {
38 fn to_option(self) -> Option<u32> {
39 if self.bits == u32::MAX {
40 None
41 } else {
42 Some(self.bits)
43 }
44 }
45
46 fn some(bits: u32) -> OptionU32 {
47 assert_ne!(bits, u32::MAX);
48 OptionU32 { bits }
49 }
50
51 fn none() -> OptionU32 {
52 OptionU32 { bits: u32::MAX }
53 }
54}
55
56#[dom_struct]
57pub(crate) struct HTMLCollection {
58 reflector_: Reflector,
59 root: Dom<Node>,
60 #[ignore_malloc_size_of = "Trait object (Box<dyn CollectionFilter>) cannot be sized"]
61 filter: Box<dyn CollectionFilter + 'static>,
62 cached_version: Cell<u64>,
66 cached_cursor_element: MutNullableDom<Element>,
67 cached_cursor_index: Cell<OptionU32>,
68 cached_length: Cell<OptionU32>,
69}
70
71impl HTMLCollection {
72 #[cfg_attr(crown, allow(crown::unrooted_must_root))]
73 pub(crate) fn new_inherited(
74 root: &Node,
75 filter: Box<dyn CollectionFilter + 'static>,
76 ) -> HTMLCollection {
77 HTMLCollection {
78 reflector_: Reflector::new(),
79 root: Dom::from_ref(root),
80 filter,
81 cached_version: Cell::new(root.inclusive_descendants_version()),
83 cached_cursor_element: MutNullableDom::new(None),
84 cached_cursor_index: Cell::new(OptionU32::none()),
85 cached_length: Cell::new(OptionU32::none()),
86 }
87 }
88
89 pub(crate) fn always_empty(window: &Window, root: &Node, can_gc: CanGc) -> DomRoot<Self> {
91 #[derive(JSTraceable)]
92 struct NoFilter;
93 impl CollectionFilter for NoFilter {
94 fn filter<'a>(&self, _: &'a Element, _: &'a Node) -> bool {
95 false
96 }
97 }
98
99 Self::new(window, root, Box::new(NoFilter), can_gc)
100 }
101
102 #[cfg_attr(crown, allow(crown::unrooted_must_root))]
103 pub(crate) fn new(
104 window: &Window,
105 root: &Node,
106 filter: Box<dyn CollectionFilter + 'static>,
107 can_gc: CanGc,
108 ) -> DomRoot<Self> {
109 reflect_dom_object(Box::new(Self::new_inherited(root, filter)), window, can_gc)
110 }
111
112 pub(crate) fn new_with_filter_fn(
114 window: &Window,
115 root: &Node,
116 filter_function: fn(&Element, &Node) -> bool,
117 can_gc: CanGc,
118 ) -> DomRoot<Self> {
119 #[derive(JSTraceable, MallocSizeOf)]
120 pub(crate) struct StaticFunctionFilter(
121 #[no_trace]
124 #[ignore_malloc_size_of = "Static function pointer"]
125 fn(&Element, &Node) -> bool,
126 );
127 impl CollectionFilter for StaticFunctionFilter {
128 fn filter(&self, element: &Element, root: &Node) -> bool {
129 (self.0)(element, root)
130 }
131 }
132 Self::new(
133 window,
134 root,
135 Box::new(StaticFunctionFilter(filter_function)),
136 can_gc,
137 )
138 }
139
140 pub(crate) fn create(
141 window: &Window,
142 root: &Node,
143 filter: Box<dyn CollectionFilter + 'static>,
144 can_gc: CanGc,
145 ) -> DomRoot<Self> {
146 Self::new(window, root, filter, can_gc)
147 }
148
149 fn validate_cache(&self) {
150 let cached_version = self.cached_version.get();
152 let curr_version = self.root.inclusive_descendants_version();
153 if curr_version != cached_version {
154 self.cached_version.set(curr_version);
156 self.cached_cursor_element.set(None);
157 self.cached_length.set(OptionU32::none());
158 self.cached_cursor_index.set(OptionU32::none());
159 }
160 }
161
162 fn set_cached_cursor(
163 &self,
164 index: u32,
165 element: Option<DomRoot<Element>>,
166 ) -> Option<DomRoot<Element>> {
167 if let Some(element) = element {
168 self.cached_cursor_index.set(OptionU32::some(index));
169 self.cached_cursor_element.set(Some(&element));
170 Some(element)
171 } else {
172 None
173 }
174 }
175
176 pub(crate) fn by_qualified_name(
178 window: &Window,
179 root: &Node,
180 qualified_name: LocalName,
181 can_gc: CanGc,
182 ) -> DomRoot<HTMLCollection> {
183 if qualified_name == local_name!("*") {
185 #[derive(JSTraceable, MallocSizeOf)]
186 struct AllFilter;
187 impl CollectionFilter for AllFilter {
188 fn filter(&self, _elem: &Element, _root: &Node) -> bool {
189 true
190 }
191 }
192 return HTMLCollection::create(window, root, Box::new(AllFilter), can_gc);
193 }
194
195 #[derive(JSTraceable, MallocSizeOf)]
196 struct HtmlDocumentFilter {
197 #[no_trace]
198 qualified_name: LocalName,
199 #[no_trace]
200 ascii_lower_qualified_name: LocalName,
201 }
202 impl CollectionFilter for HtmlDocumentFilter {
203 fn filter(&self, elem: &Element, root: &Node) -> bool {
204 if root.is_in_html_doc() && elem.namespace() == &ns!(html) {
205 HTMLCollection::match_element(elem, &self.ascii_lower_qualified_name)
207 } else {
208 HTMLCollection::match_element(elem, &self.qualified_name)
210 }
211 }
212 }
213
214 let filter = HtmlDocumentFilter {
215 ascii_lower_qualified_name: qualified_name.to_ascii_lowercase(),
216 qualified_name,
217 };
218 HTMLCollection::create(window, root, Box::new(filter), can_gc)
219 }
220
221 fn match_element(elem: &Element, qualified_name: &LocalName) -> bool {
222 match elem.prefix().as_ref() {
223 None => elem.local_name() == qualified_name,
224 Some(prefix) => {
225 qualified_name.starts_with(&**prefix) &&
226 qualified_name.find(':') == Some(prefix.len()) &&
227 qualified_name.ends_with(&**elem.local_name())
228 },
229 }
230 }
231
232 pub(crate) fn by_tag_name_ns(
233 window: &Window,
234 root: &Node,
235 tag: DOMString,
236 maybe_ns: Option<DOMString>,
237 can_gc: CanGc,
238 ) -> DomRoot<HTMLCollection> {
239 let local = LocalName::from(tag);
240 let ns = namespace_from_domstring(maybe_ns);
241 let qname = QualName::new(None, ns, local);
242 HTMLCollection::by_qual_tag_name(window, root, qname, can_gc)
243 }
244
245 pub(crate) fn by_qual_tag_name(
246 window: &Window,
247 root: &Node,
248 qname: QualName,
249 can_gc: CanGc,
250 ) -> DomRoot<HTMLCollection> {
251 #[derive(JSTraceable, MallocSizeOf)]
252 struct TagNameNSFilter {
253 #[no_trace]
254 qname: QualName,
255 }
256 impl CollectionFilter for TagNameNSFilter {
257 fn filter(&self, elem: &Element, _root: &Node) -> bool {
258 ((self.qname.ns == namespace_url!("*")) || (self.qname.ns == *elem.namespace())) &&
259 ((self.qname.local == local_name!("*")) ||
260 (self.qname.local == *elem.local_name()))
261 }
262 }
263 let filter = TagNameNSFilter { qname };
264 HTMLCollection::create(window, root, Box::new(filter), can_gc)
265 }
266
267 pub(crate) fn by_class_name(
268 window: &Window,
269 root: &Node,
270 classes: DOMString,
271 can_gc: CanGc,
272 ) -> DomRoot<HTMLCollection> {
273 let class_atoms = split_html_space_chars(&classes).map(Atom::from).collect();
274 HTMLCollection::by_atomic_class_name(window, root, class_atoms, can_gc)
275 }
276
277 pub(crate) fn by_atomic_class_name(
278 window: &Window,
279 root: &Node,
280 classes: Vec<Atom>,
281 can_gc: CanGc,
282 ) -> DomRoot<HTMLCollection> {
283 #[derive(JSTraceable, MallocSizeOf)]
284 struct ClassNameFilter {
285 #[no_trace]
286 classes: Vec<Atom>,
287 }
288 impl CollectionFilter for ClassNameFilter {
289 fn filter(&self, elem: &Element, _root: &Node) -> bool {
290 let case_sensitivity = elem
291 .owner_document()
292 .quirks_mode()
293 .classes_and_ids_case_sensitivity();
294
295 self.classes
296 .iter()
297 .all(|class| elem.has_class(class, case_sensitivity))
298 }
299 }
300
301 if classes.is_empty() {
302 return HTMLCollection::always_empty(window, root, can_gc);
303 }
304
305 let filter = ClassNameFilter { classes };
306 HTMLCollection::create(window, root, Box::new(filter), can_gc)
307 }
308
309 pub(crate) fn children(window: &Window, root: &Node, can_gc: CanGc) -> DomRoot<HTMLCollection> {
310 HTMLCollection::new_with_filter_fn(
311 window,
312 root,
313 |element, root| root.is_parent_of(element.upcast()),
314 can_gc,
315 )
316 }
317
318 pub(crate) fn elements_iter_after<'a>(
319 &'a self,
320 after: &'a Node,
321 ) -> impl Iterator<Item = DomRoot<Element>> + 'a {
322 after
324 .following_nodes(&self.root)
325 .filter_map(DomRoot::downcast)
326 .filter(move |element| self.filter.filter(element, &self.root))
327 }
328
329 pub(crate) fn elements_iter(&self) -> impl Iterator<Item = DomRoot<Element>> + '_ {
330 self.elements_iter_after(&self.root)
332 }
333
334 pub(crate) fn elements_iter_before<'a>(
335 &'a self,
336 before: &'a Node,
337 ) -> impl Iterator<Item = DomRoot<Element>> + 'a {
338 before
340 .preceding_nodes(&self.root)
341 .filter_map(DomRoot::downcast)
342 .filter(move |element| self.filter.filter(element, &self.root))
343 }
344
345 pub(crate) fn root_node(&self) -> DomRoot<Node> {
346 DomRoot::from_ref(&self.root)
347 }
348}
349
350impl HTMLCollectionMethods<crate::DomTypeHolder> for HTMLCollection {
351 fn Length(&self) -> u32 {
353 self.validate_cache();
354
355 if let Some(cached_length) = self.cached_length.get().to_option() {
356 cached_length
358 } else {
359 let length = self.elements_iter().count() as u32;
361 self.cached_length.set(OptionU32::some(length));
362 length
363 }
364 }
365
366 fn Item(&self, index: u32) -> Option<DomRoot<Element>> {
368 self.validate_cache();
369
370 if let Some(element) = self.cached_cursor_element.get() {
371 if let Some(cached_index) = self.cached_cursor_index.get().to_option() {
373 match cached_index.cmp(&index) {
374 Ordering::Equal => {
375 Some(element)
377 },
378 Ordering::Less => {
379 let offset = index - (cached_index + 1);
382 let node: DomRoot<Node> = DomRoot::upcast(element);
383 let mut iter = self.elements_iter_after(&node);
384 self.set_cached_cursor(index, iter.nth(offset as usize))
385 },
386 Ordering::Greater => {
387 let offset = cached_index - (index + 1);
390 let node: DomRoot<Node> = DomRoot::upcast(element);
391 let mut iter = self.elements_iter_before(&node);
392 self.set_cached_cursor(index, iter.nth(offset as usize))
393 },
394 }
395 } else {
396 self.set_cached_cursor(index, self.elements_iter().nth(index as usize))
399 }
400 } else {
401 self.set_cached_cursor(index, self.elements_iter().nth(index as usize))
404 }
405 }
406
407 fn NamedItem(&self, key: DOMString) -> Option<DomRoot<Element>> {
409 if key.is_empty() {
411 return None;
412 }
413
414 let key = Atom::from(key);
415
416 self.elements_iter().find(|elem| {
418 elem.get_id().is_some_and(|id| id == key) ||
419 (elem.namespace() == &ns!(html) && elem.get_name().is_some_and(|id| id == key))
420 })
421 }
422
423 fn IndexedGetter(&self, index: u32) -> Option<DomRoot<Element>> {
425 self.Item(index)
426 }
427
428 fn NamedGetter(&self, name: DOMString) -> Option<DomRoot<Element>> {
430 self.NamedItem(name)
431 }
432
433 fn SupportedPropertyNames(&self) -> Vec<DOMString> {
435 let mut result = vec![];
437
438 for elem in self.elements_iter() {
440 if let Some(id_atom) = elem.get_id() {
442 let id_str = DOMString::from(&*id_atom);
443 if !result.contains(&id_str) {
444 result.push(id_str);
445 }
446 }
447 if *elem.namespace() == ns!(html) {
449 if let Some(name_atom) = elem.get_name() {
450 let name_str = DOMString::from(&*name_atom);
451 if !result.contains(&name_str) {
452 result.push(name_str)
453 }
454 }
455 }
456 }
457
458 result
460 }
461}