oxml 0.0.11

A pure Rust XML toolkit with zero unsafe code: parsing, an ergonomic tree, and XPath
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
// SPDX-License-Identifier: MIT OR Apache-2.0
// Copyright (c) 2026 oxml. All rights reserved.

//! Writing a [`Document`] back out as XML.
//!
//! The contract -- what is guaranteed and what is not preserved -- is
//! documented on [`Document::to_xml`], where a caller will find it.
//!
//! Names are written through a scope of namespace bindings that the
//! writer keeps as it descends. A parsed document carries every
//! declaration it needs as namespace nodes, so the scope only confirms
//! the stored prefix is bound. A name the mutation API created has no
//! prefix and may be in a namespace nothing declares; the scope finds
//! a binding in force or declares one on the element, so what is
//! written always parses back to the same expanded names.

use alloc::borrow::Cow;
use alloc::string::String;
use core::fmt::Write;
#[cfg(feature = "std")]
use std::io;

use crate::tree::{Document, NameId, NodeId, NodeKind};

mod escape;
mod options;
mod scope;
mod version;

pub use options::{EmptyElement, Indent, Newline, SerialiseOptions};
pub use version::SerialiseError;

#[cfg(feature = "std")]
use escape::IoSink;
use escape::{write_attribute_value, write_text};
use scope::{
    Scope, Written, resolve_qualified, resolve_unqualified,
    write_auto_declarations, write_declaration,
};

impl Document {
    /// Write this document as XML.
    ///
    /// # What is guaranteed
    ///
    /// Serialisation is **idempotent over parsing**: for any document
    /// that parses,
    ///
    /// ```text
    /// to_xml(parse(to_xml(parse(s))))  ==  to_xml(parse(s))
    /// ```
    ///
    /// The first pass may change the text -- expanding an entity,
    /// dropping a CDATA wrapper -- and no pass after it changes
    /// anything. The output always parses, and parses to a tree with
    /// the same elements, attributes, text, comments and processing
    /// instructions in the same order. Every document in the W3C
    /// conformance corpus that this parser accepts is checked against
    /// that property.
    ///
    /// A document built or changed through the mutation API parses
    /// back to the same expanded names: a name in a namespace nothing
    /// declares is given a declaration on its element, with the prefix
    /// it was parsed with where there was one and `ns1`, `ns2`, ...
    /// otherwise.
    ///
    /// # What is not preserved
    ///
    /// This is **not** a byte-exact round trip, and cannot be: the
    /// tree does not retain what one would need.
    ///
    /// - **Entity references are expanded at parse.** `&`, `&`
    ///   and a general entity expanding to `&` all become the same
    ///   character, and nothing records which was written.
    /// - **CDATA sections become text.**
    /// - **Quoting style is not kept**; values come back double-quoted.
    /// - **`<a/>` and `<a></a>` are the same element**, and an element
    ///   with no children is written self-closing.
    /// - **Whitespace outside the root, and any DOCTYPE, are dropped.**
    ///
    /// Each is a lexical difference over identical content, which is
    /// what makes the guarantee above achievable instead.
    ///
    /// # What no version can hold
    ///
    /// The mutation API can put a control character, which only XML
    /// 1.1 can write, beside a comment holding `U+0085`, which only
    /// XML 1.0 keeps literally. This writes such a document as 1.1,
    /// and the comment does not read back the same.
    /// [`Document::try_to_xml`] refuses it instead.
    #[must_use]
    pub fn to_xml(&self) -> String {
        let mut out = String::new();
        // Writing into a `String` cannot fail; the `Result` exists for
        // the generic writer below.
        let _ = self.write_xml(&mut out);
        out
    }

    /// Write this document as XML, refusing one that no XML version
    /// can write so that it reads back as itself.
    ///
    /// On success the output is exactly [`Document::to_xml`]'s. A
    /// parsed document always succeeds; see
    /// [`SerialiseError::VersionConflict`] for what a built one can
    /// hold that no version expresses.
    ///
    /// # Errors
    ///
    /// [`SerialiseError::NoRootElement`] when the document has no element.
    ///
    /// [`SerialiseError::VersionConflict`] when the document needs XML
    /// 1.1 and a comment or processing instruction holds a character
    /// 1.1 does not keep literally.
    ///
    /// # Examples
    ///
    /// ```
    /// use oxml::{Document, SerialiseError};
    ///
    /// let mut doc = Document::empty();
    /// let top = doc.append_element(doc.root(), None, "r").unwrap();
    /// let note = doc.append_comment(top, "caf\u{85}").unwrap();
    /// assert!(doc.try_to_xml().is_ok());
    ///
    /// // A control character can only be written in XML 1.1, which
    /// // reads U+0085 in the comment as a line ending.
    /// let _ = doc.append_text(top, "\u{1}").unwrap();
    /// assert_eq!(
    ///     doc.try_to_xml(),
    ///     Err(SerialiseError::VersionConflict {
    ///         node: note,
    ///         character: '\u{85}',
    ///     })
    /// );
    /// ```
    pub fn try_to_xml(&self) -> Result<String, SerialiseError> {
        if self.root_element().is_none() {
            return Err(SerialiseError::NoRootElement);
        }
        match self.version_conflict() {
            Some(conflict) => Err(conflict),
            None => Ok(self.to_xml()),
        }
    }

    /// Write this document as XML into any [`core::fmt::Write`].
    ///
    /// # Errors
    ///
    /// Whatever the writer returns.
    pub fn write_xml<W: Write>(&self, out: &mut W) -> core::fmt::Result {
        self.write_xml_with(out, SerialiseOptions::default())
    }

    /// Write this document as XML into any [`std::io::Write`].
    ///
    /// The byte-oriented counterpart of [`Document::write_xml_with`],
    /// for a file or a socket. The output is UTF-8 and identical to
    /// [`Document::to_xml_with`] with the same options. The writer is
    /// flushed before returning.
    ///
    /// # Errors
    ///
    /// The first error the writer returned.
    #[cfg(feature = "std")]
    #[cfg_attr(docsrs, doc(cfg(feature = "std")))]
    pub fn write_xml_to<W: io::Write>(
        &self,
        w: W,
        options: &SerialiseOptions,
    ) -> io::Result<()> {
        let mut sink = IoSink {
            inner: w,
            error: None,
        };
        let outcome = self.write_xml_with(&mut sink, *options);
        match (outcome, sink.error) {
            (_, Some(e)) => Err(e),
            (Err(core::fmt::Error), None) => {
                Err(io::Error::other("formatting failed"))
            }
            (Ok(()), None) => sink.inner.flush(),
        }
    }

    /// The document as XML, formatted according to `options`.
    ///
    /// With `SerialiseOptions::default()` this is exactly
    /// [`Document::to_xml`]. With indentation set, whitespace is added
    /// only between children of elements whose children are all
    /// elements -- see [`SerialiseOptions::indent`] for why that
    /// restriction is what makes pretty-printing safe.
    ///
    /// A pretty-printed document is **not** guaranteed equal to its
    /// source, and reparsing it yields a tree containing the inserted
    /// whitespace as text nodes. The fixed-point guarantee belongs to
    /// the default options alone.
    #[must_use]
    pub fn to_xml_with(&self, options: SerialiseOptions) -> String {
        let mut out = String::new();
        let _ = self.write_xml_with(&mut out, options);
        out
    }

    /// Write this document as XML into any writer, with options.
    ///
    /// # Errors
    ///
    /// Whatever the writer returns.
    pub fn write_xml_with<W: Write>(
        &self,
        out: &mut W,
        options: SerialiseOptions,
    ) -> core::fmt::Result {
        #[cfg(feature = "tracing")]
        let _span = tracing::trace_span!("oxml.serialise").entered();

        // A declaration is written only when the content requires one.
        // The tree does not record which version it was parsed as, and
        // without a declaration the output is read as 1.0 -- so a 1.1
        // document containing `&#1;` serialised to a bare `&#1;`
        // produced something that would not parse at all.
        if self.needs_xml_11() {
            out.write_str("<?xml version=\"1.1\"?>")?;
            if options.indent.is_some() {
                out.write_str(options.newline.as_str())?;
            }
        }
        let mut scope = Scope::new();
        let top = self.children(self.root());
        for (i, &child) in top.iter().enumerate() {
            self.write_node(child, out, options, 0, &mut scope)?;
            // A newline between top-level items (the root element and
            // any comments or PIs beside it), but not after the last.
            if options.indent.is_some() && i + 1 < top.len() {
                out.write_str(options.newline.as_str())?;
            }
        }
        Ok(())
    }

    fn write_indent<W: Write>(
        out: &mut W,
        options: SerialiseOptions,
        depth: usize,
    ) -> core::fmt::Result {
        out.write_str(options.newline.as_str())?;
        match options.indent {
            Some(Indent::Spaces(n)) => {
                for _ in 0..(depth * n as usize) {
                    out.write_char(' ')?;
                }
            }
            Some(Indent::Tab) => {
                for _ in 0..depth {
                    out.write_char('\t')?;
                }
            }
            None => {}
        }
        Ok(())
    }

    fn write_node<'a, W: Write>(
        &'a self,
        id: NodeId,
        out: &mut W,
        options: SerialiseOptions,
        depth: usize,
        scope: &mut Scope<'a>,
    ) -> core::fmt::Result {
        match self.kind(id) {
            Some(NodeKind::Element { name, .. }) => {
                self.write_element(id, name, out, options, depth, scope)
            }
            // XML's document-level whitespace is not a text node on
            // reparse, so omit it from the canonical representation.
            Some(NodeKind::Text(_)) if self.parent(id) == Some(self.root()) => {
                Ok(())
            }
            Some(NodeKind::Text(t)) => write_text(t, out),
            Some(NodeKind::Comment(c)) => write!(out, "<!--{c}-->"),
            Some(NodeKind::ProcessingInstruction { target, data }) => {
                if data.is_empty() {
                    write!(out, "<?{target}?>")
                } else {
                    write!(out, "<?{target} {data}?>")
                }
            }
            // Attributes and namespaces are written by their element,
            // and the root has no representation of its own.
            _ => Ok(()),
        }
    }

    fn write_element<'a, W: Write>(
        &'a self,
        id: NodeId,
        name: NameId,
        out: &mut W,
        options: SerialiseOptions,
        depth: usize,
        scope: &mut Scope<'a>,
    ) -> core::fmt::Result {
        let mark = scope.len();
        let how = self.write_start_tag(id, name, out, scope)?;
        let children = self.children(id);
        let result = if children.is_empty() {
            self.write_empty_end(name, how, out, options, scope)
        } else {
            out.write_char('>')?;
            self.write_children(children, out, options, depth, scope)?;
            out.write_str("</")?;
            self.write_qname(name, how, out, scope)?;
            out.write_char('>')
        };
        scope.truncate(mark);
        result
    }

    /// `<name`, the declarations and the attributes, without the `>`.
    ///
    /// Names are resolved before anything is written, because resolving
    /// one may add a declaration that must appear in this tag.
    fn write_start_tag<'a, W: Write>(
        &'a self,
        id: NodeId,
        name: NameId,
        out: &mut W,
        scope: &mut Scope<'a>,
    ) -> Result<Written, core::fmt::Error> {
        self.push_declared(id, scope);
        let auto_from = scope.len();
        let how = self.resolve_name(name, false, scope);
        for &attr in self.attribute_nodes(id) {
            if let Some(NodeKind::Attr(a)) = self.kind(attr) {
                let _ = self.resolve_name(a.name, true, scope);
            }
        }
        out.write_char('<')?;
        self.write_qname(name, how, out, scope)?;
        self.write_declared(id, out)?;
        write_auto_declarations(out, scope, auto_from)?;
        self.write_attributes(id, out, scope)?;
        Ok(how)
    }

    fn write_empty_end<W: Write>(
        &self,
        name: NameId,
        how: Written,
        out: &mut W,
        options: SerialiseOptions,
        scope: &Scope<'_>,
    ) -> core::fmt::Result {
        match options.empty_elements {
            EmptyElement::SelfClosing => out.write_str("/>"),
            EmptyElement::SelfClosingSpaced => out.write_str(" />"),
            EmptyElement::Expanded => {
                out.write_str("></")?;
                self.write_qname(name, how, out, scope)?;
                out.write_char('>')
            }
        }
    }

    fn write_children<'a, W: Write>(
        &'a self,
        children: &[NodeId],
        out: &mut W,
        options: SerialiseOptions,
        depth: usize,
        scope: &mut Scope<'a>,
    ) -> core::fmt::Result {
        // Indent only where every child is an element. Any text,
        // comment or PI child means the element's content is written
        // byte-for-byte as the default mode writes it: inserting
        // whitespace next to character data would change that data.
        if options.indent.is_some()
            && children.iter().all(|&c| self.is_element(c))
        {
            return self.write_indented(children, out, options, depth, scope);
        }
        // Inside mixed content no whitespace may be added, but the
        // empty-element spelling still applies -- `<b/>` versus
        // `<b />` is a difference between tags, not inside character
        // data. Recurse with indentation off rather than falling back
        // to a plain writer, which would silently drop every option
        // below this point.
        let compact = SerialiseOptions {
            indent: None,
            ..options
        };
        for &child in children {
            self.write_node(child, out, compact, depth, scope)?;
        }
        Ok(())
    }

    fn write_indented<'a, W: Write>(
        &'a self,
        children: &[NodeId],
        out: &mut W,
        options: SerialiseOptions,
        depth: usize,
        scope: &mut Scope<'a>,
    ) -> core::fmt::Result {
        for &child in children {
            Self::write_indent(out, options, depth + 1)?;
            self.write_node(child, out, options, depth + 1, scope)?;
        }
        Self::write_indent(out, options, depth)
    }

    /// Bring the element's own namespace nodes into scope.
    fn push_declared<'a>(&'a self, id: NodeId, scope: &mut Scope<'a>) {
        for &ns in self.namespace_nodes(id) {
            if let Some(NodeKind::Namespace { prefix, uri }) = self.kind(ns) {
                let _ = scope.push(Cow::Borrowed(prefix), uri);
            }
        }
    }

    /// Decide how `name` is written here, declaring a namespace in
    /// `scope` if nothing in force binds it.
    fn resolve_name<'a>(
        &'a self,
        name: NameId,
        attribute: bool,
        scope: &mut Scope<'a>,
    ) -> Written {
        let stored = self.prefix(name).filter(|p| !p.is_empty());
        match self.name(name).and_then(|n| n.namespace.as_deref()) {
            Some(uri) => resolve_qualified(stored, uri, attribute, scope),
            None => resolve_unqualified(attribute, scope),
        }
    }

    fn write_qname<W: Write>(
        &self,
        name: NameId,
        how: Written,
        out: &mut W,
        scope: &Scope<'_>,
    ) -> core::fmt::Result {
        if let Written::Prefixed(i) = how {
            out.write_str(scope.prefix(i))?;
            out.write_char(':')?;
        }
        out.write_str(self.name(name).map_or("", |n| n.local.as_str()))
    }

    /// The element's own `xmlns` declarations.
    ///
    /// `xml` is skipped: it is bound in every document by definition,
    /// and writing it back would be a redeclaration the specification
    /// forbids in some positions.
    fn write_declared<W: Write>(
        &self,
        id: NodeId,
        out: &mut W,
    ) -> core::fmt::Result {
        for &ns in self.namespace_nodes(id) {
            if let Some(NodeKind::Namespace { prefix, uri }) = self.kind(ns)
                && prefix != "xml"
            {
                write_declaration(out, prefix, uri)?;
            }
        }
        Ok(())
    }

    fn write_attributes<'a, W: Write>(
        &'a self,
        id: NodeId,
        out: &mut W,
        scope: &mut Scope<'a>,
    ) -> core::fmt::Result {
        for &attr in self.attribute_nodes(id) {
            if let Some(NodeKind::Attr(a)) = self.kind(attr) {
                // Resolved once already by `write_start_tag`, so this
                // finds the binding and declares nothing.
                let how = self.resolve_name(a.name, true, scope);
                out.write_char(' ')?;
                self.write_qname(a.name, how, out, scope)?;
                out.write_str("=\"")?;
                write_attribute_value(a.value, out)?;
                out.write_char('"')?;
            }
        }
        Ok(())
    }
}