1#![allow(unused_variables)]
15
16use alloc::{boxed::Box, collections::BTreeMap, string::String, vec::Vec};
17use core::fmt;
18#[cfg(feature = "std")]
19use std::path::Path;
20
21#[cfg(feature = "svg")]
22pub mod svg;
23
24fn decode_xml_entities(s: &str) -> std::borrow::Cow<'_, str> {
29 if !s.contains('&') {
31 return std::borrow::Cow::Borrowed(s);
32 }
33 decode_xml_entities_slow(s)
34}
35
36fn decode_xml_entities_slow(s: &str) -> std::borrow::Cow<'_, str> {
37 let mut result = String::with_capacity(s.len());
38 let mut chars = s.chars().peekable();
39
40 while let Some(c) = chars.next() {
41 if c == '&' {
42 let mut entity = String::new();
44 let mut found_semicolon = false;
45
46 while let Some(&next) = chars.peek() {
47 if next == ';' {
48 chars.next();
49 found_semicolon = true;
50 break;
51 }
52 if !next.is_alphanumeric() && next != '#' {
53 break;
54 }
55 entity.push(chars.next().unwrap());
56 if entity.len() > 10 {
57 break;
59 }
60 }
61
62 if found_semicolon {
63 match entity.as_str() {
65 "lt" => result.push('<'),
66 "gt" => result.push('>'),
67 "amp" => result.push('&'),
68 "apos" => result.push('\''),
69 "quot" => result.push('"'),
70 "nbsp" => result.push('\u{00A0}'),
71 s if s.starts_with('#') => {
72 let num_str = &s[1..];
74 let code_point = if num_str.starts_with('x') || num_str.starts_with('X') {
75 u32::from_str_radix(&num_str[1..], 16).ok()
77 } else {
78 num_str.parse::<u32>().ok()
80 };
81 if let Some(cp) = code_point {
82 if let Some(ch) = char::from_u32(cp) {
83 result.push(ch);
84 } else {
85 result.push('&');
87 result.push_str(&entity);
88 result.push(';');
89 }
90 } else {
91 result.push('&');
93 result.push_str(&entity);
94 result.push(';');
95 }
96 }
97 _ => {
98 result.push('&');
100 result.push_str(&entity);
101 result.push(';');
102 }
103 }
104 } else {
105 result.push('&');
107 result.push_str(&entity);
108 }
109 } else {
110 result.push(c);
111 }
112 }
113
114 std::borrow::Cow::Owned(result)
115}
116
117pub use azul_core::xml::*;
118use azul_core::{dom::Dom, impl_from, styled_dom::StyledDom, window::StringPairVec};
119#[cfg(feature = "parser")]
120use azul_css::parser2::CssParseError;
121use azul_css::{css::Css, AzString, OptionString, U8Vec};
122use xmlparser::Tokenizer;
123
124#[cfg(feature = "xml")]
125#[must_use] pub fn domxml_from_str(xml: &str, component_map: &ComponentMap) -> DomXml {
126 let error_css = Css::empty();
127
128 let parsed = match parse_xml_string(xml) {
129 Ok(parsed) => parsed,
130 Err(e) => {
131 return DomXml {
132 parsed_dom: {
133 let mut dom = Dom::create_body()
134 .with_children(vec![Dom::create_text(format!("{e}"))].into());
135 StyledDom::create(&mut dom, error_css)
136 },
137 };
138 }
139 };
140
141 let parsed_dom = match str_to_dom(parsed.as_ref(), component_map, None) {
142 Ok(o) => o,
143 Err(e) => {
144 return DomXml {
145 parsed_dom: {
146 let mut dom = Dom::create_body()
147 .with_children(vec![Dom::create_text(format!("{e}"))].into());
148 StyledDom::create(&mut dom, error_css)
149 },
150 };
151 }
152 };
153
154 DomXml { parsed_dom }
155}
156
157#[allow(clippy::needless_pass_by_value)]
164#[must_use] pub fn dom_from_parsed_xml(xml: Xml) -> Dom {
165 let component_map = ComponentMap::with_builtin();
166 match str_to_dom_unstyled(xml.root.as_ref(), &component_map) {
167 Ok(dom) => dom,
168 Err(e) => Dom::create_body().with_children(vec![Dom::create_text(format!("{e}"))].into()),
169 }
170}
171
172pub fn parse_xml_to_fast_dom(xml: &str) -> Result<azul_core::dom::FastDom, XmlError> {
187 let (fast_dom, _css) = parse_xml_to_fast_dom_with_css(xml)?;
188 Ok(fast_dom)
189}
190
191#[allow(clippy::cast_precision_loss)] pub fn parse_xml_to_styled_dom(xml: &str) -> Result<StyledDom, XmlError> {
197 static MEM_ENABLED: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
205 let mem_on = *MEM_ENABLED.get_or_init(azul_core::profile::memory_enabled);
206
207 let rss0 = if mem_on { peak_rss_bytes() } else { 0 };
208 let (mut fast_dom, css) = parse_xml_to_fast_dom_with_css(xml)?;
209 if mem_on {
210 let rss1 = peak_rss_bytes();
211 eprintln!(
212 "[XML] tokenize+fast_dom : +{:.2} MiB",
213 (rss1.saturating_sub(rss0)) as f64 / 1024.0 / 1024.0,
214 );
215 }
216
217 let rss1 = if mem_on { peak_rss_bytes() } else { 0 };
218 if !css.is_empty() {
220 let combined_css = Css::new(css.into_iter()
221 .flat_map(|c| c.rules.into_library_owned_vec())
222 .collect());
223 fast_dom.css = vec![azul_core::dom::CssWithNodeId {
224 node_id: 0, css: combined_css,
226 }].into();
227 }
228 if mem_on {
229 let rss2 = peak_rss_bytes();
230 eprintln!(
231 "[XML] css attach : +{:.2} MiB",
232 (rss2.saturating_sub(rss1)) as f64 / 1024.0 / 1024.0,
233 );
234 }
235
236 crate::probe::hint_purge_allocator();
241
242 let rss2 = if mem_on { peak_rss_bytes() } else { 0 };
243 let styled = StyledDom::create_from_fast_dom(fast_dom);
244
245 crate::probe::hint_purge_allocator();
250
251 if mem_on {
252 let rss3 = peak_rss_bytes();
253 eprintln!(
254 "[XML] create_from_fast_dom : +{:.2} MiB",
255 (rss3.saturating_sub(rss2)) as f64 / 1024.0 / 1024.0,
256 );
257 }
258
259 Ok(styled)
260}
261
262#[cfg(all(unix, feature = "probe"))]
267fn peak_rss_bytes() -> u64 {
268 let mut usage: libc::rusage = unsafe { std::mem::zeroed() };
269 if unsafe { libc::getrusage(libc::RUSAGE_SELF, &mut usage) } != 0 {
270 return 0;
271 }
272 let ru = usage.ru_maxrss as u64;
273 #[cfg(target_os = "macos")]
275 { ru }
276 #[cfg(not(target_os = "macos"))]
277 { ru.saturating_mul(1024) }
278}
279
280#[cfg(not(all(unix, feature = "probe")))]
281const fn peak_rss_bytes() -> u64 {
282 0
283}
284
285#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)] #[allow(clippy::too_many_lines, clippy::cognitive_complexity)] fn parse_xml_to_fast_dom_with_css(xml: &str) -> Result<(azul_core::dom::FastDom, Vec<Css>), XmlError> {
289 use xmlparser::{ElementEnd::{Open, Empty, Close}, Token::{ElementStart, Attribute, ElementEnd, Text}, Tokenizer};
290 use azul_core::dom::{NodeData, NodeType, IdOrClass, TabIndex};
291 use azul_core::xml::CompactDomBuilder;
292
293 const ESTIMATED_BYTES_PER_NODE: usize = 20;
294
295 const VOID_ELEMENTS: &[&str] = &[
296 "area", "base", "br", "col", "embed", "hr", "img", "input", "link", "meta",
297 "param", "source", "track", "wbr",
298 ];
299
300 fn lowercase_into(dst: &mut String, src: &str) {
304 dst.clear();
305 if src.bytes().all(|b| !b.is_ascii_uppercase()) {
306 dst.push_str(src);
307 } else {
308 dst.reserve(src.len());
309 for b in src.bytes() {
310 dst.push(b.to_ascii_lowercase() as char);
311 }
312 }
313 }
314
315 let xml = xml.strip_prefix('\u{FEFF}').unwrap_or(xml);
317 let mut xml = xml.trim();
318
319 if xml.starts_with("<?") {
321 if let Some(pos) = xml.find("?>") {
322 xml = &xml[(pos + 2)..];
323 }
324 }
325
326 let mut xml = xml.trim();
328 if xml.len() > 9 && xml.is_char_boundary(9) && xml[..9].to_ascii_lowercase().starts_with("<!doctype") {
329 if let Some(pos) = xml.find('>') {
330 xml = &xml[(pos + 1)..];
331 }
332 } else if xml.starts_with("<!--") {
333 if let Some(end) = xml.find("-->") {
334 xml = &xml[(end + 3)..];
335 xml = xml.trim();
336 }
337 }
338
339 let tokenizer = Tokenizer::from_fragment(xml, 0..xml.len());
340
341 let estimated_nodes = xml.len() / ESTIMATED_BYTES_PER_NODE;
342 let mut builder = CompactDomBuilder::with_capacity(estimated_nodes);
343 let mut collected_css: Vec<Css> = Vec::new();
344 let mut inside_style_tag = false;
345 let mut style_text = String::new();
346 let mut head_depth: usize = 0;
349
350 let mut current_tag: String = String::new();
352 let mut current_attrs: Vec<(String, String)> = Vec::new();
353 let mut pending_open = false;
354
355 let css_key_map = azul_css::props::property::get_css_key_map();
357
358 let mut str_arena = azul_css::corety::StringArena::new();
364
365 let finalize_open = |
368 builder: &mut CompactDomBuilder,
369 str_arena: &mut azul_css::corety::StringArena,
370 tag: &str,
371 attrs: &[(String, String)],
372 css_key_map: &azul_css::props::property::CssKeyMap,
373 | {
374 let node_type = tag_to_node_type(tag);
375 let mut nd = NodeData::create_node(node_type);
376
377 let mut attr_vec: Vec<azul_core::dom::AttributeType> = Vec::new();
380 for (key, value) in attrs {
381 match key.as_str() {
382 "id" => {
383 for id in value.split_whitespace() {
384 attr_vec.push(azul_core::dom::AttributeType::Id(str_arena.intern(id)));
385 }
386 }
387 "class" => {
388 for class in value.split_whitespace() {
389 attr_vec.push(azul_core::dom::AttributeType::Class(str_arena.intern(class)));
390 }
391 }
392 "focusable" => {
393 if let Some(f) = parse_bool(value.as_str()) {
394 nd.set_tab_index(if f { TabIndex::Auto } else { TabIndex::NoKeyboardFocus });
395 }
396 }
397 "tabindex" => {
398 if let Ok(ti) = value.parse::<isize>() {
399 match ti {
400 0 => nd.set_tab_index(TabIndex::Auto),
401 i if i > 0 => nd.set_tab_index(TabIndex::OverrideInParent(i as u32)),
402 _ => nd.set_tab_index(TabIndex::NoKeyboardFocus),
403 }
404 }
405 }
406 "style" => {
407 let mut css_attrs = Vec::new();
408 for s in value.split(';') {
409 let mut s = s.split(':');
410 let Some(key) = s.next() else { continue };
411 let Some(val) = s.next() else { continue };
412 drop(azul_css::parser2::parse_css_declaration(
415 key.trim(), val.trim(),
416 azul_css::parser2::ErrorLocationRange::default(),
417 css_key_map, &mut Vec::new(), &mut css_attrs,
418 ));
419 }
420 let props = css_attrs.into_iter().filter_map(|s| {
421 use azul_css::css::CssDeclaration;
422 use azul_css::dynamic_selector::CssPropertyWithConditions;
423 match s {
424 CssDeclaration::Static(s) => Some(CssPropertyWithConditions::simple(s)),
425 CssDeclaration::Dynamic(_) => None,
426 }
427 }).collect::<Vec<_>>();
428 if !props.is_empty() {
429 nd.set_css_props(props.into());
430 }
431 }
432 "contenteditable" => {
433 if parse_bool(value.as_str()).unwrap_or(false) {
434 nd.set_contenteditable(true);
435 }
436 }
437 _ => {}
438 }
439 }
440 if !attr_vec.is_empty() {
441 nd.set_attributes(attr_vec.into());
442 }
443
444 builder.open_node(nd);
445 };
446
447 let mut last_was_void = false;
448 let mut tag_stack: Vec<String> = Vec::new(); for token in tokenizer {
451 let token = token.map_err(|e| XmlError::ParserError(translate_xmlparser_error(e)))?;
452 match token {
453 ElementStart { local, .. } => {
454 if pending_open {
456 let is_void = VOID_ELEMENTS.contains(¤t_tag.as_str());
457 if current_tag == "head" { head_depth += 1; }
458 if head_depth == 0 {
459 finalize_open(&mut builder, &mut str_arena, ¤t_tag, ¤t_attrs, &css_key_map);
460 if is_void { builder.close_node(); }
461 }
462 if !is_void {
463 tag_stack.push(core::mem::take(&mut current_tag));
464 }
465 }
466
467 lowercase_into(&mut current_tag, local.as_str());
470 current_attrs.clear();
471 pending_open = true;
472 last_was_void = VOID_ELEMENTS.contains(¤t_tag.as_str());
473 }
474 Attribute { local, value, .. } => {
475 current_attrs.push((local.to_string(), decode_xml_entities(value.as_str()).into_owned()));
482 }
483 ElementEnd { end: Open, .. } => {
484 if pending_open {
485 let is_void = VOID_ELEMENTS.contains(¤t_tag.as_str());
486 if current_tag == "style" {
487 inside_style_tag = true;
488 style_text.clear();
489 }
490 if current_tag == "head" { head_depth += 1; }
491 if head_depth == 0 {
492 finalize_open(&mut builder, &mut str_arena, ¤t_tag, ¤t_attrs, &css_key_map);
493 if is_void { builder.close_node(); }
494 }
495 if !is_void {
496 tag_stack.push(core::mem::take(&mut current_tag));
500 }
501 pending_open = false;
502 }
503 }
504 ElementEnd { end: Empty, .. } => {
505 if pending_open {
507 if current_tag == "head" { head_depth += 1; }
508 if head_depth == 0 {
509 finalize_open(&mut builder, &mut str_arena, ¤t_tag, ¤t_attrs, &css_key_map);
510 builder.close_node();
511 }
512 if current_tag == "head" && head_depth > 0 { head_depth -= 1; }
513 pending_open = false;
514 }
515 }
516 ElementEnd { end: Close(_, close_value), .. } => {
517 if pending_open {
518 let is_void = VOID_ELEMENTS.contains(¤t_tag.as_str());
519 if current_tag == "head" { head_depth += 1; }
520 if head_depth == 0 {
521 finalize_open(&mut builder, &mut str_arena, ¤t_tag, ¤t_attrs, &css_key_map);
522 if is_void { builder.close_node(); }
523 }
524 if !is_void {
525 tag_stack.push(core::mem::take(&mut current_tag));
526 }
527 pending_open = false;
528 }
529
530 let close_lower = close_value.as_str().to_ascii_lowercase();
531 let close_str = close_lower.as_str();
532 if VOID_ELEMENTS.contains(&close_str) {
533 continue;
534 }
535
536 if close_str == "style" && inside_style_tag {
538 if !style_text.is_empty() {
539 let parsed_css = Css::from_string(core::mem::take(&mut style_text).into());
540 collected_css.push(parsed_css);
541 }
542 inside_style_tag = false;
543 }
544
545 while let Some(top) = tag_stack.last() {
547 let is_match = top == close_str;
548 let was_head = top == "head";
549 let popped = tag_stack.pop().unwrap();
551 if popped == "head" && head_depth > 0 { head_depth -= 1; }
552 if head_depth == 0 && !was_head {
553 builder.close_node();
554 }
555 if is_match { break; }
556 }
557 }
558 Text { text } => {
559 if pending_open {
560 let is_void = VOID_ELEMENTS.contains(¤t_tag.as_str());
561 if current_tag == "style" {
562 inside_style_tag = true;
563 style_text.clear();
564 }
565 if current_tag == "head" { head_depth += 1; }
566 if head_depth == 0 {
567 finalize_open(&mut builder, &mut str_arena, ¤t_tag, ¤t_attrs, &css_key_map);
568 if is_void { builder.close_node(); }
569 }
570 if !is_void {
571 tag_stack.push(current_tag.clone());
572 }
573 pending_open = false;
574 }
575
576 let text_str = text.as_str();
577 if !text_str.is_empty() {
578 if inside_style_tag {
579 style_text.push_str(text_str);
580 } else if head_depth == 0 {
581 let inside_body = tag_stack.iter().any(|t| t == "body");
584 if inside_body || !text_str.trim().is_empty() {
585 let decoded = decode_xml_entities(text_str);
586 builder.add_leaf(NodeData::create_text(str_arena.intern(&decoded)));
587 }
588 }
589 }
590 }
591 _ => {}
592 }
593 }
594
595 if pending_open {
597 finalize_open(&mut builder, &mut str_arena, ¤t_tag, ¤t_attrs, &css_key_map);
598 }
599 while tag_stack.pop().is_some() {
600 builder.close_node();
601 }
602
603 drop(str_arena);
606
607 Ok((builder.finish(), collected_css))
608}
609
610#[cfg(all(feature = "std", feature = "xml"))]
616pub fn domxml_from_file<I: AsRef<Path>>(
617 file_path: I,
618 component_map: &ComponentMap,
619) -> DomXml {
620 use std::fs;
621
622 let error_css = Css::empty();
623
624 let xml = match fs::read_to_string(file_path.as_ref()) {
625 Ok(xml) => xml,
626 Err(e) => {
627 return DomXml {
628 parsed_dom: {
629 let mut dom = Dom::create_body()
630 .with_children(
631 vec![Dom::create_text(format!(
632 "Error reading: \"{}\": {}",
633 file_path.as_ref().to_string_lossy(),
634 e
635 ))]
636 .into(),
637 );
638 StyledDom::create(&mut dom, error_css)
639 },
640 };
641 }
642 };
643
644 domxml_from_str(&xml, component_map)
645}
646
647#[cfg(feature = "xml")]
654#[allow(clippy::too_many_lines)] pub fn parse_xml_string(xml: &str) -> Result<Vec<XmlNodeChild>, XmlError> {
659 use xmlparser::{ElementEnd::{Empty, Close}, Token::{ElementStart, ElementEnd, Attribute, Text}, Tokenizer};
660
661 use self::XmlParseError::*;
662
663 const VOID_ELEMENTS: &[&str] = &[
666 "area", "base", "br", "col", "embed", "hr", "img", "input", "link", "meta", "param",
667 "source", "track", "wbr",
668 ];
669
670 const AUTO_CLOSE_RULES: &[(&str, &[&str])] = &[
673 ("li", &["li"]),
675 ("td", &["td", "th", "tr"]),
677 ("th", &["td", "th", "tr"]),
678 ("tr", &["tr"]),
679 (
681 "p",
682 &[
683 "address",
684 "article",
685 "aside",
686 "blockquote",
687 "div",
688 "dl",
689 "fieldset",
690 "footer",
691 "form",
692 "h1",
693 "h2",
694 "h3",
695 "h4",
696 "h5",
697 "h6",
698 "header",
699 "hr",
700 "main",
701 "nav",
702 "ol",
703 "p",
704 "pre",
705 "section",
706 "table",
707 "ul",
708 ],
709 ),
710 ("option", &["option", "optgroup"]),
712 ("optgroup", &["optgroup"]),
713 ("dd", &["dd", "dt"]),
715 ("dt", &["dd", "dt"]),
716 ];
717
718 let mut root_node = XmlNode::default();
719
720 let xml = xml.strip_prefix('\u{FEFF}').unwrap_or(xml);
722
723 let mut xml = xml.trim();
725 if xml.starts_with("<?") {
726 let pos = xml.find("?>").ok_or(XmlError::MalformedHierarchy(
727 MalformedHierarchyError {
728 expected: "<?xml".into(),
729 got: "?>".into(),
730 },
731 ))?;
732 xml = &xml[(pos + 2)..];
733 }
734
735 let mut xml = xml.trim();
737 if xml.len() > 9 && xml.is_char_boundary(9) && xml[..9].to_ascii_lowercase().starts_with("<!doctype") {
738 let pos = xml.find('>').ok_or(XmlError::MalformedHierarchy(
739 MalformedHierarchyError {
740 expected: "<!DOCTYPE".into(),
741 got: ">".into(),
742 },
743 ))?;
744 xml = &xml[(pos + 1)..];
745 } else if xml.starts_with("<!--") {
746 if let Some(end) = xml.find("-->") {
748 xml = &xml[(end + 3)..];
749 xml = xml.trim();
750 }
751 }
752
753 let tokenizer = Tokenizer::from_fragment(xml, 0..xml.len());
754
755 let mut node_stack: Vec<*mut XmlNode> = vec![&raw mut root_node];
761
762 let mut last_was_void = false;
764
765 for token in tokenizer {
766 let token = token.map_err(|e| XmlError::ParserError(translate_xmlparser_error(e)))?;
767 match token {
768 ElementStart { local, .. } => {
769 let tag_name = local.to_string();
770 let is_void_element = VOID_ELEMENTS.contains(&tag_name.as_str());
771
772 if last_was_void {
775 node_stack.pop();
776 last_was_void = false;
777 }
778
779 if node_stack.len() > 1 {
781 let current_element = unsafe { &*node_stack[node_stack.len() - 1] };
783 let current_tag = current_element.node_type.as_str();
784
785 for (element, closes_on) in AUTO_CLOSE_RULES {
787 if current_tag == *element && closes_on.contains(&tag_name.as_str()) {
788 node_stack.pop();
790 break;
791 }
792 }
793 }
794
795 if let Some(¤t_parent_ptr) = node_stack.last() {
797 let current_parent = unsafe { &mut *current_parent_ptr };
798
799 current_parent.children.push(XmlNodeChild::Element(XmlNode {
800 node_type: tag_name.into(),
801 attributes: StringPairVec::new().into(),
802 children: Vec::new().into(),
803 }));
804
805 let children_len = current_parent.children.len();
807 if let Some(XmlNodeChild::Element(ref mut new_child)) = current_parent.children.as_mut().get_mut(children_len - 1) {
808 node_stack.push(std::ptr::from_mut::<XmlNode>(new_child));
809 }
810
811 last_was_void = is_void_element;
812 }
813 }
814 ElementEnd { end: Empty, .. } => {
815 if node_stack.len() > 1 {
817 node_stack.pop();
818 }
819 last_was_void = false;
820 }
821 ElementEnd {
822 end: Close(_, close_value),
823 ..
824 } => {
825 if last_was_void {
827 node_stack.pop();
828 last_was_void = false;
829 }
830
831 let is_void_element = VOID_ELEMENTS.contains(&close_value.as_str());
833 if is_void_element {
834 continue;
836 }
837
838 let close_value_str = close_value.as_str();
841
842 let mut found_idx = None;
844 for i in (1..node_stack.len()).rev() {
845 let node = unsafe { &*node_stack[i] };
847 if node.node_type.as_str() == close_value_str {
848 found_idx = Some(i);
849 break;
850 }
851 }
852
853 if let Some(idx) = found_idx {
854 node_stack.truncate(idx);
856 }
857 last_was_void = false;
860 }
861 Attribute { local, value, .. } => {
862 if let Some(&last_ptr) = node_stack.last() {
864 let last = unsafe { &mut *last_ptr };
865 last.attributes.push(azul_core::window::AzStringPair {
868 key: local.to_string().into(),
869 value: AzString::from(&*decode_xml_entities(value.as_str())),
870 });
871 }
872 }
873 Text { text } => {
874 if last_was_void {
876 node_stack.pop();
877 last_was_void = false;
878 }
879
880 let text_str = text.as_str();
888
889 if !text_str.is_empty() {
890 if let Some(¤t_parent_ptr) = node_stack.last() {
892 let current_parent = unsafe { &mut *current_parent_ptr };
893 let decoded_text = decode_xml_entities(text_str);
895 current_parent
897 .children
898 .push(XmlNodeChild::Text(AzString::from(&*decoded_text)));
899 }
900 }
901 }
902 _ => {}
903 }
904 }
905
906 if last_was_void {
908 node_stack.pop();
909 }
910
911 if node_stack.len() != 1 {
916 return Err(XmlError::UnclosedRootNode);
917 }
918
919 Ok(root_node.children.into())
920}
921
922#[cfg(feature = "xml")]
923pub fn parse_xml(s: &str) -> Result<Xml, XmlError> {
927 Ok(Xml {
928 root: parse_xml_string(s)?.into(),
929 })
930}
931
932#[cfg(not(feature = "xml"))]
933pub fn parse_xml(s: &str) -> Result<Xml, XmlError> {
934 Err(XmlError::NoParserAvailable)
935}
936
937#[cfg(feature = "xml")]
940#[must_use] pub fn translate_roxmltree_expandedname(
941 e: roxmltree::ExpandedName<'_, '_>,
942) -> XmlQualifiedName {
943 let ns: Option<AzString> = e.namespace().map(|e| e.to_string().into());
944 XmlQualifiedName {
945 local_name: e.name().to_string().into(),
946 namespace: ns.into(),
947 }
948}
949
950#[cfg(feature = "xml")]
951fn translate_roxmltree_attribute(e: roxmltree::Attribute<'_, '_>) -> XmlQualifiedName {
952 XmlQualifiedName {
953 local_name: e.name().to_string().into(),
954 namespace: e.namespace().map(|e| e.to_string().into()).into(),
955 }
956}
957
958#[cfg(feature = "xml")]
959fn translate_xmlparser_streamerror(e: xmlparser::StreamError) -> XmlStreamError {
960 match e {
961 xmlparser::StreamError::UnexpectedEndOfStream => XmlStreamError::UnexpectedEndOfStream,
962 xmlparser::StreamError::InvalidName => XmlStreamError::InvalidName,
963 xmlparser::StreamError::InvalidReference => XmlStreamError::InvalidReference,
964 xmlparser::StreamError::InvalidExternalID => XmlStreamError::InvalidExternalID,
965 xmlparser::StreamError::InvalidCommentData => XmlStreamError::InvalidCommentData,
966 xmlparser::StreamError::InvalidCommentEnd => XmlStreamError::InvalidCommentEnd,
967 xmlparser::StreamError::InvalidCharacterData => XmlStreamError::InvalidCharacterData,
968 xmlparser::StreamError::NonXmlChar(c, tp) => XmlStreamError::NonXmlChar(NonXmlCharError {
969 ch: c.into(),
970 pos: translate_xmlparser_textpos(tp),
971 }),
972 xmlparser::StreamError::InvalidChar(a, b, tp) => {
973 XmlStreamError::InvalidChar(InvalidCharError {
974 expected: a,
975 got: b,
976 pos: translate_xmlparser_textpos(tp),
977 })
978 }
979 xmlparser::StreamError::InvalidCharMultiple(a, b, tp) => {
980 XmlStreamError::InvalidCharMultiple(InvalidCharMultipleError {
981 expected: a,
982 got: b.to_vec().into(),
983 pos: translate_xmlparser_textpos(tp),
984 })
985 }
986 xmlparser::StreamError::InvalidQuote(a, tp) => {
987 XmlStreamError::InvalidQuote(InvalidQuoteError {
988 got: a,
989 pos: translate_xmlparser_textpos(tp),
990 })
991 }
992 xmlparser::StreamError::InvalidSpace(a, tp) => {
993 XmlStreamError::InvalidSpace(InvalidSpaceError {
994 got: a,
995 pos: translate_xmlparser_textpos(tp),
996 })
997 }
998 xmlparser::StreamError::InvalidString(a, tp) => {
999 XmlStreamError::InvalidString(InvalidStringError {
1000 got: a.to_string().into(),
1001 pos: translate_xmlparser_textpos(tp),
1002 })
1003 }
1004 }
1005}
1006
1007#[cfg(feature = "xml")]
1008fn translate_xmlparser_error(e: xmlparser::Error) -> XmlParseError {
1009 match e {
1010 xmlparser::Error::InvalidDeclaration(se, tp) => {
1011 XmlParseError::InvalidDeclaration(XmlTextError {
1012 stream_error: translate_xmlparser_streamerror(se),
1013 pos: translate_xmlparser_textpos(tp),
1014 })
1015 }
1016 xmlparser::Error::InvalidComment(se, tp) => XmlParseError::InvalidComment(XmlTextError {
1017 stream_error: translate_xmlparser_streamerror(se),
1018 pos: translate_xmlparser_textpos(tp),
1019 }),
1020 xmlparser::Error::InvalidPI(se, tp) => XmlParseError::InvalidPI(XmlTextError {
1021 stream_error: translate_xmlparser_streamerror(se),
1022 pos: translate_xmlparser_textpos(tp),
1023 }),
1024 xmlparser::Error::InvalidDoctype(se, tp) => XmlParseError::InvalidDoctype(XmlTextError {
1025 stream_error: translate_xmlparser_streamerror(se),
1026 pos: translate_xmlparser_textpos(tp),
1027 }),
1028 xmlparser::Error::InvalidEntity(se, tp) => XmlParseError::InvalidEntity(XmlTextError {
1029 stream_error: translate_xmlparser_streamerror(se),
1030 pos: translate_xmlparser_textpos(tp),
1031 }),
1032 xmlparser::Error::InvalidElement(se, tp) => XmlParseError::InvalidElement(XmlTextError {
1033 stream_error: translate_xmlparser_streamerror(se),
1034 pos: translate_xmlparser_textpos(tp),
1035 }),
1036 xmlparser::Error::InvalidAttribute(se, tp) => {
1037 XmlParseError::InvalidAttribute(XmlTextError {
1038 stream_error: translate_xmlparser_streamerror(se),
1039 pos: translate_xmlparser_textpos(tp),
1040 })
1041 }
1042 xmlparser::Error::InvalidCdata(se, tp) => XmlParseError::InvalidCdata(XmlTextError {
1043 stream_error: translate_xmlparser_streamerror(se),
1044 pos: translate_xmlparser_textpos(tp),
1045 }),
1046 xmlparser::Error::InvalidCharData(se, tp) => XmlParseError::InvalidCharData(XmlTextError {
1047 stream_error: translate_xmlparser_streamerror(se),
1048 pos: translate_xmlparser_textpos(tp),
1049 }),
1050 xmlparser::Error::UnknownToken(tp) => {
1051 XmlParseError::UnknownToken(translate_xmlparser_textpos(tp))
1052 }
1053 }
1054}
1055
1056#[cfg(feature = "xml")]
1057#[must_use] pub fn translate_roxmltree_error(e: roxmltree::Error) -> XmlError {
1058 match e {
1059 roxmltree::Error::InvalidXmlPrefixUri(s) => {
1060 XmlError::InvalidXmlPrefixUri(translate_roxml_textpos(s))
1061 }
1062 roxmltree::Error::UnexpectedXmlUri(s) => {
1063 XmlError::UnexpectedXmlUri(translate_roxml_textpos(s))
1064 }
1065 roxmltree::Error::UnexpectedXmlnsUri(s) => {
1066 XmlError::UnexpectedXmlnsUri(translate_roxml_textpos(s))
1067 }
1068 roxmltree::Error::InvalidElementNamePrefix(s) => {
1069 XmlError::InvalidElementNamePrefix(translate_roxml_textpos(s))
1070 }
1071 roxmltree::Error::DuplicatedNamespace(s, tp) => {
1072 XmlError::DuplicatedNamespace(DuplicatedNamespaceError {
1073 ns: s.into(),
1074 pos: translate_roxml_textpos(tp),
1075 })
1076 }
1077 roxmltree::Error::UnknownNamespace(s, tp) => {
1078 XmlError::UnknownNamespace(UnknownNamespaceError {
1079 ns: s.into(),
1080 pos: translate_roxml_textpos(tp),
1081 })
1082 }
1083 roxmltree::Error::UnexpectedCloseTag(expected, actual, pos) => {
1084 XmlError::UnexpectedCloseTag(UnexpectedCloseTagError {
1085 expected: expected.into(),
1086 actual: actual.into(),
1087 pos: translate_roxml_textpos(pos),
1088 })
1089 }
1090 roxmltree::Error::UnexpectedEntityCloseTag(s) => {
1091 XmlError::UnexpectedEntityCloseTag(translate_roxml_textpos(s))
1092 }
1093 roxmltree::Error::UnknownEntityReference(s, tp) => {
1094 XmlError::UnknownEntityReference(UnknownEntityReferenceError {
1095 entity: s.into(),
1096 pos: translate_roxml_textpos(tp),
1097 })
1098 }
1099 roxmltree::Error::MalformedEntityReference(s) => {
1100 XmlError::MalformedEntityReference(translate_roxml_textpos(s))
1101 }
1102 roxmltree::Error::EntityReferenceLoop(s) => {
1103 XmlError::EntityReferenceLoop(translate_roxml_textpos(s))
1104 }
1105 roxmltree::Error::InvalidAttributeValue(s) => {
1106 XmlError::InvalidAttributeValue(translate_roxml_textpos(s))
1107 }
1108 roxmltree::Error::DuplicatedAttribute(s, tp) => {
1109 XmlError::DuplicatedAttribute(DuplicatedAttributeError {
1110 attribute: s.into(),
1111 pos: translate_roxml_textpos(tp),
1112 })
1113 }
1114 roxmltree::Error::NoRootNode => XmlError::NoRootNode,
1115 roxmltree::Error::DtdDetected => XmlError::DtdDetected,
1116 roxmltree::Error::UnclosedRootNode => XmlError::UnclosedRootNode,
1117 roxmltree::Error::UnexpectedDeclaration(tp) => {
1118 XmlError::UnexpectedDeclaration(translate_roxml_textpos(tp))
1119 }
1120 roxmltree::Error::NodesLimitReached => XmlError::NodesLimitReached,
1121 roxmltree::Error::AttributesLimitReached => XmlError::AttributesLimitReached,
1122 roxmltree::Error::NamespacesLimitReached => XmlError::NamespacesLimitReached,
1123 roxmltree::Error::InvalidName(tp) => XmlError::InvalidName(translate_roxml_textpos(tp)),
1124 roxmltree::Error::NonXmlChar(_, tp) => XmlError::NonXmlChar(translate_roxml_textpos(tp)),
1125 roxmltree::Error::InvalidChar(_, _, tp) => {
1126 XmlError::InvalidChar(translate_roxml_textpos(tp))
1127 }
1128 roxmltree::Error::InvalidChar2(_, _, tp) => {
1129 XmlError::InvalidChar2(translate_roxml_textpos(tp))
1130 }
1131 roxmltree::Error::InvalidString(_, tp) => {
1132 XmlError::InvalidString(translate_roxml_textpos(tp))
1133 }
1134 roxmltree::Error::InvalidExternalID(tp) => {
1135 XmlError::InvalidExternalID(translate_roxml_textpos(tp))
1136 }
1137 roxmltree::Error::InvalidComment(tp) => {
1138 XmlError::InvalidComment(translate_roxml_textpos(tp))
1139 }
1140 roxmltree::Error::InvalidCharacterData(tp) => {
1141 XmlError::InvalidCharacterData(translate_roxml_textpos(tp))
1142 }
1143 roxmltree::Error::UnknownToken(tp) => XmlError::UnknownToken(translate_roxml_textpos(tp)),
1144 roxmltree::Error::UnexpectedEndOfStream => XmlError::UnexpectedEndOfStream,
1145 roxmltree::Error::EntityResolver(tp, s) => {
1146 XmlError::UnknownEntityReference(UnknownEntityReferenceError {
1149 entity: s.into(),
1150 pos: translate_roxml_textpos(tp),
1151 })
1152 }
1153 }
1154}
1155
1156#[cfg(feature = "xml")]
1157#[inline]
1158const fn translate_xmlparser_textpos(o: xmlparser::TextPos) -> XmlTextPos {
1159 XmlTextPos {
1160 row: o.row,
1161 col: o.col,
1162 }
1163}
1164
1165#[cfg(feature = "xml")]
1166#[inline]
1167const fn translate_roxml_textpos(o: roxmltree::TextPos) -> XmlTextPos {
1168 XmlTextPos {
1169 row: o.row,
1170 col: o.col,
1171 }
1172}
1173
1174#[cfg(feature = "xml")]
1180pub trait DomXmlExt {
1181 fn from_xml_string<S: AsRef<str>>(xml: S) -> StyledDom;
1192}
1193
1194#[cfg(feature = "xml")]
1195impl DomXmlExt for Dom {
1196 fn from_xml_string<S: AsRef<str>>(xml: S) -> StyledDom {
1197 let component_map = ComponentMap::with_builtin();
1198 let dom_xml = domxml_from_str(xml.as_ref(), &component_map);
1199 dom_xml.parsed_dom
1200 }
1201}
1202
1203#[cfg(test)]
1210mod autotest_generated {
1211 use azul_core::dom::{FastDom, NodeData, NodeType, TabIndex};
1212
1213 use super::*;
1214
1215 #[cfg(feature = "xml")]
1221 fn elements(children: &[XmlNodeChild]) -> Vec<&XmlNode> {
1222 children
1223 .iter()
1224 .filter_map(XmlNodeChild::as_element)
1225 .collect()
1226 }
1227
1228 #[cfg(feature = "xml")]
1230 fn texts(children: &[XmlNodeChild]) -> Vec<&str> {
1231 children.iter().filter_map(XmlNodeChild::as_text).collect()
1232 }
1233
1234 fn doc(body: &str) -> String {
1241 format!("<html><body>{body}</body></html>")
1242 }
1243
1244 fn nodes(dom: &FastDom) -> &[NodeData] {
1246 dom.node_data.as_ref()
1247 }
1248
1249 fn text_of(nd: &NodeData) -> Option<String> {
1251 match nd.get_node_type() {
1252 NodeType::Text(_) => nd.get_node_type().format(),
1253 _ => None,
1254 }
1255 }
1256
1257 #[cfg(feature = "xml")]
1259 fn escape(s: &str) -> String {
1260 let mut out = String::with_capacity(s.len());
1261 for c in s.chars() {
1262 match c {
1263 '&' => out.push_str("&"),
1264 '<' => out.push_str("<"),
1265 '>' => out.push_str(">"),
1266 '"' => out.push_str("""),
1267 '\'' => out.push_str("'"),
1268 _ => out.push(c),
1269 }
1270 }
1271 out
1272 }
1273
1274 const GARBAGE: &[&str] = &[
1277 "<<<<>>>>",
1278 "!!!not xml at all!!!",
1279 "<a b=c>",
1280 "</>",
1281 "</div>",
1282 "<a></a",
1283 "&&&&&&&&&&&&",
1284 "]]>",
1285 "<!--",
1286 "<![CDATA[",
1287 "<?",
1288 "<!DOCTYPE",
1289 "\u{0}\u{1}\u{2}",
1290 "<a><<a><<<a>",
1291 "= = = = = = = = = =",
1292 ];
1293
1294 #[test]
1299 fn decode_xml_entities_borrows_when_there_is_no_ampersand() {
1300 for s in ["", "hello", " ", "日本語 🙂", "<tag/>", "a;b;c;"] {
1301 assert!(
1302 matches!(decode_xml_entities(s), std::borrow::Cow::Borrowed(_)),
1303 "{s:?} has no '&' and must take the zero-alloc path"
1304 );
1305 assert_eq!(&*decode_xml_entities(s), s);
1306 }
1307 }
1308
1309 #[test]
1310 fn decode_xml_entities_decodes_the_five_named_entities_and_nbsp() {
1311 assert_eq!(&*decode_xml_entities("<"), "<");
1312 assert_eq!(&*decode_xml_entities(">"), ">");
1313 assert_eq!(&*decode_xml_entities("&"), "&");
1314 assert_eq!(&*decode_xml_entities("'"), "'");
1315 assert_eq!(&*decode_xml_entities("""), "\"");
1316 assert_eq!(&*decode_xml_entities(" "), "\u{00A0}");
1317 assert_eq!(
1318 &*decode_xml_entities("a<b>c&d"e'f"),
1319 "a<b>c&d\"e'f"
1320 );
1321 }
1322
1323 #[test]
1324 fn decode_xml_entities_decodes_numeric_references() {
1325 assert_eq!(&*decode_xml_entities("<"), "<");
1327 assert_eq!(&*decode_xml_entities("<"), "<");
1328 assert_eq!(&*decode_xml_entities("<"), "<");
1329 assert_eq!(&*decode_xml_entities("A"), "A");
1330 assert_eq!(&*decode_xml_entities("�"), "\u{0}");
1332 assert_eq!(&*decode_xml_entities(""), "\u{FFFF}");
1333 assert_eq!(&*decode_xml_entities("𐀀"), "\u{10000}");
1334 assert_eq!(&*decode_xml_entities(""), "\u{10FFFF}");
1335 assert_eq!(&*decode_xml_entities(""), "\u{10FFFF}");
1336 assert_eq!(&*decode_xml_entities("é"), "e\u{301}");
1338 }
1339
1340 #[test]
1341 fn decode_xml_entities_keeps_out_of_range_and_surrogate_code_points_verbatim() {
1342 for s in [
1345 "�", "�", "�", "�", "�", "�", "�", "�",
1353 "&#x;",
1354 "&#;",
1355 "&#xZZ;",
1356 "&#-1;",
1357 ] {
1358 assert_eq!(
1359 &*decode_xml_entities(s),
1360 s,
1361 "{s:?} is not a decodable reference and must be preserved byte-for-byte"
1362 );
1363 }
1364 }
1365
1366 #[test]
1367 fn decode_xml_entities_keeps_unterminated_and_unknown_entities_verbatim() {
1368 for s in [
1369 "&",
1370 "&&",
1371 "<",
1372 "&#",
1373 "&#x",
1374 "&foo;",
1375 "<", "&Amp;",
1377 "& lt;",
1378 "a & b",
1379 "&;",
1380 ] {
1381 assert_eq!(&*decode_xml_entities(s), s, "{s:?} must be preserved");
1382 }
1383 assert_eq!(&*decode_xml_entities("<&"), "<&");
1385 }
1386
1387 #[test]
1388 fn decode_xml_entities_does_not_double_decode() {
1389 assert_eq!(&*decode_xml_entities("&lt;"), "<");
1393 assert_eq!(&*decode_xml_entities("&amp;"), "&");
1394 assert_eq!(&*decode_xml_entities("&#60;"), "<");
1395 }
1396
1397 #[test]
1398 fn decode_xml_entities_handles_pathological_input_without_panicking() {
1399 let long_name = format!("&{};", "a".repeat(10_000));
1401 assert_eq!(&*decode_xml_entities(&long_name), long_name);
1402
1403 let long_tail = format!("&{}", "x".repeat(1_000_000));
1405 assert_eq!(decode_xml_entities(&long_tail).len(), long_tail.len());
1406
1407 for s in [
1411 "&\u{1F600}\u{1F600};",
1412 "&½;",
1413 "&日本;",
1414 "&#\u{1F600};",
1415 "&e\u{301};",
1416 "🙂&🙂",
1417 ] {
1418 let out = decode_xml_entities(s);
1419 assert!(
1420 !out.is_empty(),
1421 "{s:?} decoded to nothing (input was non-empty)"
1422 );
1423 }
1424
1425 let many = "<".repeat(50_000);
1427 assert_eq!(decode_xml_entities(&many).chars().count(), 50_000);
1428 }
1429
1430 #[test]
1431 fn decode_xml_entities_slow_matches_the_fast_path() {
1432 for s in [
1436 "",
1437 "plain",
1438 "日本語 🙂",
1439 "<",
1440 "&lt;",
1441 "😀",
1442 "&unknown;",
1443 "&",
1444 ] {
1445 assert_eq!(
1446 &*decode_xml_entities(s),
1447 &*decode_xml_entities_slow(s),
1448 "fast/slow path disagree on {s:?}"
1449 );
1450 }
1451 for s in ["", "plain", "日本語 🙂", "a;b", "<>"] {
1452 assert_eq!(&*decode_xml_entities_slow(s), s);
1453 }
1454 }
1455
1456 #[cfg(feature = "xml")]
1477 #[test]
1478 fn parse_entrypoints_do_not_panic_on_short_multibyte_input() {
1479 const INPUT: &str = "😀😀😀";
1482 assert!(INPUT.len() > 9 && !INPUT.is_char_boundary(9));
1483
1484 let a = std::panic::catch_unwind(|| parse_xml_string(INPUT).is_ok());
1485 let b = std::panic::catch_unwind(|| parse_xml_to_fast_dom(INPUT).is_ok());
1486
1487 assert!(
1488 a.is_ok(),
1489 "parse_xml_string panicked on {INPUT:?}: the DOCTYPE sniff slices \
1490 xml[..9] without an is_char_boundary check"
1491 );
1492 assert!(
1493 b.is_ok(),
1494 "parse_xml_to_fast_dom panicked on {INPUT:?}: same unchecked \
1495 xml[..9] slice"
1496 );
1497 }
1498
1499 #[cfg(feature = "xml")]
1504 #[test]
1505 fn parse_xml_string_accepts_empty_and_whitespace_only_input() {
1506 for s in ["", " ", " ", "\t\n", "\r\n\r\n", "\u{FEFF}", "\u{FEFF} "] {
1507 let parsed = parse_xml_string(s)
1508 .unwrap_or_else(|e| panic!("{s:?} should parse to an empty tree, got {e}"));
1509 assert!(parsed.is_empty(), "{s:?} produced {} roots", parsed.len());
1510 }
1511 }
1512
1513 #[cfg(feature = "xml")]
1514 #[test]
1515 fn parse_xml_string_parses_a_minimal_document() {
1516 let parsed = parse_xml_string(&doc("<div>hi</div>")).expect("valid document");
1517 let roots = elements(&parsed);
1518 assert_eq!(roots.len(), 1);
1519 assert_eq!(roots[0].node_type.as_str(), "html");
1520
1521 let body = elements(roots[0].children.as_ref());
1522 assert_eq!(body.len(), 1);
1523 assert_eq!(body[0].node_type.as_str(), "body");
1524
1525 let div = elements(body[0].children.as_ref());
1526 assert_eq!(div.len(), 1);
1527 assert_eq!(div[0].node_type.as_str(), "div");
1528 assert_eq!(texts(div[0].children.as_ref()), vec!["hi"]);
1529 }
1530
1531 #[cfg(feature = "xml")]
1532 #[test]
1533 fn parse_xml_string_rejects_unclosed_elements() {
1534 assert!(matches!(
1537 parse_xml_string("<div>"),
1538 Err(XmlError::UnclosedRootNode)
1539 ));
1540 assert!(matches!(
1541 parse_xml_string("<html><body><div>"),
1542 Err(XmlError::UnclosedRootNode)
1543 ));
1544 assert!(
1545 parse_xml_string("<html><body><div>text").is_err(),
1546 "an unclosed element must not yield a partial 'valid' tree"
1547 );
1548 }
1549
1550 #[cfg(feature = "xml")]
1551 #[test]
1552 fn parse_xml_string_rejects_truncated_declaration_and_doctype() {
1553 assert!(matches!(
1554 parse_xml_string("<?xml version=\"1.0\""),
1555 Err(XmlError::MalformedHierarchy(_))
1556 ));
1557 assert!(matches!(
1558 parse_xml_string("<!DOCTYPE html PUBLIC \"x\""),
1559 Err(XmlError::MalformedHierarchy(_))
1560 ));
1561 for prefix in [
1563 "<?xml version=\"1.0\"?>",
1564 "<!DOCTYPE html>",
1565 "<!doctype HTML>",
1566 "<!-- leading comment -->",
1567 "\u{FEFF}",
1568 ] {
1569 let src = format!("{prefix}{}", doc("<div/>"));
1570 let parsed = parse_xml_string(&src)
1571 .unwrap_or_else(|e| panic!("{prefix:?} prefix should be stripped, got {e}"));
1572 let roots = elements(&parsed);
1573 assert_eq!(roots.len(), 1, "{prefix:?} -> {roots:?}");
1574 assert_eq!(roots[0].node_type.as_str(), "html");
1575 }
1576 }
1577
1578 #[cfg(feature = "xml")]
1579 #[test]
1580 fn parse_xml_string_is_deterministic_on_garbage() {
1581 for g in GARBAGE {
1582 let a = parse_xml_string(g);
1585 let b = parse_xml_string(g);
1586 assert_eq!(a.is_ok(), b.is_ok(), "{g:?} parsed non-deterministically");
1587 assert_eq!(a.ok(), b.ok(), "{g:?} produced two different trees");
1588 }
1589 }
1590
1591 #[cfg(feature = "xml")]
1592 #[test]
1593 fn parse_xml_string_trims_leading_and_trailing_whitespace() {
1594 let padded = format!(" \t\n{}\n\t ", doc("<div/>"));
1595 let a = parse_xml_string(&padded).expect("padded document");
1596 let b = parse_xml_string(&doc("<div/>")).expect("bare document");
1597 assert_eq!(a, b, "surrounding whitespace must not change the tree");
1598 }
1599
1600 #[cfg(feature = "xml")]
1601 #[test]
1602 fn parse_xml_string_keeps_trailing_junk_as_text() {
1603 let parsed = parse_xml_string(&format!("{};garbage", doc("<div/>"))).expect("lenient parse");
1606 assert_eq!(elements(&parsed).len(), 1);
1607 assert_eq!(texts(&parsed), vec![";garbage"]);
1608 }
1609
1610 #[cfg(feature = "xml")]
1611 #[test]
1612 fn parse_xml_string_round_trips_escaped_text() {
1613 for raw in [
1614 "a",
1615 "<b>bold</b> & \"quotes\" 'apos'",
1616 "&&&&",
1617 " spaced ",
1618 "日本語 🙂 combining e\u{301}",
1619 "1 < 2 > 0 && true",
1620 ] {
1621 let src = doc(&escape(raw));
1622 let parsed = parse_xml_string(&src)
1623 .unwrap_or_else(|e| panic!("{src:?} should parse, got {e}"));
1624 let html = elements(&parsed);
1625 let body = elements(html[0].children.as_ref());
1626 assert_eq!(
1627 texts(body[0].children.as_ref()),
1628 vec![raw],
1629 "escape -> parse must be the identity for {raw:?}"
1630 );
1631 }
1632 }
1633
1634 #[cfg(feature = "xml")]
1635 #[test]
1636 fn parse_xml_string_decodes_attribute_entities() {
1637 let parsed = parse_xml_string(&doc(
1638 r#"<div t="&<>"'x" u="&nosuch;" v="😀"></div>"#,
1639 ))
1640 .expect("valid document");
1641 let html = elements(&parsed);
1642 let body = elements(html[0].children.as_ref());
1643 let div = elements(body[0].children.as_ref());
1644 let attrs = &div[0].attributes;
1645
1646 assert_eq!(attrs.get_key("t").map(AzString::as_str), Some("&<>\"'x"));
1647 assert_eq!(attrs.get_key("u").map(AzString::as_str), Some("&nosuch;"));
1648 assert_eq!(attrs.get_key("v").map(AzString::as_str), Some("😀"));
1649 }
1650
1651 #[cfg(feature = "xml")]
1652 #[test]
1653 fn parse_xml_string_tolerates_extra_and_mismatched_close_tags() {
1654 let nested = doc("<div></span></div>");
1655 let paragraphs = doc("<p>one<p>two");
1656 for src in [
1657 "<a></a></a>",
1658 "<a></a></b>",
1659 nested.as_str(),
1660 paragraphs.as_str(),
1661 "<br></br>",
1662 "<br>",
1663 "<br><br><br>",
1664 ] {
1665 let a = parse_xml_string(src);
1666 let b = parse_xml_string(src);
1667 assert_eq!(a.is_ok(), b.is_ok(), "{src:?} is non-deterministic");
1668 assert_eq!(a.ok(), b.ok(), "{src:?} produced two different trees");
1669 }
1670 let parsed = parse_xml_string("<br>").expect("bare void element");
1672 assert_eq!(elements(&parsed).len(), 1);
1673 assert_eq!(elements(&parsed)[0].node_type.as_str(), "br");
1674 }
1675
1676 #[cfg(feature = "xml")]
1677 #[test]
1678 fn parse_xml_string_handles_deep_nesting_without_stack_overflow() {
1679 const DEPTH: usize = 1_000;
1684 let mut src = String::with_capacity(DEPTH * 12);
1685 for _ in 0..DEPTH {
1686 src.push_str("<a>");
1687 }
1688 for _ in 0..DEPTH {
1689 src.push_str("</a>");
1690 }
1691
1692 let parsed = parse_xml_string(&src).expect("balanced nesting is valid");
1693 let mut depth = 0_usize;
1694 {
1695 let mut cursor: Vec<&XmlNode> = elements(&parsed);
1696 while !cursor.is_empty() {
1697 depth += 1;
1698 let node: &XmlNode = cursor[0];
1699 cursor = elements(node.children.as_ref());
1700 }
1701 }
1702 assert_eq!(depth, DEPTH, "every nesting level must be preserved");
1703 drop(parsed);
1706 }
1707
1708 #[cfg(feature = "xml")]
1709 #[test]
1710 fn parse_xml_string_handles_a_one_million_char_text_node() {
1711 let payload = "x".repeat(1_000_000);
1712 let parsed = parse_xml_string(&doc(&payload)).expect("long text is valid");
1713 let html = elements(&parsed);
1714 let body = elements(html[0].children.as_ref());
1715 let t = texts(body[0].children.as_ref());
1716 assert_eq!(t.len(), 1);
1717 assert_eq!(t[0].len(), 1_000_000);
1718 }
1719
1720 #[cfg(feature = "xml")]
1721 #[test]
1722 fn parse_xml_string_handles_many_sibling_elements() {
1723 const N: usize = 2_000;
1724 let parsed = parse_xml_string(&doc(&"<i>x</i>".repeat(N))).expect("wide tree is valid");
1725 let html = elements(&parsed);
1726 let body = elements(html[0].children.as_ref());
1727 assert_eq!(elements(body[0].children.as_ref()).len(), N);
1728 }
1729
1730 #[cfg(feature = "xml")]
1735 #[test]
1736 fn parse_xml_agrees_with_parse_xml_string() {
1737 let one = doc("<div>hi</div>");
1738 let two = doc("<i/><i/>");
1739 for src in ["", " ", one.as_str(), two.as_str()] {
1740 let via_xml = parse_xml(src).expect("valid");
1741 let via_string = parse_xml_string(src).expect("valid");
1742 assert_eq!(
1743 via_xml.root.as_ref(),
1744 via_string.as_slice(),
1745 "parse_xml must be a thin wrapper over parse_xml_string for {src:?}"
1746 );
1747 }
1748 assert!(parse_xml("<div>").is_err());
1749 }
1750
1751 #[cfg(not(feature = "xml"))]
1752 #[test]
1753 fn parse_xml_without_the_xml_feature_reports_no_parser() {
1754 for s in ["", " ", "<div/>", "garbage"] {
1755 assert!(matches!(parse_xml(s), Err(XmlError::NoParserAvailable)));
1756 }
1757 }
1758
1759 #[test]
1764 fn parse_xml_to_fast_dom_accepts_empty_and_whitespace_only_input() {
1765 for s in ["", " ", " ", "\t\n", "\u{FEFF}", "\u{FEFF} \n "] {
1766 let dom = parse_xml_to_fast_dom(s)
1767 .unwrap_or_else(|e| panic!("{s:?} should yield an empty arena, got {e}"));
1768 assert!(nodes(&dom).is_empty(), "{s:?} produced {} nodes", nodes(&dom).len());
1769 assert_eq!(dom.node_hierarchy.as_ref().len(), nodes(&dom).len());
1770 }
1771 }
1772
1773 #[test]
1774 fn parse_xml_to_fast_dom_builds_the_expected_arena() {
1775 let dom = parse_xml_to_fast_dom(&doc("<div>hi</div>")).expect("valid document");
1776 let n = nodes(&dom);
1777 assert_eq!(n.len(), 4, "html + body + div + text");
1778 assert_eq!(
1779 dom.node_hierarchy.as_ref().len(),
1780 n.len(),
1781 "hierarchy and node_data arenas must stay parallel"
1782 );
1783 assert!(matches!(n[0].get_node_type(), NodeType::Html));
1784 assert!(matches!(n[1].get_node_type(), NodeType::Body));
1785 assert!(matches!(n[2].get_node_type(), NodeType::Div));
1786 assert_eq!(text_of(&n[3]).as_deref(), Some("hi"));
1787 }
1788
1789 #[test]
1790 fn parse_xml_to_fast_dom_lowercases_tag_names() {
1791 let dom = parse_xml_to_fast_dom("<HTML><BODY><DiV/></BODY></HTML>").expect("valid");
1792 let n = nodes(&dom);
1793 assert_eq!(n.len(), 3);
1794 assert!(matches!(n[0].get_node_type(), NodeType::Html));
1795 assert!(matches!(n[1].get_node_type(), NodeType::Body));
1796 assert!(matches!(n[2].get_node_type(), NodeType::Div));
1797 }
1798
1799 #[test]
1800 fn parse_xml_to_fast_dom_skips_head_but_collects_style_css() {
1801 let src = "<html><head><title>T</title>\
1802 <style>div { width: 10px; }</style></head>\
1803 <body>x</body></html>";
1804 let (dom, css) = parse_xml_to_fast_dom_with_css(src).expect("valid document");
1805 let n = nodes(&dom);
1806
1807 assert_eq!(n.len(), 3, "html + body + text; <head> subtree is dropped");
1808 assert!(
1809 !n.iter()
1810 .any(|nd| matches!(nd.get_node_type(), NodeType::Head | NodeType::Title)),
1811 "no <head>/<title> node may reach the arena"
1812 );
1813 assert_eq!(text_of(&n[2]).as_deref(), Some("x"));
1814 assert_eq!(css.len(), 1, "the <style> body must still be collected");
1815 assert!(!css[0].rules.as_ref().is_empty(), "the CSS must have parsed");
1816 }
1817
1818 #[test]
1819 fn parse_xml_to_fast_dom_splits_ids_and_classes_on_whitespace() {
1820 let dom = parse_xml_to_fast_dom(&doc(r#"<div id="a b" class="c d
1821 e"></div>"#))
1822 .expect("valid document");
1823 let div = &nodes(&dom)[2];
1824
1825 assert!(div.has_id("a") && div.has_id("b"));
1826 assert!(div.has_class("c") && div.has_class("d") && div.has_class("e"));
1827 assert!(!div.has_id("a b"), "the raw joined value must not survive");
1828 assert_eq!(div.get_ids_and_classes().as_ref().len(), 5);
1829 }
1830
1831 fn tab_index_with(attrs: &str) -> Option<TabIndex> {
1833 let dom = parse_xml_to_fast_dom(&doc(&format!("<div {attrs}></div>")))
1834 .unwrap_or_else(|e| panic!("{attrs:?} should parse, got {e}"));
1835 nodes(&dom)[2].get_tab_index()
1836 }
1837
1838 #[test]
1839 fn parse_xml_to_fast_dom_maps_tabindex_boundaries() {
1840 assert_eq!(tab_index_with(r#"tabindex="0""#), Some(TabIndex::Auto));
1841 assert_eq!(tab_index_with(r#"tabindex="-0""#), Some(TabIndex::Auto));
1842 assert_eq!(
1843 tab_index_with(r#"tabindex="1""#),
1844 Some(TabIndex::OverrideInParent(1))
1845 );
1846 assert_eq!(
1847 tab_index_with(r#"tabindex="+3""#),
1848 Some(TabIndex::OverrideInParent(3)),
1849 "isize::from_str accepts a leading '+'"
1850 );
1851 assert_eq!(
1852 tab_index_with(r#"tabindex="-1""#),
1853 Some(TabIndex::NoKeyboardFocus)
1854 );
1855 assert_eq!(
1856 tab_index_with(r#"tabindex="-9223372036854775808""#),
1857 Some(TabIndex::NoKeyboardFocus),
1858 "i64::MIN is still just 'negative'"
1859 );
1860
1861 const MAX_EXACT: u32 = (1 << 28) - 1;
1863 assert_eq!(
1864 tab_index_with(&format!(r#"tabindex="{MAX_EXACT}""#)),
1865 Some(TabIndex::OverrideInParent(MAX_EXACT))
1866 );
1867 assert_eq!(
1872 tab_index_with(r#"tabindex="268435456""#),
1873 Some(TabIndex::OverrideInParent(0)),
1874 "1 << 28 truncates to 0"
1875 );
1876 assert_eq!(
1877 tab_index_with(r#"tabindex="9223372036854775807""#),
1878 Some(TabIndex::OverrideInParent(MAX_EXACT)),
1879 "i64::MAX -> u32::MAX -> 28-bit mask"
1880 );
1881 }
1882
1883 #[test]
1884 fn parse_xml_to_fast_dom_ignores_unparseable_tabindex() {
1885 let baseline = tab_index_with("");
1886 for junk in [
1887 r#"tabindex="""#,
1888 r#"tabindex="NaN""#,
1889 r#"tabindex="inf""#,
1890 r#"tabindex="-inf""#,
1891 r#"tabindex="1.0""#,
1892 r#"tabindex="1e5""#,
1893 r#"tabindex=" 3 ""#,
1894 r#"tabindex="0x10""#,
1895 r#"tabindex="99999999999999999999999999""#,
1896 r#"tabindex="-99999999999999999999999999""#,
1897 r#"tabindex="🙂""#,
1898 ] {
1899 assert_eq!(
1900 tab_index_with(junk),
1901 baseline,
1902 "{junk} must leave the tab index untouched"
1903 );
1904 }
1905 }
1906
1907 #[test]
1908 fn parse_xml_to_fast_dom_parses_bool_attributes_case_sensitively() {
1909 assert_eq!(
1910 tab_index_with(r#"focusable="true""#),
1911 Some(TabIndex::Auto)
1912 );
1913 assert_eq!(
1914 tab_index_with(r#"focusable="false""#),
1915 Some(TabIndex::NoKeyboardFocus)
1916 );
1917 for junk in [r#"focusable="TRUE""#, r#"focusable="1""#, r#"focusable="yes""#] {
1918 assert_eq!(
1919 tab_index_with(junk),
1920 tab_index_with(""),
1921 "{junk} is not a bool literal and must be ignored"
1922 );
1923 }
1924
1925 let editable = |v: &str| {
1926 let dom = parse_xml_to_fast_dom(&doc(&format!(r#"<div contenteditable="{v}"></div>"#)))
1927 .expect("valid");
1928 nodes(&dom)[2].is_contenteditable()
1929 };
1930 assert!(editable("true"));
1931 assert!(!editable("false"));
1932 assert!(!editable("TRUE"));
1933 assert!(!editable(""));
1934 assert!(!editable("1"));
1935 }
1936
1937 #[test]
1938 fn parse_xml_to_fast_dom_survives_malformed_style_attributes() {
1939 let big = "a:b;".repeat(2_000);
1940 for style in [
1941 "",
1942 ";;;;",
1943 "::::",
1944 ":",
1945 "width",
1946 "width:",
1947 ":10px",
1948 "a:b:c",
1949 ";:;:;:",
1950 "width:10px",
1951 "width:10px;;;height:;;",
1952 "width:not-a-length",
1953 "🙂:🙂",
1954 big.as_str(),
1955 ] {
1956 let dom = parse_xml_to_fast_dom(&doc(&format!(r#"<div style="{style}"></div>"#)))
1957 .unwrap_or_else(|e| panic!("style={style:?} should parse, got {e}"));
1958 assert_eq!(
1959 nodes(&dom).len(),
1960 3,
1961 "style={style:?} must not change the node count"
1962 );
1963 }
1964 }
1965
1966 #[test]
1967 fn parse_xml_to_fast_dom_survives_unbalanced_tags() {
1968 let dom = parse_xml_to_fast_dom("<html><head><title>").expect("lenient parse");
1973 assert_eq!(nodes(&dom).len(), 1, "only <html> survives");
1974 assert!(matches!(nodes(&dom)[0].get_node_type(), NodeType::Html));
1975
1976 for src in [
1977 "</div>",
1978 "</div></div></div>",
1979 "<a></a></a>",
1980 "<html><body></body></body></html>",
1981 "<html><head><head><head>",
1982 "<html><body><div></span></div></body></html>",
1983 ] {
1984 let a = parse_xml_to_fast_dom(src);
1985 let b = parse_xml_to_fast_dom(src);
1986 assert_eq!(a.is_ok(), b.is_ok(), "{src:?} is non-deterministic");
1987 if let (Ok(a), Ok(b)) = (&a, &b) {
1988 assert_eq!(nodes(a).len(), nodes(b).len(), "{src:?} node count drifted");
1989 }
1990 }
1991 }
1992
1993 #[test]
1994 fn parse_xml_to_fast_dom_is_deterministic_on_garbage() {
1995 for g in GARBAGE {
1996 let a = parse_xml_to_fast_dom(g);
1997 let b = parse_xml_to_fast_dom(g);
1998 assert_eq!(a.is_ok(), b.is_ok(), "{g:?} parsed non-deterministically");
1999 if let (Ok(a), Ok(b)) = (&a, &b) {
2000 assert_eq!(nodes(a).len(), nodes(b).len(), "{g:?} node count drifted");
2001 }
2002 }
2003 }
2004
2005 #[test]
2006 fn parse_xml_to_fast_dom_handles_ten_thousand_nested_elements() {
2007 const DEPTH: usize = 10_000;
2010 let mut src = String::with_capacity(DEPTH * 12 + 32);
2011 src.push_str("<html><body>");
2012 for _ in 0..DEPTH {
2013 src.push_str("<div>");
2014 }
2015 for _ in 0..DEPTH {
2016 src.push_str("</div>");
2017 }
2018 src.push_str("</body></html>");
2019
2020 let dom = parse_xml_to_fast_dom(&src).expect("balanced nesting is valid");
2021 assert_eq!(nodes(&dom).len(), DEPTH + 2);
2022 }
2023
2024 #[test]
2025 fn parse_xml_to_fast_dom_handles_a_one_million_char_document() {
2026 let payload = "x".repeat(1_000_000);
2027 let dom = parse_xml_to_fast_dom(&doc(&payload)).expect("long text is valid");
2028 let n = nodes(&dom);
2029 assert_eq!(n.len(), 3, "html + body + one text node");
2030 assert_eq!(text_of(&n[2]).map(|s| s.len()), Some(1_000_000));
2031 }
2032
2033 #[test]
2034 fn parse_xml_to_fast_dom_strips_bom_declaration_doctype_and_comments() {
2035 let expected = nodes(&parse_xml_to_fast_dom(&doc("<div/>")).expect("baseline")).len();
2036 for prefix in [
2037 "\u{FEFF}",
2038 "<?xml version=\"1.0\" encoding=\"utf-8\"?>",
2039 "<!DOCTYPE html>",
2040 "<!doctype HTML>",
2041 "<!DoCtYpE html SYSTEM \"about:legacy-compat\">",
2042 "<!-- leading comment -->",
2043 ] {
2044 let src = format!("{prefix}{}", doc("<div/>"));
2045 let dom = parse_xml_to_fast_dom(&src)
2046 .unwrap_or_else(|e| panic!("{prefix:?} should be stripped, got {e}"));
2047 assert_eq!(nodes(&dom).len(), expected, "{prefix:?} changed the arena");
2048 }
2049 }
2050
2051 #[test]
2052 fn parse_xml_to_fast_dom_preserves_unicode_text() {
2053 for payload in [
2054 "日本語",
2055 "🙂🙂🙂🙂",
2056 "e\u{301}\u{302}\u{303}",
2057 "\u{200B}\u{FEFF}mid-string BOM",
2058 "ﷺ",
2059 ] {
2060 let dom = parse_xml_to_fast_dom(&doc(payload))
2061 .unwrap_or_else(|e| panic!("{payload:?} should parse, got {e}"));
2062 let n = nodes(&dom);
2063 assert_eq!(n.len(), 3, "{payload:?}");
2064 assert_eq!(text_of(&n[2]).as_deref(), Some(payload));
2065 }
2066 }
2067
2068 #[test]
2069 fn parse_xml_to_fast_dom_treats_numeric_looking_documents_as_text() {
2070 for payload in [
2073 "0",
2074 "-0",
2075 "9223372036854775807",
2076 "-9223372036854775808",
2077 "18446744073709551616",
2078 "1e309",
2079 "-1e-309",
2080 "NaN",
2081 "inf",
2082 "-inf",
2083 ] {
2084 let dom = parse_xml_to_fast_dom(&doc(payload))
2085 .unwrap_or_else(|e| panic!("{payload:?} should parse, got {e}"));
2086 assert_eq!(text_of(&nodes(&dom)[2]).as_deref(), Some(payload));
2087 }
2088 }
2089
2090 #[test]
2095 fn parse_xml_to_styled_dom_accepts_empty_and_whitespace_only_input() {
2096 for s in ["", " ", "\t\n", "\u{FEFF}"] {
2097 let styled = parse_xml_to_styled_dom(s)
2098 .unwrap_or_else(|e| panic!("{s:?} should cascade cleanly, got {e}"));
2099 assert!(styled.node_data.as_ref().is_empty(), "{s:?}");
2100 }
2101 }
2102
2103 #[test]
2104 fn parse_xml_to_styled_dom_keeps_the_fast_dom_node_count() {
2105 for src in [
2106 doc("<div>hi</div>"),
2107 doc("<div><span>a</span><span>b</span></div>"),
2108 "<html><head><style>div { width: 10px; }</style></head><body><div/></body></html>"
2109 .to_string(),
2110 ] {
2111 let fast = parse_xml_to_fast_dom(&src).expect("fast path");
2112 let styled = parse_xml_to_styled_dom(&src).expect("styled path");
2113 assert_eq!(
2114 styled.node_data.as_ref().len(),
2115 nodes(&fast).len(),
2116 "the cascade must not add or drop nodes for {src:?}"
2117 );
2118 assert_eq!(
2119 styled.node_hierarchy.as_ref().len(),
2120 styled.node_data.as_ref().len()
2121 );
2122 }
2123 }
2124
2125 #[test]
2126 fn parse_xml_to_styled_dom_is_deterministic_on_garbage() {
2127 for g in GARBAGE {
2128 let a = parse_xml_to_styled_dom(g);
2129 let b = parse_xml_to_styled_dom(g);
2130 assert_eq!(a.is_ok(), b.is_ok(), "{g:?} cascaded non-deterministically");
2131 }
2132 }
2133
2134 #[test]
2139 fn dom_from_parsed_xml_reports_errors_instead_of_panicking() {
2140 for root in [
2143 Vec::new(),
2144 vec![XmlNodeChild::Text("bare text".into())],
2145 vec![XmlNodeChild::Element(XmlNode::create("div"))],
2146 vec![XmlNodeChild::Element(XmlNode::create("html"))],
2147 ] {
2148 let dom = dom_from_parsed_xml(Xml { root: root.into() });
2149 assert!(
2150 matches!(dom.root.get_node_type(), NodeType::Body),
2151 "the error Dom is rendered as a <body> with a label"
2152 );
2153 assert_eq!(dom.children.as_ref().len(), 1);
2154 }
2155 }
2156
2157 #[test]
2158 fn dom_from_parsed_xml_builds_a_dom_for_a_minimal_document() {
2159 let body = XmlNode::create("body")
2160 .with_children(vec![XmlNodeChild::Element(XmlNode::create("div"))]);
2161 let html = XmlNode::create("html").with_children(vec![XmlNodeChild::Element(body)]);
2162 let dom = dom_from_parsed_xml(Xml {
2163 root: vec![XmlNodeChild::Element(html)].into(),
2164 });
2165
2166 assert!(matches!(dom.root.get_node_type(), NodeType::Html));
2167 assert_eq!(dom.children.as_ref().len(), 1, "the <body> subtree");
2168 }
2169
2170 #[test]
2171 fn dom_from_parsed_xml_caps_recursion_on_deeply_nested_input() {
2172 const DEPTH: usize = 550;
2175 let mut node = XmlNode::create("div");
2176 for _ in 0..DEPTH {
2177 node = XmlNode::create("div").with_children(vec![XmlNodeChild::Element(node)]);
2178 }
2179 let body = XmlNode::create("body").with_children(vec![XmlNodeChild::Element(node)]);
2180 let html = XmlNode::create("html").with_children(vec![XmlNodeChild::Element(body)]);
2181
2182 let dom = dom_from_parsed_xml(Xml {
2183 root: vec![XmlNodeChild::Element(html)].into(),
2184 });
2185 assert!(matches!(dom.root.get_node_type(), NodeType::Html));
2186 }
2187
2188 #[cfg(feature = "xml")]
2193 #[test]
2194 fn domxml_from_str_never_fails() {
2195 let map = ComponentMap::with_builtin();
2196 let mut cases: Vec<String> = GARBAGE.iter().map(|s| (*s).to_string()).collect();
2197 cases.push(String::new());
2198 cases.push(" ".to_string());
2199 cases.push("<svg".to_string());
2200 cases.push("<?xml".to_string());
2201 cases.push(doc("<div>hi</div>"));
2202
2203 for src in cases {
2204 let dom_xml = domxml_from_str(&src, &map);
2205 assert!(
2206 !dom_xml.parsed_dom.node_data.as_ref().is_empty(),
2207 "{src:?} produced an empty StyledDom; errors must render as a label"
2208 );
2209 }
2210 }
2211
2212 #[cfg(all(feature = "std", feature = "xml"))]
2213 #[test]
2214 fn domxml_from_file_renders_io_errors_as_a_dom() {
2215 let map = ComponentMap::with_builtin();
2216 for path in [
2217 "/nonexistent-azul-autotest-dir/definitely-not-here.xml",
2218 "",
2219 "/",
2220 "/proc/self/nonexistent-🙂",
2221 ] {
2222 let dom_xml = domxml_from_file(path, &map);
2223 assert!(
2224 !dom_xml.parsed_dom.node_data.as_ref().is_empty(),
2225 "{path:?} must render the io::Error as a label, not fail"
2226 );
2227 }
2228 }
2229
2230 #[cfg(feature = "xml")]
2231 #[test]
2232 fn dom_xml_ext_matches_domxml_from_str() {
2233 let map = ComponentMap::with_builtin();
2234 let valid = doc("<div>hi</div>");
2235 for src in ["", "<svg", valid.as_str()] {
2236 let via_ext = <Dom as DomXmlExt>::from_xml_string(src);
2237 let via_fn = domxml_from_str(src, &map).parsed_dom;
2238 assert_eq!(
2239 via_ext.node_data.as_ref().len(),
2240 via_fn.node_data.as_ref().len(),
2241 "the extension trait must be a pure delegation for {src:?}"
2242 );
2243 }
2244 }
2245
2246 #[test]
2251 fn peak_rss_bytes_never_panics_and_never_goes_backwards() {
2252 let a = peak_rss_bytes();
2253 let _ballast = "x".repeat(4 * 1024 * 1024);
2254 let b = peak_rss_bytes();
2255
2256 #[cfg(all(unix, feature = "probe"))]
2257 assert!(
2258 b >= a,
2259 "ru_maxrss is a high-water mark and must never decrease ({a} -> {b})"
2260 );
2261 #[cfg(not(all(unix, feature = "probe")))]
2262 assert_eq!(
2263 (a, b),
2264 (0, 0),
2265 "without the probe feature the stub must be a constant 0"
2266 );
2267 }
2268
2269 #[cfg(feature = "xml")]
2274 #[test]
2275 fn translate_textpos_round_trips_boundary_values() {
2276 for (row, col) in [
2277 (0, 0),
2278 (1, 1),
2279 (0, u32::MAX),
2280 (u32::MAX, 0),
2281 (u32::MAX, u32::MAX),
2282 ] {
2283 let expected = XmlTextPos { row, col };
2284 assert_eq!(
2285 translate_xmlparser_textpos(xmlparser::TextPos::new(row, col)),
2286 expected
2287 );
2288 assert_eq!(
2289 translate_roxml_textpos(roxmltree::TextPos::new(row, col)),
2290 expected
2291 );
2292 }
2293 }
2294
2295 #[cfg(feature = "xml")]
2296 #[test]
2297 fn translate_roxmltree_expandedname_preserves_name_and_namespace() {
2298 let plain: roxmltree::ExpandedName<'_, '_> = "rect".into();
2299 let out = translate_roxmltree_expandedname(plain);
2300 assert_eq!(out.local_name.as_str(), "rect");
2301 assert!(out.namespace.as_ref().is_none());
2302
2303 let ns: roxmltree::ExpandedName<'_, '_> = ("http://www.w3.org/2000/svg", "rect").into();
2304 let out = translate_roxmltree_expandedname(ns);
2305 assert_eq!(out.local_name.as_str(), "rect");
2306 assert_eq!(
2307 out.namespace.as_ref().map(AzString::as_str),
2308 Some("http://www.w3.org/2000/svg")
2309 );
2310
2311 for name in ["", " ", "日本語-🙂", "a:b"] {
2313 let e: roxmltree::ExpandedName<'_, '_> = name.into();
2314 assert_eq!(translate_roxmltree_expandedname(e).local_name.as_str(), name);
2315 }
2316 let empty_ns: roxmltree::ExpandedName<'_, '_> = ("", "x").into();
2317 assert_eq!(
2318 translate_roxmltree_expandedname(empty_ns)
2319 .namespace
2320 .as_ref()
2321 .map(AzString::as_str),
2322 Some(""),
2323 "an empty namespace URI is Some(\"\"), not None"
2324 );
2325 }
2326
2327 #[cfg(feature = "xml")]
2328 #[test]
2329 fn translate_roxmltree_attribute_preserves_name_and_namespace() {
2330 let rdoc = roxmltree::Document::parse(r#"<e xmlns:x="urn:x" x:a="1" b="2"/>"#)
2331 .expect("valid XML");
2332 let attrs: Vec<XmlQualifiedName> = rdoc
2333 .root_element()
2334 .attributes()
2335 .map(translate_roxmltree_attribute)
2336 .collect();
2337
2338 assert_eq!(attrs.len(), 2, "xmlns declarations are not attributes");
2339 let a = attrs
2340 .iter()
2341 .find(|q| q.local_name.as_str() == "a")
2342 .expect("x:a");
2343 assert_eq!(a.namespace.as_ref().map(AzString::as_str), Some("urn:x"));
2344 let b = attrs
2345 .iter()
2346 .find(|q| q.local_name.as_str() == "b")
2347 .expect("b");
2348 assert!(
2349 b.namespace.as_ref().is_none(),
2350 "an unprefixed attribute has no namespace"
2351 );
2352 }
2353
2354 #[cfg(feature = "xml")]
2355 #[test]
2356 fn translate_xmlparser_streamerror_maps_every_variant() {
2357 use xmlparser::StreamError as Se;
2358
2359 let p = xmlparser::TextPos::new(3, 7);
2360 let x = XmlTextPos { row: 3, col: 7 };
2361
2362 assert_eq!(
2363 translate_xmlparser_streamerror(Se::UnexpectedEndOfStream),
2364 XmlStreamError::UnexpectedEndOfStream
2365 );
2366 assert_eq!(
2367 translate_xmlparser_streamerror(Se::InvalidName),
2368 XmlStreamError::InvalidName
2369 );
2370 assert_eq!(
2371 translate_xmlparser_streamerror(Se::InvalidReference),
2372 XmlStreamError::InvalidReference
2373 );
2374 assert_eq!(
2375 translate_xmlparser_streamerror(Se::InvalidExternalID),
2376 XmlStreamError::InvalidExternalID
2377 );
2378 assert_eq!(
2379 translate_xmlparser_streamerror(Se::InvalidCommentData),
2380 XmlStreamError::InvalidCommentData
2381 );
2382 assert_eq!(
2383 translate_xmlparser_streamerror(Se::InvalidCommentEnd),
2384 XmlStreamError::InvalidCommentEnd
2385 );
2386 assert_eq!(
2387 translate_xmlparser_streamerror(Se::InvalidCharacterData),
2388 XmlStreamError::InvalidCharacterData
2389 );
2390 assert_eq!(
2392 translate_xmlparser_streamerror(Se::NonXmlChar('\u{1F600}', p)),
2393 XmlStreamError::NonXmlChar(NonXmlCharError {
2394 ch: 0x1F600,
2395 pos: x
2396 })
2397 );
2398 assert_eq!(
2399 translate_xmlparser_streamerror(Se::InvalidQuote(b'`', p)),
2400 XmlStreamError::InvalidQuote(InvalidQuoteError { got: b'`', pos: x })
2401 );
2402 assert_eq!(
2403 translate_xmlparser_streamerror(Se::InvalidSpace(b'\t', p)),
2404 XmlStreamError::InvalidSpace(InvalidSpaceError { got: b'\t', pos: x })
2405 );
2406 assert_eq!(
2407 translate_xmlparser_streamerror(Se::InvalidString("?>", p)),
2408 XmlStreamError::InvalidString(InvalidStringError {
2409 got: "?>".into(),
2410 pos: x
2411 })
2412 );
2413 assert_eq!(
2420 translate_xmlparser_streamerror(Se::InvalidChar(b'a', b'b', p)),
2421 XmlStreamError::InvalidChar(InvalidCharError {
2422 expected: b'a',
2423 got: b'b',
2424 pos: x
2425 })
2426 );
2427 assert_eq!(
2428 translate_xmlparser_streamerror(Se::InvalidCharMultiple(b'a', &b"xy"[..], p)),
2429 XmlStreamError::InvalidCharMultiple(InvalidCharMultipleError {
2430 expected: b'a',
2431 got: vec![b'x', b'y'].into(),
2432 pos: x
2433 })
2434 );
2435 }
2436
2437 #[cfg(feature = "xml")]
2438 #[test]
2439 fn translate_xmlparser_error_maps_every_variant() {
2440 use xmlparser::{Error as Xe, StreamError as Se};
2441
2442 let p = xmlparser::TextPos::new(9, 4);
2443 let x = XmlTextPos { row: 9, col: 4 };
2444 let te = XmlTextError {
2445 stream_error: XmlStreamError::InvalidName,
2446 pos: x,
2447 };
2448
2449 assert_eq!(
2450 translate_xmlparser_error(Xe::InvalidDeclaration(Se::InvalidName, p)),
2451 XmlParseError::InvalidDeclaration(te.clone())
2452 );
2453 assert_eq!(
2454 translate_xmlparser_error(Xe::InvalidComment(Se::InvalidName, p)),
2455 XmlParseError::InvalidComment(te.clone())
2456 );
2457 assert_eq!(
2458 translate_xmlparser_error(Xe::InvalidPI(Se::InvalidName, p)),
2459 XmlParseError::InvalidPI(te.clone())
2460 );
2461 assert_eq!(
2462 translate_xmlparser_error(Xe::InvalidDoctype(Se::InvalidName, p)),
2463 XmlParseError::InvalidDoctype(te.clone())
2464 );
2465 assert_eq!(
2466 translate_xmlparser_error(Xe::InvalidEntity(Se::InvalidName, p)),
2467 XmlParseError::InvalidEntity(te.clone())
2468 );
2469 assert_eq!(
2470 translate_xmlparser_error(Xe::InvalidElement(Se::InvalidName, p)),
2471 XmlParseError::InvalidElement(te.clone())
2472 );
2473 assert_eq!(
2474 translate_xmlparser_error(Xe::InvalidAttribute(Se::InvalidName, p)),
2475 XmlParseError::InvalidAttribute(te.clone())
2476 );
2477 assert_eq!(
2478 translate_xmlparser_error(Xe::InvalidCdata(Se::InvalidName, p)),
2479 XmlParseError::InvalidCdata(te.clone())
2480 );
2481 assert_eq!(
2482 translate_xmlparser_error(Xe::InvalidCharData(Se::InvalidName, p)),
2483 XmlParseError::InvalidCharData(te)
2484 );
2485 assert_eq!(
2486 translate_xmlparser_error(Xe::UnknownToken(p)),
2487 XmlParseError::UnknownToken(x)
2488 );
2489 }
2490
2491 #[cfg(feature = "xml")]
2492 #[test]
2493 fn translate_roxmltree_error_maps_every_variant() {
2494 use roxmltree::Error as Re;
2495
2496 let p = roxmltree::TextPos::new(2, 5);
2497 let x = XmlTextPos { row: 2, col: 5 };
2498
2499 assert_eq!(
2500 translate_roxmltree_error(Re::InvalidXmlPrefixUri(p)),
2501 XmlError::InvalidXmlPrefixUri(x)
2502 );
2503 assert_eq!(
2504 translate_roxmltree_error(Re::UnexpectedXmlUri(p)),
2505 XmlError::UnexpectedXmlUri(x)
2506 );
2507 assert_eq!(
2508 translate_roxmltree_error(Re::UnexpectedXmlnsUri(p)),
2509 XmlError::UnexpectedXmlnsUri(x)
2510 );
2511 assert_eq!(
2512 translate_roxmltree_error(Re::InvalidElementNamePrefix(p)),
2513 XmlError::InvalidElementNamePrefix(x)
2514 );
2515 assert_eq!(
2516 translate_roxmltree_error(Re::DuplicatedNamespace(String::from("ns"), p)),
2517 XmlError::DuplicatedNamespace(DuplicatedNamespaceError {
2518 ns: "ns".into(),
2519 pos: x
2520 })
2521 );
2522 assert_eq!(
2523 translate_roxmltree_error(Re::UnknownNamespace(String::from("ns"), p)),
2524 XmlError::UnknownNamespace(UnknownNamespaceError {
2525 ns: "ns".into(),
2526 pos: x
2527 })
2528 );
2529 assert_eq!(
2530 translate_roxmltree_error(Re::UnexpectedCloseTag(
2531 String::from("a"),
2532 String::from("b"),
2533 p
2534 )),
2535 XmlError::UnexpectedCloseTag(UnexpectedCloseTagError {
2536 expected: "a".into(),
2537 actual: "b".into(),
2538 pos: x
2539 })
2540 );
2541 assert_eq!(
2542 translate_roxmltree_error(Re::UnexpectedEntityCloseTag(p)),
2543 XmlError::UnexpectedEntityCloseTag(x)
2544 );
2545 assert_eq!(
2546 translate_roxmltree_error(Re::UnknownEntityReference(String::from("e"), p)),
2547 XmlError::UnknownEntityReference(UnknownEntityReferenceError {
2548 entity: "e".into(),
2549 pos: x
2550 })
2551 );
2552 assert_eq!(
2553 translate_roxmltree_error(Re::MalformedEntityReference(p)),
2554 XmlError::MalformedEntityReference(x)
2555 );
2556 assert_eq!(
2557 translate_roxmltree_error(Re::EntityReferenceLoop(p)),
2558 XmlError::EntityReferenceLoop(x)
2559 );
2560 assert_eq!(
2561 translate_roxmltree_error(Re::InvalidAttributeValue(p)),
2562 XmlError::InvalidAttributeValue(x)
2563 );
2564 assert_eq!(
2565 translate_roxmltree_error(Re::DuplicatedAttribute(String::from("a"), p)),
2566 XmlError::DuplicatedAttribute(DuplicatedAttributeError {
2567 attribute: "a".into(),
2568 pos: x
2569 })
2570 );
2571 assert_eq!(
2572 translate_roxmltree_error(Re::NoRootNode),
2573 XmlError::NoRootNode
2574 );
2575 assert_eq!(
2576 translate_roxmltree_error(Re::DtdDetected),
2577 XmlError::DtdDetected
2578 );
2579 assert_eq!(
2580 translate_roxmltree_error(Re::UnclosedRootNode),
2581 XmlError::UnclosedRootNode
2582 );
2583 assert_eq!(
2584 translate_roxmltree_error(Re::UnexpectedDeclaration(p)),
2585 XmlError::UnexpectedDeclaration(x)
2586 );
2587 assert_eq!(
2588 translate_roxmltree_error(Re::NodesLimitReached),
2589 XmlError::NodesLimitReached
2590 );
2591 assert_eq!(
2592 translate_roxmltree_error(Re::AttributesLimitReached),
2593 XmlError::AttributesLimitReached
2594 );
2595 assert_eq!(
2596 translate_roxmltree_error(Re::NamespacesLimitReached),
2597 XmlError::NamespacesLimitReached
2598 );
2599 assert_eq!(
2600 translate_roxmltree_error(Re::InvalidName(p)),
2601 XmlError::InvalidName(x)
2602 );
2603 assert_eq!(
2604 translate_roxmltree_error(Re::NonXmlChar('\u{0}', p)),
2605 XmlError::NonXmlChar(x)
2606 );
2607 assert_eq!(
2608 translate_roxmltree_error(Re::InvalidChar(b'a', b'b', p)),
2609 XmlError::InvalidChar(x)
2610 );
2611 assert_eq!(
2612 translate_roxmltree_error(Re::InvalidChar2("ab", b'c', p)),
2613 XmlError::InvalidChar2(x)
2614 );
2615 assert_eq!(
2616 translate_roxmltree_error(Re::InvalidString("s", p)),
2617 XmlError::InvalidString(x)
2618 );
2619 assert_eq!(
2620 translate_roxmltree_error(Re::InvalidExternalID(p)),
2621 XmlError::InvalidExternalID(x)
2622 );
2623 assert_eq!(
2624 translate_roxmltree_error(Re::InvalidComment(p)),
2625 XmlError::InvalidComment(x)
2626 );
2627 assert_eq!(
2628 translate_roxmltree_error(Re::InvalidCharacterData(p)),
2629 XmlError::InvalidCharacterData(x)
2630 );
2631 assert_eq!(
2632 translate_roxmltree_error(Re::UnknownToken(p)),
2633 XmlError::UnknownToken(x)
2634 );
2635 assert_eq!(
2636 translate_roxmltree_error(Re::UnexpectedEndOfStream),
2637 XmlError::UnexpectedEndOfStream
2638 );
2639 assert_eq!(
2641 translate_roxmltree_error(Re::EntityResolver(p, String::from("e"))),
2642 XmlError::UnknownEntityReference(UnknownEntityReferenceError {
2643 entity: "e".into(),
2644 pos: x
2645 })
2646 );
2647 }
2648}