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 line_spacing: Option<TextSpacing>,
844    pub space_before: Option<TextSpacing>,
845    pub space_after: Option<TextSpacing>,
846    pub bullet: Option<TextBullet>,
847    pub default_run_properties: Option<CT_TextCharacterProperties>,
848    raw_attributes: Vec<(String, String)>,
849    raw_children: OrderedRawChildren,
850}
851
852impl CT_TextParagraphProperties {
853    /// Parses one complete paragraph-property element with any prefix.
854    pub fn from_xml(xml: &[u8]) -> Result<Self> {
855        let expected = root_local_name(xml)?;
856        if !is_paragraph_property_tag(&expected) {
857            return Err(TextError::UnexpectedElement(
858                String::from_utf8_lossy(&expected).into_owned(),
859            ));
860        }
861        parse_complete(xml, &expected, Self::from_element, Self::from_start)
862    }
863
864    fn from_start(start: &BytesStart<'_>) -> Result<Self> {
865        reject_conflicting_a_prefix(start)?;
866        let element = String::from_utf8_lossy(local_name(start.name().as_ref())).into_owned();
867        let level = text_attr(start, b"lvl")?
868            .map(|value| parse_i32_value(&element, "lvl", &value, 0, 8).map(|v| v as u8))
869            .transpose()?;
870        Ok(Self {
871            left_margin: parse_optional_i32(start, b"marL", 0, MAX_TEXT_MARGIN)?,
872            right_margin: parse_optional_i32(start, b"marR", 0, MAX_TEXT_MARGIN)?,
873            level,
874            indent: parse_optional_i32(start, b"indent", -MAX_TEXT_MARGIN, MAX_TEXT_MARGIN)?,
875            alignment: parse_optional_enum(start, b"algn", TextAlignment::parse)?,
876            raw_attributes: capture_raw_attributes(
877                start,
878                &[b"marL", b"marR", b"lvl", b"indent", b"algn"],
879            )?,
880            ..Self::default()
881        })
882    }
883
884    fn from_element(reader: &mut Reader<&[u8]>, start: &BytesStart<'_>) -> Result<Self> {
885        let expected = local_name(start.name().as_ref()).to_vec();
886        let mut properties = Self::from_start(start)?;
887        let mut boundary = 0;
888        let mut seen = [false; 10];
889        let mut buffer = Vec::new();
890        loop {
891            match reader
892                .read_event_into(&mut buffer)
893                .map_err(OxmlError::from)?
894            {
895                Event::Start(element) => {
896                    let name = local_name(element.name().as_ref()).to_vec();
897                    let raw = capture_element(reader, &element)?;
898                    properties.capture_child(&name, raw, &mut boundary, &mut seen)?;
899                }
900                Event::Empty(element) => {
901                    let name = local_name(element.name().as_ref()).to_vec();
902                    let raw = capture_empty_element(&element)?;
903                    properties.capture_child(&name, raw, &mut boundary, &mut seen)?;
904                }
905                Event::End(element) if matches_local_name(element.name().as_ref(), &expected) => {
906                    return Ok(properties);
907                }
908                Event::Eof => return Err(missing_end(&String::from_utf8_lossy(&expected))),
909                _ => {}
910            }
911            buffer.clear();
912        }
913    }
914
915    fn capture_child(
916        &mut self,
917        name: &[u8],
918        raw: Vec<u8>,
919        boundary: &mut usize,
920        seen: &mut [bool; 10],
921    ) -> Result<()> {
922        let slot = paragraph_property_slot(name);
923        if let Some(slot) = slot {
924            if seen[slot - 1] {
925                return Err(duplicate(&String::from_utf8_lossy(name)));
926            }
927            seen[slot - 1] = true;
928        }
929        match name {
930            b"lnSpc" => self.line_spacing = Some(TextSpacing::from_xml(&raw, b"lnSpc")?),
931            b"spcBef" => self.space_before = Some(TextSpacing::from_xml(&raw, b"spcBef")?),
932            b"spcAft" => self.space_after = Some(TextSpacing::from_xml(&raw, b"spcAft")?),
933            b"defRPr" => {
934                self.default_run_properties = Some(CT_TextCharacterProperties::from_xml(&raw)?)
935            }
936            _ => {
937                let mut bullet = self.bullet.take().unwrap_or_default();
938                if bullet.capture_component(name, &raw)? {
939                    self.bullet = Some(bullet);
940                } else {
941                    if !bullet.is_empty() {
942                        self.bullet = Some(bullet);
943                    }
944                    self.raw_children.push(
945                        slot.map_or(*boundary, |slot| (*boundary).max(slot - 1)),
946                        raw,
947                    );
948                }
949            }
950        }
951        if let Some(slot) = slot {
952            *boundary = (*boundary).max(slot);
953        }
954        Ok(())
955    }
956
957    /// Writes paragraph properties with a caller-selected fixed `a:` tag.
958    pub fn write_xml<W: Write>(&self, writer: &mut Writer<W>, tag: &str) -> Result<()> {
959        let mut start = BytesStart::new(tag);
960        if let Some(value) = self.left_margin {
961            parse_i32_value(tag, "marL", &value.to_string(), 0, MAX_TEXT_MARGIN)?;
962            push_owned_attr(&mut start, "marL", value.to_string());
963        }
964        if let Some(value) = self.right_margin {
965            parse_i32_value(tag, "marR", &value.to_string(), 0, MAX_TEXT_MARGIN)?;
966            push_owned_attr(&mut start, "marR", value.to_string());
967        }
968        if let Some(value) = self.level {
969            if value > 8 {
970                return Err(invalid_attribute(tag, "lvl", &value.to_string()));
971            }
972            push_owned_attr(&mut start, "lvl", value.to_string());
973        }
974        if let Some(value) = self.indent {
975            parse_i32_value(
976                tag,
977                "indent",
978                &value.to_string(),
979                -MAX_TEXT_MARGIN,
980                MAX_TEXT_MARGIN,
981            )?;
982            push_owned_attr(&mut start, "indent", value.to_string());
983        }
984        if let Some(value) = self.alignment {
985            start.push_attribute(("algn", value.as_str()));
986        }
987        push_raw_attributes(&mut start, &self.raw_attributes);
988
989        let modelled = self.line_spacing.is_some()
990            || self.space_before.is_some()
991            || self.space_after.is_some()
992            || self.bullet.is_some()
993            || self.default_run_properties.is_some();
994        if !modelled && self.raw_children.is_empty() {
995            return write_empty(writer, start);
996        }
997        write_start(writer, start)?;
998        emit_raw(writer, self.raw_children.at(0))?;
999        if let Some(spacing) = &self.line_spacing {
1000            spacing.write_xml(writer, "a:lnSpc")?;
1001        }
1002        emit_raw(writer, self.raw_children.at(1))?;
1003        if let Some(spacing) = &self.space_before {
1004            spacing.write_xml(writer, "a:spcBef")?;
1005        }
1006        emit_raw(writer, self.raw_children.at(2))?;
1007        if let Some(spacing) = &self.space_after {
1008            spacing.write_xml(writer, "a:spcAft")?;
1009        }
1010        emit_raw(writer, self.raw_children.at(3))?;
1011        if let Some(bullet) = &self.bullet {
1012            bullet.write_color(writer)?;
1013        }
1014        emit_raw(writer, self.raw_children.at(4))?;
1015        if let Some(bullet) = &self.bullet {
1016            bullet.write_size(writer)?;
1017        }
1018        emit_raw(writer, self.raw_children.at(5))?;
1019        if let Some(bullet) = &self.bullet {
1020            bullet.write_font(writer)?;
1021        }
1022        emit_raw(writer, self.raw_children.at(6))?;
1023        if let Some(bullet) = &self.bullet {
1024            bullet.write_choice(writer)?;
1025        }
1026        emit_raw(writer, self.raw_children.at(7))?;
1027        emit_raw(writer, self.raw_children.at(8))?;
1028        if let Some(properties) = &self.default_run_properties {
1029            properties.write_xml(writer, "a:defRPr")?;
1030        }
1031        emit_raw(writer, self.raw_children.at(9))?;
1032        emit_raw(writer, self.raw_children.at(10))?;
1033        write_end(writer, tag)
1034    }
1035
1036    pub fn raw_children(&self) -> &OrderedRawChildren {
1037        &self.raw_children
1038    }
1039}
1040
1041fn paragraph_property_slot(name: &[u8]) -> Option<usize> {
1042    match name {
1043        b"lnSpc" => Some(1),
1044        b"spcBef" => Some(2),
1045        b"spcAft" => Some(3),
1046        b"buClrTx" | b"buClr" => Some(4),
1047        b"buSzTx" | b"buSzPct" | b"buSzPts" => Some(5),
1048        b"buFontTx" | b"buFont" => Some(6),
1049        b"buNone" | b"buAutoNum" | b"buChar" | b"buBlip" => Some(7),
1050        b"tabLst" => Some(8),
1051        b"defRPr" => Some(9),
1052        b"extLst" => Some(10),
1053        _ => None,
1054    }
1055}
1056
1057fn is_paragraph_property_tag(name: &[u8]) -> bool {
1058    matches!(
1059        name,
1060        b"pPr"
1061            | b"defPPr"
1062            | b"lvl1pPr"
1063            | b"lvl2pPr"
1064            | b"lvl3pPr"
1065            | b"lvl4pPr"
1066            | b"lvl5pPr"
1067            | b"lvl6pPr"
1068            | b"lvl7pPr"
1069            | b"lvl8pPr"
1070            | b"lvl9pPr"
1071    )
1072}
1073
1074/// A regular DrawingML run containing optional properties and required text.
1075#[allow(non_camel_case_types)]
1076#[derive(Clone, Debug, Eq, PartialEq)]
1077pub struct CT_RegularTextRun {
1078    pub properties: Option<CT_TextCharacterProperties>,
1079    pub text: TextValue,
1080    raw_children: OrderedRawChildren,
1081}
1082
1083impl CT_RegularTextRun {
1084    /// Creates a regular text run without direct character formatting.
1085    pub fn new(text: impl Into<String>) -> Self {
1086        Self {
1087            properties: None,
1088            text: TextValue {
1089                value: text.into(),
1090                ..TextValue::default()
1091            },
1092            raw_children: OrderedRawChildren::default(),
1093        }
1094    }
1095
1096    /// Replaces the run text while retaining properties and unmodelled XML.
1097    pub fn set_text(&mut self, text: &str) {
1098        self.text.value = text.to_owned();
1099    }
1100
1101    /// Parses one complete `a:r` element with any prefix.
1102    pub fn from_xml(xml: &[u8]) -> Result<Self> {
1103        parse_complete(xml, b"r", Self::from_element, |_| {
1104            Err(TextError::UnexpectedElement("r".to_owned()))
1105        })
1106    }
1107
1108    fn from_element(reader: &mut Reader<&[u8]>, _start: &BytesStart<'_>) -> Result<Self> {
1109        let mut properties = None;
1110        let mut text = None;
1111        let mut raw_children = OrderedRawChildren::default();
1112        let mut boundary = 0;
1113        let mut buffer = Vec::new();
1114        loop {
1115            match reader
1116                .read_event_into(&mut buffer)
1117                .map_err(OxmlError::from)?
1118            {
1119                Event::Start(element) if matches_local_name(element.name().as_ref(), b"rPr") => {
1120                    if properties.is_some() {
1121                        return Err(duplicate("rPr"));
1122                    }
1123                    properties = Some(CT_TextCharacterProperties::from_element(reader, &element)?);
1124                    boundary = boundary.max(1);
1125                }
1126                Event::Empty(element) if matches_local_name(element.name().as_ref(), b"rPr") => {
1127                    if properties.is_some() {
1128                        return Err(duplicate("rPr"));
1129                    }
1130                    properties = Some(CT_TextCharacterProperties::from_start(&element)?);
1131                    boundary = boundary.max(1);
1132                }
1133                Event::Start(element) if matches_local_name(element.name().as_ref(), b"t") => {
1134                    if text.is_some() {
1135                        return Err(duplicate("t"));
1136                    }
1137                    text = Some(TextValue::from_element(reader, &element)?);
1138                    boundary = boundary.max(2);
1139                }
1140                Event::Empty(element) if matches_local_name(element.name().as_ref(), b"t") => {
1141                    if text.is_some() {
1142                        return Err(duplicate("t"));
1143                    }
1144                    text = Some(TextValue::from_start(&element)?);
1145                    boundary = boundary.max(2);
1146                }
1147                Event::Start(element) => {
1148                    raw_children.push(boundary, capture_element(reader, &element)?)
1149                }
1150                Event::Empty(element) => {
1151                    raw_children.push(boundary, capture_empty_element(&element)?)
1152                }
1153                Event::End(element) if matches_local_name(element.name().as_ref(), b"r") => break,
1154                Event::Eof => return Err(missing_end("r")),
1155                _ => {}
1156            }
1157            buffer.clear();
1158        }
1159        Ok(Self {
1160            properties,
1161            text: text.ok_or_else(|| {
1162                TextError::Xml(OxmlError::MissingElement("DrawingML run text".to_owned()))
1163            })?,
1164            raw_children,
1165        })
1166    }
1167
1168    fn write_xml<W: Write>(&self, writer: &mut Writer<W>) -> Result<()> {
1169        write_start(writer, BytesStart::new("a:r"))?;
1170        emit_raw(writer, self.raw_children.at(0))?;
1171        if let Some(properties) = &self.properties {
1172            properties.write_xml(writer, "a:rPr")?;
1173        }
1174        emit_raw(writer, self.raw_children.at(1))?;
1175        self.text.write_xml(writer)?;
1176        emit_raw(writer, self.raw_children.at(2))?;
1177        write_end(writer, "a:r")
1178    }
1179}
1180
1181/// A DrawingML explicit line break and its optional run properties.
1182#[allow(non_camel_case_types)]
1183#[derive(Clone, Debug, Default, Eq, PartialEq)]
1184pub struct CT_TextLineBreak {
1185    pub properties: Option<CT_TextCharacterProperties>,
1186    raw_children: OrderedRawChildren,
1187}
1188
1189impl CT_TextLineBreak {
1190    /// Parses one complete `a:br` element with any prefix.
1191    pub fn from_xml(xml: &[u8]) -> Result<Self> {
1192        parse_complete(xml, b"br", Self::from_element, |_| Ok(Self::default()))
1193    }
1194
1195    fn from_element(reader: &mut Reader<&[u8]>, _start: &BytesStart<'_>) -> Result<Self> {
1196        let mut line_break = Self::default();
1197        let mut boundary = 0;
1198        let mut buffer = Vec::new();
1199        loop {
1200            match reader
1201                .read_event_into(&mut buffer)
1202                .map_err(OxmlError::from)?
1203            {
1204                Event::Start(element) if matches_local_name(element.name().as_ref(), b"rPr") => {
1205                    if line_break.properties.is_some() {
1206                        return Err(duplicate("rPr"));
1207                    }
1208                    line_break.properties =
1209                        Some(CT_TextCharacterProperties::from_element(reader, &element)?);
1210                    boundary = 1;
1211                }
1212                Event::Empty(element) if matches_local_name(element.name().as_ref(), b"rPr") => {
1213                    if line_break.properties.is_some() {
1214                        return Err(duplicate("rPr"));
1215                    }
1216                    line_break.properties = Some(CT_TextCharacterProperties::from_start(&element)?);
1217                    boundary = 1;
1218                }
1219                Event::Start(element) => line_break
1220                    .raw_children
1221                    .push(boundary, capture_element(reader, &element)?),
1222                Event::Empty(element) => line_break
1223                    .raw_children
1224                    .push(boundary, capture_empty_element(&element)?),
1225                Event::End(element) if matches_local_name(element.name().as_ref(), b"br") => {
1226                    return Ok(line_break);
1227                }
1228                Event::Eof => return Err(missing_end("br")),
1229                _ => {}
1230            }
1231            buffer.clear();
1232        }
1233    }
1234
1235    fn write_xml<W: Write>(&self, writer: &mut Writer<W>) -> Result<()> {
1236        if self.properties.is_none() && self.raw_children.is_empty() {
1237            return write_empty(writer, BytesStart::new("a:br"));
1238        }
1239        write_start(writer, BytesStart::new("a:br"))?;
1240        emit_raw(writer, self.raw_children.at(0))?;
1241        if let Some(properties) = &self.properties {
1242            properties.write_xml(writer, "a:rPr")?;
1243        }
1244        emit_raw(writer, self.raw_children.at(1))?;
1245        write_end(writer, "a:br")
1246    }
1247}
1248
1249/// A DrawingML field with required GUID, optional type, properties, and text.
1250#[allow(non_camel_case_types)]
1251#[derive(Clone, Debug, Eq, PartialEq)]
1252pub struct CT_TextField {
1253    pub id: String,
1254    pub field_type: Option<String>,
1255    pub run_properties: Option<CT_TextCharacterProperties>,
1256    pub paragraph_properties: Option<CT_TextParagraphProperties>,
1257    pub text: Option<TextValue>,
1258    raw_children: OrderedRawChildren,
1259}
1260
1261impl CT_TextField {
1262    /// Parses one complete `a:fld` element with any prefix.
1263    pub fn from_xml(xml: &[u8]) -> Result<Self> {
1264        parse_complete(xml, b"fld", Self::from_element, Self::from_empty)
1265    }
1266
1267    fn from_start(start: &BytesStart<'_>) -> Result<Self> {
1268        let id = required_attr(start, b"id")?;
1269        if !is_guid(&id) {
1270            return Err(invalid_attribute("fld", "id", &id));
1271        }
1272        Ok(Self {
1273            id,
1274            field_type: text_attr(start, b"type")?,
1275            run_properties: None,
1276            paragraph_properties: None,
1277            text: None,
1278            raw_children: OrderedRawChildren::default(),
1279        })
1280    }
1281
1282    fn from_empty(start: &BytesStart<'_>) -> Result<Self> {
1283        Self::from_start(start)
1284    }
1285
1286    fn from_element(reader: &mut Reader<&[u8]>, start: &BytesStart<'_>) -> Result<Self> {
1287        let mut field = Self::from_start(start)?;
1288        let mut boundary = 0;
1289        let mut buffer = Vec::new();
1290        loop {
1291            match reader
1292                .read_event_into(&mut buffer)
1293                .map_err(OxmlError::from)?
1294            {
1295                Event::Start(element) if matches_local_name(element.name().as_ref(), b"rPr") => {
1296                    if field.run_properties.is_some() {
1297                        return Err(duplicate("rPr"));
1298                    }
1299                    field.run_properties =
1300                        Some(CT_TextCharacterProperties::from_element(reader, &element)?);
1301                    boundary = boundary.max(1);
1302                }
1303                Event::Empty(element) if matches_local_name(element.name().as_ref(), b"rPr") => {
1304                    if field.run_properties.is_some() {
1305                        return Err(duplicate("rPr"));
1306                    }
1307                    field.run_properties = Some(CT_TextCharacterProperties::from_start(&element)?);
1308                    boundary = boundary.max(1);
1309                }
1310                Event::Start(element) if matches_local_name(element.name().as_ref(), b"pPr") => {
1311                    if field.paragraph_properties.is_some() {
1312                        return Err(duplicate("pPr"));
1313                    }
1314                    field.paragraph_properties =
1315                        Some(CT_TextParagraphProperties::from_element(reader, &element)?);
1316                    boundary = boundary.max(2);
1317                }
1318                Event::Empty(element) if matches_local_name(element.name().as_ref(), b"pPr") => {
1319                    if field.paragraph_properties.is_some() {
1320                        return Err(duplicate("pPr"));
1321                    }
1322                    field.paragraph_properties =
1323                        Some(CT_TextParagraphProperties::from_start(&element)?);
1324                    boundary = boundary.max(2);
1325                }
1326                Event::Start(element) if matches_local_name(element.name().as_ref(), b"t") => {
1327                    if field.text.is_some() {
1328                        return Err(duplicate("t"));
1329                    }
1330                    field.text = Some(TextValue::from_element(reader, &element)?);
1331                    boundary = boundary.max(3);
1332                }
1333                Event::Empty(element) if matches_local_name(element.name().as_ref(), b"t") => {
1334                    if field.text.is_some() {
1335                        return Err(duplicate("t"));
1336                    }
1337                    field.text = Some(TextValue::from_start(&element)?);
1338                    boundary = boundary.max(3);
1339                }
1340                Event::Start(element) => field
1341                    .raw_children
1342                    .push(boundary, capture_element(reader, &element)?),
1343                Event::Empty(element) => field
1344                    .raw_children
1345                    .push(boundary, capture_empty_element(&element)?),
1346                Event::End(element) if matches_local_name(element.name().as_ref(), b"fld") => {
1347                    return Ok(field);
1348                }
1349                Event::Eof => return Err(missing_end("fld")),
1350                _ => {}
1351            }
1352            buffer.clear();
1353        }
1354    }
1355
1356    fn write_xml<W: Write>(&self, writer: &mut Writer<W>) -> Result<()> {
1357        if !is_guid(&self.id) {
1358            return Err(invalid_attribute("fld", "id", &self.id));
1359        }
1360        let mut start = BytesStart::new("a:fld");
1361        start.push_attribute(("id", self.id.as_str()));
1362        push_optional_attr(&mut start, "type", self.field_type.as_deref());
1363        let modelled = self.run_properties.is_some()
1364            || self.paragraph_properties.is_some()
1365            || self.text.is_some();
1366        if !modelled && self.raw_children.is_empty() {
1367            return write_empty(writer, start);
1368        }
1369        write_start(writer, start)?;
1370        emit_raw(writer, self.raw_children.at(0))?;
1371        if let Some(properties) = &self.run_properties {
1372            properties.write_xml(writer, "a:rPr")?;
1373        }
1374        emit_raw(writer, self.raw_children.at(1))?;
1375        if let Some(properties) = &self.paragraph_properties {
1376            properties.write_xml(writer, "a:pPr")?;
1377        }
1378        emit_raw(writer, self.raw_children.at(2))?;
1379        if let Some(text) = &self.text {
1380            text.write_xml(writer)?;
1381        }
1382        emit_raw(writer, self.raw_children.at(3))?;
1383        write_end(writer, "a:fld")
1384    }
1385}
1386
1387/// One member of the paragraph text-content choice.
1388#[derive(Clone, Debug, Eq, PartialEq)]
1389pub enum TextRun {
1390    Run(CT_RegularTextRun),
1391    Break(CT_TextLineBreak),
1392    Field(Box<CT_TextField>),
1393}
1394
1395impl TextRun {
1396    fn from_xml(xml: &[u8], name: &[u8]) -> Result<Self> {
1397        match name {
1398            b"r" => Ok(Self::Run(CT_RegularTextRun::from_xml(xml)?)),
1399            b"br" => Ok(Self::Break(CT_TextLineBreak::from_xml(xml)?)),
1400            b"fld" => Ok(Self::Field(Box::new(CT_TextField::from_xml(xml)?))),
1401            _ => Err(TextError::UnexpectedElement(
1402                String::from_utf8_lossy(name).into_owned(),
1403            )),
1404        }
1405    }
1406
1407    fn write_xml<W: Write>(&self, writer: &mut Writer<W>) -> Result<()> {
1408        match self {
1409            Self::Run(run) => run.write_xml(writer),
1410            Self::Break(line_break) => line_break.write_xml(writer),
1411            Self::Field(field) => field.write_xml(writer),
1412        }
1413    }
1414}
1415
1416/// A DrawingML paragraph with ordered runs, breaks, and fields.
1417#[allow(non_camel_case_types)]
1418#[derive(Clone, Debug, Default, Eq, PartialEq)]
1419pub struct CT_TextParagraph {
1420    pub properties: Option<CT_TextParagraphProperties>,
1421    pub runs: Vec<TextRun>,
1422    pub end_properties: Option<CT_TextCharacterProperties>,
1423    raw_children: OrderedRawChildren,
1424}
1425
1426impl CT_TextParagraph {
1427    /// Returns direct properties, inserting them before preserved content.
1428    pub fn properties_mut(&mut self) -> &mut CT_TextParagraphProperties {
1429        if self.properties.is_none() {
1430            let mut raw_children = OrderedRawChildren::default();
1431            for boundary in 0..=2 + self.runs.len() {
1432                let new_boundary = if boundary == 0 { 1 } else { boundary };
1433                for child in self.raw_children.at(boundary) {
1434                    raw_children.push(new_boundary, child.to_vec());
1435                }
1436            }
1437            self.raw_children = raw_children;
1438            self.properties = Some(CT_TextParagraphProperties::default());
1439        }
1440        self.properties
1441            .as_mut()
1442            .expect("paragraph properties were inserted")
1443    }
1444
1445    /// Replaces ordered text choices with one regular run.
1446    pub fn set_text(&mut self, text: &str) {
1447        let old_run_count = self.runs.len();
1448        let mut raw_children = OrderedRawChildren::default();
1449        for boundary in 0..=2 + old_run_count {
1450            let new_boundary = if boundary <= 1 {
1451                boundary
1452            } else if boundary <= 1 + old_run_count {
1453                2
1454            } else {
1455                3
1456            };
1457            for child in self.raw_children.at(boundary) {
1458                raw_children.push(new_boundary, child.to_vec());
1459            }
1460        }
1461        self.raw_children = raw_children;
1462        self.runs = vec![TextRun::Run(CT_RegularTextRun::new(text))];
1463    }
1464
1465    /// Appends one regular run after the existing ordered text choices.
1466    pub fn add_run(&mut self, text: &str) -> &mut CT_RegularTextRun {
1467        self.raw_children.shift_boundaries_from(2 + self.runs.len());
1468        self.runs.push(TextRun::Run(CT_RegularTextRun::new(text)));
1469        let Some(TextRun::Run(run)) = self.runs.last_mut() else {
1470            unreachable!("the appended text choice is a regular run")
1471        };
1472        run
1473    }
1474
1475    /// Parses one complete `a:p` element with any prefix.
1476    pub fn from_xml(xml: &[u8]) -> Result<Self> {
1477        parse_complete(xml, b"p", Self::from_element, |_| Ok(Self::default()))
1478    }
1479
1480    fn from_element(reader: &mut Reader<&[u8]>, _start: &BytesStart<'_>) -> Result<Self> {
1481        let mut paragraph = Self::default();
1482        let mut boundary = 0;
1483        let mut buffer = Vec::new();
1484        loop {
1485            match reader
1486                .read_event_into(&mut buffer)
1487                .map_err(OxmlError::from)?
1488            {
1489                Event::Start(element) if matches_local_name(element.name().as_ref(), b"pPr") => {
1490                    if paragraph.properties.is_some() {
1491                        return Err(duplicate("pPr"));
1492                    }
1493                    paragraph.properties =
1494                        Some(CT_TextParagraphProperties::from_element(reader, &element)?);
1495                    boundary = boundary.max(1);
1496                }
1497                Event::Empty(element) if matches_local_name(element.name().as_ref(), b"pPr") => {
1498                    if paragraph.properties.is_some() {
1499                        return Err(duplicate("pPr"));
1500                    }
1501                    paragraph.properties = Some(CT_TextParagraphProperties::from_start(&element)?);
1502                    boundary = boundary.max(1);
1503                }
1504                Event::Start(element) if is_text_run(element.name().as_ref()) => {
1505                    let name = local_name(element.name().as_ref()).to_vec();
1506                    let raw = capture_element(reader, &element)?;
1507                    paragraph.runs.push(TextRun::from_xml(&raw, &name)?);
1508                    boundary = boundary.max(1 + paragraph.runs.len());
1509                }
1510                Event::Empty(element) if is_text_run(element.name().as_ref()) => {
1511                    let name = local_name(element.name().as_ref()).to_vec();
1512                    let raw = capture_empty_element(&element)?;
1513                    paragraph.runs.push(TextRun::from_xml(&raw, &name)?);
1514                    boundary = boundary.max(1 + paragraph.runs.len());
1515                }
1516                Event::Start(element)
1517                    if matches_local_name(element.name().as_ref(), b"endParaRPr") =>
1518                {
1519                    if paragraph.end_properties.is_some() {
1520                        return Err(duplicate("endParaRPr"));
1521                    }
1522                    paragraph.end_properties =
1523                        Some(CT_TextCharacterProperties::from_element(reader, &element)?);
1524                    boundary = boundary.max(2 + paragraph.runs.len());
1525                }
1526                Event::Empty(element)
1527                    if matches_local_name(element.name().as_ref(), b"endParaRPr") =>
1528                {
1529                    if paragraph.end_properties.is_some() {
1530                        return Err(duplicate("endParaRPr"));
1531                    }
1532                    paragraph.end_properties =
1533                        Some(CT_TextCharacterProperties::from_start(&element)?);
1534                    boundary = boundary.max(2 + paragraph.runs.len());
1535                }
1536                Event::Start(element) => paragraph
1537                    .raw_children
1538                    .push(boundary, capture_element(reader, &element)?),
1539                Event::Empty(element) => paragraph
1540                    .raw_children
1541                    .push(boundary, capture_empty_element(&element)?),
1542                Event::End(element) if matches_local_name(element.name().as_ref(), b"p") => {
1543                    return Ok(paragraph);
1544                }
1545                Event::Eof => return Err(missing_end("p")),
1546                _ => {}
1547            }
1548            buffer.clear();
1549        }
1550    }
1551
1552    /// Writes a paragraph with fixed prefixes and schema child order.
1553    pub fn write_xml<W: Write>(&self, writer: &mut Writer<W>) -> Result<()> {
1554        if self.properties.is_none()
1555            && self.runs.is_empty()
1556            && self.end_properties.is_none()
1557            && self.raw_children.is_empty()
1558        {
1559            return write_empty(writer, BytesStart::new("a:p"));
1560        }
1561        write_start(writer, BytesStart::new("a:p"))?;
1562        emit_raw(writer, self.raw_children.at(0))?;
1563        if let Some(properties) = &self.properties {
1564            properties.write_xml(writer, "a:pPr")?;
1565        }
1566        emit_raw(writer, self.raw_children.at(1))?;
1567        for (index, run) in self.runs.iter().enumerate() {
1568            run.write_xml(writer)?;
1569            emit_raw(writer, self.raw_children.at(2 + index))?;
1570        }
1571        if let Some(properties) = &self.end_properties {
1572            properties.write_xml(writer, "a:endParaRPr")?;
1573        }
1574        emit_raw(writer, self.raw_children.at(2 + self.runs.len()))?;
1575        write_end(writer, "a:p")
1576    }
1577
1578    /// Serialises a complete paragraph fragment.
1579    pub fn to_xml(&self) -> Result<Vec<u8>> {
1580        let mut writer = Writer::new(Vec::new());
1581        self.write_xml(&mut writer)?;
1582        Ok(writer.into_inner())
1583    }
1584
1585    pub fn raw_children(&self) -> &OrderedRawChildren {
1586        &self.raw_children
1587    }
1588}
1589
1590fn is_text_run(name: &[u8]) -> bool {
1591    matches!(local_name(name), b"r" | b"br" | b"fld")
1592}
1593
1594fn parse_complete<T>(
1595    xml: &[u8],
1596    expected: &[u8],
1597    parse_start: impl FnOnce(&mut Reader<&[u8]>, &BytesStart<'_>) -> Result<T>,
1598    parse_empty: impl FnOnce(&BytesStart<'_>) -> Result<T>,
1599) -> Result<T> {
1600    let mut reader = Reader::from_reader(xml);
1601    let mut buffer = Vec::new();
1602    loop {
1603        match reader
1604            .read_event_into(&mut buffer)
1605            .map_err(OxmlError::from)?
1606        {
1607            Event::Start(element) if matches_local_name(element.name().as_ref(), expected) => {
1608                reject_conflicting_a_prefix(&element)?;
1609                return parse_start(&mut reader, &element);
1610            }
1611            Event::Empty(element) if matches_local_name(element.name().as_ref(), expected) => {
1612                reject_conflicting_a_prefix(&element)?;
1613                return parse_empty(&element);
1614            }
1615            Event::Start(element) | Event::Empty(element) => return Err(unexpected(&element)),
1616            Event::Eof => {
1617                return Err(TextError::Xml(OxmlError::MissingElement(
1618                    String::from_utf8_lossy(expected).into_owned(),
1619                )));
1620            }
1621            _ => {}
1622        }
1623        buffer.clear();
1624    }
1625}
1626
1627fn root_local_name(xml: &[u8]) -> Result<Vec<u8>> {
1628    let mut reader = Reader::from_reader(xml);
1629    let mut buffer = Vec::new();
1630    loop {
1631        match reader
1632            .read_event_into(&mut buffer)
1633            .map_err(OxmlError::from)?
1634        {
1635            Event::Start(element) | Event::Empty(element) => {
1636                return Ok(local_name(element.name().as_ref()).to_vec());
1637            }
1638            Event::Eof => {
1639                return Err(TextError::Xml(OxmlError::MissingElement(
1640                    "DrawingML text properties".to_owned(),
1641                )));
1642            }
1643            _ => {}
1644        }
1645        buffer.clear();
1646    }
1647}
1648
1649fn ensure_empty(reader: &mut Reader<&[u8]>, expected: &[u8]) -> Result<()> {
1650    let mut buffer = Vec::new();
1651    loop {
1652        match reader
1653            .read_event_into(&mut buffer)
1654            .map_err(OxmlError::from)?
1655        {
1656            Event::End(element) if matches_local_name(element.name().as_ref(), expected) => {
1657                return Ok(());
1658            }
1659            Event::Text(text) if text.iter().all(u8::is_ascii_whitespace) => {}
1660            Event::Comment(_) => {}
1661            Event::Start(element) | Event::Empty(element) => return Err(unexpected(&element)),
1662            Event::Eof => return Err(missing_end(&String::from_utf8_lossy(expected))),
1663            _ => {
1664                return Err(TextError::UnexpectedElement(
1665                    String::from_utf8_lossy(expected).into_owned(),
1666                ));
1667            }
1668        }
1669        buffer.clear();
1670    }
1671}
1672
1673fn text_attr(start: &BytesStart<'_>, name: &[u8]) -> Result<Option<String>> {
1674    for attribute in start.attributes() {
1675        let attribute = attribute.map_err(OxmlError::from)?;
1676        if attribute.key.as_ref() == name {
1677            let value = attribute
1678                .decoded_and_normalized_value(XmlVersion::Implicit1_0, start.decoder())
1679                .map_err(OxmlError::from)?;
1680            return Ok(Some(value.into_owned()));
1681        }
1682    }
1683    Ok(None)
1684}
1685
1686fn capture_raw_attributes(
1687    start: &BytesStart<'_>,
1688    modelled: &[&[u8]],
1689) -> Result<Vec<(String, String)>> {
1690    let mut raw = Vec::new();
1691    for attribute in start.attributes() {
1692        let attribute = attribute.map_err(OxmlError::from)?;
1693        if modelled.iter().any(|name| attribute.key.as_ref() == *name) {
1694            continue;
1695        }
1696        let name = std::str::from_utf8(attribute.key.as_ref())
1697            .map_err(OxmlError::from)?
1698            .to_owned();
1699        let value = attribute
1700            .decoded_and_normalized_value(XmlVersion::Implicit1_0, start.decoder())
1701            .map_err(OxmlError::from)?
1702            .into_owned();
1703        raw.push((name, value));
1704    }
1705    Ok(raw)
1706}
1707
1708fn push_raw_attributes(start: &mut BytesStart<'_>, attributes: &[(String, String)]) {
1709    for (name, value) in attributes {
1710        start.push_attribute((name.as_str(), value.as_str()));
1711    }
1712}
1713
1714fn required_attr(start: &BytesStart<'_>, name: &[u8]) -> Result<String> {
1715    text_attr(start, name)?
1716        .filter(|value| !value.is_empty())
1717        .ok_or_else(|| {
1718            missing_attribute(
1719                &String::from_utf8_lossy(local_name(start.name().as_ref())),
1720                &String::from_utf8_lossy(name),
1721            )
1722        })
1723}
1724
1725fn parse_optional_i32(
1726    start: &BytesStart<'_>,
1727    name: &[u8],
1728    min: i32,
1729    max: i32,
1730) -> Result<Option<i32>> {
1731    text_attr(start, name)?
1732        .map(|value| {
1733            parse_i32_value(
1734                &String::from_utf8_lossy(local_name(start.name().as_ref())),
1735                &String::from_utf8_lossy(name),
1736                &value,
1737                min,
1738                max,
1739            )
1740        })
1741        .transpose()
1742}
1743
1744fn parse_i32_attr(start: &BytesStart<'_>, name: &[u8], min: i32, max: i32) -> Result<i32> {
1745    let value = required_attr(start, name)?;
1746    parse_i32_value(
1747        &String::from_utf8_lossy(local_name(start.name().as_ref())),
1748        &String::from_utf8_lossy(name),
1749        &value,
1750        min,
1751        max,
1752    )
1753}
1754
1755fn parse_i32_value(element: &str, attribute: &str, value: &str, min: i32, max: i32) -> Result<i32> {
1756    let parsed = value
1757        .parse::<i32>()
1758        .map_err(|_| invalid_attribute(element, attribute, value))?;
1759    if (min..=max).contains(&parsed) {
1760        Ok(parsed)
1761    } else {
1762        Err(invalid_attribute(element, attribute, value))
1763    }
1764}
1765
1766fn parse_optional_bool(start: &BytesStart<'_>, name: &[u8]) -> Result<Option<bool>> {
1767    text_attr(start, name)?
1768        .map(|value| match value.as_str() {
1769            "true" | "1" => Ok(true),
1770            "false" | "0" => Ok(false),
1771            _ => Err(invalid_attribute(
1772                &String::from_utf8_lossy(local_name(start.name().as_ref())),
1773                &String::from_utf8_lossy(name),
1774                &value,
1775            )),
1776        })
1777        .transpose()
1778}
1779
1780fn parse_optional_enum<T>(
1781    start: &BytesStart<'_>,
1782    name: &[u8],
1783    parse: impl FnOnce(&str) -> Option<T>,
1784) -> Result<Option<T>> {
1785    let Some(value) = text_attr(start, name)? else {
1786        return Ok(None);
1787    };
1788    parse(&value).map(Some).ok_or_else(|| {
1789        invalid_attribute(
1790            &String::from_utf8_lossy(local_name(start.name().as_ref())),
1791            &String::from_utf8_lossy(name),
1792            &value,
1793        )
1794    })
1795}
1796
1797fn validate_spacing_percent(value: &str) -> Result<()> {
1798    if is_percentage_string(value) {
1799        return Ok(());
1800    }
1801    parse_i32_value("spcPct", "val", value, 0, MAX_TRANSITIONAL_SPACING_PERCENT)?;
1802    Ok(())
1803}
1804
1805fn validate_baseline(value: &str) -> Result<()> {
1806    if is_percentage_string(value) {
1807        return Ok(());
1808    }
1809    parse_i32_value("rPr", "baseline", value, -100_000, 100_000)?;
1810    Ok(())
1811}
1812
1813fn is_percentage_string(value: &str) -> bool {
1814    value.strip_suffix('%').is_some_and(is_signed_decimal)
1815}
1816
1817fn is_universal_measure(value: &str) -> bool {
1818    ["mm", "cm", "in", "pt", "pc", "pi"]
1819        .iter()
1820        .find_map(|unit| value.strip_suffix(unit))
1821        .is_some_and(is_signed_decimal)
1822}
1823
1824fn is_signed_decimal(value: &str) -> bool {
1825    let unsigned = value.strip_prefix('-').unwrap_or(value);
1826    let mut parts = unsigned.split('.');
1827    let Some(integer) = parts.next() else {
1828        return false;
1829    };
1830    if integer.is_empty() || !integer.bytes().all(|byte| byte.is_ascii_digit()) {
1831        return false;
1832    }
1833    if let Some(fraction) = parts.next()
1834        && (fraction.is_empty() || !fraction.bytes().all(|byte| byte.is_ascii_digit()))
1835    {
1836        return false;
1837    }
1838    parts.next().is_none()
1839}
1840
1841fn is_guid(value: &str) -> bool {
1842    if value.len() != 38 || !value.starts_with('{') || !value.ends_with('}') {
1843        return false;
1844    }
1845    value.bytes().enumerate().all(|(index, byte)| match index {
1846        0 => byte == b'{',
1847        37 => byte == b'}',
1848        9 | 14 | 19 | 24 => byte == b'-',
1849        _ => byte.is_ascii_hexdigit(),
1850    })
1851}
1852
1853fn push_optional_attr<'a>(start: &mut BytesStart<'a>, name: &'a str, value: Option<&'a str>) {
1854    if let Some(value) = value {
1855        start.push_attribute((name, value));
1856    }
1857}
1858
1859fn push_optional_bool(start: &mut BytesStart<'_>, name: &'static str, value: Option<bool>) {
1860    if let Some(value) = value {
1861        start.push_attribute((name, if value { "1" } else { "0" }));
1862    }
1863}
1864
1865fn push_owned_attr(start: &mut BytesStart<'_>, name: &'static str, value: String) {
1866    start.push_attribute((name, value.as_str()));
1867}
1868
1869fn emit_raw<'a, W: Write>(
1870    writer: &mut Writer<W>,
1871    children: impl Iterator<Item = &'a [u8]>,
1872) -> Result<()> {
1873    for child in children {
1874        writer.get_mut().write_all(child).map_err(OxmlError::from)?;
1875    }
1876    Ok(())
1877}
1878
1879fn write_start<W: Write>(writer: &mut Writer<W>, start: BytesStart<'_>) -> Result<()> {
1880    writer
1881        .write_event(Event::Start(start))
1882        .map_err(OxmlError::from)?;
1883    Ok(())
1884}
1885
1886fn write_empty<W: Write>(writer: &mut Writer<W>, start: BytesStart<'_>) -> Result<()> {
1887    writer
1888        .write_event(Event::Empty(start))
1889        .map_err(OxmlError::from)?;
1890    Ok(())
1891}
1892
1893fn write_end<W: Write>(writer: &mut Writer<W>, tag: &str) -> Result<()> {
1894    writer
1895        .write_event(Event::End(BytesEnd::new(tag)))
1896        .map_err(OxmlError::from)?;
1897    Ok(())
1898}
1899
1900fn unexpected(element: &BytesStart<'_>) -> TextError {
1901    TextError::UnexpectedElement(String::from_utf8_lossy(element.name().as_ref()).into_owned())
1902}
1903
1904fn duplicate(element: &str) -> TextError {
1905    TextError::DuplicateElement(element.to_owned())
1906}
1907
1908fn missing_attribute(element: &str, attribute: &str) -> TextError {
1909    TextError::MissingAttribute {
1910        element: element.to_owned(),
1911        attribute: attribute.to_owned(),
1912    }
1913}
1914
1915fn invalid_attribute(element: &str, attribute: &str, value: &str) -> TextError {
1916    TextError::InvalidAttribute {
1917        element: element.to_owned(),
1918        attribute: attribute.to_owned(),
1919        value: value.to_owned(),
1920    }
1921}
1922
1923#[cfg(test)]
1924mod tests {
1925    use std::panic;
1926
1927    use super::{
1928        CT_TextCharacterProperties, CT_TextParagraph, CT_TextParagraphProperties, TextRun,
1929        TextSpace, TextSpacing,
1930    };
1931    use crate::color::ColorChoice;
1932    use crate::text::CT_TextBody;
1933    use crate::text::{TextAutoNumberScheme, TextBulletChoice, TextBulletSizeValue};
1934
1935    #[test]
1936    fn leading_and_trailing_text_whitespace_survives_via_xml_space_preserve() {
1937        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>"#;
1938        let paragraph = CT_TextParagraph::from_xml(xml).unwrap();
1939        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>"#);
1940
1941        let TextRun::Run(first) = &paragraph.runs[0] else {
1942            panic!("expected regular run");
1943        };
1944        assert_eq!(first.text.value, " leading");
1945        assert_eq!(first.text.space, TextSpace::Preserve);
1946
1947        let body = CT_TextBody::from_xml(
1948            br#"<q:txBody><q:bodyPr/><q:p><q:r><q:t> body </q:t></q:r></q:p></q:txBody>"#,
1949        )
1950        .unwrap();
1951        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>"#);
1952
1953        let hostile = CT_TextParagraph::from_xml(
1954            br#"<q:p><q:r><q:t x:space="preserve">plain</q:t></q:r></q:p>"#,
1955        )
1956        .unwrap();
1957        let TextRun::Run(run) = &hostile.runs[0] else {
1958            panic!("expected regular run");
1959        };
1960        assert_eq!(run.text.space, TextSpace::Default);
1961        assert_eq!(
1962            hostile.to_xml().unwrap(),
1963            br#"<a:p><a:r><a:t x:space="preserve">plain</a:t></a:r></a:p>"#
1964        );
1965    }
1966
1967    #[test]
1968    fn canonical_text_state_matches_whitespace_and_empty_hyperlinks_from_real_decks() {
1969        let paragraph = CT_TextParagraph::from_xml(
1970            br#"<q:p><q:r><q:rPr><q:hlinkClick r:id=""/></q:rPr><q:t>trailing </q:t></q:r></q:p>"#,
1971        )
1972        .unwrap();
1973        let TextRun::Run(run) = &paragraph.runs[0] else {
1974            panic!("expected regular run");
1975        };
1976        assert_eq!(run.text.space, TextSpace::Preserve);
1977        assert_eq!(
1978            run.properties
1979                .as_ref()
1980                .and_then(|properties| properties.hyperlink_click.as_ref())
1981                .and_then(|hyperlink| hyperlink.relationship_id.as_deref()),
1982            Some("")
1983        );
1984        let written = paragraph.to_xml().unwrap();
1985        assert_eq!(CT_TextParagraph::from_xml(&written).unwrap(), paragraph);
1986    }
1987
1988    #[test]
1989    fn paragraph_runs_fields_and_breaks_round_trip_structurally() {
1990        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>"#;
1991        let paragraph = CT_TextParagraph::from_xml(xml).unwrap();
1992        assert_eq!(paragraph.runs.len(), 3);
1993        let written = paragraph.to_xml().unwrap();
1994        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>"#);
1995        assert_eq!(CT_TextParagraph::from_xml(&written).unwrap(), paragraph);
1996
1997        let hostile = CT_TextCharacterProperties::from_xml(
1998            br#"<q:rPr><q:hlinkClick x:id="not-a-relationship"/></q:rPr>"#,
1999        )
2000        .unwrap();
2001        assert!(
2002            hostile
2003                .hyperlink_click
2004                .as_ref()
2005                .unwrap()
2006                .relationship_id
2007                .is_none()
2008        );
2009        let mut writer = quick_xml::Writer::new(Vec::new());
2010        hostile.write_xml(&mut writer, "a:rPr").unwrap();
2011        assert_eq!(
2012            writer.into_inner(),
2013            br#"<a:rPr><a:hlinkClick x:id="not-a-relationship"/></a:rPr>"#
2014        );
2015    }
2016
2017    #[test]
2018    fn character_all_caps_round_trips_while_small_caps_stays_unmodelled() {
2019        let all = CT_TextCharacterProperties::from_xml(
2020            br#"<q:rPr cap="all" sz="1800" x:producer="kept"/>"#,
2021        )
2022        .unwrap();
2023        assert_eq!(all.all_caps, Some(true));
2024        let mut writer = quick_xml::Writer::new(Vec::new());
2025        all.write_xml(&mut writer, "a:rPr").unwrap();
2026        let written = writer.into_inner();
2027        assert_eq!(
2028            written,
2029            br#"<a:rPr sz="1800" cap="all" x:producer="kept"/>"#
2030        );
2031        assert_eq!(CT_TextCharacterProperties::from_xml(&written).unwrap(), all);
2032
2033        let none = CT_TextCharacterProperties::from_xml(br#"<q:rPr cap="none"/>"#).unwrap();
2034        assert_eq!(none.all_caps, Some(false));
2035
2036        let small = CT_TextCharacterProperties::from_xml(br#"<q:rPr cap="small"/>"#).unwrap();
2037        assert_eq!(small.all_caps, None);
2038        let mut writer = quick_xml::Writer::new(Vec::new());
2039        small.write_xml(&mut writer, "a:rPr").unwrap();
2040        assert_eq!(writer.into_inner(), br#"<a:rPr cap="small"/>"#);
2041    }
2042
2043    #[test]
2044    fn paragraph_and_run_properties_use_drawingml_units_and_schema_order() {
2045        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();
2046        let written = {
2047            let mut writer = quick_xml::Writer::new(Vec::new());
2048            paragraph_properties
2049                .write_xml(&mut writer, "a:pPr")
2050                .unwrap();
2051            writer.into_inner()
2052        };
2053        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>"#);
2054
2055        let run_properties = CT_TextCharacterProperties::from_xml(
2056            br#"<q:rPr sz="100" spc="1.25pt" baseline="-100000"/>"#,
2057        )
2058        .unwrap();
2059        let mut writer = quick_xml::Writer::new(Vec::new());
2060        run_properties.write_xml(&mut writer, "a:rPr").unwrap();
2061        assert_eq!(
2062            writer.into_inner(),
2063            br#"<a:rPr sz="100" spc="1.25pt" baseline="-100000"/>"#
2064        );
2065
2066        let mut inserted_spacing =
2067            CT_TextParagraphProperties::from_xml(br#"<q:pPr><q:buChar char="*"/></q:pPr>"#)
2068                .unwrap();
2069        inserted_spacing.line_spacing = Some(TextSpacing::Percent("100000".to_owned()));
2070        let mut writer = quick_xml::Writer::new(Vec::new());
2071        inserted_spacing.write_xml(&mut writer, "a:pPr").unwrap();
2072        assert_eq!(
2073            writer.into_inner(),
2074            br#"<a:pPr><a:lnSpc><a:spcPct val="100000"/></a:lnSpc><a:buChar char="*"/></a:pPr>"#
2075        );
2076    }
2077
2078    #[test]
2079    fn every_modelled_bullet_form_round_trips_in_schema_order() {
2080        let cases: &[(&[u8], &[u8])] = &[
2081            (
2082                br#"<q:pPr><q:buClr><q:srgbClr val="AABBCC"/></q:buClr><q:buSzPct val="125000"/><q:buFont typeface="Wingdings"/><q:buChar char="*"/></q:pPr>"#,
2083                br#"<a:pPr><a:buClr><a:srgbClr val="AABBCC"/></a:buClr><a:buSzPct val="125000"/><a:buFont typeface="Wingdings"/><a:buChar char="*"/></a:pPr>"#,
2084            ),
2085            (
2086                br#"<q:pPr><q:buSzPts val="1800"/><q:buAutoNum type="arabicPeriod" startAt="3"/></q:pPr>"#,
2087                br#"<a:pPr><a:buSzPts val="1800"/><a:buAutoNum type="arabicPeriod" startAt="3"/></a:pPr>"#,
2088            ),
2089            (br#"<q:pPr><q:buNone/></q:pPr>"#, br#"<a:pPr><a:buNone/></a:pPr>"#),
2090        ];
2091
2092        for (xml, expected) in cases {
2093            let properties = CT_TextParagraphProperties::from_xml(xml).unwrap();
2094            let mut writer = quick_xml::Writer::new(Vec::new());
2095            properties.write_xml(&mut writer, "a:pPr").unwrap();
2096            let written = writer.into_inner();
2097            assert_eq!(written, *expected);
2098            assert_eq!(
2099                CT_TextParagraphProperties::from_xml(&written).unwrap(),
2100                properties
2101            );
2102        }
2103
2104        let character = CT_TextParagraphProperties::from_xml(cases[0].0).unwrap();
2105        let bullet = character.bullet.as_ref().unwrap();
2106        assert!(matches!(
2107            bullet.choice,
2108            Some(TextBulletChoice::Character(_))
2109        ));
2110        assert_eq!(bullet.font.as_ref().unwrap().typeface, "Wingdings");
2111        assert!(matches!(
2112            bullet.size.as_ref().unwrap().value,
2113            TextBulletSizeValue::Percent(ref value) if value == "125000"
2114        ));
2115        assert!(matches!(
2116            bullet.color.as_ref().unwrap().color,
2117            ColorChoice::Srgb { .. }
2118        ));
2119
2120        let automatic = CT_TextParagraphProperties::from_xml(cases[1].0).unwrap();
2121        let Some(TextBulletChoice::AutoNumber(numbering)) =
2122            automatic.bullet.as_ref().unwrap().choice.as_ref()
2123        else {
2124            panic!("expected automatic-number bullet");
2125        };
2126        assert_eq!(numbering.scheme, TextAutoNumberScheme::ArabicPeriod);
2127        assert_eq!(numbering.start_at, Some(3));
2128        assert!(matches!(
2129            automatic
2130                .bullet
2131                .as_ref()
2132                .unwrap()
2133                .size
2134                .as_ref()
2135                .unwrap()
2136                .value,
2137            TextBulletSizeValue::Points(1800)
2138        ));
2139
2140        let none = CT_TextParagraphProperties::from_xml(cases[2].0).unwrap();
2141        assert!(matches!(
2142            none.bullet.as_ref().unwrap().choice,
2143            Some(TextBulletChoice::None(_))
2144        ));
2145    }
2146
2147    #[test]
2148    fn bullet_font_size_and_colour_keep_their_schema_positions() {
2149        let properties = CT_TextParagraphProperties::from_xml(
2150            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>"#,
2151        )
2152        .unwrap();
2153        let mut writer = quick_xml::Writer::new(Vec::new());
2154        properties.write_xml(&mut writer, "a:pPr").unwrap();
2155        assert_eq!(
2156            writer.into_inner(),
2157            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>"#
2158        );
2159    }
2160
2161    #[test]
2162    fn unknown_bullet_children_round_trip_byte_for_byte() {
2163        let properties = CT_TextParagraphProperties::from_xml(
2164            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>"#,
2165        )
2166        .unwrap();
2167        let mut writer = quick_xml::Writer::new(Vec::new());
2168        properties.write_xml(&mut writer, "a:pPr").unwrap();
2169        assert_eq!(
2170            writer.into_inner(),
2171            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>"#
2172        );
2173    }
2174
2175    #[test]
2176    fn malformed_bullet_values_return_errors_without_panicking() {
2177        let cases: &[&[u8]] = &[
2178            br#"<q:pPr><q:buChar/></q:pPr>"#,
2179            br#"<q:pPr><q:buChar char=""/></q:pPr>"#,
2180            br#"<q:pPr><q:buSzPct val="24999"/></q:pPr>"#,
2181            br#"<q:pPr><q:buSzPct val="400001"/></q:pPr>"#,
2182            br#"<q:pPr><q:buSzPts val="99"/></q:pPr>"#,
2183            br#"<q:pPr><q:buSzPts val="400001"/></q:pPr>"#,
2184            br#"<q:pPr><q:buAutoNum type="unknown"/></q:pPr>"#,
2185            br#"<q:pPr><q:buAutoNum/></q:pPr>"#,
2186            br#"<q:pPr><q:buAutoNum type="arabicPeriod" startAt="0"/></q:pPr>"#,
2187            br#"<q:pPr><q:buAutoNum type="arabicPeriod" startAt="32768"/></q:pPr>"#,
2188            br#"<q:pPr><q:buFont/></q:pPr>"#,
2189            br#"<q:pPr><q:buClr/></q:pPr>"#,
2190            br#"<q:pPr><q:buClr><q:srgbClr x:val="AABBCC"/></q:buClr></q:pPr>"#,
2191            br#"<q:pPr><q:buClr><q:srgbClr val="GGGGGG"/></q:buClr></q:pPr>"#,
2192            br#"<q:pPr><q:buClr><q:srgbClr val="AABBCC"/><q:schemeClr val="accent1"/></q:buClr></q:pPr>"#,
2193            br#"<q:pPr><q:buChar char="*"/><q:buNone/></q:pPr>"#,
2194            br#"<q:pPr><q:buSzPct val="100000"/><q:buSzPts val="1200"/></q:pPr>"#,
2195        ];
2196
2197        for xml in cases {
2198            let result = panic::catch_unwind(|| CT_TextParagraphProperties::from_xml(xml));
2199            assert!(result.is_ok(), "bullet parser panicked");
2200            assert!(result.unwrap().is_err(), "malformed bullet parsed");
2201        }
2202    }
2203
2204    #[test]
2205    fn malformed_text_content_returns_errors_without_panicking() {
2206        let cases: &[&[u8]] = &[
2207            br#"<q:r/>"#,
2208            br#"<q:r><q:t><x:nested/></q:t></q:r>"#,
2209            br#"<q:r><q:t xml:space="keep">x</q:t></q:r>"#,
2210            br#"<q:r><q:t>x</q:t><q:t>y</q:t></q:r>"#,
2211            br#"<q:pPr lvl="9"/>"#,
2212            br#"<q:pPr marL="-1"/>"#,
2213            br#"<q:pPr indent="51206401"/>"#,
2214            br#"<q:pPr algn="middle"/>"#,
2215            br#"<q:pPr><q:lnSpc/></q:pPr>"#,
2216            br#"<q:pPr><q:lnSpc><q:spcPts val="158401"/></q:lnSpc></q:pPr>"#,
2217            br#"<q:pPr><q:lnSpc><q:spcPct val="201170"/></q:lnSpc></q:pPr>"#,
2218            br#"<q:rPr sz="99"/>"#,
2219            br#"<q:rPr b="yes"/>"#,
2220            br#"<q:rPr u="triple"/>"#,
2221            br#"<q:rPr spc="400001"/>"#,
2222            r#"<q:rPr spc="éx"/>"#.as_bytes(),
2223            br#"<q:rPr baseline="100001"/>"#,
2224            br#"<q:rPr><q:latin/></q:rPr>"#,
2225            br#"<q:fld type="slidenum"/>"#,
2226            br#"<q:fld id="not-a-guid"/>"#,
2227            br#"<q:p><q:endParaRPr/><q:endParaRPr/></q:p>"#,
2228            br#"<q:notPPr/>"#,
2229            br#"<q:notRPr/>"#,
2230        ];
2231        for xml in cases {
2232            let result = panic::catch_unwind(|| match super::root_local_name(xml).as_deref() {
2233                Ok(b"r") => super::CT_RegularTextRun::from_xml(xml).map(|_| ()),
2234                Ok(b"pPr") => CT_TextParagraphProperties::from_xml(xml).map(|_| ()),
2235                Ok(b"rPr") => CT_TextCharacterProperties::from_xml(xml).map(|_| ()),
2236                Ok(b"fld") => super::CT_TextField::from_xml(xml).map(|_| ()),
2237                Ok(b"p") => CT_TextParagraph::from_xml(xml).map(|_| ()),
2238                Ok(b"notPPr") => CT_TextParagraphProperties::from_xml(xml).map(|_| ()),
2239                Ok(b"notRPr") => CT_TextCharacterProperties::from_xml(xml).map(|_| ()),
2240                _ => unreachable!(),
2241            });
2242            assert!(result.is_ok(), "text parser panicked");
2243            assert!(result.unwrap().is_err(), "malformed text parsed");
2244        }
2245    }
2246}