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}
1563
1564#[allow(non_snake_case)]
1565impl CT_Styles {
1566    pub fn new() -> Self {
1567        CT_Styles {
1568            doc_defaults: None,
1569            styles: Vec::new(),
1570        }
1571    }
1572
1573    /// Parse from XML bytes (the content of word/styles.xml).
1574    pub fn from_xml(xml: &[u8]) -> Result<Self> {
1575        let mut reader = Reader::from_reader(xml);
1576        reader.config_mut().trim_text(true);
1577
1578        let mut doc_defaults = None;
1579        let mut styles = Vec::new();
1580        let mut buf = Vec::new();
1581        let mut word_prefixes = Vec::new();
1582        let mut namespace_bindings = Vec::new();
1583
1584        loop {
1585            match reader.read_event_into(&mut buf) {
1586                Ok(Event::Start(ref e)) => {
1587                    let name = e.name();
1588                    let prefixes = word_prefixes_at(e, &word_prefixes)?;
1589                    if is_word_element(name.as_ref(), b"docDefaults", &prefixes) {
1590                        let bindings = namespace_bindings_at(e, &namespace_bindings)?;
1591                        reader.config_mut().trim_text(false);
1592                        let captured = capture_element(&mut reader, e)?;
1593                        reader.config_mut().trim_text(true);
1594                        let raw = crate::text::raw_with_external_bindings(&captured, &bindings)?;
1595                        doc_defaults = Some(CT_DocDefaults::from_source_xml(&raw, &prefixes)?);
1596                    } else if is_word_element(name.as_ref(), b"style", &prefixes) {
1597                        let bindings = namespace_bindings_at(e, &namespace_bindings)?;
1598                        styles.push(CT_Style::from_xml_with_prefixes(
1599                            &mut reader,
1600                            e,
1601                            &prefixes,
1602                            &bindings,
1603                        )?);
1604                    } else if is_word_element(name.as_ref(), b"styles", &prefixes) {
1605                        // Root element, continue
1606                        word_prefixes = prefixes;
1607                        namespace_bindings = namespace_bindings_at(e, &namespace_bindings)?;
1608                    } else {
1609                        reader.read_to_end_into(name, &mut Vec::new())?;
1610                    }
1611                }
1612                Ok(Event::Empty(ref e)) => {
1613                    let prefixes = word_prefixes_at(e, &word_prefixes)?;
1614                    if is_word_element(e.name().as_ref(), b"docDefaults", &prefixes) {
1615                        let bindings = namespace_bindings_at(e, &namespace_bindings)?;
1616                        let raw = crate::text::raw_with_external_bindings(
1617                            &capture_empty_element(e)?,
1618                            &bindings,
1619                        )?;
1620                        doc_defaults = Some(CT_DocDefaults::from_source_xml(&raw, &prefixes)?);
1621                    }
1622                }
1623                Ok(Event::Eof) => break,
1624                Err(e) => return Err(e.into()),
1625                _ => {}
1626            }
1627            buf.clear();
1628        }
1629
1630        Ok(CT_Styles {
1631            doc_defaults,
1632            styles,
1633        })
1634    }
1635
1636    /// Serialize to XML bytes.
1637    pub fn to_xml(&self) -> Result<Vec<u8>> {
1638        let mut writer = Writer::new(Vec::new());
1639
1640        writer.write_event(Event::Decl(BytesDecl::new(
1641            "1.0",
1642            Some("UTF-8"),
1643            Some("yes"),
1644        )))?;
1645
1646        let mut styles_start = BytesStart::new("w:styles");
1647        styles_start.push_attribute(("xmlns:w", W_NS));
1648        styles_start.push_attribute((
1649            "xmlns:r",
1650            "http://schemas.openxmlformats.org/officeDocument/2006/relationships",
1651        ));
1652        writer.write_event(Event::Start(styles_start))?;
1653
1654        if let Some(ref defaults) = self.doc_defaults {
1655            defaults.to_xml(&mut writer)?;
1656        }
1657
1658        for style in &self.styles {
1659            style.to_xml(&mut writer)?;
1660        }
1661
1662        writer.write_event(Event::End(BytesEnd::new("w:styles")))?;
1663
1664        Ok(writer.into_inner())
1665    }
1666
1667    /// Find a style by its ID.
1668    pub fn get_by_id(&self, style_id: &str) -> Option<&CT_Style> {
1669        self.styles.iter().find(|s| s.style_id == style_id)
1670    }
1671
1672    /// Find the default style for a given type.
1673    pub fn get_default(&self, style_type: StyleType) -> Option<&CT_Style> {
1674        self.styles
1675            .iter()
1676            .find(|s| s.style_type == style_type && s.is_default)
1677    }
1678
1679    /// Create a minimal default styles part for a new document.
1680    pub fn new_default() -> Self {
1681        use crate::units::HalfPoint;
1682
1683        let normal = CT_Style {
1684            style_id: "Normal".to_string(),
1685            style_type: StyleType::Paragraph,
1686            name: Some("Normal".to_string()),
1687            based_on: None,
1688            next_style: None,
1689            linked_style: None,
1690            auto_redefine: None,
1691            hidden: None,
1692            ui_priority: None,
1693            semi_hidden: None,
1694            unhide_when_used: None,
1695            quick_format: None,
1696            locked: None,
1697            is_default: true,
1698            ppr: None,
1699            rpr: None,
1700            table_properties: None,
1701            table_properties_original: None,
1702            table_properties_xml: None,
1703            table_row_properties: None,
1704            table_cell_properties: None,
1705            conditional_table_styles: Vec::new(),
1706            extra_attributes: Vec::new(),
1707            modeled_xml: Vec::new(),
1708            extra_xml: Vec::new(),
1709        };
1710
1711        let heading1 = CT_Style {
1712            style_id: "Heading1".to_string(),
1713            style_type: StyleType::Paragraph,
1714            name: Some("heading 1".to_string()),
1715            based_on: Some("Normal".to_string()),
1716            next_style: Some("Normal".to_string()),
1717            linked_style: None,
1718            auto_redefine: None,
1719            hidden: None,
1720            ui_priority: None,
1721            semi_hidden: None,
1722            unhide_when_used: None,
1723            quick_format: None,
1724            locked: None,
1725            is_default: false,
1726            ppr: Some(CT_PPr {
1727                keep_next: Some(true),
1728                keep_lines: Some(true),
1729                space_before: Some(crate::units::Twips(240)),
1730                space_after: Some(crate::units::Twips(0)),
1731                ..Default::default()
1732            }),
1733            rpr: Some(CT_RPr {
1734                sz: Some(HalfPoint(32)),
1735                sz_cs: Some(HalfPoint(32)),
1736                bold: Some(true),
1737                bold_cs: Some(true),
1738                color: Some("2F5496".to_string()),
1739                ..Default::default()
1740            }),
1741            table_properties: None,
1742            table_properties_original: None,
1743            table_properties_xml: None,
1744            table_row_properties: None,
1745            table_cell_properties: None,
1746            conditional_table_styles: Vec::new(),
1747            extra_attributes: Vec::new(),
1748            modeled_xml: Vec::new(),
1749            extra_xml: Vec::new(),
1750        };
1751
1752        let doc_defaults = CT_DocDefaults {
1753            source_xml: None,
1754            rpr: Some(CT_RPr {
1755                font_ascii: Some("Calibri".to_string()),
1756                font_hansi: Some("Calibri".to_string()),
1757                font_east_asia: Some("Calibri".to_string()),
1758                font_cs: Some("Times New Roman".to_string()),
1759                sz: Some(HalfPoint(22)),
1760                sz_cs: Some(HalfPoint(22)),
1761                ..Default::default()
1762            }),
1763            ppr: Some(CT_PPr {
1764                space_after: Some(crate::units::Twips(160)),
1765                line_spacing: Some(crate::units::Twips(259)),
1766                line_rule: Some("auto".to_string()),
1767                ..Default::default()
1768            }),
1769        };
1770
1771        CT_Styles {
1772            doc_defaults: Some(doc_defaults),
1773            styles: vec![normal, heading1],
1774        }
1775    }
1776}
1777
1778impl Default for CT_Styles {
1779    fn default() -> Self {
1780        Self::new()
1781    }
1782}
1783
1784fn style_child_rank(name: &[u8], word_prefixes: &[String]) -> u8 {
1785    let local = name.rsplit(|byte| *byte == b':').next().unwrap_or(name);
1786    if !is_word_element(name, local, word_prefixes) {
1787        return 26;
1788    }
1789    match local {
1790        b"name" => 0,
1791        b"aliases" => 1,
1792        b"basedOn" => 2,
1793        b"next" => 3,
1794        b"link" => 4,
1795        b"autoRedefine" => 5,
1796        b"hidden" => 6,
1797        b"uiPriority" => 7,
1798        b"semiHidden" => 8,
1799        b"unhideWhenUsed" => 9,
1800        b"qFormat" => 10,
1801        b"locked" => 11,
1802        b"personal" => 12,
1803        b"personalCompose" => 13,
1804        b"personalReply" => 14,
1805        b"rsid" => 19,
1806        b"pPr" => 20,
1807        b"rPr" => 21,
1808        b"tblPr" => 22,
1809        b"trPr" => 23,
1810        b"tcPr" => 24,
1811        b"tblStylePr" => 25,
1812        _ => 26,
1813    }
1814}
1815
1816fn style_scalar_rank(name: &[u8], word_prefixes: &[String]) -> Option<u8> {
1817    let rank = style_child_rank(name, word_prefixes);
1818    matches!(rank, 0 | 2..=11).then_some(rank)
1819}
1820
1821fn style_scalar_empty_requires_snapshot(element: &BytesStart<'_>, rank: u8) -> Result<bool> {
1822    let canonical_name: &[u8] = match rank {
1823        0 => b"w:name",
1824        2 => b"w:basedOn",
1825        3 => b"w:next",
1826        4 => b"w:link",
1827        5 => b"w:autoRedefine",
1828        6 => b"w:hidden",
1829        7 => b"w:uiPriority",
1830        8 => b"w:semiHidden",
1831        9 => b"w:unhideWhenUsed",
1832        10 => b"w:qFormat",
1833        11 => b"w:locked",
1834        _ => unreachable!("style scalar rank"),
1835    };
1836    if element.name().as_ref() != canonical_name {
1837        return Ok(true);
1838    }
1839
1840    for attribute in element.attributes() {
1841        if attribute?.key.as_ref() != b"w:val" {
1842            return Ok(true);
1843        }
1844    }
1845    Ok(false)
1846}
1847
1848#[allow(clippy::too_many_arguments)]
1849fn apply_style_scalar(
1850    rank: u8,
1851    string_value: Option<String>,
1852    toggle_value: Option<bool>,
1853    priority_value: Option<u32>,
1854    name: &mut Option<String>,
1855    based_on: &mut Option<String>,
1856    next_style: &mut Option<String>,
1857    linked_style: &mut Option<String>,
1858    auto_redefine: &mut Option<bool>,
1859    hidden: &mut Option<bool>,
1860    ui_priority: &mut Option<u32>,
1861    semi_hidden: &mut Option<bool>,
1862    unhide_when_used: &mut Option<bool>,
1863    quick_format: &mut Option<bool>,
1864    locked: &mut Option<bool>,
1865) {
1866    match rank {
1867        0 => *name = string_value,
1868        2 => *based_on = string_value,
1869        3 => *next_style = string_value,
1870        4 => *linked_style = string_value,
1871        5 => *auto_redefine = toggle_value,
1872        6 => *hidden = toggle_value,
1873        7 => *ui_priority = priority_value,
1874        8 => *semi_hidden = toggle_value,
1875        9 => *unhide_when_used = toggle_value,
1876        10 => *quick_format = toggle_value,
1877        11 => *locked = toggle_value,
1878        _ => unreachable!("style scalar rank"),
1879    }
1880}
1881
1882fn write_style_extras<W: std::io::Write>(
1883    writer: &mut Writer<W>,
1884    extras: &[&(u8, Vec<u8>)],
1885    index: &mut usize,
1886    before_rank: u8,
1887) -> Result<()> {
1888    while let Some((rank, raw)) = extras.get(*index).copied() {
1889        if *rank >= before_rank {
1890            break;
1891        }
1892        writer.get_mut().write_all(raw)?;
1893        *index += 1;
1894    }
1895    Ok(())
1896}
1897
1898fn namespace_bindings_at(
1899    element: &BytesStart,
1900    inherited: &[(String, String)],
1901) -> Result<Vec<(String, String)>> {
1902    let mut bindings = inherited.to_vec();
1903    for attribute in element.attributes() {
1904        let attribute = attribute?;
1905        let key = attribute.key.as_ref();
1906        let prefix = if key == b"xmlns" {
1907            Some(String::new())
1908        } else {
1909            key.strip_prefix(b"xmlns:")
1910                .map(|prefix| String::from_utf8_lossy(prefix).into_owned())
1911        };
1912        let Some(prefix) = prefix else {
1913            continue;
1914        };
1915        let uri = std::str::from_utf8(&attribute.value)?.to_owned();
1916        if let Some(binding) = bindings
1917            .iter_mut()
1918            .find(|(candidate, _)| candidate == &prefix)
1919        {
1920            binding.1 = uri;
1921        } else {
1922            bindings.push((prefix, uri));
1923        }
1924    }
1925    Ok(bindings)
1926}
1927
1928fn reject_conflicting_style_prefixes(element: &BytesStart<'_>) -> Result<()> {
1929    for attribute in element.attributes() {
1930        let attribute = attribute?;
1931        let expected = match attribute.key.as_ref() {
1932            b"xmlns:w" => Some(W_NS),
1933            b"xmlns:r" => {
1934                Some("http://schemas.openxmlformats.org/officeDocument/2006/relationships")
1935            }
1936            _ => None,
1937        };
1938        let Some(expected) = expected else {
1939            continue;
1940        };
1941        let value = std::str::from_utf8(&attribute.value)?;
1942        if value != expected {
1943            return Err(OxmlError::InvalidValue(format!(
1944                "{} conflicts with the fixed style writer namespace",
1945                String::from_utf8_lossy(attribute.key.as_ref())
1946            )));
1947        }
1948    }
1949    Ok(())
1950}
1951
1952fn raw_style_attributes(
1953    element: &BytesStart<'_>,
1954    modeled: &[&[u8]],
1955    word_prefixes: &[String],
1956    namespace_bindings: &[(String, String)],
1957) -> Result<Vec<(String, String)>> {
1958    let mut attributes = Vec::new();
1959    for attribute in element.attributes() {
1960        let attribute = attribute?;
1961        let key = attribute.key.as_ref();
1962        let value = attribute
1963            .decoded_and_normalized_value(XmlVersion::Implicit1_0, element.decoder())?
1964            .into_owned();
1965        let canonical_namespace = (key == b"xmlns:w" && value == W_NS)
1966            || (key == b"xmlns:r"
1967                && value == "http://schemas.openxmlformats.org/officeDocument/2006/relationships");
1968        if !canonical_namespace
1969            && !modeled
1970                .iter()
1971                .any(|local| is_word_attribute(key, local, word_prefixes))
1972        {
1973            attributes.push((std::str::from_utf8(key)?.to_owned(), value));
1974        }
1975    }
1976    if !attributes.is_empty() {
1977        for (prefix, namespace) in namespace_bindings {
1978            if prefix.is_empty()
1979                || (prefix == "w" && namespace == W_NS)
1980                || (prefix == "r"
1981                    && namespace
1982                        == "http://schemas.openxmlformats.org/officeDocument/2006/relationships")
1983            {
1984                continue;
1985            }
1986            let declaration = format!("xmlns:{prefix}");
1987            if !attributes.iter().any(|(name, _)| name == &declaration) {
1988                attributes.push((declaration, namespace.clone()));
1989            }
1990        }
1991    }
1992    Ok(attributes)
1993}
1994
1995fn reject_conflicting_style_bindings(bindings: &[(String, String)]) -> Result<()> {
1996    for (prefix, namespace) in bindings {
1997        let expected = match prefix.as_str() {
1998            "w" => Some(W_NS),
1999            "r" => Some("http://schemas.openxmlformats.org/officeDocument/2006/relationships"),
2000            _ => None,
2001        };
2002        if expected.is_some_and(|expected| namespace != expected) {
2003            return Err(OxmlError::InvalidValue(format!(
2004                "xmlns:{prefix} conflicts with the fixed style writer namespace"
2005            )));
2006        }
2007    }
2008    Ok(())
2009}
2010
2011fn push_style_attributes(element: &mut BytesStart<'_>, attributes: &[(String, String)]) {
2012    for (name, value) in attributes {
2013        element.push_attribute((name.as_str(), value.as_str()));
2014    }
2015}
2016
2017fn make_style_raw_self_contained(
2018    raw: &[u8],
2019    namespace_bindings: &[(String, String)],
2020) -> Result<Vec<u8>> {
2021    let external_bindings = namespace_bindings
2022        .iter()
2023        .filter(|(prefix, namespace)| {
2024            !((prefix == "w" && namespace == W_NS)
2025                || (prefix == "r"
2026                    && namespace
2027                        == "http://schemas.openxmlformats.org/officeDocument/2006/relationships"))
2028        })
2029        .cloned()
2030        .collect::<Vec<_>>();
2031    crate::text::raw_with_external_bindings(raw, &external_bindings)
2032}
2033
2034fn parse_style_table_properties(raw: &[u8], word_prefixes: &[String]) -> Result<CT_TblPr> {
2035    let mut reader = Reader::from_reader(raw);
2036    let mut buf = Vec::new();
2037    loop {
2038        match reader.read_event_into(&mut buf)? {
2039            Event::Start(ref start)
2040                if is_word_element(start.name().as_ref(), b"tblPr", word_prefixes) =>
2041            {
2042                let prefixes = word_prefixes_at(start, word_prefixes)?;
2043                return CT_TblPr::from_xml_with_prefixes(&mut reader, &prefixes);
2044            }
2045            Event::Eof => return Ok(CT_TblPr::default()),
2046            _ => {}
2047        }
2048        buf.clear();
2049    }
2050}
2051
2052fn explicit_style_element_has_only_trivia(raw: &[u8]) -> Result<bool> {
2053    let mut reader = Reader::from_reader(raw);
2054    let mut inside = false;
2055    let mut buf = Vec::new();
2056    loop {
2057        match reader.read_event_into(&mut buf)? {
2058            Event::Start(_) if !inside => inside = true,
2059            Event::End(_) if inside => return Ok(true),
2060            Event::Text(text) if inside && text.as_ref().iter().all(u8::is_ascii_whitespace) => {}
2061            Event::Comment(_) | Event::PI(_) if inside => {}
2062            Event::Eof => return Ok(false),
2063            _ if inside => return Ok(false),
2064            _ => {}
2065        }
2066        buf.clear();
2067    }
2068}
2069
2070/// The five typed property layers a `w:tblStylePr` region carries.
2071#[derive(Default)]
2072struct ConditionalStyleLayers {
2073    paragraph_properties: Option<CT_PPr>,
2074    run_properties: Option<Box<CT_RPr>>,
2075    table_properties: Option<CT_TblPr>,
2076    row_properties: Option<Box<CT_TrPr>>,
2077    cell_properties: Option<CT_TcPr>,
2078}
2079
2080fn parse_conditional_style_properties(
2081    raw: &[u8],
2082    word_prefixes: &[String],
2083) -> Result<ConditionalStyleLayers> {
2084    let mut reader = Reader::from_reader(raw);
2085    let mut layers = ConditionalStyleLayers::default();
2086    let mut buf = Vec::new();
2087    let mut depth = 0usize;
2088    loop {
2089        match reader.read_event_into(&mut buf)? {
2090            Event::Start(ref start) => {
2091                depth += 1;
2092                let prefixes = word_prefixes_at(start, word_prefixes)?;
2093                if depth != 2 {
2094                    // Only the region's own children are layers. Anything
2095                    // deeper belongs to a layer or to a preserved subtree.
2096                } else if is_word_element(start.name().as_ref(), b"pPr", &prefixes) {
2097                    let captured = capture_element(&mut reader, start)?;
2098                    layers.paragraph_properties = Some(parse_scoped_ppr(&captured, &prefixes)?);
2099                    depth -= 1;
2100                } else if is_word_element(start.name().as_ref(), b"rPr", &prefixes) {
2101                    let captured = capture_element(&mut reader, start)?;
2102                    layers.run_properties = Some(Box::new(parse_style_rpr(&captured, &prefixes)?));
2103                    depth -= 1;
2104                } else if is_word_element(start.name().as_ref(), b"tblPr", &prefixes) {
2105                    layers.table_properties =
2106                        Some(CT_TblPr::from_xml_with_prefixes(&mut reader, &prefixes)?);
2107                    depth -= 1;
2108                } else if is_word_element(start.name().as_ref(), b"trPr", &prefixes) {
2109                    layers.row_properties = Some(Box::new(CT_TrPr::from_xml_with_prefixes(
2110                        &mut reader,
2111                        &prefixes,
2112                    )?));
2113                    depth -= 1;
2114                } else if is_word_element(start.name().as_ref(), b"tcPr", &prefixes) {
2115                    layers.cell_properties =
2116                        Some(CT_TcPr::from_xml_with_prefixes(&mut reader, &prefixes)?);
2117                    depth -= 1;
2118                }
2119            }
2120            Event::End(_) => depth = depth.saturating_sub(1),
2121            Event::Eof => break,
2122            _ => {}
2123        }
2124        buf.clear();
2125    }
2126    Ok(layers)
2127}
2128
2129fn serialize_conditional_table_style(conditional: &CT_TblStylePr) -> Result<Vec<u8>> {
2130    let mut reader = Reader::from_reader(conditional.raw_xml.as_slice());
2131    let mut prefixes = vec!["w".to_owned()];
2132    let mut original_region = None;
2133    let mut buf = Vec::new();
2134    loop {
2135        match reader.read_event_into(&mut buf)? {
2136            Event::Start(ref start) => {
2137                prefixes = word_prefixes_at(start, &prefixes)?;
2138                original_region = get_word_attr(start, b"type", &prefixes)?;
2139                break;
2140            }
2141            Event::Eof => break,
2142            _ => {}
2143        }
2144        buf.clear();
2145    }
2146    let Some(region) = conditional.region else {
2147        // An unrecognised `w:type` was never projected, so its preserved bytes
2148        // are the only faithful serialisation of it.
2149        return Ok(conditional.raw_xml.clone());
2150    };
2151    let original = parse_conditional_style_properties(&conditional.raw_xml, &prefixes)?;
2152    if original_region.as_deref() == Some(region.to_str())
2153        && original.paragraph_properties == conditional.paragraph_properties
2154        && original.run_properties == conditional.run_properties
2155        && original.table_properties == conditional.table_properties
2156        && original.row_properties == conditional.row_properties
2157        && original.cell_properties == conditional.cell_properties
2158    {
2159        return Ok(conditional.raw_xml.clone());
2160    }
2161
2162    let mut extras = preserved_conditional_style_children(&conditional.raw_xml)?;
2163    extras.sort_by_key(|(rank, _)| *rank);
2164    let extra_refs = extras.iter().collect::<Vec<_>>();
2165    let mut extra_index = 0;
2166    let mut writer = Writer::new(Vec::new());
2167    let mut start = BytesStart::new("w:tblStylePr");
2168    start.push_attribute(("w:type", region.to_str()));
2169    push_style_attributes(&mut start, &conditional.extra_attributes);
2170    writer.write_event(Event::Start(start))?;
2171    write_style_extras(&mut writer, &extra_refs, &mut extra_index, 0)?;
2172    if let Some(properties) = &conditional.paragraph_properties {
2173        properties.to_xml(&mut writer)?;
2174    }
2175    write_style_extras(&mut writer, &extra_refs, &mut extra_index, 1)?;
2176    if let Some(properties) = &conditional.run_properties {
2177        properties.to_xml(&mut writer)?;
2178    }
2179    write_style_extras(&mut writer, &extra_refs, &mut extra_index, 2)?;
2180    if let Some(properties) = &conditional.table_properties {
2181        properties.to_xml(&mut writer)?;
2182    }
2183    write_style_extras(&mut writer, &extra_refs, &mut extra_index, 3)?;
2184    if let Some(properties) = &conditional.row_properties {
2185        properties.to_xml(&mut writer)?;
2186    }
2187    write_style_extras(&mut writer, &extra_refs, &mut extra_index, 4)?;
2188    if let Some(properties) = &conditional.cell_properties {
2189        properties.to_xml(&mut writer)?;
2190    }
2191    write_style_extras(&mut writer, &extra_refs, &mut extra_index, u8::MAX)?;
2192    writer.write_event(Event::End(BytesEnd::new("w:tblStylePr")))?;
2193    Ok(writer.into_inner())
2194}
2195
2196fn preserved_conditional_style_children(raw: &[u8]) -> Result<Vec<(u8, Vec<u8>)>> {
2197    let mut reader = Reader::from_reader(raw);
2198    let mut inside = false;
2199    let mut word_prefixes = vec!["w".to_owned()];
2200    let mut extras = Vec::new();
2201    let mut buf = Vec::new();
2202    loop {
2203        match reader.read_event_into(&mut buf)? {
2204            Event::Start(ref start) if !inside => {
2205                word_prefixes = word_prefixes_at(start, &word_prefixes)?;
2206                inside = true;
2207            }
2208            Event::Start(ref start) => {
2209                let prefixes = word_prefixes_at(start, &word_prefixes)?;
2210                let local = start.local_name();
2211                if is_word_element(start.name().as_ref(), local.as_ref(), &prefixes)
2212                    && matches!(
2213                        local.as_ref(),
2214                        b"pPr" | b"rPr" | b"tblPr" | b"trPr" | b"tcPr"
2215                    )
2216                {
2217                    reader.read_to_end_into(start.name(), &mut Vec::new())?;
2218                } else {
2219                    extras.push((5, capture_element(&mut reader, start)?));
2220                }
2221            }
2222            Event::Empty(ref empty) => {
2223                let prefixes = word_prefixes_at(empty, &word_prefixes)?;
2224                let local = empty.local_name();
2225                if !(is_word_element(empty.name().as_ref(), local.as_ref(), &prefixes)
2226                    && matches!(
2227                        local.as_ref(),
2228                        b"pPr" | b"rPr" | b"tblPr" | b"trPr" | b"tcPr"
2229                    ))
2230                {
2231                    extras.push((5, capture_empty_element(empty)?));
2232                }
2233            }
2234            Event::End(_) | Event::Eof => break,
2235            _ => {}
2236        }
2237        buf.clear();
2238    }
2239    Ok(extras)
2240}
2241
2242/// Extract the `w:val` attribute from an element.
2243fn get_val_attr(e: &BytesStart, word_prefixes: &[String]) -> Result<Option<String>> {
2244    get_word_attr(e, b"val", word_prefixes)
2245}
2246
2247fn get_word_attr(e: &BytesStart, local: &[u8], word_prefixes: &[String]) -> Result<Option<String>> {
2248    for attr in e.attributes() {
2249        let attr = attr?;
2250        if is_word_attribute(attr.key.as_ref(), local, word_prefixes) {
2251            return Ok(Some(std::str::from_utf8(&attr.value)?.to_string()));
2252        }
2253    }
2254    Ok(None)
2255}
2256
2257fn parse_style_toggle(e: &BytesStart, word_prefixes: &[String]) -> Result<Option<bool>> {
2258    let value = get_word_attr(e, b"val", word_prefixes)?;
2259    match value.as_deref() {
2260        None | Some("1" | "true" | "on") => Ok(Some(true)),
2261        Some("0" | "false" | "off") => Ok(Some(false)),
2262        Some(value) => Err(OxmlError::InvalidValue(format!(
2263            "invalid style toggle value: {value}"
2264        ))),
2265    }
2266}
2267
2268fn parse_style_priority(e: &BytesStart, word_prefixes: &[String]) -> Result<Option<u32>> {
2269    get_word_attr(e, b"val", word_prefixes)?
2270        .map(|value| {
2271            value
2272                .parse::<u32>()
2273                .map_err(|_| OxmlError::InvalidValue(format!("invalid style UI priority: {value}")))
2274        })
2275        .transpose()
2276}
2277
2278fn preserved_style_attribute(raw: &[u8], local: &[u8]) -> Result<Option<String>> {
2279    let mut reader = Reader::from_reader(raw);
2280    let mut buf = Vec::new();
2281    loop {
2282        match reader.read_event_into(&mut buf)? {
2283            Event::Start(ref start) | Event::Empty(ref start) => {
2284                let prefixes = word_prefixes_at(start, &["w".to_owned()])?;
2285                return get_word_attr(start, local, &prefixes);
2286            }
2287            Event::Eof => return Ok(None),
2288            _ => {}
2289        }
2290        buf.clear();
2291    }
2292}
2293
2294fn preserved_modeled_xml(modeled_xml: &[(u8, Vec<u8>)], rank: u8) -> Option<&[u8]> {
2295    modeled_xml
2296        .iter()
2297        .find(|(candidate, _)| *candidate == rank)
2298        .map(|(_, raw)| raw.as_slice())
2299}
2300
2301fn rewritten_style_scalar_start(
2302    element: &BytesStart<'_>,
2303    value: &str,
2304) -> Result<BytesStart<'static>> {
2305    let prefixes = word_prefixes_at(element, &["w".to_owned()])?;
2306    let element_name = std::str::from_utf8(element.name().as_ref())?.to_owned();
2307    let mut rewritten = BytesStart::new(element_name.clone());
2308    let mut wrote_value = false;
2309    for attribute in element.attributes() {
2310        let attribute = attribute?;
2311        let key = std::str::from_utf8(attribute.key.as_ref())?.to_owned();
2312        if is_word_attribute(attribute.key.as_ref(), b"val", &prefixes) {
2313            if !wrote_value {
2314                rewritten.push_attribute((key.as_str(), value));
2315                wrote_value = true;
2316            }
2317        } else {
2318            let attribute_value = attribute
2319                .decoded_and_normalized_value(XmlVersion::Implicit1_0, element.decoder())?
2320                .into_owned();
2321            rewritten.push_attribute((key.as_str(), attribute_value.as_str()));
2322        }
2323    }
2324    if !wrote_value {
2325        let element_prefix = element_name
2326            .split_once(':')
2327            .map(|(prefix, _)| prefix.to_owned());
2328        let existing_prefix = element_prefix.or_else(|| {
2329            prefixes
2330                .iter()
2331                .find(|prefix| !prefix.is_empty() && !prefix.starts_with('\0'))
2332                .cloned()
2333        });
2334        let prefix = existing_prefix.unwrap_or_else(|| {
2335            let mut candidate = "rdocxStyle".to_owned();
2336            let mut suffix = 1usize;
2337            while element
2338                .attributes()
2339                .filter_map(std::result::Result::ok)
2340                .any(|attribute| attribute.key.as_ref() == format!("xmlns:{candidate}").as_bytes())
2341            {
2342                candidate = format!("rdocxStyle{suffix}");
2343                suffix += 1;
2344            }
2345            let declaration = format!("xmlns:{candidate}");
2346            rewritten.push_attribute((declaration.as_str(), W_NS));
2347            candidate
2348        });
2349        let key = format!("{prefix}:val");
2350        rewritten.push_attribute((key.as_str(), value));
2351    }
2352    Ok(rewritten.into_owned())
2353}
2354
2355fn write_rewritten_style_scalar<W: std::io::Write>(
2356    writer: &mut Writer<W>,
2357    raw: &[u8],
2358    value: &str,
2359) -> Result<()> {
2360    let mut reader = Reader::from_reader(raw);
2361    let mut root_pending = true;
2362    let mut buf = Vec::new();
2363    loop {
2364        match reader.read_event_into(&mut buf)? {
2365            Event::Start(element) if root_pending => {
2366                root_pending = false;
2367                writer.write_event(Event::Start(rewritten_style_scalar_start(&element, value)?))?;
2368            }
2369            Event::Empty(element) if root_pending => {
2370                writer.write_event(Event::Empty(rewritten_style_scalar_start(&element, value)?))?;
2371                break;
2372            }
2373            Event::Eof => break,
2374            event => writer.write_event(event.into_owned())?,
2375        }
2376        buf.clear();
2377    }
2378    Ok(())
2379}
2380
2381fn write_style_string<W: std::io::Write>(
2382    writer: &mut Writer<W>,
2383    name: &str,
2384    rank: u8,
2385    value: Option<&str>,
2386    modeled_xml: &[(u8, Vec<u8>)],
2387) -> Result<()> {
2388    let Some(value) = value else {
2389        return Ok(());
2390    };
2391    if let Some(raw) = preserved_modeled_xml(modeled_xml, rank)
2392        && preserved_style_attribute(raw, b"val")?.as_deref() == Some(value)
2393    {
2394        writer.get_mut().write_all(raw)?;
2395        return Ok(());
2396    }
2397    if let Some(raw) = preserved_modeled_xml(modeled_xml, rank) {
2398        return write_rewritten_style_scalar(writer, raw, value);
2399    }
2400    let mut element = BytesStart::new(name);
2401    element.push_attribute(("w:val", value));
2402    writer.write_event(Event::Empty(element))?;
2403    Ok(())
2404}
2405
2406fn write_style_toggle<W: std::io::Write>(
2407    writer: &mut Writer<W>,
2408    name: &str,
2409    rank: u8,
2410    value: Option<bool>,
2411    modeled_xml: &[(u8, Vec<u8>)],
2412) -> Result<()> {
2413    if let Some(value) = value {
2414        if let Some(raw) = preserved_modeled_xml(modeled_xml, rank) {
2415            let prefixes = vec!["w".to_owned()];
2416            let mut reader = Reader::from_reader(raw);
2417            let mut buf = Vec::new();
2418            let preserved = loop {
2419                match reader.read_event_into(&mut buf)? {
2420                    Event::Start(ref start) | Event::Empty(ref start) => {
2421                        let prefixes = word_prefixes_at(start, &prefixes)?;
2422                        break parse_style_toggle(start, &prefixes)?;
2423                    }
2424                    Event::Eof => break None,
2425                    _ => {}
2426                }
2427                buf.clear();
2428            };
2429            if preserved == Some(value) {
2430                writer.get_mut().write_all(raw)?;
2431                return Ok(());
2432            }
2433            return write_rewritten_style_scalar(writer, raw, if value { "1" } else { "0" });
2434        }
2435        let mut element = BytesStart::new(name);
2436        if !value {
2437            element.push_attribute(("w:val", "0"));
2438        }
2439        writer.write_event(Event::Empty(element))?;
2440    }
2441    Ok(())
2442}
2443
2444fn write_style_priority<W: std::io::Write>(
2445    writer: &mut Writer<W>,
2446    priority: Option<u32>,
2447    modeled_xml: &[(u8, Vec<u8>)],
2448) -> Result<()> {
2449    let Some(priority) = priority else {
2450        return Ok(());
2451    };
2452    if let Some(raw) = preserved_modeled_xml(modeled_xml, 7)
2453        && preserved_style_attribute(raw, b"val")?
2454            .as_deref()
2455            .and_then(|value| value.parse::<u32>().ok())
2456            == Some(priority)
2457    {
2458        writer.get_mut().write_all(raw)?;
2459        return Ok(());
2460    }
2461    let value = priority.to_string();
2462    if let Some(raw) = preserved_modeled_xml(modeled_xml, 7) {
2463        return write_rewritten_style_scalar(writer, raw, &value);
2464    }
2465    let mut element = BytesStart::new("w:uiPriority");
2466    element.push_attribute(("w:val", value.as_str()));
2467    writer.write_event(Event::Empty(element))?;
2468    Ok(())
2469}
2470
2471#[cfg(test)]
2472mod tests {
2473    use super::*;
2474
2475    #[test]
2476    fn round_trip_styles() {
2477        let styles = CT_Styles::new_default();
2478        let xml = styles.to_xml().unwrap();
2479        let parsed = CT_Styles::from_xml(&xml).unwrap();
2480
2481        // Namespace-close only the retained owners. Every other byte remains exact.
2482        let original = String::from_utf8(xml.clone()).unwrap();
2483        let (prefix, heading) = original
2484            .split_once(r#"<w:style w:type="paragraph" w:styleId="Heading1">"#)
2485            .unwrap();
2486        assert_eq!(prefix.matches("<w:docDefaults>").count(), 1);
2487        let prefix = prefix.replace(
2488            "<w:docDefaults>",
2489            &format!("<w:docDefaults xmlns:w=\"{W_NS}\">"),
2490        );
2491        let mut heading = heading.to_owned();
2492        for owner in ["pPr", "rPr"] {
2493            let opening = format!("<w:{owner}>");
2494            assert_eq!(heading.matches(&opening).count(), 1);
2495            heading = heading.replace(&opening, &format!("<w:{owner} xmlns:w=\"{W_NS}\">"));
2496        }
2497        let expected =
2498            format!(r#"{prefix}<w:style w:type="paragraph" w:styleId="Heading1">{heading}"#);
2499        let imported = parsed.to_xml().unwrap();
2500        assert_eq!(imported, expected.as_bytes());
2501        let reopened = CT_Styles::from_xml(&imported).unwrap();
2502        assert_eq!(reopened.to_xml().unwrap(), imported);
2503        assert_eq!(reopened, parsed);
2504
2505        assert_eq!(parsed.styles.len(), 2);
2506        assert!(parsed.doc_defaults.is_some());
2507
2508        let normal = parsed.get_by_id("Normal").unwrap();
2509        assert_eq!(normal.name, Some("Normal".to_string()));
2510        assert!(normal.is_default);
2511
2512        let h1 = parsed.get_by_id("Heading1").unwrap();
2513        assert_eq!(h1.based_on, Some("Normal".to_string()));
2514    }
2515
2516    #[test]
2517    fn find_default_style() {
2518        let styles = CT_Styles::new_default();
2519        let default_para = styles.get_default(StyleType::Paragraph).unwrap();
2520        assert_eq!(default_para.style_id, "Normal");
2521    }
2522
2523    #[test]
2524    fn linked_style_and_ui_flags_accept_aliases_and_serialize_in_schema_order() {
2525        let xml = format!(
2526            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>"#
2527        );
2528        let mut styles = CT_Styles::from_xml(xml.as_bytes()).unwrap();
2529        let style = styles.get_by_id("Corpus").unwrap();
2530        assert_eq!(style.next_style.as_deref(), Some("Normal"));
2531        assert_eq!(style.linked_style.as_deref(), Some("CorpusChar"));
2532        assert_eq!(style.auto_redefine, Some(false));
2533        assert_eq!(style.hidden, Some(true));
2534        assert_eq!(style.ui_priority, Some(17));
2535        assert_eq!(style.semi_hidden, Some(false));
2536        assert_eq!(style.unhide_when_used, Some(true));
2537        assert_eq!(style.quick_format, Some(true));
2538        assert_eq!(style.locked, Some(true));
2539
2540        let serialized = String::from_utf8(styles.to_xml().unwrap()).unwrap();
2541        let positions = [
2542            "<q:next",
2543            "<q:link",
2544            "<q:autoRedefine",
2545            "<q:hidden",
2546            "<q:uiPriority",
2547            "<q:semiHidden",
2548            "<q:unhideWhenUsed",
2549            "<q:qFormat",
2550            "<q:locked",
2551            "<q:pPr",
2552        ]
2553        .map(|token| serialized.find(token).unwrap());
2554        assert!(positions.windows(2).all(|pair| pair[0] < pair[1]));
2555        assert!(serialized.contains(&format!(
2556            r#"<q:pPr xmlns:q="{W_NS}"><q:spacing q:after="0"/></q:pPr>"#
2557        )));
2558        assert_eq!(serialized.matches("name-trivia").count(), 1);
2559        assert_eq!(serialized.matches("next kept").count(), 1);
2560        assert_eq!(serialized.matches("link-trivia").count(), 1);
2561        assert_eq!(serialized.matches(r#"x:root="&amp;""#).count(), 1);
2562        assert_eq!(serialized.matches(r#"x:keep="yes""#).count(), 1);
2563        let reparsed = CT_Styles::from_xml(serialized.as_bytes()).unwrap();
2564        assert_eq!(reparsed.styles[0], styles.styles[0]);
2565
2566        styles.styles[0].next_style = Some("Other".to_owned());
2567        styles.styles[0].auto_redefine = Some(true);
2568        styles.styles[0].ui_priority = Some(23);
2569        let changed = String::from_utf8(styles.to_xml().unwrap()).unwrap();
2570        assert!(changed.contains("<q:next"));
2571        assert!(changed.contains(r#"q:val="Other""#));
2572        assert_eq!(changed.matches("next kept").count(), 1);
2573        assert!(changed.contains("<q:autoRedefine"));
2574        assert!(changed.contains(r#"q:val="1""#));
2575        assert_eq!(changed.matches(r#"x:keep="yes""#).count(), 1);
2576        assert!(changed.contains("<q:uiPriority"));
2577        assert!(changed.contains(r#"q:val="23""#));
2578        let reparsed = CT_Styles::from_xml(changed.as_bytes()).unwrap();
2579        assert_eq!(reparsed.styles[0].next_style.as_deref(), Some("Other"));
2580        assert_eq!(reparsed.styles[0].auto_redefine, Some(true));
2581        assert_eq!(reparsed.styles[0].ui_priority, Some(23));
2582    }
2583
2584    #[test]
2585    fn style_parser_rejects_prefixes_that_conflict_with_fixed_writer_bindings() {
2586        let xml = format!(
2587            r#"<q:styles xmlns:q="{W_NS}" xmlns:w="urn:not-word"><q:style q:type="paragraph" q:styleId="Corpus"></q:style></q:styles>"#
2588        );
2589        let error = CT_Styles::from_xml(xml.as_bytes()).unwrap_err();
2590        assert!(error.to_string().contains("xmlns:w"));
2591    }
2592
2593    #[test]
2594    fn changed_table_style_properties_keep_one_unknown_child_in_schema_order() {
2595        let xml = format!(
2596            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>"#
2597        );
2598        let mut styles = CT_Styles::from_xml(xml.as_bytes()).unwrap();
2599        styles.styles[0]
2600            .table_properties
2601            .as_mut()
2602            .unwrap()
2603            .shading
2604            .as_mut()
2605            .unwrap()
2606            .fill = Some("D9EAF7".to_owned());
2607
2608        let serialized = String::from_utf8(styles.to_xml().unwrap()).unwrap();
2609        assert_eq!(serialized.matches("tblStyleRowBandSize").count(), 1);
2610        assert_eq!(serialized.matches("x:producer").count(), 1);
2611        assert!(
2612            serialized.find("tblStyleRowBandSize").unwrap() < serialized.find("w:shd").unwrap()
2613        );
2614        assert_eq!(
2615            CT_Styles::from_xml(serialized.as_bytes()).unwrap().styles[0]
2616                .table_properties
2617                .as_ref()
2618                .and_then(|properties| properties.shading.as_ref())
2619                .and_then(|shading| shading.fill.as_deref()),
2620            Some("D9EAF7")
2621        );
2622    }
2623
2624    #[test]
2625    fn aliased_style_paragraph_properties_use_ancestor_namespace_scope() {
2626        let xml = format!(
2627            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>"#
2628        );
2629        let parsed = CT_Styles::from_xml(xml.as_bytes()).unwrap();
2630        let ppr = parsed.styles[0].ppr.as_ref().unwrap();
2631        assert_eq!(ppr.jc, Some(crate::shared::ST_Jc::Center));
2632    }
2633
2634    #[test]
2635    fn direct_style_parser_uses_supplied_start_ancestor_scope() {
2636        let xml = format!(
2637            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>"#
2638        );
2639        let mut reader = Reader::from_str(&xml);
2640        let mut buf = Vec::new();
2641        let parsed = loop {
2642            match reader.read_event_into(&mut buf) {
2643                Ok(Event::Start(ref element)) if element.local_name().as_ref() == b"style" => {
2644                    break CT_Style::from_xml(&mut reader, element).unwrap();
2645                }
2646                Ok(Event::Eof) => panic!("missing style"),
2647                event => {
2648                    event.unwrap();
2649                }
2650            }
2651            buf.clear();
2652        };
2653        assert_eq!(
2654            parsed.ppr.as_ref().unwrap().jc,
2655            Some(crate::shared::ST_Jc::Center)
2656        );
2657    }
2658
2659    #[test]
2660    fn direct_style_parser_does_not_promote_foreign_start_prefix() {
2661        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>"#;
2662        let mut reader = Reader::from_str(xml);
2663        let mut buf = Vec::new();
2664        let parsed = loop {
2665            match reader.read_event_into(&mut buf) {
2666                Ok(Event::Start(ref element)) if element.local_name().as_ref() == b"style" => {
2667                    break CT_Style::from_xml(&mut reader, element).unwrap();
2668                }
2669                Ok(Event::Eof) => panic!("missing style"),
2670                event => {
2671                    event.unwrap();
2672                }
2673            }
2674            buf.clear();
2675        };
2676        assert!(parsed.ppr.is_none());
2677    }
2678
2679    #[test]
2680    fn direct_style_parser_accepts_default_word_namespace() {
2681        let xml = format!(
2682            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>"#
2683        );
2684        let mut reader = Reader::from_str(&xml);
2685        let mut buf = Vec::new();
2686        let parsed = loop {
2687            match reader.read_event_into(&mut buf) {
2688                Ok(Event::Start(ref element)) if element.local_name().as_ref() == b"style" => {
2689                    break CT_Style::from_xml(&mut reader, element).unwrap();
2690                }
2691                Ok(Event::Eof) => panic!("missing style"),
2692                event => {
2693                    event.unwrap();
2694                }
2695            }
2696            buf.clear();
2697        };
2698        assert_eq!(
2699            parsed.ppr.as_ref().unwrap().jc,
2700            Some(crate::shared::ST_Jc::Center)
2701        );
2702    }
2703
2704    #[test]
2705    fn table_style_properties_are_namespace_aware_schema_ordered_and_preserved() {
2706        let xml = format!(
2707            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>"#
2708        );
2709        let mut styles = CT_Styles::from_xml(xml.as_bytes()).unwrap();
2710        let style = styles.get_by_id("Dense").unwrap();
2711        assert_eq!(
2712            style
2713                .table_properties
2714                .as_ref()
2715                .and_then(|properties| properties.borders.as_ref())
2716                .and_then(|borders| borders.top.as_ref())
2717                .and_then(|border| border.sz),
2718            Some(8)
2719        );
2720        assert_eq!(style.conditional_table_styles.len(), 1);
2721        assert_eq!(
2722            style.conditional_table_styles[0].region,
2723            Some(TableStyleRegion::FirstRow)
2724        );
2725
2726        let serialized = String::from_utf8(styles.to_xml().unwrap()).unwrap();
2727        assert_eq!(serialized.matches("<q:tblPr").count(), 1);
2728        assert!(
2729            serialized
2730                .contains(r#"<ext:keep xmlns:ext="urn:producer" ext:value="byte-identical"/>"#)
2731        );
2732        let ppr = serialized.find("<q:pPr").unwrap();
2733        let rpr = serialized.find("<q:rPr").unwrap();
2734        let table_properties = serialized.find("<q:tblPr").unwrap();
2735        let conditional = serialized.find("<q:tblStylePr").unwrap();
2736        assert!(ppr < rpr && rpr < table_properties && table_properties < conditional);
2737        assert!(serialized.contains(&format!(
2738            r#"<q:pPr xmlns:q="{W_NS}"><q:spacing q:after="40"/></q:pPr>"#
2739        )));
2740        assert!(serialized.contains(&format!(r#"<q:rPr xmlns:q="{W_NS}"><q:b/></q:rPr>"#)));
2741        assert!(serialized.contains(r#"<bad:tblPr xmlns:bad="urn:not-word">"#));
2742        CT_Styles::from_xml(serialized.as_bytes()).expect("preserved prefixes remain bound");
2743
2744        styles.styles[0]
2745            .table_properties
2746            .as_mut()
2747            .unwrap()
2748            .borders
2749            .as_mut()
2750            .unwrap()
2751            .top
2752            .as_mut()
2753            .unwrap()
2754            .color = Some("445566".to_owned());
2755        let changed = String::from_utf8(styles.to_xml().unwrap()).unwrap();
2756        assert_eq!(changed.matches("<w:tblPr>").count(), 1);
2757        assert_eq!(changed.matches("445566").count(), 1);
2758        assert!(
2759            changed.contains(r#"<ext:keep xmlns:ext="urn:producer" ext:value="byte-identical"/>"#)
2760        );
2761        styles.styles[0].conditional_table_styles[0]
2762            .cell_properties
2763            .as_mut()
2764            .unwrap()
2765            .shading
2766            .as_mut()
2767            .unwrap()
2768            .fill = Some("DDEEFF".to_owned());
2769        styles.styles[0].conditional_table_styles[0].region = Some(TableStyleRegion::LastRow);
2770        let changed = String::from_utf8(styles.to_xml().unwrap()).unwrap();
2771        assert_eq!(changed.matches("DDEEFF").count(), 1);
2772        assert!(!changed.contains("AABBCC"));
2773        assert!(changed.contains(r#"<w:tblStylePr w:type="lastRow" x:region="&amp;""#));
2774        assert_eq!(changed.matches(r#"x:region="&amp;""#).count(), 1);
2775        assert!(!changed.contains(r#"tblStylePr q:type="firstRow""#));
2776        assert!(
2777            changed
2778                .contains(r#"<ext:conditional xmlns:ext="urn:producer" ext:value="preserved"/>"#)
2779        );
2780        let reparsed = CT_Styles::from_xml(changed.as_bytes())
2781            .expect("typed conditional projection remains valid XML");
2782        assert_eq!(
2783            reparsed.styles[0].conditional_table_styles[0].region,
2784            Some(TableStyleRegion::LastRow)
2785        );
2786        assert_eq!(
2787            reparsed.styles[0].conditional_table_styles[0]
2788                .cell_properties
2789                .as_ref()
2790                .and_then(|properties| properties.shading.as_ref())
2791                .and_then(|shading| shading.fill.as_deref()),
2792            Some("DDEEFF")
2793        );
2794    }
2795    #[test]
2796    fn fx200_defaults_source_preserves_unknown_wrappers_and_qualified_owner_after_font_edit() {
2797        let foreign =
2798            r#"<f:rPrDefault xmlns:f="urn:foreign" f:flag='&#x41;'><f:rPr/></f:rPrDefault>"#;
2799        let opaque = r#"<z:keep xmlns:z="urn:opaque" z:value='&#65;'><z:child/></z:keep>"#;
2800        let source = format!(
2801            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>"#
2802        );
2803        let mut styles = CT_Styles::from_xml(source.as_bytes()).unwrap();
2804        let defaults = styles.doc_defaults.as_ref().unwrap();
2805        assert_eq!(
2806            defaults.rpr.as_ref().unwrap().font_ascii.as_deref(),
2807            Some("Prior")
2808        );
2809        assert_eq!(defaults.rpr.as_ref().unwrap().bold, Some(true));
2810        let original_ppr = defaults.ppr.clone();
2811        let original_source = defaults.source_xml.clone().unwrap();
2812        let mut untouched = Writer::new(Vec::new());
2813        defaults.to_xml(&mut untouched).unwrap();
2814        assert_eq!(untouched.into_inner(), original_source);
2815        let properties = styles.doc_defaults.as_mut().unwrap().rpr.as_mut().unwrap();
2816        properties.font_ascii = Some("Selected".to_owned());
2817        properties.font_hansi = Some("Selected".to_owned());
2818        let saved = styles.to_xml().unwrap();
2819        let text = std::str::from_utf8(&saved).unwrap();
2820        assert!(text.contains(foreign) && text.contains(opaque));
2821        assert!(text.contains("x:root='&#65;'") && text.contains("x:wrapper='keep'"));
2822        assert!(text.contains("<!--before-->") && text.contains("<!--after-->"));
2823        assert!(text.contains("x:property='&#65;'"));
2824        assert!(text.contains("<q:b q:val='true'/><!--inside-->"));
2825        let original_text = std::str::from_utf8(&original_source).unwrap();
2826        let begin = original_text.find("<q:pPrDefault").unwrap();
2827        let end = original_text.find("</q:pPrDefault>").unwrap() + "</q:pPrDefault>".len();
2828        assert!(text.contains(&original_text[begin..end]));
2829        let reopened = CT_Styles::from_xml(&saved).unwrap();
2830        let defaults = reopened.doc_defaults.as_ref().unwrap();
2831        assert_eq!(
2832            defaults.rpr.as_ref().unwrap().font_ascii.as_deref(),
2833            Some("Selected")
2834        );
2835        assert_eq!(defaults.rpr.as_ref().unwrap().bold, Some(true));
2836        assert_eq!(defaults.ppr, original_ppr);
2837    }
2838
2839    #[test]
2840    fn fx200_empty_default_wrapper_and_duplicate_wrapper_last_owner_are_preserved() {
2841        let source = format!(
2842            r#"<w:styles xmlns:w="{W_NS}" xmlns:f="urn:foreign"><w:docDefaults f:keep='yes'/></w:styles>"#
2843        );
2844        let styles = CT_Styles::from_xml(source.as_bytes()).unwrap();
2845        let defaults = styles.doc_defaults.as_ref().unwrap();
2846        assert!(defaults.rpr.is_none() && defaults.ppr.is_none());
2847        assert!(
2848            std::str::from_utf8(&styles.to_xml().unwrap())
2849                .unwrap()
2850                .contains("f:keep='yes'")
2851        );
2852        let source = format!(
2853            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>"#
2854        );
2855        let mut styles = CT_Styles::from_xml(source.as_bytes()).unwrap();
2856        assert!(styles.doc_defaults.as_ref().unwrap().rpr.is_none());
2857        styles.doc_defaults.as_mut().unwrap().rpr = Some(CT_RPr {
2858            font_ascii: Some("Selected".to_owned()),
2859            ..Default::default()
2860        });
2861        let saved = styles.to_xml().unwrap();
2862        assert!(std::str::from_utf8(&saved).unwrap().contains("<w:b/>"));
2863        let reopened = CT_Styles::from_xml(&saved).unwrap();
2864        let properties = reopened.doc_defaults.unwrap().rpr.unwrap();
2865        assert_eq!(properties.font_ascii.as_deref(), Some("Selected"));
2866        assert!(properties.bold.is_none());
2867    }
2868    #[test]
2869    fn fx200_same_wrapper_duplicate_default_properties_do_not_resurface_on_font_clear() {
2870        let source = format!(
2871            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>"#
2872        );
2873        let mut styles = CT_Styles::from_xml(source.as_bytes()).unwrap();
2874        let properties = styles.doc_defaults.as_mut().unwrap().rpr.as_mut().unwrap();
2875        assert!(properties.bold.is_none());
2876        assert_eq!(properties.font_ascii.as_deref(), Some("Selected"));
2877        properties.font_ascii = None;
2878        let saved = styles.to_xml().unwrap();
2879        let text = std::str::from_utf8(&saved).unwrap();
2880        assert!(text.contains("<w:b/>") && text.contains("f:last='&#65;'"));
2881        let reopened = CT_Styles::from_xml(&saved).unwrap();
2882        let properties = reopened.doc_defaults.unwrap().rpr.unwrap();
2883        assert!(properties.bold.is_none() && properties.font_ascii.is_none());
2884    }
2885    #[test]
2886    fn fx200_duplicate_default_font_children_clear_owned_slots_without_reviving_or_erasing_raw() {
2887        for replacement in [None, Some("B")] {
2888            let source = format!(
2889                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>"#
2890            );
2891            let mut styles = CT_Styles::from_xml(source.as_bytes()).unwrap();
2892            let properties = styles.doc_defaults.as_mut().unwrap().rpr.as_mut().unwrap();
2893            assert_eq!(properties.font_ascii.as_deref(), Some("A"));
2894            assert_eq!(properties.font_hint.as_deref(), Some("cs"));
2895            properties.font_ascii = replacement.map(str::to_owned);
2896            let saved = styles.to_xml().unwrap();
2897            let text = std::str::from_utf8(&saved).unwrap();
2898            for raw in [
2899                "f:earlier='&#65;'",
2900                "<f:opaque/><!--font-child-->",
2901                "<!--between-->",
2902                "w:hint='cs'",
2903                "f:last='&#66;'",
2904            ] {
2905                assert!(text.contains(raw), "missing {raw}: {text}");
2906            }
2907            assert!(!text.contains("q:ascii='A'"));
2908            let reopened = CT_Styles::from_xml(&saved).unwrap();
2909            let properties = reopened.doc_defaults.unwrap().rpr.unwrap();
2910            assert_eq!(properties.font_ascii.as_deref(), replacement);
2911            assert_eq!(properties.font_hint.as_deref(), Some("cs"));
2912        }
2913    }
2914
2915    #[test]
2916    fn fx200_duplicate_default_size_children_clear_all_owned_values_preserving_foreign_payload() {
2917        let source = format!(
2918            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>"#
2919        );
2920        let mut styles = CT_Styles::from_xml(source.as_bytes()).unwrap();
2921        let properties = styles.doc_defaults.as_mut().unwrap().rpr.as_mut().unwrap();
2922        assert_eq!(properties.sz, Some(crate::units::HalfPoint(28)));
2923        assert_eq!(properties.sz_cs, Some(crate::units::HalfPoint(32)));
2924        properties.sz = None;
2925        properties.sz_cs = None;
2926        let saved = styles.to_xml().unwrap();
2927        let text = std::str::from_utf8(&saved).unwrap();
2928        for raw in [
2929            "f:first='&#65;'",
2930            "<!--size-->",
2931            "f:last='&#66;'",
2932            "<f:opaque/>",
2933            "f:first='CS'",
2934            "f:last='CS2'",
2935            "<f:sz f:val='999'/>",
2936        ] {
2937            assert!(text.contains(raw), "missing {raw}: {text}");
2938        }
2939        for value in ["q:val='24'", "w:val='28'", "q:val='20'", "w:val='32'"] {
2940            assert!(!text.contains(value));
2941        }
2942        let reopened = CT_Styles::from_xml(&saved).unwrap();
2943        let properties = reopened.doc_defaults.unwrap().rpr.unwrap();
2944        assert!(properties.sz.is_none() && properties.sz_cs.is_none());
2945    }
2946    #[test]
2947    fn fx200_style_property_root_attributes_and_untouched_owner_bytes_survive_updates() {
2948        let source = format!(
2949            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>"#
2950        );
2951        let mut styles = CT_Styles::from_xml(source.as_bytes()).unwrap();
2952        let original = styles.styles[0]
2953            .modeled_xml
2954            .iter()
2955            .find(|(rank, _)| *rank == 20)
2956            .unwrap()
2957            .1
2958            .clone();
2959        styles.styles[0].rpr.as_mut().unwrap().font_ascii = Some("B".to_owned());
2960        let saved = styles.to_xml().unwrap();
2961        assert!(
2962            saved
2963                .windows(original.len())
2964                .any(|window| window == original)
2965        );
2966        let text = std::str::from_utf8(&saved).unwrap();
2967        for raw in [
2968            "f:keep='&#65;'",
2969            "f:keep='&#66;'",
2970            "q:hint='cs'",
2971            "<!--rpr-->",
2972            "<f:opaque/>",
2973        ] {
2974            assert!(text.contains(raw), "{text}");
2975        }
2976        let reopened = CT_Styles::from_xml(&saved).unwrap();
2977        assert_eq!(
2978            reopened.styles[0]
2979                .rpr
2980                .as_ref()
2981                .unwrap()
2982                .font_ascii
2983                .as_deref(),
2984            Some("B")
2985        );
2986        assert_eq!(
2987            reopened.styles[0].ppr.as_ref().unwrap().keep_next,
2988            Some(true)
2989        );
2990    }
2991
2992    #[test]
2993    fn fx200_duplicate_style_property_owners_keep_empty_final_owner_and_foreign_peers() {
2994        let source = format!(
2995            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>"#
2996        );
2997        let mut styles = CT_Styles::from_xml(source.as_bytes()).unwrap();
2998        assert!(styles.styles[0].ppr.as_ref().unwrap().keep_next.is_none());
2999        assert!(styles.styles[0].rpr.as_ref().unwrap().bold.is_none());
3000        styles.styles[0].rpr.as_mut().unwrap().font_ascii = None;
3001        let saved = styles.to_xml().unwrap();
3002        let text = std::str::from_utf8(&saved).unwrap();
3003        for raw in [
3004            "<w:b/>",
3005            "<w:keepNext/>",
3006            "f:last='&#65;'",
3007            "f:last='&#66;'",
3008            "f:keep='peer'",
3009        ] {
3010            assert!(text.contains(raw), "{text}");
3011        }
3012        let first = text.find("<w:b/>").unwrap();
3013        let peer = text.find("f:keep='peer'").unwrap();
3014        let last = text.find("f:last='&#66;'").unwrap();
3015        assert!(first < peer && peer < last, "{text}");
3016        let reopened = CT_Styles::from_xml(&saved).unwrap();
3017        let style = &reopened.styles[0];
3018        assert!(
3019            style.rpr.as_ref().unwrap().bold.is_none()
3020                && style.rpr.as_ref().unwrap().font_ascii.is_none()
3021        );
3022        assert!(style.ppr.as_ref().unwrap().keep_next.is_none());
3023        styles.styles[0].rpr = None;
3024        let cleared = styles.to_xml().unwrap();
3025        let reopened = CT_Styles::from_xml(&cleared).unwrap();
3026        assert!(reopened.styles[0].rpr.is_none());
3027        assert!(
3028            std::str::from_utf8(&cleared)
3029                .unwrap()
3030                .contains("f:keep='peer'")
3031        );
3032    }
3033    #[test]
3034    fn fx200_style_border_container_edit_retains_aliased_root_and_opaque_payload() {
3035        let source = format!(
3036            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>"#
3037        );
3038        let mut styles = CT_Styles::from_xml(source.as_bytes()).unwrap();
3039        let edge = styles.styles[0]
3040            .ppr
3041            .as_mut()
3042            .unwrap()
3043            .borders
3044            .as_mut()
3045            .unwrap()
3046            .bottom
3047            .as_mut()
3048            .unwrap();
3049        edge.val = crate::shared::ST_Border::Double;
3050        edge.sz = Some(8);
3051        edge.color = Some("123456".to_owned());
3052        let saved = styles.to_xml().unwrap();
3053        let text = std::str::from_utf8(&saved).unwrap();
3054        for raw in [
3055            "f:outer='&#65;'",
3056            "f:keep='&#66;'",
3057            "f:edge='&#67;'",
3058            "<!--border-->",
3059            "<f:opaque>  keep  </f:opaque>",
3060            "<f:bottom f:val='foreign'/>",
3061        ] {
3062            assert!(text.contains(raw), "{text}");
3063        }
3064        let reopened = CT_Styles::from_xml(&saved).unwrap();
3065        let properties = reopened.styles[0].ppr.as_ref().unwrap();
3066        let border = properties
3067            .borders
3068            .as_ref()
3069            .unwrap()
3070            .bottom
3071            .as_ref()
3072            .unwrap();
3073        assert_eq!(border.val, crate::shared::ST_Border::Double);
3074        assert_eq!(border.sz, Some(8));
3075        assert_eq!(border.color.as_deref(), Some("123456"));
3076        assert_eq!(properties.keep_next, Some(true));
3077        assert_eq!(reopened.to_xml().unwrap(), saved);
3078    }
3079
3080    #[test]
3081    fn fx200_duplicate_style_border_clear_keeps_payload_but_suppresses_earlier_edges() {
3082        let source = format!(
3083            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>"#
3084        );
3085        let mut styles = CT_Styles::from_xml(source.as_bytes()).unwrap();
3086        assert!(
3087            styles.styles[0]
3088                .ppr
3089                .as_ref()
3090                .unwrap()
3091                .borders
3092                .as_ref()
3093                .unwrap()
3094                .bottom
3095                .is_some()
3096        );
3097        styles.styles[0].ppr.as_mut().unwrap().borders = None;
3098        let saved = styles.to_xml().unwrap();
3099        let text = std::str::from_utf8(&saved).unwrap();
3100        for raw in [
3101            "f:first='&#65;'",
3102            "f:last='&#66;'",
3103            "<f:opaque first='same'/>",
3104            "<f:opaque last='same'/>",
3105            "<!--last-->",
3106            "<f:pBdr f:keep='foreign'/>",
3107        ] {
3108            assert!(text.contains(raw), "{text}");
3109        }
3110        assert!(text.find("f:first='&#65;'").unwrap() < text.find("f:keep='foreign'").unwrap());
3111        assert!(text.find("f:keep='foreign'").unwrap() < text.find("f:last='&#66;'").unwrap());
3112        let reopened = CT_Styles::from_xml(&saved).unwrap();
3113        let properties = reopened.styles[0].ppr.as_ref().unwrap();
3114        let border = properties.borders.as_ref().unwrap();
3115        assert!(
3116            border.top.is_none()
3117                && border.bottom.is_none()
3118                && border.left.is_none()
3119                && border.right.is_none()
3120                && border.between.is_none()
3121                && border.bar.is_none()
3122        );
3123        assert_eq!(properties.keep_next, Some(true));
3124        assert_eq!(reopened.to_xml().unwrap(), saved);
3125    }
3126
3127    #[test]
3128    fn fx200_direct_style_reader_restores_both_trim_flags_and_opaque_sibling_whitespace() {
3129        for property in [
3130            "<w:pPr><w:keepNext/></w:pPr>",
3131            "<f:pPr><f:payload>  inner  </f:payload></f:pPr>",
3132            "<w:rPr><w:b/></w:rPr>",
3133            "<f:rPr><f:payload>  inner  </f:payload></f:rPr>",
3134        ] {
3135            for (trim_start, trim_end) in [(false, false), (false, true), (true, false)] {
3136                let source = format!(
3137                    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>"#
3138                );
3139                let mut reader = Reader::from_reader(source.as_bytes());
3140                reader.config_mut().trim_text_start = trim_start;
3141                reader.config_mut().trim_text_end = trim_end;
3142                let Event::Start(start) = reader.read_event().unwrap() else {
3143                    panic!("style root missing")
3144                };
3145                let style = CT_Style::from_xml(&mut reader, &start).unwrap();
3146                assert_eq!(reader.config().trim_text_start, trim_start);
3147                assert_eq!(reader.config().trim_text_end, trim_end);
3148                let expected = match (trim_start, trim_end) {
3149                    (false, false) => ">  keep  </f:opaque>",
3150                    (false, true) => ">  keep</f:opaque>",
3151                    _ => ">keep  </f:opaque>",
3152                };
3153                assert!(
3154                    style
3155                        .extra_xml
3156                        .iter()
3157                        .any(|(_, raw)| std::str::from_utf8(raw).unwrap().contains(expected)),
3158                    "{style:?}"
3159                );
3160            }
3161            let opening = &property[..property.find('>').unwrap() + 1];
3162            let malformed = format!(
3163                r#"<w:style xmlns:w="{W_NS}" xmlns:f="urn:foreign" w:type="paragraph" w:styleId="Direct">{opening}</f:wrong></w:style>"#
3164            );
3165            let mut reader = Reader::from_reader(malformed.as_bytes());
3166            reader.config_mut().trim_text(false);
3167            let Event::Start(start) = reader.read_event().unwrap() else {
3168                panic!("style root missing")
3169            };
3170            assert!(CT_Style::from_xml(&mut reader, &start).is_err());
3171            assert!(!reader.config().trim_text_start && !reader.config().trim_text_end);
3172        }
3173    }
3174    #[test]
3175    fn fx200_started_style_border_edges_match_empty_projection_and_keep_source_on_updates() {
3176        for prefix in ["w", "q"] {
3177            let source = format!(
3178                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>"#
3179            );
3180            let mut styles = CT_Styles::from_xml(source.as_bytes()).unwrap();
3181            let original = styles.to_xml().unwrap();
3182            let before = styles.styles[0]
3183                .ppr
3184                .as_ref()
3185                .unwrap()
3186                .borders
3187                .as_ref()
3188                .unwrap()
3189                .bottom
3190                .as_ref()
3191                .unwrap();
3192            assert_eq!(before.val, crate::shared::ST_Border::Single);
3193            assert!(before.source_xml.is_some());
3194            assert_eq!(
3195                CT_Styles::from_xml(&original).unwrap().to_xml().unwrap(),
3196                original
3197            );
3198            let edge = styles.styles[0]
3199                .ppr
3200                .as_mut()
3201                .unwrap()
3202                .borders
3203                .as_mut()
3204                .unwrap()
3205                .bottom
3206                .as_mut()
3207                .unwrap();
3208            edge.val = crate::shared::ST_Border::Double;
3209            edge.sz = Some(8);
3210            edge.color = Some("123456".to_owned());
3211            let saved = styles.to_xml().unwrap();
3212            let text = std::str::from_utf8(&saved).unwrap();
3213            for raw in [
3214                "f:edge='&#65;'",
3215                "<f:opaque>  keep  </f:opaque>",
3216                "<!--edge-->",
3217            ] {
3218                assert!(text.contains(raw), "{text}");
3219            }
3220            assert!(text.contains(&format!("</{prefix}:bottom>")), "{text}");
3221            let reopened = CT_Styles::from_xml(&saved).unwrap();
3222            let actual = reopened.styles[0]
3223                .ppr
3224                .as_ref()
3225                .unwrap()
3226                .borders
3227                .as_ref()
3228                .unwrap()
3229                .bottom
3230                .as_ref()
3231                .unwrap();
3232            assert_eq!(actual.val, crate::shared::ST_Border::Double);
3233            assert_eq!(actual.sz, Some(8));
3234            assert_eq!(actual.color.as_deref(), Some("123456"));
3235            assert_eq!(reopened.to_xml().unwrap(), saved);
3236            let empty = format!(
3237                r#"<w:pPr xmlns:w="{W_NS}"><w:pBdr><w:bottom w:val="double" w:sz="8" w:color="123456"/></w:pBdr></w:pPr>"#
3238            );
3239            let canonical =
3240                crate::numbering::parse_scoped_ppr(empty.as_bytes(), &["w".to_owned()]).unwrap();
3241            let expected = canonical.borders.as_ref().unwrap().bottom.as_ref().unwrap();
3242            assert_eq!(
3243                (actual.val, actual.sz, actual.color.as_deref()),
3244                (expected.val, expected.sz, expected.color.as_deref())
3245            );
3246        }
3247    }
3248    #[test]
3249    fn fx200_style_border_opaque_descendants_keep_bytes_and_never_become_direct_edges() {
3250        for opaque in [
3251            r#"<f:opaque><q:bottom q:val='double'></q:bottom></f:opaque>"#,
3252            r#"<f:opaque xmlns:q='urn:shadow'><q:bottom q:val='double'></q:bottom><w:left w:val='double'/></f:opaque>"#,
3253        ] {
3254            let source = format!(
3255                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>"#
3256            );
3257            let mut styles = CT_Styles::from_xml(source.as_bytes()).unwrap();
3258            let borders = styles.styles[0]
3259                .ppr
3260                .as_ref()
3261                .unwrap()
3262                .borders
3263                .as_ref()
3264                .unwrap();
3265            assert!(borders.bottom.is_none() && borders.left.is_none());
3266            assert_eq!(
3267                borders.right.as_ref().unwrap().val,
3268                crate::shared::ST_Border::Single
3269            );
3270            let noop = styles.to_xml().unwrap();
3271            assert!(std::str::from_utf8(&noop).unwrap().contains(opaque));
3272            styles.styles[0]
3273                .ppr
3274                .as_mut()
3275                .unwrap()
3276                .borders
3277                .as_mut()
3278                .unwrap()
3279                .right
3280                .as_mut()
3281                .unwrap()
3282                .val = crate::shared::ST_Border::Double;
3283            let saved = styles.to_xml().unwrap();
3284            let text = std::str::from_utf8(&saved).unwrap();
3285            assert!(
3286                text.contains(opaque)
3287                    && text.contains("<f:inside>  real  </f:inside>")
3288                    && text.contains("f:edge='&#65;'")
3289            );
3290            let reopened = CT_Styles::from_xml(&saved).unwrap();
3291            let actual = reopened.styles[0]
3292                .ppr
3293                .as_ref()
3294                .unwrap()
3295                .borders
3296                .as_ref()
3297                .unwrap();
3298            assert!(actual.bottom.is_none() && actual.left.is_none());
3299            assert_eq!(
3300                actual.right.as_ref().unwrap().val,
3301                crate::shared::ST_Border::Double
3302            );
3303            assert_eq!(reopened.to_xml().unwrap(), saved);
3304        }
3305    }
3306
3307    #[test]
3308    fn fx200_started_logical_style_border_edges_update_once_without_payload_duplication() {
3309        for (physical, logical) in [("start", "left"), ("end", "right")] {
3310            let source = format!(
3311                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>"#
3312            );
3313            let mut styles = CT_Styles::from_xml(source.as_bytes()).unwrap();
3314            let initial = styles.to_xml().unwrap();
3315            assert_eq!(
3316                CT_Styles::from_xml(&initial).unwrap().to_xml().unwrap(),
3317                initial
3318            );
3319            for (value, size, color) in [
3320                (crate::shared::ST_Border::Double, 8, "123456"),
3321                (crate::shared::ST_Border::Single, 12, "654321"),
3322            ] {
3323                let borders = styles.styles[0]
3324                    .ppr
3325                    .as_mut()
3326                    .unwrap()
3327                    .borders
3328                    .as_mut()
3329                    .unwrap();
3330                let edge = if logical == "left" {
3331                    borders.left.as_mut().unwrap()
3332                } else {
3333                    borders.right.as_mut().unwrap()
3334                };
3335                edge.val = value;
3336                edge.sz = Some(size);
3337                edge.color = Some(color.to_owned());
3338                let saved = styles.to_xml().unwrap();
3339                let text = std::str::from_utf8(&saved).unwrap();
3340                assert_eq!(
3341                    text.matches(&format!("<q:{physical} ")).count(),
3342                    1,
3343                    "{text}"
3344                );
3345                assert_eq!(
3346                    text.matches(&format!("</q:{physical}>")).count(),
3347                    1,
3348                    "{text}"
3349                );
3350                assert_eq!(
3351                    text.matches("<f:payload>  unique  </f:payload>").count(),
3352                    1,
3353                    "{text}"
3354                );
3355                assert_eq!(text.matches("<!--unique-->").count(), 1, "{text}");
3356                assert!(text.contains("f:edge='&#66;'") && text.contains("f:container='&#65;'"));
3357                styles = CT_Styles::from_xml(&saved).unwrap();
3358                let borders = styles.styles[0]
3359                    .ppr
3360                    .as_ref()
3361                    .unwrap()
3362                    .borders
3363                    .as_ref()
3364                    .unwrap();
3365                let actual = if logical == "left" {
3366                    borders.left.as_ref().unwrap()
3367                } else {
3368                    borders.right.as_ref().unwrap()
3369                };
3370                assert_eq!(
3371                    (actual.val, actual.sz, actual.color.as_deref()),
3372                    (value, Some(size), Some(color))
3373                );
3374                assert_eq!(styles.to_xml().unwrap(), saved);
3375            }
3376        }
3377    }
3378
3379    #[test]
3380    fn fx200_style_partial_border_clear_removes_all_aliases_without_touching_siblings() {
3381        let source = format!(
3382            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>"#
3383        );
3384        let mut styles = CT_Styles::from_xml(source.as_bytes()).unwrap();
3385        styles.styles[0]
3386            .ppr
3387            .as_mut()
3388            .unwrap()
3389            .borders
3390            .as_mut()
3391            .unwrap()
3392            .left = None;
3393        let saved = styles.to_xml().unwrap();
3394        let text = std::str::from_utf8(&saved).unwrap();
3395        assert!(!text.contains("<q:start") && !text.contains("<q:left"));
3396        for exact in [
3397            "f:root='&#65;'",
3398            "<!--keep-->",
3399            "f:stay='&#66;'",
3400            "<f:stay>  body  </f:stay>",
3401            "<f:left f:val='foreign'/>",
3402        ] {
3403            assert!(text.contains(exact), "{text}");
3404        }
3405        let reopened = CT_Styles::from_xml(&saved).unwrap();
3406        let borders = reopened.styles[0]
3407            .ppr
3408            .as_ref()
3409            .unwrap()
3410            .borders
3411            .as_ref()
3412            .unwrap();
3413        assert!(borders.left.is_none());
3414        assert_eq!(
3415            borders.right.as_ref().unwrap().val,
3416            crate::shared::ST_Border::Single
3417        );
3418        assert_eq!(reopened.to_xml().unwrap(), saved);
3419    }
3420    #[test]
3421    fn fx200_changed_style_scalar_keeps_physical_foreign_attributes_and_started_payload() {
3422        for started in [false, true] {
3423            let leaf = if started {
3424                "<q:u q:val='single' f:keep='&#65;'><f:opaque>  keep  </f:opaque><!--inside--></q:u>"
3425            } else {
3426                "<q:u q:val='single' f:keep='&#65;'/>"
3427            };
3428            let source = format!(
3429                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>"#
3430            );
3431            let mut styles = CT_Styles::from_xml(source.as_bytes()).unwrap();
3432            styles.styles[0].rpr.as_mut().unwrap().underline =
3433                Some(crate::shared::ST_Underline::Double);
3434            let saved = styles.to_xml().unwrap();
3435            let text = std::str::from_utf8(&saved).unwrap();
3436            assert!(
3437                text.contains("f:keep='&#65;'")
3438                    && text.contains("f:root='&#66;'")
3439                    && text.contains("<q:b/>"),
3440                "{text}"
3441            );
3442            if started {
3443                assert!(
3444                    text.contains("<f:opaque>  keep  </f:opaque><!--inside-->"),
3445                    "{text}"
3446                );
3447            }
3448            let reopened = CT_Styles::from_xml(&saved).unwrap();
3449            assert_eq!(
3450                reopened.styles[0].rpr.as_ref().unwrap().underline,
3451                Some(crate::shared::ST_Underline::Double)
3452            );
3453            assert_eq!(reopened.to_xml().unwrap(), saved);
3454        }
3455    }
3456}