Skip to main content

odox_core/doc/
mod.rs

1//! The three kinds of document, over one shared reader.
2//!
3//! Everything a package carries that is not the body — its styles, its
4//! metadata, its pictures, its manifest — is read the same way for all three
5//! formats, and [`Document`] is that. Each format then adds one thing: the way
6//! its body is addressed. A text document is a flow, a spreadsheet is a set of
7//! sheets indexed by row and column, a presentation is a sequence of pages.
8//
9// Author: David M. Anderson
10// Built with AI assistance (Claude, Anthropic)
11
12mod pres;
13mod sheet;
14mod text;
15
16pub use pres::{Presentation, Slide};
17pub use sheet::{Cell, Column, Sheet, SheetDocument, Value};
18pub use text::TextDocument;
19
20use crate::xml::{Element, Name, Ns};
21use crate::{Error, Meta, Package, Styles};
22
23/// A document read from a package: everything the three formats share.
24pub struct Document {
25    /// The package itself, which keeps every entry whether or not it was read
26    /// here, so that writing gives back a document and not a subset of one.
27    pub package: Package,
28    /// The parsed `content.xml`.
29    pub content: Element,
30    /// The parsed `styles.xml`, if the package has one.
31    pub styles_part: Option<Element>,
32    /// Every style in the document, from both parts.
33    pub styles: Styles,
34    /// What the document says about itself.
35    pub meta: Meta,
36}
37
38impl Document {
39    /// Read a package and check its media type.
40    ///
41    /// # Errors
42    ///
43    /// The bytes are not a package, the package declares a different media type,
44    /// or `content.xml` is absent or not well formed.
45    pub fn read(bytes: &[u8], wanted: &'static [&'static str]) -> Result<Self, Error> {
46        let package = Package::read(bytes)?;
47        package.expect_media_type(wanted)?;
48        let content = package.xml("content.xml")?;
49        let styles_part = package.optional_xml("styles.xml")?;
50        let styles = Styles::collect(Some(&content), styles_part.as_ref());
51        let meta = match package.optional_xml("meta.xml")? {
52            Some(part) => Meta::read(&part),
53            None => Meta::default(),
54        };
55        Ok(Self {
56            package,
57            content,
58            styles_part,
59            styles,
60            meta,
61        })
62    }
63
64    /// The `office:body` element, which every format has exactly one of.
65    pub fn body(&self) -> Option<&Element> {
66        self.content.child(&Ns::Office, "body")
67    }
68
69    /// The part of the body this format keeps its content in: `office:text`,
70    /// `office:spreadsheet` or `office:presentation`.
71    pub fn body_of(&self, local: &str) -> Option<&Element> {
72        self.body()?.child(&Ns::Office, local)
73    }
74
75    /// A name for writing into the document, in the prefix the document
76    /// declares for the namespace on its content root, or the conventional one
77    /// where it declares none.
78    pub fn name(&self, ns: &Ns, local: &str) -> Name {
79        let declared = self
80            .content
81            .attrs
82            .iter()
83            .filter(|a| a.name.ns == Ns::Xmlns)
84            .find(|a| Ns::from_uri(&a.value) == *ns)
85            .map(|a| &*a.name.local);
86        Name::new(
87            declared.unwrap_or(ns.conventional_prefix()),
88            local,
89            ns.clone(),
90        )
91    }
92
93    /// Whether the content root declares a namespace, which is what decides
94    /// whether an attribute in it may be written at all.
95    pub fn declares(&self, ns: &Ns) -> bool {
96        self.content
97            .attrs
98            .iter()
99            .any(|a| a.name.ns == Ns::Xmlns && Ns::from_uri(&a.value) == *ns)
100    }
101
102    /// The version of the format the document declares, as `office:version`.
103    pub fn version(&self) -> Option<&str> {
104        self.content.attr(&Ns::Office, "version")
105    }
106
107    /// The bytes of a picture, by the `xlink:href` that referred to it.
108    ///
109    /// A href that names an entry of the package gives those bytes. A href that
110    /// is a URL to somewhere else gives `None`: reaching it would be a network
111    /// request, which nothing in this suite makes.
112    pub fn picture(&self, href: &str) -> Option<&[u8]> {
113        // A href inside a package is a relative path, sometimes written with a
114        // leading `./`.
115        let path = href.strip_prefix("./").unwrap_or(href);
116        if path.contains("://") {
117            return None;
118        }
119        self.package.part(path).map(|p| p.data.as_slice())
120    }
121
122    /// Write the document back out, with the parsed parts re-serialized over the
123    /// ones they were read from.
124    ///
125    /// # Errors
126    ///
127    /// The package could not be assembled.
128    pub fn write(&mut self) -> Result<Vec<u8>, Error> {
129        let content = crate::xml::serialize(&self.content);
130        self.package.set_part("content.xml", content);
131        if let Some(styles) = &self.styles_part {
132            let bytes = crate::xml::serialize(styles);
133            self.package.set_part("styles.xml", bytes);
134        }
135        self.package.write()
136    }
137
138    /// Write the document and prove the bytes read back as the tree they were
139    /// written from, which is what a save has to know before it touches the
140    /// file.
141    ///
142    /// The round-trip tests make the same claim over the corpus, and a person's
143    /// document is not in the corpus. One re-parse per save is the price, and
144    /// the alternative is an editor that finds out it damaged a document after
145    /// it has.
146    ///
147    /// # Errors
148    ///
149    /// The package could not be assembled, or a part came back different, in
150    /// which case nothing should be written.
151    pub fn write_verified(&mut self) -> Result<Vec<u8>, Error> {
152        let bytes = self.write()?;
153        let written = Package::read(&bytes)?;
154        if written.xml("content.xml")? != self.content {
155            return Err(Error::Unfaithful("content.xml"));
156        }
157        if let Some(styles) = &self.styles_part
158            && written.optional_xml("styles.xml")?.as_ref() != Some(styles)
159        {
160            return Err(Error::Unfaithful("styles.xml"));
161        }
162        Ok(bytes)
163    }
164}