Skip to main content

rdocx_oxml/
styles.rs

1//! Style elements: `CT_Styles`, `CT_Style`, `CT_DocDefaults`.
2
3use quick_xml::events::{BytesDecl, BytesEnd, BytesStart, Event};
4use quick_xml::{Reader, Writer};
5
6use crate::error::{OxmlError, Result};
7use crate::namespace::{W_NS, matches_local_name};
8use crate::numbering::{parse_scoped_ppr, word_prefixes_at};
9use crate::properties::{CT_PPr, CT_RPr, is_word_element};
10use crate::raw_xml::capture_element;
11
12/// The type of a style.
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14pub enum StyleType {
15    Paragraph,
16    Character,
17    Table,
18    Numbering,
19}
20
21impl StyleType {
22    pub fn from_str(s: &str) -> Result<Self> {
23        match s {
24            "paragraph" => Ok(StyleType::Paragraph),
25            "character" => Ok(StyleType::Character),
26            "table" => Ok(StyleType::Table),
27            "numbering" => Ok(StyleType::Numbering),
28            _ => Err(OxmlError::InvalidValue(format!("invalid style type: {s}"))),
29        }
30    }
31
32    pub fn to_str(self) -> &'static str {
33        match self {
34            StyleType::Paragraph => "paragraph",
35            StyleType::Character => "character",
36            StyleType::Table => "table",
37            StyleType::Numbering => "numbering",
38        }
39    }
40}
41
42/// `CT_Style` — A single style definition.
43#[derive(Debug, Clone, PartialEq)]
44#[allow(non_snake_case)]
45pub struct CT_Style {
46    pub style_id: String,
47    pub style_type: StyleType,
48    pub name: Option<String>,
49    pub based_on: Option<String>,
50    pub next_style: Option<String>,
51    pub is_default: bool,
52    pub ppr: Option<CT_PPr>,
53    pub rpr: Option<CT_RPr>,
54}
55
56#[allow(non_snake_case)]
57impl CT_Style {
58    pub fn from_xml(reader: &mut Reader<&[u8]>, attrs: &BytesStart) -> Result<Self> {
59        let prefixes = word_prefixes_at(attrs, &["w".to_string()])?;
60        Self::from_xml_with_prefixes(reader, attrs, &prefixes)
61    }
62
63    fn from_xml_with_prefixes(
64        reader: &mut Reader<&[u8]>,
65        attrs: &BytesStart,
66        word_prefixes: &[String],
67    ) -> Result<Self> {
68        let mut style_id = String::new();
69        let mut style_type = StyleType::Paragraph;
70        let mut is_default = false;
71
72        for attr in attrs.attributes() {
73            let attr = attr?;
74            let key = attr.key.as_ref();
75            if matches_local_name(key, b"styleId") {
76                style_id = std::str::from_utf8(&attr.value)?.to_string();
77            } else if matches_local_name(key, b"type") {
78                style_type = StyleType::from_str(std::str::from_utf8(&attr.value)?)?;
79            } else if matches_local_name(key, b"default") {
80                is_default = std::str::from_utf8(&attr.value)? == "1"
81                    || std::str::from_utf8(&attr.value)? == "true";
82            }
83        }
84
85        let mut name = None;
86        let mut based_on = None;
87        let mut next_style = None;
88        let mut ppr = None;
89        let mut rpr = None;
90        let mut buf = Vec::new();
91
92        loop {
93            match reader.read_event_into(&mut buf) {
94                Ok(Event::Empty(ref e)) => {
95                    let ename = e.name();
96                    if matches_local_name(ename.as_ref(), b"name") {
97                        name = get_val_attr(e)?;
98                    } else if matches_local_name(ename.as_ref(), b"basedOn") {
99                        based_on = get_val_attr(e)?;
100                    } else if matches_local_name(ename.as_ref(), b"next") {
101                        next_style = get_val_attr(e)?;
102                    }
103                }
104                Ok(Event::Start(ref e)) => {
105                    let ename = e.name();
106                    let prefixes = word_prefixes_at(e, word_prefixes)?;
107                    if is_word_element(ename.as_ref(), b"pPr", &prefixes) {
108                        let raw = capture_element(reader, e)?;
109                        ppr = Some(parse_scoped_ppr(&raw, &prefixes)?);
110                    } else if matches_local_name(ename.as_ref(), b"rPr") {
111                        rpr = Some(CT_RPr::from_xml(reader)?);
112                    } else {
113                        reader.read_to_end_into(ename, &mut Vec::new())?;
114                    }
115                }
116                Ok(Event::End(ref e)) if matches_local_name(e.name().as_ref(), b"style") => {
117                    break;
118                }
119                Ok(Event::Eof) => break,
120                Err(e) => return Err(e.into()),
121                _ => {}
122            }
123            buf.clear();
124        }
125
126        Ok(CT_Style {
127            style_id,
128            style_type,
129            name,
130            based_on,
131            next_style,
132            is_default,
133            ppr,
134            rpr,
135        })
136    }
137
138    pub fn to_xml<W: std::io::Write>(&self, writer: &mut Writer<W>) -> Result<()> {
139        let mut e = BytesStart::new("w:style");
140        e.push_attribute(("w:type", self.style_type.to_str()));
141        e.push_attribute(("w:styleId", self.style_id.as_str()));
142        if self.is_default {
143            e.push_attribute(("w:default", "1"));
144        }
145        writer.write_event(Event::Start(e))?;
146
147        if let Some(ref name) = self.name {
148            let mut ne = BytesStart::new("w:name");
149            ne.push_attribute(("w:val", name.as_str()));
150            writer.write_event(Event::Empty(ne))?;
151        }
152
153        if let Some(ref based_on) = self.based_on {
154            let mut be = BytesStart::new("w:basedOn");
155            be.push_attribute(("w:val", based_on.as_str()));
156            writer.write_event(Event::Empty(be))?;
157        }
158
159        if let Some(ref next) = self.next_style {
160            let mut ne = BytesStart::new("w:next");
161            ne.push_attribute(("w:val", next.as_str()));
162            writer.write_event(Event::Empty(ne))?;
163        }
164
165        if let Some(ref ppr) = self.ppr {
166            ppr.to_xml(writer)?;
167        }
168        if let Some(ref rpr) = self.rpr {
169            rpr.to_xml(writer)?;
170        }
171
172        writer.write_event(Event::End(BytesEnd::new("w:style")))?;
173        Ok(())
174    }
175}
176
177/// `CT_DocDefaults` — Document-level default properties.
178#[derive(Debug, Clone, Default, PartialEq)]
179#[allow(non_snake_case)]
180pub struct CT_DocDefaults {
181    pub rpr: Option<CT_RPr>,
182    pub ppr: Option<CT_PPr>,
183}
184
185#[allow(non_snake_case)]
186impl CT_DocDefaults {
187    pub fn from_xml(reader: &mut Reader<&[u8]>) -> Result<Self> {
188        Self::from_xml_with_prefixes(reader, &["w".to_string()])
189    }
190
191    fn from_xml_with_prefixes(
192        reader: &mut Reader<&[u8]>,
193        word_prefixes: &[String],
194    ) -> Result<Self> {
195        let mut defaults = CT_DocDefaults::default();
196        let mut buf = Vec::new();
197
198        loop {
199            match reader.read_event_into(&mut buf) {
200                Ok(Event::Start(ref e)) => {
201                    let name = e.name();
202                    let prefixes = word_prefixes_at(e, word_prefixes)?;
203                    if matches_local_name(name.as_ref(), b"rPrDefault") {
204                        // Read into rPrDefault, expecting rPr child
205                        defaults.rpr = Self::parse_pr_default(reader, b"rPrDefault")?;
206                    } else if matches_local_name(name.as_ref(), b"pPrDefault") {
207                        defaults.ppr = Self::parse_ppr_default(reader, &prefixes)?;
208                    } else {
209                        reader.read_to_end_into(name, &mut Vec::new())?;
210                    }
211                }
212                Ok(Event::End(ref e)) if matches_local_name(e.name().as_ref(), b"docDefaults") => {
213                    break;
214                }
215                Ok(Event::Eof) => break,
216                Err(e) => return Err(e.into()),
217                _ => {}
218            }
219            buf.clear();
220        }
221
222        Ok(defaults)
223    }
224
225    fn parse_pr_default(reader: &mut Reader<&[u8]>, end_tag: &[u8]) -> Result<Option<CT_RPr>> {
226        let mut rpr = None;
227        let mut buf = Vec::new();
228
229        loop {
230            match reader.read_event_into(&mut buf) {
231                Ok(Event::Start(ref e)) => {
232                    let name = e.name();
233                    if matches_local_name(name.as_ref(), b"rPr") {
234                        rpr = Some(CT_RPr::from_xml(reader)?);
235                    } else {
236                        reader.read_to_end_into(name, &mut Vec::new())?;
237                    }
238                }
239                Ok(Event::End(ref e)) if matches_local_name(e.name().as_ref(), end_tag) => {
240                    break;
241                }
242                Ok(Event::Eof) => break,
243                Err(e) => return Err(e.into()),
244                _ => {}
245            }
246            buf.clear();
247        }
248
249        Ok(rpr)
250    }
251
252    fn parse_ppr_default(
253        reader: &mut Reader<&[u8]>,
254        word_prefixes: &[String],
255    ) -> Result<Option<CT_PPr>> {
256        let mut ppr = None;
257        let mut buf = Vec::new();
258
259        loop {
260            match reader.read_event_into(&mut buf) {
261                Ok(Event::Start(ref e)) => {
262                    let name = e.name();
263                    let prefixes = word_prefixes_at(e, word_prefixes)?;
264                    if is_word_element(name.as_ref(), b"pPr", &prefixes) {
265                        let raw = capture_element(reader, e)?;
266                        ppr = Some(parse_scoped_ppr(&raw, &prefixes)?);
267                    } else {
268                        reader.read_to_end_into(name, &mut Vec::new())?;
269                    }
270                }
271                Ok(Event::End(ref e)) if matches_local_name(e.name().as_ref(), b"pPrDefault") => {
272                    break;
273                }
274                Ok(Event::Eof) => break,
275                Err(e) => return Err(e.into()),
276                _ => {}
277            }
278            buf.clear();
279        }
280
281        Ok(ppr)
282    }
283
284    pub fn to_xml<W: std::io::Write>(&self, writer: &mut Writer<W>) -> Result<()> {
285        writer.write_event(Event::Start(BytesStart::new("w:docDefaults")))?;
286
287        if let Some(ref rpr) = self.rpr {
288            writer.write_event(Event::Start(BytesStart::new("w:rPrDefault")))?;
289            rpr.to_xml(writer)?;
290            writer.write_event(Event::End(BytesEnd::new("w:rPrDefault")))?;
291        }
292
293        if let Some(ref ppr) = self.ppr {
294            writer.write_event(Event::Start(BytesStart::new("w:pPrDefault")))?;
295            ppr.to_xml(writer)?;
296            writer.write_event(Event::End(BytesEnd::new("w:pPrDefault")))?;
297        }
298
299        writer.write_event(Event::End(BytesEnd::new("w:docDefaults")))?;
300        Ok(())
301    }
302}
303
304/// `CT_Styles` — The styles part (word/styles.xml).
305#[derive(Debug, Clone, PartialEq)]
306#[allow(non_snake_case)]
307pub struct CT_Styles {
308    pub doc_defaults: Option<CT_DocDefaults>,
309    pub styles: Vec<CT_Style>,
310}
311
312#[allow(non_snake_case)]
313impl CT_Styles {
314    pub fn new() -> Self {
315        CT_Styles {
316            doc_defaults: None,
317            styles: Vec::new(),
318        }
319    }
320
321    /// Parse from XML bytes (the content of word/styles.xml).
322    pub fn from_xml(xml: &[u8]) -> Result<Self> {
323        let mut reader = Reader::from_reader(xml);
324        reader.config_mut().trim_text(true);
325
326        let mut doc_defaults = None;
327        let mut styles = Vec::new();
328        let mut buf = Vec::new();
329        let mut word_prefixes = Vec::new();
330
331        loop {
332            match reader.read_event_into(&mut buf) {
333                Ok(Event::Start(ref e)) => {
334                    let name = e.name();
335                    let prefixes = word_prefixes_at(e, &word_prefixes)?;
336                    if matches_local_name(name.as_ref(), b"docDefaults") {
337                        doc_defaults = Some(CT_DocDefaults::from_xml_with_prefixes(
338                            &mut reader,
339                            &prefixes,
340                        )?);
341                    } else if matches_local_name(name.as_ref(), b"style") {
342                        styles.push(CT_Style::from_xml_with_prefixes(&mut reader, e, &prefixes)?);
343                    } else if matches_local_name(name.as_ref(), b"styles") {
344                        // Root element, continue
345                        word_prefixes = prefixes;
346                    } else {
347                        reader.read_to_end_into(name, &mut Vec::new())?;
348                    }
349                }
350                Ok(Event::Eof) => break,
351                Err(e) => return Err(e.into()),
352                _ => {}
353            }
354            buf.clear();
355        }
356
357        Ok(CT_Styles {
358            doc_defaults,
359            styles,
360        })
361    }
362
363    /// Serialize to XML bytes.
364    pub fn to_xml(&self) -> Result<Vec<u8>> {
365        let mut writer = Writer::new_with_indent(Vec::new(), b' ', 2);
366
367        writer.write_event(Event::Decl(BytesDecl::new(
368            "1.0",
369            Some("UTF-8"),
370            Some("yes"),
371        )))?;
372
373        let mut styles_start = BytesStart::new("w:styles");
374        styles_start.push_attribute(("xmlns:w", W_NS));
375        styles_start.push_attribute((
376            "xmlns:r",
377            "http://schemas.openxmlformats.org/officeDocument/2006/relationships",
378        ));
379        writer.write_event(Event::Start(styles_start))?;
380
381        if let Some(ref defaults) = self.doc_defaults {
382            defaults.to_xml(&mut writer)?;
383        }
384
385        for style in &self.styles {
386            style.to_xml(&mut writer)?;
387        }
388
389        writer.write_event(Event::End(BytesEnd::new("w:styles")))?;
390
391        Ok(writer.into_inner())
392    }
393
394    /// Find a style by its ID.
395    pub fn get_by_id(&self, style_id: &str) -> Option<&CT_Style> {
396        self.styles.iter().find(|s| s.style_id == style_id)
397    }
398
399    /// Find the default style for a given type.
400    pub fn get_default(&self, style_type: StyleType) -> Option<&CT_Style> {
401        self.styles
402            .iter()
403            .find(|s| s.style_type == style_type && s.is_default)
404    }
405
406    /// Create a minimal default styles part for a new document.
407    pub fn new_default() -> Self {
408        use crate::units::HalfPoint;
409
410        let normal = CT_Style {
411            style_id: "Normal".to_string(),
412            style_type: StyleType::Paragraph,
413            name: Some("Normal".to_string()),
414            based_on: None,
415            next_style: None,
416            is_default: true,
417            ppr: None,
418            rpr: None,
419        };
420
421        let heading1 = CT_Style {
422            style_id: "Heading1".to_string(),
423            style_type: StyleType::Paragraph,
424            name: Some("heading 1".to_string()),
425            based_on: Some("Normal".to_string()),
426            next_style: Some("Normal".to_string()),
427            is_default: false,
428            ppr: Some(CT_PPr {
429                keep_next: Some(true),
430                keep_lines: Some(true),
431                space_before: Some(crate::units::Twips(240)),
432                space_after: Some(crate::units::Twips(0)),
433                ..Default::default()
434            }),
435            rpr: Some(CT_RPr {
436                sz: Some(HalfPoint(32)),
437                sz_cs: Some(HalfPoint(32)),
438                bold: Some(true),
439                bold_cs: Some(true),
440                color: Some("2F5496".to_string()),
441                ..Default::default()
442            }),
443        };
444
445        let doc_defaults = CT_DocDefaults {
446            rpr: Some(CT_RPr {
447                font_ascii: Some("Calibri".to_string()),
448                font_hansi: Some("Calibri".to_string()),
449                font_east_asia: Some("Calibri".to_string()),
450                font_cs: Some("Times New Roman".to_string()),
451                sz: Some(HalfPoint(22)),
452                sz_cs: Some(HalfPoint(22)),
453                ..Default::default()
454            }),
455            ppr: Some(CT_PPr {
456                space_after: Some(crate::units::Twips(160)),
457                line_spacing: Some(crate::units::Twips(259)),
458                line_rule: Some("auto".to_string()),
459                ..Default::default()
460            }),
461        };
462
463        CT_Styles {
464            doc_defaults: Some(doc_defaults),
465            styles: vec![normal, heading1],
466        }
467    }
468}
469
470impl Default for CT_Styles {
471    fn default() -> Self {
472        Self::new()
473    }
474}
475
476/// Extract the `w:val` attribute from an element.
477fn get_val_attr(e: &BytesStart) -> Result<Option<String>> {
478    for attr in e.attributes() {
479        let attr = attr?;
480        if matches_local_name(attr.key.as_ref(), b"val") {
481            return Ok(Some(std::str::from_utf8(&attr.value)?.to_string()));
482        }
483    }
484    Ok(None)
485}
486
487#[cfg(test)]
488mod tests {
489    use super::*;
490
491    #[test]
492    fn round_trip_styles() {
493        let styles = CT_Styles::new_default();
494        let xml = styles.to_xml().unwrap();
495        let parsed = CT_Styles::from_xml(&xml).unwrap();
496
497        assert_eq!(parsed.styles.len(), 2);
498        assert!(parsed.doc_defaults.is_some());
499
500        let normal = parsed.get_by_id("Normal").unwrap();
501        assert_eq!(normal.name, Some("Normal".to_string()));
502        assert!(normal.is_default);
503
504        let h1 = parsed.get_by_id("Heading1").unwrap();
505        assert_eq!(h1.based_on, Some("Normal".to_string()));
506    }
507
508    #[test]
509    fn find_default_style() {
510        let styles = CT_Styles::new_default();
511        let default_para = styles.get_default(StyleType::Paragraph).unwrap();
512        assert_eq!(default_para.style_id, "Normal");
513    }
514
515    #[test]
516    fn aliased_style_paragraph_properties_use_ancestor_namespace_scope() {
517        let xml = format!(
518            r#"<q:styles xmlns:q="{W_NS}" xmlns:ext="urn:producer"><q:style q:type="paragraph" q:styleId="Alias"><q:pPr><ext:jc ext:val="right"/><q:jc q:val="center"/></q:pPr></q:style></q:styles>"#
519        );
520        let parsed = CT_Styles::from_xml(xml.as_bytes()).unwrap();
521        let ppr = parsed.styles[0].ppr.as_ref().unwrap();
522        assert_eq!(ppr.jc, Some(crate::shared::ST_Jc::Center));
523    }
524
525    #[test]
526    fn direct_style_parser_uses_supplied_start_ancestor_scope() {
527        let xml = format!(
528            r#"<outer xmlns:ext="urn:producer"><q:style xmlns:q="{W_NS}" q:type="paragraph" q:styleId="Direct"><ext:pPr><ext:jc ext:val="right"/></ext:pPr><q:pPr><ext:jc ext:val="right"/><q:jc q:val="center"/></q:pPr></q:style></outer>"#
529        );
530        let mut reader = Reader::from_str(&xml);
531        let mut buf = Vec::new();
532        let parsed = loop {
533            match reader.read_event_into(&mut buf) {
534                Ok(Event::Start(ref element)) if element.local_name().as_ref() == b"style" => {
535                    break CT_Style::from_xml(&mut reader, element).unwrap();
536                }
537                Ok(Event::Eof) => panic!("missing style"),
538                event => {
539                    event.unwrap();
540                }
541            }
542            buf.clear();
543        };
544        assert_eq!(
545            parsed.ppr.as_ref().unwrap().jc,
546            Some(crate::shared::ST_Jc::Center)
547        );
548    }
549
550    #[test]
551    fn direct_style_parser_does_not_promote_foreign_start_prefix() {
552        let xml = r#"<outer><ext:style xmlns:ext="urn:producer" ext:type="paragraph" ext:styleId="Foreign"><ext:pPr><ext:jc ext:val="right"/></ext:pPr></ext:style></outer>"#;
553        let mut reader = Reader::from_str(xml);
554        let mut buf = Vec::new();
555        let parsed = loop {
556            match reader.read_event_into(&mut buf) {
557                Ok(Event::Start(ref element)) if element.local_name().as_ref() == b"style" => {
558                    break CT_Style::from_xml(&mut reader, element).unwrap();
559                }
560                Ok(Event::Eof) => panic!("missing style"),
561                event => {
562                    event.unwrap();
563                }
564            }
565            buf.clear();
566        };
567        assert!(parsed.ppr.is_none());
568    }
569
570    #[test]
571    fn direct_style_parser_accepts_default_word_namespace() {
572        let xml = format!(
573            r#"<outer xmlns:ext="urn:producer"><style xmlns="{W_NS}" xmlns:w="{W_NS}" w:type="paragraph" w:styleId="Direct"><ext:pPr><ext:jc ext:val="right"/></ext:pPr><pPr><ext:jc ext:val="right"/><jc w:val="center"/></pPr></style></outer>"#
574        );
575        let mut reader = Reader::from_str(&xml);
576        let mut buf = Vec::new();
577        let parsed = loop {
578            match reader.read_event_into(&mut buf) {
579                Ok(Event::Start(ref element)) if element.local_name().as_ref() == b"style" => {
580                    break CT_Style::from_xml(&mut reader, element).unwrap();
581                }
582                Ok(Event::Eof) => panic!("missing style"),
583                event => {
584                    event.unwrap();
585                }
586            }
587            buf.clear();
588        };
589        assert_eq!(
590            parsed.ppr.as_ref().unwrap().jc,
591            Some(crate::shared::ST_Jc::Center)
592        );
593    }
594}