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) {
1469 let old_run_count = self.runs.len();
1470 let mut raw_children = OrderedRawChildren::default();
1471 for boundary in 0..=2 + old_run_count {
1472 let new_boundary = if boundary <= 1 {
1473 boundary
1474 } else if boundary <= 1 + old_run_count {
1475 2
1476 } else {
1477 3
1478 };
1479 for child in self.raw_children.at(boundary) {
1480 raw_children.push(new_boundary, child.to_vec());
1481 }
1482 }
1483 self.raw_children = raw_children;
1484 self.runs = vec![TextRun::Run(CT_RegularTextRun::new(text))];
1485 }
1486
1487 pub fn add_run(&mut self, text: &str) -> &mut CT_RegularTextRun {
1489 self.raw_children.shift_boundaries_from(2 + self.runs.len());
1490 self.runs.push(TextRun::Run(CT_RegularTextRun::new(text)));
1491 let Some(TextRun::Run(run)) = self.runs.last_mut() else {
1492 unreachable!("the appended text choice is a regular run")
1493 };
1494 run
1495 }
1496
1497 pub fn from_xml(xml: &[u8]) -> Result<Self> {
1499 parse_complete(xml, b"p", Self::from_element, |_| Ok(Self::default()))
1500 }
1501
1502 fn from_element(reader: &mut Reader<&[u8]>, _start: &BytesStart<'_>) -> Result<Self> {
1503 let mut paragraph = Self::default();
1504 let mut boundary = 0;
1505 let mut buffer = Vec::new();
1506 loop {
1507 match reader
1508 .read_event_into(&mut buffer)
1509 .map_err(OxmlError::from)?
1510 {
1511 Event::Start(element) if matches_local_name(element.name().as_ref(), b"pPr") => {
1512 if paragraph.properties.is_some() {
1513 return Err(duplicate("pPr"));
1514 }
1515 paragraph.properties =
1516 Some(CT_TextParagraphProperties::from_element(reader, &element)?);
1517 boundary = boundary.max(1);
1518 }
1519 Event::Empty(element) if matches_local_name(element.name().as_ref(), b"pPr") => {
1520 if paragraph.properties.is_some() {
1521 return Err(duplicate("pPr"));
1522 }
1523 paragraph.properties = Some(CT_TextParagraphProperties::from_start(&element)?);
1524 boundary = boundary.max(1);
1525 }
1526 Event::Start(element) if is_text_run(element.name().as_ref()) => {
1527 let name = local_name(element.name().as_ref()).to_vec();
1528 let raw = capture_element(reader, &element)?;
1529 paragraph.runs.push(TextRun::from_xml(&raw, &name)?);
1530 boundary = boundary.max(1 + paragraph.runs.len());
1531 }
1532 Event::Empty(element) if is_text_run(element.name().as_ref()) => {
1533 let name = local_name(element.name().as_ref()).to_vec();
1534 let raw = capture_empty_element(&element)?;
1535 paragraph.runs.push(TextRun::from_xml(&raw, &name)?);
1536 boundary = boundary.max(1 + paragraph.runs.len());
1537 }
1538 Event::Start(element)
1539 if matches_local_name(element.name().as_ref(), b"endParaRPr") =>
1540 {
1541 if paragraph.end_properties.is_some() {
1542 return Err(duplicate("endParaRPr"));
1543 }
1544 paragraph.end_properties =
1545 Some(CT_TextCharacterProperties::from_element(reader, &element)?);
1546 boundary = boundary.max(2 + paragraph.runs.len());
1547 }
1548 Event::Empty(element)
1549 if matches_local_name(element.name().as_ref(), b"endParaRPr") =>
1550 {
1551 if paragraph.end_properties.is_some() {
1552 return Err(duplicate("endParaRPr"));
1553 }
1554 paragraph.end_properties =
1555 Some(CT_TextCharacterProperties::from_start(&element)?);
1556 boundary = boundary.max(2 + paragraph.runs.len());
1557 }
1558 Event::Start(element) => paragraph
1559 .raw_children
1560 .push(boundary, capture_element(reader, &element)?),
1561 Event::Empty(element) => paragraph
1562 .raw_children
1563 .push(boundary, capture_empty_element(&element)?),
1564 Event::End(element) if matches_local_name(element.name().as_ref(), b"p") => {
1565 return Ok(paragraph);
1566 }
1567 Event::Eof => return Err(missing_end("p")),
1568 _ => {}
1569 }
1570 buffer.clear();
1571 }
1572 }
1573
1574 pub fn write_xml<W: Write>(&self, writer: &mut Writer<W>) -> Result<()> {
1576 if self.properties.is_none()
1577 && self.runs.is_empty()
1578 && self.end_properties.is_none()
1579 && self.raw_children.is_empty()
1580 {
1581 return write_empty(writer, BytesStart::new("a:p"));
1582 }
1583 write_start(writer, BytesStart::new("a:p"))?;
1584 emit_raw(writer, self.raw_children.at(0))?;
1585 if let Some(properties) = &self.properties {
1586 properties.write_xml(writer, "a:pPr")?;
1587 }
1588 emit_raw(writer, self.raw_children.at(1))?;
1589 for (index, run) in self.runs.iter().enumerate() {
1590 run.write_xml(writer)?;
1591 emit_raw(writer, self.raw_children.at(2 + index))?;
1592 }
1593 if let Some(properties) = &self.end_properties {
1594 properties.write_xml(writer, "a:endParaRPr")?;
1595 }
1596 emit_raw(writer, self.raw_children.at(2 + self.runs.len()))?;
1597 write_end(writer, "a:p")
1598 }
1599
1600 pub fn to_xml(&self) -> Result<Vec<u8>> {
1602 let mut writer = Writer::new(Vec::new());
1603 self.write_xml(&mut writer)?;
1604 Ok(writer.into_inner())
1605 }
1606
1607 pub fn raw_children(&self) -> &OrderedRawChildren {
1608 &self.raw_children
1609 }
1610}
1611
1612fn is_text_run(name: &[u8]) -> bool {
1613 matches!(local_name(name), b"r" | b"br" | b"fld")
1614}
1615
1616fn parse_complete<T>(
1617 xml: &[u8],
1618 expected: &[u8],
1619 parse_start: impl FnOnce(&mut Reader<&[u8]>, &BytesStart<'_>) -> Result<T>,
1620 parse_empty: impl FnOnce(&BytesStart<'_>) -> Result<T>,
1621) -> Result<T> {
1622 let mut reader = Reader::from_reader(xml);
1623 let mut buffer = Vec::new();
1624 loop {
1625 match reader
1626 .read_event_into(&mut buffer)
1627 .map_err(OxmlError::from)?
1628 {
1629 Event::Start(element) if matches_local_name(element.name().as_ref(), expected) => {
1630 reject_conflicting_a_prefix(&element)?;
1631 return parse_start(&mut reader, &element);
1632 }
1633 Event::Empty(element) if matches_local_name(element.name().as_ref(), expected) => {
1634 reject_conflicting_a_prefix(&element)?;
1635 return parse_empty(&element);
1636 }
1637 Event::Start(element) | Event::Empty(element) => return Err(unexpected(&element)),
1638 Event::Eof => {
1639 return Err(TextError::Xml(OxmlError::MissingElement(
1640 String::from_utf8_lossy(expected).into_owned(),
1641 )));
1642 }
1643 _ => {}
1644 }
1645 buffer.clear();
1646 }
1647}
1648
1649fn root_local_name(xml: &[u8]) -> Result<Vec<u8>> {
1650 let mut reader = Reader::from_reader(xml);
1651 let mut buffer = Vec::new();
1652 loop {
1653 match reader
1654 .read_event_into(&mut buffer)
1655 .map_err(OxmlError::from)?
1656 {
1657 Event::Start(element) | Event::Empty(element) => {
1658 return Ok(local_name(element.name().as_ref()).to_vec());
1659 }
1660 Event::Eof => {
1661 return Err(TextError::Xml(OxmlError::MissingElement(
1662 "DrawingML text properties".to_owned(),
1663 )));
1664 }
1665 _ => {}
1666 }
1667 buffer.clear();
1668 }
1669}
1670
1671fn ensure_empty(reader: &mut Reader<&[u8]>, expected: &[u8]) -> Result<()> {
1672 let mut buffer = Vec::new();
1673 loop {
1674 match reader
1675 .read_event_into(&mut buffer)
1676 .map_err(OxmlError::from)?
1677 {
1678 Event::End(element) if matches_local_name(element.name().as_ref(), expected) => {
1679 return Ok(());
1680 }
1681 Event::Text(text) if text.iter().all(u8::is_ascii_whitespace) => {}
1682 Event::Comment(_) => {}
1683 Event::Start(element) | Event::Empty(element) => return Err(unexpected(&element)),
1684 Event::Eof => return Err(missing_end(&String::from_utf8_lossy(expected))),
1685 _ => {
1686 return Err(TextError::UnexpectedElement(
1687 String::from_utf8_lossy(expected).into_owned(),
1688 ));
1689 }
1690 }
1691 buffer.clear();
1692 }
1693}
1694
1695fn text_attr(start: &BytesStart<'_>, name: &[u8]) -> Result<Option<String>> {
1696 for attribute in start.attributes() {
1697 let attribute = attribute.map_err(OxmlError::from)?;
1698 if attribute.key.as_ref() == name {
1699 let value = attribute
1700 .decoded_and_normalized_value(XmlVersion::Implicit1_0, start.decoder())
1701 .map_err(OxmlError::from)?;
1702 return Ok(Some(value.into_owned()));
1703 }
1704 }
1705 Ok(None)
1706}
1707
1708fn capture_raw_attributes(
1709 start: &BytesStart<'_>,
1710 modelled: &[&[u8]],
1711) -> Result<Vec<(String, String)>> {
1712 let mut raw = Vec::new();
1713 for attribute in start.attributes() {
1714 let attribute = attribute.map_err(OxmlError::from)?;
1715 if modelled.iter().any(|name| attribute.key.as_ref() == *name) {
1716 continue;
1717 }
1718 let name = std::str::from_utf8(attribute.key.as_ref())
1719 .map_err(OxmlError::from)?
1720 .to_owned();
1721 let value = attribute
1722 .decoded_and_normalized_value(XmlVersion::Implicit1_0, start.decoder())
1723 .map_err(OxmlError::from)?
1724 .into_owned();
1725 raw.push((name, value));
1726 }
1727 Ok(raw)
1728}
1729
1730fn push_raw_attributes(start: &mut BytesStart<'_>, attributes: &[(String, String)]) {
1731 for (name, value) in attributes {
1732 start.push_attribute((name.as_str(), value.as_str()));
1733 }
1734}
1735
1736fn required_attr(start: &BytesStart<'_>, name: &[u8]) -> Result<String> {
1737 text_attr(start, name)?
1738 .filter(|value| !value.is_empty())
1739 .ok_or_else(|| {
1740 missing_attribute(
1741 &String::from_utf8_lossy(local_name(start.name().as_ref())),
1742 &String::from_utf8_lossy(name),
1743 )
1744 })
1745}
1746
1747fn parse_optional_i32(
1748 start: &BytesStart<'_>,
1749 name: &[u8],
1750 min: i32,
1751 max: i32,
1752) -> Result<Option<i32>> {
1753 text_attr(start, name)?
1754 .map(|value| {
1755 parse_i32_value(
1756 &String::from_utf8_lossy(local_name(start.name().as_ref())),
1757 &String::from_utf8_lossy(name),
1758 &value,
1759 min,
1760 max,
1761 )
1762 })
1763 .transpose()
1764}
1765
1766fn parse_i32_attr(start: &BytesStart<'_>, name: &[u8], min: i32, max: i32) -> Result<i32> {
1767 let value = required_attr(start, name)?;
1768 parse_i32_value(
1769 &String::from_utf8_lossy(local_name(start.name().as_ref())),
1770 &String::from_utf8_lossy(name),
1771 &value,
1772 min,
1773 max,
1774 )
1775}
1776
1777fn parse_i32_value(element: &str, attribute: &str, value: &str, min: i32, max: i32) -> Result<i32> {
1778 let parsed = value
1779 .parse::<i32>()
1780 .map_err(|_| invalid_attribute(element, attribute, value))?;
1781 if (min..=max).contains(&parsed) {
1782 Ok(parsed)
1783 } else {
1784 Err(invalid_attribute(element, attribute, value))
1785 }
1786}
1787
1788fn parse_optional_bool(start: &BytesStart<'_>, name: &[u8]) -> Result<Option<bool>> {
1789 text_attr(start, name)?
1790 .map(|value| match value.as_str() {
1791 "true" | "1" => Ok(true),
1792 "false" | "0" => Ok(false),
1793 _ => Err(invalid_attribute(
1794 &String::from_utf8_lossy(local_name(start.name().as_ref())),
1795 &String::from_utf8_lossy(name),
1796 &value,
1797 )),
1798 })
1799 .transpose()
1800}
1801
1802fn parse_optional_enum<T>(
1803 start: &BytesStart<'_>,
1804 name: &[u8],
1805 parse: impl FnOnce(&str) -> Option<T>,
1806) -> Result<Option<T>> {
1807 let Some(value) = text_attr(start, name)? else {
1808 return Ok(None);
1809 };
1810 parse(&value).map(Some).ok_or_else(|| {
1811 invalid_attribute(
1812 &String::from_utf8_lossy(local_name(start.name().as_ref())),
1813 &String::from_utf8_lossy(name),
1814 &value,
1815 )
1816 })
1817}
1818
1819fn validate_spacing_percent(value: &str) -> Result<()> {
1820 if is_percentage_string(value) {
1821 return Ok(());
1822 }
1823 parse_i32_value("spcPct", "val", value, 0, MAX_TRANSITIONAL_SPACING_PERCENT)?;
1824 Ok(())
1825}
1826
1827fn validate_baseline(value: &str) -> Result<()> {
1828 if is_percentage_string(value) {
1829 return Ok(());
1830 }
1831 parse_i32_value("rPr", "baseline", value, -100_000, 100_000)?;
1832 Ok(())
1833}
1834
1835fn is_percentage_string(value: &str) -> bool {
1836 value.strip_suffix('%').is_some_and(is_signed_decimal)
1837}
1838
1839fn is_universal_measure(value: &str) -> bool {
1840 ["mm", "cm", "in", "pt", "pc", "pi"]
1841 .iter()
1842 .find_map(|unit| value.strip_suffix(unit))
1843 .is_some_and(is_signed_decimal)
1844}
1845
1846fn is_signed_decimal(value: &str) -> bool {
1847 let unsigned = value.strip_prefix('-').unwrap_or(value);
1848 let mut parts = unsigned.split('.');
1849 let Some(integer) = parts.next() else {
1850 return false;
1851 };
1852 if integer.is_empty() || !integer.bytes().all(|byte| byte.is_ascii_digit()) {
1853 return false;
1854 }
1855 if let Some(fraction) = parts.next()
1856 && (fraction.is_empty() || !fraction.bytes().all(|byte| byte.is_ascii_digit()))
1857 {
1858 return false;
1859 }
1860 parts.next().is_none()
1861}
1862
1863fn is_guid(value: &str) -> bool {
1864 if value.len() != 38 || !value.starts_with('{') || !value.ends_with('}') {
1865 return false;
1866 }
1867 value.bytes().enumerate().all(|(index, byte)| match index {
1868 0 => byte == b'{',
1869 37 => byte == b'}',
1870 9 | 14 | 19 | 24 => byte == b'-',
1871 _ => byte.is_ascii_hexdigit(),
1872 })
1873}
1874
1875fn push_optional_attr<'a>(start: &mut BytesStart<'a>, name: &'a str, value: Option<&'a str>) {
1876 if let Some(value) = value {
1877 start.push_attribute((name, value));
1878 }
1879}
1880
1881fn push_optional_bool(start: &mut BytesStart<'_>, name: &'static str, value: Option<bool>) {
1882 if let Some(value) = value {
1883 start.push_attribute((name, if value { "1" } else { "0" }));
1884 }
1885}
1886
1887fn push_owned_attr(start: &mut BytesStart<'_>, name: &'static str, value: String) {
1888 start.push_attribute((name, value.as_str()));
1889}
1890
1891fn emit_raw<'a, W: Write>(
1892 writer: &mut Writer<W>,
1893 children: impl Iterator<Item = &'a [u8]>,
1894) -> Result<()> {
1895 for child in children {
1896 writer.get_mut().write_all(child).map_err(OxmlError::from)?;
1897 }
1898 Ok(())
1899}
1900
1901fn write_start<W: Write>(writer: &mut Writer<W>, start: BytesStart<'_>) -> Result<()> {
1902 writer
1903 .write_event(Event::Start(start))
1904 .map_err(OxmlError::from)?;
1905 Ok(())
1906}
1907
1908fn write_empty<W: Write>(writer: &mut Writer<W>, start: BytesStart<'_>) -> Result<()> {
1909 writer
1910 .write_event(Event::Empty(start))
1911 .map_err(OxmlError::from)?;
1912 Ok(())
1913}
1914
1915fn write_end<W: Write>(writer: &mut Writer<W>, tag: &str) -> Result<()> {
1916 writer
1917 .write_event(Event::End(BytesEnd::new(tag)))
1918 .map_err(OxmlError::from)?;
1919 Ok(())
1920}
1921
1922fn unexpected(element: &BytesStart<'_>) -> TextError {
1923 TextError::UnexpectedElement(String::from_utf8_lossy(element.name().as_ref()).into_owned())
1924}
1925
1926fn duplicate(element: &str) -> TextError {
1927 TextError::DuplicateElement(element.to_owned())
1928}
1929
1930fn missing_attribute(element: &str, attribute: &str) -> TextError {
1931 TextError::MissingAttribute {
1932 element: element.to_owned(),
1933 attribute: attribute.to_owned(),
1934 }
1935}
1936
1937fn invalid_attribute(element: &str, attribute: &str, value: &str) -> TextError {
1938 TextError::InvalidAttribute {
1939 element: element.to_owned(),
1940 attribute: attribute.to_owned(),
1941 value: value.to_owned(),
1942 }
1943}
1944
1945#[cfg(test)]
1946mod tests {
1947 use std::panic;
1948
1949 use super::{
1950 CT_TextCharacterProperties, CT_TextParagraph, CT_TextParagraphProperties, TextRun,
1951 TextSpace, TextSpacing,
1952 };
1953 use crate::color::ColorChoice;
1954 use crate::text::CT_TextBody;
1955 use crate::text::{TextAutoNumberScheme, TextBulletChoice, TextBulletSizeValue};
1956
1957 #[test]
1958 fn drawingml_rtl_attribute_becomes_typed_without_reordering_unknown_content() {
1959 let parsed = CT_TextParagraphProperties::from_xml(
1960 br#"<q:pPr xmlns:q="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:x="urn:test" defTabSz="457200" rtl="1" x:keep="yes"/>"#,
1961 )
1962 .unwrap();
1963
1964 assert_eq!(parsed.right_to_left, Some(true));
1965 let serialized = String::from_utf8(parsed.to_xml().unwrap()).unwrap();
1966 assert!(serialized.contains("rtl=\"1\""));
1967 assert!(serialized.contains("x:keep=\"yes\""));
1968 assert!(
1969 serialized.find("defTabSz").unwrap() < serialized.find("rtl").unwrap(),
1970 "{serialized}"
1971 );
1972
1973 let mut changed = parsed;
1974 changed.right_to_left = Some(false);
1975 let serialized = String::from_utf8(changed.to_xml().unwrap()).unwrap();
1976 assert!(serialized.contains("rtl=\"0\""), "{serialized}");
1977 assert!(!serialized.contains("rtl=\"1\""), "{serialized}");
1978 }
1979
1980 #[test]
1981 fn leading_and_trailing_text_whitespace_survives_via_xml_space_preserve() {
1982 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>"#;
1983 let paragraph = CT_TextParagraph::from_xml(xml).unwrap();
1984 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>"#);
1985
1986 let TextRun::Run(first) = ¶graph.runs[0] else {
1987 panic!("expected regular run");
1988 };
1989 assert_eq!(first.text.value, " leading");
1990 assert_eq!(first.text.space, TextSpace::Preserve);
1991
1992 let body = CT_TextBody::from_xml(
1993 br#"<q:txBody><q:bodyPr/><q:p><q:r><q:t> body </q:t></q:r></q:p></q:txBody>"#,
1994 )
1995 .unwrap();
1996 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>"#);
1997
1998 let hostile = CT_TextParagraph::from_xml(
1999 br#"<q:p><q:r><q:t x:space="preserve">plain</q:t></q:r></q:p>"#,
2000 )
2001 .unwrap();
2002 let TextRun::Run(run) = &hostile.runs[0] else {
2003 panic!("expected regular run");
2004 };
2005 assert_eq!(run.text.space, TextSpace::Default);
2006 assert_eq!(
2007 hostile.to_xml().unwrap(),
2008 br#"<a:p><a:r><a:t x:space="preserve">plain</a:t></a:r></a:p>"#
2009 );
2010 }
2011
2012 #[test]
2013 fn canonical_text_state_matches_whitespace_and_empty_hyperlinks_from_real_decks() {
2014 let paragraph = CT_TextParagraph::from_xml(
2015 br#"<q:p><q:r><q:rPr><q:hlinkClick r:id=""/></q:rPr><q:t>trailing </q:t></q:r></q:p>"#,
2016 )
2017 .unwrap();
2018 let TextRun::Run(run) = ¶graph.runs[0] else {
2019 panic!("expected regular run");
2020 };
2021 assert_eq!(run.text.space, TextSpace::Preserve);
2022 assert_eq!(
2023 run.properties
2024 .as_ref()
2025 .and_then(|properties| properties.hyperlink_click.as_ref())
2026 .and_then(|hyperlink| hyperlink.relationship_id.as_deref()),
2027 Some("")
2028 );
2029 let written = paragraph.to_xml().unwrap();
2030 assert_eq!(CT_TextParagraph::from_xml(&written).unwrap(), paragraph);
2031 }
2032
2033 #[test]
2034 fn paragraph_runs_fields_and_breaks_round_trip_structurally() {
2035 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>"#;
2036 let paragraph = CT_TextParagraph::from_xml(xml).unwrap();
2037 assert_eq!(paragraph.runs.len(), 3);
2038 let written = paragraph.to_xml().unwrap();
2039 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>"#);
2040 assert_eq!(CT_TextParagraph::from_xml(&written).unwrap(), paragraph);
2041
2042 let hostile = CT_TextCharacterProperties::from_xml(
2043 br#"<q:rPr><q:hlinkClick x:id="not-a-relationship"/></q:rPr>"#,
2044 )
2045 .unwrap();
2046 assert!(
2047 hostile
2048 .hyperlink_click
2049 .as_ref()
2050 .unwrap()
2051 .relationship_id
2052 .is_none()
2053 );
2054 let mut writer = quick_xml::Writer::new(Vec::new());
2055 hostile.write_xml(&mut writer, "a:rPr").unwrap();
2056 assert_eq!(
2057 writer.into_inner(),
2058 br#"<a:rPr><a:hlinkClick x:id="not-a-relationship"/></a:rPr>"#
2059 );
2060 }
2061
2062 #[test]
2063 fn character_all_caps_round_trips_while_small_caps_stays_unmodelled() {
2064 let all = CT_TextCharacterProperties::from_xml(
2065 br#"<q:rPr cap="all" sz="1800" x:producer="kept"/>"#,
2066 )
2067 .unwrap();
2068 assert_eq!(all.all_caps, Some(true));
2069 let mut writer = quick_xml::Writer::new(Vec::new());
2070 all.write_xml(&mut writer, "a:rPr").unwrap();
2071 let written = writer.into_inner();
2072 assert_eq!(
2073 written,
2074 br#"<a:rPr sz="1800" cap="all" x:producer="kept"/>"#
2075 );
2076 assert_eq!(CT_TextCharacterProperties::from_xml(&written).unwrap(), all);
2077
2078 let none = CT_TextCharacterProperties::from_xml(br#"<q:rPr cap="none"/>"#).unwrap();
2079 assert_eq!(none.all_caps, Some(false));
2080
2081 let small = CT_TextCharacterProperties::from_xml(br#"<q:rPr cap="small"/>"#).unwrap();
2082 assert_eq!(small.all_caps, None);
2083 let mut writer = quick_xml::Writer::new(Vec::new());
2084 small.write_xml(&mut writer, "a:rPr").unwrap();
2085 assert_eq!(writer.into_inner(), br#"<a:rPr cap="small"/>"#);
2086 }
2087
2088 #[test]
2089 fn paragraph_and_run_properties_use_drawingml_units_and_schema_order() {
2090 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();
2091 let written = {
2092 let mut writer = quick_xml::Writer::new(Vec::new());
2093 paragraph_properties
2094 .write_xml(&mut writer, "a:pPr")
2095 .unwrap();
2096 writer.into_inner()
2097 };
2098 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>"#);
2099
2100 let run_properties = CT_TextCharacterProperties::from_xml(
2101 br#"<q:rPr sz="100" spc="1.25pt" baseline="-100000"/>"#,
2102 )
2103 .unwrap();
2104 let mut writer = quick_xml::Writer::new(Vec::new());
2105 run_properties.write_xml(&mut writer, "a:rPr").unwrap();
2106 assert_eq!(
2107 writer.into_inner(),
2108 br#"<a:rPr sz="100" spc="1.25pt" baseline="-100000"/>"#
2109 );
2110
2111 let mut inserted_spacing =
2112 CT_TextParagraphProperties::from_xml(br#"<q:pPr><q:buChar char="*"/></q:pPr>"#)
2113 .unwrap();
2114 inserted_spacing.line_spacing = Some(TextSpacing::Percent("100000".to_owned()));
2115 let mut writer = quick_xml::Writer::new(Vec::new());
2116 inserted_spacing.write_xml(&mut writer, "a:pPr").unwrap();
2117 assert_eq!(
2118 writer.into_inner(),
2119 br#"<a:pPr><a:lnSpc><a:spcPct val="100000"/></a:lnSpc><a:buChar char="*"/></a:pPr>"#
2120 );
2121 }
2122
2123 #[test]
2124 fn every_modelled_bullet_form_round_trips_in_schema_order() {
2125 let cases: &[(&[u8], &[u8])] = &[
2126 (
2127 br#"<q:pPr><q:buClr><q:srgbClr val="AABBCC"/></q:buClr><q:buSzPct val="125000"/><q:buFont typeface="Wingdings"/><q:buChar char="*"/></q:pPr>"#,
2128 br#"<a:pPr><a:buClr><a:srgbClr val="AABBCC"/></a:buClr><a:buSzPct val="125000"/><a:buFont typeface="Wingdings"/><a:buChar char="*"/></a:pPr>"#,
2129 ),
2130 (
2131 br#"<q:pPr><q:buSzPts val="1800"/><q:buAutoNum type="arabicPeriod" startAt="3"/></q:pPr>"#,
2132 br#"<a:pPr><a:buSzPts val="1800"/><a:buAutoNum type="arabicPeriod" startAt="3"/></a:pPr>"#,
2133 ),
2134 (br#"<q:pPr><q:buNone/></q:pPr>"#, br#"<a:pPr><a:buNone/></a:pPr>"#),
2135 ];
2136
2137 for (xml, expected) in cases {
2138 let properties = CT_TextParagraphProperties::from_xml(xml).unwrap();
2139 let mut writer = quick_xml::Writer::new(Vec::new());
2140 properties.write_xml(&mut writer, "a:pPr").unwrap();
2141 let written = writer.into_inner();
2142 assert_eq!(written, *expected);
2143 assert_eq!(
2144 CT_TextParagraphProperties::from_xml(&written).unwrap(),
2145 properties
2146 );
2147 }
2148
2149 let character = CT_TextParagraphProperties::from_xml(cases[0].0).unwrap();
2150 let bullet = character.bullet.as_ref().unwrap();
2151 assert!(matches!(
2152 bullet.choice,
2153 Some(TextBulletChoice::Character(_))
2154 ));
2155 assert_eq!(bullet.font.as_ref().unwrap().typeface, "Wingdings");
2156 assert!(matches!(
2157 bullet.size.as_ref().unwrap().value,
2158 TextBulletSizeValue::Percent(ref value) if value == "125000"
2159 ));
2160 assert!(matches!(
2161 bullet.color.as_ref().unwrap().color,
2162 ColorChoice::Srgb { .. }
2163 ));
2164
2165 let automatic = CT_TextParagraphProperties::from_xml(cases[1].0).unwrap();
2166 let Some(TextBulletChoice::AutoNumber(numbering)) =
2167 automatic.bullet.as_ref().unwrap().choice.as_ref()
2168 else {
2169 panic!("expected automatic-number bullet");
2170 };
2171 assert_eq!(numbering.scheme, TextAutoNumberScheme::ArabicPeriod);
2172 assert_eq!(numbering.start_at, Some(3));
2173 assert!(matches!(
2174 automatic
2175 .bullet
2176 .as_ref()
2177 .unwrap()
2178 .size
2179 .as_ref()
2180 .unwrap()
2181 .value,
2182 TextBulletSizeValue::Points(1800)
2183 ));
2184
2185 let none = CT_TextParagraphProperties::from_xml(cases[2].0).unwrap();
2186 assert!(matches!(
2187 none.bullet.as_ref().unwrap().choice,
2188 Some(TextBulletChoice::None(_))
2189 ));
2190 }
2191
2192 #[test]
2193 fn bullet_font_size_and_colour_keep_their_schema_positions() {
2194 let properties = CT_TextParagraphProperties::from_xml(
2195 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>"#,
2196 )
2197 .unwrap();
2198 let mut writer = quick_xml::Writer::new(Vec::new());
2199 properties.write_xml(&mut writer, "a:pPr").unwrap();
2200 assert_eq!(
2201 writer.into_inner(),
2202 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>"#
2203 );
2204 }
2205
2206 #[test]
2207 fn unknown_bullet_children_round_trip_byte_for_byte() {
2208 let properties = CT_TextParagraphProperties::from_xml(
2209 br#"<q:pPr><q:buClrTx x:mode="stay"><x:nested>one & two</x:nested><!--note--></q:buClrTx><q:buChar char="*"/><x:after/></q:pPr>"#,
2210 )
2211 .unwrap();
2212 let mut writer = quick_xml::Writer::new(Vec::new());
2213 properties.write_xml(&mut writer, "a:pPr").unwrap();
2214 assert_eq!(
2215 writer.into_inner(),
2216 br#"<a:pPr><q:buClrTx x:mode="stay"><x:nested>one & two</x:nested><!--note--></q:buClrTx><a:buChar char="*"/><x:after/></a:pPr>"#
2217 );
2218 }
2219
2220 #[test]
2221 fn malformed_bullet_values_return_errors_without_panicking() {
2222 let cases: &[&[u8]] = &[
2223 br#"<q:pPr><q:buChar/></q:pPr>"#,
2224 br#"<q:pPr><q:buChar char=""/></q:pPr>"#,
2225 br#"<q:pPr><q:buSzPct val="24999"/></q:pPr>"#,
2226 br#"<q:pPr><q:buSzPct val="400001"/></q:pPr>"#,
2227 br#"<q:pPr><q:buSzPts val="99"/></q:pPr>"#,
2228 br#"<q:pPr><q:buSzPts val="400001"/></q:pPr>"#,
2229 br#"<q:pPr><q:buAutoNum type="unknown"/></q:pPr>"#,
2230 br#"<q:pPr><q:buAutoNum/></q:pPr>"#,
2231 br#"<q:pPr><q:buAutoNum type="arabicPeriod" startAt="0"/></q:pPr>"#,
2232 br#"<q:pPr><q:buAutoNum type="arabicPeriod" startAt="32768"/></q:pPr>"#,
2233 br#"<q:pPr><q:buFont/></q:pPr>"#,
2234 br#"<q:pPr><q:buClr/></q:pPr>"#,
2235 br#"<q:pPr><q:buClr><q:srgbClr x:val="AABBCC"/></q:buClr></q:pPr>"#,
2236 br#"<q:pPr><q:buClr><q:srgbClr val="GGGGGG"/></q:buClr></q:pPr>"#,
2237 br#"<q:pPr><q:buClr><q:srgbClr val="AABBCC"/><q:schemeClr val="accent1"/></q:buClr></q:pPr>"#,
2238 br#"<q:pPr><q:buChar char="*"/><q:buNone/></q:pPr>"#,
2239 br#"<q:pPr><q:buSzPct val="100000"/><q:buSzPts val="1200"/></q:pPr>"#,
2240 ];
2241
2242 for xml in cases {
2243 let result = panic::catch_unwind(|| CT_TextParagraphProperties::from_xml(xml));
2244 assert!(result.is_ok(), "bullet parser panicked");
2245 assert!(result.unwrap().is_err(), "malformed bullet parsed");
2246 }
2247 }
2248
2249 #[test]
2250 fn malformed_text_content_returns_errors_without_panicking() {
2251 let cases: &[&[u8]] = &[
2252 br#"<q:r/>"#,
2253 br#"<q:r><q:t><x:nested/></q:t></q:r>"#,
2254 br#"<q:r><q:t xml:space="keep">x</q:t></q:r>"#,
2255 br#"<q:r><q:t>x</q:t><q:t>y</q:t></q:r>"#,
2256 br#"<q:pPr lvl="9"/>"#,
2257 br#"<q:pPr marL="-1"/>"#,
2258 br#"<q:pPr indent="51206401"/>"#,
2259 br#"<q:pPr algn="middle"/>"#,
2260 br#"<q:pPr><q:lnSpc/></q:pPr>"#,
2261 br#"<q:pPr><q:lnSpc><q:spcPts val="158401"/></q:lnSpc></q:pPr>"#,
2262 br#"<q:pPr><q:lnSpc><q:spcPct val="201170"/></q:lnSpc></q:pPr>"#,
2263 br#"<q:rPr sz="99"/>"#,
2264 br#"<q:rPr b="yes"/>"#,
2265 br#"<q:rPr u="triple"/>"#,
2266 br#"<q:rPr spc="400001"/>"#,
2267 r#"<q:rPr spc="éx"/>"#.as_bytes(),
2268 br#"<q:rPr baseline="100001"/>"#,
2269 br#"<q:rPr><q:latin/></q:rPr>"#,
2270 br#"<q:fld type="slidenum"/>"#,
2271 br#"<q:fld id="not-a-guid"/>"#,
2272 br#"<q:p><q:endParaRPr/><q:endParaRPr/></q:p>"#,
2273 br#"<q:notPPr/>"#,
2274 br#"<q:notRPr/>"#,
2275 ];
2276 for xml in cases {
2277 let result = panic::catch_unwind(|| match super::root_local_name(xml).as_deref() {
2278 Ok(b"r") => super::CT_RegularTextRun::from_xml(xml).map(|_| ()),
2279 Ok(b"pPr") => CT_TextParagraphProperties::from_xml(xml).map(|_| ()),
2280 Ok(b"rPr") => CT_TextCharacterProperties::from_xml(xml).map(|_| ()),
2281 Ok(b"fld") => super::CT_TextField::from_xml(xml).map(|_| ()),
2282 Ok(b"p") => CT_TextParagraph::from_xml(xml).map(|_| ()),
2283 Ok(b"notPPr") => CT_TextParagraphProperties::from_xml(xml).map(|_| ()),
2284 Ok(b"notRPr") => CT_TextCharacterProperties::from_xml(xml).map(|_| ()),
2285 _ => unreachable!(),
2286 });
2287 assert!(result.is_ok(), "text parser panicked");
2288 assert!(result.unwrap().is_err(), "malformed text parsed");
2289 }
2290 }
2291}