Skip to main content

oxml_drawing/text/
paragraph.rs

1use std::io::Write;
2
3use oxml_core::OxmlError;
4use oxml_core::raw_xml::{capture_element, capture_empty_element};
5use oxml_core::xml::{local_name, matches_local_name};
6use oxml_core::xml_text::{decode_plain, resolve_entity};
7use quick_xml::events::{BytesEnd, BytesStart, BytesText, Event};
8use quick_xml::{Reader, Writer, XmlVersion};
9
10use crate::fill::Fill;
11use crate::namespace::reject_conflicting_a_prefix;
12use crate::order::OrderedRawChildren;
13
14use super::body::{Result, TextError, missing_end};
15use super::bullet::TextBullet;
16
17const MAX_TEXT_MARGIN: i32 = 51_206_400;
18const MAX_TEXT_POINT: i32 = 400_000;
19const MAX_TEXT_SPACING_POINT: i32 = 158_400;
20const MAX_TRANSITIONAL_SPACING_PERCENT: i32 = 201_169;
21
22/// Whether source text explicitly requested XML whitespace preservation.
23#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
24pub enum TextSpace {
25    #[default]
26    Default,
27    Preserve,
28}
29
30/// One DrawingML `a:t` value and its source whitespace intent.
31#[derive(Clone, Debug, Default, Eq, PartialEq)]
32pub struct TextValue {
33    pub value: String,
34    pub space: TextSpace,
35    raw_attributes: Vec<(String, String)>,
36}
37
38impl TextValue {
39    fn from_start(start: &BytesStart<'_>) -> Result<Self> {
40        if !matches_local_name(start.name().as_ref(), b"t") {
41            return Err(unexpected(start));
42        }
43        let space_value = text_attr(start, b"xml:space")?;
44        let space = match space_value.as_deref() {
45            None | Some("default") => TextSpace::Default,
46            Some("preserve") => TextSpace::Preserve,
47            Some(value) => return Err(invalid_attribute("t", "xml:space", value)),
48        };
49        Ok(Self {
50            value: String::new(),
51            space,
52            raw_attributes: capture_raw_attributes(start, &[b"xml:space"])?,
53        })
54    }
55
56    fn from_element(reader: &mut Reader<&[u8]>, start: &BytesStart<'_>) -> Result<Self> {
57        let mut text = Self::from_start(start)?;
58        let mut buffer = Vec::new();
59        loop {
60            match reader
61                .read_event_into(&mut buffer)
62                .map_err(OxmlError::from)?
63            {
64                Event::Text(value) => text.value.push_str(&decode_plain(&value)),
65                Event::CData(value) => {
66                    let value = std::str::from_utf8(value.as_ref()).map_err(OxmlError::from)?;
67                    text.value.push_str(value);
68                }
69                Event::GeneralRef(value) => text.value.push_str(&resolve_entity(&value)),
70                Event::End(element) if matches_local_name(element.name().as_ref(), b"t") => {
71                    if text.space == TextSpace::Default
72                        && (text.value.chars().next().is_some_and(char::is_whitespace)
73                            || text
74                                .value
75                                .chars()
76                                .next_back()
77                                .is_some_and(char::is_whitespace))
78                    {
79                        text.space = TextSpace::Preserve;
80                    }
81                    return Ok(text);
82                }
83                Event::Start(element) | Event::Empty(element) => {
84                    return Err(unexpected(&element));
85                }
86                Event::Eof => return Err(missing_end("t")),
87                _ => {}
88            }
89            buffer.clear();
90        }
91    }
92
93    fn write_xml<W: Write>(&self, writer: &mut Writer<W>) -> Result<()> {
94        let preserve = self.space == TextSpace::Preserve
95            || self.value.chars().next().is_some_and(char::is_whitespace)
96            || self
97                .value
98                .chars()
99                .next_back()
100                .is_some_and(char::is_whitespace);
101        let mut start = BytesStart::new("a:t");
102        if preserve {
103            start.push_attribute(("xml:space", "preserve"));
104        }
105        push_raw_attributes(&mut start, &self.raw_attributes);
106        if self.value.is_empty() {
107            return write_empty(writer, start);
108        }
109        write_start(writer, start)?;
110        writer
111            .write_event(Event::Text(BytesText::new(&self.value)))
112            .map_err(OxmlError::from)?;
113        write_end(writer, "a:t")
114    }
115}
116
117/// The seven members of `ST_TextAlignType`.
118#[derive(Clone, Copy, Debug, Eq, PartialEq)]
119pub enum TextAlignment {
120    Left,
121    Center,
122    Right,
123    Justified,
124    JustifiedLow,
125    Distributed,
126    ThaiDistributed,
127}
128
129impl TextAlignment {
130    fn parse(value: &str) -> Option<Self> {
131        match value {
132            "l" => Some(Self::Left),
133            "ctr" => Some(Self::Center),
134            "r" => Some(Self::Right),
135            "just" => Some(Self::Justified),
136            "justLow" => Some(Self::JustifiedLow),
137            "dist" => Some(Self::Distributed),
138            "thaiDist" => Some(Self::ThaiDistributed),
139            _ => None,
140        }
141    }
142
143    const fn as_str(self) -> &'static str {
144        match self {
145            Self::Left => "l",
146            Self::Center => "ctr",
147            Self::Right => "r",
148            Self::Justified => "just",
149            Self::JustifiedLow => "justLow",
150            Self::Distributed => "dist",
151            Self::ThaiDistributed => "thaiDist",
152        }
153    }
154}
155
156/// DrawingML paragraph spacing in percentage text or centipoints.
157#[derive(Clone, Debug, Eq, PartialEq)]
158pub enum TextSpacing {
159    Percent(String),
160    Points(i32),
161}
162
163impl TextSpacing {
164    fn from_xml(xml: &[u8], wrapper: &[u8]) -> Result<Self> {
165        parse_complete(
166            xml,
167            wrapper,
168            |reader, start| Self::from_element(reader, start, wrapper),
169            |_| {
170                Err(TextError::UnexpectedElement(
171                    String::from_utf8_lossy(wrapper).into_owned(),
172                ))
173            },
174        )
175    }
176
177    fn from_element(
178        reader: &mut Reader<&[u8]>,
179        start: &BytesStart<'_>,
180        wrapper: &[u8],
181    ) -> Result<Self> {
182        reject_conflicting_a_prefix(start)?;
183        let mut spacing = None;
184        let mut buffer = Vec::new();
185        loop {
186            match reader
187                .read_event_into(&mut buffer)
188                .map_err(OxmlError::from)?
189            {
190                Event::Empty(element) if matches_local_name(element.name().as_ref(), b"spcPct") => {
191                    reject_conflicting_a_prefix(&element)?;
192                    if spacing.is_some() {
193                        return Err(duplicate("text spacing choice"));
194                    }
195                    let value = required_attr(&element, b"val")?;
196                    validate_spacing_percent(&value)?;
197                    spacing = Some(Self::Percent(value));
198                }
199                Event::Empty(element) if matches_local_name(element.name().as_ref(), b"spcPts") => {
200                    reject_conflicting_a_prefix(&element)?;
201                    if spacing.is_some() {
202                        return Err(duplicate("text spacing choice"));
203                    }
204                    let value = parse_i32_attr(&element, b"val", 0, MAX_TEXT_SPACING_POINT)?;
205                    spacing = Some(Self::Points(value));
206                }
207                Event::Start(element)
208                    if matches_local_name(element.name().as_ref(), b"spcPct")
209                        || matches_local_name(element.name().as_ref(), b"spcPts") =>
210                {
211                    reject_conflicting_a_prefix(&element)?;
212                    if spacing.is_some() {
213                        return Err(duplicate("text spacing choice"));
214                    }
215                    let name = local_name(element.name().as_ref()).to_vec();
216                    let value = if name == b"spcPct" {
217                        let value = required_attr(&element, b"val")?;
218                        validate_spacing_percent(&value)?;
219                        Self::Percent(value)
220                    } else {
221                        Self::Points(parse_i32_attr(&element, b"val", 0, MAX_TEXT_SPACING_POINT)?)
222                    };
223                    ensure_empty(reader, &name)?;
224                    spacing = Some(value);
225                }
226                Event::Start(element) | Event::Empty(element) => return Err(unexpected(&element)),
227                Event::End(element) if matches_local_name(element.name().as_ref(), wrapper) => {
228                    return spacing.ok_or_else(|| {
229                        TextError::UnexpectedElement(String::from_utf8_lossy(wrapper).into_owned())
230                    });
231                }
232                Event::Eof => return Err(missing_end(&String::from_utf8_lossy(wrapper))),
233                _ => {}
234            }
235            buffer.clear();
236        }
237    }
238
239    fn write_xml<W: Write>(&self, writer: &mut Writer<W>, wrapper: &str) -> Result<()> {
240        write_start(writer, BytesStart::new(wrapper))?;
241        match self {
242            Self::Percent(value) => {
243                validate_spacing_percent(value)?;
244                let mut start = BytesStart::new("a:spcPct");
245                start.push_attribute(("val", value.as_str()));
246                write_empty(writer, start)?;
247            }
248            Self::Points(value) if (0..=MAX_TEXT_SPACING_POINT).contains(value) => {
249                let value = value.to_string();
250                let mut start = BytesStart::new("a:spcPts");
251                start.push_attribute(("val", value.as_str()));
252                write_empty(writer, start)?;
253            }
254            Self::Points(value) => {
255                return Err(invalid_attribute("spcPts", "val", &value.to_string()));
256            }
257        }
258        write_end(writer, wrapper)
259    }
260}
261
262/// Character spacing in centipoints or a strict universal measure.
263#[derive(Clone, Debug, Eq, PartialEq)]
264pub enum TextPointValue {
265    Centipoints(i32),
266    UniversalMeasure(String),
267}
268
269impl TextPointValue {
270    fn parse(value: String) -> Result<Self> {
271        if let Ok(point) = value.parse::<i32>() {
272            if (-MAX_TEXT_POINT..=MAX_TEXT_POINT).contains(&point) {
273                return Ok(Self::Centipoints(point));
274            }
275            return Err(invalid_attribute("rPr", "spc", &value));
276        }
277        if is_universal_measure(&value) {
278            Ok(Self::UniversalMeasure(value))
279        } else {
280            Err(invalid_attribute("rPr", "spc", &value))
281        }
282    }
283
284    fn as_xml(&self) -> Result<String> {
285        match self {
286            Self::Centipoints(value) if (-MAX_TEXT_POINT..=MAX_TEXT_POINT).contains(value) => {
287                Ok(value.to_string())
288            }
289            Self::Centipoints(value) => Err(invalid_attribute("rPr", "spc", &value.to_string())),
290            Self::UniversalMeasure(value) if is_universal_measure(value) => Ok(value.clone()),
291            Self::UniversalMeasure(value) => Err(invalid_attribute("rPr", "spc", value)),
292        }
293    }
294}
295
296/// DrawingML underline styles.
297#[derive(Clone, Copy, Debug, Eq, PartialEq)]
298pub enum TextUnderline {
299    None,
300    Words,
301    Single,
302    Double,
303    Heavy,
304    Dotted,
305    DottedHeavy,
306    Dash,
307    DashHeavy,
308    DashLong,
309    DashLongHeavy,
310    DotDash,
311    DotDashHeavy,
312    DotDotDash,
313    DotDotDashHeavy,
314    Wavy,
315    WavyHeavy,
316    WavyDouble,
317}
318
319impl TextUnderline {
320    fn parse(value: &str) -> Option<Self> {
321        Some(match value {
322            "none" => Self::None,
323            "words" => Self::Words,
324            "sng" => Self::Single,
325            "dbl" => Self::Double,
326            "heavy" => Self::Heavy,
327            "dotted" => Self::Dotted,
328            "dottedHeavy" => Self::DottedHeavy,
329            "dash" => Self::Dash,
330            "dashHeavy" => Self::DashHeavy,
331            "dashLong" => Self::DashLong,
332            "dashLongHeavy" => Self::DashLongHeavy,
333            "dotDash" => Self::DotDash,
334            "dotDashHeavy" => Self::DotDashHeavy,
335            "dotDotDash" => Self::DotDotDash,
336            "dotDotDashHeavy" => Self::DotDotDashHeavy,
337            "wavy" => Self::Wavy,
338            "wavyHeavy" => Self::WavyHeavy,
339            "wavyDbl" => Self::WavyDouble,
340            _ => return None,
341        })
342    }
343
344    const fn as_str(self) -> &'static str {
345        match self {
346            Self::None => "none",
347            Self::Words => "words",
348            Self::Single => "sng",
349            Self::Double => "dbl",
350            Self::Heavy => "heavy",
351            Self::Dotted => "dotted",
352            Self::DottedHeavy => "dottedHeavy",
353            Self::Dash => "dash",
354            Self::DashHeavy => "dashHeavy",
355            Self::DashLong => "dashLong",
356            Self::DashLongHeavy => "dashLongHeavy",
357            Self::DotDash => "dotDash",
358            Self::DotDashHeavy => "dotDashHeavy",
359            Self::DotDotDash => "dotDotDash",
360            Self::DotDotDashHeavy => "dotDotDashHeavy",
361            Self::Wavy => "wavy",
362            Self::WavyHeavy => "wavyHeavy",
363            Self::WavyDouble => "wavyDbl",
364        }
365    }
366}
367
368/// DrawingML strike styles.
369#[derive(Clone, Copy, Debug, Eq, PartialEq)]
370pub enum TextStrike {
371    None,
372    Single,
373    Double,
374}
375
376impl TextStrike {
377    fn parse(value: &str) -> Option<Self> {
378        match value {
379            "noStrike" => Some(Self::None),
380            "sngStrike" => Some(Self::Single),
381            "dblStrike" => Some(Self::Double),
382            _ => None,
383        }
384    }
385
386    const fn as_str(self) -> &'static str {
387        match self {
388            Self::None => "noStrike",
389            Self::Single => "sngStrike",
390            Self::Double => "dblStrike",
391        }
392    }
393}
394
395/// A DrawingML text typeface reference.
396#[derive(Clone, Debug, Eq, PartialEq)]
397pub struct TextFont {
398    pub typeface: String,
399    raw_attributes: Vec<(String, String)>,
400}
401
402impl TextFont {
403    pub fn new(typeface: impl Into<String>) -> Result<Self> {
404        let typeface = typeface.into();
405        if typeface.is_empty() {
406            return Err(invalid_attribute("font", "typeface", ""));
407        }
408        Ok(Self {
409            typeface,
410            raw_attributes: Vec::new(),
411        })
412    }
413
414    pub(crate) fn from_xml(xml: &[u8], expected: &[u8]) -> Result<Self> {
415        parse_complete(
416            xml,
417            expected,
418            |reader, start| {
419                let font = Self::from_start(start)?;
420                ensure_empty(reader, expected)?;
421                Ok(font)
422            },
423            Self::from_start,
424        )
425    }
426
427    fn from_start(start: &BytesStart<'_>) -> Result<Self> {
428        reject_conflicting_a_prefix(start)?;
429        Ok(Self {
430            typeface: required_attr(start, b"typeface")?,
431            raw_attributes: capture_raw_attributes(start, &[b"typeface"])?,
432        })
433    }
434
435    pub(crate) fn write_xml<W: Write>(&self, writer: &mut Writer<W>, tag: &str) -> Result<()> {
436        if self.typeface.is_empty() {
437            return Err(invalid_attribute(tag, "typeface", ""));
438        }
439        let mut start = BytesStart::new(tag);
440        start.push_attribute(("typeface", self.typeface.as_str()));
441        push_raw_attributes(&mut start, &self.raw_attributes);
442        write_empty(writer, start)
443    }
444}
445
446/// Hyperlink attributes retained on text character properties.
447#[derive(Clone, Debug, Default, Eq, PartialEq)]
448pub struct TextHyperlink {
449    pub relationship_id: Option<String>,
450    pub invalid_url: Option<String>,
451    pub action: Option<String>,
452    pub target_frame: Option<String>,
453    pub tooltip: Option<String>,
454    pub history: Option<bool>,
455    pub highlight_click: Option<bool>,
456    pub end_sound: Option<bool>,
457    raw_attributes: Vec<(String, String)>,
458    raw_children: OrderedRawChildren,
459}
460
461impl TextHyperlink {
462    fn from_xml(xml: &[u8], expected: &[u8]) -> Result<Self> {
463        parse_complete(
464            xml,
465            expected,
466            |reader, start| Self::from_element(reader, start, expected),
467            Self::from_start,
468        )
469    }
470
471    fn from_start(start: &BytesStart<'_>) -> Result<Self> {
472        reject_conflicting_a_prefix(start)?;
473        let relationship_id = text_attr(start, b"r:id")?;
474        Ok(Self {
475            relationship_id,
476            invalid_url: text_attr(start, b"invalidUrl")?,
477            action: text_attr(start, b"action")?,
478            target_frame: text_attr(start, b"tgtFrame")?,
479            tooltip: text_attr(start, b"tooltip")?,
480            history: parse_optional_bool(start, b"history")?,
481            highlight_click: parse_optional_bool(start, b"highlightClick")?,
482            end_sound: parse_optional_bool(start, b"endSnd")?,
483            raw_attributes: capture_raw_attributes(
484                start,
485                &[
486                    b"r:id",
487                    b"invalidUrl",
488                    b"action",
489                    b"tgtFrame",
490                    b"tooltip",
491                    b"history",
492                    b"highlightClick",
493                    b"endSnd",
494                ],
495            )?,
496            raw_children: OrderedRawChildren::default(),
497        })
498    }
499
500    fn from_element(
501        reader: &mut Reader<&[u8]>,
502        start: &BytesStart<'_>,
503        expected: &[u8],
504    ) -> Result<Self> {
505        let mut hyperlink = Self::from_start(start)?;
506        let mut boundary = 0;
507        let mut seen = [false; 2];
508        let mut buffer = Vec::new();
509        loop {
510            match reader
511                .read_event_into(&mut buffer)
512                .map_err(OxmlError::from)?
513            {
514                Event::Start(element) => {
515                    let name = local_name(element.name().as_ref()).to_vec();
516                    let raw = capture_element(reader, &element)?;
517                    capture_hyperlink_raw(&mut hyperlink, &name, raw, &mut boundary, &mut seen)?;
518                }
519                Event::Empty(element) => {
520                    let name = local_name(element.name().as_ref()).to_vec();
521                    let raw = capture_empty_element(&element)?;
522                    capture_hyperlink_raw(&mut hyperlink, &name, raw, &mut boundary, &mut seen)?;
523                }
524                Event::End(element) if matches_local_name(element.name().as_ref(), expected) => {
525                    return Ok(hyperlink);
526                }
527                Event::Eof => return Err(missing_end(&String::from_utf8_lossy(expected))),
528                _ => {}
529            }
530            buffer.clear();
531        }
532    }
533
534    fn write_xml<W: Write>(&self, writer: &mut Writer<W>, tag: &str) -> Result<()> {
535        let mut start = BytesStart::new(tag);
536        push_optional_attr(&mut start, "r:id", self.relationship_id.as_deref());
537        push_optional_attr(&mut start, "invalidUrl", self.invalid_url.as_deref());
538        push_optional_attr(&mut start, "action", self.action.as_deref());
539        push_optional_attr(&mut start, "tgtFrame", self.target_frame.as_deref());
540        push_optional_attr(&mut start, "tooltip", self.tooltip.as_deref());
541        push_optional_bool(&mut start, "history", self.history);
542        push_optional_bool(&mut start, "highlightClick", self.highlight_click);
543        push_optional_bool(&mut start, "endSnd", self.end_sound);
544        push_raw_attributes(&mut start, &self.raw_attributes);
545        if self.raw_children.is_empty() {
546            return write_empty(writer, start);
547        }
548        write_start(writer, start)?;
549        for boundary in 0..=2 {
550            emit_raw(writer, self.raw_children.at(boundary))?;
551        }
552        write_end(writer, tag)
553    }
554}
555
556fn capture_hyperlink_raw(
557    hyperlink: &mut TextHyperlink,
558    name: &[u8],
559    raw: Vec<u8>,
560    boundary: &mut usize,
561    seen: &mut [bool; 2],
562) -> Result<()> {
563    let slot = match name {
564        b"snd" => Some(0),
565        b"extLst" => Some(1),
566        _ => None,
567    };
568    if let Some(slot) = slot {
569        if seen[slot] {
570            return Err(duplicate(&String::from_utf8_lossy(name)));
571        }
572        seen[slot] = true;
573        hyperlink.raw_children.push((*boundary).max(slot), raw);
574        *boundary = (*boundary).max(slot + 1);
575    } else {
576        hyperlink.raw_children.push(*boundary, raw);
577    }
578    Ok(())
579}
580
581/// DrawingML character properties shared by runs, breaks, fields, and defaults.
582#[allow(non_camel_case_types)]
583#[derive(Clone, Debug, Default, Eq, PartialEq)]
584pub struct CT_TextCharacterProperties {
585    pub font_size: Option<i32>,
586    pub bold: Option<bool>,
587    pub italic: Option<bool>,
588    pub all_caps: Option<bool>,
589    pub underline: Option<TextUnderline>,
590    pub strike: Option<TextStrike>,
591    pub spacing: Option<TextPointValue>,
592    pub baseline: Option<String>,
593    pub fill: Option<Fill>,
594    pub latin: Option<TextFont>,
595    pub east_asian: Option<TextFont>,
596    pub complex_script: Option<TextFont>,
597    pub symbol: Option<TextFont>,
598    pub hyperlink_click: Option<TextHyperlink>,
599    pub hyperlink_mouse_over: Option<TextHyperlink>,
600    raw_attributes: Vec<(String, String)>,
601    raw_children: OrderedRawChildren,
602}
603
604impl CT_TextCharacterProperties {
605    /// Parses one complete character-property element with any prefix.
606    pub fn from_xml(xml: &[u8]) -> Result<Self> {
607        let expected = root_local_name(xml)?;
608        if !is_character_property_tag(&expected) {
609            return Err(TextError::UnexpectedElement(
610                String::from_utf8_lossy(&expected).into_owned(),
611            ));
612        }
613        parse_complete(xml, &expected, Self::from_element, Self::from_start)
614    }
615
616    fn from_start(start: &BytesStart<'_>) -> Result<Self> {
617        reject_conflicting_a_prefix(start)?;
618        let element = String::from_utf8_lossy(local_name(start.name().as_ref())).into_owned();
619        let font_size = text_attr(start, b"sz")?
620            .map(|value| parse_i32_value(&element, "sz", &value, 100, MAX_TEXT_POINT))
621            .transpose()?;
622        let spacing = text_attr(start, b"spc")?
623            .map(TextPointValue::parse)
624            .transpose()?;
625        let baseline = text_attr(start, b"baseline")?;
626        if let Some(value) = baseline.as_deref() {
627            validate_baseline(value)?;
628        }
629        let all_caps = match text_attr(start, b"cap")?.as_deref() {
630            Some("all") => Some(true),
631            Some("none") => Some(false),
632            _ => None,
633        };
634        let mut raw_attributes = capture_raw_attributes(
635            start,
636            &[b"sz", b"b", b"i", b"u", b"strike", b"spc", b"baseline"],
637        )?;
638        if all_caps.is_some() {
639            raw_attributes.retain(|(name, _)| name != "cap");
640        }
641        Ok(Self {
642            font_size,
643            bold: parse_optional_bool(start, b"b")?,
644            italic: parse_optional_bool(start, b"i")?,
645            all_caps,
646            underline: parse_optional_enum(start, b"u", TextUnderline::parse)?,
647            strike: parse_optional_enum(start, b"strike", TextStrike::parse)?,
648            spacing,
649            baseline,
650            raw_attributes,
651            ..Self::default()
652        })
653    }
654
655    fn from_element(reader: &mut Reader<&[u8]>, start: &BytesStart<'_>) -> Result<Self> {
656        let expected = local_name(start.name().as_ref()).to_vec();
657        let mut properties = Self::from_start(start)?;
658        let mut boundary = 0;
659        let mut seen = [false; 14];
660        let mut buffer = Vec::new();
661        loop {
662            match reader
663                .read_event_into(&mut buffer)
664                .map_err(OxmlError::from)?
665            {
666                Event::Start(element) => {
667                    let name = local_name(element.name().as_ref()).to_vec();
668                    let raw = capture_element(reader, &element)?;
669                    properties.capture_child(&name, raw, &mut boundary, &mut seen)?;
670                }
671                Event::Empty(element) => {
672                    let name = local_name(element.name().as_ref()).to_vec();
673                    let raw = capture_empty_element(&element)?;
674                    properties.capture_child(&name, raw, &mut boundary, &mut seen)?;
675                }
676                Event::End(element) if matches_local_name(element.name().as_ref(), &expected) => {
677                    return Ok(properties);
678                }
679                Event::Eof => return Err(missing_end(&String::from_utf8_lossy(&expected))),
680                _ => {}
681            }
682            buffer.clear();
683        }
684    }
685
686    fn capture_child(
687        &mut self,
688        name: &[u8],
689        raw: Vec<u8>,
690        boundary: &mut usize,
691        seen: &mut [bool; 14],
692    ) -> Result<()> {
693        let slot = character_property_slot(name);
694        if let Some(slot) = slot {
695            if seen[slot - 1] {
696                return Err(duplicate(&String::from_utf8_lossy(name)));
697            }
698            seen[slot - 1] = true;
699        }
700        match name {
701            name if is_fill(name) => self.fill = Some(Fill::from_xml(&raw)?),
702            b"latin" => self.latin = Some(TextFont::from_xml(&raw, b"latin")?),
703            b"ea" => self.east_asian = Some(TextFont::from_xml(&raw, b"ea")?),
704            b"cs" => self.complex_script = Some(TextFont::from_xml(&raw, b"cs")?),
705            b"sym" => self.symbol = Some(TextFont::from_xml(&raw, b"sym")?),
706            b"hlinkClick" => {
707                self.hyperlink_click = Some(TextHyperlink::from_xml(&raw, b"hlinkClick")?)
708            }
709            b"hlinkMouseOver" => {
710                self.hyperlink_mouse_over = Some(TextHyperlink::from_xml(&raw, b"hlinkMouseOver")?)
711            }
712            _ => self.raw_children.push(
713                slot.map_or(*boundary, |slot| (*boundary).max(slot - 1)),
714                raw,
715            ),
716        }
717        if let Some(slot) = slot {
718            *boundary = (*boundary).max(slot);
719        }
720        Ok(())
721    }
722
723    /// Writes character properties with a caller-selected fixed `a:` tag.
724    pub fn write_xml<W: Write>(&self, writer: &mut Writer<W>, tag: &str) -> Result<()> {
725        let mut start = BytesStart::new(tag);
726        if let Some(value) = self.font_size {
727            parse_i32_value(tag, "sz", &value.to_string(), 100, MAX_TEXT_POINT)?;
728            push_owned_attr(&mut start, "sz", value.to_string());
729        }
730        push_optional_bool(&mut start, "b", self.bold);
731        push_optional_bool(&mut start, "i", self.italic);
732        if let Some(value) = self.all_caps {
733            start.push_attribute(("cap", if value { "all" } else { "none" }));
734        }
735        if let Some(value) = self.underline {
736            start.push_attribute(("u", value.as_str()));
737        }
738        if let Some(value) = self.strike {
739            start.push_attribute(("strike", value.as_str()));
740        }
741        if let Some(value) = &self.spacing {
742            push_owned_attr(&mut start, "spc", value.as_xml()?);
743        }
744        if let Some(value) = self.baseline.as_deref() {
745            validate_baseline(value)?;
746            start.push_attribute(("baseline", value));
747        }
748        push_raw_attributes(&mut start, &self.raw_attributes);
749
750        let has_modelled_children = self.fill.is_some()
751            || self.latin.is_some()
752            || self.east_asian.is_some()
753            || self.complex_script.is_some()
754            || self.symbol.is_some()
755            || self.hyperlink_click.is_some()
756            || self.hyperlink_mouse_over.is_some();
757        if !has_modelled_children && self.raw_children.is_empty() {
758            return write_empty(writer, start);
759        }
760        write_start(writer, start)?;
761        emit_raw(writer, self.raw_children.at(0))?;
762        emit_raw(writer, self.raw_children.at(1))?;
763        if let Some(fill) = &self.fill {
764            fill.write_xml(writer)?;
765        }
766        for boundary in 2..=6 {
767            emit_raw(writer, self.raw_children.at(boundary))?;
768        }
769        if let Some(font) = &self.latin {
770            font.write_xml(writer, "a:latin")?;
771        }
772        emit_raw(writer, self.raw_children.at(7))?;
773        if let Some(font) = &self.east_asian {
774            font.write_xml(writer, "a:ea")?;
775        }
776        emit_raw(writer, self.raw_children.at(8))?;
777        if let Some(font) = &self.complex_script {
778            font.write_xml(writer, "a:cs")?;
779        }
780        emit_raw(writer, self.raw_children.at(9))?;
781        if let Some(font) = &self.symbol {
782            font.write_xml(writer, "a:sym")?;
783        }
784        emit_raw(writer, self.raw_children.at(10))?;
785        if let Some(hyperlink) = &self.hyperlink_click {
786            hyperlink.write_xml(writer, "a:hlinkClick")?;
787        }
788        emit_raw(writer, self.raw_children.at(11))?;
789        if let Some(hyperlink) = &self.hyperlink_mouse_over {
790            hyperlink.write_xml(writer, "a:hlinkMouseOver")?;
791        }
792        for boundary in 12..=14 {
793            emit_raw(writer, self.raw_children.at(boundary))?;
794        }
795        write_end(writer, tag)
796    }
797
798    pub fn raw_children(&self) -> &OrderedRawChildren {
799        &self.raw_children
800    }
801}
802
803fn character_property_slot(name: &[u8]) -> Option<usize> {
804    match name {
805        b"ln" => Some(1),
806        name if is_fill(name) => Some(2),
807        b"effectLst" | b"effectDag" => Some(3),
808        b"highlight" => Some(4),
809        b"uLnTx" | b"uLn" => Some(5),
810        b"uFillTx" | b"uFill" => Some(6),
811        b"latin" => Some(7),
812        b"ea" => Some(8),
813        b"cs" => Some(9),
814        b"sym" => Some(10),
815        b"hlinkClick" => Some(11),
816        b"hlinkMouseOver" => Some(12),
817        b"rtl" => Some(13),
818        b"extLst" => Some(14),
819        _ => None,
820    }
821}
822
823fn is_character_property_tag(name: &[u8]) -> bool {
824    matches!(name, b"rPr" | b"defRPr" | b"endParaRPr")
825}
826
827fn is_fill(name: &[u8]) -> bool {
828    matches!(
829        name,
830        b"noFill" | b"solidFill" | b"gradFill" | b"pattFill" | b"blipFill"
831    )
832}
833
834/// Paragraph attributes and the story-owned spacing and default-run children.
835#[allow(non_camel_case_types)]
836#[derive(Clone, Debug, Default, Eq, PartialEq)]
837pub struct CT_TextParagraphProperties {
838    pub left_margin: Option<i32>,
839    pub right_margin: Option<i32>,
840    pub level: Option<u8>,
841    pub indent: Option<i32>,
842    pub alignment: Option<TextAlignment>,
843    pub right_to_left: Option<bool>,
844    pub line_spacing: Option<TextSpacing>,
845    pub space_before: Option<TextSpacing>,
846    pub space_after: Option<TextSpacing>,
847    pub bullet: Option<TextBullet>,
848    pub default_run_properties: Option<CT_TextCharacterProperties>,
849    raw_attributes: Vec<(String, String)>,
850    raw_children: OrderedRawChildren,
851}
852
853impl CT_TextParagraphProperties {
854    /// Parses one complete paragraph-property element with any prefix.
855    pub fn from_xml(xml: &[u8]) -> Result<Self> {
856        let expected = root_local_name(xml)?;
857        if !is_paragraph_property_tag(&expected) {
858            return Err(TextError::UnexpectedElement(
859                String::from_utf8_lossy(&expected).into_owned(),
860            ));
861        }
862        parse_complete(xml, &expected, Self::from_element, Self::from_start)
863    }
864
865    fn from_start(start: &BytesStart<'_>) -> Result<Self> {
866        reject_conflicting_a_prefix(start)?;
867        let element = String::from_utf8_lossy(local_name(start.name().as_ref())).into_owned();
868        let level = text_attr(start, b"lvl")?
869            .map(|value| parse_i32_value(&element, "lvl", &value, 0, 8).map(|v| v as u8))
870            .transpose()?;
871        Ok(Self {
872            left_margin: parse_optional_i32(start, b"marL", 0, MAX_TEXT_MARGIN)?,
873            right_margin: parse_optional_i32(start, b"marR", 0, MAX_TEXT_MARGIN)?,
874            level,
875            indent: parse_optional_i32(start, b"indent", -MAX_TEXT_MARGIN, MAX_TEXT_MARGIN)?,
876            alignment: parse_optional_enum(start, b"algn", TextAlignment::parse)?,
877            right_to_left: parse_optional_bool(start, b"rtl")?,
878            raw_attributes: capture_raw_attributes(
879                start,
880                &[b"marL", b"marR", b"lvl", b"indent", b"algn"],
881            )?,
882            ..Self::default()
883        })
884    }
885
886    fn from_element(reader: &mut Reader<&[u8]>, start: &BytesStart<'_>) -> Result<Self> {
887        let expected = local_name(start.name().as_ref()).to_vec();
888        let mut properties = Self::from_start(start)?;
889        let mut boundary = 0;
890        let mut seen = [false; 10];
891        let mut buffer = Vec::new();
892        loop {
893            match reader
894                .read_event_into(&mut buffer)
895                .map_err(OxmlError::from)?
896            {
897                Event::Start(element) => {
898                    let name = local_name(element.name().as_ref()).to_vec();
899                    let raw = capture_element(reader, &element)?;
900                    properties.capture_child(&name, raw, &mut boundary, &mut seen)?;
901                }
902                Event::Empty(element) => {
903                    let name = local_name(element.name().as_ref()).to_vec();
904                    let raw = capture_empty_element(&element)?;
905                    properties.capture_child(&name, raw, &mut boundary, &mut seen)?;
906                }
907                Event::End(element) if matches_local_name(element.name().as_ref(), &expected) => {
908                    return Ok(properties);
909                }
910                Event::Eof => return Err(missing_end(&String::from_utf8_lossy(&expected))),
911                _ => {}
912            }
913            buffer.clear();
914        }
915    }
916
917    fn capture_child(
918        &mut self,
919        name: &[u8],
920        raw: Vec<u8>,
921        boundary: &mut usize,
922        seen: &mut [bool; 10],
923    ) -> Result<()> {
924        let slot = paragraph_property_slot(name);
925        if let Some(slot) = slot {
926            if seen[slot - 1] {
927                return Err(duplicate(&String::from_utf8_lossy(name)));
928            }
929            seen[slot - 1] = true;
930        }
931        match name {
932            b"lnSpc" => self.line_spacing = Some(TextSpacing::from_xml(&raw, b"lnSpc")?),
933            b"spcBef" => self.space_before = Some(TextSpacing::from_xml(&raw, b"spcBef")?),
934            b"spcAft" => self.space_after = Some(TextSpacing::from_xml(&raw, b"spcAft")?),
935            b"defRPr" => {
936                self.default_run_properties = Some(CT_TextCharacterProperties::from_xml(&raw)?)
937            }
938            _ => {
939                let mut bullet = self.bullet.take().unwrap_or_default();
940                if bullet.capture_component(name, &raw)? {
941                    self.bullet = Some(bullet);
942                } else {
943                    if !bullet.is_empty() {
944                        self.bullet = Some(bullet);
945                    }
946                    self.raw_children.push(
947                        slot.map_or(*boundary, |slot| (*boundary).max(slot - 1)),
948                        raw,
949                    );
950                }
951            }
952        }
953        if let Some(slot) = slot {
954            *boundary = (*boundary).max(slot);
955        }
956        Ok(())
957    }
958
959    /// Writes paragraph properties with a caller-selected fixed `a:` tag.
960    pub fn write_xml<W: Write>(&self, writer: &mut Writer<W>, tag: &str) -> Result<()> {
961        let mut start = BytesStart::new(tag);
962        if let Some(value) = self.left_margin {
963            parse_i32_value(tag, "marL", &value.to_string(), 0, MAX_TEXT_MARGIN)?;
964            push_owned_attr(&mut start, "marL", value.to_string());
965        }
966        if let Some(value) = self.right_margin {
967            parse_i32_value(tag, "marR", &value.to_string(), 0, MAX_TEXT_MARGIN)?;
968            push_owned_attr(&mut start, "marR", value.to_string());
969        }
970        if let Some(value) = self.level {
971            if value > 8 {
972                return Err(invalid_attribute(tag, "lvl", &value.to_string()));
973            }
974            push_owned_attr(&mut start, "lvl", value.to_string());
975        }
976        if let Some(value) = self.indent {
977            parse_i32_value(
978                tag,
979                "indent",
980                &value.to_string(),
981                -MAX_TEXT_MARGIN,
982                MAX_TEXT_MARGIN,
983            )?;
984            push_owned_attr(&mut start, "indent", value.to_string());
985        }
986        if let Some(value) = self.alignment {
987            start.push_attribute(("algn", value.as_str()));
988        }
989        let mut wrote_right_to_left = false;
990        for (name, value) in &self.raw_attributes {
991            if name == "rtl" {
992                if let Some(value) = self.right_to_left {
993                    start.push_attribute(("rtl", if value { "1" } else { "0" }));
994                    wrote_right_to_left = true;
995                }
996            } else {
997                start.push_attribute((name.as_str(), value.as_str()));
998            }
999        }
1000        if !wrote_right_to_left {
1001            push_optional_bool(&mut start, "rtl", self.right_to_left);
1002        }
1003
1004        let modelled = self.line_spacing.is_some()
1005            || self.space_before.is_some()
1006            || self.space_after.is_some()
1007            || self.bullet.is_some()
1008            || self.default_run_properties.is_some();
1009        if !modelled && self.raw_children.is_empty() {
1010            return write_empty(writer, start);
1011        }
1012        write_start(writer, start)?;
1013        emit_raw(writer, self.raw_children.at(0))?;
1014        if let Some(spacing) = &self.line_spacing {
1015            spacing.write_xml(writer, "a:lnSpc")?;
1016        }
1017        emit_raw(writer, self.raw_children.at(1))?;
1018        if let Some(spacing) = &self.space_before {
1019            spacing.write_xml(writer, "a:spcBef")?;
1020        }
1021        emit_raw(writer, self.raw_children.at(2))?;
1022        if let Some(spacing) = &self.space_after {
1023            spacing.write_xml(writer, "a:spcAft")?;
1024        }
1025        emit_raw(writer, self.raw_children.at(3))?;
1026        if let Some(bullet) = &self.bullet {
1027            bullet.write_color(writer)?;
1028        }
1029        emit_raw(writer, self.raw_children.at(4))?;
1030        if let Some(bullet) = &self.bullet {
1031            bullet.write_size(writer)?;
1032        }
1033        emit_raw(writer, self.raw_children.at(5))?;
1034        if let Some(bullet) = &self.bullet {
1035            bullet.write_font(writer)?;
1036        }
1037        emit_raw(writer, self.raw_children.at(6))?;
1038        if let Some(bullet) = &self.bullet {
1039            bullet.write_choice(writer)?;
1040        }
1041        emit_raw(writer, self.raw_children.at(7))?;
1042        emit_raw(writer, self.raw_children.at(8))?;
1043        if let Some(properties) = &self.default_run_properties {
1044            properties.write_xml(writer, "a:defRPr")?;
1045        }
1046        emit_raw(writer, self.raw_children.at(9))?;
1047        emit_raw(writer, self.raw_children.at(10))?;
1048        write_end(writer, tag)
1049    }
1050
1051    pub fn raw_children(&self) -> &OrderedRawChildren {
1052        &self.raw_children
1053    }
1054
1055    /// Serialises a complete paragraph-property fragment.
1056    pub fn to_xml(&self) -> Result<Vec<u8>> {
1057        let mut writer = Writer::new(Vec::new());
1058        self.write_xml(&mut writer, "a:pPr")?;
1059        Ok(writer.into_inner())
1060    }
1061}
1062
1063fn paragraph_property_slot(name: &[u8]) -> Option<usize> {
1064    match name {
1065        b"lnSpc" => Some(1),
1066        b"spcBef" => Some(2),
1067        b"spcAft" => Some(3),
1068        b"buClrTx" | b"buClr" => Some(4),
1069        b"buSzTx" | b"buSzPct" | b"buSzPts" => Some(5),
1070        b"buFontTx" | b"buFont" => Some(6),
1071        b"buNone" | b"buAutoNum" | b"buChar" | b"buBlip" => Some(7),
1072        b"tabLst" => Some(8),
1073        b"defRPr" => Some(9),
1074        b"extLst" => Some(10),
1075        _ => None,
1076    }
1077}
1078
1079fn is_paragraph_property_tag(name: &[u8]) -> bool {
1080    matches!(
1081        name,
1082        b"pPr"
1083            | b"defPPr"
1084            | b"lvl1pPr"
1085            | b"lvl2pPr"
1086            | b"lvl3pPr"
1087            | b"lvl4pPr"
1088            | b"lvl5pPr"
1089            | b"lvl6pPr"
1090            | b"lvl7pPr"
1091            | b"lvl8pPr"
1092            | b"lvl9pPr"
1093    )
1094}
1095
1096/// A regular DrawingML run containing optional properties and required text.
1097#[allow(non_camel_case_types)]
1098#[derive(Clone, Debug, Eq, PartialEq)]
1099pub struct CT_RegularTextRun {
1100    pub properties: Option<CT_TextCharacterProperties>,
1101    pub text: TextValue,
1102    raw_children: OrderedRawChildren,
1103}
1104
1105impl CT_RegularTextRun {
1106    /// Creates a regular text run without direct character formatting.
1107    pub fn new(text: impl Into<String>) -> Self {
1108        Self {
1109            properties: None,
1110            text: TextValue {
1111                value: text.into(),
1112                ..TextValue::default()
1113            },
1114            raw_children: OrderedRawChildren::default(),
1115        }
1116    }
1117
1118    /// Replaces the run text while retaining properties and unmodelled XML.
1119    pub fn set_text(&mut self, text: &str) {
1120        self.text.value = text.to_owned();
1121    }
1122
1123    /// Parses one complete `a:r` element with any prefix.
1124    pub fn from_xml(xml: &[u8]) -> Result<Self> {
1125        parse_complete(xml, b"r", Self::from_element, |_| {
1126            Err(TextError::UnexpectedElement("r".to_owned()))
1127        })
1128    }
1129
1130    fn from_element(reader: &mut Reader<&[u8]>, _start: &BytesStart<'_>) -> Result<Self> {
1131        let mut properties = None;
1132        let mut text = None;
1133        let mut raw_children = OrderedRawChildren::default();
1134        let mut boundary = 0;
1135        let mut buffer = Vec::new();
1136        loop {
1137            match reader
1138                .read_event_into(&mut buffer)
1139                .map_err(OxmlError::from)?
1140            {
1141                Event::Start(element) if matches_local_name(element.name().as_ref(), b"rPr") => {
1142                    if properties.is_some() {
1143                        return Err(duplicate("rPr"));
1144                    }
1145                    properties = Some(CT_TextCharacterProperties::from_element(reader, &element)?);
1146                    boundary = boundary.max(1);
1147                }
1148                Event::Empty(element) if matches_local_name(element.name().as_ref(), b"rPr") => {
1149                    if properties.is_some() {
1150                        return Err(duplicate("rPr"));
1151                    }
1152                    properties = Some(CT_TextCharacterProperties::from_start(&element)?);
1153                    boundary = boundary.max(1);
1154                }
1155                Event::Start(element) if matches_local_name(element.name().as_ref(), b"t") => {
1156                    if text.is_some() {
1157                        return Err(duplicate("t"));
1158                    }
1159                    text = Some(TextValue::from_element(reader, &element)?);
1160                    boundary = boundary.max(2);
1161                }
1162                Event::Empty(element) if matches_local_name(element.name().as_ref(), b"t") => {
1163                    if text.is_some() {
1164                        return Err(duplicate("t"));
1165                    }
1166                    text = Some(TextValue::from_start(&element)?);
1167                    boundary = boundary.max(2);
1168                }
1169                Event::Start(element) => {
1170                    raw_children.push(boundary, capture_element(reader, &element)?)
1171                }
1172                Event::Empty(element) => {
1173                    raw_children.push(boundary, capture_empty_element(&element)?)
1174                }
1175                Event::End(element) if matches_local_name(element.name().as_ref(), b"r") => break,
1176                Event::Eof => return Err(missing_end("r")),
1177                _ => {}
1178            }
1179            buffer.clear();
1180        }
1181        Ok(Self {
1182            properties,
1183            text: text.ok_or_else(|| {
1184                TextError::Xml(OxmlError::MissingElement("DrawingML run text".to_owned()))
1185            })?,
1186            raw_children,
1187        })
1188    }
1189
1190    fn write_xml<W: Write>(&self, writer: &mut Writer<W>) -> Result<()> {
1191        write_start(writer, BytesStart::new("a:r"))?;
1192        emit_raw(writer, self.raw_children.at(0))?;
1193        if let Some(properties) = &self.properties {
1194            properties.write_xml(writer, "a:rPr")?;
1195        }
1196        emit_raw(writer, self.raw_children.at(1))?;
1197        self.text.write_xml(writer)?;
1198        emit_raw(writer, self.raw_children.at(2))?;
1199        write_end(writer, "a:r")
1200    }
1201}
1202
1203/// A DrawingML explicit line break and its optional run properties.
1204#[allow(non_camel_case_types)]
1205#[derive(Clone, Debug, Default, Eq, PartialEq)]
1206pub struct CT_TextLineBreak {
1207    pub properties: Option<CT_TextCharacterProperties>,
1208    raw_children: OrderedRawChildren,
1209}
1210
1211impl CT_TextLineBreak {
1212    /// Parses one complete `a:br` element with any prefix.
1213    pub fn from_xml(xml: &[u8]) -> Result<Self> {
1214        parse_complete(xml, b"br", Self::from_element, |_| Ok(Self::default()))
1215    }
1216
1217    fn from_element(reader: &mut Reader<&[u8]>, _start: &BytesStart<'_>) -> Result<Self> {
1218        let mut line_break = Self::default();
1219        let mut boundary = 0;
1220        let mut buffer = Vec::new();
1221        loop {
1222            match reader
1223                .read_event_into(&mut buffer)
1224                .map_err(OxmlError::from)?
1225            {
1226                Event::Start(element) if matches_local_name(element.name().as_ref(), b"rPr") => {
1227                    if line_break.properties.is_some() {
1228                        return Err(duplicate("rPr"));
1229                    }
1230                    line_break.properties =
1231                        Some(CT_TextCharacterProperties::from_element(reader, &element)?);
1232                    boundary = 1;
1233                }
1234                Event::Empty(element) if matches_local_name(element.name().as_ref(), b"rPr") => {
1235                    if line_break.properties.is_some() {
1236                        return Err(duplicate("rPr"));
1237                    }
1238                    line_break.properties = Some(CT_TextCharacterProperties::from_start(&element)?);
1239                    boundary = 1;
1240                }
1241                Event::Start(element) => line_break
1242                    .raw_children
1243                    .push(boundary, capture_element(reader, &element)?),
1244                Event::Empty(element) => line_break
1245                    .raw_children
1246                    .push(boundary, capture_empty_element(&element)?),
1247                Event::End(element) if matches_local_name(element.name().as_ref(), b"br") => {
1248                    return Ok(line_break);
1249                }
1250                Event::Eof => return Err(missing_end("br")),
1251                _ => {}
1252            }
1253            buffer.clear();
1254        }
1255    }
1256
1257    fn write_xml<W: Write>(&self, writer: &mut Writer<W>) -> Result<()> {
1258        if self.properties.is_none() && self.raw_children.is_empty() {
1259            return write_empty(writer, BytesStart::new("a:br"));
1260        }
1261        write_start(writer, BytesStart::new("a:br"))?;
1262        emit_raw(writer, self.raw_children.at(0))?;
1263        if let Some(properties) = &self.properties {
1264            properties.write_xml(writer, "a:rPr")?;
1265        }
1266        emit_raw(writer, self.raw_children.at(1))?;
1267        write_end(writer, "a:br")
1268    }
1269}
1270
1271/// A DrawingML field with required GUID, optional type, properties, and text.
1272#[allow(non_camel_case_types)]
1273#[derive(Clone, Debug, Eq, PartialEq)]
1274pub struct CT_TextField {
1275    pub id: String,
1276    pub field_type: Option<String>,
1277    pub run_properties: Option<CT_TextCharacterProperties>,
1278    pub paragraph_properties: Option<CT_TextParagraphProperties>,
1279    pub text: Option<TextValue>,
1280    raw_children: OrderedRawChildren,
1281}
1282
1283impl CT_TextField {
1284    /// Parses one complete `a:fld` element with any prefix.
1285    pub fn from_xml(xml: &[u8]) -> Result<Self> {
1286        parse_complete(xml, b"fld", Self::from_element, Self::from_empty)
1287    }
1288
1289    fn from_start(start: &BytesStart<'_>) -> Result<Self> {
1290        let id = required_attr(start, b"id")?;
1291        if !is_guid(&id) {
1292            return Err(invalid_attribute("fld", "id", &id));
1293        }
1294        Ok(Self {
1295            id,
1296            field_type: text_attr(start, b"type")?,
1297            run_properties: None,
1298            paragraph_properties: None,
1299            text: None,
1300            raw_children: OrderedRawChildren::default(),
1301        })
1302    }
1303
1304    fn from_empty(start: &BytesStart<'_>) -> Result<Self> {
1305        Self::from_start(start)
1306    }
1307
1308    fn from_element(reader: &mut Reader<&[u8]>, start: &BytesStart<'_>) -> Result<Self> {
1309        let mut field = Self::from_start(start)?;
1310        let mut boundary = 0;
1311        let mut buffer = Vec::new();
1312        loop {
1313            match reader
1314                .read_event_into(&mut buffer)
1315                .map_err(OxmlError::from)?
1316            {
1317                Event::Start(element) if matches_local_name(element.name().as_ref(), b"rPr") => {
1318                    if field.run_properties.is_some() {
1319                        return Err(duplicate("rPr"));
1320                    }
1321                    field.run_properties =
1322                        Some(CT_TextCharacterProperties::from_element(reader, &element)?);
1323                    boundary = boundary.max(1);
1324                }
1325                Event::Empty(element) if matches_local_name(element.name().as_ref(), b"rPr") => {
1326                    if field.run_properties.is_some() {
1327                        return Err(duplicate("rPr"));
1328                    }
1329                    field.run_properties = Some(CT_TextCharacterProperties::from_start(&element)?);
1330                    boundary = boundary.max(1);
1331                }
1332                Event::Start(element) if matches_local_name(element.name().as_ref(), b"pPr") => {
1333                    if field.paragraph_properties.is_some() {
1334                        return Err(duplicate("pPr"));
1335                    }
1336                    field.paragraph_properties =
1337                        Some(CT_TextParagraphProperties::from_element(reader, &element)?);
1338                    boundary = boundary.max(2);
1339                }
1340                Event::Empty(element) if matches_local_name(element.name().as_ref(), b"pPr") => {
1341                    if field.paragraph_properties.is_some() {
1342                        return Err(duplicate("pPr"));
1343                    }
1344                    field.paragraph_properties =
1345                        Some(CT_TextParagraphProperties::from_start(&element)?);
1346                    boundary = boundary.max(2);
1347                }
1348                Event::Start(element) if matches_local_name(element.name().as_ref(), b"t") => {
1349                    if field.text.is_some() {
1350                        return Err(duplicate("t"));
1351                    }
1352                    field.text = Some(TextValue::from_element(reader, &element)?);
1353                    boundary = boundary.max(3);
1354                }
1355                Event::Empty(element) if matches_local_name(element.name().as_ref(), b"t") => {
1356                    if field.text.is_some() {
1357                        return Err(duplicate("t"));
1358                    }
1359                    field.text = Some(TextValue::from_start(&element)?);
1360                    boundary = boundary.max(3);
1361                }
1362                Event::Start(element) => field
1363                    .raw_children
1364                    .push(boundary, capture_element(reader, &element)?),
1365                Event::Empty(element) => field
1366                    .raw_children
1367                    .push(boundary, capture_empty_element(&element)?),
1368                Event::End(element) if matches_local_name(element.name().as_ref(), b"fld") => {
1369                    return Ok(field);
1370                }
1371                Event::Eof => return Err(missing_end("fld")),
1372                _ => {}
1373            }
1374            buffer.clear();
1375        }
1376    }
1377
1378    fn write_xml<W: Write>(&self, writer: &mut Writer<W>) -> Result<()> {
1379        if !is_guid(&self.id) {
1380            return Err(invalid_attribute("fld", "id", &self.id));
1381        }
1382        let mut start = BytesStart::new("a:fld");
1383        start.push_attribute(("id", self.id.as_str()));
1384        push_optional_attr(&mut start, "type", self.field_type.as_deref());
1385        let modelled = self.run_properties.is_some()
1386            || self.paragraph_properties.is_some()
1387            || self.text.is_some();
1388        if !modelled && self.raw_children.is_empty() {
1389            return write_empty(writer, start);
1390        }
1391        write_start(writer, start)?;
1392        emit_raw(writer, self.raw_children.at(0))?;
1393        if let Some(properties) = &self.run_properties {
1394            properties.write_xml(writer, "a:rPr")?;
1395        }
1396        emit_raw(writer, self.raw_children.at(1))?;
1397        if let Some(properties) = &self.paragraph_properties {
1398            properties.write_xml(writer, "a:pPr")?;
1399        }
1400        emit_raw(writer, self.raw_children.at(2))?;
1401        if let Some(text) = &self.text {
1402            text.write_xml(writer)?;
1403        }
1404        emit_raw(writer, self.raw_children.at(3))?;
1405        write_end(writer, "a:fld")
1406    }
1407}
1408
1409/// One member of the paragraph text-content choice.
1410#[derive(Clone, Debug, Eq, PartialEq)]
1411pub enum TextRun {
1412    Run(CT_RegularTextRun),
1413    Break(CT_TextLineBreak),
1414    Field(Box<CT_TextField>),
1415}
1416
1417impl TextRun {
1418    fn from_xml(xml: &[u8], name: &[u8]) -> Result<Self> {
1419        match name {
1420            b"r" => Ok(Self::Run(CT_RegularTextRun::from_xml(xml)?)),
1421            b"br" => Ok(Self::Break(CT_TextLineBreak::from_xml(xml)?)),
1422            b"fld" => Ok(Self::Field(Box::new(CT_TextField::from_xml(xml)?))),
1423            _ => Err(TextError::UnexpectedElement(
1424                String::from_utf8_lossy(name).into_owned(),
1425            )),
1426        }
1427    }
1428
1429    fn write_xml<W: Write>(&self, writer: &mut Writer<W>) -> Result<()> {
1430        match self {
1431            Self::Run(run) => run.write_xml(writer),
1432            Self::Break(line_break) => line_break.write_xml(writer),
1433            Self::Field(field) => field.write_xml(writer),
1434        }
1435    }
1436}
1437
1438/// A DrawingML paragraph with ordered runs, breaks, and fields.
1439#[allow(non_camel_case_types)]
1440#[derive(Clone, Debug, Default, Eq, PartialEq)]
1441pub struct CT_TextParagraph {
1442    pub properties: Option<CT_TextParagraphProperties>,
1443    pub runs: Vec<TextRun>,
1444    pub end_properties: Option<CT_TextCharacterProperties>,
1445    raw_children: OrderedRawChildren,
1446}
1447
1448impl CT_TextParagraph {
1449    /// Returns direct properties, inserting them before preserved content.
1450    pub fn properties_mut(&mut self) -> &mut CT_TextParagraphProperties {
1451        if self.properties.is_none() {
1452            let mut raw_children = OrderedRawChildren::default();
1453            for boundary in 0..=2 + self.runs.len() {
1454                let new_boundary = if boundary == 0 { 1 } else { boundary };
1455                for child in self.raw_children.at(boundary) {
1456                    raw_children.push(new_boundary, child.to_vec());
1457                }
1458            }
1459            self.raw_children = raw_children;
1460            self.properties = Some(CT_TextParagraphProperties::default());
1461        }
1462        self.properties
1463            .as_mut()
1464            .expect("paragraph properties were inserted")
1465    }
1466
1467    /// Replaces ordered text choices with one regular run.
1468    ///
1469    /// The first existing regular run supplies direct formatting and
1470    /// unmodelled run content for the replacement.
1471    pub fn set_text(&mut self, text: &str) {
1472        let old_run_count = self.runs.len();
1473        let retained_run = self.runs.iter().find_map(|run| match run {
1474            TextRun::Run(run) => Some(run.clone()),
1475            TextRun::Break(_) | TextRun::Field(_) => None,
1476        });
1477        let mut raw_children = OrderedRawChildren::default();
1478        for boundary in 0..=2 + old_run_count {
1479            let new_boundary = if boundary <= 1 {
1480                boundary
1481            } else if boundary <= 1 + old_run_count {
1482                2
1483            } else {
1484                3
1485            };
1486            for child in self.raw_children.at(boundary) {
1487                raw_children.push(new_boundary, child.to_vec());
1488            }
1489        }
1490        self.raw_children = raw_children;
1491        let mut run = retained_run.unwrap_or_else(|| CT_RegularTextRun::new(text));
1492        run.set_text(text);
1493        self.runs = vec![TextRun::Run(run)];
1494    }
1495
1496    /// Appends one regular run after the existing ordered text choices.
1497    pub fn add_run(&mut self, text: &str) -> &mut CT_RegularTextRun {
1498        self.raw_children.shift_boundaries_from(2 + self.runs.len());
1499        self.runs.push(TextRun::Run(CT_RegularTextRun::new(text)));
1500        let Some(TextRun::Run(run)) = self.runs.last_mut() else {
1501            unreachable!("the appended text choice is a regular run")
1502        };
1503        run
1504    }
1505
1506    /// Parses one complete `a:p` element with any prefix.
1507    pub fn from_xml(xml: &[u8]) -> Result<Self> {
1508        parse_complete(xml, b"p", Self::from_element, |_| Ok(Self::default()))
1509    }
1510
1511    fn from_element(reader: &mut Reader<&[u8]>, _start: &BytesStart<'_>) -> Result<Self> {
1512        let mut paragraph = Self::default();
1513        let mut boundary = 0;
1514        let mut buffer = Vec::new();
1515        loop {
1516            match reader
1517                .read_event_into(&mut buffer)
1518                .map_err(OxmlError::from)?
1519            {
1520                Event::Start(element) if matches_local_name(element.name().as_ref(), b"pPr") => {
1521                    if paragraph.properties.is_some() {
1522                        return Err(duplicate("pPr"));
1523                    }
1524                    paragraph.properties =
1525                        Some(CT_TextParagraphProperties::from_element(reader, &element)?);
1526                    boundary = boundary.max(1);
1527                }
1528                Event::Empty(element) if matches_local_name(element.name().as_ref(), b"pPr") => {
1529                    if paragraph.properties.is_some() {
1530                        return Err(duplicate("pPr"));
1531                    }
1532                    paragraph.properties = Some(CT_TextParagraphProperties::from_start(&element)?);
1533                    boundary = boundary.max(1);
1534                }
1535                Event::Start(element) if is_text_run(element.name().as_ref()) => {
1536                    let name = local_name(element.name().as_ref()).to_vec();
1537                    let raw = capture_element(reader, &element)?;
1538                    paragraph.runs.push(TextRun::from_xml(&raw, &name)?);
1539                    boundary = boundary.max(1 + paragraph.runs.len());
1540                }
1541                Event::Empty(element) if is_text_run(element.name().as_ref()) => {
1542                    let name = local_name(element.name().as_ref()).to_vec();
1543                    let raw = capture_empty_element(&element)?;
1544                    paragraph.runs.push(TextRun::from_xml(&raw, &name)?);
1545                    boundary = boundary.max(1 + paragraph.runs.len());
1546                }
1547                Event::Start(element)
1548                    if matches_local_name(element.name().as_ref(), b"endParaRPr") =>
1549                {
1550                    if paragraph.end_properties.is_some() {
1551                        return Err(duplicate("endParaRPr"));
1552                    }
1553                    paragraph.end_properties =
1554                        Some(CT_TextCharacterProperties::from_element(reader, &element)?);
1555                    boundary = boundary.max(2 + paragraph.runs.len());
1556                }
1557                Event::Empty(element)
1558                    if matches_local_name(element.name().as_ref(), b"endParaRPr") =>
1559                {
1560                    if paragraph.end_properties.is_some() {
1561                        return Err(duplicate("endParaRPr"));
1562                    }
1563                    paragraph.end_properties =
1564                        Some(CT_TextCharacterProperties::from_start(&element)?);
1565                    boundary = boundary.max(2 + paragraph.runs.len());
1566                }
1567                Event::Start(element) => paragraph
1568                    .raw_children
1569                    .push(boundary, capture_element(reader, &element)?),
1570                Event::Empty(element) => paragraph
1571                    .raw_children
1572                    .push(boundary, capture_empty_element(&element)?),
1573                Event::End(element) if matches_local_name(element.name().as_ref(), b"p") => {
1574                    return Ok(paragraph);
1575                }
1576                Event::Eof => return Err(missing_end("p")),
1577                _ => {}
1578            }
1579            buffer.clear();
1580        }
1581    }
1582
1583    /// Writes a paragraph with fixed prefixes and schema child order.
1584    pub fn write_xml<W: Write>(&self, writer: &mut Writer<W>) -> Result<()> {
1585        if self.properties.is_none()
1586            && self.runs.is_empty()
1587            && self.end_properties.is_none()
1588            && self.raw_children.is_empty()
1589        {
1590            return write_empty(writer, BytesStart::new("a:p"));
1591        }
1592        write_start(writer, BytesStart::new("a:p"))?;
1593        emit_raw(writer, self.raw_children.at(0))?;
1594        if let Some(properties) = &self.properties {
1595            properties.write_xml(writer, "a:pPr")?;
1596        }
1597        emit_raw(writer, self.raw_children.at(1))?;
1598        for (index, run) in self.runs.iter().enumerate() {
1599            run.write_xml(writer)?;
1600            emit_raw(writer, self.raw_children.at(2 + index))?;
1601        }
1602        if let Some(properties) = &self.end_properties {
1603            properties.write_xml(writer, "a:endParaRPr")?;
1604        }
1605        emit_raw(writer, self.raw_children.at(2 + self.runs.len()))?;
1606        write_end(writer, "a:p")
1607    }
1608
1609    /// Serialises a complete paragraph fragment.
1610    pub fn to_xml(&self) -> Result<Vec<u8>> {
1611        let mut writer = Writer::new(Vec::new());
1612        self.write_xml(&mut writer)?;
1613        Ok(writer.into_inner())
1614    }
1615
1616    pub fn raw_children(&self) -> &OrderedRawChildren {
1617        &self.raw_children
1618    }
1619}
1620
1621fn is_text_run(name: &[u8]) -> bool {
1622    matches!(local_name(name), b"r" | b"br" | b"fld")
1623}
1624
1625fn parse_complete<T>(
1626    xml: &[u8],
1627    expected: &[u8],
1628    parse_start: impl FnOnce(&mut Reader<&[u8]>, &BytesStart<'_>) -> Result<T>,
1629    parse_empty: impl FnOnce(&BytesStart<'_>) -> Result<T>,
1630) -> Result<T> {
1631    let mut reader = Reader::from_reader(xml);
1632    let mut buffer = Vec::new();
1633    loop {
1634        match reader
1635            .read_event_into(&mut buffer)
1636            .map_err(OxmlError::from)?
1637        {
1638            Event::Start(element) if matches_local_name(element.name().as_ref(), expected) => {
1639                reject_conflicting_a_prefix(&element)?;
1640                return parse_start(&mut reader, &element);
1641            }
1642            Event::Empty(element) if matches_local_name(element.name().as_ref(), expected) => {
1643                reject_conflicting_a_prefix(&element)?;
1644                return parse_empty(&element);
1645            }
1646            Event::Start(element) | Event::Empty(element) => return Err(unexpected(&element)),
1647            Event::Eof => {
1648                return Err(TextError::Xml(OxmlError::MissingElement(
1649                    String::from_utf8_lossy(expected).into_owned(),
1650                )));
1651            }
1652            _ => {}
1653        }
1654        buffer.clear();
1655    }
1656}
1657
1658fn root_local_name(xml: &[u8]) -> Result<Vec<u8>> {
1659    let mut reader = Reader::from_reader(xml);
1660    let mut buffer = Vec::new();
1661    loop {
1662        match reader
1663            .read_event_into(&mut buffer)
1664            .map_err(OxmlError::from)?
1665        {
1666            Event::Start(element) | Event::Empty(element) => {
1667                return Ok(local_name(element.name().as_ref()).to_vec());
1668            }
1669            Event::Eof => {
1670                return Err(TextError::Xml(OxmlError::MissingElement(
1671                    "DrawingML text properties".to_owned(),
1672                )));
1673            }
1674            _ => {}
1675        }
1676        buffer.clear();
1677    }
1678}
1679
1680fn ensure_empty(reader: &mut Reader<&[u8]>, expected: &[u8]) -> Result<()> {
1681    let mut buffer = Vec::new();
1682    loop {
1683        match reader
1684            .read_event_into(&mut buffer)
1685            .map_err(OxmlError::from)?
1686        {
1687            Event::End(element) if matches_local_name(element.name().as_ref(), expected) => {
1688                return Ok(());
1689            }
1690            Event::Text(text) if text.iter().all(u8::is_ascii_whitespace) => {}
1691            Event::Comment(_) => {}
1692            Event::Start(element) | Event::Empty(element) => return Err(unexpected(&element)),
1693            Event::Eof => return Err(missing_end(&String::from_utf8_lossy(expected))),
1694            _ => {
1695                return Err(TextError::UnexpectedElement(
1696                    String::from_utf8_lossy(expected).into_owned(),
1697                ));
1698            }
1699        }
1700        buffer.clear();
1701    }
1702}
1703
1704fn text_attr(start: &BytesStart<'_>, name: &[u8]) -> Result<Option<String>> {
1705    for attribute in start.attributes() {
1706        let attribute = attribute.map_err(OxmlError::from)?;
1707        if attribute.key.as_ref() == name {
1708            let value = attribute
1709                .decoded_and_normalized_value(XmlVersion::Implicit1_0, start.decoder())
1710                .map_err(OxmlError::from)?;
1711            return Ok(Some(value.into_owned()));
1712        }
1713    }
1714    Ok(None)
1715}
1716
1717fn capture_raw_attributes(
1718    start: &BytesStart<'_>,
1719    modelled: &[&[u8]],
1720) -> Result<Vec<(String, String)>> {
1721    let mut raw = Vec::new();
1722    for attribute in start.attributes() {
1723        let attribute = attribute.map_err(OxmlError::from)?;
1724        if modelled.iter().any(|name| attribute.key.as_ref() == *name) {
1725            continue;
1726        }
1727        let name = std::str::from_utf8(attribute.key.as_ref())
1728            .map_err(OxmlError::from)?
1729            .to_owned();
1730        let value = attribute
1731            .decoded_and_normalized_value(XmlVersion::Implicit1_0, start.decoder())
1732            .map_err(OxmlError::from)?
1733            .into_owned();
1734        raw.push((name, value));
1735    }
1736    Ok(raw)
1737}
1738
1739fn push_raw_attributes(start: &mut BytesStart<'_>, attributes: &[(String, String)]) {
1740    for (name, value) in attributes {
1741        start.push_attribute((name.as_str(), value.as_str()));
1742    }
1743}
1744
1745fn required_attr(start: &BytesStart<'_>, name: &[u8]) -> Result<String> {
1746    text_attr(start, name)?
1747        .filter(|value| !value.is_empty())
1748        .ok_or_else(|| {
1749            missing_attribute(
1750                &String::from_utf8_lossy(local_name(start.name().as_ref())),
1751                &String::from_utf8_lossy(name),
1752            )
1753        })
1754}
1755
1756fn parse_optional_i32(
1757    start: &BytesStart<'_>,
1758    name: &[u8],
1759    min: i32,
1760    max: i32,
1761) -> Result<Option<i32>> {
1762    text_attr(start, name)?
1763        .map(|value| {
1764            parse_i32_value(
1765                &String::from_utf8_lossy(local_name(start.name().as_ref())),
1766                &String::from_utf8_lossy(name),
1767                &value,
1768                min,
1769                max,
1770            )
1771        })
1772        .transpose()
1773}
1774
1775fn parse_i32_attr(start: &BytesStart<'_>, name: &[u8], min: i32, max: i32) -> Result<i32> {
1776    let value = required_attr(start, name)?;
1777    parse_i32_value(
1778        &String::from_utf8_lossy(local_name(start.name().as_ref())),
1779        &String::from_utf8_lossy(name),
1780        &value,
1781        min,
1782        max,
1783    )
1784}
1785
1786fn parse_i32_value(element: &str, attribute: &str, value: &str, min: i32, max: i32) -> Result<i32> {
1787    let parsed = value
1788        .parse::<i32>()
1789        .map_err(|_| invalid_attribute(element, attribute, value))?;
1790    if (min..=max).contains(&parsed) {
1791        Ok(parsed)
1792    } else {
1793        Err(invalid_attribute(element, attribute, value))
1794    }
1795}
1796
1797fn parse_optional_bool(start: &BytesStart<'_>, name: &[u8]) -> Result<Option<bool>> {
1798    text_attr(start, name)?
1799        .map(|value| match value.as_str() {
1800            "true" | "1" => Ok(true),
1801            "false" | "0" => Ok(false),
1802            _ => Err(invalid_attribute(
1803                &String::from_utf8_lossy(local_name(start.name().as_ref())),
1804                &String::from_utf8_lossy(name),
1805                &value,
1806            )),
1807        })
1808        .transpose()
1809}
1810
1811fn parse_optional_enum<T>(
1812    start: &BytesStart<'_>,
1813    name: &[u8],
1814    parse: impl FnOnce(&str) -> Option<T>,
1815) -> Result<Option<T>> {
1816    let Some(value) = text_attr(start, name)? else {
1817        return Ok(None);
1818    };
1819    parse(&value).map(Some).ok_or_else(|| {
1820        invalid_attribute(
1821            &String::from_utf8_lossy(local_name(start.name().as_ref())),
1822            &String::from_utf8_lossy(name),
1823            &value,
1824        )
1825    })
1826}
1827
1828fn validate_spacing_percent(value: &str) -> Result<()> {
1829    if is_percentage_string(value) {
1830        return Ok(());
1831    }
1832    parse_i32_value("spcPct", "val", value, 0, MAX_TRANSITIONAL_SPACING_PERCENT)?;
1833    Ok(())
1834}
1835
1836fn validate_baseline(value: &str) -> Result<()> {
1837    if is_percentage_string(value) {
1838        return Ok(());
1839    }
1840    parse_i32_value("rPr", "baseline", value, -100_000, 100_000)?;
1841    Ok(())
1842}
1843
1844fn is_percentage_string(value: &str) -> bool {
1845    value.strip_suffix('%').is_some_and(is_signed_decimal)
1846}
1847
1848fn is_universal_measure(value: &str) -> bool {
1849    ["mm", "cm", "in", "pt", "pc", "pi"]
1850        .iter()
1851        .find_map(|unit| value.strip_suffix(unit))
1852        .is_some_and(is_signed_decimal)
1853}
1854
1855fn is_signed_decimal(value: &str) -> bool {
1856    let unsigned = value.strip_prefix('-').unwrap_or(value);
1857    let mut parts = unsigned.split('.');
1858    let Some(integer) = parts.next() else {
1859        return false;
1860    };
1861    if integer.is_empty() || !integer.bytes().all(|byte| byte.is_ascii_digit()) {
1862        return false;
1863    }
1864    if let Some(fraction) = parts.next()
1865        && (fraction.is_empty() || !fraction.bytes().all(|byte| byte.is_ascii_digit()))
1866    {
1867        return false;
1868    }
1869    parts.next().is_none()
1870}
1871
1872fn is_guid(value: &str) -> bool {
1873    if value.len() != 38 || !value.starts_with('{') || !value.ends_with('}') {
1874        return false;
1875    }
1876    value.bytes().enumerate().all(|(index, byte)| match index {
1877        0 => byte == b'{',
1878        37 => byte == b'}',
1879        9 | 14 | 19 | 24 => byte == b'-',
1880        _ => byte.is_ascii_hexdigit(),
1881    })
1882}
1883
1884fn push_optional_attr<'a>(start: &mut BytesStart<'a>, name: &'a str, value: Option<&'a str>) {
1885    if let Some(value) = value {
1886        start.push_attribute((name, value));
1887    }
1888}
1889
1890fn push_optional_bool(start: &mut BytesStart<'_>, name: &'static str, value: Option<bool>) {
1891    if let Some(value) = value {
1892        start.push_attribute((name, if value { "1" } else { "0" }));
1893    }
1894}
1895
1896fn push_owned_attr(start: &mut BytesStart<'_>, name: &'static str, value: String) {
1897    start.push_attribute((name, value.as_str()));
1898}
1899
1900fn emit_raw<'a, W: Write>(
1901    writer: &mut Writer<W>,
1902    children: impl Iterator<Item = &'a [u8]>,
1903) -> Result<()> {
1904    for child in children {
1905        writer.get_mut().write_all(child).map_err(OxmlError::from)?;
1906    }
1907    Ok(())
1908}
1909
1910fn write_start<W: Write>(writer: &mut Writer<W>, start: BytesStart<'_>) -> Result<()> {
1911    writer
1912        .write_event(Event::Start(start))
1913        .map_err(OxmlError::from)?;
1914    Ok(())
1915}
1916
1917fn write_empty<W: Write>(writer: &mut Writer<W>, start: BytesStart<'_>) -> Result<()> {
1918    writer
1919        .write_event(Event::Empty(start))
1920        .map_err(OxmlError::from)?;
1921    Ok(())
1922}
1923
1924fn write_end<W: Write>(writer: &mut Writer<W>, tag: &str) -> Result<()> {
1925    writer
1926        .write_event(Event::End(BytesEnd::new(tag)))
1927        .map_err(OxmlError::from)?;
1928    Ok(())
1929}
1930
1931fn unexpected(element: &BytesStart<'_>) -> TextError {
1932    TextError::UnexpectedElement(String::from_utf8_lossy(element.name().as_ref()).into_owned())
1933}
1934
1935fn duplicate(element: &str) -> TextError {
1936    TextError::DuplicateElement(element.to_owned())
1937}
1938
1939fn missing_attribute(element: &str, attribute: &str) -> TextError {
1940    TextError::MissingAttribute {
1941        element: element.to_owned(),
1942        attribute: attribute.to_owned(),
1943    }
1944}
1945
1946fn invalid_attribute(element: &str, attribute: &str, value: &str) -> TextError {
1947    TextError::InvalidAttribute {
1948        element: element.to_owned(),
1949        attribute: attribute.to_owned(),
1950        value: value.to_owned(),
1951    }
1952}
1953
1954#[cfg(test)]
1955mod tests {
1956    use std::panic;
1957
1958    use super::{
1959        CT_TextCharacterProperties, CT_TextParagraph, CT_TextParagraphProperties, TextRun,
1960        TextSpace, TextSpacing,
1961    };
1962    use crate::color::ColorChoice;
1963    use crate::text::CT_TextBody;
1964    use crate::text::{TextAutoNumberScheme, TextBulletChoice, TextBulletSizeValue};
1965
1966    #[test]
1967    fn drawingml_rtl_attribute_becomes_typed_without_reordering_unknown_content() {
1968        let parsed = CT_TextParagraphProperties::from_xml(
1969            br#"<q:pPr xmlns:q="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:x="urn:test" defTabSz="457200" rtl="1" x:keep="yes"/>"#,
1970        )
1971        .unwrap();
1972
1973        assert_eq!(parsed.right_to_left, Some(true));
1974        let serialized = String::from_utf8(parsed.to_xml().unwrap()).unwrap();
1975        assert!(serialized.contains("rtl=\"1\""));
1976        assert!(serialized.contains("x:keep=\"yes\""));
1977        assert!(
1978            serialized.find("defTabSz").unwrap() < serialized.find("rtl").unwrap(),
1979            "{serialized}"
1980        );
1981
1982        let mut changed = parsed;
1983        changed.right_to_left = Some(false);
1984        let serialized = String::from_utf8(changed.to_xml().unwrap()).unwrap();
1985        assert!(serialized.contains("rtl=\"0\""), "{serialized}");
1986        assert!(!serialized.contains("rtl=\"1\""), "{serialized}");
1987    }
1988
1989    #[test]
1990    fn leading_and_trailing_text_whitespace_survives_via_xml_space_preserve() {
1991        let xml = br#"<q:p><q:r><q:t> leading</q:t></q:r><q:r><q:t>trailing </q:t></q:r><q:fld id="{00112233-4455-6677-8899-AABBCCDDEEFF}"><q:t xml:space="preserve">middle</q:t></q:fld></q:p>"#;
1992        let paragraph = CT_TextParagraph::from_xml(xml).unwrap();
1993        assert_eq!(paragraph.to_xml().unwrap(), br#"<a:p><a:r><a:t xml:space="preserve"> leading</a:t></a:r><a:r><a:t xml:space="preserve">trailing </a:t></a:r><a:fld id="{00112233-4455-6677-8899-AABBCCDDEEFF}"><a:t xml:space="preserve">middle</a:t></a:fld></a:p>"#);
1994
1995        let TextRun::Run(first) = &paragraph.runs[0] else {
1996            panic!("expected regular run");
1997        };
1998        assert_eq!(first.text.value, " leading");
1999        assert_eq!(first.text.space, TextSpace::Preserve);
2000
2001        let body = CT_TextBody::from_xml(
2002            br#"<q:txBody><q:bodyPr/><q:p><q:r><q:t> body </q:t></q:r></q:p></q:txBody>"#,
2003        )
2004        .unwrap();
2005        assert_eq!(body.to_xml().unwrap(), br#"<a:txBody><a:bodyPr/><a:p><a:r><a:t xml:space="preserve"> body </a:t></a:r></a:p></a:txBody>"#);
2006
2007        let hostile = CT_TextParagraph::from_xml(
2008            br#"<q:p><q:r><q:t x:space="preserve">plain</q:t></q:r></q:p>"#,
2009        )
2010        .unwrap();
2011        let TextRun::Run(run) = &hostile.runs[0] else {
2012            panic!("expected regular run");
2013        };
2014        assert_eq!(run.text.space, TextSpace::Default);
2015        assert_eq!(
2016            hostile.to_xml().unwrap(),
2017            br#"<a:p><a:r><a:t x:space="preserve">plain</a:t></a:r></a:p>"#
2018        );
2019    }
2020
2021    #[test]
2022    fn canonical_text_state_matches_whitespace_and_empty_hyperlinks_from_real_decks() {
2023        let paragraph = CT_TextParagraph::from_xml(
2024            br#"<q:p><q:r><q:rPr><q:hlinkClick r:id=""/></q:rPr><q:t>trailing </q:t></q:r></q:p>"#,
2025        )
2026        .unwrap();
2027        let TextRun::Run(run) = &paragraph.runs[0] else {
2028            panic!("expected regular run");
2029        };
2030        assert_eq!(run.text.space, TextSpace::Preserve);
2031        assert_eq!(
2032            run.properties
2033                .as_ref()
2034                .and_then(|properties| properties.hyperlink_click.as_ref())
2035                .and_then(|hyperlink| hyperlink.relationship_id.as_deref()),
2036            Some("")
2037        );
2038        let written = paragraph.to_xml().unwrap();
2039        assert_eq!(CT_TextParagraph::from_xml(&written).unwrap(), paragraph);
2040    }
2041
2042    #[test]
2043    fn paragraph_runs_fields_and_breaks_round_trip_structurally() {
2044        let xml = br#"<q:p><x:before/><q:pPr algn="ctr"/><x:afterPPr/><q:r><q:rPr b="1"><q:hlinkClick x:id="not-a-relationship" r:id="rId7" action="ppaction://hlinksldjump" tooltip="go &amp; stay" history="0" highlightClick="1" endSnd="0"><x:sound/></q:hlinkClick></q:rPr><q:t>one</q:t></q:r><x:between/><q:br><q:rPr i="true"/></q:br><q:fld id="{00112233-4455-6677-8899-AABBCCDDEEFF}" type="slidenum"><q:rPr sz="1200"/><q:pPr lvl="1"/><q:t>2</q:t></q:fld><q:endParaRPr sz="1400"/><x:after/></q:p>"#;
2045        let paragraph = CT_TextParagraph::from_xml(xml).unwrap();
2046        assert_eq!(paragraph.runs.len(), 3);
2047        let written = paragraph.to_xml().unwrap();
2048        assert_eq!(written, br#"<a:p><x:before/><a:pPr algn="ctr"/><x:afterPPr/><a:r><a:rPr b="1"><a:hlinkClick r:id="rId7" action="ppaction://hlinksldjump" tooltip="go &amp; stay" history="0" highlightClick="1" endSnd="0" x:id="not-a-relationship"><x:sound/></a:hlinkClick></a:rPr><a:t>one</a:t></a:r><x:between/><a:br><a:rPr i="1"/></a:br><a:fld id="{00112233-4455-6677-8899-AABBCCDDEEFF}" type="slidenum"><a:rPr sz="1200"/><a:pPr lvl="1"/><a:t>2</a:t></a:fld><a:endParaRPr sz="1400"/><x:after/></a:p>"#);
2049        assert_eq!(CT_TextParagraph::from_xml(&written).unwrap(), paragraph);
2050
2051        let hostile = CT_TextCharacterProperties::from_xml(
2052            br#"<q:rPr><q:hlinkClick x:id="not-a-relationship"/></q:rPr>"#,
2053        )
2054        .unwrap();
2055        assert!(
2056            hostile
2057                .hyperlink_click
2058                .as_ref()
2059                .unwrap()
2060                .relationship_id
2061                .is_none()
2062        );
2063        let mut writer = quick_xml::Writer::new(Vec::new());
2064        hostile.write_xml(&mut writer, "a:rPr").unwrap();
2065        assert_eq!(
2066            writer.into_inner(),
2067            br#"<a:rPr><a:hlinkClick x:id="not-a-relationship"/></a:rPr>"#
2068        );
2069    }
2070
2071    #[test]
2072    fn character_all_caps_round_trips_while_small_caps_stays_unmodelled() {
2073        let all = CT_TextCharacterProperties::from_xml(
2074            br#"<q:rPr cap="all" sz="1800" x:producer="kept"/>"#,
2075        )
2076        .unwrap();
2077        assert_eq!(all.all_caps, Some(true));
2078        let mut writer = quick_xml::Writer::new(Vec::new());
2079        all.write_xml(&mut writer, "a:rPr").unwrap();
2080        let written = writer.into_inner();
2081        assert_eq!(
2082            written,
2083            br#"<a:rPr sz="1800" cap="all" x:producer="kept"/>"#
2084        );
2085        assert_eq!(CT_TextCharacterProperties::from_xml(&written).unwrap(), all);
2086
2087        let none = CT_TextCharacterProperties::from_xml(br#"<q:rPr cap="none"/>"#).unwrap();
2088        assert_eq!(none.all_caps, Some(false));
2089
2090        let small = CT_TextCharacterProperties::from_xml(br#"<q:rPr cap="small"/>"#).unwrap();
2091        assert_eq!(small.all_caps, None);
2092        let mut writer = quick_xml::Writer::new(Vec::new());
2093        small.write_xml(&mut writer, "a:rPr").unwrap();
2094        assert_eq!(writer.into_inner(), br#"<a:rPr cap="small"/>"#);
2095    }
2096
2097    #[test]
2098    fn paragraph_and_run_properties_use_drawingml_units_and_schema_order() {
2099        let paragraph_properties = CT_TextParagraphProperties::from_xml(br#"<q:pPr marL="0" marR="51206400" lvl="8" indent="-51206400" algn="thaiDist" rtl="1" defTabSz="914400"><x:before/><q:lnSpc><q:spcPct val="100000"/></q:lnSpc><q:spcBef><q:spcPts val="158400"/></q:spcBef><q:spcAft><q:spcPct val="125.5%"/></q:spcAft><q:buChar char="*"/><x:afterBullet/><q:defRPr sz="400000" b="true" i="0" u="wavyDbl" strike="dblStrike" spc="-400000" baseline="25%" xmlns:b="urn:test" lang="en-US" kern="1200"><q:solidFill><q:srgbClr val="AABBCC"/></q:solidFill><q:latin typeface="Aptos" pitchFamily="34" charset="0"/><q:ea typeface="Yu Gothic"/><q:cs typeface="Arial"/><q:sym typeface="Symbol"/><q:hlinkMouseOver r:id="rId8" tgtFrame="_blank" invalidUrl="bad"/></q:defRPr><x:last/></q:pPr>"#).unwrap();
2100        let written = {
2101            let mut writer = quick_xml::Writer::new(Vec::new());
2102            paragraph_properties
2103                .write_xml(&mut writer, "a:pPr")
2104                .unwrap();
2105            writer.into_inner()
2106        };
2107        assert_eq!(written, br#"<a:pPr marL="0" marR="51206400" lvl="8" indent="-51206400" algn="thaiDist" rtl="1" defTabSz="914400"><x:before/><a:lnSpc><a:spcPct val="100000"/></a:lnSpc><a:spcBef><a:spcPts val="158400"/></a:spcBef><a:spcAft><a:spcPct val="125.5%"/></a:spcAft><a:buChar char="*"/><x:afterBullet/><a:defRPr sz="400000" b="1" i="0" u="wavyDbl" strike="dblStrike" spc="-400000" baseline="25%" xmlns:b="urn:test" lang="en-US" kern="1200"><a:solidFill><a:srgbClr val="AABBCC"/></a:solidFill><a:latin typeface="Aptos" pitchFamily="34" charset="0"/><a:ea typeface="Yu Gothic"/><a:cs typeface="Arial"/><a:sym typeface="Symbol"/><a:hlinkMouseOver r:id="rId8" invalidUrl="bad" tgtFrame="_blank"/></a:defRPr><x:last/></a:pPr>"#);
2108
2109        let run_properties = CT_TextCharacterProperties::from_xml(
2110            br#"<q:rPr sz="100" spc="1.25pt" baseline="-100000"/>"#,
2111        )
2112        .unwrap();
2113        let mut writer = quick_xml::Writer::new(Vec::new());
2114        run_properties.write_xml(&mut writer, "a:rPr").unwrap();
2115        assert_eq!(
2116            writer.into_inner(),
2117            br#"<a:rPr sz="100" spc="1.25pt" baseline="-100000"/>"#
2118        );
2119
2120        let mut inserted_spacing =
2121            CT_TextParagraphProperties::from_xml(br#"<q:pPr><q:buChar char="*"/></q:pPr>"#)
2122                .unwrap();
2123        inserted_spacing.line_spacing = Some(TextSpacing::Percent("100000".to_owned()));
2124        let mut writer = quick_xml::Writer::new(Vec::new());
2125        inserted_spacing.write_xml(&mut writer, "a:pPr").unwrap();
2126        assert_eq!(
2127            writer.into_inner(),
2128            br#"<a:pPr><a:lnSpc><a:spcPct val="100000"/></a:lnSpc><a:buChar char="*"/></a:pPr>"#
2129        );
2130    }
2131
2132    #[test]
2133    fn every_modelled_bullet_form_round_trips_in_schema_order() {
2134        let cases: &[(&[u8], &[u8])] = &[
2135            (
2136                br#"<q:pPr><q:buClr><q:srgbClr val="AABBCC"/></q:buClr><q:buSzPct val="125000"/><q:buFont typeface="Wingdings"/><q:buChar char="*"/></q:pPr>"#,
2137                br#"<a:pPr><a:buClr><a:srgbClr val="AABBCC"/></a:buClr><a:buSzPct val="125000"/><a:buFont typeface="Wingdings"/><a:buChar char="*"/></a:pPr>"#,
2138            ),
2139            (
2140                br#"<q:pPr><q:buSzPts val="1800"/><q:buAutoNum type="arabicPeriod" startAt="3"/></q:pPr>"#,
2141                br#"<a:pPr><a:buSzPts val="1800"/><a:buAutoNum type="arabicPeriod" startAt="3"/></a:pPr>"#,
2142            ),
2143            (br#"<q:pPr><q:buNone/></q:pPr>"#, br#"<a:pPr><a:buNone/></a:pPr>"#),
2144        ];
2145
2146        for (xml, expected) in cases {
2147            let properties = CT_TextParagraphProperties::from_xml(xml).unwrap();
2148            let mut writer = quick_xml::Writer::new(Vec::new());
2149            properties.write_xml(&mut writer, "a:pPr").unwrap();
2150            let written = writer.into_inner();
2151            assert_eq!(written, *expected);
2152            assert_eq!(
2153                CT_TextParagraphProperties::from_xml(&written).unwrap(),
2154                properties
2155            );
2156        }
2157
2158        let character = CT_TextParagraphProperties::from_xml(cases[0].0).unwrap();
2159        let bullet = character.bullet.as_ref().unwrap();
2160        assert!(matches!(
2161            bullet.choice,
2162            Some(TextBulletChoice::Character(_))
2163        ));
2164        assert_eq!(bullet.font.as_ref().unwrap().typeface, "Wingdings");
2165        assert!(matches!(
2166            bullet.size.as_ref().unwrap().value,
2167            TextBulletSizeValue::Percent(ref value) if value == "125000"
2168        ));
2169        assert!(matches!(
2170            bullet.color.as_ref().unwrap().color,
2171            ColorChoice::Srgb { .. }
2172        ));
2173
2174        let automatic = CT_TextParagraphProperties::from_xml(cases[1].0).unwrap();
2175        let Some(TextBulletChoice::AutoNumber(numbering)) =
2176            automatic.bullet.as_ref().unwrap().choice.as_ref()
2177        else {
2178            panic!("expected automatic-number bullet");
2179        };
2180        assert_eq!(numbering.scheme, TextAutoNumberScheme::ArabicPeriod);
2181        assert_eq!(numbering.start_at, Some(3));
2182        assert!(matches!(
2183            automatic
2184                .bullet
2185                .as_ref()
2186                .unwrap()
2187                .size
2188                .as_ref()
2189                .unwrap()
2190                .value,
2191            TextBulletSizeValue::Points(1800)
2192        ));
2193
2194        let none = CT_TextParagraphProperties::from_xml(cases[2].0).unwrap();
2195        assert!(matches!(
2196            none.bullet.as_ref().unwrap().choice,
2197            Some(TextBulletChoice::None(_))
2198        ));
2199    }
2200
2201    #[test]
2202    fn bullet_font_size_and_colour_keep_their_schema_positions() {
2203        let properties = CT_TextParagraphProperties::from_xml(
2204            br#"<q:pPr><q:buFont typeface="Aptos"/><q:buChar char="*"/><q:buClr><q:schemeClr val="accent1"/></q:buClr><q:buSzPct val="80%"/><q:defRPr b="1"/></q:pPr>"#,
2205        )
2206        .unwrap();
2207        let mut writer = quick_xml::Writer::new(Vec::new());
2208        properties.write_xml(&mut writer, "a:pPr").unwrap();
2209        assert_eq!(
2210            writer.into_inner(),
2211            br#"<a:pPr><a:buClr><a:schemeClr val="accent1"/></a:buClr><a:buSzPct val="80%"/><a:buFont typeface="Aptos"/><a:buChar char="*"/><a:defRPr b="1"/></a:pPr>"#
2212        );
2213    }
2214
2215    #[test]
2216    fn unknown_bullet_children_round_trip_byte_for_byte() {
2217        let properties = CT_TextParagraphProperties::from_xml(
2218            br#"<q:pPr><q:buClrTx x:mode="stay"><x:nested>one &amp; two</x:nested><!--note--></q:buClrTx><q:buChar char="*"/><x:after/></q:pPr>"#,
2219        )
2220        .unwrap();
2221        let mut writer = quick_xml::Writer::new(Vec::new());
2222        properties.write_xml(&mut writer, "a:pPr").unwrap();
2223        assert_eq!(
2224            writer.into_inner(),
2225            br#"<a:pPr><q:buClrTx x:mode="stay"><x:nested>one &amp; two</x:nested><!--note--></q:buClrTx><a:buChar char="*"/><x:after/></a:pPr>"#
2226        );
2227    }
2228
2229    #[test]
2230    fn malformed_bullet_values_return_errors_without_panicking() {
2231        let cases: &[&[u8]] = &[
2232            br#"<q:pPr><q:buChar/></q:pPr>"#,
2233            br#"<q:pPr><q:buChar char=""/></q:pPr>"#,
2234            br#"<q:pPr><q:buSzPct val="24999"/></q:pPr>"#,
2235            br#"<q:pPr><q:buSzPct val="400001"/></q:pPr>"#,
2236            br#"<q:pPr><q:buSzPts val="99"/></q:pPr>"#,
2237            br#"<q:pPr><q:buSzPts val="400001"/></q:pPr>"#,
2238            br#"<q:pPr><q:buAutoNum type="unknown"/></q:pPr>"#,
2239            br#"<q:pPr><q:buAutoNum/></q:pPr>"#,
2240            br#"<q:pPr><q:buAutoNum type="arabicPeriod" startAt="0"/></q:pPr>"#,
2241            br#"<q:pPr><q:buAutoNum type="arabicPeriod" startAt="32768"/></q:pPr>"#,
2242            br#"<q:pPr><q:buFont/></q:pPr>"#,
2243            br#"<q:pPr><q:buClr/></q:pPr>"#,
2244            br#"<q:pPr><q:buClr><q:srgbClr x:val="AABBCC"/></q:buClr></q:pPr>"#,
2245            br#"<q:pPr><q:buClr><q:srgbClr val="GGGGGG"/></q:buClr></q:pPr>"#,
2246            br#"<q:pPr><q:buClr><q:srgbClr val="AABBCC"/><q:schemeClr val="accent1"/></q:buClr></q:pPr>"#,
2247            br#"<q:pPr><q:buChar char="*"/><q:buNone/></q:pPr>"#,
2248            br#"<q:pPr><q:buSzPct val="100000"/><q:buSzPts val="1200"/></q:pPr>"#,
2249        ];
2250
2251        for xml in cases {
2252            let result = panic::catch_unwind(|| CT_TextParagraphProperties::from_xml(xml));
2253            assert!(result.is_ok(), "bullet parser panicked");
2254            assert!(result.unwrap().is_err(), "malformed bullet parsed");
2255        }
2256    }
2257
2258    #[test]
2259    fn malformed_text_content_returns_errors_without_panicking() {
2260        let cases: &[&[u8]] = &[
2261            br#"<q:r/>"#,
2262            br#"<q:r><q:t><x:nested/></q:t></q:r>"#,
2263            br#"<q:r><q:t xml:space="keep">x</q:t></q:r>"#,
2264            br#"<q:r><q:t>x</q:t><q:t>y</q:t></q:r>"#,
2265            br#"<q:pPr lvl="9"/>"#,
2266            br#"<q:pPr marL="-1"/>"#,
2267            br#"<q:pPr indent="51206401"/>"#,
2268            br#"<q:pPr algn="middle"/>"#,
2269            br#"<q:pPr><q:lnSpc/></q:pPr>"#,
2270            br#"<q:pPr><q:lnSpc><q:spcPts val="158401"/></q:lnSpc></q:pPr>"#,
2271            br#"<q:pPr><q:lnSpc><q:spcPct val="201170"/></q:lnSpc></q:pPr>"#,
2272            br#"<q:rPr sz="99"/>"#,
2273            br#"<q:rPr b="yes"/>"#,
2274            br#"<q:rPr u="triple"/>"#,
2275            br#"<q:rPr spc="400001"/>"#,
2276            r#"<q:rPr spc="éx"/>"#.as_bytes(),
2277            br#"<q:rPr baseline="100001"/>"#,
2278            br#"<q:rPr><q:latin/></q:rPr>"#,
2279            br#"<q:fld type="slidenum"/>"#,
2280            br#"<q:fld id="not-a-guid"/>"#,
2281            br#"<q:p><q:endParaRPr/><q:endParaRPr/></q:p>"#,
2282            br#"<q:notPPr/>"#,
2283            br#"<q:notRPr/>"#,
2284        ];
2285        for xml in cases {
2286            let result = panic::catch_unwind(|| match super::root_local_name(xml).as_deref() {
2287                Ok(b"r") => super::CT_RegularTextRun::from_xml(xml).map(|_| ()),
2288                Ok(b"pPr") => CT_TextParagraphProperties::from_xml(xml).map(|_| ()),
2289                Ok(b"rPr") => CT_TextCharacterProperties::from_xml(xml).map(|_| ()),
2290                Ok(b"fld") => super::CT_TextField::from_xml(xml).map(|_| ()),
2291                Ok(b"p") => CT_TextParagraph::from_xml(xml).map(|_| ()),
2292                Ok(b"notPPr") => CT_TextParagraphProperties::from_xml(xml).map(|_| ()),
2293                Ok(b"notRPr") => CT_TextCharacterProperties::from_xml(xml).map(|_| ()),
2294                _ => unreachable!(),
2295            });
2296            assert!(result.is_ok(), "text parser panicked");
2297            assert!(result.unwrap().is_err(), "malformed text parsed");
2298        }
2299    }
2300}