Skip to main content

domtree/serialize/
html.rs

1//! Re-serialize a parsed tree back to HTML.
2//!
3//! Iterative (explicit stack), so it handles arbitrarily deep trees without
4//! risking a stack overflow. Text and attribute values are escaped; the content
5//! of raw-text elements (`<script>`/`<style>`) is emitted verbatim, so a
6//! `parse` → `to_html` → `parse` round-trip is stable.
7
8use alloc::string::String;
9use alloc::vec::Vec;
10
11use crate::node::NodeKind;
12use crate::tags;
13use crate::tree::{Dom, NodeRef};
14
15impl Dom {
16    /// Serialize the whole document back to an HTML string.
17    ///
18    /// ```
19    /// let dom = domtree::parse("<p class='x'>1 &lt; 2 &amp; 3</p>");
20    /// assert_eq!(dom.to_html(), r#"<p class="x">1 &lt; 2 &amp; 3</p>"#);
21    /// ```
22    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    /// Serialize just this node and its subtree back to an HTML string.
35    ///
36    /// ```
37    /// let dom = domtree::parse("<div><img src=a.png><br></div>");
38    /// let div = dom.find_by_tag("div").next().unwrap();
39    /// assert_eq!(div.to_html(), r#"<div><img src="a.png"><br></div>"#);
40    /// ```
41    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; // void elements have no children and no end tag
85            }
86            stack.push(Step::Close(tag));
87            // Push children in reverse so they emit in document order.
88            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            // Raw-text element content (script/style) is emitted verbatim.
96            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("&amp;"),
123            '<' => out.push_str("&lt;"),
124            '>' => out.push_str("&gt;"),
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("&amp;"),
134            '"' => out.push_str("&quot;"),
135            c => out.push(c),
136        }
137    }
138}