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 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#[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}