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 line_spacing: Option<TextSpacing>,
844 pub space_before: Option<TextSpacing>,
845 pub space_after: Option<TextSpacing>,
846 pub bullet: Option<TextBullet>,
847 pub default_run_properties: Option<CT_TextCharacterProperties>,
848 raw_attributes: Vec<(String, String)>,
849 raw_children: OrderedRawChildren,
850}
851
852impl CT_TextParagraphProperties {
853 pub fn from_xml(xml: &[u8]) -> Result<Self> {
855 let expected = root_local_name(xml)?;
856 if !is_paragraph_property_tag(&expected) {
857 return Err(TextError::UnexpectedElement(
858 String::from_utf8_lossy(&expected).into_owned(),
859 ));
860 }
861 parse_complete(xml, &expected, Self::from_element, Self::from_start)
862 }
863
864 fn from_start(start: &BytesStart<'_>) -> Result<Self> {
865 reject_conflicting_a_prefix(start)?;
866 let element = String::from_utf8_lossy(local_name(start.name().as_ref())).into_owned();
867 let level = text_attr(start, b"lvl")?
868 .map(|value| parse_i32_value(&element, "lvl", &value, 0, 8).map(|v| v as u8))
869 .transpose()?;
870 Ok(Self {
871 left_margin: parse_optional_i32(start, b"marL", 0, MAX_TEXT_MARGIN)?,
872 right_margin: parse_optional_i32(start, b"marR", 0, MAX_TEXT_MARGIN)?,
873 level,
874 indent: parse_optional_i32(start, b"indent", -MAX_TEXT_MARGIN, MAX_TEXT_MARGIN)?,
875 alignment: parse_optional_enum(start, b"algn", TextAlignment::parse)?,
876 raw_attributes: capture_raw_attributes(
877 start,
878 &[b"marL", b"marR", b"lvl", b"indent", b"algn"],
879 )?,
880 ..Self::default()
881 })
882 }
883
884 fn from_element(reader: &mut Reader<&[u8]>, start: &BytesStart<'_>) -> Result<Self> {
885 let expected = local_name(start.name().as_ref()).to_vec();
886 let mut properties = Self::from_start(start)?;
887 let mut boundary = 0;
888 let mut seen = [false; 10];
889 let mut buffer = Vec::new();
890 loop {
891 match reader
892 .read_event_into(&mut buffer)
893 .map_err(OxmlError::from)?
894 {
895 Event::Start(element) => {
896 let name = local_name(element.name().as_ref()).to_vec();
897 let raw = capture_element(reader, &element)?;
898 properties.capture_child(&name, raw, &mut boundary, &mut seen)?;
899 }
900 Event::Empty(element) => {
901 let name = local_name(element.name().as_ref()).to_vec();
902 let raw = capture_empty_element(&element)?;
903 properties.capture_child(&name, raw, &mut boundary, &mut seen)?;
904 }
905 Event::End(element) if matches_local_name(element.name().as_ref(), &expected) => {
906 return Ok(properties);
907 }
908 Event::Eof => return Err(missing_end(&String::from_utf8_lossy(&expected))),
909 _ => {}
910 }
911 buffer.clear();
912 }
913 }
914
915 fn capture_child(
916 &mut self,
917 name: &[u8],
918 raw: Vec<u8>,
919 boundary: &mut usize,
920 seen: &mut [bool; 10],
921 ) -> Result<()> {
922 let slot = paragraph_property_slot(name);
923 if let Some(slot) = slot {
924 if seen[slot - 1] {
925 return Err(duplicate(&String::from_utf8_lossy(name)));
926 }
927 seen[slot - 1] = true;
928 }
929 match name {
930 b"lnSpc" => self.line_spacing = Some(TextSpacing::from_xml(&raw, b"lnSpc")?),
931 b"spcBef" => self.space_before = Some(TextSpacing::from_xml(&raw, b"spcBef")?),
932 b"spcAft" => self.space_after = Some(TextSpacing::from_xml(&raw, b"spcAft")?),
933 b"defRPr" => {
934 self.default_run_properties = Some(CT_TextCharacterProperties::from_xml(&raw)?)
935 }
936 _ => {
937 let mut bullet = self.bullet.take().unwrap_or_default();
938 if bullet.capture_component(name, &raw)? {
939 self.bullet = Some(bullet);
940 } else {
941 if !bullet.is_empty() {
942 self.bullet = Some(bullet);
943 }
944 self.raw_children.push(
945 slot.map_or(*boundary, |slot| (*boundary).max(slot - 1)),
946 raw,
947 );
948 }
949 }
950 }
951 if let Some(slot) = slot {
952 *boundary = (*boundary).max(slot);
953 }
954 Ok(())
955 }
956
957 pub fn write_xml<W: Write>(&self, writer: &mut Writer<W>, tag: &str) -> Result<()> {
959 let mut start = BytesStart::new(tag);
960 if let Some(value) = self.left_margin {
961 parse_i32_value(tag, "marL", &value.to_string(), 0, MAX_TEXT_MARGIN)?;
962 push_owned_attr(&mut start, "marL", value.to_string());
963 }
964 if let Some(value) = self.right_margin {
965 parse_i32_value(tag, "marR", &value.to_string(), 0, MAX_TEXT_MARGIN)?;
966 push_owned_attr(&mut start, "marR", value.to_string());
967 }
968 if let Some(value) = self.level {
969 if value > 8 {
970 return Err(invalid_attribute(tag, "lvl", &value.to_string()));
971 }
972 push_owned_attr(&mut start, "lvl", value.to_string());
973 }
974 if let Some(value) = self.indent {
975 parse_i32_value(
976 tag,
977 "indent",
978 &value.to_string(),
979 -MAX_TEXT_MARGIN,
980 MAX_TEXT_MARGIN,
981 )?;
982 push_owned_attr(&mut start, "indent", value.to_string());
983 }
984 if let Some(value) = self.alignment {
985 start.push_attribute(("algn", value.as_str()));
986 }
987 push_raw_attributes(&mut start, &self.raw_attributes);
988
989 let modelled = self.line_spacing.is_some()
990 || self.space_before.is_some()
991 || self.space_after.is_some()
992 || self.bullet.is_some()
993 || self.default_run_properties.is_some();
994 if !modelled && self.raw_children.is_empty() {
995 return write_empty(writer, start);
996 }
997 write_start(writer, start)?;
998 emit_raw(writer, self.raw_children.at(0))?;
999 if let Some(spacing) = &self.line_spacing {
1000 spacing.write_xml(writer, "a:lnSpc")?;
1001 }
1002 emit_raw(writer, self.raw_children.at(1))?;
1003 if let Some(spacing) = &self.space_before {
1004 spacing.write_xml(writer, "a:spcBef")?;
1005 }
1006 emit_raw(writer, self.raw_children.at(2))?;
1007 if let Some(spacing) = &self.space_after {
1008 spacing.write_xml(writer, "a:spcAft")?;
1009 }
1010 emit_raw(writer, self.raw_children.at(3))?;
1011 if let Some(bullet) = &self.bullet {
1012 bullet.write_color(writer)?;
1013 }
1014 emit_raw(writer, self.raw_children.at(4))?;
1015 if let Some(bullet) = &self.bullet {
1016 bullet.write_size(writer)?;
1017 }
1018 emit_raw(writer, self.raw_children.at(5))?;
1019 if let Some(bullet) = &self.bullet {
1020 bullet.write_font(writer)?;
1021 }
1022 emit_raw(writer, self.raw_children.at(6))?;
1023 if let Some(bullet) = &self.bullet {
1024 bullet.write_choice(writer)?;
1025 }
1026 emit_raw(writer, self.raw_children.at(7))?;
1027 emit_raw(writer, self.raw_children.at(8))?;
1028 if let Some(properties) = &self.default_run_properties {
1029 properties.write_xml(writer, "a:defRPr")?;
1030 }
1031 emit_raw(writer, self.raw_children.at(9))?;
1032 emit_raw(writer, self.raw_children.at(10))?;
1033 write_end(writer, tag)
1034 }
1035
1036 pub fn raw_children(&self) -> &OrderedRawChildren {
1037 &self.raw_children
1038 }
1039}
1040
1041fn paragraph_property_slot(name: &[u8]) -> Option<usize> {
1042 match name {
1043 b"lnSpc" => Some(1),
1044 b"spcBef" => Some(2),
1045 b"spcAft" => Some(3),
1046 b"buClrTx" | b"buClr" => Some(4),
1047 b"buSzTx" | b"buSzPct" | b"buSzPts" => Some(5),
1048 b"buFontTx" | b"buFont" => Some(6),
1049 b"buNone" | b"buAutoNum" | b"buChar" | b"buBlip" => Some(7),
1050 b"tabLst" => Some(8),
1051 b"defRPr" => Some(9),
1052 b"extLst" => Some(10),
1053 _ => None,
1054 }
1055}
1056
1057fn is_paragraph_property_tag(name: &[u8]) -> bool {
1058 matches!(
1059 name,
1060 b"pPr"
1061 | b"defPPr"
1062 | b"lvl1pPr"
1063 | b"lvl2pPr"
1064 | b"lvl3pPr"
1065 | b"lvl4pPr"
1066 | b"lvl5pPr"
1067 | b"lvl6pPr"
1068 | b"lvl7pPr"
1069 | b"lvl8pPr"
1070 | b"lvl9pPr"
1071 )
1072}
1073
1074#[allow(non_camel_case_types)]
1076#[derive(Clone, Debug, Eq, PartialEq)]
1077pub struct CT_RegularTextRun {
1078 pub properties: Option<CT_TextCharacterProperties>,
1079 pub text: TextValue,
1080 raw_children: OrderedRawChildren,
1081}
1082
1083impl CT_RegularTextRun {
1084 pub fn new(text: impl Into<String>) -> Self {
1086 Self {
1087 properties: None,
1088 text: TextValue {
1089 value: text.into(),
1090 ..TextValue::default()
1091 },
1092 raw_children: OrderedRawChildren::default(),
1093 }
1094 }
1095
1096 pub fn set_text(&mut self, text: &str) {
1098 self.text.value = text.to_owned();
1099 }
1100
1101 pub fn from_xml(xml: &[u8]) -> Result<Self> {
1103 parse_complete(xml, b"r", Self::from_element, |_| {
1104 Err(TextError::UnexpectedElement("r".to_owned()))
1105 })
1106 }
1107
1108 fn from_element(reader: &mut Reader<&[u8]>, _start: &BytesStart<'_>) -> Result<Self> {
1109 let mut properties = None;
1110 let mut text = None;
1111 let mut raw_children = OrderedRawChildren::default();
1112 let mut boundary = 0;
1113 let mut buffer = Vec::new();
1114 loop {
1115 match reader
1116 .read_event_into(&mut buffer)
1117 .map_err(OxmlError::from)?
1118 {
1119 Event::Start(element) if matches_local_name(element.name().as_ref(), b"rPr") => {
1120 if properties.is_some() {
1121 return Err(duplicate("rPr"));
1122 }
1123 properties = Some(CT_TextCharacterProperties::from_element(reader, &element)?);
1124 boundary = boundary.max(1);
1125 }
1126 Event::Empty(element) if matches_local_name(element.name().as_ref(), b"rPr") => {
1127 if properties.is_some() {
1128 return Err(duplicate("rPr"));
1129 }
1130 properties = Some(CT_TextCharacterProperties::from_start(&element)?);
1131 boundary = boundary.max(1);
1132 }
1133 Event::Start(element) if matches_local_name(element.name().as_ref(), b"t") => {
1134 if text.is_some() {
1135 return Err(duplicate("t"));
1136 }
1137 text = Some(TextValue::from_element(reader, &element)?);
1138 boundary = boundary.max(2);
1139 }
1140 Event::Empty(element) if matches_local_name(element.name().as_ref(), b"t") => {
1141 if text.is_some() {
1142 return Err(duplicate("t"));
1143 }
1144 text = Some(TextValue::from_start(&element)?);
1145 boundary = boundary.max(2);
1146 }
1147 Event::Start(element) => {
1148 raw_children.push(boundary, capture_element(reader, &element)?)
1149 }
1150 Event::Empty(element) => {
1151 raw_children.push(boundary, capture_empty_element(&element)?)
1152 }
1153 Event::End(element) if matches_local_name(element.name().as_ref(), b"r") => break,
1154 Event::Eof => return Err(missing_end("r")),
1155 _ => {}
1156 }
1157 buffer.clear();
1158 }
1159 Ok(Self {
1160 properties,
1161 text: text.ok_or_else(|| {
1162 TextError::Xml(OxmlError::MissingElement("DrawingML run text".to_owned()))
1163 })?,
1164 raw_children,
1165 })
1166 }
1167
1168 fn write_xml<W: Write>(&self, writer: &mut Writer<W>) -> Result<()> {
1169 write_start(writer, BytesStart::new("a:r"))?;
1170 emit_raw(writer, self.raw_children.at(0))?;
1171 if let Some(properties) = &self.properties {
1172 properties.write_xml(writer, "a:rPr")?;
1173 }
1174 emit_raw(writer, self.raw_children.at(1))?;
1175 self.text.write_xml(writer)?;
1176 emit_raw(writer, self.raw_children.at(2))?;
1177 write_end(writer, "a:r")
1178 }
1179}
1180
1181#[allow(non_camel_case_types)]
1183#[derive(Clone, Debug, Default, Eq, PartialEq)]
1184pub struct CT_TextLineBreak {
1185 pub properties: Option<CT_TextCharacterProperties>,
1186 raw_children: OrderedRawChildren,
1187}
1188
1189impl CT_TextLineBreak {
1190 pub fn from_xml(xml: &[u8]) -> Result<Self> {
1192 parse_complete(xml, b"br", Self::from_element, |_| Ok(Self::default()))
1193 }
1194
1195 fn from_element(reader: &mut Reader<&[u8]>, _start: &BytesStart<'_>) -> Result<Self> {
1196 let mut line_break = Self::default();
1197 let mut boundary = 0;
1198 let mut buffer = Vec::new();
1199 loop {
1200 match reader
1201 .read_event_into(&mut buffer)
1202 .map_err(OxmlError::from)?
1203 {
1204 Event::Start(element) if matches_local_name(element.name().as_ref(), b"rPr") => {
1205 if line_break.properties.is_some() {
1206 return Err(duplicate("rPr"));
1207 }
1208 line_break.properties =
1209 Some(CT_TextCharacterProperties::from_element(reader, &element)?);
1210 boundary = 1;
1211 }
1212 Event::Empty(element) if matches_local_name(element.name().as_ref(), b"rPr") => {
1213 if line_break.properties.is_some() {
1214 return Err(duplicate("rPr"));
1215 }
1216 line_break.properties = Some(CT_TextCharacterProperties::from_start(&element)?);
1217 boundary = 1;
1218 }
1219 Event::Start(element) => line_break
1220 .raw_children
1221 .push(boundary, capture_element(reader, &element)?),
1222 Event::Empty(element) => line_break
1223 .raw_children
1224 .push(boundary, capture_empty_element(&element)?),
1225 Event::End(element) if matches_local_name(element.name().as_ref(), b"br") => {
1226 return Ok(line_break);
1227 }
1228 Event::Eof => return Err(missing_end("br")),
1229 _ => {}
1230 }
1231 buffer.clear();
1232 }
1233 }
1234
1235 fn write_xml<W: Write>(&self, writer: &mut Writer<W>) -> Result<()> {
1236 if self.properties.is_none() && self.raw_children.is_empty() {
1237 return write_empty(writer, BytesStart::new("a:br"));
1238 }
1239 write_start(writer, BytesStart::new("a:br"))?;
1240 emit_raw(writer, self.raw_children.at(0))?;
1241 if let Some(properties) = &self.properties {
1242 properties.write_xml(writer, "a:rPr")?;
1243 }
1244 emit_raw(writer, self.raw_children.at(1))?;
1245 write_end(writer, "a:br")
1246 }
1247}
1248
1249#[allow(non_camel_case_types)]
1251#[derive(Clone, Debug, Eq, PartialEq)]
1252pub struct CT_TextField {
1253 pub id: String,
1254 pub field_type: Option<String>,
1255 pub run_properties: Option<CT_TextCharacterProperties>,
1256 pub paragraph_properties: Option<CT_TextParagraphProperties>,
1257 pub text: Option<TextValue>,
1258 raw_children: OrderedRawChildren,
1259}
1260
1261impl CT_TextField {
1262 pub fn from_xml(xml: &[u8]) -> Result<Self> {
1264 parse_complete(xml, b"fld", Self::from_element, Self::from_empty)
1265 }
1266
1267 fn from_start(start: &BytesStart<'_>) -> Result<Self> {
1268 let id = required_attr(start, b"id")?;
1269 if !is_guid(&id) {
1270 return Err(invalid_attribute("fld", "id", &id));
1271 }
1272 Ok(Self {
1273 id,
1274 field_type: text_attr(start, b"type")?,
1275 run_properties: None,
1276 paragraph_properties: None,
1277 text: None,
1278 raw_children: OrderedRawChildren::default(),
1279 })
1280 }
1281
1282 fn from_empty(start: &BytesStart<'_>) -> Result<Self> {
1283 Self::from_start(start)
1284 }
1285
1286 fn from_element(reader: &mut Reader<&[u8]>, start: &BytesStart<'_>) -> Result<Self> {
1287 let mut field = Self::from_start(start)?;
1288 let mut boundary = 0;
1289 let mut buffer = Vec::new();
1290 loop {
1291 match reader
1292 .read_event_into(&mut buffer)
1293 .map_err(OxmlError::from)?
1294 {
1295 Event::Start(element) if matches_local_name(element.name().as_ref(), b"rPr") => {
1296 if field.run_properties.is_some() {
1297 return Err(duplicate("rPr"));
1298 }
1299 field.run_properties =
1300 Some(CT_TextCharacterProperties::from_element(reader, &element)?);
1301 boundary = boundary.max(1);
1302 }
1303 Event::Empty(element) if matches_local_name(element.name().as_ref(), b"rPr") => {
1304 if field.run_properties.is_some() {
1305 return Err(duplicate("rPr"));
1306 }
1307 field.run_properties = Some(CT_TextCharacterProperties::from_start(&element)?);
1308 boundary = boundary.max(1);
1309 }
1310 Event::Start(element) if matches_local_name(element.name().as_ref(), b"pPr") => {
1311 if field.paragraph_properties.is_some() {
1312 return Err(duplicate("pPr"));
1313 }
1314 field.paragraph_properties =
1315 Some(CT_TextParagraphProperties::from_element(reader, &element)?);
1316 boundary = boundary.max(2);
1317 }
1318 Event::Empty(element) if matches_local_name(element.name().as_ref(), b"pPr") => {
1319 if field.paragraph_properties.is_some() {
1320 return Err(duplicate("pPr"));
1321 }
1322 field.paragraph_properties =
1323 Some(CT_TextParagraphProperties::from_start(&element)?);
1324 boundary = boundary.max(2);
1325 }
1326 Event::Start(element) if matches_local_name(element.name().as_ref(), b"t") => {
1327 if field.text.is_some() {
1328 return Err(duplicate("t"));
1329 }
1330 field.text = Some(TextValue::from_element(reader, &element)?);
1331 boundary = boundary.max(3);
1332 }
1333 Event::Empty(element) if matches_local_name(element.name().as_ref(), b"t") => {
1334 if field.text.is_some() {
1335 return Err(duplicate("t"));
1336 }
1337 field.text = Some(TextValue::from_start(&element)?);
1338 boundary = boundary.max(3);
1339 }
1340 Event::Start(element) => field
1341 .raw_children
1342 .push(boundary, capture_element(reader, &element)?),
1343 Event::Empty(element) => field
1344 .raw_children
1345 .push(boundary, capture_empty_element(&element)?),
1346 Event::End(element) if matches_local_name(element.name().as_ref(), b"fld") => {
1347 return Ok(field);
1348 }
1349 Event::Eof => return Err(missing_end("fld")),
1350 _ => {}
1351 }
1352 buffer.clear();
1353 }
1354 }
1355
1356 fn write_xml<W: Write>(&self, writer: &mut Writer<W>) -> Result<()> {
1357 if !is_guid(&self.id) {
1358 return Err(invalid_attribute("fld", "id", &self.id));
1359 }
1360 let mut start = BytesStart::new("a:fld");
1361 start.push_attribute(("id", self.id.as_str()));
1362 push_optional_attr(&mut start, "type", self.field_type.as_deref());
1363 let modelled = self.run_properties.is_some()
1364 || self.paragraph_properties.is_some()
1365 || self.text.is_some();
1366 if !modelled && self.raw_children.is_empty() {
1367 return write_empty(writer, start);
1368 }
1369 write_start(writer, start)?;
1370 emit_raw(writer, self.raw_children.at(0))?;
1371 if let Some(properties) = &self.run_properties {
1372 properties.write_xml(writer, "a:rPr")?;
1373 }
1374 emit_raw(writer, self.raw_children.at(1))?;
1375 if let Some(properties) = &self.paragraph_properties {
1376 properties.write_xml(writer, "a:pPr")?;
1377 }
1378 emit_raw(writer, self.raw_children.at(2))?;
1379 if let Some(text) = &self.text {
1380 text.write_xml(writer)?;
1381 }
1382 emit_raw(writer, self.raw_children.at(3))?;
1383 write_end(writer, "a:fld")
1384 }
1385}
1386
1387#[derive(Clone, Debug, Eq, PartialEq)]
1389pub enum TextRun {
1390 Run(CT_RegularTextRun),
1391 Break(CT_TextLineBreak),
1392 Field(Box<CT_TextField>),
1393}
1394
1395impl TextRun {
1396 fn from_xml(xml: &[u8], name: &[u8]) -> Result<Self> {
1397 match name {
1398 b"r" => Ok(Self::Run(CT_RegularTextRun::from_xml(xml)?)),
1399 b"br" => Ok(Self::Break(CT_TextLineBreak::from_xml(xml)?)),
1400 b"fld" => Ok(Self::Field(Box::new(CT_TextField::from_xml(xml)?))),
1401 _ => Err(TextError::UnexpectedElement(
1402 String::from_utf8_lossy(name).into_owned(),
1403 )),
1404 }
1405 }
1406
1407 fn write_xml<W: Write>(&self, writer: &mut Writer<W>) -> Result<()> {
1408 match self {
1409 Self::Run(run) => run.write_xml(writer),
1410 Self::Break(line_break) => line_break.write_xml(writer),
1411 Self::Field(field) => field.write_xml(writer),
1412 }
1413 }
1414}
1415
1416#[allow(non_camel_case_types)]
1418#[derive(Clone, Debug, Default, Eq, PartialEq)]
1419pub struct CT_TextParagraph {
1420 pub properties: Option<CT_TextParagraphProperties>,
1421 pub runs: Vec<TextRun>,
1422 pub end_properties: Option<CT_TextCharacterProperties>,
1423 raw_children: OrderedRawChildren,
1424}
1425
1426impl CT_TextParagraph {
1427 pub fn properties_mut(&mut self) -> &mut CT_TextParagraphProperties {
1429 if self.properties.is_none() {
1430 let mut raw_children = OrderedRawChildren::default();
1431 for boundary in 0..=2 + self.runs.len() {
1432 let new_boundary = if boundary == 0 { 1 } else { boundary };
1433 for child in self.raw_children.at(boundary) {
1434 raw_children.push(new_boundary, child.to_vec());
1435 }
1436 }
1437 self.raw_children = raw_children;
1438 self.properties = Some(CT_TextParagraphProperties::default());
1439 }
1440 self.properties
1441 .as_mut()
1442 .expect("paragraph properties were inserted")
1443 }
1444
1445 pub fn set_text(&mut self, text: &str) {
1447 let old_run_count = self.runs.len();
1448 let mut raw_children = OrderedRawChildren::default();
1449 for boundary in 0..=2 + old_run_count {
1450 let new_boundary = if boundary <= 1 {
1451 boundary
1452 } else if boundary <= 1 + old_run_count {
1453 2
1454 } else {
1455 3
1456 };
1457 for child in self.raw_children.at(boundary) {
1458 raw_children.push(new_boundary, child.to_vec());
1459 }
1460 }
1461 self.raw_children = raw_children;
1462 self.runs = vec![TextRun::Run(CT_RegularTextRun::new(text))];
1463 }
1464
1465 pub fn add_run(&mut self, text: &str) -> &mut CT_RegularTextRun {
1467 self.raw_children.shift_boundaries_from(2 + self.runs.len());
1468 self.runs.push(TextRun::Run(CT_RegularTextRun::new(text)));
1469 let Some(TextRun::Run(run)) = self.runs.last_mut() else {
1470 unreachable!("the appended text choice is a regular run")
1471 };
1472 run
1473 }
1474
1475 pub fn from_xml(xml: &[u8]) -> Result<Self> {
1477 parse_complete(xml, b"p", Self::from_element, |_| Ok(Self::default()))
1478 }
1479
1480 fn from_element(reader: &mut Reader<&[u8]>, _start: &BytesStart<'_>) -> Result<Self> {
1481 let mut paragraph = Self::default();
1482 let mut boundary = 0;
1483 let mut buffer = Vec::new();
1484 loop {
1485 match reader
1486 .read_event_into(&mut buffer)
1487 .map_err(OxmlError::from)?
1488 {
1489 Event::Start(element) if matches_local_name(element.name().as_ref(), b"pPr") => {
1490 if paragraph.properties.is_some() {
1491 return Err(duplicate("pPr"));
1492 }
1493 paragraph.properties =
1494 Some(CT_TextParagraphProperties::from_element(reader, &element)?);
1495 boundary = boundary.max(1);
1496 }
1497 Event::Empty(element) if matches_local_name(element.name().as_ref(), b"pPr") => {
1498 if paragraph.properties.is_some() {
1499 return Err(duplicate("pPr"));
1500 }
1501 paragraph.properties = Some(CT_TextParagraphProperties::from_start(&element)?);
1502 boundary = boundary.max(1);
1503 }
1504 Event::Start(element) if is_text_run(element.name().as_ref()) => {
1505 let name = local_name(element.name().as_ref()).to_vec();
1506 let raw = capture_element(reader, &element)?;
1507 paragraph.runs.push(TextRun::from_xml(&raw, &name)?);
1508 boundary = boundary.max(1 + paragraph.runs.len());
1509 }
1510 Event::Empty(element) if is_text_run(element.name().as_ref()) => {
1511 let name = local_name(element.name().as_ref()).to_vec();
1512 let raw = capture_empty_element(&element)?;
1513 paragraph.runs.push(TextRun::from_xml(&raw, &name)?);
1514 boundary = boundary.max(1 + paragraph.runs.len());
1515 }
1516 Event::Start(element)
1517 if matches_local_name(element.name().as_ref(), b"endParaRPr") =>
1518 {
1519 if paragraph.end_properties.is_some() {
1520 return Err(duplicate("endParaRPr"));
1521 }
1522 paragraph.end_properties =
1523 Some(CT_TextCharacterProperties::from_element(reader, &element)?);
1524 boundary = boundary.max(2 + paragraph.runs.len());
1525 }
1526 Event::Empty(element)
1527 if matches_local_name(element.name().as_ref(), b"endParaRPr") =>
1528 {
1529 if paragraph.end_properties.is_some() {
1530 return Err(duplicate("endParaRPr"));
1531 }
1532 paragraph.end_properties =
1533 Some(CT_TextCharacterProperties::from_start(&element)?);
1534 boundary = boundary.max(2 + paragraph.runs.len());
1535 }
1536 Event::Start(element) => paragraph
1537 .raw_children
1538 .push(boundary, capture_element(reader, &element)?),
1539 Event::Empty(element) => paragraph
1540 .raw_children
1541 .push(boundary, capture_empty_element(&element)?),
1542 Event::End(element) if matches_local_name(element.name().as_ref(), b"p") => {
1543 return Ok(paragraph);
1544 }
1545 Event::Eof => return Err(missing_end("p")),
1546 _ => {}
1547 }
1548 buffer.clear();
1549 }
1550 }
1551
1552 pub fn write_xml<W: Write>(&self, writer: &mut Writer<W>) -> Result<()> {
1554 if self.properties.is_none()
1555 && self.runs.is_empty()
1556 && self.end_properties.is_none()
1557 && self.raw_children.is_empty()
1558 {
1559 return write_empty(writer, BytesStart::new("a:p"));
1560 }
1561 write_start(writer, BytesStart::new("a:p"))?;
1562 emit_raw(writer, self.raw_children.at(0))?;
1563 if let Some(properties) = &self.properties {
1564 properties.write_xml(writer, "a:pPr")?;
1565 }
1566 emit_raw(writer, self.raw_children.at(1))?;
1567 for (index, run) in self.runs.iter().enumerate() {
1568 run.write_xml(writer)?;
1569 emit_raw(writer, self.raw_children.at(2 + index))?;
1570 }
1571 if let Some(properties) = &self.end_properties {
1572 properties.write_xml(writer, "a:endParaRPr")?;
1573 }
1574 emit_raw(writer, self.raw_children.at(2 + self.runs.len()))?;
1575 write_end(writer, "a:p")
1576 }
1577
1578 pub fn to_xml(&self) -> Result<Vec<u8>> {
1580 let mut writer = Writer::new(Vec::new());
1581 self.write_xml(&mut writer)?;
1582 Ok(writer.into_inner())
1583 }
1584
1585 pub fn raw_children(&self) -> &OrderedRawChildren {
1586 &self.raw_children
1587 }
1588}
1589
1590fn is_text_run(name: &[u8]) -> bool {
1591 matches!(local_name(name), b"r" | b"br" | b"fld")
1592}
1593
1594fn parse_complete<T>(
1595 xml: &[u8],
1596 expected: &[u8],
1597 parse_start: impl FnOnce(&mut Reader<&[u8]>, &BytesStart<'_>) -> Result<T>,
1598 parse_empty: impl FnOnce(&BytesStart<'_>) -> Result<T>,
1599) -> Result<T> {
1600 let mut reader = Reader::from_reader(xml);
1601 let mut buffer = Vec::new();
1602 loop {
1603 match reader
1604 .read_event_into(&mut buffer)
1605 .map_err(OxmlError::from)?
1606 {
1607 Event::Start(element) if matches_local_name(element.name().as_ref(), expected) => {
1608 reject_conflicting_a_prefix(&element)?;
1609 return parse_start(&mut reader, &element);
1610 }
1611 Event::Empty(element) if matches_local_name(element.name().as_ref(), expected) => {
1612 reject_conflicting_a_prefix(&element)?;
1613 return parse_empty(&element);
1614 }
1615 Event::Start(element) | Event::Empty(element) => return Err(unexpected(&element)),
1616 Event::Eof => {
1617 return Err(TextError::Xml(OxmlError::MissingElement(
1618 String::from_utf8_lossy(expected).into_owned(),
1619 )));
1620 }
1621 _ => {}
1622 }
1623 buffer.clear();
1624 }
1625}
1626
1627fn root_local_name(xml: &[u8]) -> Result<Vec<u8>> {
1628 let mut reader = Reader::from_reader(xml);
1629 let mut buffer = Vec::new();
1630 loop {
1631 match reader
1632 .read_event_into(&mut buffer)
1633 .map_err(OxmlError::from)?
1634 {
1635 Event::Start(element) | Event::Empty(element) => {
1636 return Ok(local_name(element.name().as_ref()).to_vec());
1637 }
1638 Event::Eof => {
1639 return Err(TextError::Xml(OxmlError::MissingElement(
1640 "DrawingML text properties".to_owned(),
1641 )));
1642 }
1643 _ => {}
1644 }
1645 buffer.clear();
1646 }
1647}
1648
1649fn ensure_empty(reader: &mut Reader<&[u8]>, expected: &[u8]) -> Result<()> {
1650 let mut buffer = Vec::new();
1651 loop {
1652 match reader
1653 .read_event_into(&mut buffer)
1654 .map_err(OxmlError::from)?
1655 {
1656 Event::End(element) if matches_local_name(element.name().as_ref(), expected) => {
1657 return Ok(());
1658 }
1659 Event::Text(text) if text.iter().all(u8::is_ascii_whitespace) => {}
1660 Event::Comment(_) => {}
1661 Event::Start(element) | Event::Empty(element) => return Err(unexpected(&element)),
1662 Event::Eof => return Err(missing_end(&String::from_utf8_lossy(expected))),
1663 _ => {
1664 return Err(TextError::UnexpectedElement(
1665 String::from_utf8_lossy(expected).into_owned(),
1666 ));
1667 }
1668 }
1669 buffer.clear();
1670 }
1671}
1672
1673fn text_attr(start: &BytesStart<'_>, name: &[u8]) -> Result<Option<String>> {
1674 for attribute in start.attributes() {
1675 let attribute = attribute.map_err(OxmlError::from)?;
1676 if attribute.key.as_ref() == name {
1677 let value = attribute
1678 .decoded_and_normalized_value(XmlVersion::Implicit1_0, start.decoder())
1679 .map_err(OxmlError::from)?;
1680 return Ok(Some(value.into_owned()));
1681 }
1682 }
1683 Ok(None)
1684}
1685
1686fn capture_raw_attributes(
1687 start: &BytesStart<'_>,
1688 modelled: &[&[u8]],
1689) -> Result<Vec<(String, String)>> {
1690 let mut raw = Vec::new();
1691 for attribute in start.attributes() {
1692 let attribute = attribute.map_err(OxmlError::from)?;
1693 if modelled.iter().any(|name| attribute.key.as_ref() == *name) {
1694 continue;
1695 }
1696 let name = std::str::from_utf8(attribute.key.as_ref())
1697 .map_err(OxmlError::from)?
1698 .to_owned();
1699 let value = attribute
1700 .decoded_and_normalized_value(XmlVersion::Implicit1_0, start.decoder())
1701 .map_err(OxmlError::from)?
1702 .into_owned();
1703 raw.push((name, value));
1704 }
1705 Ok(raw)
1706}
1707
1708fn push_raw_attributes(start: &mut BytesStart<'_>, attributes: &[(String, String)]) {
1709 for (name, value) in attributes {
1710 start.push_attribute((name.as_str(), value.as_str()));
1711 }
1712}
1713
1714fn required_attr(start: &BytesStart<'_>, name: &[u8]) -> Result<String> {
1715 text_attr(start, name)?
1716 .filter(|value| !value.is_empty())
1717 .ok_or_else(|| {
1718 missing_attribute(
1719 &String::from_utf8_lossy(local_name(start.name().as_ref())),
1720 &String::from_utf8_lossy(name),
1721 )
1722 })
1723}
1724
1725fn parse_optional_i32(
1726 start: &BytesStart<'_>,
1727 name: &[u8],
1728 min: i32,
1729 max: i32,
1730) -> Result<Option<i32>> {
1731 text_attr(start, name)?
1732 .map(|value| {
1733 parse_i32_value(
1734 &String::from_utf8_lossy(local_name(start.name().as_ref())),
1735 &String::from_utf8_lossy(name),
1736 &value,
1737 min,
1738 max,
1739 )
1740 })
1741 .transpose()
1742}
1743
1744fn parse_i32_attr(start: &BytesStart<'_>, name: &[u8], min: i32, max: i32) -> Result<i32> {
1745 let value = required_attr(start, name)?;
1746 parse_i32_value(
1747 &String::from_utf8_lossy(local_name(start.name().as_ref())),
1748 &String::from_utf8_lossy(name),
1749 &value,
1750 min,
1751 max,
1752 )
1753}
1754
1755fn parse_i32_value(element: &str, attribute: &str, value: &str, min: i32, max: i32) -> Result<i32> {
1756 let parsed = value
1757 .parse::<i32>()
1758 .map_err(|_| invalid_attribute(element, attribute, value))?;
1759 if (min..=max).contains(&parsed) {
1760 Ok(parsed)
1761 } else {
1762 Err(invalid_attribute(element, attribute, value))
1763 }
1764}
1765
1766fn parse_optional_bool(start: &BytesStart<'_>, name: &[u8]) -> Result<Option<bool>> {
1767 text_attr(start, name)?
1768 .map(|value| match value.as_str() {
1769 "true" | "1" => Ok(true),
1770 "false" | "0" => Ok(false),
1771 _ => Err(invalid_attribute(
1772 &String::from_utf8_lossy(local_name(start.name().as_ref())),
1773 &String::from_utf8_lossy(name),
1774 &value,
1775 )),
1776 })
1777 .transpose()
1778}
1779
1780fn parse_optional_enum<T>(
1781 start: &BytesStart<'_>,
1782 name: &[u8],
1783 parse: impl FnOnce(&str) -> Option<T>,
1784) -> Result<Option<T>> {
1785 let Some(value) = text_attr(start, name)? else {
1786 return Ok(None);
1787 };
1788 parse(&value).map(Some).ok_or_else(|| {
1789 invalid_attribute(
1790 &String::from_utf8_lossy(local_name(start.name().as_ref())),
1791 &String::from_utf8_lossy(name),
1792 &value,
1793 )
1794 })
1795}
1796
1797fn validate_spacing_percent(value: &str) -> Result<()> {
1798 if is_percentage_string(value) {
1799 return Ok(());
1800 }
1801 parse_i32_value("spcPct", "val", value, 0, MAX_TRANSITIONAL_SPACING_PERCENT)?;
1802 Ok(())
1803}
1804
1805fn validate_baseline(value: &str) -> Result<()> {
1806 if is_percentage_string(value) {
1807 return Ok(());
1808 }
1809 parse_i32_value("rPr", "baseline", value, -100_000, 100_000)?;
1810 Ok(())
1811}
1812
1813fn is_percentage_string(value: &str) -> bool {
1814 value.strip_suffix('%').is_some_and(is_signed_decimal)
1815}
1816
1817fn is_universal_measure(value: &str) -> bool {
1818 ["mm", "cm", "in", "pt", "pc", "pi"]
1819 .iter()
1820 .find_map(|unit| value.strip_suffix(unit))
1821 .is_some_and(is_signed_decimal)
1822}
1823
1824fn is_signed_decimal(value: &str) -> bool {
1825 let unsigned = value.strip_prefix('-').unwrap_or(value);
1826 let mut parts = unsigned.split('.');
1827 let Some(integer) = parts.next() else {
1828 return false;
1829 };
1830 if integer.is_empty() || !integer.bytes().all(|byte| byte.is_ascii_digit()) {
1831 return false;
1832 }
1833 if let Some(fraction) = parts.next()
1834 && (fraction.is_empty() || !fraction.bytes().all(|byte| byte.is_ascii_digit()))
1835 {
1836 return false;
1837 }
1838 parts.next().is_none()
1839}
1840
1841fn is_guid(value: &str) -> bool {
1842 if value.len() != 38 || !value.starts_with('{') || !value.ends_with('}') {
1843 return false;
1844 }
1845 value.bytes().enumerate().all(|(index, byte)| match index {
1846 0 => byte == b'{',
1847 37 => byte == b'}',
1848 9 | 14 | 19 | 24 => byte == b'-',
1849 _ => byte.is_ascii_hexdigit(),
1850 })
1851}
1852
1853fn push_optional_attr<'a>(start: &mut BytesStart<'a>, name: &'a str, value: Option<&'a str>) {
1854 if let Some(value) = value {
1855 start.push_attribute((name, value));
1856 }
1857}
1858
1859fn push_optional_bool(start: &mut BytesStart<'_>, name: &'static str, value: Option<bool>) {
1860 if let Some(value) = value {
1861 start.push_attribute((name, if value { "1" } else { "0" }));
1862 }
1863}
1864
1865fn push_owned_attr(start: &mut BytesStart<'_>, name: &'static str, value: String) {
1866 start.push_attribute((name, value.as_str()));
1867}
1868
1869fn emit_raw<'a, W: Write>(
1870 writer: &mut Writer<W>,
1871 children: impl Iterator<Item = &'a [u8]>,
1872) -> Result<()> {
1873 for child in children {
1874 writer.get_mut().write_all(child).map_err(OxmlError::from)?;
1875 }
1876 Ok(())
1877}
1878
1879fn write_start<W: Write>(writer: &mut Writer<W>, start: BytesStart<'_>) -> Result<()> {
1880 writer
1881 .write_event(Event::Start(start))
1882 .map_err(OxmlError::from)?;
1883 Ok(())
1884}
1885
1886fn write_empty<W: Write>(writer: &mut Writer<W>, start: BytesStart<'_>) -> Result<()> {
1887 writer
1888 .write_event(Event::Empty(start))
1889 .map_err(OxmlError::from)?;
1890 Ok(())
1891}
1892
1893fn write_end<W: Write>(writer: &mut Writer<W>, tag: &str) -> Result<()> {
1894 writer
1895 .write_event(Event::End(BytesEnd::new(tag)))
1896 .map_err(OxmlError::from)?;
1897 Ok(())
1898}
1899
1900fn unexpected(element: &BytesStart<'_>) -> TextError {
1901 TextError::UnexpectedElement(String::from_utf8_lossy(element.name().as_ref()).into_owned())
1902}
1903
1904fn duplicate(element: &str) -> TextError {
1905 TextError::DuplicateElement(element.to_owned())
1906}
1907
1908fn missing_attribute(element: &str, attribute: &str) -> TextError {
1909 TextError::MissingAttribute {
1910 element: element.to_owned(),
1911 attribute: attribute.to_owned(),
1912 }
1913}
1914
1915fn invalid_attribute(element: &str, attribute: &str, value: &str) -> TextError {
1916 TextError::InvalidAttribute {
1917 element: element.to_owned(),
1918 attribute: attribute.to_owned(),
1919 value: value.to_owned(),
1920 }
1921}
1922
1923#[cfg(test)]
1924mod tests {
1925 use std::panic;
1926
1927 use super::{
1928 CT_TextCharacterProperties, CT_TextParagraph, CT_TextParagraphProperties, TextRun,
1929 TextSpace, TextSpacing,
1930 };
1931 use crate::color::ColorChoice;
1932 use crate::text::CT_TextBody;
1933 use crate::text::{TextAutoNumberScheme, TextBulletChoice, TextBulletSizeValue};
1934
1935 #[test]
1936 fn leading_and_trailing_text_whitespace_survives_via_xml_space_preserve() {
1937 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>"#;
1938 let paragraph = CT_TextParagraph::from_xml(xml).unwrap();
1939 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>"#);
1940
1941 let TextRun::Run(first) = ¶graph.runs[0] else {
1942 panic!("expected regular run");
1943 };
1944 assert_eq!(first.text.value, " leading");
1945 assert_eq!(first.text.space, TextSpace::Preserve);
1946
1947 let body = CT_TextBody::from_xml(
1948 br#"<q:txBody><q:bodyPr/><q:p><q:r><q:t> body </q:t></q:r></q:p></q:txBody>"#,
1949 )
1950 .unwrap();
1951 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>"#);
1952
1953 let hostile = CT_TextParagraph::from_xml(
1954 br#"<q:p><q:r><q:t x:space="preserve">plain</q:t></q:r></q:p>"#,
1955 )
1956 .unwrap();
1957 let TextRun::Run(run) = &hostile.runs[0] else {
1958 panic!("expected regular run");
1959 };
1960 assert_eq!(run.text.space, TextSpace::Default);
1961 assert_eq!(
1962 hostile.to_xml().unwrap(),
1963 br#"<a:p><a:r><a:t x:space="preserve">plain</a:t></a:r></a:p>"#
1964 );
1965 }
1966
1967 #[test]
1968 fn canonical_text_state_matches_whitespace_and_empty_hyperlinks_from_real_decks() {
1969 let paragraph = CT_TextParagraph::from_xml(
1970 br#"<q:p><q:r><q:rPr><q:hlinkClick r:id=""/></q:rPr><q:t>trailing </q:t></q:r></q:p>"#,
1971 )
1972 .unwrap();
1973 let TextRun::Run(run) = ¶graph.runs[0] else {
1974 panic!("expected regular run");
1975 };
1976 assert_eq!(run.text.space, TextSpace::Preserve);
1977 assert_eq!(
1978 run.properties
1979 .as_ref()
1980 .and_then(|properties| properties.hyperlink_click.as_ref())
1981 .and_then(|hyperlink| hyperlink.relationship_id.as_deref()),
1982 Some("")
1983 );
1984 let written = paragraph.to_xml().unwrap();
1985 assert_eq!(CT_TextParagraph::from_xml(&written).unwrap(), paragraph);
1986 }
1987
1988 #[test]
1989 fn paragraph_runs_fields_and_breaks_round_trip_structurally() {
1990 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>"#;
1991 let paragraph = CT_TextParagraph::from_xml(xml).unwrap();
1992 assert_eq!(paragraph.runs.len(), 3);
1993 let written = paragraph.to_xml().unwrap();
1994 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>"#);
1995 assert_eq!(CT_TextParagraph::from_xml(&written).unwrap(), paragraph);
1996
1997 let hostile = CT_TextCharacterProperties::from_xml(
1998 br#"<q:rPr><q:hlinkClick x:id="not-a-relationship"/></q:rPr>"#,
1999 )
2000 .unwrap();
2001 assert!(
2002 hostile
2003 .hyperlink_click
2004 .as_ref()
2005 .unwrap()
2006 .relationship_id
2007 .is_none()
2008 );
2009 let mut writer = quick_xml::Writer::new(Vec::new());
2010 hostile.write_xml(&mut writer, "a:rPr").unwrap();
2011 assert_eq!(
2012 writer.into_inner(),
2013 br#"<a:rPr><a:hlinkClick x:id="not-a-relationship"/></a:rPr>"#
2014 );
2015 }
2016
2017 #[test]
2018 fn character_all_caps_round_trips_while_small_caps_stays_unmodelled() {
2019 let all = CT_TextCharacterProperties::from_xml(
2020 br#"<q:rPr cap="all" sz="1800" x:producer="kept"/>"#,
2021 )
2022 .unwrap();
2023 assert_eq!(all.all_caps, Some(true));
2024 let mut writer = quick_xml::Writer::new(Vec::new());
2025 all.write_xml(&mut writer, "a:rPr").unwrap();
2026 let written = writer.into_inner();
2027 assert_eq!(
2028 written,
2029 br#"<a:rPr sz="1800" cap="all" x:producer="kept"/>"#
2030 );
2031 assert_eq!(CT_TextCharacterProperties::from_xml(&written).unwrap(), all);
2032
2033 let none = CT_TextCharacterProperties::from_xml(br#"<q:rPr cap="none"/>"#).unwrap();
2034 assert_eq!(none.all_caps, Some(false));
2035
2036 let small = CT_TextCharacterProperties::from_xml(br#"<q:rPr cap="small"/>"#).unwrap();
2037 assert_eq!(small.all_caps, None);
2038 let mut writer = quick_xml::Writer::new(Vec::new());
2039 small.write_xml(&mut writer, "a:rPr").unwrap();
2040 assert_eq!(writer.into_inner(), br#"<a:rPr cap="small"/>"#);
2041 }
2042
2043 #[test]
2044 fn paragraph_and_run_properties_use_drawingml_units_and_schema_order() {
2045 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();
2046 let written = {
2047 let mut writer = quick_xml::Writer::new(Vec::new());
2048 paragraph_properties
2049 .write_xml(&mut writer, "a:pPr")
2050 .unwrap();
2051 writer.into_inner()
2052 };
2053 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>"#);
2054
2055 let run_properties = CT_TextCharacterProperties::from_xml(
2056 br#"<q:rPr sz="100" spc="1.25pt" baseline="-100000"/>"#,
2057 )
2058 .unwrap();
2059 let mut writer = quick_xml::Writer::new(Vec::new());
2060 run_properties.write_xml(&mut writer, "a:rPr").unwrap();
2061 assert_eq!(
2062 writer.into_inner(),
2063 br#"<a:rPr sz="100" spc="1.25pt" baseline="-100000"/>"#
2064 );
2065
2066 let mut inserted_spacing =
2067 CT_TextParagraphProperties::from_xml(br#"<q:pPr><q:buChar char="*"/></q:pPr>"#)
2068 .unwrap();
2069 inserted_spacing.line_spacing = Some(TextSpacing::Percent("100000".to_owned()));
2070 let mut writer = quick_xml::Writer::new(Vec::new());
2071 inserted_spacing.write_xml(&mut writer, "a:pPr").unwrap();
2072 assert_eq!(
2073 writer.into_inner(),
2074 br#"<a:pPr><a:lnSpc><a:spcPct val="100000"/></a:lnSpc><a:buChar char="*"/></a:pPr>"#
2075 );
2076 }
2077
2078 #[test]
2079 fn every_modelled_bullet_form_round_trips_in_schema_order() {
2080 let cases: &[(&[u8], &[u8])] = &[
2081 (
2082 br#"<q:pPr><q:buClr><q:srgbClr val="AABBCC"/></q:buClr><q:buSzPct val="125000"/><q:buFont typeface="Wingdings"/><q:buChar char="*"/></q:pPr>"#,
2083 br#"<a:pPr><a:buClr><a:srgbClr val="AABBCC"/></a:buClr><a:buSzPct val="125000"/><a:buFont typeface="Wingdings"/><a:buChar char="*"/></a:pPr>"#,
2084 ),
2085 (
2086 br#"<q:pPr><q:buSzPts val="1800"/><q:buAutoNum type="arabicPeriod" startAt="3"/></q:pPr>"#,
2087 br#"<a:pPr><a:buSzPts val="1800"/><a:buAutoNum type="arabicPeriod" startAt="3"/></a:pPr>"#,
2088 ),
2089 (br#"<q:pPr><q:buNone/></q:pPr>"#, br#"<a:pPr><a:buNone/></a:pPr>"#),
2090 ];
2091
2092 for (xml, expected) in cases {
2093 let properties = CT_TextParagraphProperties::from_xml(xml).unwrap();
2094 let mut writer = quick_xml::Writer::new(Vec::new());
2095 properties.write_xml(&mut writer, "a:pPr").unwrap();
2096 let written = writer.into_inner();
2097 assert_eq!(written, *expected);
2098 assert_eq!(
2099 CT_TextParagraphProperties::from_xml(&written).unwrap(),
2100 properties
2101 );
2102 }
2103
2104 let character = CT_TextParagraphProperties::from_xml(cases[0].0).unwrap();
2105 let bullet = character.bullet.as_ref().unwrap();
2106 assert!(matches!(
2107 bullet.choice,
2108 Some(TextBulletChoice::Character(_))
2109 ));
2110 assert_eq!(bullet.font.as_ref().unwrap().typeface, "Wingdings");
2111 assert!(matches!(
2112 bullet.size.as_ref().unwrap().value,
2113 TextBulletSizeValue::Percent(ref value) if value == "125000"
2114 ));
2115 assert!(matches!(
2116 bullet.color.as_ref().unwrap().color,
2117 ColorChoice::Srgb { .. }
2118 ));
2119
2120 let automatic = CT_TextParagraphProperties::from_xml(cases[1].0).unwrap();
2121 let Some(TextBulletChoice::AutoNumber(numbering)) =
2122 automatic.bullet.as_ref().unwrap().choice.as_ref()
2123 else {
2124 panic!("expected automatic-number bullet");
2125 };
2126 assert_eq!(numbering.scheme, TextAutoNumberScheme::ArabicPeriod);
2127 assert_eq!(numbering.start_at, Some(3));
2128 assert!(matches!(
2129 automatic
2130 .bullet
2131 .as_ref()
2132 .unwrap()
2133 .size
2134 .as_ref()
2135 .unwrap()
2136 .value,
2137 TextBulletSizeValue::Points(1800)
2138 ));
2139
2140 let none = CT_TextParagraphProperties::from_xml(cases[2].0).unwrap();
2141 assert!(matches!(
2142 none.bullet.as_ref().unwrap().choice,
2143 Some(TextBulletChoice::None(_))
2144 ));
2145 }
2146
2147 #[test]
2148 fn bullet_font_size_and_colour_keep_their_schema_positions() {
2149 let properties = CT_TextParagraphProperties::from_xml(
2150 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>"#,
2151 )
2152 .unwrap();
2153 let mut writer = quick_xml::Writer::new(Vec::new());
2154 properties.write_xml(&mut writer, "a:pPr").unwrap();
2155 assert_eq!(
2156 writer.into_inner(),
2157 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>"#
2158 );
2159 }
2160
2161 #[test]
2162 fn unknown_bullet_children_round_trip_byte_for_byte() {
2163 let properties = CT_TextParagraphProperties::from_xml(
2164 br#"<q:pPr><q:buClrTx x:mode="stay"><x:nested>one & two</x:nested><!--note--></q:buClrTx><q:buChar char="*"/><x:after/></q:pPr>"#,
2165 )
2166 .unwrap();
2167 let mut writer = quick_xml::Writer::new(Vec::new());
2168 properties.write_xml(&mut writer, "a:pPr").unwrap();
2169 assert_eq!(
2170 writer.into_inner(),
2171 br#"<a:pPr><q:buClrTx x:mode="stay"><x:nested>one & two</x:nested><!--note--></q:buClrTx><a:buChar char="*"/><x:after/></a:pPr>"#
2172 );
2173 }
2174
2175 #[test]
2176 fn malformed_bullet_values_return_errors_without_panicking() {
2177 let cases: &[&[u8]] = &[
2178 br#"<q:pPr><q:buChar/></q:pPr>"#,
2179 br#"<q:pPr><q:buChar char=""/></q:pPr>"#,
2180 br#"<q:pPr><q:buSzPct val="24999"/></q:pPr>"#,
2181 br#"<q:pPr><q:buSzPct val="400001"/></q:pPr>"#,
2182 br#"<q:pPr><q:buSzPts val="99"/></q:pPr>"#,
2183 br#"<q:pPr><q:buSzPts val="400001"/></q:pPr>"#,
2184 br#"<q:pPr><q:buAutoNum type="unknown"/></q:pPr>"#,
2185 br#"<q:pPr><q:buAutoNum/></q:pPr>"#,
2186 br#"<q:pPr><q:buAutoNum type="arabicPeriod" startAt="0"/></q:pPr>"#,
2187 br#"<q:pPr><q:buAutoNum type="arabicPeriod" startAt="32768"/></q:pPr>"#,
2188 br#"<q:pPr><q:buFont/></q:pPr>"#,
2189 br#"<q:pPr><q:buClr/></q:pPr>"#,
2190 br#"<q:pPr><q:buClr><q:srgbClr x:val="AABBCC"/></q:buClr></q:pPr>"#,
2191 br#"<q:pPr><q:buClr><q:srgbClr val="GGGGGG"/></q:buClr></q:pPr>"#,
2192 br#"<q:pPr><q:buClr><q:srgbClr val="AABBCC"/><q:schemeClr val="accent1"/></q:buClr></q:pPr>"#,
2193 br#"<q:pPr><q:buChar char="*"/><q:buNone/></q:pPr>"#,
2194 br#"<q:pPr><q:buSzPct val="100000"/><q:buSzPts val="1200"/></q:pPr>"#,
2195 ];
2196
2197 for xml in cases {
2198 let result = panic::catch_unwind(|| CT_TextParagraphProperties::from_xml(xml));
2199 assert!(result.is_ok(), "bullet parser panicked");
2200 assert!(result.unwrap().is_err(), "malformed bullet parsed");
2201 }
2202 }
2203
2204 #[test]
2205 fn malformed_text_content_returns_errors_without_panicking() {
2206 let cases: &[&[u8]] = &[
2207 br#"<q:r/>"#,
2208 br#"<q:r><q:t><x:nested/></q:t></q:r>"#,
2209 br#"<q:r><q:t xml:space="keep">x</q:t></q:r>"#,
2210 br#"<q:r><q:t>x</q:t><q:t>y</q:t></q:r>"#,
2211 br#"<q:pPr lvl="9"/>"#,
2212 br#"<q:pPr marL="-1"/>"#,
2213 br#"<q:pPr indent="51206401"/>"#,
2214 br#"<q:pPr algn="middle"/>"#,
2215 br#"<q:pPr><q:lnSpc/></q:pPr>"#,
2216 br#"<q:pPr><q:lnSpc><q:spcPts val="158401"/></q:lnSpc></q:pPr>"#,
2217 br#"<q:pPr><q:lnSpc><q:spcPct val="201170"/></q:lnSpc></q:pPr>"#,
2218 br#"<q:rPr sz="99"/>"#,
2219 br#"<q:rPr b="yes"/>"#,
2220 br#"<q:rPr u="triple"/>"#,
2221 br#"<q:rPr spc="400001"/>"#,
2222 r#"<q:rPr spc="éx"/>"#.as_bytes(),
2223 br#"<q:rPr baseline="100001"/>"#,
2224 br#"<q:rPr><q:latin/></q:rPr>"#,
2225 br#"<q:fld type="slidenum"/>"#,
2226 br#"<q:fld id="not-a-guid"/>"#,
2227 br#"<q:p><q:endParaRPr/><q:endParaRPr/></q:p>"#,
2228 br#"<q:notPPr/>"#,
2229 br#"<q:notRPr/>"#,
2230 ];
2231 for xml in cases {
2232 let result = panic::catch_unwind(|| match super::root_local_name(xml).as_deref() {
2233 Ok(b"r") => super::CT_RegularTextRun::from_xml(xml).map(|_| ()),
2234 Ok(b"pPr") => CT_TextParagraphProperties::from_xml(xml).map(|_| ()),
2235 Ok(b"rPr") => CT_TextCharacterProperties::from_xml(xml).map(|_| ()),
2236 Ok(b"fld") => super::CT_TextField::from_xml(xml).map(|_| ()),
2237 Ok(b"p") => CT_TextParagraph::from_xml(xml).map(|_| ()),
2238 Ok(b"notPPr") => CT_TextParagraphProperties::from_xml(xml).map(|_| ()),
2239 Ok(b"notRPr") => CT_TextCharacterProperties::from_xml(xml).map(|_| ()),
2240 _ => unreachable!(),
2241 });
2242 assert!(result.is_ok(), "text parser panicked");
2243 assert!(result.unwrap().is_err(), "malformed text parsed");
2244 }
2245 }
2246}