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, 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 /// The version of the format the document declares, as `office:version`.
76 pub fn version(&self) -> Option<&str> {
77 self.content.attr(&Ns::Office, "version")
78 }
79
80 /// The bytes of a picture, by the `xlink:href` that referred to it.
81 ///
82 /// A href that names an entry of the package gives those bytes. A href that
83 /// is a URL to somewhere else gives `None`: reaching it would be a network
84 /// request, which nothing in this suite makes.
85 pub fn picture(&self, href: &str) -> Option<&[u8]> {
86 // A href inside a package is a relative path, sometimes written with a
87 // leading `./`.
88 let path = href.strip_prefix("./").unwrap_or(href);
89 if path.contains("://") {
90 return None;
91 }
92 self.package.part(path).map(|p| p.data.as_slice())
93 }
94
95 /// Write the document back out, with the parsed parts re-serialized over the
96 /// ones they were read from.
97 ///
98 /// # Errors
99 ///
100 /// The package could not be assembled.
101 pub fn write(&mut self) -> Result<Vec<u8>, Error> {
102 let content = crate::xml::serialize(&self.content);
103 self.package.set_part("content.xml", content);
104 if let Some(styles) = &self.styles_part {
105 let bytes = crate::xml::serialize(styles);
106 self.package.set_part("styles.xml", bytes);
107 }
108 self.package.write()
109 }
110}