Skip to main content

oxml_drawing/text/
list_style.rs

1use std::io::Write;
2
3use oxml_core::OxmlError;
4use oxml_core::raw_xml::{capture_element, capture_empty_element};
5use oxml_core::xml::{local_name, matches_local_name};
6use quick_xml::events::{BytesEnd, BytesStart, Event};
7use quick_xml::{Reader, Writer, XmlVersion};
8
9use super::paragraph::{
10    TextNamespaceScope, text_close_generated_child, text_root_spellings_scoped, text_validate_root,
11};
12use crate::order::OrderedRawChildren;
13
14use super::body::{Result, TextError, missing_end};
15use super::paragraph::{
16    CT_TextParagraphProperties, text_capture_opaque_event, text_fragment_scope, text_owned_name,
17    text_root_spellings, text_source_start,
18};
19
20/// DrawingML list defaults for each of the nine paragraph levels.
21#[allow(non_camel_case_types)]
22#[derive(Clone, Debug, Default)]
23pub struct CT_TextListStyle {
24    pub default_paragraph_properties: Option<CT_TextParagraphProperties>,
25    pub level1: Option<CT_TextParagraphProperties>,
26    pub level2: Option<CT_TextParagraphProperties>,
27    pub level3: Option<CT_TextParagraphProperties>,
28    pub level4: Option<CT_TextParagraphProperties>,
29    pub level5: Option<CT_TextParagraphProperties>,
30    pub level6: Option<CT_TextParagraphProperties>,
31    pub level7: Option<CT_TextParagraphProperties>,
32    pub level8: Option<CT_TextParagraphProperties>,
33    pub level9: Option<CT_TextParagraphProperties>,
34    raw_attributes: Vec<(String, String)>,
35    source_root: Vec<String>,
36    source_tag: String,
37    source_namespaces: Vec<(String, String)>,
38    raw_children: OrderedRawChildren,
39}
40impl PartialEq for CT_TextListStyle {
41    fn eq(&self, other: &Self) -> bool {
42        self.default_paragraph_properties == other.default_paragraph_properties
43            && self.level1 == other.level1
44            && self.level2 == other.level2
45            && self.level3 == other.level3
46            && self.level4 == other.level4
47            && self.level5 == other.level5
48            && self.level6 == other.level6
49            && self.level7 == other.level7
50            && self.level8 == other.level8
51            && self.level9 == other.level9
52            && self
53                .raw_attributes
54                .iter()
55                .filter(|(key, _)| key != "xmlns" && !key.starts_with("xmlns:"))
56                .eq(other
57                    .raw_attributes
58                    .iter()
59                    .filter(|(key, _)| key != "xmlns" && !key.starts_with("xmlns:")))
60            && matches!(
61                (
62                    super::paragraph::text_namespace_scope(
63                        &text_source_start(&self.source_tag, &self.source_root),
64                        &[],
65                    ),
66                    super::paragraph::text_namespace_scope(
67                        &text_source_start(&other.source_tag, &other.source_root),
68                        &[],
69                    ),
70                ),
71                (Ok(left), Ok(right)) if left.iter().filter(|(key, value)| key != "r" || value != "http://schemas.openxmlformats.org/officeDocument/2006/relationships").map(|(key, value)| (key, value)).collect::<std::collections::BTreeMap<_, _>>()
72                    == right.iter().filter(|(key, value)| key != "r" || value != "http://schemas.openxmlformats.org/officeDocument/2006/relationships").map(|(key, value)| (key, value)).collect::<std::collections::BTreeMap<_, _>>()
73            )
74            && self.raw_children == other.raw_children
75    }
76}
77impl Eq for CT_TextListStyle {}
78
79impl CT_TextListStyle {
80    /// Parses a complete list-style element with any prefix, including the
81    /// PresentationML wrappers that share this DrawingML content model.
82    pub fn from_xml(xml: &[u8]) -> Result<Self> {
83        Self::from_xml_with_namespaces(xml, &TextNamespaceScope::default())
84    }
85
86    /// Parses a list-style fragment using its actual enclosing namespace bindings.
87    /// The original root attributes and opaque children remain retained source facts.
88    pub fn from_xml_with_inherited_namespaces(
89        xml: &[u8],
90        inherited: &[(String, String)],
91    ) -> Result<Self> {
92        Self::from_xml_with_namespaces(xml, &TextNamespaceScope::inherited(inherited))
93    }
94
95    pub(crate) fn from_xml_with_namespaces(
96        xml: &[u8],
97        inherited: &TextNamespaceScope,
98    ) -> Result<Self> {
99        Self::from_xml_boxed_with_namespaces(xml, inherited).map(|style| *style)
100    }
101
102    pub(super) fn from_xml_boxed_with_namespaces(
103        xml: &[u8],
104        inherited: &TextNamespaceScope,
105    ) -> Result<Box<Self>> {
106        let mut reader = Reader::from_reader(xml);
107        let mut buffer = Vec::new();
108        loop {
109            match reader
110                .read_event_into(&mut buffer)
111                .map_err(OxmlError::from)?
112            {
113                Event::Start(element) if is_list_style_root(element.name().as_ref()) => {
114                    validate_list_style_root(&element)?;
115                    return Self::from_element(&mut reader, &element, inherited);
116                }
117                Event::Empty(element) if is_list_style_root(element.name().as_ref()) => {
118                    validate_list_style_root(&element)?;
119                    let mut style = Self::from_start(&element)?;
120                    style.source_namespaces = text_fragment_scope(&element, inherited)?.physical;
121                    style.source_root = text_root_spellings_scoped(
122                        &element,
123                        inherited,
124                        &capture_empty_element(&element)?,
125                    )?;
126                    return Ok(style);
127                }
128                Event::Start(element) | Event::Empty(element) => {
129                    return Err(unexpected(&element));
130                }
131                Event::Eof => {
132                    return Err(TextError::Xml(OxmlError::MissingElement(
133                        "DrawingML list style".to_owned(),
134                    )));
135                }
136                _ => {}
137            }
138            buffer.clear();
139        }
140    }
141
142    fn from_start(start: &BytesStart<'_>) -> Result<Box<Self>> {
143        Ok(Box::new(Self {
144            raw_attributes: capture_raw_attributes(start)?,
145            source_root: text_root_spellings(start)?,
146            source_tag: std::str::from_utf8(start.name().as_ref())
147                .map_err(OxmlError::from)?
148                .to_owned(),
149            ..Self::default()
150        }))
151    }
152
153    fn from_element(
154        reader: &mut Reader<&[u8]>,
155        start: &BytesStart<'_>,
156        inherited: &TextNamespaceScope,
157    ) -> Result<Box<Self>> {
158        let mut namespaces = text_fragment_scope(start, inherited)?;
159        namespaces.fixed_list_style_children = true;
160        let mut style = Self::from_start(start)?;
161        style.source_namespaces = namespaces.physical.clone();
162        style.source_root = text_root_spellings_scoped(
163            start,
164            inherited,
165            &capture_element(&mut reader.clone(), start)?,
166        )?;
167        let root_name = local_name(start.name().as_ref()).to_vec();
168        let mut boundary = 0;
169        let mut buffer = Vec::new();
170        loop {
171            match reader
172                .read_event_into(&mut buffer)
173                .map_err(OxmlError::from)?
174            {
175                Event::Start(element) => {
176                    let name = text_owned_name(&element, &namespaces)?;
177                    if name == b"defPPr" || list_level(&name)?.is_some() {
178                        validate_list_style_root(&element)?;
179                    }
180                    let raw = capture_element(reader, &element)?;
181                    style.capture_child(&name, raw, &mut boundary, &namespaces)?;
182                }
183                Event::Empty(element) => {
184                    let name = text_owned_name(&element, &namespaces)?;
185                    if name == b"defPPr" || list_level(&name)?.is_some() {
186                        validate_list_style_root(&element)?;
187                    }
188                    let raw = capture_empty_element(&element)?;
189                    style.capture_child(&name, raw, &mut boundary, &namespaces)?;
190                }
191                Event::End(element)
192                    if local_name(element.name().as_ref()) == root_name.as_slice() =>
193                {
194                    return Ok(style);
195                }
196                Event::Eof => {
197                    return Err(missing_end(
198                        std::str::from_utf8(&root_name).unwrap_or("list style"),
199                    ));
200                }
201                event @ (Event::Comment(_) | Event::PI(_) | Event::Text(_) | Event::CData(_)) => {
202                    style
203                        .raw_children
204                        .push(boundary, text_capture_opaque_event(event)?);
205                }
206                _ => {}
207            }
208            buffer.clear();
209        }
210    }
211
212    fn capture_child(
213        &mut self,
214        name: &[u8],
215        raw: Vec<u8>,
216        boundary: &mut usize,
217        namespaces: &TextNamespaceScope,
218    ) -> Result<()> {
219        if name == b"defPPr" {
220            if self.default_paragraph_properties.is_some() {
221                return Err(TextError::DuplicateElement("defPPr".to_owned()));
222            }
223            self.default_paragraph_properties = Some(
224                CT_TextParagraphProperties::from_xml_with_namespaces(&raw, namespaces)?,
225            );
226            *boundary = (*boundary).max(1);
227            return Ok(());
228        }
229        if let Some(level) = list_level(name)? {
230            if self.level(level).is_some() {
231                return Err(TextError::DuplicateElement(
232                    String::from_utf8_lossy(name).into_owned(),
233                ));
234            }
235            self.set_level(
236                level,
237                CT_TextParagraphProperties::from_xml_with_namespaces(&raw, namespaces)?,
238            );
239            *boundary = (*boundary).max(level + 1);
240            return Ok(());
241        }
242
243        let schema_boundary = match name {
244            b"extLst" => 10,
245            _ => *boundary,
246        };
247        self.raw_children
248            .push((*boundary).max(schema_boundary), raw);
249        *boundary = (*boundary).max(schema_boundary);
250        Ok(())
251    }
252
253    /// Returns local list defaults over the inherited default and each level.
254    pub fn with_inherited_defaults(&self, defaults: &Self) -> Result<Self> {
255        let mut local = Writer::new(Vec::new());
256        self.write_xml(&mut local)?;
257        let mut inherited = Writer::new(Vec::new());
258        defaults.write_xml(&mut inherited)?;
259        let xml = super::paragraph::text_inherit_property_xml(
260            &local.into_inner(),
261            &self.source_namespaces,
262            &inherited.into_inner(),
263            &defaults.source_namespaces,
264        )?;
265        let mut result = Self::from_xml_with_inherited_namespaces(&xml, &self.source_namespaces)?;
266        for level in 1..=9 {
267            if defaults.default_paragraph_properties.is_none()
268                && defaults.level(level).is_none()
269                && self.default_paragraph_properties.is_none()
270                && self.level(level).is_none()
271            {
272                continue;
273            }
274            let mut properties = defaults
275                .default_paragraph_properties
276                .clone()
277                .unwrap_or_default();
278            for overlay in [
279                defaults.level(level),
280                self.default_paragraph_properties.as_ref(),
281                self.level(level),
282            ]
283            .into_iter()
284            .flatten()
285            {
286                properties = overlay.with_inherited_defaults(&properties)?;
287            }
288            result.set_level(level, properties);
289        }
290        Ok(result)
291    }
292
293    /// Writes the list style with fixed prefixes and ascending level order.
294    pub fn write_xml<W: Write>(&self, writer: &mut Writer<W>) -> Result<()> {
295        self.write_xml_as(writer, "a:lstStyle")
296    }
297
298    /// Writes the list style using a caller-selected OOXML wrapper tag.
299    pub fn write_xml_as<W: Write>(&self, writer: &mut Writer<W>, tag: &str) -> Result<()> {
300        let mut start =
301            super::paragraph::text_wrapper_start(tag, &self.source_tag, &self.source_root)?;
302        let owned_tag = std::str::from_utf8(start.name().as_ref())
303            .map_err(OxmlError::from)?
304            .to_owned();
305        let tag = owned_tag.as_str();
306        if self.source_root.is_empty() {
307            push_raw_attributes(&mut start, &self.raw_attributes);
308        }
309        let has_modelled = self.default_paragraph_properties.is_some()
310            || (1..=9).any(|level| self.level(level).is_some());
311        if !has_modelled && self.raw_children.is_empty() {
312            return write_empty(writer, start);
313        }
314
315        write_start(writer, start)?;
316        emit_raw(writer, self.raw_children.at(0))?;
317        if let Some(properties) = &self.default_paragraph_properties {
318            {
319                let mut child = Writer::new(Vec::new());
320                properties.write_xml(&mut child, "a:defPPr")?;
321                writer
322                    .get_mut()
323                    .write_all(&text_close_generated_child(
324                        &child.into_inner(),
325                        &super::paragraph::text_namespace_scope(
326                            &text_source_start(tag, &self.source_root),
327                            &[],
328                        )?,
329                    )?)
330                    .map_err(OxmlError::from)?;
331            }
332        }
333        for level in 1..=9 {
334            emit_raw(writer, self.raw_children.at(level))?;
335            if let Some(properties) = self.level(level) {
336                let tag = level_tag(level)
337                    .ok_or_else(|| TextError::UnexpectedElement(format!("list level {level}")))?;
338                {
339                    let mut child = Writer::new(Vec::new());
340                    properties.write_xml(&mut child, tag)?;
341                    writer
342                        .get_mut()
343                        .write_all(&text_close_generated_child(
344                            &child.into_inner(),
345                            &super::paragraph::text_namespace_scope(
346                                &text_source_start(tag, &self.source_root),
347                                &[],
348                            )?,
349                        )?)
350                        .map_err(OxmlError::from)?;
351                }
352            }
353        }
354        emit_raw(writer, self.raw_children.at(10))?;
355        write_end(writer, tag)
356    }
357
358    /// Serialises a complete list-style fragment.
359    pub fn to_xml(&self) -> Result<Vec<u8>> {
360        let mut writer = Writer::new(Vec::new());
361        self.write_xml(&mut writer)?;
362        Ok(writer.into_inner())
363    }
364
365    /// Returns one level by its one-based DrawingML number.
366    pub fn level(&self, level: usize) -> Option<&CT_TextParagraphProperties> {
367        match level {
368            1 => self.level1.as_ref(),
369            2 => self.level2.as_ref(),
370            3 => self.level3.as_ref(),
371            4 => self.level4.as_ref(),
372            5 => self.level5.as_ref(),
373            6 => self.level6.as_ref(),
374            7 => self.level7.as_ref(),
375            8 => self.level8.as_ref(),
376            9 => self.level9.as_ref(),
377            _ => None,
378        }
379    }
380
381    pub fn raw_children(&self) -> &OrderedRawChildren {
382        &self.raw_children
383    }
384
385    fn set_level(&mut self, level: usize, properties: CT_TextParagraphProperties) {
386        let slot = match level {
387            1 => &mut self.level1,
388            2 => &mut self.level2,
389            3 => &mut self.level3,
390            4 => &mut self.level4,
391            5 => &mut self.level5,
392            6 => &mut self.level6,
393            7 => &mut self.level7,
394            8 => &mut self.level8,
395            9 => &mut self.level9,
396            _ => return,
397        };
398        *slot = Some(properties);
399    }
400}
401
402fn validate_list_style_root(start: &BytesStart<'_>) -> Result<()> {
403    let namespaces = super::paragraph::text_namespace_scope(start, &[])?;
404    let name = start.name();
405    let raw = std::str::from_utf8(name.as_ref()).map_err(OxmlError::from)?;
406    let (prefix, local) = raw.split_once(':').unwrap_or(("", raw));
407    if local != "lstStyle"
408        && namespaces.iter().any(|(key, uri)| {
409            key == prefix && uri == "http://schemas.openxmlformats.org/presentationml/2006/main"
410        })
411    {
412        return Ok(());
413    }
414    text_validate_root(start)
415}
416
417fn is_list_style_root(name: &[u8]) -> bool {
418    matches_local_name(name, b"lstStyle")
419        || matches_local_name(name, b"defaultTextStyle")
420        || matches_local_name(name, b"titleStyle")
421        || matches_local_name(name, b"bodyStyle")
422        || matches_local_name(name, b"otherStyle")
423        || matches_local_name(name, b"notesStyle")
424}
425
426fn list_level(name: &[u8]) -> Result<Option<usize>> {
427    let Some(number) = name
428        .strip_prefix(b"lvl")
429        .and_then(|name| name.strip_suffix(b"pPr"))
430    else {
431        return Ok(None);
432    };
433    if !number.is_empty() && number.iter().all(u8::is_ascii_digit) {
434        if number.len() == 1 && (b'1'..=b'9').contains(&number[0]) {
435            return Ok(Some(usize::from(number[0] - b'0')));
436        }
437        return Err(TextError::UnexpectedElement(
438            String::from_utf8_lossy(name).into_owned(),
439        ));
440    }
441    Ok(None)
442}
443
444fn level_tag(level: usize) -> Option<&'static str> {
445    match level {
446        1 => Some("a:lvl1pPr"),
447        2 => Some("a:lvl2pPr"),
448        3 => Some("a:lvl3pPr"),
449        4 => Some("a:lvl4pPr"),
450        5 => Some("a:lvl5pPr"),
451        6 => Some("a:lvl6pPr"),
452        7 => Some("a:lvl7pPr"),
453        8 => Some("a:lvl8pPr"),
454        9 => Some("a:lvl9pPr"),
455        _ => None,
456    }
457}
458
459fn capture_raw_attributes(start: &BytesStart<'_>) -> Result<Vec<(String, String)>> {
460    let mut raw = Vec::new();
461    for attribute in start.attributes() {
462        let attribute = attribute.map_err(OxmlError::from)?;
463        let name = std::str::from_utf8(attribute.key.as_ref())
464            .map_err(OxmlError::from)?
465            .to_owned();
466        let value = attribute
467            .decoded_and_normalized_value(XmlVersion::Implicit1_0, start.decoder())
468            .map_err(OxmlError::from)?
469            .into_owned();
470        raw.push((name, value));
471    }
472    Ok(raw)
473}
474
475fn push_raw_attributes(start: &mut BytesStart<'_>, attributes: &[(String, String)]) {
476    for (name, value) in attributes {
477        start.push_attribute((name.as_str(), value.as_str()));
478    }
479}
480
481fn emit_raw<'a, W: Write>(
482    writer: &mut Writer<W>,
483    children: impl Iterator<Item = &'a [u8]>,
484) -> Result<()> {
485    for child in children {
486        writer.get_mut().write_all(child).map_err(OxmlError::from)?;
487    }
488    Ok(())
489}
490
491fn write_start<W: Write>(writer: &mut Writer<W>, start: BytesStart<'_>) -> Result<()> {
492    writer
493        .write_event(Event::Start(start))
494        .map_err(OxmlError::from)?;
495    Ok(())
496}
497
498fn write_empty<W: Write>(writer: &mut Writer<W>, start: BytesStart<'_>) -> Result<()> {
499    writer
500        .write_event(Event::Empty(start))
501        .map_err(OxmlError::from)?;
502    Ok(())
503}
504
505fn write_end<W: Write>(writer: &mut Writer<W>, tag: &str) -> Result<()> {
506    writer
507        .write_event(Event::End(BytesEnd::new(tag)))
508        .map_err(OxmlError::from)?;
509    Ok(())
510}
511
512fn unexpected(element: &BytesStart<'_>) -> TextError {
513    TextError::UnexpectedElement(String::from_utf8_lossy(element.name().as_ref()).into_owned())
514}