Skip to main content

rusty_xml_writer/
lib.rs

1//! xmlsave + xmlTextWriter matching libxml2 `xmlsave.h` / `xmlwriter.h` for M2.
2
3#![forbid(unsafe_code)]
4
5use rusty_xml_tree::{NodeId, NodeKind, XmlDoc};
6
7/// libxml2 `xmlSaveOption` bits.
8pub const XML_SAVE_FORMAT: i32 = 1 << 0;
9pub const XML_SAVE_NO_DECL: i32 = 1 << 1;
10pub const XML_SAVE_NO_EMPTY: i32 = 1 << 2;
11pub const XML_SAVE_NO_XHTML: i32 = 1 << 3;
12pub const XML_SAVE_XHTML: i32 = 1 << 4;
13pub const XML_SAVE_AS_XML: i32 = 1 << 5;
14pub const XML_SAVE_AS_HTML: i32 = 1 << 6;
15pub const XML_SAVE_WSNONSIG: i32 = 1 << 7;
16pub const XML_SAVE_EMPTY: i32 = 1 << 8;
17pub const XML_SAVE_NO_INDENT: i32 = 1 << 9;
18pub const XML_SAVE_INDENT: i32 = 1 << 10;
19
20fn hex_ref(c: u32) -> String {
21    format!("&#x{c:X};")
22}
23
24fn escape_text(s: &str, attr: bool, non_ascii: bool) -> String {
25    let mut out = String::new();
26    for c in s.chars() {
27        let u = c as u32;
28        if non_ascii && u >= 0x80 {
29            out.push_str(&hex_ref(u));
30            continue;
31        }
32        match c {
33            '&' => out.push_str("&amp;"),
34            '<' => out.push_str("&lt;"),
35            '>' => out.push_str("&gt;"),
36            '"' if attr => out.push_str("&quot;"),
37            '\r' => out.push_str("&#13;"),
38            '\t' if attr => out.push_str("&#9;"),
39            '\n' if attr => out.push_str("&#10;"),
40            c if u < 0x20 && c != '\t' && c != '\n' => out.push_str(&hex_ref(0xfffd)),
41            c => out.push(c),
42        }
43    }
44    out
45}
46
47fn qname(prefix: Option<&str>, local: &str) -> String {
48    match prefix {
49        Some(p) if !p.is_empty() => format!("{p}:{local}"),
50        _ => local.to_string(),
51    }
52}
53
54fn indent_unit() -> String {
55    std::env::var("XMLLINT_INDENT").unwrap_or_else(|_| "  ".into())
56}
57
58fn write_indent(out: &mut String, level: i32) {
59    let unit = indent_unit();
60    for _ in 0..level {
61        out.push_str(&unit);
62    }
63}
64
65fn write_node(doc: &XmlDoc, id: NodeId, out: &mut String, opts: i32, level: i32, format: bool) {
66    match doc.kind(id) {
67        NodeKind::Element => {
68            out.push('<');
69            out.push_str(&qname(doc.prefix(id), doc.name(id)));
70            for (pre, href) in doc.ns_defs(id) {
71                out.push_str(" xmlns");
72                if let Some(p) = pre {
73                    out.push(':');
74                    out.push_str(p);
75                }
76                let non_ascii = doc.encoding.is_none();
77                out.push_str("=\"");
78                out.push_str(&escape_text(href, true, non_ascii));
79                out.push('"');
80            }
81            for a in doc.attrs(id) {
82                out.push(' ');
83                out.push_str(&qname(doc.prefix(a), doc.name(a)));
84                out.push_str("=\"");
85                out.push_str(&escape_text(
86                    doc.content(a),
87                    true,
88                    doc.encoding.is_none(),
89                ));
90                out.push('"');
91            }
92            let has_kids = doc.first_child(id).is_some();
93            if !has_kids {
94                if (opts & XML_SAVE_NO_EMPTY) == 0 {
95                    out.push_str("/>");
96                } else {
97                    out.push_str("></");
98                    out.push_str(&qname(doc.prefix(id), doc.name(id)));
99                    out.push('>');
100                }
101                return;
102            }
103            let mixed = {
104                let mut c = doc.first_child(id);
105                let mut m = false;
106                while let Some(ch) = c {
107                    if matches!(doc.kind(ch), NodeKind::Text | NodeKind::CData) {
108                        m = true;
109                        break;
110                    }
111                    c = doc.next_sibling(ch);
112                }
113                m
114            };
115            out.push('>');
116            let child_format = format && !mixed;
117            if child_format {
118                out.push('\n');
119            }
120            let mut c = doc.first_child(id);
121            while let Some(ch) = c {
122                if child_format {
123                    write_indent(out, level + 1);
124                }
125                write_node(doc, ch, out, opts, level + 1, child_format);
126                if child_format {
127                    out.push('\n');
128                }
129                c = doc.next_sibling(ch);
130            }
131            if child_format {
132                write_indent(out, level);
133            }
134            out.push_str("</");
135            out.push_str(&qname(doc.prefix(id), doc.name(id)));
136            out.push('>');
137        }
138        NodeKind::Text => {
139            out.push_str(&escape_text(
140                doc.content(id),
141                false,
142                doc.encoding.is_none(),
143            ));
144        }
145        NodeKind::CData => {
146            let content = doc.content(id);
147            if content.is_empty() {
148                out.push_str("<![CDATA[]]>");
149            } else {
150                // Split on ]]> like C.
151                let bytes = content.as_bytes();
152                let mut start = 0usize;
153                let mut i = 0usize;
154                while i + 2 < bytes.len() {
155                    if bytes[i] == b']' && bytes[i + 1] == b']' && bytes[i + 2] == b'>' {
156                        out.push_str("<![CDATA[");
157                        out.push_str(&content[start..=i + 1]);
158                        out.push_str("]]>");
159                        start = i + 2;
160                        i += 2;
161                    }
162                    i += 1;
163                }
164                if start < content.len() {
165                    out.push_str("<![CDATA[");
166                    out.push_str(&content[start..]);
167                    out.push_str("]]>");
168                }
169            }
170        }
171        NodeKind::Comment => {
172            let _ = (format, level);
173            out.push_str("<!--");
174            out.push_str(doc.content(id));
175            out.push_str("-->");
176        }
177        NodeKind::Pi => {
178            let _ = (format, level);
179            out.push_str("<?");
180            out.push_str(doc.name(id));
181            if !doc.content(id).is_empty() {
182                out.push(' ');
183                out.push_str(doc.content(id));
184            }
185            out.push_str("?>");
186        }
187        _ => {}
188    }
189}
190
191/// `xmlSaveDoc` / `xmlDocDumpMemory` with `xmlSaveOption` bits.
192#[doc(alias = "xmlSaveDoc")]
193pub fn xml_save_doc(doc: &XmlDoc, options: i32) -> Vec<u8> {
194    let mut out = String::new();
195    if (options & XML_SAVE_NO_DECL) == 0 {
196        out.push_str("<?xml version=\"");
197        out.push_str(if doc.version.is_empty() {
198            "1.0"
199        } else {
200            &doc.version
201        });
202        out.push('"');
203        if let Some(enc) = &doc.encoding {
204            out.push_str(" encoding=\"");
205            out.push_str(enc);
206            out.push('"');
207        }
208        match doc.standalone {
209            Some(true) => out.push_str(" standalone=\"yes\""),
210            Some(false) => out.push_str(" standalone=\"no\""),
211            None => {}
212        }
213        out.push_str("?>\n");
214    }
215    let format = (options & XML_SAVE_FORMAT) != 0;
216    let mut child = doc.first_child(rusty_xml_tree::NodeId::DOCUMENT);
217    while let Some(id) = child {
218        write_node(doc, id, &mut out, options, 0, format);
219        out.push('\n');
220        child = doc.next_sibling(id);
221    }
222    out.into_bytes()
223}
224
225/// `xmlDocDumpFormatMemory`.
226#[doc(alias = "xmlDocDumpFormatMemory")]
227pub fn xml_doc_dump_format_memory(doc: &XmlDoc, format: bool) -> Vec<u8> {
228    xml_save_doc(doc, if format { XML_SAVE_FORMAT } else { 0 })
229}
230
231/// `xmlDocDumpMemory`.
232#[doc(alias = "xmlDocDumpMemory")]
233pub fn xml_doc_dump_memory(doc: &XmlDoc) -> Vec<u8> {
234    xml_save_doc(doc, 0)
235}
236
237/// `xmlNodeDump` of a subtree (no XML declaration).
238#[doc(alias = "xmlNodeDump")]
239pub fn xml_node_dump(doc: &XmlDoc, node: NodeId, options: i32) -> Vec<u8> {
240    let mut out = String::new();
241    write_node(doc, node, &mut out, options, 0, (options & XML_SAVE_FORMAT) != 0);
242    out.into_bytes()
243}
244
245#[derive(Clone, Copy, PartialEq, Eq)]
246enum FrameKind {
247    Document,
248    Element,
249}
250
251struct Frame {
252    kind: FrameKind,
253    name: String,
254    prefix: Option<String>,
255    open: bool,
256    has_content: bool,
257}
258
259/// `xmlTextWriter` writing into an in-memory buffer.
260pub struct XmlTextWriter {
261    buf: String,
262    stack: Vec<Frame>,
263    indent: bool,
264    indent_unit: String,
265    started: bool,
266}
267
268impl Default for XmlTextWriter {
269    fn default() -> Self {
270        Self::xml_new_text_writer_memory()
271    }
272}
273
274impl XmlTextWriter {
275    /// `xmlNewTextWriterMemory`.
276    #[doc(alias = "xmlNewTextWriterMemory")]
277    pub fn xml_new_text_writer_memory() -> Self {
278        Self {
279            buf: String::new(),
280            stack: Vec::new(),
281            indent: false,
282            indent_unit: indent_unit(),
283            started: false,
284        }
285    }
286
287    pub fn set_indent(&mut self, indent: bool) {
288        self.indent = indent;
289    }
290
291    fn close_start_tag(&mut self) {
292        if let Some(f) = self.stack.last_mut() {
293            if f.kind == FrameKind::Element && !f.open {
294                self.buf.push('>');
295                f.open = true;
296                f.has_content = true;
297            }
298        }
299    }
300
301    /// `xmlTextWriterStartDocument`.
302    #[doc(alias = "xmlTextWriterStartDocument")]
303    pub fn start_document(
304        &mut self,
305        version: Option<&str>,
306        encoding: Option<&str>,
307        standalone: Option<&str>,
308    ) -> Result<(), String> {
309        self.buf.push_str("<?xml version=\"");
310        self.buf.push_str(version.unwrap_or("1.0"));
311        self.buf.push('"');
312        if let Some(e) = encoding {
313            self.buf.push_str(" encoding=\"");
314            self.buf.push_str(e);
315            self.buf.push('"');
316        }
317        if let Some(s) = standalone {
318            self.buf.push_str(" standalone=\"");
319            self.buf.push_str(s);
320            self.buf.push('"');
321        }
322        self.buf.push_str("?>\n");
323        self.stack.push(Frame {
324            kind: FrameKind::Document,
325            name: "#document".into(),
326            prefix: None,
327            open: true,
328            has_content: true,
329        });
330        self.started = true;
331        Ok(())
332    }
333
334    /// `xmlTextWriterStartElement`.
335    #[doc(alias = "xmlTextWriterStartElement")]
336    pub fn start_element(&mut self, name: &str) -> Result<(), String> {
337        self.start_element_ns(None, name, None)
338    }
339
340    /// `xmlTextWriterStartElementNS`.
341    #[doc(alias = "xmlTextWriterStartElementNS")]
342    pub fn start_element_ns(
343        &mut self,
344        prefix: Option<&str>,
345        name: &str,
346        ns_uri: Option<&str>,
347    ) -> Result<(), String> {
348        self.close_start_tag();
349        if self.indent && !self.buf.is_empty() && !self.buf.ends_with('\n') {
350            self.buf.push('\n');
351        }
352        if self.indent {
353            let depth = self.stack.iter().filter(|f| f.kind == FrameKind::Element).count();
354            for _ in 0..depth {
355                self.buf.push_str(&self.indent_unit);
356            }
357        }
358        self.buf.push('<');
359        self.buf.push_str(&qname(prefix, name));
360        if let Some(uri) = ns_uri {
361            if let Some(p) = prefix {
362                self.buf.push_str(" xmlns:");
363                self.buf.push_str(p);
364            } else {
365                self.buf.push_str(" xmlns");
366            }
367            self.buf.push_str("=\"");
368            self.buf.push_str(&escape_text(uri, true, false));
369            self.buf.push('"');
370        }
371        self.stack.push(Frame {
372            kind: FrameKind::Element,
373            name: name.to_string(),
374            prefix: prefix.map(str::to_string),
375            open: false,
376            has_content: false,
377        });
378        Ok(())
379    }
380
381    /// `xmlTextWriterWriteAttribute`.
382    #[doc(alias = "xmlTextWriterWriteAttribute")]
383    pub fn write_attribute(&mut self, name: &str, value: &str) -> Result<(), String> {
384        let top = self.stack.last().ok_or_else(|| "no open element".to_string())?;
385        if top.kind != FrameKind::Element || top.open {
386            return Err("attribute after element content".into());
387        }
388        self.buf.push(' ');
389        self.buf.push_str(name);
390        self.buf.push_str("=\"");
391        self.buf.push_str(&escape_text(value, true, false));
392        self.buf.push('"');
393        Ok(())
394    }
395
396    /// `xmlTextWriterWriteAttributeNS`.
397    #[doc(alias = "xmlTextWriterWriteAttributeNS")]
398    pub fn write_attribute_ns(
399        &mut self,
400        prefix: Option<&str>,
401        name: &str,
402        _ns_uri: Option<&str>,
403        value: &str,
404    ) -> Result<(), String> {
405        self.write_attribute(&qname(prefix, name), value)
406    }
407
408    /// `xmlTextWriterWriteString`.
409    #[doc(alias = "xmlTextWriterWriteString")]
410    pub fn write_string(&mut self, content: &str) -> Result<(), String> {
411        self.close_start_tag();
412        self.buf.push_str(&escape_text(content, false, false));
413        Ok(())
414    }
415
416    /// `xmlTextWriterWriteComment`.
417    #[doc(alias = "xmlTextWriterWriteComment")]
418    pub fn write_comment(&mut self, content: &str) -> Result<(), String> {
419        self.close_start_tag();
420        self.buf.push_str("<!--");
421        self.buf.push_str(content);
422        self.buf.push_str("-->");
423        Ok(())
424    }
425
426    /// `xmlTextWriterWritePI`.
427    #[doc(alias = "xmlTextWriterWritePI")]
428    pub fn write_pi(&mut self, target: &str, data: Option<&str>) -> Result<(), String> {
429        self.close_start_tag();
430        self.buf.push_str("<?");
431        self.buf.push_str(target);
432        if let Some(d) = data {
433            self.buf.push(' ');
434            self.buf.push_str(d);
435        }
436        self.buf.push_str("?>");
437        Ok(())
438    }
439
440    /// `xmlTextWriterWriteCDATA`.
441    #[doc(alias = "xmlTextWriterWriteCDATA")]
442    pub fn write_cdata(&mut self, content: &str) -> Result<(), String> {
443        self.close_start_tag();
444        self.buf.push_str("<![CDATA[");
445        self.buf.push_str(content);
446        self.buf.push_str("]]>");
447        Ok(())
448    }
449
450    /// `xmlTextWriterWriteRaw`.
451    #[doc(alias = "xmlTextWriterWriteRaw")]
452    pub fn write_raw(&mut self, content: &str) -> Result<(), String> {
453        self.close_start_tag();
454        self.buf.push_str(content);
455        Ok(())
456    }
457
458    /// `xmlTextWriterEndElement`.
459    #[doc(alias = "xmlTextWriterEndElement")]
460    pub fn end_element(&mut self) -> Result<(), String> {
461        let f = self.stack.pop().ok_or_else(|| "no open element".to_string())?;
462        if f.kind != FrameKind::Element {
463            return Err("end_element on document".into());
464        }
465        if !f.open {
466            self.buf.push_str("/>");
467        } else {
468            if self.indent && f.has_content {
469                // keep compact unless we wrote nested elements with indent
470            }
471            self.buf.push_str("</");
472            self.buf.push_str(&qname(f.prefix.as_deref(), &f.name));
473            self.buf.push('>');
474        }
475        Ok(())
476    }
477
478    /// `xmlTextWriterEndDocument`.
479    #[doc(alias = "xmlTextWriterEndDocument")]
480    pub fn end_document(&mut self) -> Result<(), String> {
481        while self
482            .stack
483            .last()
484            .map(|f| f.kind == FrameKind::Element)
485            .unwrap_or(false)
486        {
487            self.end_element()?;
488        }
489        if !self.buf.ends_with('\n') {
490            self.buf.push('\n');
491        }
492        self.stack.clear();
493        Ok(())
494    }
495
496    pub fn into_bytes(self) -> Vec<u8> {
497        self.buf.into_bytes()
498    }
499
500    pub fn as_str(&self) -> &str {
501        &self.buf
502    }
503}
504
505/// `xmlNewTextWriterMemory`.
506#[doc(alias = "xmlNewTextWriterMemory")]
507pub fn xml_new_text_writer_memory() -> XmlTextWriter {
508    XmlTextWriter::xml_new_text_writer_memory()
509}