1use 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
28pub(crate) trait CollectionSource: JSTraceable {
33 fn iter<'a>(&'a self, root: &'a Node) -> Box<dyn Iterator<Item = DomRoot<Element>> + 'a>;
34}
35
36#[derive(JSTraceable)]
38enum CollectionKind {
39 Filter(Box<dyn CollectionFilter + 'static>),
41 Source(Box<dyn CollectionSource + 'static>),
43}
44
45#[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 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 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 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 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 #[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 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 let cached_version = self.cached_version.get();
195 let curr_version = self.root.inclusive_descendants_version();
196 if curr_version != cached_version {
197 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 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 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 HTMLCollection::match_element(elem, &self.ascii_lower_qualified_name)
250 } else {
251 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 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 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 fn Length(&self) -> u32 {
407 self.validate_cache();
408
409 if let Some(cached_length) = self.cached_length.get().to_option() {
410 cached_length
412 } else {
413 let length = self.elements_iter().count() as u32;
415 self.cached_length.set(OptionU32::some(length));
416 length
417 }
418 }
419
420 fn Item(&self, index: u32) -> Option<DomRoot<Element>> {
422 self.validate_cache();
423
424 if let Some(element) = self.cached_cursor_element.get() {
425 if let Some(cached_index) = self.cached_cursor_index.get().to_option() {
427 if cached_index == index {
428 return Some(element);
430 }
431
432 if let CollectionKind::Filter(ref filter) = self.kind {
435 let node: DomRoot<Node> = DomRoot::upcast(element);
436 return if cached_index < index {
437 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 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 self.set_cached_cursor(index, self.elements_iter().nth(index as usize))
459 }
460
461 fn NamedItem(&self, key: DOMString) -> Option<DomRoot<Element>> {
463 if key.is_empty() {
465 return None;
466 }
467
468 let key = Atom::from(key);
469
470 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 fn IndexedGetter(&self, index: u32) -> Option<DomRoot<Element>> {
479 self.Item(index)
480 }
481
482 fn NamedGetter(&self, name: DOMString) -> Option<DomRoot<Element>> {
484 self.NamedItem(name)
485 }
486
487 fn SupportedPropertyNames(&self, _: &mut js::context::JSContext) -> Vec<DOMString> {
489 let mut result = vec![];
491
492 for elem in self.elements_iter() {
494 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 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 result
514 }
515}