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    fn parse(value: &str) -> Option<Self> {
487        Some(match value {
488            "alphaLcParenBoth" => Self::AlphaLowerParenBoth,
489            "alphaUcParenBoth" => Self::AlphaUpperParenBoth,
490            "alphaLcParenR" => Self::AlphaLowerParenRight,
491            "alphaUcParenR" => Self::AlphaUpperParenRight,
492            "alphaLcPeriod" => Self::AlphaLowerPeriod,
493            "alphaUcPeriod" => Self::AlphaUpperPeriod,
494            "arabicParenBoth" => Self::ArabicParenBoth,
495            "arabicParenR" => Self::ArabicParenRight,
496            "arabicPeriod" => Self::ArabicPeriod,
497            "arabicPlain" => Self::ArabicPlain,
498            "romanLcParenBoth" => Self::RomanLowerParenBoth,
499            "romanUcParenBoth" => Self::RomanUpperParenBoth,
500            "romanLcParenR" => Self::RomanLowerParenRight,
501            "romanUcParenR" => Self::RomanUpperParenRight,
502            "romanLcPeriod" => Self::RomanLowerPeriod,
503            "romanUcPeriod" => Self::RomanUpperPeriod,
504            "circleNumDbPlain" => Self::CircleNumberDoubleBytePlain,
505            "circleNumWdBlackPlain" => Self::CircleNumberWingdingsBlackPlain,
506            "circleNumWdWhitePlain" => Self::CircleNumberWingdingsWhitePlain,
507            "arabicDbPeriod" => Self::ArabicDoubleBytePeriod,
508            "arabicDbPlain" => Self::ArabicDoubleBytePlain,
509            "ea1ChsPeriod" => Self::EastAsianSimplifiedChinesePeriod,
510            "ea1ChsPlain" => Self::EastAsianSimplifiedChinesePlain,
511            "ea1ChtPeriod" => Self::EastAsianTraditionalChinesePeriod,
512            "ea1ChtPlain" => Self::EastAsianTraditionalChinesePlain,
513            "ea1JpnChsDbPeriod" => Self::EastAsianJapaneseDoubleBytePeriod,
514            "ea1JpnKorPlain" => Self::EastAsianJapaneseKoreanPlain,
515            "ea1JpnKorPeriod" => Self::EastAsianJapaneseKoreanPeriod,
516            "arabic1Minus" => Self::Arabic1Minus,
517            "arabic2Minus" => Self::Arabic2Minus,
518            "hebrew2Minus" => Self::Hebrew2Minus,
519            "thaiAlphaPeriod" => Self::ThaiAlphaPeriod,
520            "thaiAlphaParenR" => Self::ThaiAlphaParenRight,
521            "thaiAlphaParenBoth" => Self::ThaiAlphaParenBoth,
522            "thaiNumPeriod" => Self::ThaiNumberPeriod,
523            "thaiNumParenR" => Self::ThaiNumberParenRight,
524            "thaiNumParenBoth" => Self::ThaiNumberParenBoth,
525            "hindiAlphaPeriod" => Self::HindiAlphaPeriod,
526            "hindiNumPeriod" => Self::HindiNumberPeriod,
527            "hindiNumParenR" => Self::HindiNumberParenRight,
528            "hindiAlpha1Period" => Self::HindiAlpha1Period,
529            _ => return None,
530        })
531    }
532
533    pub const fn as_str(self) -> &'static str {
534        match self {
535            Self::AlphaLowerParenBoth => "alphaLcParenBoth",
536            Self::AlphaUpperParenBoth => "alphaUcParenBoth",
537            Self::AlphaLowerParenRight => "alphaLcParenR",
538            Self::AlphaUpperParenRight => "alphaUcParenR",
539            Self::AlphaLowerPeriod => "alphaLcPeriod",
540            Self::AlphaUpperPeriod => "alphaUcPeriod",
541            Self::ArabicParenBoth => "arabicParenBoth",
542            Self::ArabicParenRight => "arabicParenR",
543            Self::ArabicPeriod => "arabicPeriod",
544            Self::ArabicPlain => "arabicPlain",
545            Self::RomanLowerParenBoth => "romanLcParenBoth",
546            Self::RomanUpperParenBoth => "romanUcParenBoth",
547            Self::RomanLowerParenRight => "romanLcParenR",
548            Self::RomanUpperParenRight => "romanUcParenR",
549            Self::RomanLowerPeriod => "romanLcPeriod",
550            Self::RomanUpperPeriod => "romanUcPeriod",
551            Self::CircleNumberDoubleBytePlain => "circleNumDbPlain",
552            Self::CircleNumberWingdingsBlackPlain => "circleNumWdBlackPlain",
553            Self::CircleNumberWingdingsWhitePlain => "circleNumWdWhitePlain",
554            Self::ArabicDoubleBytePeriod => "arabicDbPeriod",
555            Self::ArabicDoubleBytePlain => "arabicDbPlain",
556            Self::EastAsianSimplifiedChinesePeriod => "ea1ChsPeriod",
557            Self::EastAsianSimplifiedChinesePlain => "ea1ChsPlain",
558            Self::EastAsianTraditionalChinesePeriod => "ea1ChtPeriod",
559            Self::EastAsianTraditionalChinesePlain => "ea1ChtPlain",
560            Self::EastAsianJapaneseDoubleBytePeriod => "ea1JpnChsDbPeriod",
561            Self::EastAsianJapaneseKoreanPlain => "ea1JpnKorPlain",
562            Self::EastAsianJapaneseKoreanPeriod => "ea1JpnKorPeriod",
563            Self::Arabic1Minus => "arabic1Minus",
564            Self::Arabic2Minus => "arabic2Minus",
565            Self::Hebrew2Minus => "hebrew2Minus",
566            Self::ThaiAlphaPeriod => "thaiAlphaPeriod",
567            Self::ThaiAlphaParenRight => "thaiAlphaParenR",
568            Self::ThaiAlphaParenBoth => "thaiAlphaParenBoth",
569            Self::ThaiNumberPeriod => "thaiNumPeriod",
570            Self::ThaiNumberParenRight => "thaiNumParenR",
571            Self::ThaiNumberParenBoth => "thaiNumParenBoth",
572            Self::HindiAlphaPeriod => "hindiAlphaPeriod",
573            Self::HindiNumberPeriod => "hindiNumPeriod",
574            Self::HindiNumberParenRight => "hindiNumParenR",
575            Self::HindiAlpha1Period => "hindiAlpha1Period",
576        }
577    }
578}
579
580/// An explicit `a:buNone` choice with unmodelled attributes retained.
581#[derive(Clone, Debug, Default, Eq, PartialEq)]
582pub struct TextNoBullet {
583    raw_attributes: Vec<(String, String)>,
584}
585
586impl TextNoBullet {
587    fn from_xml(xml: &[u8]) -> Result<Self> {
588        parse_complete(
589            xml,
590            b"buNone",
591            |reader, start| {
592                let value = Self::from_start(start)?;
593                ensure_empty(reader, b"buNone")?;
594                Ok(value)
595            },
596            Self::from_start,
597        )
598    }
599
600    fn from_start(start: &BytesStart<'_>) -> Result<Self> {
601        Ok(Self {
602            raw_attributes: capture_raw_attributes(start, &[])?,
603        })
604    }
605
606    fn write_xml<W: Write>(&self, writer: &mut Writer<W>) -> Result<()> {
607        let mut start = BytesStart::new("a:buNone");
608        push_raw_attributes(&mut start, &self.raw_attributes);
609        write_empty(writer, start)
610    }
611}
612
613fn validate_bullet_percent(value: &str) -> Result<()> {
614    if let Ok(value) = value.parse::<i32>() {
615        return validate_range(
616            "buSzPct",
617            "val",
618            value,
619            MIN_BULLET_PERCENT,
620            MAX_BULLET_PERCENT,
621        );
622    }
623    let Some(percent) = value.strip_suffix('%') else {
624        return Err(invalid("buSzPct", "val", value));
625    };
626    if !is_decimal(percent) {
627        return Err(invalid("buSzPct", "val", value));
628    }
629    let percent = percent
630        .parse::<f64>()
631        .map_err(|_| invalid("buSzPct", "val", value))?;
632    if (25.0..=400.0).contains(&percent) {
633        Ok(())
634    } else {
635        Err(invalid("buSzPct", "val", value))
636    }
637}
638
639fn is_decimal(value: &str) -> bool {
640    let mut parts = value.split('.');
641    let Some(integer) = parts.next() else {
642        return false;
643    };
644    if integer.is_empty() || !integer.bytes().all(|byte| byte.is_ascii_digit()) {
645        return false;
646    }
647    if let Some(fraction) = parts.next()
648        && (fraction.is_empty() || !fraction.bytes().all(|byte| byte.is_ascii_digit()))
649    {
650        return false;
651    }
652    parts.next().is_none()
653}
654
655fn validate_range(element: &str, attribute: &str, value: i32, min: i32, max: i32) -> Result<()> {
656    if (min..=max).contains(&value) {
657        Ok(())
658    } else {
659        Err(invalid(element, attribute, &value.to_string()))
660    }
661}
662
663fn parse_range(element: &str, attribute: &str, value: &str, min: i32, max: i32) -> Result<i32> {
664    let parsed = value
665        .parse::<i32>()
666        .map_err(|_| invalid(element, attribute, value))?;
667    validate_range(element, attribute, parsed, min, max)?;
668    Ok(parsed)
669}
670
671fn is_color(name: &[u8]) -> bool {
672    matches!(
673        local_name(name),
674        b"srgbClr" | b"schemeClr" | b"sysClr" | b"prstClr"
675    )
676}
677
678fn validate_color_attributes(element: &BytesStart<'_>) -> Result<()> {
679    required_attr(element, b"val")?;
680    for attribute in element.attributes() {
681        let attribute = attribute.map_err(OxmlError::from)?;
682        let name = attribute.key.as_ref();
683        if name != b"val" && name != b"lastClr" && matches!(local_name(name), b"val" | b"lastClr") {
684            return Err(invalid(
685                &String::from_utf8_lossy(local_name(element.name().as_ref())),
686                &String::from_utf8_lossy(name),
687                &String::from_utf8_lossy(attribute.value.as_ref()),
688            ));
689        }
690    }
691    Ok(())
692}
693
694fn parse_complete<T>(
695    xml: &[u8],
696    expected: &[u8],
697    parse_start: impl FnOnce(&mut Reader<&[u8]>, &BytesStart<'_>) -> Result<T>,
698    parse_empty: impl FnOnce(&BytesStart<'_>) -> Result<T>,
699) -> Result<T> {
700    let mut reader = Reader::from_reader(xml);
701    let mut buffer = Vec::new();
702    loop {
703        match reader
704            .read_event_into(&mut buffer)
705            .map_err(OxmlError::from)?
706        {
707            Event::Start(element) if matches_local_name(element.name().as_ref(), expected) => {
708                reject_conflicting_a_prefix(&element)?;
709                return parse_start(&mut reader, &element);
710            }
711            Event::Empty(element) if matches_local_name(element.name().as_ref(), expected) => {
712                reject_conflicting_a_prefix(&element)?;
713                return parse_empty(&element);
714            }
715            Event::Start(element) | Event::Empty(element) => return Err(unexpected(&element)),
716            Event::Eof => {
717                return Err(TextError::Xml(OxmlError::MissingElement(
718                    String::from_utf8_lossy(expected).into_owned(),
719                )));
720            }
721            _ => {}
722        }
723        buffer.clear();
724    }
725}
726
727fn root_local_name(xml: &[u8]) -> Result<Vec<u8>> {
728    let mut reader = Reader::from_reader(xml);
729    let mut buffer = Vec::new();
730    loop {
731        match reader
732            .read_event_into(&mut buffer)
733            .map_err(OxmlError::from)?
734        {
735            Event::Start(element) | Event::Empty(element) => {
736                return Ok(local_name(element.name().as_ref()).to_vec());
737            }
738            Event::Eof => {
739                return Err(TextError::Xml(OxmlError::MissingElement(
740                    "DrawingML bullet".to_owned(),
741                )));
742            }
743            _ => {}
744        }
745        buffer.clear();
746    }
747}
748
749fn ensure_empty(reader: &mut Reader<&[u8]>, expected: &[u8]) -> Result<()> {
750    let mut buffer = Vec::new();
751    loop {
752        match reader
753            .read_event_into(&mut buffer)
754            .map_err(OxmlError::from)?
755        {
756            Event::End(element) if matches_local_name(element.name().as_ref(), expected) => {
757                return Ok(());
758            }
759            Event::Text(text) if text.iter().all(u8::is_ascii_whitespace) => {}
760            Event::Comment(_) => {}
761            Event::Start(element) | Event::Empty(element) => return Err(unexpected(&element)),
762            Event::Eof => return Err(missing_end(&String::from_utf8_lossy(expected))),
763            _ => {
764                return Err(TextError::UnexpectedElement(
765                    String::from_utf8_lossy(expected).into_owned(),
766                ));
767            }
768        }
769        buffer.clear();
770    }
771}
772
773fn text_attr(start: &BytesStart<'_>, name: &[u8]) -> Result<Option<String>> {
774    for attribute in start.attributes() {
775        let attribute = attribute.map_err(OxmlError::from)?;
776        if attribute.key.as_ref() == name {
777            let value = attribute
778                .decoded_and_normalized_value(XmlVersion::Implicit1_0, start.decoder())
779                .map_err(OxmlError::from)?;
780            return Ok(Some(value.into_owned()));
781        }
782    }
783    Ok(None)
784}
785
786fn required_attr(start: &BytesStart<'_>, name: &[u8]) -> Result<String> {
787    text_attr(start, name)?
788        .filter(|value| !value.is_empty())
789        .ok_or_else(|| {
790            missing(
791                &String::from_utf8_lossy(local_name(start.name().as_ref())),
792                &String::from_utf8_lossy(name),
793            )
794        })
795}
796
797fn capture_raw_attributes(
798    start: &BytesStart<'_>,
799    modelled: &[&[u8]],
800) -> Result<Vec<(String, String)>> {
801    let mut raw = Vec::new();
802    for attribute in start.attributes() {
803        let attribute = attribute.map_err(OxmlError::from)?;
804        if modelled.iter().any(|name| attribute.key.as_ref() == *name) {
805            continue;
806        }
807        let name = std::str::from_utf8(attribute.key.as_ref())
808            .map_err(OxmlError::from)?
809            .to_owned();
810        let value = attribute
811            .decoded_and_normalized_value(XmlVersion::Implicit1_0, start.decoder())
812            .map_err(OxmlError::from)?
813            .into_owned();
814        raw.push((name, value));
815    }
816    Ok(raw)
817}
818
819fn push_raw_attributes(start: &mut BytesStart<'_>, attributes: &[(String, String)]) {
820    for (name, value) in attributes {
821        start.push_attribute((name.as_str(), value.as_str()));
822    }
823}
824
825fn emit_raw<'a, W: Write>(
826    writer: &mut Writer<W>,
827    children: impl Iterator<Item = &'a [u8]>,
828) -> Result<()> {
829    for child in children {
830        writer.get_mut().write_all(child).map_err(OxmlError::from)?;
831    }
832    Ok(())
833}
834
835fn write_start<W: Write>(writer: &mut Writer<W>, start: BytesStart<'_>) -> Result<()> {
836    writer
837        .write_event(Event::Start(start))
838        .map_err(OxmlError::from)?;
839    Ok(())
840}
841
842fn write_empty<W: Write>(writer: &mut Writer<W>, start: BytesStart<'_>) -> Result<()> {
843    writer
844        .write_event(Event::Empty(start))
845        .map_err(OxmlError::from)?;
846    Ok(())
847}
848
849fn write_end<W: Write>(writer: &mut Writer<W>, tag: &str) -> Result<()> {
850    writer
851        .write_event(Event::End(BytesEnd::new(tag)))
852        .map_err(OxmlError::from)?;
853    Ok(())
854}
855
856fn unexpected(element: &BytesStart<'_>) -> TextError {
857    TextError::UnexpectedElement(String::from_utf8_lossy(element.name().as_ref()).into_owned())
858}
859
860fn duplicate(element: &str) -> TextError {
861    TextError::DuplicateElement(element.to_owned())
862}
863
864fn missing(element: &str, attribute: &str) -> TextError {
865    TextError::MissingAttribute {
866        element: element.to_owned(),
867        attribute: attribute.to_owned(),
868    }
869}
870
871fn invalid(element: &str, attribute: &str, value: &str) -> TextError {
872    TextError::InvalidAttribute {
873        element: element.to_owned(),
874        attribute: attribute.to_owned(),
875        value: value.to_owned(),
876    }
877}
878
879#[cfg(test)]
880mod tests {
881    use super::TextAutoNumberScheme;
882
883    #[test]
884    fn every_auto_number_scheme_token_maps_without_a_fallback() {
885        let tokens = [
886            "alphaLcParenBoth",
887            "alphaUcParenBoth",
888            "alphaLcParenR",
889            "alphaUcParenR",
890            "alphaLcPeriod",
891            "alphaUcPeriod",
892            "arabicParenBoth",
893            "arabicParenR",
894            "arabicPeriod",
895            "arabicPlain",
896            "romanLcParenBoth",
897            "romanUcParenBoth",
898            "romanLcParenR",
899            "romanUcParenR",
900            "romanLcPeriod",
901            "romanUcPeriod",
902            "circleNumDbPlain",
903            "circleNumWdBlackPlain",
904            "circleNumWdWhitePlain",
905            "arabicDbPeriod",
906            "arabicDbPlain",
907            "ea1ChsPeriod",
908            "ea1ChsPlain",
909            "ea1ChtPeriod",
910            "ea1ChtPlain",
911            "ea1JpnChsDbPeriod",
912            "ea1JpnKorPlain",
913            "ea1JpnKorPeriod",
914            "arabic1Minus",
915            "arabic2Minus",
916            "hebrew2Minus",
917            "thaiAlphaPeriod",
918            "thaiAlphaParenR",
919            "thaiAlphaParenBoth",
920            "thaiNumPeriod",
921            "thaiNumParenR",
922            "thaiNumParenBoth",
923            "hindiAlphaPeriod",
924            "hindiNumPeriod",
925            "hindiNumParenR",
926            "hindiAlpha1Period",
927        ];
928
929        for token in tokens {
930            assert_eq!(TextAutoNumberScheme::parse(token).unwrap().as_str(), token);
931        }
932    }
933}