1use std::cell::Cell;
6
7use dom_struct::dom_struct;
8use html5ever::{LocalName, QualName, local_name, namespace_url, ns};
9use js::context::{JSContext, NoGC};
10use script_bindings::dom::UnrootedDom;
11use script_bindings::domstring::TracedDOMString;
12use script_bindings::reflector::{Reflector, reflect_dom_object};
13use style::str::split_html_space_chars;
14use stylo_atoms::Atom;
15
16use crate::dom::bindings::codegen::Bindings::HTMLCollectionBinding::HTMLCollectionMethods;
17use crate::dom::bindings::domname::namespace_from_domstring;
18use crate::dom::bindings::inheritance::Castable;
19use crate::dom::bindings::root::{Dom, DomRoot, MutNullableDom};
20use crate::dom::bindings::str::DOMString;
21use crate::dom::bindings::trace::JSTraceable;
22use crate::dom::element::Element;
23use crate::dom::iterators::ShadowIncluding;
24use crate::dom::node::{Node, NodeTraits};
25use crate::dom::window::Window;
26
27pub(crate) trait CollectionFilter: JSTraceable {
28 fn filter<'a>(&self, elem: &'a Element, root: &'a Node) -> bool;
29}
30
31pub(crate) trait CollectionSource: JSTraceable {
36 fn iter<'b>(
37 &'b self,
38 no_gc: &'b NoGC,
39 root: &'b Node,
40 ) -> Box<dyn Iterator<Item = UnrootedDom<'b, Element>> + 'b>;
41}
42
43#[derive(JSTraceable)]
45enum CollectionKind {
46 Filter(Box<dyn CollectionFilter>),
48 Source(Box<dyn CollectionSource>),
50}
51
52#[derive(Clone, Copy, JSTraceable, MallocSizeOf)]
56struct OptionU32 {
57 bits: u32,
58}
59
60impl OptionU32 {
61 fn to_option(self) -> Option<u32> {
62 if self.bits == u32::MAX {
63 None
64 } else {
65 Some(self.bits)
66 }
67 }
68
69 fn some(bits: u32) -> OptionU32 {
70 assert_ne!(bits, u32::MAX);
71 OptionU32 { bits }
72 }
73
74 fn none() -> OptionU32 {
75 OptionU32 { bits: u32::MAX }
76 }
77}
78
79#[dom_struct]
80pub(crate) struct HTMLCollection {
81 reflector_: Reflector,
82 root: Dom<Node>,
83 #[ignore_malloc_size_of = "Trait objects cannot be sized"]
84 kind: CollectionKind,
85 cached_version: Cell<u64>,
89 cached_cursor_element: MutNullableDom<Element>,
90 cached_cursor_index: Cell<OptionU32>,
91 cached_length: Cell<OptionU32>,
92}
93
94impl HTMLCollection {
95 fn new_inherited_with_kind(root: &Node, kind: CollectionKind) -> HTMLCollection {
96 HTMLCollection {
97 reflector_: Reflector::new(),
98 root: Dom::from_ref(root),
99 kind,
100 cached_version: Cell::new(root.inclusive_descendants_version()),
102 cached_cursor_element: MutNullableDom::new(None),
103 cached_cursor_index: Cell::new(OptionU32::none()),
104 cached_length: Cell::new(OptionU32::none()),
105 }
106 }
107
108 pub(crate) fn new_inherited(
109 root: &Node,
110 filter: Box<dyn CollectionFilter + 'static>,
111 ) -> HTMLCollection {
112 Self::new_inherited_with_kind(root, CollectionKind::Filter(filter))
113 }
114
115 pub(crate) fn new_inherited_with_source(
116 root: &Node,
117 source: Box<dyn CollectionSource + 'static>,
118 ) -> HTMLCollection {
119 Self::new_inherited_with_kind(root, CollectionKind::Source(source))
120 }
121
122 pub(crate) fn always_empty(
124 cx: &mut js::context::JSContext,
125 window: &Window,
126 root: &Node,
127 ) -> DomRoot<Self> {
128 #[derive(JSTraceable)]
129 struct NoFilter;
130 impl CollectionFilter for NoFilter {
131 fn filter<'a>(&self, _: &'a Element, _: &'a Node) -> bool {
132 false
133 }
134 }
135
136 Self::new(cx, window, root, Box::new(NoFilter))
137 }
138
139 pub(crate) fn new(
140 cx: &mut js::context::JSContext,
141 window: &Window,
142 root: &Node,
143 filter: Box<dyn CollectionFilter + 'static>,
144 ) -> DomRoot<Self> {
145 reflect_dom_object(cx, Box::new(Self::new_inherited(root, filter)), window)
146 }
147
148 pub(crate) fn new_with_filter_fn(
150 cx: &mut js::context::JSContext,
151 window: &Window,
152 root: &Node,
153 filter_function: fn(&Element, &Node) -> bool,
154 ) -> DomRoot<Self> {
155 #[derive(JSTraceable, MallocSizeOf)]
158 pub(crate) struct StaticFunctionFilter(
159 #[no_trace]
160 #[ignore_malloc_size_of = "Static function pointer"]
161 fn(&Element, &Node) -> bool,
162 );
163 impl CollectionFilter for StaticFunctionFilter {
164 fn filter(&self, element: &Element, root: &Node) -> bool {
165 (self.0)(element, root)
166 }
167 }
168 Self::new(
169 cx,
170 window,
171 root,
172 Box::new(StaticFunctionFilter(filter_function)),
173 )
174 }
175
176 pub(crate) fn create(
177 cx: &mut js::context::JSContext,
178 window: &Window,
179 root: &Node,
180 filter: Box<dyn CollectionFilter + 'static>,
181 ) -> DomRoot<Self> {
182 Self::new(cx, window, root, filter)
183 }
184
185 pub(crate) fn new_with_source(
187 cx: &mut js::context::JSContext,
188 window: &Window,
189 root: &Node,
190 source: Box<dyn CollectionSource + 'static>,
191 ) -> DomRoot<Self> {
192 reflect_dom_object(
193 cx,
194 Box::new(Self::new_inherited_with_source(root, source)),
195 window,
196 )
197 }
198
199 fn validate_cache(&self) {
200 let cached_version = self.cached_version.get();
202 let curr_version = self.root.inclusive_descendants_version();
203 if curr_version != cached_version {
204 self.cached_version.set(curr_version);
206 self.cached_cursor_element.set(None);
207 self.cached_length.set(OptionU32::none());
208 self.cached_cursor_index.set(OptionU32::none());
209 }
210 }
211
212 fn set_cached_cursor(
213 &self,
214 index: u32,
215 element: Option<DomRoot<Element>>,
216 ) -> Option<DomRoot<Element>> {
217 if let Some(element) = element {
218 self.cached_cursor_index.set(OptionU32::some(index));
219 self.cached_cursor_element.set(Some(&element));
220 Some(element)
221 } else {
222 None
223 }
224 }
225
226 pub(crate) fn by_qualified_name(
228 cx: &mut js::context::JSContext,
229 window: &Window,
230 root: &Node,
231 qualified_name: LocalName,
232 ) -> DomRoot<HTMLCollection> {
233 if qualified_name == local_name!("*") {
235 #[derive(JSTraceable, MallocSizeOf)]
236 struct AllFilter;
237 impl CollectionFilter for AllFilter {
238 fn filter(&self, _elem: &Element, _root: &Node) -> bool {
239 true
240 }
241 }
242 return HTMLCollection::create(cx, window, root, Box::new(AllFilter));
243 }
244
245 #[derive(JSTraceable, MallocSizeOf)]
246 struct HtmlDocumentFilter {
247 #[no_trace]
248 qualified_name: LocalName,
249 #[no_trace]
250 ascii_lower_qualified_name: LocalName,
251 }
252 impl CollectionFilter for HtmlDocumentFilter {
253 fn filter(&self, elem: &Element, root: &Node) -> bool {
254 if root.is_in_html_doc() && elem.namespace() == &ns!(html) {
255 HTMLCollection::match_element(elem, &self.ascii_lower_qualified_name)
257 } else {
258 HTMLCollection::match_element(elem, &self.qualified_name)
260 }
261 }
262 }
263
264 let filter = HtmlDocumentFilter {
265 ascii_lower_qualified_name: qualified_name.to_ascii_lowercase(),
266 qualified_name,
267 };
268 HTMLCollection::create(cx, window, root, Box::new(filter))
269 }
270
271 fn match_element(elem: &Element, qualified_name: &LocalName) -> bool {
272 match elem.prefix().as_ref() {
273 None => elem.local_name() == qualified_name,
274 Some(prefix) => {
275 qualified_name.starts_with(&**prefix) &&
276 qualified_name.find(':') == Some(prefix.len()) &&
277 qualified_name.ends_with(&**elem.local_name())
278 },
279 }
280 }
281
282 pub(crate) fn by_tag_name_ns(
283 cx: &mut js::context::JSContext,
284 window: &Window,
285 root: &Node,
286 tag: DOMString,
287 maybe_ns: Option<DOMString>,
288 ) -> DomRoot<HTMLCollection> {
289 let local = LocalName::from(tag);
290 let ns = namespace_from_domstring(maybe_ns);
291 let qname = QualName::new(None, ns, local);
292 HTMLCollection::by_qual_tag_name(cx, window, root, qname)
293 }
294
295 pub(crate) fn by_qual_tag_name(
296 cx: &mut js::context::JSContext,
297 window: &Window,
298 root: &Node,
299 qname: QualName,
300 ) -> DomRoot<HTMLCollection> {
301 #[derive(JSTraceable, MallocSizeOf)]
302 struct TagNameNSFilter {
303 #[no_trace]
304 qname: QualName,
305 }
306 impl CollectionFilter for TagNameNSFilter {
307 fn filter(&self, elem: &Element, _root: &Node) -> bool {
308 ((self.qname.ns == namespace_url!("*")) || (self.qname.ns == *elem.namespace())) &&
309 ((self.qname.local == local_name!("*")) ||
310 (self.qname.local == *elem.local_name()))
311 }
312 }
313 let filter = TagNameNSFilter { qname };
314 HTMLCollection::create(cx, window, root, Box::new(filter))
315 }
316
317 pub(crate) fn by_class_name(
318 cx: &mut js::context::JSContext,
319 window: &Window,
320 root: &Node,
321 classes: DOMString,
322 ) -> DomRoot<HTMLCollection> {
323 let class_atoms = split_html_space_chars(&classes.str())
324 .map(Atom::from)
325 .collect();
326 HTMLCollection::by_atomic_class_name(cx, window, root, class_atoms)
327 }
328
329 pub(crate) fn by_atomic_class_name(
330 cx: &mut js::context::JSContext,
331 window: &Window,
332 root: &Node,
333 classes: Vec<Atom>,
334 ) -> DomRoot<HTMLCollection> {
335 #[derive(JSTraceable, MallocSizeOf)]
336 struct ClassNameFilter {
337 #[no_trace]
338 classes: Vec<Atom>,
339 }
340 impl CollectionFilter for ClassNameFilter {
341 fn filter(&self, elem: &Element, _root: &Node) -> bool {
342 let case_sensitivity = elem
343 .owner_document()
344 .quirks_mode()
345 .classes_and_ids_case_sensitivity();
346
347 self.classes
348 .iter()
349 .all(|class| elem.has_class(class, case_sensitivity))
350 }
351 }
352
353 if classes.is_empty() {
354 return HTMLCollection::always_empty(cx, window, root);
355 }
356
357 let filter = ClassNameFilter { classes };
358 HTMLCollection::create(cx, window, root, Box::new(filter))
359 }
360
361 pub(crate) fn children(
362 cx: &mut js::context::JSContext,
363 window: &Window,
364 root: &Node,
365 ) -> DomRoot<HTMLCollection> {
366 HTMLCollection::new_with_filter_fn(cx, window, root, |element, root| {
367 root.is_parent_of(element.upcast())
368 })
369 }
370
371 fn filter_iter_after<'b>(
374 &'b self,
375 no_gc: &'b NoGC,
376 after: &'b Node,
377 filter: &'b (dyn CollectionFilter + 'static),
378 ) -> impl Iterator<Item = UnrootedDom<'b, Element>> + 'b {
379 after
380 .following_nodes_unrooted(no_gc, &self.root, ShadowIncluding::No)
381 .filter_map(UnrootedDom::downcast)
382 .filter(move |element| filter.filter(element, &self.root))
383 }
384
385 fn filter_iter_before<'a, 'b>(
388 &'a self,
389 no_gc: &'b NoGC,
390 before: &'a Node,
391 filter: &'a (dyn CollectionFilter + 'static),
392 ) -> impl Iterator<Item = UnrootedDom<'b, Element>> + 'a
393 where
394 'b: 'a,
395 {
396 before
397 .preceding_nodes_unrooted(no_gc, &self.root)
398 .filter_map(UnrootedDom::downcast)
399 .filter(move |element| filter.filter(element, &self.root))
400 }
401
402 pub(crate) fn elements_iter<'b>(
403 &'b self,
404 no_gc: &'b NoGC,
405 ) -> Box<dyn Iterator<Item = UnrootedDom<'b, Element>> + 'b> {
406 match &self.kind {
407 CollectionKind::Filter(filter) => {
408 Box::new(self.filter_iter_after(no_gc, &self.root, filter.as_ref()))
409 },
410 CollectionKind::Source(source) => source.iter(no_gc, &self.root),
411 }
412 }
413
414 pub(crate) fn root_node(&self) -> DomRoot<Node> {
415 DomRoot::from_ref(&self.root)
416 }
417}
418
419impl HTMLCollectionMethods<crate::DomTypeHolder> for HTMLCollection {
420 fn Length(&self, cx: &JSContext) -> u32 {
422 self.validate_cache();
423
424 if let Some(cached_length) = self.cached_length.get().to_option() {
425 cached_length
427 } else {
428 let length = self.elements_iter(cx.no_gc()).count() as u32;
430 self.cached_length.set(OptionU32::some(length));
431 length
432 }
433 }
434
435 fn Item(&self, cx: &JSContext, index: u32) -> Option<DomRoot<Element>> {
437 self.validate_cache();
438
439 if let Some(element) = self.cached_cursor_element.get() {
440 if let Some(cached_index) = self.cached_cursor_index.get().to_option() {
442 if cached_index == index {
443 return Some(element);
445 }
446
447 if let CollectionKind::Filter(ref filter) = self.kind {
450 let node: DomRoot<Node> = DomRoot::upcast(element);
451 return if cached_index < index {
452 let offset = index - (cached_index + 1);
454 self.set_cached_cursor(
455 index,
456 self.filter_iter_after(cx.no_gc(), &node, filter.as_ref())
457 .nth(offset as usize)
458 .map(|node| node.as_rooted()),
459 )
460 } else {
461 let offset = cached_index - (index + 1);
463 self.set_cached_cursor(
464 index,
465 self.filter_iter_before(cx.no_gc(), &node, filter.as_ref())
466 .nth(offset as usize)
467 .map(|node| node.as_rooted()),
468 )
469 };
470 }
471 }
472 }
473
474 self.set_cached_cursor(
476 index,
477 self.elements_iter(cx.no_gc())
478 .nth(index as usize)
479 .map(|node| node.as_rooted()),
480 )
481 }
482
483 fn NamedItem(&self, cx: &JSContext, key: DOMString) -> Option<DomRoot<Element>> {
485 if key.is_empty() {
487 return None;
488 }
489
490 let key = Atom::from(key);
491
492 self.elements_iter(cx.no_gc())
494 .find(|elem| {
495 elem.get_id().is_some_and(|id| id == key) ||
496 (elem.namespace() == &ns!(html) &&
497 elem.get_name().is_some_and(|id| id == key))
498 })
499 .map(|node| node.as_rooted())
500 }
501
502 fn IndexedGetter(&self, cx: &JSContext, index: u32) -> Option<DomRoot<Element>> {
504 self.Item(cx, index)
505 }
506
507 fn NamedGetter(&self, cx: &JSContext, name: DOMString) -> Option<DomRoot<Element>> {
509 self.NamedItem(cx, name)
510 }
511
512 fn SupportedPropertyNames(&self, no_gc: &NoGC) -> Vec<DOMString> {
514 let mut result = vec![];
516
517 for elem in self.elements_iter(no_gc) {
519 if let Some(id_atom) = elem.get_id() {
521 let id_str = TracedDOMString::from(id_atom).root();
522 if !result.contains(&id_str) {
523 result.push(id_str);
524 }
525 }
526 if *elem.namespace() == ns!(html) &&
528 let Some(name_atom) = elem.get_name()
529 {
530 let name_str = TracedDOMString::from(name_atom).root();
531 if !result.contains(&name_str) {
532 result.push(name_str)
533 }
534 }
535 }
536
537 result
539 }
540}