Skip to main content

oxml_drawing/text/
paragraph.rs

1use std::borrow::Cow;
2use std::io::Write;
3
4use oxml_core::OxmlError;
5use oxml_core::raw_xml::{capture_element, capture_empty_element};
6use oxml_core::xml::{local_name, matches_local_name};
7use oxml_core::xml_text::{decode_plain, resolve_entity};
8use quick_xml::events::{BytesEnd, BytesStart, BytesText, Event};
9use quick_xml::name::{Namespace, ResolveResult};
10use quick_xml::{NsReader, Reader, Writer, XmlVersion};
11
12use crate::color::{ColorChoice, ColorTransform};
13use crate::fill::Fill;
14use crate::namespace::A_NS;
15use crate::order::OrderedRawChildren;
16
17use super::body::{Result, TextError, missing_end};
18use super::bullet::TextBullet;
19
20const TEXT_REL_NS: &str = "http://schemas.openxmlformats.org/officeDocument/2006/relationships";
21
22pub(crate) fn text_validate_root(start: &BytesStart<'_>) -> Result<()> {
23    let name = start.name();
24    let raw = std::str::from_utf8(name.as_ref()).map_err(OxmlError::from)?;
25    let prefix = raw.split_once(':').map_or("", |(prefix, _)| prefix);
26    let scope = text_namespace_scope(start, &[])?;
27    if scope.iter().any(|(key, uri)| key == prefix && uri != A_NS) {
28        return Err(invalid_attribute(
29            "DrawingML text",
30            "namespace",
31            "foreign root",
32        ));
33    }
34    Ok(())
35}
36
37pub(crate) fn text_source_tag(tag: &str, original: &str, physical: &[(String, String)]) -> String {
38    let requested_prefix = tag.split_once(':').map_or("", |(prefix, _)| prefix);
39    let expected = match requested_prefix {
40        "a" => Some(A_NS),
41        "p" if matches!(
42            tag.split_once(':').map_or(tag, |(_, local)| local),
43            "defaultTextStyle"
44                | "titleStyle"
45                | "bodyStyle"
46                | "otherStyle"
47                | "notesStyle"
48                | "txBody"
49        ) =>
50        {
51            Some("http://schemas.openxmlformats.org/presentationml/2006/main")
52        }
53        "dgm" if tag == "dgm:t" => Some("http://schemas.openxmlformats.org/drawingml/2006/diagram"),
54        _ => None,
55    };
56    let prefix = original.split_once(':').map_or("", |(prefix, _)| prefix);
57    let owned = expected.is_some_and(|expected| {
58        physical
59            .iter()
60            .any(|(name, uri)| name == prefix && uri == expected)
61    });
62    if original.is_empty() || !owned {
63        return tag.to_owned();
64    }
65    // An imported alias/default owner is safer than promoting a canonical
66    // spelling absent from, or foreign-bound in, its actual source scope.
67    let local = tag.split_once(':').map_or(tag, |(_, local)| local);
68    original.split_once(':').map_or_else(
69        || local.to_owned(),
70        |(prefix, _)| format!("{prefix}:{local}"),
71    )
72}
73
74// A converted wrapper needs its destination namespace without rebinding any
75// imported prefix used by opaque descendants. Empty authored shells rely on
76// their surrounding owner's declarations as before.
77pub(crate) fn text_wrapper_start(
78    tag: &str,
79    original: &str,
80    spellings: &[String],
81) -> Result<BytesStart<'static>> {
82    let physical = text_namespace_scope(&text_source_start(tag, spellings), &[])?;
83    let owned = text_source_tag(tag, original, &physical);
84    let mut start = text_source_start(&owned, spellings);
85    if original.is_empty() || owned != tag {
86        return Ok(start);
87    }
88    let Some((prefix, local)) = tag.split_once(':') else {
89        return Ok(start);
90    };
91    let expected = match prefix {
92        "a" => A_NS,
93        "p" => "http://schemas.openxmlformats.org/presentationml/2006/main",
94        "dgm" => "http://schemas.openxmlformats.org/drawingml/2006/diagram",
95        _ => return Ok(start),
96    };
97    let original_prefix = original.split_once(':').map_or("", |(prefix, _)| prefix);
98    if !physical.iter().any(|(name, _)| name == original_prefix) {
99        // Legacy standalone fragments have no physical owner declaration.
100        // Preserve their existing surrounding-scope serialization contract.
101        return Ok(start);
102    }
103    if physical
104        .iter()
105        .any(|(name, uri)| name == prefix && uri == expected)
106    {
107        return Ok(start);
108    }
109    let mut destination = prefix.to_owned();
110    let mut suffix = 1;
111    while physical.iter().any(|(name, _)| name == &destination) {
112        destination = format!("{prefix}{suffix}");
113        suffix += 1;
114    }
115    start = text_source_start(&format!("{destination}:{local}"), spellings);
116    start.push_attribute((format!("xmlns:{destination}").as_str(), expected));
117    Ok(start)
118}
119
120fn text_has_physical_owner(tag: &str, physical: &[(String, String)]) -> bool {
121    let prefix = tag.split_once(':').map_or("", |(prefix, _)| prefix);
122    physical
123        .iter()
124        .any(|(name, uri)| name == prefix && uri == A_NS)
125}
126
127// Newly generated canonical descendants must declare their own URI when an
128// imported parent uses that spelling for a foreign namespace.
129pub(crate) fn text_close_generated_child(
130    raw: &[u8],
131    inherited: &[(String, String)],
132) -> Result<Vec<u8>> {
133    if !inherited
134        .iter()
135        .any(|(prefix, uri)| prefix == "a" && uri != A_NS)
136    {
137        return Ok(raw.to_vec());
138    }
139    let mut reader = Reader::from_reader(raw);
140    let event = reader.read_event().map_err(OxmlError::from)?;
141    let (start, empty) = match event {
142        Event::Start(start) => (start, false),
143        Event::Empty(start) => (start, true),
144        _ => return Ok(raw.to_vec()),
145    };
146    if !start.name().as_ref().starts_with(b"a:")
147        || start
148            .attributes()
149            .any(|attribute| attribute.is_ok_and(|attribute| attribute.key.as_ref() == b"xmlns:a"))
150    {
151        return Ok(raw.to_vec());
152    }
153    let name = std::str::from_utf8(start.name().as_ref())
154        .map_err(OxmlError::from)?
155        .to_owned();
156    let content = format!(
157        "{} xmlns:a=\"{A_NS}\"",
158        std::str::from_utf8(start.as_ref()).map_err(OxmlError::from)?
159    );
160    let replacement = BytesStart::from_content(content, name.len());
161    let mut writer = Writer::new(Vec::new());
162    if empty {
163        write_empty(&mut writer, replacement)?;
164    } else {
165        write_start(&mut writer, replacement)?;
166        writer
167            .get_mut()
168            .write_all(&raw[reader.buffer_position() as usize..])
169            .map_err(OxmlError::from)?;
170    }
171    Ok(writer.into_inner())
172}
173
174pub(crate) fn text_namespace_scope(
175    start: &BytesStart<'_>,
176    inherited: &[(String, String)],
177) -> Result<Vec<(String, String)>> {
178    let mut scope = inherited.to_vec();
179    for attribute in start.attributes() {
180        let attribute = attribute.map_err(OxmlError::from)?;
181        let key = attribute.key.as_ref();
182        let prefix = if key == b"xmlns" {
183            Some("")
184        } else if let Some(prefix) = key.strip_prefix(b"xmlns:") {
185            Some(std::str::from_utf8(prefix).map_err(OxmlError::from)?)
186        } else {
187            None
188        };
189        if let Some(prefix) = prefix {
190            let value = attribute
191                .decoded_and_normalized_value(XmlVersion::Implicit1_0, start.decoder())
192                .map_err(OxmlError::from)?
193                .into_owned();
194            if let Some((_, existing)) = scope.iter_mut().find(|(name, _)| name == prefix) {
195                *existing = value;
196            } else {
197                scope.push((prefix.to_owned(), value));
198            }
199        }
200    }
201    Ok(scope)
202}
203
204/// Physical bindings are preserved source facts. Lookup fallbacks exist only
205/// for the established standalone, undeclared-prefix fragment API.
206#[derive(Clone, Debug, Default)]
207pub(crate) struct TextNamespaceScope {
208    pub(crate) physical: Vec<(String, String)>,
209    lookup: Vec<(String, String)>,
210    pub(crate) fixed_list_style_children: bool,
211}
212
213impl TextNamespaceScope {
214    pub(crate) fn inherited(bindings: &[(String, String)]) -> Self {
215        Self {
216            physical: bindings.to_vec(),
217            lookup: bindings.to_vec(),
218            fixed_list_style_children: false,
219        }
220    }
221}
222
223pub(crate) fn text_fragment_scope(
224    start: &BytesStart<'_>,
225    inherited: &TextNamespaceScope,
226) -> Result<TextNamespaceScope> {
227    let physical = text_namespace_scope(start, &inherited.physical)?;
228    let mut lookup = inherited.lookup.clone();
229    if lookup.is_empty() {
230        lookup.push(("a".to_owned(), A_NS.to_owned()));
231        lookup.push(("r".to_owned(), TEXT_REL_NS.to_owned()));
232        let name = start.name();
233        let root = std::str::from_utf8(name.as_ref()).map_err(OxmlError::from)?;
234        let prefix = root.split_once(':').map_or("", |(prefix, _)| prefix);
235        if !lookup.iter().any(|(existing, _)| existing == prefix) {
236            lookup.push((prefix.to_owned(), A_NS.to_owned()));
237        }
238    }
239    let lookup = text_namespace_scope(start, &lookup)?;
240    Ok(TextNamespaceScope {
241        physical,
242        lookup,
243        fixed_list_style_children: inherited.fixed_list_style_children,
244    })
245}
246
247pub(crate) fn text_owned_name(
248    start: &BytesStart<'_>,
249    inherited: &TextNamespaceScope,
250) -> Result<Vec<u8>> {
251    let scope = text_namespace_scope(start, &inherited.lookup)?;
252    let name = start.name();
253    let raw = std::str::from_utf8(name.as_ref()).map_err(OxmlError::from)?;
254    let (prefix, local) = raw.split_once(':').unwrap_or(("", raw));
255    let owned = scope
256        .iter()
257        .any(|(name, uri)| name == prefix && uri == A_NS);
258    // List-style typed descendants use fixed-prefix lower-level writers.
259    // Opaque nodes and ordinary paragraph/run alias preservation are unchanged.
260    if owned
261        && inherited.fixed_list_style_children
262        && (is_paragraph_property_tag(local.as_bytes())
263            || is_character_property_tag(local.as_bytes())
264            || matches!(local, "lnSpc" | "spcBef" | "spcAft"))
265    {
266        crate::namespace::reject_conflicting_a_prefix(start)?;
267    }
268    Ok(if owned {
269        local.as_bytes().to_vec()
270    } else {
271        Vec::new()
272    })
273}
274
275// Attribute token spelling includes preceding whitespace, original quote and
276// entities. The normal XML attribute reader validates names/duplicates first.
277fn text_attribute_tokens(start: &BytesStart<'_>) -> Result<Vec<(String, String)>> {
278    for attribute in start.attributes() {
279        attribute.map_err(OxmlError::from)?;
280    }
281    let raw: &[u8] = start.as_ref();
282    let mut index = start.name().as_ref().len();
283    let mut output = Vec::new();
284    while index < raw.len() {
285        let token_start = index;
286        while index < raw.len() && raw[index].is_ascii_whitespace() {
287            index += 1;
288        }
289        if index == raw.len() {
290            break;
291        }
292        let name_start = index;
293        while index < raw.len() && !raw[index].is_ascii_whitespace() && raw[index] != b'=' {
294            index += 1;
295        }
296        let name = std::str::from_utf8(&raw[name_start..index])
297            .map_err(OxmlError::from)?
298            .to_owned();
299        while index < raw.len() && raw[index].is_ascii_whitespace() {
300            index += 1;
301        }
302        if raw.get(index) != Some(&b'=') {
303            return Err(invalid_attribute("text source", "token", "missing equals"));
304        }
305        index += 1;
306        while index < raw.len() && raw[index].is_ascii_whitespace() {
307            index += 1;
308        }
309        let quote = *raw
310            .get(index)
311            .ok_or_else(|| invalid_attribute("text source", "token", "missing quote"))?;
312        if quote != b'\'' && quote != b'"' {
313            return Err(invalid_attribute("text source", "token", "invalid quote"));
314        }
315        index += 1;
316        while index < raw.len() && raw[index] != quote {
317            index += 1;
318        }
319        if index == raw.len() {
320            return Err(invalid_attribute(
321                "text source",
322                "token",
323                "unterminated quote",
324            ));
325        }
326        index += 1;
327        output.push((
328            name,
329            std::str::from_utf8(&raw[token_start..index])
330                .map_err(OxmlError::from)?
331                .to_owned(),
332        ));
333    }
334    Ok(output)
335}
336
337pub(crate) fn text_root_spellings(start: &BytesStart<'_>) -> Result<Vec<String>> {
338    Ok(text_attribute_tokens(start)?
339        .into_iter()
340        .map(|(_, token)| token)
341        .collect())
342}
343
344pub(crate) fn text_root_spellings_scoped(
345    start: &BytesStart<'_>,
346    inherited: &TextNamespaceScope,
347    raw: &[u8],
348) -> Result<Vec<String>> {
349    let mut tokens = text_attribute_tokens(start)?;
350    for (prefix, uri) in text_owner_closure(raw, &text_fragment_scope(start, inherited)?.physical)?
351    {
352        let key = if prefix.is_empty() {
353            "xmlns".to_owned()
354        } else {
355            format!("xmlns:{prefix}")
356        };
357        if !tokens.iter().any(|(name, _)| name == &key) {
358            tokens.push((
359                key.clone(),
360                format!(" {key}=\"{}\"", quick_xml::escape::escape(&uri)),
361            ));
362        }
363    }
364    Ok(tokens.into_iter().map(|(_, token)| token).collect())
365}
366
367fn text_field_root_spellings(
368    start: &BytesStart<'_>,
369    inherited: &TextNamespaceScope,
370) -> Result<Vec<String>> {
371    let mut tokens = text_attribute_tokens(start)?;
372    for (prefix, uri) in text_fragment_scope(start, inherited)?.physical {
373        let key = if prefix.is_empty() {
374            "xmlns".to_owned()
375        } else {
376            format!("xmlns:{prefix}")
377        };
378        if !tokens.iter().any(|(name, _)| name == &key) {
379            tokens.push((
380                key.clone(),
381                format!(" {key}=\"{}\"", quick_xml::escape::escape(&uri)),
382            ));
383        }
384    }
385    Ok(tokens
386        .into_iter()
387        .filter(|(key, _)| key != "id" && key != "type")
388        .map(|(_, token)| token)
389        .collect())
390}
391
392pub(crate) fn text_source_start(tag: &str, spellings: &[String]) -> BytesStart<'static> {
393    BytesStart::from_content(format!("{tag}{}", spellings.concat()), tag.len())
394}
395
396pub(crate) fn text_capture_opaque_event(event: Event<'_>) -> Result<Vec<u8>> {
397    let mut writer = Writer::new(Vec::new());
398    writer.write_event(event).map_err(OxmlError::from)?;
399    Ok(writer.into_inner())
400}
401
402fn text_referenced_prefixes(raw: &[u8]) -> Result<Vec<String>> {
403    let mut reader = Reader::from_reader(raw);
404    let mut prefixes = Vec::new();
405    loop {
406        match reader.read_event().map_err(OxmlError::from)? {
407            Event::Start(start) | Event::Empty(start) => {
408                let name = start.name();
409                let name = std::str::from_utf8(name.as_ref()).map_err(OxmlError::from)?;
410                let prefix = name.split_once(':').map_or("", |(prefix, _)| prefix);
411                if !prefixes.iter().any(|p| p == prefix) {
412                    prefixes.push(prefix.to_owned());
413                }
414                for attribute in start.attributes() {
415                    let attribute = attribute.map_err(OxmlError::from)?;
416                    let key =
417                        std::str::from_utf8(attribute.key.as_ref()).map_err(OxmlError::from)?;
418                    if let Some((prefix, _)) = key.split_once(':')
419                        && prefix != "xmlns"
420                        && !prefixes.iter().any(|p| p == prefix)
421                    {
422                        prefixes.push(prefix.to_owned());
423                    }
424                }
425            }
426            Event::Eof => return Ok(prefixes),
427            _ => {}
428        }
429    }
430}
431
432fn text_owner_closure(raw: &[u8], physical: &[(String, String)]) -> Result<Vec<(String, String)>> {
433    let mut reader = Reader::from_reader(raw);
434    let start = match reader.read_event().map_err(OxmlError::from)? {
435        Event::Start(start) | Event::Empty(start) => start,
436        _ => return Ok(Vec::new()),
437    };
438    let mut referenced = text_referenced_prefixes(raw)?;
439    // A canonical fragment's conventional prefix remains supplied by its
440    // enclosing owner. Actual local declarations stay in the original tokens.
441    // Aliases and foreign payload prefixes still need source-bound closure.
442    if start.name().as_ref().starts_with(b"a:")
443        && !start
444            .attributes()
445            .any(|attribute| attribute.is_ok_and(|attribute| attribute.key.as_ref() == b"xmlns:a"))
446    {
447        referenced.retain(|prefix| prefix != "a");
448    }
449    Ok(physical
450        .iter()
451        .filter(|(prefix, _)| referenced.contains(prefix))
452        .cloned()
453        .collect())
454}
455
456pub(crate) fn text_close_source_owner(
457    raw: &[u8],
458    inherited: &[(String, String)],
459    tag: &str,
460) -> Result<Vec<u8>> {
461    let mut reader = Reader::from_reader(raw);
462    let event = reader.read_event().map_err(OxmlError::from)?;
463    let (start, empty) = match event {
464        Event::Start(start) => (start, false),
465        Event::Empty(start) => (start, true),
466        _ => return Err(invalid_attribute("text source", "root", "missing owner")),
467    };
468    text_validate_root(&start)?;
469    let scope = text_namespace_scope(&start, inherited)?;
470    let closure = text_owner_closure(raw, &scope)?;
471    let mut referenced = closure
472        .iter()
473        .map(|(prefix, _)| prefix.clone())
474        .collect::<Vec<_>>();
475    if !start.name().as_ref().starts_with(b"a:") {
476        referenced.push(
477            tag.split_once(':')
478                .map_or("", |(prefix, _)| prefix)
479                .to_owned(),
480        );
481    }
482    let tokens = text_attribute_tokens(&start)?;
483    let mut body = format!(
484        "{tag}{}",
485        tokens
486            .iter()
487            .map(|(_, token)| token.as_str())
488            .collect::<String>()
489    );
490    for (prefix, uri) in &scope {
491        if !referenced.contains(prefix) {
492            continue;
493        }
494        let key = if prefix.is_empty() {
495            "xmlns".to_owned()
496        } else {
497            format!("xmlns:{prefix}")
498        };
499        if !tokens.iter().any(|(name, _)| name == &key) {
500            body.push_str(&format!(
501                " {key}=\"{}\"",
502                quick_xml::escape::escape(uri.as_str())
503            ));
504        }
505    }
506    let original_name = std::str::from_utf8(start.name().as_ref())
507        .map_err(OxmlError::from)?
508        .to_owned();
509    let owned_tag = text_source_tag(tag, &original_name, &scope);
510    let tag = owned_tag.as_str();
511    let suffix = body.strip_prefix(tag).map(str::to_owned);
512    let original_requested = body.find(char::is_whitespace).unwrap_or(body.len());
513    let body = format!(
514        "{tag}{}",
515        suffix.unwrap_or_else(|| body[original_requested..].to_owned())
516    );
517    let replacement = BytesStart::from_content(body, tag.len());
518    let mut writer = Writer::new(Vec::new());
519    if empty {
520        write_empty(&mut writer, replacement)?;
521    } else {
522        write_start(&mut writer, replacement)?;
523        let end = raw
524            .iter()
525            .rposition(|byte| *byte == b'<')
526            .ok_or_else(|| missing_end("text source"))?;
527        let begin = reader.buffer_position() as usize;
528        if end < begin {
529            return Err(missing_end("text source"));
530        }
531        writer
532            .get_mut()
533            .write_all(&raw[begin..end])
534            .map_err(OxmlError::from)?;
535        write_end(&mut writer, tag)?;
536    }
537    Ok(writer.into_inner())
538}
539
540const MAX_TEXT_MARGIN: i32 = 51_206_400;
541const MAX_TEXT_POINT: i32 = 400_000;
542const MAX_TEXT_SPACING_POINT: i32 = 158_400;
543const MAX_TRANSITIONAL_SPACING_PERCENT: i32 = 13_200_000;
544
545/// Whether source text explicitly requested XML whitespace preservation.
546#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
547pub enum TextSpace {
548    #[default]
549    Default,
550    Preserve,
551}
552
553/// One DrawingML `a:t` value and its source whitespace intent.
554///
555/// `value` is the text as the part stores it and PowerPoint shows it. The
556/// text setters store a character XML 1.0 cannot carry, such as U+0001, as
557/// `_xHHHH_` with four uppercase hexadecimal digits, as python-pptx does, and
558/// reading never decodes that form, as neither python-pptx nor PowerPoint
559/// does.
560#[derive(Clone, Debug, Default)]
561pub struct TextValue {
562    pub value: String,
563    pub space: TextSpace,
564    raw_attributes: Vec<(String, String)>,
565    source_xml: Option<Vec<u8>>,
566    source_value: String,
567    source_space: TextSpace,
568    source_namespaces: Vec<(String, String)>,
569    source_attributes: Vec<(String, String)>,
570    source_tag: String,
571}
572impl PartialEq for TextValue {
573    fn eq(&self, other: &Self) -> bool {
574        self.value == other.value
575            && self.space == other.space
576            && self.raw_attributes == other.raw_attributes
577    }
578}
579impl Eq for TextValue {}
580
581impl TextValue {
582    fn from_start(start: &BytesStart<'_>) -> Result<Self> {
583        if !matches_local_name(start.name().as_ref(), b"t") {
584            return Err(unexpected(start));
585        }
586        let space_value = text_attr(start, b"xml:space")?;
587        let space = match space_value.as_deref() {
588            None | Some("default") => TextSpace::Default,
589            Some("preserve") => TextSpace::Preserve,
590            Some(value) => return Err(invalid_attribute("t", "xml:space", value)),
591        };
592        Ok(Self {
593            value: String::new(),
594            space,
595            raw_attributes: capture_raw_attributes(start, &[b"xml:space"])?,
596            source_xml: Some(capture_empty_element(start)?),
597            source_value: String::new(),
598            source_space: space,
599            source_namespaces: text_fragment_scope(start, &TextNamespaceScope::default())?.physical,
600            source_attributes: text_attribute_tokens(start)?,
601            source_tag: std::str::from_utf8(start.name().as_ref())
602                .map_err(OxmlError::from)?
603                .to_owned(),
604        })
605    }
606
607    fn from_start_with_namespaces(
608        start: &BytesStart<'_>,
609        inherited: &TextNamespaceScope,
610    ) -> Result<Self> {
611        let mut value = Self::from_start(start)?;
612        value.source_namespaces = text_fragment_scope(start, inherited)?.physical;
613        Ok(value)
614    }
615
616    fn from_element_with_namespaces(
617        reader: &mut Reader<&[u8]>,
618        start: &BytesStart<'_>,
619        inherited: &TextNamespaceScope,
620    ) -> Result<Self> {
621        let raw = capture_element(reader, start)?;
622        let mut parsed = parse_complete(&raw, b"t", Self::from_element_model, Self::from_start)?;
623        parsed.source_namespaces = text_fragment_scope(start, inherited)?.physical;
624        parsed.source_value = parsed.value.clone();
625        parsed.source_space = parsed.space;
626        // Keep the established explicit whitespace-preserve normalization.
627        let had_explicit_space = text_attr(start, b"xml:space")?.is_some();
628        if parsed.space != TextSpace::Preserve || had_explicit_space {
629            parsed.source_xml = Some(raw);
630        } else {
631            parsed.source_xml = None;
632        }
633        Ok(parsed)
634    }
635
636    fn from_element_model(reader: &mut Reader<&[u8]>, start: &BytesStart<'_>) -> Result<Self> {
637        let mut text = Self::from_start(start)?;
638        let mut buffer = Vec::new();
639        loop {
640            match reader
641                .read_event_into(&mut buffer)
642                .map_err(OxmlError::from)?
643            {
644                Event::Text(value) => text.value.push_str(&decode_plain(&value)),
645                Event::CData(value) => {
646                    let value = std::str::from_utf8(value.as_ref()).map_err(OxmlError::from)?;
647                    text.value.push_str(value);
648                }
649                Event::GeneralRef(value) => text.value.push_str(&resolve_entity(&value)),
650                Event::End(element) if matches_local_name(element.name().as_ref(), b"t") => {
651                    if text.space == TextSpace::Default
652                        && (text.value.chars().next().is_some_and(char::is_whitespace)
653                            || text
654                                .value
655                                .chars()
656                                .next_back()
657                                .is_some_and(char::is_whitespace))
658                    {
659                        text.space = TextSpace::Preserve;
660                    }
661                    return Ok(text);
662                }
663                Event::Start(element) | Event::Empty(element) => {
664                    return Err(unexpected(&element));
665                }
666                Event::Eof => return Err(missing_end("t")),
667                _ => {}
668            }
669            buffer.clear();
670        }
671    }
672
673    fn write_xml<W: Write>(&self, writer: &mut Writer<W>) -> Result<()> {
674        if text_has_physical_owner(&self.source_tag, &self.source_namespaces)
675            && self.value == self.source_value
676            && self.space == self.source_space
677            && let Some(raw) = &self.source_xml
678        {
679            writer
680                .get_mut()
681                .write_all(&text_close_source_owner(
682                    raw,
683                    &self.source_namespaces,
684                    "a:t",
685                )?)
686                .map_err(OxmlError::from)?;
687            return Ok(());
688        }
689        let preserve = self.space == TextSpace::Preserve
690            || self.value.chars().next().is_some_and(char::is_whitespace)
691            || self
692                .value
693                .chars()
694                .next_back()
695                .is_some_and(char::is_whitespace);
696        let owned_tag = text_source_tag("a:t", &self.source_tag, &self.source_namespaces);
697        let tag = owned_tag.as_str();
698        let mut start = BytesStart::new(tag);
699        if preserve {
700            start.push_attribute(("xml:space", "preserve"));
701        }
702        if self.source_attributes.is_empty() {
703            push_raw_attributes(&mut start, &self.raw_attributes);
704        } else {
705            let unknown: String = self
706                .source_attributes
707                .iter()
708                .filter(|(key, _)| key != "xml:space")
709                .map(|(_, token)| token.as_str())
710                .collect();
711            let generated = std::str::from_utf8(start.as_ref())
712                .map_err(OxmlError::from)?
713                .to_owned();
714            start = BytesStart::from_content(format!("{generated}{unknown}"), tag.len());
715        }
716        let existing_keys: Vec<String> = start
717            .attributes()
718            .map(|attribute| {
719                attribute.map_err(OxmlError::from).and_then(|attribute| {
720                    std::str::from_utf8(attribute.key.as_ref())
721                        .map(str::to_owned)
722                        .map_err(OxmlError::from)
723                })
724            })
725            .collect::<std::result::Result<_, OxmlError>>()?;
726        let source = self
727            .source_xml
728            .clone()
729            .unwrap_or(capture_empty_element(&start)?);
730        for (prefix, uri) in text_owner_closure(&source, &self.source_namespaces)? {
731            let key = if prefix.is_empty() {
732                "xmlns".to_owned()
733            } else {
734                format!("xmlns:{prefix}")
735            };
736            if !existing_keys.contains(&key) {
737                start.push_attribute((key.as_str(), uri.as_str()));
738            }
739        }
740        if self.value.is_empty() {
741            return write_empty(writer, start);
742        }
743        write_start(writer, start)?;
744        writer
745            .write_event(Event::Text(BytesText::new(&self.value)))
746            .map_err(OxmlError::from)?;
747        write_end(writer, tag)
748    }
749}
750
751/// Spells each character XML 1.0 cannot carry as `_xHHHH_`, as python-pptx's
752/// run text setter does, so text holding one can still be saved.
753pub fn escape_invalid_xml_characters(text: &str) -> Cow<'_, str> {
754    let is_invalid = |character: char| !oxml_core::xml::is_xml_1_0_character(character);
755    if !text.chars().any(is_invalid) {
756        return Cow::Borrowed(text);
757    }
758    let mut escaped = String::with_capacity(text.len() + 6);
759    for character in text.chars() {
760        if is_invalid(character) {
761            escaped.push_str(&format!("_x{:04X}_", u32::from(character)));
762        } else {
763            escaped.push(character);
764        }
765    }
766    Cow::Owned(escaped)
767}
768
769/// The seven members of `ST_TextAlignType`.
770#[derive(Clone, Copy, Debug, Eq, PartialEq)]
771pub enum TextAlignment {
772    Left,
773    Center,
774    Right,
775    Justified,
776    JustifiedLow,
777    Distributed,
778    ThaiDistributed,
779}
780
781impl TextAlignment {
782    fn parse(value: &str) -> Option<Self> {
783        match value {
784            "l" => Some(Self::Left),
785            "ctr" => Some(Self::Center),
786            "r" => Some(Self::Right),
787            "just" => Some(Self::Justified),
788            "justLow" => Some(Self::JustifiedLow),
789            "dist" => Some(Self::Distributed),
790            "thaiDist" => Some(Self::ThaiDistributed),
791            _ => None,
792        }
793    }
794
795    const fn as_str(self) -> &'static str {
796        match self {
797            Self::Left => "l",
798            Self::Center => "ctr",
799            Self::Right => "r",
800            Self::Justified => "just",
801            Self::JustifiedLow => "justLow",
802            Self::Distributed => "dist",
803            Self::ThaiDistributed => "thaiDist",
804        }
805    }
806}
807
808/// DrawingML paragraph spacing in percentage text or centipoints.
809#[derive(Clone, Debug, Eq, PartialEq)]
810pub enum TextSpacing {
811    Percent(String),
812    Points(i32),
813}
814
815impl TextSpacing {
816    fn from_xml(xml: &[u8], wrapper: &[u8]) -> Result<Self> {
817        parse_complete(
818            xml,
819            wrapper,
820            |reader, start| Self::from_element(reader, start, wrapper),
821            |_| {
822                Err(TextError::UnexpectedElement(
823                    String::from_utf8_lossy(wrapper).into_owned(),
824                ))
825            },
826        )
827    }
828
829    fn from_element(
830        reader: &mut Reader<&[u8]>,
831        start: &BytesStart<'_>,
832        wrapper: &[u8],
833    ) -> Result<Self> {
834        text_validate_root(start)?;
835        let mut spacing = None;
836        let mut buffer = Vec::new();
837        loop {
838            match reader
839                .read_event_into(&mut buffer)
840                .map_err(OxmlError::from)?
841            {
842                Event::Empty(element) if matches_local_name(element.name().as_ref(), b"spcPct") => {
843                    text_validate_root(&element)?;
844                    if spacing.is_some() {
845                        return Err(duplicate("text spacing choice"));
846                    }
847                    let value = required_attr(&element, b"val")?;
848                    validate_spacing_percent(&value)?;
849                    spacing = Some(Self::Percent(value));
850                }
851                Event::Empty(element) if matches_local_name(element.name().as_ref(), b"spcPts") => {
852                    text_validate_root(&element)?;
853                    if spacing.is_some() {
854                        return Err(duplicate("text spacing choice"));
855                    }
856                    let value = parse_i32_attr(&element, b"val", 0, MAX_TEXT_SPACING_POINT)?;
857                    spacing = Some(Self::Points(value));
858                }
859                Event::Start(element)
860                    if matches_local_name(element.name().as_ref(), b"spcPct")
861                        || matches_local_name(element.name().as_ref(), b"spcPts") =>
862                {
863                    text_validate_root(&element)?;
864                    if spacing.is_some() {
865                        return Err(duplicate("text spacing choice"));
866                    }
867                    let name = local_name(element.name().as_ref()).to_vec();
868                    let value = if name == b"spcPct" {
869                        let value = required_attr(&element, b"val")?;
870                        validate_spacing_percent(&value)?;
871                        Self::Percent(value)
872                    } else {
873                        Self::Points(parse_i32_attr(&element, b"val", 0, MAX_TEXT_SPACING_POINT)?)
874                    };
875                    ensure_empty(reader, &name)?;
876                    spacing = Some(value);
877                }
878                Event::Start(element) | Event::Empty(element) => return Err(unexpected(&element)),
879                Event::End(element) if matches_local_name(element.name().as_ref(), wrapper) => {
880                    return spacing.ok_or_else(|| {
881                        TextError::UnexpectedElement(String::from_utf8_lossy(wrapper).into_owned())
882                    });
883                }
884                Event::Eof => return Err(missing_end(&String::from_utf8_lossy(wrapper))),
885                _ => {}
886            }
887            buffer.clear();
888        }
889    }
890
891    fn write_xml<W: Write>(&self, writer: &mut Writer<W>, wrapper: &str) -> Result<()> {
892        write_start(writer, BytesStart::new(wrapper))?;
893        match self {
894            Self::Percent(value) => {
895                validate_spacing_percent(value)?;
896                let mut start = BytesStart::new("a:spcPct");
897                start.push_attribute(("val", value.as_str()));
898                write_empty(writer, start)?;
899            }
900            Self::Points(value) if (0..=MAX_TEXT_SPACING_POINT).contains(value) => {
901                let value = value.to_string();
902                let mut start = BytesStart::new("a:spcPts");
903                start.push_attribute(("val", value.as_str()));
904                write_empty(writer, start)?;
905            }
906            Self::Points(value) => {
907                return Err(invalid_attribute("spcPts", "val", &value.to_string()));
908            }
909        }
910        write_end(writer, wrapper)
911    }
912}
913
914/// Character spacing in centipoints or a strict universal measure.
915#[derive(Clone, Debug, Eq, PartialEq)]
916pub enum TextPointValue {
917    Centipoints(i32),
918    UniversalMeasure(String),
919}
920
921impl TextPointValue {
922    fn parse(value: String) -> Result<Self> {
923        if let Ok(point) = value.parse::<i32>() {
924            if (-MAX_TEXT_POINT..=MAX_TEXT_POINT).contains(&point) {
925                return Ok(Self::Centipoints(point));
926            }
927            return Err(invalid_attribute("rPr", "spc", &value));
928        }
929        if is_universal_measure(&value) {
930            Ok(Self::UniversalMeasure(value))
931        } else {
932            Err(invalid_attribute("rPr", "spc", &value))
933        }
934    }
935
936    fn as_xml(&self) -> Result<String> {
937        match self {
938            Self::Centipoints(value) if (-MAX_TEXT_POINT..=MAX_TEXT_POINT).contains(value) => {
939                Ok(value.to_string())
940            }
941            Self::Centipoints(value) => Err(invalid_attribute("rPr", "spc", &value.to_string())),
942            Self::UniversalMeasure(value) if is_universal_measure(value) => Ok(value.clone()),
943            Self::UniversalMeasure(value) => Err(invalid_attribute("rPr", "spc", value)),
944        }
945    }
946}
947
948/// DrawingML underline styles.
949#[derive(Clone, Copy, Debug, Eq, PartialEq)]
950pub enum TextUnderline {
951    None,
952    Words,
953    Single,
954    Double,
955    Heavy,
956    Dotted,
957    DottedHeavy,
958    Dash,
959    DashHeavy,
960    DashLong,
961    DashLongHeavy,
962    DotDash,
963    DotDashHeavy,
964    DotDotDash,
965    DotDotDashHeavy,
966    Wavy,
967    WavyHeavy,
968    WavyDouble,
969}
970
971impl TextUnderline {
972    fn parse(value: &str) -> Option<Self> {
973        Some(match value {
974            "none" => Self::None,
975            "words" => Self::Words,
976            "sng" => Self::Single,
977            "dbl" => Self::Double,
978            "heavy" => Self::Heavy,
979            "dotted" => Self::Dotted,
980            "dottedHeavy" => Self::DottedHeavy,
981            "dash" => Self::Dash,
982            "dashHeavy" => Self::DashHeavy,
983            "dashLong" => Self::DashLong,
984            "dashLongHeavy" => Self::DashLongHeavy,
985            "dotDash" => Self::DotDash,
986            "dotDashHeavy" => Self::DotDashHeavy,
987            "dotDotDash" => Self::DotDotDash,
988            "dotDotDashHeavy" => Self::DotDotDashHeavy,
989            "wavy" => Self::Wavy,
990            "wavyHeavy" => Self::WavyHeavy,
991            "wavyDbl" => Self::WavyDouble,
992            _ => return None,
993        })
994    }
995
996    /// Returns the `ST_TextUnderlineType` token as written in the file.
997    pub const fn as_str(self) -> &'static str {
998        match self {
999            Self::None => "none",
1000            Self::Words => "words",
1001            Self::Single => "sng",
1002            Self::Double => "dbl",
1003            Self::Heavy => "heavy",
1004            Self::Dotted => "dotted",
1005            Self::DottedHeavy => "dottedHeavy",
1006            Self::Dash => "dash",
1007            Self::DashHeavy => "dashHeavy",
1008            Self::DashLong => "dashLong",
1009            Self::DashLongHeavy => "dashLongHeavy",
1010            Self::DotDash => "dotDash",
1011            Self::DotDashHeavy => "dotDashHeavy",
1012            Self::DotDotDash => "dotDotDash",
1013            Self::DotDotDashHeavy => "dotDotDashHeavy",
1014            Self::Wavy => "wavy",
1015            Self::WavyHeavy => "wavyHeavy",
1016            Self::WavyDouble => "wavyDbl",
1017        }
1018    }
1019}
1020
1021/// DrawingML strike styles.
1022#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1023pub enum TextStrike {
1024    None,
1025    Single,
1026    Double,
1027}
1028
1029impl TextStrike {
1030    fn parse(value: &str) -> Option<Self> {
1031        match value {
1032            "noStrike" => Some(Self::None),
1033            "sngStrike" => Some(Self::Single),
1034            "dblStrike" => Some(Self::Double),
1035            _ => None,
1036        }
1037    }
1038
1039    const fn as_str(self) -> &'static str {
1040        match self {
1041            Self::None => "noStrike",
1042            Self::Single => "sngStrike",
1043            Self::Double => "dblStrike",
1044        }
1045    }
1046}
1047
1048/// A DrawingML text typeface reference.
1049#[derive(Clone, Debug, Eq, PartialEq)]
1050pub struct TextFont {
1051    pub typeface: String,
1052    raw_attributes: Vec<(String, String)>,
1053}
1054
1055impl TextFont {
1056    pub fn new(typeface: impl Into<String>) -> Result<Self> {
1057        let typeface = typeface.into();
1058        oxml_core::xml::reject_non_xml_characters("font typeface", &typeface)?;
1059        if typeface.is_empty() {
1060            return Err(invalid_attribute("font", "typeface", ""));
1061        }
1062        Ok(Self {
1063            typeface,
1064            raw_attributes: Vec::new(),
1065        })
1066    }
1067
1068    pub(crate) fn from_xml(xml: &[u8], expected: &[u8]) -> Result<Self> {
1069        parse_complete(
1070            xml,
1071            expected,
1072            |reader, start| {
1073                let font = Self::from_start(start)?;
1074                ensure_empty(reader, expected)?;
1075                Ok(font)
1076            },
1077            Self::from_start,
1078        )
1079    }
1080
1081    fn from_start(start: &BytesStart<'_>) -> Result<Self> {
1082        text_validate_root(start)?;
1083        Ok(Self {
1084            typeface: required_attr(start, b"typeface")?,
1085            raw_attributes: capture_raw_attributes(start, &[b"typeface"])?,
1086        })
1087    }
1088
1089    pub(crate) fn write_xml<W: Write>(&self, writer: &mut Writer<W>, tag: &str) -> Result<()> {
1090        if self.typeface.is_empty() {
1091            return Err(invalid_attribute(tag, "typeface", ""));
1092        }
1093        let mut start = BytesStart::new(tag);
1094        start.push_attribute(("typeface", self.typeface.as_str()));
1095        push_raw_attributes(&mut start, &self.raw_attributes);
1096        write_empty(writer, start)
1097    }
1098}
1099
1100/// Hyperlink attributes retained on text character properties.
1101#[derive(Clone, Debug, Default)]
1102pub struct TextHyperlink {
1103    pub relationship_id: Option<String>,
1104    pub invalid_url: Option<String>,
1105    pub action: Option<String>,
1106    pub target_frame: Option<String>,
1107    pub tooltip: Option<String>,
1108    pub history: Option<bool>,
1109    pub highlight_click: Option<bool>,
1110    pub end_sound: Option<bool>,
1111    raw_attributes: Vec<(String, String)>,
1112    raw_children: OrderedRawChildren,
1113    source_xml: Option<Vec<u8>>,
1114    source_namespaces: Vec<(String, String)>,
1115    source_values: Vec<(String, Option<String>)>,
1116    source_id_key: Option<String>,
1117}
1118impl PartialEq for TextHyperlink {
1119    fn eq(&self, other: &Self) -> bool {
1120        self.relationship_id == other.relationship_id
1121            && self.invalid_url == other.invalid_url
1122            && self.action == other.action
1123            && self.target_frame == other.target_frame
1124            && self.tooltip == other.tooltip
1125            && self.history == other.history
1126            && self.highlight_click == other.highlight_click
1127            && self.end_sound == other.end_sound
1128            && self
1129                .raw_attributes
1130                .iter()
1131                .filter(|(name, value)| {
1132                    !((name == "xmlns:a" && value == A_NS)
1133                        || (name == "xmlns:r" && value == TEXT_REL_NS))
1134                })
1135                .eq(other.raw_attributes.iter().filter(|(name, value)| {
1136                    !((name == "xmlns:a" && value == A_NS)
1137                        || (name == "xmlns:r" && value == TEXT_REL_NS))
1138                }))
1139            && self.raw_children == other.raw_children
1140    }
1141}
1142impl Eq for TextHyperlink {}
1143
1144impl TextHyperlink {
1145    fn from_xml_with_namespaces(
1146        xml: &[u8],
1147        expected: &[u8],
1148        inherited: &TextNamespaceScope,
1149    ) -> Result<Self> {
1150        let mut result = parse_complete(
1151            xml,
1152            expected,
1153            |reader, start| Self::from_element_with_namespaces(reader, start, expected, inherited),
1154            |start| Self::from_start_with_namespaces(start, inherited),
1155        )?;
1156        result.source_xml = Some(xml.to_vec());
1157        result.source_values = result.attribute_values();
1158        Ok(result)
1159    }
1160
1161    fn from_start_with_namespaces(
1162        start: &BytesStart<'_>,
1163        inherited: &TextNamespaceScope,
1164    ) -> Result<Self> {
1165        text_validate_root(start)?;
1166        let namespaces = text_fragment_scope(start, inherited)?;
1167        if text_owned_name(start, &namespaces)?.is_empty() {
1168            return Err(invalid_attribute("hyperlink", "namespace", "foreign owner"));
1169        }
1170        let mut relationship_id = None;
1171        let mut source_id_key = None;
1172        for attribute in start.attributes() {
1173            let attribute = attribute.map_err(OxmlError::from)?;
1174            let key = std::str::from_utf8(attribute.key.as_ref()).map_err(OxmlError::from)?;
1175            if let Some((prefix, "id")) = key.split_once(':')
1176                && namespaces
1177                    .lookup
1178                    .iter()
1179                    .any(|(name, uri)| name == prefix && uri == TEXT_REL_NS)
1180            {
1181                if source_id_key.is_some() {
1182                    return Err(duplicate("hyperlink relationship id"));
1183                }
1184                relationship_id = Some(
1185                    attribute
1186                        .decoded_and_normalized_value(XmlVersion::Implicit1_0, start.decoder())
1187                        .map_err(OxmlError::from)?
1188                        .into_owned(),
1189                );
1190                source_id_key = Some(key.to_owned());
1191            }
1192        }
1193        let mut raw_attributes = capture_raw_attributes(
1194            start,
1195            &[
1196                b"invalidUrl",
1197                b"action",
1198                b"tgtFrame",
1199                b"tooltip",
1200                b"history",
1201                b"highlightClick",
1202                b"endSnd",
1203            ],
1204        )?;
1205        raw_attributes.retain(|(key, _)| Some(key) != source_id_key.as_ref());
1206        Ok(Self {
1207            relationship_id,
1208            invalid_url: text_attr(start, b"invalidUrl")?,
1209            action: text_attr(start, b"action")?,
1210            target_frame: text_attr(start, b"tgtFrame")?,
1211            tooltip: text_attr(start, b"tooltip")?,
1212            history: parse_optional_bool(start, b"history")?,
1213            highlight_click: parse_optional_bool(start, b"highlightClick")?,
1214            end_sound: parse_optional_bool(start, b"endSnd")?,
1215            raw_attributes,
1216            raw_children: OrderedRawChildren::default(),
1217            source_xml: None,
1218            source_namespaces: namespaces.physical,
1219            source_values: Vec::new(),
1220            source_id_key,
1221        })
1222    }
1223
1224    fn from_element_with_namespaces(
1225        reader: &mut Reader<&[u8]>,
1226        start: &BytesStart<'_>,
1227        expected: &[u8],
1228        inherited: &TextNamespaceScope,
1229    ) -> Result<Self> {
1230        let mut hyperlink = Self::from_start_with_namespaces(start, inherited)?;
1231        let namespaces = text_fragment_scope(start, inherited)?;
1232        let mut boundary = 0;
1233        let mut seen = [false; 2];
1234        let mut buffer = Vec::new();
1235        loop {
1236            match reader
1237                .read_event_into(&mut buffer)
1238                .map_err(OxmlError::from)?
1239            {
1240                Event::Start(element) => {
1241                    let name = text_owned_name(&element, &namespaces)?;
1242                    let raw = capture_element(reader, &element)?;
1243                    capture_hyperlink_raw(&mut hyperlink, &name, raw, &mut boundary, &mut seen)?;
1244                }
1245                Event::Empty(element) => {
1246                    let name = text_owned_name(&element, &namespaces)?;
1247                    let raw = capture_empty_element(&element)?;
1248                    capture_hyperlink_raw(&mut hyperlink, &name, raw, &mut boundary, &mut seen)?;
1249                }
1250                Event::End(element) if matches_local_name(element.name().as_ref(), expected) => {
1251                    return Ok(hyperlink);
1252                }
1253                Event::Eof => return Err(missing_end(&String::from_utf8_lossy(expected))),
1254                _ => {}
1255            }
1256            buffer.clear();
1257        }
1258    }
1259
1260    fn write_xml<W: Write>(&self, writer: &mut Writer<W>, tag: &str) -> Result<()> {
1261        if self.source_namespaces.iter().any(|(_, uri)| uri == A_NS)
1262            && let Some(source) = &self.source_xml
1263        {
1264            return self.write_source_xml(writer, tag, source);
1265        }
1266        let mut start = BytesStart::new(tag);
1267        // A newly authored click must own its relationship prefix locally.
1268        // Preserve opaque raw r:* attributes by selecting a different prefix.
1269        if let Some(id) = self.relationship_id.as_deref() {
1270            let occupied = |prefix: &str| {
1271                self.raw_attributes.iter().any(|(key, value)| {
1272                    key.starts_with(&format!("{prefix}:"))
1273                        || (key == &format!("xmlns:{prefix}") && value != TEXT_REL_NS)
1274                })
1275            };
1276            let prefix = (0..=self.raw_attributes.len() + 1)
1277                .map(|index| {
1278                    if index == 0 {
1279                        "r".to_owned()
1280                    } else {
1281                        format!("rLink{index}")
1282                    }
1283                })
1284                .find(|prefix| !occupied(prefix))
1285                .ok_or_else(|| {
1286                    invalid_attribute("hyperlink", "namespace", "no relationship prefix")
1287                })?;
1288            let declaration = format!("xmlns:{prefix}");
1289            if self.source_xml.is_none()
1290                && !self
1291                    .raw_attributes
1292                    .iter()
1293                    .any(|(key, value)| key == &declaration && value == TEXT_REL_NS)
1294            {
1295                start.push_attribute((declaration.as_str(), TEXT_REL_NS));
1296            }
1297            let key = format!("{prefix}:id");
1298            start.push_attribute((key.as_str(), id));
1299        }
1300        push_optional_attr(&mut start, "invalidUrl", self.invalid_url.as_deref());
1301        push_optional_attr(&mut start, "action", self.action.as_deref());
1302        push_optional_attr(&mut start, "tgtFrame", self.target_frame.as_deref());
1303        push_optional_attr(&mut start, "tooltip", self.tooltip.as_deref());
1304        push_optional_bool(&mut start, "history", self.history);
1305        push_optional_bool(&mut start, "highlightClick", self.highlight_click);
1306        push_optional_bool(&mut start, "endSnd", self.end_sound);
1307        push_raw_attributes(&mut start, &self.raw_attributes);
1308        if self.raw_children.is_empty() {
1309            return write_empty(writer, start);
1310        }
1311        write_start(writer, start)?;
1312        for boundary in 0..=2 {
1313            emit_raw(writer, self.raw_children.at(boundary))?;
1314        }
1315        write_end(writer, tag)
1316    }
1317
1318    fn attribute_values(&self) -> Vec<(String, Option<String>)> {
1319        vec![
1320            ("id".to_owned(), self.relationship_id.clone()),
1321            ("invalidUrl".to_owned(), self.invalid_url.clone()),
1322            ("action".to_owned(), self.action.clone()),
1323            ("tgtFrame".to_owned(), self.target_frame.clone()),
1324            ("tooltip".to_owned(), self.tooltip.clone()),
1325            (
1326                "history".to_owned(),
1327                self.history.map(|v| if v { "1" } else { "0" }.to_owned()),
1328            ),
1329            (
1330                "highlightClick".to_owned(),
1331                self.highlight_click
1332                    .map(|v| if v { "1" } else { "0" }.to_owned()),
1333            ),
1334            (
1335                "endSnd".to_owned(),
1336                self.end_sound.map(|v| if v { "1" } else { "0" }.to_owned()),
1337            ),
1338        ]
1339    }
1340
1341    fn write_source_xml<W: Write>(
1342        &self,
1343        writer: &mut Writer<W>,
1344        tag: &str,
1345        source: &[u8],
1346    ) -> Result<()> {
1347        let mut reader = Reader::from_reader(source);
1348        let mut buffer = Vec::new();
1349        let (start, empty) = match reader
1350            .read_event_into(&mut buffer)
1351            .map_err(OxmlError::from)?
1352        {
1353            Event::Start(start) => (start, false),
1354            Event::Empty(start) => (start, true),
1355            _ => return Err(invalid_attribute("hyperlink", "source", "missing owner")),
1356        };
1357        let head_end = reader.buffer_position() as usize;
1358        let tokens = text_attribute_tokens(&start)?;
1359        let current = self.attribute_values();
1360        let changed: Vec<_> = current
1361            .iter()
1362            .filter(|(name, value)| {
1363                self.source_values
1364                    .iter()
1365                    .find(|(old, _)| old == name)
1366                    .map(|(_, original)| original)
1367                    != Some(value)
1368            })
1369            .collect();
1370        let original_name = std::str::from_utf8(start.name().as_ref())
1371            .map_err(OxmlError::from)?
1372            .to_owned();
1373        let owned_tag = text_source_tag(tag, &original_name, &self.source_namespaces);
1374        let tag = owned_tag.as_str();
1375        let mut body = tag.to_owned();
1376        let mut emitted = Vec::new();
1377        let mut id_key = self.source_id_key.clone();
1378        let mut namespace_additions = Vec::new();
1379        let mut referenced = text_referenced_prefixes(source)?;
1380        referenced.push(
1381            tag.split_once(':')
1382                .map_or("", |(prefix, _)| prefix)
1383                .to_owned(),
1384        );
1385        if self.relationship_id.is_some() && id_key.is_none() {
1386            let prefix = self
1387                .source_namespaces
1388                .iter()
1389                .find(|(prefix, uri)| !prefix.is_empty() && uri == TEXT_REL_NS)
1390                .map(|(prefix, _)| prefix.clone())
1391                .or_else(|| {
1392                    (0..=self.source_namespaces.len())
1393                        .map(|index| {
1394                            if index == 0 {
1395                                "r".to_owned()
1396                            } else {
1397                                format!("rLink{index}")
1398                            }
1399                        })
1400                        .find(|prefix| {
1401                            !self
1402                                .source_namespaces
1403                                .iter()
1404                                .any(|(name, uri)| name == prefix && uri != TEXT_REL_NS)
1405                        })
1406                })
1407                .ok_or_else(|| {
1408                    invalid_attribute("hyperlink", "namespace", "no relationship prefix")
1409                })?;
1410            if !self
1411                .source_namespaces
1412                .iter()
1413                .any(|(name, uri)| name == &prefix && uri == TEXT_REL_NS)
1414            {
1415                namespace_additions.push((prefix.clone(), TEXT_REL_NS.to_owned()));
1416            }
1417            referenced.push(prefix.clone());
1418            id_key = Some(format!("{prefix}:id"));
1419        }
1420        for (key, token) in &tokens {
1421            // Only the URI-qualified source token represents relationship_id.
1422            // An unqualified lexical id is opaque, even when no owned id exists.
1423            let owned = if self.source_id_key.as_ref() == Some(key) {
1424                Some("id")
1425            } else if key == "id" {
1426                None
1427            } else {
1428                Some(key.as_str())
1429            };
1430            if let Some((name, value)) = changed
1431                .iter()
1432                .find(|(name, _)| Some(name.as_str()) == owned)
1433            {
1434                emitted.push(name.as_str());
1435                if let Some(value) = value {
1436                    let key = if owned == Some("id") {
1437                        id_key.as_deref().unwrap_or("r:id")
1438                    } else {
1439                        key.as_str()
1440                    };
1441                    body.push_str(&format!(" {key}=\"{}\"", quick_xml::escape::escape(value)));
1442                }
1443            } else {
1444                body.push_str(token);
1445            }
1446        }
1447        for (name, value) in &changed {
1448            if emitted.contains(&name.as_str()) {
1449                continue;
1450            }
1451            if let Some(value) = value {
1452                let key = if name == "id" {
1453                    id_key.as_deref().unwrap_or("r:id")
1454                } else {
1455                    name.as_str()
1456                };
1457                body.push_str(&format!(" {key}=\"{}\"", quick_xml::escape::escape(value)));
1458            }
1459        }
1460        for (prefix, uri) in self
1461            .source_namespaces
1462            .iter()
1463            .chain(namespace_additions.iter())
1464        {
1465            if !referenced.contains(prefix) {
1466                continue;
1467            }
1468            let key = if prefix.is_empty() {
1469                "xmlns".to_owned()
1470            } else {
1471                format!("xmlns:{prefix}")
1472            };
1473            if tokens.iter().any(|(name, _)| name == &key) {
1474                continue;
1475            }
1476            body.push_str(&format!(" {key}=\"{}\"", quick_xml::escape::escape(uri)));
1477        }
1478        let replacement = BytesStart::from_content(body, tag.len());
1479        if empty {
1480            return write_empty(writer, replacement);
1481        }
1482        write_start(writer, replacement)?;
1483        let end_start = source
1484            .iter()
1485            .rposition(|byte| *byte == b'<')
1486            .ok_or_else(|| missing_end("hyperlink"))?;
1487        if end_start < head_end {
1488            return Err(missing_end("hyperlink"));
1489        }
1490        writer
1491            .get_mut()
1492            .write_all(&source[head_end..end_start])
1493            .map_err(OxmlError::from)?;
1494        write_end(writer, tag)
1495    }
1496}
1497
1498fn capture_hyperlink_raw(
1499    hyperlink: &mut TextHyperlink,
1500    name: &[u8],
1501    raw: Vec<u8>,
1502    boundary: &mut usize,
1503    seen: &mut [bool; 2],
1504) -> Result<()> {
1505    let slot = match name {
1506        b"snd" => Some(0),
1507        b"extLst" => Some(1),
1508        _ => None,
1509    };
1510    if let Some(slot) = slot {
1511        if seen[slot] {
1512            return Err(duplicate(&String::from_utf8_lossy(name)));
1513        }
1514        seen[slot] = true;
1515        hyperlink.raw_children.push((*boundary).max(slot), raw);
1516        *boundary = (*boundary).max(slot + 1);
1517    } else {
1518        hyperlink.raw_children.push(*boundary, raw);
1519    }
1520    Ok(())
1521}
1522
1523/// DrawingML character properties shared by runs, breaks, fields, and defaults.
1524#[allow(non_camel_case_types)]
1525#[derive(Clone, Debug, Default)]
1526pub struct CT_TextCharacterProperties {
1527    pub font_size: Option<i32>,
1528    pub bold: Option<bool>,
1529    pub italic: Option<bool>,
1530    pub all_caps: Option<bool>,
1531    pub small_caps: Option<bool>,
1532    pub highlight: Option<ColorChoice>,
1533    highlight_source: Option<(Vec<u8>, std::ops::Range<usize>, ColorChoice)>,
1534    pub underline: Option<TextUnderline>,
1535    pub strike: Option<TextStrike>,
1536    pub spacing: Option<TextPointValue>,
1537    pub baseline: Option<String>,
1538    pub fill: Option<Fill>,
1539    pub latin: Option<TextFont>,
1540    pub east_asian: Option<TextFont>,
1541    pub complex_script: Option<TextFont>,
1542    pub symbol: Option<TextFont>,
1543    pub hyperlink_click: Option<TextHyperlink>,
1544    pub hyperlink_mouse_over: Option<TextHyperlink>,
1545    raw_attributes: Vec<(String, String)>,
1546    source_attributes: Vec<(String, String)>,
1547    source_tag: String,
1548    source_namespaces: Vec<(String, String)>,
1549    source_closure: Vec<(String, String)>,
1550    source_fill: Option<(Vec<u8>, Fill)>,
1551    source_latin: Option<(Vec<u8>, TextFont)>,
1552    source_east_asian: Option<(Vec<u8>, TextFont)>,
1553    source_complex_script: Option<(Vec<u8>, TextFont)>,
1554    source_symbol: Option<(Vec<u8>, TextFont)>,
1555    source_cap_modeled: bool,
1556    raw_children: OrderedRawChildren,
1557}
1558impl PartialEq for CT_TextCharacterProperties {
1559    fn eq(&self, other: &Self) -> bool {
1560        self.font_size == other.font_size
1561            && self.bold == other.bold
1562            && self.italic == other.italic
1563            && self.all_caps == other.all_caps
1564            && self.small_caps == other.small_caps
1565            && self.highlight == other.highlight
1566            && self.highlight_source == other.highlight_source
1567            && self.underline == other.underline
1568            && self.strike == other.strike
1569            && self.spacing == other.spacing
1570            && self.baseline == other.baseline
1571            && self.fill == other.fill
1572            && self.latin == other.latin
1573            && self.east_asian == other.east_asian
1574            && self.complex_script == other.complex_script
1575            && self.symbol == other.symbol
1576            && self.hyperlink_click == other.hyperlink_click
1577            && self.hyperlink_mouse_over == other.hyperlink_mouse_over
1578            && self
1579                .raw_attributes
1580                .iter()
1581                .filter(|(name, value)| {
1582                    !((name == "xmlns:a" && value == A_NS)
1583                        || (name == "xmlns:r" && value == TEXT_REL_NS))
1584                })
1585                .eq(other.raw_attributes.iter().filter(|(name, value)| {
1586                    !((name == "xmlns:a" && value == A_NS)
1587                        || (name == "xmlns:r" && value == TEXT_REL_NS))
1588                }))
1589            && self.raw_children == other.raw_children
1590    }
1591}
1592impl Eq for CT_TextCharacterProperties {}
1593
1594impl CT_TextCharacterProperties {
1595    /// Returns local character properties with absent groups inherited from defaults.
1596    pub fn with_inherited_defaults(&self, defaults: &Self) -> Result<Self> {
1597        let mut local = Writer::new(Vec::new());
1598        self.write_xml(&mut local, "a:rPr")?;
1599        let mut inherited = Writer::new(Vec::new());
1600        defaults.write_xml(&mut inherited, "a:rPr")?;
1601        let xml = text_inherit_property_xml(
1602            &local.into_inner(),
1603            &self.source_namespaces,
1604            &inherited.into_inner(),
1605            &defaults.source_namespaces,
1606        )?;
1607        Self::from_xml_with_namespaces(
1608            &xml,
1609            &TextNamespaceScope::inherited(&self.source_namespaces),
1610        )
1611    }
1612
1613    fn highlight_scope_attributes(&self) -> Vec<(String, String)> {
1614        let mut attributes = self.raw_attributes.clone();
1615        for (prefix, uri) in &self.source_namespaces {
1616            let key = if prefix.is_empty() {
1617                "xmlns".to_owned()
1618            } else {
1619                format!("xmlns:{prefix}")
1620            };
1621            if !attributes.iter().any(|(name, _)| name == &key) {
1622                attributes.push((key, uri.clone()));
1623            }
1624        }
1625        attributes
1626    }
1627
1628    /// Reads the unqualified DrawingML language without promoting or rewriting source.
1629    pub fn language(&self) -> Option<&str> {
1630        self.raw_attributes
1631            .iter()
1632            .find(|(name, _)| name == "lang")
1633            .map(|(_, value)| value.as_str())
1634    }
1635
1636    /// Replaces or removes the `lang` language tag, keeping its position
1637    /// among the preserved attributes.
1638    pub fn set_language(&mut self, language: Option<&str>) {
1639        if self.language() == language {
1640            return;
1641        }
1642        let position = self
1643            .raw_attributes
1644            .iter()
1645            .position(|(name, _)| name == "lang");
1646        match (position, language) {
1647            (Some(index), Some(value)) => self.raw_attributes[index].1 = value.to_owned(),
1648            (Some(index), None) => {
1649                self.raw_attributes.remove(index);
1650            }
1651            (None, Some(value)) => self
1652                .raw_attributes
1653                .push(("lang".to_owned(), value.to_owned())),
1654            (None, None) => {}
1655        }
1656        if !self.source_attributes.is_empty() {
1657            let position = self
1658                .source_attributes
1659                .iter()
1660                .position(|(name, _)| name == "lang");
1661            let token =
1662                language.map(|value| format!(" lang=\"{}\"", quick_xml::escape::escape(value)));
1663            match (position, token) {
1664                (Some(index), Some(token)) => self.source_attributes[index].1 = token,
1665                (Some(index), None) => {
1666                    self.source_attributes.remove(index);
1667                }
1668                (None, Some(token)) => self.source_attributes.push(("lang".to_owned(), token)),
1669                (None, None) => {}
1670            }
1671        }
1672    }
1673
1674    /// Parses one complete character-property element with any prefix.
1675    pub fn from_xml(xml: &[u8]) -> Result<Self> {
1676        let expected = root_local_name(xml)?;
1677        if !is_character_property_tag(&expected) {
1678            return Err(TextError::UnexpectedElement(
1679                String::from_utf8_lossy(&expected).into_owned(),
1680            ));
1681        }
1682        Self::from_xml_with_namespaces(xml, &TextNamespaceScope::default())
1683    }
1684
1685    pub(crate) fn from_xml_with_namespaces(
1686        xml: &[u8],
1687        inherited: &TextNamespaceScope,
1688    ) -> Result<Self> {
1689        let expected = root_local_name(xml)?;
1690        parse_complete(
1691            xml,
1692            &expected,
1693            |reader, start| Self::from_element_with_namespaces(reader, start, inherited),
1694            |start| Self::from_start_with_namespaces(start, inherited),
1695        )
1696    }
1697
1698    fn from_start_with_namespaces(
1699        start: &BytesStart<'_>,
1700        inherited: &TextNamespaceScope,
1701    ) -> Result<Self> {
1702        let mut parsed = Self::from_start(start)?;
1703        parsed.source_namespaces = text_fragment_scope(start, inherited)?.physical;
1704        parsed.source_closure =
1705            text_owner_closure(&capture_empty_element(start)?, &parsed.source_namespaces)?;
1706        Ok(parsed)
1707    }
1708
1709    fn from_start(start: &BytesStart<'_>) -> Result<Self> {
1710        text_validate_root(start)?;
1711        let element = String::from_utf8_lossy(local_name(start.name().as_ref())).into_owned();
1712        let font_size = text_attr(start, b"sz")?
1713            .map(|value| parse_i32_value(&element, "sz", &value, 100, MAX_TEXT_POINT))
1714            .transpose()?;
1715        let spacing = text_attr(start, b"spc")?
1716            .map(TextPointValue::parse)
1717            .transpose()?;
1718        let baseline = text_attr(start, b"baseline")?;
1719        if let Some(value) = baseline.as_deref() {
1720            validate_baseline(value)?;
1721        }
1722        let all_caps = match text_attr(start, b"cap")?.as_deref() {
1723            Some("all") => Some(true),
1724            Some("none") => Some(false),
1725            _ => None,
1726        };
1727        let small_caps = match text_attr(start, b"cap")?.as_deref() {
1728            Some("small") => Some(true),
1729            Some("none") => Some(false),
1730            _ => None,
1731        };
1732        let mut raw_attributes = capture_raw_attributes(
1733            start,
1734            &[b"sz", b"b", b"i", b"u", b"strike", b"spc", b"baseline"],
1735        )?;
1736        if all_caps.is_some() || small_caps.is_some() {
1737            raw_attributes.retain(|(name, _)| name != "cap");
1738        }
1739        Ok(Self {
1740            font_size,
1741            bold: parse_optional_bool(start, b"b")?,
1742            italic: parse_optional_bool(start, b"i")?,
1743            all_caps,
1744            small_caps,
1745            underline: parse_optional_enum(start, b"u", TextUnderline::parse)?,
1746            strike: parse_optional_enum(start, b"strike", TextStrike::parse)?,
1747            spacing,
1748            baseline,
1749            source_attributes: text_attribute_tokens(start)?,
1750            source_tag: std::str::from_utf8(start.name().as_ref())
1751                .map_err(OxmlError::from)?
1752                .to_owned(),
1753            source_namespaces: text_fragment_scope(start, &TextNamespaceScope::default())?.physical,
1754            source_closure: text_owner_closure(
1755                &capture_empty_element(start)?,
1756                &text_fragment_scope(start, &TextNamespaceScope::default())?.physical,
1757            )?,
1758            source_cap_modeled: matches!(
1759                text_attr(start, b"cap")?.as_deref(),
1760                Some("all" | "small" | "none")
1761            ),
1762            raw_attributes,
1763            ..Self::default()
1764        })
1765    }
1766
1767    fn from_element_with_namespaces(
1768        reader: &mut Reader<&[u8]>,
1769        start: &BytesStart<'_>,
1770        inherited: &TextNamespaceScope,
1771    ) -> Result<Self> {
1772        let namespaces = text_fragment_scope(start, inherited)?;
1773        let expected = local_name(start.name().as_ref()).to_vec();
1774        let mut properties = Self::from_start(start)?;
1775        properties.source_namespaces = namespaces.physical.clone();
1776        properties.source_closure = text_owner_closure(
1777            &capture_element(&mut reader.clone(), start)?,
1778            &properties.source_namespaces,
1779        )?;
1780        let mut boundary = 0;
1781        let mut seen = [false; 14];
1782        let mut buffer = Vec::new();
1783        loop {
1784            match reader
1785                .read_event_into(&mut buffer)
1786                .map_err(OxmlError::from)?
1787            {
1788                Event::Start(element) => {
1789                    let name = text_owned_name(&element, &namespaces)?;
1790                    let raw = capture_element(reader, &element)?;
1791                    properties.capture_child(&name, raw, &mut boundary, &mut seen, &namespaces)?;
1792                }
1793                Event::Empty(element) => {
1794                    let name = text_owned_name(&element, &namespaces)?;
1795                    let raw = capture_empty_element(&element)?;
1796                    properties.capture_child(&name, raw, &mut boundary, &mut seen, &namespaces)?;
1797                }
1798                Event::End(element) if matches_local_name(element.name().as_ref(), &expected) => {
1799                    return Ok(properties);
1800                }
1801                Event::Eof => return Err(missing_end(&String::from_utf8_lossy(&expected))),
1802                event @ (Event::Comment(_) | Event::PI(_) | Event::Text(_) | Event::CData(_)) => {
1803                    properties
1804                        .raw_children
1805                        .push(boundary, text_capture_opaque_event(event)?);
1806                }
1807                _ => {}
1808            }
1809            buffer.clear();
1810        }
1811    }
1812
1813    fn capture_child(
1814        &mut self,
1815        name: &[u8],
1816        raw: Vec<u8>,
1817        boundary: &mut usize,
1818        seen: &mut [bool; 14],
1819        namespaces: &TextNamespaceScope,
1820    ) -> Result<()> {
1821        let mut source_reader = Reader::from_reader(raw.as_slice());
1822        let physical_owner = match source_reader.read_event().map_err(OxmlError::from)? {
1823            Event::Start(start) | Event::Empty(start) => {
1824                let name = start.name();
1825                text_has_physical_owner(
1826                    std::str::from_utf8(name.as_ref()).map_err(OxmlError::from)?,
1827                    &text_namespace_scope(&start, &namespaces.physical)?,
1828                )
1829            }
1830            _ => false,
1831        };
1832        // Namespace lookalikes do not consume the owned highlight schema slot.
1833        let name = if name == b"highlight"
1834            && !character_highlight_is_drawingml(&raw, &self.highlight_scope_attributes())?
1835        {
1836            b"".as_slice()
1837        } else {
1838            name
1839        };
1840        let slot = character_property_slot(name);
1841        if let Some(slot) = slot {
1842            if seen[slot - 1] {
1843                return Err(duplicate(&String::from_utf8_lossy(name)));
1844            }
1845            seen[slot - 1] = true;
1846        }
1847        match name {
1848            name if is_fill(name) => {
1849                let value = Fill::from_xml(&raw)?;
1850                self.source_fill = physical_owner.then(|| (raw, value.clone()));
1851                self.fill = Some(value);
1852            }
1853            b"highlight" => {
1854                let (colour, range) =
1855                    character_highlight_colour(&raw, &self.highlight_scope_attributes())?;
1856                self.highlight = Some(colour.clone());
1857                self.highlight_source = Some((raw, range, colour));
1858            }
1859            b"latin" => {
1860                let value = TextFont::from_xml(&raw, b"latin")?;
1861                self.source_latin = physical_owner.then(|| (raw, value.clone()));
1862                self.latin = Some(value);
1863            }
1864            b"ea" => {
1865                let value = TextFont::from_xml(&raw, b"ea")?;
1866                self.source_east_asian = physical_owner.then(|| (raw, value.clone()));
1867                self.east_asian = Some(value);
1868            }
1869            b"cs" => {
1870                let value = TextFont::from_xml(&raw, b"cs")?;
1871                self.source_complex_script = physical_owner.then(|| (raw, value.clone()));
1872                self.complex_script = Some(value);
1873            }
1874            b"sym" => {
1875                let value = TextFont::from_xml(&raw, b"sym")?;
1876                self.source_symbol = physical_owner.then(|| (raw, value.clone()));
1877                self.symbol = Some(value);
1878            }
1879            b"hlinkClick" => {
1880                self.hyperlink_click = Some(TextHyperlink::from_xml_with_namespaces(
1881                    &raw,
1882                    b"hlinkClick",
1883                    namespaces,
1884                )?)
1885            }
1886            b"hlinkMouseOver" => {
1887                self.hyperlink_mouse_over = Some(TextHyperlink::from_xml_with_namespaces(
1888                    &raw,
1889                    b"hlinkMouseOver",
1890                    namespaces,
1891                )?)
1892            }
1893            _ => self.raw_children.push(
1894                slot.map_or(*boundary, |slot| (*boundary).max(slot - 1)),
1895                raw,
1896            ),
1897        }
1898        if let Some(slot) = slot {
1899            *boundary = (*boundary).max(slot);
1900        }
1901        Ok(())
1902    }
1903
1904    /// Writes character properties with a caller-selected fixed `a:` tag.
1905    pub fn write_xml<W: Write>(&self, writer: &mut Writer<W>, tag: &str) -> Result<()> {
1906        let owned_tag = text_source_tag(tag, &self.source_tag, &self.source_namespaces);
1907        let tag = owned_tag.as_str();
1908        if self.all_caps == Some(true) && self.small_caps == Some(true) {
1909            return Err(invalid_attribute(tag, "cap", "conflicting all and small"));
1910        }
1911        self.write_highlight(&mut Writer::new(Vec::new()))?;
1912        let mut start = BytesStart::new(tag);
1913        if let Some(value) = self.font_size {
1914            parse_i32_value(tag, "sz", &value.to_string(), 100, MAX_TEXT_POINT)?;
1915            push_owned_attr(&mut start, "sz", value.to_string());
1916        }
1917        push_optional_bool(&mut start, "b", self.bold);
1918        push_optional_bool(&mut start, "i", self.italic);
1919        let caps = if self.all_caps == Some(true) {
1920            Some("all")
1921        } else if self.small_caps == Some(true) {
1922            Some("small")
1923        } else if self.all_caps == Some(false) || self.small_caps == Some(false) {
1924            Some("none")
1925        } else {
1926            None
1927        };
1928        if let Some(value) = caps {
1929            start.push_attribute(("cap", value));
1930        }
1931        if let Some(value) = self.underline {
1932            start.push_attribute(("u", value.as_str()));
1933        }
1934        if let Some(value) = self.strike {
1935            start.push_attribute(("strike", value.as_str()));
1936        }
1937        if let Some(value) = &self.spacing {
1938            push_owned_attr(&mut start, "spc", value.as_xml()?);
1939        }
1940        if let Some(value) = self.baseline.as_deref() {
1941            validate_baseline(value)?;
1942            start.push_attribute(("baseline", value));
1943        }
1944        if self.source_attributes.is_empty() {
1945            push_raw_attributes(&mut start, &self.raw_attributes);
1946        } else {
1947            let modeled = ["sz", "b", "i", "u", "strike", "spc", "baseline"];
1948            let preserved: Vec<String> = self
1949                .source_attributes
1950                .iter()
1951                .filter(|(key, _)| {
1952                    !modeled.contains(&key.as_str())
1953                        && !(key == "cap"
1954                            && (self.source_cap_modeled
1955                                || self.all_caps.is_some()
1956                                || self.small_caps.is_some()))
1957                })
1958                .map(|(_, token)| token.clone())
1959                .collect();
1960            let generated = std::str::from_utf8(start.as_ref())
1961                .map_err(OxmlError::from)?
1962                .to_owned();
1963            start =
1964                BytesStart::from_content(format!("{generated}{}", preserved.concat()), tag.len());
1965        }
1966
1967        let existing_keys: Vec<String> = start
1968            .attributes()
1969            .map(|attribute| {
1970                attribute.map_err(OxmlError::from).and_then(|attribute| {
1971                    std::str::from_utf8(attribute.key.as_ref())
1972                        .map(str::to_owned)
1973                        .map_err(OxmlError::from)
1974                })
1975            })
1976            .collect::<std::result::Result<_, OxmlError>>()?;
1977        for (prefix, uri) in &self.source_closure {
1978            let key = if prefix.is_empty() {
1979                "xmlns".to_owned()
1980            } else {
1981                format!("xmlns:{prefix}")
1982            };
1983            if !existing_keys.contains(&key) {
1984                start.push_attribute((key.as_str(), uri.as_str()));
1985            }
1986        }
1987        let has_modelled_children = self.highlight.is_some()
1988            || self.highlight_source.is_some()
1989            || self.fill.is_some()
1990            || self.latin.is_some()
1991            || self.east_asian.is_some()
1992            || self.complex_script.is_some()
1993            || self.symbol.is_some()
1994            || self.hyperlink_click.is_some()
1995            || self.hyperlink_mouse_over.is_some();
1996        if !has_modelled_children && self.raw_children.is_empty() {
1997            return write_empty(writer, start);
1998        }
1999        write_start(writer, start)?;
2000        emit_raw(writer, self.raw_children.at(0))?;
2001        emit_raw(writer, self.raw_children.at(1))?;
2002        if let Some(fill) = &self.fill {
2003            let mut child = Writer::new(Vec::new());
2004            {
2005                let writer = &mut child;
2006                if let Some((raw, original)) = &self.source_fill {
2007                    if fill == original {
2008                        writer.get_mut().write_all(raw).map_err(OxmlError::from)?;
2009                    } else {
2010                        fill.write_xml(writer)?;
2011                    }
2012                } else {
2013                    fill.write_xml(writer)?;
2014                }
2015            }
2016            writer
2017                .get_mut()
2018                .write_all(&text_close_generated_child(
2019                    &child.into_inner(),
2020                    &self.source_namespaces,
2021                )?)
2022                .map_err(OxmlError::from)?;
2023        }
2024        for boundary in 2..=6 {
2025            if boundary == 4 {
2026                let mut child = Writer::new(Vec::new());
2027                self.write_highlight(&mut child)?;
2028                writer
2029                    .get_mut()
2030                    .write_all(&text_close_generated_child(
2031                        &child.into_inner(),
2032                        &self.source_namespaces,
2033                    )?)
2034                    .map_err(OxmlError::from)?;
2035            }
2036            emit_raw(writer, self.raw_children.at(boundary))?;
2037        }
2038        if let Some(font) = &self.latin {
2039            let mut child = Writer::new(Vec::new());
2040            {
2041                let writer = &mut child;
2042                if let Some((raw, original)) = &self.source_latin {
2043                    if font == original {
2044                        writer.get_mut().write_all(raw).map_err(OxmlError::from)?;
2045                    } else {
2046                        font.write_xml(writer, "a:latin")?;
2047                    }
2048                } else {
2049                    font.write_xml(writer, "a:latin")?;
2050                }
2051            }
2052            writer
2053                .get_mut()
2054                .write_all(&text_close_generated_child(
2055                    &child.into_inner(),
2056                    &self.source_namespaces,
2057                )?)
2058                .map_err(OxmlError::from)?;
2059        }
2060        emit_raw(writer, self.raw_children.at(7))?;
2061        if let Some(font) = &self.east_asian {
2062            let mut child = Writer::new(Vec::new());
2063            {
2064                let writer = &mut child;
2065                if let Some((raw, original)) = &self.source_east_asian {
2066                    if font == original {
2067                        writer.get_mut().write_all(raw).map_err(OxmlError::from)?;
2068                    } else {
2069                        font.write_xml(writer, "a:ea")?;
2070                    }
2071                } else {
2072                    font.write_xml(writer, "a:ea")?;
2073                }
2074            }
2075            writer
2076                .get_mut()
2077                .write_all(&text_close_generated_child(
2078                    &child.into_inner(),
2079                    &self.source_namespaces,
2080                )?)
2081                .map_err(OxmlError::from)?;
2082        }
2083        emit_raw(writer, self.raw_children.at(8))?;
2084        if let Some(font) = &self.complex_script {
2085            let mut child = Writer::new(Vec::new());
2086            {
2087                let writer = &mut child;
2088                if let Some((raw, original)) = &self.source_complex_script {
2089                    if font == original {
2090                        writer.get_mut().write_all(raw).map_err(OxmlError::from)?;
2091                    } else {
2092                        font.write_xml(writer, "a:cs")?;
2093                    }
2094                } else {
2095                    font.write_xml(writer, "a:cs")?;
2096                }
2097            }
2098            writer
2099                .get_mut()
2100                .write_all(&text_close_generated_child(
2101                    &child.into_inner(),
2102                    &self.source_namespaces,
2103                )?)
2104                .map_err(OxmlError::from)?;
2105        }
2106        emit_raw(writer, self.raw_children.at(9))?;
2107        if let Some(font) = &self.symbol {
2108            let mut child = Writer::new(Vec::new());
2109            {
2110                let writer = &mut child;
2111                if let Some((raw, original)) = &self.source_symbol {
2112                    if font == original {
2113                        writer.get_mut().write_all(raw).map_err(OxmlError::from)?;
2114                    } else {
2115                        font.write_xml(writer, "a:sym")?;
2116                    }
2117                } else {
2118                    font.write_xml(writer, "a:sym")?;
2119                }
2120            }
2121            writer
2122                .get_mut()
2123                .write_all(&text_close_generated_child(
2124                    &child.into_inner(),
2125                    &self.source_namespaces,
2126                )?)
2127                .map_err(OxmlError::from)?;
2128        }
2129        emit_raw(writer, self.raw_children.at(10))?;
2130        if let Some(hyperlink) = &self.hyperlink_click {
2131            let mut child = Writer::new(Vec::new());
2132            {
2133                let writer = &mut child;
2134                hyperlink.write_xml(writer, "a:hlinkClick")?;
2135            }
2136            writer
2137                .get_mut()
2138                .write_all(&text_close_generated_child(
2139                    &child.into_inner(),
2140                    &self.source_namespaces,
2141                )?)
2142                .map_err(OxmlError::from)?;
2143        }
2144        emit_raw(writer, self.raw_children.at(11))?;
2145        if let Some(hyperlink) = &self.hyperlink_mouse_over {
2146            let mut child = Writer::new(Vec::new());
2147            {
2148                let writer = &mut child;
2149                hyperlink.write_xml(writer, "a:hlinkMouseOver")?;
2150            }
2151            writer
2152                .get_mut()
2153                .write_all(&text_close_generated_child(
2154                    &child.into_inner(),
2155                    &self.source_namespaces,
2156                )?)
2157                .map_err(OxmlError::from)?;
2158        }
2159        for boundary in 12..=14 {
2160            emit_raw(writer, self.raw_children.at(boundary))?;
2161        }
2162        write_end(writer, tag)
2163    }
2164
2165    /// Changes only the owned highlight colour after checking opaque-source preservation.
2166    pub fn set_highlight(&mut self, value: Option<ColorChoice>) -> Result<()> {
2167        let mut candidate = self.clone();
2168        candidate.highlight = value;
2169        candidate.write_highlight(&mut Writer::new(Vec::new()))?;
2170        *self = candidate;
2171        Ok(())
2172    }
2173
2174    fn write_highlight<W: Write>(&self, writer: &mut Writer<W>) -> Result<()> {
2175        if let Some((raw, _, original)) = &self.highlight_source
2176            && self.highlight.as_ref() != Some(original)
2177            && highlight_has_opaque_transform_attributes(raw, &self.highlight_scope_attributes())?
2178        {
2179            return Err(invalid_attribute(
2180                "highlight",
2181                "transform",
2182                "opaque transform attributes cannot be replaced or cleared",
2183            ));
2184        }
2185        match (&self.highlight_source, &self.highlight) {
2186            (Some((raw, _, original)), Some(value)) if original == value => {
2187                writer.get_mut().write_all(raw).map_err(OxmlError::from)?;
2188            }
2189            (Some((raw, range, original)), Some(value)) => {
2190                if !original.raw_children().is_empty()
2191                    && original.raw_children() != value.raw_children()
2192                {
2193                    return Err(invalid_attribute(
2194                        "highlight",
2195                        "colour",
2196                        "opaque colour children cannot be replaced",
2197                    ));
2198                }
2199                let mut reader = Reader::from_reader(&raw[range.clone()]);
2200                let event = reader.read_event().map_err(OxmlError::from)?;
2201                let start = match event {
2202                    Event::Start(start) | Event::Empty(start) => start,
2203                    _ => return Err(missing_end("highlight colour")),
2204                };
2205                if start.attributes().with_checks(false).any(|attribute| {
2206                    attribute.map_or(true, |attribute| {
2207                        let name = attribute.key.as_ref();
2208                        !matches!(name, b"val" | b"lastClr" | b"xmlns")
2209                            && !name.starts_with(b"xmlns:")
2210                    })
2211                }) {
2212                    return Err(invalid_attribute(
2213                        "highlight",
2214                        "colour",
2215                        "opaque colour attributes cannot be replaced",
2216                    ));
2217                }
2218                writer
2219                    .get_mut()
2220                    .write_all(&raw[..range.start])
2221                    .map_err(OxmlError::from)?;
2222                let namespaces = start
2223                    .attributes()
2224                    .with_checks(false)
2225                    .filter_map(|attribute| attribute.ok())
2226                    .filter(|attribute| {
2227                        attribute.key.as_ref() == b"xmlns"
2228                            || attribute.key.as_ref().starts_with(b"xmlns:")
2229                    })
2230                    .map(|attribute| {
2231                        Ok((
2232                            String::from_utf8_lossy(attribute.key.as_ref()).into_owned(),
2233                            attribute
2234                                .normalized_value(XmlVersion::Implicit1_0)
2235                                .map_err(OxmlError::from)?
2236                                .into_owned(),
2237                        ))
2238                    })
2239                    .collect::<Result<Vec<_>>>()?;
2240                if namespaces
2241                    .iter()
2242                    .any(|(name, uri)| name == "xmlns:a" && uri != A_NS)
2243                {
2244                    return Err(invalid_attribute(
2245                        "highlight",
2246                        "colour",
2247                        "rebound canonical namespace cannot be replaced",
2248                    ));
2249                }
2250                write_namespace_closed_highlight_colour(writer, value, &namespaces)?;
2251                writer
2252                    .get_mut()
2253                    .write_all(&raw[range.end..])
2254                    .map_err(OxmlError::from)?;
2255            }
2256            (Some((raw, range, original)), None) => {
2257                if !original.raw_children().is_empty() {
2258                    return Err(invalid_attribute(
2259                        "highlight",
2260                        "colour",
2261                        "opaque colour children cannot be cleared",
2262                    ));
2263                }
2264                let mut colour_reader = Reader::from_reader(&raw[range.clone()]);
2265                let colour_event = colour_reader.read_event().map_err(OxmlError::from)?;
2266                let colour_start = match colour_event {
2267                    Event::Start(start) | Event::Empty(start) => start,
2268                    _ => return Err(missing_end("highlight colour")),
2269                };
2270                if colour_start
2271                    .attributes()
2272                    .with_checks(false)
2273                    .any(|attribute| {
2274                        attribute.map_or(true, |attribute| {
2275                            let name = attribute.key.as_ref();
2276                            !matches!(name, b"val" | b"lastClr" | b"xmlns")
2277                                && !name.starts_with(b"xmlns:")
2278                        })
2279                    })
2280                {
2281                    return Err(invalid_attribute(
2282                        "highlight",
2283                        "colour",
2284                        "opaque colour attributes cannot be cleared",
2285                    ));
2286                }
2287                // A colourless wrapper cannot preserve opaque payload and remain valid.
2288                let mut reader = Reader::from_reader(raw.as_slice());
2289                let first = reader.read_event().map_err(OxmlError::from)?;
2290                let Event::Start(start) = first else {
2291                    return Err(invalid_attribute("highlight", "source", "not a wrapper"));
2292                };
2293                if start.attributes().with_checks(false).any(|item| {
2294                    item.map_or(true, |attribute| {
2295                        let name = attribute.key.as_ref();
2296                        name != b"xmlns" && !name.starts_with(b"xmlns:")
2297                    })
2298                }) {
2299                    return Err(invalid_attribute(
2300                        "highlight",
2301                        "source",
2302                        "opaque wrapper attributes cannot be cleared",
2303                    ));
2304                }
2305                let prefix_end = reader.buffer_position() as usize;
2306                let suffix_start = raw[range.end..]
2307                    .iter()
2308                    .position(|byte| *byte == b'<')
2309                    .map(|offset| range.end + offset)
2310                    .ok_or_else(|| missing_end("highlight"))?;
2311                if !raw[prefix_end..range.start]
2312                    .iter()
2313                    .all(u8::is_ascii_whitespace)
2314                    || !raw[range.end..suffix_start]
2315                        .iter()
2316                        .all(u8::is_ascii_whitespace)
2317                    || !raw[suffix_start..].starts_with(b"</")
2318                {
2319                    return Err(invalid_attribute(
2320                        "highlight",
2321                        "source",
2322                        "opaque wrapper children cannot be cleared",
2323                    ));
2324                }
2325            }
2326            (None, Some(value)) => {
2327                let mut start = BytesStart::new("a:highlight");
2328                start.push_attribute(("xmlns:a", A_NS));
2329                write_start(writer, start)?;
2330                value.to_xml(writer)?;
2331                write_end(writer, "a:highlight")?;
2332            }
2333            (None, None) => {}
2334        }
2335        Ok(())
2336    }
2337
2338    /// Updates direct all-caps without leaving a conflicting small-caps mode.
2339    pub fn set_all_caps(&mut self, value: Option<bool>) {
2340        self.all_caps = value;
2341        if value.is_some() {
2342            self.small_caps = None;
2343        }
2344    }
2345
2346    /// Updates direct small-caps without leaving a conflicting all-caps mode.
2347    pub fn set_small_caps(&mut self, value: Option<bool>) {
2348        self.small_caps = value;
2349        if value.is_some() {
2350            self.all_caps = None;
2351        }
2352    }
2353
2354    pub fn raw_children(&self) -> &OrderedRawChildren {
2355        &self.raw_children
2356    }
2357
2358    /// Returns these properties as formatting for new text, without the click
2359    /// and mouse-over hyperlinks, which name relationships rather than format.
2360    pub(crate) fn without_hyperlinks(mut self) -> Self {
2361        self.hyperlink_click = None;
2362        self.hyperlink_mouse_over = None;
2363        self
2364    }
2365}
2366
2367fn write_namespace_closed_highlight_colour<W: Write>(
2368    writer: &mut Writer<W>,
2369    colour: &ColorChoice,
2370    namespaces: &[(String, String)],
2371) -> Result<()> {
2372    let mut canonical = Writer::new(Vec::new());
2373    colour.to_xml(&mut canonical)?;
2374    let bytes = canonical.into_inner();
2375    let mut reader = Reader::from_reader(bytes.as_slice());
2376    match reader.read_event().map_err(OxmlError::from)? {
2377        Event::Start(start) => {
2378            let mut start = start.into_owned();
2379            start.push_attribute(("xmlns:a", A_NS));
2380            for (name, value) in namespaces {
2381                if name != "xmlns:a" {
2382                    start.push_attribute((name.as_str(), value.as_str()));
2383                }
2384            }
2385            write_start(writer, start)?;
2386        }
2387        Event::Empty(start) => {
2388            let mut start = start.into_owned();
2389            start.push_attribute(("xmlns:a", A_NS));
2390            for (name, value) in namespaces {
2391                if name != "xmlns:a" {
2392                    start.push_attribute((name.as_str(), value.as_str()));
2393                }
2394            }
2395            write_empty(writer, start)?;
2396        }
2397        _ => return Err(invalid_attribute("highlight", "colour", "missing colour")),
2398    }
2399    writer
2400        .get_mut()
2401        .write_all(&bytes[reader.buffer_position() as usize..])
2402        .map_err(OxmlError::from)?;
2403    Ok(())
2404}
2405
2406// Imported modeled transforms have no attribute-source carrier. Refuse an edit
2407// that would regenerate one and discard opaque attributes. Unchanged source
2408// remains verbatim, and namespace-foreign transform lookalikes remain raw.
2409fn highlight_has_opaque_transform_attributes(
2410    raw: &[u8],
2411    attributes: &[(String, String)],
2412) -> Result<bool> {
2413    let mut scope = BytesStart::new("scope");
2414    if !attributes.iter().any(|(name, _)| name == "xmlns:a") {
2415        scope.push_attribute(("xmlns:a", A_NS));
2416    }
2417    for (name, value) in attributes {
2418        if name == "xmlns" || name.starts_with("xmlns:") {
2419            scope.push_attribute((name.as_str(), value.as_str()));
2420        }
2421    }
2422    let mut writer = Writer::new(Vec::new());
2423    write_start(&mut writer, scope)?;
2424    writer.get_mut().extend_from_slice(raw);
2425    write_end(&mut writer, "scope")?;
2426    let xml = writer.into_inner();
2427    let mut reader = NsReader::from_reader(xml.as_slice());
2428    let mut depth = 0usize;
2429    let mut buffer = Vec::new();
2430    let mut started_transform = None;
2431    loop {
2432        let before = reader.buffer_position() as usize;
2433        let (namespace, event) = reader
2434            .read_resolved_event_into(&mut buffer)
2435            .map_err(OxmlError::from)?;
2436        match event {
2437            Event::Start(start) => {
2438                if depth == 3
2439                    && matches!(namespace, ResolveResult::Bound(Namespace(uri)) if uri == A_NS.as_bytes())
2440                    && ColorTransform::from_xml(&start)?.is_some()
2441                    && start.attributes().with_checks(false).any(|attribute| {
2442                        attribute.map_or(true, |attribute| {
2443                            let name = attribute.key.as_ref();
2444                            name != b"val" && name != b"xmlns" && !name.starts_with(b"xmlns:")
2445                        })
2446                    })
2447                {
2448                    started_transform = Some(reader.buffer_position() as usize);
2449                }
2450                depth += 1;
2451            }
2452            Event::Empty(start) => {
2453                if depth == 3
2454                    && matches!(namespace, ResolveResult::Bound(Namespace(uri)) if uri == A_NS.as_bytes())
2455                    && ColorTransform::from_xml(&start)?.is_some()
2456                    && start.attributes().with_checks(false).any(|attribute| {
2457                        attribute.map_or(true, |attribute| {
2458                            let name = attribute.key.as_ref();
2459                            name != b"val" && name != b"xmlns" && !name.starts_with(b"xmlns:")
2460                        })
2461                    })
2462                {
2463                    return Ok(true);
2464                }
2465            }
2466            Event::End(_) => {
2467                if depth == 4
2468                    && let Some(content_start) = started_transform.take()
2469                    && xml[content_start..before]
2470                        .iter()
2471                        .all(u8::is_ascii_whitespace)
2472                {
2473                    return Ok(true);
2474                }
2475                depth = depth
2476                    .checked_sub(1)
2477                    .ok_or_else(|| missing_end("highlight"))?;
2478            }
2479            Event::Eof => return Ok(false),
2480            _ => {}
2481        }
2482        buffer.clear();
2483    }
2484}
2485
2486fn character_highlight_colour(
2487    raw: &[u8],
2488    attributes: &[(String, String)],
2489) -> Result<(ColorChoice, std::ops::Range<usize>)> {
2490    let mut scope = BytesStart::new("scope");
2491    if !attributes.iter().any(|(name, _)| name == "xmlns:a") {
2492        scope.push_attribute(("xmlns:a", A_NS));
2493    }
2494    for (name, value) in attributes {
2495        if name == "xmlns" || name.starts_with("xmlns:") {
2496            scope.push_attribute((name.as_str(), value.as_str()));
2497        }
2498    }
2499    let mut writer = Writer::new(Vec::new());
2500    write_start(&mut writer, scope)?;
2501    let prefix = writer.get_ref().len();
2502    writer.get_mut().extend_from_slice(raw);
2503    write_end(&mut writer, "scope")?;
2504    let xml = writer.into_inner();
2505    let mut reader = NsReader::from_reader(xml.as_slice());
2506    let mut depth = 0usize;
2507    let mut selected = None;
2508    let mut colour_start = None;
2509    let mut buffer = Vec::new();
2510    loop {
2511        let before = reader.buffer_position() as usize;
2512        let (namespace, event) = reader
2513            .read_resolved_event_into(&mut buffer)
2514            .map_err(OxmlError::from)?;
2515        match event {
2516            Event::Start(start) => {
2517                if depth == 2
2518                    && matches!(namespace, ResolveResult::Bound(Namespace(uri)) if uri == A_NS.as_bytes())
2519                    && matches!(
2520                        local_name(start.name().as_ref()),
2521                        b"scrgbClr"
2522                            | b"srgbClr"
2523                            | b"hslClr"
2524                            | b"sysClr"
2525                            | b"schemeClr"
2526                            | b"prstClr"
2527                    )
2528                {
2529                    if selected.is_some() || colour_start.is_some() {
2530                        return Err(duplicate("highlight colour"));
2531                    }
2532                    colour_start = Some(before);
2533                }
2534                depth += 1;
2535            }
2536            Event::Empty(start)
2537                if depth == 2
2538                    && matches!(namespace, ResolveResult::Bound(Namespace(uri)) if uri == A_NS.as_bytes())
2539                    && matches!(
2540                        local_name(start.name().as_ref()),
2541                        b"scrgbClr"
2542                            | b"srgbClr"
2543                            | b"hslClr"
2544                            | b"sysClr"
2545                            | b"schemeClr"
2546                            | b"prstClr"
2547                    ) =>
2548            {
2549                if selected.is_some() {
2550                    return Err(duplicate("highlight colour"));
2551                }
2552                selected = Some(before..reader.buffer_position() as usize);
2553            }
2554            Event::End(_) => {
2555                if depth == 3
2556                    && let Some(start) = colour_start.take()
2557                {
2558                    selected = Some(start..reader.buffer_position() as usize);
2559                }
2560                depth = depth
2561                    .checked_sub(1)
2562                    .ok_or_else(|| missing_end("highlight"))?;
2563                if depth == 0 {
2564                    break;
2565                }
2566            }
2567            Event::Eof => return Err(missing_end("highlight")),
2568            _ => {}
2569        }
2570        buffer.clear();
2571    }
2572    let range = selected
2573        .ok_or_else(|| invalid_attribute("highlight", "colour", "missing qualified colour"))?;
2574    let colour = qualified_highlight_colour(&xml, range.clone())?;
2575    Ok((colour, range.start - prefix..range.end - prefix))
2576}
2577
2578fn qualified_highlight_colour(xml: &[u8], range: std::ops::Range<usize>) -> Result<ColorChoice> {
2579    let mut reader = NsReader::from_reader(xml);
2580    let mut buffer = Vec::new();
2581    let mut selected_start = None;
2582    let mut root_depth = None;
2583    let mut depth = 0usize;
2584    let mut transforms = Vec::new();
2585    let mut raw_children = OrderedRawChildren::default();
2586    let mut child_start = None;
2587    loop {
2588        let before = reader.buffer_position() as usize;
2589        let (namespace, event) = reader
2590            .read_resolved_event_into(&mut buffer)
2591            .map_err(OxmlError::from)?;
2592        match event {
2593            Event::Start(start) => {
2594                if before == range.start {
2595                    selected_start = Some(start.into_owned());
2596                    root_depth = Some(depth);
2597                } else if root_depth.is_some_and(|root| depth == root + 1) {
2598                    let transform = if matches!(namespace, ResolveResult::Bound(Namespace(uri)) if uri == A_NS.as_bytes())
2599                    {
2600                        ColorTransform::from_xml(&start)?
2601                    } else {
2602                        None
2603                    };
2604                    child_start = Some((before, reader.buffer_position() as usize, transform));
2605                }
2606                depth += 1;
2607            }
2608            Event::Empty(start) if before == range.start => {
2609                return Ok(ColorChoice::from_parts(&start, transforms, raw_children)?);
2610            }
2611            Event::Empty(start) if root_depth.is_some_and(|root| depth == root + 1) => {
2612                let qualified = matches!(namespace, ResolveResult::Bound(Namespace(uri)) if uri == A_NS.as_bytes());
2613                let transform = if qualified {
2614                    ColorTransform::from_xml(&start)?
2615                } else {
2616                    None
2617                };
2618                if let Some(transform) = transform {
2619                    transforms.push(transform);
2620                } else {
2621                    raw_children.push(
2622                        transforms.len(),
2623                        xml[before..reader.buffer_position() as usize].to_vec(),
2624                    );
2625                }
2626            }
2627            Event::End(_) => {
2628                depth = depth
2629                    .checked_sub(1)
2630                    .ok_or_else(|| missing_end("highlight colour"))?;
2631                if root_depth.is_some_and(|root| depth == root + 1) {
2632                    let (start, content_start, transform) = child_start
2633                        .take()
2634                        .ok_or_else(|| missing_end("highlight colour child"))?;
2635                    if let Some(transform) = transform.filter(|_| {
2636                        xml[content_start..before]
2637                            .iter()
2638                            .all(u8::is_ascii_whitespace)
2639                    }) {
2640                        transforms.push(transform);
2641                    } else {
2642                        raw_children.push(
2643                            transforms.len(),
2644                            xml[start..reader.buffer_position() as usize].to_vec(),
2645                        );
2646                    }
2647                }
2648                if root_depth == Some(depth) {
2649                    return Ok(ColorChoice::from_parts(
2650                        &selected_start.ok_or_else(|| missing_end("highlight colour"))?,
2651                        transforms,
2652                        raw_children,
2653                    )?);
2654                }
2655            }
2656            Event::Eof => return Err(missing_end("highlight colour")),
2657            _ if root_depth.is_some_and(|root| depth == root + 1) => {
2658                raw_children.push(
2659                    transforms.len(),
2660                    xml[before..reader.buffer_position() as usize].to_vec(),
2661                );
2662            }
2663            _ => {}
2664        }
2665        buffer.clear();
2666    }
2667}
2668
2669fn character_highlight_is_drawingml(raw: &[u8], attributes: &[(String, String)]) -> Result<bool> {
2670    let mut scope = BytesStart::new("scope");
2671    if !attributes.iter().any(|(name, _)| name == "xmlns:a") {
2672        scope.push_attribute(("xmlns:a", A_NS));
2673    }
2674    for (name, value) in attributes {
2675        if name == "xmlns" || name.starts_with("xmlns:") {
2676            scope.push_attribute((name.as_str(), value.as_str()));
2677        }
2678    }
2679    let mut writer = Writer::new(Vec::new());
2680    write_start(&mut writer, scope)?;
2681    writer.get_mut().extend_from_slice(raw);
2682    write_end(&mut writer, "scope")?;
2683    let xml = writer.into_inner();
2684    let mut reader = NsReader::from_reader(xml.as_slice());
2685    let mut buffer = Vec::new();
2686    let mut in_scope = false;
2687    loop {
2688        match reader
2689            .read_resolved_event_into(&mut buffer)
2690            .map_err(OxmlError::from)?
2691        {
2692            (_, Event::Start(_)) if !in_scope => in_scope = true,
2693            (namespace, Event::Start(_) | Event::Empty(_)) => {
2694                return Ok(
2695                    matches!(namespace, ResolveResult::Bound(Namespace(uri)) if uri == A_NS.as_bytes()),
2696                );
2697            }
2698            (_, Event::Eof) => return Err(missing_end("highlight")),
2699            _ => {}
2700        }
2701        buffer.clear();
2702    }
2703}
2704
2705fn character_property_slot(name: &[u8]) -> Option<usize> {
2706    match name {
2707        b"ln" => Some(1),
2708        name if is_fill(name) => Some(2),
2709        b"effectLst" | b"effectDag" => Some(3),
2710        b"highlight" => Some(4),
2711        b"uLnTx" | b"uLn" => Some(5),
2712        b"uFillTx" | b"uFill" => Some(6),
2713        b"latin" => Some(7),
2714        b"ea" => Some(8),
2715        b"cs" => Some(9),
2716        b"sym" => Some(10),
2717        b"hlinkClick" => Some(11),
2718        b"hlinkMouseOver" => Some(12),
2719        b"rtl" => Some(13),
2720        b"extLst" => Some(14),
2721        _ => None,
2722    }
2723}
2724
2725fn is_character_property_tag(name: &[u8]) -> bool {
2726    matches!(name, b"rPr" | b"defRPr" | b"endParaRPr")
2727}
2728
2729fn is_fill(name: &[u8]) -> bool {
2730    matches!(
2731        name,
2732        b"noFill" | b"solidFill" | b"gradFill" | b"pattFill" | b"blipFill"
2733    )
2734}
2735
2736/// Paragraph attributes and the story-owned spacing and default-run children.
2737#[allow(non_camel_case_types)]
2738#[derive(Clone, Debug, Default)]
2739pub struct CT_TextParagraphProperties {
2740    pub left_margin: Option<i32>,
2741    pub right_margin: Option<i32>,
2742    pub level: Option<u8>,
2743    pub indent: Option<i32>,
2744    pub alignment: Option<TextAlignment>,
2745    pub right_to_left: Option<bool>,
2746    pub line_spacing: Option<TextSpacing>,
2747    pub space_before: Option<TextSpacing>,
2748    pub space_after: Option<TextSpacing>,
2749    pub bullet: Option<TextBullet>,
2750    pub default_run_properties: Option<CT_TextCharacterProperties>,
2751    raw_attributes: Vec<(String, String)>,
2752    source_attributes: Vec<(String, String)>,
2753    source_tag: String,
2754    source_xml: Option<Vec<u8>>,
2755    source_projection: Vec<u8>,
2756    source_namespaces: Vec<(String, String)>,
2757    raw_children: OrderedRawChildren,
2758}
2759impl PartialEq for CT_TextParagraphProperties {
2760    fn eq(&self, other: &Self) -> bool {
2761        self.left_margin == other.left_margin
2762            && self.right_margin == other.right_margin
2763            && self.level == other.level
2764            && self.indent == other.indent
2765            && self.alignment == other.alignment
2766            && self.right_to_left == other.right_to_left
2767            && self.line_spacing == other.line_spacing
2768            && self.space_before == other.space_before
2769            && self.space_after == other.space_after
2770            && self.bullet == other.bullet
2771            && self.default_run_properties == other.default_run_properties
2772            && {
2773                let mut left = self.raw_attributes.clone();
2774                let mut right = other.raw_attributes.clone();
2775                let inherited_binding =
2776                    |key: &str, value: &str, namespaces: &[(String, String)]| {
2777                        let prefix = if key == "xmlns" {
2778                            Some("")
2779                        } else {
2780                            key.strip_prefix("xmlns:")
2781                        };
2782                        prefix.is_some_and(|prefix| {
2783                            namespaces
2784                                .iter()
2785                                .any(|(name, uri)| name == prefix && uri == value)
2786                        })
2787                    };
2788                left.retain(|(key, value)| {
2789                    !(key == "xmlns:r" && value == TEXT_REL_NS)
2790                        && !inherited_binding(key, value, &other.source_namespaces)
2791                });
2792                right.retain(|(key, value)| {
2793                    !(key == "xmlns:r" && value == TEXT_REL_NS)
2794                        && !inherited_binding(key, value, &self.source_namespaces)
2795                });
2796                left.sort();
2797                right.sort();
2798                left == right
2799            }
2800            && matches!(
2801                (
2802                    text_owner_closure(self.source_xml.as_deref().unwrap_or_default(), &self.source_namespaces),
2803                    text_owner_closure(other.source_xml.as_deref().unwrap_or_default(), &other.source_namespaces),
2804                ),
2805                (Ok(left), Ok(right)) if left == right
2806            )
2807            && self.raw_children == other.raw_children
2808    }
2809}
2810impl Eq for CT_TextParagraphProperties {}
2811
2812impl CT_TextParagraphProperties {
2813    /// Returns local paragraph properties with absent groups inherited from defaults.
2814    pub fn with_inherited_defaults(&self, defaults: &Self) -> Result<Self> {
2815        let xml = text_inherit_property_xml(
2816            &self.to_xml()?,
2817            &self.source_namespaces,
2818            &defaults.to_xml()?,
2819            &defaults.source_namespaces,
2820        )?;
2821        Self::from_xml_with_namespaces(
2822            &xml,
2823            &TextNamespaceScope::inherited(&self.source_namespaces),
2824        )
2825    }
2826
2827    /// Parses one complete paragraph-property element with any prefix.
2828    pub fn from_xml(xml: &[u8]) -> Result<Self> {
2829        let expected = root_local_name(xml)?;
2830        if !is_paragraph_property_tag(&expected) {
2831            return Err(TextError::UnexpectedElement(
2832                String::from_utf8_lossy(&expected).into_owned(),
2833            ));
2834        }
2835        Self::from_xml_with_namespaces(xml, &TextNamespaceScope::default())
2836    }
2837
2838    pub(crate) fn from_xml_with_namespaces(
2839        xml: &[u8],
2840        inherited: &TextNamespaceScope,
2841    ) -> Result<Self> {
2842        let expected = root_local_name(xml)?;
2843        let mut parsed = parse_complete(
2844            xml,
2845            &expected,
2846            |reader, start| Self::from_element_model(reader, start, inherited),
2847            |start| Self::from_start_with_namespaces(start, inherited),
2848        )?;
2849        let mut writer = Writer::new(Vec::new());
2850        parsed.write_xml(&mut writer, "a:pPr")?;
2851        parsed.source_projection = writer.into_inner();
2852        parsed.source_xml = Some(xml.to_vec());
2853        Ok(parsed)
2854    }
2855
2856    fn from_start_with_namespaces(
2857        start: &BytesStart<'_>,
2858        inherited: &TextNamespaceScope,
2859    ) -> Result<Self> {
2860        let mut parsed = Self::from_start(start)?;
2861        parsed.source_namespaces = text_fragment_scope(start, inherited)?.physical;
2862        Ok(parsed)
2863    }
2864
2865    fn from_start(start: &BytesStart<'_>) -> Result<Self> {
2866        text_validate_root(start)?;
2867        let element = String::from_utf8_lossy(local_name(start.name().as_ref())).into_owned();
2868        let level = text_attr(start, b"lvl")?
2869            .map(|value| parse_i32_value(&element, "lvl", &value, 0, 8).map(|v| v as u8))
2870            .transpose()?;
2871        Ok(Self {
2872            left_margin: parse_optional_i32(start, b"marL", 0, MAX_TEXT_MARGIN)?,
2873            right_margin: parse_optional_i32(start, b"marR", 0, MAX_TEXT_MARGIN)?,
2874            level,
2875            indent: parse_optional_i32(start, b"indent", -MAX_TEXT_MARGIN, MAX_TEXT_MARGIN)?,
2876            alignment: parse_optional_enum(start, b"algn", TextAlignment::parse)?,
2877            right_to_left: parse_optional_bool(start, b"rtl")?,
2878            source_attributes: text_attribute_tokens(start)?,
2879            source_tag: std::str::from_utf8(start.name().as_ref())
2880                .map_err(OxmlError::from)?
2881                .to_owned(),
2882            raw_attributes: capture_raw_attributes(
2883                start,
2884                &[b"marL", b"marR", b"lvl", b"indent", b"algn"],
2885            )?,
2886            ..Self::default()
2887        })
2888    }
2889
2890    fn from_element_with_namespaces(
2891        reader: &mut Reader<&[u8]>,
2892        start: &BytesStart<'_>,
2893        inherited: &TextNamespaceScope,
2894    ) -> Result<Self> {
2895        let raw = capture_element(reader, start)?;
2896        Self::from_xml_with_namespaces(&raw, inherited)
2897    }
2898
2899    fn from_element_model(
2900        reader: &mut Reader<&[u8]>,
2901        start: &BytesStart<'_>,
2902        inherited: &TextNamespaceScope,
2903    ) -> Result<Self> {
2904        let namespaces = text_fragment_scope(start, inherited)?;
2905        let expected = local_name(start.name().as_ref()).to_vec();
2906        let mut properties = Self::from_start(start)?;
2907        properties.source_namespaces = namespaces.physical.clone();
2908        let mut boundary = 0;
2909        let mut seen = [false; 10];
2910        let mut buffer = Vec::new();
2911        loop {
2912            match reader
2913                .read_event_into(&mut buffer)
2914                .map_err(OxmlError::from)?
2915            {
2916                Event::Start(element) => {
2917                    let name = text_owned_name(&element, &namespaces)?;
2918                    let raw = capture_element(reader, &element)?;
2919                    properties.capture_child(&name, raw, &mut boundary, &mut seen, &namespaces)?;
2920                }
2921                Event::Empty(element) => {
2922                    let name = text_owned_name(&element, &namespaces)?;
2923                    let raw = capture_empty_element(&element)?;
2924                    properties.capture_child(&name, raw, &mut boundary, &mut seen, &namespaces)?;
2925                }
2926                Event::End(element) if matches_local_name(element.name().as_ref(), &expected) => {
2927                    return Ok(properties);
2928                }
2929                Event::Eof => return Err(missing_end(&String::from_utf8_lossy(&expected))),
2930                event @ (Event::Comment(_) | Event::PI(_) | Event::Text(_) | Event::CData(_)) => {
2931                    properties
2932                        .raw_children
2933                        .push(boundary, text_capture_opaque_event(event)?);
2934                }
2935                _ => {}
2936            }
2937            buffer.clear();
2938        }
2939    }
2940
2941    fn capture_child(
2942        &mut self,
2943        name: &[u8],
2944        raw: Vec<u8>,
2945        boundary: &mut usize,
2946        seen: &mut [bool; 10],
2947        namespaces: &TextNamespaceScope,
2948    ) -> Result<()> {
2949        if namespaces.fixed_list_style_children && matches!(name, b"lnSpc" | b"spcBef" | b"spcAft")
2950        {
2951            let mut spacing = Reader::from_reader(raw.as_slice());
2952            loop {
2953                match spacing.read_event().map_err(OxmlError::from)? {
2954                    Event::Start(start) | Event::Empty(start) => {
2955                        crate::namespace::reject_conflicting_a_prefix(&start)?;
2956                    }
2957                    Event::Eof => break,
2958                    _ => {}
2959                }
2960            }
2961        }
2962        let slot = paragraph_property_slot(name);
2963        if let Some(slot) = slot {
2964            if seen[slot - 1] {
2965                return Err(duplicate(&String::from_utf8_lossy(name)));
2966            }
2967            seen[slot - 1] = true;
2968        }
2969        match name {
2970            b"lnSpc" => self.line_spacing = Some(TextSpacing::from_xml(&raw, b"lnSpc")?),
2971            b"spcBef" => self.space_before = Some(TextSpacing::from_xml(&raw, b"spcBef")?),
2972            b"spcAft" => self.space_after = Some(TextSpacing::from_xml(&raw, b"spcAft")?),
2973            b"defRPr" => {
2974                self.default_run_properties = Some(
2975                    CT_TextCharacterProperties::from_xml_with_namespaces(&raw, namespaces)?,
2976                )
2977            }
2978            _ => {
2979                let mut bullet = self.bullet.take().unwrap_or_default();
2980                if bullet.capture_component(name, &raw)? {
2981                    self.bullet = Some(bullet);
2982                } else {
2983                    if !bullet.is_empty() {
2984                        self.bullet = Some(bullet);
2985                    }
2986                    self.raw_children.push(
2987                        slot.map_or(*boundary, |slot| (*boundary).max(slot - 1)),
2988                        raw,
2989                    );
2990                }
2991            }
2992        }
2993        if let Some(slot) = slot {
2994            *boundary = (*boundary).max(slot);
2995        }
2996        Ok(())
2997    }
2998
2999    /// Writes paragraph properties with a caller-selected fixed `a:` tag.
3000    pub fn write_xml<W: Write>(&self, writer: &mut Writer<W>, tag: &str) -> Result<()> {
3001        let owned_tag = text_source_tag(tag, &self.source_tag, &self.source_namespaces);
3002        let tag = owned_tag.as_str();
3003        if text_has_physical_owner(&self.source_tag, &self.source_namespaces)
3004            && let Some(raw) = &self.source_xml
3005        {
3006            let mut candidate = self.clone();
3007            candidate.source_xml = None;
3008            let mut projection = Writer::new(Vec::new());
3009            candidate.write_xml(&mut projection, "a:pPr")?;
3010            if projection.into_inner() == self.source_projection {
3011                writer
3012                    .get_mut()
3013                    .write_all(&text_close_source_owner(raw, &self.source_namespaces, tag)?)
3014                    .map_err(OxmlError::from)?;
3015                return Ok(());
3016            }
3017        }
3018        let mut start = BytesStart::new(tag);
3019        if let Some(value) = self.left_margin {
3020            parse_i32_value(tag, "marL", &value.to_string(), 0, MAX_TEXT_MARGIN)?;
3021            push_owned_attr(&mut start, "marL", value.to_string());
3022        }
3023        if let Some(value) = self.right_margin {
3024            parse_i32_value(tag, "marR", &value.to_string(), 0, MAX_TEXT_MARGIN)?;
3025            push_owned_attr(&mut start, "marR", value.to_string());
3026        }
3027        if let Some(value) = self.level {
3028            if value > 8 {
3029                return Err(invalid_attribute(tag, "lvl", &value.to_string()));
3030            }
3031            push_owned_attr(&mut start, "lvl", value.to_string());
3032        }
3033        if let Some(value) = self.indent {
3034            parse_i32_value(
3035                tag,
3036                "indent",
3037                &value.to_string(),
3038                -MAX_TEXT_MARGIN,
3039                MAX_TEXT_MARGIN,
3040            )?;
3041            push_owned_attr(&mut start, "indent", value.to_string());
3042        }
3043        if let Some(value) = self.alignment {
3044            start.push_attribute(("algn", value.as_str()));
3045        }
3046        push_optional_bool(&mut start, "rtl", self.right_to_left);
3047        if self.source_attributes.is_empty() {
3048            for (name, value) in &self.raw_attributes {
3049                if name != "rtl" {
3050                    start.push_attribute((name.as_str(), value.as_str()));
3051                }
3052            }
3053        } else {
3054            let modeled = ["marL", "marR", "lvl", "indent", "algn", "rtl"];
3055            let preserved: Vec<String> = self
3056                .source_attributes
3057                .iter()
3058                .filter(|(key, _)| !modeled.contains(&key.as_str()))
3059                .map(|(_, token)| token.clone())
3060                .collect();
3061            let generated = std::str::from_utf8(start.as_ref())
3062                .map_err(OxmlError::from)?
3063                .to_owned();
3064            start =
3065                BytesStart::from_content(format!("{generated}{}", preserved.concat()), tag.len());
3066        }
3067
3068        let existing_keys: Vec<String> = start
3069            .attributes()
3070            .map(|attribute| {
3071                attribute.map_err(OxmlError::from).and_then(|attribute| {
3072                    std::str::from_utf8(attribute.key.as_ref())
3073                        .map(str::to_owned)
3074                        .map_err(OxmlError::from)
3075                })
3076            })
3077            .collect::<std::result::Result<_, OxmlError>>()?;
3078        let source = self
3079            .source_xml
3080            .clone()
3081            .unwrap_or(capture_empty_element(&start)?);
3082        for (prefix, uri) in text_owner_closure(&source, &self.source_namespaces)? {
3083            let key = if prefix.is_empty() {
3084                "xmlns".to_owned()
3085            } else {
3086                format!("xmlns:{prefix}")
3087            };
3088            if !existing_keys.contains(&key) {
3089                start.push_attribute((key.as_str(), uri.as_str()));
3090            }
3091        }
3092
3093        let modelled = self.line_spacing.is_some()
3094            || self.space_before.is_some()
3095            || self.space_after.is_some()
3096            || self.bullet.is_some()
3097            || self.default_run_properties.is_some();
3098        if !modelled && self.raw_children.is_empty() {
3099            return write_empty(writer, start);
3100        }
3101        write_start(writer, start)?;
3102        emit_raw(writer, self.raw_children.at(0))?;
3103        if let Some(spacing) = &self.line_spacing {
3104            {
3105                let mut child = Writer::new(Vec::new());
3106                spacing.write_xml(&mut child, "a:lnSpc")?;
3107                writer
3108                    .get_mut()
3109                    .write_all(&text_close_generated_child(
3110                        &child.into_inner(),
3111                        &self.source_namespaces,
3112                    )?)
3113                    .map_err(OxmlError::from)?;
3114            }
3115        }
3116        emit_raw(writer, self.raw_children.at(1))?;
3117        if let Some(spacing) = &self.space_before {
3118            {
3119                let mut child = Writer::new(Vec::new());
3120                spacing.write_xml(&mut child, "a:spcBef")?;
3121                writer
3122                    .get_mut()
3123                    .write_all(&text_close_generated_child(
3124                        &child.into_inner(),
3125                        &self.source_namespaces,
3126                    )?)
3127                    .map_err(OxmlError::from)?;
3128            }
3129        }
3130        emit_raw(writer, self.raw_children.at(2))?;
3131        if let Some(spacing) = &self.space_after {
3132            {
3133                let mut child = Writer::new(Vec::new());
3134                spacing.write_xml(&mut child, "a:spcAft")?;
3135                writer
3136                    .get_mut()
3137                    .write_all(&text_close_generated_child(
3138                        &child.into_inner(),
3139                        &self.source_namespaces,
3140                    )?)
3141                    .map_err(OxmlError::from)?;
3142            }
3143        }
3144        emit_raw(writer, self.raw_children.at(3))?;
3145        if let Some(bullet) = &self.bullet {
3146            {
3147                let mut child = Writer::new(Vec::new());
3148                bullet.write_color(&mut child)?;
3149                writer
3150                    .get_mut()
3151                    .write_all(&text_close_generated_child(
3152                        &child.into_inner(),
3153                        &self.source_namespaces,
3154                    )?)
3155                    .map_err(OxmlError::from)?;
3156            }
3157        }
3158        emit_raw(writer, self.raw_children.at(4))?;
3159        if let Some(bullet) = &self.bullet {
3160            {
3161                let mut child = Writer::new(Vec::new());
3162                bullet.write_size(&mut child)?;
3163                writer
3164                    .get_mut()
3165                    .write_all(&text_close_generated_child(
3166                        &child.into_inner(),
3167                        &self.source_namespaces,
3168                    )?)
3169                    .map_err(OxmlError::from)?;
3170            }
3171        }
3172        emit_raw(writer, self.raw_children.at(5))?;
3173        if let Some(bullet) = &self.bullet {
3174            {
3175                let mut child = Writer::new(Vec::new());
3176                bullet.write_font(&mut child)?;
3177                writer
3178                    .get_mut()
3179                    .write_all(&text_close_generated_child(
3180                        &child.into_inner(),
3181                        &self.source_namespaces,
3182                    )?)
3183                    .map_err(OxmlError::from)?;
3184            }
3185        }
3186        emit_raw(writer, self.raw_children.at(6))?;
3187        if let Some(bullet) = &self.bullet {
3188            {
3189                let mut child = Writer::new(Vec::new());
3190                bullet.write_choice(&mut child)?;
3191                writer
3192                    .get_mut()
3193                    .write_all(&text_close_generated_child(
3194                        &child.into_inner(),
3195                        &self.source_namespaces,
3196                    )?)
3197                    .map_err(OxmlError::from)?;
3198            }
3199        }
3200        emit_raw(writer, self.raw_children.at(7))?;
3201        emit_raw(writer, self.raw_children.at(8))?;
3202        if let Some(properties) = &self.default_run_properties {
3203            {
3204                let mut child = Writer::new(Vec::new());
3205                properties.write_xml(&mut child, "a:defRPr")?;
3206                writer
3207                    .get_mut()
3208                    .write_all(&text_close_generated_child(
3209                        &child.into_inner(),
3210                        &self.source_namespaces,
3211                    )?)
3212                    .map_err(OxmlError::from)?;
3213            }
3214        }
3215        emit_raw(writer, self.raw_children.at(9))?;
3216        emit_raw(writer, self.raw_children.at(10))?;
3217        write_end(writer, tag)
3218    }
3219
3220    pub fn raw_children(&self) -> &OrderedRawChildren {
3221        &self.raw_children
3222    }
3223
3224    /// Returns whether the paragraph keeps a preserved `a:buBlip` picture bullet.
3225    pub fn has_picture_bullet(&self) -> bool {
3226        (0..=10).any(|boundary| {
3227            self.raw_children
3228                .at(boundary)
3229                .any(|raw| raw_local_name(raw) == b"buBlip")
3230        })
3231    }
3232
3233    /// Replaces the direct bullet.
3234    ///
3235    /// Each bullet group holds one member, so a colour, size, font, or choice
3236    /// set here removes its preserved `a:buClrTx`, `a:buSzTx`, `a:buFontTx`,
3237    /// or `a:buBlip` alternative. Clearing the bullet removes all four.
3238    pub fn set_bullet(&mut self, bullet: Option<TextBullet>) {
3239        let replaces = |name: &[u8]| {
3240            let Some(bullet) = bullet.as_ref() else {
3241                return matches!(name, b"buClrTx" | b"buSzTx" | b"buFontTx" | b"buBlip");
3242            };
3243            match name {
3244                b"buClrTx" => bullet.color.is_some(),
3245                b"buSzTx" => bullet.size.is_some(),
3246                b"buFontTx" => bullet.font.is_some(),
3247                b"buBlip" => bullet.choice.is_some(),
3248                _ => false,
3249            }
3250        };
3251        self.raw_children
3252            .retain(|raw| !replaces(raw_local_name(raw)));
3253        self.bullet = bullet;
3254    }
3255
3256    /// Serialises a complete paragraph-property fragment.
3257    pub fn to_xml(&self) -> Result<Vec<u8>> {
3258        let mut writer = Writer::new(Vec::new());
3259        self.write_xml(&mut writer, "a:pPr")?;
3260        Ok(writer.into_inner())
3261    }
3262}
3263
3264fn paragraph_property_slot(name: &[u8]) -> Option<usize> {
3265    match name {
3266        b"lnSpc" => Some(1),
3267        b"spcBef" => Some(2),
3268        b"spcAft" => Some(3),
3269        b"buClrTx" | b"buClr" => Some(4),
3270        b"buSzTx" | b"buSzPct" | b"buSzPts" => Some(5),
3271        b"buFontTx" | b"buFont" => Some(6),
3272        b"buNone" | b"buAutoNum" | b"buChar" | b"buBlip" => Some(7),
3273        b"tabLst" => Some(8),
3274        b"defRPr" => Some(9),
3275        b"extLst" => Some(10),
3276        _ => None,
3277    }
3278}
3279
3280/// Returns the local name of a captured raw element.
3281fn raw_local_name(raw: &[u8]) -> &[u8] {
3282    let tag = raw.strip_prefix(b"<").unwrap_or(raw);
3283    let end = tag
3284        .iter()
3285        .position(|byte| byte.is_ascii_whitespace() || matches!(byte, b'/' | b'>'))
3286        .unwrap_or(tag.len());
3287    local_name(&tag[..end])
3288}
3289
3290fn is_paragraph_property_tag(name: &[u8]) -> bool {
3291    matches!(
3292        name,
3293        b"pPr"
3294            | b"defPPr"
3295            | b"lvl1pPr"
3296            | b"lvl2pPr"
3297            | b"lvl3pPr"
3298            | b"lvl4pPr"
3299            | b"lvl5pPr"
3300            | b"lvl6pPr"
3301            | b"lvl7pPr"
3302            | b"lvl8pPr"
3303            | b"lvl9pPr"
3304    )
3305}
3306
3307/// A regular DrawingML run containing optional properties and required text.
3308#[allow(non_camel_case_types)]
3309#[derive(Clone, Debug)]
3310pub struct CT_RegularTextRun {
3311    pub properties: Option<CT_TextCharacterProperties>,
3312    pub text: TextValue,
3313    raw_children: OrderedRawChildren,
3314    source_root: Vec<String>,
3315    source_tag: String,
3316}
3317impl PartialEq for CT_RegularTextRun {
3318    fn eq(&self, other: &Self) -> bool {
3319        self.properties == other.properties
3320            && self.text == other.text
3321            && self.raw_children == other.raw_children
3322    }
3323}
3324impl Eq for CT_RegularTextRun {}
3325
3326impl CT_RegularTextRun {
3327    /// Creates a regular text run without direct character formatting.
3328    pub fn new(text: impl Into<String>) -> Self {
3329        Self {
3330            properties: None,
3331            text: TextValue {
3332                value: escape_invalid_xml_characters(&text.into()).into_owned(),
3333                ..TextValue::default()
3334            },
3335            raw_children: OrderedRawChildren::default(),
3336            source_root: Vec::new(),
3337            source_tag: String::new(),
3338        }
3339    }
3340
3341    /// Replaces the run text while retaining properties and unmodelled XML.
3342    ///
3343    /// A character XML 1.0 cannot carry is stored as `_xHHHH_`, as
3344    /// python-pptx stores it.
3345    pub fn set_text(&mut self, text: &str) {
3346        self.text.value = escape_invalid_xml_characters(text).into_owned();
3347    }
3348
3349    /// Serializes this regular run as a standalone DrawingML element.
3350    pub fn to_xml(&self) -> Result<Vec<u8>> {
3351        let mut writer = Writer::new(Vec::new());
3352        self.write_xml(&mut writer)?;
3353        text_close_generated_child(&writer.into_inner(), &[])
3354    }
3355
3356    /// Parses one complete `a:r` element with any prefix.
3357    pub fn from_xml(xml: &[u8]) -> Result<Self> {
3358        Self::from_xml_with_namespaces(xml, &TextNamespaceScope::default())
3359    }
3360
3361    pub(crate) fn from_xml_with_namespaces(
3362        xml: &[u8],
3363        inherited: &TextNamespaceScope,
3364    ) -> Result<Self> {
3365        parse_complete(
3366            xml,
3367            b"r",
3368            |reader, start| Self::from_element_with_namespaces(reader, start, inherited),
3369            |_| Err(TextError::UnexpectedElement("r".to_owned())),
3370        )
3371    }
3372
3373    fn from_element_with_namespaces(
3374        reader: &mut Reader<&[u8]>,
3375        start: &BytesStart<'_>,
3376        inherited: &TextNamespaceScope,
3377    ) -> Result<Self> {
3378        let source_root = text_root_spellings_scoped(
3379            start,
3380            inherited,
3381            &capture_element(&mut reader.clone(), start)?,
3382        )?;
3383        let namespaces = text_fragment_scope(start, inherited)?;
3384        let mut properties = None;
3385        let mut text = None;
3386        let mut raw_children = OrderedRawChildren::default();
3387        let mut boundary = 0;
3388        let mut buffer = Vec::new();
3389        loop {
3390            match reader
3391                .read_event_into(&mut buffer)
3392                .map_err(OxmlError::from)?
3393            {
3394                Event::Start(element) if text_owned_name(&element, &namespaces)? == b"rPr" => {
3395                    if properties.is_some() {
3396                        return Err(duplicate("rPr"));
3397                    }
3398                    properties = Some(CT_TextCharacterProperties::from_element_with_namespaces(
3399                        reader,
3400                        &element,
3401                        &namespaces,
3402                    )?);
3403                    boundary = boundary.max(1);
3404                }
3405                Event::Empty(element) if text_owned_name(&element, &namespaces)? == b"rPr" => {
3406                    if properties.is_some() {
3407                        return Err(duplicate("rPr"));
3408                    }
3409                    properties = Some(CT_TextCharacterProperties::from_start_with_namespaces(
3410                        &element,
3411                        &namespaces,
3412                    )?);
3413                    boundary = boundary.max(1);
3414                }
3415                Event::Start(element) if text_owned_name(&element, &namespaces)? == b"t" => {
3416                    if text.is_some() {
3417                        return Err(duplicate("t"));
3418                    }
3419                    text = Some(TextValue::from_element_with_namespaces(
3420                        reader,
3421                        &element,
3422                        &namespaces,
3423                    )?);
3424                    boundary = boundary.max(2);
3425                }
3426                Event::Empty(element) if text_owned_name(&element, &namespaces)? == b"t" => {
3427                    if text.is_some() {
3428                        return Err(duplicate("t"));
3429                    }
3430                    text = Some(TextValue::from_start_with_namespaces(
3431                        &element,
3432                        &namespaces,
3433                    )?);
3434                    boundary = boundary.max(2);
3435                }
3436                Event::Start(element) => {
3437                    raw_children.push(boundary, capture_element(reader, &element)?)
3438                }
3439                Event::Empty(element) => {
3440                    raw_children.push(boundary, capture_empty_element(&element)?)
3441                }
3442                Event::End(element) if matches_local_name(element.name().as_ref(), b"r") => break,
3443                Event::Eof => return Err(missing_end("r")),
3444                event @ (Event::Comment(_) | Event::PI(_) | Event::Text(_) | Event::CData(_)) => {
3445                    raw_children.push(boundary, text_capture_opaque_event(event)?);
3446                }
3447                _ => {}
3448            }
3449            buffer.clear();
3450        }
3451        Ok(Self {
3452            properties,
3453            text: text.ok_or_else(|| {
3454                TextError::Xml(OxmlError::MissingElement("DrawingML run text".to_owned()))
3455            })?,
3456            raw_children,
3457            source_root,
3458            source_tag: std::str::from_utf8(start.name().as_ref())
3459                .map_err(OxmlError::from)?
3460                .to_owned(),
3461        })
3462    }
3463
3464    fn write_xml<W: Write>(&self, writer: &mut Writer<W>) -> Result<()> {
3465        let owned_tag = text_source_tag(
3466            "a:r",
3467            &self.source_tag,
3468            &text_namespace_scope(&text_source_start("a:r", &self.source_root), &[])?,
3469        );
3470        let tag = owned_tag.as_str();
3471        write_start(writer, text_source_start(tag, &self.source_root))?;
3472        emit_raw(writer, self.raw_children.at(0))?;
3473        if let Some(properties) = &self.properties {
3474            {
3475                let mut child = Writer::new(Vec::new());
3476                properties.write_xml(&mut child, "a:rPr")?;
3477                writer
3478                    .get_mut()
3479                    .write_all(&text_close_generated_child(
3480                        &child.into_inner(),
3481                        &text_namespace_scope(&text_source_start(tag, &self.source_root), &[])?,
3482                    )?)
3483                    .map_err(OxmlError::from)?;
3484            }
3485        }
3486        emit_raw(writer, self.raw_children.at(1))?;
3487        self.text.write_xml(writer)?;
3488        emit_raw(writer, self.raw_children.at(2))?;
3489        write_end(writer, tag)
3490    }
3491}
3492
3493/// A DrawingML explicit line break and its optional run properties.
3494#[allow(non_camel_case_types)]
3495#[derive(Clone, Debug, Default)]
3496pub struct CT_TextLineBreak {
3497    pub properties: Option<CT_TextCharacterProperties>,
3498    raw_children: OrderedRawChildren,
3499    source_root: Vec<String>,
3500    source_tag: String,
3501}
3502impl PartialEq for CT_TextLineBreak {
3503    fn eq(&self, other: &Self) -> bool {
3504        self.properties == other.properties && self.raw_children == other.raw_children
3505    }
3506}
3507impl Eq for CT_TextLineBreak {}
3508
3509impl CT_TextLineBreak {
3510    /// Parses one complete `a:br` element with any prefix.
3511    pub fn from_xml(xml: &[u8]) -> Result<Self> {
3512        Self::from_xml_with_namespaces(xml, &TextNamespaceScope::default())
3513    }
3514
3515    pub(crate) fn from_xml_with_namespaces(
3516        xml: &[u8],
3517        inherited: &TextNamespaceScope,
3518    ) -> Result<Self> {
3519        parse_complete(
3520            xml,
3521            b"br",
3522            |reader, start| Self::from_element_with_namespaces(reader, start, inherited),
3523            |start| {
3524                Ok(Self {
3525                    source_root: text_root_spellings_scoped(
3526                        start,
3527                        inherited,
3528                        &capture_empty_element(start)?,
3529                    )?,
3530                    source_tag: std::str::from_utf8(start.name().as_ref())
3531                        .map_err(OxmlError::from)?
3532                        .to_owned(),
3533                    ..Self::default()
3534                })
3535            },
3536        )
3537    }
3538
3539    fn from_element_with_namespaces(
3540        reader: &mut Reader<&[u8]>,
3541        start: &BytesStart<'_>,
3542        inherited: &TextNamespaceScope,
3543    ) -> Result<Self> {
3544        let namespaces = text_fragment_scope(start, inherited)?;
3545        let mut line_break = Self {
3546            source_root: text_root_spellings_scoped(
3547                start,
3548                inherited,
3549                &capture_element(&mut reader.clone(), start)?,
3550            )?,
3551            source_tag: std::str::from_utf8(start.name().as_ref())
3552                .map_err(OxmlError::from)?
3553                .to_owned(),
3554            ..Self::default()
3555        };
3556        let mut boundary = 0;
3557        let mut buffer = Vec::new();
3558        loop {
3559            match reader
3560                .read_event_into(&mut buffer)
3561                .map_err(OxmlError::from)?
3562            {
3563                Event::Start(element) if text_owned_name(&element, &namespaces)? == b"rPr" => {
3564                    if line_break.properties.is_some() {
3565                        return Err(duplicate("rPr"));
3566                    }
3567                    line_break.properties =
3568                        Some(CT_TextCharacterProperties::from_element_with_namespaces(
3569                            reader,
3570                            &element,
3571                            &namespaces,
3572                        )?);
3573                    boundary = 1;
3574                }
3575                Event::Empty(element) if text_owned_name(&element, &namespaces)? == b"rPr" => {
3576                    if line_break.properties.is_some() {
3577                        return Err(duplicate("rPr"));
3578                    }
3579                    line_break.properties =
3580                        Some(CT_TextCharacterProperties::from_start_with_namespaces(
3581                            &element,
3582                            &namespaces,
3583                        )?);
3584                    boundary = 1;
3585                }
3586                Event::Start(element) => line_break
3587                    .raw_children
3588                    .push(boundary, capture_element(reader, &element)?),
3589                Event::Empty(element) => line_break
3590                    .raw_children
3591                    .push(boundary, capture_empty_element(&element)?),
3592                Event::End(element) if matches_local_name(element.name().as_ref(), b"br") => {
3593                    return Ok(line_break);
3594                }
3595                Event::Eof => return Err(missing_end("br")),
3596                event @ (Event::Comment(_) | Event::PI(_) | Event::Text(_) | Event::CData(_)) => {
3597                    line_break
3598                        .raw_children
3599                        .push(boundary, text_capture_opaque_event(event)?);
3600                }
3601                _ => {}
3602            }
3603            buffer.clear();
3604        }
3605    }
3606
3607    fn write_xml<W: Write>(&self, writer: &mut Writer<W>) -> Result<()> {
3608        let owned_tag = text_source_tag(
3609            "a:br",
3610            &self.source_tag,
3611            &text_namespace_scope(&text_source_start("a:br", &self.source_root), &[])?,
3612        );
3613        let tag = owned_tag.as_str();
3614        if self.properties.is_none() && self.raw_children.is_empty() {
3615            return write_empty(writer, text_source_start(tag, &self.source_root));
3616        }
3617        write_start(writer, text_source_start(tag, &self.source_root))?;
3618        emit_raw(writer, self.raw_children.at(0))?;
3619        if let Some(properties) = &self.properties {
3620            {
3621                let mut child = Writer::new(Vec::new());
3622                properties.write_xml(&mut child, "a:rPr")?;
3623                writer
3624                    .get_mut()
3625                    .write_all(&text_close_generated_child(
3626                        &child.into_inner(),
3627                        &text_namespace_scope(&text_source_start(tag, &self.source_root), &[])?,
3628                    )?)
3629                    .map_err(OxmlError::from)?;
3630            }
3631        }
3632        emit_raw(writer, self.raw_children.at(1))?;
3633        write_end(writer, tag)
3634    }
3635}
3636
3637/// A DrawingML field with required GUID, optional type, properties, and text.
3638#[allow(non_camel_case_types)]
3639#[derive(Clone, Debug)]
3640pub struct CT_TextField {
3641    pub id: String,
3642    pub field_type: Option<String>,
3643    pub run_properties: Option<CT_TextCharacterProperties>,
3644    pub paragraph_properties: Option<CT_TextParagraphProperties>,
3645    pub text: Option<TextValue>,
3646    raw_children: OrderedRawChildren,
3647    source_root: Vec<String>,
3648    source_tag: String,
3649}
3650impl PartialEq for CT_TextField {
3651    fn eq(&self, other: &Self) -> bool {
3652        self.id == other.id
3653            && self.field_type == other.field_type
3654            && self.run_properties == other.run_properties
3655            && self.paragraph_properties == other.paragraph_properties
3656            && self.text == other.text
3657            && self.raw_children == other.raw_children
3658    }
3659}
3660impl Eq for CT_TextField {}
3661
3662impl CT_TextField {
3663    /// Parses one complete `a:fld` element with any prefix.
3664    pub fn from_xml(xml: &[u8]) -> Result<Self> {
3665        Self::from_xml_with_namespaces(xml, &TextNamespaceScope::default())
3666    }
3667
3668    pub(crate) fn from_xml_with_namespaces(
3669        xml: &[u8],
3670        inherited: &TextNamespaceScope,
3671    ) -> Result<Self> {
3672        parse_complete(
3673            xml,
3674            b"fld",
3675            |reader, start| Self::from_element_with_namespaces(reader, start, inherited),
3676            |start| {
3677                let mut field = Self::from_empty(start)?;
3678                field.source_root = text_field_root_spellings(start, inherited)?;
3679                Ok(field)
3680            },
3681        )
3682    }
3683
3684    fn from_start(start: &BytesStart<'_>) -> Result<Self> {
3685        let id = required_attr(start, b"id")?;
3686        if !is_guid(&id) {
3687            return Err(invalid_attribute("fld", "id", &id));
3688        }
3689        Ok(Self {
3690            id,
3691            field_type: text_attr(start, b"type")?,
3692            run_properties: None,
3693            paragraph_properties: None,
3694            text: None,
3695            raw_children: OrderedRawChildren::default(),
3696            source_root: text_attribute_tokens(start)?
3697                .into_iter()
3698                .filter(|(key, _)| key != "id" && key != "type")
3699                .map(|(_, token)| token)
3700                .collect(),
3701            source_tag: std::str::from_utf8(start.name().as_ref())
3702                .map_err(OxmlError::from)?
3703                .to_owned(),
3704        })
3705    }
3706
3707    fn from_empty(start: &BytesStart<'_>) -> Result<Self> {
3708        Self::from_start(start)
3709    }
3710
3711    fn from_element_with_namespaces(
3712        reader: &mut Reader<&[u8]>,
3713        start: &BytesStart<'_>,
3714        inherited: &TextNamespaceScope,
3715    ) -> Result<Self> {
3716        let namespaces = text_fragment_scope(start, inherited)?;
3717        let mut field = Self::from_start(start)?;
3718        field.source_root = text_field_root_spellings(start, inherited)?;
3719        let mut boundary = 0;
3720        let mut buffer = Vec::new();
3721        loop {
3722            match reader
3723                .read_event_into(&mut buffer)
3724                .map_err(OxmlError::from)?
3725            {
3726                Event::Start(element) if text_owned_name(&element, &namespaces)? == b"rPr" => {
3727                    if field.run_properties.is_some() {
3728                        return Err(duplicate("rPr"));
3729                    }
3730                    field.run_properties =
3731                        Some(CT_TextCharacterProperties::from_element_with_namespaces(
3732                            reader,
3733                            &element,
3734                            &namespaces,
3735                        )?);
3736                    boundary = boundary.max(1);
3737                }
3738                Event::Empty(element) if text_owned_name(&element, &namespaces)? == b"rPr" => {
3739                    if field.run_properties.is_some() {
3740                        return Err(duplicate("rPr"));
3741                    }
3742                    field.run_properties =
3743                        Some(CT_TextCharacterProperties::from_start_with_namespaces(
3744                            &element,
3745                            &namespaces,
3746                        )?);
3747                    boundary = boundary.max(1);
3748                }
3749                Event::Start(element) if text_owned_name(&element, &namespaces)? == b"pPr" => {
3750                    if field.paragraph_properties.is_some() {
3751                        return Err(duplicate("pPr"));
3752                    }
3753                    field.paragraph_properties =
3754                        Some(CT_TextParagraphProperties::from_element_with_namespaces(
3755                            reader,
3756                            &element,
3757                            &namespaces,
3758                        )?);
3759                    boundary = boundary.max(2);
3760                }
3761                Event::Empty(element) if text_owned_name(&element, &namespaces)? == b"pPr" => {
3762                    if field.paragraph_properties.is_some() {
3763                        return Err(duplicate("pPr"));
3764                    }
3765                    field.paragraph_properties =
3766                        Some(CT_TextParagraphProperties::from_start_with_namespaces(
3767                            &element,
3768                            &namespaces,
3769                        )?);
3770                    boundary = boundary.max(2);
3771                }
3772                Event::Start(element) if text_owned_name(&element, &namespaces)? == b"t" => {
3773                    if field.text.is_some() {
3774                        return Err(duplicate("t"));
3775                    }
3776                    field.text = Some(TextValue::from_element_with_namespaces(
3777                        reader,
3778                        &element,
3779                        &namespaces,
3780                    )?);
3781                    boundary = boundary.max(3);
3782                }
3783                Event::Empty(element) if text_owned_name(&element, &namespaces)? == b"t" => {
3784                    if field.text.is_some() {
3785                        return Err(duplicate("t"));
3786                    }
3787                    field.text = Some(TextValue::from_start_with_namespaces(
3788                        &element,
3789                        &namespaces,
3790                    )?);
3791                    boundary = boundary.max(3);
3792                }
3793                Event::Start(element) => field
3794                    .raw_children
3795                    .push(boundary, capture_element(reader, &element)?),
3796                Event::Empty(element) => field
3797                    .raw_children
3798                    .push(boundary, capture_empty_element(&element)?),
3799                Event::End(element) if matches_local_name(element.name().as_ref(), b"fld") => {
3800                    return Ok(field);
3801                }
3802                Event::Eof => return Err(missing_end("fld")),
3803                event @ (Event::Comment(_) | Event::PI(_) | Event::Text(_) | Event::CData(_)) => {
3804                    field
3805                        .raw_children
3806                        .push(boundary, text_capture_opaque_event(event)?);
3807                }
3808                _ => {}
3809            }
3810            buffer.clear();
3811        }
3812    }
3813
3814    fn write_xml<W: Write>(&self, writer: &mut Writer<W>) -> Result<()> {
3815        let owned_tag = text_source_tag(
3816            "a:fld",
3817            &self.source_tag,
3818            &text_namespace_scope(&text_source_start("a:fld", &self.source_root), &[])?,
3819        );
3820        let tag = owned_tag.as_str();
3821        if !is_guid(&self.id) {
3822            return Err(invalid_attribute("fld", "id", &self.id));
3823        }
3824        let mut start = text_source_start(tag, &self.source_root);
3825        start.push_attribute(("id", self.id.as_str()));
3826        push_optional_attr(&mut start, "type", self.field_type.as_deref());
3827        let modelled = self.run_properties.is_some()
3828            || self.paragraph_properties.is_some()
3829            || self.text.is_some();
3830        if !modelled && self.raw_children.is_empty() {
3831            return write_empty(writer, start);
3832        }
3833        write_start(writer, start)?;
3834        emit_raw(writer, self.raw_children.at(0))?;
3835        if let Some(properties) = &self.run_properties {
3836            {
3837                let mut child = Writer::new(Vec::new());
3838                properties.write_xml(&mut child, "a:rPr")?;
3839                writer
3840                    .get_mut()
3841                    .write_all(&text_close_generated_child(
3842                        &child.into_inner(),
3843                        &text_namespace_scope(&text_source_start(tag, &self.source_root), &[])?,
3844                    )?)
3845                    .map_err(OxmlError::from)?;
3846            }
3847        }
3848        emit_raw(writer, self.raw_children.at(1))?;
3849        if let Some(properties) = &self.paragraph_properties {
3850            {
3851                let mut child = Writer::new(Vec::new());
3852                properties.write_xml(&mut child, "a:pPr")?;
3853                writer
3854                    .get_mut()
3855                    .write_all(&text_close_generated_child(
3856                        &child.into_inner(),
3857                        &text_namespace_scope(&text_source_start(tag, &self.source_root), &[])?,
3858                    )?)
3859                    .map_err(OxmlError::from)?;
3860            }
3861        }
3862        emit_raw(writer, self.raw_children.at(2))?;
3863        if let Some(text) = &self.text {
3864            text.write_xml(writer)?;
3865        }
3866        emit_raw(writer, self.raw_children.at(3))?;
3867        write_end(writer, tag)
3868    }
3869}
3870
3871/// One member of the paragraph text-content choice.
3872#[derive(Clone, Debug, Eq, PartialEq)]
3873pub enum TextRun {
3874    Run(CT_RegularTextRun),
3875    Break(CT_TextLineBreak),
3876    Field(Box<CT_TextField>),
3877}
3878
3879impl TextRun {
3880    fn from_xml_with_namespaces(
3881        xml: &[u8],
3882        name: &[u8],
3883        namespaces: &TextNamespaceScope,
3884    ) -> Result<Self> {
3885        match name {
3886            b"r" => Ok(Self::Run(CT_RegularTextRun::from_xml_with_namespaces(
3887                xml, namespaces,
3888            )?)),
3889            b"br" => Ok(Self::Break(CT_TextLineBreak::from_xml_with_namespaces(
3890                xml, namespaces,
3891            )?)),
3892            b"fld" => Ok(Self::Field(Box::new(
3893                CT_TextField::from_xml_with_namespaces(xml, namespaces)?,
3894            ))),
3895            _ => Err(TextError::UnexpectedElement(
3896                String::from_utf8_lossy(name).into_owned(),
3897            )),
3898        }
3899    }
3900
3901    fn write_xml<W: Write>(&self, writer: &mut Writer<W>) -> Result<()> {
3902        match self {
3903            Self::Run(run) => run.write_xml(writer),
3904            Self::Break(line_break) => line_break.write_xml(writer),
3905            Self::Field(field) => field.write_xml(writer),
3906        }
3907    }
3908}
3909
3910/// A DrawingML paragraph with ordered runs, breaks, and fields.
3911#[allow(non_camel_case_types)]
3912#[derive(Clone, Debug, Default)]
3913pub struct CT_TextParagraph {
3914    pub properties: Option<CT_TextParagraphProperties>,
3915    pub runs: Vec<TextRun>,
3916    pub end_properties: Option<CT_TextCharacterProperties>,
3917    raw_children: OrderedRawChildren,
3918    source_root: Vec<String>,
3919    source_tag: String,
3920}
3921impl PartialEq for CT_TextParagraph {
3922    fn eq(&self, other: &Self) -> bool {
3923        self.properties == other.properties
3924            && self.runs == other.runs
3925            && self.end_properties == other.end_properties
3926            && self.raw_children == other.raw_children
3927    }
3928}
3929impl Eq for CT_TextParagraph {}
3930
3931impl CT_TextParagraph {
3932    /// Returns direct properties, inserting them before preserved content.
3933    pub fn properties_mut(&mut self) -> &mut CT_TextParagraphProperties {
3934        if self.properties.is_none() {
3935            let mut raw_children = OrderedRawChildren::default();
3936            for boundary in 0..=2 + self.runs.len() {
3937                let new_boundary = if boundary == 0 { 1 } else { boundary };
3938                for child in self.raw_children.at(boundary) {
3939                    raw_children.push(new_boundary, child.to_vec());
3940                }
3941            }
3942            self.raw_children = raw_children;
3943            self.properties = Some(CT_TextParagraphProperties::default());
3944        }
3945        self.properties
3946            .as_mut()
3947            .expect("paragraph properties were inserted")
3948    }
3949
3950    /// Replaces ordered text choices with one regular run, split at each
3951    /// vertical tab by an `a:br` with the run's formatting, since a vertical
3952    /// tab is how the paragraph text reports a line break.
3953    ///
3954    /// The first existing regular run supplies direct formatting and
3955    /// unmodelled run content for the replacement. A paragraph without one
3956    /// formats the new run with its end-of-paragraph properties, without
3957    /// their hyperlinks, as PowerPoint formats text typed into an empty
3958    /// paragraph.
3959    pub fn set_text(&mut self, text: &str) {
3960        let old_run_count = self.runs.len();
3961        let retained_run = self.runs.iter().find_map(|run| match run {
3962            TextRun::Run(run) => Some(run.clone()),
3963            TextRun::Break(_) | TextRun::Field(_) => None,
3964        });
3965        let mut raw_children = OrderedRawChildren::default();
3966        for boundary in 0..=2 + old_run_count {
3967            let new_boundary = if boundary <= 1 {
3968                boundary
3969            } else if boundary <= 1 + old_run_count {
3970                2
3971            } else {
3972                3
3973            };
3974            for child in self.raw_children.at(boundary) {
3975                raw_children.push(new_boundary, child.to_vec());
3976            }
3977        }
3978        self.raw_children = raw_children;
3979        let mut run = retained_run.unwrap_or_else(|| CT_RegularTextRun {
3980            properties: self
3981                .end_properties
3982                .clone()
3983                .map(CT_TextCharacterProperties::without_hyperlinks),
3984            ..CT_RegularTextRun::new(text)
3985        });
3986        run.set_text(text);
3987        self.runs = vec![TextRun::Run(run)];
3988        self.split_line_breaks(text);
3989    }
3990
3991    /// Splits the one run `set_text` leaves at each vertical tab of `text`.
3992    /// A later run that would be empty is left out, as python-pptx leaves it
3993    /// out, and content preserved after the old runs stays after the new ones.
3994    fn split_line_breaks(&mut self, text: &str) {
3995        let [TextRun::Run(first)] = self.runs.as_mut_slice() else {
3996            return;
3997        };
3998        if !text.contains('\u{b}') {
3999            return;
4000        }
4001        let mut lines = text.split('\u{b}');
4002        first.set_text(lines.next().unwrap_or_default());
4003        let properties = first.properties.clone();
4004        for line in lines {
4005            self.raw_children.shift_boundaries_from(2);
4006            self.runs.push(TextRun::Break(CT_TextLineBreak {
4007                properties: properties.clone(),
4008                ..CT_TextLineBreak::default()
4009            }));
4010            if !line.is_empty() {
4011                self.raw_children.shift_boundaries_from(2);
4012                self.runs.push(TextRun::Run(CT_RegularTextRun {
4013                    properties: properties.clone(),
4014                    ..CT_RegularTextRun::new(line)
4015                }));
4016            }
4017        }
4018    }
4019
4020    /// Appends one regular run after the existing ordered text choices.
4021    pub fn add_run(&mut self, text: &str) -> &mut CT_RegularTextRun {
4022        self.raw_children.shift_boundaries_from(2 + self.runs.len());
4023        self.runs.push(TextRun::Run(CT_RegularTextRun::new(text)));
4024        let Some(TextRun::Run(run)) = self.runs.last_mut() else {
4025            unreachable!("the appended text choice is a regular run")
4026        };
4027        run
4028    }
4029
4030    /// Parses one complete `a:p` element with any prefix.
4031    pub fn from_xml(xml: &[u8]) -> Result<Self> {
4032        Self::from_xml_with_namespaces(xml, &TextNamespaceScope::default())
4033    }
4034
4035    pub(crate) fn from_xml_with_namespaces(
4036        xml: &[u8],
4037        inherited: &TextNamespaceScope,
4038    ) -> Result<Self> {
4039        parse_complete(
4040            xml,
4041            b"p",
4042            |reader, start| Self::from_element_with_namespaces(reader, start, inherited),
4043            |start| {
4044                Ok(Self {
4045                    source_root: text_root_spellings_scoped(
4046                        start,
4047                        inherited,
4048                        &capture_empty_element(start)?,
4049                    )?,
4050                    source_tag: std::str::from_utf8(start.name().as_ref())
4051                        .map_err(OxmlError::from)?
4052                        .to_owned(),
4053                    ..Self::default()
4054                })
4055            },
4056        )
4057    }
4058
4059    fn from_element_with_namespaces(
4060        reader: &mut Reader<&[u8]>,
4061        start: &BytesStart<'_>,
4062        inherited: &TextNamespaceScope,
4063    ) -> Result<Self> {
4064        let namespaces = text_fragment_scope(start, inherited)?;
4065        let mut paragraph = Self {
4066            source_root: text_root_spellings_scoped(
4067                start,
4068                inherited,
4069                &capture_element(&mut reader.clone(), start)?,
4070            )?,
4071            source_tag: std::str::from_utf8(start.name().as_ref())
4072                .map_err(OxmlError::from)?
4073                .to_owned(),
4074            ..Self::default()
4075        };
4076        let mut boundary = 0;
4077        let mut buffer = Vec::new();
4078        loop {
4079            match reader
4080                .read_event_into(&mut buffer)
4081                .map_err(OxmlError::from)?
4082            {
4083                // The schema allows one leading pPr, but real decks carry a
4084                // later one between runs. The first governs the paragraph and
4085                // every later one falls through to verbatim preservation at
4086                // its position, which is also how python-pptx treats it.
4087                Event::Start(element)
4088                    if text_owned_name(&element, &namespaces)? == b"pPr"
4089                        && paragraph.properties.is_none() =>
4090                {
4091                    paragraph.properties =
4092                        Some(CT_TextParagraphProperties::from_element_with_namespaces(
4093                            reader,
4094                            &element,
4095                            &namespaces,
4096                        )?);
4097                    boundary = boundary.max(1);
4098                }
4099                Event::Empty(element)
4100                    if text_owned_name(&element, &namespaces)? == b"pPr"
4101                        && paragraph.properties.is_none() =>
4102                {
4103                    paragraph.properties =
4104                        Some(CT_TextParagraphProperties::from_start_with_namespaces(
4105                            &element,
4106                            &namespaces,
4107                        )?);
4108                    boundary = boundary.max(1);
4109                }
4110                Event::Start(element) if is_text_run(&text_owned_name(&element, &namespaces)?) => {
4111                    let name = local_name(element.name().as_ref()).to_vec();
4112                    let raw = capture_element(reader, &element)?;
4113                    paragraph.runs.push(TextRun::from_xml_with_namespaces(
4114                        &raw,
4115                        &name,
4116                        &namespaces,
4117                    )?);
4118                    boundary = boundary.max(1 + paragraph.runs.len());
4119                }
4120                Event::Empty(element) if is_text_run(&text_owned_name(&element, &namespaces)?) => {
4121                    let name = local_name(element.name().as_ref()).to_vec();
4122                    let raw = capture_empty_element(&element)?;
4123                    paragraph.runs.push(TextRun::from_xml_with_namespaces(
4124                        &raw,
4125                        &name,
4126                        &namespaces,
4127                    )?);
4128                    boundary = boundary.max(1 + paragraph.runs.len());
4129                }
4130                Event::Start(element)
4131                    if text_owned_name(&element, &namespaces)? == b"endParaRPr" =>
4132                {
4133                    if paragraph.end_properties.is_some() {
4134                        return Err(duplicate("endParaRPr"));
4135                    }
4136                    paragraph.end_properties =
4137                        Some(CT_TextCharacterProperties::from_element_with_namespaces(
4138                            reader,
4139                            &element,
4140                            &namespaces,
4141                        )?);
4142                    boundary = boundary.max(2 + paragraph.runs.len());
4143                }
4144                Event::Empty(element)
4145                    if text_owned_name(&element, &namespaces)? == b"endParaRPr" =>
4146                {
4147                    if paragraph.end_properties.is_some() {
4148                        return Err(duplicate("endParaRPr"));
4149                    }
4150                    paragraph.end_properties =
4151                        Some(CT_TextCharacterProperties::from_start_with_namespaces(
4152                            &element,
4153                            &namespaces,
4154                        )?);
4155                    boundary = boundary.max(2 + paragraph.runs.len());
4156                }
4157                Event::Start(element) => paragraph
4158                    .raw_children
4159                    .push(boundary, capture_element(reader, &element)?),
4160                Event::Empty(element) => paragraph
4161                    .raw_children
4162                    .push(boundary, capture_empty_element(&element)?),
4163                Event::End(element) if matches_local_name(element.name().as_ref(), b"p") => {
4164                    return Ok(paragraph);
4165                }
4166                Event::Eof => return Err(missing_end("p")),
4167                event @ (Event::Comment(_) | Event::PI(_) | Event::Text(_) | Event::CData(_)) => {
4168                    paragraph
4169                        .raw_children
4170                        .push(boundary, text_capture_opaque_event(event)?);
4171                }
4172                _ => {}
4173            }
4174            buffer.clear();
4175        }
4176    }
4177
4178    /// Writes a paragraph with fixed prefixes and schema child order.
4179    pub fn write_xml<W: Write>(&self, writer: &mut Writer<W>) -> Result<()> {
4180        let owned_tag = text_source_tag(
4181            "a:p",
4182            &self.source_tag,
4183            &text_namespace_scope(&text_source_start("a:p", &self.source_root), &[])?,
4184        );
4185        let tag = owned_tag.as_str();
4186        if self.properties.is_none()
4187            && self.runs.is_empty()
4188            && self.end_properties.is_none()
4189            && self.raw_children.is_empty()
4190        {
4191            return write_empty(writer, text_source_start(tag, &self.source_root));
4192        }
4193        write_start(writer, text_source_start(tag, &self.source_root))?;
4194        emit_raw(writer, self.raw_children.at(0))?;
4195        if let Some(properties) = &self.properties {
4196            {
4197                let mut child = Writer::new(Vec::new());
4198                properties.write_xml(&mut child, "a:pPr")?;
4199                writer
4200                    .get_mut()
4201                    .write_all(&text_close_generated_child(
4202                        &child.into_inner(),
4203                        &text_namespace_scope(&text_source_start(tag, &self.source_root), &[])?,
4204                    )?)
4205                    .map_err(OxmlError::from)?;
4206            }
4207        }
4208        emit_raw(writer, self.raw_children.at(1))?;
4209        for (index, run) in self.runs.iter().enumerate() {
4210            run.write_xml(writer)?;
4211            emit_raw(writer, self.raw_children.at(2 + index))?;
4212        }
4213        if let Some(properties) = &self.end_properties {
4214            {
4215                let mut child = Writer::new(Vec::new());
4216                properties.write_xml(&mut child, "a:endParaRPr")?;
4217                writer
4218                    .get_mut()
4219                    .write_all(&text_close_generated_child(
4220                        &child.into_inner(),
4221                        &text_namespace_scope(&text_source_start(tag, &self.source_root), &[])?,
4222                    )?)
4223                    .map_err(OxmlError::from)?;
4224            }
4225        }
4226        emit_raw(writer, self.raw_children.at(2 + self.runs.len()))?;
4227        write_end(writer, tag)
4228    }
4229
4230    /// Serialises a complete paragraph fragment.
4231    pub fn to_xml(&self) -> Result<Vec<u8>> {
4232        let mut writer = Writer::new(Vec::new());
4233        self.write_xml(&mut writer)?;
4234        Ok(writer.into_inner())
4235    }
4236
4237    pub fn raw_children(&self) -> &OrderedRawChildren {
4238        &self.raw_children
4239    }
4240}
4241
4242fn is_text_run(name: &[u8]) -> bool {
4243    matches!(local_name(name), b"r" | b"br" | b"fld")
4244}
4245
4246fn parse_complete<T>(
4247    xml: &[u8],
4248    expected: &[u8],
4249    parse_start: impl FnOnce(&mut Reader<&[u8]>, &BytesStart<'_>) -> Result<T>,
4250    parse_empty: impl FnOnce(&BytesStart<'_>) -> Result<T>,
4251) -> Result<T> {
4252    let mut reader = Reader::from_reader(xml);
4253    let mut buffer = Vec::new();
4254    loop {
4255        match reader
4256            .read_event_into(&mut buffer)
4257            .map_err(OxmlError::from)?
4258        {
4259            Event::Start(element) if matches_local_name(element.name().as_ref(), expected) => {
4260                text_validate_root(&element)?;
4261                let parsed = parse_start(&mut reader, &element)?;
4262                text_reject_trailing(&mut reader)?;
4263                return Ok(parsed);
4264            }
4265            Event::Empty(element) if matches_local_name(element.name().as_ref(), expected) => {
4266                text_validate_root(&element)?;
4267                let parsed = parse_empty(&element)?;
4268                text_reject_trailing(&mut reader)?;
4269                return Ok(parsed);
4270            }
4271            Event::Start(element) | Event::Empty(element) => return Err(unexpected(&element)),
4272            Event::Eof => {
4273                return Err(TextError::Xml(OxmlError::MissingElement(
4274                    String::from_utf8_lossy(expected).into_owned(),
4275                )));
4276            }
4277            _ => {}
4278        }
4279        buffer.clear();
4280    }
4281}
4282
4283pub(crate) fn text_reject_trailing(reader: &mut Reader<&[u8]>) -> Result<()> {
4284    loop {
4285        match reader.read_event().map_err(OxmlError::from)? {
4286            Event::Eof => return Ok(()),
4287            Event::Comment(_) | Event::PI(_) => {}
4288            Event::Text(text) if text.iter().all(u8::is_ascii_whitespace) => {}
4289            _ => {
4290                return Err(TextError::UnexpectedElement(
4291                    "trailing XML content".to_owned(),
4292                ));
4293            }
4294        }
4295    }
4296}
4297
4298fn root_local_name(xml: &[u8]) -> Result<Vec<u8>> {
4299    let mut reader = Reader::from_reader(xml);
4300    let mut buffer = Vec::new();
4301    loop {
4302        match reader
4303            .read_event_into(&mut buffer)
4304            .map_err(OxmlError::from)?
4305        {
4306            Event::Start(element) | Event::Empty(element) => {
4307                return Ok(local_name(element.name().as_ref()).to_vec());
4308            }
4309            Event::Eof => {
4310                return Err(TextError::Xml(OxmlError::MissingElement(
4311                    "DrawingML text properties".to_owned(),
4312                )));
4313            }
4314            _ => {}
4315        }
4316        buffer.clear();
4317    }
4318}
4319
4320fn ensure_empty(reader: &mut Reader<&[u8]>, expected: &[u8]) -> Result<()> {
4321    let mut buffer = Vec::new();
4322    loop {
4323        match reader
4324            .read_event_into(&mut buffer)
4325            .map_err(OxmlError::from)?
4326        {
4327            Event::End(element) if matches_local_name(element.name().as_ref(), expected) => {
4328                return Ok(());
4329            }
4330            Event::Text(text) if text.iter().all(u8::is_ascii_whitespace) => {}
4331            Event::Comment(_) => {}
4332            Event::Start(element) | Event::Empty(element) => return Err(unexpected(&element)),
4333            Event::Eof => return Err(missing_end(&String::from_utf8_lossy(expected))),
4334            _ => {
4335                return Err(TextError::UnexpectedElement(
4336                    String::from_utf8_lossy(expected).into_owned(),
4337                ));
4338            }
4339        }
4340        buffer.clear();
4341    }
4342}
4343
4344fn text_attr(start: &BytesStart<'_>, name: &[u8]) -> Result<Option<String>> {
4345    for attribute in start.attributes() {
4346        let attribute = attribute.map_err(OxmlError::from)?;
4347        if attribute.key.as_ref() == name {
4348            let value = attribute
4349                .decoded_and_normalized_value(XmlVersion::Implicit1_0, start.decoder())
4350                .map_err(OxmlError::from)?;
4351            return Ok(Some(value.into_owned()));
4352        }
4353    }
4354    Ok(None)
4355}
4356
4357fn capture_raw_attributes(
4358    start: &BytesStart<'_>,
4359    modelled: &[&[u8]],
4360) -> Result<Vec<(String, String)>> {
4361    let mut raw = Vec::new();
4362    for attribute in start.attributes() {
4363        let attribute = attribute.map_err(OxmlError::from)?;
4364        if modelled.iter().any(|name| attribute.key.as_ref() == *name) {
4365            continue;
4366        }
4367        let name = std::str::from_utf8(attribute.key.as_ref())
4368            .map_err(OxmlError::from)?
4369            .to_owned();
4370        let value = attribute
4371            .decoded_and_normalized_value(XmlVersion::Implicit1_0, start.decoder())
4372            .map_err(OxmlError::from)?
4373            .into_owned();
4374        raw.push((name, value));
4375    }
4376    Ok(raw)
4377}
4378
4379fn push_raw_attributes(start: &mut BytesStart<'_>, attributes: &[(String, String)]) {
4380    for (name, value) in attributes {
4381        start.push_attribute((name.as_str(), value.as_str()));
4382    }
4383}
4384
4385fn required_attr(start: &BytesStart<'_>, name: &[u8]) -> Result<String> {
4386    text_attr(start, name)?
4387        .filter(|value| !value.is_empty())
4388        .ok_or_else(|| {
4389            missing_attribute(
4390                &String::from_utf8_lossy(local_name(start.name().as_ref())),
4391                &String::from_utf8_lossy(name),
4392            )
4393        })
4394}
4395
4396fn parse_optional_i32(
4397    start: &BytesStart<'_>,
4398    name: &[u8],
4399    min: i32,
4400    max: i32,
4401) -> Result<Option<i32>> {
4402    text_attr(start, name)?
4403        .map(|value| {
4404            parse_i32_value(
4405                &String::from_utf8_lossy(local_name(start.name().as_ref())),
4406                &String::from_utf8_lossy(name),
4407                &value,
4408                min,
4409                max,
4410            )
4411        })
4412        .transpose()
4413}
4414
4415fn parse_i32_attr(start: &BytesStart<'_>, name: &[u8], min: i32, max: i32) -> Result<i32> {
4416    let value = required_attr(start, name)?;
4417    parse_i32_value(
4418        &String::from_utf8_lossy(local_name(start.name().as_ref())),
4419        &String::from_utf8_lossy(name),
4420        &value,
4421        min,
4422        max,
4423    )
4424}
4425
4426fn parse_i32_value(element: &str, attribute: &str, value: &str, min: i32, max: i32) -> Result<i32> {
4427    let parsed = value
4428        .parse::<i32>()
4429        .map_err(|_| invalid_attribute(element, attribute, value))?;
4430    if (min..=max).contains(&parsed) {
4431        Ok(parsed)
4432    } else {
4433        Err(invalid_attribute(element, attribute, value))
4434    }
4435}
4436
4437fn parse_optional_bool(start: &BytesStart<'_>, name: &[u8]) -> Result<Option<bool>> {
4438    text_attr(start, name)?
4439        .map(|value| match value.as_str() {
4440            "true" | "1" => Ok(true),
4441            "false" | "0" => Ok(false),
4442            _ => Err(invalid_attribute(
4443                &String::from_utf8_lossy(local_name(start.name().as_ref())),
4444                &String::from_utf8_lossy(name),
4445                &value,
4446            )),
4447        })
4448        .transpose()
4449}
4450
4451fn parse_optional_enum<T>(
4452    start: &BytesStart<'_>,
4453    name: &[u8],
4454    parse: impl FnOnce(&str) -> Option<T>,
4455) -> Result<Option<T>> {
4456    let Some(value) = text_attr(start, name)? else {
4457        return Ok(None);
4458    };
4459    parse(&value).map(Some).ok_or_else(|| {
4460        invalid_attribute(
4461            &String::from_utf8_lossy(local_name(start.name().as_ref())),
4462            &String::from_utf8_lossy(name),
4463            &value,
4464        )
4465    })
4466}
4467
4468fn validate_spacing_percent(value: &str) -> Result<()> {
4469    if is_percentage_string(value) {
4470        return Ok(());
4471    }
4472    parse_i32_value("spcPct", "val", value, 0, MAX_TRANSITIONAL_SPACING_PERCENT)?;
4473    Ok(())
4474}
4475
4476fn validate_baseline(value: &str) -> Result<()> {
4477    if is_percentage_string(value) {
4478        return Ok(());
4479    }
4480    parse_i32_value("rPr", "baseline", value, -100_000, 100_000)?;
4481    Ok(())
4482}
4483
4484fn is_percentage_string(value: &str) -> bool {
4485    value.strip_suffix('%').is_some_and(is_signed_decimal)
4486}
4487
4488fn is_universal_measure(value: &str) -> bool {
4489    ["mm", "cm", "in", "pt", "pc", "pi"]
4490        .iter()
4491        .find_map(|unit| value.strip_suffix(unit))
4492        .is_some_and(is_signed_decimal)
4493}
4494
4495fn is_signed_decimal(value: &str) -> bool {
4496    let unsigned = value.strip_prefix('-').unwrap_or(value);
4497    let mut parts = unsigned.split('.');
4498    let Some(integer) = parts.next() else {
4499        return false;
4500    };
4501    if integer.is_empty() || !integer.bytes().all(|byte| byte.is_ascii_digit()) {
4502        return false;
4503    }
4504    if let Some(fraction) = parts.next()
4505        && (fraction.is_empty() || !fraction.bytes().all(|byte| byte.is_ascii_digit()))
4506    {
4507        return false;
4508    }
4509    parts.next().is_none()
4510}
4511
4512fn is_guid(value: &str) -> bool {
4513    if value.len() != 38 || !value.starts_with('{') || !value.ends_with('}') {
4514        return false;
4515    }
4516    value.bytes().enumerate().all(|(index, byte)| match index {
4517        0 => byte == b'{',
4518        37 => byte == b'}',
4519        9 | 14 | 19 | 24 => byte == b'-',
4520        _ => byte.is_ascii_hexdigit(),
4521    })
4522}
4523
4524fn push_optional_attr<'a>(start: &mut BytesStart<'a>, name: &'a str, value: Option<&'a str>) {
4525    if let Some(value) = value {
4526        start.push_attribute((name, value));
4527    }
4528}
4529
4530fn push_optional_bool(start: &mut BytesStart<'_>, name: &'static str, value: Option<bool>) {
4531    if let Some(value) = value {
4532        start.push_attribute((name, if value { "1" } else { "0" }));
4533    }
4534}
4535
4536fn push_owned_attr(start: &mut BytesStart<'_>, name: &'static str, value: String) {
4537    start.push_attribute((name, value.as_str()));
4538}
4539
4540fn emit_raw<'a, W: Write>(
4541    writer: &mut Writer<W>,
4542    children: impl Iterator<Item = &'a [u8]>,
4543) -> Result<()> {
4544    for child in children {
4545        writer.get_mut().write_all(child).map_err(OxmlError::from)?;
4546    }
4547    Ok(())
4548}
4549
4550fn write_start<W: Write>(writer: &mut Writer<W>, start: BytesStart<'_>) -> Result<()> {
4551    writer
4552        .write_event(Event::Start(start))
4553        .map_err(OxmlError::from)?;
4554    Ok(())
4555}
4556
4557fn write_empty<W: Write>(writer: &mut Writer<W>, start: BytesStart<'_>) -> Result<()> {
4558    writer
4559        .write_event(Event::Empty(start))
4560        .map_err(OxmlError::from)?;
4561    Ok(())
4562}
4563
4564fn write_end<W: Write>(writer: &mut Writer<W>, tag: &str) -> Result<()> {
4565    writer
4566        .write_event(Event::End(BytesEnd::new(tag)))
4567        .map_err(OxmlError::from)?;
4568    Ok(())
4569}
4570
4571fn unexpected(element: &BytesStart<'_>) -> TextError {
4572    TextError::UnexpectedElement(String::from_utf8_lossy(element.name().as_ref()).into_owned())
4573}
4574
4575fn duplicate(element: &str) -> TextError {
4576    TextError::DuplicateElement(element.to_owned())
4577}
4578
4579fn missing_attribute(element: &str, attribute: &str) -> TextError {
4580    TextError::MissingAttribute {
4581        element: element.to_owned(),
4582        attribute: attribute.to_owned(),
4583    }
4584}
4585
4586fn invalid_attribute(element: &str, attribute: &str, value: &str) -> TextError {
4587    TextError::InvalidAttribute {
4588        element: element.to_owned(),
4589        attribute: attribute.to_owned(),
4590        value: value.to_owned(),
4591    }
4592}
4593
4594#[cfg(test)]
4595mod tests {
4596    use quick_xml::Writer;
4597    use std::panic;
4598
4599    use super::{
4600        CT_TextCharacterProperties, CT_TextParagraph, CT_TextParagraphProperties, TextAlignment,
4601        TextHyperlink, TextRun, TextSpace, TextSpacing,
4602    };
4603    use crate::color::ColorChoice;
4604    use crate::text::CT_TextBody;
4605    use crate::text::{
4606        TextAutoNumberScheme, TextBullet, TextBulletCharacter, TextBulletChoice,
4607        TextBulletSizeValue,
4608    };
4609
4610    #[test]
4611    fn fx205_language_edits_preserve_other_attributes_and_font_source() {
4612        let source = br#"<a:rPr xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:e="urn:opaque" lang="en-US" e:lang="unchanged" dirty="0"><a:ea typeface="Arial" panose="020B0604020202020204"/><e:payload keep="yes"/></a:rPr>"#;
4613        let mut properties = CT_TextCharacterProperties::from_xml(source).unwrap();
4614        let original = properties.clone();
4615        properties.set_language(Some("ja-JP"));
4616        assert_eq!(properties.language(), Some("ja-JP"));
4617        let mut writer = Writer::new(Vec::new());
4618        properties.write_xml(&mut writer, "a:rPr").unwrap();
4619        let saved = writer.into_inner();
4620        let xml = String::from_utf8(saved.clone()).unwrap();
4621        assert!(xml.contains("e:lang=\"unchanged\""));
4622        assert!(xml.contains("<e:payload keep=\"yes\"/>"));
4623        assert!(xml.contains("panose=\"020B0604020202020204\""));
4624        let reopened = CT_TextCharacterProperties::from_xml(&saved).unwrap();
4625        assert_eq!(reopened.language(), Some("ja-JP"));
4626        assert_eq!(reopened.east_asian, properties.east_asian);
4627        let mut writer = Writer::new(Vec::new());
4628        reopened.write_xml(&mut writer, "a:rPr").unwrap();
4629        assert_eq!(writer.into_inner(), saved);
4630        properties.set_language(Some("en-US"));
4631        assert_eq!(properties, original);
4632        properties.set_language(None);
4633        assert_eq!(properties.language(), None);
4634        assert!(super::TextFont::new("Arial\u{0}").is_err());
4635    }
4636
4637    #[test]
4638    fn fx206_picture_bullet_list_equality_preserves_foreign_and_opaque_owners() {
4639        let source = br#"<a:txBody xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"><a:bodyPr/><a:lstStyle><a:lvl1pPr><a:defRPr><a:solidFill><a:srgbClr val="5D5F64"/></a:solidFill></a:defRPr></a:lvl1pPr><a:lvl2pPr><a:buFontTx/><a:buBlip><a:blip r:embed="rId3"/></a:buBlip><a:defRPr><a:solidFill><a:srgbClr val="F89728"/></a:solidFill></a:defRPr></a:lvl2pPr></a:lstStyle><a:p/></a:txBody>"#;
4640        let body = CT_TextBody::from_xml(source).unwrap();
4641        let saved = body.to_xml().unwrap();
4642        let reopened = CT_TextBody::from_xml(&saved).unwrap();
4643        assert_eq!(reopened, body);
4644        assert_eq!(reopened.to_xml().unwrap(), saved);
4645        let changed_owner = String::from_utf8_lossy(source).replace("rId3", "rId4");
4646        assert_ne!(
4647            body,
4648            CT_TextBody::from_xml(changed_owner.as_bytes()).unwrap()
4649        );
4650        assert!(String::from_utf8_lossy(&saved).contains("r:embed=\"rId3\""));
4651
4652        let plain = crate::text::CT_TextListStyle::from_xml(
4653            br#"<a:lstStyle xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"/>"#,
4654        )
4655        .unwrap();
4656        let canonical = crate::text::CT_TextListStyle::from_xml(br#"<a:lstStyle xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"/>"#).unwrap();
4657        let foreign = crate::text::CT_TextListStyle::from_xml(br#"<a:lstStyle xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:r="urn:foreign"/>"#).unwrap();
4658        let opaque = crate::text::CT_TextListStyle::from_xml(br#"<a:lstStyle xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" keep="opaque"/>"#).unwrap();
4659        assert_eq!(plain, canonical);
4660        assert_ne!(plain, foreign);
4661        assert_ne!(plain, opaque);
4662        let opaque_child = crate::text::CT_TextListStyle::from_xml(br#"<a:lstStyle xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"><a:extLst/></a:lstStyle>"#).unwrap();
4663        assert_ne!(plain, opaque_child);
4664        assert!(
4665            String::from_utf8_lossy(&foreign.to_xml().unwrap()).contains("xmlns:r=\"urn:foreign\"")
4666        );
4667        assert!(String::from_utf8_lossy(&opaque.to_xml().unwrap()).contains("keep=\"opaque\""));
4668
4669        let first = CT_TextParagraphProperties::default();
4670        let mut transport = first.clone();
4671        transport
4672            .raw_attributes
4673            .push(("xmlns:r".into(), super::TEXT_REL_NS.into()));
4674        assert_eq!(first, transport);
4675        transport.raw_attributes[0].1 = "urn:foreign".into();
4676        assert_ne!(first, transport);
4677        transport.raw_attributes.clear();
4678        transport
4679            .raw_attributes
4680            .push(("keep".into(), "opaque".into()));
4681        assert_ne!(first, transport);
4682        transport.raw_attributes.clear();
4683        transport.level = Some(1);
4684        assert_ne!(first, transport);
4685    }
4686
4687    #[test]
4688    fn fx206_hyperlink_equality_ignores_only_canonical_transport_declarations() {
4689        let body = CT_TextBody::from_xml(br#"<a:txBody xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"><a:bodyPr/><a:lstStyle/><a:p><a:r><a:rPr><a:hlinkClick r:id=""/></a:rPr><a:t>link</a:t></a:r></a:p></a:txBody>"#).unwrap();
4690        let bytes = body.to_xml().unwrap();
4691        assert_eq!(CT_TextBody::from_xml(&bytes).unwrap(), body);
4692        let mut first = TextHyperlink::default();
4693        let mut canonical = first.clone();
4694        canonical.raw_attributes = vec![
4695            ("xmlns:a".into(), super::A_NS.into()),
4696            ("xmlns:r".into(), super::TEXT_REL_NS.into()),
4697        ];
4698        assert_eq!(first, canonical);
4699        first
4700            .raw_attributes
4701            .push(("xmlns:r".into(), "urn:foreign".into()));
4702        assert_ne!(first, canonical);
4703        canonical
4704            .raw_attributes
4705            .push(("foreign".into(), "kept".into()));
4706        assert_ne!(TextHyperlink::default(), canonical);
4707    }
4708
4709    #[test]
4710    fn drawingml_rtl_attribute_becomes_typed_without_reordering_unknown_content() {
4711        let parsed = CT_TextParagraphProperties::from_xml(
4712            br#"<q:pPr xmlns:q="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:x="urn:test" defTabSz="457200" rtl="1" x:keep="yes"/>"#,
4713        )
4714        .unwrap();
4715
4716        assert_eq!(parsed.right_to_left, Some(true));
4717        let serialized = String::from_utf8(parsed.to_xml().unwrap()).unwrap();
4718        assert!(serialized.contains("rtl=\"1\""));
4719        assert!(serialized.contains("x:keep=\"yes\""));
4720        assert!(
4721            serialized.find("defTabSz").unwrap() < serialized.find("rtl").unwrap(),
4722            "{serialized}"
4723        );
4724
4725        let mut changed = parsed;
4726        changed.right_to_left = Some(false);
4727        let serialized = String::from_utf8(changed.to_xml().unwrap()).unwrap();
4728        assert!(serialized.contains("rtl=\"0\""), "{serialized}");
4729        assert!(!serialized.contains("rtl=\"1\""), "{serialized}");
4730    }
4731
4732    #[test]
4733    fn leading_and_trailing_text_whitespace_survives_via_xml_space_preserve() {
4734        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>"#;
4735        let paragraph = CT_TextParagraph::from_xml(xml).unwrap();
4736        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>"#);
4737
4738        let TextRun::Run(first) = &paragraph.runs[0] else {
4739            panic!("expected regular run");
4740        };
4741        assert_eq!(first.text.value, " leading");
4742        assert_eq!(first.text.space, TextSpace::Preserve);
4743
4744        let body = CT_TextBody::from_xml(
4745            br#"<q:txBody><q:bodyPr/><q:p><q:r><q:t> body </q:t></q:r></q:p></q:txBody>"#,
4746        )
4747        .unwrap();
4748        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>"#);
4749
4750        let hostile = CT_TextParagraph::from_xml(
4751            br#"<q:p><q:r><q:t x:space="preserve">plain</q:t></q:r></q:p>"#,
4752        )
4753        .unwrap();
4754        let TextRun::Run(run) = &hostile.runs[0] else {
4755            panic!("expected regular run");
4756        };
4757        assert_eq!(run.text.space, TextSpace::Default);
4758        assert_eq!(
4759            hostile.to_xml().unwrap(),
4760            br#"<a:p><a:r><a:t x:space="preserve">plain</a:t></a:r></a:p>"#
4761        );
4762    }
4763
4764    #[test]
4765    fn canonical_text_state_matches_whitespace_and_empty_hyperlinks_from_real_decks() {
4766        let paragraph = CT_TextParagraph::from_xml(
4767            br#"<q:p><q:r><q:rPr><q:hlinkClick r:id=""/></q:rPr><q:t>trailing </q:t></q:r></q:p>"#,
4768        )
4769        .unwrap();
4770        let TextRun::Run(run) = &paragraph.runs[0] else {
4771            panic!("expected regular run");
4772        };
4773        assert_eq!(run.text.space, TextSpace::Preserve);
4774        assert_eq!(
4775            run.properties
4776                .as_ref()
4777                .and_then(|properties| properties.hyperlink_click.as_ref())
4778                .and_then(|hyperlink| hyperlink.relationship_id.as_deref()),
4779            Some("")
4780        );
4781        let written = paragraph.to_xml().unwrap();
4782        assert_eq!(CT_TextParagraph::from_xml(&written).unwrap(), paragraph);
4783    }
4784
4785    #[test]
4786    fn text_written_into_an_empty_paragraph_takes_its_end_formatting() {
4787        let mut paragraph = CT_TextParagraph::from_xml(
4788            br#"<a:p xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"><a:pPr algn="r"/><a:endParaRPr sz="1500" b="1"><a:hlinkClick r:id="rId9"/></a:endParaRPr></a:p>"#,
4789        )
4790        .unwrap();
4791        paragraph.set_text("Risk");
4792        assert_eq!(
4793            String::from_utf8(paragraph.to_xml().unwrap()).unwrap(),
4794            r#"<a:p xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"><a:pPr algn="r"/><a:r><a:rPr sz="1500" b="1" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"/><a:t>Risk</a:t></a:r><a:endParaRPr sz="1500" b="1" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"><a:hlinkClick r:id="rId9" xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"/></a:endParaRPr></a:p>"#
4795        );
4796
4797        let saved = paragraph.to_xml().unwrap();
4798        assert_eq!(
4799            CT_TextParagraph::from_xml(&saved)
4800                .unwrap()
4801                .to_xml()
4802                .unwrap(),
4803            saved
4804        );
4805
4806        let mut formatted = CT_TextParagraph::from_xml(
4807            br#"<a:p xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"><a:r><a:rPr i="1"/><a:t>old</a:t></a:r><a:endParaRPr sz="1500"/></a:p>"#,
4808        )
4809        .unwrap();
4810        formatted.set_text("new");
4811        assert_eq!(
4812            String::from_utf8(formatted.to_xml().unwrap()).unwrap(),
4813            r#"<a:p xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"><a:r><a:rPr i="1"/><a:t>new</a:t></a:r><a:endParaRPr sz="1500"/></a:p>"#
4814        );
4815
4816        let mut bare = CT_TextParagraph::default();
4817        bare.set_text("plain");
4818        assert_eq!(
4819            bare.to_xml().unwrap(),
4820            br#"<a:p><a:r><a:t>plain</a:t></a:r></a:p>"#
4821        );
4822    }
4823
4824    #[test]
4825    fn paragraph_runs_fields_and_breaks_round_trip_structurally() {
4826        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>"#;
4827        let paragraph = CT_TextParagraph::from_xml(xml).unwrap();
4828        assert_eq!(paragraph.runs.len(), 3);
4829        let written = paragraph.to_xml().unwrap();
4830        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>"#);
4831        assert_eq!(CT_TextParagraph::from_xml(&written).unwrap(), paragraph);
4832
4833        let hostile = CT_TextCharacterProperties::from_xml(
4834            br#"<q:rPr><q:hlinkClick x:id="not-a-relationship"/></q:rPr>"#,
4835        )
4836        .unwrap();
4837        assert!(
4838            hostile
4839                .hyperlink_click
4840                .as_ref()
4841                .unwrap()
4842                .relationship_id
4843                .is_none()
4844        );
4845        let mut writer = quick_xml::Writer::new(Vec::new());
4846        hostile.write_xml(&mut writer, "a:rPr").unwrap();
4847        assert_eq!(
4848            writer.into_inner(),
4849            br#"<a:rPr><a:hlinkClick x:id="not-a-relationship"/></a:rPr>"#
4850        );
4851    }
4852
4853    #[test]
4854    fn a_later_paragraph_properties_element_is_preserved_where_it_stands() {
4855        let xml = br#"<a:p><a:pPr algn="l"/><a:r><a:t>One.</a:t></a:r><a:pPr algn="r"><a:lnSpc><a:spcPct val="90000"/></a:lnSpc></a:pPr><a:r><a:t>Two.</a:t></a:r><a:pPr/><a:endParaRPr lang="en-US"/></a:p>"#;
4856        let mut paragraph = CT_TextParagraph::from_xml(xml).unwrap();
4857        let properties = paragraph.properties.as_ref().unwrap();
4858        assert_eq!(properties.alignment, Some(TextAlignment::Left));
4859        assert_eq!(properties.line_spacing, None);
4860        let texts = paragraph
4861            .runs
4862            .iter()
4863            .map(|run| match run {
4864                TextRun::Run(run) => run.text.value.as_str(),
4865                TextRun::Break(_) | TextRun::Field(_) => panic!("expected regular runs"),
4866            })
4867            .collect::<Vec<_>>();
4868        assert_eq!(texts, ["One.", "Two."]);
4869        assert_eq!(paragraph.to_xml().unwrap(), xml);
4870        assert_eq!(CT_TextParagraph::from_xml(xml).unwrap(), paragraph);
4871
4872        let TextRun::Run(first) = &mut paragraph.runs[0] else {
4873            panic!("expected regular run");
4874        };
4875        first.set_text("Uno.");
4876        paragraph.properties_mut().level = Some(1);
4877        paragraph.add_run("Three.");
4878        assert_eq!(
4879            paragraph.to_xml().unwrap(),
4880            br#"<a:p><a:pPr lvl="1" algn="l"/><a:r><a:t>Uno.</a:t></a:r><a:pPr algn="r"><a:lnSpc><a:spcPct val="90000"/></a:lnSpc></a:pPr><a:r><a:t>Two.</a:t></a:r><a:pPr/><a:r><a:t>Three.</a:t></a:r><a:endParaRPr lang="en-US"/></a:p>"#
4881        );
4882
4883        paragraph.set_text("Whole.");
4884        assert_eq!(
4885            paragraph.to_xml().unwrap(),
4886            br#"<a:p><a:pPr lvl="1" algn="l"/><a:r><a:t>Whole.</a:t></a:r><a:pPr algn="r"><a:lnSpc><a:spcPct val="90000"/></a:lnSpc></a:pPr><a:pPr/><a:endParaRPr lang="en-US"/></a:p>"#
4887        );
4888
4889        let late = CT_TextParagraph::from_xml(
4890            br#"<a:p><a:r><a:t>One.</a:t></a:r><a:pPr algn="l"/><a:r><a:t>Two.</a:t></a:r><a:pPr algn="r"/></a:p>"#,
4891        )
4892        .unwrap();
4893        assert_eq!(
4894            late.properties.as_ref().unwrap().alignment,
4895            Some(TextAlignment::Left)
4896        );
4897        assert_eq!(
4898            late.to_xml().unwrap(),
4899            br#"<a:p><a:pPr algn="l"/><a:r><a:t>One.</a:t></a:r><a:r><a:t>Two.</a:t></a:r><a:pPr algn="r"/></a:p>"#
4900        );
4901    }
4902
4903    #[test]
4904    fn fx206_foreign_highlight_does_not_collide_with_owned_highlight() {
4905        let foreign =
4906            br#"<x:highlight xmlns:x="urn:producer"><x:payload code="kept"/></x:highlight>"#;
4907        let xml = br#"<a:rPr xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"><x:highlight xmlns:x="urn:producer"><x:payload code="kept"/></x:highlight><a:highlight><a:srgbClr val="FFFF00"/></a:highlight><a:latin typeface="Arial"/></a:rPr>"#;
4908        let properties = CT_TextCharacterProperties::from_xml(xml)
4909            .expect("foreign extension and owned highlight are different expanded names");
4910        let mut writer = quick_xml::Writer::new(Vec::new());
4911        properties.write_xml(&mut writer, "a:rPr").unwrap();
4912        let written = writer.into_inner();
4913        assert!(written.windows(foreign.len()).any(|part| part == foreign));
4914        assert_eq!(
4915            String::from_utf8_lossy(&written)
4916                .matches("<a:highlight>")
4917                .count(),
4918            1
4919        );
4920        assert!(CT_TextCharacterProperties::from_xml(&written).is_ok());
4921    }
4922
4923    #[test]
4924    fn fx206_typed_highlight_qualified_colour_preserves_opaque_and_source_order() {
4925        let xml = br#"<a:rPr xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:q="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:x="urn:producer"><x:before/><q:highlight x:owner="keep"><x:beforeColour/><q:srgbClr val="FFFF00" xmlns:y="urn:colour"><x:alpha val="12345"/><q:alpha val="50000"></q:alpha><y:keep token="a&amp;b"/></q:srgbClr><x:afterColour/></q:highlight><x:after/><a:latin typeface="Carlito"/></a:rPr>"#;
4926        let mut properties = CT_TextCharacterProperties::from_xml(xml).unwrap();
4927        let colour = properties.highlight.as_ref().unwrap().clone();
4928        assert_eq!(
4929            colour.transforms().len(),
4930            1,
4931            "foreign alpha is not an owned colour transform"
4932        );
4933        assert!(!colour.raw_children().is_empty());
4934        let mut writer = Writer::new(Vec::new());
4935        properties.write_xml(&mut writer, "a:rPr").unwrap();
4936        assert_eq!(writer.into_inner(), xml);
4937        let before = properties.clone();
4938        assert!(properties.set_highlight(None).is_err());
4939        assert_eq!(properties, before, "opaque clear refuses before mutation");
4940        let mut replacement = colour.clone();
4941        if let ColorChoice::Srgb { value, .. } = &mut replacement {
4942            *value = crate::color::RgbColor::new(0, 255, 255);
4943        } else {
4944            panic!("fixture selected exact srgb colour");
4945        }
4946        properties.set_highlight(Some(replacement)).unwrap();
4947        let mut writer = Writer::new(Vec::new());
4948        properties.write_xml(&mut writer, "a:rPr").unwrap();
4949        let updated = writer.into_inner();
4950        let text = std::str::from_utf8(&updated).unwrap();
4951        assert!(text.contains("<x:alpha val=\"12345\"/>"));
4952        assert!(text.contains("<y:keep token=\"a&amp;b\"/>"));
4953        assert!(text.contains("xmlns:y=\"urn:colour\""));
4954        assert!(text.find("<x:beforeColour/>").unwrap() < text.find("val=\"00FFFF\"").unwrap());
4955        assert!(text.find("<x:afterColour/>").unwrap() < text.find("<x:after/>").unwrap());
4956        let reopened = CT_TextCharacterProperties::from_xml(&updated).unwrap();
4957        assert_eq!(reopened.highlight, properties.highlight);
4958        let mut again = Writer::new(Vec::new());
4959        reopened.write_xml(&mut again, "a:rPr").unwrap();
4960        assert_eq!(again.into_inner(), updated);
4961    }
4962
4963    #[test]
4964    fn fx206_highlight_colour_opaque_attributes_refuse_edit_before_publication() {
4965        let xml = br#"<a:rPr xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:x="urn:producer"><a:highlight><a:srgbClr val="FFFF00" x:token="raw&amp;value"/></a:highlight></a:rPr>"#;
4966        let mut properties = CT_TextCharacterProperties::from_xml(xml).unwrap();
4967        let before = properties.clone();
4968        assert!(
4969            properties
4970                .set_highlight(Some(ColorChoice::srgb(crate::color::RgbColor::new(
4971                    0, 255, 255
4972                ))))
4973                .is_err()
4974        );
4975        assert_eq!(properties, before);
4976        // A supplied direct native edit is checked before any rPr byte is written.
4977        properties.highlight = Some(ColorChoice::srgb(crate::color::RgbColor::new(0, 255, 255)));
4978        let mut writer = Writer::new(Vec::new());
4979        assert!(properties.write_xml(&mut writer, "a:rPr").is_err());
4980        assert!(writer.into_inner().is_empty());
4981    }
4982
4983    #[test]
4984    fn fx206_highlight_transform_opaque_attributes_refuse_destructive_edits() {
4985        for transform in [
4986            r#"<q:alpha val="50000" x:keep="&#65;"/>"#,
4987            r#"<q:alpha val="50000" x:keep="&#65;"> </q:alpha>"#,
4988        ] {
4989            let xml = format!(
4990                r#"<a:rPr xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:q="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:x="urn:producer"><q:highlight><q:srgbClr val="FFFF00">{transform}</q:srgbClr></q:highlight></a:rPr>"#
4991            );
4992            let mut properties = CT_TextCharacterProperties::from_xml(xml.as_bytes()).unwrap();
4993            assert_eq!(properties.highlight.as_ref().unwrap().transforms().len(), 1);
4994            let before = properties.clone();
4995            let mut writer = Writer::new(Vec::new());
4996            properties.write_xml(&mut writer, "a:rPr").unwrap();
4997            assert_eq!(writer.into_inner(), xml.as_bytes());
4998            let mut changed = properties.highlight.clone().unwrap();
4999            if let ColorChoice::Srgb { value, .. } = &mut changed {
5000                *value = crate::color::RgbColor::new(0, 255, 255);
5001            } else {
5002                panic!("fixture must select RGB");
5003            }
5004            for supplied in [Some(changed), None] {
5005                assert!(properties.set_highlight(supplied.clone()).is_err());
5006                assert_eq!(properties, before);
5007                let mut direct = properties.clone();
5008                direct.highlight = supplied;
5009                let mut rejected = Writer::new(Vec::new());
5010                assert!(direct.write_xml(&mut rejected, "a:rPr").is_err());
5011                assert!(rejected.into_inner().is_empty());
5012            }
5013            // Refusal leaves source and typed alpha intact across a real parse/write cycle.
5014            let mut writer = Writer::new(Vec::new());
5015            properties.write_xml(&mut writer, "a:rPr").unwrap();
5016            let bytes = writer.into_inner();
5017            assert_eq!(bytes, xml.as_bytes());
5018            let reopened = CT_TextCharacterProperties::from_xml(&bytes).unwrap();
5019            assert_eq!(reopened.highlight, before.highlight);
5020            let mut again = Writer::new(Vec::new());
5021            reopened.write_xml(&mut again, "a:rPr").unwrap();
5022            assert_eq!(again.into_inner(), bytes);
5023        }
5024        // An equally named foreign element is opaque, not a modeled transform.
5025        let xml = br#"<a:rPr xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:x="urn:producer"><a:highlight><a:srgbClr val="FFFF00"><x:alpha val="50000" x:keep="&#65;"/></a:srgbClr></a:highlight></a:rPr>"#;
5026        let mut properties = CT_TextCharacterProperties::from_xml(xml).unwrap();
5027        let mut changed = properties.highlight.clone().unwrap();
5028        assert!(changed.transforms().is_empty());
5029        if let ColorChoice::Srgb { value, .. } = &mut changed {
5030            *value = crate::color::RgbColor::new(0, 255, 255);
5031        }
5032        properties.set_highlight(Some(changed)).unwrap();
5033        let mut writer = Writer::new(Vec::new());
5034        properties.write_xml(&mut writer, "a:rPr").unwrap();
5035        let updated = writer.into_inner();
5036        assert!(
5037            std::str::from_utf8(&updated)
5038                .unwrap()
5039                .contains(r#"<x:alpha val="50000" x:keep="&#65;"/>"#)
5040        );
5041        let reopened = CT_TextCharacterProperties::from_xml(&updated).unwrap();
5042        assert_eq!(reopened.highlight, properties.highlight);
5043    }
5044
5045    #[test]
5046    fn fx206_duplicate_owned_highlight_is_refused_control() {
5047        let xml = br#"<a:rPr xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"><a:highlight><a:srgbClr val="FFFF00"/></a:highlight><a:highlight><a:srgbClr val="00FFFF"/></a:highlight></a:rPr>"#;
5048        assert!(CT_TextCharacterProperties::from_xml(xml).is_err());
5049    }
5050
5051    #[test]
5052    fn fx206_known_small_cap_is_owned_while_unknown_cap_remains_raw() {
5053        let small =
5054            CT_TextCharacterProperties::from_xml(br#"<a:rPr cap="small" lang="en-US"/>"#).unwrap();
5055        let unknown =
5056            CT_TextCharacterProperties::from_xml(br#"<a:rPr cap="producer" lang="en-US"/>"#)
5057                .unwrap();
5058        assert_eq!(
5059            small.all_caps, None,
5060            "legacy all-caps getter for small stays unset"
5061        );
5062        assert!(
5063            unknown
5064                .raw_attributes
5065                .iter()
5066                .any(|(name, value)| name == "cap" && value == "producer")
5067        );
5068        assert!(
5069            !small.raw_attributes.iter().any(|(name, _)| name == "cap"),
5070            "schema small-cap mode must have typed ownership, not an opaque attribute"
5071        );
5072        let mut writer = quick_xml::Writer::new(Vec::new());
5073        small.write_xml(&mut writer, "a:rPr").unwrap();
5074        assert_eq!(writer.into_inner(), br#"<a:rPr cap="small" lang="en-US"/>"#);
5075    }
5076
5077    #[test]
5078    fn fx206_conflicting_caps_refuse_before_serialized_output() {
5079        let properties = CT_TextCharacterProperties {
5080            all_caps: Some(true),
5081            small_caps: Some(true),
5082            ..Default::default()
5083        };
5084        let mut writer = quick_xml::Writer::new(Vec::new());
5085        assert!(properties.write_xml(&mut writer, "a:rPr").is_err());
5086        assert!(writer.into_inner().is_empty());
5087    }
5088
5089    #[test]
5090    fn fx206_checked_cap_updates_select_explicit_modes_and_preserve_other_on_none() {
5091        for initial in ["all", "small", "none"] {
5092            for update_all in [true, false] {
5093                for value in [None, Some(false), Some(true)] {
5094                    let input = format!(r#"<a:rPr cap="{initial}"/>"#);
5095                    let mut properties =
5096                        CT_TextCharacterProperties::from_xml(input.as_bytes()).unwrap();
5097                    if update_all {
5098                        properties.set_all_caps(value);
5099                    } else {
5100                        properties.set_small_caps(value);
5101                    }
5102                    let expected = match value {
5103                        Some(false) => Some("none"),
5104                        Some(true) if update_all => Some("all"),
5105                        Some(true) => Some("small"),
5106                        None if update_all && initial == "small" => Some("small"),
5107                        None if !update_all && initial == "all" => Some("all"),
5108                        None if initial == "none" => Some("none"),
5109                        None => None,
5110                    };
5111                    let mut writer = quick_xml::Writer::new(Vec::new());
5112                    properties.write_xml(&mut writer, "a:rPr").unwrap();
5113                    let written = writer.into_inner();
5114                    let expected_xml = expected.map_or_else(
5115                        || "<a:rPr/>".to_owned(),
5116                        |mode| format!(r#"<a:rPr cap="{mode}"/>"#),
5117                    );
5118                    assert_eq!(
5119                        written,
5120                        expected_xml.as_bytes(),
5121                        "initial={initial}, update_all={update_all}, value={value:?}"
5122                    );
5123                    let reparsed = CT_TextCharacterProperties::from_xml(&written).unwrap();
5124                    let mut reopened_writer = quick_xml::Writer::new(Vec::new());
5125                    reparsed.write_xml(&mut reopened_writer, "a:rPr").unwrap();
5126                    assert_eq!(reopened_writer.into_inner(), written);
5127                    let (requested, other) = if update_all {
5128                        (properties.all_caps, properties.small_caps)
5129                    } else {
5130                        (properties.small_caps, properties.all_caps)
5131                    };
5132                    assert_eq!(requested, value);
5133                    if value.is_some() {
5134                        assert_eq!(other, None, "explicit mode must clear the other flag");
5135                    }
5136                    assert!(
5137                        !(properties.all_caps == Some(true) && properties.small_caps == Some(true))
5138                    );
5139                }
5140            }
5141        }
5142    }
5143
5144    #[test]
5145    fn fx206_ancestor_highlight_scope_survives_typed_edit_and_foreign_shadow() {
5146        let source = br#"<a:r xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:q="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:f="urn:foreign"><a:rPr><q:highlight xmlns:q='urn:foreign'><f:payload code='&#65;'/></q:highlight><q:highlight><q:srgbClr val='FF0000'><f:extension code='&#66;'/></q:srgbClr></q:highlight></a:rPr><a:t>x</a:t></a:r>"#;
5147        let mut run = super::CT_RegularTextRun::from_xml(source).unwrap();
5148        let colour = run
5149            .properties
5150            .as_ref()
5151            .unwrap()
5152            .highlight
5153            .as_ref()
5154            .expect("ancestor-owned highlight must be typed, not opaque")
5155            .clone();
5156        let ColorChoice::Srgb { value, .. } = &colour else {
5157            panic!("source owner must resolve srgb");
5158        };
5159        assert_eq!(*value, crate::color::RgbColor::new(255, 0, 0));
5160        let save = |run: &super::CT_RegularTextRun| {
5161            let mut writer = quick_xml::Writer::new(Vec::new());
5162            run.write_xml(&mut writer).unwrap();
5163            writer.into_inner()
5164        };
5165        let first = save(&run);
5166        assert_eq!(
5167            save(&super::CT_RegularTextRun::from_xml(&first).unwrap()),
5168            first
5169        );
5170        let mut replacement = colour.clone();
5171        let ColorChoice::Srgb { value, .. } = &mut replacement else {
5172            unreachable!();
5173        };
5174        *value = crate::color::RgbColor::new(0, 255, 255);
5175        run.properties
5176            .as_mut()
5177            .unwrap()
5178            .set_highlight(Some(replacement))
5179            .unwrap();
5180        let changed = save(&run);
5181        for raw in [
5182            b"<q:highlight xmlns:q='urn:foreign'><f:payload code='&#65;'/></q:highlight>"
5183                .as_slice(),
5184            b"<f:extension code='&#66;'/>".as_slice(),
5185        ] {
5186            assert!(changed.windows(raw.len()).any(|part| part == raw));
5187        }
5188        let reopened = super::CT_RegularTextRun::from_xml(&changed).unwrap();
5189        let ColorChoice::Srgb { value, .. } = reopened
5190            .properties
5191            .as_ref()
5192            .unwrap()
5193            .highlight
5194            .as_ref()
5195            .unwrap()
5196        else {
5197            panic!("saved owner must remain srgb");
5198        };
5199        assert_eq!(*value, crate::color::RgbColor::new(0, 255, 255));
5200        assert_eq!(save(&reopened), changed);
5201        let duplicate = br#"<a:r xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:q="http://schemas.openxmlformats.org/drawingml/2006/main"><a:rPr><q:highlight><q:srgbClr val="FF0000"/></q:highlight><q:highlight><q:srgbClr val="00FF00"/></q:highlight></a:rPr><a:t>x</a:t></a:r>"#;
5202        assert!(super::CT_RegularTextRun::from_xml(duplicate).is_err());
5203    }
5204
5205    #[test]
5206    fn fx206_highlight_admits_alias_and_default_namespace_without_foreign_collision() {
5207        for xml in [
5208            br#"<q:rPr xmlns:q="http://schemas.openxmlformats.org/drawingml/2006/main"><q:highlight><q:srgbClr val="FFFF00"/></q:highlight><x:highlight xmlns:x="urn:foreign"/></q:rPr>"#.as_slice(),
5209            br#"<rPr xmlns="http://schemas.openxmlformats.org/drawingml/2006/main"><highlight><srgbClr val="FFFF00"/></highlight><highlight xmlns="urn:foreign"/></rPr>"#.as_slice(),
5210            br#"<a:rPr xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"><q:highlight xmlns:q="urn:foreign"/><q:highlight xmlns:q="http://schemas.openxmlformats.org/drawingml/2006/main"><q:srgbClr val="FFFF00"/></q:highlight></a:rPr>"#.as_slice(),
5211            br#"<a:rPr xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"><a:highlight xmlns:a="urn:foreign"/><a:highlight><a:srgbClr val="FFFF00"/></a:highlight></a:rPr>"#.as_slice(),
5212        ] {
5213            let properties = CT_TextCharacterProperties::from_xml(xml).unwrap();
5214            let mut writer = quick_xml::Writer::new(Vec::new());
5215            properties.write_xml(&mut writer, "a:rPr").unwrap();
5216            let written = writer.into_inner();
5217            assert!(String::from_utf8_lossy(&written).contains("urn:foreign"));
5218            assert!(String::from_utf8_lossy(&written).contains("FFFF00"));
5219            assert_eq!(CT_TextCharacterProperties::from_xml(&written).unwrap(), properties);
5220        }
5221    }
5222
5223    #[test]
5224    fn character_all_and_small_caps_round_trip_as_distinct_modes() {
5225        let all = CT_TextCharacterProperties::from_xml(
5226            br#"<q:rPr cap="all" sz="1800" x:producer="kept"/>"#,
5227        )
5228        .unwrap();
5229        assert_eq!(all.all_caps, Some(true));
5230        let mut writer = quick_xml::Writer::new(Vec::new());
5231        all.write_xml(&mut writer, "a:rPr").unwrap();
5232        let written = writer.into_inner();
5233        assert_eq!(
5234            written,
5235            br#"<a:rPr sz="1800" cap="all" x:producer="kept"/>"#
5236        );
5237        assert_eq!(CT_TextCharacterProperties::from_xml(&written).unwrap(), all);
5238
5239        let none = CT_TextCharacterProperties::from_xml(br#"<q:rPr cap="none"/>"#).unwrap();
5240        assert_eq!(none.all_caps, Some(false));
5241
5242        let small = CT_TextCharacterProperties::from_xml(br#"<q:rPr cap="small"/>"#).unwrap();
5243        assert_eq!(small.all_caps, None);
5244        assert_eq!(small.small_caps, Some(true));
5245        let mut writer = quick_xml::Writer::new(Vec::new());
5246        small.write_xml(&mut writer, "a:rPr").unwrap();
5247        assert_eq!(writer.into_inner(), br#"<a:rPr cap="small"/>"#);
5248    }
5249
5250    #[test]
5251    fn setting_all_caps_over_preserved_unknown_caps_writes_one_cap_attribute() {
5252        let mut properties =
5253            CT_TextCharacterProperties::from_xml(br#"<q:rPr cap="producer" lang="en-US"/>"#)
5254                .unwrap();
5255        properties.all_caps = Some(true);
5256        let mut writer = quick_xml::Writer::new(Vec::new());
5257        properties.write_xml(&mut writer, "a:rPr").unwrap();
5258        let written = writer.into_inner();
5259        assert_eq!(written, br#"<a:rPr cap="all" lang="en-US"/>"#);
5260        assert_eq!(
5261            CT_TextCharacterProperties::from_xml(&written)
5262                .unwrap()
5263                .all_caps,
5264            Some(true)
5265        );
5266
5267        properties.all_caps = None;
5268        let mut writer = quick_xml::Writer::new(Vec::new());
5269        properties.write_xml(&mut writer, "a:rPr").unwrap();
5270        assert_eq!(
5271            writer.into_inner(),
5272            br#"<a:rPr cap="producer" lang="en-US"/>"#
5273        );
5274    }
5275
5276    #[test]
5277    fn spacing_percentages_above_two_lines_parse_up_to_the_schema_maximum() {
5278        let xml = br#"<q:pPr><q:lnSpc><q:spcPct val="250000"/></q:lnSpc><q:spcBef><q:spcPct val="13200000"/></q:spcBef></q:pPr>"#;
5279        let properties = CT_TextParagraphProperties::from_xml(xml).unwrap();
5280        assert_eq!(
5281            properties.line_spacing,
5282            Some(TextSpacing::Percent("250000".to_owned()))
5283        );
5284        assert_eq!(
5285            properties.space_before,
5286            Some(TextSpacing::Percent("13200000".to_owned()))
5287        );
5288        assert_eq!(
5289            properties.to_xml().unwrap(),
5290            br#"<a:pPr><a:lnSpc><a:spcPct val="250000"/></a:lnSpc><a:spcBef><a:spcPct val="13200000"/></a:spcBef></a:pPr>"#
5291        );
5292    }
5293
5294    #[test]
5295    fn setting_a_bullet_part_removes_the_preserved_alternative_it_replaces() {
5296        let mut properties = CT_TextParagraphProperties::from_xml(
5297            br#"<q:pPr><q:buClrTx/><q:buSzTx/><q:buFontTx/><q:buBlip><q:blip x:embed="rId9"/></q:buBlip><q:tabLst/></q:pPr>"#,
5298        )
5299        .unwrap();
5300        assert!(properties.has_picture_bullet());
5301
5302        properties.set_bullet(Some(TextBullet {
5303            choice: Some(TextBulletChoice::Character(
5304                TextBulletCharacter::new("*").unwrap(),
5305            )),
5306            ..TextBullet::default()
5307        }));
5308        assert!(!properties.has_picture_bullet());
5309        assert_eq!(
5310            properties.to_xml().unwrap(),
5311            br#"<a:pPr><q:buClrTx/><q:buSzTx/><q:buFontTx/><a:buChar char="*"/><q:tabLst/></a:pPr>"#
5312        );
5313
5314        properties.set_bullet(None);
5315        assert_eq!(
5316            properties.to_xml().unwrap(),
5317            br#"<a:pPr><q:tabLst/></a:pPr>"#
5318        );
5319    }
5320
5321    #[test]
5322    fn paragraph_and_run_properties_use_drawingml_units_and_schema_order() {
5323        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();
5324        let written = {
5325            let mut writer = quick_xml::Writer::new(Vec::new());
5326            paragraph_properties
5327                .write_xml(&mut writer, "a:pPr")
5328                .unwrap();
5329            writer.into_inner()
5330        };
5331        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>"#);
5332
5333        let run_properties = CT_TextCharacterProperties::from_xml(
5334            br#"<q:rPr sz="100" spc="1.25pt" baseline="-100000"/>"#,
5335        )
5336        .unwrap();
5337        let mut writer = quick_xml::Writer::new(Vec::new());
5338        run_properties.write_xml(&mut writer, "a:rPr").unwrap();
5339        assert_eq!(
5340            writer.into_inner(),
5341            br#"<a:rPr sz="100" spc="1.25pt" baseline="-100000"/>"#
5342        );
5343
5344        let mut inserted_spacing =
5345            CT_TextParagraphProperties::from_xml(br#"<q:pPr><q:buChar char="*"/></q:pPr>"#)
5346                .unwrap();
5347        inserted_spacing.line_spacing = Some(TextSpacing::Percent("100000".to_owned()));
5348        let mut writer = quick_xml::Writer::new(Vec::new());
5349        inserted_spacing.write_xml(&mut writer, "a:pPr").unwrap();
5350        assert_eq!(
5351            writer.into_inner(),
5352            br#"<a:pPr><a:lnSpc><a:spcPct val="100000"/></a:lnSpc><a:buChar char="*"/></a:pPr>"#
5353        );
5354    }
5355
5356    #[test]
5357    fn every_modelled_bullet_form_round_trips_in_schema_order() {
5358        let cases: &[(&[u8], &[u8])] = &[
5359            (
5360                br#"<q:pPr><q:buClr><q:srgbClr val="AABBCC"/></q:buClr><q:buSzPct val="125000"/><q:buFont typeface="Wingdings"/><q:buChar char="*"/></q:pPr>"#,
5361                br#"<a:pPr><a:buClr><a:srgbClr val="AABBCC"/></a:buClr><a:buSzPct val="125000"/><a:buFont typeface="Wingdings"/><a:buChar char="*"/></a:pPr>"#,
5362            ),
5363            (
5364                br#"<q:pPr><q:buSzPts val="1800"/><q:buAutoNum type="arabicPeriod" startAt="3"/></q:pPr>"#,
5365                br#"<a:pPr><a:buSzPts val="1800"/><a:buAutoNum type="arabicPeriod" startAt="3"/></a:pPr>"#,
5366            ),
5367            (br#"<q:pPr><q:buNone/></q:pPr>"#, br#"<a:pPr><a:buNone/></a:pPr>"#),
5368        ];
5369
5370        for (xml, expected) in cases {
5371            let properties = CT_TextParagraphProperties::from_xml(xml).unwrap();
5372            let mut writer = quick_xml::Writer::new(Vec::new());
5373            properties.write_xml(&mut writer, "a:pPr").unwrap();
5374            let written = writer.into_inner();
5375            assert_eq!(written, *expected);
5376            assert_eq!(
5377                CT_TextParagraphProperties::from_xml(&written).unwrap(),
5378                properties
5379            );
5380        }
5381
5382        let character = CT_TextParagraphProperties::from_xml(cases[0].0).unwrap();
5383        let bullet = character.bullet.as_ref().unwrap();
5384        assert!(matches!(
5385            bullet.choice,
5386            Some(TextBulletChoice::Character(_))
5387        ));
5388        assert_eq!(bullet.font.as_ref().unwrap().typeface, "Wingdings");
5389        assert!(matches!(
5390            bullet.size.as_ref().unwrap().value,
5391            TextBulletSizeValue::Percent(ref value) if value == "125000"
5392        ));
5393        assert!(matches!(
5394            bullet.color.as_ref().unwrap().color,
5395            ColorChoice::Srgb { .. }
5396        ));
5397
5398        let automatic = CT_TextParagraphProperties::from_xml(cases[1].0).unwrap();
5399        let Some(TextBulletChoice::AutoNumber(numbering)) =
5400            automatic.bullet.as_ref().unwrap().choice.as_ref()
5401        else {
5402            panic!("expected automatic-number bullet");
5403        };
5404        assert_eq!(numbering.scheme, TextAutoNumberScheme::ArabicPeriod);
5405        assert_eq!(numbering.start_at, Some(3));
5406        assert!(matches!(
5407            automatic
5408                .bullet
5409                .as_ref()
5410                .unwrap()
5411                .size
5412                .as_ref()
5413                .unwrap()
5414                .value,
5415            TextBulletSizeValue::Points(1800)
5416        ));
5417
5418        let none = CT_TextParagraphProperties::from_xml(cases[2].0).unwrap();
5419        assert!(matches!(
5420            none.bullet.as_ref().unwrap().choice,
5421            Some(TextBulletChoice::None(_))
5422        ));
5423    }
5424
5425    #[test]
5426    fn bullet_font_size_and_colour_keep_their_schema_positions() {
5427        let properties = CT_TextParagraphProperties::from_xml(
5428            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>"#,
5429        )
5430        .unwrap();
5431        let mut writer = quick_xml::Writer::new(Vec::new());
5432        properties.write_xml(&mut writer, "a:pPr").unwrap();
5433        assert_eq!(
5434            writer.into_inner(),
5435            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>"#
5436        );
5437    }
5438
5439    #[test]
5440    fn unknown_bullet_children_round_trip_byte_for_byte() {
5441        let properties = CT_TextParagraphProperties::from_xml(
5442            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>"#,
5443        )
5444        .unwrap();
5445        let mut writer = quick_xml::Writer::new(Vec::new());
5446        properties.write_xml(&mut writer, "a:pPr").unwrap();
5447        assert_eq!(
5448            writer.into_inner(),
5449            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>"#
5450        );
5451    }
5452
5453    #[test]
5454    fn malformed_bullet_values_return_errors_without_panicking() {
5455        let cases: &[&[u8]] = &[
5456            br#"<q:pPr><q:buChar/></q:pPr>"#,
5457            br#"<q:pPr><q:buChar char=""/></q:pPr>"#,
5458            br#"<q:pPr><q:buSzPct val="24999"/></q:pPr>"#,
5459            br#"<q:pPr><q:buSzPct val="400001"/></q:pPr>"#,
5460            br#"<q:pPr><q:buSzPts val="99"/></q:pPr>"#,
5461            br#"<q:pPr><q:buSzPts val="400001"/></q:pPr>"#,
5462            br#"<q:pPr><q:buAutoNum type="unknown"/></q:pPr>"#,
5463            br#"<q:pPr><q:buAutoNum/></q:pPr>"#,
5464            br#"<q:pPr><q:buAutoNum type="arabicPeriod" startAt="0"/></q:pPr>"#,
5465            br#"<q:pPr><q:buAutoNum type="arabicPeriod" startAt="32768"/></q:pPr>"#,
5466            br#"<q:pPr><q:buFont/></q:pPr>"#,
5467            br#"<q:pPr><q:buClr/></q:pPr>"#,
5468            br#"<q:pPr><q:buClr><q:srgbClr x:val="AABBCC"/></q:buClr></q:pPr>"#,
5469            br#"<q:pPr><q:buClr><q:srgbClr val="GGGGGG"/></q:buClr></q:pPr>"#,
5470            br#"<q:pPr><q:buClr><q:srgbClr val="AABBCC"/><q:schemeClr val="accent1"/></q:buClr></q:pPr>"#,
5471            br#"<q:pPr><q:buChar char="*"/><q:buNone/></q:pPr>"#,
5472            br#"<q:pPr><q:buSzPct val="100000"/><q:buSzPts val="1200"/></q:pPr>"#,
5473        ];
5474
5475        for xml in cases {
5476            let result = panic::catch_unwind(|| CT_TextParagraphProperties::from_xml(xml));
5477            assert!(result.is_ok(), "bullet parser panicked");
5478            assert!(result.unwrap().is_err(), "malformed bullet parsed");
5479        }
5480    }
5481
5482    #[test]
5483    fn malformed_text_content_returns_errors_without_panicking() {
5484        let cases: &[&[u8]] = &[
5485            br#"<q:r/>"#,
5486            br#"<q:r><q:t><x:nested/></q:t></q:r>"#,
5487            br#"<q:r><q:t xml:space="keep">x</q:t></q:r>"#,
5488            br#"<q:r><q:t>x</q:t><q:t>y</q:t></q:r>"#,
5489            br#"<q:pPr lvl="9"/>"#,
5490            br#"<q:pPr marL="-1"/>"#,
5491            br#"<q:pPr indent="51206401"/>"#,
5492            br#"<q:pPr algn="middle"/>"#,
5493            br#"<q:pPr><q:lnSpc/></q:pPr>"#,
5494            br#"<q:pPr><q:lnSpc><q:spcPts val="158401"/></q:lnSpc></q:pPr>"#,
5495            br#"<q:pPr><q:lnSpc><q:spcPct val="13200001"/></q:lnSpc></q:pPr>"#,
5496            br#"<q:rPr sz="99"/>"#,
5497            br#"<q:rPr b="yes"/>"#,
5498            br#"<q:rPr u="triple"/>"#,
5499            br#"<q:rPr spc="400001"/>"#,
5500            r#"<q:rPr spc="éx"/>"#.as_bytes(),
5501            br#"<q:rPr baseline="100001"/>"#,
5502            br#"<q:rPr><q:latin/></q:rPr>"#,
5503            br#"<q:fld type="slidenum"/>"#,
5504            br#"<q:fld id="not-a-guid"/>"#,
5505            br#"<q:p><q:endParaRPr/><q:endParaRPr/></q:p>"#,
5506            br#"<q:notPPr/>"#,
5507            br#"<q:notRPr/>"#,
5508        ];
5509        for xml in cases {
5510            let result = panic::catch_unwind(|| match super::root_local_name(xml).as_deref() {
5511                Ok(b"r") => super::CT_RegularTextRun::from_xml(xml).map(|_| ()),
5512                Ok(b"pPr") => CT_TextParagraphProperties::from_xml(xml).map(|_| ()),
5513                Ok(b"rPr") => CT_TextCharacterProperties::from_xml(xml).map(|_| ()),
5514                Ok(b"fld") => super::CT_TextField::from_xml(xml).map(|_| ()),
5515                Ok(b"p") => CT_TextParagraph::from_xml(xml).map(|_| ()),
5516                Ok(b"notPPr") => CT_TextParagraphProperties::from_xml(xml).map(|_| ()),
5517                Ok(b"notRPr") => CT_TextCharacterProperties::from_xml(xml).map(|_| ()),
5518                _ => unreachable!(),
5519            });
5520            assert!(result.is_ok(), "text parser panicked");
5521            assert!(result.unwrap().is_err(), "malformed text parsed");
5522        }
5523    }
5524
5525    #[test]
5526    fn characters_xml_cannot_carry_are_stored_escaped_and_never_decoded() {
5527        let mut paragraph = CT_TextParagraph::default();
5528        paragraph.add_run("a\u{1}b\u{b}c\u{1f}d\u{fffe}e\u{0}");
5529        let TextRun::Run(run) = &paragraph.runs[0] else {
5530            panic!("expected a run");
5531        };
5532        assert_eq!(
5533            run.text.value, "a_x0001_b_x000B_c_x001F_d_xFFFE_e_x0000_",
5534            "the setter stores the escaped form, as python-pptx does"
5535        );
5536        let written = paragraph.to_xml().unwrap();
5537        assert_eq!(
5538            written,
5539            br#"<a:p><a:r><a:t>a_x0001_b_x000B_c_x001F_d_xFFFE_e_x0000_</a:t></a:r></a:p>"#
5540        );
5541        let declared = [
5542            br#"<a:p xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main""#.as_slice(),
5543            &written[4..],
5544        ]
5545        .concat();
5546        oxml_core::xml::validate_strict_xml_1_0(&declared)
5547            .expect("the written paragraph is XML 1.0");
5548        assert_eq!(CT_TextParagraph::from_xml(&written).unwrap(), paragraph);
5549
5550        // Reading keeps every `_xHHHH_` spelling as text, as PowerPoint shows
5551        // it and python-pptx reads it.
5552        let xml = br#"<a:p><a:r><a:t>C_x0041_D bell_x0007_ _x000b_ _x0001</a:t></a:r></a:p>"#;
5553        let paragraph = CT_TextParagraph::from_xml(xml).unwrap();
5554        let TextRun::Run(run) = &paragraph.runs[0] else {
5555            panic!("expected a run");
5556        };
5557        assert_eq!(run.text.value, "C_x0041_D bell_x0007_ _x000b_ _x0001");
5558        assert_eq!(paragraph.to_xml().unwrap(), xml);
5559    }
5560
5561    #[test]
5562    fn a_vertical_tab_set_as_paragraph_text_becomes_a_line_break() {
5563        let mut paragraph = CT_TextParagraph::from_xml(
5564            br#"<a:p xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:x="urn:x"><a:r><a:rPr b="1"/><a:t>old</a:t></a:r><x:after/><a:endParaRPr sz="1200"/><x:end/></a:p>"#,
5565        )
5566        .unwrap();
5567        paragraph.set_text("one\u{b}two\u{1}\u{b}");
5568        assert_eq!(
5569            String::from_utf8(paragraph.to_xml().unwrap()).unwrap(),
5570            r#"<a:p xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:x="urn:x"><a:r><a:rPr b="1"/><a:t>one</a:t></a:r><a:br><a:rPr b="1"/></a:br><a:r><a:rPr b="1"/><a:t>two_x0001_</a:t></a:r><a:br><a:rPr b="1"/></a:br><x:after/><a:endParaRPr sz="1200"/><x:end/></a:p>"#
5571        );
5572
5573        let saved = paragraph.to_xml().unwrap();
5574        assert_eq!(
5575            CT_TextParagraph::from_xml(&saved)
5576                .unwrap()
5577                .to_xml()
5578                .unwrap(),
5579            saved
5580        );
5581
5582        let mut plain = CT_TextParagraph::default();
5583        plain.set_text("\u{b}");
5584        assert_eq!(
5585            plain.to_xml().unwrap(),
5586            br#"<a:p><a:r><a:t/></a:r><a:br/></a:p>"#
5587        );
5588    }
5589    fn fx206_link_saved(properties: &CT_TextCharacterProperties) -> Vec<u8> {
5590        let mut writer = Writer::new(Vec::new());
5591        properties.write_xml(&mut writer, "a:rPr").unwrap();
5592        writer.into_inner()
5593    }
5594
5595    #[test]
5596    fn fx206_run_link_foreign_owner_and_qualified_relationship_alias_base() {
5597        let positive = CT_TextCharacterProperties::from_xml(br#"<a:rPr xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"><a:hlinkClick r:id="rId1"/></a:rPr>"#).unwrap();
5598        assert_eq!(
5599            positive
5600                .hyperlink_click
5601                .as_ref()
5602                .unwrap()
5603                .relationship_id
5604                .as_deref(),
5605            Some("rId1")
5606        );
5607        assert!(!fx206_link_saved(&positive).is_empty());
5608        let input = br#"<a:rPr xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:rel="http://schemas.openxmlformats.org/officeDocument/2006/relationships" xmlns:f="urn:foreign"><f:hlinkClick rel:id="foreign"><f:opaque/></f:hlinkClick><a:hlinkClick rel:id="owned" action="ppaction://hlinksldjump" f:keep='&#65;'><f:payload/></a:hlinkClick></a:rPr>"#;
5609        let result = CT_TextCharacterProperties::from_xml(input);
5610        assert!(
5611            result.is_ok(),
5612            "foreign hlinkClick must not consume the owned schema slot: {result:?}"
5613        );
5614        let mut properties = result.unwrap();
5615        assert_eq!(
5616            properties
5617                .hyperlink_click
5618                .as_ref()
5619                .unwrap()
5620                .relationship_id
5621                .as_deref(),
5622            Some("owned")
5623        );
5624        let unchanged = fx206_link_saved(&properties);
5625        assert!(
5626            std::str::from_utf8(&unchanged)
5627                .unwrap()
5628                .contains(r#"<f:hlinkClick rel:id="foreign"><f:opaque/></f:hlinkClick>"#)
5629        );
5630        assert!(
5631            std::str::from_utf8(&unchanged)
5632                .unwrap()
5633                .contains("f:keep='&#65;'")
5634        );
5635        properties.hyperlink_click.as_mut().unwrap().relationship_id = Some("next".to_owned());
5636        let changed = fx206_link_saved(&properties);
5637        let output = std::str::from_utf8(&changed).unwrap();
5638        assert!(output.contains(r#"<f:hlinkClick rel:id="foreign"><f:opaque/></f:hlinkClick>"#));
5639        assert!(output.contains("f:keep='&#65;'"));
5640        assert!(output.contains("<f:payload/>"));
5641        let reopened = CT_TextCharacterProperties::from_xml(&changed).unwrap();
5642        assert_eq!(
5643            reopened
5644                .hyperlink_click
5645                .as_ref()
5646                .unwrap()
5647                .relationship_id
5648                .as_deref(),
5649            Some("next")
5650        );
5651        assert_eq!(fx206_link_saved(&reopened), changed);
5652    }
5653
5654    #[test]
5655    fn fx206_run_link_alias_and_local_rebound_id_are_distinct_base() {
5656        let positive = CT_TextCharacterProperties::from_xml(br#"<a:rPr xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"><a:hlinkClick r:id="canonical"/></a:rPr>"#).unwrap();
5657        assert_eq!(
5658            positive
5659                .hyperlink_click
5660                .as_ref()
5661                .unwrap()
5662                .relationship_id
5663                .as_deref(),
5664            Some("canonical"),
5665            "independent existing canonical relationship positive setup"
5666        );
5667        assert!(!fx206_link_saved(&positive).is_empty());
5668        let alias = CT_TextCharacterProperties::from_xml(br#"<a:rPr xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:rel="http://schemas.openxmlformats.org/officeDocument/2006/relationships"><a:hlinkClick rel:id="owned"/></a:rPr>"#).unwrap();
5669        assert!(
5670            alias.hyperlink_click.is_some(),
5671            "owned click exists before deciding alias assertion"
5672        );
5673        assert_eq!(
5674            alias
5675                .hyperlink_click
5676                .as_ref()
5677                .unwrap()
5678                .relationship_id
5679                .as_deref(),
5680            Some("owned"),
5681            "deciding relationship-URI alias interpretation"
5682        );
5683        let foreign = CT_TextCharacterProperties::from_xml(br#"<a:rPr xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"><a:hlinkClick xmlns:r="urn:foreign" r:id="opaque"/></a:rPr>"#).unwrap();
5684        assert_eq!(
5685            foreign.hyperlink_click.as_ref().unwrap().relationship_id,
5686            None
5687        );
5688        assert!(
5689            std::str::from_utf8(&fx206_link_saved(&foreign))
5690                .unwrap()
5691                .contains(r#"r:id="opaque""#)
5692        );
5693    }
5694
5695    #[test]
5696    fn fx206_link_uri_owned_duplicates_refuse_but_foreign_shadow_preserves() {
5697        let namespaces = r#"xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" xmlns:rel="http://schemas.openxmlformats.org/officeDocument/2006/relationships" xmlns:f="urn:foreign""#;
5698        for children in [
5699            r#"<a:hlinkClick r:id="one" rel:id="two"/>"#,
5700            r#"<a:hlinkClick r:id="one"/><a:hlinkClick rel:id="two"/>"#,
5701        ] {
5702            let xml = format!("<a:rPr {namespaces}>{children}</a:rPr>");
5703            assert!(CT_TextCharacterProperties::from_xml(xml.as_bytes()).is_err());
5704        }
5705        let xml = format!(
5706            r#"<a:rPr {namespaces}><!-- lexical --><f:hlinkClick rel:id='opaque'><a:hlinkClick r:id='shadow'/></f:hlinkClick><a:hlinkClick r:id='owned'/></a:rPr>"#
5707        );
5708        let parsed = CT_TextCharacterProperties::from_xml(xml.as_bytes()).unwrap();
5709        assert_eq!(
5710            parsed
5711                .hyperlink_click
5712                .as_ref()
5713                .unwrap()
5714                .relationship_id
5715                .as_deref(),
5716            Some("owned")
5717        );
5718        let saved = fx206_link_saved(&parsed);
5719        let text = std::str::from_utf8(&saved).unwrap();
5720        assert!(text.contains("<!-- lexical -->"));
5721        assert!(text.contains(
5722            "<f:hlinkClick rel:id='opaque'><a:hlinkClick r:id='shadow'/></f:hlinkClick>"
5723        ));
5724        assert_eq!(
5725            fx206_link_saved(&CT_TextCharacterProperties::from_xml(&saved).unwrap()),
5726            saved
5727        );
5728    }
5729
5730    #[test]
5731    fn fx206_owned_alias_root_retains_foreign_a_and_closes_new_click_descendant() {
5732        let xml = br#"<q:rPr xmlns:q="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:a='urn:foreign' xmlns:rel="http://schemas.openxmlformats.org/officeDocument/2006/relationships" xmlns:f="urn:payload" f:keep='&#65;'><q:latin typeface='Arial' f:font='&#66;'/><a:hlinkClick rel:id='foreign'><a:payload f:keep='&#67;'/></a:hlinkClick><q:hlinkClick rel:id='owned' tooltip='&#68;'><a:payload f:keep='&#69;'/></q:hlinkClick></q:rPr>"#;
5733        assert!(CT_TextCharacterProperties::from_xml(br#"<q:rPr xmlns:q="urn:foreign" xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"/>"#).is_err());
5734        assert!(
5735            CT_TextCharacterProperties::from_xml(br#"<a:rPr xmlns:a="urn:foreign"/>"#).is_err()
5736        );
5737        let mut properties = CT_TextCharacterProperties::from_xml(xml).unwrap();
5738        assert_eq!(
5739            properties
5740                .hyperlink_click
5741                .as_ref()
5742                .unwrap()
5743                .relationship_id
5744                .as_deref(),
5745            Some("owned")
5746        );
5747        properties.hyperlink_click.as_mut().unwrap().relationship_id =
5748            Some("retargeted".to_owned());
5749        properties.hyperlink_mouse_over = Some(TextHyperlink {
5750            relationship_id: Some("hover".to_owned()),
5751            ..TextHyperlink::default()
5752        });
5753        let saved = fx206_link_saved(&properties);
5754        let text = std::str::from_utf8(&saved).unwrap();
5755        assert!(text.starts_with("<q:rPr"));
5756        for token in [
5757            "xmlns:a='urn:foreign'",
5758            "f:keep='&#65;'",
5759            "<q:latin typeface='Arial' f:font='&#66;'/>",
5760            "<a:hlinkClick rel:id='foreign'><a:payload f:keep='&#67;'/></a:hlinkClick>",
5761            "tooltip='&#68;'",
5762            "<a:payload f:keep='&#69;'/>",
5763        ] {
5764            assert!(text.contains(token), "{token}");
5765        }
5766        assert!(text.contains("<a:hlinkMouseOver"));
5767        assert!(text.contains("xmlns:a=\"http://schemas.openxmlformats.org/drawingml/2006/main\""));
5768        let reopened = CT_TextCharacterProperties::from_xml(&saved).unwrap();
5769        assert_eq!(
5770            reopened
5771                .hyperlink_click
5772                .as_ref()
5773                .unwrap()
5774                .relationship_id
5775                .as_deref(),
5776            Some("retargeted")
5777        );
5778        assert_eq!(
5779            reopened
5780                .hyperlink_mouse_over
5781                .as_ref()
5782                .unwrap()
5783                .relationship_id
5784                .as_deref(),
5785            Some("hover")
5786        );
5787        assert_eq!(fx206_link_saved(&reopened), saved);
5788        let mut provenance_only = properties.clone();
5789        provenance_only.source_tag.clear();
5790        provenance_only.source_namespaces.clear();
5791        assert_eq!(provenance_only, properties);
5792        let mut foreign_change = properties.clone();
5793        let attribute = foreign_change
5794            .raw_attributes
5795            .iter_mut()
5796            .find(|(key, _)| key == "f:keep")
5797            .unwrap();
5798        attribute.1 = "different".to_owned();
5799        assert_ne!(foreign_change, properties);
5800    }
5801
5802    #[test]
5803    fn fx206_hyperlink_opaque_unqualified_id_survives_retarget_and_clear() {
5804        for owned in ["", " rel:id='before'"] {
5805            let input = format!(
5806                r#"<q:rPr xmlns:q="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:r="urn:foreign" xmlns:rel="http://schemas.openxmlformats.org/officeDocument/2006/relationships" xmlns:f="urn:payload"><q:hlinkClick id='&#65;' {owned} action='ppaction://hlinksldjump' f:keep='&#66;'><f:opaque/></q:hlinkClick></q:rPr>"#
5807            );
5808            let mut properties = CT_TextCharacterProperties::from_xml(input.as_bytes()).unwrap();
5809            assert_eq!(
5810                properties
5811                    .hyperlink_click
5812                    .as_ref()
5813                    .unwrap()
5814                    .relationship_id
5815                    .as_deref(),
5816                if owned.is_empty() {
5817                    None
5818                } else {
5819                    Some("before")
5820                }
5821            );
5822            for target in [Some("after"), None] {
5823                properties.hyperlink_click.as_mut().unwrap().relationship_id =
5824                    target.map(str::to_owned);
5825                let saved = fx206_link_saved(&properties);
5826                let xml = std::str::from_utf8(&saved).unwrap();
5827                for token in [
5828                    "id='&#65;'",
5829                    "f:keep='&#66;'",
5830                    "<f:opaque/>",
5831                    "xmlns:r=\"urn:foreign\"",
5832                ] {
5833                    assert!(xml.contains(token), "lost opaque token {token}: {xml}");
5834                }
5835                assert_eq!(xml.matches("id='&#65;'").count(), 1);
5836                let reopened = CT_TextCharacterProperties::from_xml(&saved).unwrap();
5837                assert_eq!(
5838                    reopened
5839                        .hyperlink_click
5840                        .as_ref()
5841                        .unwrap()
5842                        .relationship_id
5843                        .as_deref(),
5844                    target
5845                );
5846                assert_eq!(fx206_link_saved(&reopened), saved);
5847                properties = reopened;
5848            }
5849        }
5850    }
5851
5852    #[test]
5853    fn fx206_new_hyperlink_closes_relationship_uri_without_requalifying_raw_payload() {
5854        let input = br#"<q:rPr xmlns:q="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:a="urn:foreign-a" xmlns:r="urn:foreign-r"><r:opaque id='&#65;'/></q:rPr>"#;
5855        for raw_namespace in [false, true] {
5856            let mut properties = CT_TextCharacterProperties::from_xml(input).unwrap();
5857            assert!(properties.hyperlink_click.is_none());
5858            let mut click = TextHyperlink {
5859                relationship_id: Some("target".to_owned()),
5860                ..TextHyperlink::default()
5861            };
5862            if raw_namespace {
5863                click
5864                    .raw_attributes
5865                    .push(("xmlns:r".to_owned(), "urn:opaque-click".to_owned()));
5866                click
5867                    .raw_attributes
5868                    .push(("r:opaque".to_owned(), "preserved".to_owned()));
5869            }
5870            properties.hyperlink_click = Some(click);
5871            let saved = fx206_link_saved(&properties);
5872            let xml = std::str::from_utf8(&saved).unwrap();
5873            assert!(xml.contains("xmlns:r=\"urn:foreign-r\""));
5874            assert!(xml.contains("<r:opaque id='&#65;'/>"));
5875            if raw_namespace {
5876                assert!(xml.contains("xmlns:r=\"urn:opaque-click\""));
5877                assert!(xml.contains("r:opaque=\"preserved\""));
5878                assert!(xml.contains("xmlns:rLink1=\"http://schemas.openxmlformats.org/officeDocument/2006/relationships\""));
5879            } else {
5880                assert!(xml.contains("xmlns:r=\"http://schemas.openxmlformats.org/officeDocument/2006/relationships\""));
5881            }
5882            let reopened = CT_TextCharacterProperties::from_xml(&saved).unwrap();
5883            assert_eq!(
5884                reopened
5885                    .hyperlink_click
5886                    .as_ref()
5887                    .unwrap()
5888                    .relationship_id
5889                    .as_deref(),
5890                Some("target")
5891            );
5892            assert_eq!(fx206_link_saved(&reopened), saved);
5893        }
5894    }
5895    #[test]
5896    fn fx206_lookup_fallbacks_do_not_replace_physical_namespace_provenance() {
5897        let fallback =
5898            CT_TextParagraph::from_xml(br#"<q:p><q:r><q:rPr b="1"/><q:t>text</q:t></q:r></q:p>"#)
5899                .unwrap();
5900        assert_eq!(
5901            fallback.to_xml().unwrap(),
5902            br#"<a:p><a:r><a:rPr b="1"/><a:t>text</a:t></a:r></a:p>"#
5903        );
5904        let input = br#"<q:p xmlns:q="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:a='urn:foreign'><a:opaque a:keep='&#65;'/><q:r><q:rPr b="1"/><q:t>text</q:t></q:r></q:p>"#;
5905        let physical = CT_TextParagraph::from_xml(input).unwrap();
5906        let saved = physical.to_xml().unwrap();
5907        let xml = std::str::from_utf8(&saved).unwrap();
5908        assert!(xml.starts_with("<q:p "));
5909        assert!(xml.contains("xmlns:a='urn:foreign'"));
5910        assert!(xml.contains("<a:opaque a:keep='&#65;'/>"));
5911        assert!(xml.contains("xmlns:q=\"http://schemas.openxmlformats.org/drawingml/2006/main\""));
5912        assert_eq!(
5913            CT_TextParagraph::from_xml(&saved)
5914                .unwrap()
5915                .to_xml()
5916                .unwrap(),
5917            saved
5918        );
5919    }
5920    #[test]
5921    fn fx206_relocated_run_retains_opaque_inherited_namespace_closure() {
5922        let input = br#"<q:p xmlns:q="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:x='urn:foreign'><q:r><x:opaque x:keep='&#65;'/><q:t>text</q:t></q:r></q:p>"#;
5923        let parsed = super::CT_TextParagraph::from_xml(input).unwrap();
5924        let relocated = super::CT_TextParagraph {
5925            runs: parsed.runs,
5926            ..Default::default()
5927        };
5928        let saved = relocated.to_xml().unwrap();
5929        let xml = std::str::from_utf8(&saved).unwrap();
5930        assert!(xml.contains("<x:opaque x:keep='&#65;'/>"));
5931        assert!(xml.contains("xmlns:x=\"urn:foreign\""));
5932        assert!(
5933            matches!(&relocated.runs[..], [super::TextRun::Run(run)] if run.text.value == "text")
5934        );
5935        assert_eq!(
5936            super::CT_TextParagraph::from_xml(&saved)
5937                .unwrap()
5938                .to_xml()
5939                .unwrap(),
5940            saved
5941        );
5942    }
5943    #[test]
5944    fn fx206_paragraph_attribute_order_is_semantic_but_raw_save_stays_exact() {
5945        let left_source = br#"<a:pPr xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:f="urn:foreign" defTabSz="457200" rtl="0" f:keep='&#65;'><f:opaque/></a:pPr>"#;
5946        let right_source = br#"<a:pPr xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:f="urn:foreign" rtl="0" defTabSz="457200" f:keep='&#65;'><f:opaque/></a:pPr>"#;
5947        let left = CT_TextParagraphProperties::from_xml(left_source).unwrap();
5948        let right = CT_TextParagraphProperties::from_xml(right_source).unwrap();
5949        assert_eq!(left, right);
5950        for (properties, source) in [
5951            (&left, left_source.as_slice()),
5952            (&right, right_source.as_slice()),
5953        ] {
5954            let mut writer = Writer::new(Vec::new());
5955            properties.write_xml(&mut writer, "a:pPr").unwrap();
5956            assert_eq!(writer.into_inner(), source);
5957        }
5958        // Value and opaque-body differences remain semantic differences.
5959        let changed_value = std::str::from_utf8(right_source)
5960            .unwrap()
5961            .replace("457200", "457201");
5962        assert_ne!(
5963            left,
5964            CT_TextParagraphProperties::from_xml(changed_value.as_bytes()).unwrap()
5965        );
5966        let changed_body = std::str::from_utf8(right_source)
5967            .unwrap()
5968            .replace("<f:opaque/>", "<f:other/>");
5969        assert_ne!(
5970            left,
5971            CT_TextParagraphProperties::from_xml(changed_body.as_bytes()).unwrap()
5972        );
5973    }
5974}
5975
5976/// Close an inherited property at its original owner scope without guessing
5977/// which opaque attribute or text values might contain producer QNames.
5978fn text_close_inherited_property(raw: &[u8], inherited: &[(String, String)]) -> Result<Vec<u8>> {
5979    let mut reader = Reader::from_reader(raw);
5980    let event = reader.read_event().map_err(OxmlError::from)?;
5981    let empty = matches!(&event, Event::Empty(_));
5982    let start = match event {
5983        Event::Start(start) | Event::Empty(start) => start,
5984        _ => return Ok(raw.to_vec()),
5985    };
5986    let mut content = std::str::from_utf8(start.as_ref())
5987        .map_err(OxmlError::from)?
5988        .to_owned();
5989    let declared = start
5990        .attributes()
5991        .map(|attribute| attribute.map(|attribute| attribute.key.as_ref().to_vec()))
5992        .collect::<std::result::Result<Vec<_>, _>>()
5993        .map_err(OxmlError::from)?;
5994    for (prefix, uri) in inherited {
5995        let key = if prefix.is_empty() {
5996            "xmlns".to_owned()
5997        } else {
5998            format!("xmlns:{prefix}")
5999        };
6000        if !declared.iter().any(|old| old == key.as_bytes()) {
6001            content.push_str(&format!(" {key}=\"{}\"", quick_xml::escape::escape(uri)));
6002        }
6003    }
6004    if !inherited.iter().any(|(prefix, _)| prefix.is_empty())
6005        && !declared.iter().any(|key| key == b"xmlns")
6006    {
6007        content.push_str(" xmlns=\"\"");
6008    }
6009    let start = BytesStart::from_content(content, start.name().as_ref().len());
6010    let mut writer = Writer::new(Vec::new());
6011    if empty {
6012        write_empty(&mut writer, start)?;
6013    } else {
6014        write_start(&mut writer, start)?;
6015    }
6016    let mut closed = writer.into_inner();
6017    closed.extend_from_slice(&raw[reader.buffer_position() as usize..]);
6018    Ok(closed)
6019}
6020
6021/// Combine only the known DrawingML inheritance groups. Every inherited opaque
6022/// child carries its original namespace closure, and local groups stay dominant.
6023pub(crate) fn text_inherit_property_xml(
6024    local: &[u8],
6025    local_scope: &[(String, String)],
6026    inherited: &[u8],
6027    inherited_scope: &[(String, String)],
6028) -> Result<Vec<u8>> {
6029    let read = |xml: &[u8], scope: &[(String, String)]| -> Result<_> {
6030        let mut reader = Reader::from_reader(xml);
6031        let start = match reader.read_event().map_err(OxmlError::from)? {
6032            Event::Start(start) | Event::Empty(start) => start.into_owned(),
6033            _ => {
6034                return Err(invalid_attribute(
6035                    "inherit text properties",
6036                    "root",
6037                    "missing owner",
6038                ));
6039            }
6040        };
6041        let empty = xml[reader.buffer_position() as usize - 2] == b'/';
6042        let scope = text_fragment_scope(&start, &TextNamespaceScope::inherited(scope))?;
6043        let root_name = local_name(start.name().as_ref()).to_vec();
6044        let mut children = Vec::new();
6045        let mut boundary = 0;
6046        if !empty {
6047            loop {
6048                let event = reader.read_event().map_err(OxmlError::from)?;
6049                let raw = match event {
6050                    Event::Start(child) | Event::Empty(child) => {
6051                        let child_empty = xml[reader.buffer_position() as usize - 2] == b'/';
6052                        let name = text_owned_name(&child, &scope)?;
6053                        let slot = if is_character_property_tag(&root_name) {
6054                            character_property_slot(&name)
6055                        } else if is_paragraph_property_tag(&root_name) {
6056                            paragraph_property_slot(&name)
6057                        } else if name == b"defPPr" {
6058                            Some(1)
6059                        } else if root_name != b"extLst" && name == b"extLst" {
6060                            Some(11)
6061                        } else if root_name != b"extLst" {
6062                            name.strip_prefix(b"lvl")
6063                                .and_then(|tail| tail.strip_suffix(b"pPr"))
6064                                .and_then(|level| {
6065                                    std::str::from_utf8(level).ok()?.parse::<usize>().ok()
6066                                })
6067                                .filter(|level| (1..=9).contains(level))
6068                                .map(|level| level + 1)
6069                        } else {
6070                            None
6071                        };
6072                        if let Some(slot) = slot {
6073                            boundary = boundary.max(slot);
6074                        }
6075                        let raw = if child_empty {
6076                            capture_empty_element(&child)?
6077                        } else {
6078                            capture_element(&mut reader, &child)?
6079                        };
6080                        children.push((boundary, slot, raw));
6081                        continue;
6082                    }
6083                    Event::End(_) => break,
6084                    Event::Eof => return Err(missing_end("inherited property owner")),
6085                    event @ (Event::Comment(_)
6086                    | Event::PI(_)
6087                    | Event::Text(_)
6088                    | Event::CData(_)) => text_capture_opaque_event(event)?,
6089                    _ => continue,
6090                };
6091                children.push((boundary, None, raw));
6092            }
6093        }
6094        Ok((start, scope, children))
6095    };
6096    let (mut start, local_scope, mut children) = read(local, local_scope)?;
6097    let (parent, inherited_scope, inherited_children) = read(inherited, inherited_scope)?;
6098    let root_name = local_name(start.name().as_ref()).to_vec();
6099    let attribute_identity = |key: &[u8], scope: &TextNamespaceScope| -> Result<_> {
6100        let key = std::str::from_utf8(key).map_err(OxmlError::from)?;
6101        let (prefix, name) = key.split_once(':').unwrap_or(("", key));
6102        let uri = if prefix.is_empty() {
6103            None
6104        } else if prefix == "xml" {
6105            Some("http://www.w3.org/XML/1998/namespace".to_owned())
6106        } else {
6107            scope
6108                .physical
6109                .iter()
6110                .find(|(p, _)| p == prefix)
6111                .map(|(_, uri)| uri.clone())
6112        };
6113        if !prefix.is_empty() && uri.is_none() {
6114            return Err(invalid_attribute(
6115                "inherit text properties",
6116                "namespace",
6117                "unbound attribute prefix",
6118            ));
6119        }
6120        Ok((uri, name.to_owned()))
6121    };
6122    let known_attribute = |key: &[u8]| {
6123        if is_character_property_tag(&root_name) {
6124            matches!(
6125                key,
6126                b"sz"
6127                    | b"b"
6128                    | b"i"
6129                    | b"cap"
6130                    | b"u"
6131                    | b"strike"
6132                    | b"spc"
6133                    | b"baseline"
6134                    | b"lang"
6135                    | b"altLang"
6136                    | b"kern"
6137                    | b"kumimoji"
6138                    | b"normalizeH"
6139                    | b"noProof"
6140                    | b"dirty"
6141                    | b"err"
6142                    | b"smtClean"
6143                    | b"smtId"
6144                    | b"bmk"
6145            )
6146        } else if is_paragraph_property_tag(&root_name) {
6147            matches!(
6148                key,
6149                b"marL"
6150                    | b"marR"
6151                    | b"lvl"
6152                    | b"indent"
6153                    | b"algn"
6154                    | b"rtl"
6155                    | b"defTabSz"
6156                    | b"eaLnBrk"
6157                    | b"latinLnBrk"
6158                    | b"hangingPunct"
6159                    | b"fontAlgn"
6160            )
6161        } else {
6162            false
6163        }
6164    };
6165    let namespaces = |scope: &TextNamespaceScope| {
6166        scope
6167            .physical
6168            .iter()
6169            .filter(|(prefix, uri)| !prefix.is_empty() || !uri.is_empty())
6170            .cloned()
6171            .collect::<std::collections::BTreeMap<_, _>>()
6172    };
6173    let local_tokens = text_attribute_tokens(&start)?;
6174    let parent_tokens = text_attribute_tokens(&parent)?;
6175    let mut additions = String::new();
6176    for attribute in parent.attributes() {
6177        let attribute = attribute.map_err(OxmlError::from)?;
6178        let key = attribute.key.as_ref();
6179        if key == b"xmlns" || key.starts_with(b"xmlns:") {
6180            continue;
6181        }
6182        let identity = attribute_identity(key, &inherited_scope)?;
6183        let mut existing = None;
6184        for old in start.attributes() {
6185            let old = old.map_err(OxmlError::from)?;
6186            if old.key.as_ref() != b"xmlns"
6187                && !old.key.as_ref().starts_with(b"xmlns:")
6188                && attribute_identity(old.key.as_ref(), &local_scope)? == identity
6189            {
6190                existing = Some(old);
6191                break;
6192            }
6193        }
6194        if let Some(old) = existing {
6195            if !known_attribute(key)
6196                && (old
6197                    .decoded_and_normalized_value(
6198                        quick_xml::XmlVersion::Implicit1_0,
6199                        start.decoder(),
6200                    )
6201                    .map_err(OxmlError::from)?
6202                    != attribute
6203                        .decoded_and_normalized_value(
6204                            quick_xml::XmlVersion::Implicit1_0,
6205                            parent.decoder(),
6206                        )
6207                        .map_err(OxmlError::from)?
6208                    || namespaces(&local_scope) != namespaces(&inherited_scope))
6209            {
6210                return Err(invalid_attribute(
6211                    "inherit text properties",
6212                    "metadata",
6213                    "conflicting opaque attribute authority",
6214                ));
6215            }
6216            continue;
6217        }
6218        if !known_attribute(key)
6219            && (!children.is_empty()
6220                || local_tokens
6221                    .iter()
6222                    .any(|(key, _)| !key.starts_with("xmlns") && !known_attribute(key.as_bytes())))
6223            && namespaces(&local_scope) != namespaces(&inherited_scope)
6224        {
6225            return Err(invalid_attribute(
6226                "inherit text properties",
6227                "metadata",
6228                "unrepresentable opaque namespace contexts",
6229            ));
6230        }
6231        if !known_attribute(key) {
6232            for (prefix, uri) in &inherited_scope.physical {
6233                if let Some((_, old)) = local_scope.physical.iter().find(|(old, _)| old == prefix) {
6234                    if old != uri {
6235                        return Err(invalid_attribute(
6236                            "inherit text properties",
6237                            "namespace",
6238                            "conflicting inherited root binding",
6239                        ));
6240                    }
6241                } else {
6242                    let key = if prefix.is_empty() {
6243                        "xmlns".to_owned()
6244                    } else {
6245                        format!("xmlns:{prefix}")
6246                    };
6247                    if !additions.contains(&format!(" {key}=")) {
6248                        additions
6249                            .push_str(&format!(" {key}=\"{}\"", quick_xml::escape::escape(uri)));
6250                    }
6251                }
6252            }
6253        }
6254        let (_, token) = parent_tokens
6255            .iter()
6256            .find(|(old, _)| old.as_bytes() == key)
6257            .ok_or_else(|| {
6258                invalid_attribute(
6259                    "inherit text properties",
6260                    "attribute",
6261                    "missing source token",
6262                )
6263            })?;
6264        additions.push_str(token);
6265    }
6266    for (boundary, slot, raw) in inherited_children {
6267        if let Some(slot) = slot
6268            && let Some(index) = children.iter().position(|(_, old, _)| *old == Some(slot))
6269        {
6270            let name = raw_local_name(&raw);
6271            if matches!(name, b"defRPr" | b"defPPr" | b"extLst") || name.starts_with(b"lvl") {
6272                children[index].2 = text_inherit_property_xml(
6273                    &children[index].2,
6274                    &local_scope.physical,
6275                    &raw,
6276                    &inherited_scope.physical,
6277                )?;
6278            }
6279            continue;
6280        }
6281        let raw = text_close_inherited_property(&raw, &inherited_scope.physical)?;
6282        if children.iter().any(|(_, _, old)| old == &raw) {
6283            continue;
6284        }
6285        let position = children
6286            .iter()
6287            .position(|(old_boundary, _, _)| *old_boundary > boundary)
6288            .unwrap_or(children.len());
6289        children.insert(position, (boundary, slot, raw));
6290    }
6291    if !additions.is_empty() {
6292        let name_len = start.name().as_ref().len();
6293        let content = format!(
6294            "{}{additions}",
6295            std::str::from_utf8(start.as_ref()).map_err(OxmlError::from)?
6296        );
6297        start = BytesStart::from_content(content, name_len);
6298    }
6299    let name = std::str::from_utf8(start.name().as_ref())
6300        .map_err(OxmlError::from)?
6301        .to_owned();
6302    let mut writer = Writer::new(Vec::new());
6303    if children.is_empty() {
6304        write_empty(&mut writer, start)?;
6305    } else {
6306        write_start(&mut writer, start)?;
6307        for (_, _, raw) in children {
6308            writer.get_mut().write_all(&raw).map_err(OxmlError::from)?;
6309        }
6310        write_end(&mut writer, &name)?;
6311    }
6312    Ok(writer.into_inner())
6313}
6314
6315#[cfg(test)]
6316#[test]
6317fn fx216_inherited_scalar_retains_local_opaque_namespace_authority() {
6318    let local = br#"<a:rPr xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"><opaque/></a:rPr>"#;
6319    let inherited = br#"<a:rPr xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns="urn:parent" b="1"/>"#;
6320    let local = CT_TextCharacterProperties::from_xml(local).unwrap();
6321    let inherited = CT_TextCharacterProperties::from_xml(inherited).unwrap();
6322    let merged = local.with_inherited_defaults(&inherited).unwrap();
6323    let mut writer = Writer::new(Vec::new());
6324    merged.write_xml(&mut writer, "a:rPr").unwrap();
6325    let xml = writer.into_inner();
6326    let mut reader = quick_xml::NsReader::from_reader(xml.as_slice());
6327    let mut seen = false;
6328    loop {
6329        let (namespace, event) = reader.read_resolved_event().unwrap();
6330        match event {
6331            Event::Empty(element) if element.local_name().as_ref() == b"opaque" => {
6332                assert!(matches!(namespace, quick_xml::name::ResolveResult::Unbound));
6333                seen = true;
6334            }
6335            Event::Eof => break,
6336            _ => {}
6337        }
6338    }
6339    assert!(seen);
6340    assert_eq!(merged.bold, Some(true));
6341}
6342
6343#[cfg(test)]
6344#[test]
6345fn fx216_inherited_scalars_keep_false_zero_and_harmless_aliases() {
6346    let local = CT_TextCharacterProperties::from_xml(br#"<q:rPr xmlns:q="http://schemas.openxmlformats.org/drawingml/2006/main" b="0" i="false" spc="0" baseline="0"/>"#).unwrap();
6347    let inherited = CT_TextCharacterProperties::from_xml(br#"<a:defRPr xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns="urn:unused" b="1" i="1" spc="200" baseline="5000" sz="2400"/>"#).unwrap();
6348    let merged = local.with_inherited_defaults(&inherited).unwrap();
6349    assert_eq!(merged.bold, Some(false));
6350    assert_eq!(merged.italic, Some(false));
6351    assert_eq!(merged.spacing, Some(TextPointValue::Centipoints(0)));
6352    assert_eq!(merged.baseline.as_deref(), Some("0"));
6353    assert_eq!(merged.font_size, Some(2400));
6354    let mut writer = Writer::new(Vec::new());
6355    merged.write_xml(&mut writer, "a:rPr").unwrap();
6356    let xml = writer.into_inner();
6357    assert!(!String::from_utf8_lossy(&xml).contains("urn:unused"));
6358    assert_eq!(CT_TextCharacterProperties::from_xml(&xml).unwrap(), merged);
6359    assert_eq!(local.bold, Some(false));
6360    assert_eq!(inherited.bold, Some(true));
6361}