Skip to main content

oxml_drawing/text/
body.rs

1use std::fmt;
2use std::io::Write;
3
4use oxml_core::OxmlError;
5use oxml_core::raw_xml::{capture_element, capture_empty_element};
6use oxml_core::xml::{get_attr, local_name, matches_local_name};
7use quick_xml::events::{BytesEnd, BytesStart, Event};
8use quick_xml::{Reader, Writer, XmlVersion};
9
10use crate::color::ColorError;
11use crate::fill::FillError;
12use crate::namespace::reject_conflicting_a_prefix;
13use crate::order::OrderedRawChildren;
14
15const MAX_TEXT_SPACING_PERCENT: i32 = 13_200_000;
16
17/// Errors produced while parsing or writing DrawingML text shells.
18#[derive(Debug)]
19pub enum TextError {
20    Xml(OxmlError),
21    Color(ColorError),
22    Fill(FillError),
23    UnexpectedElement(String),
24    MissingAttribute {
25        element: String,
26        attribute: String,
27    },
28    MissingBodyProperties,
29    MissingParagraph,
30    DuplicateElement(String),
31    InvalidAttribute {
32        element: String,
33        attribute: String,
34        value: String,
35    },
36}
37
38impl fmt::Display for TextError {
39    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
40        match self {
41            Self::Xml(error) => error.fmt(formatter),
42            Self::Color(error) => error.fmt(formatter),
43            Self::Fill(error) => error.fmt(formatter),
44            Self::UnexpectedElement(element) => {
45                write!(formatter, "unexpected DrawingML text element: {element}")
46            }
47            Self::MissingAttribute { element, attribute } => {
48                write!(formatter, "DrawingML {element} requires @{attribute}")
49            }
50            Self::MissingBodyProperties => write!(formatter, "DrawingML txBody requires bodyPr"),
51            Self::MissingParagraph => write!(formatter, "DrawingML txBody requires at least one p"),
52            Self::DuplicateElement(element) => {
53                write!(formatter, "DrawingML text contains duplicate {element}")
54            }
55            Self::InvalidAttribute {
56                element,
57                attribute,
58                value,
59            } => write!(
60                formatter,
61                "DrawingML {element} has invalid @{attribute}: {value}"
62            ),
63        }
64    }
65}
66
67impl std::error::Error for TextError {
68    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
69        match self {
70            Self::Xml(error) => Some(error),
71            Self::Color(error) => Some(error),
72            Self::Fill(error) => Some(error),
73            _ => None,
74        }
75    }
76}
77
78impl From<OxmlError> for TextError {
79    fn from(error: OxmlError) -> Self {
80        Self::Xml(error)
81    }
82}
83
84impl From<ColorError> for TextError {
85    fn from(error: ColorError) -> Self {
86        Self::Color(error)
87    }
88}
89
90impl From<FillError> for TextError {
91    fn from(error: FillError) -> Self {
92        Self::Fill(error)
93    }
94}
95
96pub type Result<T> = std::result::Result<T, TextError>;
97
98/// One strict or transitional lexical form of `ST_Coordinate32`.
99#[derive(Clone, Debug, Eq, PartialEq)]
100pub enum Coordinate32Value {
101    Emu(i32),
102    UniversalMeasure(String),
103}
104
105impl Coordinate32Value {
106    fn parse(element: &str, attribute: &str, value: String) -> Result<Self> {
107        if let Ok(value) = value.parse::<i32>() {
108            return Ok(Self::Emu(value));
109        }
110        if is_universal_measure(&value) {
111            return Ok(Self::UniversalMeasure(value));
112        }
113        Err(invalid_attribute(element, attribute, value))
114    }
115
116    fn as_xml(&self) -> Result<String> {
117        match self {
118            Self::Emu(value) => Ok(value.to_string()),
119            Self::UniversalMeasure(value) if is_universal_measure(value) => Ok(value.clone()),
120            Self::UniversalMeasure(value) => {
121                Err(invalid_attribute("bodyPr", "inset", value.clone()))
122            }
123        }
124    }
125}
126
127/// Vertical anchoring inside the text rectangle.
128#[derive(Clone, Copy, Debug, Eq, PartialEq)]
129pub enum TextAnchor {
130    Top,
131    Center,
132    Bottom,
133    Justified,
134    Distributed,
135}
136
137impl TextAnchor {
138    fn parse(value: &str) -> Option<Self> {
139        match value {
140            "t" => Some(Self::Top),
141            "ctr" => Some(Self::Center),
142            "b" => Some(Self::Bottom),
143            "just" => Some(Self::Justified),
144            "dist" => Some(Self::Distributed),
145            _ => None,
146        }
147    }
148
149    const fn as_str(self) -> &'static str {
150        match self {
151            Self::Top => "t",
152            Self::Center => "ctr",
153            Self::Bottom => "b",
154            Self::Justified => "just",
155            Self::Distributed => "dist",
156        }
157    }
158}
159
160/// Text wrapping at the shape boundary.
161#[derive(Clone, Copy, Debug, Eq, PartialEq)]
162pub enum TextWrap {
163    None,
164    Square,
165}
166
167impl TextWrap {
168    fn parse(value: &str) -> Option<Self> {
169        match value {
170            "none" => Some(Self::None),
171            "square" => Some(Self::Square),
172            _ => None,
173        }
174    }
175
176    const fn as_str(self) -> &'static str {
177        match self {
178            Self::None => "none",
179            Self::Square => "square",
180        }
181    }
182}
183
184/// The seven values of `ST_TextVerticalType`.
185#[derive(Clone, Copy, Debug, Eq, PartialEq)]
186pub enum TextVertical {
187    Horizontal,
188    Vertical,
189    Vertical270,
190    WordArtVertical,
191    EastAsianVertical,
192    MongolianVertical,
193    WordArtVerticalRtl,
194}
195
196impl TextVertical {
197    fn parse(value: &str) -> Option<Self> {
198        match value {
199            "horz" => Some(Self::Horizontal),
200            "vert" => Some(Self::Vertical),
201            "vert270" => Some(Self::Vertical270),
202            "wordArtVert" => Some(Self::WordArtVertical),
203            "eaVert" => Some(Self::EastAsianVertical),
204            "mongolianVert" => Some(Self::MongolianVertical),
205            "wordArtVertRtl" => Some(Self::WordArtVerticalRtl),
206            _ => None,
207        }
208    }
209
210    const fn as_str(self) -> &'static str {
211        match self {
212            Self::Horizontal => "horz",
213            Self::Vertical => "vert",
214            Self::Vertical270 => "vert270",
215            Self::WordArtVertical => "wordArtVert",
216            Self::EastAsianVertical => "eaVert",
217            Self::MongolianVertical => "mongolianVert",
218            Self::WordArtVerticalRtl => "wordArtVertRtl",
219        }
220    }
221}
222
223/// Stored values on one `a:normAutofit` choice.
224#[derive(Clone, Debug, Default, Eq, PartialEq)]
225pub struct NormalAutofit {
226    pub font_scale: Option<String>,
227    pub line_spacing_reduction: Option<String>,
228}
229
230/// The three members of `EG_TextAutofit`.
231#[derive(Clone, Debug, Eq, PartialEq)]
232pub enum TextAutofit {
233    NoAutofit,
234    Normal(NormalAutofit),
235    ShapeAutofit,
236}
237
238impl TextAutofit {
239    fn from_xml(xml: &[u8]) -> Result<Self> {
240        let mut reader = Reader::from_reader(xml);
241        let mut buffer = Vec::new();
242        loop {
243            match reader
244                .read_event_into(&mut buffer)
245                .map_err(OxmlError::from)?
246            {
247                Event::Empty(element) => return Self::from_start(&element),
248                Event::Start(element) => {
249                    let autofit = Self::from_start(&element)?;
250                    ensure_empty_element(&mut reader, local_name(element.name().as_ref()))?;
251                    return Ok(autofit);
252                }
253                Event::Eof => {
254                    return Err(TextError::UnexpectedElement("EOF".to_owned()));
255                }
256                _ => {}
257            }
258            buffer.clear();
259        }
260    }
261
262    fn from_start(start: &BytesStart<'_>) -> Result<Self> {
263        match local_name(start.name().as_ref()) {
264            b"noAutofit" => Ok(Self::NoAutofit),
265            b"spAutoFit" => Ok(Self::ShapeAutofit),
266            b"normAutofit" => {
267                let font_scale = get_attr(start, b"fontScale");
268                let line_spacing_reduction = get_attr(start, b"lnSpcReduction");
269                if let Some(value) = font_scale.as_deref() {
270                    validate_autofit_percent("fontScale", value, PercentKind::FontScale)?;
271                }
272                if let Some(value) = line_spacing_reduction.as_deref() {
273                    validate_autofit_percent("lnSpcReduction", value, PercentKind::LineSpacing)?;
274                }
275                Ok(Self::Normal(NormalAutofit {
276                    font_scale,
277                    line_spacing_reduction,
278                }))
279            }
280            _ => Err(TextError::UnexpectedElement(element_name(start))),
281        }
282    }
283
284    fn write_xml<W: Write>(&self, writer: &mut Writer<W>) -> Result<()> {
285        match self {
286            Self::NoAutofit => write_empty(writer, BytesStart::new("a:noAutofit")),
287            Self::ShapeAutofit => write_empty(writer, BytesStart::new("a:spAutoFit")),
288            Self::Normal(normal) => {
289                let mut start = BytesStart::new("a:normAutofit");
290                if let Some(value) = normal.font_scale.as_deref() {
291                    validate_autofit_percent("fontScale", value, PercentKind::FontScale)?;
292                    start.push_attribute(("fontScale", value));
293                }
294                if let Some(value) = normal.line_spacing_reduction.as_deref() {
295                    validate_autofit_percent("lnSpcReduction", value, PercentKind::LineSpacing)?;
296                    start.push_attribute(("lnSpcReduction", value));
297                }
298                write_empty(writer, start)
299            }
300        }
301    }
302}
303
304/// Insets, anchoring, wrapping, vertical direction, and autofit on `a:bodyPr`.
305///
306/// Every other attribute, such as `rot`, `numCol`, `spcCol` or `anchorCtr`,
307/// is kept verbatim in source order and written after the modelled ones.
308#[allow(non_camel_case_types)]
309#[derive(Clone, Debug, Default, Eq, PartialEq)]
310pub struct CT_TextBodyProperties {
311    pub left_inset: Option<Coordinate32Value>,
312    pub top_inset: Option<Coordinate32Value>,
313    pub right_inset: Option<Coordinate32Value>,
314    pub bottom_inset: Option<Coordinate32Value>,
315    pub anchor: Option<TextAnchor>,
316    pub wrap: Option<TextWrap>,
317    pub vertical: Option<TextVertical>,
318    pub space_first_last_paragraph: Option<bool>,
319    pub autofit: Option<TextAutofit>,
320    raw_attributes: Vec<(String, String)>,
321    raw_children: OrderedRawChildren,
322}
323
324impl CT_TextBodyProperties {
325    /// Parses one complete `a:bodyPr` element with any namespace prefix.
326    pub fn from_xml(xml: &[u8]) -> Result<Self> {
327        let mut reader = Reader::from_reader(xml);
328        let mut buffer = Vec::new();
329        loop {
330            match reader
331                .read_event_into(&mut buffer)
332                .map_err(OxmlError::from)?
333            {
334                Event::Start(element) if matches_local_name(element.name().as_ref(), b"bodyPr") => {
335                    return Self::from_element(&mut reader, &element);
336                }
337                Event::Empty(element) if matches_local_name(element.name().as_ref(), b"bodyPr") => {
338                    return Self::from_start(&element);
339                }
340                Event::Start(element) | Event::Empty(element) => {
341                    return Err(TextError::UnexpectedElement(element_name(&element)));
342                }
343                Event::Eof => {
344                    return Err(TextError::Xml(OxmlError::MissingElement(
345                        "DrawingML body properties".to_owned(),
346                    )));
347                }
348                _ => {}
349            }
350            buffer.clear();
351        }
352    }
353
354    pub(crate) fn from_element(reader: &mut Reader<&[u8]>, start: &BytesStart<'_>) -> Result<Self> {
355        let mut properties = Self::from_start(start)?;
356        let mut boundary = 0;
357        let mut occurrences = [false; 5];
358        let mut buffer = Vec::new();
359        loop {
360            match reader
361                .read_event_into(&mut buffer)
362                .map_err(OxmlError::from)?
363            {
364                Event::Start(element) => {
365                    let name = local_name(element.name().as_ref()).to_vec();
366                    let raw = capture_element(reader, &element)?;
367                    properties.capture_child(&name, raw, &mut boundary, &mut occurrences)?;
368                }
369                Event::Empty(element) => {
370                    let name = local_name(element.name().as_ref()).to_vec();
371                    let raw = capture_empty_element(&element)?;
372                    properties.capture_child(&name, raw, &mut boundary, &mut occurrences)?;
373                }
374                Event::End(element) if matches_local_name(element.name().as_ref(), b"bodyPr") => {
375                    break;
376                }
377                Event::Eof => return Err(missing_end("bodyPr")),
378                _ => {}
379            }
380            buffer.clear();
381        }
382        Ok(properties)
383    }
384
385    pub(crate) fn from_start(start: &BytesStart<'_>) -> Result<Self> {
386        if !matches_local_name(start.name().as_ref(), b"bodyPr") {
387            return Err(TextError::UnexpectedElement(element_name(start)));
388        }
389        // Raw attributes are written back, so a local `xmlns:a` would rebind
390        // the fixed prefix of this element and of its typed autofit child.
391        reject_conflicting_a_prefix(start)?;
392        Ok(Self {
393            left_inset: parse_coordinate(start, b"lIns")?,
394            top_inset: parse_coordinate(start, b"tIns")?,
395            right_inset: parse_coordinate(start, b"rIns")?,
396            bottom_inset: parse_coordinate(start, b"bIns")?,
397            anchor: parse_enum(start, b"anchor", TextAnchor::parse)?,
398            wrap: parse_enum(start, b"wrap", TextWrap::parse)?,
399            vertical: parse_enum(start, b"vert", TextVertical::parse)?,
400            space_first_last_paragraph: parse_optional_bool(start, b"spcFirstLastPara")?,
401            autofit: None,
402            raw_attributes: capture_raw_attributes(start)?,
403            raw_children: OrderedRawChildren::default(),
404        })
405    }
406
407    fn capture_child(
408        &mut self,
409        name: &[u8],
410        raw: Vec<u8>,
411        boundary: &mut usize,
412        occurrences: &mut [bool; 5],
413    ) -> Result<()> {
414        if let Some(index) = schema_choice_index(name) {
415            if occurrences[index] {
416                return Err(TextError::DuplicateElement(
417                    String::from_utf8_lossy(name).into_owned(),
418                ));
419            }
420            occurrences[index] = true;
421        }
422        if is_autofit(name) {
423            self.autofit = Some(TextAutofit::from_xml(&raw)?);
424            *boundary = (*boundary).max(2);
425            return Ok(());
426        }
427        // A known child keeps its schema slot, so an autofit choice added
428        // after parsing is still written before a preserved scene or extension.
429        let slot = raw_boundary_after(name).saturating_sub(1);
430        self.raw_children.push((*boundary).max(slot), raw);
431        *boundary = (*boundary).max(raw_boundary_after(name));
432        Ok(())
433    }
434
435    /// Writes body properties with fixed prefixes and schema child order.
436    pub fn to_xml(&self) -> Result<Vec<u8>> {
437        let mut writer = Writer::new(Vec::new());
438        self.write_xml(&mut writer)?;
439        Ok(writer.into_inner())
440    }
441
442    pub(crate) fn write_xml<W: Write>(&self, writer: &mut Writer<W>) -> Result<()> {
443        let mut start = BytesStart::new("a:bodyPr");
444        let left_inset = self
445            .left_inset
446            .as_ref()
447            .map(Coordinate32Value::as_xml)
448            .transpose()?;
449        let top_inset = self
450            .top_inset
451            .as_ref()
452            .map(Coordinate32Value::as_xml)
453            .transpose()?;
454        let right_inset = self
455            .right_inset
456            .as_ref()
457            .map(Coordinate32Value::as_xml)
458            .transpose()?;
459        let bottom_inset = self
460            .bottom_inset
461            .as_ref()
462            .map(Coordinate32Value::as_xml)
463            .transpose()?;
464        for (name, value) in [
465            ("lIns", left_inset.as_deref()),
466            ("tIns", top_inset.as_deref()),
467            ("rIns", right_inset.as_deref()),
468            ("bIns", bottom_inset.as_deref()),
469        ] {
470            if let Some(value) = value {
471                start.push_attribute((name, value));
472            }
473        }
474        if let Some(anchor) = self.anchor {
475            start.push_attribute(("anchor", anchor.as_str()));
476        }
477        if let Some(wrap) = self.wrap {
478            start.push_attribute(("wrap", wrap.as_str()));
479        }
480        if let Some(vertical) = self.vertical {
481            start.push_attribute(("vert", vertical.as_str()));
482        }
483        if let Some(value) = self.space_first_last_paragraph {
484            start.push_attribute(("spcFirstLastPara", if value { "1" } else { "0" }));
485        }
486        for (name, value) in &self.raw_attributes {
487            start.push_attribute((name.as_str(), value.as_str()));
488        }
489
490        if self.autofit.is_none() && self.raw_children.is_empty() {
491            return write_empty(writer, start);
492        }
493        writer
494            .write_event(Event::Start(start))
495            .map_err(OxmlError::from)?;
496        emit_raw(writer, self.raw_children.at(0))?;
497        emit_raw(writer, self.raw_children.at(1))?;
498        if let Some(autofit) = &self.autofit {
499            autofit.write_xml(writer)?;
500        }
501        for boundary in 2..=5 {
502            emit_raw(writer, self.raw_children.at(boundary))?;
503        }
504        writer
505            .write_event(Event::End(BytesEnd::new("a:bodyPr")))
506            .map_err(OxmlError::from)?;
507        Ok(())
508    }
509
510    pub fn raw_children(&self) -> &OrderedRawChildren {
511        &self.raw_children
512    }
513}
514
515/// The attributes `from_start` models, which the raw list must not repeat.
516const MODELLED_ATTRIBUTES: [&[u8]; 8] = [
517    b"lIns",
518    b"tIns",
519    b"rIns",
520    b"bIns",
521    b"anchor",
522    b"wrap",
523    b"vert",
524    b"spcFirstLastPara",
525];
526
527fn capture_raw_attributes(start: &BytesStart<'_>) -> Result<Vec<(String, String)>> {
528    let mut raw = Vec::new();
529    for attribute in start.attributes() {
530        let attribute = attribute.map_err(OxmlError::from)?;
531        let key = attribute.key.as_ref();
532        if MODELLED_ATTRIBUTES.contains(&key) {
533            continue;
534        }
535        let name = std::str::from_utf8(key)
536            .map_err(OxmlError::from)?
537            .to_owned();
538        let value = attribute
539            .decoded_and_normalized_value(XmlVersion::Implicit1_0, start.decoder())
540            .map_err(OxmlError::from)?
541            .into_owned();
542        raw.push((name, value));
543    }
544    Ok(raw)
545}
546
547fn parse_coordinate(start: &BytesStart<'_>, attribute: &[u8]) -> Result<Option<Coordinate32Value>> {
548    get_body_attr(start, attribute)
549        .map(|value| Coordinate32Value::parse("bodyPr", &String::from_utf8_lossy(attribute), value))
550        .transpose()
551}
552
553fn parse_enum<T>(
554    start: &BytesStart<'_>,
555    attribute: &[u8],
556    parse: impl FnOnce(&str) -> Option<T>,
557) -> Result<Option<T>> {
558    let Some(value) = get_body_attr(start, attribute) else {
559        return Ok(None);
560    };
561    parse(&value)
562        .map(Some)
563        .ok_or_else(|| invalid_attribute("bodyPr", &String::from_utf8_lossy(attribute), value))
564}
565
566fn parse_optional_bool(start: &BytesStart<'_>, attribute: &[u8]) -> Result<Option<bool>> {
567    let Some(value) = get_body_attr(start, attribute) else {
568        return Ok(None);
569    };
570    match value.as_str() {
571        "1" | "true" => Ok(Some(true)),
572        "0" | "false" => Ok(Some(false)),
573        _ => Err(invalid_attribute(
574            "bodyPr",
575            &String::from_utf8_lossy(attribute),
576            value,
577        )),
578    }
579}
580
581fn get_body_attr(start: &BytesStart<'_>, name: &[u8]) -> Option<String> {
582    start
583        .attributes()
584        .flatten()
585        .find(|attribute| attribute.key.as_ref() == name)
586        .and_then(|attribute| {
587            std::str::from_utf8(&attribute.value)
588                .ok()
589                .map(str::to_owned)
590        })
591}
592
593fn is_autofit(name: &[u8]) -> bool {
594    matches!(name, b"noAutofit" | b"normAutofit" | b"spAutoFit")
595}
596
597fn schema_choice_index(name: &[u8]) -> Option<usize> {
598    match name {
599        b"prstTxWarp" => Some(0),
600        name if is_autofit(name) => Some(1),
601        b"scene3d" => Some(2),
602        b"sp3d" | b"flatTx" => Some(3),
603        b"extLst" => Some(4),
604        _ => None,
605    }
606}
607
608fn raw_boundary_after(name: &[u8]) -> usize {
609    match name {
610        b"prstTxWarp" => 1,
611        name if is_autofit(name) => 2,
612        b"scene3d" => 3,
613        b"sp3d" | b"flatTx" => 4,
614        b"extLst" => 5,
615        _ => 0,
616    }
617}
618
619enum PercentKind {
620    FontScale,
621    LineSpacing,
622}
623
624fn validate_autofit_percent(attribute: &str, value: &str, kind: PercentKind) -> Result<()> {
625    if is_percentage_string(value) {
626        return Ok(());
627    }
628    let integer = value
629        .parse::<i32>()
630        .map_err(|_| invalid_attribute("normAutofit", attribute, value.to_owned()))?;
631    let valid = match kind {
632        PercentKind::FontScale => (1_000..=100_000).contains(&integer),
633        PercentKind::LineSpacing => (0..=MAX_TEXT_SPACING_PERCENT).contains(&integer),
634    };
635    if valid {
636        Ok(())
637    } else {
638        Err(invalid_attribute(
639            "normAutofit",
640            attribute,
641            value.to_owned(),
642        ))
643    }
644}
645
646fn is_percentage_string(value: &str) -> bool {
647    let Some(number) = value.strip_suffix('%') else {
648        return false;
649    };
650    is_signed_decimal(number)
651}
652
653fn is_universal_measure(value: &str) -> bool {
654    if value.len() < 3 {
655        return false;
656    }
657    let (number, unit) = value.split_at(value.len() - 2);
658    matches!(unit, "mm" | "cm" | "in" | "pt" | "pc" | "pi") && is_signed_decimal(number)
659}
660
661fn is_signed_decimal(value: &str) -> bool {
662    let unsigned = value.strip_prefix('-').unwrap_or(value);
663    let mut parts = unsigned.split('.');
664    let Some(integer) = parts.next() else {
665        return false;
666    };
667    if integer.is_empty() || !integer.bytes().all(|byte| byte.is_ascii_digit()) {
668        return false;
669    }
670    if let Some(fraction) = parts.next()
671        && (fraction.is_empty() || !fraction.bytes().all(|byte| byte.is_ascii_digit()))
672    {
673        return false;
674    }
675    parts.next().is_none()
676}
677
678fn ensure_empty_element(reader: &mut Reader<&[u8]>, expected: &[u8]) -> Result<()> {
679    let mut buffer = Vec::new();
680    loop {
681        match reader
682            .read_event_into(&mut buffer)
683            .map_err(OxmlError::from)?
684        {
685            Event::End(element) if matches_local_name(element.name().as_ref(), expected) => {
686                return Ok(());
687            }
688            Event::Text(text) if text.iter().all(u8::is_ascii_whitespace) => {}
689            Event::Comment(_) => {}
690            Event::Eof => return Err(missing_end(&String::from_utf8_lossy(expected))),
691            Event::Start(element) | Event::Empty(element) => {
692                return Err(TextError::UnexpectedElement(element_name(&element)));
693            }
694            _ => {
695                return Err(TextError::UnexpectedElement(
696                    String::from_utf8_lossy(expected).into_owned(),
697                ));
698            }
699        }
700        buffer.clear();
701    }
702}
703
704fn emit_raw<'a, W: Write>(
705    writer: &mut Writer<W>,
706    children: impl Iterator<Item = &'a [u8]>,
707) -> Result<()> {
708    for child in children {
709        writer.get_mut().write_all(child).map_err(OxmlError::from)?;
710    }
711    Ok(())
712}
713
714fn write_empty<W: Write>(writer: &mut Writer<W>, start: BytesStart<'_>) -> Result<()> {
715    writer
716        .write_event(Event::Empty(start))
717        .map_err(OxmlError::from)?;
718    Ok(())
719}
720
721fn invalid_attribute(element: &str, attribute: &str, value: String) -> TextError {
722    TextError::InvalidAttribute {
723        element: element.to_owned(),
724        attribute: attribute.to_owned(),
725        value,
726    }
727}
728
729fn element_name(element: &BytesStart<'_>) -> String {
730    String::from_utf8_lossy(element.name().as_ref()).into_owned()
731}
732
733pub(crate) fn missing_end(element: &str) -> TextError {
734    TextError::Xml(OxmlError::MissingElement(format!(
735        "closing DrawingML {element}"
736    )))
737}
738
739#[cfg(test)]
740mod tests {
741    use std::panic;
742
743    use super::{CT_TextBodyProperties, Coordinate32Value, TextAnchor, TextAutofit};
744    use crate::text::CT_TextBody;
745
746    #[test]
747    fn every_body_property_autofit_form_round_trips_in_schema_order() {
748        let cases: &[(&[u8], &[u8])] = &[
749            (
750                br#"<q:bodyPr lIns="-2147483648" tIns="2147483647" rIns="1.25in" bIns="0" anchor="dist" wrap="square" vert="wordArtVertRtl" spcFirstLastPara="true"><x:warp/><q:prstTxWarp prst="textPlain"/><x:beforeFit/><q:noAutofit/></q:bodyPr>"#,
751                br#"<a:bodyPr lIns="-2147483648" tIns="2147483647" rIns="1.25in" bIns="0" anchor="dist" wrap="square" vert="wordArtVertRtl" spcFirstLastPara="1"><x:warp/><q:prstTxWarp prst="textPlain"/><x:beforeFit/><a:noAutofit/></a:bodyPr>"#,
752            ),
753            (
754                br#"<q:bodyPr><q:spAutoFit/></q:bodyPr>"#,
755                br#"<a:bodyPr><a:spAutoFit/></a:bodyPr>"#,
756            ),
757            (
758                br#"<q:bodyPr><q:normAutofit fontScale="62.500%" lnSpcReduction="20000"/></q:bodyPr>"#,
759                br#"<a:bodyPr><a:normAutofit fontScale="62.500%" lnSpcReduction="20000"/></a:bodyPr>"#,
760            ),
761        ];
762
763        for (xml, expected) in cases {
764            let parsed = CT_TextBodyProperties::from_xml(xml).unwrap();
765            let written = parsed.to_xml().unwrap();
766            assert_eq!(&written, expected);
767            assert_eq!(CT_TextBodyProperties::from_xml(&written).unwrap(), parsed);
768        }
769
770        let parsed = CT_TextBodyProperties::from_xml(cases[0].0).unwrap();
771        assert_eq!(parsed.left_inset, Some(Coordinate32Value::Emu(i32::MIN)));
772        assert_eq!(parsed.space_first_last_paragraph, Some(true));
773        assert!(matches!(parsed.autofit, Some(TextAutofit::NoAutofit)));
774    }
775
776    #[test]
777    fn every_body_property_attribute_survives_a_round_trip() {
778        let xml = br#"<q:bodyPr xmlns:x="urn:producer" rot="5400000" spcFirstLastPara="1" vertOverflow="clip" horzOverflow="overflow" vert="vert270" wrap="square" lIns="1" tIns="2" rIns="3" bIns="4" numCol="2" spcCol="91440" rtlCol="1" fromWordArt="1" anchor="ctr" anchorCtr="0" forceAA="1" upright="1" compatLnSpc="0" x:marker="kept"><q:normAutofit fontScale="90000"/></q:bodyPr>"#;
779        let parsed = CT_TextBodyProperties::from_xml(xml).unwrap();
780        let written = parsed.to_xml().unwrap();
781        // Modelled attributes keep their fixed order and every other one
782        // follows in source order, so a second pass writes the same bytes.
783        assert_eq!(
784            written,
785            br#"<a:bodyPr lIns="1" tIns="2" rIns="3" bIns="4" anchor="ctr" wrap="square" vert="vert270" spcFirstLastPara="1" xmlns:x="urn:producer" rot="5400000" vertOverflow="clip" horzOverflow="overflow" numCol="2" spcCol="91440" rtlCol="1" fromWordArt="1" anchorCtr="0" forceAA="1" upright="1" compatLnSpc="0" x:marker="kept"><a:normAutofit fontScale="90000"/></a:bodyPr>"#
786        );
787        let reparsed = CT_TextBodyProperties::from_xml(&written).unwrap();
788        assert_eq!(reparsed, parsed);
789        assert_eq!(reparsed.to_xml().unwrap(), written);
790
791        let mut edited = CT_TextBodyProperties::from_xml(
792            br#"<q:bodyPr numCol="3" spcCol="45720" rot="-600000" anchorCtr="1"/>"#,
793        )
794        .unwrap();
795        edited.anchor = Some(TextAnchor::Bottom);
796        edited.left_inset = Some(Coordinate32Value::Emu(0));
797        assert_eq!(
798            edited.to_xml().unwrap(),
799            br#"<a:bodyPr lIns="0" anchor="b" numCol="3" spcCol="45720" rot="-600000" anchorCtr="1"/>"#
800        );
801    }
802
803    #[test]
804    fn foreign_attribute_with_modelled_local_name_is_retained() {
805        let parsed = CT_TextBodyProperties::from_xml(
806            br#"<q:bodyPr xmlns:x="urn:producer" x:wrap="foreign" wrap="square"/>"#,
807        )
808        .unwrap();
809        assert!(parsed.wrap.is_some());
810        assert_eq!(
811            parsed.to_xml().unwrap(),
812            br#"<a:bodyPr wrap="square" xmlns:x="urn:producer" x:wrap="foreign"/>"#
813        );
814    }
815
816    #[test]
817    fn body_properties_preserve_unknown_children_at_their_boundaries() {
818        let xml = br#"<q:bodyPr><x:before/><q:prstTxWarp prst="textPlain"><x:warp/></q:prstTxWarp><x:beforeFit/><q:normAutofit fontScale="62500"/><x:afterFit/><q:scene3d><x:scene/></q:scene3d><x:afterScene/><q:sp3d><x:shape/></q:sp3d><x:after3d/><q:extLst><x:ext/></q:extLst><x:afterExt/></q:bodyPr>"#;
819        let written = CT_TextBodyProperties::from_xml(xml)
820            .unwrap()
821            .to_xml()
822            .unwrap();
823        assert_eq!(written, br#"<a:bodyPr><x:before/><q:prstTxWarp prst="textPlain"><x:warp/></q:prstTxWarp><x:beforeFit/><a:normAutofit fontScale="62500"/><x:afterFit/><q:scene3d><x:scene/></q:scene3d><x:afterScene/><q:sp3d><x:shape/></q:sp3d><x:after3d/><q:extLst><x:ext/></q:extLst><x:afterExt/></a:bodyPr>"#);
824    }
825
826    #[test]
827    fn an_added_autofit_is_written_before_preserved_scene_and_extension_children() {
828        let mut properties = CT_TextBodyProperties::from_xml(
829            br#"<q:bodyPr><x:first/><q:scene3d><x:scene/></q:scene3d><q:flatTx/><q:extLst><x:ext/></q:extLst></q:bodyPr>"#,
830        )
831        .unwrap();
832        properties.autofit = Some(TextAutofit::ShapeAutofit);
833        let written = properties.to_xml().unwrap();
834        assert_eq!(
835            written,
836            br#"<a:bodyPr><x:first/><a:spAutoFit/><q:scene3d><x:scene/></q:scene3d><q:flatTx/><q:extLst><x:ext/></q:extLst></a:bodyPr>"#
837        );
838        assert_eq!(
839            CT_TextBodyProperties::from_xml(&written).unwrap(),
840            properties
841        );
842    }
843
844    #[test]
845    fn malformed_body_properties_return_errors_without_panicking() {
846        let body_cases: &[&[u8]] = &[
847            br#"<q:notBodyPr/>"#,
848            br#"<q:bodyPr anchor="middle"/>"#,
849            br#"<q:bodyPr wrap="tight"/>"#,
850            br#"<q:bodyPr vert="sideways"/>"#,
851            br#"<q:bodyPr lIns="2147483648"/>"#,
852            br#"<q:bodyPr rIns="1.in"/>"#,
853            br#"<q:bodyPr spcFirstLastPara="yes"/>"#,
854            br#"<q:bodyPr><q:normAutofit fontScale="999"/></q:bodyPr>"#,
855            br#"<q:bodyPr><q:normAutofit fontScale="100001"/></q:bodyPr>"#,
856            br#"<q:bodyPr><q:normAutofit fontScale=".5%"/></q:bodyPr>"#,
857            br#"<q:bodyPr><q:normAutofit lnSpcReduction="13200001"/></q:bodyPr>"#,
858            br#"<q:bodyPr><q:noAutofit><x:child/></q:noAutofit></q:bodyPr>"#,
859            br#"<q:bodyPr><q:noAutofit/><q:spAutoFit/></q:bodyPr>"#,
860            br#"<q:bodyPr><q:sp3d/><q:flatTx/></q:bodyPr>"#,
861            br#"<q:bodyPr xmlns:a="urn:producer" numCol="2"/>"#,
862        ];
863        for xml in body_cases {
864            let result = panic::catch_unwind(|| CT_TextBodyProperties::from_xml(xml));
865            assert!(result.is_ok(), "body-property parser panicked");
866            assert!(result.unwrap().is_err(), "malformed body properties parsed");
867        }
868
869        let shell_cases: &[&[u8]] = &[
870            br#"<q:txBody><q:p/></q:txBody>"#,
871            br#"<q:txBody/>"#,
872            br#"<q:txBody><q:bodyPr/><q:bodyPr/><q:p/></q:txBody>"#,
873            br#"<q:txBody><q:bodyPr/><q:lstStyle/><q:lstStyle/><q:p/></q:txBody>"#,
874        ];
875        for xml in shell_cases {
876            let result = panic::catch_unwind(|| CT_TextBody::from_xml(xml));
877            assert!(result.is_ok(), "text-body parser panicked");
878            assert!(result.unwrap().is_err(), "malformed text body parsed");
879        }
880
881        let empty = CT_TextBody::from_xml(br#"<q:txBody><q:bodyPr/></q:txBody>"#).unwrap();
882        assert_eq!(empty.paragraph_count(), 0);
883        assert!(empty.to_xml().is_ok());
884
885        let mut properties = CT_TextBodyProperties {
886            left_inset: Some(Coordinate32Value::UniversalMeasure("NaNin".to_owned())),
887            ..CT_TextBodyProperties::default()
888        };
889        assert!(properties.to_xml().is_err());
890        properties.left_inset = Some(Coordinate32Value::Emu(i32::MAX));
891        assert!(properties.to_xml().is_ok());
892    }
893}