Skip to main content

rdocx_oxml/
run_properties.rs

1//! Run properties (`CT_RPr`).
2
3use quick_xml::events::{BytesEnd, BytesStart, Event};
4use quick_xml::{Reader, Writer, XmlVersion};
5
6use std::borrow::Cow;
7
8use quick_xml::events::attributes::Attribute;
9use quick_xml::name::QName;
10
11use crate::borders::CT_BorderEdge;
12use crate::error::Result;
13use crate::namespace::matches_local_name;
14use crate::properties::{
15    CT_Shd, append_modeled_toggle_attributes, get_word_val_attr, is_word_attribute,
16    is_word_element, parse_integer_measurement, parse_uchar_hex, parse_word_toggle, push_uchar_hex,
17    raw_is_modeled_attribute_carrier, raw_occurrence, record_modeled_toggle_candidate,
18    remove_redundant_modeled_toggle_candidate, replay_modeled_toggle_raw,
19    toggle_element_is_explicitly_empty, toggle_has_unsupported_attributes, word_prefixes_at,
20    write_toggle,
21};
22use crate::raw_xml::{capture_element, capture_empty_element};
23use crate::revision::CT_Revision;
24use crate::shared::{ST_HighlightColor, ST_Underline};
25use crate::units::{HalfPoint, Twips};
26
27const RPR_STYLE_SLOT: u8 = 0;
28const RPR_FONTS_SLOT: u8 = 1;
29const RPR_BOLD_SLOT: u8 = 2;
30const RPR_BOLD_CS_SLOT: u8 = 3;
31const RPR_ITALIC_SLOT: u8 = 4;
32const RPR_ITALIC_CS_SLOT: u8 = 5;
33const RPR_CAPS_SLOT: u8 = 6;
34const RPR_SMALL_CAPS_SLOT: u8 = 7;
35const RPR_STRIKE_SLOT: u8 = 8;
36const RPR_DSTRIKE_SLOT: u8 = 9;
37const RPR_OUTLINE_SLOT: u8 = 10;
38const RPR_SHADOW_SLOT: u8 = 11;
39const RPR_EMBOSS_SLOT: u8 = 12;
40const RPR_IMPRINT_SLOT: u8 = 13;
41const RPR_NO_PROOF_SLOT: u8 = 14;
42const RPR_SNAP_TO_GRID_SLOT: u8 = 15;
43const RPR_VANISH_SLOT: u8 = 16;
44const RPR_WEB_HIDDEN_SLOT: u8 = 17;
45const RPR_COLOR_SLOT: u8 = 18;
46const RPR_SPACING_SLOT: u8 = 19;
47const RPR_WIDTH_SLOT: u8 = 20;
48const RPR_KERN_SLOT: u8 = 21;
49const RPR_POSITION_SLOT: u8 = 22;
50const RPR_SIZE_SLOT: u8 = 23;
51const RPR_SIZE_CS_SLOT: u8 = 24;
52const RPR_HIGHLIGHT_SLOT: u8 = 25;
53const RPR_UNDERLINE_SLOT: u8 = 26;
54const RPR_EFFECT_SLOT: u8 = 27;
55const RPR_BORDER_SLOT: u8 = 28;
56const RPR_SHADING_SLOT: u8 = 29;
57const RPR_FIT_TEXT_SLOT: u8 = 30;
58const RPR_VERT_ALIGN_SLOT: u8 = 31;
59const RPR_RTL_SLOT: u8 = 32;
60const RPR_COMPLEX_SCRIPT_SLOT: u8 = 33;
61const RPR_EMPHASIS_MARK_SLOT: u8 = 34;
62const RPR_LANG_SLOT: u8 = 35;
63const RPR_EAST_ASIAN_LAYOUT_SLOT: u8 = 36;
64const RPR_SPEC_VANISH_SLOT: u8 = 37;
65const RPR_OFFICE_MATH_SLOT: u8 = 38;
66const RPR_MARKER_SLOT: u8 = 39;
67const RPR_CHANGE_SLOT: u8 = 40;
68const RPR_END_SLOT: u8 = 41;
69
70/// The schema slot and typed field for a `w:rPr` toggle that carries no
71/// attribute beyond `w:val`.
72///
73/// `w:b`, `w:i` and the other long-standing toggles keep their own arms. This
74/// covers the ten `EG_RPrBase` toggles F-265 modeled, so one table decides both
75/// the slot and the field rather than ten near-identical arms.
76fn rpr_toggle_slot<'a>(
77    rpr: &'a mut CT_RPr,
78    name: &[u8],
79    prefixes: &[String],
80) -> Option<(u8, &'a mut Option<bool>)> {
81    if is_word_element(name, b"outline", prefixes) {
82        Some((RPR_OUTLINE_SLOT, &mut rpr.outline))
83    } else if is_word_element(name, b"shadow", prefixes) {
84        Some((RPR_SHADOW_SLOT, &mut rpr.shadow))
85    } else if is_word_element(name, b"emboss", prefixes) {
86        Some((RPR_EMBOSS_SLOT, &mut rpr.emboss))
87    } else if is_word_element(name, b"imprint", prefixes) {
88        Some((RPR_IMPRINT_SLOT, &mut rpr.imprint))
89    } else if is_word_element(name, b"noProof", prefixes) {
90        Some((RPR_NO_PROOF_SLOT, &mut rpr.no_proof))
91    } else if is_word_element(name, b"snapToGrid", prefixes) {
92        Some((RPR_SNAP_TO_GRID_SLOT, &mut rpr.snap_to_grid))
93    } else if is_word_element(name, b"webHidden", prefixes) {
94        Some((RPR_WEB_HIDDEN_SLOT, &mut rpr.web_hidden))
95    } else if is_word_element(name, b"cs", prefixes) {
96        Some((RPR_COMPLEX_SCRIPT_SLOT, &mut rpr.complex_script))
97    } else if is_word_element(name, b"specVanish", prefixes) {
98        Some((RPR_SPEC_VANISH_SLOT, &mut rpr.spec_vanish))
99    } else if is_word_element(name, b"oMath", prefixes) {
100        Some((RPR_OFFICE_MATH_SLOT, &mut rpr.office_math))
101    } else {
102        None
103    }
104}
105
106fn record_rpr_modeled(
107    rpr: &mut CT_RPr,
108    pending_raw: &mut Vec<Vec<u8>>,
109    occurrences: &mut [usize],
110    slot: u8,
111) {
112    let occurrence = if slot == RPR_MARKER_SLOT {
113        occurrences[slot as usize]
114    } else {
115        0
116    };
117    flush_rpr_raw(rpr, pending_raw, slot, occurrence);
118    occurrences[slot as usize] += 1;
119}
120
121fn record_rpr_raw_at(rpr: &mut CT_RPr, raw: Vec<u8>, slot: u8, occurrence: usize) {
122    rpr.revision_xml.push(raw);
123    rpr.revision_xml_positions.push((slot, occurrence));
124}
125
126fn flush_rpr_raw(rpr: &mut CT_RPr, pending_raw: &mut Vec<Vec<u8>>, slot: u8, occurrence: usize) {
127    for raw in pending_raw.drain(..) {
128        rpr.revision_xml.push(raw);
129        rpr.revision_xml_positions.push((slot, occurrence));
130    }
131}
132
133/// `ST_TextEffect` — the animated text effect on `w:effect/@w:val`.
134///
135/// Producer-defined tokens are retained through `Other`, matching
136/// `ST_NumberFormat`.
137#[derive(Debug, Clone, PartialEq, Eq)]
138#[allow(non_camel_case_types)]
139pub enum ST_TextEffect {
140    None,
141    BlinkBackground,
142    Lights,
143    AntsBlack,
144    AntsRed,
145    Shimmer,
146    SparkleText,
147    /// A token outside the ECMA-376 inventory, retained verbatim.
148    Other(String),
149}
150
151impl ST_TextEffect {
152    pub fn from_str(value: &str) -> Self {
153        match value {
154            "none" => ST_TextEffect::None,
155            "blinkBackground" => ST_TextEffect::BlinkBackground,
156            "lights" => ST_TextEffect::Lights,
157            "antsBlack" => ST_TextEffect::AntsBlack,
158            "antsRed" => ST_TextEffect::AntsRed,
159            "shimmer" => ST_TextEffect::Shimmer,
160            "sparkleText" => ST_TextEffect::SparkleText,
161            other => ST_TextEffect::Other(other.to_owned()),
162        }
163    }
164
165    pub fn as_str(&self) -> &str {
166        match self {
167            ST_TextEffect::None => "none",
168            ST_TextEffect::BlinkBackground => "blinkBackground",
169            ST_TextEffect::Lights => "lights",
170            ST_TextEffect::AntsBlack => "antsBlack",
171            ST_TextEffect::AntsRed => "antsRed",
172            ST_TextEffect::Shimmer => "shimmer",
173            ST_TextEffect::SparkleText => "sparkleText",
174            ST_TextEffect::Other(value) => value.as_str(),
175        }
176    }
177}
178
179/// `ST_Em` — the emphasis mark on `w:em/@w:val`.
180#[derive(Debug, Clone, PartialEq, Eq)]
181#[allow(non_camel_case_types)]
182pub enum ST_Em {
183    None,
184    Dot,
185    Comma,
186    Circle,
187    UnderDot,
188    /// A token outside the ECMA-376 inventory, retained verbatim.
189    Other(String),
190}
191
192impl ST_Em {
193    pub fn from_str(value: &str) -> Self {
194        match value {
195            "none" => ST_Em::None,
196            "dot" => ST_Em::Dot,
197            "comma" => ST_Em::Comma,
198            "circle" => ST_Em::Circle,
199            "underDot" => ST_Em::UnderDot,
200            other => ST_Em::Other(other.to_owned()),
201        }
202    }
203
204    pub fn as_str(&self) -> &str {
205        match self {
206            ST_Em::None => "none",
207            ST_Em::Dot => "dot",
208            ST_Em::Comma => "comma",
209            ST_Em::Circle => "circle",
210            ST_Em::UnderDot => "underDot",
211            ST_Em::Other(value) => value.as_str(),
212        }
213    }
214}
215
216/// `CT_FitText` — `w:fitText`, the twip width a run segment is fitted into.
217#[derive(Debug, Clone, PartialEq, Eq)]
218#[allow(non_camel_case_types)]
219pub struct CT_FitText {
220    /// Target width in twips (`w:val`).
221    pub val: Twips,
222    /// Identifier grouping the runs that share one fitted segment (`w:id`).
223    pub id: Option<i32>,
224    /// Attributes this type does not model, in source order.
225    pub extra_attributes: Vec<(String, String)>,
226}
227
228/// `CT_EastAsianLayout` — `w:eastAsianLayout`, two-lines-in-one and vertical
229/// text within a run.
230#[derive(Debug, Clone, Default, PartialEq, Eq)]
231#[allow(non_camel_case_types)]
232pub struct CT_EastAsianLayout {
233    /// Identifier grouping the runs that share one layout (`w:id`).
234    pub id: Option<i32>,
235    /// Whether the run is combined into one character cell (`w:combine`).
236    pub combine: Option<bool>,
237    /// The bracket pair drawn around a combined run (`w:combineBrackets`).
238    pub combine_brackets: Option<String>,
239    /// Whether the run is rotated into vertical text (`w:vert`).
240    pub vert: Option<bool>,
241    /// Whether rotated vertical text is compressed (`w:vertCompress`).
242    pub vert_compress: Option<bool>,
243    /// Attributes this type does not model, in source order.
244    pub extra_attributes: Vec<(String, String)>,
245}
246
247/// Parse an `ST_OnOff` attribute value.
248fn parse_on_off(value: &str) -> bool {
249    crate::shared::ST_OnOff::from_str_or_default(Some(value)).is_on()
250}
251
252fn push_on_off(e: &mut BytesStart<'_>, name: &str, value: bool) {
253    e.push_attribute((name, if value { "1" } else { "0" }));
254}
255
256fn push_retained_attributes(e: &mut BytesStart<'_>, attributes: &[(String, String)]) {
257    for (name, value) in attributes {
258        e.push_attribute(Attribute {
259            key: QName(name.as_bytes()),
260            value: Cow::Borrowed(value.as_bytes()),
261        });
262    }
263}
264
265impl CT_FitText {
266    /// Read `w:fitText`, or `None` when its required `w:val` is absent.
267    fn from_xml_attrs(e: &BytesStart<'_>, prefixes: &[String]) -> Result<Option<Self>> {
268        let mut fit_text = CT_FitText {
269            val: Twips(0),
270            id: None,
271            extra_attributes: Vec::new(),
272        };
273        let mut width_seen = false;
274        for attribute in e.attributes() {
275            let attribute = attribute?;
276            let key = attribute.key.as_ref();
277            let value = std::str::from_utf8(&attribute.value)?;
278            if is_word_attribute(key, b"val", prefixes) {
279                fit_text.val = Twips(parse_integer_measurement(e.name().as_ref(), key, value)?);
280                width_seen = true;
281            } else if is_word_attribute(key, b"id", prefixes) {
282                fit_text.id = Some(value.parse()?);
283            } else {
284                fit_text
285                    .extra_attributes
286                    .push((std::str::from_utf8(key)?.to_owned(), value.to_owned()));
287            }
288        }
289        Ok(width_seen.then_some(fit_text))
290    }
291
292    fn write_xml<W: std::io::Write>(&self, writer: &mut Writer<W>) -> Result<()> {
293        let mut buf = itoa::Buffer::new();
294        let mut e = BytesStart::new("w:fitText");
295        push_retained_attributes(&mut e, &self.extra_attributes);
296        e.push_attribute(("w:val", buf.format(self.val.0)));
297        if let Some(id) = self.id {
298            e.push_attribute(("w:id", buf.format(id)));
299        }
300        writer.write_event(Event::Empty(e))?;
301        Ok(())
302    }
303}
304
305impl CT_EastAsianLayout {
306    fn from_xml_attrs(e: &BytesStart<'_>, prefixes: &[String]) -> Result<Self> {
307        let mut layout = CT_EastAsianLayout::default();
308        for attribute in e.attributes() {
309            let attribute = attribute?;
310            let key = attribute.key.as_ref();
311            let value = std::str::from_utf8(&attribute.value)?;
312            if is_word_attribute(key, b"id", prefixes) {
313                layout.id = Some(value.parse()?);
314            } else if is_word_attribute(key, b"combine", prefixes) {
315                layout.combine = Some(parse_on_off(value));
316            } else if is_word_attribute(key, b"combineBrackets", prefixes) {
317                layout.combine_brackets = Some(value.to_owned());
318            } else if is_word_attribute(key, b"vert", prefixes) {
319                layout.vert = Some(parse_on_off(value));
320            } else if is_word_attribute(key, b"vertCompress", prefixes) {
321                layout.vert_compress = Some(parse_on_off(value));
322            } else {
323                layout
324                    .extra_attributes
325                    .push((std::str::from_utf8(key)?.to_owned(), value.to_owned()));
326            }
327        }
328        Ok(layout)
329    }
330
331    fn write_xml<W: std::io::Write>(&self, writer: &mut Writer<W>) -> Result<()> {
332        let mut buf = itoa::Buffer::new();
333        let mut e = BytesStart::new("w:eastAsianLayout");
334        push_retained_attributes(&mut e, &self.extra_attributes);
335        if let Some(id) = self.id {
336            e.push_attribute(("w:id", buf.format(id)));
337        }
338        if let Some(combine) = self.combine {
339            push_on_off(&mut e, "w:combine", combine);
340        }
341        if let Some(ref brackets) = self.combine_brackets {
342            e.push_attribute(("w:combineBrackets", brackets.as_str()));
343        }
344        if let Some(vert) = self.vert {
345            push_on_off(&mut e, "w:vert", vert);
346        }
347        if let Some(vert_compress) = self.vert_compress {
348            push_on_off(&mut e, "w:vertCompress", vert_compress);
349        }
350        writer.write_event(Event::Empty(e))?;
351        Ok(())
352    }
353}
354
355/// `CT_RPr` — Run properties.
356#[derive(Debug, Clone, Default, PartialEq)]
357#[allow(non_snake_case)]
358pub struct CT_RPr {
359    /// Character style ID (rStyle)
360    pub style_id: Option<String>,
361    /// Font name for ASCII range (rFonts/@w:ascii)
362    pub font_ascii: Option<String>,
363    /// Font name for high-ANSI range (rFonts/@w:hAnsi)
364    pub font_hansi: Option<String>,
365    /// Font name for East Asian text (rFonts/@w:eastAsia)
366    pub font_east_asia: Option<String>,
367    /// Font name for complex script (rFonts/@w:cs)
368    pub font_cs: Option<String>,
369    /// Theme font for ASCII range (rFonts/@w:asciiTheme), e.g. "minorHAnsi", "majorHAnsi"
370    pub font_ascii_theme: Option<String>,
371    /// Theme font for hAnsi range (rFonts/@w:hAnsiTheme)
372    pub font_hansi_theme: Option<String>,
373    /// Theme font for East Asian text (rFonts/@w:eastAsiaTheme)
374    pub font_east_asia_theme: Option<String>,
375    /// Theme font for complex script (rFonts/@w:cstheme)
376    pub font_cs_theme: Option<String>,
377    /// Font slot hint for ambiguous characters (rFonts/@w:hint)
378    pub font_hint: Option<String>,
379    /// Namespace declarations and foreign attributes retained from `w:rFonts`.
380    #[doc(hidden)]
381    pub font_extra_attributes: Vec<(String, String)>,
382    /// Bold (b)
383    pub bold: Option<bool>,
384    /// Bold complex script (bCs)
385    pub bold_cs: Option<bool>,
386    /// Italic (i)
387    pub italic: Option<bool>,
388    /// Italic complex script (iCs)
389    pub italic_cs: Option<bool>,
390    /// Underline type (u)
391    pub underline: Option<ST_Underline>,
392    /// Strikethrough (strike)
393    pub strike: Option<bool>,
394    /// Double strikethrough (dstrike)
395    pub dstrike: Option<bool>,
396    /// Font size in half-points (sz)
397    pub sz: Option<HalfPoint>,
398    /// Complex-script font size in half-points (szCs)
399    pub sz_cs: Option<HalfPoint>,
400    /// Text color as hex string, e.g. "FF0000" (color/@w:val)
401    pub color: Option<String>,
402    /// Color theme reference (color/@w:themeColor)
403    pub color_theme: Option<String>,
404    /// Theme colour tint, 0 to 255 (color/@w:themeTint)
405    pub color_theme_tint: Option<u8>,
406    /// Theme colour shade, 0 to 255 (color/@w:themeShade)
407    pub color_theme_shade: Option<u8>,
408    /// Namespace declarations and foreign attributes retained from `w:color`.
409    #[doc(hidden)]
410    pub color_extra_attributes: Vec<(String, String)>,
411    /// Highlight color (highlight)
412    pub highlight: Option<ST_HighlightColor>,
413    /// All caps (caps)
414    pub caps: Option<bool>,
415    /// Small caps (smallCaps)
416    pub small_caps: Option<bool>,
417    /// Superscript/subscript (vertAlign)
418    pub vert_align: Option<String>,
419    /// Character spacing in twips (spacing/@w:val)
420    pub spacing: Option<Twips>,
421    /// Character width scale in percent (w/@w:val)
422    pub width_scale: Option<u32>,
423    /// Text position (raised/lowered) in half-points (position/@w:val)
424    pub position: Option<i32>,
425    /// Run shading (shd)
426    pub shading: Option<Box<CT_Shd>>,
427    /// Vanish/hidden text (vanish)
428    pub vanish: Option<bool>,
429    /// Outline, stroke-only glyphs (outline), schema slot 10.
430    pub outline: Option<bool>,
431    /// Drop shadow behind the glyphs (shadow), schema slot 11.
432    pub shadow: Option<bool>,
433    /// Raised relief (emboss), schema slot 12.
434    pub emboss: Option<bool>,
435    /// Sunken relief (imprint), schema slot 13.
436    pub imprint: Option<bool>,
437    /// Exclude from proofing (noProof), schema slot 14.
438    pub no_proof: Option<bool>,
439    /// Snap to the document character grid (snapToGrid), schema slot 15.
440    pub snap_to_grid: Option<bool>,
441    /// Hidden in web view only (webHidden), schema slot 17.
442    pub web_hidden: Option<bool>,
443    /// Kerning threshold in half-points (kern), schema slot 21.
444    pub kern: Option<HalfPoint>,
445    /// Animated text effect (effect), schema slot 27.
446    pub effect: Option<ST_TextEffect>,
447    /// Character border (bdr), schema slot 28.
448    ///
449    /// Boxed so a run property struct that models the whole `EG_RPrBase`
450    /// sequence still fits the test-thread stack budget, following the
451    /// `CT_PPr::borders` precedent.
452    pub border: Option<Box<CT_BorderEdge>>,
453    /// Fitted segment width (fitText), schema slot 30.
454    pub fit_text: Option<Box<CT_FitText>>,
455    /// Complex-script formatting toggle (cs), schema slot 33.
456    pub complex_script: Option<bool>,
457    /// East Asian emphasis mark (em), schema slot 34.
458    pub emphasis_mark: Option<ST_Em>,
459    /// East Asian run layout (eastAsianLayout), schema slot 36.
460    pub east_asian_layout: Option<Box<CT_EastAsianLayout>>,
461    /// Vanish only at the end of a numbered paragraph (specVanish), slot 37.
462    pub spec_vanish: Option<bool>,
463    /// Office Math run (oMath), schema slot 38.
464    pub office_math: Option<bool>,
465    /// Character-level right-to-left direction (rtl).
466    pub rtl: Option<bool>,
467    /// Language for Latin and high-ANSI text (`lang/@w:val`).
468    pub language: Option<String>,
469    /// Language for East Asian text (`lang/@w:eastAsia`).
470    pub language_east_asia: Option<String>,
471    /// Language for complex-script text (`lang/@w:bidi`).
472    pub language_bidi: Option<String>,
473    /// Namespace declarations and foreign attributes retained from `w:lang`.
474    #[doc(hidden)]
475    pub language_extra_attributes: Vec<(String, String)>,
476    /// Contextual insertion and deletion markers retained in schema order.
477    pub revision_markers: Vec<CT_Revision>,
478    /// Prior run properties from the schema-final `w:rPrChange`.
479    ///
480    /// Boxed for the same stack-budget reason as `border`.
481    pub change: Option<Box<CT_Revision>>,
482    /// Foreign and unmodelled children retained without a typed projection.
483    pub revision_xml: Vec<Vec<u8>>,
484    /// Schema slots and occurrences for retained raw children.
485    #[doc(hidden)]
486    pub revision_xml_positions: Vec<(u8, usize)>,
487}
488
489#[allow(non_snake_case)]
490impl CT_RPr {
491    /// Resolve qualified solid text fill retained as opaque source.
492    /// Other extension representations retain their source without being guessed.
493    pub fn solid_text_fill(&self) -> Option<(String, f64)> {
494        const NS: &[u8] = b"http://schemas.microsoft.com/office/word/2010/wordml";
495        for raw in &self.revision_xml {
496            let mut probe = quick_xml::NsReader::from_reader(raw.as_slice());
497            let Ok((namespace, event)) = probe.read_resolved_event() else {
498                continue;
499            };
500            if !matches!(namespace, quick_xml::name::ResolveResult::Bound(ns) if ns.as_ref() == NS)
501                || !matches!(event, Event::Start(ref element) if element.local_name().as_ref() == b"textFill")
502            {
503                continue;
504            }
505            let mut reader = quick_xml::NsReader::from_reader(raw.as_slice());
506            let mut path = Vec::new();
507            let mut color = None;
508            let mut alpha = 1.0;
509            let mut valid = true;
510            loop {
511                let (namespace, event) = reader.read_resolved_event().ok()?;
512                match event {
513                    Event::Start(ref element) | Event::Empty(ref element) => {
514                        let name = element.local_name().as_ref().to_vec();
515                        let qualified = matches!(namespace, quick_xml::name::ResolveResult::Bound(ns) if ns.as_ref() == NS);
516                        if !qualified {
517                            valid = false;
518                        }
519                        path.push(name);
520                        let expected: &[&[u8]] = &[b"textFill", b"solidFill", b"srgbClr", b"alpha"];
521                        if path.len() > expected.len()
522                            || !path
523                                .iter()
524                                .zip(expected)
525                                .all(|(actual, expected)| actual.as_slice() == *expected)
526                        {
527                            valid = false;
528                        }
529                        if path.len() == 3 || path.len() == 4 {
530                            let mut value = None;
531                            for attribute in element.attributes() {
532                                let attribute = attribute.ok()?;
533                                let (ns, local) =
534                                    reader.resolver().resolve_attribute(attribute.key);
535                                if matches!(ns, quick_xml::name::ResolveResult::Bound(ns) if ns.as_ref() == NS)
536                                    && local.as_ref() == b"val"
537                                {
538                                    if value.is_some() {
539                                        valid = false;
540                                    }
541                                    value = Some(
542                                        attribute
543                                            .decoded_and_normalized_value(
544                                                quick_xml::XmlVersion::Implicit1_0,
545                                                reader.decoder(),
546                                            )
547                                            .ok()?
548                                            .into_owned(),
549                                    );
550                                }
551                            }
552                            if path.len() == 3 {
553                                let hex = value?;
554                                if hex.len() != 6
555                                    || !hex.bytes().all(|byte| byte.is_ascii_hexdigit())
556                                    || color.is_some()
557                                {
558                                    valid = false;
559                                }
560                                color = Some(hex);
561                            } else {
562                                let amount: u32 = value?.parse().ok()?;
563                                if amount > 100_000 {
564                                    valid = false;
565                                }
566                                alpha = f64::from(amount) / 100_000.0;
567                            }
568                        }
569                        if matches!(event, Event::Empty(_)) {
570                            path.pop();
571                        }
572                    }
573                    Event::End(_) => {
574                        path.pop();
575                    }
576                    Event::Eof => break,
577                    Event::Text(ref text) if text.as_ref().iter().all(u8::is_ascii_whitespace) => {}
578                    _ => {
579                        valid = false;
580                    }
581                }
582            }
583            if valid && let Some(color) = color {
584                return Some((color, alpha));
585            }
586        }
587        None
588    }
589
590    pub fn from_xml(reader: &mut Reader<&[u8]>) -> Result<Self> {
591        Self::from_xml_with_prefixes(reader, &["w".to_owned()])
592    }
593
594    pub(crate) fn from_xml_with_prefixes(
595        reader: &mut Reader<&[u8]>,
596        word_prefixes: &[String],
597    ) -> Result<Self> {
598        Self::from_xml_with_prefixes_and_owner_bindings(reader, word_prefixes, &[])
599    }
600
601    pub(crate) fn from_xml_with_prefixes_and_owner_bindings(
602        reader: &mut Reader<&[u8]>,
603        word_prefixes: &[String],
604        owner_bindings: &[(String, String)],
605    ) -> Result<Self> {
606        let mut rpr = CT_RPr::default();
607        let mut change_raw_index = 0usize;
608        let mut pending_raw = Vec::new();
609        let mut occurrences = [0usize; RPR_END_SLOT as usize + 1];
610        let mut rtl_carrier_required = false;
611        let mut buf = Vec::new();
612
613        loop {
614            match reader.read_event_into(&mut buf) {
615                Ok(Event::Empty(ref e)) => {
616                    let name = e.name();
617                    let prefixes = word_prefixes_at(e, word_prefixes)?;
618                    if is_word_element(name.as_ref(), b"rStyle", &prefixes) {
619                        record_rpr_modeled(
620                            &mut rpr,
621                            &mut pending_raw,
622                            &mut occurrences,
623                            RPR_STYLE_SLOT,
624                        );
625                        rpr.style_id = get_word_val_attr(e, &prefixes)?;
626                    } else if is_word_element(name.as_ref(), b"rFonts", &prefixes) {
627                        record_rpr_modeled(
628                            &mut rpr,
629                            &mut pending_raw,
630                            &mut occurrences,
631                            RPR_FONTS_SLOT,
632                        );
633                        for attr in e.attributes() {
634                            let attr = attr?;
635                            let key = attr.key.as_ref();
636                            let val = std::str::from_utf8(&attr.value)?.to_string();
637                            if is_word_attribute(key, b"ascii", &prefixes) {
638                                rpr.font_ascii = Some(val);
639                            } else if is_word_attribute(key, b"hAnsi", &prefixes) {
640                                rpr.font_hansi = Some(val);
641                            } else if is_word_attribute(key, b"eastAsia", &prefixes) {
642                                rpr.font_east_asia = Some(val);
643                            } else if is_word_attribute(key, b"cs", &prefixes) {
644                                rpr.font_cs = Some(val);
645                            } else if is_word_attribute(key, b"asciiTheme", &prefixes) {
646                                rpr.font_ascii_theme = Some(val);
647                            } else if is_word_attribute(key, b"hAnsiTheme", &prefixes) {
648                                rpr.font_hansi_theme = Some(val);
649                            } else if is_word_attribute(key, b"eastAsiaTheme", &prefixes) {
650                                rpr.font_east_asia_theme = Some(val);
651                            } else if is_word_attribute(key, b"cstheme", &prefixes) {
652                                rpr.font_cs_theme = Some(val);
653                            } else if is_word_attribute(key, b"hint", &prefixes) {
654                                rpr.font_hint = Some(val);
655                            } else {
656                                rpr.font_extra_attributes
657                                    .push((std::str::from_utf8(key)?.to_owned(), val));
658                            }
659                        }
660                    } else if is_word_element(name.as_ref(), b"b", &prefixes) {
661                        record_rpr_modeled(
662                            &mut rpr,
663                            &mut pending_raw,
664                            &mut occurrences,
665                            RPR_BOLD_SLOT,
666                        );
667                        rpr.bold = Some(parse_word_toggle(e, &prefixes)?);
668                    } else if is_word_element(name.as_ref(), b"bCs", &prefixes) {
669                        record_rpr_modeled(
670                            &mut rpr,
671                            &mut pending_raw,
672                            &mut occurrences,
673                            RPR_BOLD_CS_SLOT,
674                        );
675                        rpr.bold_cs = Some(parse_word_toggle(e, &prefixes)?);
676                    } else if is_word_element(name.as_ref(), b"i", &prefixes) {
677                        record_rpr_modeled(
678                            &mut rpr,
679                            &mut pending_raw,
680                            &mut occurrences,
681                            RPR_ITALIC_SLOT,
682                        );
683                        rpr.italic = Some(parse_word_toggle(e, &prefixes)?);
684                    } else if is_word_element(name.as_ref(), b"iCs", &prefixes) {
685                        record_rpr_modeled(
686                            &mut rpr,
687                            &mut pending_raw,
688                            &mut occurrences,
689                            RPR_ITALIC_CS_SLOT,
690                        );
691                        rpr.italic_cs = Some(parse_word_toggle(e, &prefixes)?);
692                    } else if is_word_element(name.as_ref(), b"u", &prefixes) {
693                        record_rpr_modeled(
694                            &mut rpr,
695                            &mut pending_raw,
696                            &mut occurrences,
697                            RPR_UNDERLINE_SLOT,
698                        );
699                        if let Some(val) = get_word_val_attr(e, &prefixes)? {
700                            rpr.underline = ST_Underline::from_str(&val).ok();
701                        } else {
702                            rpr.underline = Some(ST_Underline::Single);
703                        }
704                    } else if is_word_element(name.as_ref(), b"strike", &prefixes) {
705                        record_rpr_modeled(
706                            &mut rpr,
707                            &mut pending_raw,
708                            &mut occurrences,
709                            RPR_STRIKE_SLOT,
710                        );
711                        rpr.strike = Some(parse_word_toggle(e, &prefixes)?);
712                    } else if is_word_element(name.as_ref(), b"dstrike", &prefixes) {
713                        record_rpr_modeled(
714                            &mut rpr,
715                            &mut pending_raw,
716                            &mut occurrences,
717                            RPR_DSTRIKE_SLOT,
718                        );
719                        rpr.dstrike = Some(parse_word_toggle(e, &prefixes)?);
720                    } else if is_word_element(name.as_ref(), b"sz", &prefixes) {
721                        record_rpr_modeled(
722                            &mut rpr,
723                            &mut pending_raw,
724                            &mut occurrences,
725                            RPR_SIZE_SLOT,
726                        );
727                        if let Some(val) = get_word_val_attr(e, &prefixes)? {
728                            rpr.sz = Some(HalfPoint(parse_integer_measurement(
729                                name.as_ref(),
730                                b"w:val",
731                                &val,
732                            )?));
733                        }
734                    } else if is_word_element(name.as_ref(), b"szCs", &prefixes) {
735                        record_rpr_modeled(
736                            &mut rpr,
737                            &mut pending_raw,
738                            &mut occurrences,
739                            RPR_SIZE_CS_SLOT,
740                        );
741                        if let Some(val) = get_word_val_attr(e, &prefixes)? {
742                            rpr.sz_cs = Some(HalfPoint(parse_integer_measurement(
743                                name.as_ref(),
744                                b"w:val",
745                                &val,
746                            )?));
747                        }
748                    } else if is_word_element(name.as_ref(), b"color", &prefixes) {
749                        record_rpr_modeled(
750                            &mut rpr,
751                            &mut pending_raw,
752                            &mut occurrences,
753                            RPR_COLOR_SLOT,
754                        );
755                        for attr in e.attributes() {
756                            let attr = attr?;
757                            let key = attr.key.as_ref();
758                            let v = std::str::from_utf8(&attr.value)?.to_string();
759                            if is_word_attribute(key, b"val", &prefixes) {
760                                rpr.color = Some(v);
761                            } else if is_word_attribute(key, b"themeColor", &prefixes) {
762                                rpr.color_theme = Some(v);
763                            } else if is_word_attribute(key, b"themeTint", &prefixes) {
764                                match parse_uchar_hex(&v) {
765                                    Some(tint) => rpr.color_theme_tint = Some(tint),
766                                    None => rpr
767                                        .color_extra_attributes
768                                        .push((std::str::from_utf8(key)?.to_owned(), v)),
769                                }
770                            } else if is_word_attribute(key, b"themeShade", &prefixes) {
771                                match parse_uchar_hex(&v) {
772                                    Some(shade) => rpr.color_theme_shade = Some(shade),
773                                    None => rpr
774                                        .color_extra_attributes
775                                        .push((std::str::from_utf8(key)?.to_owned(), v)),
776                                }
777                            } else {
778                                rpr.color_extra_attributes
779                                    .push((std::str::from_utf8(key)?.to_owned(), v));
780                            }
781                        }
782                    } else if is_word_element(name.as_ref(), b"highlight", &prefixes) {
783                        record_rpr_modeled(
784                            &mut rpr,
785                            &mut pending_raw,
786                            &mut occurrences,
787                            RPR_HIGHLIGHT_SLOT,
788                        );
789                        if let Some(val) = get_word_val_attr(e, &prefixes)? {
790                            rpr.highlight = ST_HighlightColor::from_str(&val).ok();
791                        }
792                    } else if is_word_element(name.as_ref(), b"caps", &prefixes) {
793                        record_rpr_modeled(
794                            &mut rpr,
795                            &mut pending_raw,
796                            &mut occurrences,
797                            RPR_CAPS_SLOT,
798                        );
799                        rpr.caps = Some(parse_word_toggle(e, &prefixes)?);
800                    } else if is_word_element(name.as_ref(), b"smallCaps", &prefixes) {
801                        record_rpr_modeled(
802                            &mut rpr,
803                            &mut pending_raw,
804                            &mut occurrences,
805                            RPR_SMALL_CAPS_SLOT,
806                        );
807                        rpr.small_caps = Some(parse_word_toggle(e, &prefixes)?);
808                    } else if is_word_element(name.as_ref(), b"vertAlign", &prefixes) {
809                        record_rpr_modeled(
810                            &mut rpr,
811                            &mut pending_raw,
812                            &mut occurrences,
813                            RPR_VERT_ALIGN_SLOT,
814                        );
815                        rpr.vert_align = get_word_val_attr(e, &prefixes)?;
816                    } else if is_word_element(name.as_ref(), b"spacing", &prefixes) {
817                        record_rpr_modeled(
818                            &mut rpr,
819                            &mut pending_raw,
820                            &mut occurrences,
821                            RPR_SPACING_SLOT,
822                        );
823                        if let Some(val) = get_word_val_attr(e, &prefixes)? {
824                            rpr.spacing = Some(Twips(parse_integer_measurement(
825                                name.as_ref(),
826                                b"w:val",
827                                &val,
828                            )?));
829                        }
830                    } else if is_word_element(name.as_ref(), b"w", &prefixes) {
831                        record_rpr_modeled(
832                            &mut rpr,
833                            &mut pending_raw,
834                            &mut occurrences,
835                            RPR_WIDTH_SLOT,
836                        );
837                        if let Some(val) = get_word_val_attr(e, &prefixes)? {
838                            rpr.width_scale = Some(val.parse()?);
839                        }
840                    } else if is_word_element(name.as_ref(), b"position", &prefixes) {
841                        record_rpr_modeled(
842                            &mut rpr,
843                            &mut pending_raw,
844                            &mut occurrences,
845                            RPR_POSITION_SLOT,
846                        );
847                        if let Some(val) = get_word_val_attr(e, &prefixes)? {
848                            rpr.position =
849                                Some(parse_integer_measurement(name.as_ref(), b"w:val", &val)?);
850                        }
851                    } else if is_word_element(name.as_ref(), b"shd", &prefixes) {
852                        record_rpr_modeled(
853                            &mut rpr,
854                            &mut pending_raw,
855                            &mut occurrences,
856                            RPR_SHADING_SLOT,
857                        );
858                        rpr.shading = Some(Box::new(CT_Shd::from_xml_attrs_with_prefixes(
859                            e, &prefixes,
860                        )?));
861                    } else if is_word_element(name.as_ref(), b"vanish", &prefixes) {
862                        record_rpr_modeled(
863                            &mut rpr,
864                            &mut pending_raw,
865                            &mut occurrences,
866                            RPR_VANISH_SLOT,
867                        );
868                        rpr.vanish = Some(parse_word_toggle(e, &prefixes)?);
869                    } else if let Some((slot, toggle)) =
870                        rpr_toggle_slot(&mut rpr, name.as_ref(), &prefixes)
871                    {
872                        let value = parse_word_toggle(e, &prefixes)?;
873                        *toggle = Some(value);
874                        record_rpr_modeled(&mut rpr, &mut pending_raw, &mut occurrences, slot);
875                    } else if is_word_element(name.as_ref(), b"kern", &prefixes)
876                        && let Some(val) = get_word_val_attr(e, &prefixes)?
877                    {
878                        // A `w:kern` without its required `w:val` carries no
879                        // typed value, so it stays raw rather than being
880                        // consumed into a field that would drop it on save.
881                        record_rpr_modeled(
882                            &mut rpr,
883                            &mut pending_raw,
884                            &mut occurrences,
885                            RPR_KERN_SLOT,
886                        );
887                        rpr.kern = Some(HalfPoint(parse_integer_measurement(
888                            name.as_ref(),
889                            b"w:val",
890                            &val,
891                        )?));
892                    } else if is_word_element(name.as_ref(), b"effect", &prefixes) {
893                        record_rpr_modeled(
894                            &mut rpr,
895                            &mut pending_raw,
896                            &mut occurrences,
897                            RPR_EFFECT_SLOT,
898                        );
899                        rpr.effect = Some(
900                            get_word_val_attr(e, &prefixes)?
901                                .map_or(ST_TextEffect::None, |val| ST_TextEffect::from_str(&val)),
902                        );
903                    } else if is_word_element(name.as_ref(), b"bdr", &prefixes) {
904                        record_rpr_modeled(
905                            &mut rpr,
906                            &mut pending_raw,
907                            &mut occurrences,
908                            RPR_BORDER_SLOT,
909                        );
910                        rpr.border = Some(Box::new(CT_BorderEdge::from_xml_attrs_with_prefixes(
911                            e, &prefixes,
912                        )?));
913                    } else if is_word_element(name.as_ref(), b"fitText", &prefixes)
914                        && let Some(fit_text) = CT_FitText::from_xml_attrs(e, &prefixes)?
915                    {
916                        // As for `w:kern`, a `w:fitText` without its required
917                        // `w:val` stays raw.
918                        record_rpr_modeled(
919                            &mut rpr,
920                            &mut pending_raw,
921                            &mut occurrences,
922                            RPR_FIT_TEXT_SLOT,
923                        );
924                        rpr.fit_text = Some(Box::new(fit_text));
925                    } else if is_word_element(name.as_ref(), b"em", &prefixes) {
926                        record_rpr_modeled(
927                            &mut rpr,
928                            &mut pending_raw,
929                            &mut occurrences,
930                            RPR_EMPHASIS_MARK_SLOT,
931                        );
932                        rpr.emphasis_mark = Some(
933                            get_word_val_attr(e, &prefixes)?
934                                .map_or(ST_Em::None, |val| ST_Em::from_str(&val)),
935                        );
936                    } else if is_word_element(name.as_ref(), b"eastAsianLayout", &prefixes) {
937                        record_rpr_modeled(
938                            &mut rpr,
939                            &mut pending_raw,
940                            &mut occurrences,
941                            RPR_EAST_ASIAN_LAYOUT_SLOT,
942                        );
943                        rpr.east_asian_layout =
944                            Some(Box::new(CT_EastAsianLayout::from_xml_attrs(e, &prefixes)?));
945                    } else if is_word_element(name.as_ref(), b"rtl", &prefixes) {
946                        record_rpr_modeled(
947                            &mut rpr,
948                            &mut pending_raw,
949                            &mut occurrences,
950                            RPR_RTL_SLOT,
951                        );
952                        rpr.rtl = Some(parse_word_toggle(e, &prefixes)?);
953                        rtl_carrier_required = toggle_has_unsupported_attributes(e, &prefixes)?;
954                        let raw = crate::text::raw_with_external_bindings(
955                            &capture_empty_element(e)?,
956                            owner_bindings,
957                        )?;
958                        record_modeled_toggle_candidate(
959                            &mut rpr.revision_xml,
960                            &mut rpr.revision_xml_positions,
961                            raw,
962                            RPR_RTL_SLOT,
963                            occurrences[RPR_RTL_SLOT as usize] - 1,
964                        );
965                    } else if is_word_element(name.as_ref(), b"lang", &prefixes) {
966                        record_rpr_modeled(
967                            &mut rpr,
968                            &mut pending_raw,
969                            &mut occurrences,
970                            RPR_LANG_SLOT,
971                        );
972                        parse_language_attributes(&mut rpr, e, &prefixes)?;
973                    } else if is_word_element(name.as_ref(), b"ins", &prefixes)
974                        || is_word_element(name.as_ref(), b"del", &prefixes)
975                    {
976                        let raw = crate::text::raw_with_external_bindings(
977                            &capture_empty_element(e)?,
978                            owner_bindings,
979                        )?;
980                        if let Some(revision) = CT_Revision::from_raw(raw.clone(), &prefixes) {
981                            record_rpr_modeled(
982                                &mut rpr,
983                                &mut pending_raw,
984                                &mut occurrences,
985                                RPR_MARKER_SLOT,
986                            );
987                            rpr.revision_markers.push(revision);
988                        } else {
989                            pending_raw.push(raw);
990                        }
991                    } else if is_word_element(name.as_ref(), b"rPrChange", &prefixes) {
992                        let raw = crate::text::raw_with_external_bindings(
993                            &capture_empty_element(e)?,
994                            owner_bindings,
995                        )?;
996                        if let Some(revision) = CT_Revision::from_raw(raw.clone(), &prefixes) {
997                            if let Some(previous) = rpr.change.take() {
998                                rpr.revision_xml
999                                    .insert(change_raw_index, previous.into_raw_xml());
1000                                rpr.revision_xml_positions
1001                                    .insert(change_raw_index, (RPR_CHANGE_SLOT, 0));
1002                            }
1003                            record_rpr_modeled(
1004                                &mut rpr,
1005                                &mut pending_raw,
1006                                &mut occurrences,
1007                                RPR_CHANGE_SLOT,
1008                            );
1009                            rpr.change = Some(Box::new(revision));
1010                            change_raw_index = rpr.revision_xml.len();
1011                        } else {
1012                            flush_rpr_raw(&mut rpr, &mut pending_raw, RPR_CHANGE_SLOT, 0);
1013                            record_rpr_raw_at(&mut rpr, raw, RPR_CHANGE_SLOT, 0);
1014                        }
1015                    } else {
1016                        let raw = crate::text::raw_with_external_bindings(
1017                            &capture_empty_element(e)?,
1018                            owner_bindings,
1019                        )?;
1020                        if let Some(slot) = rpr_schema_slot(name.as_ref(), &prefixes) {
1021                            record_rpr_raw_at(&mut rpr, raw, slot, 0);
1022                        } else {
1023                            pending_raw.push(raw);
1024                        }
1025                    }
1026                }
1027                Ok(Event::Start(ref e)) => {
1028                    let prefixes = word_prefixes_at(e, word_prefixes)?;
1029                    if is_word_element(e.name().as_ref(), b"rtl", &prefixes) {
1030                        let captured = capture_element(reader, e)?;
1031                        let raw =
1032                            crate::text::raw_with_external_bindings(&captured, owner_bindings)?;
1033                        if toggle_element_is_explicitly_empty(&captured)? {
1034                            record_rpr_modeled(
1035                                &mut rpr,
1036                                &mut pending_raw,
1037                                &mut occurrences,
1038                                RPR_RTL_SLOT,
1039                            );
1040                            rpr.rtl = Some(parse_word_toggle(e, &prefixes)?);
1041                            rtl_carrier_required = toggle_has_unsupported_attributes(e, &prefixes)?;
1042                            record_modeled_toggle_candidate(
1043                                &mut rpr.revision_xml,
1044                                &mut rpr.revision_xml_positions,
1045                                raw,
1046                                RPR_RTL_SLOT,
1047                                occurrences[RPR_RTL_SLOT as usize] - 1,
1048                            );
1049                        } else {
1050                            let occurrence = occurrences[RPR_RTL_SLOT as usize];
1051                            occurrences[RPR_RTL_SLOT as usize] += 1;
1052                            record_rpr_raw_at(&mut rpr, raw, RPR_RTL_SLOT, occurrence);
1053                        }
1054                    } else if is_word_element(e.name().as_ref(), b"lang", &prefixes) {
1055                        let captured = capture_element(reader, e)?;
1056                        let raw =
1057                            crate::text::raw_with_external_bindings(&captured, owner_bindings)?;
1058                        if language_element_is_explicitly_empty(&captured)? {
1059                            record_rpr_modeled(
1060                                &mut rpr,
1061                                &mut pending_raw,
1062                                &mut occurrences,
1063                                RPR_LANG_SLOT,
1064                            );
1065                            parse_language_attributes(&mut rpr, e, &prefixes)?;
1066                        } else {
1067                            record_rpr_raw_at(
1068                                &mut rpr,
1069                                raw,
1070                                RPR_LANG_SLOT,
1071                                occurrences[RPR_LANG_SLOT as usize],
1072                            );
1073                        }
1074                    } else if is_word_element(e.name().as_ref(), b"ins", &prefixes)
1075                        || is_word_element(e.name().as_ref(), b"del", &prefixes)
1076                    {
1077                        let raw = crate::text::raw_with_external_bindings(
1078                            &capture_element(reader, e)?,
1079                            owner_bindings,
1080                        )?;
1081                        if let Some(revision) = CT_Revision::from_raw(raw.clone(), &prefixes) {
1082                            record_rpr_modeled(
1083                                &mut rpr,
1084                                &mut pending_raw,
1085                                &mut occurrences,
1086                                RPR_MARKER_SLOT,
1087                            );
1088                            rpr.revision_markers.push(revision);
1089                        } else {
1090                            pending_raw.push(raw);
1091                        }
1092                    } else if is_word_element(e.name().as_ref(), b"rPrChange", &prefixes) {
1093                        let raw = crate::text::raw_with_external_bindings(
1094                            &capture_element(reader, e)?,
1095                            owner_bindings,
1096                        )?;
1097                        if let Some(revision) = CT_Revision::from_raw(raw.clone(), &prefixes) {
1098                            if let Some(previous) = rpr.change.take() {
1099                                rpr.revision_xml
1100                                    .insert(change_raw_index, previous.into_raw_xml());
1101                                rpr.revision_xml_positions
1102                                    .insert(change_raw_index, (RPR_CHANGE_SLOT, 0));
1103                            }
1104                            record_rpr_modeled(
1105                                &mut rpr,
1106                                &mut pending_raw,
1107                                &mut occurrences,
1108                                RPR_CHANGE_SLOT,
1109                            );
1110                            rpr.change = Some(Box::new(revision));
1111                            change_raw_index = rpr.revision_xml.len();
1112                        } else {
1113                            flush_rpr_raw(&mut rpr, &mut pending_raw, RPR_CHANGE_SLOT, 0);
1114                            record_rpr_raw_at(&mut rpr, raw, RPR_CHANGE_SLOT, 0);
1115                        }
1116                    } else {
1117                        let raw = crate::text::raw_with_external_bindings(
1118                            &capture_element(reader, e)?,
1119                            owner_bindings,
1120                        )?;
1121                        if let Some(slot) = rpr_schema_slot(e.name().as_ref(), &prefixes) {
1122                            record_rpr_raw_at(&mut rpr, raw, slot, 0);
1123                        } else {
1124                            pending_raw.push(raw);
1125                        }
1126                    }
1127                }
1128                Ok(Event::End(ref e)) if matches_local_name(e.name().as_ref(), b"rPr") => {
1129                    break;
1130                }
1131                Ok(Event::Eof) => break,
1132                Err(e) => return Err(e.into()),
1133                _ => {}
1134            }
1135            buf.clear();
1136        }
1137
1138        let final_slot = if rpr.change.is_some() {
1139            RPR_CHANGE_SLOT
1140        } else {
1141            RPR_END_SLOT
1142        };
1143        flush_rpr_raw(&mut rpr, &mut pending_raw, final_slot, 0);
1144        remove_redundant_modeled_toggle_candidate(
1145            &mut rpr.revision_xml,
1146            &mut rpr.revision_xml_positions,
1147            RPR_RTL_SLOT,
1148            rtl_carrier_required,
1149        );
1150
1151        Ok(rpr)
1152    }
1153
1154    pub fn to_xml<W: std::io::Write>(&self, writer: &mut Writer<W>) -> Result<()> {
1155        self.to_xml_with_word_override(writer, None)
1156    }
1157
1158    pub(crate) fn to_xml_with_word_override<W: std::io::Write>(
1159        &self,
1160        writer: &mut Writer<W>,
1161        foreign_word_namespace: Option<&str>,
1162    ) -> Result<()> {
1163        if self.is_empty() {
1164            return Ok(());
1165        }
1166
1167        if !self.revision_xml.is_empty()
1168            && self.revision_xml_positions.len() == self.revision_xml.len()
1169        {
1170            let mut modeled = self.clone();
1171            modeled.revision_xml.clear();
1172            modeled.revision_xml_positions.clear();
1173            let mut generated = Writer::new(Vec::new());
1174            if modeled.is_empty() {
1175                generated.write_event(Event::Start(BytesStart::new("w:rPr")))?;
1176                generated.write_event(Event::End(BytesEnd::new("w:rPr")))?;
1177            } else {
1178                modeled.to_xml_with_word_override(&mut generated, foreign_word_namespace)?;
1179            }
1180            return write_rpr_with_positioned_raw(
1181                writer,
1182                &generated.into_inner(),
1183                self,
1184                foreign_word_namespace,
1185            );
1186        }
1187
1188        let mut buf = itoa::Buffer::new();
1189        writer.write_event(Event::Start(BytesStart::new("w:rPr")))?;
1190
1191        if let Some(ref style_id) = self.style_id {
1192            let mut e = BytesStart::new("w:rStyle");
1193            e.push_attribute(("w:val", style_id.as_str()));
1194            writer.write_event(Event::Empty(e))?;
1195        }
1196
1197        // rFonts
1198        if self.has_fonts() {
1199            let mut e = BytesStart::new("w:rFonts");
1200            push_retained_attributes(&mut e, &self.font_extra_attributes);
1201            if let Some(ref f) = self.font_hint {
1202                e.push_attribute(("w:hint", f.as_str()));
1203            }
1204            if let Some(ref f) = self.font_ascii {
1205                e.push_attribute(("w:ascii", f.as_str()));
1206            }
1207            if let Some(ref f) = self.font_hansi {
1208                e.push_attribute(("w:hAnsi", f.as_str()));
1209            }
1210            if let Some(ref f) = self.font_east_asia {
1211                e.push_attribute(("w:eastAsia", f.as_str()));
1212            }
1213            if let Some(ref f) = self.font_cs {
1214                e.push_attribute(("w:cs", f.as_str()));
1215            }
1216            if let Some(ref f) = self.font_ascii_theme {
1217                e.push_attribute(("w:asciiTheme", f.as_str()));
1218            }
1219            if let Some(ref f) = self.font_hansi_theme {
1220                e.push_attribute(("w:hAnsiTheme", f.as_str()));
1221            }
1222            if let Some(ref f) = self.font_east_asia_theme {
1223                e.push_attribute(("w:eastAsiaTheme", f.as_str()));
1224            }
1225            if let Some(ref f) = self.font_cs_theme {
1226                e.push_attribute(("w:cstheme", f.as_str()));
1227            }
1228            writer.write_event(Event::Empty(e))?;
1229        }
1230
1231        if let Some(bold) = self.bold {
1232            write_toggle(writer, "w:b", bold)?;
1233        }
1234        if let Some(bold_cs) = self.bold_cs {
1235            write_toggle(writer, "w:bCs", bold_cs)?;
1236        }
1237        if let Some(italic) = self.italic {
1238            write_toggle(writer, "w:i", italic)?;
1239        }
1240        if let Some(italic_cs) = self.italic_cs {
1241            write_toggle(writer, "w:iCs", italic_cs)?;
1242        }
1243        if let Some(caps) = self.caps {
1244            write_toggle(writer, "w:caps", caps)?;
1245        }
1246        if let Some(small_caps) = self.small_caps {
1247            write_toggle(writer, "w:smallCaps", small_caps)?;
1248        }
1249        if let Some(strike) = self.strike {
1250            write_toggle(writer, "w:strike", strike)?;
1251        }
1252        if let Some(dstrike) = self.dstrike {
1253            write_toggle(writer, "w:dstrike", dstrike)?;
1254        }
1255        if let Some(outline) = self.outline {
1256            write_toggle(writer, "w:outline", outline)?;
1257        }
1258        if let Some(shadow) = self.shadow {
1259            write_toggle(writer, "w:shadow", shadow)?;
1260        }
1261        if let Some(emboss) = self.emboss {
1262            write_toggle(writer, "w:emboss", emboss)?;
1263        }
1264        if let Some(imprint) = self.imprint {
1265            write_toggle(writer, "w:imprint", imprint)?;
1266        }
1267        if let Some(no_proof) = self.no_proof {
1268            write_toggle(writer, "w:noProof", no_proof)?;
1269        }
1270        if let Some(snap_to_grid) = self.snap_to_grid {
1271            write_toggle(writer, "w:snapToGrid", snap_to_grid)?;
1272        }
1273        if let Some(vanish) = self.vanish {
1274            write_toggle(writer, "w:vanish", vanish)?;
1275        }
1276        if let Some(web_hidden) = self.web_hidden {
1277            write_toggle(writer, "w:webHidden", web_hidden)?;
1278        }
1279
1280        if self.has_color() {
1281            let mut e = BytesStart::new("w:color");
1282            push_retained_attributes(&mut e, &self.color_extra_attributes);
1283            if let Some(ref color) = self.color {
1284                e.push_attribute(("w:val", color.as_str()));
1285            }
1286            if let Some(ref tc) = self.color_theme {
1287                e.push_attribute(("w:themeColor", tc.as_str()));
1288            }
1289            if let Some(tint) = self.color_theme_tint {
1290                push_uchar_hex(&mut e, "w:themeTint", tint);
1291            }
1292            if let Some(shade) = self.color_theme_shade {
1293                push_uchar_hex(&mut e, "w:themeShade", shade);
1294            }
1295            writer.write_event(Event::Empty(e))?;
1296        }
1297
1298        if let Some(ref spacing) = self.spacing {
1299            let mut e = BytesStart::new("w:spacing");
1300            e.push_attribute(("w:val", buf.format(spacing.0)));
1301            writer.write_event(Event::Empty(e))?;
1302        }
1303        if let Some(ws) = self.width_scale {
1304            let mut e = BytesStart::new("w:w");
1305            e.push_attribute(("w:val", buf.format(ws)));
1306            writer.write_event(Event::Empty(e))?;
1307        }
1308        if let Some(ref kern) = self.kern {
1309            let mut e = BytesStart::new("w:kern");
1310            e.push_attribute(("w:val", buf.format(kern.0)));
1311            writer.write_event(Event::Empty(e))?;
1312        }
1313        if let Some(pos) = self.position {
1314            let mut e = BytesStart::new("w:position");
1315            e.push_attribute(("w:val", buf.format(pos)));
1316            writer.write_event(Event::Empty(e))?;
1317        }
1318
1319        if let Some(ref sz) = self.sz {
1320            let mut e = BytesStart::new("w:sz");
1321            e.push_attribute(("w:val", buf.format(sz.0)));
1322            writer.write_event(Event::Empty(e))?;
1323        }
1324        if let Some(ref sz_cs) = self.sz_cs {
1325            let mut e = BytesStart::new("w:szCs");
1326            e.push_attribute(("w:val", buf.format(sz_cs.0)));
1327            writer.write_event(Event::Empty(e))?;
1328        }
1329
1330        if let Some(ref highlight) = self.highlight {
1331            let mut e = BytesStart::new("w:highlight");
1332            e.push_attribute(("w:val", highlight.to_str()));
1333            writer.write_event(Event::Empty(e))?;
1334        }
1335
1336        if let Some(underline) = self.underline {
1337            let mut e = BytesStart::new("w:u");
1338            e.push_attribute(("w:val", underline.to_str()));
1339            writer.write_event(Event::Empty(e))?;
1340        }
1341
1342        if let Some(ref effect) = self.effect {
1343            let mut e = BytesStart::new("w:effect");
1344            e.push_attribute(("w:val", effect.as_str()));
1345            writer.write_event(Event::Empty(e))?;
1346        }
1347        if let Some(ref border) = self.border {
1348            border.to_xml(writer, "w:bdr")?;
1349        }
1350
1351        if let Some(ref shd) = self.shading {
1352            shd.write_xml(writer, "w:shd")?;
1353        }
1354
1355        if let Some(ref fit_text) = self.fit_text {
1356            fit_text.write_xml(writer)?;
1357        }
1358
1359        if let Some(ref vert_align) = self.vert_align {
1360            let mut e = BytesStart::new("w:vertAlign");
1361            e.push_attribute(("w:val", vert_align.as_str()));
1362            writer.write_event(Event::Empty(e))?;
1363        }
1364
1365        if let Some(rtl) = self.rtl {
1366            write_toggle(writer, "w:rtl", rtl)?;
1367        }
1368        if let Some(complex_script) = self.complex_script {
1369            write_toggle(writer, "w:cs", complex_script)?;
1370        }
1371        if let Some(ref emphasis_mark) = self.emphasis_mark {
1372            let mut e = BytesStart::new("w:em");
1373            e.push_attribute(("w:val", emphasis_mark.as_str()));
1374            writer.write_event(Event::Empty(e))?;
1375        }
1376
1377        if self.language.is_some()
1378            || self.language_east_asia.is_some()
1379            || self.language_bidi.is_some()
1380            || !self.language_extra_attributes.is_empty()
1381        {
1382            let mut e = BytesStart::new("w:lang");
1383            if let Some(language) = &self.language {
1384                e.push_attribute(("w:val", language.as_str()));
1385            }
1386            if let Some(language) = &self.language_east_asia {
1387                e.push_attribute(("w:eastAsia", language.as_str()));
1388            }
1389            if let Some(language) = &self.language_bidi {
1390                e.push_attribute(("w:bidi", language.as_str()));
1391            }
1392            for (name, value) in &self.language_extra_attributes {
1393                e.push_attribute((name.as_str(), value.as_str()));
1394            }
1395            writer.write_event(Event::Empty(e))?;
1396        }
1397
1398        if let Some(ref layout) = self.east_asian_layout {
1399            layout.write_xml(writer)?;
1400        }
1401        if let Some(spec_vanish) = self.spec_vanish {
1402            write_toggle(writer, "w:specVanish", spec_vanish)?;
1403        }
1404        if let Some(office_math) = self.office_math {
1405            write_toggle(writer, "w:oMath", office_math)?;
1406        }
1407
1408        for revision in &self.revision_markers {
1409            revision.write_xml_with_word_override(writer, foreign_word_namespace)?;
1410        }
1411        for raw in &self.revision_xml {
1412            crate::text::write_raw_with_word_override(writer, raw, foreign_word_namespace)?;
1413        }
1414        if let Some(change) = &self.change {
1415            change.write_xml_with_word_override(writer, foreign_word_namespace)?;
1416        }
1417
1418        writer.write_event(Event::End(BytesEnd::new("w:rPr")))?;
1419        Ok(())
1420    }
1421
1422    /// Whether any `w:rFonts` attribute is set.
1423    fn has_fonts(&self) -> bool {
1424        self.font_ascii.is_some()
1425            || self.font_hansi.is_some()
1426            || self.font_east_asia.is_some()
1427            || self.font_cs.is_some()
1428            || self.font_ascii_theme.is_some()
1429            || self.font_hansi_theme.is_some()
1430            || self.font_east_asia_theme.is_some()
1431            || self.font_cs_theme.is_some()
1432            || self.font_hint.is_some()
1433            || !self.font_extra_attributes.is_empty()
1434    }
1435
1436    /// Whether any `w:color` attribute is set.
1437    fn has_color(&self) -> bool {
1438        self.color.is_some()
1439            || self.color_theme.is_some()
1440            || self.color_theme_tint.is_some()
1441            || self.color_theme_shade.is_some()
1442            || !self.color_extra_attributes.is_empty()
1443    }
1444
1445    fn is_empty(&self) -> bool {
1446        self.style_id.is_none()
1447            && self.font_ascii.is_none()
1448            && self.font_hansi.is_none()
1449            && self.font_east_asia.is_none()
1450            && self.font_cs.is_none()
1451            && self.font_ascii_theme.is_none()
1452            && self.font_hansi_theme.is_none()
1453            && self.font_east_asia_theme.is_none()
1454            && self.font_cs_theme.is_none()
1455            && self.font_hint.is_none()
1456            && self.font_extra_attributes.is_empty()
1457            && self.bold.is_none()
1458            && self.bold_cs.is_none()
1459            && self.italic.is_none()
1460            && self.italic_cs.is_none()
1461            && self.underline.is_none()
1462            && self.strike.is_none()
1463            && self.dstrike.is_none()
1464            && self.sz.is_none()
1465            && self.sz_cs.is_none()
1466            && self.color.is_none()
1467            && self.color_theme.is_none()
1468            && self.color_theme_tint.is_none()
1469            && self.color_theme_shade.is_none()
1470            && self.color_extra_attributes.is_empty()
1471            && self.highlight.is_none()
1472            && self.caps.is_none()
1473            && self.small_caps.is_none()
1474            && self.vert_align.is_none()
1475            && self.spacing.is_none()
1476            && self.width_scale.is_none()
1477            && self.position.is_none()
1478            && self.shading.is_none()
1479            && self.vanish.is_none()
1480            && self.outline.is_none()
1481            && self.shadow.is_none()
1482            && self.emboss.is_none()
1483            && self.imprint.is_none()
1484            && self.no_proof.is_none()
1485            && self.snap_to_grid.is_none()
1486            && self.web_hidden.is_none()
1487            && self.kern.is_none()
1488            && self.effect.is_none()
1489            && self.border.is_none()
1490            && self.fit_text.is_none()
1491            && self.complex_script.is_none()
1492            && self.emphasis_mark.is_none()
1493            && self.east_asian_layout.is_none()
1494            && self.spec_vanish.is_none()
1495            && self.office_math.is_none()
1496            && self.rtl.is_none()
1497            && self.language.is_none()
1498            && self.language_east_asia.is_none()
1499            && self.language_bidi.is_none()
1500            && self.language_extra_attributes.is_empty()
1501            && self.revision_markers.is_empty()
1502            && self.change.is_none()
1503            && self.revision_xml.is_empty()
1504    }
1505
1506    /// Merge another CT_RPr into this one (non-None fields override).
1507    /// Used for style inheritance.
1508    pub fn merge_from(&mut self, other: &CT_RPr) {
1509        // Only a recognized semantic solid fill participates in style precedence.
1510        // Opaque extension children are not transported into an effective style.
1511        if let Some((color, alpha)) = other.solid_text_fill() {
1512            let mut retained = Vec::new();
1513            let mut positions = Vec::new();
1514            for (index, raw) in self.revision_xml.iter().enumerate() {
1515                let probe = CT_RPr {
1516                    revision_xml: vec![raw.clone()],
1517                    ..CT_RPr::default()
1518                };
1519                if probe.solid_text_fill().is_none() {
1520                    retained.push(raw.clone());
1521                    positions.push(
1522                        self.revision_xml_positions
1523                            .get(index)
1524                            .copied()
1525                            .unwrap_or((RPR_END_SLOT, 0)),
1526                    );
1527                }
1528            }
1529            let amount = (alpha * 100_000.0).round() as u32;
1530            retained.push(format!("<w14:textFill xmlns:w14=\"http://schemas.microsoft.com/office/word/2010/wordml\"><w14:solidFill><w14:srgbClr w14:val=\"{color}\"><w14:alpha w14:val=\"{amount}\"/></w14:srgbClr></w14:solidFill></w14:textFill>").into_bytes());
1531            positions.push((RPR_END_SLOT, 0));
1532            self.revision_xml = retained;
1533            self.revision_xml_positions = positions;
1534        }
1535        if other.style_id.is_some() {
1536            self.style_id = other.style_id.clone();
1537        }
1538        if other.font_ascii.is_some() {
1539            self.font_ascii = other.font_ascii.clone();
1540        }
1541        if other.font_hansi.is_some() {
1542            self.font_hansi = other.font_hansi.clone();
1543        }
1544        if other.font_east_asia.is_some() {
1545            self.font_east_asia = other.font_east_asia.clone();
1546        }
1547        if other.font_cs.is_some() {
1548            self.font_cs = other.font_cs.clone();
1549        }
1550        if other.font_ascii_theme.is_some() {
1551            self.font_ascii_theme = other.font_ascii_theme.clone();
1552        }
1553        if other.font_hansi_theme.is_some() {
1554            self.font_hansi_theme = other.font_hansi_theme.clone();
1555        }
1556        if other.font_east_asia_theme.is_some() {
1557            self.font_east_asia_theme = other.font_east_asia_theme.clone();
1558        }
1559        if other.font_cs_theme.is_some() {
1560            self.font_cs_theme = other.font_cs_theme.clone();
1561        }
1562        if other.font_hint.is_some() {
1563            self.font_hint = other.font_hint.clone();
1564        }
1565        if !other.font_extra_attributes.is_empty() {
1566            self.font_extra_attributes = other.font_extra_attributes.clone();
1567        }
1568        if other.bold.is_some() {
1569            self.bold = other.bold;
1570        }
1571        if other.bold_cs.is_some() {
1572            self.bold_cs = other.bold_cs;
1573        }
1574        if other.italic.is_some() {
1575            self.italic = other.italic;
1576        }
1577        if other.italic_cs.is_some() {
1578            self.italic_cs = other.italic_cs;
1579        }
1580        if other.underline.is_some() {
1581            self.underline = other.underline;
1582        }
1583        if other.strike.is_some() {
1584            self.strike = other.strike;
1585        }
1586        if other.dstrike.is_some() {
1587            self.dstrike = other.dstrike;
1588        }
1589        if other.sz.is_some() {
1590            self.sz = other.sz;
1591        }
1592        if other.sz_cs.is_some() {
1593            self.sz_cs = other.sz_cs;
1594        }
1595        if other.color.is_some() {
1596            self.color = other.color.clone();
1597        }
1598        if other.color_theme.is_some() {
1599            self.color_theme = other.color_theme.clone();
1600        }
1601        if other.color_theme_tint.is_some() {
1602            self.color_theme_tint = other.color_theme_tint;
1603        }
1604        if other.color_theme_shade.is_some() {
1605            self.color_theme_shade = other.color_theme_shade;
1606        }
1607        if !other.color_extra_attributes.is_empty() {
1608            self.color_extra_attributes = other.color_extra_attributes.clone();
1609        }
1610        if other.highlight.is_some() {
1611            self.highlight = other.highlight;
1612        }
1613        if other.caps.is_some() {
1614            self.caps = other.caps;
1615        }
1616        if other.small_caps.is_some() {
1617            self.small_caps = other.small_caps;
1618        }
1619        if other.vert_align.is_some() {
1620            self.vert_align = other.vert_align.clone();
1621        }
1622        if other.spacing.is_some() {
1623            self.spacing = other.spacing;
1624        }
1625        if other.width_scale.is_some() {
1626            self.width_scale = other.width_scale;
1627        }
1628        if other.position.is_some() {
1629            self.position = other.position;
1630        }
1631        if other.shading.is_some() {
1632            self.shading = other.shading.clone();
1633        }
1634        if other.vanish.is_some() {
1635            self.vanish = other.vanish;
1636        }
1637        if other.outline.is_some() {
1638            self.outline = other.outline;
1639        }
1640        if other.shadow.is_some() {
1641            self.shadow = other.shadow;
1642        }
1643        if other.emboss.is_some() {
1644            self.emboss = other.emboss;
1645        }
1646        if other.imprint.is_some() {
1647            self.imprint = other.imprint;
1648        }
1649        if other.no_proof.is_some() {
1650            self.no_proof = other.no_proof;
1651        }
1652        if other.snap_to_grid.is_some() {
1653            self.snap_to_grid = other.snap_to_grid;
1654        }
1655        if other.web_hidden.is_some() {
1656            self.web_hidden = other.web_hidden;
1657        }
1658        if other.kern.is_some() {
1659            self.kern = other.kern;
1660        }
1661        if other.effect.is_some() {
1662            self.effect = other.effect.clone();
1663        }
1664        if other.border.is_some() {
1665            self.border = other.border.clone();
1666        }
1667        if other.fit_text.is_some() {
1668            self.fit_text = other.fit_text.clone();
1669        }
1670        if other.complex_script.is_some() {
1671            self.complex_script = other.complex_script;
1672        }
1673        if other.emphasis_mark.is_some() {
1674            self.emphasis_mark = other.emphasis_mark.clone();
1675        }
1676        if other.east_asian_layout.is_some() {
1677            self.east_asian_layout = other.east_asian_layout.clone();
1678        }
1679        if other.spec_vanish.is_some() {
1680            self.spec_vanish = other.spec_vanish;
1681        }
1682        if other.office_math.is_some() {
1683            self.office_math = other.office_math;
1684        }
1685        if other.rtl.is_some() {
1686            self.rtl = other.rtl;
1687        }
1688        if other.language.is_some() {
1689            self.language = other.language.clone();
1690        }
1691        if other.language_east_asia.is_some() {
1692            self.language_east_asia = other.language_east_asia.clone();
1693        }
1694        if other.language_bidi.is_some() {
1695            self.language_bidi = other.language_bidi.clone();
1696        }
1697        if !other.language_extra_attributes.is_empty() {
1698            self.language_extra_attributes = other.language_extra_attributes.clone();
1699        }
1700    }
1701}
1702
1703fn parse_language_attributes(
1704    rpr: &mut CT_RPr,
1705    element: &BytesStart<'_>,
1706    prefixes: &[String],
1707) -> Result<()> {
1708    for attribute in element.attributes() {
1709        let attribute = attribute?;
1710        let key = attribute.key.as_ref();
1711        let value = attribute
1712            .decoded_and_normalized_value(XmlVersion::Implicit1_0, element.decoder())?
1713            .into_owned();
1714        if is_word_attribute(key, b"val", prefixes) {
1715            rpr.language = Some(value);
1716        } else if is_word_attribute(key, b"eastAsia", prefixes) {
1717            rpr.language_east_asia = Some(value);
1718        } else if is_word_attribute(key, b"bidi", prefixes) {
1719            rpr.language_bidi = Some(value);
1720        } else {
1721            rpr.language_extra_attributes
1722                .push((std::str::from_utf8(key)?.to_owned(), value));
1723        }
1724    }
1725    Ok(())
1726}
1727
1728pub(crate) fn language_element_is_explicitly_empty(xml: &[u8]) -> Result<bool> {
1729    let mut reader = Reader::from_reader(xml);
1730    let mut buffer = Vec::new();
1731    if !matches!(reader.read_event_into(&mut buffer)?, Event::Start(_)) {
1732        return Ok(false);
1733    }
1734    buffer.clear();
1735    Ok(matches!(
1736        reader.read_event_into(&mut buffer)?,
1737        Event::End(_)
1738    ))
1739}
1740
1741fn write_rpr_with_positioned_raw<W: std::io::Write>(
1742    writer: &mut Writer<W>,
1743    generated: &[u8],
1744    rpr: &CT_RPr,
1745    foreign_word_namespace: Option<&str>,
1746) -> Result<()> {
1747    let mut raw_order = (0..rpr.revision_xml.len())
1748        .filter(|index| {
1749            replay_modeled_toggle_raw(
1750                rpr.revision_xml_positions[*index],
1751                rpr.revision_xml_positions[*index].0 != RPR_RTL_SLOT || rpr.rtl.is_some(),
1752            )
1753        })
1754        .collect::<Vec<_>>();
1755    raw_order.sort_by_key(|index| {
1756        let (slot, occurrence) = effective_rpr_raw_position(rpr, *index);
1757        (slot, occurrence, *index)
1758    });
1759    let mut raw_index = 0usize;
1760    let mut occurrences = [0usize; RPR_END_SLOT as usize + 1];
1761    let mut reader = Reader::from_reader(generated);
1762    reader.config_mut().trim_text(false);
1763    let mut buffer = Vec::new();
1764    let mut inside = false;
1765    loop {
1766        match reader.read_event_into(&mut buffer)? {
1767            Event::Start(element) if !inside => {
1768                inside = true;
1769                writer.write_event(Event::Start(element.into_owned()))?;
1770            }
1771            Event::Start(element) => {
1772                let slot = rpr_slot_for_name(element.local_name().as_ref());
1773                write_rpr_raw_before(
1774                    writer,
1775                    rpr,
1776                    &raw_order,
1777                    &mut raw_index,
1778                    slot,
1779                    occurrences[slot as usize],
1780                    foreign_word_namespace,
1781                )?;
1782                let raw = capture_element(&mut reader, &element)?;
1783                writer.get_mut().write_all(&raw)?;
1784                occurrences[slot as usize] += 1;
1785            }
1786            Event::Empty(mut element) => {
1787                let slot = rpr_slot_for_name(element.local_name().as_ref());
1788                write_rpr_raw_before(
1789                    writer,
1790                    rpr,
1791                    &raw_order,
1792                    &mut raw_index,
1793                    slot,
1794                    occurrences[slot as usize],
1795                    foreign_word_namespace,
1796                )?;
1797                append_modeled_toggle_attributes(
1798                    &mut element,
1799                    &rpr.revision_xml,
1800                    &rpr.revision_xml_positions,
1801                    slot,
1802                    occurrences[slot as usize],
1803                )?;
1804                writer.write_event(Event::Empty(element.into_owned()))?;
1805                occurrences[slot as usize] += 1;
1806            }
1807            Event::End(element) if inside => {
1808                write_rpr_raw_before(
1809                    writer,
1810                    rpr,
1811                    &raw_order,
1812                    &mut raw_index,
1813                    RPR_END_SLOT,
1814                    0,
1815                    foreign_word_namespace,
1816                )?;
1817                while let Some(index) = raw_order.get(raw_index).copied() {
1818                    crate::text::write_raw_with_word_override(
1819                        writer,
1820                        &rpr.revision_xml[index],
1821                        foreign_word_namespace,
1822                    )?;
1823                    raw_index += 1;
1824                }
1825                writer.write_event(Event::End(element.into_owned()))?;
1826                return Ok(());
1827            }
1828            Event::Eof => return Ok(()),
1829            event => writer.write_event(event.into_owned())?,
1830        }
1831        buffer.clear();
1832    }
1833}
1834
1835fn write_rpr_raw_before<W: std::io::Write>(
1836    writer: &mut Writer<W>,
1837    rpr: &CT_RPr,
1838    raw_order: &[usize],
1839    raw_index: &mut usize,
1840    slot: u8,
1841    occurrence: usize,
1842    foreign_word_namespace: Option<&str>,
1843) -> Result<()> {
1844    while let Some(index) = raw_order.get(*raw_index).copied() {
1845        let position = effective_rpr_raw_position(rpr, index);
1846        if position.0 > slot || (position.0 == slot && position.1 > occurrence) {
1847            break;
1848        }
1849        crate::text::write_raw_with_word_override(
1850            writer,
1851            &rpr.revision_xml[index],
1852            foreign_word_namespace,
1853        )?;
1854        *raw_index += 1;
1855    }
1856    Ok(())
1857}
1858
1859fn effective_rpr_raw_position(rpr: &CT_RPr, index: usize) -> (u8, usize) {
1860    let mut position = rpr.revision_xml_positions[index];
1861    position.1 = raw_occurrence(position);
1862    if position.0 == RPR_RTL_SLOT
1863        && let Some((carrier_index, carrier_occurrence)) = rpr
1864            .revision_xml_positions
1865            .iter()
1866            .enumerate()
1867            .find(|candidate| {
1868                candidate.1.0 == RPR_RTL_SLOT && raw_is_modeled_attribute_carrier(*candidate.1)
1869            })
1870            .map(|(carrier_index, candidate)| (carrier_index, raw_occurrence(*candidate)))
1871    {
1872        position.1 = usize::from(
1873            position.1 > carrier_occurrence
1874                || (position.1 == carrier_occurrence && index > carrier_index),
1875        );
1876    }
1877    if rpr.change.is_some() && position.0 >= RPR_CHANGE_SLOT {
1878        (RPR_CHANGE_SLOT, 0)
1879    } else {
1880        position
1881    }
1882}
1883
1884fn rpr_slot_for_name(local: &[u8]) -> u8 {
1885    match local {
1886        b"rStyle" => RPR_STYLE_SLOT,
1887        b"rFonts" => RPR_FONTS_SLOT,
1888        b"b" => RPR_BOLD_SLOT,
1889        b"bCs" => RPR_BOLD_CS_SLOT,
1890        b"i" => RPR_ITALIC_SLOT,
1891        b"iCs" => RPR_ITALIC_CS_SLOT,
1892        b"caps" => RPR_CAPS_SLOT,
1893        b"smallCaps" => RPR_SMALL_CAPS_SLOT,
1894        b"strike" => RPR_STRIKE_SLOT,
1895        b"dstrike" => RPR_DSTRIKE_SLOT,
1896        b"outline" => 10,
1897        b"shadow" => 11,
1898        b"emboss" => 12,
1899        b"imprint" => 13,
1900        b"noProof" => 14,
1901        b"snapToGrid" => 15,
1902        b"vanish" => RPR_VANISH_SLOT,
1903        b"webHidden" => 17,
1904        b"color" => RPR_COLOR_SLOT,
1905        b"spacing" => RPR_SPACING_SLOT,
1906        b"w" => RPR_WIDTH_SLOT,
1907        b"kern" => 21,
1908        b"position" => RPR_POSITION_SLOT,
1909        b"sz" => RPR_SIZE_SLOT,
1910        b"szCs" => RPR_SIZE_CS_SLOT,
1911        b"highlight" => RPR_HIGHLIGHT_SLOT,
1912        b"u" => RPR_UNDERLINE_SLOT,
1913        b"effect" => 27,
1914        b"bdr" => 28,
1915        b"shd" => RPR_SHADING_SLOT,
1916        b"fitText" => 30,
1917        b"vertAlign" => RPR_VERT_ALIGN_SLOT,
1918        b"rtl" => 32,
1919        b"cs" => 33,
1920        b"em" => 34,
1921        b"lang" => RPR_LANG_SLOT,
1922        b"eastAsianLayout" => 36,
1923        b"specVanish" => 37,
1924        b"oMath" => 38,
1925        b"ins" | b"del" => RPR_MARKER_SLOT,
1926        b"rPrChange" => RPR_CHANGE_SLOT,
1927        _ => RPR_END_SLOT,
1928    }
1929}
1930
1931/// Validate modeled run-property leaves on checked replacement only.
1932pub(crate) fn validate_checked_property_child(
1933    element: &BytesStart<'_>,
1934    prefixes: &[String],
1935) -> Result<()> {
1936    let local = element.local_name();
1937    let name = local.as_ref();
1938    let value = get_word_val_attr(element, prefixes)?;
1939    if matches!(
1940        name,
1941        b"b" | b"bCs"
1942            | b"i"
1943            | b"iCs"
1944            | b"caps"
1945            | b"smallCaps"
1946            | b"strike"
1947            | b"dstrike"
1948            | b"outline"
1949            | b"shadow"
1950            | b"emboss"
1951            | b"imprint"
1952            | b"noProof"
1953            | b"snapToGrid"
1954            | b"vanish"
1955            | b"webHidden"
1956            | b"rtl"
1957            | b"cs"
1958            | b"specVanish"
1959            | b"oMath"
1960    ) {
1961        if value
1962            .as_deref()
1963            .is_some_and(|value| !matches!(value, "true" | "false" | "on" | "off" | "1" | "0"))
1964        {
1965            return Err(crate::error::OxmlError::InvalidValue(
1966                "element XML run toggle has an invalid Word boolean".into(),
1967            ));
1968        }
1969    } else if name == b"u" {
1970        if value
1971            .as_deref()
1972            .is_some_and(|value| ST_Underline::from_str(value).is_err())
1973        {
1974            return Err(crate::error::OxmlError::InvalidValue(
1975                "element XML underline has an invalid w:val".into(),
1976            ));
1977        }
1978    } else if matches!(
1979        name,
1980        b"sz"
1981            | b"szCs"
1982            | b"kern"
1983            | b"spacing"
1984            | b"position"
1985            | b"w"
1986            | b"rStyle"
1987            | b"highlight"
1988            | b"vertAlign"
1989            | b"color"
1990    ) {
1991        let value = value.ok_or_else(|| {
1992            crate::error::OxmlError::InvalidValue(format!(
1993                "element XML {} requires w:val",
1994                String::from_utf8_lossy(name)
1995            ))
1996        })?;
1997        let valid = match name {
1998            b"sz" | b"szCs" | b"kern" | b"w" => {
1999                parse_integer_measurement::<i32>(element.name().as_ref(), b"w:val", &value)
2000                    .is_ok_and(|value| value >= 0)
2001            }
2002            b"spacing" | b"position" => {
2003                parse_integer_measurement::<i32>(element.name().as_ref(), b"w:val", &value).is_ok()
2004            }
2005            b"highlight" => ST_HighlightColor::from_str(&value).is_ok(),
2006            b"vertAlign" => matches!(value.as_str(), "baseline" | "superscript" | "subscript"),
2007            b"color" => {
2008                value == "auto"
2009                    || (value.len() == 6 && value.bytes().all(|byte| byte.is_ascii_hexdigit()))
2010            }
2011            _ => !value.is_empty(),
2012        };
2013        if !valid {
2014            return Err(crate::error::OxmlError::InvalidValue(format!(
2015                "element XML {} has an invalid w:val",
2016                String::from_utf8_lossy(name)
2017            )));
2018        }
2019    }
2020    Ok(())
2021}
2022
2023pub(crate) fn rpr_schema_slot(name: &[u8], word_prefixes: &[String]) -> Option<u8> {
2024    let local = name.rsplit(|byte| *byte == b':').next().unwrap_or(name);
2025    let slot = rpr_slot_for_name(local);
2026    (slot != RPR_END_SLOT && is_word_element(name, local, word_prefixes)).then_some(slot)
2027}
2028
2029#[cfg(test)]
2030mod tests {
2031    use super::*;
2032    use crate::namespace::W_NS;
2033
2034    fn parse_rpr(xml: &str) -> CT_RPr {
2035        let full = format!("<w:rPr>{xml}</w:rPr>");
2036        let mut reader = Reader::from_str(&full);
2037        reader.config_mut().trim_text(true);
2038        let mut buf = Vec::new();
2039        loop {
2040            match reader.read_event_into(&mut buf) {
2041                Ok(Event::Start(ref e)) if matches_local_name(e.name().as_ref(), b"rPr") => break,
2042                _ => {}
2043            }
2044            buf.clear();
2045        }
2046        CT_RPr::from_xml(&mut reader).unwrap()
2047    }
2048
2049    #[test]
2050    fn parse_basic_rpr() {
2051        let rpr = parse_rpr(r#"<w:b/><w:i/><w:sz w:val="24"/><w:color w:val="FF0000"/>"#);
2052        assert_eq!(rpr.bold, Some(true));
2053        assert_eq!(rpr.italic, Some(true));
2054        assert_eq!(rpr.sz, Some(HalfPoint(24)));
2055        assert_eq!(rpr.color, Some("FF0000".to_string()));
2056    }
2057
2058    #[test]
2059    fn parse_rpr_spacing() {
2060        let rpr = parse_rpr(r#"<w:spacing w:val="20"/><w:w w:val="150"/><w:position w:val="-4"/>"#);
2061        assert_eq!(rpr.spacing, Some(Twips(20)));
2062        assert_eq!(rpr.width_scale, Some(150));
2063        assert_eq!(rpr.position, Some(-4));
2064    }
2065
2066    #[test]
2067    fn round_trip_rpr() {
2068        let original = CT_RPr {
2069            bold: Some(true),
2070            italic: Some(true),
2071            sz: Some(HalfPoint(24)),
2072            color: Some("FF0000".to_string()),
2073            underline: Some(ST_Underline::Single),
2074            spacing: Some(Twips(20)),
2075            ..Default::default()
2076        };
2077
2078        let mut output = Vec::new();
2079        let mut writer = Writer::new(&mut output);
2080        original.to_xml(&mut writer).unwrap();
2081        let xml = String::from_utf8(output).unwrap();
2082
2083        let inner = xml
2084            .strip_prefix("<w:rPr>")
2085            .unwrap()
2086            .strip_suffix("</w:rPr>")
2087            .unwrap();
2088        let parsed = parse_rpr(inner);
2089        assert_eq!(parsed.bold, original.bold);
2090        assert_eq!(parsed.italic, original.italic);
2091        assert_eq!(parsed.sz, original.sz);
2092        assert_eq!(parsed.color, original.color);
2093        assert_eq!(parsed.underline, original.underline);
2094        assert_eq!(parsed.spacing, original.spacing);
2095    }
2096
2097    #[test]
2098    fn merge_rpr() {
2099        let mut base = CT_RPr {
2100            bold: Some(true),
2101            sz: Some(HalfPoint(24)),
2102            ..Default::default()
2103        };
2104        let override_rpr = CT_RPr {
2105            sz: Some(HalfPoint(28)),
2106            italic: Some(true),
2107            ..Default::default()
2108        };
2109        base.merge_from(&override_rpr);
2110        assert_eq!(base.bold, Some(true)); // kept
2111        assert_eq!(base.sz, Some(HalfPoint(28))); // overridden
2112        assert_eq!(base.italic, Some(true)); // added
2113    }
2114
2115    #[test]
2116    fn run_language_parses_aliases_and_rejects_foreign_same_local_attributes() {
2117        let rpr = parse_rpr(&format!(
2118            r#"<q:lang xmlns:q="{}" xmlns:x="urn:foreign" q:val="en-US" q:eastAsia="zh-CN" q:bidi="ar-SA" x:val="ignored" x:kept="raw"/>"#,
2119            W_NS
2120        ));
2121        assert_eq!(rpr.language.as_deref(), Some("en-US"));
2122        assert_eq!(rpr.language_east_asia.as_deref(), Some("zh-CN"));
2123        assert_eq!(rpr.language_bidi.as_deref(), Some("ar-SA"));
2124
2125        let mut output = Vec::new();
2126        rpr.to_xml(&mut Writer::new(&mut output)).unwrap();
2127        let output = String::from_utf8(output).unwrap();
2128        assert!(output.contains(r#"<w:lang w:val="en-US" w:eastAsia="zh-CN" w:bidi="ar-SA""#));
2129        assert!(output.contains(r#"x:kept="raw""#));
2130        assert!(!output.contains(r#"w:val="ignored""#));
2131
2132        let explicit = parse_rpr(&format!(
2133            r#"<q:lang xmlns:q="{}" q:val="en-GB"></q:lang>"#,
2134            W_NS
2135        ));
2136        assert_eq!(explicit.language.as_deref(), Some("en-GB"));
2137    }
2138
2139    #[test]
2140    fn run_language_round_trips_at_its_schema_slot_and_cascades() {
2141        let mut base = CT_RPr {
2142            language: Some("fr-FR".to_owned()),
2143            language_east_asia: Some("ja-JP".to_owned()),
2144            ..Default::default()
2145        };
2146        base.merge_from(&CT_RPr {
2147            language: Some("de-DE".to_owned()),
2148            language_bidi: Some("ar-SA".to_owned()),
2149            ..Default::default()
2150        });
2151        assert_eq!(base.language.as_deref(), Some("de-DE"));
2152        assert_eq!(base.language_east_asia.as_deref(), Some("ja-JP"));
2153        assert_eq!(base.language_bidi.as_deref(), Some("ar-SA"));
2154
2155        let mut output = Vec::new();
2156        base.to_xml(&mut Writer::new(&mut output)).unwrap();
2157        let output = String::from_utf8(output).unwrap();
2158        assert!(output.find("w:vertAlign").is_none());
2159        assert!(output.find("w:lang").unwrap() < output.rfind("</w:rPr>").unwrap());
2160    }
2161
2162    /// Every `EG_RPrBase` child F-265 typed, in schema order, with one
2163    /// unmodelled producer sibling between two of them.
2164    const F265_EVERY_NEW_CHILD: &str = concat!(
2165        r#"<w:outline/><w:shadow/><w:emboss/><w:imprint/><w:noProof/><w:snapToGrid/>"#,
2166        r#"<w:webHidden/><w:kern w:val="16"/><w:effect w:val="antsRed"/>"#,
2167        r#"<w:bdr w:val="single" w:sz="4" w:space="1" w:color="FF0000"/>"#,
2168        r#"<w:fitText w:val="1440" w:id="3"/><w:cs/><w:em w:val="dot"/>"#,
2169        r#"<w:eastAsianLayout w:id="7" w:combine="1" w:combineBrackets="round" w:vert="1" w:vertCompress="1"/>"#,
2170        r#"<w:specVanish/><w:oMath/>"#,
2171    );
2172
2173    #[test]
2174    fn every_new_run_property_parses_writes_and_merges() {
2175        let rpr = parse_rpr(F265_EVERY_NEW_CHILD);
2176        assert_eq!(rpr.outline, Some(true));
2177        assert_eq!(rpr.shadow, Some(true));
2178        assert_eq!(rpr.emboss, Some(true));
2179        assert_eq!(rpr.imprint, Some(true));
2180        assert_eq!(rpr.no_proof, Some(true));
2181        assert_eq!(rpr.snap_to_grid, Some(true));
2182        assert_eq!(rpr.web_hidden, Some(true));
2183        assert_eq!(rpr.kern, Some(HalfPoint(16)));
2184        assert_eq!(rpr.effect, Some(ST_TextEffect::AntsRed));
2185        assert_eq!(rpr.border.as_ref().map(|border| border.sz), Some(Some(4)));
2186        assert_eq!(
2187            rpr.fit_text.as_deref(),
2188            Some(&CT_FitText {
2189                val: Twips(1440),
2190                id: Some(3),
2191                extra_attributes: Vec::new(),
2192            })
2193        );
2194        assert_eq!(rpr.complex_script, Some(true));
2195        assert_eq!(rpr.emphasis_mark, Some(ST_Em::Dot));
2196        assert_eq!(
2197            rpr.east_asian_layout.as_deref(),
2198            Some(&CT_EastAsianLayout {
2199                id: Some(7),
2200                combine: Some(true),
2201                combine_brackets: Some("round".to_owned()),
2202                vert: Some(true),
2203                vert_compress: Some(true),
2204                extra_attributes: Vec::new(),
2205            })
2206        );
2207        assert_eq!(rpr.spec_vanish, Some(true));
2208        assert_eq!(rpr.office_math, Some(true));
2209
2210        // A run property set carrying only one new child is not empty, and
2211        // every new child survives a write and a re-read in schema order.
2212        for single in [
2213            "<w:outline/>",
2214            "<w:webHidden/>",
2215            r#"<w:kern w:val="18"/>"#,
2216            r#"<w:effect w:val="shimmer"/>"#,
2217            r#"<w:bdr w:val="double"/>"#,
2218            r#"<w:fitText w:val="720"/>"#,
2219            "<w:cs/>",
2220            r#"<w:em w:val="circle"/>"#,
2221            r#"<w:eastAsianLayout w:id="1"/>"#,
2222            "<w:specVanish/>",
2223            "<w:oMath/>",
2224        ] {
2225            let single = parse_rpr(single);
2226            assert!(!single.is_empty(), "{single:?}");
2227            let mut output = Vec::new();
2228            single.to_xml(&mut Writer::new(&mut output)).unwrap();
2229            assert!(!output.is_empty());
2230        }
2231
2232        let mut merged = CT_RPr::default();
2233        merged.merge_from(&rpr);
2234        assert_eq!(merged, rpr);
2235    }
2236
2237    #[test]
2238    fn new_run_properties_write_at_their_schema_ordinals() {
2239        let rpr = parse_rpr(F265_EVERY_NEW_CHILD);
2240        let mut output = Vec::new();
2241        rpr.to_xml(&mut Writer::new(&mut output)).unwrap();
2242        let output = String::from_utf8(output).unwrap();
2243        let ordered = [
2244            "<w:outline/>",
2245            "<w:shadow/>",
2246            "<w:emboss/>",
2247            "<w:imprint/>",
2248            "<w:noProof/>",
2249            "<w:snapToGrid/>",
2250            "<w:webHidden/>",
2251            "<w:kern ",
2252            "<w:effect ",
2253            "<w:bdr ",
2254            "<w:fitText ",
2255            "<w:cs/>",
2256            "<w:em ",
2257            "<w:eastAsianLayout ",
2258            "<w:specVanish/>",
2259            "<w:oMath/>",
2260        ]
2261        .map(|needle| {
2262            output
2263                .find(needle)
2264                .unwrap_or_else(|| panic!("{needle} in {output}"))
2265        });
2266        assert!(ordered.windows(2).all(|pair| pair[0] < pair[1]), "{output}");
2267
2268        let reparsed = parse_rpr(
2269            output
2270                .strip_prefix("<w:rPr>")
2271                .unwrap()
2272                .strip_suffix("</w:rPr>")
2273                .unwrap(),
2274        );
2275        assert_eq!(reparsed, rpr);
2276    }
2277
2278    #[test]
2279    fn a_new_element_missing_its_required_value_stays_raw() {
2280        for source in ["<w:kern/>", r#"<w:fitText w:id="4"/>"#] {
2281            let rpr = parse_rpr(source);
2282            assert_eq!(rpr.kern, None, "{source}");
2283            assert_eq!(rpr.fit_text, None, "{source}");
2284
2285            let mut output = Vec::new();
2286            rpr.to_xml(&mut Writer::new(&mut output)).unwrap();
2287            let output = String::from_utf8(output).unwrap();
2288            assert!(output.contains(source), "{source} missing from {output}");
2289        }
2290    }
2291
2292    #[test]
2293    fn new_run_properties_read_under_an_aliased_word_prefix() {
2294        let rpr = parse_rpr(&format!(
2295            concat!(
2296                r#"<q:outline xmlns:q="{ns}"/><q:kern xmlns:q="{ns}" q:val="20"/>"#,
2297                r#"<q:effect xmlns:q="{ns}" q:val="lights"/>"#,
2298                r#"<q:fitText xmlns:q="{ns}" q:val="960" q:id="4"/>"#,
2299                r#"<q:em xmlns:q="{ns}" q:val="comma"/>"#,
2300                r#"<q:eastAsianLayout xmlns:q="{ns}" q:vert="0"/>"#,
2301            ),
2302            ns = W_NS
2303        ));
2304        assert_eq!(rpr.outline, Some(true));
2305        assert_eq!(rpr.kern, Some(HalfPoint(20)));
2306        assert_eq!(rpr.effect, Some(ST_TextEffect::Lights));
2307        assert_eq!(rpr.fit_text.as_ref().map(|fit| fit.val), Some(Twips(960)));
2308        assert_eq!(rpr.emphasis_mark, Some(ST_Em::Comma));
2309        assert_eq!(
2310            rpr.east_asian_layout
2311                .as_ref()
2312                .and_then(|layout| layout.vert),
2313            Some(false)
2314        );
2315
2316        let mut output = Vec::new();
2317        rpr.to_xml(&mut Writer::new(&mut output)).unwrap();
2318        let output = String::from_utf8(output).unwrap();
2319        assert!(output.contains("<w:outline/>"), "{output}");
2320        assert!(output.contains(r#"<w:kern w:val="20"/>"#), "{output}");
2321        assert!(output.contains(r#"<w:effect w:val="lights"/>"#), "{output}");
2322    }
2323
2324    #[test]
2325    fn producer_tokens_and_foreign_attributes_survive_the_new_elements() {
2326        let rpr = parse_rpr(concat!(
2327            r#"<w:rFonts xmlns:x="urn:producer" w:ascii="Arial" w:eastAsiaTheme="minorEastAsia" w:cstheme="minorBidi" w:hint="eastAsia" x:kept="rfonts"/>"#,
2328            r#"<w:color xmlns:x="urn:producer" w:val="4472C4" w:themeColor="accent1" w:themeTint="66" w:themeShade="zz" x:kept="color"/>"#,
2329            r#"<w:effect w:val="producerEffect"/><w:em w:val="producerMark"/>"#,
2330            r#"<w:fitText xmlns:x="urn:producer" w:val="600" x:kept="fit"/>"#,
2331            r#"<w:eastAsianLayout xmlns:x="urn:producer" w:id="2" x:kept="layout"/>"#,
2332        ));
2333        assert_eq!(rpr.font_east_asia_theme.as_deref(), Some("minorEastAsia"));
2334        assert_eq!(rpr.font_cs_theme.as_deref(), Some("minorBidi"));
2335        assert_eq!(rpr.font_hint.as_deref(), Some("eastAsia"));
2336        assert_eq!(rpr.color_theme_tint, Some(0x66));
2337        // "zz" is not two hex digits, so it is retained verbatim rather than
2338        // parsed into the typed slot.
2339        assert_eq!(rpr.color_theme_shade, None);
2340        assert_eq!(
2341            rpr.effect,
2342            Some(ST_TextEffect::Other("producerEffect".to_owned()))
2343        );
2344        assert_eq!(
2345            rpr.emphasis_mark,
2346            Some(ST_Em::Other("producerMark".to_owned()))
2347        );
2348
2349        let mut output = Vec::new();
2350        rpr.to_xml(&mut Writer::new(&mut output)).unwrap();
2351        let output = String::from_utf8(output).unwrap();
2352        for retained in [
2353            r#"x:kept="rfonts""#,
2354            r#"x:kept="color""#,
2355            r#"x:kept="fit""#,
2356            r#"x:kept="layout""#,
2357            r#"w:themeShade="zz""#,
2358            r#"w:eastAsiaTheme="minorEastAsia""#,
2359            r#"w:cstheme="minorBidi""#,
2360            r#"w:hint="eastAsia""#,
2361            r#"w:themeTint="66""#,
2362            r#"w:val="producerEffect""#,
2363            r#"w:val="producerMark""#,
2364        ] {
2365            assert!(
2366                output.contains(retained),
2367                "{retained} missing from {output}"
2368            );
2369        }
2370    }
2371
2372    #[test]
2373    fn a_theme_only_colour_reaches_the_serialized_element() {
2374        let rpr = parse_rpr(r#"<w:color w:themeColor="accent2" w:themeShade="BF"/>"#);
2375        assert_eq!(rpr.color, None);
2376        assert_eq!(rpr.color_theme.as_deref(), Some("accent2"));
2377        assert_eq!(rpr.color_theme_shade, Some(0xBF));
2378
2379        let mut output = Vec::new();
2380        rpr.to_xml(&mut Writer::new(&mut output)).unwrap();
2381        let output = String::from_utf8(output).unwrap();
2382        assert!(
2383            output.contains(r#"<w:color w:themeColor="accent2" w:themeShade="BF"/>"#),
2384            "{output}"
2385        );
2386    }
2387
2388    #[test]
2389    fn malformed_language_after_the_modeled_occurrence_keeps_its_relative_position() {
2390        let rpr = parse_rpr(r#"<w:lang w:val="en-US"/><w:lang><w:producerExtension/></w:lang>"#);
2391
2392        let mut output = Vec::new();
2393        rpr.to_xml(&mut Writer::new(&mut output)).unwrap();
2394        let output_text = std::str::from_utf8(&output).unwrap();
2395        assert!(
2396            output_text.find(r#"w:val="en-US""#).unwrap()
2397                < output_text.find("w:producerExtension").unwrap()
2398        );
2399
2400        let mut reader = Reader::from_reader(output.as_slice());
2401        let mut buffer = Vec::new();
2402        loop {
2403            match reader.read_event_into(&mut buffer).unwrap() {
2404                Event::Start(element) if element.local_name().as_ref() == b"rPr" => break,
2405                Event::Eof => panic!("serialized run properties have no root"),
2406                _ => {}
2407            }
2408            buffer.clear();
2409        }
2410        let reparsed = CT_RPr::from_xml(&mut reader).unwrap();
2411        let mut repeated = Vec::new();
2412        reparsed.to_xml(&mut Writer::new(&mut repeated)).unwrap();
2413        assert_eq!(repeated, output);
2414    }
2415}