Skip to main content

odox_core/
style.rs

1//! Styles: the property sets a document names, the chain each one inherits
2//! through, and the resolution of a name into the properties a renderer wants.
3//!
4//! ODF keeps formatting in two places that work identically. A *named* style in
5//! `styles.xml` is the one a person chose and can see in an application's style
6//! list; an *automatic* style in `content.xml` is the one an application
7//! generated for a direct formatting change, named `P1`, `T3`, `ce2`. Both are
8//! `style:style` elements, both inherit through `style:parent-style-name`, and
9//! nothing downstream needs to know which kind it is holding. They are collected
10//! into one table here, keyed by family and name, because ODF scopes a style
11//! name within its family and a paragraph style and a cell style may share one.
12//!
13//! Resolution walks the chain from its root down, so that the nearest style
14//! wins, beginning at the family's `style:default-style`. The answer is cached,
15//! because a spreadsheet asks for the same handful of cell styles once per
16//! visible cell per frame.
17//
18// Author: David M. Anderson
19// Built with AI assistance (Claude, Anthropic)
20
21use std::cell::RefCell;
22use std::collections::HashMap;
23use std::rc::Rc;
24
25use crate::value::{Color, Length, Measure, Percent};
26use crate::xml::{Element, Ns};
27
28/// How a list marks its items.
29#[derive(Debug, Clone, Copy, PartialEq, Eq)]
30pub enum ListKind {
31    /// A bullet, or a picture standing for one.
32    Bullet,
33    /// A number or a letter.
34    Number,
35}
36
37/// Which kind of thing a style applies to.
38///
39/// ODF calls this the style family, and it is what makes a style name
40/// meaningful: `Standard` names a paragraph style and a table style and they are
41/// unrelated.
42#[derive(Debug, Clone, PartialEq, Eq, Hash)]
43pub enum Family {
44    /// `paragraph`
45    Paragraph,
46    /// `text`, which is a character style.
47    Text,
48    /// `section`
49    Section,
50    /// `table`
51    Table,
52    /// `table-column`
53    TableColumn,
54    /// `table-row`
55    TableRow,
56    /// `table-cell`
57    TableCell,
58    /// `graphic`
59    Graphic,
60    /// `presentation`, the family a placeholder on a slide takes.
61    Presentation,
62    /// `drawing-page`, which is a slide's own background and transition.
63    DrawingPage,
64    /// `chart`
65    Chart,
66    /// `ruby`
67    Ruby,
68    /// A family this crate has no name for, kept so that its styles are still
69    /// collected and still resolve.
70    Other(Box<str>),
71}
72
73impl Family {
74    /// The family an attribute value names.
75    pub fn parse(text: &str) -> Self {
76        match text {
77            "paragraph" => Self::Paragraph,
78            "text" => Self::Text,
79            "section" => Self::Section,
80            "table" => Self::Table,
81            "table-column" => Self::TableColumn,
82            "table-row" => Self::TableRow,
83            "table-cell" => Self::TableCell,
84            "graphic" => Self::Graphic,
85            "presentation" => Self::Presentation,
86            "drawing-page" => Self::DrawingPage,
87            "chart" => Self::Chart,
88            "ruby" => Self::Ruby,
89            other => Self::Other(other.into()),
90        }
91    }
92}
93
94/// How a paragraph's lines sit against its edges.
95#[derive(Debug, Clone, Copy, PartialEq, Eq)]
96pub enum TextAlign {
97    /// Against the start edge, which for a left-to-right document is the left.
98    Start,
99    /// Against the end edge.
100    End,
101    /// Centred.
102    Center,
103    /// Both edges, by stretching the spaces.
104    Justify,
105}
106
107/// Where a run sits relative to the baseline.
108#[derive(Debug, Clone, Copy, PartialEq, Eq)]
109pub enum Position {
110    /// On it.
111    Baseline,
112    /// Above it, smaller.
113    Super,
114    /// Below it, smaller.
115    Sub,
116}
117
118/// Vertical placement inside a cell.
119#[derive(Debug, Clone, Copy, PartialEq, Eq)]
120pub enum VerticalAlign {
121    /// Against the top.
122    Top,
123    /// Centred.
124    Middle,
125    /// Against the bottom.
126    Bottom,
127}
128
129/// A page or column break asked for before or after a paragraph.
130#[derive(Debug, Clone, Copy, PartialEq, Eq)]
131pub enum Break {
132    /// No break.
133    Auto,
134    /// A new page.
135    Page,
136    /// A new column.
137    Column,
138}
139
140/// One edge of a border, as ODF writes all three of its parts in one attribute.
141#[derive(Debug, Clone, Copy, PartialEq)]
142pub struct Border {
143    /// How thick.
144    pub width: Length,
145    /// What colour.
146    pub color: Color,
147}
148
149impl Border {
150    /// Parse `0.5pt solid #000000`, in any order of the three parts.
151    ///
152    /// `None` for `none` and for `hidden`, which are how ODF says there is no
153    /// border on this edge — a distinction from the attribute being absent,
154    /// which means inherit.
155    fn parse(text: &str) -> Option<Self> {
156        let mut width = None;
157        let mut color = None;
158        for word in text.split_whitespace() {
159            match word {
160                "none" | "hidden" => return None,
161                _ => {
162                    if let Some(length) = Length::parse(word) {
163                        width = Some(length);
164                    } else if let Some(parsed) = Color::parse(word) {
165                        color = Some(parsed);
166                    }
167                }
168            }
169        }
170        Some(Self {
171            // A border with a style and a colour but no width is the hairline
172            // every application draws for one.
173            width: width.unwrap_or(Length(0.5)),
174            color: color.unwrap_or(Color { r: 0, g: 0, b: 0 }),
175        })
176    }
177}
178
179/// The four edges of a box, in the order ODF's shorthand implies.
180#[derive(Debug, Clone, Copy, PartialEq)]
181pub struct Edges<T> {
182    /// Left.
183    pub left: Option<T>,
184    /// Right.
185    pub right: Option<T>,
186    /// Top.
187    pub top: Option<T>,
188    /// Bottom.
189    pub bottom: Option<T>,
190}
191
192/// Four edges, none of them set. Written out rather than derived because
193/// deriving it would require the edge's own type to have a default, and neither
194/// a length nor a border has one that means anything: the absence of a border is
195/// `None`, not a border of zero width.
196impl<T> Default for Edges<T> {
197    fn default() -> Self {
198        Self {
199            left: None,
200            right: None,
201            top: None,
202            bottom: None,
203        }
204    }
205}
206
207/// Character formatting.
208#[derive(Debug, Clone, Default, PartialEq)]
209pub struct TextProperties {
210    /// The family asked for, after a `style:font-name` has been resolved through
211    /// the document's font face declarations.
212    pub font_family: Option<String>,
213    /// Size, absolute or a proportion of the parent's.
214    pub size: Option<Measure>,
215    /// Bold.
216    pub bold: Option<bool>,
217    /// Italic or oblique, which are not distinguished here because a renderer
218    /// picking a face cannot honour the difference.
219    pub italic: Option<bool>,
220    /// Underlined, of any line style.
221    pub underline: Option<bool>,
222    /// Struck through, of any line style.
223    pub strike: Option<bool>,
224    /// Ink colour.
225    pub color: Option<Color>,
226    /// Highlight behind the characters.
227    pub background: Option<Color>,
228    /// Superscript or subscript.
229    pub position: Option<Position>,
230    /// Drawn in capitals whatever the text says.
231    pub uppercase: Option<bool>,
232}
233
234/// Paragraph formatting.
235#[derive(Debug, Clone, Default, PartialEq)]
236pub struct ParagraphProperties {
237    /// Horizontal alignment.
238    pub align: Option<TextAlign>,
239    /// Space outside the paragraph on each edge.
240    pub margin: Edges<Length>,
241    /// The first line's extra indent, which is negative for a hanging indent.
242    pub text_indent: Option<Length>,
243    /// Line spacing, absolute or a proportion of the font size.
244    pub line_height: Option<Measure>,
245    /// Fill behind the paragraph.
246    pub background: Option<Color>,
247    /// A break asked for before the paragraph.
248    pub break_before: Option<Break>,
249    /// A break asked for after it.
250    pub break_after: Option<Break>,
251    /// Borders, per edge.
252    pub border: Edges<Border>,
253    /// Space between the border and the text, per edge.
254    pub padding: Edges<Length>,
255}
256
257/// Cell formatting, which a spreadsheet reads for every visible cell.
258#[derive(Debug, Clone, Default, PartialEq)]
259pub struct CellProperties {
260    /// Fill.
261    pub background: Option<Color>,
262    /// Vertical placement of the text in the cell.
263    pub vertical_align: Option<VerticalAlign>,
264    /// Borders, per edge.
265    pub border: Edges<Border>,
266    /// Space between the border and the text, per edge.
267    pub padding: Edges<Length>,
268    /// Whether a line too long for the cell wraps rather than overflowing.
269    pub wrap: Option<bool>,
270}
271
272/// What fills a shape or the ground behind a slide.
273///
274/// A named gradient rather than the gradient itself, because ODF defines each
275/// one once in `office:styles` and refers to it by name from every style that
276/// uses it — resolving it here would copy it per style. [`Styles::gradient`]
277/// looks it up.
278#[derive(Debug, Clone, Default, PartialEq)]
279pub enum Fill {
280    /// Nothing is drawn: whatever is behind shows through.
281    #[default]
282    None,
283    /// One colour.
284    Solid(Color),
285    /// The gradient of this name.
286    Gradient(String),
287    /// The picture of this name, stretched over the shape.
288    ///
289    /// Only stretched. ODF also tiles a fill image, at a size the style gives,
290    /// and a tiled fill is [`Fill::None`] here rather than a stretched
291    /// approximation — one tile blown up to the size of a slide is not a
292    /// smaller version of the same thing.
293    Image(String),
294}
295
296/// How a gradient runs, which is the part of it a renderer has to understand.
297#[derive(Debug, Clone, Copy, PartialEq, Eq)]
298pub enum GradientStyle {
299    /// Along an axis, from one edge to the other.
300    Linear,
301    /// Along an axis, from the middle outwards to both edges.
302    Axial,
303    /// Out from a point, in circles.
304    Radial,
305    /// Out from a point, in ellipses.
306    Ellipsoidal,
307    /// Out from a point, in squares.
308    Square,
309    /// Out from a point, in rectangles.
310    Rectangular,
311}
312
313/// One of a document's named gradients.
314///
315/// ODF 1.3 also permits a list of `loext:gradient-stop` children, which
316/// `LibreOffice` writes alongside the two colour attributes and which say the same
317/// thing for a two-stop gradient. The attributes are read and the stops are not:
318/// every gradient in the corpus has exactly two stops that repeat what
319/// `draw:start-color` and `draw:end-color` already say, and a renderer that
320/// interpolated more of them would be drawing something no fixture can check.
321#[derive(Debug, Clone, PartialEq)]
322pub struct Gradient {
323    /// How it runs.
324    pub style: GradientStyle,
325    /// The colour it begins at.
326    pub start: Color,
327    /// The colour it ends at.
328    pub end: Color,
329    /// The direction, in degrees. ODF measures it counter-clockwise from the
330    /// direction that runs bottom to top, so 0 is upward and 90 points left.
331    pub angle: f32,
332    /// How much of each end is the flat colour before the blend begins, as a
333    /// proportion of the whole.
334    pub border: f32,
335    /// Where the centre is, for the styles that have one, as a proportion of the
336    /// shape's width and height.
337    pub center: (f32, f32),
338}
339
340/// Which of ODF's fill styles a shape uses, before the value it needs is found.
341#[derive(Debug, Clone, Copy, PartialEq, Eq)]
342enum FillKind {
343    None,
344    Solid,
345    Gradient,
346    /// A tiled or stretched picture.
347    Bitmap,
348    /// A pattern of lines, which nothing here draws.
349    Hatch,
350}
351
352/// How a shape is filled and outlined.
353///
354/// **The kind of fill and the value it uses are separate properties and inherit
355/// separately**, which is the whole reason they are separate fields here. A
356/// style may set `draw:fill-color` and say nothing about `draw:fill`: that names
357/// the colour a solid fill would use and does *not* turn the fill on, so a shape
358/// whose parent style says `draw:fill="none"` stays empty. Reading the colour as
359/// though it were the fill puts a white box over the slide, which is what a
360/// template's subtitle placeholder did until this was split.
361#[derive(Debug, Clone, Default, PartialEq)]
362pub struct GraphicProperties {
363    kind: Option<FillKind>,
364    color: Option<Color>,
365    gradient: Option<String>,
366    image: Option<String>,
367    /// Outline colour, absent when the stroke is `none`.
368    pub stroke: Option<Color>,
369    /// Outline width.
370    pub stroke_width: Option<Length>,
371    /// How opaque the fill is, from zero to one.
372    pub opacity: Option<f32>,
373    /// Where the shape's own label sits between its top and bottom edges.
374    pub text_anchor: Option<Anchor>,
375}
376
377/// Where a shape's label sits between its top and bottom edges.
378///
379/// `draw:textarea-vertical-align`. A drawing shape is a box a label is centred
380/// in far more often than it is a box a label starts at the top of, so the two
381/// look nothing alike and the attribute cannot be ignored: the numeral in a
382/// circle is the ordinary case.
383#[derive(Debug, Clone, Copy, PartialEq, Eq)]
384pub enum Anchor {
385    /// Against the top edge.
386    Top,
387    /// Centred between the two.
388    Middle,
389    /// Against the bottom edge.
390    Bottom,
391}
392
393impl Anchor {
394    /// How much of the room left over above and below the label goes above it.
395    #[must_use]
396    pub fn share(self) -> f32 {
397        match self {
398            Self::Top => 0.0,
399            Self::Middle => 0.5,
400            Self::Bottom => 1.0,
401        }
402    }
403}
404
405impl GraphicProperties {
406    /// What actually fills the shape, once the kind and its value are put
407    /// together.
408    ///
409    /// A kind whose value the style never gave — `draw:fill="gradient"` with no
410    /// gradient named — fills nothing, which is the honest answer and not a
411    /// guess at which gradient was meant.
412    pub fn fill(&self) -> Fill {
413        match self.kind {
414            None | Some(FillKind::None | FillKind::Hatch) => Fill::None,
415            Some(FillKind::Solid) => self.color.map_or(Fill::None, Fill::Solid),
416            Some(FillKind::Gradient) => self.gradient.clone().map_or(Fill::None, Fill::Gradient),
417            Some(FillKind::Bitmap) => self.image.clone().map_or(Fill::None, Fill::Image),
418        }
419    }
420}
421
422/// Everything a resolved style says, across every family.
423///
424/// One type rather than one per family, because a paragraph carries character
425/// properties, a cell carries paragraph properties, and a renderer asking for a
426/// cell's font would otherwise have to resolve three styles and merge them
427/// itself.
428#[derive(Debug, Clone, Default, PartialEq)]
429pub struct Properties {
430    /// Character formatting.
431    pub text: TextProperties,
432    /// Paragraph formatting.
433    pub paragraph: ParagraphProperties,
434    /// Cell formatting.
435    pub cell: CellProperties,
436    /// Shape formatting.
437    pub graphic: GraphicProperties,
438    /// Whether the master page's own background shows through, from a
439    /// `drawing-page` style. A presentation's, and `None` where nothing said.
440    pub background_visible: Option<bool>,
441    /// Whether the master page's decorations are drawn.
442    pub background_objects_visible: Option<bool>,
443    /// A column's width, from a `table-column` style.
444    pub column_width: Option<Length>,
445    /// A row's height, from a `table-row` style.
446    pub row_height: Option<Length>,
447}
448
449/// A page's dimensions and margins, from a `style:page-layout`.
450#[derive(Debug, Clone, PartialEq)]
451pub struct PageLayout {
452    /// The page's width.
453    pub width: Length,
454    /// Its height.
455    pub height: Length,
456    /// The margins inside it.
457    pub margin: Edges<Length>,
458}
459
460impl Default for PageLayout {
461    /// US Letter portrait with one-inch margins.
462    ///
463    /// Reached only by a document that declares no page layout at all, which no
464    /// office application produces: the answer normally comes from the document's
465    /// own master page and this is never consulted. The size still has to be
466    /// something, and it is the paper the producer on this machine writes —
467    /// `LibreOffice` 25.2 puts `8.5in` by `11in` in every document it creates here.
468    /// A4 would be the other choice and neither is more right.
469    fn default() -> Self {
470        Self {
471            width: Length(612.0),
472            height: Length(792.0),
473            margin: Edges {
474                left: Some(Length(72.0)),
475                right: Some(Length(72.0)),
476                top: Some(Length(72.0)),
477                bottom: Some(Length(72.0)),
478            },
479        }
480    }
481}
482
483/// One style as the document declares it.
484pub struct Style {
485    /// Its name, as everything else refers to it.
486    pub name: String,
487    /// The name a person sees in an application's style list, where it differs
488    /// from the name in the file. ODF encodes a space as `_20_` in a style name
489    /// and carries the readable spelling in this attribute.
490    pub display_name: Option<String>,
491    /// Its family.
492    pub family: Family,
493    /// The style it inherits from.
494    pub parent: Option<String>,
495    /// The `style:style` element itself, so that a property this crate does not
496    /// model is still reachable.
497    pub element: Element,
498}
499
500/// Every style in a document, from both of the places they live.
501pub struct Styles {
502    by_name: HashMap<(Family, String), Style>,
503    defaults: HashMap<Family, Element>,
504    page_layouts: HashMap<String, PageLayout>,
505    master_pages: HashMap<String, Element>,
506    lists: HashMap<String, Element>,
507    gradients: HashMap<String, Gradient>,
508    fill_images: HashMap<String, String>,
509    font_faces: HashMap<String, String>,
510    cache: RefCell<HashMap<(Family, String), Rc<Properties>>>,
511}
512
513impl Styles {
514    /// Collect the styles of a document from its two parts.
515    ///
516    /// Both are optional because both are optional in the format: a package may
517    /// carry all its formatting in automatic styles in `content.xml` and no
518    /// `styles.xml` at all.
519    pub fn collect(content: Option<&Element>, styles: Option<&Element>) -> Self {
520        let mut this = Self {
521            by_name: HashMap::new(),
522            defaults: HashMap::new(),
523            page_layouts: HashMap::new(),
524            master_pages: HashMap::new(),
525            lists: HashMap::new(),
526            gradients: HashMap::new(),
527            fill_images: HashMap::new(),
528            font_faces: HashMap::new(),
529            cache: RefCell::new(HashMap::new()),
530        };
531        // styles.xml first, so that an automatic style in content.xml with the
532        // same family and name as a named one wins. They do not collide in
533        // practice, and the order is what decides if they ever do.
534        for root in [styles, content].into_iter().flatten() {
535            this.collect_from(root);
536        }
537        this
538    }
539
540    fn collect_from(&mut self, root: &Element) {
541        for container in root.elements() {
542            match () {
543                () if container.is(&Ns::Office, "font-face-decls") => {
544                    for face in container.elements() {
545                        let Some(name) = face.attr(&Ns::Style, "name") else {
546                            continue;
547                        };
548                        // `svg:font-family` is the family a renderer asks the
549                        // system for; the declaration's own name is only a
550                        // handle that the styles refer to it by, and the two
551                        // usually but not always agree.
552                        let family = face
553                            .attr(&Ns::Svg, "font-family")
554                            .unwrap_or(name)
555                            .trim_matches('\'')
556                            .to_owned();
557                        self.font_faces.insert(name.to_owned(), family);
558                    }
559                }
560                () if container.is(&Ns::Office, "styles")
561                    || container.is(&Ns::Office, "automatic-styles") =>
562                {
563                    for element in container.elements() {
564                        self.collect_style(element);
565                    }
566                }
567                () if container.is(&Ns::Office, "master-styles") => {
568                    for master in container.elements() {
569                        if let Some(name) = master.attr(&Ns::Style, "name") {
570                            self.master_pages.insert(name.to_owned(), master.clone());
571                        }
572                    }
573                }
574                () => {}
575            }
576        }
577    }
578
579    fn collect_style(&mut self, element: &Element) {
580        if element.is(&Ns::Style, "style") {
581            let Some(name) = element.attr(&Ns::Style, "name") else {
582                return;
583            };
584            let family = Family::parse(element.attr(&Ns::Style, "family").unwrap_or_default());
585            let style = Style {
586                name: name.to_owned(),
587                display_name: element
588                    .attr(&Ns::Style, "display-name")
589                    .map(ToOwned::to_owned),
590                family: family.clone(),
591                parent: element
592                    .attr(&Ns::Style, "parent-style-name")
593                    .map(ToOwned::to_owned),
594                element: element.clone(),
595            };
596            self.by_name.insert((family, name.to_owned()), style);
597        } else if element.is(&Ns::Style, "default-style") {
598            let family = Family::parse(element.attr(&Ns::Style, "family").unwrap_or_default());
599            self.defaults.insert(family, element.clone());
600        } else if element.is(&Ns::Style, "page-layout") {
601            if let Some(name) = element.attr(&Ns::Style, "name") {
602                self.page_layouts
603                    .insert(name.to_owned(), page_layout(element));
604            }
605        } else if element.is(&Ns::Draw, "fill-image") {
606            if let (Some(name), Some(href)) = (
607                element.attr(&Ns::Draw, "name"),
608                element.attr(&Ns::Xlink, "href"),
609            ) {
610                self.fill_images.insert(name.to_owned(), href.to_owned());
611            }
612        } else if element.is(&Ns::Draw, "gradient") {
613            if let Some(name) = element.attr(&Ns::Draw, "name") {
614                self.gradients.insert(name.to_owned(), gradient(element));
615            }
616        } else if element.is(&Ns::Text, "list-style")
617            && let Some(name) = element.attr(&Ns::Style, "name")
618        {
619            self.lists.insert(name.to_owned(), element.clone());
620        }
621    }
622
623    /// Take in a style an edit has written into the document, so that it
624    /// resolves from now on.
625    ///
626    /// Nothing is ever taken out: a style an undo removed from the tree stays
627    /// here, which keeps its name from being given to a different style while
628    /// a resolution of it may still be cached.
629    pub fn add(&mut self, element: &Element) {
630        if let (Some(name), Some(family)) = (
631            element.attr(&Ns::Style, "name"),
632            element.attr(&Ns::Style, "family"),
633        ) {
634            let key = (Family::parse(family), name.to_owned());
635            self.cache.borrow_mut().remove(&key);
636        }
637        self.collect_style(element);
638    }
639
640    /// A style by family and name.
641    pub fn style(&self, family: &Family, name: &str) -> Option<&Style> {
642        self.by_name.get(&(family.clone(), name.to_owned()))
643    }
644
645    /// A gradient by name, as a fill refers to one.
646    pub fn gradient(&self, name: &str) -> Option<&Gradient> {
647        self.gradients.get(name)
648    }
649
650    /// Where the picture of a named fill image lives inside the package.
651    pub fn fill_image(&self, name: &str) -> Option<&str> {
652        self.fill_images.get(name).map(String::as_str)
653    }
654
655    /// The paragraph style a document gives headings of a level: the one that
656    /// declares `style:default-outline-level` as that level, and failing that
657    /// the one `LibreOffice` names `Heading_20_{level}`.
658    pub fn heading_style(&self, level: u8) -> Option<&str> {
659        let wanted = level.to_string();
660        let declared = self
661            .by_name
662            .iter()
663            .filter(|((family, _), style)| {
664                *family == Family::Paragraph
665                    && style.element.attr(&Ns::Style, "default-outline-level")
666                        == Some(wanted.as_str())
667            })
668            .map(|((_, name), _)| name.as_str())
669            .min();
670        declared.or_else(|| {
671            let conventional = format!("Heading_20_{level}");
672            self.by_name
673                .get_key_value(&(Family::Paragraph, conventional))
674                .map(|((_, name), _)| name.as_str())
675        })
676    }
677
678    /// Whether a style of a family and name is in the document.
679    pub fn has_style(&self, family: &Family, name: &str) -> bool {
680        self.by_name
681            .contains_key(&(family.clone(), name.to_owned()))
682    }
683
684    /// Whether a list style numbers its items or marks them, by the first of
685    /// its levels that says.
686    pub fn list_kind(&self, name: &str) -> Option<ListKind> {
687        self.lists.get(name)?.elements().find_map(|level| {
688            if level.name.ns != Ns::Text {
689                return None;
690            }
691            match &*level.name.local {
692                "list-level-style-number" => Some(ListKind::Number),
693                "list-level-style-bullet" | "list-level-style-image" => Some(ListKind::Bullet),
694                _ => None,
695            }
696        })
697    }
698
699    /// A list style of a kind the document already has, the first by name.
700    pub fn list_style_for(&self, kind: ListKind) -> Option<&str> {
701        self.lists
702            .keys()
703            .filter(|name| self.list_kind(name) == Some(kind))
704            .map(String::as_str)
705            .min()
706    }
707
708    /// A list style by name, as the `text:list-style-name` of a list refers to
709    /// one.
710    pub fn list_style(&self, name: &str) -> Option<&Element> {
711        self.lists.get(name)
712    }
713
714    /// A master page by name.
715    pub fn master_page(&self, name: &str) -> Option<&Element> {
716        self.master_pages.get(name)
717    }
718
719    /// The page layout a master page points at.
720    pub fn page_layout_of(&self, master_page: &str) -> Option<&PageLayout> {
721        let master = self.master_pages.get(master_page)?;
722        let layout = master.attr(&Ns::Style, "page-layout-name")?;
723        self.page_layouts.get(layout)
724    }
725
726    /// The resolved properties of a style, with its whole inheritance chain and
727    /// its family's default applied.
728    ///
729    /// A name that is not in the document resolves to the family's default,
730    /// which is what an application does with a dangling style reference: the
731    /// paragraph is shown rather than refused.
732    pub fn resolve(&self, family: &Family, name: &str) -> Rc<Properties> {
733        let key = (family.clone(), name.to_owned());
734        if let Some(cached) = self.cache.borrow().get(&key) {
735            return Rc::clone(cached);
736        }
737
738        let mut properties = Properties::default();
739        if let Some(default) = self.defaults.get(family) {
740            properties.apply(default, &self.font_faces);
741        }
742        // Root first, so that the style asked for is applied last and wins.
743        for style in self.chain(family, name).into_iter().rev() {
744            properties.apply(&style.element, &self.font_faces);
745        }
746
747        let properties = Rc::new(properties);
748        self.cache.borrow_mut().insert(key, Rc::clone(&properties));
749        properties
750    }
751
752    /// The style and its ancestors, nearest first.
753    ///
754    /// A cycle in the chain — which no writer produces and a hand-edited file
755    /// can — stops at the style it returns to rather than hanging.
756    fn chain(&self, family: &Family, name: &str) -> Vec<&Style> {
757        let mut chain: Vec<&Style> = Vec::new();
758        let mut next: Option<&str> = Some(name);
759        while let Some(current) = next {
760            if chain.iter().any(|s| s.name == current) {
761                break;
762            }
763            let Some(style) = self.style(family, current) else {
764                break;
765            };
766            next = style.parent.as_deref();
767            chain.push(style);
768        }
769        chain
770    }
771
772    /// The family the properties of a cell's text come from: a cell style's
773    /// `style:parent-style-name` chain carries the paragraph and text
774    /// properties, so a cell resolves in one call.
775    pub fn font_family(&self, declared: &str) -> String {
776        self.font_faces
777            .get(declared)
778            .cloned()
779            .unwrap_or_else(|| declared.to_owned())
780    }
781}
782
783fn gradient(element: &Element) -> Gradient {
784    let color = |local: &str, fallback: Color| {
785        element
786            .attr(&Ns::Draw, local)
787            .and_then(Color::parse)
788            .unwrap_or(fallback)
789    };
790    let proportion = |local: &str| {
791        element
792            .attr(&Ns::Draw, local)
793            .and_then(Percent::parse)
794            .map_or(0.0, Percent::fraction)
795    };
796    Gradient {
797        style: match element.attr(&Ns::Draw, "style") {
798            Some("axial") => GradientStyle::Axial,
799            Some("radial") => GradientStyle::Radial,
800            Some("ellipsoid") => GradientStyle::Ellipsoidal,
801            Some("square") => GradientStyle::Square,
802            Some("rectangular") => GradientStyle::Rectangular,
803            _ => GradientStyle::Linear,
804        },
805        start: color("start-color", Color { r: 0, g: 0, b: 0 }),
806        end: color(
807            "end-color",
808            Color {
809                r: 0xff,
810                g: 0xff,
811                b: 0xff,
812            },
813        ),
814        // Written as `270deg`, and occasionally as a bare tenth of a degree by
815        // producers older than the unit.
816        angle: element.attr(&Ns::Draw, "angle").map_or(0.0, parse_angle),
817        border: proportion("border"),
818        center: (proportion("cx"), proportion("cy")),
819    }
820}
821
822/// An ODF angle in degrees.
823///
824/// `270deg` is the spelling ODF 1.2 introduced. Before it the attribute was a
825/// plain number in tenths of a degree, which some producers still write, so a
826/// value with no unit is read that way.
827fn parse_angle(text: &str) -> f32 {
828    let text = text.trim();
829    match text.strip_suffix("deg") {
830        Some(degrees) => degrees.trim().parse().unwrap_or(0.0),
831        None => text.parse::<f32>().unwrap_or(0.0) / 10.0,
832    }
833}
834
835fn page_layout(element: &Element) -> PageLayout {
836    let mut layout = PageLayout::default();
837    if let Some(properties) = element.child(&Ns::Style, "page-layout-properties") {
838        if let Some(width) = properties
839            .attr(&Ns::Fo, "page-width")
840            .and_then(Length::parse)
841        {
842            layout.width = width;
843        }
844        if let Some(height) = properties
845            .attr(&Ns::Fo, "page-height")
846            .and_then(Length::parse)
847        {
848            layout.height = height;
849        }
850        let mut margin = Edges::default();
851        read_edges(properties, "margin", &mut margin, Length::parse);
852        // An edge the layout does not name keeps the default rather than
853        // becoming nothing, because a page layout that sets only its top margin
854        // is not asking for the other three to be zero.
855        layout.margin.left = margin.left.or(layout.margin.left);
856        layout.margin.right = margin.right.or(layout.margin.right);
857        layout.margin.top = margin.top.or(layout.margin.top);
858        layout.margin.bottom = margin.bottom.or(layout.margin.bottom);
859    }
860    layout
861}
862
863/// Read ODF's edge shorthand: `fo:margin` sets all four, and
864/// `fo:margin-left` and its siblings override one each.
865fn read_edges<T: Copy>(
866    properties: &Element,
867    base: &str,
868    into: &mut Edges<T>,
869    parse: impl Fn(&str) -> Option<T>,
870) {
871    if let Some(all) = properties.attr(&Ns::Fo, base).and_then(&parse) {
872        *into = Edges {
873            left: Some(all),
874            right: Some(all),
875            top: Some(all),
876            bottom: Some(all),
877        };
878    }
879    if let Some(v) = properties
880        .attr(&Ns::Fo, &format!("{base}-left"))
881        .and_then(&parse)
882    {
883        into.left = Some(v);
884    }
885    if let Some(v) = properties
886        .attr(&Ns::Fo, &format!("{base}-right"))
887        .and_then(&parse)
888    {
889        into.right = Some(v);
890    }
891    if let Some(v) = properties
892        .attr(&Ns::Fo, &format!("{base}-top"))
893        .and_then(&parse)
894    {
895        into.top = Some(v);
896    }
897    if let Some(v) = properties
898        .attr(&Ns::Fo, &format!("{base}-bottom"))
899        .and_then(&parse)
900    {
901        into.bottom = Some(v);
902    }
903}
904
905impl Properties {
906    /// Apply one style's property elements over what is already here.
907    ///
908    /// Only a property the element states is changed; everything else keeps the
909    /// value it inherited, which is what makes walking a chain root first give
910    /// the right answer.
911    fn apply(&mut self, style: &Element, font_faces: &HashMap<String, String>) {
912        for properties in style.elements() {
913            if properties.is(&Ns::Style, "text-properties") {
914                self.text.apply(properties, font_faces);
915            } else if properties.is(&Ns::Style, "paragraph-properties") {
916                self.paragraph.apply(properties);
917            } else if properties.is(&Ns::Style, "table-cell-properties") {
918                self.cell.apply(properties);
919            } else if properties.is(&Ns::Style, "graphic-properties")
920                || properties.is(&Ns::Style, "drawing-page-properties")
921            {
922                self.graphic.apply(properties);
923                // Two of a slide's own switches over what its master gives it.
924                // They live on the same element as the fill and are read here so
925                // that they inherit through the style chain like everything else.
926                if let Some(visible) = properties
927                    .attr(&Ns::Presentation, "background-visible")
928                    .and_then(crate::value::boolean)
929                {
930                    self.background_visible = Some(visible);
931                }
932                if let Some(visible) = properties
933                    .attr(&Ns::Presentation, "background-objects-visible")
934                    .and_then(crate::value::boolean)
935                {
936                    self.background_objects_visible = Some(visible);
937                }
938            } else if properties.is(&Ns::Style, "table-column-properties") {
939                if let Some(width) = properties
940                    .attr(&Ns::Style, "column-width")
941                    .and_then(Length::parse)
942                {
943                    self.column_width = Some(width);
944                }
945            } else if properties.is(&Ns::Style, "table-row-properties")
946                && let Some(height) = properties
947                    .attr(&Ns::Style, "row-height")
948                    .and_then(Length::parse)
949            {
950                self.row_height = Some(height);
951            }
952        }
953    }
954}
955
956impl TextProperties {
957    fn apply(&mut self, p: &Element, font_faces: &HashMap<String, String>) {
958        // `style:font-name` points at a font face declaration and `fo:font-family`
959        // names a family directly. A style may carry both, and the declaration is
960        // the more specific of the two.
961        if let Some(name) = p.attr(&Ns::Fo, "font-family") {
962            self.font_family = Some(name.trim_matches('\'').to_owned());
963        }
964        if let Some(name) = p.attr(&Ns::Style, "font-name") {
965            self.font_family = Some(
966                font_faces
967                    .get(name)
968                    .cloned()
969                    .unwrap_or_else(|| name.to_owned()),
970            );
971        }
972        if let Some(size) = p.attr(&Ns::Fo, "font-size").and_then(Measure::parse) {
973            self.size = Some(size);
974        }
975        if let Some(weight) = p.attr(&Ns::Fo, "font-weight") {
976            // A numeric weight is the CSS scale, where 600 and above reads as
977            // bold to anything that has two faces to choose between.
978            self.bold = Some(match weight {
979                "normal" => false,
980                "bold" => true,
981                other => other.parse::<u32>().is_ok_and(|n| n >= 600),
982            });
983        }
984        if let Some(style) = p.attr(&Ns::Fo, "font-style") {
985            self.italic = Some(style != "normal");
986        }
987        if let Some(line) = p.attr(&Ns::Style, "text-underline-style") {
988            self.underline = Some(line != "none");
989        }
990        if let Some(line) = p.attr(&Ns::Style, "text-line-through-style") {
991            self.strike = Some(line != "none");
992        }
993        if let Some(color) = p.attr(&Ns::Fo, "color") {
994            self.color = Color::parse(color);
995        }
996        if let Some(color) = p.attr(&Ns::Fo, "background-color") {
997            self.background = Color::parse(color);
998        }
999        if let Some(position) = p.attr(&Ns::Style, "text-position") {
1000            // The attribute is a vertical offset and optionally a size, as in
1001            // `super 58%` or `-33% 58%`. Only the direction is read: a renderer
1002            // that placed the glyph at the stated offset and scaled it by the
1003            // stated amount would be doing typesetting, and what is wanted here
1004            // is the distinction between superscript and subscript.
1005            let first = position.split_whitespace().next().unwrap_or_default();
1006            self.position = Some(match first {
1007                "super" => Position::Super,
1008                "sub" => Position::Sub,
1009                _ => match Percent::parse(first) {
1010                    Some(percent) if percent.0 > 0.0 => Position::Super,
1011                    Some(percent) if percent.0 < 0.0 => Position::Sub,
1012                    _ => Position::Baseline,
1013                },
1014            });
1015        }
1016        if let Some(transform) = p.attr(&Ns::Fo, "text-transform") {
1017            self.uppercase = Some(transform == "uppercase");
1018        }
1019    }
1020}
1021
1022impl ParagraphProperties {
1023    fn apply(&mut self, p: &Element) {
1024        if let Some(align) = p.attr(&Ns::Fo, "text-align") {
1025            self.align = match align {
1026                // `left` and `right` are the writing-direction-independent
1027                // spellings' siblings, and for a left-to-right document they are
1028                // the same thing. A right-to-left document would need the
1029                // direction to tell them apart, which is what §6 of DESIGN.md
1030                // says this release does not do.
1031                "start" | "left" => Some(TextAlign::Start),
1032                "end" | "right" => Some(TextAlign::End),
1033                "center" => Some(TextAlign::Center),
1034                "justify" => Some(TextAlign::Justify),
1035                _ => self.align,
1036            };
1037        }
1038        read_edges(p, "margin", &mut self.margin, Length::parse);
1039        read_edges(p, "padding", &mut self.padding, Length::parse);
1040        read_edges(p, "border", &mut self.border, Border::parse);
1041        if let Some(indent) = p.attr(&Ns::Fo, "text-indent").and_then(Length::parse) {
1042            self.text_indent = Some(indent);
1043        }
1044        if let Some(height) = p.attr(&Ns::Fo, "line-height") {
1045            self.line_height = Measure::parse(height);
1046        }
1047        if let Some(color) = p.attr(&Ns::Fo, "background-color") {
1048            self.background = Color::parse(color);
1049        }
1050        if let Some(before) = p.attr(&Ns::Fo, "break-before") {
1051            self.break_before = Some(parse_break(before));
1052        }
1053        if let Some(after) = p.attr(&Ns::Fo, "break-after") {
1054            self.break_after = Some(parse_break(after));
1055        }
1056    }
1057}
1058
1059fn parse_break(text: &str) -> Break {
1060    match text {
1061        "page" => Break::Page,
1062        "column" => Break::Column,
1063        _ => Break::Auto,
1064    }
1065}
1066
1067impl CellProperties {
1068    fn apply(&mut self, p: &Element) {
1069        if let Some(color) = p.attr(&Ns::Fo, "background-color") {
1070            self.background = Color::parse(color);
1071        }
1072        if let Some(align) = p.attr(&Ns::Style, "vertical-align") {
1073            self.vertical_align = match align {
1074                "top" => Some(VerticalAlign::Top),
1075                "middle" => Some(VerticalAlign::Middle),
1076                // `bottom`, and `automatic`, which is the fourth value ODF
1077                // defines and means bottom for a cell: it is what a spreadsheet
1078                // shows for a cell nobody has set.
1079                _ => Some(VerticalAlign::Bottom),
1080            };
1081        }
1082        read_edges(p, "border", &mut self.border, Border::parse);
1083        read_edges(p, "padding", &mut self.padding, Length::parse);
1084        if let Some(wrap) = p.attr(&Ns::Fo, "wrap-option") {
1085            self.wrap = Some(wrap == "wrap");
1086        }
1087    }
1088}
1089
1090impl GraphicProperties {
1091    fn apply(&mut self, p: &Element) {
1092        // Each of these is its own property and each inherits on its own. A
1093        // style that changes only the shade of an already-solid shape writes the
1094        // colour and nothing else; one that turns the fill off writes the kind
1095        // and nothing else.
1096        if let Some(kind) = p.attr(&Ns::Draw, "fill") {
1097            self.kind = Some(match kind {
1098                "solid" => FillKind::Solid,
1099                "gradient" => FillKind::Gradient,
1100                "bitmap" => FillKind::Bitmap,
1101                "hatch" => FillKind::Hatch,
1102                _ => FillKind::None,
1103            });
1104        }
1105        if let Some(color) = p.attr(&Ns::Draw, "fill-color").and_then(Color::parse) {
1106            self.color = Some(color);
1107        }
1108        if let Some(name) = p.attr(&Ns::Draw, "fill-gradient-name") {
1109            self.gradient = Some(name.to_owned());
1110        }
1111        // Only a stretched picture: see `Fill::Image`. A tiled one leaves the
1112        // name unset, so the fill resolves to nothing rather than to one tile
1113        // blown up to the size of the shape.
1114        if let Some(name) = p.attr(&Ns::Draw, "fill-image-name") {
1115            self.image = (p.attr(&Ns::Style, "repeat").unwrap_or("stretch") == "stretch")
1116                .then(|| name.to_owned());
1117        }
1118
1119        match p.attr(&Ns::Draw, "stroke") {
1120            Some("none") => self.stroke = None,
1121            _ => {
1122                if let Some(color) = p.attr(&Ns::Svg, "stroke-color").and_then(Color::parse) {
1123                    self.stroke = Some(color);
1124                }
1125            }
1126        }
1127        if let Some(width) = p.attr(&Ns::Svg, "stroke-width").and_then(Length::parse) {
1128            self.stroke_width = Some(width);
1129        }
1130        if let Some(opacity) = p.attr(&Ns::Draw, "opacity").and_then(Percent::parse) {
1131            self.opacity = Some(opacity.fraction().clamp(0.0, 1.0));
1132        }
1133        // `justify` spreads the lines to fill the height, which needs a line
1134        // box this renderer does not build; it reads as the top, which is where
1135        // the first line goes either way.
1136        if let Some(anchor) = p.attr(&Ns::Draw, "textarea-vertical-align") {
1137            self.text_anchor = Some(match anchor {
1138                "middle" => Anchor::Middle,
1139                "bottom" => Anchor::Bottom,
1140                _ => Anchor::Top,
1141            });
1142        }
1143    }
1144}