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 for child in &tree.children {
85 serialize_xml_node(child, &mut out, &tree.output, "", &tree.character_map);
86 }
87 out
88}
89
90fn should_emit_xml_decl(output: &OutputSpec) -> bool {
91 !output.omit_xml_declaration.unwrap_or(false)
93}
94
95fn first_element_name(nodes: &[ResultNode]) -> Option<String> {
96 nodes.iter().find_map(|n| match n {
97 ResultNode::Element { name, .. } => Some(name.to_qname_string()),
98 _ => None,
99 })
100}
101
102fn serialize_xml_node(
109 node: &ResultNode,
110 out: &mut String,
111 opts: &OutputSpec,
112 parent_default_ns: &str,
113 cmap: &[(char, String)],
114) {
115 let xml_11 = opts.version.as_deref() == Some("1.1");
116 let enc_cap = encoding_capability(opts.encoding.as_deref());
117 match node {
118 ResultNode::Element { name, namespaces, attributes, children, .. } => {
119 let q = name.to_qname_string();
120 out.push('<');
121 out.push_str(&q);
122 let mut child_default_ns: &str = parent_default_ns;
128 for (prefix, uri) in namespaces {
129 match prefix {
130 Some(p) if p == "xml"
138 && uri == "http://www.w3.org/XML/1998/namespace" => {}
139 Some(p) => { let _ = write!(out, r#" xmlns:{p}="{}""#, escape_attr(uri, xml_11, enc_cap)); }
140 None => {
141 if uri != parent_default_ns {
147 let _ = write!(out, r#" xmlns="{}""#, escape_attr(uri, xml_11, enc_cap));
148 }
149 child_default_ns = uri.as_str();
150 }
151 }
152 }
153 for (aname, value) in attributes {
154 let _ = write!(out, r#" {}="{}""#,
155 aname.to_qname_string(),
156 escape_attr_with_map(value, xml_11, enc_cap, cmap));
157 }
158 if children.is_empty() {
159 out.push_str("/>");
160 return;
161 }
162 out.push('>');
163 let is_cdata = opts.cdata_section_elements.iter()
166 .any(|q| q.uri == name.uri && q.local == name.local);
167 for c in children {
168 if is_cdata {
169 if let ResultNode::Text { content, .. } = c {
170 out.push_str("<![CDATA[");
171 out.push_str(&content.replace("]]>", "]]]]><![CDATA[>"));
172 out.push_str("]]>");
173 continue;
174 }
175 }
176 serialize_xml_node(c, out, opts, child_default_ns, cmap);
177 }
178 out.push_str("</");
179 out.push_str(&q);
180 out.push('>');
181 }
182 ResultNode::Text { content, dose } => {
183 if *dose {
184 out.push_str(content);
185 } else {
186 out.push_str(&escape_text_with_map(content, xml_11, enc_cap, cmap));
187 }
188 }
189 ResultNode::Comment(s) => {
190 let _ = write!(out, "<!--{}-->", s);
191 }
192 ResultNode::ProcessingInstruction { target, data } => {
193 if data.is_empty() {
194 let _ = write!(out, "<?{target}?>");
195 } else {
196 let _ = write!(out, "<?{target} {data}?>");
197 }
198 }
199 ResultNode::Attribute { .. } => {}
203 }
204}
205
206#[inline]
213fn xml_11_must_escape(c: char) -> bool {
214 matches!(c as u32,
215 0x01..=0x08 | 0x0B..=0x0C | 0x0E..=0x1F |
216 0x7F..=0x84 | 0x85 | 0x86..=0x9F |
217 0x2028
218 )
219}
220
221fn encoding_capability(enc: Option<&str>) -> Option<u32> {
228 let name = enc.unwrap_or("UTF-8").to_ascii_lowercase();
229 let norm: String = name.chars().filter(|c| !matches!(c, '-' | '_' | ' ')).collect();
230 match norm.as_str() {
231 "ascii" | "usascii" | "iso646us" => Some(0x7F),
233 "iso88591" | "latin1" | "l1" | "cp1252" | "windows1252" => Some(0xFF),
237 _ => None,
241 }
242}
243
244#[inline]
245fn must_ncr_escape(c: char, enc_cap: Option<u32>) -> bool {
246 matches!(enc_cap, Some(max) if c as u32 > max)
247}
248
249fn escape_text(s: &str, xml_11: bool, enc_cap: Option<u32>) -> String {
250 escape_text_with_map(s, xml_11, enc_cap, &[])
251}
252
253fn escape_attr(s: &str, xml_11: bool, enc_cap: Option<u32>) -> String {
254 escape_attr_with_map(s, xml_11, enc_cap, &[])
255}
256
257#[inline]
262fn cmap_lookup<'a>(c: char, cmap: &'a [(char, String)]) -> Option<&'a str> {
263 for (k, v) in cmap {
264 if *k == c { return Some(v.as_str()); }
265 }
266 None
267}
268
269fn escape_text_with_map(
270 s: &str,
271 xml_11: bool,
272 enc_cap: Option<u32>,
273 cmap: &[(char, String)],
274) -> String {
275 let mut out = String::with_capacity(s.len());
276 for c in s.chars() {
277 if let Some(replacement) = cmap_lookup(c, cmap) {
278 out.push_str(replacement);
279 continue;
280 }
281 match c {
282 '<' => out.push_str("<"),
283 '>' => out.push_str(">"),
284 '&' => out.push_str("&"),
285 '\r' => out.push_str("
"),
289 c if xml_11 && xml_11_must_escape(c) => {
290 let _ = write!(out, "&#{};", c as u32);
291 }
292 c if must_ncr_escape(c, enc_cap) => {
293 let _ = write!(out, "&#{};", c as u32);
294 }
295 _ => out.push(c),
296 }
297 }
298 out
299}
300
301fn escape_attr_with_map(
302 s: &str,
303 xml_11: bool,
304 enc_cap: Option<u32>,
305 cmap: &[(char, String)],
306) -> String {
307 let mut out = String::with_capacity(s.len());
308 for c in s.chars() {
309 if let Some(replacement) = cmap_lookup(c, cmap) {
310 out.push_str(replacement);
311 continue;
312 }
313 match c {
314 '<' => out.push_str("<"),
315 '>' => out.push_str(">"),
316 '&' => out.push_str("&"),
317 '"' => out.push_str("""),
318 '\n' => out.push_str(" "),
319 '\r' => out.push_str(" "),
320 '\t' => out.push_str("	"),
321 c if xml_11 && xml_11_must_escape(c) => {
322 let _ = write!(out, "&#{};", c as u32);
323 }
324 c if must_ncr_escape(c, enc_cap) => {
325 let _ = write!(out, "&#{};", c as u32);
326 }
327 _ => out.push(c),
328 }
329 }
330 out
331}
332
333const VOID_ELEMENTS: &[&str] = &[
339 "area", "base", "br", "col", "embed", "hr", "img", "input",
340 "link", "meta", "param", "source", "track", "wbr",
341];
342
343const RAW_TEXT_ELEMENTS: &[&str] = &["script", "style"];
346
347pub fn serialize_html(tree: &ResultTree) -> String {
348 let mut out = String::new();
349 if let Some(dt_sys) = tree.output.doctype_system.as_deref() {
350 if let Some(root) = first_element_name(&tree.children) {
351 if let Some(pubid) = tree.output.doctype_public.as_deref() {
352 let _ = writeln!(out, r#"<!DOCTYPE {root} PUBLIC "{pubid}" "{dt_sys}">"#);
353 } else {
354 let _ = writeln!(out, r#"<!DOCTYPE {root} SYSTEM "{dt_sys}">"#);
355 }
356 }
357 } else if let Some(pubid) = tree.output.doctype_public.as_deref() {
358 if let Some(root) = first_element_name(&tree.children) {
359 let _ = writeln!(out, r#"<!DOCTYPE {root} PUBLIC "{pubid}">"#);
360 }
361 }
362 for c in &tree.children { serialize_html_node(c, &mut out); }
363 out
364}
365
366fn serialize_html_node(node: &ResultNode, out: &mut String) {
367 match node {
368 ResultNode::Element { name, namespaces, attributes, children, .. } => {
369 let local_lc = name.local.to_lowercase();
370 let q = name.to_qname_string();
371 out.push('<');
372 out.push_str(&q);
373 for (prefix, uri) in namespaces {
374 match prefix {
375 Some(p) => { let _ = write!(out, r#" xmlns:{p}="{}""#, escape_attr(uri, false, None)); }
376 None => { let _ = write!(out, r#" xmlns="{}""#, escape_attr(uri, false, None)); }
377 }
378 }
379 for (aname, value) in attributes {
380 let _ = write!(out, r#" {}="{}""#,
381 aname.to_qname_string(), escape_attr(value, false, None));
382 }
383 if name.uri.is_empty() && VOID_ELEMENTS.iter().any(|v| *v == local_lc) {
385 out.push('>');
386 return;
387 }
388 out.push('>');
389 let raw_text = name.uri.is_empty()
390 && RAW_TEXT_ELEMENTS.iter().any(|v| *v == local_lc);
391 for c in children {
392 if raw_text {
393 if let ResultNode::Text { content, .. } = c {
394 out.push_str(content);
395 continue;
396 }
397 }
398 serialize_html_node(c, out);
399 }
400 out.push_str("</");
401 out.push_str(&q);
402 out.push('>');
403 }
404 ResultNode::Text { content, dose } => {
405 if *dose { out.push_str(content); }
406 else { out.push_str(&escape_text(content, false, None)); }
407 }
408 ResultNode::Comment(s) => {
409 let _ = write!(out, "<!--{s}-->");
410 }
411 ResultNode::ProcessingInstruction { target, data } => {
412 if data.is_empty() {
415 let _ = write!(out, "<?{target}>");
416 } else {
417 let _ = write!(out, "<?{target} {data}>");
418 }
419 }
420 ResultNode::Attribute { .. } => {}
422 }
423}
424
425pub fn serialize_text(tree: &ResultTree) -> String {
428 let mut out = String::new();
429 for c in &tree.children { append_text(c, &mut out); }
430 out
431}
432
433fn append_text(node: &ResultNode, out: &mut String) {
434 match node {
435 ResultNode::Text { content, .. } => out.push_str(content),
436 ResultNode::Element { children, .. } => {
437 for c in children { append_text(c, out); }
438 }
439 _ => {}
441 }
442}
443
444#[cfg(test)]
445mod tests {
446 use super::*;
447 use crate::ast::QName;
448
449 fn elt(name: &str, children: Vec<ResultNode>) -> ResultNode {
450 ResultNode::Element {
451 name: QName { prefix: None, local: name.into(), uri: String::new() },
452 namespaces: Vec::new(),
453 attributes: Vec::new(),
454 children,
455 schema_type: None,
456 attr_types: Vec::new(),
457 }
458 }
459
460 fn text(s: &str) -> ResultNode {
461 ResultNode::Text { content: s.into(), dose: false }
462 }
463
464 fn tree_of(nodes: Vec<ResultNode>, method: Option<&str>) -> ResultTree {
465 let mut spec = OutputSpec::default();
466 spec.method = method.map(str::to_string);
467 spec.omit_xml_declaration = Some(true); ResultTree { children: nodes, output: spec, character_map: Vec::new(), secondary: Vec::new() }
469 }
470
471 #[test]
474 fn xml_empty_element_self_closes() {
475 let t = tree_of(vec![elt("br", vec![])], None);
476 assert_eq!(t.to_string().unwrap(), "<br/>");
477 }
478
479 #[test]
480 fn xml_escapes_text_specials() {
481 let t = tree_of(
482 vec![elt("p", vec![text("a < b && c > d")])],
483 None,
484 );
485 assert_eq!(t.to_string().unwrap(), "<p>a < b && c > d</p>");
486 }
487
488 #[test]
489 fn xml_escapes_attr_quote_and_specials() {
490 let t = tree_of(vec![ResultNode::Element {
491 name: QName { prefix: None, local: "a".into(), uri: String::new() },
492 namespaces: Vec::new(),
493 attributes: vec![(
494 QName { prefix: None, local: "href".into(), uri: String::new() },
495 r#"x"&y<z"#.to_string(),
496 )],
497 children: Vec::new(),
498 schema_type: None,
499 attr_types: Vec::new(),
500 }],None);
501 assert_eq!(t.to_string().unwrap(), r#"<a href="x"&y<z"/>"#);
502 }
503
504 #[test]
505 fn xml_text_dose_skips_escape() {
506 let t = tree_of(
507 vec![elt("p", vec![ResultNode::Text { content: "<raw/>".into(), dose: true }])],
508 None,
509 );
510 assert_eq!(t.to_string().unwrap(), "<p><raw/></p>");
511 }
512
513 #[test]
516 fn html_void_elements_get_no_close_no_slash() {
517 let t = tree_of(vec![
518 elt("html", vec![
519 elt("head", vec![ elt("meta", vec![]) ]),
520 elt("body", vec![ elt("br", vec![]), elt("img", vec![]) ]),
521 ]),
522 ], Some("html"));
523 let s = t.to_string().unwrap();
524 assert!(s.contains("<meta>"), "got: {s}");
525 assert!(s.contains("<br>"), "got: {s}");
526 assert!(s.contains("<img>"), "got: {s}");
527 assert!(!s.contains("<br/>"), "got: {s}");
528 assert!(!s.contains("<meta/>"), "got: {s}");
529 }
530
531 #[test]
532 fn html_script_content_not_escaped() {
533 let t = tree_of(vec![ elt("script", vec![ text("if (a < b) alert('x');") ]) ],
534 Some("html"));
535 let s = t.to_string().unwrap();
536 assert!(s.contains("if (a < b)"), "script body should be raw: {s}");
537 }
538
539 #[test]
540 fn html_default_detected_by_root_html_element() {
541 let t = tree_of(vec![ elt("html", vec![ elt("br", vec![]) ]) ], None);
543 let s = t.to_string().unwrap();
544 assert!(s.contains("<br>"), "got: {s}");
546 assert!(!s.contains("<br/>"));
547 }
548
549 #[test]
552 fn text_strips_markup() {
553 let t = tree_of(vec![ elt("p", vec![
554 text("Hello, "),
555 elt("b", vec![text("world")]),
556 text("!"),
557 ]) ], Some("text"));
558 assert_eq!(t.to_string().unwrap(), "Hello, world!");
559 }
560
561 #[test]
562 fn text_strips_comments_and_pis() {
563 let t = tree_of(vec![
564 ResultNode::Comment("ignored".into()),
565 elt("p", vec![text("kept")]),
566 ResultNode::ProcessingInstruction { target: "pi".into(), data: "ignored".into() },
567 ], Some("text"));
568 assert_eq!(t.to_string().unwrap(), "kept");
569 }
570
571 #[test]
574 fn write_to_io_writer() {
575 let t = tree_of(vec![elt("r", vec![text("hi")])], None);
576 let mut buf = Vec::<u8>::new();
577 t.write_to(&mut buf).unwrap();
578 assert_eq!(buf, b"<r>hi</r>");
579 }
580
581 #[test]
584 fn xml_decl_emitted_when_not_omitted() {
585 let mut spec = OutputSpec::default();
586 spec.omit_xml_declaration = Some(false);
587 spec.version = Some("1.0".into());
588 spec.encoding = Some("UTF-8".into());
589 let t = ResultTree {
590 children: vec![elt("r", vec![])],
591 output: spec,
592 character_map: Vec::new(),
593 secondary: Vec::new(),
594 };
595 let s = t.to_string().unwrap();
596 assert!(s.starts_with(r#"<?xml version="1.0" encoding="UTF-8"?>"#), "got: {s}");
597 }
598
599 #[test]
600 fn xml_decl_emits_standalone_yes() {
601 let mut spec = OutputSpec::default();
602 spec.omit_xml_declaration = Some(false);
603 spec.standalone = Some(true);
604 let t = ResultTree {
605 children: vec![elt("r", vec![])],
606 output: spec,
607 character_map: Vec::new(),
608 secondary: Vec::new(),
609 };
610 let s = t.to_string().unwrap();
611 assert!(s.contains(r#"standalone="yes""#), "got: {s}");
612 }
613
614 #[test]
615 fn xml_decl_emits_standalone_no() {
616 let mut spec = OutputSpec::default();
617 spec.omit_xml_declaration = Some(false);
618 spec.standalone = Some(false);
619 let t = ResultTree {
620 children: vec![elt("r", vec![])],
621 output: spec,
622 character_map: Vec::new(),
623 secondary: Vec::new(),
624 };
625 let s = t.to_string().unwrap();
626 assert!(s.contains(r#"standalone="no""#), "got: {s}");
627 }
628
629 #[test]
632 fn xml_doctype_system() {
633 let mut spec = OutputSpec::default();
634 spec.omit_xml_declaration = Some(true);
635 spec.doctype_system = Some("foo.dtd".into());
636 let t = ResultTree {
637 children: vec![elt("r", vec![])],
638 output: spec,
639 character_map: Vec::new(),
640 secondary: Vec::new(),
641 };
642 let s = t.to_string().unwrap();
643 assert!(s.contains(r#"<!DOCTYPE r SYSTEM "foo.dtd">"#), "got: {s}");
644 }
645
646 #[test]
647 fn xml_doctype_public() {
648 let mut spec = OutputSpec::default();
649 spec.omit_xml_declaration = Some(true);
650 spec.doctype_system = Some("foo.dtd".into());
651 spec.doctype_public = Some("-//ID//PUB".into());
652 let t = ResultTree {
653 children: vec![elt("r", vec![])],
654 output: spec,
655 character_map: Vec::new(),
656 secondary: Vec::new(),
657 };
658 let s = t.to_string().unwrap();
659 assert!(s.contains(r#"<!DOCTYPE r PUBLIC "-//ID//PUB" "foo.dtd">"#), "got: {s}");
660 }
661
662 #[test]
665 fn xml_emits_namespace_declarations() {
666 let t = tree_of(vec![ResultNode::Element {
667 name: QName { prefix: Some("xs".into()), local: "schema".into(), uri: "http://www.w3.org/2001/XMLSchema".into() },
668 namespaces: vec![
669 (Some("xs".into()), "http://www.w3.org/2001/XMLSchema".into()),
670 (None, "http://example.com/default".into()),
671 ],
672 attributes: Vec::new(),
673 children: Vec::new(),
674 schema_type: None,
675 attr_types: Vec::new(),
676 }],None);
677 let s = t.to_string().unwrap();
678 assert!(s.contains(r#"xmlns:xs="http://www.w3.org/2001/XMLSchema""#), "got: {s}");
679 assert!(s.contains(r#"xmlns="http://example.com/default""#), "got: {s}");
680 }
681
682 #[test]
685 fn xml_serializes_comment() {
686 let t = tree_of(vec![ResultNode::Comment(" hello ".into())], None);
687 assert_eq!(t.to_string().unwrap(), "<!-- hello -->");
688 }
689
690 #[test]
691 fn xml_serializes_pi_no_data() {
692 let t = tree_of(vec![
693 ResultNode::ProcessingInstruction { target: "pi".into(), data: String::new() },
694 ], None);
695 assert_eq!(t.to_string().unwrap(), "<?pi?>");
696 }
697
698 #[test]
699 fn xml_serializes_pi_with_data() {
700 let t = tree_of(vec![
701 ResultNode::ProcessingInstruction {
702 target: "xml-stylesheet".into(),
703 data: r#"href="s.xsl""#.into(),
704 },
705 ], None);
706 assert_eq!(t.to_string().unwrap(),
707 r#"<?xml-stylesheet href="s.xsl"?>"#);
708 }
709
710 #[test]
713 fn xml_cdata_section_elements_wrap_text_children() {
714 let mut spec = OutputSpec::default();
715 spec.omit_xml_declaration = Some(true);
716 spec.cdata_section_elements = vec![
717 QName { prefix: None, local: "raw".into(), uri: String::new() },
718 ];
719 let t = ResultTree {
720 children: vec![elt("raw", vec![text("a < b & c")])],
721 output: spec,
722 character_map: Vec::new(),
723 secondary: Vec::new(),
724 };
725 let s = t.to_string().unwrap();
726 assert!(s.contains("<![CDATA[a < b & c]]>"), "got: {s}");
727 }
728
729 #[test]
730 fn xml_cdata_section_splits_embedded_close_seq() {
731 let mut spec = OutputSpec::default();
733 spec.omit_xml_declaration = Some(true);
734 spec.cdata_section_elements = vec![
735 QName { prefix: None, local: "raw".into(), uri: String::new() },
736 ];
737 let t = ResultTree {
738 children: vec![elt("raw", vec![text("end ]]> here")])],
739 output: spec,
740 character_map: Vec::new(),
741 secondary: Vec::new(),
742 };
743 let s = t.to_string().unwrap();
744 assert!(s.contains("]]]]><![CDATA[>"), "got: {s}");
746 }
747
748 #[test]
751 fn xml_attr_escapes_newline_tab_cr() {
752 let t = tree_of(vec![ResultNode::Element {
753 name: QName { prefix: None, local: "a".into(), uri: String::new() },
754 namespaces: Vec::new(),
755 attributes: vec![(
756 QName { prefix: None, local: "v".into(), uri: String::new() },
757 "x\ny\tz\rw".to_string(),
758 )],
759 children: Vec::new(),
760 schema_type: None,
761 attr_types: Vec::new(),
762 }],None);
763 let s = t.to_string().unwrap();
764 assert!(s.contains(" "), "got: {s}");
765 assert!(s.contains("	"), "got: {s}");
766 assert!(s.contains(" "), "got: {s}");
767 }
768
769 #[test]
772 fn html_doctype_system_only() {
773 let mut spec = OutputSpec::default();
774 spec.method = Some("html".into());
775 spec.doctype_system = Some("about:legacy-compat".into());
776 let t = ResultTree {
777 children: vec![elt("html", vec![])],
778 output: spec,
779 character_map: Vec::new(),
780 secondary: Vec::new(),
781 };
782 let s = t.to_string().unwrap();
783 assert!(s.contains(r#"<!DOCTYPE html SYSTEM "about:legacy-compat">"#), "got: {s}");
784 }
785
786 #[test]
787 fn html_doctype_public_only() {
788 let mut spec = OutputSpec::default();
790 spec.method = Some("html".into());
791 spec.doctype_public = Some("-//W3C//DTD HTML 4.01//EN".into());
792 let t = ResultTree {
793 children: vec![elt("html", vec![])],
794 output: spec,
795 character_map: Vec::new(),
796 secondary: Vec::new(),
797 };
798 let s = t.to_string().unwrap();
799 assert!(s.contains(r#"<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN">"#),
800 "got: {s}");
801 }
802
803 #[test]
804 fn html_doctype_public_and_system() {
805 let mut spec = OutputSpec::default();
806 spec.method = Some("html".into());
807 spec.doctype_public = Some("-//W3C//DTD HTML 4.01//EN".into());
808 spec.doctype_system = Some("http://www.w3.org/TR/html4/strict.dtd".into());
809 let t = ResultTree {
810 children: vec![elt("html", vec![])],
811 output: spec,
812 character_map: Vec::new(),
813 secondary: Vec::new(),
814 };
815 let s = t.to_string().unwrap();
816 assert!(s.contains(r#"<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">"#),
817 "got: {s}");
818 }
819
820 #[test]
823 fn html_emits_namespace_declarations() {
824 let t = tree_of(vec![ResultNode::Element {
825 name: QName { prefix: None, local: "html".into(), uri: String::new() },
826 namespaces: vec![
827 (Some("svg".into()), "http://www.w3.org/2000/svg".into()),
828 (None, "http://www.w3.org/1999/xhtml".into()),
829 ],
830 attributes: Vec::new(),
831 children: Vec::new(),
832 schema_type: None,
833 attr_types: Vec::new(),
834 }],Some("html"));
835 let s = t.to_string().unwrap();
836 assert!(s.contains(r#"xmlns:svg="http://www.w3.org/2000/svg""#), "got: {s}");
837 assert!(s.contains(r#"xmlns="http://www.w3.org/1999/xhtml""#), "got: {s}");
838 }
839
840 #[test]
841 fn html_serializes_comment() {
842 let t = tree_of(vec![
843 elt("html", vec![ResultNode::Comment(" hi ".into())]),
844 ], Some("html"));
845 let s = t.to_string().unwrap();
846 assert!(s.contains("<!-- hi -->"), "got: {s}");
847 }
848
849 #[test]
850 fn html_serializes_pi_with_and_without_data() {
851 let t = tree_of(vec![
852 elt("html", vec![
853 ResultNode::ProcessingInstruction { target: "a".into(), data: String::new() },
854 ResultNode::ProcessingInstruction { target: "b".into(), data: "x".into() },
855 ]),
856 ], Some("html"));
857 let s = t.to_string().unwrap();
858 assert!(s.contains("<?a>"), "got: {s}");
860 assert!(s.contains("<?b x>"), "got: {s}");
861 }
862
863 #[test]
864 fn html_text_with_dose_skips_escape() {
865 let t = tree_of(vec![
866 elt("html", vec![
867 ResultNode::Text { content: "<raw>".into(), dose: true },
868 ]),
869 ], Some("html"));
870 let s = t.to_string().unwrap();
871 assert!(s.contains("<raw>"), "got: {s}");
872 }
873
874 #[test]
875 fn html_default_method_when_no_root_html() {
876 let t = tree_of(vec![elt("r", vec![])], None);
878 let s = t.to_string().unwrap();
879 assert_eq!(s, "<r/>");
881 }
882
883 #[test]
886 fn text_method_concatenates_all_text() {
887 let t = tree_of(vec![
888 elt("a", vec![
889 text("one"),
890 elt("b", vec![text("two")]),
891 ResultNode::Text { content: "three".into(), dose: true }, ]),
893 ], Some("text"));
894 assert_eq!(t.to_string().unwrap(), "onetwothree");
895 }
896}