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
102/// A name as it appeared, and the namespace it resolved to.
103///
104/// The prefix is kept alongside the resolved namespace because writing is a
105/// faithful re-emission: a document that spells the text namespace `t:` rather
106/// than `text:` is written back its own way.
107#[derive(Debug, Clone, PartialEq, Eq)]
108pub struct Name {
109    /// The prefix as written, without the colon. `None` for an unprefixed name.
110    pub prefix: Option<Box<str>>,
111    /// The part after the colon.
112    pub local: Box<str>,
113    /// Where the prefix pointed.
114    pub ns: Ns,
115}
116
117impl Name {
118    /// Whether this is the given ODF name.
119    pub fn is(&self, ns: &Ns, local: &str) -> bool {
120        self.ns == *ns && &*self.local == local
121    }
122}
123
124/// An attribute and its value, with the value unescaped.
125#[derive(Debug, Clone, PartialEq, Eq)]
126pub struct Attribute {
127    /// The attribute's name.
128    pub name: Name,
129    /// Its value, unescaped.
130    pub value: String,
131}
132
133/// One node of a parsed part.
134#[derive(Debug, Clone, PartialEq, Eq)]
135pub enum Node {
136    /// An element and everything under it.
137    Element(Element),
138    /// Character data, unescaped. Whitespace is significant inside a paragraph
139    /// and is never trimmed here.
140    Text(String),
141    /// `<![CDATA[...]]>`, kept as its own kind so that it is written back as one.
142    CData(String),
143    /// `<!-- ... -->`.
144    Comment(String),
145    /// `<?target content?>`, excluding the XML declaration.
146    ProcessingInstruction(String),
147}
148
149/// An element: its name, its attributes in the order they were written, and its
150/// children.
151#[derive(Debug, Clone, PartialEq, Eq)]
152pub struct Element {
153    /// The element's name.
154    pub name: Name,
155    /// Attributes in document order, including any namespace declarations.
156    pub attrs: Vec<Attribute>,
157    /// Children in document order.
158    pub children: Vec<Node>,
159    /// True when the element was written `<a/>`. Only consulted when there are
160    /// no children, and exists so that a round trip does not rewrite every
161    /// empty element in the document.
162    pub self_closing: bool,
163}
164
165impl Element {
166    /// A new element with no attributes and no children.
167    pub fn new(prefix: &str, local: &str, ns: Ns) -> Self {
168        Self {
169            name: Name {
170                prefix: if prefix.is_empty() {
171                    None
172                } else {
173                    Some(prefix.into())
174                },
175                local: local.into(),
176                ns,
177            },
178            attrs: Vec::new(),
179            children: Vec::new(),
180            self_closing: true,
181        }
182    }
183
184    /// Whether this element has the given ODF name.
185    pub fn is(&self, ns: &Ns, local: &str) -> bool {
186        self.name.is(ns, local)
187    }
188
189    /// The value of an attribute, by namespace and local name.
190    pub fn attr(&self, ns: &Ns, local: &str) -> Option<&str> {
191        self.attrs
192            .iter()
193            .find(|a| a.name.is(ns, local))
194            .map(|a| a.value.as_str())
195    }
196
197    /// The value of an attribute, read as an integer. Absent, or present and
198    /// unreadable, are the same answer: ODF's own default applies either way and
199    /// a document that says `table:number-columns-repeated="many"` is not a
200    /// reason to refuse the document.
201    pub fn attr_usize(&self, ns: &Ns, local: &str) -> Option<usize> {
202        self.attr(ns, local)?.trim().parse().ok()
203    }
204
205    /// Every child element, in order.
206    pub fn elements(&self) -> impl Iterator<Item = &Element> {
207        self.children.iter().filter_map(|n| match n {
208            Node::Element(e) => Some(e),
209            _ => None,
210        })
211    }
212
213    /// The first child element with the given name.
214    pub fn child(&self, ns: &Ns, local: &str) -> Option<&Element> {
215        self.elements().find(|e| e.is(ns, local))
216    }
217
218    /// The first descendant element with the given name, depth first.
219    pub fn descendant(&self, ns: &Ns, local: &str) -> Option<&Element> {
220        for e in self.elements() {
221            if e.is(ns, local) {
222                return Some(e);
223            }
224            if let Some(found) = e.descendant(ns, local) {
225                return Some(found);
226            }
227        }
228        None
229    }
230
231    /// Every character of text under this element, with ODF's own whitespace
232    /// elements turned back into the characters they stand for.
233    ///
234    /// `text:s` is a run of spaces, `text:tab` a tab and `text:line-break` a
235    /// newline, because ODF collapses literal runs of whitespace and spells out
236    /// what it means instead. This is what a spreadsheet cell's displayed string
237    /// and a search over a document both read.
238    pub fn plain_text(&self) -> String {
239        let mut out = String::new();
240        self.write_plain_text(&mut out);
241        out
242    }
243
244    fn write_plain_text(&self, out: &mut String) {
245        for child in &self.children {
246            match child {
247                Node::Text(t) | Node::CData(t) => out.push_str(t),
248                Node::Element(e) if e.is(&Ns::Text, "s") => {
249                    let n = e.attr_usize(&Ns::Text, "c").unwrap_or(1);
250                    for _ in 0..n {
251                        out.push(' ');
252                    }
253                }
254                Node::Element(e) if e.is(&Ns::Text, "tab") => out.push('\t'),
255                Node::Element(e) if e.is(&Ns::Text, "line-break") => out.push('\n'),
256                // A note's body is not part of the text it hangs off, and
257                // neither is the deleted half of a tracked change.
258                Node::Element(e) if e.is(&Ns::Text, "note") => {}
259                Node::Element(e) => e.write_plain_text(out),
260                Node::Comment(_) | Node::ProcessingInstruction(_) => {}
261            }
262        }
263    }
264}
265
266pub use read::parse;
267pub use write::serialize;