1use crate::dom::Dom;
9use crate::node::NodeData;
10use crate::node_id::NodeId;
11
12pub const VOID_ELEMENTS: &[&str] = &[
18 "area", "base", "basefont", "bgsound", "br", "col", "embed", "frame", "hr", "img", "input",
19 "keygen", "link", "meta", "param", "source", "track", "wbr",
20];
21
22pub fn is_void_element(tag: &str) -> bool {
24 VOID_ELEMENTS.contains(&tag)
25}
26
27fn escape_for_attr(s: &str, out: &mut String) {
30 for c in s.chars() {
31 match c {
32 '&' => out.push_str("&"),
33 '<' => out.push_str("<"),
34 '>' => out.push_str(">"),
35 '"' => out.push_str("""),
36 '\'' => out.push_str("'"),
37 c => out.push(c),
38 }
39 }
40}
41
42fn serializes_children_raw(tag: &str) -> bool {
48 matches!(
49 tag,
50 "style" | "script" | "xmp" | "iframe" | "noembed" | "noframes" | "plaintext"
51 )
52}
53
54fn escape_for_text(s: &str, out: &mut String) {
56 for c in s.chars() {
57 match c {
58 '&' => out.push_str("&"),
59 '<' => out.push_str("<"),
60 '>' => out.push_str(">"),
61 c => out.push(c),
62 }
63 }
64}
65
66impl<Ext> Dom<Ext> {
67 pub fn outer_markup(&self, id: NodeId) -> String {
75 let mut out = String::new();
76 self.write_node(id, &mut out, false);
77 out
78 }
79
80 pub fn inner_markup(&self, id: NodeId) -> String {
84 let mut out = String::new();
85 let Some(node) = self.get_node(id) else {
86 return out;
87 };
88 match &node.data {
89 NodeData::Element { tag, .. } => {
90 self.write_children(id, &mut out, serializes_children_raw(tag));
91 }
92 NodeData::Fragment => self.write_children(id, &mut out, false),
93 _ => {}
94 }
95 out
96 }
97
98 fn write_children(&self, id: NodeId, out: &mut String, raw_text: bool) {
99 let mut child = self.get_node(id).and_then(|n| n.first_child);
100 while let Some(c) = child {
101 self.write_node(c, out, raw_text);
102 child = self.get_node(c).and_then(|n| n.next_sibling);
103 }
104 }
105
106 fn write_node(&self, id: NodeId, out: &mut String, raw_text: bool) {
111 enum Step<'a> {
112 Node(NodeId, bool),
113 EndTag(&'a str),
114 }
115 let mut stack = vec![Step::Node(id, raw_text)];
116 let push_children = |stack: &mut Vec<Step<'_>>, id: NodeId, raw: bool| {
118 let mut child = self.get_node(id).and_then(|n| n.last_child);
119 while let Some(c) = child {
120 stack.push(Step::Node(c, raw));
121 child = self.get_node(c).and_then(|n| n.prev_sibling);
122 }
123 };
124 while let Some(step) = stack.pop() {
125 let (id, raw_text) = match step {
126 Step::EndTag(tag) => {
127 out.push_str("</");
128 out.push_str(tag);
129 out.push('>');
130 continue;
131 }
132 Step::Node(id, raw) => (id, raw),
133 };
134 let Some(node) = self.get_node(id) else {
135 continue;
136 };
137 match &node.data {
138 NodeData::Element {
139 tag,
140 attrs,
141 classes,
142 ..
143 } => {
144 out.push('<');
145 out.push_str(tag);
146 if !classes.is_empty() {
148 out.push_str(" class=\"");
149 let mut first = true;
150 for c in classes {
151 if !first {
152 out.push(' ');
153 }
154 first = false;
155 escape_for_attr(c, out);
156 }
157 out.push('"');
158 }
159 for (k, v) in attrs.iter().filter(|(k, _)| k.as_str() != "class") {
162 out.push(' ');
163 out.push_str(k);
164 if !v.is_empty() {
165 out.push_str("=\"");
166 escape_for_attr(v, out);
167 out.push('"');
168 } else {
169 }
173 }
174
175 if is_void_element(tag) && node.first_child.is_none() {
176 out.push_str("/>");
177 continue;
178 }
179 out.push('>');
180 stack.push(Step::EndTag(tag));
181 push_children(&mut stack, id, serializes_children_raw(tag));
182 }
183 NodeData::Text { data } => {
184 if raw_text {
185 out.push_str(data);
186 } else {
187 escape_for_text(data, out);
188 }
189 }
190 NodeData::Comment { data } => {
191 out.push_str("<!--");
192 out.push_str(data); out.push_str("-->");
194 }
195 NodeData::Fragment => push_children(&mut stack, id, false),
196 }
197 }
198 }
199}
200
201#[cfg(test)]
202mod tests {
203 use crate::Dom;
204
205 #[test]
206 fn element_without_attrs() {
207 let mut dom: Dom = Dom::new();
208 let el = dom.create_element("div");
209 assert_eq!(dom.outer_markup(el), "<div></div>");
210 }
211
212 #[test]
213 fn element_with_attrs() {
214 let mut dom: Dom = Dom::new();
215 let el = dom.create_element("div");
216 dom.set_attribute(el, "role", "banner").unwrap();
217 dom.set_attribute(el, "data-x", "1").unwrap();
218 assert_eq!(
220 dom.outer_markup(el),
221 r#"<div data-x="1" role="banner"></div>"#
222 );
223 }
224
225 #[test]
226 fn element_with_classes() {
227 let mut dom: Dom = Dom::new();
228 let el = dom.create_element("div");
229 dom.add_class(el, "two").unwrap();
230 dom.add_class(el, "one").unwrap();
231 assert_eq!(dom.outer_markup(el), r#"<div class="one two"></div>"#);
233 }
234
235 #[test]
236 fn void_tag_self_closes() {
237 let mut dom: Dom = Dom::new();
238 let el = dom.create_element("hr");
239 assert_eq!(dom.outer_markup(el), "<hr/>");
240 }
241
242 #[test]
243 fn nested_elements() {
244 let mut dom: Dom = Dom::new();
245 let div = dom.create_element("div");
246 let span = dom.create_element("span");
247 let text = dom.create_text_node("hi");
248 dom.append_child(span, text).unwrap();
249 dom.append_child(div, span).unwrap();
250 assert_eq!(dom.outer_markup(div), "<div><span>hi</span></div>");
251 }
252
253 #[test]
254 fn text_content_is_escaped() {
255 let mut dom: Dom = Dom::new();
256 let div = dom.create_element("div");
257 let t = dom.create_text_node("a & b <c>");
258 dom.append_child(div, t).unwrap();
259 assert_eq!(dom.outer_markup(div), "<div>a & b <c></div>");
260 }
261
262 #[test]
266 fn raw_text_element_children_are_not_escaped() {
267 let mut dom: Dom = Dom::new();
268 let style = dom.create_element("style");
269 let css = dom.create_text_node("a > b { content: \"<&\"; }");
270 dom.append_child(style, css).unwrap();
271 assert_eq!(
272 dom.outer_markup(style),
273 "<style>a > b { content: \"<&\"; }</style>"
274 );
275 assert_eq!(dom.inner_markup(style), "a > b { content: \"<&\"; }");
276
277 let ta = dom.create_element("textarea");
278 let t = dom.create_text_node("<b>&");
279 dom.append_child(ta, t).unwrap();
280 assert_eq!(dom.outer_markup(ta), "<textarea><b>&</textarea>");
281 }
282
283 #[test]
286 fn void_elements_are_the_html_serialization_set() {
287 let expected = [
288 "area", "base", "basefont", "bgsound", "br", "col", "embed", "frame", "hr", "img",
289 "input", "keygen", "link", "meta", "param", "source", "track", "wbr",
290 ];
291 assert_eq!(crate::VOID_ELEMENTS, &expected);
292 assert!(crate::is_void_element("br"));
293 assert!(!crate::is_void_element("vr"));
294 assert!(!crate::is_void_element("div"));
295 let mut dom: Dom = Dom::new();
296 let vr = dom.create_element("vr");
297 assert_eq!(dom.outer_markup(vr), "<vr></vr>");
298 }
299
300 #[test]
301 fn attribute_values_are_escaped() {
302 let mut dom: Dom = Dom::new();
303 let el = dom.create_element("div");
304 dom.set_attribute(el, "title", r#"she said "hi""#).unwrap();
305 assert_eq!(
306 dom.outer_markup(el),
307 r#"<div title="she said "hi""></div>"#
308 );
309 }
310
311 #[test]
312 fn comment_node_serializes() {
313 let mut dom: Dom = Dom::new();
314 let c = dom.create_comment(" note ");
315 assert_eq!(dom.outer_markup(c), "<!-- note -->");
316 }
317
318 #[test]
319 fn fragment_is_childrens_concat() {
320 let mut dom: Dom = Dom::new();
321 let frag = dom.create_document_fragment();
322 let a = dom.create_element("a");
323 let b = dom.create_element("b");
324 dom.append_child(frag, a).unwrap();
325 dom.append_child(frag, b).unwrap();
326 assert_eq!(dom.outer_markup(frag), "<a></a><b></b>");
327 }
328
329 #[test]
330 fn inner_markup_omits_wrapper() {
331 let mut dom: Dom = Dom::new();
332 let div = dom.create_element("div");
333 let span = dom.create_element("span");
334 let text = dom.create_text_node("inner");
335 dom.append_child(span, text).unwrap();
336 dom.append_child(div, span).unwrap();
337 assert_eq!(dom.inner_markup(div), "<span>inner</span>");
338 }
339
340 #[test]
341 fn boolean_attribute_emits_bare_name() {
342 let mut dom: Dom = Dom::new();
343 let el = dom.create_element("input");
344 dom.toggle_attribute(el, "disabled").unwrap(); assert_eq!(dom.outer_markup(el), "<input disabled/>");
347 }
348
349 #[test]
350 fn class_attribute_goes_through_class_list() {
351 let mut dom: Dom = Dom::new();
355 let el = dom.create_element("div");
356 dom.add_class(el, "a").unwrap();
357 dom.add_class(el, "b").unwrap();
358 assert_eq!(dom.outer_markup(el), r#"<div class="a b"></div>"#);
360 }
361
362 #[test]
363 fn deep_tree() {
364 let mut dom: Dom = Dom::new();
365 let html = dom.create_element("html");
366 let body = dom.create_element("body");
367 let h1 = dom.create_element("h1");
368 let t = dom.create_text_node("Welcome");
369 dom.append_child(h1, t).unwrap();
370 dom.append_child(body, h1).unwrap();
371 dom.append_child(html, body).unwrap();
372 assert_eq!(
373 dom.outer_markup(html),
374 "<html><body><h1>Welcome</h1></body></html>"
375 );
376 }
377}