1use std::borrow::Cow;
2use std::io::Write;
3
4use oxml_core::OxmlError;
5use oxml_core::raw_xml::{capture_element, capture_empty_element};
6use oxml_core::xml::{local_name, matches_local_name};
7use oxml_core::xml_text::{decode_plain, resolve_entity};
8use quick_xml::events::{BytesEnd, BytesStart, BytesText, Event};
9use quick_xml::{Reader, Writer, XmlVersion};
10
11use crate::fill::Fill;
12use crate::namespace::reject_conflicting_a_prefix;
13use crate::order::OrderedRawChildren;
14
15use super::body::{Result, TextError, missing_end};
16use super::bullet::TextBullet;
17
18const MAX_TEXT_MARGIN: i32 = 51_206_400;
19const MAX_TEXT_POINT: i32 = 400_000;
20const MAX_TEXT_SPACING_POINT: i32 = 158_400;
21const MAX_TRANSITIONAL_SPACING_PERCENT: i32 = 13_200_000;
22
23#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
25pub enum TextSpace {
26 #[default]
27 Default,
28 Preserve,
29}
30
31#[derive(Clone, Debug, Default, Eq, PartialEq)]
39pub struct TextValue {
40 pub value: String,
41 pub space: TextSpace,
42 raw_attributes: Vec<(String, String)>,
43}
44
45impl TextValue {
46 fn from_start(start: &BytesStart<'_>) -> Result<Self> {
47 if !matches_local_name(start.name().as_ref(), b"t") {
48 return Err(unexpected(start));
49 }
50 let space_value = text_attr(start, b"xml:space")?;
51 let space = match space_value.as_deref() {
52 None | Some("default") => TextSpace::Default,
53 Some("preserve") => TextSpace::Preserve,
54 Some(value) => return Err(invalid_attribute("t", "xml:space", value)),
55 };
56 Ok(Self {
57 value: String::new(),
58 space,
59 raw_attributes: capture_raw_attributes(start, &[b"xml:space"])?,
60 })
61 }
62
63 fn from_element(reader: &mut Reader<&[u8]>, start: &BytesStart<'_>) -> Result<Self> {
64 let mut text = Self::from_start(start)?;
65 let mut buffer = Vec::new();
66 loop {
67 match reader
68 .read_event_into(&mut buffer)
69 .map_err(OxmlError::from)?
70 {
71 Event::Text(value) => text.value.push_str(&decode_plain(&value)),
72 Event::CData(value) => {
73 let value = std::str::from_utf8(value.as_ref()).map_err(OxmlError::from)?;
74 text.value.push_str(value);
75 }
76 Event::GeneralRef(value) => text.value.push_str(&resolve_entity(&value)),
77 Event::End(element) if matches_local_name(element.name().as_ref(), b"t") => {
78 if text.space == TextSpace::Default
79 && (text.value.chars().next().is_some_and(char::is_whitespace)
80 || text
81 .value
82 .chars()
83 .next_back()
84 .is_some_and(char::is_whitespace))
85 {
86 text.space = TextSpace::Preserve;
87 }
88 return Ok(text);
89 }
90 Event::Start(element) | Event::Empty(element) => {
91 return Err(unexpected(&element));
92 }
93 Event::Eof => return Err(missing_end("t")),
94 _ => {}
95 }
96 buffer.clear();
97 }
98 }
99
100 fn write_xml<W: Write>(&self, writer: &mut Writer<W>) -> Result<()> {
101 let preserve = self.space == TextSpace::Preserve
102 || self.value.chars().next().is_some_and(char::is_whitespace)
103 || self
104 .value
105 .chars()
106 .next_back()
107 .is_some_and(char::is_whitespace);
108 let mut start = BytesStart::new("a:t");
109 if preserve {
110 start.push_attribute(("xml:space", "preserve"));
111 }
112 push_raw_attributes(&mut start, &self.raw_attributes);
113 if self.value.is_empty() {
114 return write_empty(writer, start);
115 }
116 write_start(writer, start)?;
117 writer
118 .write_event(Event::Text(BytesText::new(&self.value)))
119 .map_err(OxmlError::from)?;
120 write_end(writer, "a:t")
121 }
122}
123
124pub fn escape_invalid_xml_characters(text: &str) -> Cow<'_, str> {
127 let is_invalid = |character: char| !oxml_core::xml::is_xml_1_0_character(character);
128 if !text.chars().any(is_invalid) {
129 return Cow::Borrowed(text);
130 }
131 let mut escaped = String::with_capacity(text.len() + 6);
132 for character in text.chars() {
133 if is_invalid(character) {
134 escaped.push_str(&format!("_x{:04X}_", u32::from(character)));
135 } else {
136 escaped.push(character);
137 }
138 }
139 Cow::Owned(escaped)
140}
141
142#[derive(Clone, Copy, Debug, Eq, PartialEq)]
144pub enum TextAlignment {
145 Left,
146 Center,
147 Right,
148 Justified,
149 JustifiedLow,
150 Distributed,
151 ThaiDistributed,
152}
153
154impl TextAlignment {
155 fn parse(value: &str) -> Option<Self> {
156 match value {
157 "l" => Some(Self::Left),
158 "ctr" => Some(Self::Center),
159 "r" => Some(Self::Right),
160 "just" => Some(Self::Justified),
161 "justLow" => Some(Self::JustifiedLow),
162 "dist" => Some(Self::Distributed),
163 "thaiDist" => Some(Self::ThaiDistributed),
164 _ => None,
165 }
166 }
167
168 const fn as_str(self) -> &'static str {
169 match self {
170 Self::Left => "l",
171 Self::Center => "ctr",
172 Self::Right => "r",
173 Self::Justified => "just",
174 Self::JustifiedLow => "justLow",
175 Self::Distributed => "dist",
176 Self::ThaiDistributed => "thaiDist",
177 }
178 }
179}
180
181#[derive(Clone, Debug, Eq, PartialEq)]
183pub enum TextSpacing {
184 Percent(String),
185 Points(i32),
186}
187
188impl TextSpacing {
189 fn from_xml(xml: &[u8], wrapper: &[u8]) -> Result<Self> {
190 parse_complete(
191 xml,
192 wrapper,
193 |reader, start| Self::from_element(reader, start, wrapper),
194 |_| {
195 Err(TextError::UnexpectedElement(
196 String::from_utf8_lossy(wrapper).into_owned(),
197 ))
198 },
199 )
200 }
201
202 fn from_element(
203 reader: &mut Reader<&[u8]>,
204 start: &BytesStart<'_>,
205 wrapper: &[u8],
206 ) -> Result<Self> {
207 reject_conflicting_a_prefix(start)?;
208 let mut spacing = None;
209 let mut buffer = Vec::new();
210 loop {
211 match reader
212 .read_event_into(&mut buffer)
213 .map_err(OxmlError::from)?
214 {
215 Event::Empty(element) if matches_local_name(element.name().as_ref(), b"spcPct") => {
216 reject_conflicting_a_prefix(&element)?;
217 if spacing.is_some() {
218 return Err(duplicate("text spacing choice"));
219 }
220 let value = required_attr(&element, b"val")?;
221 validate_spacing_percent(&value)?;
222 spacing = Some(Self::Percent(value));
223 }
224 Event::Empty(element) if matches_local_name(element.name().as_ref(), b"spcPts") => {
225 reject_conflicting_a_prefix(&element)?;
226 if spacing.is_some() {
227 return Err(duplicate("text spacing choice"));
228 }
229 let value = parse_i32_attr(&element, b"val", 0, MAX_TEXT_SPACING_POINT)?;
230 spacing = Some(Self::Points(value));
231 }
232 Event::Start(element)
233 if matches_local_name(element.name().as_ref(), b"spcPct")
234 || matches_local_name(element.name().as_ref(), b"spcPts") =>
235 {
236 reject_conflicting_a_prefix(&element)?;
237 if spacing.is_some() {
238 return Err(duplicate("text spacing choice"));
239 }
240 let name = local_name(element.name().as_ref()).to_vec();
241 let value = if name == b"spcPct" {
242 let value = required_attr(&element, b"val")?;
243 validate_spacing_percent(&value)?;
244 Self::Percent(value)
245 } else {
246 Self::Points(parse_i32_attr(&element, b"val", 0, MAX_TEXT_SPACING_POINT)?)
247 };
248 ensure_empty(reader, &name)?;
249 spacing = Some(value);
250 }
251 Event::Start(element) | Event::Empty(element) => return Err(unexpected(&element)),
252 Event::End(element) if matches_local_name(element.name().as_ref(), wrapper) => {
253 return spacing.ok_or_else(|| {
254 TextError::UnexpectedElement(String::from_utf8_lossy(wrapper).into_owned())
255 });
256 }
257 Event::Eof => return Err(missing_end(&String::from_utf8_lossy(wrapper))),
258 _ => {}
259 }
260 buffer.clear();
261 }
262 }
263
264 fn write_xml<W: Write>(&self, writer: &mut Writer<W>, wrapper: &str) -> Result<()> {
265 write_start(writer, BytesStart::new(wrapper))?;
266 match self {
267 Self::Percent(value) => {
268 validate_spacing_percent(value)?;
269 let mut start = BytesStart::new("a:spcPct");
270 start.push_attribute(("val", value.as_str()));
271 write_empty(writer, start)?;
272 }
273 Self::Points(value) if (0..=MAX_TEXT_SPACING_POINT).contains(value) => {
274 let value = value.to_string();
275 let mut start = BytesStart::new("a:spcPts");
276 start.push_attribute(("val", value.as_str()));
277 write_empty(writer, start)?;
278 }
279 Self::Points(value) => {
280 return Err(invalid_attribute("spcPts", "val", &value.to_string()));
281 }
282 }
283 write_end(writer, wrapper)
284 }
285}
286
287#[derive(Clone, Debug, Eq, PartialEq)]
289pub enum TextPointValue {
290 Centipoints(i32),
291 UniversalMeasure(String),
292}
293
294impl TextPointValue {
295 fn parse(value: String) -> Result<Self> {
296 if let Ok(point) = value.parse::<i32>() {
297 if (-MAX_TEXT_POINT..=MAX_TEXT_POINT).contains(&point) {
298 return Ok(Self::Centipoints(point));
299 }
300 return Err(invalid_attribute("rPr", "spc", &value));
301 }
302 if is_universal_measure(&value) {
303 Ok(Self::UniversalMeasure(value))
304 } else {
305 Err(invalid_attribute("rPr", "spc", &value))
306 }
307 }
308
309 fn as_xml(&self) -> Result<String> {
310 match self {
311 Self::Centipoints(value) if (-MAX_TEXT_POINT..=MAX_TEXT_POINT).contains(value) => {
312 Ok(value.to_string())
313 }
314 Self::Centipoints(value) => Err(invalid_attribute("rPr", "spc", &value.to_string())),
315 Self::UniversalMeasure(value) if is_universal_measure(value) => Ok(value.clone()),
316 Self::UniversalMeasure(value) => Err(invalid_attribute("rPr", "spc", value)),
317 }
318 }
319}
320
321#[derive(Clone, Copy, Debug, Eq, PartialEq)]
323pub enum TextUnderline {
324 None,
325 Words,
326 Single,
327 Double,
328 Heavy,
329 Dotted,
330 DottedHeavy,
331 Dash,
332 DashHeavy,
333 DashLong,
334 DashLongHeavy,
335 DotDash,
336 DotDashHeavy,
337 DotDotDash,
338 DotDotDashHeavy,
339 Wavy,
340 WavyHeavy,
341 WavyDouble,
342}
343
344impl TextUnderline {
345 fn parse(value: &str) -> Option<Self> {
346 Some(match value {
347 "none" => Self::None,
348 "words" => Self::Words,
349 "sng" => Self::Single,
350 "dbl" => Self::Double,
351 "heavy" => Self::Heavy,
352 "dotted" => Self::Dotted,
353 "dottedHeavy" => Self::DottedHeavy,
354 "dash" => Self::Dash,
355 "dashHeavy" => Self::DashHeavy,
356 "dashLong" => Self::DashLong,
357 "dashLongHeavy" => Self::DashLongHeavy,
358 "dotDash" => Self::DotDash,
359 "dotDashHeavy" => Self::DotDashHeavy,
360 "dotDotDash" => Self::DotDotDash,
361 "dotDotDashHeavy" => Self::DotDotDashHeavy,
362 "wavy" => Self::Wavy,
363 "wavyHeavy" => Self::WavyHeavy,
364 "wavyDbl" => Self::WavyDouble,
365 _ => return None,
366 })
367 }
368
369 pub const fn as_str(self) -> &'static str {
371 match self {
372 Self::None => "none",
373 Self::Words => "words",
374 Self::Single => "sng",
375 Self::Double => "dbl",
376 Self::Heavy => "heavy",
377 Self::Dotted => "dotted",
378 Self::DottedHeavy => "dottedHeavy",
379 Self::Dash => "dash",
380 Self::DashHeavy => "dashHeavy",
381 Self::DashLong => "dashLong",
382 Self::DashLongHeavy => "dashLongHeavy",
383 Self::DotDash => "dotDash",
384 Self::DotDashHeavy => "dotDashHeavy",
385 Self::DotDotDash => "dotDotDash",
386 Self::DotDotDashHeavy => "dotDotDashHeavy",
387 Self::Wavy => "wavy",
388 Self::WavyHeavy => "wavyHeavy",
389 Self::WavyDouble => "wavyDbl",
390 }
391 }
392}
393
394#[derive(Clone, Copy, Debug, Eq, PartialEq)]
396pub enum TextStrike {
397 None,
398 Single,
399 Double,
400}
401
402impl TextStrike {
403 fn parse(value: &str) -> Option<Self> {
404 match value {
405 "noStrike" => Some(Self::None),
406 "sngStrike" => Some(Self::Single),
407 "dblStrike" => Some(Self::Double),
408 _ => None,
409 }
410 }
411
412 const fn as_str(self) -> &'static str {
413 match self {
414 Self::None => "noStrike",
415 Self::Single => "sngStrike",
416 Self::Double => "dblStrike",
417 }
418 }
419}
420
421#[derive(Clone, Debug, Eq, PartialEq)]
423pub struct TextFont {
424 pub typeface: String,
425 raw_attributes: Vec<(String, String)>,
426}
427
428impl TextFont {
429 pub fn new(typeface: impl Into<String>) -> Result<Self> {
430 let typeface = typeface.into();
431 if typeface.is_empty() {
432 return Err(invalid_attribute("font", "typeface", ""));
433 }
434 Ok(Self {
435 typeface,
436 raw_attributes: Vec::new(),
437 })
438 }
439
440 pub(crate) fn from_xml(xml: &[u8], expected: &[u8]) -> Result<Self> {
441 parse_complete(
442 xml,
443 expected,
444 |reader, start| {
445 let font = Self::from_start(start)?;
446 ensure_empty(reader, expected)?;
447 Ok(font)
448 },
449 Self::from_start,
450 )
451 }
452
453 fn from_start(start: &BytesStart<'_>) -> Result<Self> {
454 reject_conflicting_a_prefix(start)?;
455 Ok(Self {
456 typeface: required_attr(start, b"typeface")?,
457 raw_attributes: capture_raw_attributes(start, &[b"typeface"])?,
458 })
459 }
460
461 pub(crate) fn write_xml<W: Write>(&self, writer: &mut Writer<W>, tag: &str) -> Result<()> {
462 if self.typeface.is_empty() {
463 return Err(invalid_attribute(tag, "typeface", ""));
464 }
465 let mut start = BytesStart::new(tag);
466 start.push_attribute(("typeface", self.typeface.as_str()));
467 push_raw_attributes(&mut start, &self.raw_attributes);
468 write_empty(writer, start)
469 }
470}
471
472#[derive(Clone, Debug, Default, Eq, PartialEq)]
474pub struct TextHyperlink {
475 pub relationship_id: Option<String>,
476 pub invalid_url: Option<String>,
477 pub action: Option<String>,
478 pub target_frame: Option<String>,
479 pub tooltip: Option<String>,
480 pub history: Option<bool>,
481 pub highlight_click: Option<bool>,
482 pub end_sound: Option<bool>,
483 raw_attributes: Vec<(String, String)>,
484 raw_children: OrderedRawChildren,
485}
486
487impl TextHyperlink {
488 fn from_xml(xml: &[u8], expected: &[u8]) -> Result<Self> {
489 parse_complete(
490 xml,
491 expected,
492 |reader, start| Self::from_element(reader, start, expected),
493 Self::from_start,
494 )
495 }
496
497 fn from_start(start: &BytesStart<'_>) -> Result<Self> {
498 reject_conflicting_a_prefix(start)?;
499 let relationship_id = text_attr(start, b"r:id")?;
500 Ok(Self {
501 relationship_id,
502 invalid_url: text_attr(start, b"invalidUrl")?,
503 action: text_attr(start, b"action")?,
504 target_frame: text_attr(start, b"tgtFrame")?,
505 tooltip: text_attr(start, b"tooltip")?,
506 history: parse_optional_bool(start, b"history")?,
507 highlight_click: parse_optional_bool(start, b"highlightClick")?,
508 end_sound: parse_optional_bool(start, b"endSnd")?,
509 raw_attributes: capture_raw_attributes(
510 start,
511 &[
512 b"r:id",
513 b"invalidUrl",
514 b"action",
515 b"tgtFrame",
516 b"tooltip",
517 b"history",
518 b"highlightClick",
519 b"endSnd",
520 ],
521 )?,
522 raw_children: OrderedRawChildren::default(),
523 })
524 }
525
526 fn from_element(
527 reader: &mut Reader<&[u8]>,
528 start: &BytesStart<'_>,
529 expected: &[u8],
530 ) -> Result<Self> {
531 let mut hyperlink = Self::from_start(start)?;
532 let mut boundary = 0;
533 let mut seen = [false; 2];
534 let mut buffer = Vec::new();
535 loop {
536 match reader
537 .read_event_into(&mut buffer)
538 .map_err(OxmlError::from)?
539 {
540 Event::Start(element) => {
541 let name = local_name(element.name().as_ref()).to_vec();
542 let raw = capture_element(reader, &element)?;
543 capture_hyperlink_raw(&mut hyperlink, &name, raw, &mut boundary, &mut seen)?;
544 }
545 Event::Empty(element) => {
546 let name = local_name(element.name().as_ref()).to_vec();
547 let raw = capture_empty_element(&element)?;
548 capture_hyperlink_raw(&mut hyperlink, &name, raw, &mut boundary, &mut seen)?;
549 }
550 Event::End(element) if matches_local_name(element.name().as_ref(), expected) => {
551 return Ok(hyperlink);
552 }
553 Event::Eof => return Err(missing_end(&String::from_utf8_lossy(expected))),
554 _ => {}
555 }
556 buffer.clear();
557 }
558 }
559
560 fn write_xml<W: Write>(&self, writer: &mut Writer<W>, tag: &str) -> Result<()> {
561 let mut start = BytesStart::new(tag);
562 push_optional_attr(&mut start, "r:id", self.relationship_id.as_deref());
563 push_optional_attr(&mut start, "invalidUrl", self.invalid_url.as_deref());
564 push_optional_attr(&mut start, "action", self.action.as_deref());
565 push_optional_attr(&mut start, "tgtFrame", self.target_frame.as_deref());
566 push_optional_attr(&mut start, "tooltip", self.tooltip.as_deref());
567 push_optional_bool(&mut start, "history", self.history);
568 push_optional_bool(&mut start, "highlightClick", self.highlight_click);
569 push_optional_bool(&mut start, "endSnd", self.end_sound);
570 push_raw_attributes(&mut start, &self.raw_attributes);
571 if self.raw_children.is_empty() {
572 return write_empty(writer, start);
573 }
574 write_start(writer, start)?;
575 for boundary in 0..=2 {
576 emit_raw(writer, self.raw_children.at(boundary))?;
577 }
578 write_end(writer, tag)
579 }
580}
581
582fn capture_hyperlink_raw(
583 hyperlink: &mut TextHyperlink,
584 name: &[u8],
585 raw: Vec<u8>,
586 boundary: &mut usize,
587 seen: &mut [bool; 2],
588) -> Result<()> {
589 let slot = match name {
590 b"snd" => Some(0),
591 b"extLst" => Some(1),
592 _ => None,
593 };
594 if let Some(slot) = slot {
595 if seen[slot] {
596 return Err(duplicate(&String::from_utf8_lossy(name)));
597 }
598 seen[slot] = true;
599 hyperlink.raw_children.push((*boundary).max(slot), raw);
600 *boundary = (*boundary).max(slot + 1);
601 } else {
602 hyperlink.raw_children.push(*boundary, raw);
603 }
604 Ok(())
605}
606
607#[allow(non_camel_case_types)]
609#[derive(Clone, Debug, Default, Eq, PartialEq)]
610pub struct CT_TextCharacterProperties {
611 pub font_size: Option<i32>,
612 pub bold: Option<bool>,
613 pub italic: Option<bool>,
614 pub all_caps: Option<bool>,
615 pub underline: Option<TextUnderline>,
616 pub strike: Option<TextStrike>,
617 pub spacing: Option<TextPointValue>,
618 pub baseline: Option<String>,
619 pub fill: Option<Fill>,
620 pub latin: Option<TextFont>,
621 pub east_asian: Option<TextFont>,
622 pub complex_script: Option<TextFont>,
623 pub symbol: Option<TextFont>,
624 pub hyperlink_click: Option<TextHyperlink>,
625 pub hyperlink_mouse_over: Option<TextHyperlink>,
626 raw_attributes: Vec<(String, String)>,
627 raw_children: OrderedRawChildren,
628}
629
630impl CT_TextCharacterProperties {
631 pub fn from_xml(xml: &[u8]) -> Result<Self> {
633 let expected = root_local_name(xml)?;
634 if !is_character_property_tag(&expected) {
635 return Err(TextError::UnexpectedElement(
636 String::from_utf8_lossy(&expected).into_owned(),
637 ));
638 }
639 parse_complete(xml, &expected, Self::from_element, Self::from_start)
640 }
641
642 fn from_start(start: &BytesStart<'_>) -> Result<Self> {
643 reject_conflicting_a_prefix(start)?;
644 let element = String::from_utf8_lossy(local_name(start.name().as_ref())).into_owned();
645 let font_size = text_attr(start, b"sz")?
646 .map(|value| parse_i32_value(&element, "sz", &value, 100, MAX_TEXT_POINT))
647 .transpose()?;
648 let spacing = text_attr(start, b"spc")?
649 .map(TextPointValue::parse)
650 .transpose()?;
651 let baseline = text_attr(start, b"baseline")?;
652 if let Some(value) = baseline.as_deref() {
653 validate_baseline(value)?;
654 }
655 let all_caps = match text_attr(start, b"cap")?.as_deref() {
656 Some("all") => Some(true),
657 Some("none") => Some(false),
658 _ => None,
659 };
660 let mut raw_attributes = capture_raw_attributes(
661 start,
662 &[b"sz", b"b", b"i", b"u", b"strike", b"spc", b"baseline"],
663 )?;
664 if all_caps.is_some() {
665 raw_attributes.retain(|(name, _)| name != "cap");
666 }
667 Ok(Self {
668 font_size,
669 bold: parse_optional_bool(start, b"b")?,
670 italic: parse_optional_bool(start, b"i")?,
671 all_caps,
672 underline: parse_optional_enum(start, b"u", TextUnderline::parse)?,
673 strike: parse_optional_enum(start, b"strike", TextStrike::parse)?,
674 spacing,
675 baseline,
676 raw_attributes,
677 ..Self::default()
678 })
679 }
680
681 fn from_element(reader: &mut Reader<&[u8]>, start: &BytesStart<'_>) -> Result<Self> {
682 let expected = local_name(start.name().as_ref()).to_vec();
683 let mut properties = Self::from_start(start)?;
684 let mut boundary = 0;
685 let mut seen = [false; 14];
686 let mut buffer = Vec::new();
687 loop {
688 match reader
689 .read_event_into(&mut buffer)
690 .map_err(OxmlError::from)?
691 {
692 Event::Start(element) => {
693 let name = local_name(element.name().as_ref()).to_vec();
694 let raw = capture_element(reader, &element)?;
695 properties.capture_child(&name, raw, &mut boundary, &mut seen)?;
696 }
697 Event::Empty(element) => {
698 let name = local_name(element.name().as_ref()).to_vec();
699 let raw = capture_empty_element(&element)?;
700 properties.capture_child(&name, raw, &mut boundary, &mut seen)?;
701 }
702 Event::End(element) if matches_local_name(element.name().as_ref(), &expected) => {
703 return Ok(properties);
704 }
705 Event::Eof => return Err(missing_end(&String::from_utf8_lossy(&expected))),
706 _ => {}
707 }
708 buffer.clear();
709 }
710 }
711
712 fn capture_child(
713 &mut self,
714 name: &[u8],
715 raw: Vec<u8>,
716 boundary: &mut usize,
717 seen: &mut [bool; 14],
718 ) -> Result<()> {
719 let slot = character_property_slot(name);
720 if let Some(slot) = slot {
721 if seen[slot - 1] {
722 return Err(duplicate(&String::from_utf8_lossy(name)));
723 }
724 seen[slot - 1] = true;
725 }
726 match name {
727 name if is_fill(name) => self.fill = Some(Fill::from_xml(&raw)?),
728 b"latin" => self.latin = Some(TextFont::from_xml(&raw, b"latin")?),
729 b"ea" => self.east_asian = Some(TextFont::from_xml(&raw, b"ea")?),
730 b"cs" => self.complex_script = Some(TextFont::from_xml(&raw, b"cs")?),
731 b"sym" => self.symbol = Some(TextFont::from_xml(&raw, b"sym")?),
732 b"hlinkClick" => {
733 self.hyperlink_click = Some(TextHyperlink::from_xml(&raw, b"hlinkClick")?)
734 }
735 b"hlinkMouseOver" => {
736 self.hyperlink_mouse_over = Some(TextHyperlink::from_xml(&raw, b"hlinkMouseOver")?)
737 }
738 _ => self.raw_children.push(
739 slot.map_or(*boundary, |slot| (*boundary).max(slot - 1)),
740 raw,
741 ),
742 }
743 if let Some(slot) = slot {
744 *boundary = (*boundary).max(slot);
745 }
746 Ok(())
747 }
748
749 pub fn write_xml<W: Write>(&self, writer: &mut Writer<W>, tag: &str) -> Result<()> {
751 let mut start = BytesStart::new(tag);
752 if let Some(value) = self.font_size {
753 parse_i32_value(tag, "sz", &value.to_string(), 100, MAX_TEXT_POINT)?;
754 push_owned_attr(&mut start, "sz", value.to_string());
755 }
756 push_optional_bool(&mut start, "b", self.bold);
757 push_optional_bool(&mut start, "i", self.italic);
758 if let Some(value) = self.all_caps {
759 start.push_attribute(("cap", if value { "all" } else { "none" }));
760 }
761 if let Some(value) = self.underline {
762 start.push_attribute(("u", value.as_str()));
763 }
764 if let Some(value) = self.strike {
765 start.push_attribute(("strike", value.as_str()));
766 }
767 if let Some(value) = &self.spacing {
768 push_owned_attr(&mut start, "spc", value.as_xml()?);
769 }
770 if let Some(value) = self.baseline.as_deref() {
771 validate_baseline(value)?;
772 start.push_attribute(("baseline", value));
773 }
774 for (name, value) in &self.raw_attributes {
775 if name == "cap" && self.all_caps.is_some() {
777 continue;
778 }
779 start.push_attribute((name.as_str(), value.as_str()));
780 }
781
782 let has_modelled_children = self.fill.is_some()
783 || self.latin.is_some()
784 || self.east_asian.is_some()
785 || self.complex_script.is_some()
786 || self.symbol.is_some()
787 || self.hyperlink_click.is_some()
788 || self.hyperlink_mouse_over.is_some();
789 if !has_modelled_children && self.raw_children.is_empty() {
790 return write_empty(writer, start);
791 }
792 write_start(writer, start)?;
793 emit_raw(writer, self.raw_children.at(0))?;
794 emit_raw(writer, self.raw_children.at(1))?;
795 if let Some(fill) = &self.fill {
796 fill.write_xml(writer)?;
797 }
798 for boundary in 2..=6 {
799 emit_raw(writer, self.raw_children.at(boundary))?;
800 }
801 if let Some(font) = &self.latin {
802 font.write_xml(writer, "a:latin")?;
803 }
804 emit_raw(writer, self.raw_children.at(7))?;
805 if let Some(font) = &self.east_asian {
806 font.write_xml(writer, "a:ea")?;
807 }
808 emit_raw(writer, self.raw_children.at(8))?;
809 if let Some(font) = &self.complex_script {
810 font.write_xml(writer, "a:cs")?;
811 }
812 emit_raw(writer, self.raw_children.at(9))?;
813 if let Some(font) = &self.symbol {
814 font.write_xml(writer, "a:sym")?;
815 }
816 emit_raw(writer, self.raw_children.at(10))?;
817 if let Some(hyperlink) = &self.hyperlink_click {
818 hyperlink.write_xml(writer, "a:hlinkClick")?;
819 }
820 emit_raw(writer, self.raw_children.at(11))?;
821 if let Some(hyperlink) = &self.hyperlink_mouse_over {
822 hyperlink.write_xml(writer, "a:hlinkMouseOver")?;
823 }
824 for boundary in 12..=14 {
825 emit_raw(writer, self.raw_children.at(boundary))?;
826 }
827 write_end(writer, tag)
828 }
829
830 pub fn raw_children(&self) -> &OrderedRawChildren {
831 &self.raw_children
832 }
833
834 pub(crate) fn without_hyperlinks(mut self) -> Self {
837 self.hyperlink_click = None;
838 self.hyperlink_mouse_over = None;
839 self
840 }
841}
842
843fn character_property_slot(name: &[u8]) -> Option<usize> {
844 match name {
845 b"ln" => Some(1),
846 name if is_fill(name) => Some(2),
847 b"effectLst" | b"effectDag" => Some(3),
848 b"highlight" => Some(4),
849 b"uLnTx" | b"uLn" => Some(5),
850 b"uFillTx" | b"uFill" => Some(6),
851 b"latin" => Some(7),
852 b"ea" => Some(8),
853 b"cs" => Some(9),
854 b"sym" => Some(10),
855 b"hlinkClick" => Some(11),
856 b"hlinkMouseOver" => Some(12),
857 b"rtl" => Some(13),
858 b"extLst" => Some(14),
859 _ => None,
860 }
861}
862
863fn is_character_property_tag(name: &[u8]) -> bool {
864 matches!(name, b"rPr" | b"defRPr" | b"endParaRPr")
865}
866
867fn is_fill(name: &[u8]) -> bool {
868 matches!(
869 name,
870 b"noFill" | b"solidFill" | b"gradFill" | b"pattFill" | b"blipFill"
871 )
872}
873
874#[allow(non_camel_case_types)]
876#[derive(Clone, Debug, Default, Eq, PartialEq)]
877pub struct CT_TextParagraphProperties {
878 pub left_margin: Option<i32>,
879 pub right_margin: Option<i32>,
880 pub level: Option<u8>,
881 pub indent: Option<i32>,
882 pub alignment: Option<TextAlignment>,
883 pub right_to_left: Option<bool>,
884 pub line_spacing: Option<TextSpacing>,
885 pub space_before: Option<TextSpacing>,
886 pub space_after: Option<TextSpacing>,
887 pub bullet: Option<TextBullet>,
888 pub default_run_properties: Option<CT_TextCharacterProperties>,
889 raw_attributes: Vec<(String, String)>,
890 raw_children: OrderedRawChildren,
891}
892
893impl CT_TextParagraphProperties {
894 pub fn from_xml(xml: &[u8]) -> Result<Self> {
896 let expected = root_local_name(xml)?;
897 if !is_paragraph_property_tag(&expected) {
898 return Err(TextError::UnexpectedElement(
899 String::from_utf8_lossy(&expected).into_owned(),
900 ));
901 }
902 parse_complete(xml, &expected, Self::from_element, Self::from_start)
903 }
904
905 fn from_start(start: &BytesStart<'_>) -> Result<Self> {
906 reject_conflicting_a_prefix(start)?;
907 let element = String::from_utf8_lossy(local_name(start.name().as_ref())).into_owned();
908 let level = text_attr(start, b"lvl")?
909 .map(|value| parse_i32_value(&element, "lvl", &value, 0, 8).map(|v| v as u8))
910 .transpose()?;
911 Ok(Self {
912 left_margin: parse_optional_i32(start, b"marL", 0, MAX_TEXT_MARGIN)?,
913 right_margin: parse_optional_i32(start, b"marR", 0, MAX_TEXT_MARGIN)?,
914 level,
915 indent: parse_optional_i32(start, b"indent", -MAX_TEXT_MARGIN, MAX_TEXT_MARGIN)?,
916 alignment: parse_optional_enum(start, b"algn", TextAlignment::parse)?,
917 right_to_left: parse_optional_bool(start, b"rtl")?,
918 raw_attributes: capture_raw_attributes(
919 start,
920 &[b"marL", b"marR", b"lvl", b"indent", b"algn"],
921 )?,
922 ..Self::default()
923 })
924 }
925
926 fn from_element(reader: &mut Reader<&[u8]>, start: &BytesStart<'_>) -> Result<Self> {
927 let expected = local_name(start.name().as_ref()).to_vec();
928 let mut properties = Self::from_start(start)?;
929 let mut boundary = 0;
930 let mut seen = [false; 10];
931 let mut buffer = Vec::new();
932 loop {
933 match reader
934 .read_event_into(&mut buffer)
935 .map_err(OxmlError::from)?
936 {
937 Event::Start(element) => {
938 let name = local_name(element.name().as_ref()).to_vec();
939 let raw = capture_element(reader, &element)?;
940 properties.capture_child(&name, raw, &mut boundary, &mut seen)?;
941 }
942 Event::Empty(element) => {
943 let name = local_name(element.name().as_ref()).to_vec();
944 let raw = capture_empty_element(&element)?;
945 properties.capture_child(&name, raw, &mut boundary, &mut seen)?;
946 }
947 Event::End(element) if matches_local_name(element.name().as_ref(), &expected) => {
948 return Ok(properties);
949 }
950 Event::Eof => return Err(missing_end(&String::from_utf8_lossy(&expected))),
951 _ => {}
952 }
953 buffer.clear();
954 }
955 }
956
957 fn capture_child(
958 &mut self,
959 name: &[u8],
960 raw: Vec<u8>,
961 boundary: &mut usize,
962 seen: &mut [bool; 10],
963 ) -> Result<()> {
964 let slot = paragraph_property_slot(name);
965 if let Some(slot) = slot {
966 if seen[slot - 1] {
967 return Err(duplicate(&String::from_utf8_lossy(name)));
968 }
969 seen[slot - 1] = true;
970 }
971 match name {
972 b"lnSpc" => self.line_spacing = Some(TextSpacing::from_xml(&raw, b"lnSpc")?),
973 b"spcBef" => self.space_before = Some(TextSpacing::from_xml(&raw, b"spcBef")?),
974 b"spcAft" => self.space_after = Some(TextSpacing::from_xml(&raw, b"spcAft")?),
975 b"defRPr" => {
976 self.default_run_properties = Some(CT_TextCharacterProperties::from_xml(&raw)?)
977 }
978 _ => {
979 let mut bullet = self.bullet.take().unwrap_or_default();
980 if bullet.capture_component(name, &raw)? {
981 self.bullet = Some(bullet);
982 } else {
983 if !bullet.is_empty() {
984 self.bullet = Some(bullet);
985 }
986 self.raw_children.push(
987 slot.map_or(*boundary, |slot| (*boundary).max(slot - 1)),
988 raw,
989 );
990 }
991 }
992 }
993 if let Some(slot) = slot {
994 *boundary = (*boundary).max(slot);
995 }
996 Ok(())
997 }
998
999 pub fn write_xml<W: Write>(&self, writer: &mut Writer<W>, tag: &str) -> Result<()> {
1001 let mut start = BytesStart::new(tag);
1002 if let Some(value) = self.left_margin {
1003 parse_i32_value(tag, "marL", &value.to_string(), 0, MAX_TEXT_MARGIN)?;
1004 push_owned_attr(&mut start, "marL", value.to_string());
1005 }
1006 if let Some(value) = self.right_margin {
1007 parse_i32_value(tag, "marR", &value.to_string(), 0, MAX_TEXT_MARGIN)?;
1008 push_owned_attr(&mut start, "marR", value.to_string());
1009 }
1010 if let Some(value) = self.level {
1011 if value > 8 {
1012 return Err(invalid_attribute(tag, "lvl", &value.to_string()));
1013 }
1014 push_owned_attr(&mut start, "lvl", value.to_string());
1015 }
1016 if let Some(value) = self.indent {
1017 parse_i32_value(
1018 tag,
1019 "indent",
1020 &value.to_string(),
1021 -MAX_TEXT_MARGIN,
1022 MAX_TEXT_MARGIN,
1023 )?;
1024 push_owned_attr(&mut start, "indent", value.to_string());
1025 }
1026 if let Some(value) = self.alignment {
1027 start.push_attribute(("algn", value.as_str()));
1028 }
1029 let mut wrote_right_to_left = false;
1030 for (name, value) in &self.raw_attributes {
1031 if name == "rtl" {
1032 if let Some(value) = self.right_to_left {
1033 start.push_attribute(("rtl", if value { "1" } else { "0" }));
1034 wrote_right_to_left = true;
1035 }
1036 } else {
1037 start.push_attribute((name.as_str(), value.as_str()));
1038 }
1039 }
1040 if !wrote_right_to_left {
1041 push_optional_bool(&mut start, "rtl", self.right_to_left);
1042 }
1043
1044 let modelled = self.line_spacing.is_some()
1045 || self.space_before.is_some()
1046 || self.space_after.is_some()
1047 || self.bullet.is_some()
1048 || self.default_run_properties.is_some();
1049 if !modelled && self.raw_children.is_empty() {
1050 return write_empty(writer, start);
1051 }
1052 write_start(writer, start)?;
1053 emit_raw(writer, self.raw_children.at(0))?;
1054 if let Some(spacing) = &self.line_spacing {
1055 spacing.write_xml(writer, "a:lnSpc")?;
1056 }
1057 emit_raw(writer, self.raw_children.at(1))?;
1058 if let Some(spacing) = &self.space_before {
1059 spacing.write_xml(writer, "a:spcBef")?;
1060 }
1061 emit_raw(writer, self.raw_children.at(2))?;
1062 if let Some(spacing) = &self.space_after {
1063 spacing.write_xml(writer, "a:spcAft")?;
1064 }
1065 emit_raw(writer, self.raw_children.at(3))?;
1066 if let Some(bullet) = &self.bullet {
1067 bullet.write_color(writer)?;
1068 }
1069 emit_raw(writer, self.raw_children.at(4))?;
1070 if let Some(bullet) = &self.bullet {
1071 bullet.write_size(writer)?;
1072 }
1073 emit_raw(writer, self.raw_children.at(5))?;
1074 if let Some(bullet) = &self.bullet {
1075 bullet.write_font(writer)?;
1076 }
1077 emit_raw(writer, self.raw_children.at(6))?;
1078 if let Some(bullet) = &self.bullet {
1079 bullet.write_choice(writer)?;
1080 }
1081 emit_raw(writer, self.raw_children.at(7))?;
1082 emit_raw(writer, self.raw_children.at(8))?;
1083 if let Some(properties) = &self.default_run_properties {
1084 properties.write_xml(writer, "a:defRPr")?;
1085 }
1086 emit_raw(writer, self.raw_children.at(9))?;
1087 emit_raw(writer, self.raw_children.at(10))?;
1088 write_end(writer, tag)
1089 }
1090
1091 pub fn raw_children(&self) -> &OrderedRawChildren {
1092 &self.raw_children
1093 }
1094
1095 pub fn has_picture_bullet(&self) -> bool {
1097 (0..=10).any(|boundary| {
1098 self.raw_children
1099 .at(boundary)
1100 .any(|raw| raw_local_name(raw) == b"buBlip")
1101 })
1102 }
1103
1104 pub fn set_bullet(&mut self, bullet: Option<TextBullet>) {
1110 let replaces = |name: &[u8]| {
1111 let Some(bullet) = bullet.as_ref() else {
1112 return matches!(name, b"buClrTx" | b"buSzTx" | b"buFontTx" | b"buBlip");
1113 };
1114 match name {
1115 b"buClrTx" => bullet.color.is_some(),
1116 b"buSzTx" => bullet.size.is_some(),
1117 b"buFontTx" => bullet.font.is_some(),
1118 b"buBlip" => bullet.choice.is_some(),
1119 _ => false,
1120 }
1121 };
1122 self.raw_children
1123 .retain(|raw| !replaces(raw_local_name(raw)));
1124 self.bullet = bullet;
1125 }
1126
1127 pub fn to_xml(&self) -> Result<Vec<u8>> {
1129 let mut writer = Writer::new(Vec::new());
1130 self.write_xml(&mut writer, "a:pPr")?;
1131 Ok(writer.into_inner())
1132 }
1133}
1134
1135fn paragraph_property_slot(name: &[u8]) -> Option<usize> {
1136 match name {
1137 b"lnSpc" => Some(1),
1138 b"spcBef" => Some(2),
1139 b"spcAft" => Some(3),
1140 b"buClrTx" | b"buClr" => Some(4),
1141 b"buSzTx" | b"buSzPct" | b"buSzPts" => Some(5),
1142 b"buFontTx" | b"buFont" => Some(6),
1143 b"buNone" | b"buAutoNum" | b"buChar" | b"buBlip" => Some(7),
1144 b"tabLst" => Some(8),
1145 b"defRPr" => Some(9),
1146 b"extLst" => Some(10),
1147 _ => None,
1148 }
1149}
1150
1151fn raw_local_name(raw: &[u8]) -> &[u8] {
1153 let tag = raw.strip_prefix(b"<").unwrap_or(raw);
1154 let end = tag
1155 .iter()
1156 .position(|byte| byte.is_ascii_whitespace() || matches!(byte, b'/' | b'>'))
1157 .unwrap_or(tag.len());
1158 local_name(&tag[..end])
1159}
1160
1161fn is_paragraph_property_tag(name: &[u8]) -> bool {
1162 matches!(
1163 name,
1164 b"pPr"
1165 | b"defPPr"
1166 | b"lvl1pPr"
1167 | b"lvl2pPr"
1168 | b"lvl3pPr"
1169 | b"lvl4pPr"
1170 | b"lvl5pPr"
1171 | b"lvl6pPr"
1172 | b"lvl7pPr"
1173 | b"lvl8pPr"
1174 | b"lvl9pPr"
1175 )
1176}
1177
1178#[allow(non_camel_case_types)]
1180#[derive(Clone, Debug, Eq, PartialEq)]
1181pub struct CT_RegularTextRun {
1182 pub properties: Option<CT_TextCharacterProperties>,
1183 pub text: TextValue,
1184 raw_children: OrderedRawChildren,
1185}
1186
1187impl CT_RegularTextRun {
1188 pub fn new(text: impl Into<String>) -> Self {
1190 Self {
1191 properties: None,
1192 text: TextValue {
1193 value: escape_invalid_xml_characters(&text.into()).into_owned(),
1194 ..TextValue::default()
1195 },
1196 raw_children: OrderedRawChildren::default(),
1197 }
1198 }
1199
1200 pub fn set_text(&mut self, text: &str) {
1205 self.text.value = escape_invalid_xml_characters(text).into_owned();
1206 }
1207
1208 pub fn from_xml(xml: &[u8]) -> Result<Self> {
1210 parse_complete(xml, b"r", Self::from_element, |_| {
1211 Err(TextError::UnexpectedElement("r".to_owned()))
1212 })
1213 }
1214
1215 fn from_element(reader: &mut Reader<&[u8]>, _start: &BytesStart<'_>) -> Result<Self> {
1216 let mut properties = None;
1217 let mut text = None;
1218 let mut raw_children = OrderedRawChildren::default();
1219 let mut boundary = 0;
1220 let mut buffer = Vec::new();
1221 loop {
1222 match reader
1223 .read_event_into(&mut buffer)
1224 .map_err(OxmlError::from)?
1225 {
1226 Event::Start(element) if matches_local_name(element.name().as_ref(), b"rPr") => {
1227 if properties.is_some() {
1228 return Err(duplicate("rPr"));
1229 }
1230 properties = Some(CT_TextCharacterProperties::from_element(reader, &element)?);
1231 boundary = boundary.max(1);
1232 }
1233 Event::Empty(element) if matches_local_name(element.name().as_ref(), b"rPr") => {
1234 if properties.is_some() {
1235 return Err(duplicate("rPr"));
1236 }
1237 properties = Some(CT_TextCharacterProperties::from_start(&element)?);
1238 boundary = boundary.max(1);
1239 }
1240 Event::Start(element) if matches_local_name(element.name().as_ref(), b"t") => {
1241 if text.is_some() {
1242 return Err(duplicate("t"));
1243 }
1244 text = Some(TextValue::from_element(reader, &element)?);
1245 boundary = boundary.max(2);
1246 }
1247 Event::Empty(element) if matches_local_name(element.name().as_ref(), b"t") => {
1248 if text.is_some() {
1249 return Err(duplicate("t"));
1250 }
1251 text = Some(TextValue::from_start(&element)?);
1252 boundary = boundary.max(2);
1253 }
1254 Event::Start(element) => {
1255 raw_children.push(boundary, capture_element(reader, &element)?)
1256 }
1257 Event::Empty(element) => {
1258 raw_children.push(boundary, capture_empty_element(&element)?)
1259 }
1260 Event::End(element) if matches_local_name(element.name().as_ref(), b"r") => break,
1261 Event::Eof => return Err(missing_end("r")),
1262 _ => {}
1263 }
1264 buffer.clear();
1265 }
1266 Ok(Self {
1267 properties,
1268 text: text.ok_or_else(|| {
1269 TextError::Xml(OxmlError::MissingElement("DrawingML run text".to_owned()))
1270 })?,
1271 raw_children,
1272 })
1273 }
1274
1275 fn write_xml<W: Write>(&self, writer: &mut Writer<W>) -> Result<()> {
1276 write_start(writer, BytesStart::new("a:r"))?;
1277 emit_raw(writer, self.raw_children.at(0))?;
1278 if let Some(properties) = &self.properties {
1279 properties.write_xml(writer, "a:rPr")?;
1280 }
1281 emit_raw(writer, self.raw_children.at(1))?;
1282 self.text.write_xml(writer)?;
1283 emit_raw(writer, self.raw_children.at(2))?;
1284 write_end(writer, "a:r")
1285 }
1286}
1287
1288#[allow(non_camel_case_types)]
1290#[derive(Clone, Debug, Default, Eq, PartialEq)]
1291pub struct CT_TextLineBreak {
1292 pub properties: Option<CT_TextCharacterProperties>,
1293 raw_children: OrderedRawChildren,
1294}
1295
1296impl CT_TextLineBreak {
1297 pub fn from_xml(xml: &[u8]) -> Result<Self> {
1299 parse_complete(xml, b"br", Self::from_element, |_| Ok(Self::default()))
1300 }
1301
1302 fn from_element(reader: &mut Reader<&[u8]>, _start: &BytesStart<'_>) -> Result<Self> {
1303 let mut line_break = Self::default();
1304 let mut boundary = 0;
1305 let mut buffer = Vec::new();
1306 loop {
1307 match reader
1308 .read_event_into(&mut buffer)
1309 .map_err(OxmlError::from)?
1310 {
1311 Event::Start(element) if matches_local_name(element.name().as_ref(), b"rPr") => {
1312 if line_break.properties.is_some() {
1313 return Err(duplicate("rPr"));
1314 }
1315 line_break.properties =
1316 Some(CT_TextCharacterProperties::from_element(reader, &element)?);
1317 boundary = 1;
1318 }
1319 Event::Empty(element) if matches_local_name(element.name().as_ref(), b"rPr") => {
1320 if line_break.properties.is_some() {
1321 return Err(duplicate("rPr"));
1322 }
1323 line_break.properties = Some(CT_TextCharacterProperties::from_start(&element)?);
1324 boundary = 1;
1325 }
1326 Event::Start(element) => line_break
1327 .raw_children
1328 .push(boundary, capture_element(reader, &element)?),
1329 Event::Empty(element) => line_break
1330 .raw_children
1331 .push(boundary, capture_empty_element(&element)?),
1332 Event::End(element) if matches_local_name(element.name().as_ref(), b"br") => {
1333 return Ok(line_break);
1334 }
1335 Event::Eof => return Err(missing_end("br")),
1336 _ => {}
1337 }
1338 buffer.clear();
1339 }
1340 }
1341
1342 fn write_xml<W: Write>(&self, writer: &mut Writer<W>) -> Result<()> {
1343 if self.properties.is_none() && self.raw_children.is_empty() {
1344 return write_empty(writer, BytesStart::new("a:br"));
1345 }
1346 write_start(writer, BytesStart::new("a:br"))?;
1347 emit_raw(writer, self.raw_children.at(0))?;
1348 if let Some(properties) = &self.properties {
1349 properties.write_xml(writer, "a:rPr")?;
1350 }
1351 emit_raw(writer, self.raw_children.at(1))?;
1352 write_end(writer, "a:br")
1353 }
1354}
1355
1356#[allow(non_camel_case_types)]
1358#[derive(Clone, Debug, Eq, PartialEq)]
1359pub struct CT_TextField {
1360 pub id: String,
1361 pub field_type: Option<String>,
1362 pub run_properties: Option<CT_TextCharacterProperties>,
1363 pub paragraph_properties: Option<CT_TextParagraphProperties>,
1364 pub text: Option<TextValue>,
1365 raw_children: OrderedRawChildren,
1366}
1367
1368impl CT_TextField {
1369 pub fn from_xml(xml: &[u8]) -> Result<Self> {
1371 parse_complete(xml, b"fld", Self::from_element, Self::from_empty)
1372 }
1373
1374 fn from_start(start: &BytesStart<'_>) -> Result<Self> {
1375 let id = required_attr(start, b"id")?;
1376 if !is_guid(&id) {
1377 return Err(invalid_attribute("fld", "id", &id));
1378 }
1379 Ok(Self {
1380 id,
1381 field_type: text_attr(start, b"type")?,
1382 run_properties: None,
1383 paragraph_properties: None,
1384 text: None,
1385 raw_children: OrderedRawChildren::default(),
1386 })
1387 }
1388
1389 fn from_empty(start: &BytesStart<'_>) -> Result<Self> {
1390 Self::from_start(start)
1391 }
1392
1393 fn from_element(reader: &mut Reader<&[u8]>, start: &BytesStart<'_>) -> Result<Self> {
1394 let mut field = Self::from_start(start)?;
1395 let mut boundary = 0;
1396 let mut buffer = Vec::new();
1397 loop {
1398 match reader
1399 .read_event_into(&mut buffer)
1400 .map_err(OxmlError::from)?
1401 {
1402 Event::Start(element) if matches_local_name(element.name().as_ref(), b"rPr") => {
1403 if field.run_properties.is_some() {
1404 return Err(duplicate("rPr"));
1405 }
1406 field.run_properties =
1407 Some(CT_TextCharacterProperties::from_element(reader, &element)?);
1408 boundary = boundary.max(1);
1409 }
1410 Event::Empty(element) if matches_local_name(element.name().as_ref(), b"rPr") => {
1411 if field.run_properties.is_some() {
1412 return Err(duplicate("rPr"));
1413 }
1414 field.run_properties = Some(CT_TextCharacterProperties::from_start(&element)?);
1415 boundary = boundary.max(1);
1416 }
1417 Event::Start(element) if matches_local_name(element.name().as_ref(), b"pPr") => {
1418 if field.paragraph_properties.is_some() {
1419 return Err(duplicate("pPr"));
1420 }
1421 field.paragraph_properties =
1422 Some(CT_TextParagraphProperties::from_element(reader, &element)?);
1423 boundary = boundary.max(2);
1424 }
1425 Event::Empty(element) if matches_local_name(element.name().as_ref(), b"pPr") => {
1426 if field.paragraph_properties.is_some() {
1427 return Err(duplicate("pPr"));
1428 }
1429 field.paragraph_properties =
1430 Some(CT_TextParagraphProperties::from_start(&element)?);
1431 boundary = boundary.max(2);
1432 }
1433 Event::Start(element) if matches_local_name(element.name().as_ref(), b"t") => {
1434 if field.text.is_some() {
1435 return Err(duplicate("t"));
1436 }
1437 field.text = Some(TextValue::from_element(reader, &element)?);
1438 boundary = boundary.max(3);
1439 }
1440 Event::Empty(element) if matches_local_name(element.name().as_ref(), b"t") => {
1441 if field.text.is_some() {
1442 return Err(duplicate("t"));
1443 }
1444 field.text = Some(TextValue::from_start(&element)?);
1445 boundary = boundary.max(3);
1446 }
1447 Event::Start(element) => field
1448 .raw_children
1449 .push(boundary, capture_element(reader, &element)?),
1450 Event::Empty(element) => field
1451 .raw_children
1452 .push(boundary, capture_empty_element(&element)?),
1453 Event::End(element) if matches_local_name(element.name().as_ref(), b"fld") => {
1454 return Ok(field);
1455 }
1456 Event::Eof => return Err(missing_end("fld")),
1457 _ => {}
1458 }
1459 buffer.clear();
1460 }
1461 }
1462
1463 fn write_xml<W: Write>(&self, writer: &mut Writer<W>) -> Result<()> {
1464 if !is_guid(&self.id) {
1465 return Err(invalid_attribute("fld", "id", &self.id));
1466 }
1467 let mut start = BytesStart::new("a:fld");
1468 start.push_attribute(("id", self.id.as_str()));
1469 push_optional_attr(&mut start, "type", self.field_type.as_deref());
1470 let modelled = self.run_properties.is_some()
1471 || self.paragraph_properties.is_some()
1472 || self.text.is_some();
1473 if !modelled && self.raw_children.is_empty() {
1474 return write_empty(writer, start);
1475 }
1476 write_start(writer, start)?;
1477 emit_raw(writer, self.raw_children.at(0))?;
1478 if let Some(properties) = &self.run_properties {
1479 properties.write_xml(writer, "a:rPr")?;
1480 }
1481 emit_raw(writer, self.raw_children.at(1))?;
1482 if let Some(properties) = &self.paragraph_properties {
1483 properties.write_xml(writer, "a:pPr")?;
1484 }
1485 emit_raw(writer, self.raw_children.at(2))?;
1486 if let Some(text) = &self.text {
1487 text.write_xml(writer)?;
1488 }
1489 emit_raw(writer, self.raw_children.at(3))?;
1490 write_end(writer, "a:fld")
1491 }
1492}
1493
1494#[derive(Clone, Debug, Eq, PartialEq)]
1496pub enum TextRun {
1497 Run(CT_RegularTextRun),
1498 Break(CT_TextLineBreak),
1499 Field(Box<CT_TextField>),
1500}
1501
1502impl TextRun {
1503 fn from_xml(xml: &[u8], name: &[u8]) -> Result<Self> {
1504 match name {
1505 b"r" => Ok(Self::Run(CT_RegularTextRun::from_xml(xml)?)),
1506 b"br" => Ok(Self::Break(CT_TextLineBreak::from_xml(xml)?)),
1507 b"fld" => Ok(Self::Field(Box::new(CT_TextField::from_xml(xml)?))),
1508 _ => Err(TextError::UnexpectedElement(
1509 String::from_utf8_lossy(name).into_owned(),
1510 )),
1511 }
1512 }
1513
1514 fn write_xml<W: Write>(&self, writer: &mut Writer<W>) -> Result<()> {
1515 match self {
1516 Self::Run(run) => run.write_xml(writer),
1517 Self::Break(line_break) => line_break.write_xml(writer),
1518 Self::Field(field) => field.write_xml(writer),
1519 }
1520 }
1521}
1522
1523#[allow(non_camel_case_types)]
1525#[derive(Clone, Debug, Default, Eq, PartialEq)]
1526pub struct CT_TextParagraph {
1527 pub properties: Option<CT_TextParagraphProperties>,
1528 pub runs: Vec<TextRun>,
1529 pub end_properties: Option<CT_TextCharacterProperties>,
1530 raw_children: OrderedRawChildren,
1531}
1532
1533impl CT_TextParagraph {
1534 pub fn properties_mut(&mut self) -> &mut CT_TextParagraphProperties {
1536 if self.properties.is_none() {
1537 let mut raw_children = OrderedRawChildren::default();
1538 for boundary in 0..=2 + self.runs.len() {
1539 let new_boundary = if boundary == 0 { 1 } else { boundary };
1540 for child in self.raw_children.at(boundary) {
1541 raw_children.push(new_boundary, child.to_vec());
1542 }
1543 }
1544 self.raw_children = raw_children;
1545 self.properties = Some(CT_TextParagraphProperties::default());
1546 }
1547 self.properties
1548 .as_mut()
1549 .expect("paragraph properties were inserted")
1550 }
1551
1552 pub fn set_text(&mut self, text: &str) {
1562 let old_run_count = self.runs.len();
1563 let retained_run = self.runs.iter().find_map(|run| match run {
1564 TextRun::Run(run) => Some(run.clone()),
1565 TextRun::Break(_) | TextRun::Field(_) => None,
1566 });
1567 let mut raw_children = OrderedRawChildren::default();
1568 for boundary in 0..=2 + old_run_count {
1569 let new_boundary = if boundary <= 1 {
1570 boundary
1571 } else if boundary <= 1 + old_run_count {
1572 2
1573 } else {
1574 3
1575 };
1576 for child in self.raw_children.at(boundary) {
1577 raw_children.push(new_boundary, child.to_vec());
1578 }
1579 }
1580 self.raw_children = raw_children;
1581 let mut run = retained_run.unwrap_or_else(|| CT_RegularTextRun {
1582 properties: self
1583 .end_properties
1584 .clone()
1585 .map(CT_TextCharacterProperties::without_hyperlinks),
1586 ..CT_RegularTextRun::new(text)
1587 });
1588 run.set_text(text);
1589 self.runs = vec![TextRun::Run(run)];
1590 self.split_line_breaks(text);
1591 }
1592
1593 fn split_line_breaks(&mut self, text: &str) {
1597 let [TextRun::Run(first)] = self.runs.as_mut_slice() else {
1598 return;
1599 };
1600 if !text.contains('\u{b}') {
1601 return;
1602 }
1603 let mut lines = text.split('\u{b}');
1604 first.set_text(lines.next().unwrap_or_default());
1605 let properties = first.properties.clone();
1606 for line in lines {
1607 self.raw_children.shift_boundaries_from(2);
1608 self.runs.push(TextRun::Break(CT_TextLineBreak {
1609 properties: properties.clone(),
1610 ..CT_TextLineBreak::default()
1611 }));
1612 if !line.is_empty() {
1613 self.raw_children.shift_boundaries_from(2);
1614 self.runs.push(TextRun::Run(CT_RegularTextRun {
1615 properties: properties.clone(),
1616 ..CT_RegularTextRun::new(line)
1617 }));
1618 }
1619 }
1620 }
1621
1622 pub fn add_run(&mut self, text: &str) -> &mut CT_RegularTextRun {
1624 self.raw_children.shift_boundaries_from(2 + self.runs.len());
1625 self.runs.push(TextRun::Run(CT_RegularTextRun::new(text)));
1626 let Some(TextRun::Run(run)) = self.runs.last_mut() else {
1627 unreachable!("the appended text choice is a regular run")
1628 };
1629 run
1630 }
1631
1632 pub fn from_xml(xml: &[u8]) -> Result<Self> {
1634 parse_complete(xml, b"p", Self::from_element, |_| Ok(Self::default()))
1635 }
1636
1637 fn from_element(reader: &mut Reader<&[u8]>, _start: &BytesStart<'_>) -> Result<Self> {
1638 let mut paragraph = Self::default();
1639 let mut boundary = 0;
1640 let mut buffer = Vec::new();
1641 loop {
1642 match reader
1643 .read_event_into(&mut buffer)
1644 .map_err(OxmlError::from)?
1645 {
1646 Event::Start(element)
1651 if matches_local_name(element.name().as_ref(), b"pPr")
1652 && paragraph.properties.is_none() =>
1653 {
1654 paragraph.properties =
1655 Some(CT_TextParagraphProperties::from_element(reader, &element)?);
1656 boundary = boundary.max(1);
1657 }
1658 Event::Empty(element)
1659 if matches_local_name(element.name().as_ref(), b"pPr")
1660 && paragraph.properties.is_none() =>
1661 {
1662 paragraph.properties = Some(CT_TextParagraphProperties::from_start(&element)?);
1663 boundary = boundary.max(1);
1664 }
1665 Event::Start(element) if is_text_run(element.name().as_ref()) => {
1666 let name = local_name(element.name().as_ref()).to_vec();
1667 let raw = capture_element(reader, &element)?;
1668 paragraph.runs.push(TextRun::from_xml(&raw, &name)?);
1669 boundary = boundary.max(1 + paragraph.runs.len());
1670 }
1671 Event::Empty(element) if is_text_run(element.name().as_ref()) => {
1672 let name = local_name(element.name().as_ref()).to_vec();
1673 let raw = capture_empty_element(&element)?;
1674 paragraph.runs.push(TextRun::from_xml(&raw, &name)?);
1675 boundary = boundary.max(1 + paragraph.runs.len());
1676 }
1677 Event::Start(element)
1678 if matches_local_name(element.name().as_ref(), b"endParaRPr") =>
1679 {
1680 if paragraph.end_properties.is_some() {
1681 return Err(duplicate("endParaRPr"));
1682 }
1683 paragraph.end_properties =
1684 Some(CT_TextCharacterProperties::from_element(reader, &element)?);
1685 boundary = boundary.max(2 + paragraph.runs.len());
1686 }
1687 Event::Empty(element)
1688 if matches_local_name(element.name().as_ref(), b"endParaRPr") =>
1689 {
1690 if paragraph.end_properties.is_some() {
1691 return Err(duplicate("endParaRPr"));
1692 }
1693 paragraph.end_properties =
1694 Some(CT_TextCharacterProperties::from_start(&element)?);
1695 boundary = boundary.max(2 + paragraph.runs.len());
1696 }
1697 Event::Start(element) => paragraph
1698 .raw_children
1699 .push(boundary, capture_element(reader, &element)?),
1700 Event::Empty(element) => paragraph
1701 .raw_children
1702 .push(boundary, capture_empty_element(&element)?),
1703 Event::End(element) if matches_local_name(element.name().as_ref(), b"p") => {
1704 return Ok(paragraph);
1705 }
1706 Event::Eof => return Err(missing_end("p")),
1707 _ => {}
1708 }
1709 buffer.clear();
1710 }
1711 }
1712
1713 pub fn write_xml<W: Write>(&self, writer: &mut Writer<W>) -> Result<()> {
1715 if self.properties.is_none()
1716 && self.runs.is_empty()
1717 && self.end_properties.is_none()
1718 && self.raw_children.is_empty()
1719 {
1720 return write_empty(writer, BytesStart::new("a:p"));
1721 }
1722 write_start(writer, BytesStart::new("a:p"))?;
1723 emit_raw(writer, self.raw_children.at(0))?;
1724 if let Some(properties) = &self.properties {
1725 properties.write_xml(writer, "a:pPr")?;
1726 }
1727 emit_raw(writer, self.raw_children.at(1))?;
1728 for (index, run) in self.runs.iter().enumerate() {
1729 run.write_xml(writer)?;
1730 emit_raw(writer, self.raw_children.at(2 + index))?;
1731 }
1732 if let Some(properties) = &self.end_properties {
1733 properties.write_xml(writer, "a:endParaRPr")?;
1734 }
1735 emit_raw(writer, self.raw_children.at(2 + self.runs.len()))?;
1736 write_end(writer, "a:p")
1737 }
1738
1739 pub fn to_xml(&self) -> Result<Vec<u8>> {
1741 let mut writer = Writer::new(Vec::new());
1742 self.write_xml(&mut writer)?;
1743 Ok(writer.into_inner())
1744 }
1745
1746 pub fn raw_children(&self) -> &OrderedRawChildren {
1747 &self.raw_children
1748 }
1749}
1750
1751fn is_text_run(name: &[u8]) -> bool {
1752 matches!(local_name(name), b"r" | b"br" | b"fld")
1753}
1754
1755fn parse_complete<T>(
1756 xml: &[u8],
1757 expected: &[u8],
1758 parse_start: impl FnOnce(&mut Reader<&[u8]>, &BytesStart<'_>) -> Result<T>,
1759 parse_empty: impl FnOnce(&BytesStart<'_>) -> Result<T>,
1760) -> Result<T> {
1761 let mut reader = Reader::from_reader(xml);
1762 let mut buffer = Vec::new();
1763 loop {
1764 match reader
1765 .read_event_into(&mut buffer)
1766 .map_err(OxmlError::from)?
1767 {
1768 Event::Start(element) if matches_local_name(element.name().as_ref(), expected) => {
1769 reject_conflicting_a_prefix(&element)?;
1770 return parse_start(&mut reader, &element);
1771 }
1772 Event::Empty(element) if matches_local_name(element.name().as_ref(), expected) => {
1773 reject_conflicting_a_prefix(&element)?;
1774 return parse_empty(&element);
1775 }
1776 Event::Start(element) | Event::Empty(element) => return Err(unexpected(&element)),
1777 Event::Eof => {
1778 return Err(TextError::Xml(OxmlError::MissingElement(
1779 String::from_utf8_lossy(expected).into_owned(),
1780 )));
1781 }
1782 _ => {}
1783 }
1784 buffer.clear();
1785 }
1786}
1787
1788fn root_local_name(xml: &[u8]) -> Result<Vec<u8>> {
1789 let mut reader = Reader::from_reader(xml);
1790 let mut buffer = Vec::new();
1791 loop {
1792 match reader
1793 .read_event_into(&mut buffer)
1794 .map_err(OxmlError::from)?
1795 {
1796 Event::Start(element) | Event::Empty(element) => {
1797 return Ok(local_name(element.name().as_ref()).to_vec());
1798 }
1799 Event::Eof => {
1800 return Err(TextError::Xml(OxmlError::MissingElement(
1801 "DrawingML text properties".to_owned(),
1802 )));
1803 }
1804 _ => {}
1805 }
1806 buffer.clear();
1807 }
1808}
1809
1810fn ensure_empty(reader: &mut Reader<&[u8]>, expected: &[u8]) -> Result<()> {
1811 let mut buffer = Vec::new();
1812 loop {
1813 match reader
1814 .read_event_into(&mut buffer)
1815 .map_err(OxmlError::from)?
1816 {
1817 Event::End(element) if matches_local_name(element.name().as_ref(), expected) => {
1818 return Ok(());
1819 }
1820 Event::Text(text) if text.iter().all(u8::is_ascii_whitespace) => {}
1821 Event::Comment(_) => {}
1822 Event::Start(element) | Event::Empty(element) => return Err(unexpected(&element)),
1823 Event::Eof => return Err(missing_end(&String::from_utf8_lossy(expected))),
1824 _ => {
1825 return Err(TextError::UnexpectedElement(
1826 String::from_utf8_lossy(expected).into_owned(),
1827 ));
1828 }
1829 }
1830 buffer.clear();
1831 }
1832}
1833
1834fn text_attr(start: &BytesStart<'_>, name: &[u8]) -> Result<Option<String>> {
1835 for attribute in start.attributes() {
1836 let attribute = attribute.map_err(OxmlError::from)?;
1837 if attribute.key.as_ref() == name {
1838 let value = attribute
1839 .decoded_and_normalized_value(XmlVersion::Implicit1_0, start.decoder())
1840 .map_err(OxmlError::from)?;
1841 return Ok(Some(value.into_owned()));
1842 }
1843 }
1844 Ok(None)
1845}
1846
1847fn capture_raw_attributes(
1848 start: &BytesStart<'_>,
1849 modelled: &[&[u8]],
1850) -> Result<Vec<(String, String)>> {
1851 let mut raw = Vec::new();
1852 for attribute in start.attributes() {
1853 let attribute = attribute.map_err(OxmlError::from)?;
1854 if modelled.iter().any(|name| attribute.key.as_ref() == *name) {
1855 continue;
1856 }
1857 let name = std::str::from_utf8(attribute.key.as_ref())
1858 .map_err(OxmlError::from)?
1859 .to_owned();
1860 let value = attribute
1861 .decoded_and_normalized_value(XmlVersion::Implicit1_0, start.decoder())
1862 .map_err(OxmlError::from)?
1863 .into_owned();
1864 raw.push((name, value));
1865 }
1866 Ok(raw)
1867}
1868
1869fn push_raw_attributes(start: &mut BytesStart<'_>, attributes: &[(String, String)]) {
1870 for (name, value) in attributes {
1871 start.push_attribute((name.as_str(), value.as_str()));
1872 }
1873}
1874
1875fn required_attr(start: &BytesStart<'_>, name: &[u8]) -> Result<String> {
1876 text_attr(start, name)?
1877 .filter(|value| !value.is_empty())
1878 .ok_or_else(|| {
1879 missing_attribute(
1880 &String::from_utf8_lossy(local_name(start.name().as_ref())),
1881 &String::from_utf8_lossy(name),
1882 )
1883 })
1884}
1885
1886fn parse_optional_i32(
1887 start: &BytesStart<'_>,
1888 name: &[u8],
1889 min: i32,
1890 max: i32,
1891) -> Result<Option<i32>> {
1892 text_attr(start, name)?
1893 .map(|value| {
1894 parse_i32_value(
1895 &String::from_utf8_lossy(local_name(start.name().as_ref())),
1896 &String::from_utf8_lossy(name),
1897 &value,
1898 min,
1899 max,
1900 )
1901 })
1902 .transpose()
1903}
1904
1905fn parse_i32_attr(start: &BytesStart<'_>, name: &[u8], min: i32, max: i32) -> Result<i32> {
1906 let value = required_attr(start, name)?;
1907 parse_i32_value(
1908 &String::from_utf8_lossy(local_name(start.name().as_ref())),
1909 &String::from_utf8_lossy(name),
1910 &value,
1911 min,
1912 max,
1913 )
1914}
1915
1916fn parse_i32_value(element: &str, attribute: &str, value: &str, min: i32, max: i32) -> Result<i32> {
1917 let parsed = value
1918 .parse::<i32>()
1919 .map_err(|_| invalid_attribute(element, attribute, value))?;
1920 if (min..=max).contains(&parsed) {
1921 Ok(parsed)
1922 } else {
1923 Err(invalid_attribute(element, attribute, value))
1924 }
1925}
1926
1927fn parse_optional_bool(start: &BytesStart<'_>, name: &[u8]) -> Result<Option<bool>> {
1928 text_attr(start, name)?
1929 .map(|value| match value.as_str() {
1930 "true" | "1" => Ok(true),
1931 "false" | "0" => Ok(false),
1932 _ => Err(invalid_attribute(
1933 &String::from_utf8_lossy(local_name(start.name().as_ref())),
1934 &String::from_utf8_lossy(name),
1935 &value,
1936 )),
1937 })
1938 .transpose()
1939}
1940
1941fn parse_optional_enum<T>(
1942 start: &BytesStart<'_>,
1943 name: &[u8],
1944 parse: impl FnOnce(&str) -> Option<T>,
1945) -> Result<Option<T>> {
1946 let Some(value) = text_attr(start, name)? else {
1947 return Ok(None);
1948 };
1949 parse(&value).map(Some).ok_or_else(|| {
1950 invalid_attribute(
1951 &String::from_utf8_lossy(local_name(start.name().as_ref())),
1952 &String::from_utf8_lossy(name),
1953 &value,
1954 )
1955 })
1956}
1957
1958fn validate_spacing_percent(value: &str) -> Result<()> {
1959 if is_percentage_string(value) {
1960 return Ok(());
1961 }
1962 parse_i32_value("spcPct", "val", value, 0, MAX_TRANSITIONAL_SPACING_PERCENT)?;
1963 Ok(())
1964}
1965
1966fn validate_baseline(value: &str) -> Result<()> {
1967 if is_percentage_string(value) {
1968 return Ok(());
1969 }
1970 parse_i32_value("rPr", "baseline", value, -100_000, 100_000)?;
1971 Ok(())
1972}
1973
1974fn is_percentage_string(value: &str) -> bool {
1975 value.strip_suffix('%').is_some_and(is_signed_decimal)
1976}
1977
1978fn is_universal_measure(value: &str) -> bool {
1979 ["mm", "cm", "in", "pt", "pc", "pi"]
1980 .iter()
1981 .find_map(|unit| value.strip_suffix(unit))
1982 .is_some_and(is_signed_decimal)
1983}
1984
1985fn is_signed_decimal(value: &str) -> bool {
1986 let unsigned = value.strip_prefix('-').unwrap_or(value);
1987 let mut parts = unsigned.split('.');
1988 let Some(integer) = parts.next() else {
1989 return false;
1990 };
1991 if integer.is_empty() || !integer.bytes().all(|byte| byte.is_ascii_digit()) {
1992 return false;
1993 }
1994 if let Some(fraction) = parts.next()
1995 && (fraction.is_empty() || !fraction.bytes().all(|byte| byte.is_ascii_digit()))
1996 {
1997 return false;
1998 }
1999 parts.next().is_none()
2000}
2001
2002fn is_guid(value: &str) -> bool {
2003 if value.len() != 38 || !value.starts_with('{') || !value.ends_with('}') {
2004 return false;
2005 }
2006 value.bytes().enumerate().all(|(index, byte)| match index {
2007 0 => byte == b'{',
2008 37 => byte == b'}',
2009 9 | 14 | 19 | 24 => byte == b'-',
2010 _ => byte.is_ascii_hexdigit(),
2011 })
2012}
2013
2014fn push_optional_attr<'a>(start: &mut BytesStart<'a>, name: &'a str, value: Option<&'a str>) {
2015 if let Some(value) = value {
2016 start.push_attribute((name, value));
2017 }
2018}
2019
2020fn push_optional_bool(start: &mut BytesStart<'_>, name: &'static str, value: Option<bool>) {
2021 if let Some(value) = value {
2022 start.push_attribute((name, if value { "1" } else { "0" }));
2023 }
2024}
2025
2026fn push_owned_attr(start: &mut BytesStart<'_>, name: &'static str, value: String) {
2027 start.push_attribute((name, value.as_str()));
2028}
2029
2030fn emit_raw<'a, W: Write>(
2031 writer: &mut Writer<W>,
2032 children: impl Iterator<Item = &'a [u8]>,
2033) -> Result<()> {
2034 for child in children {
2035 writer.get_mut().write_all(child).map_err(OxmlError::from)?;
2036 }
2037 Ok(())
2038}
2039
2040fn write_start<W: Write>(writer: &mut Writer<W>, start: BytesStart<'_>) -> Result<()> {
2041 writer
2042 .write_event(Event::Start(start))
2043 .map_err(OxmlError::from)?;
2044 Ok(())
2045}
2046
2047fn write_empty<W: Write>(writer: &mut Writer<W>, start: BytesStart<'_>) -> Result<()> {
2048 writer
2049 .write_event(Event::Empty(start))
2050 .map_err(OxmlError::from)?;
2051 Ok(())
2052}
2053
2054fn write_end<W: Write>(writer: &mut Writer<W>, tag: &str) -> Result<()> {
2055 writer
2056 .write_event(Event::End(BytesEnd::new(tag)))
2057 .map_err(OxmlError::from)?;
2058 Ok(())
2059}
2060
2061fn unexpected(element: &BytesStart<'_>) -> TextError {
2062 TextError::UnexpectedElement(String::from_utf8_lossy(element.name().as_ref()).into_owned())
2063}
2064
2065fn duplicate(element: &str) -> TextError {
2066 TextError::DuplicateElement(element.to_owned())
2067}
2068
2069fn missing_attribute(element: &str, attribute: &str) -> TextError {
2070 TextError::MissingAttribute {
2071 element: element.to_owned(),
2072 attribute: attribute.to_owned(),
2073 }
2074}
2075
2076fn invalid_attribute(element: &str, attribute: &str, value: &str) -> TextError {
2077 TextError::InvalidAttribute {
2078 element: element.to_owned(),
2079 attribute: attribute.to_owned(),
2080 value: value.to_owned(),
2081 }
2082}
2083
2084#[cfg(test)]
2085mod tests {
2086 use std::panic;
2087
2088 use super::{
2089 CT_TextCharacterProperties, CT_TextParagraph, CT_TextParagraphProperties, TextAlignment,
2090 TextRun, TextSpace, TextSpacing,
2091 };
2092 use crate::color::ColorChoice;
2093 use crate::text::CT_TextBody;
2094 use crate::text::{
2095 TextAutoNumberScheme, TextBullet, TextBulletCharacter, TextBulletChoice,
2096 TextBulletSizeValue,
2097 };
2098
2099 #[test]
2100 fn drawingml_rtl_attribute_becomes_typed_without_reordering_unknown_content() {
2101 let parsed = CT_TextParagraphProperties::from_xml(
2102 br#"<q:pPr xmlns:q="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:x="urn:test" defTabSz="457200" rtl="1" x:keep="yes"/>"#,
2103 )
2104 .unwrap();
2105
2106 assert_eq!(parsed.right_to_left, Some(true));
2107 let serialized = String::from_utf8(parsed.to_xml().unwrap()).unwrap();
2108 assert!(serialized.contains("rtl=\"1\""));
2109 assert!(serialized.contains("x:keep=\"yes\""));
2110 assert!(
2111 serialized.find("defTabSz").unwrap() < serialized.find("rtl").unwrap(),
2112 "{serialized}"
2113 );
2114
2115 let mut changed = parsed;
2116 changed.right_to_left = Some(false);
2117 let serialized = String::from_utf8(changed.to_xml().unwrap()).unwrap();
2118 assert!(serialized.contains("rtl=\"0\""), "{serialized}");
2119 assert!(!serialized.contains("rtl=\"1\""), "{serialized}");
2120 }
2121
2122 #[test]
2123 fn leading_and_trailing_text_whitespace_survives_via_xml_space_preserve() {
2124 let xml = br#"<q:p><q:r><q:t> leading</q:t></q:r><q:r><q:t>trailing </q:t></q:r><q:fld id="{00112233-4455-6677-8899-AABBCCDDEEFF}"><q:t xml:space="preserve">middle</q:t></q:fld></q:p>"#;
2125 let paragraph = CT_TextParagraph::from_xml(xml).unwrap();
2126 assert_eq!(paragraph.to_xml().unwrap(), br#"<a:p><a:r><a:t xml:space="preserve"> leading</a:t></a:r><a:r><a:t xml:space="preserve">trailing </a:t></a:r><a:fld id="{00112233-4455-6677-8899-AABBCCDDEEFF}"><a:t xml:space="preserve">middle</a:t></a:fld></a:p>"#);
2127
2128 let TextRun::Run(first) = ¶graph.runs[0] else {
2129 panic!("expected regular run");
2130 };
2131 assert_eq!(first.text.value, " leading");
2132 assert_eq!(first.text.space, TextSpace::Preserve);
2133
2134 let body = CT_TextBody::from_xml(
2135 br#"<q:txBody><q:bodyPr/><q:p><q:r><q:t> body </q:t></q:r></q:p></q:txBody>"#,
2136 )
2137 .unwrap();
2138 assert_eq!(body.to_xml().unwrap(), br#"<a:txBody><a:bodyPr/><a:p><a:r><a:t xml:space="preserve"> body </a:t></a:r></a:p></a:txBody>"#);
2139
2140 let hostile = CT_TextParagraph::from_xml(
2141 br#"<q:p><q:r><q:t x:space="preserve">plain</q:t></q:r></q:p>"#,
2142 )
2143 .unwrap();
2144 let TextRun::Run(run) = &hostile.runs[0] else {
2145 panic!("expected regular run");
2146 };
2147 assert_eq!(run.text.space, TextSpace::Default);
2148 assert_eq!(
2149 hostile.to_xml().unwrap(),
2150 br#"<a:p><a:r><a:t x:space="preserve">plain</a:t></a:r></a:p>"#
2151 );
2152 }
2153
2154 #[test]
2155 fn canonical_text_state_matches_whitespace_and_empty_hyperlinks_from_real_decks() {
2156 let paragraph = CT_TextParagraph::from_xml(
2157 br#"<q:p><q:r><q:rPr><q:hlinkClick r:id=""/></q:rPr><q:t>trailing </q:t></q:r></q:p>"#,
2158 )
2159 .unwrap();
2160 let TextRun::Run(run) = ¶graph.runs[0] else {
2161 panic!("expected regular run");
2162 };
2163 assert_eq!(run.text.space, TextSpace::Preserve);
2164 assert_eq!(
2165 run.properties
2166 .as_ref()
2167 .and_then(|properties| properties.hyperlink_click.as_ref())
2168 .and_then(|hyperlink| hyperlink.relationship_id.as_deref()),
2169 Some("")
2170 );
2171 let written = paragraph.to_xml().unwrap();
2172 assert_eq!(CT_TextParagraph::from_xml(&written).unwrap(), paragraph);
2173 }
2174
2175 #[test]
2176 fn text_written_into_an_empty_paragraph_takes_its_end_formatting() {
2177 let mut paragraph = CT_TextParagraph::from_xml(
2178 br#"<a:p xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"><a:pPr algn="r"/><a:endParaRPr sz="1500" b="1"><a:hlinkClick r:id="rId9"/></a:endParaRPr></a:p>"#,
2179 )
2180 .unwrap();
2181 paragraph.set_text("Risk");
2182 assert_eq!(
2183 String::from_utf8(paragraph.to_xml().unwrap()).unwrap(),
2184 r#"<a:p><a:pPr algn="r"/><a:r><a:rPr sz="1500" b="1"/><a:t>Risk</a:t></a:r><a:endParaRPr sz="1500" b="1"><a:hlinkClick r:id="rId9"/></a:endParaRPr></a:p>"#
2185 );
2186
2187 let mut formatted = CT_TextParagraph::from_xml(
2188 br#"<a:p xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"><a:r><a:rPr i="1"/><a:t>old</a:t></a:r><a:endParaRPr sz="1500"/></a:p>"#,
2189 )
2190 .unwrap();
2191 formatted.set_text("new");
2192 assert_eq!(
2193 String::from_utf8(formatted.to_xml().unwrap()).unwrap(),
2194 r#"<a:p><a:r><a:rPr i="1"/><a:t>new</a:t></a:r><a:endParaRPr sz="1500"/></a:p>"#
2195 );
2196
2197 let mut bare = CT_TextParagraph::default();
2198 bare.set_text("plain");
2199 assert_eq!(
2200 bare.to_xml().unwrap(),
2201 br#"<a:p><a:r><a:t>plain</a:t></a:r></a:p>"#
2202 );
2203 }
2204
2205 #[test]
2206 fn paragraph_runs_fields_and_breaks_round_trip_structurally() {
2207 let xml = br#"<q:p><x:before/><q:pPr algn="ctr"/><x:afterPPr/><q:r><q:rPr b="1"><q:hlinkClick x:id="not-a-relationship" r:id="rId7" action="ppaction://hlinksldjump" tooltip="go & stay" history="0" highlightClick="1" endSnd="0"><x:sound/></q:hlinkClick></q:rPr><q:t>one</q:t></q:r><x:between/><q:br><q:rPr i="true"/></q:br><q:fld id="{00112233-4455-6677-8899-AABBCCDDEEFF}" type="slidenum"><q:rPr sz="1200"/><q:pPr lvl="1"/><q:t>2</q:t></q:fld><q:endParaRPr sz="1400"/><x:after/></q:p>"#;
2208 let paragraph = CT_TextParagraph::from_xml(xml).unwrap();
2209 assert_eq!(paragraph.runs.len(), 3);
2210 let written = paragraph.to_xml().unwrap();
2211 assert_eq!(written, br#"<a:p><x:before/><a:pPr algn="ctr"/><x:afterPPr/><a:r><a:rPr b="1"><a:hlinkClick r:id="rId7" action="ppaction://hlinksldjump" tooltip="go & stay" history="0" highlightClick="1" endSnd="0" x:id="not-a-relationship"><x:sound/></a:hlinkClick></a:rPr><a:t>one</a:t></a:r><x:between/><a:br><a:rPr i="1"/></a:br><a:fld id="{00112233-4455-6677-8899-AABBCCDDEEFF}" type="slidenum"><a:rPr sz="1200"/><a:pPr lvl="1"/><a:t>2</a:t></a:fld><a:endParaRPr sz="1400"/><x:after/></a:p>"#);
2212 assert_eq!(CT_TextParagraph::from_xml(&written).unwrap(), paragraph);
2213
2214 let hostile = CT_TextCharacterProperties::from_xml(
2215 br#"<q:rPr><q:hlinkClick x:id="not-a-relationship"/></q:rPr>"#,
2216 )
2217 .unwrap();
2218 assert!(
2219 hostile
2220 .hyperlink_click
2221 .as_ref()
2222 .unwrap()
2223 .relationship_id
2224 .is_none()
2225 );
2226 let mut writer = quick_xml::Writer::new(Vec::new());
2227 hostile.write_xml(&mut writer, "a:rPr").unwrap();
2228 assert_eq!(
2229 writer.into_inner(),
2230 br#"<a:rPr><a:hlinkClick x:id="not-a-relationship"/></a:rPr>"#
2231 );
2232 }
2233
2234 #[test]
2235 fn a_later_paragraph_properties_element_is_preserved_where_it_stands() {
2236 let xml = br#"<a:p><a:pPr algn="l"/><a:r><a:t>One.</a:t></a:r><a:pPr algn="r"><a:lnSpc><a:spcPct val="90000"/></a:lnSpc></a:pPr><a:r><a:t>Two.</a:t></a:r><a:pPr/><a:endParaRPr lang="en-US"/></a:p>"#;
2237 let mut paragraph = CT_TextParagraph::from_xml(xml).unwrap();
2238 let properties = paragraph.properties.as_ref().unwrap();
2239 assert_eq!(properties.alignment, Some(TextAlignment::Left));
2240 assert_eq!(properties.line_spacing, None);
2241 let texts = paragraph
2242 .runs
2243 .iter()
2244 .map(|run| match run {
2245 TextRun::Run(run) => run.text.value.as_str(),
2246 TextRun::Break(_) | TextRun::Field(_) => panic!("expected regular runs"),
2247 })
2248 .collect::<Vec<_>>();
2249 assert_eq!(texts, ["One.", "Two."]);
2250 assert_eq!(paragraph.to_xml().unwrap(), xml);
2251 assert_eq!(CT_TextParagraph::from_xml(xml).unwrap(), paragraph);
2252
2253 let TextRun::Run(first) = &mut paragraph.runs[0] else {
2254 panic!("expected regular run");
2255 };
2256 first.set_text("Uno.");
2257 paragraph.properties_mut().level = Some(1);
2258 paragraph.add_run("Three.");
2259 assert_eq!(
2260 paragraph.to_xml().unwrap(),
2261 br#"<a:p><a:pPr lvl="1" algn="l"/><a:r><a:t>Uno.</a:t></a:r><a:pPr algn="r"><a:lnSpc><a:spcPct val="90000"/></a:lnSpc></a:pPr><a:r><a:t>Two.</a:t></a:r><a:pPr/><a:r><a:t>Three.</a:t></a:r><a:endParaRPr lang="en-US"/></a:p>"#
2262 );
2263
2264 paragraph.set_text("Whole.");
2265 assert_eq!(
2266 paragraph.to_xml().unwrap(),
2267 br#"<a:p><a:pPr lvl="1" algn="l"/><a:r><a:t>Whole.</a:t></a:r><a:pPr algn="r"><a:lnSpc><a:spcPct val="90000"/></a:lnSpc></a:pPr><a:pPr/><a:endParaRPr lang="en-US"/></a:p>"#
2268 );
2269
2270 let late = CT_TextParagraph::from_xml(
2271 br#"<a:p><a:r><a:t>One.</a:t></a:r><a:pPr algn="l"/><a:r><a:t>Two.</a:t></a:r><a:pPr algn="r"/></a:p>"#,
2272 )
2273 .unwrap();
2274 assert_eq!(
2275 late.properties.as_ref().unwrap().alignment,
2276 Some(TextAlignment::Left)
2277 );
2278 assert_eq!(
2279 late.to_xml().unwrap(),
2280 br#"<a:p><a:pPr algn="l"/><a:r><a:t>One.</a:t></a:r><a:r><a:t>Two.</a:t></a:r><a:pPr algn="r"/></a:p>"#
2281 );
2282 }
2283
2284 #[test]
2285 fn character_all_caps_round_trips_while_small_caps_stays_unmodelled() {
2286 let all = CT_TextCharacterProperties::from_xml(
2287 br#"<q:rPr cap="all" sz="1800" x:producer="kept"/>"#,
2288 )
2289 .unwrap();
2290 assert_eq!(all.all_caps, Some(true));
2291 let mut writer = quick_xml::Writer::new(Vec::new());
2292 all.write_xml(&mut writer, "a:rPr").unwrap();
2293 let written = writer.into_inner();
2294 assert_eq!(
2295 written,
2296 br#"<a:rPr sz="1800" cap="all" x:producer="kept"/>"#
2297 );
2298 assert_eq!(CT_TextCharacterProperties::from_xml(&written).unwrap(), all);
2299
2300 let none = CT_TextCharacterProperties::from_xml(br#"<q:rPr cap="none"/>"#).unwrap();
2301 assert_eq!(none.all_caps, Some(false));
2302
2303 let small = CT_TextCharacterProperties::from_xml(br#"<q:rPr cap="small"/>"#).unwrap();
2304 assert_eq!(small.all_caps, None);
2305 let mut writer = quick_xml::Writer::new(Vec::new());
2306 small.write_xml(&mut writer, "a:rPr").unwrap();
2307 assert_eq!(writer.into_inner(), br#"<a:rPr cap="small"/>"#);
2308 }
2309
2310 #[test]
2311 fn setting_all_caps_over_preserved_small_caps_writes_one_cap_attribute() {
2312 let mut properties =
2313 CT_TextCharacterProperties::from_xml(br#"<q:rPr cap="small" lang="en-US"/>"#).unwrap();
2314 properties.all_caps = Some(true);
2315 let mut writer = quick_xml::Writer::new(Vec::new());
2316 properties.write_xml(&mut writer, "a:rPr").unwrap();
2317 let written = writer.into_inner();
2318 assert_eq!(written, br#"<a:rPr cap="all" lang="en-US"/>"#);
2319 assert_eq!(
2320 CT_TextCharacterProperties::from_xml(&written)
2321 .unwrap()
2322 .all_caps,
2323 Some(true)
2324 );
2325
2326 properties.all_caps = None;
2327 let mut writer = quick_xml::Writer::new(Vec::new());
2328 properties.write_xml(&mut writer, "a:rPr").unwrap();
2329 assert_eq!(writer.into_inner(), br#"<a:rPr cap="small" lang="en-US"/>"#);
2330 }
2331
2332 #[test]
2333 fn spacing_percentages_above_two_lines_parse_up_to_the_schema_maximum() {
2334 let xml = br#"<q:pPr><q:lnSpc><q:spcPct val="250000"/></q:lnSpc><q:spcBef><q:spcPct val="13200000"/></q:spcBef></q:pPr>"#;
2335 let properties = CT_TextParagraphProperties::from_xml(xml).unwrap();
2336 assert_eq!(
2337 properties.line_spacing,
2338 Some(TextSpacing::Percent("250000".to_owned()))
2339 );
2340 assert_eq!(
2341 properties.space_before,
2342 Some(TextSpacing::Percent("13200000".to_owned()))
2343 );
2344 assert_eq!(
2345 properties.to_xml().unwrap(),
2346 br#"<a:pPr><a:lnSpc><a:spcPct val="250000"/></a:lnSpc><a:spcBef><a:spcPct val="13200000"/></a:spcBef></a:pPr>"#
2347 );
2348 }
2349
2350 #[test]
2351 fn setting_a_bullet_part_removes_the_preserved_alternative_it_replaces() {
2352 let mut properties = CT_TextParagraphProperties::from_xml(
2353 br#"<q:pPr><q:buClrTx/><q:buSzTx/><q:buFontTx/><q:buBlip><q:blip x:embed="rId9"/></q:buBlip><q:tabLst/></q:pPr>"#,
2354 )
2355 .unwrap();
2356 assert!(properties.has_picture_bullet());
2357
2358 properties.set_bullet(Some(TextBullet {
2359 choice: Some(TextBulletChoice::Character(
2360 TextBulletCharacter::new("*").unwrap(),
2361 )),
2362 ..TextBullet::default()
2363 }));
2364 assert!(!properties.has_picture_bullet());
2365 assert_eq!(
2366 properties.to_xml().unwrap(),
2367 br#"<a:pPr><q:buClrTx/><q:buSzTx/><q:buFontTx/><a:buChar char="*"/><q:tabLst/></a:pPr>"#
2368 );
2369
2370 properties.set_bullet(None);
2371 assert_eq!(
2372 properties.to_xml().unwrap(),
2373 br#"<a:pPr><q:tabLst/></a:pPr>"#
2374 );
2375 }
2376
2377 #[test]
2378 fn paragraph_and_run_properties_use_drawingml_units_and_schema_order() {
2379 let paragraph_properties = CT_TextParagraphProperties::from_xml(br#"<q:pPr marL="0" marR="51206400" lvl="8" indent="-51206400" algn="thaiDist" rtl="1" defTabSz="914400"><x:before/><q:lnSpc><q:spcPct val="100000"/></q:lnSpc><q:spcBef><q:spcPts val="158400"/></q:spcBef><q:spcAft><q:spcPct val="125.5%"/></q:spcAft><q:buChar char="*"/><x:afterBullet/><q:defRPr sz="400000" b="true" i="0" u="wavyDbl" strike="dblStrike" spc="-400000" baseline="25%" xmlns:b="urn:test" lang="en-US" kern="1200"><q:solidFill><q:srgbClr val="AABBCC"/></q:solidFill><q:latin typeface="Aptos" pitchFamily="34" charset="0"/><q:ea typeface="Yu Gothic"/><q:cs typeface="Arial"/><q:sym typeface="Symbol"/><q:hlinkMouseOver r:id="rId8" tgtFrame="_blank" invalidUrl="bad"/></q:defRPr><x:last/></q:pPr>"#).unwrap();
2380 let written = {
2381 let mut writer = quick_xml::Writer::new(Vec::new());
2382 paragraph_properties
2383 .write_xml(&mut writer, "a:pPr")
2384 .unwrap();
2385 writer.into_inner()
2386 };
2387 assert_eq!(written, br#"<a:pPr marL="0" marR="51206400" lvl="8" indent="-51206400" algn="thaiDist" rtl="1" defTabSz="914400"><x:before/><a:lnSpc><a:spcPct val="100000"/></a:lnSpc><a:spcBef><a:spcPts val="158400"/></a:spcBef><a:spcAft><a:spcPct val="125.5%"/></a:spcAft><a:buChar char="*"/><x:afterBullet/><a:defRPr sz="400000" b="1" i="0" u="wavyDbl" strike="dblStrike" spc="-400000" baseline="25%" xmlns:b="urn:test" lang="en-US" kern="1200"><a:solidFill><a:srgbClr val="AABBCC"/></a:solidFill><a:latin typeface="Aptos" pitchFamily="34" charset="0"/><a:ea typeface="Yu Gothic"/><a:cs typeface="Arial"/><a:sym typeface="Symbol"/><a:hlinkMouseOver r:id="rId8" invalidUrl="bad" tgtFrame="_blank"/></a:defRPr><x:last/></a:pPr>"#);
2388
2389 let run_properties = CT_TextCharacterProperties::from_xml(
2390 br#"<q:rPr sz="100" spc="1.25pt" baseline="-100000"/>"#,
2391 )
2392 .unwrap();
2393 let mut writer = quick_xml::Writer::new(Vec::new());
2394 run_properties.write_xml(&mut writer, "a:rPr").unwrap();
2395 assert_eq!(
2396 writer.into_inner(),
2397 br#"<a:rPr sz="100" spc="1.25pt" baseline="-100000"/>"#
2398 );
2399
2400 let mut inserted_spacing =
2401 CT_TextParagraphProperties::from_xml(br#"<q:pPr><q:buChar char="*"/></q:pPr>"#)
2402 .unwrap();
2403 inserted_spacing.line_spacing = Some(TextSpacing::Percent("100000".to_owned()));
2404 let mut writer = quick_xml::Writer::new(Vec::new());
2405 inserted_spacing.write_xml(&mut writer, "a:pPr").unwrap();
2406 assert_eq!(
2407 writer.into_inner(),
2408 br#"<a:pPr><a:lnSpc><a:spcPct val="100000"/></a:lnSpc><a:buChar char="*"/></a:pPr>"#
2409 );
2410 }
2411
2412 #[test]
2413 fn every_modelled_bullet_form_round_trips_in_schema_order() {
2414 let cases: &[(&[u8], &[u8])] = &[
2415 (
2416 br#"<q:pPr><q:buClr><q:srgbClr val="AABBCC"/></q:buClr><q:buSzPct val="125000"/><q:buFont typeface="Wingdings"/><q:buChar char="*"/></q:pPr>"#,
2417 br#"<a:pPr><a:buClr><a:srgbClr val="AABBCC"/></a:buClr><a:buSzPct val="125000"/><a:buFont typeface="Wingdings"/><a:buChar char="*"/></a:pPr>"#,
2418 ),
2419 (
2420 br#"<q:pPr><q:buSzPts val="1800"/><q:buAutoNum type="arabicPeriod" startAt="3"/></q:pPr>"#,
2421 br#"<a:pPr><a:buSzPts val="1800"/><a:buAutoNum type="arabicPeriod" startAt="3"/></a:pPr>"#,
2422 ),
2423 (br#"<q:pPr><q:buNone/></q:pPr>"#, br#"<a:pPr><a:buNone/></a:pPr>"#),
2424 ];
2425
2426 for (xml, expected) in cases {
2427 let properties = CT_TextParagraphProperties::from_xml(xml).unwrap();
2428 let mut writer = quick_xml::Writer::new(Vec::new());
2429 properties.write_xml(&mut writer, "a:pPr").unwrap();
2430 let written = writer.into_inner();
2431 assert_eq!(written, *expected);
2432 assert_eq!(
2433 CT_TextParagraphProperties::from_xml(&written).unwrap(),
2434 properties
2435 );
2436 }
2437
2438 let character = CT_TextParagraphProperties::from_xml(cases[0].0).unwrap();
2439 let bullet = character.bullet.as_ref().unwrap();
2440 assert!(matches!(
2441 bullet.choice,
2442 Some(TextBulletChoice::Character(_))
2443 ));
2444 assert_eq!(bullet.font.as_ref().unwrap().typeface, "Wingdings");
2445 assert!(matches!(
2446 bullet.size.as_ref().unwrap().value,
2447 TextBulletSizeValue::Percent(ref value) if value == "125000"
2448 ));
2449 assert!(matches!(
2450 bullet.color.as_ref().unwrap().color,
2451 ColorChoice::Srgb { .. }
2452 ));
2453
2454 let automatic = CT_TextParagraphProperties::from_xml(cases[1].0).unwrap();
2455 let Some(TextBulletChoice::AutoNumber(numbering)) =
2456 automatic.bullet.as_ref().unwrap().choice.as_ref()
2457 else {
2458 panic!("expected automatic-number bullet");
2459 };
2460 assert_eq!(numbering.scheme, TextAutoNumberScheme::ArabicPeriod);
2461 assert_eq!(numbering.start_at, Some(3));
2462 assert!(matches!(
2463 automatic
2464 .bullet
2465 .as_ref()
2466 .unwrap()
2467 .size
2468 .as_ref()
2469 .unwrap()
2470 .value,
2471 TextBulletSizeValue::Points(1800)
2472 ));
2473
2474 let none = CT_TextParagraphProperties::from_xml(cases[2].0).unwrap();
2475 assert!(matches!(
2476 none.bullet.as_ref().unwrap().choice,
2477 Some(TextBulletChoice::None(_))
2478 ));
2479 }
2480
2481 #[test]
2482 fn bullet_font_size_and_colour_keep_their_schema_positions() {
2483 let properties = CT_TextParagraphProperties::from_xml(
2484 br#"<q:pPr><q:buFont typeface="Aptos"/><q:buChar char="*"/><q:buClr><q:schemeClr val="accent1"/></q:buClr><q:buSzPct val="80%"/><q:defRPr b="1"/></q:pPr>"#,
2485 )
2486 .unwrap();
2487 let mut writer = quick_xml::Writer::new(Vec::new());
2488 properties.write_xml(&mut writer, "a:pPr").unwrap();
2489 assert_eq!(
2490 writer.into_inner(),
2491 br#"<a:pPr><a:buClr><a:schemeClr val="accent1"/></a:buClr><a:buSzPct val="80%"/><a:buFont typeface="Aptos"/><a:buChar char="*"/><a:defRPr b="1"/></a:pPr>"#
2492 );
2493 }
2494
2495 #[test]
2496 fn unknown_bullet_children_round_trip_byte_for_byte() {
2497 let properties = CT_TextParagraphProperties::from_xml(
2498 br#"<q:pPr><q:buClrTx x:mode="stay"><x:nested>one & two</x:nested><!--note--></q:buClrTx><q:buChar char="*"/><x:after/></q:pPr>"#,
2499 )
2500 .unwrap();
2501 let mut writer = quick_xml::Writer::new(Vec::new());
2502 properties.write_xml(&mut writer, "a:pPr").unwrap();
2503 assert_eq!(
2504 writer.into_inner(),
2505 br#"<a:pPr><q:buClrTx x:mode="stay"><x:nested>one & two</x:nested><!--note--></q:buClrTx><a:buChar char="*"/><x:after/></a:pPr>"#
2506 );
2507 }
2508
2509 #[test]
2510 fn malformed_bullet_values_return_errors_without_panicking() {
2511 let cases: &[&[u8]] = &[
2512 br#"<q:pPr><q:buChar/></q:pPr>"#,
2513 br#"<q:pPr><q:buChar char=""/></q:pPr>"#,
2514 br#"<q:pPr><q:buSzPct val="24999"/></q:pPr>"#,
2515 br#"<q:pPr><q:buSzPct val="400001"/></q:pPr>"#,
2516 br#"<q:pPr><q:buSzPts val="99"/></q:pPr>"#,
2517 br#"<q:pPr><q:buSzPts val="400001"/></q:pPr>"#,
2518 br#"<q:pPr><q:buAutoNum type="unknown"/></q:pPr>"#,
2519 br#"<q:pPr><q:buAutoNum/></q:pPr>"#,
2520 br#"<q:pPr><q:buAutoNum type="arabicPeriod" startAt="0"/></q:pPr>"#,
2521 br#"<q:pPr><q:buAutoNum type="arabicPeriod" startAt="32768"/></q:pPr>"#,
2522 br#"<q:pPr><q:buFont/></q:pPr>"#,
2523 br#"<q:pPr><q:buClr/></q:pPr>"#,
2524 br#"<q:pPr><q:buClr><q:srgbClr x:val="AABBCC"/></q:buClr></q:pPr>"#,
2525 br#"<q:pPr><q:buClr><q:srgbClr val="GGGGGG"/></q:buClr></q:pPr>"#,
2526 br#"<q:pPr><q:buClr><q:srgbClr val="AABBCC"/><q:schemeClr val="accent1"/></q:buClr></q:pPr>"#,
2527 br#"<q:pPr><q:buChar char="*"/><q:buNone/></q:pPr>"#,
2528 br#"<q:pPr><q:buSzPct val="100000"/><q:buSzPts val="1200"/></q:pPr>"#,
2529 ];
2530
2531 for xml in cases {
2532 let result = panic::catch_unwind(|| CT_TextParagraphProperties::from_xml(xml));
2533 assert!(result.is_ok(), "bullet parser panicked");
2534 assert!(result.unwrap().is_err(), "malformed bullet parsed");
2535 }
2536 }
2537
2538 #[test]
2539 fn malformed_text_content_returns_errors_without_panicking() {
2540 let cases: &[&[u8]] = &[
2541 br#"<q:r/>"#,
2542 br#"<q:r><q:t><x:nested/></q:t></q:r>"#,
2543 br#"<q:r><q:t xml:space="keep">x</q:t></q:r>"#,
2544 br#"<q:r><q:t>x</q:t><q:t>y</q:t></q:r>"#,
2545 br#"<q:pPr lvl="9"/>"#,
2546 br#"<q:pPr marL="-1"/>"#,
2547 br#"<q:pPr indent="51206401"/>"#,
2548 br#"<q:pPr algn="middle"/>"#,
2549 br#"<q:pPr><q:lnSpc/></q:pPr>"#,
2550 br#"<q:pPr><q:lnSpc><q:spcPts val="158401"/></q:lnSpc></q:pPr>"#,
2551 br#"<q:pPr><q:lnSpc><q:spcPct val="13200001"/></q:lnSpc></q:pPr>"#,
2552 br#"<q:rPr sz="99"/>"#,
2553 br#"<q:rPr b="yes"/>"#,
2554 br#"<q:rPr u="triple"/>"#,
2555 br#"<q:rPr spc="400001"/>"#,
2556 r#"<q:rPr spc="éx"/>"#.as_bytes(),
2557 br#"<q:rPr baseline="100001"/>"#,
2558 br#"<q:rPr><q:latin/></q:rPr>"#,
2559 br#"<q:fld type="slidenum"/>"#,
2560 br#"<q:fld id="not-a-guid"/>"#,
2561 br#"<q:p><q:endParaRPr/><q:endParaRPr/></q:p>"#,
2562 br#"<q:notPPr/>"#,
2563 br#"<q:notRPr/>"#,
2564 ];
2565 for xml in cases {
2566 let result = panic::catch_unwind(|| match super::root_local_name(xml).as_deref() {
2567 Ok(b"r") => super::CT_RegularTextRun::from_xml(xml).map(|_| ()),
2568 Ok(b"pPr") => CT_TextParagraphProperties::from_xml(xml).map(|_| ()),
2569 Ok(b"rPr") => CT_TextCharacterProperties::from_xml(xml).map(|_| ()),
2570 Ok(b"fld") => super::CT_TextField::from_xml(xml).map(|_| ()),
2571 Ok(b"p") => CT_TextParagraph::from_xml(xml).map(|_| ()),
2572 Ok(b"notPPr") => CT_TextParagraphProperties::from_xml(xml).map(|_| ()),
2573 Ok(b"notRPr") => CT_TextCharacterProperties::from_xml(xml).map(|_| ()),
2574 _ => unreachable!(),
2575 });
2576 assert!(result.is_ok(), "text parser panicked");
2577 assert!(result.unwrap().is_err(), "malformed text parsed");
2578 }
2579 }
2580
2581 #[test]
2582 fn characters_xml_cannot_carry_are_stored_escaped_and_never_decoded() {
2583 let mut paragraph = CT_TextParagraph::default();
2584 paragraph.add_run("a\u{1}b\u{b}c\u{1f}d\u{fffe}e\u{0}");
2585 let TextRun::Run(run) = ¶graph.runs[0] else {
2586 panic!("expected a run");
2587 };
2588 assert_eq!(
2589 run.text.value, "a_x0001_b_x000B_c_x001F_d_xFFFE_e_x0000_",
2590 "the setter stores the escaped form, as python-pptx does"
2591 );
2592 let written = paragraph.to_xml().unwrap();
2593 assert_eq!(
2594 written,
2595 br#"<a:p><a:r><a:t>a_x0001_b_x000B_c_x001F_d_xFFFE_e_x0000_</a:t></a:r></a:p>"#
2596 );
2597 let declared = [
2598 br#"<a:p xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main""#.as_slice(),
2599 &written[4..],
2600 ]
2601 .concat();
2602 oxml_core::xml::validate_strict_xml_1_0(&declared)
2603 .expect("the written paragraph is XML 1.0");
2604 assert_eq!(CT_TextParagraph::from_xml(&written).unwrap(), paragraph);
2605
2606 let xml = br#"<a:p><a:r><a:t>C_x0041_D bell_x0007_ _x000b_ _x0001</a:t></a:r></a:p>"#;
2609 let paragraph = CT_TextParagraph::from_xml(xml).unwrap();
2610 let TextRun::Run(run) = ¶graph.runs[0] else {
2611 panic!("expected a run");
2612 };
2613 assert_eq!(run.text.value, "C_x0041_D bell_x0007_ _x000b_ _x0001");
2614 assert_eq!(paragraph.to_xml().unwrap(), xml);
2615 }
2616
2617 #[test]
2618 fn a_vertical_tab_set_as_paragraph_text_becomes_a_line_break() {
2619 let mut paragraph = CT_TextParagraph::from_xml(
2620 br#"<a:p xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:x="urn:x"><a:r><a:rPr b="1"/><a:t>old</a:t></a:r><x:after/><a:endParaRPr sz="1200"/><x:end/></a:p>"#,
2621 )
2622 .unwrap();
2623 paragraph.set_text("one\u{b}two\u{1}\u{b}");
2624 assert_eq!(
2625 String::from_utf8(paragraph.to_xml().unwrap()).unwrap(),
2626 r#"<a:p><a:r><a:rPr b="1"/><a:t>one</a:t></a:r><a:br><a:rPr b="1"/></a:br><a:r><a:rPr b="1"/><a:t>two_x0001_</a:t></a:r><a:br><a:rPr b="1"/></a:br><x:after/><a:endParaRPr sz="1200"/><x:end/></a:p>"#
2627 );
2628
2629 let mut plain = CT_TextParagraph::default();
2630 plain.set_text("\u{b}");
2631 assert_eq!(
2632 plain.to_xml().unwrap(),
2633 br#"<a:p><a:r><a:t/></a:r><a:br/></a:p>"#
2634 );
2635 }
2636}