1use std::fmt::Write;
20
21use crate::ast::OutputSpec;
22use crate::error::XsltError;
23use crate::result_tree::{ResultNode, ResultTree};
24
25fn effective_method(tree: &ResultTree) -> &str {
31 if let Some(m) = tree.output.method.as_deref() { return m; }
32 if let Some(ResultNode::Element { name, .. }) = tree.children.iter()
33 .find(|n| matches!(n, ResultNode::Element { .. }))
34 {
35 if name.local.eq_ignore_ascii_case("html") && name.uri.is_empty() {
36 return "html";
37 }
38 }
39 "xml"
40}
41
42impl ResultTree {
43 pub fn to_string(&self) -> Result<String, XsltError> {
46 match effective_method(self) {
47 "html" => Ok(serialize_html(self)),
48 "text" => Ok(serialize_text(self)),
49 _ => Ok(serialize_xml(self)),
50 }
51 }
52
53 pub fn write_to(&self, w: &mut dyn std::io::Write) -> Result<(), XsltError> {
55 let s = self.to_string()?;
56 w.write_all(s.as_bytes())
57 .map_err(|e| XsltError::InvalidStylesheet(format!("write failed: {e}")))
58 }
59}
60
61pub fn serialize_xml(tree: &ResultTree) -> String {
64 let mut out = String::new();
65 if should_emit_xml_decl(&tree.output) {
66 let _ = write!(out, r#"<?xml version="{}" encoding="{}""#,
67 tree.output.version.as_deref().unwrap_or("1.0"),
68 tree.output.encoding.as_deref().unwrap_or("UTF-8"),
69 );
70 if let Some(s) = tree.output.standalone {
71 let _ = write!(out, r#" standalone="{}""#, if s { "yes" } else { "no" });
72 }
73 out.push_str("?>\n");
74 }
75 if let Some(dt_sys) = tree.output.doctype_system.as_deref() {
76 if let Some(root) = first_element_name(&tree.children) {
77 if let Some(pubid) = tree.output.doctype_public.as_deref() {
78 let _ = writeln!(out, r#"<!DOCTYPE {root} PUBLIC "{pubid}" "{dt_sys}">"#);
79 } else {
80 let _ = writeln!(out, r#"<!DOCTYPE {root} SYSTEM "{dt_sys}">"#);
81 }
82 }
83 }
84 let format = tree.output.indent == Some(true);
89 for child in &tree.children {
90 serialize_xml_node(child, &mut out, &tree.output, "", &tree.character_map, format, 0);
91 }
92 if format && !out.ends_with('\n') {
93 out.push('\n');
94 }
95 out
96}
97
98fn push_indent(out: &mut String, level: usize) {
100 for _ in 0..level {
101 out.push_str(" ");
102 }
103}
104
105fn should_emit_xml_decl(output: &OutputSpec) -> bool {
106 !output.omit_xml_declaration.unwrap_or(false)
108}
109
110fn first_element_name(nodes: &[ResultNode]) -> Option<String> {
111 nodes.iter().find_map(|n| match n {
112 ResultNode::Element { name, .. } => Some(name.to_qname_string()),
113 _ => None,
114 })
115}
116
117fn serialize_xml_node(
124 node: &ResultNode,
125 out: &mut String,
126 opts: &OutputSpec,
127 parent_default_ns: &str,
128 cmap: &[(char, String)],
129 format: bool,
130 level: usize,
131) {
132 let xml_11 = opts.version.as_deref() == Some("1.1");
133 let enc_cap = encoding_capability(opts.encoding.as_deref());
134 match node {
135 ResultNode::Element { name, namespaces, attributes, children, .. } => {
136 let q = name.to_qname_string();
137 out.push('<');
138 out.push_str(&q);
139 let mut child_default_ns: &str = parent_default_ns;
145 for (prefix, uri) in namespaces {
146 match prefix {
147 Some(p) if p == "xml"
155 && uri == "http://www.w3.org/XML/1998/namespace" => {}
156 Some(p) => { let _ = write!(out, r#" xmlns:{p}="{}""#, escape_attr(uri, xml_11, enc_cap)); }
157 None => {
158 if uri != parent_default_ns {
164 let _ = write!(out, r#" xmlns="{}""#, escape_attr(uri, xml_11, enc_cap));
165 }
166 child_default_ns = uri.as_str();
167 }
168 }
169 }
170 for (aname, value) in attributes {
171 let _ = write!(out, r#" {}="{}""#,
172 aname.to_qname_string(),
173 escape_attr_with_map(value, xml_11, enc_cap, cmap));
174 }
175 if children.is_empty() {
176 out.push_str("/>");
177 return;
178 }
179 out.push('>');
180 let is_cdata = opts.cdata_section_elements.iter()
183 .any(|q| q.uri == name.uri && q.local == name.local);
184 let child_format = format
188 && !children.iter().any(|c| matches!(c, ResultNode::Text { .. }));
189 for c in children {
190 if child_format {
191 out.push('\n');
192 push_indent(out, level + 1);
193 }
194 if is_cdata {
195 if let ResultNode::Text { content, .. } = c {
196 out.push_str("<![CDATA[");
197 out.push_str(&content.replace("]]>", "]]]]><![CDATA[>"));
198 out.push_str("]]>");
199 continue;
200 }
201 }
202 serialize_xml_node(c, out, opts, child_default_ns, cmap, child_format, level + 1);
203 }
204 if child_format {
205 out.push('\n');
206 push_indent(out, level);
207 }
208 out.push_str("</");
209 out.push_str(&q);
210 out.push('>');
211 }
212 ResultNode::Text { content, dose } => {
213 if *dose {
214 out.push_str(content);
215 } else {
216 out.push_str(&escape_text_with_map(content, xml_11, enc_cap, cmap));
217 }
218 }
219 ResultNode::Comment(s) => {
220 let _ = write!(out, "<!--{}-->", s);
221 }
222 ResultNode::ProcessingInstruction { target, data } => {
223 if data.is_empty() {
224 let _ = write!(out, "<?{target}?>");
225 } else {
226 let _ = write!(out, "<?{target} {data}?>");
227 }
228 }
229 ResultNode::Attribute { .. } => {}
233 }
234}
235
236#[inline]
243fn xml_11_must_escape(c: char) -> bool {
244 matches!(c as u32,
245 0x01..=0x08 | 0x0B..=0x0C | 0x0E..=0x1F |
246 0x7F..=0x84 | 0x85 | 0x86..=0x9F |
247 0x2028
248 )
249}
250
251fn encoding_capability(enc: Option<&str>) -> Option<u32> {
258 let name = enc.unwrap_or("UTF-8").to_ascii_lowercase();
259 let norm: String = name.chars().filter(|c| !matches!(c, '-' | '_' | ' ')).collect();
260 match norm.as_str() {
261 "ascii" | "usascii" | "iso646us" => Some(0x7F),
263 "iso88591" | "latin1" | "l1" | "cp1252" | "windows1252" => Some(0xFF),
267 _ => None,
271 }
272}
273
274#[inline]
275fn must_ncr_escape(c: char, enc_cap: Option<u32>) -> bool {
276 matches!(enc_cap, Some(max) if c as u32 > max)
277}
278
279fn escape_text(s: &str, xml_11: bool, enc_cap: Option<u32>) -> String {
280 escape_text_with_map(s, xml_11, enc_cap, &[])
281}
282
283fn escape_attr(s: &str, xml_11: bool, enc_cap: Option<u32>) -> String {
284 escape_attr_with_map(s, xml_11, enc_cap, &[])
285}
286
287#[inline]
292fn cmap_lookup<'a>(c: char, cmap: &'a [(char, String)]) -> Option<&'a str> {
293 for (k, v) in cmap {
294 if *k == c { return Some(v.as_str()); }
295 }
296 None
297}
298
299fn escape_text_with_map(
300 s: &str,
301 xml_11: bool,
302 enc_cap: Option<u32>,
303 cmap: &[(char, String)],
304) -> String {
305 let mut out = String::with_capacity(s.len());
306 for c in s.chars() {
307 if let Some(replacement) = cmap_lookup(c, cmap) {
308 out.push_str(replacement);
309 continue;
310 }
311 match c {
312 '<' => out.push_str("<"),
313 '>' => out.push_str(">"),
314 '&' => out.push_str("&"),
315 '\r' => out.push_str("
"),
319 c if xml_11 && xml_11_must_escape(c) => {
320 let _ = write!(out, "&#{};", c as u32);
321 }
322 c if must_ncr_escape(c, enc_cap) => {
323 let _ = write!(out, "&#{};", c as u32);
324 }
325 _ => out.push(c),
326 }
327 }
328 out
329}
330
331fn escape_attr_with_map(
332 s: &str,
333 xml_11: bool,
334 enc_cap: Option<u32>,
335 cmap: &[(char, String)],
336) -> String {
337 let mut out = String::with_capacity(s.len());
338 for c in s.chars() {
339 if let Some(replacement) = cmap_lookup(c, cmap) {
340 out.push_str(replacement);
341 continue;
342 }
343 match c {
344 '<' => out.push_str("<"),
345 '>' => out.push_str(">"),
346 '&' => out.push_str("&"),
347 '"' => out.push_str("""),
348 '\n' => out.push_str(" "),
349 '\r' => out.push_str(" "),
350 '\t' => out.push_str("	"),
351 c if xml_11 && xml_11_must_escape(c) => {
352 let _ = write!(out, "&#{};", c as u32);
353 }
354 c if must_ncr_escape(c, enc_cap) => {
355 let _ = write!(out, "&#{};", c as u32);
356 }
357 _ => out.push(c),
358 }
359 }
360 out
361}
362
363const VOID_ELEMENTS: &[&str] = &[
369 "area", "base", "br", "col", "embed", "hr", "img", "input",
370 "link", "meta", "param", "source", "track", "wbr",
371];
372
373const RAW_TEXT_ELEMENTS: &[&str] = &["script", "style"];
376
377pub fn serialize_html(tree: &ResultTree) -> String {
378 let mut out = String::new();
379 if let Some(dt_sys) = tree.output.doctype_system.as_deref() {
380 if let Some(root) = first_element_name(&tree.children) {
381 if let Some(pubid) = tree.output.doctype_public.as_deref() {
382 let _ = writeln!(out, r#"<!DOCTYPE {root} PUBLIC "{pubid}" "{dt_sys}">"#);
383 } else {
384 let _ = writeln!(out, r#"<!DOCTYPE {root} SYSTEM "{dt_sys}">"#);
385 }
386 }
387 } else if let Some(pubid) = tree.output.doctype_public.as_deref() {
388 if let Some(root) = first_element_name(&tree.children) {
389 let _ = writeln!(out, r#"<!DOCTYPE {root} PUBLIC "{pubid}">"#);
390 }
391 }
392 let format = tree.output.indent == Some(true);
393 for c in &tree.children { serialize_html_node(c, &mut out, format, 0); }
394 if format && !out.ends_with('\n') {
395 out.push('\n');
396 }
397 out
398}
399
400fn serialize_html_node(node: &ResultNode, out: &mut String, format: bool, level: usize) {
401 match node {
402 ResultNode::Element { name, namespaces, attributes, children, .. } => {
403 let local_lc = name.local.to_lowercase();
404 let q = name.to_qname_string();
405 out.push('<');
406 out.push_str(&q);
407 for (prefix, uri) in namespaces {
408 match prefix {
409 Some(p) => { let _ = write!(out, r#" xmlns:{p}="{}""#, escape_attr(uri, false, None)); }
410 None => { let _ = write!(out, r#" xmlns="{}""#, escape_attr(uri, false, None)); }
411 }
412 }
413 for (aname, value) in attributes {
414 let _ = write!(out, r#" {}="{}""#,
415 aname.to_qname_string(), escape_attr(value, false, None));
416 }
417 if name.uri.is_empty() && VOID_ELEMENTS.iter().any(|v| *v == local_lc) {
419 out.push('>');
420 return;
421 }
422 out.push('>');
423 let raw_text = name.uri.is_empty()
424 && RAW_TEXT_ELEMENTS.iter().any(|v| *v == local_lc);
425 let child_format = format
429 && !children.iter().any(|c| matches!(c, ResultNode::Text { .. }));
430 for c in children {
431 if child_format {
432 out.push('\n');
433 push_indent(out, level + 1);
434 }
435 if raw_text {
436 if let ResultNode::Text { content, .. } = c {
437 out.push_str(content);
438 continue;
439 }
440 }
441 serialize_html_node(c, out, child_format, level + 1);
442 }
443 if child_format {
444 out.push('\n');
445 push_indent(out, level);
446 }
447 out.push_str("</");
448 out.push_str(&q);
449 out.push('>');
450 }
451 ResultNode::Text { content, dose } => {
452 if *dose { out.push_str(content); }
453 else { out.push_str(&escape_text(content, false, None)); }
454 }
455 ResultNode::Comment(s) => {
456 let _ = write!(out, "<!--{s}-->");
457 }
458 ResultNode::ProcessingInstruction { target, data } => {
459 if data.is_empty() {
462 let _ = write!(out, "<?{target}>");
463 } else {
464 let _ = write!(out, "<?{target} {data}>");
465 }
466 }
467 ResultNode::Attribute { .. } => {}
469 }
470}
471
472pub fn serialize_text(tree: &ResultTree) -> String {
475 let mut out = String::new();
476 for c in &tree.children { append_text(c, &mut out); }
477 out
478}
479
480fn append_text(node: &ResultNode, out: &mut String) {
481 match node {
482 ResultNode::Text { content, .. } => out.push_str(content),
483 ResultNode::Element { children, .. } => {
484 for c in children { append_text(c, out); }
485 }
486 _ => {}
488 }
489}
490
491#[cfg(test)]
492mod tests {
493 use super::*;
494 use crate::ast::QName;
495
496 fn elt(name: &str, children: Vec<ResultNode>) -> ResultNode {
497 ResultNode::Element {
498 name: QName { prefix: None, local: name.into(), uri: String::new() },
499 namespaces: Vec::new(),
500 attributes: Vec::new(),
501 children,
502 schema_type: None,
503 attr_types: Vec::new(),
504 }
505 }
506
507 fn text(s: &str) -> ResultNode {
508 ResultNode::Text { content: s.into(), dose: false }
509 }
510
511 fn tree_of(nodes: Vec<ResultNode>, method: Option<&str>) -> ResultTree {
512 let mut spec = OutputSpec::default();
513 spec.method = method.map(str::to_string);
514 spec.omit_xml_declaration = Some(true); ResultTree { children: nodes, output: spec, character_map: Vec::new(), secondary: Vec::new() }
516 }
517
518 #[test]
521 fn xml_empty_element_self_closes() {
522 let t = tree_of(vec![elt("br", vec![])], None);
523 assert_eq!(t.to_string().unwrap(), "<br/>");
524 }
525
526 #[test]
527 fn xml_escapes_text_specials() {
528 let t = tree_of(
529 vec![elt("p", vec![text("a < b && c > d")])],
530 None,
531 );
532 assert_eq!(t.to_string().unwrap(), "<p>a < b && c > d</p>");
533 }
534
535 #[test]
536 fn xml_escapes_attr_quote_and_specials() {
537 let t = tree_of(vec![ResultNode::Element {
538 name: QName { prefix: None, local: "a".into(), uri: String::new() },
539 namespaces: Vec::new(),
540 attributes: vec![(
541 QName { prefix: None, local: "href".into(), uri: String::new() },
542 r#"x"&y<z"#.to_string(),
543 )],
544 children: Vec::new(),
545 schema_type: None,
546 attr_types: Vec::new(),
547 }],None);
548 assert_eq!(t.to_string().unwrap(), r#"<a href="x"&y<z"/>"#);
549 }
550
551 #[test]
552 fn xml_text_dose_skips_escape() {
553 let t = tree_of(
554 vec![elt("p", vec![ResultNode::Text { content: "<raw/>".into(), dose: true }])],
555 None,
556 );
557 assert_eq!(t.to_string().unwrap(), "<p><raw/></p>");
558 }
559
560 fn tree_indented(nodes: Vec<ResultNode>) -> ResultTree {
561 let mut spec = OutputSpec::default();
562 spec.omit_xml_declaration = Some(true);
563 spec.indent = Some(true);
564 ResultTree { children: nodes, output: spec, character_map: Vec::new(), secondary: Vec::new() }
565 }
566
567 #[test]
568 fn xml_indent_yes_pretty_prints_element_only_content() {
569 let t = tree_indented(vec![elt("root", vec![
570 elt("a", vec![elt("b", vec![])]),
571 elt("c", vec![]),
572 ])]);
573 assert_eq!(
574 t.to_string().unwrap(),
575 "<root>\n <a>\n <b/>\n </a>\n <c/>\n</root>\n",
576 );
577 }
578
579 #[test]
580 fn xml_indent_yes_preserves_mixed_content() {
581 let t = tree_indented(vec![elt("root", vec![
584 elt("p", vec![text("hello "), elt("b", vec![text("world")])]),
585 ])]);
586 assert_eq!(
587 t.to_string().unwrap(),
588 "<root>\n <p>hello <b>world</b></p>\n</root>\n",
589 );
590 }
591
592 #[test]
593 fn xml_indent_off_by_default() {
594 let t = tree_of(vec![elt("root", vec![elt("a", vec![])])], None);
595 assert_eq!(t.to_string().unwrap(), "<root><a/></root>");
596 }
597
598 #[test]
601 fn html_void_elements_get_no_close_no_slash() {
602 let t = tree_of(vec![
603 elt("html", vec![
604 elt("head", vec![ elt("meta", vec![]) ]),
605 elt("body", vec![ elt("br", vec![]), elt("img", vec![]) ]),
606 ]),
607 ], Some("html"));
608 let s = t.to_string().unwrap();
609 assert!(s.contains("<meta>"), "got: {s}");
610 assert!(s.contains("<br>"), "got: {s}");
611 assert!(s.contains("<img>"), "got: {s}");
612 assert!(!s.contains("<br/>"), "got: {s}");
613 assert!(!s.contains("<meta/>"), "got: {s}");
614 }
615
616 #[test]
617 fn html_script_content_not_escaped() {
618 let t = tree_of(vec![ elt("script", vec![ text("if (a < b) alert('x');") ]) ],
619 Some("html"));
620 let s = t.to_string().unwrap();
621 assert!(s.contains("if (a < b)"), "script body should be raw: {s}");
622 }
623
624 #[test]
625 fn html_default_detected_by_root_html_element() {
626 let t = tree_of(vec![ elt("html", vec![ elt("br", vec![]) ]) ], None);
628 let s = t.to_string().unwrap();
629 assert!(s.contains("<br>"), "got: {s}");
631 assert!(!s.contains("<br/>"));
632 }
633
634 fn tree_indented_method(nodes: Vec<ResultNode>, method: &str) -> ResultTree {
635 let mut spec = OutputSpec::default();
636 spec.method = Some(method.into());
637 spec.indent = Some(true);
638 ResultTree { children: nodes, output: spec, character_map: Vec::new(), secondary: Vec::new() }
639 }
640
641 #[test]
642 fn html_indent_yes_pretty_prints_element_only_content() {
643 let t = tree_indented_method(vec![
644 elt("html", vec![
645 elt("head", vec![elt("meta", vec![])]),
646 elt("body", vec![elt("p", vec![text("hi")])]),
647 ]),
648 ], "html");
649 assert_eq!(
650 t.to_string().unwrap(),
651 "<html>\n <head>\n <meta>\n </head>\n <body>\n <p>hi</p>\n </body>\n</html>\n",
652 );
653 }
654
655 #[test]
656 fn html_indent_yes_preserves_mixed_content_and_raw_text() {
657 let t = tree_indented_method(vec![
658 elt("body", vec![
659 elt("p", vec![text("a "), elt("b", vec![text("c")]), text(" d")]),
660 elt("script", vec![text("if (a < b) x();")]),
661 ]),
662 ], "html");
663 assert_eq!(
664 t.to_string().unwrap(),
665 "<body>\n <p>a <b>c</b> d</p>\n <script>if (a < b) x();</script>\n</body>\n",
666 );
667 }
668
669 #[test]
670 fn html_indent_off_by_default() {
671 let t = tree_of(vec![elt("html", vec![elt("body", vec![elt("br", vec![])])])],
672 Some("html"));
673 assert_eq!(t.to_string().unwrap(), "<html><body><br></body></html>");
674 }
675
676 #[test]
679 fn text_strips_markup() {
680 let t = tree_of(vec![ elt("p", vec![
681 text("Hello, "),
682 elt("b", vec![text("world")]),
683 text("!"),
684 ]) ], Some("text"));
685 assert_eq!(t.to_string().unwrap(), "Hello, world!");
686 }
687
688 #[test]
689 fn text_strips_comments_and_pis() {
690 let t = tree_of(vec![
691 ResultNode::Comment("ignored".into()),
692 elt("p", vec![text("kept")]),
693 ResultNode::ProcessingInstruction { target: "pi".into(), data: "ignored".into() },
694 ], Some("text"));
695 assert_eq!(t.to_string().unwrap(), "kept");
696 }
697
698 #[test]
701 fn write_to_io_writer() {
702 let t = tree_of(vec![elt("r", vec![text("hi")])], None);
703 let mut buf = Vec::<u8>::new();
704 t.write_to(&mut buf).unwrap();
705 assert_eq!(buf, b"<r>hi</r>");
706 }
707
708 #[test]
711 fn xml_decl_emitted_when_not_omitted() {
712 let mut spec = OutputSpec::default();
713 spec.omit_xml_declaration = Some(false);
714 spec.version = Some("1.0".into());
715 spec.encoding = Some("UTF-8".into());
716 let t = ResultTree {
717 children: vec![elt("r", vec![])],
718 output: spec,
719 character_map: Vec::new(),
720 secondary: Vec::new(),
721 };
722 let s = t.to_string().unwrap();
723 assert!(s.starts_with(r#"<?xml version="1.0" encoding="UTF-8"?>"#), "got: {s}");
724 }
725
726 #[test]
727 fn xml_decl_emits_standalone_yes() {
728 let mut spec = OutputSpec::default();
729 spec.omit_xml_declaration = Some(false);
730 spec.standalone = Some(true);
731 let t = ResultTree {
732 children: vec![elt("r", vec![])],
733 output: spec,
734 character_map: Vec::new(),
735 secondary: Vec::new(),
736 };
737 let s = t.to_string().unwrap();
738 assert!(s.contains(r#"standalone="yes""#), "got: {s}");
739 }
740
741 #[test]
742 fn xml_decl_emits_standalone_no() {
743 let mut spec = OutputSpec::default();
744 spec.omit_xml_declaration = Some(false);
745 spec.standalone = Some(false);
746 let t = ResultTree {
747 children: vec![elt("r", vec![])],
748 output: spec,
749 character_map: Vec::new(),
750 secondary: Vec::new(),
751 };
752 let s = t.to_string().unwrap();
753 assert!(s.contains(r#"standalone="no""#), "got: {s}");
754 }
755
756 #[test]
759 fn xml_doctype_system() {
760 let mut spec = OutputSpec::default();
761 spec.omit_xml_declaration = Some(true);
762 spec.doctype_system = Some("foo.dtd".into());
763 let t = ResultTree {
764 children: vec![elt("r", vec![])],
765 output: spec,
766 character_map: Vec::new(),
767 secondary: Vec::new(),
768 };
769 let s = t.to_string().unwrap();
770 assert!(s.contains(r#"<!DOCTYPE r SYSTEM "foo.dtd">"#), "got: {s}");
771 }
772
773 #[test]
774 fn xml_doctype_public() {
775 let mut spec = OutputSpec::default();
776 spec.omit_xml_declaration = Some(true);
777 spec.doctype_system = Some("foo.dtd".into());
778 spec.doctype_public = Some("-//ID//PUB".into());
779 let t = ResultTree {
780 children: vec![elt("r", vec![])],
781 output: spec,
782 character_map: Vec::new(),
783 secondary: Vec::new(),
784 };
785 let s = t.to_string().unwrap();
786 assert!(s.contains(r#"<!DOCTYPE r PUBLIC "-//ID//PUB" "foo.dtd">"#), "got: {s}");
787 }
788
789 #[test]
792 fn xml_emits_namespace_declarations() {
793 let t = tree_of(vec![ResultNode::Element {
794 name: QName { prefix: Some("xs".into()), local: "schema".into(), uri: "http://www.w3.org/2001/XMLSchema".into() },
795 namespaces: vec![
796 (Some("xs".into()), "http://www.w3.org/2001/XMLSchema".into()),
797 (None, "http://example.com/default".into()),
798 ],
799 attributes: Vec::new(),
800 children: Vec::new(),
801 schema_type: None,
802 attr_types: Vec::new(),
803 }],None);
804 let s = t.to_string().unwrap();
805 assert!(s.contains(r#"xmlns:xs="http://www.w3.org/2001/XMLSchema""#), "got: {s}");
806 assert!(s.contains(r#"xmlns="http://example.com/default""#), "got: {s}");
807 }
808
809 #[test]
812 fn xml_serializes_comment() {
813 let t = tree_of(vec![ResultNode::Comment(" hello ".into())], None);
814 assert_eq!(t.to_string().unwrap(), "<!-- hello -->");
815 }
816
817 #[test]
818 fn xml_serializes_pi_no_data() {
819 let t = tree_of(vec![
820 ResultNode::ProcessingInstruction { target: "pi".into(), data: String::new() },
821 ], None);
822 assert_eq!(t.to_string().unwrap(), "<?pi?>");
823 }
824
825 #[test]
826 fn xml_serializes_pi_with_data() {
827 let t = tree_of(vec![
828 ResultNode::ProcessingInstruction {
829 target: "xml-stylesheet".into(),
830 data: r#"href="s.xsl""#.into(),
831 },
832 ], None);
833 assert_eq!(t.to_string().unwrap(),
834 r#"<?xml-stylesheet href="s.xsl"?>"#);
835 }
836
837 #[test]
840 fn xml_cdata_section_elements_wrap_text_children() {
841 let mut spec = OutputSpec::default();
842 spec.omit_xml_declaration = Some(true);
843 spec.cdata_section_elements = vec![
844 QName { prefix: None, local: "raw".into(), uri: String::new() },
845 ];
846 let t = ResultTree {
847 children: vec![elt("raw", vec![text("a < b & c")])],
848 output: spec,
849 character_map: Vec::new(),
850 secondary: Vec::new(),
851 };
852 let s = t.to_string().unwrap();
853 assert!(s.contains("<![CDATA[a < b & c]]>"), "got: {s}");
854 }
855
856 #[test]
857 fn xml_cdata_section_splits_embedded_close_seq() {
858 let mut spec = OutputSpec::default();
860 spec.omit_xml_declaration = Some(true);
861 spec.cdata_section_elements = vec![
862 QName { prefix: None, local: "raw".into(), uri: String::new() },
863 ];
864 let t = ResultTree {
865 children: vec![elt("raw", vec![text("end ]]> here")])],
866 output: spec,
867 character_map: Vec::new(),
868 secondary: Vec::new(),
869 };
870 let s = t.to_string().unwrap();
871 assert!(s.contains("]]]]><![CDATA[>"), "got: {s}");
873 }
874
875 #[test]
878 fn xml_attr_escapes_newline_tab_cr() {
879 let t = tree_of(vec![ResultNode::Element {
880 name: QName { prefix: None, local: "a".into(), uri: String::new() },
881 namespaces: Vec::new(),
882 attributes: vec![(
883 QName { prefix: None, local: "v".into(), uri: String::new() },
884 "x\ny\tz\rw".to_string(),
885 )],
886 children: Vec::new(),
887 schema_type: None,
888 attr_types: Vec::new(),
889 }],None);
890 let s = t.to_string().unwrap();
891 assert!(s.contains(" "), "got: {s}");
892 assert!(s.contains("	"), "got: {s}");
893 assert!(s.contains(" "), "got: {s}");
894 }
895
896 #[test]
899 fn html_doctype_system_only() {
900 let mut spec = OutputSpec::default();
901 spec.method = Some("html".into());
902 spec.doctype_system = Some("about:legacy-compat".into());
903 let t = ResultTree {
904 children: vec![elt("html", vec![])],
905 output: spec,
906 character_map: Vec::new(),
907 secondary: Vec::new(),
908 };
909 let s = t.to_string().unwrap();
910 assert!(s.contains(r#"<!DOCTYPE html SYSTEM "about:legacy-compat">"#), "got: {s}");
911 }
912
913 #[test]
914 fn html_doctype_public_only() {
915 let mut spec = OutputSpec::default();
917 spec.method = Some("html".into());
918 spec.doctype_public = Some("-//W3C//DTD HTML 4.01//EN".into());
919 let t = ResultTree {
920 children: vec![elt("html", vec![])],
921 output: spec,
922 character_map: Vec::new(),
923 secondary: Vec::new(),
924 };
925 let s = t.to_string().unwrap();
926 assert!(s.contains(r#"<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN">"#),
927 "got: {s}");
928 }
929
930 #[test]
931 fn html_doctype_public_and_system() {
932 let mut spec = OutputSpec::default();
933 spec.method = Some("html".into());
934 spec.doctype_public = Some("-//W3C//DTD HTML 4.01//EN".into());
935 spec.doctype_system = Some("http://www.w3.org/TR/html4/strict.dtd".into());
936 let t = ResultTree {
937 children: vec![elt("html", vec![])],
938 output: spec,
939 character_map: Vec::new(),
940 secondary: Vec::new(),
941 };
942 let s = t.to_string().unwrap();
943 assert!(s.contains(r#"<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">"#),
944 "got: {s}");
945 }
946
947 #[test]
950 fn html_emits_namespace_declarations() {
951 let t = tree_of(vec![ResultNode::Element {
952 name: QName { prefix: None, local: "html".into(), uri: String::new() },
953 namespaces: vec![
954 (Some("svg".into()), "http://www.w3.org/2000/svg".into()),
955 (None, "http://www.w3.org/1999/xhtml".into()),
956 ],
957 attributes: Vec::new(),
958 children: Vec::new(),
959 schema_type: None,
960 attr_types: Vec::new(),
961 }],Some("html"));
962 let s = t.to_string().unwrap();
963 assert!(s.contains(r#"xmlns:svg="http://www.w3.org/2000/svg""#), "got: {s}");
964 assert!(s.contains(r#"xmlns="http://www.w3.org/1999/xhtml""#), "got: {s}");
965 }
966
967 #[test]
968 fn html_serializes_comment() {
969 let t = tree_of(vec![
970 elt("html", vec![ResultNode::Comment(" hi ".into())]),
971 ], Some("html"));
972 let s = t.to_string().unwrap();
973 assert!(s.contains("<!-- hi -->"), "got: {s}");
974 }
975
976 #[test]
977 fn html_serializes_pi_with_and_without_data() {
978 let t = tree_of(vec![
979 elt("html", vec![
980 ResultNode::ProcessingInstruction { target: "a".into(), data: String::new() },
981 ResultNode::ProcessingInstruction { target: "b".into(), data: "x".into() },
982 ]),
983 ], Some("html"));
984 let s = t.to_string().unwrap();
985 assert!(s.contains("<?a>"), "got: {s}");
987 assert!(s.contains("<?b x>"), "got: {s}");
988 }
989
990 #[test]
991 fn html_text_with_dose_skips_escape() {
992 let t = tree_of(vec![
993 elt("html", vec![
994 ResultNode::Text { content: "<raw>".into(), dose: true },
995 ]),
996 ], Some("html"));
997 let s = t.to_string().unwrap();
998 assert!(s.contains("<raw>"), "got: {s}");
999 }
1000
1001 #[test]
1002 fn html_default_method_when_no_root_html() {
1003 let t = tree_of(vec![elt("r", vec![])], None);
1005 let s = t.to_string().unwrap();
1006 assert_eq!(s, "<r/>");
1008 }
1009
1010 #[test]
1013 fn text_method_concatenates_all_text() {
1014 let t = tree_of(vec![
1015 elt("a", vec![
1016 text("one"),
1017 elt("b", vec![text("two")]),
1018 ResultNode::Text { content: "three".into(), dose: true }, ]),
1020 ], Some("text"));
1021 assert_eq!(t.to_string().unwrap(), "onetwothree");
1022 }
1023}