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 prefix ODF spells a namespace with, for a name written into a
104    /// document that does not declare one of its own.
105    pub fn conventional_prefix(&self) -> &str {
106        match self {
107            Self::Office => "office",
108            Self::Text => "text",
109            Self::Style => "style",
110            Self::Table => "table",
111            Self::Draw => "draw",
112            Self::Presentation => "presentation",
113            Self::Chart => "chart",
114            Self::Fo => "fo",
115            Self::Svg => "svg",
116            Self::Number => "number",
117            Self::Meta => "meta",
118            Self::Manifest => "manifest",
119            Self::Config => "config",
120            Self::Of => "of",
121            Self::Xlink => "xlink",
122            Self::Dc => "dc",
123            Self::Loext => "loext",
124            Self::Calcext => "calcext",
125            Self::Xmlns => "xmlns",
126            Self::Other(_) | Self::None => "",
127        }
128    }
129}
130
131/// A name as it appeared, and the namespace it resolved to.
132///
133/// The prefix is kept alongside the resolved namespace because writing is a
134/// faithful re-emission: a document that spells the text namespace `t:` rather
135/// than `text:` is written back its own way.
136#[derive(Debug, Clone, PartialEq, Eq)]
137pub struct Name {
138    /// The prefix as written, without the colon. `None` for an unprefixed name.
139    pub prefix: Option<Box<str>>,
140    /// The part after the colon.
141    pub local: Box<str>,
142    /// Where the prefix pointed.
143    pub ns: Ns,
144}
145
146impl Name {
147    /// A name with a prefix, or unprefixed where the prefix is empty.
148    pub fn new(prefix: &str, local: &str, ns: Ns) -> Self {
149        Self {
150            prefix: if prefix.is_empty() {
151                None
152            } else {
153                Some(prefix.into())
154            },
155            local: local.into(),
156            ns,
157        }
158    }
159
160    /// Whether this is the given ODF name.
161    pub fn is(&self, ns: &Ns, local: &str) -> bool {
162        self.ns == *ns && &*self.local == local
163    }
164}
165
166/// An attribute and its value, with the value unescaped.
167#[derive(Debug, Clone, PartialEq, Eq)]
168pub struct Attribute {
169    /// The attribute's name.
170    pub name: Name,
171    /// Its value, unescaped.
172    pub value: String,
173}
174
175/// One node of a parsed part.
176#[derive(Debug, Clone, PartialEq, Eq)]
177pub enum Node {
178    /// An element and everything under it.
179    Element(Element),
180    /// Character data, unescaped. Whitespace is significant inside a paragraph
181    /// and is never trimmed here.
182    Text(String),
183    /// `<![CDATA[...]]>`, kept as its own kind so that it is written back as one.
184    CData(String),
185    /// `<!-- ... -->`.
186    Comment(String),
187    /// `<?target content?>`, excluding the XML declaration.
188    ProcessingInstruction(String),
189}
190
191/// An element: its name, its attributes in the order they were written, and its
192/// children.
193#[derive(Debug, Clone, PartialEq, Eq)]
194pub struct Element {
195    /// The element's name.
196    pub name: Name,
197    /// Attributes in document order, including any namespace declarations.
198    pub attrs: Vec<Attribute>,
199    /// Children in document order.
200    pub children: Vec<Node>,
201    /// True when the element was written `<a/>`. Only consulted when there are
202    /// no children, and exists so that a round trip does not rewrite every
203    /// empty element in the document.
204    pub self_closing: bool,
205}
206
207impl Element {
208    /// A new element with no attributes and no children.
209    pub fn new(prefix: &str, local: &str, ns: Ns) -> Self {
210        Self {
211            name: Name::new(prefix, local, ns),
212            attrs: Vec::new(),
213            children: Vec::new(),
214            self_closing: true,
215        }
216    }
217
218    /// Whether this element has the given ODF name.
219    pub fn is(&self, ns: &Ns, local: &str) -> bool {
220        self.name.is(ns, local)
221    }
222
223    /// The value of an attribute, by namespace and local name.
224    pub fn attr(&self, ns: &Ns, local: &str) -> Option<&str> {
225        self.attrs
226            .iter()
227            .find(|a| a.name.is(ns, local))
228            .map(|a| a.value.as_str())
229    }
230
231    /// The value of an attribute, read as an integer. Absent, or present and
232    /// unreadable, are the same answer: ODF's own default applies either way and
233    /// a document that says `table:number-columns-repeated="many"` is not a
234    /// reason to refuse the document.
235    pub fn attr_usize(&self, ns: &Ns, local: &str) -> Option<usize> {
236        self.attr(ns, local)?.trim().parse().ok()
237    }
238
239    /// Every child element, in order.
240    pub fn elements(&self) -> impl Iterator<Item = &Element> {
241        self.children.iter().filter_map(|n| match n {
242            Node::Element(e) => Some(e),
243            _ => None,
244        })
245    }
246
247    /// Every child element with its index among the children, which is the
248    /// step a path through the tree takes to reach it.
249    pub fn elements_indexed(&self) -> impl Iterator<Item = (usize, &Element)> {
250        self.children
251            .iter()
252            .enumerate()
253            .filter_map(|(i, n)| match n {
254                Node::Element(e) => Some((i, e)),
255                _ => None,
256            })
257    }
258
259    /// The element a path of child indices leads to, counting every node and
260    /// not only the elements. An empty path is this element.
261    pub fn at(&self, path: &[usize]) -> Option<&Element> {
262        let mut element = self;
263        for &step in path {
264            let Node::Element(child) = element.children.get(step)? else {
265                return None;
266            };
267            element = child;
268        }
269        Some(element)
270    }
271
272    /// The first child element with the given name, for changing it.
273    pub fn child_mut(&mut self, ns: &Ns, local: &str) -> Option<&mut Element> {
274        self.children.iter_mut().find_map(|n| match n {
275            Node::Element(e) if e.is(ns, local) => Some(e),
276            _ => None,
277        })
278    }
279
280    /// The same, for changing what is there.
281    pub fn at_mut(&mut self, path: &[usize]) -> Option<&mut Element> {
282        let mut element = self;
283        for &step in path {
284            let Node::Element(child) = element.children.get_mut(step)? else {
285                return None;
286            };
287            element = child;
288        }
289        Some(element)
290    }
291
292    /// Set an attribute: the value replaced where the attribute is there, the
293    /// attribute appended where it is not.
294    pub fn set_attr(&mut self, name: Name, value: impl Into<String>) {
295        let value = value.into();
296        match self.attrs.iter_mut().find(|a| a.name == name) {
297            Some(attr) => attr.value = value,
298            None => self.attrs.push(Attribute { name, value }),
299        }
300    }
301
302    /// Remove an attribute, answering the value it had.
303    pub fn remove_attr(&mut self, ns: &Ns, local: &str) -> Option<String> {
304        let at = self.attrs.iter().position(|a| a.name.is(ns, local))?;
305        Some(self.attrs.remove(at).value)
306    }
307
308    /// A name for writing under this element, in the prefix it declares for
309    /// the namespace, or the conventional one where it declares none.
310    pub fn name_for(&self, ns: &Ns, local: &str) -> Name {
311        let declared = self
312            .attrs
313            .iter()
314            .filter(|a| a.name.ns == Ns::Xmlns)
315            .find(|a| Ns::from_uri(&a.value) == *ns)
316            .map(|a| &*a.name.local);
317        Name::new(
318            declared.unwrap_or(ns.conventional_prefix()),
319            local,
320            ns.clone(),
321        )
322    }
323
324    /// Whether this element declares a namespace.
325    pub fn declares(&self, ns: &Ns) -> bool {
326        self.attrs
327            .iter()
328            .any(|a| a.name.ns == Ns::Xmlns && Ns::from_uri(&a.value) == *ns)
329    }
330
331    /// The first child element with the given name.
332    pub fn child(&self, ns: &Ns, local: &str) -> Option<&Element> {
333        self.elements().find(|e| e.is(ns, local))
334    }
335
336    /// The first descendant element with the given name, depth first.
337    pub fn descendant(&self, ns: &Ns, local: &str) -> Option<&Element> {
338        for e in self.elements() {
339            if e.is(ns, local) {
340                return Some(e);
341            }
342            if let Some(found) = e.descendant(ns, local) {
343                return Some(found);
344            }
345        }
346        None
347    }
348
349    /// Every character of text under this element, with ODF's own whitespace
350    /// elements turned back into the characters they stand for.
351    ///
352    /// `text:s` is a run of spaces, `text:tab` a tab and `text:line-break` a
353    /// newline, because ODF collapses literal runs of whitespace and spells out
354    /// what it means instead. This is what a spreadsheet cell's displayed string
355    /// and a search over a document both read.
356    pub fn plain_text(&self) -> String {
357        let mut out = String::new();
358        self.write_plain_text(&mut out);
359        out
360    }
361
362    fn write_plain_text(&self, out: &mut String) {
363        for child in &self.children {
364            match child {
365                Node::Text(t) | Node::CData(t) => out.push_str(t),
366                Node::Element(e) if e.is(&Ns::Text, "s") => {
367                    let n = e.attr_usize(&Ns::Text, "c").unwrap_or(1);
368                    for _ in 0..n {
369                        out.push(' ');
370                    }
371                }
372                Node::Element(e) if e.is(&Ns::Text, "tab") => out.push('\t'),
373                Node::Element(e) if e.is(&Ns::Text, "line-break") => out.push('\n'),
374                // A note's body is not part of the text it hangs off, and
375                // neither is the deleted half of a tracked change.
376                Node::Element(e) if e.is(&Ns::Text, "note") => {}
377                Node::Element(e) => e.write_plain_text(out),
378                Node::Comment(_) | Node::ProcessingInstruction(_) => {}
379            }
380        }
381    }
382}
383
384pub use read::parse;
385pub use write::serialize;