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;