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#[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#[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#[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#[derive(Clone, Debug, Eq, PartialEq)]
283pub enum TextBulletSizeValue {
284 Percent(String),
285 Points(i32),
286}
287
288#[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#[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#[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#[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#[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}