1use 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
27pub(crate) trait CollectionSource: JSTraceable {
32 fn iter<'a>(&'a self, root: &'a Node) -> Box<dyn Iterator<Item = DomRoot<Element>> + 'a>;
33}
34
35#[derive(JSTraceable)]
37enum CollectionKind {
38 Filter(Box<dyn CollectionFilter + 'static>),
40 Source(Box<dyn CollectionSource + 'static>),
42}
43
44#[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 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 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 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 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 #[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 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 let cached_version = self.cached_version.get();
194 let curr_version = self.root.inclusive_descendants_version();
195 if curr_version != cached_version {
196 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 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 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 HTMLCollection::match_element(elem, &self.ascii_lower_qualified_name)
249 } else {
250 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 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 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 fn Length(&self) -> u32 {
406 self.validate_cache();
407
408 if let Some(cached_length) = self.cached_length.get().to_option() {
409 cached_length
411 } else {
412 let length = self.elements_iter().count() as u32;
414 self.cached_length.set(OptionU32::some(length));
415 length
416 }
417 }
418
419 fn Item(&self, index: u32) -> Option<DomRoot<Element>> {
421 self.validate_cache();
422
423 if let Some(element) = self.cached_cursor_element.get() {
424 if let Some(cached_index) = self.cached_cursor_index.get().to_option() {
426 if cached_index == index {
427 return Some(element);
429 }
430
431 if let CollectionKind::Filter(ref filter) = self.kind {
434 let node: DomRoot<Node> = DomRoot::upcast(element);
435 return if cached_index < index {
436 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 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 self.set_cached_cursor(index, self.elements_iter().nth(index as usize))
458 }
459
460 fn NamedItem(&self, key: DOMString) -> Option<DomRoot<Element>> {
462 if key.is_empty() {
464 return None;
465 }
466
467 let key = Atom::from(key);
468
469 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 fn IndexedGetter(&self, index: u32) -> Option<DomRoot<Element>> {
478 self.Item(index)
479 }
480
481 fn NamedGetter(&self, name: DOMString) -> Option<DomRoot<Element>> {
483 self.NamedItem(name)
484 }
485
486 fn SupportedPropertyNames(&self) -> Vec<DOMString> {
488 let mut result = vec![];
490
491 for elem in self.elements_iter() {
493 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 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 result
513 }
514}