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    pub fn set_text(&mut self, text: &str) {
1469        let old_run_count = self.runs.len();
1470        let mut raw_children = OrderedRawChildren::default();
1471        for boundary in 0..=2 + old_run_count {
1472            let new_boundary = if boundary <= 1 {
1473                boundary
1474            } else if boundary <= 1 + old_run_count {
1475                2
1476            } else {
1477                3
1478            };
1479            for child in self.raw_children.at(boundary) {
1480                raw_children.push(new_boundary, child.to_vec());
1481            }
1482        }
1483        self.raw_children = raw_children;
1484        self.runs = vec![TextRun::Run(CT_RegularTextRun::new(text))];
1485    }
1486
1487    /// Appends one regular run after the existing ordered text choices.
1488    pub fn add_run(&mut self, text: &str) -> &mut CT_RegularTextRun {
1489        self.raw_children.shift_boundaries_from(2 + self.runs.len());
1490        self.runs.push(TextRun::Run(CT_RegularTextRun::new(text)));
1491        let Some(TextRun::Run(run)) = self.runs.last_mut() else {
1492            unreachable!("the appended text choice is a regular run")
1493        };
1494        run
1495    }
1496
1497    /// Parses one complete `a:p` element with any prefix.
1498    pub fn from_xml(xml: &[u8]) -> Result<Self> {
1499        parse_complete(xml, b"p", Self::from_element, |_| Ok(Self::default()))
1500    }
1501
1502    fn from_element(reader: &mut Reader<&[u8]>, _start: &BytesStart<'_>) -> Result<Self> {
1503        let mut paragraph = Self::default();
1504        let mut boundary = 0;
1505        let mut buffer = Vec::new();
1506        loop {
1507            match reader
1508                .read_event_into(&mut buffer)
1509                .map_err(OxmlError::from)?
1510            {
1511                Event::Start(element) if matches_local_name(element.name().as_ref(), b"pPr") => {
1512                    if paragraph.properties.is_some() {
1513                        return Err(duplicate("pPr"));
1514                    }
1515                    paragraph.properties =
1516                        Some(CT_TextParagraphProperties::from_element(reader, &element)?);
1517                    boundary = boundary.max(1);
1518                }
1519                Event::Empty(element) if matches_local_name(element.name().as_ref(), b"pPr") => {
1520                    if paragraph.properties.is_some() {
1521                        return Err(duplicate("pPr"));
1522                    }
1523                    paragraph.properties = Some(CT_TextParagraphProperties::from_start(&element)?);
1524                    boundary = boundary.max(1);
1525                }
1526                Event::Start(element) if is_text_run(element.name().as_ref()) => {
1527                    let name = local_name(element.name().as_ref()).to_vec();
1528                    let raw = capture_element(reader, &element)?;
1529                    paragraph.runs.push(TextRun::from_xml(&raw, &name)?);
1530                    boundary = boundary.max(1 + paragraph.runs.len());
1531                }
1532                Event::Empty(element) if is_text_run(element.name().as_ref()) => {
1533                    let name = local_name(element.name().as_ref()).to_vec();
1534                    let raw = capture_empty_element(&element)?;
1535                    paragraph.runs.push(TextRun::from_xml(&raw, &name)?);
1536                    boundary = boundary.max(1 + paragraph.runs.len());
1537                }
1538                Event::Start(element)
1539                    if matches_local_name(element.name().as_ref(), b"endParaRPr") =>
1540                {
1541                    if paragraph.end_properties.is_some() {
1542                        return Err(duplicate("endParaRPr"));
1543                    }
1544                    paragraph.end_properties =
1545                        Some(CT_TextCharacterProperties::from_element(reader, &element)?);
1546                    boundary = boundary.max(2 + paragraph.runs.len());
1547                }
1548                Event::Empty(element)
1549                    if matches_local_name(element.name().as_ref(), b"endParaRPr") =>
1550                {
1551                    if paragraph.end_properties.is_some() {
1552                        return Err(duplicate("endParaRPr"));
1553                    }
1554                    paragraph.end_properties =
1555                        Some(CT_TextCharacterProperties::from_start(&element)?);
1556                    boundary = boundary.max(2 + paragraph.runs.len());
1557                }
1558                Event::Start(element) => paragraph
1559                    .raw_children
1560                    .push(boundary, capture_element(reader, &element)?),
1561                Event::Empty(element) => paragraph
1562                    .raw_children
1563                    .push(boundary, capture_empty_element(&element)?),
1564                Event::End(element) if matches_local_name(element.name().as_ref(), b"p") => {
1565                    return Ok(paragraph);
1566                }
1567                Event::Eof => return Err(missing_end("p")),
1568                _ => {}
1569            }
1570            buffer.clear();
1571        }
1572    }
1573
1574    /// Writes a paragraph with fixed prefixes and schema child order.
1575    pub fn write_xml<W: Write>(&self, writer: &mut Writer<W>) -> Result<()> {
1576        if self.properties.is_none()
1577            && self.runs.is_empty()
1578            && self.end_properties.is_none()
1579            && self.raw_children.is_empty()
1580        {
1581            return write_empty(writer, BytesStart::new("a:p"));
1582        }
1583        write_start(writer, BytesStart::new("a:p"))?;
1584        emit_raw(writer, self.raw_children.at(0))?;
1585        if let Some(properties) = &self.properties {
1586            properties.write_xml(writer, "a:pPr")?;
1587        }
1588        emit_raw(writer, self.raw_children.at(1))?;
1589        for (index, run) in self.runs.iter().enumerate() {
1590            run.write_xml(writer)?;
1591            emit_raw(writer, self.raw_children.at(2 + index))?;
1592        }
1593        if let Some(properties) = &self.end_properties {
1594            properties.write_xml(writer, "a:endParaRPr")?;
1595        }
1596        emit_raw(writer, self.raw_children.at(2 + self.runs.len()))?;
1597        write_end(writer, "a:p")
1598    }
1599
1600    /// Serialises a complete paragraph fragment.
1601    pub fn to_xml(&self) -> Result<Vec<u8>> {
1602        let mut writer = Writer::new(Vec::new());
1603        self.write_xml(&mut writer)?;
1604        Ok(writer.into_inner())
1605    }
1606
1607    pub fn raw_children(&self) -> &OrderedRawChildren {
1608        &self.raw_children
1609    }
1610}
1611
1612fn is_text_run(name: &[u8]) -> bool {
1613    matches!(local_name(name), b"r" | b"br" | b"fld")
1614}
1615
1616fn parse_complete<T>(
1617    xml: &[u8],
1618    expected: &[u8],
1619    parse_start: impl FnOnce(&mut Reader<&[u8]>, &BytesStart<'_>) -> Result<T>,
1620    parse_empty: impl FnOnce(&BytesStart<'_>) -> Result<T>,
1621) -> Result<T> {
1622    let mut reader = Reader::from_reader(xml);
1623    let mut buffer = Vec::new();
1624    loop {
1625        match reader
1626            .read_event_into(&mut buffer)
1627            .map_err(OxmlError::from)?
1628        {
1629            Event::Start(element) if matches_local_name(element.name().as_ref(), expected) => {
1630                reject_conflicting_a_prefix(&element)?;
1631                return parse_start(&mut reader, &element);
1632            }
1633            Event::Empty(element) if matches_local_name(element.name().as_ref(), expected) => {
1634                reject_conflicting_a_prefix(&element)?;
1635                return parse_empty(&element);
1636            }
1637            Event::Start(element) | Event::Empty(element) => return Err(unexpected(&element)),
1638            Event::Eof => {
1639                return Err(TextError::Xml(OxmlError::MissingElement(
1640                    String::from_utf8_lossy(expected).into_owned(),
1641                )));
1642            }
1643            _ => {}
1644        }
1645        buffer.clear();
1646    }
1647}
1648
1649fn root_local_name(xml: &[u8]) -> Result<Vec<u8>> {
1650    let mut reader = Reader::from_reader(xml);
1651    let mut buffer = Vec::new();
1652    loop {
1653        match reader
1654            .read_event_into(&mut buffer)
1655            .map_err(OxmlError::from)?
1656        {
1657            Event::Start(element) | Event::Empty(element) => {
1658                return Ok(local_name(element.name().as_ref()).to_vec());
1659            }
1660            Event::Eof => {
1661                return Err(TextError::Xml(OxmlError::MissingElement(
1662                    "DrawingML text properties".to_owned(),
1663                )));
1664            }
1665            _ => {}
1666        }
1667        buffer.clear();
1668    }
1669}
1670
1671fn ensure_empty(reader: &mut Reader<&[u8]>, expected: &[u8]) -> Result<()> {
1672    let mut buffer = Vec::new();
1673    loop {
1674        match reader
1675            .read_event_into(&mut buffer)
1676            .map_err(OxmlError::from)?
1677        {
1678            Event::End(element) if matches_local_name(element.name().as_ref(), expected) => {
1679                return Ok(());
1680            }
1681            Event::Text(text) if text.iter().all(u8::is_ascii_whitespace) => {}
1682            Event::Comment(_) => {}
1683            Event::Start(element) | Event::Empty(element) => return Err(unexpected(&element)),
1684            Event::Eof => return Err(missing_end(&String::from_utf8_lossy(expected))),
1685            _ => {
1686                return Err(TextError::UnexpectedElement(
1687                    String::from_utf8_lossy(expected).into_owned(),
1688                ));
1689            }
1690        }
1691        buffer.clear();
1692    }
1693}
1694
1695fn text_attr(start: &BytesStart<'_>, name: &[u8]) -> Result<Option<String>> {
1696    for attribute in start.attributes() {
1697        let attribute = attribute.map_err(OxmlError::from)?;
1698        if attribute.key.as_ref() == name {
1699            let value = attribute
1700                .decoded_and_normalized_value(XmlVersion::Implicit1_0, start.decoder())
1701                .map_err(OxmlError::from)?;
1702            return Ok(Some(value.into_owned()));
1703        }
1704    }
1705    Ok(None)
1706}
1707
1708fn capture_raw_attributes(
1709    start: &BytesStart<'_>,
1710    modelled: &[&[u8]],
1711) -> Result<Vec<(String, String)>> {
1712    let mut raw = Vec::new();
1713    for attribute in start.attributes() {
1714        let attribute = attribute.map_err(OxmlError::from)?;
1715        if modelled.iter().any(|name| attribute.key.as_ref() == *name) {
1716            continue;
1717        }
1718        let name = std::str::from_utf8(attribute.key.as_ref())
1719            .map_err(OxmlError::from)?
1720            .to_owned();
1721        let value = attribute
1722            .decoded_and_normalized_value(XmlVersion::Implicit1_0, start.decoder())
1723            .map_err(OxmlError::from)?
1724            .into_owned();
1725        raw.push((name, value));
1726    }
1727    Ok(raw)
1728}
1729
1730fn push_raw_attributes(start: &mut BytesStart<'_>, attributes: &[(String, String)]) {
1731    for (name, value) in attributes {
1732        start.push_attribute((name.as_str(), value.as_str()));
1733    }
1734}
1735
1736fn required_attr(start: &BytesStart<'_>, name: &[u8]) -> Result<String> {
1737    text_attr(start, name)?
1738        .filter(|value| !value.is_empty())
1739        .ok_or_else(|| {
1740            missing_attribute(
1741                &String::from_utf8_lossy(local_name(start.name().as_ref())),
1742                &String::from_utf8_lossy(name),
1743            )
1744        })
1745}
1746
1747fn parse_optional_i32(
1748    start: &BytesStart<'_>,
1749    name: &[u8],
1750    min: i32,
1751    max: i32,
1752) -> Result<Option<i32>> {
1753    text_attr(start, name)?
1754        .map(|value| {
1755            parse_i32_value(
1756                &String::from_utf8_lossy(local_name(start.name().as_ref())),
1757                &String::from_utf8_lossy(name),
1758                &value,
1759                min,
1760                max,
1761            )
1762        })
1763        .transpose()
1764}
1765
1766fn parse_i32_attr(start: &BytesStart<'_>, name: &[u8], min: i32, max: i32) -> Result<i32> {
1767    let value = required_attr(start, name)?;
1768    parse_i32_value(
1769        &String::from_utf8_lossy(local_name(start.name().as_ref())),
1770        &String::from_utf8_lossy(name),
1771        &value,
1772        min,
1773        max,
1774    )
1775}
1776
1777fn parse_i32_value(element: &str, attribute: &str, value: &str, min: i32, max: i32) -> Result<i32> {
1778    let parsed = value
1779        .parse::<i32>()
1780        .map_err(|_| invalid_attribute(element, attribute, value))?;
1781    if (min..=max).contains(&parsed) {
1782        Ok(parsed)
1783    } else {
1784        Err(invalid_attribute(element, attribute, value))
1785    }
1786}
1787
1788fn parse_optional_bool(start: &BytesStart<'_>, name: &[u8]) -> Result<Option<bool>> {
1789    text_attr(start, name)?
1790        .map(|value| match value.as_str() {
1791            "true" | "1" => Ok(true),
1792            "false" | "0" => Ok(false),
1793            _ => Err(invalid_attribute(
1794                &String::from_utf8_lossy(local_name(start.name().as_ref())),
1795                &String::from_utf8_lossy(name),
1796                &value,
1797            )),
1798        })
1799        .transpose()
1800}
1801
1802fn parse_optional_enum<T>(
1803    start: &BytesStart<'_>,
1804    name: &[u8],
1805    parse: impl FnOnce(&str) -> Option<T>,
1806) -> Result<Option<T>> {
1807    let Some(value) = text_attr(start, name)? else {
1808        return Ok(None);
1809    };
1810    parse(&value).map(Some).ok_or_else(|| {
1811        invalid_attribute(
1812            &String::from_utf8_lossy(local_name(start.name().as_ref())),
1813            &String::from_utf8_lossy(name),
1814            &value,
1815        )
1816    })
1817}
1818
1819fn validate_spacing_percent(value: &str) -> Result<()> {
1820    if is_percentage_string(value) {
1821        return Ok(());
1822    }
1823    parse_i32_value("spcPct", "val", value, 0, MAX_TRANSITIONAL_SPACING_PERCENT)?;
1824    Ok(())
1825}
1826
1827fn validate_baseline(value: &str) -> Result<()> {
1828    if is_percentage_string(value) {
1829        return Ok(());
1830    }
1831    parse_i32_value("rPr", "baseline", value, -100_000, 100_000)?;
1832    Ok(())
1833}
1834
1835fn is_percentage_string(value: &str) -> bool {
1836    value.strip_suffix('%').is_some_and(is_signed_decimal)
1837}
1838
1839fn is_universal_measure(value: &str) -> bool {
1840    ["mm", "cm", "in", "pt", "pc", "pi"]
1841        .iter()
1842        .find_map(|unit| value.strip_suffix(unit))
1843        .is_some_and(is_signed_decimal)
1844}
1845
1846fn is_signed_decimal(value: &str) -> bool {
1847    let unsigned = value.strip_prefix('-').unwrap_or(value);
1848    let mut parts = unsigned.split('.');
1849    let Some(integer) = parts.next() else {
1850        return false;
1851    };
1852    if integer.is_empty() || !integer.bytes().all(|byte| byte.is_ascii_digit()) {
1853        return false;
1854    }
1855    if let Some(fraction) = parts.next()
1856        && (fraction.is_empty() || !fraction.bytes().all(|byte| byte.is_ascii_digit()))
1857    {
1858        return false;
1859    }
1860    parts.next().is_none()
1861}
1862
1863fn is_guid(value: &str) -> bool {
1864    if value.len() != 38 || !value.starts_with('{') || !value.ends_with('}') {
1865        return false;
1866    }
1867    value.bytes().enumerate().all(|(index, byte)| match index {
1868        0 => byte == b'{',
1869        37 => byte == b'}',
1870        9 | 14 | 19 | 24 => byte == b'-',
1871        _ => byte.is_ascii_hexdigit(),
1872    })
1873}
1874
1875fn push_optional_attr<'a>(start: &mut BytesStart<'a>, name: &'a str, value: Option<&'a str>) {
1876    if let Some(value) = value {
1877        start.push_attribute((name, value));
1878    }
1879}
1880
1881fn push_optional_bool(start: &mut BytesStart<'_>, name: &'static str, value: Option<bool>) {
1882    if let Some(value) = value {
1883        start.push_attribute((name, if value { "1" } else { "0" }));
1884    }
1885}
1886
1887fn push_owned_attr(start: &mut BytesStart<'_>, name: &'static str, value: String) {
1888    start.push_attribute((name, value.as_str()));
1889}
1890
1891fn emit_raw<'a, W: Write>(
1892    writer: &mut Writer<W>,
1893    children: impl Iterator<Item = &'a [u8]>,
1894) -> Result<()> {
1895    for child in children {
1896        writer.get_mut().write_all(child).map_err(OxmlError::from)?;
1897    }
1898    Ok(())
1899}
1900
1901fn write_start<W: Write>(writer: &mut Writer<W>, start: BytesStart<'_>) -> Result<()> {
1902    writer
1903        .write_event(Event::Start(start))
1904        .map_err(OxmlError::from)?;
1905    Ok(())
1906}
1907
1908fn write_empty<W: Write>(writer: &mut Writer<W>, start: BytesStart<'_>) -> Result<()> {
1909    writer
1910        .write_event(Event::Empty(start))
1911        .map_err(OxmlError::from)?;
1912    Ok(())
1913}
1914
1915fn write_end<W: Write>(writer: &mut Writer<W>, tag: &str) -> Result<()> {
1916    writer
1917        .write_event(Event::End(BytesEnd::new(tag)))
1918        .map_err(OxmlError::from)?;
1919    Ok(())
1920}
1921
1922fn unexpected(element: &BytesStart<'_>) -> TextError {
1923    TextError::UnexpectedElement(String::from_utf8_lossy(element.name().as_ref()).into_owned())
1924}
1925
1926fn duplicate(element: &str) -> TextError {
1927    TextError::DuplicateElement(element.to_owned())
1928}
1929
1930fn missing_attribute(element: &str, attribute: &str) -> TextError {
1931    TextError::MissingAttribute {
1932        element: element.to_owned(),
1933        attribute: attribute.to_owned(),
1934    }
1935}
1936
1937fn invalid_attribute(element: &str, attribute: &str, value: &str) -> TextError {
1938    TextError::InvalidAttribute {
1939        element: element.to_owned(),
1940        attribute: attribute.to_owned(),
1941        value: value.to_owned(),
1942    }
1943}
1944
1945#[cfg(test)]
1946mod tests {
1947    use std::panic;
1948
1949    use super::{
1950        CT_TextCharacterProperties, CT_TextParagraph, CT_TextParagraphProperties, TextRun,
1951        TextSpace, TextSpacing,
1952    };
1953    use crate::color::ColorChoice;
1954    use crate::text::CT_TextBody;
1955    use crate::text::{TextAutoNumberScheme, TextBulletChoice, TextBulletSizeValue};
1956
1957    #[test]
1958    fn drawingml_rtl_attribute_becomes_typed_without_reordering_unknown_content() {
1959        let parsed = CT_TextParagraphProperties::from_xml(
1960            br#"<q:pPr xmlns:q="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:x="urn:test" defTabSz="457200" rtl="1" x:keep="yes"/>"#,
1961        )
1962        .unwrap();
1963
1964        assert_eq!(parsed.right_to_left, Some(true));
1965        let serialized = String::from_utf8(parsed.to_xml().unwrap()).unwrap();
1966        assert!(serialized.contains("rtl=\"1\""));
1967        assert!(serialized.contains("x:keep=\"yes\""));
1968        assert!(
1969            serialized.find("defTabSz").unwrap() < serialized.find("rtl").unwrap(),
1970            "{serialized}"
1971        );
1972
1973        let mut changed = parsed;
1974        changed.right_to_left = Some(false);
1975        let serialized = String::from_utf8(changed.to_xml().unwrap()).unwrap();
1976        assert!(serialized.contains("rtl=\"0\""), "{serialized}");
1977        assert!(!serialized.contains("rtl=\"1\""), "{serialized}");
1978    }
1979
1980    #[test]
1981    fn leading_and_trailing_text_whitespace_survives_via_xml_space_preserve() {
1982        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>"#;
1983        let paragraph = CT_TextParagraph::from_xml(xml).unwrap();
1984        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>"#);
1985
1986        let TextRun::Run(first) = &paragraph.runs[0] else {
1987            panic!("expected regular run");
1988        };
1989        assert_eq!(first.text.value, " leading");
1990        assert_eq!(first.text.space, TextSpace::Preserve);
1991
1992        let body = CT_TextBody::from_xml(
1993            br#"<q:txBody><q:bodyPr/><q:p><q:r><q:t> body </q:t></q:r></q:p></q:txBody>"#,
1994        )
1995        .unwrap();
1996        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>"#);
1997
1998        let hostile = CT_TextParagraph::from_xml(
1999            br#"<q:p><q:r><q:t x:space="preserve">plain</q:t></q:r></q:p>"#,
2000        )
2001        .unwrap();
2002        let TextRun::Run(run) = &hostile.runs[0] else {
2003            panic!("expected regular run");
2004        };
2005        assert_eq!(run.text.space, TextSpace::Default);
2006        assert_eq!(
2007            hostile.to_xml().unwrap(),
2008            br#"<a:p><a:r><a:t x:space="preserve">plain</a:t></a:r></a:p>"#
2009        );
2010    }
2011
2012    #[test]
2013    fn canonical_text_state_matches_whitespace_and_empty_hyperlinks_from_real_decks() {
2014        let paragraph = CT_TextParagraph::from_xml(
2015            br#"<q:p><q:r><q:rPr><q:hlinkClick r:id=""/></q:rPr><q:t>trailing </q:t></q:r></q:p>"#,
2016        )
2017        .unwrap();
2018        let TextRun::Run(run) = &paragraph.runs[0] else {
2019            panic!("expected regular run");
2020        };
2021        assert_eq!(run.text.space, TextSpace::Preserve);
2022        assert_eq!(
2023            run.properties
2024                .as_ref()
2025                .and_then(|properties| properties.hyperlink_click.as_ref())
2026                .and_then(|hyperlink| hyperlink.relationship_id.as_deref()),
2027            Some("")
2028        );
2029        let written = paragraph.to_xml().unwrap();
2030        assert_eq!(CT_TextParagraph::from_xml(&written).unwrap(), paragraph);
2031    }
2032
2033    #[test]
2034    fn paragraph_runs_fields_and_breaks_round_trip_structurally() {
2035        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>"#;
2036        let paragraph = CT_TextParagraph::from_xml(xml).unwrap();
2037        assert_eq!(paragraph.runs.len(), 3);
2038        let written = paragraph.to_xml().unwrap();
2039        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>"#);
2040        assert_eq!(CT_TextParagraph::from_xml(&written).unwrap(), paragraph);
2041
2042        let hostile = CT_TextCharacterProperties::from_xml(
2043            br#"<q:rPr><q:hlinkClick x:id="not-a-relationship"/></q:rPr>"#,
2044        )
2045        .unwrap();
2046        assert!(
2047            hostile
2048                .hyperlink_click
2049                .as_ref()
2050                .unwrap()
2051                .relationship_id
2052                .is_none()
2053        );
2054        let mut writer = quick_xml::Writer::new(Vec::new());
2055        hostile.write_xml(&mut writer, "a:rPr").unwrap();
2056        assert_eq!(
2057            writer.into_inner(),
2058            br#"<a:rPr><a:hlinkClick x:id="not-a-relationship"/></a:rPr>"#
2059        );
2060    }
2061
2062    #[test]
2063    fn character_all_caps_round_trips_while_small_caps_stays_unmodelled() {
2064        let all = CT_TextCharacterProperties::from_xml(
2065            br#"<q:rPr cap="all" sz="1800" x:producer="kept"/>"#,
2066        )
2067        .unwrap();
2068        assert_eq!(all.all_caps, Some(true));
2069        let mut writer = quick_xml::Writer::new(Vec::new());
2070        all.write_xml(&mut writer, "a:rPr").unwrap();
2071        let written = writer.into_inner();
2072        assert_eq!(
2073            written,
2074            br#"<a:rPr sz="1800" cap="all" x:producer="kept"/>"#
2075        );
2076        assert_eq!(CT_TextCharacterProperties::from_xml(&written).unwrap(), all);
2077
2078        let none = CT_TextCharacterProperties::from_xml(br#"<q:rPr cap="none"/>"#).unwrap();
2079        assert_eq!(none.all_caps, Some(false));
2080
2081        let small = CT_TextCharacterProperties::from_xml(br#"<q:rPr cap="small"/>"#).unwrap();
2082        assert_eq!(small.all_caps, None);
2083        let mut writer = quick_xml::Writer::new(Vec::new());
2084        small.write_xml(&mut writer, "a:rPr").unwrap();
2085        assert_eq!(writer.into_inner(), br#"<a:rPr cap="small"/>"#);
2086    }
2087
2088    #[test]
2089    fn paragraph_and_run_properties_use_drawingml_units_and_schema_order() {
2090        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();
2091        let written = {
2092            let mut writer = quick_xml::Writer::new(Vec::new());
2093            paragraph_properties
2094                .write_xml(&mut writer, "a:pPr")
2095                .unwrap();
2096            writer.into_inner()
2097        };
2098        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>"#);
2099
2100        let run_properties = CT_TextCharacterProperties::from_xml(
2101            br#"<q:rPr sz="100" spc="1.25pt" baseline="-100000"/>"#,
2102        )
2103        .unwrap();
2104        let mut writer = quick_xml::Writer::new(Vec::new());
2105        run_properties.write_xml(&mut writer, "a:rPr").unwrap();
2106        assert_eq!(
2107            writer.into_inner(),
2108            br#"<a:rPr sz="100" spc="1.25pt" baseline="-100000"/>"#
2109        );
2110
2111        let mut inserted_spacing =
2112            CT_TextParagraphProperties::from_xml(br#"<q:pPr><q:buChar char="*"/></q:pPr>"#)
2113                .unwrap();
2114        inserted_spacing.line_spacing = Some(TextSpacing::Percent("100000".to_owned()));
2115        let mut writer = quick_xml::Writer::new(Vec::new());
2116        inserted_spacing.write_xml(&mut writer, "a:pPr").unwrap();
2117        assert_eq!(
2118            writer.into_inner(),
2119            br#"<a:pPr><a:lnSpc><a:spcPct val="100000"/></a:lnSpc><a:buChar char="*"/></a:pPr>"#
2120        );
2121    }
2122
2123    #[test]
2124    fn every_modelled_bullet_form_round_trips_in_schema_order() {
2125        let cases: &[(&[u8], &[u8])] = &[
2126            (
2127                br#"<q:pPr><q:buClr><q:srgbClr val="AABBCC"/></q:buClr><q:buSzPct val="125000"/><q:buFont typeface="Wingdings"/><q:buChar char="*"/></q:pPr>"#,
2128                br#"<a:pPr><a:buClr><a:srgbClr val="AABBCC"/></a:buClr><a:buSzPct val="125000"/><a:buFont typeface="Wingdings"/><a:buChar char="*"/></a:pPr>"#,
2129            ),
2130            (
2131                br#"<q:pPr><q:buSzPts val="1800"/><q:buAutoNum type="arabicPeriod" startAt="3"/></q:pPr>"#,
2132                br#"<a:pPr><a:buSzPts val="1800"/><a:buAutoNum type="arabicPeriod" startAt="3"/></a:pPr>"#,
2133            ),
2134            (br#"<q:pPr><q:buNone/></q:pPr>"#, br#"<a:pPr><a:buNone/></a:pPr>"#),
2135        ];
2136
2137        for (xml, expected) in cases {
2138            let properties = CT_TextParagraphProperties::from_xml(xml).unwrap();
2139            let mut writer = quick_xml::Writer::new(Vec::new());
2140            properties.write_xml(&mut writer, "a:pPr").unwrap();
2141            let written = writer.into_inner();
2142            assert_eq!(written, *expected);
2143            assert_eq!(
2144                CT_TextParagraphProperties::from_xml(&written).unwrap(),
2145                properties
2146            );
2147        }
2148
2149        let character = CT_TextParagraphProperties::from_xml(cases[0].0).unwrap();
2150        let bullet = character.bullet.as_ref().unwrap();
2151        assert!(matches!(
2152            bullet.choice,
2153            Some(TextBulletChoice::Character(_))
2154        ));
2155        assert_eq!(bullet.font.as_ref().unwrap().typeface, "Wingdings");
2156        assert!(matches!(
2157            bullet.size.as_ref().unwrap().value,
2158            TextBulletSizeValue::Percent(ref value) if value == "125000"
2159        ));
2160        assert!(matches!(
2161            bullet.color.as_ref().unwrap().color,
2162            ColorChoice::Srgb { .. }
2163        ));
2164
2165        let automatic = CT_TextParagraphProperties::from_xml(cases[1].0).unwrap();
2166        let Some(TextBulletChoice::AutoNumber(numbering)) =
2167            automatic.bullet.as_ref().unwrap().choice.as_ref()
2168        else {
2169            panic!("expected automatic-number bullet");
2170        };
2171        assert_eq!(numbering.scheme, TextAutoNumberScheme::ArabicPeriod);
2172        assert_eq!(numbering.start_at, Some(3));
2173        assert!(matches!(
2174            automatic
2175                .bullet
2176                .as_ref()
2177                .unwrap()
2178                .size
2179                .as_ref()
2180                .unwrap()
2181                .value,
2182            TextBulletSizeValue::Points(1800)
2183        ));
2184
2185        let none = CT_TextParagraphProperties::from_xml(cases[2].0).unwrap();
2186        assert!(matches!(
2187            none.bullet.as_ref().unwrap().choice,
2188            Some(TextBulletChoice::None(_))
2189        ));
2190    }
2191
2192    #[test]
2193    fn bullet_font_size_and_colour_keep_their_schema_positions() {
2194        let properties = CT_TextParagraphProperties::from_xml(
2195            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>"#,
2196        )
2197        .unwrap();
2198        let mut writer = quick_xml::Writer::new(Vec::new());
2199        properties.write_xml(&mut writer, "a:pPr").unwrap();
2200        assert_eq!(
2201            writer.into_inner(),
2202            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>"#
2203        );
2204    }
2205
2206    #[test]
2207    fn unknown_bullet_children_round_trip_byte_for_byte() {
2208        let properties = CT_TextParagraphProperties::from_xml(
2209            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>"#,
2210        )
2211        .unwrap();
2212        let mut writer = quick_xml::Writer::new(Vec::new());
2213        properties.write_xml(&mut writer, "a:pPr").unwrap();
2214        assert_eq!(
2215            writer.into_inner(),
2216            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>"#
2217        );
2218    }
2219
2220    #[test]
2221    fn malformed_bullet_values_return_errors_without_panicking() {
2222        let cases: &[&[u8]] = &[
2223            br#"<q:pPr><q:buChar/></q:pPr>"#,
2224            br#"<q:pPr><q:buChar char=""/></q:pPr>"#,
2225            br#"<q:pPr><q:buSzPct val="24999"/></q:pPr>"#,
2226            br#"<q:pPr><q:buSzPct val="400001"/></q:pPr>"#,
2227            br#"<q:pPr><q:buSzPts val="99"/></q:pPr>"#,
2228            br#"<q:pPr><q:buSzPts val="400001"/></q:pPr>"#,
2229            br#"<q:pPr><q:buAutoNum type="unknown"/></q:pPr>"#,
2230            br#"<q:pPr><q:buAutoNum/></q:pPr>"#,
2231            br#"<q:pPr><q:buAutoNum type="arabicPeriod" startAt="0"/></q:pPr>"#,
2232            br#"<q:pPr><q:buAutoNum type="arabicPeriod" startAt="32768"/></q:pPr>"#,
2233            br#"<q:pPr><q:buFont/></q:pPr>"#,
2234            br#"<q:pPr><q:buClr/></q:pPr>"#,
2235            br#"<q:pPr><q:buClr><q:srgbClr x:val="AABBCC"/></q:buClr></q:pPr>"#,
2236            br#"<q:pPr><q:buClr><q:srgbClr val="GGGGGG"/></q:buClr></q:pPr>"#,
2237            br#"<q:pPr><q:buClr><q:srgbClr val="AABBCC"/><q:schemeClr val="accent1"/></q:buClr></q:pPr>"#,
2238            br#"<q:pPr><q:buChar char="*"/><q:buNone/></q:pPr>"#,
2239            br#"<q:pPr><q:buSzPct val="100000"/><q:buSzPts val="1200"/></q:pPr>"#,
2240        ];
2241
2242        for xml in cases {
2243            let result = panic::catch_unwind(|| CT_TextParagraphProperties::from_xml(xml));
2244            assert!(result.is_ok(), "bullet parser panicked");
2245            assert!(result.unwrap().is_err(), "malformed bullet parsed");
2246        }
2247    }
2248
2249    #[test]
2250    fn malformed_text_content_returns_errors_without_panicking() {
2251        let cases: &[&[u8]] = &[
2252            br#"<q:r/>"#,
2253            br#"<q:r><q:t><x:nested/></q:t></q:r>"#,
2254            br#"<q:r><q:t xml:space="keep">x</q:t></q:r>"#,
2255            br#"<q:r><q:t>x</q:t><q:t>y</q:t></q:r>"#,
2256            br#"<q:pPr lvl="9"/>"#,
2257            br#"<q:pPr marL="-1"/>"#,
2258            br#"<q:pPr indent="51206401"/>"#,
2259            br#"<q:pPr algn="middle"/>"#,
2260            br#"<q:pPr><q:lnSpc/></q:pPr>"#,
2261            br#"<q:pPr><q:lnSpc><q:spcPts val="158401"/></q:lnSpc></q:pPr>"#,
2262            br#"<q:pPr><q:lnSpc><q:spcPct val="201170"/></q:lnSpc></q:pPr>"#,
2263            br#"<q:rPr sz="99"/>"#,
2264            br#"<q:rPr b="yes"/>"#,
2265            br#"<q:rPr u="triple"/>"#,
2266            br#"<q:rPr spc="400001"/>"#,
2267            r#"<q:rPr spc="éx"/>"#.as_bytes(),
2268            br#"<q:rPr baseline="100001"/>"#,
2269            br#"<q:rPr><q:latin/></q:rPr>"#,
2270            br#"<q:fld type="slidenum"/>"#,
2271            br#"<q:fld id="not-a-guid"/>"#,
2272            br#"<q:p><q:endParaRPr/><q:endParaRPr/></q:p>"#,
2273            br#"<q:notPPr/>"#,
2274            br#"<q:notRPr/>"#,
2275        ];
2276        for xml in cases {
2277            let result = panic::catch_unwind(|| match super::root_local_name(xml).as_deref() {
2278                Ok(b"r") => super::CT_RegularTextRun::from_xml(xml).map(|_| ()),
2279                Ok(b"pPr") => CT_TextParagraphProperties::from_xml(xml).map(|_| ()),
2280                Ok(b"rPr") => CT_TextCharacterProperties::from_xml(xml).map(|_| ()),
2281                Ok(b"fld") => super::CT_TextField::from_xml(xml).map(|_| ()),
2282                Ok(b"p") => CT_TextParagraph::from_xml(xml).map(|_| ()),
2283                Ok(b"notPPr") => CT_TextParagraphProperties::from_xml(xml).map(|_| ()),
2284                Ok(b"notRPr") => CT_TextCharacterProperties::from_xml(xml).map(|_| ()),
2285                _ => unreachable!(),
2286            });
2287            assert!(result.is_ok(), "text parser panicked");
2288            assert!(result.unwrap().is_err(), "malformed text parsed");
2289        }
2290    }
2291}