odox_core/doc/text.rs
1//! A text document: `.odt`.
2//
3// Author: David M. Anderson
4// Built with AI assistance (Claude, Anthropic)
5
6use super::Document;
7use crate::media_type;
8use crate::style::PageLayout;
9use crate::xml::{Element, Ns};
10use crate::{Error, Family};
11
12/// An `OpenDocument` text document.
13///
14/// The body is a flow of block elements — paragraphs, headings, lists, tables,
15/// sections, frames — in the order they are read, which is the order they are
16/// drawn. Nothing here paginates: a page layout says how wide a line may be, and
17/// that is what [`Self::page_layout`] is for.
18pub struct TextDocument {
19 /// The package and everything shared with the other two formats.
20 pub document: Document,
21}
22
23impl TextDocument {
24 /// Read a `.odt` package.
25 ///
26 /// # Errors
27 ///
28 /// The bytes are not a text document, or its `content.xml` cannot be read.
29 pub fn read(bytes: &[u8]) -> Result<Self, Error> {
30 let document = Document::read(bytes, media_type::TEXT_ANY)?;
31 Ok(Self { document })
32 }
33
34 /// The `office:text` element: the flow.
35 ///
36 /// `None` for a package that declares itself a text document and carries no
37 /// text body, which is a broken document rather than an empty one.
38 pub fn body(&self) -> Option<&Element> {
39 self.document.body_of("text")
40 }
41
42 /// The same, for changing it.
43 pub fn body_mut(&mut self) -> Option<&mut Element> {
44 self.document
45 .content
46 .child_mut(&Ns::Office, "body")?
47 .child_mut(&Ns::Office, "text")
48 }
49
50 /// Where [`Self::body`] is under the content root, as a path of child
51 /// indices: what an editor addresses the body's paragraphs from.
52 pub fn body_path(&self) -> Option<Vec<usize>> {
53 self.document.body_path("text")
54 }
55
56 /// The page layout the document's first master page points at.
57 ///
58 /// A text document has one master page per page style, and the first is the
59 /// one the body starts on. Where there is none, or no `styles.xml` at all,
60 /// the default of [`PageLayout`] applies.
61 pub fn page_layout(&self) -> PageLayout {
62 let master = self
63 .document
64 .styles_part
65 .as_ref()
66 .and_then(|styles| styles.child(&Ns::Office, "master-styles"))
67 .and_then(|masters| masters.child(&Ns::Style, "master-page"))
68 .and_then(|master| master.attr(&Ns::Style, "name"));
69 master
70 .and_then(|name| self.document.styles.page_layout_of(name))
71 .cloned()
72 .unwrap_or_default()
73 }
74
75 /// The width a line of body text may be: the page less its side margins.
76 pub fn text_width(&self) -> f32 {
77 let layout = self.page_layout();
78 let left = layout
79 .margin
80 .left
81 .map_or(0.0, super::super::value::Length::points);
82 let right = layout
83 .margin
84 .right
85 .map_or(0.0, super::super::value::Length::points);
86 (layout.width.points() - left - right).max(1.0)
87 }
88
89 /// The document's headings, as an outline for navigating it: each heading's
90 /// level, its text, and its position among the body's children.
91 ///
92 /// Only the top level of the body is walked. A heading inside a table cell or
93 /// a frame is not a heading in the outline — ODF permits one and no
94 /// application lists it — and a heading inside a section is, which is why
95 /// sections are descended into.
96 pub fn outline(&self) -> Vec<Heading> {
97 let mut headings = Vec::new();
98 if let Some(body) = self.body() {
99 collect_headings(body, &mut headings);
100 }
101 headings
102 }
103
104 /// The style a block element asks for, resolved.
105 ///
106 /// The family is the one the element belongs to, which for everything in a
107 /// text flow is [`Family::Paragraph`] except a table and its parts.
108 pub fn paragraph_style(&self, element: &Element) -> std::rc::Rc<crate::Properties> {
109 let name = element.attr(&Ns::Text, "style-name").unwrap_or("Standard");
110 self.document.styles.resolve(&Family::Paragraph, name)
111 }
112}
113
114/// One entry of a document's outline.
115pub struct Heading {
116 /// `text:outline-level`, where 1 is the top. A heading with no level
117 /// declared is level 1, which is what ODF says.
118 pub level: u8,
119 /// The heading's text, with ODF's whitespace elements resolved.
120 pub text: String,
121}
122
123fn collect_headings(parent: &Element, into: &mut Vec<Heading>) {
124 for element in parent.elements() {
125 if element.is(&Ns::Text, "h") {
126 let level = element
127 .attr_usize(&Ns::Text, "outline-level")
128 .unwrap_or(1)
129 .clamp(1, 10);
130 #[allow(clippy::cast_possible_truncation)]
131 into.push(Heading {
132 level: level as u8,
133 text: element.plain_text(),
134 });
135 } else if element.is(&Ns::Text, "section") {
136 collect_headings(element, into);
137 }
138 }
139}