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