1use std::io::Write;
2
3use oxml_core::OxmlError;
4use oxml_core::raw_xml::{capture_element, capture_empty_element};
5use oxml_core::xml::matches_local_name;
6use quick_xml::events::{BytesEnd, BytesStart, Event};
7use quick_xml::{Reader, Writer};
8
9use crate::order::OrderedRawChildren;
10
11pub mod body;
12pub mod bullet;
13pub mod list_style;
14pub mod paragraph;
15
16pub use body::{
17 CT_TextBodyProperties, Coordinate32Value, NormalAutofit, TextAnchor, TextAutofit, TextError,
18 TextVertical, TextWrap,
19};
20pub use bullet::{
21 TextAutoNumber, TextAutoNumberScheme, TextBullet, TextBulletCharacter, TextBulletChoice,
22 TextBulletColor, TextBulletSize, TextBulletSizeValue, TextNoBullet,
23};
24pub use list_style::CT_TextListStyle;
25pub use paragraph::{
26 CT_RegularTextRun, CT_TextCharacterProperties, CT_TextField, CT_TextLineBreak,
27 CT_TextParagraph, CT_TextParagraphProperties, TextAlignment, TextFont, TextHyperlink,
28 TextPointValue, TextRun, TextSpace, TextSpacing, TextStrike, TextUnderline, TextValue,
29 escape_invalid_xml_characters,
30};
31
32use body::{Result, missing_end};
33use paragraph::{
34 TextNamespaceScope, text_capture_opaque_event, text_close_generated_child, text_fragment_scope,
35 text_namespace_scope, text_owned_name, text_reject_trailing, text_root_spellings_scoped,
36};
37
38#[derive(Clone, Debug)]
39struct SourceBodyProperties {
40 raw: Vec<u8>,
41 original: CT_TextBodyProperties,
42 _namespaces: Vec<(String, String)>,
43}
44
45#[allow(non_camel_case_types)]
47#[derive(Clone, Debug)]
48pub struct CT_TextBody {
49 pub body_properties: CT_TextBodyProperties,
50 list_style: Option<Box<CT_TextListStyle>>,
51 paragraphs: Vec<CT_TextParagraph>,
52 raw_children: OrderedRawChildren,
53 source_root: Vec<String>,
54 source_tag: String,
55 source_body_properties: Option<SourceBodyProperties>,
56}
57impl PartialEq for CT_TextBody {
58 fn eq(&self, other: &Self) -> bool {
59 self.body_properties == other.body_properties
60 && self.list_style == other.list_style
61 && self.paragraphs == other.paragraphs
62 && self.raw_children == other.raw_children
63 }
64}
65impl Eq for CT_TextBody {}
66
67impl Default for CT_TextBody {
68 fn default() -> Self {
69 Self::new()
70 }
71}
72
73impl CT_TextBody {
74 pub fn new() -> Self {
76 Self {
77 body_properties: CT_TextBodyProperties::default(),
78 list_style: None,
79 paragraphs: vec![CT_TextParagraph::default()],
80 raw_children: OrderedRawChildren::default(),
81 source_root: Vec::new(),
82 source_tag: String::new(),
83 source_body_properties: None,
84 }
85 }
86
87 pub fn from_xml(xml: &[u8]) -> Result<Self> {
89 Self::from_xml_with_inherited_namespaces(xml, &[])
90 }
91
92 pub fn from_xml_as(xml: &[u8], root_local_name: &[u8]) -> Result<Self> {
94 Self::from_xml_as_with_namespaces(xml, root_local_name, &[])
95 }
96
97 pub fn from_xml_with_inherited_namespaces(
99 xml: &[u8],
100 inherited: &[(String, String)],
101 ) -> Result<Self> {
102 Self::from_xml_as_with_namespaces(xml, b"txBody", inherited)
103 }
104
105 pub fn from_xml_as_with_namespaces(
108 xml: &[u8],
109 root_local_name: &[u8],
110 inherited: &[(String, String)],
111 ) -> Result<Self> {
112 let mut reader = Reader::from_reader(xml);
113 let mut buffer = Vec::new();
114 loop {
115 match reader
116 .read_event_into(&mut buffer)
117 .map_err(OxmlError::from)?
118 {
119 Event::Start(element)
120 if matches_local_name(element.name().as_ref(), root_local_name) =>
121 {
122 let scope = text_namespace_scope(&element, inherited)?;
123 let name = element.name();
124 let spelling = std::str::from_utf8(name.as_ref()).map_err(OxmlError::from)?;
125 let prefix = spelling.split_once(':').map_or("", |(prefix, _)| prefix);
126 if scope.iter().any(|(key, uri)| {
127 key == prefix
128 && uri != crate::namespace::A_NS
129 && uri != "http://schemas.openxmlformats.org/presentationml/2006/main"
130 && !(root_local_name == b"t"
131 && uri
132 == "http://schemas.openxmlformats.org/drawingml/2006/diagram")
133 && !(root_local_name == b"txPr"
134 && uri == "http://schemas.openxmlformats.org/drawingml/2006/chart")
135 && !(root_local_name == b"txBody"
136 && uri
137 == "http://schemas.microsoft.com/office/powerpoint/2018/8/main")
138 }) {
139 return Err(TextError::UnexpectedElement(
140 "foreign text body namespace".to_owned(),
141 ));
142 }
143 let parsed = Self::from_element(
144 &mut reader,
145 root_local_name,
146 &element,
147 &TextNamespaceScope::inherited(inherited),
148 )?;
149 text_reject_trailing(&mut reader)?;
150 return Ok(parsed);
151 }
152 Event::Empty(element)
153 if matches_local_name(element.name().as_ref(), root_local_name) =>
154 {
155 return Err(TextError::MissingBodyProperties);
156 }
157 Event::Start(element) | Event::Empty(element) => {
158 return Err(TextError::UnexpectedElement(element_name(&element)));
159 }
160 Event::Eof => {
161 return Err(TextError::Xml(OxmlError::MissingElement(
162 "DrawingML text body".to_owned(),
163 )));
164 }
165 _ => {}
166 }
167 buffer.clear();
168 }
169 }
170
171 fn from_element(
172 reader: &mut Reader<&[u8]>,
173 root_local_name: &[u8],
174 start: &BytesStart<'_>,
175 inherited: &TextNamespaceScope,
176 ) -> Result<Self> {
177 let namespaces = text_fragment_scope(start, inherited)?;
178 let source_root = text_root_spellings_scoped(
179 start,
180 inherited,
181 &capture_element(&mut reader.clone(), start)?,
182 )?;
183 let mut body_properties = None;
184 let mut source_body_properties = None;
185 let mut list_style = None;
186 let mut paragraphs = Vec::new();
187 let mut raw_children = OrderedRawChildren::default();
188 let mut boundary = 0;
189 let mut buffer = Vec::new();
190
191 loop {
192 match reader
193 .read_event_into(&mut buffer)
194 .map_err(OxmlError::from)?
195 {
196 Event::Start(element) if text_owned_name(&element, &namespaces)? == b"bodyPr" => {
197 if body_properties.is_some() {
198 return Err(TextError::DuplicateElement("bodyPr".to_owned()));
199 }
200 let raw = capture_element(reader, &element)?;
201 let parsed = CT_TextBodyProperties::from_xml(&raw)?;
202 source_body_properties = Some(SourceBodyProperties {
203 raw,
204 original: parsed.clone(),
205 _namespaces: namespaces.physical.clone(),
206 });
207 body_properties = Some(parsed);
208 boundary = boundary.max(1);
209 }
210 Event::Empty(element) if text_owned_name(&element, &namespaces)? == b"bodyPr" => {
211 if body_properties.is_some() {
212 return Err(TextError::DuplicateElement("bodyPr".to_owned()));
213 }
214 let raw = capture_empty_element(&element)?;
215 let parsed = CT_TextBodyProperties::from_xml(&raw)?;
216 source_body_properties = Some(SourceBodyProperties {
217 raw,
218 original: parsed.clone(),
219 _namespaces: namespaces.physical.clone(),
220 });
221 body_properties = Some(parsed);
222 boundary = boundary.max(1);
223 }
224 Event::Start(element) if text_owned_name(&element, &namespaces)? == b"lstStyle" => {
225 if list_style.is_some() {
226 return Err(TextError::DuplicateElement("lstStyle".to_owned()));
227 }
228 let raw = capture_element(reader, &element)?;
229 list_style = Some(CT_TextListStyle::from_xml_boxed_with_namespaces(
230 &raw,
231 &namespaces,
232 )?);
233 boundary = boundary.max(2);
234 }
235 Event::Empty(element) if text_owned_name(&element, &namespaces)? == b"lstStyle" => {
236 if list_style.is_some() {
237 return Err(TextError::DuplicateElement("lstStyle".to_owned()));
238 }
239 let raw = capture_empty_element(&element)?;
240 list_style = Some(CT_TextListStyle::from_xml_boxed_with_namespaces(
241 &raw,
242 &namespaces,
243 )?);
244 boundary = boundary.max(2);
245 }
246 Event::Start(element) if text_owned_name(&element, &namespaces)? == b"p" => {
247 let raw = capture_element(reader, &element)?;
248 paragraphs.push(CT_TextParagraph::from_xml_with_namespaces(
249 &raw,
250 &namespaces,
251 )?);
252 boundary = boundary.max(2 + paragraphs.len());
253 }
254 Event::Empty(element) if text_owned_name(&element, &namespaces)? == b"p" => {
255 let raw = capture_empty_element(&element)?;
256 paragraphs.push(CT_TextParagraph::from_xml_with_namespaces(
257 &raw,
258 &namespaces,
259 )?);
260 boundary = boundary.max(2 + paragraphs.len());
261 }
262 Event::Start(element) => {
263 raw_children.push(boundary, capture_element(reader, &element)?)
264 }
265 Event::Empty(element) => {
266 raw_children.push(boundary, capture_empty_element(&element)?)
267 }
268 Event::End(element)
269 if matches_local_name(element.name().as_ref(), root_local_name) =>
270 {
271 break;
272 }
273 Event::Eof => {
274 return Err(missing_end(&String::from_utf8_lossy(root_local_name)));
275 }
276 event @ (Event::Comment(_) | Event::PI(_) | Event::Text(_) | Event::CData(_)) => {
277 raw_children.push(boundary, text_capture_opaque_event(event)?);
278 }
279 _ => {}
280 }
281 buffer.clear();
282 }
283
284 let body_properties = body_properties.ok_or(TextError::MissingBodyProperties)?;
285 Ok(Self {
286 body_properties,
287 list_style,
288 paragraphs,
289 raw_children,
290 source_root,
291 source_tag: std::str::from_utf8(start.name().as_ref())
292 .map_err(OxmlError::from)?
293 .to_owned(),
294 source_body_properties,
295 })
296 }
297
298 pub fn to_xml(&self) -> Result<Vec<u8>> {
300 let mut writer = Writer::new(Vec::new());
301 self.write_xml(&mut writer)?;
302 Ok(writer.into_inner())
303 }
304
305 pub fn write_xml<W: Write>(&self, writer: &mut Writer<W>) -> Result<()> {
307 self.write_xml_as(writer, "a:txBody")
308 }
309
310 pub fn write_xml_as<W: Write>(&self, writer: &mut Writer<W>, tag: &str) -> Result<()> {
312 let start = paragraph::text_wrapper_start(tag, &self.source_tag, &self.source_root)?;
313 let owned_tag = std::str::from_utf8(start.name().as_ref())
314 .map_err(OxmlError::from)?
315 .to_owned();
316 let tag = owned_tag.as_str();
317 let namespaces = paragraph::text_namespace_scope(&start, &[])?;
318 writer
319 .write_event(Event::Start(start))
320 .map_err(OxmlError::from)?;
321 emit_raw(writer, self.raw_children.at(0))?;
322 if let Some(SourceBodyProperties {
323 raw,
324 original,
325 _namespaces,
326 }) = &self.source_body_properties
327 {
328 if &self.body_properties == original && !_namespaces.is_empty() {
329 writer.get_mut().write_all(raw).map_err(OxmlError::from)?;
330 } else {
331 {
332 let mut child = Writer::new(Vec::new());
333 self.body_properties.write_xml(&mut child)?;
334 writer
335 .get_mut()
336 .write_all(&text_close_generated_child(
337 &child.into_inner(),
338 &namespaces,
339 )?)
340 .map_err(OxmlError::from)?;
341 }
342 }
343 } else {
344 {
345 let mut child = Writer::new(Vec::new());
346 self.body_properties.write_xml(&mut child)?;
347 writer
348 .get_mut()
349 .write_all(&text_close_generated_child(
350 &child.into_inner(),
351 &namespaces,
352 )?)
353 .map_err(OxmlError::from)?;
354 }
355 }
356 emit_raw(writer, self.raw_children.at(1))?;
357 if let Some(list_style) = &self.list_style {
358 {
359 let mut child = Writer::new(Vec::new());
360 list_style.write_xml(&mut child)?;
361 writer
362 .get_mut()
363 .write_all(&text_close_generated_child(
364 &child.into_inner(),
365 &namespaces,
366 )?)
367 .map_err(OxmlError::from)?;
368 }
369 }
370 emit_raw(writer, self.raw_children.at(2))?;
371 for (index, paragraph) in self.paragraphs.iter().enumerate() {
372 {
373 let mut child = Writer::new(Vec::new());
374 paragraph.write_xml(&mut child)?;
375 writer
376 .get_mut()
377 .write_all(&text_close_generated_child(
378 &child.into_inner(),
379 &namespaces,
380 )?)
381 .map_err(OxmlError::from)?;
382 }
383 emit_raw(writer, self.raw_children.at(3 + index))?;
384 }
385 writer
386 .write_event(Event::End(BytesEnd::new(tag)))
387 .map_err(OxmlError::from)?;
388 Ok(())
389 }
390
391 pub fn plain_text(&self) -> String {
393 self.paragraphs
394 .iter()
395 .map(|paragraph| {
396 let mut text = String::new();
397 for run in ¶graph.runs {
398 match run {
399 TextRun::Run(run) => text.push_str(&run.text.value),
400 TextRun::Break(_) => text.push('\n'),
401 TextRun::Field(field) => {
402 if let Some(value) = &field.text {
403 text.push_str(&value.value);
404 }
405 }
406 }
407 }
408 text
409 })
410 .collect::<Vec<_>>()
411 .join("\n")
412 }
413
414 pub fn has_list_style(&self) -> bool {
415 self.list_style.is_some()
416 }
417
418 pub fn list_style(&self) -> Option<&CT_TextListStyle> {
419 self.list_style.as_deref()
420 }
421
422 pub fn paragraph_count(&self) -> usize {
423 self.paragraphs.len()
424 }
425
426 pub fn paragraphs(&self) -> &[CT_TextParagraph] {
427 &self.paragraphs
428 }
429
430 pub fn set_text(&mut self, text: &str) {
435 let old_paragraph_count = self.paragraphs.len();
436 let mut first = self.paragraphs.drain(..).next().unwrap_or_default();
437 let mut lines = text.split("\r\n").flat_map(|part| part.split('\n'));
438 first.set_text(lines.next().unwrap_or_default());
439 let mut template = CT_TextParagraph::default();
440 template.properties = first.properties.clone();
441 template.runs = first.runs.iter().take(1).cloned().collect();
442 template.end_properties = first.end_properties.clone();
443 self.paragraphs.push(first);
444 for line in lines {
445 let mut paragraph = template.clone();
446 paragraph.set_text(line);
447 self.paragraphs.push(paragraph);
448 }
449 self.reconcile_paragraph_raw_children(old_paragraph_count, self.paragraphs.len());
450 }
451
452 pub fn empty_like(&self) -> Self {
462 let mut paragraph = CT_TextParagraph::default();
463 if let Some(first) = self.paragraphs.first() {
464 let first_run = first.runs.iter().find_map(|run| match run {
465 TextRun::Run(run) => Some(run),
466 TextRun::Break(_) | TextRun::Field(_) => None,
467 });
468 paragraph.properties = first.properties.clone();
469 paragraph.end_properties = first_run
470 .and_then(|run| run.properties.clone())
471 .or_else(|| first.end_properties.clone())
472 .map(CT_TextCharacterProperties::without_hyperlinks);
473 }
474 Self {
475 body_properties: self.body_properties.clone(),
476 list_style: self.list_style.clone(),
477 paragraphs: vec![paragraph],
478 raw_children: OrderedRawChildren::default(),
479 source_root: if self.body_properties.autofit_source.is_some() {
480 self.source_root.clone()
481 } else {
482 Vec::new()
483 },
484 source_tag: if self.body_properties.autofit_source.is_some() {
485 self.source_tag.clone()
486 } else {
487 String::new()
488 },
489 source_body_properties: if self.body_properties.autofit_source.is_some() {
490 self.source_body_properties.clone()
491 } else {
492 None
493 },
494 }
495 }
496
497 pub fn paragraph_mut(&mut self, index: usize) -> Option<&mut CT_TextParagraph> {
499 self.paragraphs.get_mut(index)
500 }
501
502 pub fn add_paragraph(&mut self) -> &mut CT_TextParagraph {
504 self.raw_children
505 .shift_boundaries_from(2 + self.paragraphs.len());
506 self.paragraphs.push(CT_TextParagraph::default());
507 self.paragraphs.last_mut().expect("paragraph was appended")
508 }
509
510 pub fn move_content_to(&mut self, destination: &mut Self) {
513 if self.plain_text().is_empty() {
514 return;
515 }
516
517 let source_paragraph_count = self.paragraphs.len();
518 let moved = std::mem::take(&mut self.paragraphs);
519 self.paragraphs.push(CT_TextParagraph::default());
520 self.reconcile_paragraph_raw_children(source_paragraph_count, 1);
521
522 if destination.plain_text().is_empty() {
523 let destination_paragraph_count = destination.paragraphs.len();
524 destination.paragraphs = moved;
525 destination.reconcile_paragraph_raw_children(
526 destination_paragraph_count,
527 destination.paragraphs.len(),
528 );
529 return;
530 }
531
532 for paragraph in moved {
533 destination
534 .raw_children
535 .shift_boundaries_from(2 + destination.paragraphs.len());
536 destination.paragraphs.push(paragraph);
537 }
538 }
539
540 fn reconcile_paragraph_raw_children(
541 &mut self,
542 old_paragraph_count: usize,
543 new_paragraph_count: usize,
544 ) {
545 let mut raw_children = OrderedRawChildren::default();
546 for boundary in 0..=2 + old_paragraph_count {
547 let new_boundary = if boundary <= 2 {
548 boundary
549 } else {
550 2 + new_paragraph_count
551 };
552 for child in self.raw_children.at(boundary) {
553 raw_children.push(new_boundary, child.to_vec());
554 }
555 }
556 self.raw_children = raw_children;
557 }
558
559 pub fn raw_children(&self) -> &OrderedRawChildren {
560 &self.raw_children
561 }
562}
563
564fn emit_raw<'a, W: Write>(
565 writer: &mut Writer<W>,
566 children: impl Iterator<Item = &'a [u8]>,
567) -> Result<()> {
568 for child in children {
569 writer.get_mut().write_all(child).map_err(OxmlError::from)?;
570 }
571 Ok(())
572}
573
574fn element_name(element: &BytesStart<'_>) -> String {
575 String::from_utf8_lossy(element.name().as_ref()).into_owned()
576}
577
578#[cfg(test)]
579mod tests {
580 use std::panic;
581
582 use super::{CT_TextBody, CT_TextListStyle};
583
584 #[test]
585 fn fx225_text_style_boxing_preserves_source_and_public_callers() {
586 let opaque = "<e:foreign v='keep&exact'><!--raw--><e:child/></e:foreign>";
587 let mut levels = String::new();
588 for level in 1..=9 {
589 levels.push_str(&format!("<q:lvl{level}pPr marL=\"{level}\"><q:defRPr lang=\"en-US\"/><e:level n='{level}'/></q:lvl{level}pPr>"));
590 }
591 let xml = format!(
592 "<q:txBody xmlns:q='http://schemas.openxmlformats.org/drawingml/2006/main' xmlns:e='urn:extension'><q:bodyPr/><q:lstStyle e:owner='original'><q:defPPr algn=\"ctr\"/>{levels}{opaque}</q:lstStyle><q:p><q:r><q:t>source</q:t></q:r></q:p></q:txBody>"
593 );
594 let body = CT_TextBody::from_xml(xml.as_bytes()).unwrap();
595 let style: Option<&CT_TextListStyle> = body.list_style();
596 assert_eq!(style.unwrap().level(1).unwrap().left_margin, Some(1));
597 assert_eq!(style.unwrap().level(9).unwrap().left_margin, Some(9));
598 let serialized = body.to_xml().unwrap();
599 assert!(String::from_utf8_lossy(&serialized).contains(opaque));
600 let mut clone = body.clone();
601 assert_eq!(clone, body);
602 clone.set_text("changed");
603 clone.paragraph_mut(0).unwrap().add_run(" appended");
604 assert_eq!(body.plain_text(), "source");
605 assert_eq!(clone.list_style(), body.list_style());
606 assert_eq!(clone.empty_like().list_style(), body.list_style());
607 let saved = clone.to_xml().unwrap();
608 let saved_text = String::from_utf8_lossy(&saved);
609 assert!(saved_text.contains(opaque));
610 assert!(saved_text.contains("e:owner='original'"));
611 for level in 1..9 {
612 assert!(
613 saved_text.find(&format!("<q:lvl{level}pPr")).unwrap()
614 < saved_text.find(&format!("<q:lvl{}pPr", level + 1)).unwrap()
615 );
616 }
617 assert_eq!(
618 CT_TextBody::from_xml(&saved).unwrap().to_xml().unwrap(),
619 saved
620 );
621 let mut direct = style.unwrap().clone();
622 direct.level1.as_mut().unwrap().left_margin = Some(73);
623 assert_eq!(
624 CT_TextListStyle::from_xml(&direct.to_xml().unwrap())
625 .unwrap()
626 .level(1)
627 .unwrap()
628 .left_margin,
629 Some(73)
630 );
631 assert_eq!(
632 body.list_style().unwrap().level(1).unwrap().left_margin,
633 Some(1)
634 );
635 for xml in [
636 b"<a:txBody><a:bodyPr/><a:p/></a:txBody>".as_slice(),
637 b"<a:txBody><a:bodyPr/><a:lstStyle/><a:p/></a:txBody>".as_slice(),
638 ] {
639 let value = CT_TextBody::from_xml(xml).unwrap();
640 assert_eq!(value.clone().to_xml().unwrap(), value.to_xml().unwrap());
641 assert_eq!(
642 CT_TextBody::from_xml(&value.to_xml().unwrap()).unwrap(),
643 value
644 );
645 }
646 }
647
648 #[test]
649 fn autofit_opaque_choice_keeps_inherited_wrapper_namespace_scope() {
650 let xml = br#"<q:txBody xmlns:q='http://schemas.openxmlformats.org/drawingml/2006/main' xmlns:e='urn:example'><q:bodyPr><q:normAutofit e:proof='keep&exact'/></q:bodyPr><q:p/></q:txBody>"#;
651 let mut body = CT_TextBody::from_xml(xml).unwrap();
652 body.body_properties.autofit = Some(super::TextAutofit::NoAutofit);
653 let saved = body.to_xml().unwrap();
654 let saved = String::from_utf8(saved).unwrap();
655 assert!(saved.starts_with("<q:txBody xmlns:q='http://schemas.openxmlformats.org/drawingml/2006/main' xmlns:e='urn:example'>"));
656 assert!(saved.contains("<q:noAutofit e:proof='keep&exact'/>"));
657 let empty = String::from_utf8(body.empty_like().to_xml().unwrap()).unwrap();
658 assert!(empty.contains("xmlns:q='http://schemas.openxmlformats.org/drawingml/2006/main'"));
659 assert!(empty.contains("xmlns:e='urn:example'"));
660 assert!(empty.contains("<q:noAutofit e:proof='keep&exact'/>"));
661 assert_eq!(
662 CT_TextBody::from_xml(saved.as_bytes())
663 .unwrap()
664 .to_xml()
665 .unwrap(),
666 saved.as_bytes()
667 );
668
669 let conflicting = String::from_utf8(xml.to_vec())
672 .unwrap()
673 .replace("xmlns:e=", "xmlns:a='urn:foreign' xmlns:e=");
674 let mut body = CT_TextBody::from_xml(conflicting.as_bytes()).unwrap();
675 body.body_properties.autofit = Some(super::TextAutofit::NoAutofit);
676 let saved = String::from_utf8(body.to_xml().unwrap()).unwrap();
677 assert!(saved.starts_with("<q:txBody"));
678 assert!(saved.contains("xmlns:a='urn:foreign'"));
679 assert!(saved.contains("<q:noAutofit e:proof='keep&exact'/>"));
680 assert_eq!(
681 CT_TextBody::from_xml(saved.as_bytes())
682 .unwrap()
683 .to_xml()
684 .unwrap(),
685 saved.as_bytes()
686 );
687 }
688
689 #[test]
690 fn text_body_reads_any_prefix_and_writes_the_fixed_a_prefix() {
691 let xml = br#"<q:txBody><x:before/><q:bodyPr anchor="ctr"><q:noAutofit/></q:bodyPr><x:afterBody/><q:lstStyle><x:listChild/></q:lstStyle><x:beforeParagraph/><q:p><x:run>kept</x:run></q:p><x:afterParagraph/></q:txBody>"#;
692 let body = CT_TextBody::from_xml(xml).unwrap();
693 assert!(body.has_list_style());
694 assert_eq!(body.paragraph_count(), 1);
695 assert_eq!(body.to_xml().unwrap(), br#"<a:txBody><x:before/><a:bodyPr anchor="ctr"><a:noAutofit/></a:bodyPr><x:afterBody/><a:lstStyle><x:listChild/></a:lstStyle><x:beforeParagraph/><a:p><x:run>kept</x:run></a:p><x:afterParagraph/></a:txBody>"#);
696 }
697
698 #[test]
699 fn moving_content_retains_typed_paragraphs_and_source_body_state() {
700 let mut destination = CT_TextBody::from_xml(
701 br#"<a:txBody><a:bodyPr/><a:p><a:r><a:rPr b="1"/><a:t>one</a:t></a:r></a:p></a:txBody>"#,
702 )
703 .unwrap();
704 let mut source = CT_TextBody::from_xml(
705 br#"<a:txBody><a:bodyPr lIns="10"/><a:p><a:r><a:rPr i="1"/><a:t>two</a:t></a:r></a:p><a:p><a:r><a:t>three</a:t></a:r></a:p></a:txBody>"#,
706 )
707 .unwrap();
708
709 source.move_content_to(&mut destination);
710
711 assert_eq!(destination.plain_text(), "one\ntwo\nthree");
712 assert_eq!(destination.paragraph_count(), 3);
713 assert_eq!(source.plain_text(), "");
714 assert_eq!(source.paragraph_count(), 1);
715 assert!(
716 String::from_utf8(source.to_xml().unwrap())
717 .unwrap()
718 .contains(r#"lIns="10""#)
719 );
720 let written = String::from_utf8(destination.to_xml().unwrap()).unwrap();
721 assert!(written.contains(r#"<a:rPr b="1"/>"#));
722 assert!(written.contains(r#"<a:rPr i="1"/>"#));
723 }
724
725 #[test]
726 fn set_text_starts_a_paragraph_at_each_line_feed_and_breaks_at_each_vertical_tab() {
727 let mut body = CT_TextBody::from_xml(
728 br#"<a:txBody xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:x="urn:x"><a:bodyPr/><a:p><a:pPr algn="ctr"/><a:r><a:rPr b="1"/><a:t>old</a:t></a:r><x:in-paragraph/><a:endParaRPr sz="1200"/></a:p><a:p><a:r><a:t>gone</a:t></a:r></a:p><x:after-paragraphs/></a:txBody>"#,
729 )
730 .unwrap();
731 body.set_text("one\r\ntwo\u{b}soft\u{b}\n");
732 assert_eq!(
733 String::from_utf8(body.to_xml().unwrap()).unwrap(),
734 concat!(
735 r#"<a:txBody xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:x="urn:x"><a:bodyPr/>"#,
736 r#"<a:p xmlns:x="urn:x"><a:pPr algn="ctr"/><a:r><a:rPr b="1"/><a:t>one</a:t></a:r><x:in-paragraph/><a:endParaRPr sz="1200"/></a:p>"#,
737 r#"<a:p><a:pPr algn="ctr"/><a:r><a:rPr b="1"/><a:t>two</a:t></a:r><a:br><a:rPr b="1"/></a:br><a:r><a:rPr b="1"/><a:t>soft</a:t></a:r><a:br><a:rPr b="1"/></a:br><a:endParaRPr sz="1200"/></a:p>"#,
738 r#"<a:p><a:pPr algn="ctr"/><a:r><a:rPr b="1"/><a:t/></a:r><a:endParaRPr sz="1200"/></a:p>"#,
739 r#"<x:after-paragraphs/></a:txBody>"#,
740 )
741 );
742 assert_eq!(body.plain_text(), "one\ntwo\nsoft\n\n");
743
744 let saved = body.to_xml().unwrap();
745 assert_eq!(
746 CT_TextBody::from_xml(&saved).unwrap().to_xml().unwrap(),
747 saved
748 );
749
750 let mut plain = CT_TextBody::new();
751 plain.set_text("single line");
752 assert_eq!(plain.paragraph_count(), 1);
753 assert_eq!(plain.plain_text(), "single line");
754 }
755
756 #[test]
757 fn schema_valid_text_body_using_all_nine_list_levels_round_trips_structurally() {
758 let xml = br#"<q:txBody><q:bodyPr/><q:lstStyle><q:lvl1pPr lvl="0"><q:buChar char="*"/></q:lvl1pPr><q:lvl2pPr marL="100"><q:buAutoNum type="arabicPeriod" startAt="2"/></q:lvl2pPr><q:lvl3pPr><q:buNone/></q:lvl3pPr><q:lvl4pPr><q:defRPr sz="1200"/></q:lvl4pPr><q:lvl5pPr algn="ctr"/><x:extension x:id="5"><x:child>one & two</x:child></x:extension><q:lvl6pPr marR="200"/><q:lvl7pPr><q:spcBef><q:spcPts val="600"/></q:spcBef></q:lvl7pPr><q:lvl8pPr><q:buSzPct val="125000"/><q:buFont typeface="Wingdings"/><q:buChar char="o"/></q:lvl8pPr><q:lvl9pPr indent="-100"/></q:lstStyle><q:p><q:pPr lvl="1"/><q:r><q:t xml:space="preserve"> item </q:t></q:r></q:p></q:txBody>"#;
759 let expected = br#"<a:txBody><a:bodyPr/><a:lstStyle><a:lvl1pPr lvl="0"><a:buChar char="*"/></a:lvl1pPr><a:lvl2pPr marL="100"><a:buAutoNum type="arabicPeriod" startAt="2"/></a:lvl2pPr><a:lvl3pPr><a:buNone/></a:lvl3pPr><a:lvl4pPr><a:defRPr sz="1200"/></a:lvl4pPr><a:lvl5pPr algn="ctr"/><x:extension x:id="5"><x:child>one & two</x:child></x:extension><a:lvl6pPr marR="200"/><a:lvl7pPr><a:spcBef><a:spcPts val="600"/></a:spcBef></a:lvl7pPr><a:lvl8pPr><a:buSzPct val="125000"/><a:buFont typeface="Wingdings"/><a:buChar char="o"/></a:lvl8pPr><a:lvl9pPr indent="-100"/></a:lstStyle><a:p><a:pPr lvl="1"/><a:r><a:t xml:space="preserve"> item </a:t></a:r></a:p></a:txBody>"#;
760
761 let body = CT_TextBody::from_xml(xml).unwrap();
762 let written = body.to_xml().unwrap();
763 assert_eq!(written, expected);
764 assert_eq!(CT_TextBody::from_xml(&written).unwrap(), body);
765 }
766
767 #[test]
768 fn list_style_levels_write_in_ascending_schema_order() {
769 let body = CT_TextBody::from_xml(
770 br#"<q:txBody><q:bodyPr/><q:lstStyle><q:lvl9pPr indent="-9"/><q:lvl5pPr indent="-5"/><q:lvl1pPr indent="-1"/></q:lstStyle><q:p/></q:txBody>"#,
771 )
772 .unwrap();
773 assert_eq!(
774 body.to_xml().unwrap(),
775 br#"<a:txBody><a:bodyPr/><a:lstStyle><a:lvl1pPr indent="-1"/><a:lvl5pPr indent="-5"/><a:lvl9pPr indent="-9"/></a:lstStyle><a:p/></a:txBody>"#
776 );
777 }
778
779 #[test]
780 fn unknown_list_style_children_round_trip_byte_for_byte() {
781 let body = CT_TextBody::from_xml(
782 br#"<q:txBody><q:bodyPr/><q:lstStyle><x:before x:id="1"/><q:lvl1pPr/><x:between><x:nested>one & two</x:nested><!--note--></x:between><q:lvl2pPr/><x:after x:id="9"/></q:lstStyle><q:p/></q:txBody>"#,
783 )
784 .unwrap();
785 assert_eq!(
786 body.to_xml().unwrap(),
787 br#"<a:txBody><a:bodyPr/><a:lstStyle><x:before x:id="1"/><a:lvl1pPr/><x:between><x:nested>one & two</x:nested><!--note--></x:between><a:lvl2pPr/><x:after x:id="9"/></a:lstStyle><a:p/></a:txBody>"#
788 );
789 }
790
791 #[test]
792 fn list_style_rejects_nested_fixed_prefix_rebinding() {
793 let positive = br#"<p:defaultTextStyle xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main" xmlns:d="http://schemas.openxmlformats.org/drawingml/2006/main"><d:lvl1pPr/></p:defaultTextStyle>"#;
794 assert!(CT_TextListStyle::from_xml(positive).is_ok());
795 let xml = br#"<p:defaultTextStyle xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main" xmlns:d="http://schemas.openxmlformats.org/drawingml/2006/main"><d:lvl1pPr xmlns:a="urn:producer"><a:raw/></d:lvl1pPr></p:defaultTextStyle>"#;
796 assert!(CT_TextListStyle::from_xml(xml).is_err());
797 }
798
799 #[test]
800 fn list_style_rejects_fixed_prefix_rebinding_at_typed_descendants() {
801 let descendants: &[&[u8]] = &[
802 br#"<d:defRPr xmlns:a="urn:producer"/>"#,
803 br#"<d:spcBef xmlns:a="urn:producer"><d:spcPts val="600"/></d:spcBef>"#,
804 br#"<d:spcBef><d:spcPts xmlns:a="urn:producer" val="600"/></d:spcBef>"#,
805 br#"<d:buChar xmlns:a="urn:producer" char="*"/>"#,
806 br#"<d:buClr xmlns:a="urn:producer"><d:srgbClr val="102030"/></d:buClr>"#,
807 br#"<d:buClr><d:srgbClr xmlns:a="urn:producer" val="102030"/></d:buClr>"#,
808 br#"<d:defRPr><d:solidFill xmlns:a="urn:producer"><d:srgbClr val="102030"/></d:solidFill></d:defRPr>"#,
809 br#"<d:defRPr><d:solidFill><d:srgbClr xmlns:a="urn:producer" val="102030"/></d:solidFill></d:defRPr>"#,
810 br#"<d:defRPr><d:solidFill><d:srgbClr val="102030"><d:alpha xmlns:a="urn:producer" val="50000"/></d:srgbClr></d:solidFill></d:defRPr>"#,
811 ];
812
813 for descendant in descendants {
814 let mut xml = br#"<p:defaultTextStyle xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main" xmlns:d="http://schemas.openxmlformats.org/drawingml/2006/main"><d:lvl1pPr>"#.to_vec();
815 xml.extend_from_slice(descendant);
816 xml.extend_from_slice(br#"</d:lvl1pPr></p:defaultTextStyle>"#);
817 assert!(
818 CT_TextListStyle::from_xml(&xml).is_err(),
819 "typed descendant accepted a conflicting xmlns:a: {}",
820 String::from_utf8_lossy(descendant)
821 );
822 }
823 }
824
825 #[test]
826 fn opaque_list_style_child_preserves_its_local_prefix_binding() {
827 let opaque = br#"<x:extension xmlns:x="urn:extension" xmlns:a="urn:producer"><a:data/></x:extension>"#;
828 let xml = br#"<p:defaultTextStyle xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main" xmlns:d="http://schemas.openxmlformats.org/drawingml/2006/main"><x:extension xmlns:x="urn:extension" xmlns:a="urn:producer"><a:data/></x:extension><d:lvl1pPr/></p:defaultTextStyle>"#;
829 let parsed = CT_TextListStyle::from_xml(xml).unwrap();
830 let written = parsed.to_xml().unwrap();
831 assert!(
832 written
833 .windows(opaque.len())
834 .any(|window| window == opaque.as_slice())
835 );
836 }
837
838 #[test]
839 fn opaque_typed_descendants_preserve_their_local_prefix_bindings() {
840 let character_extension = br#"<x:extension xmlns:x="urn:extension" xmlns:a="urn:producer"><a:data/></x:extension>"#;
841 let transform_with_content =
842 br#"<d:alpha xmlns:a="urn:producer" val="50000"><a:data/></d:alpha>"#;
843 let xml = br#"<p:defaultTextStyle xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main" xmlns:d="http://schemas.openxmlformats.org/drawingml/2006/main"><d:lvl1pPr><d:defRPr><x:extension xmlns:x="urn:extension" xmlns:a="urn:producer"><a:data/></x:extension><d:solidFill><d:srgbClr val="102030"><d:alpha xmlns:a="urn:producer" val="50000"><a:data/></d:alpha></d:srgbClr></d:solidFill></d:defRPr></d:lvl1pPr></p:defaultTextStyle>"#;
844
845 let written = CT_TextListStyle::from_xml(xml).unwrap().to_xml().unwrap();
846 for opaque in [
847 character_extension.as_slice(),
848 transform_with_content.as_slice(),
849 ] {
850 assert!(
851 written.windows(opaque.len()).any(|window| window == opaque),
852 "opaque descendant was not preserved: {}",
853 String::from_utf8_lossy(opaque)
854 );
855 }
856 }
857
858 #[test]
859 fn invalid_list_levels_return_errors_without_panicking() {
860 let cases: &[&[u8]] = &[
861 br#"<q:txBody><q:bodyPr/><q:lstStyle><q:lvl0pPr/></q:lstStyle><q:p/></q:txBody>"#,
862 br#"<q:txBody><q:bodyPr/><q:lstStyle><q:lvl10pPr/></q:lstStyle><q:p/></q:txBody>"#,
863 br#"<q:txBody><q:bodyPr/><q:lstStyle><q:lvl01pPr/></q:lstStyle><q:p/></q:txBody>"#,
864 br#"<q:txBody><q:bodyPr/><q:lstStyle><q:lvl1pPr/><q:lvl1pPr/></q:lstStyle><q:p/></q:txBody>"#,
865 br#"<q:txBody><q:bodyPr/><q:lstStyle><q:lvl4pPr lvl="9"/></q:lstStyle><q:p/></q:txBody>"#,
866 br#"<q:txBody><q:bodyPr/><q:lstStyle><q:lvl7pPr><q:buChar/></q:lvl7pPr></q:lstStyle><q:p/></q:txBody>"#,
867 ];
868
869 for xml in cases {
870 let result = panic::catch_unwind(|| CT_TextBody::from_xml(xml));
871 assert!(result.is_ok(), "list-style parser panicked");
872 assert!(result.unwrap().is_err(), "invalid list level parsed");
873 }
874 }
875
876 #[test]
877 fn fx206_text_body_inherited_link_namespace_and_opaque_owner_closure() {
878 let inherited = vec![
879 ("a".to_owned(), crate::namespace::A_NS.to_owned()),
880 (
881 "p".to_owned(),
882 "http://schemas.openxmlformats.org/presentationml/2006/main".to_owned(),
883 ),
884 ("q".to_owned(), crate::namespace::A_NS.to_owned()),
885 (
886 "rel".to_owned(),
887 "http://schemas.openxmlformats.org/officeDocument/2006/relationships".to_owned(),
888 ),
889 ("f".to_owned(), "urn:foreign".to_owned()),
890 ];
891 let xml = br#"<p:txBody f:body='A'><q:bodyPr f:bodyPr='L'/><q:lstStyle f:list='B'><q:defPPr f:default='C'><q:defRPr><q:hlinkClick rel:id='default'/></q:defRPr></q:defPPr></q:lstStyle><q:p f:paragraph='D'><f:r><q:r><q:t>opaque</q:t></q:r></f:r><q:r f:run='E'><q:rPr xmlns:a='urn:foreign-a' f:properties='F'><a:opaque f:keep='P'/><q:solidFill f:fill='O'><q:srgbClr val='123456'/></q:solidFill><q:latin typeface='Arial' f:font='N'/><f:hlinkClick f:keep='G'><q:hlinkClick rel:id='shadow'/></f:hlinkClick><q:hlinkClick rel:id='owned' tooltip='H'><f:payload f:keep='I'/></q:hlinkClick></q:rPr><q:t f:text='M'>visible</q:t></q:r><q:br xmlns:rel='urn:foreign' f:break='J'><q:rPr><q:hlinkClick rel:id='opaque'/></q:rPr></q:br><q:fld id='{00000000-0000-0000-0000-000000000001}' f:field='K'><q:rPr><q:hlinkClick rel:id='field'/></q:rPr><q:t>field</q:t></q:fld><q:endParaRPr><q:hlinkMouseOver rel:id='hover'/></q:endParaRPr></q:p></p:txBody>"#;
892 let mut body = CT_TextBody::from_xml_with_inherited_namespaces(xml, &inherited).unwrap();
893 assert_eq!(body.paragraphs.len(), 1);
894 assert_eq!(body.paragraphs[0].runs.len(), 3);
895 let default = body
896 .list_style
897 .as_ref()
898 .unwrap()
899 .default_paragraph_properties
900 .as_ref()
901 .unwrap()
902 .default_run_properties
903 .as_ref()
904 .unwrap();
905 assert_eq!(
906 default
907 .hyperlink_click
908 .as_ref()
909 .unwrap()
910 .relationship_id
911 .as_deref(),
912 Some("default")
913 );
914 let super::TextRun::Run(run) = &mut body.paragraphs[0].runs[0] else {
915 panic!("owned regular run");
916 };
917 let click = run
918 .properties
919 .as_mut()
920 .unwrap()
921 .hyperlink_click
922 .as_mut()
923 .unwrap();
924 assert_eq!(click.relationship_id.as_deref(), Some("owned"));
925 assert_eq!(click.tooltip.as_deref(), Some("H"));
926 click.relationship_id = Some("next".to_owned());
927 click.action = Some("ppaction://hlinksldjump".to_owned());
928 let super::TextRun::Break(br) = &body.paragraphs[0].runs[1] else {
929 panic!("owned break");
930 };
931 assert_eq!(
932 br.properties
933 .as_ref()
934 .unwrap()
935 .hyperlink_click
936 .as_ref()
937 .unwrap()
938 .relationship_id,
939 None
940 );
941 let super::TextRun::Field(field) = &body.paragraphs[0].runs[2] else {
942 panic!("owned field");
943 };
944 assert_eq!(
945 field
946 .run_properties
947 .as_ref()
948 .unwrap()
949 .hyperlink_click
950 .as_ref()
951 .unwrap()
952 .relationship_id
953 .as_deref(),
954 Some("field")
955 );
956 assert_eq!(
957 body.paragraphs[0]
958 .end_properties
959 .as_ref()
960 .unwrap()
961 .hyperlink_mouse_over
962 .as_ref()
963 .unwrap()
964 .relationship_id
965 .as_deref(),
966 Some("hover")
967 );
968 let saved = body.to_xml().unwrap();
969 let text = std::str::from_utf8(&saved).unwrap();
970 for token in [
971 "xmlns:a='urn:foreign-a'",
972 "<a:opaque f:keep='P'/>",
973 "f:font='N'",
974 "f:fill='O'",
975 "f:bodyPr='L'",
976 "f:text='M'",
977 "f:body='A'",
978 "f:list='B'",
979 "f:default='C'",
980 "f:paragraph='D'",
981 "f:run='E'",
982 "f:properties='F'",
983 "tooltip='H'",
984 "f:break='J'",
985 "f:field='K'",
986 ] {
987 assert!(text.contains(token), "{token}");
988 }
989 assert!(text.contains("<f:r><q:r><q:t>opaque</q:t></q:r></f:r>"));
990 assert!(text.contains(
991 "<f:hlinkClick f:keep='G'><q:hlinkClick rel:id='shadow'/></f:hlinkClick>"
992 ));
993 assert!(text.contains("<f:payload f:keep='I'/>"));
994 assert!(text.contains("rel:id=\"next\""));
995 assert!(text.contains("rel:id='opaque'"));
996 assert_eq!(
997 CT_TextBody::from_xml(&saved).unwrap().to_xml().unwrap(),
998 saved
999 );
1000 }
1001 #[test]
1002 fn fx206_list_style_foreign_presentation_wrapper_is_not_owned() {
1003 let input = br#"<p:defaultTextStyle xmlns:p="urn:presentation" xmlns:d="http://schemas.openxmlformats.org/drawingml/2006/main"><d:lvl1pPr/></p:defaultTextStyle>"#;
1004 assert!(CT_TextListStyle::from_xml(input).is_err());
1005 }
1006 #[test]
1007 fn fx206_presentation_wrappers_preserve_alias_default_and_foreign_canonical_bindings() {
1008 const P: &str = "http://schemas.openxmlformats.org/presentationml/2006/main";
1009 const A: &str = "http://schemas.openxmlformats.org/drawingml/2006/main";
1010 for local in [
1011 "defaultTextStyle",
1012 "titleStyle",
1013 "bodyStyle",
1014 "otherStyle",
1015 "notesStyle",
1016 ] {
1017 for prefix in ["q:", ""] {
1018 let declaration = if prefix.is_empty() {
1019 "xmlns"
1020 } else {
1021 "xmlns:q"
1022 };
1023 let input = format!(
1024 "<{prefix}{local} {declaration}=\"{P}\" xmlns:p='urn:foreign' xmlns:d=\"{A}\"><d:lvl1pPr algn='ctr'/><x:opaque xmlns:x='urn:opaque' x:keep='A'/></{prefix}{local}>"
1025 );
1026 let parsed = CT_TextListStyle::from_xml(input.as_bytes()).unwrap();
1027 let save = |value: &CT_TextListStyle| {
1028 let mut writer = quick_xml::Writer::new(Vec::new());
1029 value
1030 .write_xml_as(&mut writer, &format!("p:{local}"))
1031 .unwrap();
1032 writer.into_inner()
1033 };
1034 let saved = save(&parsed);
1035 let xml = std::str::from_utf8(&saved).unwrap();
1036 assert!(xml.starts_with(&format!("<{prefix}{local} ")));
1037 assert!(xml.contains("xmlns:p='urn:foreign'"));
1038 assert!(xml.contains("<x:opaque xmlns:x='urn:opaque' x:keep='A'/>"));
1039 assert_eq!(
1040 parsed.level1.as_ref().unwrap().alignment,
1041 Some(super::paragraph::TextAlignment::Center)
1042 );
1043 assert_eq!(save(&CT_TextListStyle::from_xml(&saved).unwrap()), saved);
1044 let foreign = input.replacen(P, "urn:not-presentation", 1);
1045 assert!(CT_TextListStyle::from_xml(foreign.as_bytes()).is_err());
1046 }
1047 }
1048 }
1049 #[test]
1050 fn fx206_requested_wrapper_namespace_converts_both_directions_without_rebinding_opaque_prefixes()
1051 {
1052 use quick_xml::{NsReader, Writer, events::Event, name::ResolveResult};
1053 const A: &str = "http://schemas.openxmlformats.org/drawingml/2006/main";
1054 const P: &str = "http://schemas.openxmlformats.org/presentationml/2006/main";
1055 for (source, requested, expected) in [(A, "p:txBody", P), (P, "a:txBody", A)] {
1056 for alias in ["q:", ""] {
1057 let declaration = if alias.is_empty() { "xmlns" } else { "xmlns:q" };
1058 let input = format!(
1059 "<{alias}txBody {declaration}='{source}' xmlns:a='{A}' xmlns:p='urn:opaque'><a:bodyPr/><a:lstStyle/><a:p><a:r><a:t>retained text</a:t></a:r></a:p><p:opaque p:keep='A'/></{alias}txBody>"
1060 );
1061 let parsed = CT_TextBody::from_xml(input.as_bytes()).unwrap();
1062 let save = |value: &CT_TextBody| {
1063 let mut writer = Writer::new(Vec::new());
1064 value.write_xml_as(&mut writer, requested).unwrap();
1065 writer.into_inner()
1066 };
1067 let saved = save(&parsed);
1068 let mut reader = NsReader::from_reader(saved.as_slice());
1069 let (namespace, event) = reader.read_resolved_event().unwrap();
1070 assert_eq!(
1071 namespace,
1072 ResolveResult::Bound(quick_xml::name::Namespace(expected.as_bytes()))
1073 );
1074 assert!(
1075 matches!(event, Event::Start(ref start) if start.local_name().as_ref() == b"txBody")
1076 );
1077 let xml = std::str::from_utf8(&saved).unwrap();
1078 assert!(xml.contains("xmlns:p='urn:opaque'"));
1079 assert!(xml.contains("<p:opaque p:keep='A'/>"));
1080 let reopened = CT_TextBody::from_xml(&saved).unwrap();
1081 assert_eq!(reopened.plain_text(), "retained text");
1082 assert_eq!(save(&reopened), saved);
1083 }
1084 }
1085 }
1086}