zerodds_xml_wire/
emitter.rs1use alloc::string::String;
9use alloc::vec::Vec;
10
11#[derive(Debug, Clone, PartialEq, Eq)]
13pub enum EmitError {
14 UnbalancedEnd,
16 InvalidTagName(String),
18}
19
20impl core::fmt::Display for EmitError {
21 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
22 match self {
23 Self::UnbalancedEnd => f.write_str("end_element without matching start"),
24 Self::InvalidTagName(s) => write!(f, "invalid tag name `{s}`"),
25 }
26 }
27}
28
29#[cfg(feature = "std")]
30impl std::error::Error for EmitError {}
31
32#[derive(Debug, Default, Clone, PartialEq, Eq)]
34pub struct XmlEmitter {
35 buf: String,
36 stack: Vec<String>,
37}
38
39impl XmlEmitter {
40 #[must_use]
42 pub fn new() -> Self {
43 Self::default()
44 }
45
46 pub fn declaration(&mut self) {
48 self.buf
49 .push_str(r#"<?xml version="1.0" encoding="UTF-8"?>"#);
50 }
51
52 pub fn start_element(&mut self, name: &str, attrs: &[(&str, &str)]) -> Result<(), EmitError> {
57 if !is_valid_name(name) {
58 return Err(EmitError::InvalidTagName(name.into()));
59 }
60 self.buf.push('<');
61 self.buf.push_str(name);
62 for (k, v) in attrs {
63 self.buf.push(' ');
64 self.buf.push_str(k);
65 self.buf.push_str("=\"");
66 encode_to(&mut self.buf, v);
67 self.buf.push('"');
68 }
69 self.buf.push('>');
70 self.stack.push(name.into());
71 Ok(())
72 }
73
74 pub fn end_element(&mut self) -> Result<(), EmitError> {
79 let name = self.stack.pop().ok_or(EmitError::UnbalancedEnd)?;
80 self.buf.push_str("</");
81 self.buf.push_str(&name);
82 self.buf.push('>');
83 Ok(())
84 }
85
86 pub fn empty_element(&mut self, name: &str, attrs: &[(&str, &str)]) -> Result<(), EmitError> {
91 if !is_valid_name(name) {
92 return Err(EmitError::InvalidTagName(name.into()));
93 }
94 self.buf.push('<');
95 self.buf.push_str(name);
96 for (k, v) in attrs {
97 self.buf.push(' ');
98 self.buf.push_str(k);
99 self.buf.push_str("=\"");
100 encode_to(&mut self.buf, v);
101 self.buf.push('"');
102 }
103 self.buf.push_str("/>");
104 Ok(())
105 }
106
107 pub fn text(&mut self, content: &str) {
109 encode_to(&mut self.buf, content);
110 }
111
112 pub fn cdata(&mut self, content: &str) {
114 self.buf.push_str("<![CDATA[");
115 let safe = content.replace("]]>", "]]]]><![CDATA[>");
117 self.buf.push_str(&safe);
118 self.buf.push_str("]]>");
119 }
120
121 #[must_use]
123 pub fn finish(self) -> String {
124 self.buf
125 }
126
127 #[must_use]
129 pub fn len(&self) -> usize {
130 self.buf.len()
131 }
132
133 #[must_use]
135 pub fn is_empty(&self) -> bool {
136 self.buf.is_empty()
137 }
138}
139
140fn encode_to(buf: &mut String, s: &str) {
141 for c in s.chars() {
142 match c {
143 '&' => buf.push_str("&"),
144 '<' => buf.push_str("<"),
145 '>' => buf.push_str(">"),
146 '"' => buf.push_str("""),
147 '\'' => buf.push_str("'"),
148 _ => buf.push(c),
149 }
150 }
151}
152
153fn is_valid_name(s: &str) -> bool {
154 let mut chars = s.chars();
155 match chars.next() {
156 Some(c) if c.is_alphabetic() || c == '_' => {}
157 _ => return false,
158 }
159 chars.all(|c| c.is_alphanumeric() || c == '_' || c == '-' || c == '.' || c == ':')
160}
161
162#[cfg(test)]
163#[allow(clippy::expect_used, clippy::unwrap_used, clippy::panic)]
164mod tests {
165 use super::*;
166
167 #[test]
168 fn declaration_emits_xml_pi() {
169 let mut e = XmlEmitter::new();
170 e.declaration();
171 assert!(e.finish().starts_with("<?xml"));
172 }
173
174 #[test]
175 fn start_text_end_round_trip() {
176 let mut e = XmlEmitter::new();
177 e.start_element("a", &[]).unwrap();
178 e.text("hello");
179 e.end_element().unwrap();
180 assert_eq!(e.finish(), "<a>hello</a>");
181 }
182
183 #[test]
184 fn attributes_get_encoded() {
185 let mut e = XmlEmitter::new();
186 e.empty_element("a", &[("k", "v&\"")]).unwrap();
187 let out = e.finish();
188 assert!(out.contains("k=\"v&"\""));
189 }
190
191 #[test]
192 fn text_entities_encoded() {
193 let mut e = XmlEmitter::new();
194 e.start_element("a", &[]).unwrap();
195 e.text("<b&c>");
196 e.end_element().unwrap();
197 assert_eq!(e.finish(), "<a><b&c></a>");
198 }
199
200 #[test]
201 fn cdata_splits_terminator() {
202 let mut e = XmlEmitter::new();
203 e.cdata("contains ]]> within");
204 let out = e.finish();
205 assert!(out.contains("]]]]><![CDATA[>"));
207 }
208
209 #[test]
210 fn unbalanced_end_rejected() {
211 let mut e = XmlEmitter::new();
212 assert!(e.end_element().is_err());
213 }
214
215 #[test]
216 fn invalid_tag_name_rejected() {
217 let mut e = XmlEmitter::new();
218 assert!(matches!(
219 e.start_element("123abc", &[]),
220 Err(EmitError::InvalidTagName(_))
221 ));
222 assert!(matches!(
223 e.start_element("", &[]),
224 Err(EmitError::InvalidTagName(_))
225 ));
226 }
227
228 #[test]
229 fn empty_element_self_closes() {
230 let mut e = XmlEmitter::new();
231 e.empty_element("br", &[]).unwrap();
232 assert_eq!(e.finish(), "<br/>");
233 }
234
235 #[test]
236 fn nested_elements_emit_correctly() {
237 let mut e = XmlEmitter::new();
238 e.start_element("a", &[]).unwrap();
239 e.start_element("b", &[]).unwrap();
240 e.text("x");
241 e.end_element().unwrap();
242 e.end_element().unwrap();
243 assert_eq!(e.finish(), "<a><b>x</b></a>");
244 }
245}