Skip to main content

odox_core/xml/
write.rs

1//! Turning an [`Element`] tree back into the bytes of a part.
2//!
3//! Written as strings rather than through a serializer, which is what
4//! `waddle-core` already does for the same reason: the output of an office
5//! package is structural, the escaping rules are four characters wide, and a
6//! writer that is a loop over a tree is a writer whose output can be read.
7//
8// Author: David M. Anderson
9// Built with AI assistance (Claude, Anthropic)
10
11use super::{Element, Name, Node};
12
13/// Serialize a part, declaration included.
14pub fn serialize(root: &Element) -> Vec<u8> {
15    let mut out = String::with_capacity(4096);
16    out.push_str("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n");
17    element(&mut out, root);
18    out.into_bytes()
19}
20
21fn element(out: &mut String, e: &Element) {
22    out.push('<');
23    name(out, &e.name);
24    for attr in &e.attrs {
25        out.push(' ');
26        name(out, &attr.name);
27        out.push_str("=\"");
28        escape_attribute(out, &attr.value);
29        out.push('"');
30    }
31    if e.children.is_empty() && e.self_closing {
32        out.push_str("/>");
33        return;
34    }
35    out.push('>');
36    for child in &e.children {
37        match child {
38            Node::Element(child) => element(out, child),
39            Node::Text(t) => escape_text(out, t),
40            Node::CData(t) => {
41                out.push_str("<![CDATA[");
42                out.push_str(t);
43                out.push_str("]]>");
44            }
45            Node::Comment(t) => {
46                out.push_str("<!--");
47                out.push_str(t);
48                out.push_str("-->");
49            }
50            Node::ProcessingInstruction(t) => {
51                out.push_str("<?");
52                out.push_str(t);
53                out.push_str("?>");
54            }
55        }
56    }
57    out.push_str("</");
58    name(out, &e.name);
59    out.push('>');
60}
61
62fn name(out: &mut String, n: &Name) {
63    if let Some(prefix) = &n.prefix {
64        out.push_str(prefix);
65        out.push(':');
66    }
67    out.push_str(&n.local);
68}
69
70fn escape_text(out: &mut String, text: &str) {
71    for c in text.chars() {
72        match c {
73            '&' => out.push_str("&amp;"),
74            '<' => out.push_str("&lt;"),
75            // Not required by the specification outside `]]>`, and written
76            // anyway because every office application writes it and a part that
77            // differs from the one beside it only in this is a part somebody has
78            // to diff twice.
79            '>' => out.push_str("&gt;"),
80            _ => out.push(c),
81        }
82    }
83}
84
85fn escape_attribute(out: &mut String, value: &str) {
86    for c in value.chars() {
87        match c {
88            '&' => out.push_str("&amp;"),
89            '<' => out.push_str("&lt;"),
90            '>' => out.push_str("&gt;"),
91            '"' => out.push_str("&quot;"),
92            // A literal tab, newline or carriage return in an attribute value
93            // is normalized to a space by any conforming parser, so the
94            // character has to be written as a reference to survive being read
95            // back. A cell's formula and a presentation's animation parameters
96            // both carry newlines.
97            '\t' => out.push_str("&#9;"),
98            '\n' => out.push_str("&#10;"),
99            '\r' => out.push_str("&#13;"),
100            _ => out.push(c),
101        }
102    }
103}