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