domtree/serialize/
html.rs1use alloc::string::String;
9use alloc::vec::Vec;
10
11use crate::node::NodeKind;
12use crate::tags;
13use crate::tree::{Dom, NodeRef};
14
15impl Dom {
16 pub fn to_html(&self) -> String {
23 let mut out = String::with_capacity(self.len() * 16);
24 let mut stack: Vec<Step<'_>> = Vec::new();
25 for r in self.roots().collect::<Vec<_>>().into_iter().rev() {
26 stack.push(Step::Open(r));
27 }
28 process(&mut out, &mut stack);
29 out
30 }
31}
32
33impl<'a> NodeRef<'a> {
34 pub fn to_html(&self) -> String {
42 let mut out = String::new();
43 let mut stack: Vec<Step<'a>> = Vec::new();
44 stack.push(Step::Open(*self));
45 process(&mut out, &mut stack);
46 out
47 }
48}
49
50enum Step<'a> {
51 Open(NodeRef<'a>),
52 Close(&'a str),
53}
54
55fn process(out: &mut String, stack: &mut Vec<Step<'_>>) {
56 while let Some(step) = stack.pop() {
57 match step {
58 Step::Close(tag) => {
59 out.push_str("</");
60 out.push_str(tag);
61 out.push('>');
62 }
63 Step::Open(node) => write_open(out, node, stack),
64 }
65 }
66}
67
68fn write_open<'a>(out: &mut String, node: NodeRef<'a>, stack: &mut Vec<Step<'a>>) {
69 match node.kind() {
70 NodeKind::Element { tag } => {
71 out.push('<');
72 out.push_str(tag);
73 for (k, v) in node.attributes() {
74 out.push(' ');
75 out.push_str(k);
76 if !v.is_empty() {
77 out.push_str("=\"");
78 escape_attr(out, v);
79 out.push('"');
80 }
81 }
82 out.push('>');
83 if tags::is_void(tag) {
84 return; }
86 stack.push(Step::Close(tag));
87 let mut child = node.last_child();
89 while let Some(c) = child {
90 stack.push(Step::Open(c));
91 child = c.prev_sibling();
92 }
93 }
94 NodeKind::Text(t) => {
95 let raw = node
97 .parent()
98 .and_then(|p| p.tag_name())
99 .is_some_and(tags::is_raw_text);
100 if raw {
101 out.push_str(t);
102 } else {
103 escape_text(out, t);
104 }
105 }
106 NodeKind::Comment(c) => {
107 out.push_str("<!--");
108 out.push_str(c);
109 out.push_str("-->");
110 }
111 NodeKind::Doctype(d) => {
112 out.push_str("<!doctype ");
113 out.push_str(d);
114 out.push('>');
115 }
116 }
117}
118
119fn escape_text(out: &mut String, s: &str) {
120 for c in s.chars() {
121 match c {
122 '&' => out.push_str("&"),
123 '<' => out.push_str("<"),
124 '>' => out.push_str(">"),
125 c => out.push(c),
126 }
127 }
128}
129
130fn escape_attr(out: &mut String, s: &str) {
131 for c in s.chars() {
132 match c {
133 '&' => out.push_str("&"),
134 '"' => out.push_str("""),
135 c => out.push(c),
136 }
137 }
138}