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};
33
34#[allow(non_camel_case_types)]
36#[derive(Clone, Debug, Eq, PartialEq)]
37pub struct CT_TextBody {
38 pub body_properties: CT_TextBodyProperties,
39 list_style: Option<CT_TextListStyle>,
40 paragraphs: Vec<CT_TextParagraph>,
41 raw_children: OrderedRawChildren,
42}
43
44impl Default for CT_TextBody {
45 fn default() -> Self {
46 Self::new()
47 }
48}
49
50impl CT_TextBody {
51 pub fn new() -> Self {
53 Self {
54 body_properties: CT_TextBodyProperties::default(),
55 list_style: None,
56 paragraphs: vec![CT_TextParagraph::default()],
57 raw_children: OrderedRawChildren::default(),
58 }
59 }
60
61 pub fn from_xml(xml: &[u8]) -> Result<Self> {
63 Self::from_xml_as(xml, b"txBody")
64 }
65
66 pub fn from_xml_as(xml: &[u8], root_local_name: &[u8]) -> Result<Self> {
68 let mut reader = Reader::from_reader(xml);
69 let mut buffer = Vec::new();
70 loop {
71 match reader
72 .read_event_into(&mut buffer)
73 .map_err(OxmlError::from)?
74 {
75 Event::Start(element)
76 if matches_local_name(element.name().as_ref(), root_local_name) =>
77 {
78 return Self::from_element(&mut reader, root_local_name);
79 }
80 Event::Empty(element)
81 if matches_local_name(element.name().as_ref(), root_local_name) =>
82 {
83 return Err(TextError::MissingBodyProperties);
84 }
85 Event::Start(element) | Event::Empty(element) => {
86 return Err(TextError::UnexpectedElement(element_name(&element)));
87 }
88 Event::Eof => {
89 return Err(TextError::Xml(OxmlError::MissingElement(
90 "DrawingML text body".to_owned(),
91 )));
92 }
93 _ => {}
94 }
95 buffer.clear();
96 }
97 }
98
99 fn from_element(reader: &mut Reader<&[u8]>, root_local_name: &[u8]) -> Result<Self> {
100 let mut body_properties = None;
101 let mut list_style = None;
102 let mut paragraphs = Vec::new();
103 let mut raw_children = OrderedRawChildren::default();
104 let mut boundary = 0;
105 let mut buffer = Vec::new();
106
107 loop {
108 match reader
109 .read_event_into(&mut buffer)
110 .map_err(OxmlError::from)?
111 {
112 Event::Start(element) if matches_local_name(element.name().as_ref(), b"bodyPr") => {
113 if body_properties.is_some() {
114 return Err(TextError::DuplicateElement("bodyPr".to_owned()));
115 }
116 body_properties = Some(CT_TextBodyProperties::from_element(reader, &element)?);
117 boundary = boundary.max(1);
118 }
119 Event::Empty(element) if matches_local_name(element.name().as_ref(), b"bodyPr") => {
120 if body_properties.is_some() {
121 return Err(TextError::DuplicateElement("bodyPr".to_owned()));
122 }
123 body_properties = Some(CT_TextBodyProperties::from_start(&element)?);
124 boundary = boundary.max(1);
125 }
126 Event::Start(element)
127 if matches_local_name(element.name().as_ref(), b"lstStyle") =>
128 {
129 if list_style.is_some() {
130 return Err(TextError::DuplicateElement("lstStyle".to_owned()));
131 }
132 let raw = capture_element(reader, &element)?;
133 list_style = Some(CT_TextListStyle::from_xml(&raw)?);
134 boundary = boundary.max(2);
135 }
136 Event::Empty(element)
137 if matches_local_name(element.name().as_ref(), b"lstStyle") =>
138 {
139 if list_style.is_some() {
140 return Err(TextError::DuplicateElement("lstStyle".to_owned()));
141 }
142 let raw = capture_empty_element(&element)?;
143 list_style = Some(CT_TextListStyle::from_xml(&raw)?);
144 boundary = boundary.max(2);
145 }
146 Event::Start(element) if matches_local_name(element.name().as_ref(), b"p") => {
147 let raw = capture_element(reader, &element)?;
148 paragraphs.push(CT_TextParagraph::from_xml(&raw)?);
149 boundary = boundary.max(2 + paragraphs.len());
150 }
151 Event::Empty(element) if matches_local_name(element.name().as_ref(), b"p") => {
152 let raw = capture_empty_element(&element)?;
153 paragraphs.push(CT_TextParagraph::from_xml(&raw)?);
154 boundary = boundary.max(2 + paragraphs.len());
155 }
156 Event::Start(element) => {
157 raw_children.push(boundary, capture_element(reader, &element)?)
158 }
159 Event::Empty(element) => {
160 raw_children.push(boundary, capture_empty_element(&element)?)
161 }
162 Event::End(element)
163 if matches_local_name(element.name().as_ref(), root_local_name) =>
164 {
165 break;
166 }
167 Event::Eof => {
168 return Err(missing_end(&String::from_utf8_lossy(root_local_name)));
169 }
170 _ => {}
171 }
172 buffer.clear();
173 }
174
175 let body_properties = body_properties.ok_or(TextError::MissingBodyProperties)?;
176 Ok(Self {
177 body_properties,
178 list_style,
179 paragraphs,
180 raw_children,
181 })
182 }
183
184 pub fn to_xml(&self) -> Result<Vec<u8>> {
186 let mut writer = Writer::new(Vec::new());
187 self.write_xml(&mut writer)?;
188 Ok(writer.into_inner())
189 }
190
191 pub fn write_xml<W: Write>(&self, writer: &mut Writer<W>) -> Result<()> {
193 self.write_xml_as(writer, "a:txBody")
194 }
195
196 pub fn write_xml_as<W: Write>(&self, writer: &mut Writer<W>, tag: &str) -> Result<()> {
198 writer
199 .write_event(Event::Start(BytesStart::new(tag)))
200 .map_err(OxmlError::from)?;
201 emit_raw(writer, self.raw_children.at(0))?;
202 self.body_properties.write_xml(writer)?;
203 emit_raw(writer, self.raw_children.at(1))?;
204 if let Some(list_style) = &self.list_style {
205 list_style.write_xml(writer)?;
206 }
207 emit_raw(writer, self.raw_children.at(2))?;
208 for (index, paragraph) in self.paragraphs.iter().enumerate() {
209 paragraph.write_xml(writer)?;
210 emit_raw(writer, self.raw_children.at(3 + index))?;
211 }
212 writer
213 .write_event(Event::End(BytesEnd::new(tag)))
214 .map_err(OxmlError::from)?;
215 Ok(())
216 }
217
218 pub fn plain_text(&self) -> String {
220 self.paragraphs
221 .iter()
222 .map(|paragraph| {
223 let mut text = String::new();
224 for run in ¶graph.runs {
225 match run {
226 TextRun::Run(run) => text.push_str(&run.text.value),
227 TextRun::Break(_) => text.push('\n'),
228 TextRun::Field(field) => {
229 if let Some(value) = &field.text {
230 text.push_str(&value.value);
231 }
232 }
233 }
234 }
235 text
236 })
237 .collect::<Vec<_>>()
238 .join("\n")
239 }
240
241 pub fn has_list_style(&self) -> bool {
242 self.list_style.is_some()
243 }
244
245 pub fn list_style(&self) -> Option<&CT_TextListStyle> {
246 self.list_style.as_ref()
247 }
248
249 pub fn paragraph_count(&self) -> usize {
250 self.paragraphs.len()
251 }
252
253 pub fn paragraphs(&self) -> &[CT_TextParagraph] {
254 &self.paragraphs
255 }
256
257 pub fn set_text(&mut self, text: &str) {
262 let old_paragraph_count = self.paragraphs.len();
263 let mut first = self.paragraphs.drain(..).next().unwrap_or_default();
264 let mut lines = text.split("\r\n").flat_map(|part| part.split('\n'));
265 first.set_text(lines.next().unwrap_or_default());
266 let mut template = CT_TextParagraph::default();
267 template.properties = first.properties.clone();
268 template.runs = first.runs.iter().take(1).cloned().collect();
269 template.end_properties = first.end_properties.clone();
270 self.paragraphs.push(first);
271 for line in lines {
272 let mut paragraph = template.clone();
273 paragraph.set_text(line);
274 self.paragraphs.push(paragraph);
275 }
276 self.reconcile_paragraph_raw_children(old_paragraph_count, self.paragraphs.len());
277 }
278
279 pub fn empty_like(&self) -> Self {
289 let mut paragraph = CT_TextParagraph::default();
290 if let Some(first) = self.paragraphs.first() {
291 let first_run = first.runs.iter().find_map(|run| match run {
292 TextRun::Run(run) => Some(run),
293 TextRun::Break(_) | TextRun::Field(_) => None,
294 });
295 paragraph.properties = first.properties.clone();
296 paragraph.end_properties = first_run
297 .and_then(|run| run.properties.clone())
298 .or_else(|| first.end_properties.clone())
299 .map(CT_TextCharacterProperties::without_hyperlinks);
300 }
301 Self {
302 body_properties: self.body_properties.clone(),
303 list_style: self.list_style.clone(),
304 paragraphs: vec![paragraph],
305 raw_children: OrderedRawChildren::default(),
306 }
307 }
308
309 pub fn paragraph_mut(&mut self, index: usize) -> Option<&mut CT_TextParagraph> {
311 self.paragraphs.get_mut(index)
312 }
313
314 pub fn add_paragraph(&mut self) -> &mut CT_TextParagraph {
316 self.raw_children
317 .shift_boundaries_from(2 + self.paragraphs.len());
318 self.paragraphs.push(CT_TextParagraph::default());
319 self.paragraphs.last_mut().expect("paragraph was appended")
320 }
321
322 pub fn move_content_to(&mut self, destination: &mut Self) {
325 if self.plain_text().is_empty() {
326 return;
327 }
328
329 let source_paragraph_count = self.paragraphs.len();
330 let moved = std::mem::take(&mut self.paragraphs);
331 self.paragraphs.push(CT_TextParagraph::default());
332 self.reconcile_paragraph_raw_children(source_paragraph_count, 1);
333
334 if destination.plain_text().is_empty() {
335 let destination_paragraph_count = destination.paragraphs.len();
336 destination.paragraphs = moved;
337 destination.reconcile_paragraph_raw_children(
338 destination_paragraph_count,
339 destination.paragraphs.len(),
340 );
341 return;
342 }
343
344 for paragraph in moved {
345 destination
346 .raw_children
347 .shift_boundaries_from(2 + destination.paragraphs.len());
348 destination.paragraphs.push(paragraph);
349 }
350 }
351
352 fn reconcile_paragraph_raw_children(
353 &mut self,
354 old_paragraph_count: usize,
355 new_paragraph_count: usize,
356 ) {
357 let mut raw_children = OrderedRawChildren::default();
358 for boundary in 0..=2 + old_paragraph_count {
359 let new_boundary = if boundary <= 2 {
360 boundary
361 } else {
362 2 + new_paragraph_count
363 };
364 for child in self.raw_children.at(boundary) {
365 raw_children.push(new_boundary, child.to_vec());
366 }
367 }
368 self.raw_children = raw_children;
369 }
370
371 pub fn raw_children(&self) -> &OrderedRawChildren {
372 &self.raw_children
373 }
374}
375
376fn emit_raw<'a, W: Write>(
377 writer: &mut Writer<W>,
378 children: impl Iterator<Item = &'a [u8]>,
379) -> Result<()> {
380 for child in children {
381 writer.get_mut().write_all(child).map_err(OxmlError::from)?;
382 }
383 Ok(())
384}
385
386fn element_name(element: &BytesStart<'_>) -> String {
387 String::from_utf8_lossy(element.name().as_ref()).into_owned()
388}
389
390#[cfg(test)]
391mod tests {
392 use std::panic;
393
394 use super::{CT_TextBody, CT_TextListStyle};
395
396 #[test]
397 fn text_body_reads_any_prefix_and_writes_the_fixed_a_prefix() {
398 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>"#;
399 let body = CT_TextBody::from_xml(xml).unwrap();
400 assert!(body.has_list_style());
401 assert_eq!(body.paragraph_count(), 1);
402 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>"#);
403 }
404
405 #[test]
406 fn moving_content_retains_typed_paragraphs_and_source_body_state() {
407 let mut destination = CT_TextBody::from_xml(
408 br#"<a:txBody><a:bodyPr/><a:p><a:r><a:rPr b="1"/><a:t>one</a:t></a:r></a:p></a:txBody>"#,
409 )
410 .unwrap();
411 let mut source = CT_TextBody::from_xml(
412 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>"#,
413 )
414 .unwrap();
415
416 source.move_content_to(&mut destination);
417
418 assert_eq!(destination.plain_text(), "one\ntwo\nthree");
419 assert_eq!(destination.paragraph_count(), 3);
420 assert_eq!(source.plain_text(), "");
421 assert_eq!(source.paragraph_count(), 1);
422 assert!(
423 String::from_utf8(source.to_xml().unwrap())
424 .unwrap()
425 .contains(r#"lIns="10""#)
426 );
427 let written = String::from_utf8(destination.to_xml().unwrap()).unwrap();
428 assert!(written.contains(r#"<a:rPr b="1"/>"#));
429 assert!(written.contains(r#"<a:rPr i="1"/>"#));
430 }
431
432 #[test]
433 fn set_text_starts_a_paragraph_at_each_line_feed_and_breaks_at_each_vertical_tab() {
434 let mut body = CT_TextBody::from_xml(
435 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>"#,
436 )
437 .unwrap();
438 body.set_text("one\r\ntwo\u{b}soft\u{b}\n");
439 assert_eq!(
440 String::from_utf8(body.to_xml().unwrap()).unwrap(),
441 concat!(
442 r#"<a:txBody><a:bodyPr/>"#,
443 r#"<a:p><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>"#,
444 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>"#,
445 r#"<a:p><a:pPr algn="ctr"/><a:r><a:rPr b="1"/><a:t/></a:r><a:endParaRPr sz="1200"/></a:p>"#,
446 r#"<x:after-paragraphs/></a:txBody>"#,
447 )
448 );
449 assert_eq!(body.plain_text(), "one\ntwo\nsoft\n\n");
450
451 let mut plain = CT_TextBody::new();
452 plain.set_text("single line");
453 assert_eq!(plain.paragraph_count(), 1);
454 assert_eq!(plain.plain_text(), "single line");
455 }
456
457 #[test]
458 fn schema_valid_text_body_using_all_nine_list_levels_round_trips_structurally() {
459 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>"#;
460 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>"#;
461
462 let body = CT_TextBody::from_xml(xml).unwrap();
463 let written = body.to_xml().unwrap();
464 assert_eq!(written, expected);
465 assert_eq!(CT_TextBody::from_xml(&written).unwrap(), body);
466 }
467
468 #[test]
469 fn list_style_levels_write_in_ascending_schema_order() {
470 let body = CT_TextBody::from_xml(
471 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>"#,
472 )
473 .unwrap();
474 assert_eq!(
475 body.to_xml().unwrap(),
476 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>"#
477 );
478 }
479
480 #[test]
481 fn unknown_list_style_children_round_trip_byte_for_byte() {
482 let body = CT_TextBody::from_xml(
483 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>"#,
484 )
485 .unwrap();
486 assert_eq!(
487 body.to_xml().unwrap(),
488 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>"#
489 );
490 }
491
492 #[test]
493 fn list_style_rejects_nested_fixed_prefix_rebinding() {
494 let xml = br#"<p:defaultTextStyle xmlns:p="urn:presentation" xmlns:d="http://schemas.openxmlformats.org/drawingml/2006/main"><d:lvl1pPr xmlns:a="urn:producer"><a:raw/></d:lvl1pPr></p:defaultTextStyle>"#;
495 assert!(CT_TextListStyle::from_xml(xml).is_err());
496 }
497
498 #[test]
499 fn list_style_rejects_fixed_prefix_rebinding_at_typed_descendants() {
500 let descendants: &[&[u8]] = &[
501 br#"<d:defRPr xmlns:a="urn:producer"/>"#,
502 br#"<d:spcBef xmlns:a="urn:producer"><d:spcPts val="600"/></d:spcBef>"#,
503 br#"<d:spcBef><d:spcPts xmlns:a="urn:producer" val="600"/></d:spcBef>"#,
504 br#"<d:buChar xmlns:a="urn:producer" char="*"/>"#,
505 br#"<d:buClr xmlns:a="urn:producer"><d:srgbClr val="102030"/></d:buClr>"#,
506 br#"<d:buClr><d:srgbClr xmlns:a="urn:producer" val="102030"/></d:buClr>"#,
507 br#"<d:defRPr><d:solidFill xmlns:a="urn:producer"><d:srgbClr val="102030"/></d:solidFill></d:defRPr>"#,
508 br#"<d:defRPr><d:solidFill><d:srgbClr xmlns:a="urn:producer" val="102030"/></d:solidFill></d:defRPr>"#,
509 br#"<d:defRPr><d:solidFill><d:srgbClr val="102030"><d:alpha xmlns:a="urn:producer" val="50000"/></d:srgbClr></d:solidFill></d:defRPr>"#,
510 ];
511
512 for descendant in descendants {
513 let mut xml = br#"<p:defaultTextStyle xmlns:p="urn:presentation" xmlns:d="http://schemas.openxmlformats.org/drawingml/2006/main"><d:lvl1pPr>"#.to_vec();
514 xml.extend_from_slice(descendant);
515 xml.extend_from_slice(br#"</d:lvl1pPr></p:defaultTextStyle>"#);
516 assert!(
517 CT_TextListStyle::from_xml(&xml).is_err(),
518 "typed descendant accepted a conflicting xmlns:a: {}",
519 String::from_utf8_lossy(descendant)
520 );
521 }
522 }
523
524 #[test]
525 fn opaque_list_style_child_preserves_its_local_prefix_binding() {
526 let opaque = br#"<x:extension xmlns:x="urn:extension" xmlns:a="urn:producer"><a:data/></x:extension>"#;
527 let xml = br#"<p:defaultTextStyle xmlns:p="urn:presentation" 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>"#;
528 let parsed = CT_TextListStyle::from_xml(xml).unwrap();
529 let written = parsed.to_xml().unwrap();
530 assert!(
531 written
532 .windows(opaque.len())
533 .any(|window| window == opaque.as_slice())
534 );
535 }
536
537 #[test]
538 fn opaque_typed_descendants_preserve_their_local_prefix_bindings() {
539 let character_extension = br#"<x:extension xmlns:x="urn:extension" xmlns:a="urn:producer"><a:data/></x:extension>"#;
540 let transform_with_content =
541 br#"<d:alpha xmlns:a="urn:producer" val="50000"><a:data/></d:alpha>"#;
542 let xml = br#"<p:defaultTextStyle xmlns:p="urn:presentation" 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>"#;
543
544 let written = CT_TextListStyle::from_xml(xml).unwrap().to_xml().unwrap();
545 for opaque in [
546 character_extension.as_slice(),
547 transform_with_content.as_slice(),
548 ] {
549 assert!(
550 written.windows(opaque.len()).any(|window| window == opaque),
551 "opaque descendant was not preserved: {}",
552 String::from_utf8_lossy(opaque)
553 );
554 }
555 }
556
557 #[test]
558 fn invalid_list_levels_return_errors_without_panicking() {
559 let cases: &[&[u8]] = &[
560 br#"<q:txBody><q:bodyPr/><q:lstStyle><q:lvl0pPr/></q:lstStyle><q:p/></q:txBody>"#,
561 br#"<q:txBody><q:bodyPr/><q:lstStyle><q:lvl10pPr/></q:lstStyle><q:p/></q:txBody>"#,
562 br#"<q:txBody><q:bodyPr/><q:lstStyle><q:lvl01pPr/></q:lstStyle><q:p/></q:txBody>"#,
563 br#"<q:txBody><q:bodyPr/><q:lstStyle><q:lvl1pPr/><q:lvl1pPr/></q:lstStyle><q:p/></q:txBody>"#,
564 br#"<q:txBody><q:bodyPr/><q:lstStyle><q:lvl4pPr lvl="9"/></q:lstStyle><q:p/></q:txBody>"#,
565 br#"<q:txBody><q:bodyPr/><q:lstStyle><q:lvl7pPr><q:buChar/></q:lvl7pPr></q:lstStyle><q:p/></q:txBody>"#,
566 ];
567
568 for xml in cases {
569 let result = panic::catch_unwind(|| CT_TextBody::from_xml(xml));
570 assert!(result.is_ok(), "list-style parser panicked");
571 assert!(result.unwrap().is_err(), "invalid list level parsed");
572 }
573 }
574}