1#[derive(Debug, Clone, PartialEq, Eq)]
15pub enum XmlNode {
16 Element(XmlElement),
18 Text(String),
20}
21
22#[derive(Debug, Clone, PartialEq, Eq)]
25pub struct XmlElement {
26 pub name: String,
29 pub attributes: Vec<(String, String)>,
31 pub children: Vec<XmlNode>,
33}
34
35impl XmlElement {
36 pub fn group(name: impl Into<String>, children: Vec<XmlNode>) -> Self {
38 XmlElement { name: name.into(), attributes: Vec::new(), children }
39 }
40 pub fn leaf(name: impl Into<String>, text: impl Into<String>) -> Self {
42 XmlElement { name: name.into(), attributes: Vec::new(), children: vec![XmlNode::Text(text.into())] }
43 }
44 pub fn empty(name: impl Into<String>) -> Self {
46 XmlElement { name: name.into(), attributes: Vec::new(), children: Vec::new() }
47 }
48 pub fn with_attr(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
50 self.attributes.push((name.into(), value.into()));
51 self
52 }
53}
54
55#[derive(Debug, Clone)]
57pub struct GenerateOptions {
58 pub xml_declaration: bool,
60 pub indent: Option<usize>,
63}
64
65impl Default for GenerateOptions {
66 fn default() -> Self {
67 GenerateOptions { xml_declaration: false, indent: None }
68 }
69}
70
71pub fn escape_text(s: &str, out: &mut String) {
74 for c in s.chars() {
75 match c {
76 '&' => out.push_str("&"),
77 '<' => out.push_str("<"),
78 '>' => out.push_str(">"),
79 _ => out.push(c),
80 }
81 }
82}
83
84pub fn escape_attr(s: &str, out: &mut String) {
87 for c in s.chars() {
88 match c {
89 '&' => out.push_str("&"),
90 '<' => out.push_str("<"),
91 '>' => out.push_str(">"),
92 '"' => out.push_str("""),
93 '\t' => out.push_str("	"),
94 '\n' => out.push_str(" "),
95 '\r' => out.push_str(" "),
96 _ => out.push(c),
97 }
98 }
99}
100
101fn write_element(el: &XmlElement, opts: &GenerateOptions, depth: usize, out: &mut String) {
102 let pretty = opts.indent.is_some();
103 let pad = |n: usize, out: &mut String| {
104 if let Some(w) = opts.indent {
105 out.push_str(&" ".repeat(w * n));
106 }
107 };
108
109 pad(depth, out);
110 out.push('<');
111 out.push_str(&el.name);
112 for (k, v) in &el.attributes {
113 out.push(' ');
114 out.push_str(k);
115 out.push_str("=\"");
116 escape_attr(v, out);
117 out.push('"');
118 }
119
120 if el.children.is_empty() {
122 out.push_str("/>");
123 if pretty {
124 out.push('\n');
125 }
126 return;
127 }
128
129 if el.children.len() == 1 {
132 if let XmlNode::Text(t) = &el.children[0] {
133 out.push('>');
134 escape_text(t, out);
135 out.push_str("</");
136 out.push_str(&el.name);
137 out.push('>');
138 if pretty {
139 out.push('\n');
140 }
141 return;
142 }
143 }
144
145 out.push('>');
146 if pretty {
147 out.push('\n');
148 }
149 for child in &el.children {
150 match child {
151 XmlNode::Element(c) => write_element(c, opts, depth + 1, out),
152 XmlNode::Text(t) => {
153 pad(depth + 1, out);
154 escape_text(t, out);
155 if pretty {
156 out.push('\n');
157 }
158 }
159 }
160 }
161 pad(depth, out);
162 out.push_str("</");
163 out.push_str(&el.name);
164 out.push('>');
165 if pretty {
166 out.push('\n');
167 }
168}
169
170pub fn generate(root: &XmlElement, opts: &GenerateOptions) -> String {
174 let mut out = String::new();
175 if opts.xml_declaration {
176 out.push_str("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
177 out.push('\n');
178 }
179 write_element(root, opts, 0, &mut out);
180 out
181}
182
183#[cfg(test)]
184mod tests {
185 use super::*;
186
187 #[test]
188 fn escape_text_and_attr() {
189 let mut t = String::new();
190 escape_text("a<b&c>d", &mut t);
191 assert_eq!(t, "a<b&c>d");
192 let mut a = String::new();
193 escape_attr("x\"y&z\tw", &mut a);
194 assert_eq!(a, "x"y&z	w");
195 }
196
197 #[test]
198 fn generate_compact_deterministic() {
199 let tree = XmlElement::group(
200 "G",
201 vec![
202 XmlNode::Element(XmlElement::leaf("NEG", "-42")),
203 XmlNode::Element(XmlElement::leaf("SPC", "a<b&c")),
204 XmlNode::Element(
205 XmlElement::group("GRP", vec![XmlNode::Element(XmlElement::leaf("X", "hi"))])
206 .with_attr("id", "1\""),
207 ),
208 XmlNode::Element(XmlElement::empty("EMPTY")),
209 ],
210 );
211 let out = generate(&tree, &GenerateOptions::default());
212 assert_eq!(
213 out,
214 "<G><NEG>-42</NEG><SPC>a<b&c</SPC><GRP id=\"1"\"><X>hi</X></GRP><EMPTY/></G>"
215 );
216 assert_eq!(out, generate(&tree, &GenerateOptions::default())); }
218
219 #[test]
220 fn generate_pretty_and_declaration() {
221 let tree = XmlElement::group("R", vec![XmlNode::Element(XmlElement::leaf("A", "1"))]);
222 let out = generate(&tree, &GenerateOptions { xml_declaration: true, indent: Some(2) });
223 assert_eq!(out, "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<R>\n <A>1</A>\n</R>\n");
224 }
225}