Skip to main content

odox_core/xml/
mod.rs

1//! The document tree: namespace-qualified names, elements, and the parser and
2//! writer that turn a part of a package into one and back.
3//!
4//! This is the substrate the rest of the crate reads through. It is a faithful
5//! tree rather than a model: attribute order, the choice between `<a/>` and
6//! `<a></a>`, comments, processing instructions and the whitespace between
7//! elements all survive a read and a write, so that a part this crate has no
8//! opinion about is returned unchanged.
9//!
10//! Text is held *unescaped*, and escaped again on the way out in the canonical
11//! form. Byte identity across a round trip is therefore not claimed; what is
12//! claimed, and what `tests/roundtrip.rs` measures, is that parsing the output
13//! gives back an equal tree.
14//
15// Author: David M. Anderson
16// Built with AI assistance (Claude, Anthropic)
17
18mod read;
19mod write;
20
21/// An XML namespace, resolved from the URI it was declared with.
22///
23/// ODF spells every one of these with a conventional prefix and an invariant
24/// URI. Matching is on this rather than on the prefix, because the prefix is the
25/// document's choice and the URI is the format's.
26#[derive(Debug, Clone, PartialEq, Eq, Hash)]
27pub enum Ns {
28    /// `urn:oasis:names:tc:opendocument:xmlns:office:1.0`
29    Office,
30    /// `urn:oasis:names:tc:opendocument:xmlns:text:1.0`
31    Text,
32    /// `urn:oasis:names:tc:opendocument:xmlns:style:1.0`
33    Style,
34    /// `urn:oasis:names:tc:opendocument:xmlns:table:1.0`
35    Table,
36    /// `urn:oasis:names:tc:opendocument:xmlns:drawing:1.0`
37    Draw,
38    /// `urn:oasis:names:tc:opendocument:xmlns:presentation:1.0`
39    Presentation,
40    /// `urn:oasis:names:tc:opendocument:xmlns:chart:1.0`
41    Chart,
42    /// `urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0`, which is
43    /// where most of the formatting attributes live.
44    Fo,
45    /// `urn:oasis:names:tc:opendocument:xmlns:svg-compatible:1.0`
46    Svg,
47    /// `urn:oasis:names:tc:opendocument:xmlns:datastyle:1.0`
48    Number,
49    /// `urn:oasis:names:tc:opendocument:xmlns:meta:1.0`
50    Meta,
51    /// `urn:oasis:names:tc:opendocument:xmlns:manifest:1.0`
52    Manifest,
53    /// `urn:oasis:names:tc:opendocument:xmlns:config:1.0`
54    Config,
55    /// `urn:oasis:names:tc:opendocument:xmlns:of:1.2`, the formula namespace.
56    Of,
57    /// `http://www.w3.org/1999/xlink`
58    Xlink,
59    /// `http://purl.org/dc/elements/1.1/`
60    Dc,
61    /// `urn:org:documentfoundation:names:experimental:office:xmlns:loext:1.0`
62    Loext,
63    /// `urn:org:documentfoundation:names:experimental:calc:xmlns:calcext:1.0`
64    Calcext,
65    /// A namespace declaration: `xmlns:foo="..."`. Held as an attribute so that
66    /// writing a tree re-declares exactly what reading it found.
67    Xmlns,
68    /// A namespace this crate has no name for. The URI is kept so that the
69    /// element is written back into the namespace it came from.
70    Other(Box<str>),
71    /// No namespace: an unprefixed name with no default declaration in scope.
72    None,
73}
74
75impl Ns {
76    /// The namespace a URI denotes.
77    pub fn from_uri(uri: &str) -> Self {
78        match uri {
79            "urn:oasis:names:tc:opendocument:xmlns:office:1.0" => Self::Office,
80            "urn:oasis:names:tc:opendocument:xmlns:text:1.0" => Self::Text,
81            "urn:oasis:names:tc:opendocument:xmlns:style:1.0" => Self::Style,
82            "urn:oasis:names:tc:opendocument:xmlns:table:1.0" => Self::Table,
83            "urn:oasis:names:tc:opendocument:xmlns:drawing:1.0" => Self::Draw,
84            "urn:oasis:names:tc:opendocument:xmlns:presentation:1.0" => Self::Presentation,
85            "urn:oasis:names:tc:opendocument:xmlns:chart:1.0" => Self::Chart,
86            "urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0" => Self::Fo,
87            "urn:oasis:names:tc:opendocument:xmlns:svg-compatible:1.0" => Self::Svg,
88            "urn:oasis:names:tc:opendocument:xmlns:datastyle:1.0" => Self::Number,
89            "urn:oasis:names:tc:opendocument:xmlns:meta:1.0" => Self::Meta,
90            "urn:oasis:names:tc:opendocument:xmlns:manifest:1.0" => Self::Manifest,
91            "urn:oasis:names:tc:opendocument:xmlns:config:1.0" => Self::Config,
92            "urn:oasis:names:tc:opendocument:xmlns:of:1.2" => Self::Of,
93            "http://www.w3.org/1999/xlink" => Self::Xlink,
94            "http://purl.org/dc/elements/1.1/" => Self::Dc,
95            "urn:org:documentfoundation:names:experimental:office:xmlns:loext:1.0" => Self::Loext,
96            "urn:org:documentfoundation:names:experimental:calc:xmlns:calcext:1.0" => Self::Calcext,
97            other => Self::Other(other.into()),
98        }
99    }
100}
101
102impl Ns {
103    /// The URI a namespace is declared with, for one this crate names.
104    pub fn uri(&self) -> Option<&'static str> {
105        Some(match self {
106            Self::Office => "urn:oasis:names:tc:opendocument:xmlns:office:1.0",
107            Self::Text => "urn:oasis:names:tc:opendocument:xmlns:text:1.0",
108            Self::Style => "urn:oasis:names:tc:opendocument:xmlns:style:1.0",
109            Self::Table => "urn:oasis:names:tc:opendocument:xmlns:table:1.0",
110            Self::Draw => "urn:oasis:names:tc:opendocument:xmlns:drawing:1.0",
111            Self::Presentation => "urn:oasis:names:tc:opendocument:xmlns:presentation:1.0",
112            Self::Chart => "urn:oasis:names:tc:opendocument:xmlns:chart:1.0",
113            Self::Fo => "urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0",
114            Self::Svg => "urn:oasis:names:tc:opendocument:xmlns:svg-compatible:1.0",
115            Self::Number => "urn:oasis:names:tc:opendocument:xmlns:datastyle:1.0",
116            Self::Meta => "urn:oasis:names:tc:opendocument:xmlns:meta:1.0",
117            Self::Manifest => "urn:oasis:names:tc:opendocument:xmlns:manifest:1.0",
118            Self::Config => "urn:oasis:names:tc:opendocument:xmlns:config:1.0",
119            Self::Of => "urn:oasis:names:tc:opendocument:xmlns:of:1.2",
120            Self::Xlink => "http://www.w3.org/1999/xlink",
121            Self::Dc => "http://purl.org/dc/elements/1.1/",
122            Self::Loext => "urn:org:documentfoundation:names:experimental:office:xmlns:loext:1.0",
123            Self::Calcext => "urn:org:documentfoundation:names:experimental:calc:xmlns:calcext:1.0",
124            Self::Xmlns | Self::Other(_) | Self::None => return None,
125        })
126    }
127
128    /// The prefix ODF spells a namespace with, for a name written into a
129    /// document that does not declare one of its own.
130    pub fn conventional_prefix(&self) -> &str {
131        match self {
132            Self::Office => "office",
133            Self::Text => "text",
134            Self::Style => "style",
135            Self::Table => "table",
136            Self::Draw => "draw",
137            Self::Presentation => "presentation",
138            Self::Chart => "chart",
139            Self::Fo => "fo",
140            Self::Svg => "svg",
141            Self::Number => "number",
142            Self::Meta => "meta",
143            Self::Manifest => "manifest",
144            Self::Config => "config",
145            Self::Of => "of",
146            Self::Xlink => "xlink",
147            Self::Dc => "dc",
148            Self::Loext => "loext",
149            Self::Calcext => "calcext",
150            Self::Xmlns => "xmlns",
151            Self::Other(_) | Self::None => "",
152        }
153    }
154}
155
156/// A name as it appeared, and the namespace it resolved to.
157///
158/// The prefix is kept alongside the resolved namespace because writing is a
159/// faithful re-emission: a document that spells the text namespace `t:` rather
160/// than `text:` is written back its own way.
161#[derive(Debug, Clone, PartialEq, Eq)]
162pub struct Name {
163    /// The prefix as written, without the colon. `None` for an unprefixed name.
164    pub prefix: Option<Box<str>>,
165    /// The part after the colon.
166    pub local: Box<str>,
167    /// Where the prefix pointed.
168    pub ns: Ns,
169}
170
171impl Name {
172    /// A name with a prefix, or unprefixed where the prefix is empty.
173    pub fn new(prefix: &str, local: &str, ns: Ns) -> Self {
174        Self {
175            prefix: if prefix.is_empty() {
176                None
177            } else {
178                Some(prefix.into())
179            },
180            local: local.into(),
181            ns,
182        }
183    }
184
185    /// Whether this is the given ODF name.
186    pub fn is(&self, ns: &Ns, local: &str) -> bool {
187        self.ns == *ns && &*self.local == local
188    }
189}
190
191/// An attribute and its value, with the value unescaped.
192#[derive(Debug, Clone, PartialEq, Eq)]
193pub struct Attribute {
194    /// The attribute's name.
195    pub name: Name,
196    /// Its value, unescaped.
197    pub value: String,
198}
199
200/// One node of a parsed part.
201#[derive(Debug, Clone, PartialEq, Eq)]
202pub enum Node {
203    /// An element and everything under it.
204    Element(Element),
205    /// Character data, unescaped. Whitespace is significant inside a paragraph
206    /// and is never trimmed here.
207    Text(String),
208    /// `<![CDATA[...]]>`, kept as its own kind so that it is written back as one.
209    CData(String),
210    /// `<!-- ... -->`.
211    Comment(String),
212    /// `<?target content?>`, excluding the XML declaration.
213    ProcessingInstruction(String),
214}
215
216/// An element: its name, its attributes in the order they were written, and its
217/// children.
218#[derive(Debug, Clone, PartialEq, Eq)]
219pub struct Element {
220    /// The element's name.
221    pub name: Name,
222    /// Attributes in document order, including any namespace declarations.
223    pub attrs: Vec<Attribute>,
224    /// Children in document order.
225    pub children: Vec<Node>,
226    /// True when the element was written `<a/>`. Only consulted when there are
227    /// no children, and exists so that a round trip does not rewrite every
228    /// empty element in the document.
229    pub self_closing: bool,
230}
231
232impl Element {
233    /// A new element with no attributes and no children.
234    pub fn new(prefix: &str, local: &str, ns: Ns) -> Self {
235        Self {
236            name: Name::new(prefix, local, ns),
237            attrs: Vec::new(),
238            children: Vec::new(),
239            self_closing: true,
240        }
241    }
242
243    /// Whether this element has the given ODF name.
244    pub fn is(&self, ns: &Ns, local: &str) -> bool {
245        self.name.is(ns, local)
246    }
247
248    /// The value of an attribute, by namespace and local name.
249    pub fn attr(&self, ns: &Ns, local: &str) -> Option<&str> {
250        self.attrs
251            .iter()
252            .find(|a| a.name.is(ns, local))
253            .map(|a| a.value.as_str())
254    }
255
256    /// The value of an attribute, read as an integer. Absent, or present and
257    /// unreadable, are the same answer: ODF's own default applies either way and
258    /// a document that says `table:number-columns-repeated="many"` is not a
259    /// reason to refuse the document.
260    pub fn attr_usize(&self, ns: &Ns, local: &str) -> Option<usize> {
261        self.attr(ns, local)?.trim().parse().ok()
262    }
263
264    /// Every child element, in order.
265    pub fn elements(&self) -> impl Iterator<Item = &Element> {
266        self.children.iter().filter_map(|n| match n {
267            Node::Element(e) => Some(e),
268            _ => None,
269        })
270    }
271
272    /// Every child element with its index among the children, which is the
273    /// step a path through the tree takes to reach it.
274    pub fn elements_indexed(&self) -> impl Iterator<Item = (usize, &Element)> {
275        self.children
276            .iter()
277            .enumerate()
278            .filter_map(|(i, n)| match n {
279                Node::Element(e) => Some((i, e)),
280                _ => None,
281            })
282    }
283
284    /// The element a path of child indices leads to, counting every node and
285    /// not only the elements. An empty path is this element.
286    pub fn at(&self, path: &[usize]) -> Option<&Element> {
287        let mut element = self;
288        for &step in path {
289            let Node::Element(child) = element.children.get(step)? else {
290                return None;
291            };
292            element = child;
293        }
294        Some(element)
295    }
296
297    /// The first child element with the given name, for changing it.
298    pub fn child_mut(&mut self, ns: &Ns, local: &str) -> Option<&mut Element> {
299        self.children.iter_mut().find_map(|n| match n {
300            Node::Element(e) if e.is(ns, local) => Some(e),
301            _ => None,
302        })
303    }
304
305    /// The same, for changing what is there.
306    pub fn at_mut(&mut self, path: &[usize]) -> Option<&mut Element> {
307        let mut element = self;
308        for &step in path {
309            let Node::Element(child) = element.children.get_mut(step)? else {
310                return None;
311            };
312            element = child;
313        }
314        Some(element)
315    }
316
317    /// Set an attribute: the value replaced where the attribute is there, the
318    /// attribute appended where it is not.
319    pub fn set_attr(&mut self, name: Name, value: impl Into<String>) {
320        let value = value.into();
321        match self.attrs.iter_mut().find(|a| a.name == name) {
322            Some(attr) => attr.value = value,
323            None => self.attrs.push(Attribute { name, value }),
324        }
325    }
326
327    /// Remove an attribute, answering the value it had.
328    pub fn remove_attr(&mut self, ns: &Ns, local: &str) -> Option<String> {
329        let at = self.attrs.iter().position(|a| a.name.is(ns, local))?;
330        Some(self.attrs.remove(at).value)
331    }
332
333    /// A name for writing under this element, in the prefix it declares for
334    /// the namespace, or the conventional one where it declares none.
335    pub fn name_for(&self, ns: &Ns, local: &str) -> Name {
336        let declared = self
337            .attrs
338            .iter()
339            .filter(|a| a.name.ns == Ns::Xmlns)
340            .find(|a| Ns::from_uri(&a.value) == *ns)
341            .map(|a| &*a.name.local);
342        Name::new(
343            declared.unwrap_or(ns.conventional_prefix()),
344            local,
345            ns.clone(),
346        )
347    }
348
349    /// Whether this element declares a namespace.
350    pub fn declares(&self, ns: &Ns) -> bool {
351        self.attrs
352            .iter()
353            .any(|a| a.name.ns == Ns::Xmlns && Ns::from_uri(&a.value) == *ns)
354    }
355
356    /// Declare a namespace on this element under its conventional prefix,
357    /// where it is not declared already. False where it cannot be: the
358    /// prefix is another namespace's here, or the namespace has no URI this
359    /// crate knows.
360    pub fn declare(&mut self, ns: &Ns) -> bool {
361        if self.declares(ns) {
362            return true;
363        }
364        let Some(uri) = ns.uri() else {
365            return false;
366        };
367        let prefix = ns.conventional_prefix();
368        if self
369            .attrs
370            .iter()
371            .any(|a| a.name.ns == Ns::Xmlns && &*a.name.local == prefix)
372        {
373            return false;
374        }
375        self.attrs.push(Attribute {
376            name: Name::new("xmlns", prefix, Ns::Xmlns),
377            value: uri.to_owned(),
378        });
379        true
380    }
381
382    /// The first child element with the given name.
383    pub fn child(&self, ns: &Ns, local: &str) -> Option<&Element> {
384        self.elements().find(|e| e.is(ns, local))
385    }
386
387    /// The first descendant element with the given name, depth first.
388    pub fn descendant(&self, ns: &Ns, local: &str) -> Option<&Element> {
389        for e in self.elements() {
390            if e.is(ns, local) {
391                return Some(e);
392            }
393            if let Some(found) = e.descendant(ns, local) {
394                return Some(found);
395            }
396        }
397        None
398    }
399
400    /// Every character of text under this element, with ODF's own whitespace
401    /// elements turned back into the characters they stand for.
402    ///
403    /// `text:s` is a run of spaces, `text:tab` a tab and `text:line-break` a
404    /// newline, because ODF collapses literal runs of whitespace and spells out
405    /// what it means instead. This is what a spreadsheet cell's displayed string
406    /// and a search over a document both read.
407    pub fn plain_text(&self) -> String {
408        let mut out = String::new();
409        self.write_plain_text(&mut out);
410        out
411    }
412
413    fn write_plain_text(&self, out: &mut String) {
414        for child in &self.children {
415            match child {
416                Node::Text(t) | Node::CData(t) => out.push_str(t),
417                Node::Element(e) if e.is(&Ns::Text, "s") => {
418                    let n = e.attr_usize(&Ns::Text, "c").unwrap_or(1);
419                    for _ in 0..n {
420                        out.push(' ');
421                    }
422                }
423                Node::Element(e) if e.is(&Ns::Text, "tab") => out.push('\t'),
424                Node::Element(e) if e.is(&Ns::Text, "line-break") => out.push('\n'),
425                // A note's body is not part of the text it hangs off, and
426                // neither is the deleted half of a tracked change.
427                Node::Element(e) if e.is(&Ns::Text, "note") => {}
428                Node::Element(e) => e.write_plain_text(out),
429                Node::Comment(_) | Node::ProcessingInstruction(_) => {}
430            }
431        }
432    }
433}
434
435pub use read::parse;
436pub use write::serialize;