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