Skip to main content

domtree/serialize/
json.rs

1//! Zero-dependency JSON serializer.
2//!
3//! Iterative (explicit stack) so it can serialize arbitrarily deep trees
4//! without risking a stack overflow — keeping the "never panics" promise even
5//! on pathological input.
6
7use alloc::string::String;
8use alloc::vec::Vec;
9use core::fmt::Write as _;
10
11use crate::node::NodeKind;
12use crate::tree::{Dom, NodeRef};
13
14impl Dom {
15    /// Serialize the document to a compact JSON string.
16    ///
17    /// The shape is self-describing: every node is an object with a `"type"` of
18    /// `element` / `text` / `comment` / `doctype`, elements carry `tag`,
19    /// `attrs` (an object) and `children` (an array), and the whole document is
20    /// wrapped in `{ "type": "document", "errors": N, "children": [...] }`.
21    ///
22    /// ```
23    /// let dom = domtree::parse("<a href=\"/x\">hi</a>");
24    /// let json = dom.to_json();
25    /// assert!(json.contains("\"tag\":\"a\""));
26    /// assert!(json.contains("\"href\":\"/x\""));
27    /// assert!(json.contains("\"value\":\"hi\""));
28    /// ```
29    pub fn to_json(&self) -> String {
30        let mut out = String::with_capacity(self.len() * 24 + 32);
31        out.push_str("{\"type\":\"document\",\"errors\":");
32        let _ = write!(out, "{}", self.errors().len());
33        out.push_str(",\"children\":");
34        write_forest(&mut out, self);
35        out.push('}');
36        out
37    }
38}
39
40impl<'a> NodeRef<'a> {
41    /// Serialize just this node and its subtree to a compact JSON string,
42    /// using the same shape as [`Dom::to_json`].
43    ///
44    /// ```
45    /// let dom = domtree::parse("<div><b>hi</b></div>");
46    /// let b = dom.find_by_tag("b").next().unwrap();
47    /// assert_eq!(b.to_json(), r#"{"type":"element","tag":"b","attrs":{},"children":[{"type":"text","value":"hi"}]}"#);
48    /// ```
49    pub fn to_json(&self) -> String {
50        let mut out = String::new();
51        let mut stack: Vec<Step<'a>> = Vec::new();
52        stack.push(Step::Open(*self));
53        process(&mut out, &mut stack);
54        out
55    }
56}
57
58enum Step<'a> {
59    Open(NodeRef<'a>),
60    Lit(&'static str),
61}
62
63fn write_forest(out: &mut String, dom: &Dom) {
64    out.push('[');
65    let mut stack: Vec<Step<'_>> = Vec::new();
66    // Push roots in reverse so they pop (and serialize) in document order.
67    for r in dom.roots().collect::<Vec<_>>().into_iter().rev() {
68        stack.push(Step::Open(r));
69    }
70    process(out, &mut stack);
71    out.push(']');
72}
73
74fn process(out: &mut String, stack: &mut Vec<Step<'_>>) {
75    while let Some(step) = stack.pop() {
76        match step {
77            Step::Lit(s) => out.push_str(s),
78            Step::Open(node) => write_open(out, node, stack),
79        }
80    }
81}
82
83fn write_open<'a>(out: &mut String, node: NodeRef<'a>, stack: &mut Vec<Step<'a>>) {
84    maybe_comma(out);
85    match node.kind() {
86        NodeKind::Element { tag } => {
87            out.push_str("{\"type\":\"element\",\"tag\":");
88            write_str(out, tag);
89            out.push_str(",\"attrs\":{");
90            let mut first = true;
91            for (k, v) in node.attributes() {
92                if !first {
93                    out.push(',');
94                }
95                first = false;
96                write_str(out, k);
97                out.push(':');
98                write_str(out, v);
99            }
100            out.push_str("},\"children\":[");
101            // Close the children array + the object once the subtree is drained.
102            stack.push(Step::Lit("]}"));
103            // Push children in reverse (walk last_child -> prev_sibling) so they
104            // pop in document order. No allocation.
105            let mut child = node.last_child();
106            while let Some(c) = child {
107                stack.push(Step::Open(c));
108                child = c.prev_sibling();
109            }
110        }
111        NodeKind::Text(t) => {
112            out.push_str("{\"type\":\"text\",\"value\":");
113            write_str(out, t);
114            out.push('}');
115        }
116        NodeKind::Comment(c) => {
117            out.push_str("{\"type\":\"comment\",\"value\":");
118            write_str(out, c);
119            out.push('}');
120        }
121        NodeKind::Doctype(d) => {
122            out.push_str("{\"type\":\"doctype\",\"value\":");
123            write_str(out, d);
124            out.push('}');
125        }
126    }
127}
128
129/// Emit a separating comma unless we're at the very start of the buffer or
130/// right after an array opener (`[`) — the only places a node is written
131/// without a preceding sibling.
132fn maybe_comma(out: &mut String) {
133    if !out.is_empty() && out.as_bytes().last() != Some(&b'[') {
134        out.push(',');
135    }
136}
137
138/// Write a JSON string literal with RFC 8259 escaping.
139fn write_str(out: &mut String, s: &str) {
140    out.push('"');
141    for c in s.chars() {
142        match c {
143            '"' => out.push_str("\\\""),
144            '\\' => out.push_str("\\\\"),
145            '\n' => out.push_str("\\n"),
146            '\r' => out.push_str("\\r"),
147            '\t' => out.push_str("\\t"),
148            '\u{08}' => out.push_str("\\b"),
149            '\u{0c}' => out.push_str("\\f"),
150            c if (c as u32) < 0x20 => {
151                out.push_str("\\u");
152                push_hex4(out, c as u32);
153            }
154            c => out.push(c),
155        }
156    }
157    out.push('"');
158}
159
160fn push_hex4(out: &mut String, v: u32) {
161    const HEX: [u8; 16] = *b"0123456789abcdef";
162    for shift in [12u32, 8, 4, 0] {
163        let nib = ((v >> shift) & 0xf) as usize;
164        out.push(HEX[nib] as char);
165    }
166}