Skip to main content

ppt_rs/core/
xml_utils.rs

1//! XML utility functions
2//!
3//! Centralized XML utilities to avoid duplication across modules.
4
5use std::fmt::Write;
6
7/// Escape special XML characters
8pub fn escape_xml(s: &str) -> String {
9    s.replace('&', "&")
10        .replace('<', "&lt;")
11        .replace('>', "&gt;")
12        .replace('"', "&quot;")
13        .replace('\'', "&apos;")
14}
15
16/// Append a decimal integer without allocating a temporary `format!` string.
17pub fn append_usize(buf: &mut String, value: usize) {
18    let _ = write!(buf, "{value}");
19}
20
21/// Append a signed integer without allocating a temporary `format!` string.
22pub fn append_i32(buf: &mut String, value: i32) {
23    let _ = write!(buf, "{value}");
24}
25
26/// XML writer helper for building XML strings efficiently
27pub struct XmlWriter {
28    buffer: String,
29    indent_level: usize,
30    indent_str: &'static str,
31}
32
33impl XmlWriter {
34    pub fn new() -> Self {
35        Self {
36            buffer: String::new(),
37            indent_level: 0,
38            indent_str: "  ",
39        }
40    }
41
42    pub fn with_capacity(capacity: usize) -> Self {
43        Self {
44            buffer: String::with_capacity(capacity),
45            indent_level: 0,
46            indent_str: "  ",
47        }
48    }
49
50    /// Write XML declaration
51    pub fn xml_declaration(&mut self) -> &mut Self {
52        self.buffer
53            .push_str(r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>"#);
54        self.buffer.push('\n');
55        self
56    }
57
58    /// Start an element with attributes
59    pub fn start_element(&mut self, name: &str, attrs: &[(&str, &str)]) -> &mut Self {
60        self.buffer.push('<');
61        self.buffer.push_str(name);
62        for (key, value) in attrs {
63            self.buffer.push(' ');
64            self.buffer.push_str(key);
65            self.buffer.push_str("=\"");
66            self.buffer.push_str(&escape_xml(value));
67            self.buffer.push('"');
68        }
69        self.buffer.push('>');
70        self.indent_level += 1;
71        self
72    }
73
74    /// End an element
75    pub fn end_element(&mut self, name: &str) -> &mut Self {
76        self.indent_level = self.indent_level.saturating_sub(1);
77        self.buffer.push_str("</");
78        self.buffer.push_str(name);
79        self.buffer.push('>');
80        self
81    }
82
83    /// Write a self-closing element
84    pub fn empty_element(&mut self, name: &str, attrs: &[(&str, &str)]) -> &mut Self {
85        self.buffer.push('<');
86        self.buffer.push_str(name);
87        for (key, value) in attrs {
88            self.buffer.push(' ');
89            self.buffer.push_str(key);
90            self.buffer.push_str("=\"");
91            self.buffer.push_str(&escape_xml(value));
92            self.buffer.push('"');
93        }
94        self.buffer.push_str("/>");
95        self
96    }
97
98    /// Write text content
99    pub fn text(&mut self, content: &str) -> &mut Self {
100        self.buffer.push_str(&escape_xml(content));
101        self
102    }
103
104    /// Write raw XML (no escaping)
105    pub fn raw(&mut self, xml: &str) -> &mut Self {
106        self.buffer.push_str(xml);
107        self
108    }
109
110    /// Get the built XML string
111    pub fn finish(self) -> String {
112        self.buffer
113    }
114
115    /// Get a reference to the buffer
116    pub fn as_str(&self) -> &str {
117        &self.buffer
118    }
119}
120
121impl Default for XmlWriter {
122    fn default() -> Self {
123        Self::new()
124    }
125}
126
127#[cfg(test)]
128mod tests {
129    use super::*;
130
131    #[test]
132    fn test_escape_xml() {
133        assert_eq!(escape_xml("a & b"), "a &amp; b");
134        assert_eq!(escape_xml("<tag>"), "&lt;tag&gt;");
135        assert_eq!(escape_xml("\"quoted\""), "&quot;quoted&quot;");
136    }
137
138    #[test]
139    fn test_xml_writer() {
140        let mut writer = XmlWriter::new();
141        writer
142            .start_element("root", &[("attr", "value")])
143            .text("content")
144            .end_element("root");
145        assert_eq!(writer.finish(), r#"<root attr="value">content</root>"#);
146    }
147
148    #[test]
149    fn test_xml_writer_empty_element() {
150        let mut writer = XmlWriter::new();
151        writer.empty_element("br", &[]);
152        assert_eq!(writer.finish(), "<br/>");
153    }
154
155    #[test]
156    fn test_append_usize() {
157        let mut buf = String::new();
158        append_usize(&mut buf, 42);
159        assert_eq!(buf, "42");
160    }
161}