Skip to main content

rdocx_oxml/
styles.rs

1//! Style elements: `CT_Styles`, `CT_Style`, `CT_DocDefaults`.
2
3use quick_xml::events::{BytesDecl, BytesEnd, BytesStart, Event};
4use quick_xml::{Reader, Writer, XmlVersion};
5
6use crate::error::{OxmlError, Result};
7use crate::namespace::{W_NS, matches_local_name};
8use crate::numbering::{parse_scoped_ppr, parse_scoped_rpr, word_prefixes_at};
9use crate::properties::{CT_PPr, CT_RPr, is_word_attribute, is_word_element};
10use crate::raw_xml::{capture_element, capture_empty_element};
11use crate::table::{CT_TblPr, CT_TcPr, CT_TrPr};
12
13/// The type of a style.
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15pub enum StyleType {
16    Paragraph,
17    Character,
18    Table,
19    Numbering,
20}
21
22impl StyleType {
23    pub fn from_str(s: &str) -> Result<Self> {
24        match s {
25            "paragraph" => Ok(StyleType::Paragraph),
26            "character" => Ok(StyleType::Character),
27            "table" => Ok(StyleType::Table),
28            "numbering" => Ok(StyleType::Numbering),
29            _ => Err(OxmlError::InvalidValue(format!("invalid style type: {s}"))),
30        }
31    }
32
33    pub fn to_str(self) -> &'static str {
34        match self {
35            StyleType::Paragraph => "paragraph",
36            StyleType::Character => "character",
37            StyleType::Table => "table",
38            StyleType::Numbering => "numbering",
39        }
40    }
41}
42
43/// `CT_Style` — A single style definition.
44#[derive(Debug, Clone, PartialEq)]
45#[allow(non_snake_case)]
46pub struct CT_Style {
47    pub style_id: String,
48    pub style_type: StyleType,
49    pub name: Option<String>,
50    pub based_on: Option<String>,
51    pub next_style: Option<String>,
52    pub linked_style: Option<String>,
53    pub auto_redefine: Option<bool>,
54    pub hidden: Option<bool>,
55    pub ui_priority: Option<u32>,
56    pub semi_hidden: Option<bool>,
57    pub unhide_when_used: Option<bool>,
58    pub quick_format: Option<bool>,
59    pub locked: Option<bool>,
60    pub is_default: bool,
61    pub ppr: Option<CT_PPr>,
62    pub rpr: Option<CT_RPr>,
63    /// Typed projection of the style's base table properties.
64    pub table_properties: Option<CT_TblPr>,
65    #[doc(hidden)]
66    pub table_properties_original: Option<CT_TblPr>,
67    /// Preserved self-contained bytes for the base table properties.
68    pub table_properties_xml: Option<Vec<u8>>,
69    /// Typed projection of the style's base table row properties.
70    ///
71    /// Boxed, like the other composite members added to this family, so a
72    /// style stays cheap on the stack. Test threads build whole documents by
73    /// value against a 2 MiB ceiling.
74    pub table_row_properties: Option<Box<CT_TrPr>>,
75    /// Typed projection of the style's base table cell properties.
76    ///
77    /// Boxed for the same stack-budget reason as `table_row_properties`.
78    pub table_cell_properties: Option<Box<CT_TcPr>>,
79    /// Preserved conditional table-style regions and typed projections.
80    pub conditional_table_styles: Vec<CT_TblStylePr>,
81    /// Unmodeled root attributes and namespace declarations.
82    #[doc(hidden)]
83    pub extra_attributes: Vec<(String, String)>,
84    /// Preserved explicit forms of modeled scalar children.
85    #[doc(hidden)]
86    pub modeled_xml: Vec<(u8, Vec<u8>)>,
87    /// Preserved style children keyed to their schema-order rank.
88    pub extra_xml: Vec<(u8, Vec<u8>)>,
89}
90
91/// One conditional table-style region, in Word's increasing-priority order.
92///
93/// **The declaration order is the resolution order.** Resolution applies
94/// regions in ascending order and a later region overwrites an earlier one, so
95/// moving a variant changes what Word-authored tables render as. The order is
96/// whole table, vertical bands, horizontal bands, column edges, row edges, then
97/// the four corners.
98#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
99pub enum TableStyleRegion {
100    WholeTable,
101    Band1Vert,
102    Band2Vert,
103    Band1Horz,
104    Band2Horz,
105    FirstCol,
106    LastCol,
107    FirstRow,
108    LastRow,
109    NwCell,
110    NeCell,
111    SwCell,
112    SeCell,
113}
114
115impl TableStyleRegion {
116    /// Every region, in increasing-priority order.
117    pub const ALL: [TableStyleRegion; 13] = [
118        TableStyleRegion::WholeTable,
119        TableStyleRegion::Band1Vert,
120        TableStyleRegion::Band2Vert,
121        TableStyleRegion::Band1Horz,
122        TableStyleRegion::Band2Horz,
123        TableStyleRegion::FirstCol,
124        TableStyleRegion::LastCol,
125        TableStyleRegion::FirstRow,
126        TableStyleRegion::LastRow,
127        TableStyleRegion::NwCell,
128        TableStyleRegion::NeCell,
129        TableStyleRegion::SwCell,
130        TableStyleRegion::SeCell,
131    ];
132
133    /// The `w:type` value naming this region.
134    pub fn to_str(self) -> &'static str {
135        match self {
136            TableStyleRegion::WholeTable => "wholeTable",
137            TableStyleRegion::Band1Vert => "band1Vert",
138            TableStyleRegion::Band2Vert => "band2Vert",
139            TableStyleRegion::Band1Horz => "band1Horz",
140            TableStyleRegion::Band2Horz => "band2Horz",
141            TableStyleRegion::FirstCol => "firstCol",
142            TableStyleRegion::LastCol => "lastCol",
143            TableStyleRegion::FirstRow => "firstRow",
144            TableStyleRegion::LastRow => "lastRow",
145            TableStyleRegion::NwCell => "nwCell",
146            TableStyleRegion::NeCell => "neCell",
147            TableStyleRegion::SwCell => "swCell",
148            TableStyleRegion::SeCell => "seCell",
149        }
150    }
151
152    /// The region a `w:type` value names, or `None` when it names none.
153    ///
154    /// An unrecognised value is never an error. The region round-trips from
155    /// its preserved bytes and takes no part in resolution.
156    pub fn from_str(value: &str) -> Option<Self> {
157        Self::ALL
158            .into_iter()
159            .find(|region| region.to_str() == value)
160    }
161}
162
163/// One conditional table-style region.
164#[derive(Debug, Clone, PartialEq)]
165pub struct CT_TblStylePr {
166    /// The region this layer formats, or `None` when `w:type` names a region
167    /// this workspace does not recognise. An unrecognised region serialises
168    /// back from [`CT_TblStylePr::raw_xml`] unchanged.
169    pub region: Option<TableStyleRegion>,
170    pub paragraph_properties: Option<CT_PPr>,
171    /// Region run properties (`w:rPr`).
172    ///
173    /// Boxed so the five-layer region stays cheap on the stack, following the
174    /// `CT_PPr::borders` precedent.
175    pub run_properties: Option<Box<CT_RPr>>,
176    pub table_properties: Option<CT_TblPr>,
177    /// Region row properties (`w:trPr`). Modeled and round-tripped here. Its
178    /// layout application belongs to F-268a.
179    ///
180    /// Boxed for the same stack-budget reason as `run_properties`.
181    pub row_properties: Option<Box<CT_TrPr>>,
182    pub cell_properties: Option<CT_TcPr>,
183    /// Unmodeled region attributes and namespace declarations.
184    #[doc(hidden)]
185    pub extra_attributes: Vec<(String, String)>,
186    pub raw_xml: Vec<u8>,
187}
188
189#[allow(non_snake_case)]
190impl CT_Style {
191    fn style_property_bytes(
192        &self,
193        rank: u8,
194        local: &[u8],
195        generated: Option<&[u8]>,
196    ) -> Result<Vec<u8>> {
197        let sources = self
198            .modeled_xml
199            .iter()
200            .filter(|(owner_rank, _)| *owner_rank == rank)
201            .map(|(_, raw)| raw)
202            .collect::<Vec<_>>();
203        let mut selected = None;
204        for (index, source) in sources.iter().enumerate() {
205            if defaults_owned_child_name(source, &[])?.as_deref() == Some(local) {
206                selected = Some(index);
207            }
208        }
209        let Some(selected) = selected else {
210            let mut bytes = sources.into_iter().flatten().copied().collect::<Vec<_>>();
211            if let Some(generated) = generated {
212                bytes.extend_from_slice(generated);
213            }
214            return Ok(bytes);
215        };
216        let unchanged = if generated.is_none() {
217            false
218        } else if local == b"rPr" {
219            Some(parse_style_rpr(sources[selected], &[])?) == self.rpr
220        } else {
221            Some(parse_scoped_ppr(sources[selected], &[])?) == self.ppr
222        };
223        let mut bytes = Vec::new();
224        for (index, source) in sources.iter().enumerate() {
225            let owned = defaults_owned_child_name(source, &[])?.as_deref() == Some(local);
226            if !owned {
227                bytes.extend_from_slice(source);
228            } else if let Some(generated) = generated {
229                if unchanged || index != selected {
230                    bytes.extend_from_slice(source);
231                } else {
232                    bytes.extend_from_slice(&merge_defaults_property_root(
233                        source,
234                        generated,
235                        &[],
236                        local,
237                    )?);
238                }
239            }
240        }
241        Ok(bytes)
242    }
243
244    pub fn from_xml(reader: &mut Reader<&[u8]>, attrs: &BytesStart) -> Result<Self> {
245        let prefixes = word_prefixes_at(attrs, &["w".to_string()])?;
246        let namespace_bindings = namespace_bindings_at(attrs, &[])?;
247        Self::from_xml_with_prefixes(reader, attrs, &prefixes, &namespace_bindings)
248    }
249
250    fn from_xml_with_prefixes(
251        reader: &mut Reader<&[u8]>,
252        attrs: &BytesStart,
253        word_prefixes: &[String],
254        namespace_bindings: &[(String, String)],
255    ) -> Result<Self> {
256        reject_conflicting_style_prefixes(attrs)?;
257        reject_conflicting_style_bindings(namespace_bindings)?;
258        let mut style_id = String::new();
259        let mut style_type = StyleType::Paragraph;
260        let mut is_default = false;
261
262        for attr in attrs.attributes() {
263            let attr = attr?;
264            let key = attr.key.as_ref();
265            if is_word_attribute(key, b"styleId", word_prefixes) {
266                style_id = std::str::from_utf8(&attr.value)?.to_string();
267            } else if is_word_attribute(key, b"type", word_prefixes) {
268                style_type = StyleType::from_str(std::str::from_utf8(&attr.value)?)?;
269            } else if is_word_attribute(key, b"default", word_prefixes) {
270                is_default = std::str::from_utf8(&attr.value)? == "1"
271                    || std::str::from_utf8(&attr.value)? == "true";
272            }
273        }
274
275        let mut name = None;
276        let mut based_on = None;
277        let mut next_style = None;
278        let mut linked_style = None;
279        let mut auto_redefine = None;
280        let mut hidden = None;
281        let mut ui_priority = None;
282        let mut semi_hidden = None;
283        let mut unhide_when_used = None;
284        let mut quick_format = None;
285        let mut locked = None;
286        let mut ppr = None;
287        let mut rpr = None;
288        let mut table_properties = None;
289        let mut table_properties_xml = None;
290        let mut table_row_properties = None;
291        let mut table_cell_properties = None;
292        let mut conditional_table_styles = Vec::new();
293        let mut modeled_xml = Vec::new();
294        let mut extra_xml = Vec::new();
295        let extra_attributes = raw_style_attributes(
296            attrs,
297            &[b"styleId", b"type", b"default"],
298            word_prefixes,
299            namespace_bindings,
300        )?;
301        let mut buf = Vec::new();
302
303        loop {
304            match reader.read_event_into(&mut buf) {
305                Ok(Event::Empty(ref e)) => {
306                    let ename = e.name();
307                    let prefixes = word_prefixes_at(e, word_prefixes)?;
308                    let bindings = namespace_bindings_at(e, namespace_bindings)?;
309                    if is_word_element(ename.as_ref(), b"pPr", &prefixes)
310                        || is_word_element(ename.as_ref(), b"rPr", &prefixes)
311                    {
312                        let raw = crate::text::raw_with_external_bindings(
313                            &capture_empty_element(e)?,
314                            &bindings,
315                        )?;
316                        if is_word_element(ename.as_ref(), b"pPr", &prefixes) {
317                            ppr = Some(parse_scoped_ppr(&raw, &prefixes)?);
318                            modeled_xml.push((20, raw));
319                        } else {
320                            rpr = Some(parse_style_rpr(&raw, &prefixes)?);
321                            modeled_xml.push((21, raw));
322                        }
323                    } else if matches!(ename.local_name().as_ref(), b"pPr" | b"rPr") {
324                        let rank = if ename.local_name().as_ref() == b"pPr" {
325                            20
326                        } else {
327                            21
328                        };
329                        modeled_xml.push((
330                            rank,
331                            crate::text::raw_with_external_bindings(
332                                &capture_empty_element(e)?,
333                                &bindings,
334                            )?,
335                        ));
336                    } else if let Some(rank) = style_scalar_rank(ename.as_ref(), &prefixes) {
337                        let string_value = matches!(rank, 0 | 2 | 3 | 4)
338                            .then(|| get_val_attr(e, &prefixes))
339                            .transpose()?
340                            .flatten();
341                        let toggle_value = matches!(rank, 5 | 6 | 8 | 9 | 10 | 11)
342                            .then(|| parse_style_toggle(e, &prefixes))
343                            .transpose()?
344                            .flatten();
345                        let priority_value = (rank == 7)
346                            .then(|| parse_style_priority(e, &prefixes))
347                            .transpose()?
348                            .flatten();
349                        let raw = capture_empty_element(e)?;
350                        if string_value.is_some()
351                            || toggle_value.is_some()
352                            || priority_value.is_some()
353                        {
354                            apply_style_scalar(
355                                rank,
356                                string_value,
357                                toggle_value,
358                                priority_value,
359                                &mut name,
360                                &mut based_on,
361                                &mut next_style,
362                                &mut linked_style,
363                                &mut auto_redefine,
364                                &mut hidden,
365                                &mut ui_priority,
366                                &mut semi_hidden,
367                                &mut unhide_when_used,
368                                &mut quick_format,
369                                &mut locked,
370                            );
371                            if style_scalar_empty_requires_snapshot(e, rank)? {
372                                modeled_xml
373                                    .push((rank, make_style_raw_self_contained(&raw, &bindings)?));
374                            }
375                        } else {
376                            extra_xml.push((rank, make_style_raw_self_contained(&raw, &bindings)?));
377                        }
378                    } else {
379                        extra_xml.push((
380                            style_child_rank(ename.as_ref(), &prefixes),
381                            make_style_raw_self_contained(&capture_empty_element(e)?, &bindings)?,
382                        ));
383                    }
384                }
385                Ok(Event::Start(ref e)) => {
386                    let ename = e.name();
387                    let prefixes = word_prefixes_at(e, word_prefixes)?;
388                    let bindings = namespace_bindings_at(e, namespace_bindings)?;
389                    let scalar_rank = style_scalar_rank(ename.as_ref(), &prefixes);
390                    if let Some(rank) = scalar_rank {
391                        let string_value = matches!(rank, 0 | 2 | 3 | 4)
392                            .then(|| get_val_attr(e, &prefixes))
393                            .transpose()?
394                            .flatten();
395                        let toggle_value = matches!(rank, 5 | 6 | 8 | 9 | 10 | 11)
396                            .then(|| parse_style_toggle(e, &prefixes))
397                            .transpose()?
398                            .flatten();
399                        let priority_value = (rank == 7)
400                            .then(|| parse_style_priority(e, &prefixes))
401                            .transpose()?
402                            .flatten();
403                        let raw = capture_element(reader, e)?;
404                        if explicit_style_element_has_only_trivia(&raw)?
405                            && (string_value.is_some()
406                                || toggle_value.is_some()
407                                || priority_value.is_some())
408                        {
409                            apply_style_scalar(
410                                rank,
411                                string_value,
412                                toggle_value,
413                                priority_value,
414                                &mut name,
415                                &mut based_on,
416                                &mut next_style,
417                                &mut linked_style,
418                                &mut auto_redefine,
419                                &mut hidden,
420                                &mut ui_priority,
421                                &mut semi_hidden,
422                                &mut unhide_when_used,
423                                &mut quick_format,
424                                &mut locked,
425                            );
426                            modeled_xml
427                                .push((rank, make_style_raw_self_contained(&raw, &bindings)?));
428                        } else {
429                            extra_xml.push((rank, make_style_raw_self_contained(&raw, &bindings)?));
430                        }
431                    } else if is_word_element(ename.as_ref(), b"pPr", &prefixes)
432                        || is_word_element(ename.as_ref(), b"rPr", &prefixes)
433                    {
434                        let trim_start = reader.config().trim_text_start;
435                        let trim_end = reader.config().trim_text_end;
436                        reader.config_mut().trim_text(false);
437                        let captured = capture_element(reader, e);
438                        reader.config_mut().trim_text_start = trim_start;
439                        reader.config_mut().trim_text_end = trim_end;
440                        let raw = crate::text::raw_with_external_bindings(&captured?, &bindings)?;
441                        if is_word_element(ename.as_ref(), b"pPr", &prefixes) {
442                            ppr = Some(parse_scoped_ppr(&raw, &prefixes)?);
443                            modeled_xml.push((20, raw));
444                        } else {
445                            rpr = Some(parse_style_rpr(&raw, &prefixes)?);
446                            modeled_xml.push((21, raw));
447                        }
448                    } else if matches!(ename.local_name().as_ref(), b"pPr" | b"rPr") {
449                        let trim_start = reader.config().trim_text_start;
450                        let trim_end = reader.config().trim_text_end;
451                        reader.config_mut().trim_text(false);
452                        let captured = capture_element(reader, e);
453                        reader.config_mut().trim_text_start = trim_start;
454                        reader.config_mut().trim_text_end = trim_end;
455                        let rank = if ename.local_name().as_ref() == b"pPr" {
456                            20
457                        } else {
458                            21
459                        };
460                        modeled_xml.push((
461                            rank,
462                            crate::text::raw_with_external_bindings(&captured?, &bindings)?,
463                        ));
464                    } else if is_word_element(ename.as_ref(), b"tblPr", &prefixes) {
465                        let raw = capture_element(reader, e)?;
466                        let preserved = make_style_raw_self_contained(&raw, &bindings)?;
467                        table_properties =
468                            Some(parse_style_table_properties(&preserved, &prefixes)?);
469                        table_properties_xml = Some(preserved);
470                    } else if is_word_element(ename.as_ref(), b"trPr", &prefixes) {
471                        table_row_properties = Some(Box::new(CT_TrPr::from_xml_with_prefixes(
472                            reader, &prefixes,
473                        )?));
474                    } else if is_word_element(ename.as_ref(), b"tcPr", &prefixes) {
475                        table_cell_properties = Some(Box::new(CT_TcPr::from_xml_with_prefixes(
476                            reader, &prefixes,
477                        )?));
478                    } else if is_word_element(ename.as_ref(), b"tblStylePr", &prefixes) {
479                        reject_conflicting_style_prefixes(e)?;
480                        reject_conflicting_style_bindings(&bindings)?;
481                        let region = get_word_attr(e, b"type", &prefixes)?
482                            .as_deref()
483                            .and_then(TableStyleRegion::from_str);
484                        let raw =
485                            make_style_raw_self_contained(&capture_element(reader, e)?, &bindings)?;
486                        let layers = parse_conditional_style_properties(&raw, &prefixes)?;
487                        conditional_table_styles.push(CT_TblStylePr {
488                            region,
489                            paragraph_properties: layers.paragraph_properties,
490                            run_properties: layers.run_properties,
491                            table_properties: layers.table_properties,
492                            row_properties: layers.row_properties,
493                            cell_properties: layers.cell_properties,
494                            extra_attributes: raw_style_attributes(
495                                e,
496                                &[b"type"],
497                                &prefixes,
498                                &bindings,
499                            )?,
500                            raw_xml: raw,
501                        });
502                    } else {
503                        extra_xml.push((
504                            style_child_rank(ename.as_ref(), &prefixes),
505                            make_style_raw_self_contained(&capture_element(reader, e)?, &bindings)?,
506                        ));
507                    }
508                }
509                Ok(Event::End(ref e)) if matches_local_name(e.name().as_ref(), b"style") => {
510                    break;
511                }
512                Ok(Event::Eof) => break,
513                Err(e) => return Err(e.into()),
514                _ => {}
515            }
516            buf.clear();
517        }
518
519        Ok(CT_Style {
520            style_id,
521            style_type,
522            name,
523            based_on,
524            next_style,
525            linked_style,
526            auto_redefine,
527            hidden,
528            ui_priority,
529            semi_hidden,
530            unhide_when_used,
531            quick_format,
532            locked,
533            is_default,
534            ppr,
535            rpr,
536            table_properties: table_properties.clone(),
537            table_properties_original: table_properties.clone(),
538            table_properties_xml,
539            table_row_properties,
540            table_cell_properties,
541            conditional_table_styles,
542            extra_attributes,
543            modeled_xml,
544            extra_xml,
545        })
546    }
547
548    pub fn to_xml<W: std::io::Write>(&self, writer: &mut Writer<W>) -> Result<()> {
549        let mut e = BytesStart::new("w:style");
550        e.push_attribute(("w:type", self.style_type.to_str()));
551        e.push_attribute(("w:styleId", self.style_id.as_str()));
552        if self.is_default {
553            e.push_attribute(("w:default", "1"));
554        }
555        push_style_attributes(&mut e, &self.extra_attributes);
556        writer.write_event(Event::Start(e))?;
557
558        let mut extras = self.extra_xml.iter().collect::<Vec<_>>();
559        extras.sort_by_key(|(rank, _)| *rank);
560        let mut extra_index = 0;
561
562        write_style_extras(writer, &extras, &mut extra_index, 0)?;
563        write_style_string(writer, "w:name", 0, self.name.as_deref(), &self.modeled_xml)?;
564
565        write_style_extras(writer, &extras, &mut extra_index, 2)?;
566        write_style_string(
567            writer,
568            "w:basedOn",
569            2,
570            self.based_on.as_deref(),
571            &self.modeled_xml,
572        )?;
573
574        write_style_extras(writer, &extras, &mut extra_index, 3)?;
575        write_style_string(
576            writer,
577            "w:next",
578            3,
579            self.next_style.as_deref(),
580            &self.modeled_xml,
581        )?;
582
583        write_style_extras(writer, &extras, &mut extra_index, 4)?;
584        write_style_string(
585            writer,
586            "w:link",
587            4,
588            self.linked_style.as_deref(),
589            &self.modeled_xml,
590        )?;
591        write_style_extras(writer, &extras, &mut extra_index, 5)?;
592        write_style_toggle(
593            writer,
594            "w:autoRedefine",
595            5,
596            self.auto_redefine,
597            &self.modeled_xml,
598        )?;
599        write_style_extras(writer, &extras, &mut extra_index, 6)?;
600        write_style_toggle(writer, "w:hidden", 6, self.hidden, &self.modeled_xml)?;
601        write_style_extras(writer, &extras, &mut extra_index, 7)?;
602        write_style_priority(writer, self.ui_priority, &self.modeled_xml)?;
603        write_style_extras(writer, &extras, &mut extra_index, 8)?;
604        write_style_toggle(
605            writer,
606            "w:semiHidden",
607            8,
608            self.semi_hidden,
609            &self.modeled_xml,
610        )?;
611        write_style_extras(writer, &extras, &mut extra_index, 9)?;
612        write_style_toggle(
613            writer,
614            "w:unhideWhenUsed",
615            9,
616            self.unhide_when_used,
617            &self.modeled_xml,
618        )?;
619        write_style_extras(writer, &extras, &mut extra_index, 10)?;
620        write_style_toggle(
621            writer,
622            "w:qFormat",
623            10,
624            self.quick_format,
625            &self.modeled_xml,
626        )?;
627        write_style_extras(writer, &extras, &mut extra_index, 11)?;
628        write_style_toggle(writer, "w:locked", 11, self.locked, &self.modeled_xml)?;
629        write_style_extras(writer, &extras, &mut extra_index, 20)?;
630        let mut generated_ppr = Writer::new(Vec::new());
631        if let Some(ref ppr) = self.ppr {
632            ppr.to_xml(&mut generated_ppr)?;
633        }
634        let ppr = self.ppr.as_ref().map(|_| {
635            if generated_ppr.get_ref().is_empty() {
636                b"<w:pPr/>".as_slice()
637            } else {
638                generated_ppr.get_ref().as_slice()
639            }
640        });
641        writer
642            .get_mut()
643            .write_all(&self.style_property_bytes(20, b"pPr", ppr)?)?;
644        write_style_extras(writer, &extras, &mut extra_index, 21)?;
645        let mut generated_rpr = Writer::new(Vec::new());
646        if let Some(ref rpr) = self.rpr {
647            rpr.to_xml(&mut generated_rpr)?;
648        }
649        let rpr = self.rpr.as_ref().map(|_| {
650            if generated_rpr.get_ref().is_empty() {
651                b"<w:rPr/>".as_slice()
652            } else {
653                generated_rpr.get_ref().as_slice()
654            }
655        });
656        writer
657            .get_mut()
658            .write_all(&self.style_property_bytes(21, b"rPr", rpr)?)?;
659        write_style_extras(writer, &extras, &mut extra_index, 22)?;
660        if let Some(ref properties) = self.table_properties {
661            let preserved_matches = self.table_properties_original.as_ref() == Some(properties);
662            if preserved_matches {
663                writer
664                    .get_mut()
665                    .write_all(self.table_properties_xml.as_deref().unwrap_or_default())?;
666            } else {
667                properties.to_xml(writer)?;
668            }
669        }
670        write_style_extras(writer, &extras, &mut extra_index, 23)?;
671        if let Some(ref properties) = self.table_row_properties {
672            properties.to_xml(writer)?;
673        }
674        write_style_extras(writer, &extras, &mut extra_index, 24)?;
675        if let Some(ref properties) = self.table_cell_properties {
676            properties.to_xml(writer)?;
677        }
678        write_style_extras(writer, &extras, &mut extra_index, 25)?;
679        for conditional in &self.conditional_table_styles {
680            writer
681                .get_mut()
682                .write_all(&serialize_conditional_table_style(conditional)?)?;
683        }
684        write_style_extras(writer, &extras, &mut extra_index, u8::MAX)?;
685
686        writer.write_event(Event::End(BytesEnd::new("w:style")))?;
687        Ok(())
688    }
689}
690
691/// `CT_DocDefaults` — Document-level default properties.
692#[derive(Debug, Clone, Default, PartialEq)]
693#[allow(non_snake_case)]
694pub struct CT_DocDefaults {
695    pub rpr: Option<CT_RPr>,
696    pub ppr: Option<CT_PPr>,
697    /// Namespace-closed original wrapper, preserving attributes and child slots.
698    pub source_xml: Option<Vec<u8>>,
699}
700
701#[allow(non_snake_case)]
702impl CT_DocDefaults {
703    fn from_source_xml(raw: &[u8], inherited: &[String]) -> Result<Self> {
704        let (root, children) = defaults_source_children(raw)?;
705        let prefixes = word_prefixes_at(&root, inherited)?;
706        let mut defaults = Self {
707            source_xml: Some(raw.to_vec()),
708            ..Self::default()
709        };
710        for child in children {
711            let Some((start, nested)) = defaults_optional_children(&child)? else {
712                continue;
713            };
714            let scope = word_prefixes_at(&start, &prefixes)?;
715            let run = is_word_element(start.name().as_ref(), b"rPrDefault", &scope);
716            let paragraph = is_word_element(start.name().as_ref(), b"pPrDefault", &scope);
717            if !run && !paragraph {
718                continue;
719            }
720            // Last physical owned wrapper determines the direct property facts.
721            if run {
722                defaults.rpr = None;
723            }
724            if paragraph {
725                defaults.ppr = None;
726            }
727            for property in nested {
728                let Some((start, _)) = defaults_optional_children(&property)? else {
729                    continue;
730                };
731                let property_scope = word_prefixes_at(&start, &scope)?;
732                if run && is_word_element(start.name().as_ref(), b"rPr", &property_scope) {
733                    defaults.rpr = Some(parse_style_rpr(&property, &scope)?);
734                } else if paragraph
735                    && is_word_element(start.name().as_ref(), b"pPr", &property_scope)
736                {
737                    defaults.ppr = Some(parse_scoped_ppr(&property, &scope)?);
738                }
739            }
740        }
741        Ok(defaults)
742    }
743
744    pub fn from_xml(reader: &mut Reader<&[u8]>) -> Result<Self> {
745        Self::from_xml_with_prefixes(reader, &["w".to_string()])
746    }
747
748    fn from_xml_with_prefixes(
749        reader: &mut Reader<&[u8]>,
750        word_prefixes: &[String],
751    ) -> Result<Self> {
752        let mut defaults = CT_DocDefaults::default();
753        let mut buf = Vec::new();
754
755        loop {
756            match reader.read_event_into(&mut buf) {
757                Ok(Event::Start(ref e)) => {
758                    let name = e.name();
759                    let prefixes = word_prefixes_at(e, word_prefixes)?;
760                    if matches_local_name(name.as_ref(), b"rPrDefault") {
761                        // Read into rPrDefault, expecting rPr child
762                        defaults.rpr = Self::parse_pr_default(reader, b"rPrDefault")?;
763                    } else if matches_local_name(name.as_ref(), b"pPrDefault") {
764                        defaults.ppr = Self::parse_ppr_default(reader, &prefixes)?;
765                    } else {
766                        reader.read_to_end_into(name, &mut Vec::new())?;
767                    }
768                }
769                Ok(Event::End(ref e)) if matches_local_name(e.name().as_ref(), b"docDefaults") => {
770                    break;
771                }
772                Ok(Event::Eof) => break,
773                Err(e) => return Err(e.into()),
774                _ => {}
775            }
776            buf.clear();
777        }
778
779        Ok(defaults)
780    }
781
782    fn parse_pr_default(reader: &mut Reader<&[u8]>, end_tag: &[u8]) -> Result<Option<CT_RPr>> {
783        let mut rpr = None;
784        let mut buf = Vec::new();
785
786        loop {
787            match reader.read_event_into(&mut buf) {
788                Ok(Event::Start(ref e)) => {
789                    let name = e.name();
790                    if matches_local_name(name.as_ref(), b"rPr") {
791                        rpr = Some(CT_RPr::from_xml(reader)?);
792                    } else {
793                        reader.read_to_end_into(name, &mut Vec::new())?;
794                    }
795                }
796                Ok(Event::End(ref e)) if matches_local_name(e.name().as_ref(), end_tag) => {
797                    break;
798                }
799                Ok(Event::Eof) => break,
800                Err(e) => return Err(e.into()),
801                _ => {}
802            }
803            buf.clear();
804        }
805
806        Ok(rpr)
807    }
808
809    fn parse_ppr_default(
810        reader: &mut Reader<&[u8]>,
811        word_prefixes: &[String],
812    ) -> Result<Option<CT_PPr>> {
813        let mut ppr = None;
814        let mut buf = Vec::new();
815
816        loop {
817            match reader.read_event_into(&mut buf) {
818                Ok(Event::Start(ref e)) => {
819                    let name = e.name();
820                    let prefixes = word_prefixes_at(e, word_prefixes)?;
821                    if is_word_element(name.as_ref(), b"pPr", &prefixes) {
822                        let raw = capture_element(reader, e)?;
823                        ppr = Some(parse_scoped_ppr(&raw, word_prefixes)?);
824                    } else {
825                        reader.read_to_end_into(name, &mut Vec::new())?;
826                    }
827                }
828                Ok(Event::End(ref e)) if matches_local_name(e.name().as_ref(), b"pPrDefault") => {
829                    break;
830                }
831                Ok(Event::Eof) => break,
832                Err(e) => return Err(e.into()),
833                _ => {}
834            }
835            buf.clear();
836        }
837
838        Ok(ppr)
839    }
840
841    pub fn to_xml<W: std::io::Write>(&self, writer: &mut Writer<W>) -> Result<()> {
842        if let Some(raw) = &self.source_xml {
843            let original = Self::from_source_xml(raw, &[])?;
844            if original.rpr == self.rpr && original.ppr == self.ppr {
845                writer.get_mut().write_all(raw)?;
846                return Ok(());
847            }
848            let mut run = Writer::new(Vec::new());
849            if let Some(properties) = &self.rpr {
850                properties.to_xml(&mut run)?;
851            }
852            let mut paragraph = Writer::new(Vec::new());
853            if let Some(properties) = &self.ppr {
854                properties.to_xml(&mut paragraph)?;
855            }
856            // An explicitly empty final owner suppresses any earlier duplicate.
857            let run = self.rpr.as_ref().map(|_| {
858                if run.get_ref().is_empty() {
859                    b"<w:rPr/>".to_vec()
860                } else {
861                    run.into_inner()
862                }
863            });
864            let paragraph = self.ppr.as_ref().map(|_| {
865                if paragraph.get_ref().is_empty() {
866                    b"<w:pPr/>".to_vec()
867                } else {
868                    paragraph.into_inner()
869                }
870            });
871            let merged = merge_defaults_source(
872                raw,
873                run.as_deref(),
874                paragraph.as_deref(),
875                original.rpr != self.rpr,
876                original.ppr != self.ppr,
877            )?;
878            writer.get_mut().write_all(&merged)?;
879            return Ok(());
880        }
881        writer.write_event(Event::Start(BytesStart::new("w:docDefaults")))?;
882
883        if let Some(ref rpr) = self.rpr {
884            writer.write_event(Event::Start(BytesStart::new("w:rPrDefault")))?;
885            rpr.to_xml(writer)?;
886            writer.write_event(Event::End(BytesEnd::new("w:rPrDefault")))?;
887        }
888
889        if let Some(ref ppr) = self.ppr {
890            writer.write_event(Event::Start(BytesStart::new("w:pPrDefault")))?;
891            ppr.to_xml(writer)?;
892            writer.write_event(Event::End(BytesEnd::new("w:pPrDefault")))?;
893        }
894
895        writer.write_event(Event::End(BytesEnd::new("w:docDefaults")))?;
896        Ok(())
897    }
898}
899
900fn parse_style_rpr(raw: &[u8], inherited: &[String]) -> Result<CT_RPr> {
901    let (root, children) = defaults_source_children(raw)?;
902    let scope = word_prefixes_at(&root, inherited)?;
903    let end = std::str::from_utf8(root.name().as_ref())?.to_owned();
904    let mut projection = Writer::new(Vec::new());
905    projection.write_event(Event::Start(root))?;
906    for child in children {
907        let name = defaults_owned_child_name(&child, &scope)?;
908        if name.as_deref().is_some_and(|name| {
909            matches!(
910                name,
911                b"rFonts"
912                    | b"sz"
913                    | b"szCs"
914                    | b"u"
915                    | b"b"
916                    | b"bCs"
917                    | b"i"
918                    | b"iCs"
919                    | b"caps"
920                    | b"smallCaps"
921                    | b"strike"
922                    | b"dstrike"
923                    | b"vanish"
924                    | b"rtl"
925                    | b"lang"
926                    | b"color"
927                    | b"spacing"
928                    | b"position"
929                    | b"vertAlign"
930            )
931        }) {
932            let (leaf, _) = defaults_source_children(&child)?;
933            projection.write_event(Event::Empty(leaf))?;
934        } else {
935            projection.get_mut().extend_from_slice(&child);
936        }
937    }
938    projection.write_event(Event::End(BytesEnd::new(end)))?;
939    parse_scoped_rpr(projection.get_ref(), inherited)
940}
941
942// These concrete two-level wrappers retain all unowned bytes in their source slot.
943fn defaults_optional_children(raw: &[u8]) -> Result<Option<(BytesStart<'static>, Vec<Vec<u8>>)>> {
944    let mut reader = Reader::from_reader(raw);
945    match reader.read_event()? {
946        Event::Start(_) | Event::Empty(_) => defaults_source_children(raw).map(Some),
947        _ => Ok(None),
948    }
949}
950
951fn defaults_source_children(raw: &[u8]) -> Result<(BytesStart<'static>, Vec<Vec<u8>>)> {
952    let mut reader = Reader::from_reader(raw);
953    reader.config_mut().trim_text(false);
954    let (start, empty) = match reader.read_event()? {
955        Event::Start(start) => (start.into_owned(), false),
956        Event::Empty(start) => (start.into_owned(), true),
957        _ => {
958            return Err(OxmlError::InvalidValue(
959                "default wrapper root missing".to_owned(),
960            ));
961        }
962    };
963    let mut children = Vec::new();
964    if !empty {
965        loop {
966            let begin = reader.buffer_position() as usize;
967            match reader.read_event()? {
968                Event::Start(ref child) => {
969                    capture_element(&mut reader, child)?;
970                    children.push(raw[begin..reader.buffer_position() as usize].to_vec());
971                }
972                Event::End(_) => break,
973                Event::Eof => {
974                    return Err(OxmlError::InvalidValue(
975                        "default wrapper end missing".to_owned(),
976                    ));
977                }
978                _ => children.push(raw[begin..reader.buffer_position() as usize].to_vec()),
979            }
980        }
981    }
982    Ok((start, children))
983}
984
985fn defaults_owned_child_name(raw: &[u8], inherited: &[String]) -> Result<Option<Vec<u8>>> {
986    let Some((root, _)) = defaults_optional_children(raw)? else {
987        return Ok(None);
988    };
989    let scope = word_prefixes_at(&root, inherited)?;
990    let local = root.name().local_name().as_ref().to_vec();
991    Ok(is_word_element(root.name().as_ref(), &local, &scope).then_some(local))
992}
993
994// Style and defaults setters use this attribute-level merge. All physical
995// duplicates lose changed owned attributes, while their opaque bytes survive.
996fn defaults_owned_attributes(
997    raw: &[u8],
998    inherited: &[String],
999) -> Result<std::collections::HashMap<Vec<u8>, Vec<u8>>> {
1000    let mut values = std::collections::HashMap::new();
1001    if let Some((root, _)) = defaults_optional_children(raw)? {
1002        let scope = word_prefixes_at(&root, inherited)?;
1003        for attr in root.attributes() {
1004            let attr = attr?;
1005            let local = attr.key.local_name().as_ref().to_vec();
1006            if is_word_attribute(attr.key.as_ref(), &local, &scope) {
1007                values.insert(local, attr.value.to_vec());
1008            }
1009        }
1010    }
1011    Ok(values)
1012}
1013
1014pub(crate) fn merge_defaults_scalar_attributes(
1015    raw: &[u8],
1016    before: Option<&Vec<u8>>,
1017    after: Option<&Vec<u8>>,
1018    inherited: &[String],
1019    selected: bool,
1020) -> Result<Vec<u8>> {
1021    let canonical = vec!["w".to_owned()];
1022    let old = defaults_owned_attributes(before.map_or(&[], Vec::as_slice), &canonical)?;
1023    let new = defaults_owned_attributes(after.map_or(&[], Vec::as_slice), &canonical)?;
1024    let changed = old
1025        .keys()
1026        .chain(new.keys())
1027        .filter(|key| old.get(*key) != new.get(*key))
1028        .cloned()
1029        .collect::<std::collections::HashSet<_>>();
1030    if changed.is_empty() {
1031        return Ok(raw.to_vec());
1032    }
1033    let (root, _) = defaults_source_children(raw)?;
1034    let scope = word_prefixes_at(&root, inherited)?;
1035    let bindings = namespace_bindings_at(&root, &[])?;
1036    // Reuse an existing qualified Word binding. A new binding is needed only
1037    // when the physical owner has no usable attribute prefix.
1038    let existing_prefix = scope
1039        .iter()
1040        .find(|prefix| !prefix.is_empty() && !prefix.starts_with('\0'))
1041        .cloned();
1042    let mut prefix = existing_prefix
1043        .clone()
1044        .unwrap_or_else(|| "defaultsWord".to_owned());
1045    while existing_prefix.is_none()
1046        && (bindings.iter().any(|(name, _)| name == &prefix)
1047            || inherited.contains(&prefix)
1048            || raw
1049                .windows(prefix.len() + 1)
1050                .any(|bytes| bytes == format!("{prefix}:").as_bytes()))
1051    {
1052        prefix.push('_');
1053    }
1054    let mut result = Vec::new();
1055    let mut cursor = 1;
1056    while cursor < raw.len()
1057        && !raw[cursor].is_ascii_whitespace()
1058        && raw[cursor] != b'/'
1059        && raw[cursor] != b'>'
1060    {
1061        cursor += 1;
1062    }
1063    result.extend_from_slice(&raw[..cursor]);
1064    loop {
1065        let begin = cursor;
1066        while cursor < raw.len() && raw[cursor].is_ascii_whitespace() {
1067            cursor += 1;
1068        }
1069        if matches!(raw.get(cursor), Some(b'/') | Some(b'>')) {
1070            result.extend_from_slice(&raw[begin..cursor]);
1071            break;
1072        }
1073        let key_start = cursor;
1074        while cursor < raw.len() && !raw[cursor].is_ascii_whitespace() && raw[cursor] != b'=' {
1075            cursor += 1;
1076        }
1077        let key = &raw[key_start..cursor];
1078        while cursor < raw.len() && raw[cursor].is_ascii_whitespace() {
1079            cursor += 1;
1080        }
1081        if raw.get(cursor) != Some(&b'=') {
1082            return Err(OxmlError::InvalidValue(
1083                "default attribute assignment missing".to_owned(),
1084            ));
1085        }
1086        cursor += 1;
1087        while cursor < raw.len() && raw[cursor].is_ascii_whitespace() {
1088            cursor += 1;
1089        }
1090        let quote = *raw
1091            .get(cursor)
1092            .ok_or_else(|| OxmlError::InvalidValue("default attribute quote missing".to_owned()))?;
1093        if quote != b'\'' && quote != b'"' {
1094            return Err(OxmlError::InvalidValue(
1095                "default attribute quote invalid".to_owned(),
1096            ));
1097        }
1098        cursor += 1;
1099        while cursor < raw.len() && raw[cursor] != quote {
1100            cursor += 1;
1101        }
1102        if cursor == raw.len() {
1103            return Err(OxmlError::InvalidValue(
1104                "default attribute end missing".to_owned(),
1105            ));
1106        }
1107        cursor += 1;
1108        let local = key.rsplit(|byte| *byte == b':').next().unwrap_or(key);
1109        if !changed.contains(local) || !is_word_attribute(key, local, &scope) {
1110            result.extend_from_slice(&raw[begin..cursor]);
1111        }
1112    }
1113    if selected && changed.iter().any(|key| new.contains_key(key)) {
1114        if existing_prefix.is_none() {
1115            result.extend_from_slice(format!(" xmlns:{prefix}=\"{W_NS}\"").as_bytes());
1116        }
1117        let mut keys = changed.iter().collect::<Vec<_>>();
1118        keys.sort();
1119        for key in keys {
1120            if let Some(value) = new.get(key) {
1121                result.extend_from_slice(
1122                    format!(" {prefix}:{}=\"", std::str::from_utf8(key)?).as_bytes(),
1123                );
1124                result.extend_from_slice(value);
1125                result.push(b'"');
1126            }
1127        }
1128    }
1129    result.extend_from_slice(&raw[cursor..]);
1130    Ok(result)
1131}
1132
1133// The typed paragraph border owner has no raw container slot. Merge only the
1134// changed owned edge attributes while retaining its physical source payload.
1135fn style_border_semantic_key(name: Vec<u8>) -> Vec<u8> {
1136    match name.as_slice() {
1137        b"start" => b"left".to_vec(),
1138        b"end" => b"right".to_vec(),
1139        _ => name,
1140    }
1141}
1142
1143pub(crate) fn merge_style_border_source(
1144    raw: &[u8],
1145    before: Option<&Vec<u8>>,
1146    after: &[u8],
1147    inherited: &[String],
1148    selected_owner: bool,
1149) -> Result<Vec<u8>> {
1150    let (root, source) = defaults_source_children(raw)?;
1151    let scope = word_prefixes_at(&root, inherited)?;
1152    let canonical = vec!["w".to_owned()];
1153    let mut previous = std::collections::HashMap::new();
1154    if let Some(before) = before {
1155        let before =
1156            crate::text::raw_with_external_bindings(before, &[("w".to_owned(), W_NS.to_owned())])?;
1157        let (before_root, children) = defaults_source_children(&before)?;
1158        let before_scope = word_prefixes_at(&before_root, &canonical)?;
1159        let bindings = namespace_bindings_at(&before_root, &[])?;
1160        for child in children {
1161            if let Some(name) = defaults_owned_child_name(&child, &before_scope)? {
1162                previous.insert(
1163                    style_border_semantic_key(name),
1164                    crate::text::raw_with_external_bindings(&child, &bindings)?,
1165                );
1166            }
1167        }
1168    }
1169    let bindings = [("w".to_owned(), W_NS.to_owned())];
1170    let after = crate::text::raw_with_external_bindings(after, &bindings)?;
1171    let (generated_root, generated) = defaults_source_children(&after)?;
1172    let generated_scope = word_prefixes_at(&generated_root, &[])?;
1173    let mut supplied = std::collections::HashMap::new();
1174    let mut order = Vec::new();
1175    for child in generated {
1176        if let Some(name) = defaults_owned_child_name(&child, &generated_scope)? {
1177            let name = style_border_semantic_key(name);
1178            order.push(name.clone());
1179            supplied.insert(
1180                name,
1181                crate::text::raw_with_external_bindings(
1182                    &child,
1183                    &namespace_bindings_at(&generated_root, &[])?,
1184                )?,
1185            );
1186        }
1187    }
1188    let mut selected = std::collections::HashMap::new();
1189    for (index, child) in source.iter().enumerate() {
1190        if let Some(name) = defaults_owned_child_name(child, &scope)? {
1191            let name = style_border_semantic_key(name);
1192            selected.insert(name, index);
1193        }
1194    }
1195    let generated_bindings = namespace_bindings_at(&generated_root, &[])?;
1196    let end = std::str::from_utf8(root.name().as_ref())?.to_owned();
1197    let mut writer = Writer::new(Vec::new());
1198    writer.write_event(Event::Start(root))?;
1199    let mut inserted = std::collections::HashSet::new();
1200    for child in &source {
1201        if let Some(name) = defaults_owned_child_name(child, &scope)? {
1202            let name = style_border_semantic_key(name);
1203            if let Some(rank) = order.iter().position(|value| value == &name) {
1204                for new_name in order
1205                    .iter()
1206                    .take(rank)
1207                    .filter(|value| !selected.contains_key(*value))
1208                {
1209                    if selected_owner && inserted.insert(new_name.clone()) {
1210                        writer.get_mut().extend_from_slice(
1211                            &crate::text::raw_with_external_bindings(
1212                                &supplied[new_name],
1213                                &generated_bindings,
1214                            )?,
1215                        );
1216                    }
1217                }
1218            }
1219            // Explicit deletion removes the owned edge, including every physical
1220            // alias or duplicate. Opaque siblings and the enclosing owner remain.
1221            if previous.contains_key(&name) && !supplied.contains_key(&name) {
1222                continue;
1223            }
1224            if previous.get(&name) != supplied.get(&name) && supplied.contains_key(&name) {
1225                writer
1226                    .get_mut()
1227                    .extend_from_slice(&merge_defaults_scalar_attributes(
1228                        child,
1229                        previous.get(&name),
1230                        supplied.get(&name),
1231                        &scope,
1232                        true,
1233                    )?);
1234                inserted.insert(name);
1235                continue;
1236            }
1237        }
1238        writer.get_mut().extend_from_slice(child);
1239    }
1240    for name in order
1241        .iter()
1242        .filter(|name| selected_owner && !selected.contains_key(*name) && !inserted.contains(*name))
1243    {
1244        writer
1245            .get_mut()
1246            .extend_from_slice(&crate::text::raw_with_external_bindings(
1247                &supplied[name],
1248                &generated_bindings,
1249            )?);
1250    }
1251    writer.write_event(Event::End(BytesEnd::new(end)))?;
1252    Ok(writer.into_inner())
1253}
1254
1255fn merge_defaults_property_root(
1256    raw: &[u8],
1257    generated: &[u8],
1258    inherited: &[String],
1259    local: &[u8],
1260) -> Result<Vec<u8>> {
1261    let (root, source_children) = defaults_source_children(raw)?;
1262    let scope = word_prefixes_at(&root, inherited)?;
1263    let mut before = Writer::new(Vec::new());
1264    if local == b"rPr" {
1265        parse_style_rpr(raw, inherited)?.to_xml(&mut before)?;
1266    } else {
1267        parse_scoped_ppr(raw, inherited)?.to_xml(&mut before)?;
1268    }
1269    let bindings = [("w".to_owned(), W_NS.to_owned())];
1270    let generated = crate::text::raw_with_external_bindings(generated, &bindings)?;
1271    let (generated_root, children) = defaults_source_children(&generated)?;
1272    let generated_scope = word_prefixes_at(&generated_root, &[])?;
1273    let mut previous = std::collections::HashMap::new();
1274    if !before.get_ref().is_empty() {
1275        let raw_before = crate::text::raw_with_external_bindings(before.get_ref(), &bindings)?;
1276        let (before_root, before_children) = defaults_source_children(&raw_before)?;
1277        let before_scope = word_prefixes_at(&before_root, &[])?;
1278        for child in before_children {
1279            if let Some(name) = defaults_owned_child_name(&child, &before_scope)? {
1280                previous.insert(name, child);
1281            }
1282        }
1283    }
1284    let mut supplied = std::collections::HashMap::new();
1285    let mut order = Vec::new();
1286    for child in &children {
1287        if let Some(name) = defaults_owned_child_name(child, &generated_scope)? {
1288            order.push(name.clone());
1289            supplied.insert(name, child.clone());
1290        }
1291    }
1292    let mut changed = previous
1293        .keys()
1294        .chain(supplied.keys())
1295        .filter(|name| previous.get(*name) != supplied.get(*name))
1296        .cloned()
1297        .collect::<Vec<_>>();
1298    changed.sort();
1299    changed.dedup();
1300    let mut selected = std::collections::HashMap::new();
1301    for (index, child) in source_children.iter().enumerate() {
1302        if let Some(name) = defaults_owned_child_name(child, &scope)? {
1303            selected.insert(name, index);
1304        }
1305    }
1306    let generated_bindings = namespace_bindings_at(&generated_root, &[])?;
1307    let end = std::str::from_utf8(root.name().as_ref())?.to_owned();
1308    let source_bindings =
1309        namespace_bindings_at(&root, &crate::numbering::namespace_bindings(&scope))?;
1310    let mut writer = Writer::new(Vec::new());
1311    writer.write_event(Event::Start(root))?;
1312    let mut inserted = std::collections::HashSet::new();
1313    for (index, child) in source_children.iter().enumerate() {
1314        let name = defaults_owned_child_name(child, &scope)?;
1315        if let Some(ref name) = name {
1316            let rank = order.iter().position(|value| value == name);
1317            for new_name in order
1318                .iter()
1319                .filter(|value| changed.contains(value) && !selected.contains_key(*value))
1320            {
1321                if !inserted.contains(new_name)
1322                    && rank.is_some_and(|rank| {
1323                        order.iter().position(|value| value == new_name).unwrap() < rank
1324                    })
1325                {
1326                    writer
1327                        .get_mut()
1328                        .extend_from_slice(&crate::text::raw_with_external_bindings(
1329                            &supplied[new_name],
1330                            &generated_bindings,
1331                        )?);
1332                    inserted.insert(new_name.clone());
1333                }
1334            }
1335            if local == b"pPr" && name == b"pBdr" && changed.contains(name) {
1336                if let Some(generated) = supplied.get(name) {
1337                    let generated_border =
1338                        crate::text::raw_with_external_bindings(generated, &generated_bindings)?;
1339                    if crate::borders::CT_PBdr::from_source_xml(&generated_border)?
1340                        == crate::borders::CT_PBdr::default()
1341                    {
1342                        let child =
1343                            crate::text::raw_with_external_bindings(child, &source_bindings)?;
1344                        writer
1345                            .get_mut()
1346                            .extend_from_slice(&crate::borders::CT_PBdr::merge_source(
1347                                &child, &child, None, false,
1348                            )?);
1349                    } else {
1350                        writer
1351                            .get_mut()
1352                            .extend_from_slice(&merge_style_border_source(
1353                                child,
1354                                previous.get(name),
1355                                generated,
1356                                &scope,
1357                                selected.get(name) == Some(&index),
1358                            )?);
1359                    }
1360                } else {
1361                    // Explicit clear removes each owned edge, not its surrounding
1362                    // source container or opaque siblings.
1363                    let child = crate::text::raw_with_external_bindings(child, &source_bindings)?;
1364                    writer
1365                        .get_mut()
1366                        .extend_from_slice(&crate::borders::CT_PBdr::merge_source(
1367                            &child, &child, None, false,
1368                        )?);
1369                }
1370                inserted.insert(name.clone());
1371                continue;
1372            }
1373            if changed.contains(name)
1374                && matches!(
1375                    name.as_slice(),
1376                    b"rFonts"
1377                        | b"sz"
1378                        | b"szCs"
1379                        | b"u"
1380                        | b"b"
1381                        | b"bCs"
1382                        | b"i"
1383                        | b"iCs"
1384                        | b"color"
1385                        | b"lang"
1386                        | b"spacing"
1387                        | b"ind"
1388                        | b"jc"
1389                        | b"keepNext"
1390                        | b"keepLines"
1391                        | b"pageBreakBefore"
1392                        | b"shd"
1393                        | b"caps"
1394                        | b"smallCaps"
1395                        | b"strike"
1396                        | b"dstrike"
1397                        | b"vanish"
1398                        | b"rtl"
1399                        | b"position"
1400                        | b"vertAlign"
1401                )
1402            {
1403                if !supplied.contains_key(name)
1404                    && !matches!(name.as_slice(), b"rFonts" | b"sz" | b"szCs")
1405                {
1406                    inserted.insert(name.clone());
1407                    continue;
1408                }
1409                writer
1410                    .get_mut()
1411                    .extend_from_slice(&merge_defaults_scalar_attributes(
1412                        child,
1413                        previous.get(name),
1414                        supplied.get(name),
1415                        &scope,
1416                        selected.get(name) == Some(&index),
1417                    )?);
1418
1419                inserted.insert(name.clone());
1420                continue;
1421            }
1422            if changed.contains(name) && selected.get(name) == Some(&index) {
1423                if let Some(generated) = supplied.get(name) {
1424                    writer
1425                        .get_mut()
1426                        .extend_from_slice(&crate::text::raw_with_external_bindings(
1427                            generated,
1428                            &generated_bindings,
1429                        )?);
1430                }
1431                inserted.insert(name.clone());
1432                continue;
1433            }
1434        }
1435        writer.get_mut().extend_from_slice(child);
1436    }
1437    for name in order
1438        .iter()
1439        .filter(|name| changed.contains(name) && !inserted.contains(*name))
1440    {
1441        writer
1442            .get_mut()
1443            .extend_from_slice(&crate::text::raw_with_external_bindings(
1444                &supplied[name],
1445                &generated_bindings,
1446            )?);
1447    }
1448    writer.write_event(Event::End(BytesEnd::new(end)))?;
1449    Ok(writer.into_inner())
1450}
1451
1452fn merge_defaults_property_wrapper(
1453    raw: &[u8],
1454    inherited: &[String],
1455    local: &[u8],
1456    generated: Option<&[u8]>,
1457) -> Result<Vec<u8>> {
1458    let (root, children) = defaults_source_children(raw)?;
1459    let scope = word_prefixes_at(&root, inherited)?;
1460    let mut selected = None;
1461    for (index, child) in children.iter().enumerate() {
1462        if let Some((start, _)) = defaults_optional_children(child)? {
1463            let scope = word_prefixes_at(&start, &scope)?;
1464            if is_word_element(start.name().as_ref(), local, &scope) {
1465                selected = Some(index);
1466            }
1467        }
1468    }
1469    let mut writer = Writer::new(Vec::new());
1470    let end = std::str::from_utf8(root.name().as_ref())?.to_owned();
1471    writer.write_event(Event::Start(root))?;
1472    for (index, child) in children.iter().enumerate() {
1473        if Some(index) == selected {
1474            if let Some(generated) = generated {
1475                writer
1476                    .get_mut()
1477                    .extend_from_slice(&merge_defaults_property_root(
1478                        child, generated, &scope, local,
1479                    )?);
1480            }
1481        } else {
1482            writer.get_mut().extend_from_slice(child);
1483        }
1484    }
1485    if selected.is_none()
1486        && let Some(generated) = generated
1487    {
1488        writer
1489            .get_mut()
1490            .extend_from_slice(&crate::text::raw_with_external_bindings(
1491                generated,
1492                &[("w".to_owned(), W_NS.to_owned())],
1493            )?);
1494    }
1495    writer.write_event(Event::End(BytesEnd::new(end)))?;
1496    Ok(writer.into_inner())
1497}
1498
1499fn merge_defaults_source(
1500    raw: &[u8],
1501    run: Option<&[u8]>,
1502    paragraph: Option<&[u8]>,
1503    run_changed: bool,
1504    paragraph_changed: bool,
1505) -> Result<Vec<u8>> {
1506    let (root, children) = defaults_source_children(raw)?;
1507    let scope = word_prefixes_at(&root, &[])?;
1508    let mut run_slot = None;
1509    let mut paragraph_slot = None;
1510    for (index, child) in children.iter().enumerate() {
1511        if let Some((start, _)) = defaults_optional_children(child)? {
1512            let child_scope = word_prefixes_at(&start, &scope)?;
1513            if is_word_element(start.name().as_ref(), b"rPrDefault", &child_scope) {
1514                run_slot = Some(index);
1515            }
1516            if is_word_element(start.name().as_ref(), b"pPrDefault", &child_scope) {
1517                paragraph_slot = Some(index);
1518            }
1519        }
1520    }
1521    let mut writer = Writer::new(Vec::new());
1522    let end = std::str::from_utf8(root.name().as_ref())?.to_owned();
1523    writer.write_event(Event::Start(root))?;
1524    // Insert a newly supplied rPrDefault before any existing pPrDefault.
1525    if run_slot.is_none()
1526        && let Some(run) = run
1527    {
1528        writer.get_mut().extend_from_slice(&merge_defaults_property_wrapper(b"<w:rPrDefault xmlns:w=\"http://schemas.openxmlformats.org/wordprocessingml/2006/main\"/>", &scope, b"rPr", Some(run))?);
1529    }
1530    for (index, child) in children.iter().enumerate() {
1531        if Some(index) == run_slot && run_changed {
1532            writer
1533                .get_mut()
1534                .extend_from_slice(&merge_defaults_property_wrapper(
1535                    child, &scope, b"rPr", run,
1536                )?);
1537        } else if Some(index) == paragraph_slot && paragraph_changed {
1538            writer
1539                .get_mut()
1540                .extend_from_slice(&merge_defaults_property_wrapper(
1541                    child, &scope, b"pPr", paragraph,
1542                )?);
1543        } else {
1544            writer.get_mut().extend_from_slice(child);
1545        }
1546    }
1547    if paragraph_slot.is_none()
1548        && let Some(paragraph) = paragraph
1549    {
1550        writer.get_mut().extend_from_slice(&merge_defaults_property_wrapper(b"<w:pPrDefault xmlns:w=\"http://schemas.openxmlformats.org/wordprocessingml/2006/main\"/>", &scope, b"pPr", Some(paragraph))?);
1551    }
1552    writer.write_event(Event::End(BytesEnd::new(end)))?;
1553    Ok(writer.into_inner())
1554}
1555
1556/// `CT_Styles` — The styles part (word/styles.xml).
1557#[derive(Debug, Clone, PartialEq)]
1558#[allow(non_snake_case)]
1559pub struct CT_Styles {
1560    pub doc_defaults: Option<CT_DocDefaults>,
1561    pub styles: Vec<CT_Style>,
1562    /// Root children the model does not read, such as `w:latentStyles`.
1563    /// They stay in source order and are written after `w:docDefaults` and
1564    /// before the first `w:style`.
1565    pub extra_xml: Vec<Vec<u8>>,
1566}
1567
1568#[allow(non_snake_case)]
1569impl CT_Styles {
1570    pub fn new() -> Self {
1571        CT_Styles {
1572            doc_defaults: None,
1573            styles: Vec::new(),
1574            extra_xml: Vec::new(),
1575        }
1576    }
1577
1578    /// Parse from XML bytes (the content of word/styles.xml).
1579    pub fn from_xml(xml: &[u8]) -> Result<Self> {
1580        let mut reader = Reader::from_reader(xml);
1581        reader.config_mut().trim_text(true);
1582
1583        let mut doc_defaults = None;
1584        let mut styles = Vec::new();
1585        let mut extra_xml = Vec::new();
1586        let mut buf = Vec::new();
1587        let mut word_prefixes = Vec::new();
1588        let mut namespace_bindings = Vec::new();
1589
1590        loop {
1591            match reader.read_event_into(&mut buf) {
1592                Ok(Event::Start(ref e)) => {
1593                    let name = e.name();
1594                    let prefixes = word_prefixes_at(e, &word_prefixes)?;
1595                    if is_word_element(name.as_ref(), b"docDefaults", &prefixes) {
1596                        let bindings = namespace_bindings_at(e, &namespace_bindings)?;
1597                        reader.config_mut().trim_text(false);
1598                        let captured = capture_element(&mut reader, e)?;
1599                        reader.config_mut().trim_text(true);
1600                        let raw = crate::text::raw_with_external_bindings(&captured, &bindings)?;
1601                        doc_defaults = Some(CT_DocDefaults::from_source_xml(&raw, &prefixes)?);
1602                    } else if is_word_element(name.as_ref(), b"style", &prefixes) {
1603                        let bindings = namespace_bindings_at(e, &namespace_bindings)?;
1604                        styles.push(CT_Style::from_xml_with_prefixes(
1605                            &mut reader,
1606                            e,
1607                            &prefixes,
1608                            &bindings,
1609                        )?);
1610                    } else if is_word_element(name.as_ref(), b"styles", &prefixes) {
1611                        // Root element, continue
1612                        word_prefixes = prefixes;
1613                        namespace_bindings = namespace_bindings_at(e, &namespace_bindings)?;
1614                    } else {
1615                        let bindings = namespace_bindings_at(e, &namespace_bindings)?;
1616                        reader.config_mut().trim_text(false);
1617                        let captured = capture_element(&mut reader, e)?;
1618                        reader.config_mut().trim_text(true);
1619                        extra_xml.push(make_style_raw_self_contained(&captured, &bindings)?);
1620                    }
1621                }
1622                Ok(Event::Empty(ref e)) => {
1623                    let prefixes = word_prefixes_at(e, &word_prefixes)?;
1624                    let bindings = namespace_bindings_at(e, &namespace_bindings)?;
1625                    if is_word_element(e.name().as_ref(), b"docDefaults", &prefixes) {
1626                        let raw = crate::text::raw_with_external_bindings(
1627                            &capture_empty_element(e)?,
1628                            &bindings,
1629                        )?;
1630                        doc_defaults = Some(CT_DocDefaults::from_source_xml(&raw, &prefixes)?);
1631                    } else if is_word_element(e.name().as_ref(), b"style", &prefixes) {
1632                        let owned = e.to_owned();
1633                        let mut empty = Reader::from_reader(&[] as &[u8]);
1634                        styles.push(CT_Style::from_xml_with_prefixes(
1635                            &mut empty, &owned, &prefixes, &bindings,
1636                        )?);
1637                    } else if !is_word_element(e.name().as_ref(), b"styles", &prefixes) {
1638                        extra_xml.push(make_style_raw_self_contained(
1639                            &capture_empty_element(e)?,
1640                            &bindings,
1641                        )?);
1642                    }
1643                }
1644                Ok(Event::Eof) => break,
1645                Err(e) => return Err(e.into()),
1646                _ => {}
1647            }
1648            buf.clear();
1649        }
1650
1651        Ok(CT_Styles {
1652            doc_defaults,
1653            styles,
1654            extra_xml,
1655        })
1656    }
1657
1658    /// Serialize to XML bytes.
1659    pub fn to_xml(&self) -> Result<Vec<u8>> {
1660        let mut writer = Writer::new(Vec::new());
1661
1662        writer.write_event(Event::Decl(BytesDecl::new(
1663            "1.0",
1664            Some("UTF-8"),
1665            Some("yes"),
1666        )))?;
1667
1668        let mut styles_start = BytesStart::new("w:styles");
1669        styles_start.push_attribute(("xmlns:w", W_NS));
1670        styles_start.push_attribute((
1671            "xmlns:r",
1672            "http://schemas.openxmlformats.org/officeDocument/2006/relationships",
1673        ));
1674        writer.write_event(Event::Start(styles_start))?;
1675
1676        if let Some(ref defaults) = self.doc_defaults {
1677            defaults.to_xml(&mut writer)?;
1678        }
1679
1680        for raw in &self.extra_xml {
1681            writer.get_mut().extend_from_slice(raw);
1682        }
1683
1684        for style in &self.styles {
1685            style.to_xml(&mut writer)?;
1686        }
1687
1688        writer.write_event(Event::End(BytesEnd::new("w:styles")))?;
1689
1690        Ok(writer.into_inner())
1691    }
1692
1693    /// Find a style by its ID.
1694    pub fn get_by_id(&self, style_id: &str) -> Option<&CT_Style> {
1695        self.styles.iter().find(|s| s.style_id == style_id)
1696    }
1697
1698    /// Find the default style for a given type.
1699    pub fn get_default(&self, style_type: StyleType) -> Option<&CT_Style> {
1700        self.styles
1701            .iter()
1702            .find(|s| s.style_type == style_type && s.is_default)
1703    }
1704
1705    /// Create a minimal default styles part for a new document.
1706    pub fn new_default() -> Self {
1707        use crate::units::HalfPoint;
1708
1709        let normal = CT_Style {
1710            style_id: "Normal".to_string(),
1711            style_type: StyleType::Paragraph,
1712            name: Some("Normal".to_string()),
1713            based_on: None,
1714            next_style: None,
1715            linked_style: None,
1716            auto_redefine: None,
1717            hidden: None,
1718            ui_priority: None,
1719            semi_hidden: None,
1720            unhide_when_used: None,
1721            quick_format: None,
1722            locked: None,
1723            is_default: true,
1724            ppr: None,
1725            rpr: None,
1726            table_properties: None,
1727            table_properties_original: None,
1728            table_properties_xml: None,
1729            table_row_properties: None,
1730            table_cell_properties: None,
1731            conditional_table_styles: Vec::new(),
1732            extra_attributes: Vec::new(),
1733            modeled_xml: Vec::new(),
1734            extra_xml: Vec::new(),
1735        };
1736
1737        let heading1 = CT_Style {
1738            style_id: "Heading1".to_string(),
1739            style_type: StyleType::Paragraph,
1740            name: Some("heading 1".to_string()),
1741            based_on: Some("Normal".to_string()),
1742            next_style: Some("Normal".to_string()),
1743            linked_style: None,
1744            auto_redefine: None,
1745            hidden: None,
1746            ui_priority: None,
1747            semi_hidden: None,
1748            unhide_when_used: None,
1749            quick_format: None,
1750            locked: None,
1751            is_default: false,
1752            ppr: Some(CT_PPr {
1753                keep_next: Some(true),
1754                keep_lines: Some(true),
1755                space_before: Some(crate::units::Twips(240)),
1756                space_after: Some(crate::units::Twips(0)),
1757                ..Default::default()
1758            }),
1759            rpr: Some(CT_RPr {
1760                sz: Some(HalfPoint(32)),
1761                sz_cs: Some(HalfPoint(32)),
1762                bold: Some(true),
1763                bold_cs: Some(true),
1764                color: Some("2F5496".to_string()),
1765                ..Default::default()
1766            }),
1767            table_properties: None,
1768            table_properties_original: None,
1769            table_properties_xml: None,
1770            table_row_properties: None,
1771            table_cell_properties: None,
1772            conditional_table_styles: Vec::new(),
1773            extra_attributes: Vec::new(),
1774            modeled_xml: Vec::new(),
1775            extra_xml: Vec::new(),
1776        };
1777
1778        let doc_defaults = CT_DocDefaults {
1779            source_xml: None,
1780            rpr: Some(CT_RPr {
1781                font_ascii: Some("Calibri".to_string()),
1782                font_hansi: Some("Calibri".to_string()),
1783                font_east_asia: Some("Calibri".to_string()),
1784                font_cs: Some("Times New Roman".to_string()),
1785                sz: Some(HalfPoint(22)),
1786                sz_cs: Some(HalfPoint(22)),
1787                ..Default::default()
1788            }),
1789            ppr: Some(CT_PPr {
1790                space_after: Some(crate::units::Twips(160)),
1791                line_spacing: Some(crate::units::Twips(259)),
1792                line_rule: Some("auto".to_string()),
1793                ..Default::default()
1794            }),
1795        };
1796
1797        CT_Styles {
1798            doc_defaults: Some(doc_defaults),
1799            styles: vec![normal, heading1],
1800            extra_xml: Vec::new(),
1801        }
1802    }
1803}
1804
1805impl Default for CT_Styles {
1806    fn default() -> Self {
1807        Self::new()
1808    }
1809}
1810
1811fn style_child_rank(name: &[u8], word_prefixes: &[String]) -> u8 {
1812    let local = name.rsplit(|byte| *byte == b':').next().unwrap_or(name);
1813    if !is_word_element(name, local, word_prefixes) {
1814        return 26;
1815    }
1816    match local {
1817        b"name" => 0,
1818        b"aliases" => 1,
1819        b"basedOn" => 2,
1820        b"next" => 3,
1821        b"link" => 4,
1822        b"autoRedefine" => 5,
1823        b"hidden" => 6,
1824        b"uiPriority" => 7,
1825        b"semiHidden" => 8,
1826        b"unhideWhenUsed" => 9,
1827        b"qFormat" => 10,
1828        b"locked" => 11,
1829        b"personal" => 12,
1830        b"personalCompose" => 13,
1831        b"personalReply" => 14,
1832        b"rsid" => 19,
1833        b"pPr" => 20,
1834        b"rPr" => 21,
1835        b"tblPr" => 22,
1836        b"trPr" => 23,
1837        b"tcPr" => 24,
1838        b"tblStylePr" => 25,
1839        _ => 26,
1840    }
1841}
1842
1843fn style_scalar_rank(name: &[u8], word_prefixes: &[String]) -> Option<u8> {
1844    let rank = style_child_rank(name, word_prefixes);
1845    matches!(rank, 0 | 2..=11).then_some(rank)
1846}
1847
1848fn style_scalar_empty_requires_snapshot(element: &BytesStart<'_>, rank: u8) -> Result<bool> {
1849    let canonical_name: &[u8] = match rank {
1850        0 => b"w:name",
1851        2 => b"w:basedOn",
1852        3 => b"w:next",
1853        4 => b"w:link",
1854        5 => b"w:autoRedefine",
1855        6 => b"w:hidden",
1856        7 => b"w:uiPriority",
1857        8 => b"w:semiHidden",
1858        9 => b"w:unhideWhenUsed",
1859        10 => b"w:qFormat",
1860        11 => b"w:locked",
1861        _ => unreachable!("style scalar rank"),
1862    };
1863    if element.name().as_ref() != canonical_name {
1864        return Ok(true);
1865    }
1866
1867    for attribute in element.attributes() {
1868        if attribute?.key.as_ref() != b"w:val" {
1869            return Ok(true);
1870        }
1871    }
1872    Ok(false)
1873}
1874
1875#[allow(clippy::too_many_arguments)]
1876fn apply_style_scalar(
1877    rank: u8,
1878    string_value: Option<String>,
1879    toggle_value: Option<bool>,
1880    priority_value: Option<u32>,
1881    name: &mut Option<String>,
1882    based_on: &mut Option<String>,
1883    next_style: &mut Option<String>,
1884    linked_style: &mut Option<String>,
1885    auto_redefine: &mut Option<bool>,
1886    hidden: &mut Option<bool>,
1887    ui_priority: &mut Option<u32>,
1888    semi_hidden: &mut Option<bool>,
1889    unhide_when_used: &mut Option<bool>,
1890    quick_format: &mut Option<bool>,
1891    locked: &mut Option<bool>,
1892) {
1893    match rank {
1894        0 => *name = string_value,
1895        2 => *based_on = string_value,
1896        3 => *next_style = string_value,
1897        4 => *linked_style = string_value,
1898        5 => *auto_redefine = toggle_value,
1899        6 => *hidden = toggle_value,
1900        7 => *ui_priority = priority_value,
1901        8 => *semi_hidden = toggle_value,
1902        9 => *unhide_when_used = toggle_value,
1903        10 => *quick_format = toggle_value,
1904        11 => *locked = toggle_value,
1905        _ => unreachable!("style scalar rank"),
1906    }
1907}
1908
1909fn write_style_extras<W: std::io::Write>(
1910    writer: &mut Writer<W>,
1911    extras: &[&(u8, Vec<u8>)],
1912    index: &mut usize,
1913    before_rank: u8,
1914) -> Result<()> {
1915    while let Some((rank, raw)) = extras.get(*index).copied() {
1916        if *rank >= before_rank {
1917            break;
1918        }
1919        writer.get_mut().write_all(raw)?;
1920        *index += 1;
1921    }
1922    Ok(())
1923}
1924
1925fn namespace_bindings_at(
1926    element: &BytesStart,
1927    inherited: &[(String, String)],
1928) -> Result<Vec<(String, String)>> {
1929    let mut bindings = inherited.to_vec();
1930    for attribute in element.attributes() {
1931        let attribute = attribute?;
1932        let key = attribute.key.as_ref();
1933        let prefix = if key == b"xmlns" {
1934            Some(String::new())
1935        } else {
1936            key.strip_prefix(b"xmlns:")
1937                .map(|prefix| String::from_utf8_lossy(prefix).into_owned())
1938        };
1939        let Some(prefix) = prefix else {
1940            continue;
1941        };
1942        let uri = std::str::from_utf8(&attribute.value)?.to_owned();
1943        if let Some(binding) = bindings
1944            .iter_mut()
1945            .find(|(candidate, _)| candidate == &prefix)
1946        {
1947            binding.1 = uri;
1948        } else {
1949            bindings.push((prefix, uri));
1950        }
1951    }
1952    Ok(bindings)
1953}
1954
1955fn reject_conflicting_style_prefixes(element: &BytesStart<'_>) -> Result<()> {
1956    for attribute in element.attributes() {
1957        let attribute = attribute?;
1958        let expected = match attribute.key.as_ref() {
1959            b"xmlns:w" => Some(W_NS),
1960            b"xmlns:r" => {
1961                Some("http://schemas.openxmlformats.org/officeDocument/2006/relationships")
1962            }
1963            _ => None,
1964        };
1965        let Some(expected) = expected else {
1966            continue;
1967        };
1968        let value = std::str::from_utf8(&attribute.value)?;
1969        if value != expected {
1970            return Err(OxmlError::InvalidValue(format!(
1971                "{} conflicts with the fixed style writer namespace",
1972                String::from_utf8_lossy(attribute.key.as_ref())
1973            )));
1974        }
1975    }
1976    Ok(())
1977}
1978
1979fn raw_style_attributes(
1980    element: &BytesStart<'_>,
1981    modeled: &[&[u8]],
1982    word_prefixes: &[String],
1983    namespace_bindings: &[(String, String)],
1984) -> Result<Vec<(String, String)>> {
1985    let mut attributes = Vec::new();
1986    for attribute in element.attributes() {
1987        let attribute = attribute?;
1988        let key = attribute.key.as_ref();
1989        let value = attribute
1990            .decoded_and_normalized_value(XmlVersion::Implicit1_0, element.decoder())?
1991            .into_owned();
1992        let canonical_namespace = (key == b"xmlns:w" && value == W_NS)
1993            || (key == b"xmlns:r"
1994                && value == "http://schemas.openxmlformats.org/officeDocument/2006/relationships");
1995        if !canonical_namespace
1996            && !modeled
1997                .iter()
1998                .any(|local| is_word_attribute(key, local, word_prefixes))
1999        {
2000            attributes.push((std::str::from_utf8(key)?.to_owned(), value));
2001        }
2002    }
2003    if !attributes.is_empty() {
2004        for (prefix, namespace) in namespace_bindings {
2005            if prefix.is_empty()
2006                || (prefix == "w" && namespace == W_NS)
2007                || (prefix == "r"
2008                    && namespace
2009                        == "http://schemas.openxmlformats.org/officeDocument/2006/relationships")
2010            {
2011                continue;
2012            }
2013            let declaration = format!("xmlns:{prefix}");
2014            if !attributes.iter().any(|(name, _)| name == &declaration) {
2015                attributes.push((declaration, namespace.clone()));
2016            }
2017        }
2018    }
2019    Ok(attributes)
2020}
2021
2022fn reject_conflicting_style_bindings(bindings: &[(String, String)]) -> Result<()> {
2023    for (prefix, namespace) in bindings {
2024        let expected = match prefix.as_str() {
2025            "w" => Some(W_NS),
2026            "r" => Some("http://schemas.openxmlformats.org/officeDocument/2006/relationships"),
2027            _ => None,
2028        };
2029        if expected.is_some_and(|expected| namespace != expected) {
2030            return Err(OxmlError::InvalidValue(format!(
2031                "xmlns:{prefix} conflicts with the fixed style writer namespace"
2032            )));
2033        }
2034    }
2035    Ok(())
2036}
2037
2038fn push_style_attributes(element: &mut BytesStart<'_>, attributes: &[(String, String)]) {
2039    for (name, value) in attributes {
2040        element.push_attribute((name.as_str(), value.as_str()));
2041    }
2042}
2043
2044fn make_style_raw_self_contained(
2045    raw: &[u8],
2046    namespace_bindings: &[(String, String)],
2047) -> Result<Vec<u8>> {
2048    let external_bindings = namespace_bindings
2049        .iter()
2050        .filter(|(prefix, namespace)| {
2051            !((prefix == "w" && namespace == W_NS)
2052                || (prefix == "r"
2053                    && namespace
2054                        == "http://schemas.openxmlformats.org/officeDocument/2006/relationships"))
2055        })
2056        .cloned()
2057        .collect::<Vec<_>>();
2058    crate::text::raw_with_external_bindings(raw, &external_bindings)
2059}
2060
2061fn parse_style_table_properties(raw: &[u8], word_prefixes: &[String]) -> Result<CT_TblPr> {
2062    let mut reader = Reader::from_reader(raw);
2063    let mut buf = Vec::new();
2064    loop {
2065        match reader.read_event_into(&mut buf)? {
2066            Event::Start(ref start)
2067                if is_word_element(start.name().as_ref(), b"tblPr", word_prefixes) =>
2068            {
2069                let prefixes = word_prefixes_at(start, word_prefixes)?;
2070                return CT_TblPr::from_xml_with_prefixes(&mut reader, &prefixes);
2071            }
2072            Event::Eof => return Ok(CT_TblPr::default()),
2073            _ => {}
2074        }
2075        buf.clear();
2076    }
2077}
2078
2079fn explicit_style_element_has_only_trivia(raw: &[u8]) -> Result<bool> {
2080    let mut reader = Reader::from_reader(raw);
2081    let mut inside = false;
2082    let mut buf = Vec::new();
2083    loop {
2084        match reader.read_event_into(&mut buf)? {
2085            Event::Start(_) if !inside => inside = true,
2086            Event::End(_) if inside => return Ok(true),
2087            Event::Text(text) if inside && text.as_ref().iter().all(u8::is_ascii_whitespace) => {}
2088            Event::Comment(_) | Event::PI(_) if inside => {}
2089            Event::Eof => return Ok(false),
2090            _ if inside => return Ok(false),
2091            _ => {}
2092        }
2093        buf.clear();
2094    }
2095}
2096
2097/// The five typed property layers a `w:tblStylePr` region carries.
2098#[derive(Default)]
2099struct ConditionalStyleLayers {
2100    paragraph_properties: Option<CT_PPr>,
2101    run_properties: Option<Box<CT_RPr>>,
2102    table_properties: Option<CT_TblPr>,
2103    row_properties: Option<Box<CT_TrPr>>,
2104    cell_properties: Option<CT_TcPr>,
2105}
2106
2107fn parse_conditional_style_properties(
2108    raw: &[u8],
2109    word_prefixes: &[String],
2110) -> Result<ConditionalStyleLayers> {
2111    let mut reader = Reader::from_reader(raw);
2112    let mut layers = ConditionalStyleLayers::default();
2113    let mut buf = Vec::new();
2114    let mut depth = 0usize;
2115    loop {
2116        match reader.read_event_into(&mut buf)? {
2117            Event::Start(ref start) => {
2118                depth += 1;
2119                let prefixes = word_prefixes_at(start, word_prefixes)?;
2120                if depth != 2 {
2121                    // Only the region's own children are layers. Anything
2122                    // deeper belongs to a layer or to a preserved subtree.
2123                } else if is_word_element(start.name().as_ref(), b"pPr", &prefixes) {
2124                    let captured = capture_element(&mut reader, start)?;
2125                    layers.paragraph_properties = Some(parse_scoped_ppr(&captured, &prefixes)?);
2126                    depth -= 1;
2127                } else if is_word_element(start.name().as_ref(), b"rPr", &prefixes) {
2128                    let captured = capture_element(&mut reader, start)?;
2129                    layers.run_properties = Some(Box::new(parse_style_rpr(&captured, &prefixes)?));
2130                    depth -= 1;
2131                } else if is_word_element(start.name().as_ref(), b"tblPr", &prefixes) {
2132                    layers.table_properties =
2133                        Some(CT_TblPr::from_xml_with_prefixes(&mut reader, &prefixes)?);
2134                    depth -= 1;
2135                } else if is_word_element(start.name().as_ref(), b"trPr", &prefixes) {
2136                    layers.row_properties = Some(Box::new(CT_TrPr::from_xml_with_prefixes(
2137                        &mut reader,
2138                        &prefixes,
2139                    )?));
2140                    depth -= 1;
2141                } else if is_word_element(start.name().as_ref(), b"tcPr", &prefixes) {
2142                    layers.cell_properties =
2143                        Some(CT_TcPr::from_xml_with_prefixes(&mut reader, &prefixes)?);
2144                    depth -= 1;
2145                }
2146            }
2147            Event::End(_) => depth = depth.saturating_sub(1),
2148            Event::Eof => break,
2149            _ => {}
2150        }
2151        buf.clear();
2152    }
2153    Ok(layers)
2154}
2155
2156fn serialize_conditional_table_style(conditional: &CT_TblStylePr) -> Result<Vec<u8>> {
2157    let mut reader = Reader::from_reader(conditional.raw_xml.as_slice());
2158    let mut prefixes = vec!["w".to_owned()];
2159    let mut original_region = None;
2160    let mut buf = Vec::new();
2161    loop {
2162        match reader.read_event_into(&mut buf)? {
2163            Event::Start(ref start) => {
2164                prefixes = word_prefixes_at(start, &prefixes)?;
2165                original_region = get_word_attr(start, b"type", &prefixes)?;
2166                break;
2167            }
2168            Event::Eof => break,
2169            _ => {}
2170        }
2171        buf.clear();
2172    }
2173    let Some(region) = conditional.region else {
2174        // An unrecognised `w:type` was never projected, so its preserved bytes
2175        // are the only faithful serialisation of it.
2176        return Ok(conditional.raw_xml.clone());
2177    };
2178    let original = parse_conditional_style_properties(&conditional.raw_xml, &prefixes)?;
2179    if original_region.as_deref() == Some(region.to_str())
2180        && original.paragraph_properties == conditional.paragraph_properties
2181        && original.run_properties == conditional.run_properties
2182        && original.table_properties == conditional.table_properties
2183        && original.row_properties == conditional.row_properties
2184        && original.cell_properties == conditional.cell_properties
2185    {
2186        return Ok(conditional.raw_xml.clone());
2187    }
2188
2189    let mut extras = preserved_conditional_style_children(&conditional.raw_xml)?;
2190    extras.sort_by_key(|(rank, _)| *rank);
2191    let extra_refs = extras.iter().collect::<Vec<_>>();
2192    let mut extra_index = 0;
2193    let mut writer = Writer::new(Vec::new());
2194    let mut start = BytesStart::new("w:tblStylePr");
2195    start.push_attribute(("w:type", region.to_str()));
2196    push_style_attributes(&mut start, &conditional.extra_attributes);
2197    writer.write_event(Event::Start(start))?;
2198    write_style_extras(&mut writer, &extra_refs, &mut extra_index, 0)?;
2199    if let Some(properties) = &conditional.paragraph_properties {
2200        properties.to_xml(&mut writer)?;
2201    }
2202    write_style_extras(&mut writer, &extra_refs, &mut extra_index, 1)?;
2203    if let Some(properties) = &conditional.run_properties {
2204        properties.to_xml(&mut writer)?;
2205    }
2206    write_style_extras(&mut writer, &extra_refs, &mut extra_index, 2)?;
2207    if let Some(properties) = &conditional.table_properties {
2208        properties.to_xml(&mut writer)?;
2209    }
2210    write_style_extras(&mut writer, &extra_refs, &mut extra_index, 3)?;
2211    if let Some(properties) = &conditional.row_properties {
2212        properties.to_xml(&mut writer)?;
2213    }
2214    write_style_extras(&mut writer, &extra_refs, &mut extra_index, 4)?;
2215    if let Some(properties) = &conditional.cell_properties {
2216        properties.to_xml(&mut writer)?;
2217    }
2218    write_style_extras(&mut writer, &extra_refs, &mut extra_index, u8::MAX)?;
2219    writer.write_event(Event::End(BytesEnd::new("w:tblStylePr")))?;
2220    Ok(writer.into_inner())
2221}
2222
2223fn preserved_conditional_style_children(raw: &[u8]) -> Result<Vec<(u8, Vec<u8>)>> {
2224    let mut reader = Reader::from_reader(raw);
2225    let mut inside = false;
2226    let mut word_prefixes = vec!["w".to_owned()];
2227    let mut extras = Vec::new();
2228    let mut buf = Vec::new();
2229    loop {
2230        match reader.read_event_into(&mut buf)? {
2231            Event::Start(ref start) if !inside => {
2232                word_prefixes = word_prefixes_at(start, &word_prefixes)?;
2233                inside = true;
2234            }
2235            Event::Start(ref start) => {
2236                let prefixes = word_prefixes_at(start, &word_prefixes)?;
2237                let local = start.local_name();
2238                if is_word_element(start.name().as_ref(), local.as_ref(), &prefixes)
2239                    && matches!(
2240                        local.as_ref(),
2241                        b"pPr" | b"rPr" | b"tblPr" | b"trPr" | b"tcPr"
2242                    )
2243                {
2244                    reader.read_to_end_into(start.name(), &mut Vec::new())?;
2245                } else {
2246                    extras.push((5, capture_element(&mut reader, start)?));
2247                }
2248            }
2249            Event::Empty(ref empty) => {
2250                let prefixes = word_prefixes_at(empty, &word_prefixes)?;
2251                let local = empty.local_name();
2252                if !(is_word_element(empty.name().as_ref(), local.as_ref(), &prefixes)
2253                    && matches!(
2254                        local.as_ref(),
2255                        b"pPr" | b"rPr" | b"tblPr" | b"trPr" | b"tcPr"
2256                    ))
2257                {
2258                    extras.push((5, capture_empty_element(empty)?));
2259                }
2260            }
2261            Event::End(_) | Event::Eof => break,
2262            _ => {}
2263        }
2264        buf.clear();
2265    }
2266    Ok(extras)
2267}
2268
2269/// Extract the `w:val` attribute from an element.
2270fn get_val_attr(e: &BytesStart, word_prefixes: &[String]) -> Result<Option<String>> {
2271    get_word_attr(e, b"val", word_prefixes)
2272}
2273
2274fn get_word_attr(e: &BytesStart, local: &[u8], word_prefixes: &[String]) -> Result<Option<String>> {
2275    for attr in e.attributes() {
2276        let attr = attr?;
2277        if is_word_attribute(attr.key.as_ref(), local, word_prefixes) {
2278            return Ok(Some(std::str::from_utf8(&attr.value)?.to_string()));
2279        }
2280    }
2281    Ok(None)
2282}
2283
2284fn parse_style_toggle(e: &BytesStart, word_prefixes: &[String]) -> Result<Option<bool>> {
2285    let value = get_word_attr(e, b"val", word_prefixes)?;
2286    match value.as_deref() {
2287        None | Some("1" | "true" | "on") => Ok(Some(true)),
2288        Some("0" | "false" | "off") => Ok(Some(false)),
2289        Some(value) => Err(OxmlError::InvalidValue(format!(
2290            "invalid style toggle value: {value}"
2291        ))),
2292    }
2293}
2294
2295fn parse_style_priority(e: &BytesStart, word_prefixes: &[String]) -> Result<Option<u32>> {
2296    get_word_attr(e, b"val", word_prefixes)?
2297        .map(|value| {
2298            value
2299                .parse::<u32>()
2300                .map_err(|_| OxmlError::InvalidValue(format!("invalid style UI priority: {value}")))
2301        })
2302        .transpose()
2303}
2304
2305fn preserved_style_attribute(raw: &[u8], local: &[u8]) -> Result<Option<String>> {
2306    let mut reader = Reader::from_reader(raw);
2307    let mut buf = Vec::new();
2308    loop {
2309        match reader.read_event_into(&mut buf)? {
2310            Event::Start(ref start) | Event::Empty(ref start) => {
2311                let prefixes = word_prefixes_at(start, &["w".to_owned()])?;
2312                return get_word_attr(start, local, &prefixes);
2313            }
2314            Event::Eof => return Ok(None),
2315            _ => {}
2316        }
2317        buf.clear();
2318    }
2319}
2320
2321fn preserved_modeled_xml(modeled_xml: &[(u8, Vec<u8>)], rank: u8) -> Option<&[u8]> {
2322    modeled_xml
2323        .iter()
2324        .find(|(candidate, _)| *candidate == rank)
2325        .map(|(_, raw)| raw.as_slice())
2326}
2327
2328fn rewritten_style_scalar_start(
2329    element: &BytesStart<'_>,
2330    value: &str,
2331) -> Result<BytesStart<'static>> {
2332    let prefixes = word_prefixes_at(element, &["w".to_owned()])?;
2333    let element_name = std::str::from_utf8(element.name().as_ref())?.to_owned();
2334    let mut rewritten = BytesStart::new(element_name.clone());
2335    let mut wrote_value = false;
2336    for attribute in element.attributes() {
2337        let attribute = attribute?;
2338        let key = std::str::from_utf8(attribute.key.as_ref())?.to_owned();
2339        if is_word_attribute(attribute.key.as_ref(), b"val", &prefixes) {
2340            if !wrote_value {
2341                rewritten.push_attribute((key.as_str(), value));
2342                wrote_value = true;
2343            }
2344        } else {
2345            let attribute_value = attribute
2346                .decoded_and_normalized_value(XmlVersion::Implicit1_0, element.decoder())?
2347                .into_owned();
2348            rewritten.push_attribute((key.as_str(), attribute_value.as_str()));
2349        }
2350    }
2351    if !wrote_value {
2352        let element_prefix = element_name
2353            .split_once(':')
2354            .map(|(prefix, _)| prefix.to_owned());
2355        let existing_prefix = element_prefix.or_else(|| {
2356            prefixes
2357                .iter()
2358                .find(|prefix| !prefix.is_empty() && !prefix.starts_with('\0'))
2359                .cloned()
2360        });
2361        let prefix = existing_prefix.unwrap_or_else(|| {
2362            let mut candidate = "rdocxStyle".to_owned();
2363            let mut suffix = 1usize;
2364            while element
2365                .attributes()
2366                .filter_map(std::result::Result::ok)
2367                .any(|attribute| attribute.key.as_ref() == format!("xmlns:{candidate}").as_bytes())
2368            {
2369                candidate = format!("rdocxStyle{suffix}");
2370                suffix += 1;
2371            }
2372            let declaration = format!("xmlns:{candidate}");
2373            rewritten.push_attribute((declaration.as_str(), W_NS));
2374            candidate
2375        });
2376        let key = format!("{prefix}:val");
2377        rewritten.push_attribute((key.as_str(), value));
2378    }
2379    Ok(rewritten.into_owned())
2380}
2381
2382fn write_rewritten_style_scalar<W: std::io::Write>(
2383    writer: &mut Writer<W>,
2384    raw: &[u8],
2385    value: &str,
2386) -> Result<()> {
2387    let mut reader = Reader::from_reader(raw);
2388    let mut root_pending = true;
2389    let mut buf = Vec::new();
2390    loop {
2391        match reader.read_event_into(&mut buf)? {
2392            Event::Start(element) if root_pending => {
2393                root_pending = false;
2394                writer.write_event(Event::Start(rewritten_style_scalar_start(&element, value)?))?;
2395            }
2396            Event::Empty(element) if root_pending => {
2397                writer.write_event(Event::Empty(rewritten_style_scalar_start(&element, value)?))?;
2398                break;
2399            }
2400            Event::Eof => break,
2401            event => writer.write_event(event.into_owned())?,
2402        }
2403        buf.clear();
2404    }
2405    Ok(())
2406}
2407
2408fn write_style_string<W: std::io::Write>(
2409    writer: &mut Writer<W>,
2410    name: &str,
2411    rank: u8,
2412    value: Option<&str>,
2413    modeled_xml: &[(u8, Vec<u8>)],
2414) -> Result<()> {
2415    let Some(value) = value else {
2416        return Ok(());
2417    };
2418    if let Some(raw) = preserved_modeled_xml(modeled_xml, rank)
2419        && preserved_style_attribute(raw, b"val")?.as_deref() == Some(value)
2420    {
2421        writer.get_mut().write_all(raw)?;
2422        return Ok(());
2423    }
2424    if let Some(raw) = preserved_modeled_xml(modeled_xml, rank) {
2425        return write_rewritten_style_scalar(writer, raw, value);
2426    }
2427    let mut element = BytesStart::new(name);
2428    element.push_attribute(("w:val", value));
2429    writer.write_event(Event::Empty(element))?;
2430    Ok(())
2431}
2432
2433fn write_style_toggle<W: std::io::Write>(
2434    writer: &mut Writer<W>,
2435    name: &str,
2436    rank: u8,
2437    value: Option<bool>,
2438    modeled_xml: &[(u8, Vec<u8>)],
2439) -> Result<()> {
2440    if let Some(value) = value {
2441        if let Some(raw) = preserved_modeled_xml(modeled_xml, rank) {
2442            let prefixes = vec!["w".to_owned()];
2443            let mut reader = Reader::from_reader(raw);
2444            let mut buf = Vec::new();
2445            let preserved = loop {
2446                match reader.read_event_into(&mut buf)? {
2447                    Event::Start(ref start) | Event::Empty(ref start) => {
2448                        let prefixes = word_prefixes_at(start, &prefixes)?;
2449                        break parse_style_toggle(start, &prefixes)?;
2450                    }
2451                    Event::Eof => break None,
2452                    _ => {}
2453                }
2454                buf.clear();
2455            };
2456            if preserved == Some(value) {
2457                writer.get_mut().write_all(raw)?;
2458                return Ok(());
2459            }
2460            return write_rewritten_style_scalar(writer, raw, if value { "1" } else { "0" });
2461        }
2462        let mut element = BytesStart::new(name);
2463        if !value {
2464            element.push_attribute(("w:val", "0"));
2465        }
2466        writer.write_event(Event::Empty(element))?;
2467    }
2468    Ok(())
2469}
2470
2471fn write_style_priority<W: std::io::Write>(
2472    writer: &mut Writer<W>,
2473    priority: Option<u32>,
2474    modeled_xml: &[(u8, Vec<u8>)],
2475) -> Result<()> {
2476    let Some(priority) = priority else {
2477        return Ok(());
2478    };
2479    if let Some(raw) = preserved_modeled_xml(modeled_xml, 7)
2480        && preserved_style_attribute(raw, b"val")?
2481            .as_deref()
2482            .and_then(|value| value.parse::<u32>().ok())
2483            == Some(priority)
2484    {
2485        writer.get_mut().write_all(raw)?;
2486        return Ok(());
2487    }
2488    let value = priority.to_string();
2489    if let Some(raw) = preserved_modeled_xml(modeled_xml, 7) {
2490        return write_rewritten_style_scalar(writer, raw, &value);
2491    }
2492    let mut element = BytesStart::new("w:uiPriority");
2493    element.push_attribute(("w:val", value.as_str()));
2494    writer.write_event(Event::Empty(element))?;
2495    Ok(())
2496}
2497
2498#[cfg(test)]
2499mod tests {
2500    use super::*;
2501
2502    #[test]
2503    fn unmodelled_root_children_round_trip_before_the_styles() {
2504        let xml = format!(
2505            concat!(
2506                r#"<w:styles xmlns:w="{}" xmlns:x="urn:x">"#,
2507                r#"<w:docDefaults/>"#,
2508                r#"<w:latentStyles w:count="1"><w:lsdException w:name="Normal" x:a="1"/></w:latentStyles>"#,
2509                r#"<x:empty/>"#,
2510                r#"<w:style w:type="paragraph" w:styleId="Empty"/>"#,
2511                r#"<w:style w:type="paragraph" w:styleId="Normal"><w:name w:val="Normal"/></w:style>"#,
2512                r#"</w:styles>"#,
2513            ),
2514            W_NS
2515        );
2516        let styles = CT_Styles::from_xml(xml.as_bytes()).unwrap();
2517        assert_eq!(styles.extra_xml.len(), 2);
2518        assert_eq!(styles.styles.len(), 2);
2519        assert_eq!(styles.styles[0].style_id, "Empty");
2520        let written = String::from_utf8(styles.to_xml().unwrap()).unwrap();
2521        let latent = written
2522            .find(concat!(
2523                r#"<w:latentStyles w:count="1" xmlns:x="urn:x">"#,
2524                r#"<w:lsdException w:name="Normal" x:a="1"/></w:latentStyles>"#,
2525            ))
2526            .unwrap_or_else(|| panic!("{written}"));
2527        let empty = written.find(r#"<x:empty xmlns:x="urn:x"/>"#).unwrap();
2528        let empty_style = written
2529            .find(r#"<w:style w:type="paragraph" w:styleId="Empty">"#)
2530            .unwrap();
2531        assert!(written.find("<w:docDefaults").unwrap() < latent);
2532        assert!(latent < empty);
2533        assert!(empty < empty_style);
2534        assert_eq!(CT_Styles::from_xml(written.as_bytes()).unwrap(), styles);
2535    }
2536
2537    #[test]
2538    fn round_trip_styles() {
2539        let styles = CT_Styles::new_default();
2540        let xml = styles.to_xml().unwrap();
2541        let parsed = CT_Styles::from_xml(&xml).unwrap();
2542
2543        // Namespace-close only the retained owners. Every other byte remains exact.
2544        let original = String::from_utf8(xml.clone()).unwrap();
2545        let (prefix, heading) = original
2546            .split_once(r#"<w:style w:type="paragraph" w:styleId="Heading1">"#)
2547            .unwrap();
2548        assert_eq!(prefix.matches("<w:docDefaults>").count(), 1);
2549        let prefix = prefix.replace(
2550            "<w:docDefaults>",
2551            &format!("<w:docDefaults xmlns:w=\"{W_NS}\">"),
2552        );
2553        let mut heading = heading.to_owned();
2554        for owner in ["pPr", "rPr"] {
2555            let opening = format!("<w:{owner}>");
2556            assert_eq!(heading.matches(&opening).count(), 1);
2557            heading = heading.replace(&opening, &format!("<w:{owner} xmlns:w=\"{W_NS}\">"));
2558        }
2559        let expected =
2560            format!(r#"{prefix}<w:style w:type="paragraph" w:styleId="Heading1">{heading}"#);
2561        let imported = parsed.to_xml().unwrap();
2562        assert_eq!(imported, expected.as_bytes());
2563        let reopened = CT_Styles::from_xml(&imported).unwrap();
2564        assert_eq!(reopened.to_xml().unwrap(), imported);
2565        assert_eq!(reopened, parsed);
2566
2567        assert_eq!(parsed.styles.len(), 2);
2568        assert!(parsed.doc_defaults.is_some());
2569
2570        let normal = parsed.get_by_id("Normal").unwrap();
2571        assert_eq!(normal.name, Some("Normal".to_string()));
2572        assert!(normal.is_default);
2573
2574        let h1 = parsed.get_by_id("Heading1").unwrap();
2575        assert_eq!(h1.based_on, Some("Normal".to_string()));
2576    }
2577
2578    #[test]
2579    fn find_default_style() {
2580        let styles = CT_Styles::new_default();
2581        let default_para = styles.get_default(StyleType::Paragraph).unwrap();
2582        assert_eq!(default_para.style_id, "Normal");
2583    }
2584
2585    #[test]
2586    fn linked_style_and_ui_flags_accept_aliases_and_serialize_in_schema_order() {
2587        let xml = format!(
2588            r#"<q:styles xmlns:q="{W_NS}" xmlns:x="urn:producer"><q:style q:type="paragraph" q:styleId="Corpus" x:root="&amp;"><q:name q:val="Corpus"> <!--name-trivia--> </q:name><q:next q:val="Normal"><?next kept?></q:next><q:link q:val="CorpusChar"> <!--link-trivia--> </q:link><q:autoRedefine q:val="0" x:keep="yes"/><q:hidden> </q:hidden><q:uiPriority q:val="17"></q:uiPriority><q:semiHidden q:val="false"></q:semiHidden><q:unhideWhenUsed></q:unhideWhenUsed><q:qFormat q:val="1"></q:qFormat><q:locked q:val="true"></q:locked><q:pPr><q:spacing q:after="0"/></q:pPr></q:style></q:styles>"#
2589        );
2590        let mut styles = CT_Styles::from_xml(xml.as_bytes()).unwrap();
2591        let style = styles.get_by_id("Corpus").unwrap();
2592        assert_eq!(style.next_style.as_deref(), Some("Normal"));
2593        assert_eq!(style.linked_style.as_deref(), Some("CorpusChar"));
2594        assert_eq!(style.auto_redefine, Some(false));
2595        assert_eq!(style.hidden, Some(true));
2596        assert_eq!(style.ui_priority, Some(17));
2597        assert_eq!(style.semi_hidden, Some(false));
2598        assert_eq!(style.unhide_when_used, Some(true));
2599        assert_eq!(style.quick_format, Some(true));
2600        assert_eq!(style.locked, Some(true));
2601
2602        let serialized = String::from_utf8(styles.to_xml().unwrap()).unwrap();
2603        let positions = [
2604            "<q:next",
2605            "<q:link",
2606            "<q:autoRedefine",
2607            "<q:hidden",
2608            "<q:uiPriority",
2609            "<q:semiHidden",
2610            "<q:unhideWhenUsed",
2611            "<q:qFormat",
2612            "<q:locked",
2613            "<q:pPr",
2614        ]
2615        .map(|token| serialized.find(token).unwrap());
2616        assert!(positions.windows(2).all(|pair| pair[0] < pair[1]));
2617        assert!(serialized.contains(&format!(
2618            r#"<q:pPr xmlns:q="{W_NS}"><q:spacing q:after="0"/></q:pPr>"#
2619        )));
2620        assert_eq!(serialized.matches("name-trivia").count(), 1);
2621        assert_eq!(serialized.matches("next kept").count(), 1);
2622        assert_eq!(serialized.matches("link-trivia").count(), 1);
2623        assert_eq!(serialized.matches(r#"x:root="&amp;""#).count(), 1);
2624        assert_eq!(serialized.matches(r#"x:keep="yes""#).count(), 1);
2625        let reparsed = CT_Styles::from_xml(serialized.as_bytes()).unwrap();
2626        assert_eq!(reparsed.styles[0], styles.styles[0]);
2627
2628        styles.styles[0].next_style = Some("Other".to_owned());
2629        styles.styles[0].auto_redefine = Some(true);
2630        styles.styles[0].ui_priority = Some(23);
2631        let changed = String::from_utf8(styles.to_xml().unwrap()).unwrap();
2632        assert!(changed.contains("<q:next"));
2633        assert!(changed.contains(r#"q:val="Other""#));
2634        assert_eq!(changed.matches("next kept").count(), 1);
2635        assert!(changed.contains("<q:autoRedefine"));
2636        assert!(changed.contains(r#"q:val="1""#));
2637        assert_eq!(changed.matches(r#"x:keep="yes""#).count(), 1);
2638        assert!(changed.contains("<q:uiPriority"));
2639        assert!(changed.contains(r#"q:val="23""#));
2640        let reparsed = CT_Styles::from_xml(changed.as_bytes()).unwrap();
2641        assert_eq!(reparsed.styles[0].next_style.as_deref(), Some("Other"));
2642        assert_eq!(reparsed.styles[0].auto_redefine, Some(true));
2643        assert_eq!(reparsed.styles[0].ui_priority, Some(23));
2644    }
2645
2646    #[test]
2647    fn style_parser_rejects_prefixes_that_conflict_with_fixed_writer_bindings() {
2648        let xml = format!(
2649            r#"<q:styles xmlns:q="{W_NS}" xmlns:w="urn:not-word"><q:style q:type="paragraph" q:styleId="Corpus"></q:style></q:styles>"#
2650        );
2651        let error = CT_Styles::from_xml(xml.as_bytes()).unwrap_err();
2652        assert!(error.to_string().contains("xmlns:w"));
2653    }
2654
2655    #[test]
2656    fn changed_table_style_properties_keep_one_unknown_child_in_schema_order() {
2657        let xml = format!(
2658            r#"<q:styles xmlns:q="{W_NS}" xmlns:x="urn:producer"><q:style q:type="table" q:styleId="Corpus"><q:tblPr><q:tblStyleRowBandSize q:val="2"/><x:producer x:exact="kept"/><q:shd q:val="clear" q:fill="EEEEEE"/></q:tblPr></q:style></q:styles>"#
2659        );
2660        let mut styles = CT_Styles::from_xml(xml.as_bytes()).unwrap();
2661        styles.styles[0]
2662            .table_properties
2663            .as_mut()
2664            .unwrap()
2665            .shading
2666            .as_mut()
2667            .unwrap()
2668            .fill = Some("D9EAF7".to_owned());
2669
2670        let serialized = String::from_utf8(styles.to_xml().unwrap()).unwrap();
2671        assert_eq!(serialized.matches("tblStyleRowBandSize").count(), 1);
2672        assert_eq!(serialized.matches("x:producer").count(), 1);
2673        assert!(
2674            serialized.find("tblStyleRowBandSize").unwrap() < serialized.find("w:shd").unwrap()
2675        );
2676        assert_eq!(
2677            CT_Styles::from_xml(serialized.as_bytes()).unwrap().styles[0]
2678                .table_properties
2679                .as_ref()
2680                .and_then(|properties| properties.shading.as_ref())
2681                .and_then(|shading| shading.fill.as_deref()),
2682            Some("D9EAF7")
2683        );
2684    }
2685
2686    #[test]
2687    fn aliased_style_paragraph_properties_use_ancestor_namespace_scope() {
2688        let xml = format!(
2689            r#"<q:styles xmlns:q="{W_NS}" xmlns:ext="urn:producer"><q:style q:type="paragraph" q:styleId="Alias"><q:pPr><ext:jc ext:val="right"/><q:jc q:val="center"/></q:pPr></q:style></q:styles>"#
2690        );
2691        let parsed = CT_Styles::from_xml(xml.as_bytes()).unwrap();
2692        let ppr = parsed.styles[0].ppr.as_ref().unwrap();
2693        assert_eq!(ppr.jc, Some(crate::shared::ST_Jc::Center));
2694    }
2695
2696    #[test]
2697    fn direct_style_parser_uses_supplied_start_ancestor_scope() {
2698        let xml = format!(
2699            r#"<outer xmlns:ext="urn:producer"><q:style xmlns:q="{W_NS}" q:type="paragraph" q:styleId="Direct"><ext:pPr><ext:jc ext:val="right"/></ext:pPr><q:pPr><ext:jc ext:val="right"/><q:jc q:val="center"/></q:pPr></q:style></outer>"#
2700        );
2701        let mut reader = Reader::from_str(&xml);
2702        let mut buf = Vec::new();
2703        let parsed = loop {
2704            match reader.read_event_into(&mut buf) {
2705                Ok(Event::Start(ref element)) if element.local_name().as_ref() == b"style" => {
2706                    break CT_Style::from_xml(&mut reader, element).unwrap();
2707                }
2708                Ok(Event::Eof) => panic!("missing style"),
2709                event => {
2710                    event.unwrap();
2711                }
2712            }
2713            buf.clear();
2714        };
2715        assert_eq!(
2716            parsed.ppr.as_ref().unwrap().jc,
2717            Some(crate::shared::ST_Jc::Center)
2718        );
2719    }
2720
2721    #[test]
2722    fn direct_style_parser_does_not_promote_foreign_start_prefix() {
2723        let xml = r#"<outer><ext:style xmlns:ext="urn:producer" ext:type="paragraph" ext:styleId="Foreign"><ext:pPr><ext:jc ext:val="right"/></ext:pPr></ext:style></outer>"#;
2724        let mut reader = Reader::from_str(xml);
2725        let mut buf = Vec::new();
2726        let parsed = loop {
2727            match reader.read_event_into(&mut buf) {
2728                Ok(Event::Start(ref element)) if element.local_name().as_ref() == b"style" => {
2729                    break CT_Style::from_xml(&mut reader, element).unwrap();
2730                }
2731                Ok(Event::Eof) => panic!("missing style"),
2732                event => {
2733                    event.unwrap();
2734                }
2735            }
2736            buf.clear();
2737        };
2738        assert!(parsed.ppr.is_none());
2739    }
2740
2741    #[test]
2742    fn direct_style_parser_accepts_default_word_namespace() {
2743        let xml = format!(
2744            r#"<outer xmlns:ext="urn:producer"><style xmlns="{W_NS}" xmlns:w="{W_NS}" w:type="paragraph" w:styleId="Direct"><ext:pPr><ext:jc ext:val="right"/></ext:pPr><pPr><ext:jc ext:val="right"/><jc w:val="center"/></pPr></style></outer>"#
2745        );
2746        let mut reader = Reader::from_str(&xml);
2747        let mut buf = Vec::new();
2748        let parsed = loop {
2749            match reader.read_event_into(&mut buf) {
2750                Ok(Event::Start(ref element)) if element.local_name().as_ref() == b"style" => {
2751                    break CT_Style::from_xml(&mut reader, element).unwrap();
2752                }
2753                Ok(Event::Eof) => panic!("missing style"),
2754                event => {
2755                    event.unwrap();
2756                }
2757            }
2758            buf.clear();
2759        };
2760        assert_eq!(
2761            parsed.ppr.as_ref().unwrap().jc,
2762            Some(crate::shared::ST_Jc::Center)
2763        );
2764    }
2765
2766    #[test]
2767    fn table_style_properties_are_namespace_aware_schema_ordered_and_preserved() {
2768        let xml = format!(
2769            r#"<q:styles xmlns:q="{W_NS}" xmlns:bad="urn:not-word" xmlns:x="urn:region"><q:style q:type="table" q:styleId="Dense"><bad:tblPr><bad:tblBorders><bad:top bad:val="double"/></bad:tblBorders></bad:tblPr><q:pPr><q:spacing q:after="40"/></q:pPr><q:rPr><q:b/></q:rPr><q:tblPr><q:tblBorders><q:top q:val="single" q:sz="8" q:color="112233"/></q:tblBorders><ext:keep xmlns:ext="urn:producer" ext:value="byte-identical"/></q:tblPr><q:tblStylePr q:type="firstRow" x:region="&amp;"><q:pPr><q:spacing q:after="0"/></q:pPr><ext:conditional xmlns:ext="urn:producer" ext:value="preserved"/><q:tcPr><q:shd q:val="clear" q:fill="AABBCC"/></q:tcPr></q:tblStylePr></q:style></q:styles>"#
2770        );
2771        let mut styles = CT_Styles::from_xml(xml.as_bytes()).unwrap();
2772        let style = styles.get_by_id("Dense").unwrap();
2773        assert_eq!(
2774            style
2775                .table_properties
2776                .as_ref()
2777                .and_then(|properties| properties.borders.as_ref())
2778                .and_then(|borders| borders.top.as_ref())
2779                .and_then(|border| border.sz),
2780            Some(8)
2781        );
2782        assert_eq!(style.conditional_table_styles.len(), 1);
2783        assert_eq!(
2784            style.conditional_table_styles[0].region,
2785            Some(TableStyleRegion::FirstRow)
2786        );
2787
2788        let serialized = String::from_utf8(styles.to_xml().unwrap()).unwrap();
2789        assert_eq!(serialized.matches("<q:tblPr").count(), 1);
2790        assert!(
2791            serialized
2792                .contains(r#"<ext:keep xmlns:ext="urn:producer" ext:value="byte-identical"/>"#)
2793        );
2794        let ppr = serialized.find("<q:pPr").unwrap();
2795        let rpr = serialized.find("<q:rPr").unwrap();
2796        let table_properties = serialized.find("<q:tblPr").unwrap();
2797        let conditional = serialized.find("<q:tblStylePr").unwrap();
2798        assert!(ppr < rpr && rpr < table_properties && table_properties < conditional);
2799        assert!(serialized.contains(&format!(
2800            r#"<q:pPr xmlns:q="{W_NS}"><q:spacing q:after="40"/></q:pPr>"#
2801        )));
2802        assert!(serialized.contains(&format!(r#"<q:rPr xmlns:q="{W_NS}"><q:b/></q:rPr>"#)));
2803        assert!(serialized.contains(r#"<bad:tblPr xmlns:bad="urn:not-word">"#));
2804        CT_Styles::from_xml(serialized.as_bytes()).expect("preserved prefixes remain bound");
2805
2806        styles.styles[0]
2807            .table_properties
2808            .as_mut()
2809            .unwrap()
2810            .borders
2811            .as_mut()
2812            .unwrap()
2813            .top
2814            .as_mut()
2815            .unwrap()
2816            .color = Some("445566".to_owned());
2817        let changed = String::from_utf8(styles.to_xml().unwrap()).unwrap();
2818        assert_eq!(changed.matches("<w:tblPr>").count(), 1);
2819        assert_eq!(changed.matches("445566").count(), 1);
2820        assert!(
2821            changed.contains(r#"<ext:keep xmlns:ext="urn:producer" ext:value="byte-identical"/>"#)
2822        );
2823        styles.styles[0].conditional_table_styles[0]
2824            .cell_properties
2825            .as_mut()
2826            .unwrap()
2827            .shading
2828            .as_mut()
2829            .unwrap()
2830            .fill = Some("DDEEFF".to_owned());
2831        styles.styles[0].conditional_table_styles[0].region = Some(TableStyleRegion::LastRow);
2832        let changed = String::from_utf8(styles.to_xml().unwrap()).unwrap();
2833        assert_eq!(changed.matches("DDEEFF").count(), 1);
2834        assert!(!changed.contains("AABBCC"));
2835        assert!(changed.contains(r#"<w:tblStylePr w:type="lastRow" x:region="&amp;""#));
2836        assert_eq!(changed.matches(r#"x:region="&amp;""#).count(), 1);
2837        assert!(!changed.contains(r#"tblStylePr q:type="firstRow""#));
2838        assert!(
2839            changed
2840                .contains(r#"<ext:conditional xmlns:ext="urn:producer" ext:value="preserved"/>"#)
2841        );
2842        let reparsed = CT_Styles::from_xml(changed.as_bytes())
2843            .expect("typed conditional projection remains valid XML");
2844        assert_eq!(
2845            reparsed.styles[0].conditional_table_styles[0].region,
2846            Some(TableStyleRegion::LastRow)
2847        );
2848        assert_eq!(
2849            reparsed.styles[0].conditional_table_styles[0]
2850                .cell_properties
2851                .as_ref()
2852                .and_then(|properties| properties.shading.as_ref())
2853                .and_then(|shading| shading.fill.as_deref()),
2854            Some("DDEEFF")
2855        );
2856    }
2857    #[test]
2858    fn fx200_defaults_source_preserves_unknown_wrappers_and_qualified_owner_after_font_edit() {
2859        let foreign =
2860            r#"<f:rPrDefault xmlns:f="urn:foreign" f:flag='&#x41;'><f:rPr/></f:rPrDefault>"#;
2861        let opaque = r#"<z:keep xmlns:z="urn:opaque" z:value='&#65;'><z:child/></z:keep>"#;
2862        let source = format!(
2863            r#"<q:styles xmlns:q="{W_NS}" xmlns:x="urn:attr"><q:docDefaults x:root='&#65;'><!--before-->{foreign}<q:rPrDefault x:wrapper='keep'>  {opaque}<q:rPr x:property='&#65;'><q:rFonts q:ascii="Prior" q:hAnsi="Other"/><q:b q:val='true'/><!--inside-->{opaque}</q:rPr><!--after--></q:rPrDefault><q:pPrDefault x:unchanged='&#x42;'><q:pPr x:property='&#66;'><q:keepNext/></q:pPr>{opaque}</q:pPrDefault></q:docDefaults></q:styles>"#
2864        );
2865        let mut styles = CT_Styles::from_xml(source.as_bytes()).unwrap();
2866        let defaults = styles.doc_defaults.as_ref().unwrap();
2867        assert_eq!(
2868            defaults.rpr.as_ref().unwrap().font_ascii.as_deref(),
2869            Some("Prior")
2870        );
2871        assert_eq!(defaults.rpr.as_ref().unwrap().bold, Some(true));
2872        let original_ppr = defaults.ppr.clone();
2873        let original_source = defaults.source_xml.clone().unwrap();
2874        let mut untouched = Writer::new(Vec::new());
2875        defaults.to_xml(&mut untouched).unwrap();
2876        assert_eq!(untouched.into_inner(), original_source);
2877        let properties = styles.doc_defaults.as_mut().unwrap().rpr.as_mut().unwrap();
2878        properties.font_ascii = Some("Selected".to_owned());
2879        properties.font_hansi = Some("Selected".to_owned());
2880        let saved = styles.to_xml().unwrap();
2881        let text = std::str::from_utf8(&saved).unwrap();
2882        assert!(text.contains(foreign) && text.contains(opaque));
2883        assert!(text.contains("x:root='&#65;'") && text.contains("x:wrapper='keep'"));
2884        assert!(text.contains("<!--before-->") && text.contains("<!--after-->"));
2885        assert!(text.contains("x:property='&#65;'"));
2886        assert!(text.contains("<q:b q:val='true'/><!--inside-->"));
2887        let original_text = std::str::from_utf8(&original_source).unwrap();
2888        let begin = original_text.find("<q:pPrDefault").unwrap();
2889        let end = original_text.find("</q:pPrDefault>").unwrap() + "</q:pPrDefault>".len();
2890        assert!(text.contains(&original_text[begin..end]));
2891        let reopened = CT_Styles::from_xml(&saved).unwrap();
2892        let defaults = reopened.doc_defaults.as_ref().unwrap();
2893        assert_eq!(
2894            defaults.rpr.as_ref().unwrap().font_ascii.as_deref(),
2895            Some("Selected")
2896        );
2897        assert_eq!(defaults.rpr.as_ref().unwrap().bold, Some(true));
2898        assert_eq!(defaults.ppr, original_ppr);
2899    }
2900
2901    #[test]
2902    fn fx200_empty_default_wrapper_and_duplicate_wrapper_last_owner_are_preserved() {
2903        let source = format!(
2904            r#"<w:styles xmlns:w="{W_NS}" xmlns:f="urn:foreign"><w:docDefaults f:keep='yes'/></w:styles>"#
2905        );
2906        let styles = CT_Styles::from_xml(source.as_bytes()).unwrap();
2907        let defaults = styles.doc_defaults.as_ref().unwrap();
2908        assert!(defaults.rpr.is_none() && defaults.ppr.is_none());
2909        assert!(
2910            std::str::from_utf8(&styles.to_xml().unwrap())
2911                .unwrap()
2912                .contains("f:keep='yes'")
2913        );
2914        let source = format!(
2915            r#"<w:styles xmlns:w="{W_NS}"><w:docDefaults><w:rPrDefault><w:rPr><w:b/></w:rPr></w:rPrDefault><w:rPrDefault><!--last--></w:rPrDefault></w:docDefaults></w:styles>"#
2916        );
2917        let mut styles = CT_Styles::from_xml(source.as_bytes()).unwrap();
2918        assert!(styles.doc_defaults.as_ref().unwrap().rpr.is_none());
2919        styles.doc_defaults.as_mut().unwrap().rpr = Some(CT_RPr {
2920            font_ascii: Some("Selected".to_owned()),
2921            ..Default::default()
2922        });
2923        let saved = styles.to_xml().unwrap();
2924        assert!(std::str::from_utf8(&saved).unwrap().contains("<w:b/>"));
2925        let reopened = CT_Styles::from_xml(&saved).unwrap();
2926        let properties = reopened.doc_defaults.unwrap().rpr.unwrap();
2927        assert_eq!(properties.font_ascii.as_deref(), Some("Selected"));
2928        assert!(properties.bold.is_none());
2929    }
2930    #[test]
2931    fn fx200_same_wrapper_duplicate_default_properties_do_not_resurface_on_font_clear() {
2932        let source = format!(
2933            r#"<w:styles xmlns:w="{W_NS}" xmlns:f="urn:foreign"><w:docDefaults><w:rPrDefault><w:rPr><w:b/></w:rPr><w:rPr f:last='&#65;'><w:rFonts w:ascii="Selected"/></w:rPr></w:rPrDefault></w:docDefaults></w:styles>"#
2934        );
2935        let mut styles = CT_Styles::from_xml(source.as_bytes()).unwrap();
2936        let properties = styles.doc_defaults.as_mut().unwrap().rpr.as_mut().unwrap();
2937        assert!(properties.bold.is_none());
2938        assert_eq!(properties.font_ascii.as_deref(), Some("Selected"));
2939        properties.font_ascii = None;
2940        let saved = styles.to_xml().unwrap();
2941        let text = std::str::from_utf8(&saved).unwrap();
2942        assert!(text.contains("<w:b/>") && text.contains("f:last='&#65;'"));
2943        let reopened = CT_Styles::from_xml(&saved).unwrap();
2944        let properties = reopened.doc_defaults.unwrap().rpr.unwrap();
2945        assert!(properties.bold.is_none() && properties.font_ascii.is_none());
2946    }
2947    #[test]
2948    fn fx200_duplicate_default_font_children_clear_owned_slots_without_reviving_or_erasing_raw() {
2949        for replacement in [None, Some("B")] {
2950            let source = format!(
2951                r#"<w:styles xmlns:w="{W_NS}" xmlns:q="{W_NS}" xmlns:f="urn:foreign"><w:docDefaults><w:rPrDefault><w:rPr><q:rFonts q:ascii='A' f:earlier='&#65;'><f:opaque/><!--font-child--></q:rFonts><!--between--><w:rFonts w:hint='cs' f:last='&#66;'/></w:rPr></w:rPrDefault></w:docDefaults></w:styles>"#
2952            );
2953            let mut styles = CT_Styles::from_xml(source.as_bytes()).unwrap();
2954            let properties = styles.doc_defaults.as_mut().unwrap().rpr.as_mut().unwrap();
2955            assert_eq!(properties.font_ascii.as_deref(), Some("A"));
2956            assert_eq!(properties.font_hint.as_deref(), Some("cs"));
2957            properties.font_ascii = replacement.map(str::to_owned);
2958            let saved = styles.to_xml().unwrap();
2959            let text = std::str::from_utf8(&saved).unwrap();
2960            for raw in [
2961                "f:earlier='&#65;'",
2962                "<f:opaque/><!--font-child-->",
2963                "<!--between-->",
2964                "w:hint='cs'",
2965                "f:last='&#66;'",
2966            ] {
2967                assert!(text.contains(raw), "missing {raw}: {text}");
2968            }
2969            assert!(!text.contains("q:ascii='A'"));
2970            let reopened = CT_Styles::from_xml(&saved).unwrap();
2971            let properties = reopened.doc_defaults.unwrap().rpr.unwrap();
2972            assert_eq!(properties.font_ascii.as_deref(), replacement);
2973            assert_eq!(properties.font_hint.as_deref(), Some("cs"));
2974        }
2975    }
2976
2977    #[test]
2978    fn fx200_duplicate_default_size_children_clear_all_owned_values_preserving_foreign_payload() {
2979        let source = format!(
2980            r#"<w:styles xmlns:w="{W_NS}" xmlns:q="{W_NS}" xmlns:f="urn:foreign"><w:docDefaults><w:rPrDefault><w:rPr><q:sz q:val='24' f:first='&#65;'/><!--size--><w:sz w:val='28' f:last='&#66;'><f:opaque/></w:sz><q:szCs q:val='20' f:first='CS'/><w:szCs w:val='32' f:last='CS2'/><f:sz f:val='999'/></w:rPr></w:rPrDefault></w:docDefaults></w:styles>"#
2981        );
2982        let mut styles = CT_Styles::from_xml(source.as_bytes()).unwrap();
2983        let properties = styles.doc_defaults.as_mut().unwrap().rpr.as_mut().unwrap();
2984        assert_eq!(properties.sz, Some(crate::units::HalfPoint(28)));
2985        assert_eq!(properties.sz_cs, Some(crate::units::HalfPoint(32)));
2986        properties.sz = None;
2987        properties.sz_cs = None;
2988        let saved = styles.to_xml().unwrap();
2989        let text = std::str::from_utf8(&saved).unwrap();
2990        for raw in [
2991            "f:first='&#65;'",
2992            "<!--size-->",
2993            "f:last='&#66;'",
2994            "<f:opaque/>",
2995            "f:first='CS'",
2996            "f:last='CS2'",
2997            "<f:sz f:val='999'/>",
2998        ] {
2999            assert!(text.contains(raw), "missing {raw}: {text}");
3000        }
3001        for value in ["q:val='24'", "w:val='28'", "q:val='20'", "w:val='32'"] {
3002            assert!(!text.contains(value));
3003        }
3004        let reopened = CT_Styles::from_xml(&saved).unwrap();
3005        let properties = reopened.doc_defaults.unwrap().rpr.unwrap();
3006        assert!(properties.sz.is_none() && properties.sz_cs.is_none());
3007    }
3008    #[test]
3009    fn fx200_style_property_root_attributes_and_untouched_owner_bytes_survive_updates() {
3010        let source = format!(
3011            r#"<w:styles xmlns:w="{W_NS}" xmlns:q="{W_NS}" xmlns:f="urn:foreign"><w:style w:type="paragraph" w:styleId="Imported"><w:name w:val="Imported"/><q:pPr f:keep='&#65;'><q:keepNext q:val='true'/><!--ppr--><f:opaque/></q:pPr><q:rPr f:keep='&#66;'><q:rFonts q:ascii='A' q:hint='cs'/><!--rpr--><f:opaque/></q:rPr></w:style></w:styles>"#
3012        );
3013        let mut styles = CT_Styles::from_xml(source.as_bytes()).unwrap();
3014        let original = styles.styles[0]
3015            .modeled_xml
3016            .iter()
3017            .find(|(rank, _)| *rank == 20)
3018            .unwrap()
3019            .1
3020            .clone();
3021        styles.styles[0].rpr.as_mut().unwrap().font_ascii = Some("B".to_owned());
3022        let saved = styles.to_xml().unwrap();
3023        assert!(
3024            saved
3025                .windows(original.len())
3026                .any(|window| window == original)
3027        );
3028        let text = std::str::from_utf8(&saved).unwrap();
3029        for raw in [
3030            "f:keep='&#65;'",
3031            "f:keep='&#66;'",
3032            "q:hint='cs'",
3033            "<!--rpr-->",
3034            "<f:opaque/>",
3035        ] {
3036            assert!(text.contains(raw), "{text}");
3037        }
3038        let reopened = CT_Styles::from_xml(&saved).unwrap();
3039        assert_eq!(
3040            reopened.styles[0]
3041                .rpr
3042                .as_ref()
3043                .unwrap()
3044                .font_ascii
3045                .as_deref(),
3046            Some("B")
3047        );
3048        assert_eq!(
3049            reopened.styles[0].ppr.as_ref().unwrap().keep_next,
3050            Some(true)
3051        );
3052    }
3053
3054    #[test]
3055    fn fx200_duplicate_style_property_owners_keep_empty_final_owner_and_foreign_peers() {
3056        let source = format!(
3057            r#"<w:styles xmlns:w="{W_NS}" xmlns:f="urn:foreign"><w:style w:type="paragraph" w:styleId="Imported"><w:name w:val="Imported"/><w:pPr><w:keepNext/></w:pPr><w:pPr f:last='&#65;'/><w:rPr><w:b/></w:rPr><f:rPr f:keep='peer'/><w:rPr f:last='&#66;'><w:rFonts w:ascii='A'/></w:rPr></w:style></w:styles>"#
3058        );
3059        let mut styles = CT_Styles::from_xml(source.as_bytes()).unwrap();
3060        assert!(styles.styles[0].ppr.as_ref().unwrap().keep_next.is_none());
3061        assert!(styles.styles[0].rpr.as_ref().unwrap().bold.is_none());
3062        styles.styles[0].rpr.as_mut().unwrap().font_ascii = None;
3063        let saved = styles.to_xml().unwrap();
3064        let text = std::str::from_utf8(&saved).unwrap();
3065        for raw in [
3066            "<w:b/>",
3067            "<w:keepNext/>",
3068            "f:last='&#65;'",
3069            "f:last='&#66;'",
3070            "f:keep='peer'",
3071        ] {
3072            assert!(text.contains(raw), "{text}");
3073        }
3074        let first = text.find("<w:b/>").unwrap();
3075        let peer = text.find("f:keep='peer'").unwrap();
3076        let last = text.find("f:last='&#66;'").unwrap();
3077        assert!(first < peer && peer < last, "{text}");
3078        let reopened = CT_Styles::from_xml(&saved).unwrap();
3079        let style = &reopened.styles[0];
3080        assert!(
3081            style.rpr.as_ref().unwrap().bold.is_none()
3082                && style.rpr.as_ref().unwrap().font_ascii.is_none()
3083        );
3084        assert!(style.ppr.as_ref().unwrap().keep_next.is_none());
3085        styles.styles[0].rpr = None;
3086        let cleared = styles.to_xml().unwrap();
3087        let reopened = CT_Styles::from_xml(&cleared).unwrap();
3088        assert!(reopened.styles[0].rpr.is_none());
3089        assert!(
3090            std::str::from_utf8(&cleared)
3091                .unwrap()
3092                .contains("f:keep='peer'")
3093        );
3094    }
3095    #[test]
3096    fn fx200_style_border_container_edit_retains_aliased_root_and_opaque_payload() {
3097        let source = format!(
3098            r#"<w:styles xmlns:w="{W_NS}" xmlns:q="{W_NS}" xmlns:f="urn:foreign"><w:style w:type="paragraph" w:styleId="Imported"><q:pPr f:outer='&#65;'><q:pBdr f:keep='&#66;'><q:bottom q:val='single' f:edge='&#67;'/><!--border--><f:opaque>  keep  </f:opaque><f:bottom f:val='foreign'/></q:pBdr><q:keepNext/></q:pPr></w:style></w:styles>"#
3099        );
3100        let mut styles = CT_Styles::from_xml(source.as_bytes()).unwrap();
3101        let edge = styles.styles[0]
3102            .ppr
3103            .as_mut()
3104            .unwrap()
3105            .borders
3106            .as_mut()
3107            .unwrap()
3108            .bottom
3109            .as_mut()
3110            .unwrap();
3111        edge.val = crate::shared::ST_Border::Double;
3112        edge.sz = Some(8);
3113        edge.color = Some("123456".to_owned());
3114        let saved = styles.to_xml().unwrap();
3115        let text = std::str::from_utf8(&saved).unwrap();
3116        for raw in [
3117            "f:outer='&#65;'",
3118            "f:keep='&#66;'",
3119            "f:edge='&#67;'",
3120            "<!--border-->",
3121            "<f:opaque>  keep  </f:opaque>",
3122            "<f:bottom f:val='foreign'/>",
3123        ] {
3124            assert!(text.contains(raw), "{text}");
3125        }
3126        let reopened = CT_Styles::from_xml(&saved).unwrap();
3127        let properties = reopened.styles[0].ppr.as_ref().unwrap();
3128        let border = properties
3129            .borders
3130            .as_ref()
3131            .unwrap()
3132            .bottom
3133            .as_ref()
3134            .unwrap();
3135        assert_eq!(border.val, crate::shared::ST_Border::Double);
3136        assert_eq!(border.sz, Some(8));
3137        assert_eq!(border.color.as_deref(), Some("123456"));
3138        assert_eq!(properties.keep_next, Some(true));
3139        assert_eq!(reopened.to_xml().unwrap(), saved);
3140    }
3141
3142    #[test]
3143    fn fx200_duplicate_style_border_clear_keeps_payload_but_suppresses_earlier_edges() {
3144        let source = format!(
3145            r#"<w:styles xmlns:w="{W_NS}" xmlns:q="{W_NS}" xmlns:f="urn:foreign"><w:style w:type="paragraph" w:styleId="Imported"><w:pPr><q:pBdr f:first='&#65;'><q:top q:val='single'/><f:opaque first='same'/></q:pBdr><f:pBdr f:keep='foreign'/><q:pBdr f:last='&#66;'><q:bottom q:val='double'/><!--last--><f:opaque last='same'/></q:pBdr><w:keepNext/></w:pPr></w:style></w:styles>"#
3146        );
3147        let mut styles = CT_Styles::from_xml(source.as_bytes()).unwrap();
3148        assert!(
3149            styles.styles[0]
3150                .ppr
3151                .as_ref()
3152                .unwrap()
3153                .borders
3154                .as_ref()
3155                .unwrap()
3156                .bottom
3157                .is_some()
3158        );
3159        styles.styles[0].ppr.as_mut().unwrap().borders = None;
3160        let saved = styles.to_xml().unwrap();
3161        let text = std::str::from_utf8(&saved).unwrap();
3162        for raw in [
3163            "f:first='&#65;'",
3164            "f:last='&#66;'",
3165            "<f:opaque first='same'/>",
3166            "<f:opaque last='same'/>",
3167            "<!--last-->",
3168            "<f:pBdr f:keep='foreign'/>",
3169        ] {
3170            assert!(text.contains(raw), "{text}");
3171        }
3172        assert!(text.find("f:first='&#65;'").unwrap() < text.find("f:keep='foreign'").unwrap());
3173        assert!(text.find("f:keep='foreign'").unwrap() < text.find("f:last='&#66;'").unwrap());
3174        let reopened = CT_Styles::from_xml(&saved).unwrap();
3175        let properties = reopened.styles[0].ppr.as_ref().unwrap();
3176        let border = properties.borders.as_ref().unwrap();
3177        assert!(
3178            border.top.is_none()
3179                && border.bottom.is_none()
3180                && border.left.is_none()
3181                && border.right.is_none()
3182                && border.between.is_none()
3183                && border.bar.is_none()
3184        );
3185        assert_eq!(properties.keep_next, Some(true));
3186        assert_eq!(reopened.to_xml().unwrap(), saved);
3187    }
3188
3189    #[test]
3190    fn fx200_direct_style_reader_restores_both_trim_flags_and_opaque_sibling_whitespace() {
3191        for property in [
3192            "<w:pPr><w:keepNext/></w:pPr>",
3193            "<f:pPr><f:payload>  inner  </f:payload></f:pPr>",
3194            "<w:rPr><w:b/></w:rPr>",
3195            "<f:rPr><f:payload>  inner  </f:payload></f:rPr>",
3196        ] {
3197            for (trim_start, trim_end) in [(false, false), (false, true), (true, false)] {
3198                let source = format!(
3199                    r#"<w:style xmlns:w="{W_NS}" xmlns:f="urn:foreign" w:type="paragraph" w:styleId="Direct">{property}<f:opaque>  keep  </f:opaque></w:style>"#
3200                );
3201                let mut reader = Reader::from_reader(source.as_bytes());
3202                reader.config_mut().trim_text_start = trim_start;
3203                reader.config_mut().trim_text_end = trim_end;
3204                let Event::Start(start) = reader.read_event().unwrap() else {
3205                    panic!("style root missing")
3206                };
3207                let style = CT_Style::from_xml(&mut reader, &start).unwrap();
3208                assert_eq!(reader.config().trim_text_start, trim_start);
3209                assert_eq!(reader.config().trim_text_end, trim_end);
3210                let expected = match (trim_start, trim_end) {
3211                    (false, false) => ">  keep  </f:opaque>",
3212                    (false, true) => ">  keep</f:opaque>",
3213                    _ => ">keep  </f:opaque>",
3214                };
3215                assert!(
3216                    style
3217                        .extra_xml
3218                        .iter()
3219                        .any(|(_, raw)| std::str::from_utf8(raw).unwrap().contains(expected)),
3220                    "{style:?}"
3221                );
3222            }
3223            let opening = &property[..property.find('>').unwrap() + 1];
3224            let malformed = format!(
3225                r#"<w:style xmlns:w="{W_NS}" xmlns:f="urn:foreign" w:type="paragraph" w:styleId="Direct">{opening}</f:wrong></w:style>"#
3226            );
3227            let mut reader = Reader::from_reader(malformed.as_bytes());
3228            reader.config_mut().trim_text(false);
3229            let Event::Start(start) = reader.read_event().unwrap() else {
3230                panic!("style root missing")
3231            };
3232            assert!(CT_Style::from_xml(&mut reader, &start).is_err());
3233            assert!(!reader.config().trim_text_start && !reader.config().trim_text_end);
3234        }
3235    }
3236    #[test]
3237    fn fx200_started_style_border_edges_match_empty_projection_and_keep_source_on_updates() {
3238        for prefix in ["w", "q"] {
3239            let source = format!(
3240                r#"<w:styles xmlns:w="{W_NS}" xmlns:q="{W_NS}" xmlns:f="urn:foreign"><w:style w:type="paragraph" w:styleId="Started"><{prefix}:pPr><{prefix}:pBdr><{prefix}:bottom {prefix}:val='single' f:edge='&#65;'><f:opaque>  keep  </f:opaque><!--edge--></{prefix}:bottom></{prefix}:pBdr></{prefix}:pPr></w:style></w:styles>"#
3241            );
3242            let mut styles = CT_Styles::from_xml(source.as_bytes()).unwrap();
3243            let original = styles.to_xml().unwrap();
3244            let before = styles.styles[0]
3245                .ppr
3246                .as_ref()
3247                .unwrap()
3248                .borders
3249                .as_ref()
3250                .unwrap()
3251                .bottom
3252                .as_ref()
3253                .unwrap();
3254            assert_eq!(before.val, crate::shared::ST_Border::Single);
3255            assert!(before.source_xml.is_some());
3256            assert_eq!(
3257                CT_Styles::from_xml(&original).unwrap().to_xml().unwrap(),
3258                original
3259            );
3260            let edge = styles.styles[0]
3261                .ppr
3262                .as_mut()
3263                .unwrap()
3264                .borders
3265                .as_mut()
3266                .unwrap()
3267                .bottom
3268                .as_mut()
3269                .unwrap();
3270            edge.val = crate::shared::ST_Border::Double;
3271            edge.sz = Some(8);
3272            edge.color = Some("123456".to_owned());
3273            let saved = styles.to_xml().unwrap();
3274            let text = std::str::from_utf8(&saved).unwrap();
3275            for raw in [
3276                "f:edge='&#65;'",
3277                "<f:opaque>  keep  </f:opaque>",
3278                "<!--edge-->",
3279            ] {
3280                assert!(text.contains(raw), "{text}");
3281            }
3282            assert!(text.contains(&format!("</{prefix}:bottom>")), "{text}");
3283            let reopened = CT_Styles::from_xml(&saved).unwrap();
3284            let actual = reopened.styles[0]
3285                .ppr
3286                .as_ref()
3287                .unwrap()
3288                .borders
3289                .as_ref()
3290                .unwrap()
3291                .bottom
3292                .as_ref()
3293                .unwrap();
3294            assert_eq!(actual.val, crate::shared::ST_Border::Double);
3295            assert_eq!(actual.sz, Some(8));
3296            assert_eq!(actual.color.as_deref(), Some("123456"));
3297            assert_eq!(reopened.to_xml().unwrap(), saved);
3298            let empty = format!(
3299                r#"<w:pPr xmlns:w="{W_NS}"><w:pBdr><w:bottom w:val="double" w:sz="8" w:color="123456"/></w:pBdr></w:pPr>"#
3300            );
3301            let canonical =
3302                crate::numbering::parse_scoped_ppr(empty.as_bytes(), &["w".to_owned()]).unwrap();
3303            let expected = canonical.borders.as_ref().unwrap().bottom.as_ref().unwrap();
3304            assert_eq!(
3305                (actual.val, actual.sz, actual.color.as_deref()),
3306                (expected.val, expected.sz, expected.color.as_deref())
3307            );
3308        }
3309    }
3310    #[test]
3311    fn fx200_style_border_opaque_descendants_keep_bytes_and_never_become_direct_edges() {
3312        for opaque in [
3313            r#"<f:opaque><q:bottom q:val='double'></q:bottom></f:opaque>"#,
3314            r#"<f:opaque xmlns:q='urn:shadow'><q:bottom q:val='double'></q:bottom><w:left w:val='double'/></f:opaque>"#,
3315        ] {
3316            let source = format!(
3317                r#"<w:styles xmlns:w="{W_NS}" xmlns:q="{W_NS}" xmlns:f="urn:foreign"><w:style w:type="paragraph" w:styleId="Opaque"><q:pPr><q:pBdr>{opaque}<q:right q:val='single' f:edge='&#65;'><f:inside>  real  </f:inside></q:right></q:pBdr></q:pPr></w:style></w:styles>"#
3318            );
3319            let mut styles = CT_Styles::from_xml(source.as_bytes()).unwrap();
3320            let borders = styles.styles[0]
3321                .ppr
3322                .as_ref()
3323                .unwrap()
3324                .borders
3325                .as_ref()
3326                .unwrap();
3327            assert!(borders.bottom.is_none() && borders.left.is_none());
3328            assert_eq!(
3329                borders.right.as_ref().unwrap().val,
3330                crate::shared::ST_Border::Single
3331            );
3332            let noop = styles.to_xml().unwrap();
3333            assert!(std::str::from_utf8(&noop).unwrap().contains(opaque));
3334            styles.styles[0]
3335                .ppr
3336                .as_mut()
3337                .unwrap()
3338                .borders
3339                .as_mut()
3340                .unwrap()
3341                .right
3342                .as_mut()
3343                .unwrap()
3344                .val = crate::shared::ST_Border::Double;
3345            let saved = styles.to_xml().unwrap();
3346            let text = std::str::from_utf8(&saved).unwrap();
3347            assert!(
3348                text.contains(opaque)
3349                    && text.contains("<f:inside>  real  </f:inside>")
3350                    && text.contains("f:edge='&#65;'")
3351            );
3352            let reopened = CT_Styles::from_xml(&saved).unwrap();
3353            let actual = reopened.styles[0]
3354                .ppr
3355                .as_ref()
3356                .unwrap()
3357                .borders
3358                .as_ref()
3359                .unwrap();
3360            assert!(actual.bottom.is_none() && actual.left.is_none());
3361            assert_eq!(
3362                actual.right.as_ref().unwrap().val,
3363                crate::shared::ST_Border::Double
3364            );
3365            assert_eq!(reopened.to_xml().unwrap(), saved);
3366        }
3367    }
3368
3369    #[test]
3370    fn fx200_started_logical_style_border_edges_update_once_without_payload_duplication() {
3371        for (physical, logical) in [("start", "left"), ("end", "right")] {
3372            let source = format!(
3373                r#"<w:styles xmlns:w="{W_NS}" xmlns:q="{W_NS}" xmlns:f="urn:foreign"><w:style w:type="paragraph" w:styleId="Logical"><q:pPr><q:pBdr f:container='&#65;'><q:{physical} q:val='single' f:edge='&#66;'><f:payload>  unique  </f:payload><!--unique--></q:{physical}></q:pBdr></q:pPr></w:style></w:styles>"#
3374            );
3375            let mut styles = CT_Styles::from_xml(source.as_bytes()).unwrap();
3376            let initial = styles.to_xml().unwrap();
3377            assert_eq!(
3378                CT_Styles::from_xml(&initial).unwrap().to_xml().unwrap(),
3379                initial
3380            );
3381            for (value, size, color) in [
3382                (crate::shared::ST_Border::Double, 8, "123456"),
3383                (crate::shared::ST_Border::Single, 12, "654321"),
3384            ] {
3385                let borders = styles.styles[0]
3386                    .ppr
3387                    .as_mut()
3388                    .unwrap()
3389                    .borders
3390                    .as_mut()
3391                    .unwrap();
3392                let edge = if logical == "left" {
3393                    borders.left.as_mut().unwrap()
3394                } else {
3395                    borders.right.as_mut().unwrap()
3396                };
3397                edge.val = value;
3398                edge.sz = Some(size);
3399                edge.color = Some(color.to_owned());
3400                let saved = styles.to_xml().unwrap();
3401                let text = std::str::from_utf8(&saved).unwrap();
3402                assert_eq!(
3403                    text.matches(&format!("<q:{physical} ")).count(),
3404                    1,
3405                    "{text}"
3406                );
3407                assert_eq!(
3408                    text.matches(&format!("</q:{physical}>")).count(),
3409                    1,
3410                    "{text}"
3411                );
3412                assert_eq!(
3413                    text.matches("<f:payload>  unique  </f:payload>").count(),
3414                    1,
3415                    "{text}"
3416                );
3417                assert_eq!(text.matches("<!--unique-->").count(), 1, "{text}");
3418                assert!(text.contains("f:edge='&#66;'") && text.contains("f:container='&#65;'"));
3419                styles = CT_Styles::from_xml(&saved).unwrap();
3420                let borders = styles.styles[0]
3421                    .ppr
3422                    .as_ref()
3423                    .unwrap()
3424                    .borders
3425                    .as_ref()
3426                    .unwrap();
3427                let actual = if logical == "left" {
3428                    borders.left.as_ref().unwrap()
3429                } else {
3430                    borders.right.as_ref().unwrap()
3431                };
3432                assert_eq!(
3433                    (actual.val, actual.sz, actual.color.as_deref()),
3434                    (value, Some(size), Some(color))
3435                );
3436                assert_eq!(styles.to_xml().unwrap(), saved);
3437            }
3438        }
3439    }
3440
3441    #[test]
3442    fn fx200_style_partial_border_clear_removes_all_aliases_without_touching_siblings() {
3443        let source = format!(
3444            r#"<w:styles xmlns:w="{W_NS}" xmlns:q="{W_NS}" xmlns:f="urn:f"><w:style w:type="paragraph" w:styleId="Partial"><q:pPr><q:pBdr f:root='&#65;'><q:start q:val='single' f:gone='yes'><f:gone>owned</f:gone></q:start><q:left q:val='double'/><!--keep--><q:end q:val='single' f:stay='&#66;'><f:stay>  body  </f:stay></q:end><f:left f:val='foreign'/></q:pBdr></q:pPr></w:style></w:styles>"#
3445        );
3446        let mut styles = CT_Styles::from_xml(source.as_bytes()).unwrap();
3447        styles.styles[0]
3448            .ppr
3449            .as_mut()
3450            .unwrap()
3451            .borders
3452            .as_mut()
3453            .unwrap()
3454            .left = None;
3455        let saved = styles.to_xml().unwrap();
3456        let text = std::str::from_utf8(&saved).unwrap();
3457        assert!(!text.contains("<q:start") && !text.contains("<q:left"));
3458        for exact in [
3459            "f:root='&#65;'",
3460            "<!--keep-->",
3461            "f:stay='&#66;'",
3462            "<f:stay>  body  </f:stay>",
3463            "<f:left f:val='foreign'/>",
3464        ] {
3465            assert!(text.contains(exact), "{text}");
3466        }
3467        let reopened = CT_Styles::from_xml(&saved).unwrap();
3468        let borders = reopened.styles[0]
3469            .ppr
3470            .as_ref()
3471            .unwrap()
3472            .borders
3473            .as_ref()
3474            .unwrap();
3475        assert!(borders.left.is_none());
3476        assert_eq!(
3477            borders.right.as_ref().unwrap().val,
3478            crate::shared::ST_Border::Single
3479        );
3480        assert_eq!(reopened.to_xml().unwrap(), saved);
3481    }
3482    #[test]
3483    fn fx200_changed_style_scalar_keeps_physical_foreign_attributes_and_started_payload() {
3484        for started in [false, true] {
3485            let leaf = if started {
3486                "<q:u q:val='single' f:keep='&#65;'><f:opaque>  keep  </f:opaque><!--inside--></q:u>"
3487            } else {
3488                "<q:u q:val='single' f:keep='&#65;'/>"
3489            };
3490            let source = format!(
3491                r#"<w:styles xmlns:w="{W_NS}" xmlns:q="{W_NS}" xmlns:f="urn:foreign"><w:style w:type="paragraph" w:styleId="Scalar"><q:rPr f:root='&#66;'>{leaf}<q:b/></q:rPr></w:style></w:styles>"#
3492            );
3493            let mut styles = CT_Styles::from_xml(source.as_bytes()).unwrap();
3494            styles.styles[0].rpr.as_mut().unwrap().underline =
3495                Some(crate::shared::ST_Underline::Double);
3496            let saved = styles.to_xml().unwrap();
3497            let text = std::str::from_utf8(&saved).unwrap();
3498            assert!(
3499                text.contains("f:keep='&#65;'")
3500                    && text.contains("f:root='&#66;'")
3501                    && text.contains("<q:b/>"),
3502                "{text}"
3503            );
3504            if started {
3505                assert!(
3506                    text.contains("<f:opaque>  keep  </f:opaque><!--inside-->"),
3507                    "{text}"
3508                );
3509            }
3510            let reopened = CT_Styles::from_xml(&saved).unwrap();
3511            assert_eq!(
3512                reopened.styles[0].rpr.as_ref().unwrap().underline,
3513                Some(crate::shared::ST_Underline::Double)
3514            );
3515            assert_eq!(reopened.to_xml().unwrap(), saved);
3516        }
3517    }
3518}