Skip to main content

oxml_drawing/text/
bullet.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::color::ColorChoice;
10use crate::namespace::reject_conflicting_a_prefix;
11use crate::order::OrderedRawChildren;
12
13use super::body::{Result, TextError, missing_end};
14use super::paragraph::TextFont;
15
16const MIN_BULLET_PERCENT: i32 = 25_000;
17const MAX_BULLET_PERCENT: i32 = 400_000;
18const MIN_BULLET_POINTS: i32 = 100;
19const MAX_BULLET_POINTS: i32 = 400_000;
20const MAX_BULLET_START_AT: u16 = 32_767;
21
22/// The modelled colour, size, font, and mutually exclusive choice of one bullet.
23#[derive(Clone, Debug, Default, Eq, PartialEq)]
24pub struct TextBullet {
25    pub color: Option<TextBulletColor>,
26    pub size: Option<TextBulletSize>,
27    pub font: Option<TextFont>,
28    pub choice: Option<TextBulletChoice>,
29}
30
31impl TextBullet {
32    pub(crate) fn capture_component(&mut self, name: &[u8], xml: &[u8]) -> Result<bool> {
33        match name {
34            b"buClr" => {
35                if self.color.is_some() {
36                    return Err(duplicate("bullet colour choice"));
37                }
38                self.color = Some(TextBulletColor::from_xml(xml)?);
39            }
40            b"buSzPct" | b"buSzPts" => {
41                if self.size.is_some() {
42                    return Err(duplicate("bullet size choice"));
43                }
44                self.size = Some(TextBulletSize::from_xml(xml)?);
45            }
46            b"buFont" => {
47                if self.font.is_some() {
48                    return Err(duplicate("buFont"));
49                }
50                self.font = Some(TextFont::from_xml(xml, b"buFont")?);
51            }
52            b"buNone" | b"buAutoNum" | b"buChar" => {
53                if self.choice.is_some() {
54                    return Err(duplicate("bullet choice"));
55                }
56                self.choice = Some(TextBulletChoice::from_xml(xml, name)?);
57            }
58            _ => return Ok(false),
59        }
60        Ok(true)
61    }
62
63    pub(crate) fn write_color<W: Write>(&self, writer: &mut Writer<W>) -> Result<()> {
64        if let Some(color) = &self.color {
65            color.write_xml(writer)?;
66        }
67        Ok(())
68    }
69
70    pub(crate) fn write_size<W: Write>(&self, writer: &mut Writer<W>) -> Result<()> {
71        if let Some(size) = &self.size {
72            size.write_xml(writer)?;
73        }
74        Ok(())
75    }
76
77    pub(crate) fn write_font<W: Write>(&self, writer: &mut Writer<W>) -> Result<()> {
78        if let Some(font) = &self.font {
79            font.write_xml(writer, "a:buFont")?;
80        }
81        Ok(())
82    }
83
84    pub(crate) fn write_choice<W: Write>(&self, writer: &mut Writer<W>) -> Result<()> {
85        if let Some(choice) = &self.choice {
86            choice.write_xml(writer)?;
87        }
88        Ok(())
89    }
90
91    pub(crate) fn is_empty(&self) -> bool {
92        self.color.is_none() && self.size.is_none() && self.font.is_none() && self.choice.is_none()
93    }
94}
95
96/// An `a:buClr` wrapper containing one DrawingML colour choice.
97#[derive(Clone, Debug, Eq, PartialEq)]
98pub struct TextBulletColor {
99    pub color: ColorChoice,
100    raw_attributes: Vec<(String, String)>,
101    raw_children: OrderedRawChildren,
102}
103
104impl TextBulletColor {
105    pub fn new(color: ColorChoice) -> Self {
106        Self {
107            color,
108            raw_attributes: Vec::new(),
109            raw_children: OrderedRawChildren::default(),
110        }
111    }
112
113    fn from_xml(xml: &[u8]) -> Result<Self> {
114        parse_complete(xml, b"buClr", Self::from_element, |_| {
115            Err(TextError::UnexpectedElement("buClr".to_owned()))
116        })
117    }
118
119    fn from_element(reader: &mut Reader<&[u8]>, start: &BytesStart<'_>) -> Result<Self> {
120        let raw_attributes = capture_raw_attributes(start, &[])?;
121        let mut color = None;
122        let mut raw_children = OrderedRawChildren::default();
123        let mut boundary = 0;
124        let mut buffer = Vec::new();
125        loop {
126            match reader
127                .read_event_into(&mut buffer)
128                .map_err(OxmlError::from)?
129            {
130                Event::Start(element) if is_color(element.name().as_ref()) => {
131                    if color.is_some() {
132                        return Err(duplicate("bullet colour"));
133                    }
134                    validate_color_attributes(&element)?;
135                    reject_conflicting_a_prefix(&element)?;
136                    color = Some(ColorChoice::from_xml(reader, &element)?);
137                    boundary = 1;
138                }
139                Event::Empty(element) if is_color(element.name().as_ref()) => {
140                    if color.is_some() {
141                        return Err(duplicate("bullet colour"));
142                    }
143                    validate_color_attributes(&element)?;
144                    reject_conflicting_a_prefix(&element)?;
145                    color = Some(ColorChoice::from_empty_xml(&element)?);
146                    boundary = 1;
147                }
148                Event::Start(element) => {
149                    raw_children.push(boundary, capture_element(reader, &element)?)
150                }
151                Event::Empty(element) => {
152                    raw_children.push(boundary, capture_empty_element(&element)?)
153                }
154                Event::End(element) if matches_local_name(element.name().as_ref(), b"buClr") => {
155                    return Ok(Self {
156                        color: color.ok_or_else(|| missing("buClr", "colour choice"))?,
157                        raw_attributes,
158                        raw_children,
159                    });
160                }
161                Event::Eof => return Err(missing_end("buClr")),
162                _ => {}
163            }
164            buffer.clear();
165        }
166    }
167
168    fn write_xml<W: Write>(&self, writer: &mut Writer<W>) -> Result<()> {
169        let mut start = BytesStart::new("a:buClr");
170        push_raw_attributes(&mut start, &self.raw_attributes);
171        write_start(writer, start)?;
172        emit_raw(writer, self.raw_children.at(0))?;
173        self.color.to_xml(writer)?;
174        emit_raw(writer, self.raw_children.at(1))?;
175        write_end(writer, "a:buClr")
176    }
177
178    pub fn raw_children(&self) -> &OrderedRawChildren {
179        &self.raw_children
180    }
181}
182
183/// Percentage or centipoint size for a DrawingML bullet.
184#[derive(Clone, Debug, Eq, PartialEq)]
185pub struct TextBulletSize {
186    pub value: TextBulletSizeValue,
187    raw_attributes: Vec<(String, String)>,
188}
189
190impl TextBulletSize {
191    pub fn percent(value: impl Into<String>) -> Result<Self> {
192        let value = value.into();
193        validate_bullet_percent(&value)?;
194        Ok(Self {
195            value: TextBulletSizeValue::Percent(value),
196            raw_attributes: Vec::new(),
197        })
198    }
199
200    pub fn points(value: i32) -> Result<Self> {
201        validate_range(
202            "buSzPts",
203            "val",
204            value,
205            MIN_BULLET_POINTS,
206            MAX_BULLET_POINTS,
207        )?;
208        Ok(Self {
209            value: TextBulletSizeValue::Points(value),
210            raw_attributes: Vec::new(),
211        })
212    }
213
214    fn from_xml(xml: &[u8]) -> Result<Self> {
215        let expected = root_local_name(xml)?;
216        if !matches!(expected.as_slice(), b"buSzPct" | b"buSzPts") {
217            return Err(TextError::UnexpectedElement(
218                String::from_utf8_lossy(&expected).into_owned(),
219            ));
220        }
221        parse_complete(
222            xml,
223            &expected,
224            |reader, start| {
225                let size = Self::from_start(start)?;
226                ensure_empty(reader, &expected)?;
227                Ok(size)
228            },
229            Self::from_start,
230        )
231    }
232
233    fn from_start(start: &BytesStart<'_>) -> Result<Self> {
234        let qualified_name = start.name();
235        let name = local_name(qualified_name.as_ref());
236        let value = required_attr(start, b"val")?;
237        let value = match name {
238            b"buSzPct" => {
239                validate_bullet_percent(&value)?;
240                TextBulletSizeValue::Percent(value)
241            }
242            b"buSzPts" => TextBulletSizeValue::Points(parse_range(
243                "buSzPts",
244                "val",
245                &value,
246                MIN_BULLET_POINTS,
247                MAX_BULLET_POINTS,
248            )?),
249            _ => return Err(unexpected(start)),
250        };
251        Ok(Self {
252            value,
253            raw_attributes: capture_raw_attributes(start, &[b"val"])?,
254        })
255    }
256
257    fn write_xml<W: Write>(&self, writer: &mut Writer<W>) -> Result<()> {
258        let (tag, value) = match &self.value {
259            TextBulletSizeValue::Percent(value) => {
260                validate_bullet_percent(value)?;
261                ("a:buSzPct", value.clone())
262            }
263            TextBulletSizeValue::Points(value) => {
264                validate_range(
265                    "buSzPts",
266                    "val",
267                    *value,
268                    MIN_BULLET_POINTS,
269                    MAX_BULLET_POINTS,
270                )?;
271                ("a:buSzPts", value.to_string())
272            }
273        };
274        let mut start = BytesStart::new(tag);
275        start.push_attribute(("val", value.as_str()));
276        push_raw_attributes(&mut start, &self.raw_attributes);
277        write_empty(writer, start)
278    }
279}
280
281/// The two explicit DrawingML bullet-size forms.
282#[derive(Clone, Debug, Eq, PartialEq)]
283pub enum TextBulletSizeValue {
284    Percent(String),
285    Points(i32),
286}
287
288/// One member of the DrawingML paragraph bullet choice.
289#[derive(Clone, Debug, Eq, PartialEq)]
290pub enum TextBulletChoice {
291    Character(TextBulletCharacter),
292    AutoNumber(TextAutoNumber),
293    None(TextNoBullet),
294}
295
296impl TextBulletChoice {
297    fn from_xml(xml: &[u8], name: &[u8]) -> Result<Self> {
298        match name {
299            b"buChar" => Ok(Self::Character(TextBulletCharacter::from_xml(xml)?)),
300            b"buAutoNum" => Ok(Self::AutoNumber(TextAutoNumber::from_xml(xml)?)),
301            b"buNone" => Ok(Self::None(TextNoBullet::from_xml(xml)?)),
302            _ => Err(TextError::UnexpectedElement(
303                String::from_utf8_lossy(name).into_owned(),
304            )),
305        }
306    }
307
308    fn write_xml<W: Write>(&self, writer: &mut Writer<W>) -> Result<()> {
309        match self {
310            Self::Character(value) => value.write_xml(writer),
311            Self::AutoNumber(value) => value.write_xml(writer),
312            Self::None(value) => value.write_xml(writer),
313        }
314    }
315}
316
317/// A literal DrawingML bullet character kept in its wire representation.
318#[derive(Clone, Debug, Eq, PartialEq)]
319pub struct TextBulletCharacter {
320    pub character: String,
321    raw_attributes: Vec<(String, String)>,
322}
323
324impl TextBulletCharacter {
325    pub fn new(character: impl Into<String>) -> Result<Self> {
326        let character = character.into();
327        if character.is_empty() {
328            return Err(invalid("buChar", "char", ""));
329        }
330        Ok(Self {
331            character,
332            raw_attributes: Vec::new(),
333        })
334    }
335
336    fn from_xml(xml: &[u8]) -> Result<Self> {
337        parse_complete(
338            xml,
339            b"buChar",
340            |reader, start| {
341                let value = Self::from_start(start)?;
342                ensure_empty(reader, b"buChar")?;
343                Ok(value)
344            },
345            Self::from_start,
346        )
347    }
348
349    fn from_start(start: &BytesStart<'_>) -> Result<Self> {
350        Ok(Self {
351            character: required_attr(start, b"char")?,
352            raw_attributes: capture_raw_attributes(start, &[b"char"])?,
353        })
354    }
355
356    fn write_xml<W: Write>(&self, writer: &mut Writer<W>) -> Result<()> {
357        if self.character.is_empty() {
358            return Err(invalid("buChar", "char", ""));
359        }
360        let mut start = BytesStart::new("a:buChar");
361        start.push_attribute(("char", self.character.as_str()));
362        push_raw_attributes(&mut start, &self.raw_attributes);
363        write_empty(writer, start)
364    }
365}
366
367/// An automatic-number bullet with a schema-defined numbering scheme.
368#[derive(Clone, Debug, Eq, PartialEq)]
369pub struct TextAutoNumber {
370    pub scheme: TextAutoNumberScheme,
371    pub start_at: Option<u16>,
372    raw_attributes: Vec<(String, String)>,
373}
374
375impl TextAutoNumber {
376    pub fn new(scheme: TextAutoNumberScheme) -> Self {
377        Self {
378            scheme,
379            start_at: None,
380            raw_attributes: Vec::new(),
381        }
382    }
383
384    fn from_xml(xml: &[u8]) -> Result<Self> {
385        parse_complete(
386            xml,
387            b"buAutoNum",
388            |reader, start| {
389                let value = Self::from_start(start)?;
390                ensure_empty(reader, b"buAutoNum")?;
391                Ok(value)
392            },
393            Self::from_start,
394        )
395    }
396
397    fn from_start(start: &BytesStart<'_>) -> Result<Self> {
398        let scheme_value = required_attr(start, b"type")?;
399        let scheme = TextAutoNumberScheme::parse(&scheme_value)
400            .ok_or_else(|| invalid("buAutoNum", "type", &scheme_value))?;
401        let start_at = text_attr(start, b"startAt")?
402            .map(|value| {
403                parse_range(
404                    "buAutoNum",
405                    "startAt",
406                    &value,
407                    1,
408                    i32::from(MAX_BULLET_START_AT),
409                )
410                .map(|value| value as u16)
411            })
412            .transpose()?;
413        Ok(Self {
414            scheme,
415            start_at,
416            raw_attributes: capture_raw_attributes(start, &[b"type", b"startAt"])?,
417        })
418    }
419
420    fn write_xml<W: Write>(&self, writer: &mut Writer<W>) -> Result<()> {
421        let mut start = BytesStart::new("a:buAutoNum");
422        start.push_attribute(("type", self.scheme.as_str()));
423        let start_at = if let Some(start_at) = self.start_at {
424            if !(1..=MAX_BULLET_START_AT).contains(&start_at) {
425                return Err(invalid("buAutoNum", "startAt", &start_at.to_string()));
426            }
427            Some(start_at.to_string())
428        } else {
429            None
430        };
431        if let Some(value) = start_at.as_deref() {
432            start.push_attribute(("startAt", value));
433        }
434        push_raw_attributes(&mut start, &self.raw_attributes);
435        write_empty(writer, start)
436    }
437}
438
439/// The 41 values of `ST_TextAutonumberScheme`.
440#[derive(Clone, Copy, Debug, Eq, PartialEq)]
441pub enum TextAutoNumberScheme {
442    AlphaLowerParenBoth,
443    AlphaUpperParenBoth,
444    AlphaLowerParenRight,
445    AlphaUpperParenRight,
446    AlphaLowerPeriod,
447    AlphaUpperPeriod,
448    ArabicParenBoth,
449    ArabicParenRight,
450    ArabicPeriod,
451    ArabicPlain,
452    RomanLowerParenBoth,
453    RomanUpperParenBoth,
454    RomanLowerParenRight,
455    RomanUpperParenRight,
456    RomanLowerPeriod,
457    RomanUpperPeriod,
458    CircleNumberDoubleBytePlain,
459    CircleNumberWingdingsBlackPlain,
460    CircleNumberWingdingsWhitePlain,
461    ArabicDoubleBytePeriod,
462    ArabicDoubleBytePlain,
463    EastAsianSimplifiedChinesePeriod,
464    EastAsianSimplifiedChinesePlain,
465    EastAsianTraditionalChinesePeriod,
466    EastAsianTraditionalChinesePlain,
467    EastAsianJapaneseDoubleBytePeriod,
468    EastAsianJapaneseKoreanPlain,
469    EastAsianJapaneseKoreanPeriod,
470    Arabic1Minus,
471    Arabic2Minus,
472    Hebrew2Minus,
473    ThaiAlphaPeriod,
474    ThaiAlphaParenRight,
475    ThaiAlphaParenBoth,
476    ThaiNumberPeriod,
477    ThaiNumberParenRight,
478    ThaiNumberParenBoth,
479    HindiAlphaPeriod,
480    HindiNumberPeriod,
481    HindiNumberParenRight,
482    HindiAlpha1Period,
483}
484
485impl TextAutoNumberScheme {
486    /// Parses one schema-defined DrawingML numbering token for readers and authoring.
487    pub fn parse(value: &str) -> Option<Self> {
488        Some(match value {
489            "alphaLcParenBoth" => Self::AlphaLowerParenBoth,
490            "alphaUcParenBoth" => Self::AlphaUpperParenBoth,
491            "alphaLcParenR" => Self::AlphaLowerParenRight,
492            "alphaUcParenR" => Self::AlphaUpperParenRight,
493            "alphaLcPeriod" => Self::AlphaLowerPeriod,
494            "alphaUcPeriod" => Self::AlphaUpperPeriod,
495            "arabicParenBoth" => Self::ArabicParenBoth,
496            "arabicParenR" => Self::ArabicParenRight,
497            "arabicPeriod" => Self::ArabicPeriod,
498            "arabicPlain" => Self::ArabicPlain,
499            "romanLcParenBoth" => Self::RomanLowerParenBoth,
500            "romanUcParenBoth" => Self::RomanUpperParenBoth,
501            "romanLcParenR" => Self::RomanLowerParenRight,
502            "romanUcParenR" => Self::RomanUpperParenRight,
503            "romanLcPeriod" => Self::RomanLowerPeriod,
504            "romanUcPeriod" => Self::RomanUpperPeriod,
505            "circleNumDbPlain" => Self::CircleNumberDoubleBytePlain,
506            "circleNumWdBlackPlain" => Self::CircleNumberWingdingsBlackPlain,
507            "circleNumWdWhitePlain" => Self::CircleNumberWingdingsWhitePlain,
508            "arabicDbPeriod" => Self::ArabicDoubleBytePeriod,
509            "arabicDbPlain" => Self::ArabicDoubleBytePlain,
510            "ea1ChsPeriod" => Self::EastAsianSimplifiedChinesePeriod,
511            "ea1ChsPlain" => Self::EastAsianSimplifiedChinesePlain,
512            "ea1ChtPeriod" => Self::EastAsianTraditionalChinesePeriod,
513            "ea1ChtPlain" => Self::EastAsianTraditionalChinesePlain,
514            "ea1JpnChsDbPeriod" => Self::EastAsianJapaneseDoubleBytePeriod,
515            "ea1JpnKorPlain" => Self::EastAsianJapaneseKoreanPlain,
516            "ea1JpnKorPeriod" => Self::EastAsianJapaneseKoreanPeriod,
517            "arabic1Minus" => Self::Arabic1Minus,
518            "arabic2Minus" => Self::Arabic2Minus,
519            "hebrew2Minus" => Self::Hebrew2Minus,
520            "thaiAlphaPeriod" => Self::ThaiAlphaPeriod,
521            "thaiAlphaParenR" => Self::ThaiAlphaParenRight,
522            "thaiAlphaParenBoth" => Self::ThaiAlphaParenBoth,
523            "thaiNumPeriod" => Self::ThaiNumberPeriod,
524            "thaiNumParenR" => Self::ThaiNumberParenRight,
525            "thaiNumParenBoth" => Self::ThaiNumberParenBoth,
526            "hindiAlphaPeriod" => Self::HindiAlphaPeriod,
527            "hindiNumPeriod" => Self::HindiNumberPeriod,
528            "hindiNumParenR" => Self::HindiNumberParenRight,
529            "hindiAlpha1Period" => Self::HindiAlpha1Period,
530            _ => return None,
531        })
532    }
533
534    pub const fn as_str(self) -> &'static str {
535        match self {
536            Self::AlphaLowerParenBoth => "alphaLcParenBoth",
537            Self::AlphaUpperParenBoth => "alphaUcParenBoth",
538            Self::AlphaLowerParenRight => "alphaLcParenR",
539            Self::AlphaUpperParenRight => "alphaUcParenR",
540            Self::AlphaLowerPeriod => "alphaLcPeriod",
541            Self::AlphaUpperPeriod => "alphaUcPeriod",
542            Self::ArabicParenBoth => "arabicParenBoth",
543            Self::ArabicParenRight => "arabicParenR",
544            Self::ArabicPeriod => "arabicPeriod",
545            Self::ArabicPlain => "arabicPlain",
546            Self::RomanLowerParenBoth => "romanLcParenBoth",
547            Self::RomanUpperParenBoth => "romanUcParenBoth",
548            Self::RomanLowerParenRight => "romanLcParenR",
549            Self::RomanUpperParenRight => "romanUcParenR",
550            Self::RomanLowerPeriod => "romanLcPeriod",
551            Self::RomanUpperPeriod => "romanUcPeriod",
552            Self::CircleNumberDoubleBytePlain => "circleNumDbPlain",
553            Self::CircleNumberWingdingsBlackPlain => "circleNumWdBlackPlain",
554            Self::CircleNumberWingdingsWhitePlain => "circleNumWdWhitePlain",
555            Self::ArabicDoubleBytePeriod => "arabicDbPeriod",
556            Self::ArabicDoubleBytePlain => "arabicDbPlain",
557            Self::EastAsianSimplifiedChinesePeriod => "ea1ChsPeriod",
558            Self::EastAsianSimplifiedChinesePlain => "ea1ChsPlain",
559            Self::EastAsianTraditionalChinesePeriod => "ea1ChtPeriod",
560            Self::EastAsianTraditionalChinesePlain => "ea1ChtPlain",
561            Self::EastAsianJapaneseDoubleBytePeriod => "ea1JpnChsDbPeriod",
562            Self::EastAsianJapaneseKoreanPlain => "ea1JpnKorPlain",
563            Self::EastAsianJapaneseKoreanPeriod => "ea1JpnKorPeriod",
564            Self::Arabic1Minus => "arabic1Minus",
565            Self::Arabic2Minus => "arabic2Minus",
566            Self::Hebrew2Minus => "hebrew2Minus",
567            Self::ThaiAlphaPeriod => "thaiAlphaPeriod",
568            Self::ThaiAlphaParenRight => "thaiAlphaParenR",
569            Self::ThaiAlphaParenBoth => "thaiAlphaParenBoth",
570            Self::ThaiNumberPeriod => "thaiNumPeriod",
571            Self::ThaiNumberParenRight => "thaiNumParenR",
572            Self::ThaiNumberParenBoth => "thaiNumParenBoth",
573            Self::HindiAlphaPeriod => "hindiAlphaPeriod",
574            Self::HindiNumberPeriod => "hindiNumPeriod",
575            Self::HindiNumberParenRight => "hindiNumParenR",
576            Self::HindiAlpha1Period => "hindiAlpha1Period",
577        }
578    }
579}
580
581/// An explicit `a:buNone` choice with unmodelled attributes retained.
582#[derive(Clone, Debug, Default, Eq, PartialEq)]
583pub struct TextNoBullet {
584    raw_attributes: Vec<(String, String)>,
585}
586
587impl TextNoBullet {
588    fn from_xml(xml: &[u8]) -> Result<Self> {
589        parse_complete(
590            xml,
591            b"buNone",
592            |reader, start| {
593                let value = Self::from_start(start)?;
594                ensure_empty(reader, b"buNone")?;
595                Ok(value)
596            },
597            Self::from_start,
598        )
599    }
600
601    fn from_start(start: &BytesStart<'_>) -> Result<Self> {
602        Ok(Self {
603            raw_attributes: capture_raw_attributes(start, &[])?,
604        })
605    }
606
607    fn write_xml<W: Write>(&self, writer: &mut Writer<W>) -> Result<()> {
608        let mut start = BytesStart::new("a:buNone");
609        push_raw_attributes(&mut start, &self.raw_attributes);
610        write_empty(writer, start)
611    }
612}
613
614fn validate_bullet_percent(value: &str) -> Result<()> {
615    if let Ok(value) = value.parse::<i32>() {
616        return validate_range(
617            "buSzPct",
618            "val",
619            value,
620            MIN_BULLET_PERCENT,
621            MAX_BULLET_PERCENT,
622        );
623    }
624    let Some(percent) = value.strip_suffix('%') else {
625        return Err(invalid("buSzPct", "val", value));
626    };
627    if !is_decimal(percent) {
628        return Err(invalid("buSzPct", "val", value));
629    }
630    let percent = percent
631        .parse::<f64>()
632        .map_err(|_| invalid("buSzPct", "val", value))?;
633    if (25.0..=400.0).contains(&percent) {
634        Ok(())
635    } else {
636        Err(invalid("buSzPct", "val", value))
637    }
638}
639
640fn is_decimal(value: &str) -> bool {
641    let mut parts = value.split('.');
642    let Some(integer) = parts.next() else {
643        return false;
644    };
645    if integer.is_empty() || !integer.bytes().all(|byte| byte.is_ascii_digit()) {
646        return false;
647    }
648    if let Some(fraction) = parts.next()
649        && (fraction.is_empty() || !fraction.bytes().all(|byte| byte.is_ascii_digit()))
650    {
651        return false;
652    }
653    parts.next().is_none()
654}
655
656fn validate_range(element: &str, attribute: &str, value: i32, min: i32, max: i32) -> Result<()> {
657    if (min..=max).contains(&value) {
658        Ok(())
659    } else {
660        Err(invalid(element, attribute, &value.to_string()))
661    }
662}
663
664fn parse_range(element: &str, attribute: &str, value: &str, min: i32, max: i32) -> Result<i32> {
665    let parsed = value
666        .parse::<i32>()
667        .map_err(|_| invalid(element, attribute, value))?;
668    validate_range(element, attribute, parsed, min, max)?;
669    Ok(parsed)
670}
671
672fn is_color(name: &[u8]) -> bool {
673    matches!(
674        local_name(name),
675        b"srgbClr" | b"schemeClr" | b"sysClr" | b"prstClr"
676    )
677}
678
679fn validate_color_attributes(element: &BytesStart<'_>) -> Result<()> {
680    required_attr(element, b"val")?;
681    for attribute in element.attributes() {
682        let attribute = attribute.map_err(OxmlError::from)?;
683        let name = attribute.key.as_ref();
684        if name != b"val" && name != b"lastClr" && matches!(local_name(name), b"val" | b"lastClr") {
685            return Err(invalid(
686                &String::from_utf8_lossy(local_name(element.name().as_ref())),
687                &String::from_utf8_lossy(name),
688                &String::from_utf8_lossy(attribute.value.as_ref()),
689            ));
690        }
691    }
692    Ok(())
693}
694
695fn parse_complete<T>(
696    xml: &[u8],
697    expected: &[u8],
698    parse_start: impl FnOnce(&mut Reader<&[u8]>, &BytesStart<'_>) -> Result<T>,
699    parse_empty: impl FnOnce(&BytesStart<'_>) -> Result<T>,
700) -> Result<T> {
701    let mut reader = Reader::from_reader(xml);
702    let mut buffer = Vec::new();
703    loop {
704        match reader
705            .read_event_into(&mut buffer)
706            .map_err(OxmlError::from)?
707        {
708            Event::Start(element) if matches_local_name(element.name().as_ref(), expected) => {
709                reject_conflicting_a_prefix(&element)?;
710                return parse_start(&mut reader, &element);
711            }
712            Event::Empty(element) if matches_local_name(element.name().as_ref(), expected) => {
713                reject_conflicting_a_prefix(&element)?;
714                return parse_empty(&element);
715            }
716            Event::Start(element) | Event::Empty(element) => return Err(unexpected(&element)),
717            Event::Eof => {
718                return Err(TextError::Xml(OxmlError::MissingElement(
719                    String::from_utf8_lossy(expected).into_owned(),
720                )));
721            }
722            _ => {}
723        }
724        buffer.clear();
725    }
726}
727
728fn root_local_name(xml: &[u8]) -> Result<Vec<u8>> {
729    let mut reader = Reader::from_reader(xml);
730    let mut buffer = Vec::new();
731    loop {
732        match reader
733            .read_event_into(&mut buffer)
734            .map_err(OxmlError::from)?
735        {
736            Event::Start(element) | Event::Empty(element) => {
737                return Ok(local_name(element.name().as_ref()).to_vec());
738            }
739            Event::Eof => {
740                return Err(TextError::Xml(OxmlError::MissingElement(
741                    "DrawingML bullet".to_owned(),
742                )));
743            }
744            _ => {}
745        }
746        buffer.clear();
747    }
748}
749
750fn ensure_empty(reader: &mut Reader<&[u8]>, expected: &[u8]) -> Result<()> {
751    let mut buffer = Vec::new();
752    loop {
753        match reader
754            .read_event_into(&mut buffer)
755            .map_err(OxmlError::from)?
756        {
757            Event::End(element) if matches_local_name(element.name().as_ref(), expected) => {
758                return Ok(());
759            }
760            Event::Text(text) if text.iter().all(u8::is_ascii_whitespace) => {}
761            Event::Comment(_) => {}
762            Event::Start(element) | Event::Empty(element) => return Err(unexpected(&element)),
763            Event::Eof => return Err(missing_end(&String::from_utf8_lossy(expected))),
764            _ => {
765                return Err(TextError::UnexpectedElement(
766                    String::from_utf8_lossy(expected).into_owned(),
767                ));
768            }
769        }
770        buffer.clear();
771    }
772}
773
774fn text_attr(start: &BytesStart<'_>, name: &[u8]) -> Result<Option<String>> {
775    for attribute in start.attributes() {
776        let attribute = attribute.map_err(OxmlError::from)?;
777        if attribute.key.as_ref() == name {
778            let value = attribute
779                .decoded_and_normalized_value(XmlVersion::Implicit1_0, start.decoder())
780                .map_err(OxmlError::from)?;
781            return Ok(Some(value.into_owned()));
782        }
783    }
784    Ok(None)
785}
786
787fn required_attr(start: &BytesStart<'_>, name: &[u8]) -> Result<String> {
788    text_attr(start, name)?
789        .filter(|value| !value.is_empty())
790        .ok_or_else(|| {
791            missing(
792                &String::from_utf8_lossy(local_name(start.name().as_ref())),
793                &String::from_utf8_lossy(name),
794            )
795        })
796}
797
798fn capture_raw_attributes(
799    start: &BytesStart<'_>,
800    modelled: &[&[u8]],
801) -> Result<Vec<(String, String)>> {
802    let mut raw = Vec::new();
803    for attribute in start.attributes() {
804        let attribute = attribute.map_err(OxmlError::from)?;
805        if modelled.iter().any(|name| attribute.key.as_ref() == *name) {
806            continue;
807        }
808        let name = std::str::from_utf8(attribute.key.as_ref())
809            .map_err(OxmlError::from)?
810            .to_owned();
811        let value = attribute
812            .decoded_and_normalized_value(XmlVersion::Implicit1_0, start.decoder())
813            .map_err(OxmlError::from)?
814            .into_owned();
815        raw.push((name, value));
816    }
817    Ok(raw)
818}
819
820fn push_raw_attributes(start: &mut BytesStart<'_>, attributes: &[(String, String)]) {
821    for (name, value) in attributes {
822        start.push_attribute((name.as_str(), value.as_str()));
823    }
824}
825
826fn emit_raw<'a, W: Write>(
827    writer: &mut Writer<W>,
828    children: impl Iterator<Item = &'a [u8]>,
829) -> Result<()> {
830    for child in children {
831        writer.get_mut().write_all(child).map_err(OxmlError::from)?;
832    }
833    Ok(())
834}
835
836fn write_start<W: Write>(writer: &mut Writer<W>, start: BytesStart<'_>) -> Result<()> {
837    writer
838        .write_event(Event::Start(start))
839        .map_err(OxmlError::from)?;
840    Ok(())
841}
842
843fn write_empty<W: Write>(writer: &mut Writer<W>, start: BytesStart<'_>) -> Result<()> {
844    writer
845        .write_event(Event::Empty(start))
846        .map_err(OxmlError::from)?;
847    Ok(())
848}
849
850fn write_end<W: Write>(writer: &mut Writer<W>, tag: &str) -> Result<()> {
851    writer
852        .write_event(Event::End(BytesEnd::new(tag)))
853        .map_err(OxmlError::from)?;
854    Ok(())
855}
856
857fn unexpected(element: &BytesStart<'_>) -> TextError {
858    TextError::UnexpectedElement(String::from_utf8_lossy(element.name().as_ref()).into_owned())
859}
860
861fn duplicate(element: &str) -> TextError {
862    TextError::DuplicateElement(element.to_owned())
863}
864
865fn missing(element: &str, attribute: &str) -> TextError {
866    TextError::MissingAttribute {
867        element: element.to_owned(),
868        attribute: attribute.to_owned(),
869    }
870}
871
872fn invalid(element: &str, attribute: &str, value: &str) -> TextError {
873    TextError::InvalidAttribute {
874        element: element.to_owned(),
875        attribute: attribute.to_owned(),
876        value: value.to_owned(),
877    }
878}
879
880#[cfg(test)]
881mod tests {
882    use super::TextAutoNumberScheme;
883
884    #[test]
885    fn every_auto_number_scheme_token_maps_without_a_fallback() {
886        let tokens = [
887            "alphaLcParenBoth",
888            "alphaUcParenBoth",
889            "alphaLcParenR",
890            "alphaUcParenR",
891            "alphaLcPeriod",
892            "alphaUcPeriod",
893            "arabicParenBoth",
894            "arabicParenR",
895            "arabicPeriod",
896            "arabicPlain",
897            "romanLcParenBoth",
898            "romanUcParenBoth",
899            "romanLcParenR",
900            "romanUcParenR",
901            "romanLcPeriod",
902            "romanUcPeriod",
903            "circleNumDbPlain",
904            "circleNumWdBlackPlain",
905            "circleNumWdWhitePlain",
906            "arabicDbPeriod",
907            "arabicDbPlain",
908            "ea1ChsPeriod",
909            "ea1ChsPlain",
910            "ea1ChtPeriod",
911            "ea1ChtPlain",
912            "ea1JpnChsDbPeriod",
913            "ea1JpnKorPlain",
914            "ea1JpnKorPeriod",
915            "arabic1Minus",
916            "arabic2Minus",
917            "hebrew2Minus",
918            "thaiAlphaPeriod",
919            "thaiAlphaParenR",
920            "thaiAlphaParenBoth",
921            "thaiNumPeriod",
922            "thaiNumParenR",
923            "thaiNumParenBoth",
924            "hindiAlphaPeriod",
925            "hindiNumPeriod",
926            "hindiNumParenR",
927            "hindiAlpha1Period",
928        ];
929
930        for token in tokens {
931            assert_eq!(TextAutoNumberScheme::parse(token).unwrap().as_str(), token);
932        }
933    }
934}