1use blitz_dom::{BaseDocument, LocalName, NodeId, QualName};
10
11use crate::Result;
12use crate::error::DomError;
13
14pub(crate) fn attr_name(local: &str) -> QualName {
16 QualName::new(None, markup5ever::ns!(), LocalName::from(local))
17}
18
19pub(crate) fn read_attr(doc: &BaseDocument, node_id: NodeId, name: &str) -> Option<String> {
20 let element = doc.get_node(node_id)?.element_data()?;
21 element
22 .attrs()
23 .iter()
24 .find(|attr| &*attr.name.local == name)
25 .map(|attr| attr.value.to_string())
26}
27
28fn local_matches(local: &str, name: &str) -> bool {
36 local.len() == name.len()
37 && local
38 .bytes()
39 .zip(name.bytes())
40 .all(|(stored, queried)| stored == queried.to_ascii_lowercase())
41}
42
43pub(crate) fn find_attr<'doc>(
58 doc: &'doc BaseDocument,
59 node_id: NodeId,
60 name: &str,
61) -> Option<&'doc str> {
62 let element = doc.get_node(node_id)?.element_data()?;
63 element
64 .attrs()
65 .iter()
66 .find(|attr| local_matches(&attr.name.local, name))
67 .map(|attr| &*attr.value)
68}
69
70pub(crate) fn write_attr(doc: &mut BaseDocument, node_id: NodeId, name: &str, value: &str) {
71 doc.mutate().set_attribute(node_id, attr_name(name), value);
72}
73
74pub(crate) fn clear_attr(doc: &mut BaseDocument, node_id: NodeId, name: &str) {
75 doc.mutate().clear_attribute(node_id, attr_name(name));
76}
77
78pub fn tag_name(doc: &BaseDocument, node: NodeId) -> Result<String> {
84 Ok(doc
85 .get_node(node)
86 .and_then(|node| node.element_data())
87 .map(|element| element.name.local.to_uppercase())
88 .unwrap_or_default())
89}
90
91pub fn get_attribute(doc: &BaseDocument, node: NodeId, name: &str) -> Result<Option<String>> {
99 Ok(read_attr(doc, node, &name.to_ascii_lowercase()))
100}
101
102pub fn set_attribute(doc: &mut BaseDocument, node: NodeId, name: &str, value: &str) -> Result<()> {
106 write_attr(doc, node, &name.to_ascii_lowercase(), value);
107 Ok(())
108}
109
110pub fn remove_attribute(doc: &mut BaseDocument, node: NodeId, name: &str) -> Result<()> {
112 clear_attr(doc, node, &name.to_ascii_lowercase());
113 Ok(())
114}
115
116pub fn has_attribute(doc: &BaseDocument, node: NodeId, name: &str) -> Result<bool> {
123 Ok(find_attr(doc, node, name).is_some())
124}
125
126pub fn get_attribute_into(
147 doc: &BaseDocument,
148 node: NodeId,
149 name: &str,
150 out: &mut [u8],
151) -> Result<Option<usize>> {
152 let Some(value) = find_attr(doc, node, name) else {
153 return Ok(None);
154 };
155 let len = value.len();
156 if len <= out.len() {
157 out[..len].copy_from_slice(value.as_bytes());
158 }
159 Ok(Some(len))
160}
161
162fn class_tokens(doc: &BaseDocument, node_id: NodeId) -> Vec<String> {
165 read_attr(doc, node_id, "class")
166 .unwrap_or_default()
167 .split_ascii_whitespace()
168 .map(str::to_owned)
169 .collect()
170}
171
172fn write_class_tokens(doc: &mut BaseDocument, node_id: NodeId, tokens: &[String]) {
173 write_attr(doc, node_id, "class", &tokens.join(" "));
174}
175
176fn class_token(token: &str) -> Result<&str> {
178 if token.is_empty() || token.chars().any(|ch| ch.is_ascii_whitespace()) {
179 return Err(DomError::InvalidClassToken(token.to_owned()));
180 }
181 Ok(token)
182}
183
184pub fn class_list_add(doc: &mut BaseDocument, node: NodeId, tokens: &[&str]) -> Result<()> {
190 let to_add = tokens
191 .iter()
192 .map(|token| class_token(token).map(str::to_owned))
193 .collect::<Result<Vec<_>>>()?;
194 let mut current = class_tokens(doc, node);
195 for token in to_add {
196 if !current.contains(&token) {
197 current.push(token);
198 }
199 }
200 write_class_tokens(doc, node, ¤t);
201 Ok(())
202}
203
204pub fn class_list_remove(doc: &mut BaseDocument, node: NodeId, tokens: &[&str]) -> Result<()> {
206 let to_remove = tokens
207 .iter()
208 .map(|token| class_token(token).map(str::to_owned))
209 .collect::<Result<Vec<_>>>()?;
210 let mut current = class_tokens(doc, node);
211 current.retain(|token| !to_remove.contains(token));
212 write_class_tokens(doc, node, ¤t);
213 Ok(())
214}
215
216pub fn class_list_toggle(
219 doc: &mut BaseDocument,
220 node: NodeId,
221 token: &str,
222 force: Option<bool>,
223) -> Result<bool> {
224 let token = class_token(token)?.to_owned();
225 let mut tokens = class_tokens(doc, node);
226 let present = tokens.contains(&token);
227 let retain = force.unwrap_or(!present);
228 if retain && !present {
229 tokens.push(token);
230 } else if !retain && present {
231 tokens.retain(|item| item != &token);
232 }
233 write_class_tokens(doc, node, &tokens);
234 Ok(retain)
235}
236
237pub fn class_list_contains(doc: &BaseDocument, node: NodeId, token: &str) -> Result<bool> {
239 let token = class_token(token)?;
240 Ok(class_tokens(doc, node).iter().any(|item| item == token))
241}
242
243pub fn inner_html(doc: &BaseDocument, node: NodeId) -> Result<String> {
247 let mut html = String::new();
248 if let Some(node) = doc.get_node(node) {
249 for child_id in &node.children {
250 if let Some(child) = doc.get_node(*child_id) {
251 child.write_outer_html(&mut html);
252 }
253 }
254 }
255 Ok(html)
256}
257
258pub fn set_inner_html(doc: &mut BaseDocument, node: NodeId, html: &str) -> Result<()> {
272 let mut mutr = doc.mutate();
273 for child_id in mutr.child_ids(node) {
274 mutr.remove_node(child_id);
275 }
276 mutr.set_inner_html(node, html);
277 Ok(())
278}
279
280pub fn matches(doc: &BaseDocument, node: NodeId, selector: &str) -> Result<bool> {
287 doc.query_selector_all(selector)
288 .map(|matches| matches.contains(&node))
289 .map_err(|_| DomError::InvalidSelector(selector.to_owned()))
290}
291
292pub fn closest(doc: &BaseDocument, node: NodeId, selector: &str) -> Result<Option<NodeId>> {
297 let matches = doc
298 .query_selector_all(selector)
299 .map_err(|_| DomError::InvalidSelector(selector.to_owned()))?;
300 let mut current = Some(node);
301 while let Some(id) = current {
302 if matches.contains(&id) {
303 return Ok(Some(id));
304 }
305 current = doc.get_node(id).and_then(|node| node.parent);
306 }
307 Ok(None)
308}
309
310#[cfg(test)]
311mod tests {
312 use super::*;
313 use crate::document;
314 use crate::node;
315 use crate::test_support::skeleton;
316
317 fn attached_div(doc: &mut BaseDocument, parent: NodeId) -> NodeId {
318 let id = document::create_element(doc, "div").unwrap();
319 node::append_child(doc, parent, id).unwrap();
320 id
321 }
322
323 #[test]
324 fn tag_name_is_upper_case() {
325 let (mut doc, _html, _head, body) = skeleton();
326 let id = attached_div(&mut doc, body);
327 assert_eq!(tag_name(&doc, id).unwrap(), "DIV");
328 }
329
330 #[test]
331 fn tag_name_of_a_text_node_is_empty() {
332 let (mut doc, _html, _head, _body) = skeleton();
333 let text = document::create_text_node(&mut doc, "x").unwrap();
334 assert_eq!(tag_name(&doc, text).unwrap(), "");
335 }
336
337 #[test]
338 fn set_and_get_attribute_round_trip() {
339 let (mut doc, _html, _head, body) = skeleton();
340 let id = attached_div(&mut doc, body);
341 set_attribute(&mut doc, id, "data-role", "panel").unwrap();
342 assert_eq!(
343 get_attribute(&doc, id, "data-role").unwrap(),
344 Some("panel".to_string())
345 );
346 }
347
348 #[test]
349 fn attribute_names_are_lowercased_on_both_sides() {
350 let (mut doc, _html, _head, body) = skeleton();
351 let id = attached_div(&mut doc, body);
352 set_attribute(&mut doc, id, "DATA-Role", "panel").unwrap();
353 assert_eq!(
354 get_attribute(&doc, id, "data-role").unwrap(),
355 Some("panel".to_string())
356 );
357 assert!(has_attribute(&doc, id, "Data-ROLE").unwrap());
358 }
359
360 #[test]
361 fn get_attribute_is_none_when_absent_and_some_when_empty() {
362 let (mut doc, _html, _head, body) = skeleton();
363 let id = attached_div(&mut doc, body);
364 assert_eq!(get_attribute(&doc, id, "title").unwrap(), None);
365 set_attribute(&mut doc, id, "title", "").unwrap();
366 assert_eq!(
367 get_attribute(&doc, id, "title").unwrap(),
368 Some(String::new())
369 );
370 }
371
372 #[test]
377 fn get_attribute_into_agrees_with_get_attribute() {
378 let (mut doc, _html, _head, body) = skeleton();
379 let id = attached_div(&mut doc, body);
380 set_attribute(&mut doc, id, "data-role", "panel").unwrap();
381 set_attribute(&mut doc, id, "title", "").unwrap();
382
383 for name in ["data-role", "title", "absent"] {
384 let owned = get_attribute(&doc, id, name).unwrap();
385 let mut buf = [0u8; 64];
386 let written = get_attribute_into(&doc, id, name, &mut buf).unwrap();
387 match owned {
388 None => assert_eq!(written, None, "{name} should be absent both ways"),
389 Some(value) => {
390 assert_eq!(written, Some(value.len()), "{name} length");
391 assert_eq!(&buf[..value.len()], value.as_bytes(), "{name} bytes");
392 }
393 }
394 }
395 }
396
397 #[test]
400 fn get_attribute_into_reports_the_length_it_could_not_write() {
401 let (mut doc, _html, _head, body) = skeleton();
402 let id = attached_div(&mut doc, body);
403 set_attribute(&mut doc, id, "data-role", "panel").unwrap();
404
405 let mut buf = [b'.'; 4];
407 assert_eq!(
408 get_attribute_into(&doc, id, "data-role", &mut buf).unwrap(),
409 Some(5)
410 );
411 assert_eq!(
412 &buf, b"....",
413 "a value that does not fit must not be written"
414 );
415
416 assert_eq!(
418 get_attribute_into(&doc, id, "data-role", &mut []).unwrap(),
419 Some(5)
420 );
421
422 let mut buf = vec![0u8; 5];
424 assert_eq!(
425 get_attribute_into(&doc, id, "data-role", &mut buf).unwrap(),
426 Some(5)
427 );
428 assert_eq!(&buf, b"panel");
429 }
430
431 #[test]
435 fn get_attribute_into_lowercases_the_queried_name() {
436 let (mut doc, _html, _head, body) = skeleton();
437 let id = attached_div(&mut doc, body);
438 set_attribute(&mut doc, id, "data-role", "panel").unwrap();
439
440 let mut buf = [0u8; 16];
441 assert_eq!(
442 get_attribute_into(&doc, id, "DATA-Role", &mut buf).unwrap(),
443 Some(5)
444 );
445 assert_eq!(&buf[..5], b"panel");
446 assert!(has_attribute(&doc, id, "Data-ROLE").unwrap());
447
448 assert_eq!(
451 get_attribute_into(&doc, id, "data-rol", &mut buf).unwrap(),
452 None
453 );
454 }
455
456 #[test]
457 fn remove_attribute_clears_it() {
458 let (mut doc, _html, _head, body) = skeleton();
459 let id = attached_div(&mut doc, body);
460 set_attribute(&mut doc, id, "title", "x").unwrap();
461 remove_attribute(&mut doc, id, "title").unwrap();
462 assert_eq!(get_attribute(&doc, id, "title").unwrap(), None);
463 }
464
465 #[test]
466 fn has_attribute_tracks_presence() {
467 let (mut doc, _html, _head, body) = skeleton();
468 let id = attached_div(&mut doc, body);
469 assert!(!has_attribute(&doc, id, "hidden").unwrap());
470 set_attribute(&mut doc, id, "hidden", "").unwrap();
471 assert!(has_attribute(&doc, id, "hidden").unwrap());
472 }
473
474 #[test]
475 fn class_list_add_is_idempotent_and_ordered() {
476 let (mut doc, _html, _head, body) = skeleton();
477 let id = attached_div(&mut doc, body);
478 class_list_add(&mut doc, id, &["a", "b"]).unwrap();
479 class_list_add(&mut doc, id, &["a", "c"]).unwrap();
480 assert_eq!(
481 get_attribute(&doc, id, "class").unwrap(),
482 Some("a b c".to_string())
483 );
484 }
485
486 #[test]
487 fn class_list_remove_drops_every_named_token() {
488 let (mut doc, _html, _head, body) = skeleton();
489 let id = attached_div(&mut doc, body);
490 class_list_add(&mut doc, id, &["a", "b", "c"]).unwrap();
491 class_list_remove(&mut doc, id, &["a", "c"]).unwrap();
492 assert_eq!(
493 get_attribute(&doc, id, "class").unwrap(),
494 Some("b".to_string())
495 );
496 }
497
498 #[test]
499 fn class_list_toggle_flips_and_honours_force() {
500 let (mut doc, _html, _head, body) = skeleton();
501 let id = attached_div(&mut doc, body);
502 assert!(class_list_toggle(&mut doc, id, "on", None).unwrap());
503 assert!(class_list_contains(&doc, id, "on").unwrap());
504 assert!(!class_list_toggle(&mut doc, id, "on", None).unwrap());
505 assert!(!class_list_contains(&doc, id, "on").unwrap());
506
507 assert!(class_list_toggle(&mut doc, id, "on", Some(true)).unwrap());
508 assert!(class_list_toggle(&mut doc, id, "on", Some(true)).unwrap());
509 assert!(class_list_contains(&doc, id, "on").unwrap());
510 assert!(!class_list_toggle(&mut doc, id, "on", Some(false)).unwrap());
511 assert!(!class_list_contains(&doc, id, "on").unwrap());
512 }
513
514 #[test]
515 fn class_list_contains_reads_the_class_attribute() {
516 let (mut doc, _html, _head, body) = skeleton();
517 let id = attached_div(&mut doc, body);
518 set_attribute(&mut doc, id, "class", " a b ").unwrap();
519 assert!(class_list_contains(&doc, id, "a").unwrap());
520 assert!(class_list_contains(&doc, id, "b").unwrap());
521 assert!(!class_list_contains(&doc, id, "c").unwrap());
522 }
523
524 #[test]
525 fn an_invalid_class_token_is_rejected_before_anything_is_written() {
526 let (mut doc, _html, _head, body) = skeleton();
527 let id = attached_div(&mut doc, body);
528 class_list_add(&mut doc, id, &["keep"]).unwrap();
529
530 assert!(matches!(
531 class_list_add(&mut doc, id, &["ok", "two words"]),
532 Err(DomError::InvalidClassToken(_))
533 ));
534 assert!(matches!(
535 class_list_toggle(&mut doc, id, "", None),
536 Err(DomError::InvalidClassToken(_))
537 ));
538 assert!(matches!(
539 class_list_contains(&doc, id, "a b"),
540 Err(DomError::InvalidClassToken(_))
541 ));
542 assert_eq!(
543 get_attribute(&doc, id, "class").unwrap(),
544 Some("keep".to_string())
545 );
546 }
547
548 #[test]
552 fn inner_html_serialises_the_children() {
553 let (mut doc, _html, _head, body) = skeleton();
554 let outer = attached_div(&mut doc, body);
555 let child = document::create_element(&mut doc, "span").unwrap();
556 set_attribute(&mut doc, child, "class", "x").unwrap();
557 let text = document::create_text_node(&mut doc, "hi").unwrap();
558 node::append_child(&mut doc, child, text).unwrap();
559 node::append_child(&mut doc, outer, child).unwrap();
560
561 assert_eq!(
562 inner_html(&doc, outer).unwrap(),
563 "<span class=\"x\">hi</span>"
564 );
565 assert_eq!(inner_html(&doc, child).unwrap(), "hi");
566 }
567
568 #[test]
569 fn inner_html_of_a_childless_element_is_empty() {
570 let (mut doc, _html, _head, body) = skeleton();
571 let outer = attached_div(&mut doc, body);
572 assert_eq!(inner_html(&doc, outer).unwrap(), "");
573 }
574
575 #[test]
579 fn set_inner_html_detaches_the_previous_children() {
580 let (mut doc, _html, _head, body) = skeleton();
581 let outer = attached_div(&mut doc, body);
582 let old = attached_div(&mut doc, outer);
583 set_inner_html(&mut doc, outer, "<span></span>").unwrap();
584 assert_eq!(node::parent_node(&doc, old).unwrap(), None);
585 assert!(
586 doc.get_node(old).is_some(),
587 "the old child should be detached, not dropped"
588 );
589 }
590
591 #[test]
596 fn set_inner_html_parses_nothing_without_a_parser_provider() {
597 let (mut doc, _html, _head, body) = skeleton();
598 let outer = attached_div(&mut doc, body);
599 set_inner_html(&mut doc, outer, "<span>hi</span>").unwrap();
600 assert!(
601 node::child_nodes(&doc, outer).unwrap().is_empty(),
602 "DummyHtmlParserProvider should have parsed nothing"
603 );
604 }
605
606 #[test]
607 fn matches_tests_this_element_against_the_selector() {
608 let (mut doc, _html, _head, body) = skeleton();
609 let id = attached_div(&mut doc, body);
610 class_list_add(&mut doc, id, &["panel"]).unwrap();
611 assert!(matches(&doc, id, ".panel").unwrap());
612 assert!(!matches(&doc, id, ".other").unwrap());
613 }
614
615 #[test]
616 fn closest_checks_this_node_first_then_walks_up() {
617 let (mut doc, _html, _head, body) = skeleton();
618 let outer = attached_div(&mut doc, body);
619 let inner = attached_div(&mut doc, outer);
620 class_list_add(&mut doc, outer, &["panel"]).unwrap();
621
622 assert_eq!(closest(&doc, inner, ".panel").unwrap(), Some(outer));
623 class_list_add(&mut doc, inner, &["panel"]).unwrap();
624 assert_eq!(closest(&doc, inner, ".panel").unwrap(), Some(inner));
625 assert_eq!(closest(&doc, inner, ".absent").unwrap(), None);
626 }
627
628 #[test]
629 fn an_unparseable_selector_is_an_error_in_matches_and_closest() {
630 let (mut doc, _html, _head, body) = skeleton();
631 let id = attached_div(&mut doc, body);
632 assert!(matches!(
633 matches(&doc, id, "!!!"),
634 Err(DomError::InvalidSelector(_))
635 ));
636 assert!(matches!(
637 closest(&doc, id, "!!!"),
638 Err(DomError::InvalidSelector(_))
639 ));
640 }
641}