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