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 crate::namespace::reject_conflicting_a_prefix;
10use crate::order::OrderedRawChildren;
11
12use super::body::{Result, TextError, missing_end};
13use super::paragraph::CT_TextParagraphProperties;
14
15/// DrawingML list defaults for each of the nine paragraph levels.
16#[allow(non_camel_case_types)]
17#[derive(Clone, Debug, Default, Eq, PartialEq)]
18pub struct CT_TextListStyle {
19    pub default_paragraph_properties: Option<CT_TextParagraphProperties>,
20    pub level1: Option<CT_TextParagraphProperties>,
21    pub level2: Option<CT_TextParagraphProperties>,
22    pub level3: Option<CT_TextParagraphProperties>,
23    pub level4: Option<CT_TextParagraphProperties>,
24    pub level5: Option<CT_TextParagraphProperties>,
25    pub level6: Option<CT_TextParagraphProperties>,
26    pub level7: Option<CT_TextParagraphProperties>,
27    pub level8: Option<CT_TextParagraphProperties>,
28    pub level9: Option<CT_TextParagraphProperties>,
29    raw_attributes: Vec<(String, String)>,
30    raw_children: OrderedRawChildren,
31}
32
33impl CT_TextListStyle {
34    /// Parses a complete list-style element with any prefix, including the
35    /// PresentationML wrappers that share this DrawingML content model.
36    pub fn from_xml(xml: &[u8]) -> Result<Self> {
37        let mut reader = Reader::from_reader(xml);
38        let mut buffer = Vec::new();
39        loop {
40            match reader
41                .read_event_into(&mut buffer)
42                .map_err(OxmlError::from)?
43            {
44                Event::Start(element) if is_list_style_root(element.name().as_ref()) => {
45                    reject_conflicting_a_prefix(&element)?;
46                    return Self::from_element(&mut reader, &element);
47                }
48                Event::Empty(element) if is_list_style_root(element.name().as_ref()) => {
49                    reject_conflicting_a_prefix(&element)?;
50                    return Self::from_start(&element);
51                }
52                Event::Start(element) | Event::Empty(element) => {
53                    return Err(unexpected(&element));
54                }
55                Event::Eof => {
56                    return Err(TextError::Xml(OxmlError::MissingElement(
57                        "DrawingML list style".to_owned(),
58                    )));
59                }
60                _ => {}
61            }
62            buffer.clear();
63        }
64    }
65
66    fn from_start(start: &BytesStart<'_>) -> Result<Self> {
67        Ok(Self {
68            raw_attributes: capture_raw_attributes(start)?,
69            ..Self::default()
70        })
71    }
72
73    fn from_element(reader: &mut Reader<&[u8]>, start: &BytesStart<'_>) -> Result<Self> {
74        let mut style = Self::from_start(start)?;
75        let root_name = local_name(start.name().as_ref()).to_vec();
76        let mut boundary = 0;
77        let mut buffer = Vec::new();
78        loop {
79            match reader
80                .read_event_into(&mut buffer)
81                .map_err(OxmlError::from)?
82            {
83                Event::Start(element) => {
84                    let name = local_name(element.name().as_ref()).to_vec();
85                    if name == b"defPPr" || list_level(&name)?.is_some() {
86                        reject_conflicting_a_prefix(&element)?;
87                    }
88                    let raw = capture_element(reader, &element)?;
89                    style.capture_child(&name, raw, &mut boundary)?;
90                }
91                Event::Empty(element) => {
92                    let name = local_name(element.name().as_ref()).to_vec();
93                    if name == b"defPPr" || list_level(&name)?.is_some() {
94                        reject_conflicting_a_prefix(&element)?;
95                    }
96                    let raw = capture_empty_element(&element)?;
97                    style.capture_child(&name, raw, &mut boundary)?;
98                }
99                Event::End(element)
100                    if local_name(element.name().as_ref()) == root_name.as_slice() =>
101                {
102                    return Ok(style);
103                }
104                Event::Eof => {
105                    return Err(missing_end(
106                        std::str::from_utf8(&root_name).unwrap_or("list style"),
107                    ));
108                }
109                _ => {}
110            }
111            buffer.clear();
112        }
113    }
114
115    fn capture_child(&mut self, name: &[u8], raw: Vec<u8>, boundary: &mut usize) -> Result<()> {
116        if name == b"defPPr" {
117            if self.default_paragraph_properties.is_some() {
118                return Err(TextError::DuplicateElement("defPPr".to_owned()));
119            }
120            self.default_paragraph_properties = Some(CT_TextParagraphProperties::from_xml(&raw)?);
121            *boundary = (*boundary).max(1);
122            return Ok(());
123        }
124        if let Some(level) = list_level(name)? {
125            if self.level(level).is_some() {
126                return Err(TextError::DuplicateElement(
127                    String::from_utf8_lossy(name).into_owned(),
128                ));
129            }
130            self.set_level(level, CT_TextParagraphProperties::from_xml(&raw)?);
131            *boundary = (*boundary).max(level + 1);
132            return Ok(());
133        }
134
135        let schema_boundary = match name {
136            b"extLst" => 10,
137            _ => *boundary,
138        };
139        self.raw_children
140            .push((*boundary).max(schema_boundary), raw);
141        *boundary = (*boundary).max(schema_boundary);
142        Ok(())
143    }
144
145    /// Writes the list style with fixed prefixes and ascending level order.
146    pub fn write_xml<W: Write>(&self, writer: &mut Writer<W>) -> Result<()> {
147        self.write_xml_as(writer, "a:lstStyle")
148    }
149
150    /// Writes the list style using a caller-selected OOXML wrapper tag.
151    pub fn write_xml_as<W: Write>(&self, writer: &mut Writer<W>, tag: &str) -> Result<()> {
152        let mut start = BytesStart::new(tag);
153        push_raw_attributes(&mut start, &self.raw_attributes);
154        let has_modelled = self.default_paragraph_properties.is_some()
155            || (1..=9).any(|level| self.level(level).is_some());
156        if !has_modelled && self.raw_children.is_empty() {
157            return write_empty(writer, start);
158        }
159
160        write_start(writer, start)?;
161        emit_raw(writer, self.raw_children.at(0))?;
162        if let Some(properties) = &self.default_paragraph_properties {
163            properties.write_xml(writer, "a:defPPr")?;
164        }
165        for level in 1..=9 {
166            emit_raw(writer, self.raw_children.at(level))?;
167            if let Some(properties) = self.level(level) {
168                let tag = level_tag(level)
169                    .ok_or_else(|| TextError::UnexpectedElement(format!("list level {level}")))?;
170                properties.write_xml(writer, tag)?;
171            }
172        }
173        emit_raw(writer, self.raw_children.at(10))?;
174        write_end(writer, tag)
175    }
176
177    /// Serialises a complete list-style fragment.
178    pub fn to_xml(&self) -> Result<Vec<u8>> {
179        let mut writer = Writer::new(Vec::new());
180        self.write_xml(&mut writer)?;
181        Ok(writer.into_inner())
182    }
183
184    /// Returns one level by its one-based DrawingML number.
185    pub fn level(&self, level: usize) -> Option<&CT_TextParagraphProperties> {
186        match level {
187            1 => self.level1.as_ref(),
188            2 => self.level2.as_ref(),
189            3 => self.level3.as_ref(),
190            4 => self.level4.as_ref(),
191            5 => self.level5.as_ref(),
192            6 => self.level6.as_ref(),
193            7 => self.level7.as_ref(),
194            8 => self.level8.as_ref(),
195            9 => self.level9.as_ref(),
196            _ => None,
197        }
198    }
199
200    pub fn raw_children(&self) -> &OrderedRawChildren {
201        &self.raw_children
202    }
203
204    fn set_level(&mut self, level: usize, properties: CT_TextParagraphProperties) {
205        let slot = match level {
206            1 => &mut self.level1,
207            2 => &mut self.level2,
208            3 => &mut self.level3,
209            4 => &mut self.level4,
210            5 => &mut self.level5,
211            6 => &mut self.level6,
212            7 => &mut self.level7,
213            8 => &mut self.level8,
214            9 => &mut self.level9,
215            _ => return,
216        };
217        *slot = Some(properties);
218    }
219}
220
221fn is_list_style_root(name: &[u8]) -> bool {
222    matches_local_name(name, b"lstStyle")
223        || matches_local_name(name, b"defaultTextStyle")
224        || matches_local_name(name, b"titleStyle")
225        || matches_local_name(name, b"bodyStyle")
226        || matches_local_name(name, b"otherStyle")
227        || matches_local_name(name, b"notesStyle")
228}
229
230fn list_level(name: &[u8]) -> Result<Option<usize>> {
231    let Some(number) = name
232        .strip_prefix(b"lvl")
233        .and_then(|name| name.strip_suffix(b"pPr"))
234    else {
235        return Ok(None);
236    };
237    if !number.is_empty() && number.iter().all(u8::is_ascii_digit) {
238        if number.len() == 1 && (b'1'..=b'9').contains(&number[0]) {
239            return Ok(Some(usize::from(number[0] - b'0')));
240        }
241        return Err(TextError::UnexpectedElement(
242            String::from_utf8_lossy(name).into_owned(),
243        ));
244    }
245    Ok(None)
246}
247
248fn level_tag(level: usize) -> Option<&'static str> {
249    match level {
250        1 => Some("a:lvl1pPr"),
251        2 => Some("a:lvl2pPr"),
252        3 => Some("a:lvl3pPr"),
253        4 => Some("a:lvl4pPr"),
254        5 => Some("a:lvl5pPr"),
255        6 => Some("a:lvl6pPr"),
256        7 => Some("a:lvl7pPr"),
257        8 => Some("a:lvl8pPr"),
258        9 => Some("a:lvl9pPr"),
259        _ => None,
260    }
261}
262
263fn capture_raw_attributes(start: &BytesStart<'_>) -> Result<Vec<(String, String)>> {
264    let mut raw = Vec::new();
265    for attribute in start.attributes() {
266        let attribute = attribute.map_err(OxmlError::from)?;
267        let name = std::str::from_utf8(attribute.key.as_ref())
268            .map_err(OxmlError::from)?
269            .to_owned();
270        let value = attribute
271            .decoded_and_normalized_value(XmlVersion::Implicit1_0, start.decoder())
272            .map_err(OxmlError::from)?
273            .into_owned();
274        raw.push((name, value));
275    }
276    Ok(raw)
277}
278
279fn push_raw_attributes(start: &mut BytesStart<'_>, attributes: &[(String, String)]) {
280    for (name, value) in attributes {
281        start.push_attribute((name.as_str(), value.as_str()));
282    }
283}
284
285fn emit_raw<'a, W: Write>(
286    writer: &mut Writer<W>,
287    children: impl Iterator<Item = &'a [u8]>,
288) -> Result<()> {
289    for child in children {
290        writer.get_mut().write_all(child).map_err(OxmlError::from)?;
291    }
292    Ok(())
293}
294
295fn write_start<W: Write>(writer: &mut Writer<W>, start: BytesStart<'_>) -> Result<()> {
296    writer
297        .write_event(Event::Start(start))
298        .map_err(OxmlError::from)?;
299    Ok(())
300}
301
302fn write_empty<W: Write>(writer: &mut Writer<W>, start: BytesStart<'_>) -> Result<()> {
303    writer
304        .write_event(Event::Empty(start))
305        .map_err(OxmlError::from)?;
306    Ok(())
307}
308
309fn write_end<W: Write>(writer: &mut Writer<W>, tag: &str) -> Result<()> {
310    writer
311        .write_event(Event::End(BytesEnd::new(tag)))
312        .map_err(OxmlError::from)?;
313    Ok(())
314}
315
316fn unexpected(element: &BytesStart<'_>) -> TextError {
317    TextError::UnexpectedElement(String::from_utf8_lossy(element.name().as_ref()).into_owned())
318}