Skip to main content

oxml_drawing/
table.rs

1use std::borrow::Cow;
2use std::io::Write;
3use std::ops::Range;
4
5use oxml_core::OxmlError;
6use oxml_core::raw_xml::{capture_element, capture_empty_element};
7use oxml_core::units::Emu;
8use oxml_core::xml::{local_name, matches_local_name};
9use oxml_core::xml_text::read_element_text;
10use quick_xml::events::{BytesEnd, BytesStart, BytesText, Event};
11use quick_xml::{Reader, Writer, XmlVersion};
12
13use crate::fill::Fill;
14use crate::line::CT_LineProperties;
15use crate::namespace::{A_NS, reject_conflicting_a_prefix};
16use crate::order::OrderedRawChildren;
17use crate::style_ref::StyleReference;
18use crate::text::CT_TextBody;
19
20pub type Result<T> = std::result::Result<T, OxmlError>;
21type RawAttributes = Vec<(String, String)>;
22
23/// One DrawingML `a:tbl` with typed grid, rows, cells, and banding properties.
24#[allow(non_camel_case_types)]
25#[derive(Clone, Debug)]
26pub struct CT_Table {
27    pub properties: Option<CT_TableProperties>,
28    pub grid: CT_TableGrid,
29    pub rows: Vec<CT_TableRow>,
30    raw_attributes: RawAttributes,
31    raw_children: OrderedRawChildren,
32    original_rows: Vec<CT_TableRow>,
33}
34
35/// The ordered column widths in one `a:tblGrid`.
36#[allow(non_camel_case_types)]
37#[derive(Clone, Debug)]
38pub struct CT_TableGrid {
39    pub columns: Vec<Emu>,
40    column_raw: Vec<GridColumnRaw>,
41    raw_attributes: RawAttributes,
42    raw_children: OrderedRawChildren,
43}
44
45/// Direction, edge emphasis, banding, and style identity from `a:tblPr`.
46#[allow(non_camel_case_types)]
47#[derive(Clone, Debug, Default)]
48pub struct CT_TableProperties {
49    pub right_to_left: bool,
50    pub first_row: bool,
51    pub first_column: bool,
52    pub last_row: bool,
53    pub last_column: bool,
54    pub band_rows: bool,
55    pub band_columns: bool,
56    pub style_id: Option<String>,
57    raw_attributes: RawAttributes,
58    raw_children: OrderedRawChildren,
59}
60
61/// One table row and its required stored height.
62#[allow(non_camel_case_types)]
63#[derive(Clone, Debug)]
64pub struct CT_TableRow {
65    pub height: Emu,
66    pub cells: Vec<CT_TableCell>,
67    raw_attributes: RawAttributes,
68    raw_children: OrderedRawChildren,
69    original_cells: Vec<CT_TableCell>,
70    origin_index: usize,
71}
72
73/// One table cell with typed text and the OOXML merge contract retained.
74#[allow(non_camel_case_types)]
75#[derive(Clone, Debug)]
76pub struct CT_TableCell {
77    pub text_body: Option<CT_TextBody>,
78    pub row_span: u32,
79    pub grid_span: u32,
80    pub horizontal_merge: bool,
81    pub vertical_merge: bool,
82    pub properties: Option<CT_TableCellProperties>,
83    raw_attributes: RawAttributes,
84    raw_children: OrderedRawChildren,
85    origin_index: usize,
86}
87
88#[derive(Clone, Debug, Default)]
89struct GridColumnRaw {
90    width: Emu,
91    attributes: RawAttributes,
92    children: Vec<Vec<u8>>,
93}
94
95impl PartialEq for GridColumnRaw {
96    fn eq(&self, other: &Self) -> bool {
97        self.attributes == other.attributes && self.children == other.children
98    }
99}
100
101impl Eq for GridColumnRaw {}
102
103#[allow(non_camel_case_types)]
104#[derive(Clone, Debug, Default, Eq, PartialEq)]
105pub struct CT_TableCellProperties {
106    pub margin_left: Option<Emu>,
107    pub margin_right: Option<Emu>,
108    pub margin_top: Option<Emu>,
109    pub margin_bottom: Option<Emu>,
110    pub fill: Option<Fill>,
111    pub left: Option<CT_LineProperties>,
112    pub right: Option<CT_LineProperties>,
113    pub top: Option<CT_LineProperties>,
114    pub bottom: Option<CT_LineProperties>,
115    pub unsupported: Vec<String>,
116    raw_attributes: RawAttributes,
117    raw_children: OrderedRawChildren,
118}
119
120/// One DrawingML table style list from `ppt/tableStyles.xml`.
121#[allow(non_camel_case_types)]
122#[derive(Clone, Debug, Eq, PartialEq)]
123pub struct CT_TableStyleList {
124    pub default_style_id: Option<String>,
125    pub styles: Vec<CT_TableStyle>,
126    raw_attributes: RawAttributes,
127    raw_children: OrderedRawChildren,
128}
129
130/// One named table style and its ordered region overlays.
131#[allow(non_camel_case_types)]
132#[derive(Clone, Debug, Default, Eq, PartialEq)]
133pub struct CT_TableStyle {
134    pub style_id: String,
135    pub style_name: String,
136    pub background: Option<CT_TableBackgroundStyle>,
137    pub whole_table: Option<CT_TablePartStyle>,
138    pub band1_horizontal: Option<CT_TablePartStyle>,
139    pub band2_horizontal: Option<CT_TablePartStyle>,
140    pub band1_vertical: Option<CT_TablePartStyle>,
141    pub band2_vertical: Option<CT_TablePartStyle>,
142    pub first_column: Option<CT_TablePartStyle>,
143    pub last_column: Option<CT_TablePartStyle>,
144    pub first_row: Option<CT_TablePartStyle>,
145    pub last_row: Option<CT_TablePartStyle>,
146    pub north_west_cell: Option<CT_TablePartStyle>,
147    pub north_east_cell: Option<CT_TablePartStyle>,
148    pub south_west_cell: Option<CT_TablePartStyle>,
149    pub south_east_cell: Option<CT_TablePartStyle>,
150    raw_attributes: RawAttributes,
151    raw_children: OrderedRawChildren,
152}
153
154/// The fill painted behind a whole table, from a style's `a:tblBg`.
155#[allow(non_camel_case_types)]
156#[derive(Clone, Debug, Default, Eq, PartialEq)]
157pub struct CT_TableBackgroundStyle {
158    pub fill: Option<Fill>,
159    pub fill_reference: Option<StyleReference>,
160    pub unsupported: Vec<String>,
161    raw_attributes: RawAttributes,
162    raw_children: OrderedRawChildren,
163}
164
165/// Cell and text formatting contributed by one table region.
166#[allow(non_camel_case_types)]
167#[derive(Clone, Debug, Default, Eq, PartialEq)]
168pub struct CT_TablePartStyle {
169    pub cell_style: Option<CT_TableCellStyle>,
170    pub text_style: Option<CT_TableTextStyle>,
171    raw_attributes: RawAttributes,
172    raw_children: OrderedRawChildren,
173}
174
175/// Fill and edge formatting contributed by a table style region.
176#[allow(non_camel_case_types)]
177#[derive(Clone, Debug, Default, Eq, PartialEq)]
178pub struct CT_TableCellStyle {
179    pub fill: Option<Fill>,
180    pub fill_reference: Option<StyleReference>,
181    pub borders: Option<CT_TableBorders>,
182    pub unsupported: Vec<String>,
183    raw_attributes: RawAttributes,
184    raw_children: OrderedRawChildren,
185}
186
187/// The four renderer-visible table-cell edges.
188#[allow(non_camel_case_types)]
189#[derive(Clone, Debug, Default, Eq, PartialEq)]
190pub struct CT_TableBorders {
191    pub left: Option<CT_LineProperties>,
192    pub right: Option<CT_LineProperties>,
193    pub top: Option<CT_LineProperties>,
194    pub bottom: Option<CT_LineProperties>,
195    pub inside_horizontal: Option<CT_LineProperties>,
196    pub inside_vertical: Option<CT_LineProperties>,
197    pub unsupported: Vec<String>,
198    raw_attributes: RawAttributes,
199    raw_children: OrderedRawChildren,
200}
201
202/// Bold, italic, colour, and theme-font formatting for one table region.
203#[allow(non_camel_case_types)]
204#[derive(Clone, Debug, Default, Eq, PartialEq)]
205pub struct CT_TableTextStyle {
206    pub bold: Option<bool>,
207    pub italic: Option<bool>,
208    pub font_reference: Option<StyleReference>,
209    pub color: Option<crate::color::ColorChoice>,
210    raw_attributes: RawAttributes,
211    raw_children: OrderedRawChildren,
212}
213
214impl PartialEq for CT_Table {
215    fn eq(&self, other: &Self) -> bool {
216        match (self.to_xml(), other.to_xml()) {
217            (Ok(left), Ok(right)) => left == right,
218            _ => {
219                self.properties == other.properties
220                    && self.grid == other.grid
221                    && self.rows == other.rows
222                    && self.raw_attributes == other.raw_attributes
223                    && self.raw_children == other.raw_children
224            }
225        }
226    }
227}
228
229impl Eq for CT_Table {}
230
231impl PartialEq for CT_TableGrid {
232    fn eq(&self, other: &Self) -> bool {
233        match (self.canonical_xml(), other.canonical_xml()) {
234            (Ok(left), Ok(right)) => left == right,
235            _ => {
236                self.columns == other.columns
237                    && self.column_raw == other.column_raw
238                    && self.raw_attributes == other.raw_attributes
239                    && self.raw_children == other.raw_children
240            }
241        }
242    }
243}
244
245impl Eq for CT_TableGrid {}
246
247impl PartialEq for CT_TableProperties {
248    fn eq(&self, other: &Self) -> bool {
249        match (self.canonical_xml(), other.canonical_xml()) {
250            (Ok(left), Ok(right)) => left == right,
251            _ => {
252                self.right_to_left == other.right_to_left
253                    && self.first_row == other.first_row
254                    && self.first_column == other.first_column
255                    && self.last_row == other.last_row
256                    && self.last_column == other.last_column
257                    && self.band_rows == other.band_rows
258                    && self.band_columns == other.band_columns
259                    && self.style_id == other.style_id
260                    && self.raw_attributes == other.raw_attributes
261                    && self.raw_children == other.raw_children
262            }
263        }
264    }
265}
266
267impl Eq for CT_TableProperties {}
268
269impl PartialEq for CT_TableRow {
270    fn eq(&self, other: &Self) -> bool {
271        match (self.canonical_xml(), other.canonical_xml()) {
272            (Ok(left), Ok(right)) => left == right,
273            _ => {
274                self.height == other.height
275                    && self.cells == other.cells
276                    && self.raw_attributes == other.raw_attributes
277                    && self.raw_children == other.raw_children
278            }
279        }
280    }
281}
282
283impl Eq for CT_TableRow {}
284
285impl PartialEq for CT_TableCell {
286    fn eq(&self, other: &Self) -> bool {
287        match (self.canonical_xml(), other.canonical_xml()) {
288            (Ok(left), Ok(right)) => left == right,
289            _ => {
290                self.text_body == other.text_body
291                    && self.row_span == other.row_span
292                    && self.grid_span == other.grid_span
293                    && self.horizontal_merge == other.horizontal_merge
294                    && self.vertical_merge == other.vertical_merge
295                    && self.properties == other.properties
296                    && self.raw_attributes == other.raw_attributes
297                    && self.raw_children == other.raw_children
298            }
299        }
300    }
301}
302
303impl Eq for CT_TableCell {}
304
305impl CT_Table {
306    /// Creates a rectangular table using truncating quotients and assigns each
307    /// remainder to the final column or row.
308    pub fn new(rows: usize, columns: usize, width: Emu, height: Emu) -> Result<Self> {
309        let row_count = u32::try_from(rows).map_err(|_| {
310            OxmlError::InvalidValue("table row count exceeds the DrawingML span range".to_owned())
311        })?;
312        let column_count = u32::try_from(columns).map_err(|_| {
313            OxmlError::InvalidValue(
314                "table column count exceeds the DrawingML span range".to_owned(),
315            )
316        })?;
317        if row_count == 0 || column_count == 0 {
318            return Err(OxmlError::InvalidValue(
319                "table requires at least one row and one column".to_owned(),
320            ));
321        }
322        if width.0 <= 0 || height.0 <= 0 {
323            return Err(OxmlError::InvalidValue(
324                "table width and height must be positive".to_owned(),
325            ));
326        }
327
328        let column_width = Emu(width.0 / i64::from(column_count));
329        let row_height = Emu(height.0 / i64::from(row_count));
330        if column_width.0 <= 0 || row_height.0 <= 0 {
331            return Err(OxmlError::InvalidValue(
332                "table extent is too small for its row or column count".to_owned(),
333            ));
334        }
335
336        let mut column_widths = vec![column_width; columns];
337        let column_remainder = width.0 - column_width.0 * i64::from(column_count);
338        column_widths[columns - 1].0 += column_remainder;
339        let column_raw = column_widths
340            .iter()
341            .copied()
342            .map(|width| GridColumnRaw {
343                width,
344                ..GridColumnRaw::default()
345            })
346            .collect();
347        let grid = CT_TableGrid {
348            columns: column_widths,
349            column_raw,
350            raw_attributes: Vec::new(),
351            raw_children: OrderedRawChildren::default(),
352        };
353        let row_remainder = height.0 - row_height.0 * i64::from(row_count);
354        let mut table_rows = Vec::with_capacity(rows);
355        for row_index in 0..rows {
356            let mut cells = Vec::with_capacity(columns);
357            for column_index in 0..columns {
358                cells.push(CT_TableCell {
359                    text_body: Some(CT_TextBody::new()),
360                    row_span: 1,
361                    grid_span: 1,
362                    horizontal_merge: false,
363                    vertical_merge: false,
364                    properties: Some(CT_TableCellProperties::default()),
365                    raw_attributes: Vec::new(),
366                    raw_children: OrderedRawChildren::default(),
367                    origin_index: column_index,
368                });
369            }
370            table_rows.push(CT_TableRow {
371                height: Emu(row_height.0
372                    + if row_index + 1 == rows {
373                        row_remainder
374                    } else {
375                        0
376                    }),
377                original_cells: cells.clone(),
378                cells,
379                raw_attributes: Vec::new(),
380                raw_children: OrderedRawChildren::default(),
381                origin_index: row_index,
382            });
383        }
384
385        Ok(Self {
386            properties: Some(CT_TableProperties {
387                first_row: true,
388                band_rows: true,
389                ..CT_TableProperties::default()
390            }),
391            grid,
392            original_rows: table_rows.clone(),
393            rows: table_rows,
394            raw_attributes: Vec::new(),
395            raw_children: OrderedRawChildren::default(),
396        })
397    }
398
399    /// Parses a complete `a:tbl` with any namespace prefix.
400    pub fn from_xml(xml: &[u8]) -> Result<Self> {
401        let mut reader = Reader::from_reader(xml);
402        let mut buffer = Vec::new();
403        loop {
404            match reader.read_event_into(&mut buffer)? {
405                Event::Start(start) if matches_local_name(start.name().as_ref(), b"tbl") => {
406                    reject_conflicting_a_prefix(&start)?;
407                    return Self::from_element(&mut reader, &start);
408                }
409                Event::Empty(start) if matches_local_name(start.name().as_ref(), b"tbl") => {
410                    reject_conflicting_a_prefix(&start)?;
411                    return Err(missing("a:tblGrid and a:tr"));
412                }
413                Event::Start(start) | Event::Empty(start) => return Err(unexpected(&start)),
414                Event::Eof => return Err(missing("a:tbl")),
415                _ => {}
416            }
417            buffer.clear();
418        }
419    }
420
421    /// Parses an extracted table and retains namespace bindings inherited from
422    /// its original ancestors so the standalone writer remains namespace
423    /// complete.
424    pub fn from_xml_with_inherited_namespaces(
425        xml: &[u8],
426        inherited_namespaces: &[(String, String)],
427    ) -> Result<Self> {
428        let local_a_namespace = root_a_namespace_declaration(xml)?;
429        let mut table = Self::from_xml(xml)?;
430        for (prefix, uri) in inherited_namespaces {
431            if prefix == "a" {
432                if uri != A_NS && local_a_namespace.as_deref() != Some(A_NS) {
433                    return Err(OxmlError::InvalidValue(
434                        "inherited xmlns:a conflicts with the fixed DrawingML writer namespace"
435                            .to_owned(),
436                    ));
437                }
438                continue;
439            }
440            if prefix == "xml" {
441                if uri != "http://www.w3.org/XML/1998/namespace" {
442                    return Err(OxmlError::InvalidValue(
443                        "inherited xmlns:xml has the wrong namespace URI".to_owned(),
444                    ));
445                }
446                continue;
447            }
448            let attribute_name = namespace_attribute_name(prefix)?;
449            if table
450                .raw_attributes
451                .iter()
452                .any(|(name, _)| name == &attribute_name)
453            {
454                continue;
455            }
456            table.raw_attributes.push((attribute_name, uri.to_owned()));
457        }
458        Ok(table)
459    }
460
461    fn from_element(reader: &mut Reader<&[u8]>, start: &BytesStart<'_>) -> Result<Self> {
462        let mut properties = None;
463        let mut grid = None;
464        let mut rows = Vec::new();
465        let mut raw_children = OrderedRawChildren::default();
466        let mut boundary = 0usize;
467        let mut buffer = Vec::new();
468        loop {
469            match reader.read_event_into(&mut buffer)? {
470                Event::Start(child) => {
471                    let name = local_name(child.name().as_ref()).to_vec();
472                    if matches!(name.as_slice(), b"tblPr" | b"tblGrid" | b"tr") {
473                        reject_conflicting_a_prefix(&child)?;
474                    }
475                    let raw = capture_element(reader, &child)?;
476                    capture_table_child(
477                        &name,
478                        raw,
479                        &mut properties,
480                        &mut grid,
481                        &mut rows,
482                        &mut raw_children,
483                        &mut boundary,
484                    )?;
485                }
486                Event::Empty(child) => {
487                    let name = local_name(child.name().as_ref()).to_vec();
488                    if matches!(name.as_slice(), b"tblPr" | b"tblGrid" | b"tr") {
489                        reject_conflicting_a_prefix(&child)?;
490                    }
491                    let raw = capture_empty_element(&child)?;
492                    capture_table_child(
493                        &name,
494                        raw,
495                        &mut properties,
496                        &mut grid,
497                        &mut rows,
498                        &mut raw_children,
499                        &mut boundary,
500                    )?;
501                }
502                Event::End(end) if matches_local_name(end.name().as_ref(), b"tbl") => break,
503                Event::Eof => return Err(missing("closing a:tbl")),
504                _ => {}
505            }
506            buffer.clear();
507        }
508        let grid = grid.ok_or_else(|| missing("a:tblGrid"))?;
509        if rows.is_empty() {
510            return Err(missing("at least one a:tr"));
511        }
512        Ok(Self {
513            properties,
514            grid,
515            original_rows: rows.clone(),
516            rows,
517            raw_attributes: raw_attributes(start, &[], true)?,
518            raw_children,
519        })
520    }
521
522    /// Serialises a self-contained table with fixed `a:` prefixes.
523    pub fn to_xml(&self) -> Result<Vec<u8>> {
524        if self.rows.is_empty() {
525            return Err(missing("at least one a:tr"));
526        }
527        let mut writer = Writer::new(Vec::new());
528        let mut start = BytesStart::new("a:tbl");
529        start.push_attribute(("xmlns:a", A_NS));
530        push_attributes(&mut start, &self.raw_attributes);
531        writer.write_event(Event::Start(start))?;
532        emit_raw(&mut writer, self.raw_children.at(0))?;
533        if let Some(properties) = &self.properties {
534            properties.write_xml(&mut writer)?;
535        }
536        emit_raw(&mut writer, self.raw_children.at(1))?;
537        self.grid.write_xml(&mut writer)?;
538        let current_row_origins = self
539            .rows
540            .iter()
541            .map(|row| row.origin_index)
542            .collect::<Vec<_>>();
543        let original_row_origins = self
544            .original_rows
545            .iter()
546            .map(|row| row.origin_index)
547            .collect::<Vec<_>>();
548        let row_matches = matched_original_indices(&current_row_origins, &original_row_origins);
549        let original_to_current = invert_matches(&row_matches, self.original_rows.len());
550        for boundary in 0..=self.rows.len() {
551            emit_raw(
552                &mut writer,
553                self.raw_children
554                    .at_reconciled(boundary, 2, &original_to_current, self.rows.len()),
555            )?;
556            if let Some(row) = self.rows.get(boundary) {
557                row.write_xml(&mut writer)?;
558            }
559        }
560        writer.write_event(Event::End(BytesEnd::new("a:tbl")))?;
561        Ok(writer.into_inner())
562    }
563
564    /// Changes one grid-column width and keeps the column's preserved
565    /// `a:gridCol` content with it, even when other columns share the old or
566    /// the new width.
567    pub fn set_column_width(&mut self, index: usize, width: Emu) -> Result<()> {
568        if index >= self.grid.columns.len() {
569            return Err(index_out_of_range("column", index));
570        }
571        self.grid.align_column_metadata()?;
572        self.grid.columns[index] = width;
573        self.grid.column_raw[index].width = width;
574        Ok(())
575    }
576
577    /// Inserts a row before the row at `index`, or appends one when `index`
578    /// equals the row count.
579    ///
580    /// The new row copies the height of the row above it, or of the first
581    /// row when it becomes the first. Each new cell copies the cell
582    /// properties of that row's cell in the same column and the formatting of
583    /// its first paragraph without the text. A row inserted inside a merged
584    /// cell extends the merge, and every other new cell is unmerged.
585    pub fn insert_row(&mut self, index: usize) -> Result<()> {
586        let regions = self.merge_regions()?;
587        if index > self.rows.len() {
588            return Err(index_out_of_range("row", index));
589        }
590        let template = self
591            .rows
592            .get(index.saturating_sub(1))
593            .ok_or_else(|| missing("at least one a:tr"))?;
594        let grown = regions
595            .into_iter()
596            .filter(|region| region.top < index && index < region.rows().end)
597            .collect::<Vec<_>>();
598        let mut cells = template
599            .cells
600            .iter()
601            .map(CT_TableCell::empty_like)
602            .collect::<Vec<_>>();
603        for region in &grown {
604            for column in region.columns() {
605                cells[column].copy_merge_state(&self.rows[index].cells[column]);
606            }
607        }
608        let row = CT_TableRow {
609            height: template.height,
610            cells,
611            raw_attributes: Vec::new(),
612            raw_children: OrderedRawChildren::default(),
613            original_cells: Vec::new(),
614            origin_index: usize::MAX,
615        };
616        self.rows.insert(index, row);
617        for region in grown {
618            self.replace_spans(
619                region.top..region.rows().end + 1,
620                region.columns(),
621                |cell| &mut cell.row_span,
622                region.row_span,
623                region.row_span + 1,
624            );
625        }
626        Ok(())
627    }
628
629    /// Removes the row at `index`, which must not be the only row.
630    ///
631    /// A merged cell across the row loses one row of its span. When the row
632    /// holds the top of such a merge, the row below takes over the origin,
633    /// with the origin's text and cell properties.
634    pub fn remove_row(&mut self, index: usize) -> Result<()> {
635        let regions = self.merge_regions()?;
636        if index >= self.rows.len() {
637            return Err(index_out_of_range("row", index));
638        }
639        if self.rows.len() == 1 {
640            return Err(OxmlError::InvalidValue(
641                "a table keeps at least one row".to_owned(),
642            ));
643        }
644        for region in regions
645            .iter()
646            .filter(|region| region.row_span > 1 && region.rows().contains(&index))
647        {
648            if index == region.top {
649                let (above, below) = self.rows.split_at_mut(index + 1);
650                for column in region.columns() {
651                    below[0].cells[column]
652                        .take_merge_origin(&mut above[index].cells[column], column == region.left);
653                }
654            }
655            self.replace_spans(
656                region.rows(),
657                region.columns(),
658                |cell| &mut cell.row_span,
659                region.row_span,
660                region.row_span - 1,
661            );
662        }
663        self.rows.remove(index);
664        Ok(())
665    }
666
667    /// Inserts a grid column before the column at `index`, or appends one
668    /// when `index` equals the column count.
669    ///
670    /// The new column copies the width of the column left of it, or of the
671    /// first column when it becomes the first, and each new cell copies that
672    /// column's cell in the same row as `insert_row` does. A column inserted
673    /// inside a merged cell extends the merge, and every other new cell is
674    /// unmerged.
675    pub fn insert_column(&mut self, index: usize) -> Result<()> {
676        let regions = self.merge_regions()?;
677        if index > self.grid.columns.len() {
678            return Err(index_out_of_range("column", index));
679        }
680        let template = index.saturating_sub(1);
681        let width = self
682            .grid
683            .columns
684            .get(template)
685            .copied()
686            .ok_or_else(|| missing("at least one a:gridCol"))?;
687        let grown = regions
688            .into_iter()
689            .filter(|region| region.left < index && index < region.columns().end)
690            .collect::<Vec<_>>();
691        self.grid.insert_column(index, width)?;
692        for (row_index, row) in self.rows.iter_mut().enumerate() {
693            let mut cell = row.cells[template].empty_like();
694            if grown
695                .iter()
696                .any(|region| region.rows().contains(&row_index))
697            {
698                cell.copy_merge_state(&row.cells[index]);
699            }
700            row.cells.insert(index, cell);
701        }
702        for region in grown {
703            self.replace_spans(
704                region.rows(),
705                region.left..region.columns().end + 1,
706                |cell| &mut cell.grid_span,
707                region.grid_span,
708                region.grid_span + 1,
709            );
710        }
711        Ok(())
712    }
713
714    /// Removes the grid column at `index`, which must not be the only one.
715    ///
716    /// A merged cell across the column loses one column of its span. When
717    /// the column holds the left edge of such a merge, the column to its right
718    /// takes over the origin, with the origin's text and cell properties.
719    pub fn remove_column(&mut self, index: usize) -> Result<()> {
720        let regions = self.merge_regions()?;
721        if index >= self.grid.columns.len() {
722            return Err(index_out_of_range("column", index));
723        }
724        if self.grid.columns.len() == 1 {
725            return Err(OxmlError::InvalidValue(
726                "a table keeps at least one column".to_owned(),
727            ));
728        }
729        self.grid.remove_column(index)?;
730        for region in regions
731            .iter()
732            .filter(|region| region.grid_span > 1 && region.columns().contains(&index))
733        {
734            if index == region.left {
735                for row in region.rows() {
736                    let (left, right) = self.rows[row].cells.split_at_mut(index + 1);
737                    right[0].take_merge_origin(&mut left[index], row == region.top);
738                }
739            }
740            self.replace_spans(
741                region.rows(),
742                region.columns(),
743                |cell| &mut cell.grid_span,
744                region.grid_span,
745                region.grid_span - 1,
746            );
747        }
748        for row in &mut self.rows {
749            row.cells.remove(index);
750        }
751        Ok(())
752    }
753
754    /// Returns every merged rectangle by its origin, after checking that each
755    /// row has one explicit cell per grid column and that every merge fits
756    /// the grid.
757    fn merge_regions(&self) -> Result<Vec<MergeRegion>> {
758        let columns = self.grid.columns.len();
759        if self.rows.iter().any(|row| row.cells.len() != columns) {
760            return Err(OxmlError::InvalidValue(
761                "table does not have a rectangular explicit cell grid".to_owned(),
762            ));
763        }
764        let mut regions = Vec::new();
765        for (top, row) in self.rows.iter().enumerate() {
766            for (left, cell) in row.cells.iter().enumerate() {
767                if cell.horizontal_merge
768                    || cell.vertical_merge
769                    || (cell.row_span == 1 && cell.grid_span == 1)
770                {
771                    continue;
772                }
773                let region = MergeRegion {
774                    top,
775                    left,
776                    row_span: cell.row_span,
777                    grid_span: cell.grid_span,
778                };
779                if region.rows().end > self.rows.len() || region.columns().end > columns {
780                    return Err(OxmlError::InvalidValue(
781                        "merge origin span exceeds the table grid".to_owned(),
782                    ));
783                }
784                regions.push(region);
785            }
786        }
787        Ok(regions)
788    }
789
790    /// Replaces the span `old` with `new` on every cell of a rectangle, which
791    /// keeps the span on whichever cells the producer stored it.
792    fn replace_spans(
793        &mut self,
794        rows: Range<usize>,
795        columns: Range<usize>,
796        span: fn(&mut CT_TableCell) -> &mut u32,
797        old: u32,
798        new: u32,
799    ) {
800        for row in &mut self.rows[rows] {
801            for cell in &mut row.cells[columns.clone()] {
802                let value = span(cell);
803                if *value == old {
804                    *value = new;
805                }
806            }
807        }
808    }
809}
810
811/// One merged rectangle of explicit cells, by its origin and its spans.
812struct MergeRegion {
813    top: usize,
814    left: usize,
815    row_span: u32,
816    grid_span: u32,
817}
818
819impl MergeRegion {
820    fn rows(&self) -> Range<usize> {
821        self.top..self.top.saturating_add(self.row_span as usize)
822    }
823
824    fn columns(&self) -> Range<usize> {
825        self.left..self.left.saturating_add(self.grid_span as usize)
826    }
827}
828
829fn index_out_of_range(kind: &str, index: usize) -> OxmlError {
830    OxmlError::InvalidValue(format!("{kind} index {index} is out of range"))
831}
832
833fn root_a_namespace_declaration(xml: &[u8]) -> Result<Option<String>> {
834    let mut reader = Reader::from_reader(xml);
835    let mut buffer = Vec::new();
836    loop {
837        match reader.read_event_into(&mut buffer)? {
838            Event::Start(start) | Event::Empty(start) => {
839                return decoded_attr(&start, b"xmlns:a");
840            }
841            Event::Eof => return Ok(None),
842            _ => buffer.clear(),
843        }
844    }
845}
846
847fn namespace_attribute_name(prefix: &str) -> Result<String> {
848    if prefix.is_empty() {
849        return Ok("xmlns".to_owned());
850    }
851    if prefix == "xmlns" {
852        return Err(OxmlError::InvalidValue(
853            "xmlns cannot be used as an inherited namespace prefix".to_owned(),
854        ));
855    }
856    let mut chars = prefix.chars();
857    let Some(first) = chars.next() else {
858        return Ok("xmlns".to_owned());
859    };
860    if !is_ncname_start(first) || !chars.all(is_ncname_continue) {
861        return Err(OxmlError::InvalidValue(format!(
862            "invalid inherited namespace prefix {prefix}"
863        )));
864    }
865    Ok(format!("xmlns:{prefix}"))
866}
867
868fn is_ncname_start(value: char) -> bool {
869    matches!(
870        value,
871        'A'..='Z'
872            | '_'
873            | 'a'..='z'
874            | '\u{C0}'..='\u{D6}'
875            | '\u{D8}'..='\u{F6}'
876            | '\u{F8}'..='\u{2FF}'
877            | '\u{370}'..='\u{37D}'
878            | '\u{37F}'..='\u{1FFF}'
879            | '\u{200C}'..='\u{200D}'
880            | '\u{2070}'..='\u{218F}'
881            | '\u{2C00}'..='\u{2FEF}'
882            | '\u{3001}'..='\u{D7FF}'
883            | '\u{F900}'..='\u{FDCF}'
884            | '\u{FDF0}'..='\u{FFFD}'
885            | '\u{10000}'..='\u{EFFFF}'
886    )
887}
888
889fn is_ncname_continue(value: char) -> bool {
890    is_ncname_start(value)
891        || matches!(
892            value,
893            '-' | '.' | '0'..='9' | '\u{B7}' | '\u{300}'..='\u{36F}' | '\u{203F}'..='\u{2040}'
894        )
895}
896
897#[allow(clippy::too_many_arguments)]
898fn capture_table_child(
899    name: &[u8],
900    raw: Vec<u8>,
901    properties: &mut Option<CT_TableProperties>,
902    grid: &mut Option<CT_TableGrid>,
903    rows: &mut Vec<CT_TableRow>,
904    raw_children: &mut OrderedRawChildren,
905    boundary: &mut usize,
906) -> Result<()> {
907    match name {
908        b"tblPr" if *boundary == 0 && properties.is_none() => {
909            *properties = Some(CT_TableProperties::from_xml(&raw)?);
910            *boundary = 1;
911        }
912        b"tblGrid" if *boundary <= 1 && grid.is_none() => {
913            *grid = Some(CT_TableGrid::from_xml(&raw)?);
914            *boundary = 2;
915        }
916        b"tr" if grid.is_some() => {
917            let mut row = CT_TableRow::from_xml(&raw)?;
918            row.origin_index = rows.len();
919            rows.push(row);
920            *boundary = 2 + rows.len();
921        }
922        b"tblPr" | b"tblGrid" | b"tr" => {
923            return Err(OxmlError::InvalidValue(
924                "a:tbl children must be a:tblPr?, a:tblGrid, then a:tr+".to_owned(),
925            ));
926        }
927        _ => raw_children.push(*boundary, raw),
928    }
929    Ok(())
930}
931
932fn matched_original_indices<T: PartialEq>(current: &[T], original: &[T]) -> Vec<Option<usize>> {
933    let mut matches = vec![None; current.len()];
934    let mut original_used = vec![false; original.len()];
935
936    for index in 0..current.len().min(original.len()) {
937        if current[index] == original[index] {
938            matches[index] = Some(index);
939            original_used[index] = true;
940        }
941    }
942    for (current_index, value) in current.iter().enumerate() {
943        if matches[current_index].is_some() {
944            continue;
945        }
946        if let Some(original_index) = original
947            .iter()
948            .enumerate()
949            .find(|(original_index, candidate)| {
950                !original_used[*original_index] && *candidate == value
951            })
952            .map(|(original_index, _)| original_index)
953        {
954            matches[current_index] = Some(original_index);
955            original_used[original_index] = true;
956        }
957    }
958    for (current_index, matched) in matches.iter_mut().enumerate() {
959        if matched.is_none() && current_index < original.len() && !original_used[current_index] {
960            *matched = Some(current_index);
961            original_used[current_index] = true;
962        }
963    }
964    matches
965}
966
967fn invert_matches(
968    current_to_original: &[Option<usize>],
969    original_len: usize,
970) -> Vec<Option<usize>> {
971    let mut original_to_current = vec![None; original_len];
972    for (current_index, original_index) in current_to_original.iter().enumerate() {
973        if let Some(original_index) = original_index {
974            original_to_current[*original_index] = Some(current_index);
975        }
976    }
977    original_to_current
978}
979
980impl CT_TableProperties {
981    fn canonical_xml(&self) -> Result<Vec<u8>> {
982        let mut writer = Writer::new(Vec::new());
983        self.write_xml(&mut writer)?;
984        Ok(writer.into_inner())
985    }
986
987    fn from_xml(xml: &[u8]) -> Result<Self> {
988        let mut reader = Reader::from_reader(xml);
989        let mut buffer = Vec::new();
990        loop {
991            match reader.read_event_into(&mut buffer)? {
992                Event::Start(start) if matches_local_name(start.name().as_ref(), b"tblPr") => {
993                    reject_conflicting_a_prefix(&start)?;
994                    return Self::from_element(&mut reader, &start);
995                }
996                Event::Empty(start) if matches_local_name(start.name().as_ref(), b"tblPr") => {
997                    reject_conflicting_a_prefix(&start)?;
998                    return Self::from_start(&start, None, OrderedRawChildren::default());
999                }
1000                Event::Start(start) | Event::Empty(start) => return Err(unexpected(&start)),
1001                Event::Eof => return Err(missing("a:tblPr")),
1002                _ => {}
1003            }
1004            buffer.clear();
1005        }
1006    }
1007
1008    fn from_element(reader: &mut Reader<&[u8]>, start: &BytesStart<'_>) -> Result<Self> {
1009        let mut style_id = None;
1010        let mut raw_children = OrderedRawChildren::default();
1011        let mut boundary = 0usize;
1012        let mut buffer = Vec::new();
1013        loop {
1014            match reader.read_event_into(&mut buffer)? {
1015                Event::Start(child)
1016                    if matches_local_name(child.name().as_ref(), b"tableStyleId") =>
1017                {
1018                    reject_conflicting_a_prefix(&child)?;
1019                    if style_id.is_some() {
1020                        return Err(OxmlError::InvalidValue(
1021                            "duplicate a:tableStyleId".to_owned(),
1022                        ));
1023                    }
1024                    style_id = Some(read_element_text(reader, child.name()));
1025                    boundary = 1;
1026                }
1027                Event::Empty(child)
1028                    if matches_local_name(child.name().as_ref(), b"tableStyleId") =>
1029                {
1030                    reject_conflicting_a_prefix(&child)?;
1031                    if style_id.replace(String::new()).is_some() {
1032                        return Err(OxmlError::InvalidValue(
1033                            "duplicate a:tableStyleId".to_owned(),
1034                        ));
1035                    }
1036                    boundary = 1;
1037                }
1038                Event::Start(child) => {
1039                    raw_children.push(boundary, capture_element(reader, &child)?)
1040                }
1041                Event::Empty(child) => raw_children.push(boundary, capture_empty_element(&child)?),
1042                Event::End(end) if matches_local_name(end.name().as_ref(), b"tblPr") => break,
1043                Event::Eof => return Err(missing("closing a:tblPr")),
1044                _ => {}
1045            }
1046            buffer.clear();
1047        }
1048        Self::from_start(start, style_id, raw_children)
1049    }
1050
1051    fn from_start(
1052        start: &BytesStart<'_>,
1053        style_id: Option<String>,
1054        raw_children: OrderedRawChildren,
1055    ) -> Result<Self> {
1056        Ok(Self {
1057            right_to_left: bool_attr(start, b"rtl")?.unwrap_or(false),
1058            first_row: bool_attr(start, b"firstRow")?.unwrap_or(false),
1059            first_column: bool_attr(start, b"firstCol")?.unwrap_or(false),
1060            last_row: bool_attr(start, b"lastRow")?.unwrap_or(false),
1061            last_column: bool_attr(start, b"lastCol")?.unwrap_or(false),
1062            band_rows: bool_attr(start, b"bandRow")?.unwrap_or(false),
1063            band_columns: bool_attr(start, b"bandCol")?.unwrap_or(false),
1064            style_id,
1065            raw_attributes: raw_attributes(
1066                start,
1067                &[
1068                    b"rtl",
1069                    b"firstRow",
1070                    b"firstCol",
1071                    b"lastRow",
1072                    b"lastCol",
1073                    b"bandRow",
1074                    b"bandCol",
1075                ],
1076                false,
1077            )?,
1078            raw_children,
1079        })
1080    }
1081
1082    fn write_xml<W: Write>(&self, writer: &mut Writer<W>) -> Result<()> {
1083        let mut start = BytesStart::new("a:tblPr");
1084        push_true(&mut start, "rtl", self.right_to_left);
1085        push_true(&mut start, "firstRow", self.first_row);
1086        push_true(&mut start, "firstCol", self.first_column);
1087        push_true(&mut start, "lastRow", self.last_row);
1088        push_true(&mut start, "lastCol", self.last_column);
1089        push_true(&mut start, "bandRow", self.band_rows);
1090        push_true(&mut start, "bandCol", self.band_columns);
1091        push_attributes(&mut start, &self.raw_attributes);
1092        if self.style_id.is_none() && self.raw_children.is_empty() {
1093            writer.write_event(Event::Empty(start))?;
1094            return Ok(());
1095        }
1096        writer.write_event(Event::Start(start))?;
1097        emit_raw(writer, self.raw_children.at(0))?;
1098        if let Some(style_id) = &self.style_id {
1099            writer.write_event(Event::Start(BytesStart::new("a:tableStyleId")))?;
1100            writer.write_event(Event::Text(BytesText::new(style_id)))?;
1101            writer.write_event(Event::End(BytesEnd::new("a:tableStyleId")))?;
1102        }
1103        emit_raw(writer, self.raw_children.at(1))?;
1104        writer.write_event(Event::End(BytesEnd::new("a:tblPr")))?;
1105        Ok(())
1106    }
1107}
1108
1109impl CT_TableGrid {
1110    fn canonical_xml(&self) -> Result<Vec<u8>> {
1111        let mut writer = Writer::new(Vec::new());
1112        self.write_xml(&mut writer)?;
1113        Ok(writer.into_inner())
1114    }
1115
1116    fn from_xml(xml: &[u8]) -> Result<Self> {
1117        let mut reader = Reader::from_reader(xml);
1118        let mut buffer = Vec::new();
1119        loop {
1120            match reader.read_event_into(&mut buffer)? {
1121                Event::Start(start) if matches_local_name(start.name().as_ref(), b"tblGrid") => {
1122                    reject_conflicting_a_prefix(&start)?;
1123                    return Self::from_element(&mut reader, &start);
1124                }
1125                Event::Empty(start) if matches_local_name(start.name().as_ref(), b"tblGrid") => {
1126                    reject_conflicting_a_prefix(&start)?;
1127                    return Err(missing("at least one a:gridCol"));
1128                }
1129                Event::Start(start) | Event::Empty(start) => return Err(unexpected(&start)),
1130                Event::Eof => return Err(missing("a:tblGrid")),
1131                _ => {}
1132            }
1133            buffer.clear();
1134        }
1135    }
1136
1137    fn from_element(reader: &mut Reader<&[u8]>, start: &BytesStart<'_>) -> Result<Self> {
1138        let mut columns = Vec::new();
1139        let mut column_raw = Vec::new();
1140        let mut raw_children = OrderedRawChildren::default();
1141        let mut buffer = Vec::new();
1142        loop {
1143            match reader.read_event_into(&mut buffer)? {
1144                Event::Start(child) if matches_local_name(child.name().as_ref(), b"gridCol") => {
1145                    reject_conflicting_a_prefix(&child)?;
1146                    let raw = capture_element(reader, &child)?;
1147                    let (width, metadata) = parse_grid_column(&raw)?;
1148                    columns.push(width);
1149                    column_raw.push(metadata);
1150                }
1151                Event::Empty(child) if matches_local_name(child.name().as_ref(), b"gridCol") => {
1152                    reject_conflicting_a_prefix(&child)?;
1153                    let width = Emu(required_i64(&child, b"w")?);
1154                    columns.push(width);
1155                    column_raw.push(GridColumnRaw {
1156                        width,
1157                        attributes: raw_attributes(&child, &[b"w"], false)?,
1158                        children: Vec::new(),
1159                    });
1160                }
1161                Event::Start(child) => {
1162                    raw_children.push(columns.len(), capture_element(reader, &child)?)
1163                }
1164                Event::Empty(child) => {
1165                    raw_children.push(columns.len(), capture_empty_element(&child)?)
1166                }
1167                Event::End(end) if matches_local_name(end.name().as_ref(), b"tblGrid") => break,
1168                Event::Eof => return Err(missing("closing a:tblGrid")),
1169                _ => {}
1170            }
1171            buffer.clear();
1172        }
1173        if columns.is_empty() {
1174            return Err(missing("at least one a:gridCol"));
1175        }
1176        Ok(Self {
1177            columns,
1178            column_raw,
1179            raw_attributes: raw_attributes(start, &[], false)?,
1180            raw_children,
1181        })
1182    }
1183
1184    fn write_xml<W: Write>(&self, writer: &mut Writer<W>) -> Result<()> {
1185        if self.columns.is_empty() {
1186            return Err(missing("at least one a:gridCol"));
1187        }
1188        let column_matches = self.matched_column_indices()?;
1189        let original_to_current = invert_matches(&column_matches, self.column_raw.len());
1190        let mut start = BytesStart::new("a:tblGrid");
1191        push_attributes(&mut start, &self.raw_attributes);
1192        writer.write_event(Event::Start(start))?;
1193        for (boundary, (width, metadata_index)) in
1194            self.columns.iter().zip(&column_matches).enumerate()
1195        {
1196            emit_raw(
1197                writer,
1198                self.raw_children.at_reconciled(
1199                    boundary,
1200                    0,
1201                    &original_to_current,
1202                    self.columns.len(),
1203                ),
1204            )?;
1205            let metadata = metadata_index.map(|index| &self.column_raw[index]);
1206            let mut column = BytesStart::new("a:gridCol");
1207            let width = width.0.to_string();
1208            column.push_attribute(("w", width.as_str()));
1209            if let Some(metadata) = metadata {
1210                push_attributes(&mut column, &metadata.attributes);
1211            }
1212            match metadata {
1213                Some(metadata) if !metadata.children.is_empty() => {
1214                    writer.write_event(Event::Start(column))?;
1215                    for child in &metadata.children {
1216                        writer.get_mut().write_all(child)?;
1217                    }
1218                    writer.write_event(Event::End(BytesEnd::new("a:gridCol")))?;
1219                }
1220                _ => writer.write_event(Event::Empty(column))?,
1221            }
1222        }
1223        emit_raw(
1224            writer,
1225            self.raw_children.at_reconciled(
1226                self.columns.len(),
1227                0,
1228                &original_to_current,
1229                self.columns.len(),
1230            ),
1231        )?;
1232        writer.write_event(Event::End(BytesEnd::new("a:tblGrid")))?;
1233        Ok(())
1234    }
1235
1236    fn matched_column_indices(&self) -> Result<Vec<Option<usize>>> {
1237        let original_widths = self
1238            .column_raw
1239            .iter()
1240            .map(|metadata| metadata.width)
1241            .collect::<Vec<_>>();
1242        if self.columns == original_widths {
1243            return Ok((0..self.columns.len()).map(Some).collect());
1244        }
1245
1246        let identity_sensitive = self
1247            .column_raw
1248            .iter()
1249            .any(|metadata| !metadata.attributes.is_empty() || !metadata.children.is_empty())
1250            || !self.raw_children.is_empty();
1251        if !identity_sensitive {
1252            return Ok(matched_original_indices(&self.columns, &original_widths));
1253        }
1254
1255        if has_duplicate_values(&self.columns) || has_duplicate_values(&original_widths) {
1256            return Err(ambiguous_grid_metadata());
1257        }
1258
1259        let exact_matches = self
1260            .columns
1261            .iter()
1262            .map(|width| {
1263                original_widths
1264                    .iter()
1265                    .position(|original| original == width)
1266            })
1267            .collect::<Vec<_>>();
1268        let unmatched_current = exact_matches
1269            .iter()
1270            .filter(|matched| matched.is_none())
1271            .count();
1272        let unmatched_original = original_widths
1273            .iter()
1274            .enumerate()
1275            .filter(|(index, _)| !exact_matches.contains(&Some(*index)))
1276            .count();
1277        if unmatched_current > 0
1278            && unmatched_original > 0
1279            && (unmatched_current > 1 || unmatched_original > 1)
1280        {
1281            return Err(ambiguous_grid_metadata());
1282        }
1283        Ok(matched_original_indices(&self.columns, &original_widths))
1284    }
1285
1286    /// Pairs every current column with the preserved metadata it is matched
1287    /// with, and anchors raw grid children to current columns, so the next
1288    /// edit is matched by position instead of by width.
1289    fn align_column_metadata(&mut self) -> Result<()> {
1290        let matches = self.matched_column_indices()?;
1291        let original_to_current = invert_matches(&matches, self.column_raw.len());
1292        let mut raw_children = OrderedRawChildren::default();
1293        for boundary in 0..=self.columns.len() {
1294            for child in self.raw_children.at_reconciled(
1295                boundary,
1296                0,
1297                &original_to_current,
1298                self.columns.len(),
1299            ) {
1300                raw_children.push(boundary, child.to_vec());
1301            }
1302        }
1303        self.column_raw = self
1304            .columns
1305            .iter()
1306            .zip(&matches)
1307            .map(|(width, matched)| GridColumnRaw {
1308                width: *width,
1309                ..matched.map_or_else(GridColumnRaw::default, |index| {
1310                    self.column_raw[index].clone()
1311                })
1312            })
1313            .collect();
1314        self.raw_children = raw_children;
1315        Ok(())
1316    }
1317
1318    /// Inserts a column without preserved metadata before the column at
1319    /// `index`, keeping raw grid children before the column they preceded.
1320    fn insert_column(&mut self, index: usize, width: Emu) -> Result<()> {
1321        self.align_column_metadata()?;
1322        self.raw_children.shift_boundaries_from(index);
1323        self.columns.insert(index, width);
1324        self.column_raw.insert(
1325            index,
1326            GridColumnRaw {
1327                width,
1328                ..GridColumnRaw::default()
1329            },
1330        );
1331        Ok(())
1332    }
1333
1334    /// Removes one column and its preserved metadata, moving raw grid
1335    /// children that preceded it before the next column.
1336    fn remove_column(&mut self, index: usize) -> Result<()> {
1337        self.align_column_metadata()?;
1338        let mut raw_children = OrderedRawChildren::default();
1339        for boundary in 0..=self.columns.len() {
1340            let target = if boundary > index {
1341                boundary - 1
1342            } else {
1343                boundary
1344            };
1345            for child in self.raw_children.at(boundary) {
1346                raw_children.push(target, child.to_vec());
1347            }
1348        }
1349        self.raw_children = raw_children;
1350        self.columns.remove(index);
1351        self.column_raw.remove(index);
1352        Ok(())
1353    }
1354}
1355
1356fn has_duplicate_values<T: PartialEq>(values: &[T]) -> bool {
1357    values
1358        .iter()
1359        .enumerate()
1360        .any(|(index, value)| values[..index].contains(value))
1361}
1362
1363fn ambiguous_grid_metadata() -> OxmlError {
1364    OxmlError::InvalidValue(
1365        "edited table grid is ambiguous because preserved column metadata cannot be associated reliably"
1366            .to_owned(),
1367    )
1368}
1369
1370fn parse_grid_column(xml: &[u8]) -> Result<(Emu, GridColumnRaw)> {
1371    let mut reader = Reader::from_reader(xml);
1372    let mut buffer = Vec::new();
1373    loop {
1374        match reader.read_event_into(&mut buffer)? {
1375            Event::Start(start) => {
1376                let width = Emu(required_i64(&start, b"w")?);
1377                let attributes = raw_attributes(&start, &[b"w"], false)?;
1378                let mut children = Vec::new();
1379                loop {
1380                    buffer.clear();
1381                    match reader.read_event_into(&mut buffer)? {
1382                        Event::Start(child) => children.push(capture_element(&mut reader, &child)?),
1383                        Event::Empty(child) => children.push(capture_empty_element(&child)?),
1384                        Event::End(end) if matches_local_name(end.name().as_ref(), b"gridCol") => {
1385                            break;
1386                        }
1387                        Event::Eof => return Err(missing("closing a:gridCol")),
1388                        _ => {}
1389                    }
1390                }
1391                return Ok((
1392                    width,
1393                    GridColumnRaw {
1394                        width,
1395                        attributes,
1396                        children,
1397                    },
1398                ));
1399            }
1400            Event::Eof => return Err(missing("a:gridCol")),
1401            _ => {}
1402        }
1403        buffer.clear();
1404    }
1405}
1406
1407impl CT_TableRow {
1408    fn canonical_xml(&self) -> Result<Vec<u8>> {
1409        let mut writer = Writer::new(Vec::new());
1410        self.write_xml(&mut writer)?;
1411        Ok(writer.into_inner())
1412    }
1413
1414    fn from_xml(xml: &[u8]) -> Result<Self> {
1415        let mut reader = Reader::from_reader(xml);
1416        let mut buffer = Vec::new();
1417        loop {
1418            match reader.read_event_into(&mut buffer)? {
1419                Event::Start(start) if matches_local_name(start.name().as_ref(), b"tr") => {
1420                    reject_conflicting_a_prefix(&start)?;
1421                    return Self::from_element(&mut reader, &start);
1422                }
1423                Event::Empty(start) if matches_local_name(start.name().as_ref(), b"tr") => {
1424                    reject_conflicting_a_prefix(&start)?;
1425                    return Err(missing("at least one a:tc"));
1426                }
1427                Event::Start(start) | Event::Empty(start) => return Err(unexpected(&start)),
1428                Event::Eof => return Err(missing("a:tr")),
1429                _ => {}
1430            }
1431            buffer.clear();
1432        }
1433    }
1434
1435    fn from_element(reader: &mut Reader<&[u8]>, start: &BytesStart<'_>) -> Result<Self> {
1436        let mut cells = Vec::new();
1437        let mut raw_children = OrderedRawChildren::default();
1438        let mut buffer = Vec::new();
1439        loop {
1440            match reader.read_event_into(&mut buffer)? {
1441                Event::Start(child) if matches_local_name(child.name().as_ref(), b"tc") => {
1442                    reject_conflicting_a_prefix(&child)?;
1443                    let mut cell = CT_TableCell::from_xml(&capture_element(reader, &child)?)?;
1444                    cell.origin_index = cells.len();
1445                    cells.push(cell);
1446                }
1447                Event::Empty(child) if matches_local_name(child.name().as_ref(), b"tc") => {
1448                    reject_conflicting_a_prefix(&child)?;
1449                    let mut cell = CT_TableCell::from_xml(&capture_empty_element(&child)?)?;
1450                    cell.origin_index = cells.len();
1451                    cells.push(cell);
1452                }
1453                Event::Start(child) => {
1454                    raw_children.push(cells.len(), capture_element(reader, &child)?)
1455                }
1456                Event::Empty(child) => {
1457                    raw_children.push(cells.len(), capture_empty_element(&child)?)
1458                }
1459                Event::End(end) if matches_local_name(end.name().as_ref(), b"tr") => break,
1460                Event::Eof => return Err(missing("closing a:tr")),
1461                _ => {}
1462            }
1463            buffer.clear();
1464        }
1465        if cells.is_empty() {
1466            return Err(missing("at least one a:tc"));
1467        }
1468        Ok(Self {
1469            height: Emu(required_i64(start, b"h")?),
1470            original_cells: cells.clone(),
1471            cells,
1472            raw_attributes: raw_attributes(start, &[b"h"], false)?,
1473            raw_children,
1474            origin_index: 0,
1475        })
1476    }
1477
1478    fn write_xml<W: Write>(&self, writer: &mut Writer<W>) -> Result<()> {
1479        if self.cells.is_empty() {
1480            return Err(missing("at least one a:tc"));
1481        }
1482        let mut start = BytesStart::new("a:tr");
1483        let height = self.height.0.to_string();
1484        start.push_attribute(("h", height.as_str()));
1485        push_attributes(&mut start, &self.raw_attributes);
1486        writer.write_event(Event::Start(start))?;
1487        let current_cell_origins = self
1488            .cells
1489            .iter()
1490            .map(|cell| cell.origin_index)
1491            .collect::<Vec<_>>();
1492        let original_cell_origins = self
1493            .original_cells
1494            .iter()
1495            .map(|cell| cell.origin_index)
1496            .collect::<Vec<_>>();
1497        let cell_matches = matched_original_indices(&current_cell_origins, &original_cell_origins);
1498        let original_to_current = invert_matches(&cell_matches, self.original_cells.len());
1499        for boundary in 0..=self.cells.len() {
1500            emit_raw(
1501                writer,
1502                self.raw_children.at_reconciled(
1503                    boundary,
1504                    0,
1505                    &original_to_current,
1506                    self.cells.len(),
1507                ),
1508            )?;
1509            if let Some(cell) = self.cells.get(boundary) {
1510                cell.write_xml(writer)?;
1511            }
1512        }
1513        writer.write_event(Event::End(BytesEnd::new("a:tr")))?;
1514        Ok(())
1515    }
1516}
1517
1518impl CT_TableCell {
1519    fn canonical_xml(&self) -> Result<Vec<u8>> {
1520        let mut writer = Writer::new(Vec::new());
1521        self.write_xml(&mut writer)?;
1522        Ok(writer.into_inner())
1523    }
1524
1525    fn from_xml(xml: &[u8]) -> Result<Self> {
1526        let mut reader = Reader::from_reader(xml);
1527        let mut buffer = Vec::new();
1528        loop {
1529            match reader.read_event_into(&mut buffer)? {
1530                Event::Start(start) if matches_local_name(start.name().as_ref(), b"tc") => {
1531                    reject_conflicting_a_prefix(&start)?;
1532                    return Self::from_element(&mut reader, &start);
1533                }
1534                Event::Empty(start) if matches_local_name(start.name().as_ref(), b"tc") => {
1535                    reject_conflicting_a_prefix(&start)?;
1536                    return Self::from_start(&start, None, None, OrderedRawChildren::default());
1537                }
1538                Event::Start(start) | Event::Empty(start) => return Err(unexpected(&start)),
1539                Event::Eof => return Err(missing("a:tc")),
1540                _ => {}
1541            }
1542            buffer.clear();
1543        }
1544    }
1545
1546    fn from_element(reader: &mut Reader<&[u8]>, start: &BytesStart<'_>) -> Result<Self> {
1547        let mut text_body = None;
1548        let mut properties = None;
1549        let mut raw_children = OrderedRawChildren::default();
1550        let mut boundary = 0usize;
1551        let mut buffer = Vec::new();
1552        loop {
1553            match reader.read_event_into(&mut buffer)? {
1554                Event::Start(child) => {
1555                    let name = local_name(child.name().as_ref()).to_vec();
1556                    if matches!(name.as_slice(), b"txBody" | b"tcPr") {
1557                        reject_conflicting_a_prefix(&child)?;
1558                    }
1559                    let raw = capture_element(reader, &child)?;
1560                    capture_cell_child(
1561                        &name,
1562                        raw,
1563                        &mut text_body,
1564                        &mut properties,
1565                        &mut raw_children,
1566                        &mut boundary,
1567                    )?;
1568                }
1569                Event::Empty(child) => {
1570                    let name = local_name(child.name().as_ref()).to_vec();
1571                    if matches!(name.as_slice(), b"txBody" | b"tcPr") {
1572                        reject_conflicting_a_prefix(&child)?;
1573                    }
1574                    let raw = capture_empty_element(&child)?;
1575                    capture_cell_child(
1576                        &name,
1577                        raw,
1578                        &mut text_body,
1579                        &mut properties,
1580                        &mut raw_children,
1581                        &mut boundary,
1582                    )?;
1583                }
1584                Event::End(end) if matches_local_name(end.name().as_ref(), b"tc") => break,
1585                Event::Eof => return Err(missing("closing a:tc")),
1586                _ => {}
1587            }
1588            buffer.clear();
1589        }
1590        Self::from_start(start, text_body, properties, raw_children)
1591    }
1592
1593    fn from_start(
1594        start: &BytesStart<'_>,
1595        text_body: Option<CT_TextBody>,
1596        properties: Option<CT_TableCellProperties>,
1597        raw_children: OrderedRawChildren,
1598    ) -> Result<Self> {
1599        Ok(Self {
1600            text_body,
1601            row_span: positive_u32_attr(start, b"rowSpan")?.unwrap_or(1),
1602            grid_span: positive_u32_attr(start, b"gridSpan")?.unwrap_or(1),
1603            horizontal_merge: bool_attr(start, b"hMerge")?.unwrap_or(false),
1604            vertical_merge: bool_attr(start, b"vMerge")?.unwrap_or(false),
1605            properties,
1606            raw_attributes: raw_attributes(
1607                start,
1608                &[b"rowSpan", b"gridSpan", b"hMerge", b"vMerge"],
1609                false,
1610            )?,
1611            raw_children,
1612            origin_index: 0,
1613        })
1614    }
1615
1616    fn write_xml<W: Write>(&self, writer: &mut Writer<W>) -> Result<()> {
1617        if self.row_span == 0 || self.grid_span == 0 {
1618            return Err(OxmlError::InvalidValue(
1619                "a:tc spans must be positive".to_owned(),
1620            ));
1621        }
1622        let mut start = BytesStart::new("a:tc");
1623        let row_span = self.row_span.to_string();
1624        let grid_span = self.grid_span.to_string();
1625        if self.row_span != 1 {
1626            start.push_attribute(("rowSpan", row_span.as_str()));
1627        }
1628        if self.grid_span != 1 {
1629            start.push_attribute(("gridSpan", grid_span.as_str()));
1630        }
1631        push_true(&mut start, "hMerge", self.horizontal_merge);
1632        push_true(&mut start, "vMerge", self.vertical_merge);
1633        push_attributes(&mut start, &self.raw_attributes);
1634        if self.text_body.is_none() && self.properties.is_none() && self.raw_children.is_empty() {
1635            writer.write_event(Event::Empty(start))?;
1636            return Ok(());
1637        }
1638        writer.write_event(Event::Start(start))?;
1639        emit_raw(writer, self.raw_children.at(0))?;
1640        if let Some(text_body) = &self.text_body {
1641            writer
1642                .get_mut()
1643                .write_all(&text_body.to_xml().map_err(|error| {
1644                    OxmlError::InvalidValue(format!("invalid table-cell text body: {error}"))
1645                })?)?;
1646        }
1647        emit_raw(writer, self.raw_children.at(1))?;
1648        if let Some(properties) = &self.properties {
1649            properties.write_xml(writer)?;
1650        }
1651        emit_raw(writer, self.raw_children.at(2))?;
1652        writer.write_event(Event::End(BytesEnd::new("a:tc")))?;
1653        Ok(())
1654    }
1655
1656    /// Returns an unmerged cell with this cell's properties and an empty text
1657    /// body that keeps its formatting, for a new row or column.
1658    fn empty_like(&self) -> Self {
1659        Self {
1660            text_body: Some(
1661                self.text_body
1662                    .as_ref()
1663                    .map_or_else(CT_TextBody::new, CT_TextBody::empty_like),
1664            ),
1665            row_span: 1,
1666            grid_span: 1,
1667            horizontal_merge: false,
1668            vertical_merge: false,
1669            properties: self.properties.clone(),
1670            raw_attributes: Vec::new(),
1671            raw_children: OrderedRawChildren::default(),
1672            origin_index: usize::MAX,
1673        }
1674    }
1675
1676    fn copy_merge_state(&mut self, other: &Self) {
1677        self.row_span = other.row_span;
1678        self.grid_span = other.grid_span;
1679        self.horizontal_merge = other.horizontal_merge;
1680        self.vertical_merge = other.vertical_merge;
1681    }
1682
1683    /// Takes over the merge state of `edge`, a cell of a removed first row or
1684    /// column of a merge, and with `content` its text and cell properties.
1685    fn take_merge_origin(&mut self, edge: &mut Self, content: bool) {
1686        self.copy_merge_state(edge);
1687        if content {
1688            std::mem::swap(&mut self.text_body, &mut edge.text_body);
1689            std::mem::swap(&mut self.properties, &mut edge.properties);
1690        }
1691    }
1692}
1693
1694fn capture_cell_child(
1695    name: &[u8],
1696    raw: Vec<u8>,
1697    text_body: &mut Option<CT_TextBody>,
1698    properties: &mut Option<CT_TableCellProperties>,
1699    raw_children: &mut OrderedRawChildren,
1700    boundary: &mut usize,
1701) -> Result<()> {
1702    match name {
1703        b"txBody" if *boundary == 0 && text_body.is_none() => {
1704            *text_body = Some(CT_TextBody::from_xml(&raw).map_err(|error| {
1705                OxmlError::InvalidValue(format!("invalid a:tc/a:txBody: {error}"))
1706            })?);
1707            *boundary = 1;
1708        }
1709        b"tcPr" if *boundary <= 1 && properties.is_none() => {
1710            *properties = Some(CT_TableCellProperties::from_xml(&raw)?);
1711            *boundary = 2;
1712        }
1713        b"txBody" | b"tcPr" => {
1714            return Err(OxmlError::InvalidValue(
1715                "a:tc children must be a:txBody? then a:tcPr?".to_owned(),
1716            ));
1717        }
1718        _ => raw_children.push(*boundary, raw),
1719    }
1720    Ok(())
1721}
1722
1723impl CT_TableCellProperties {
1724    fn from_xml(xml: &[u8]) -> Result<Self> {
1725        let mut reader = Reader::from_reader(xml);
1726        let mut buffer = Vec::new();
1727        loop {
1728            match reader.read_event_into(&mut buffer)? {
1729                Event::Start(start) if matches_local_name(start.name().as_ref(), b"tcPr") => {
1730                    return Self::from_element(&mut reader, &start);
1731                }
1732                Event::Empty(start) if matches_local_name(start.name().as_ref(), b"tcPr") => {
1733                    return Self::from_start(&start);
1734                }
1735                Event::Start(start) | Event::Empty(start) => return Err(unexpected(&start)),
1736                Event::Eof => return Err(missing("a:tcPr")),
1737                _ => {}
1738            }
1739            buffer.clear();
1740        }
1741    }
1742
1743    fn from_start(start: &BytesStart<'_>) -> Result<Self> {
1744        Ok(Self {
1745            margin_left: optional_emu_attr(start, b"marL")?,
1746            margin_right: optional_emu_attr(start, b"marR")?,
1747            margin_top: optional_emu_attr(start, b"marT")?,
1748            margin_bottom: optional_emu_attr(start, b"marB")?,
1749            raw_attributes: raw_attributes(start, &[b"marL", b"marR", b"marT", b"marB"], false)?,
1750            ..Self::default()
1751        })
1752    }
1753
1754    fn from_element(reader: &mut Reader<&[u8]>, start: &BytesStart<'_>) -> Result<Self> {
1755        let mut properties = Self::from_start(start)?;
1756        let mut boundary = 0usize;
1757        let mut buffer = Vec::new();
1758        loop {
1759            match reader.read_event_into(&mut buffer)? {
1760                Event::Start(child) => {
1761                    let name = local_name(child.name().as_ref()).to_vec();
1762                    let raw = capture_element(reader, &child)?;
1763                    properties.capture_child(&name, raw, &mut boundary)?;
1764                }
1765                Event::Empty(child) => {
1766                    let name = local_name(child.name().as_ref()).to_vec();
1767                    let raw = capture_empty_element(&child)?;
1768                    properties.capture_child(&name, raw, &mut boundary)?;
1769                }
1770                Event::End(end) if matches_local_name(end.name().as_ref(), b"tcPr") => break,
1771                Event::Eof => return Err(missing("closing a:tcPr")),
1772                _ => {}
1773            }
1774            buffer.clear();
1775        }
1776        Ok(properties)
1777    }
1778
1779    fn capture_child(&mut self, name: &[u8], raw: Vec<u8>, boundary: &mut usize) -> Result<()> {
1780        let slot = cell_property_slot(name);
1781        ensure_schema_order(slot, *boundary, "a:tcPr")?;
1782        match name {
1783            b"lnL" => set_once(&mut self.left, parse_named_line(&raw)?, "a:tcPr/a:lnL")?,
1784            b"lnR" => set_once(&mut self.right, parse_named_line(&raw)?, "a:tcPr/a:lnR")?,
1785            b"lnT" => set_once(&mut self.top, parse_named_line(&raw)?, "a:tcPr/a:lnT")?,
1786            b"lnB" => set_once(&mut self.bottom, parse_named_line(&raw)?, "a:tcPr/a:lnB")?,
1787            name if is_fill(name) => set_once(&mut self.fill, parse_fill(&raw)?, "a:tcPr fill")?,
1788            b"lnTlToBr" | b"lnBlToTr" => {
1789                self.unsupported.push("diagonal border".to_owned());
1790                self.raw_children.push(slot.unwrap_or(*boundary), raw);
1791            }
1792            b"cell3D" => {
1793                self.unsupported.push("3-D properties".to_owned());
1794                self.raw_children.push(slot.unwrap_or(*boundary), raw);
1795            }
1796            b"effectLst" | b"effectDag" => {
1797                self.unsupported.push("effects".to_owned());
1798                self.raw_children.push(slot.unwrap_or(*boundary), raw);
1799            }
1800            _ => self.raw_children.push(slot.unwrap_or(*boundary), raw),
1801        }
1802        if let Some(slot) = slot {
1803            *boundary = (*boundary).max(slot + 1);
1804        }
1805        Ok(())
1806    }
1807
1808    fn write_xml<W: Write>(&self, writer: &mut Writer<W>) -> Result<()> {
1809        let mut start = BytesStart::new("a:tcPr");
1810        push_optional_emu(&mut start, "marL", self.margin_left);
1811        push_optional_emu(&mut start, "marR", self.margin_right);
1812        push_optional_emu(&mut start, "marT", self.margin_top);
1813        push_optional_emu(&mut start, "marB", self.margin_bottom);
1814        push_attributes(&mut start, &self.raw_attributes);
1815        let has_modelled = self.left.is_some()
1816            || self.right.is_some()
1817            || self.top.is_some()
1818            || self.bottom.is_some()
1819            || self.fill.is_some();
1820        if !has_modelled && self.raw_children.is_empty() {
1821            writer.write_event(Event::Empty(start))?;
1822            return Ok(());
1823        }
1824        writer.write_event(Event::Start(start))?;
1825        emit_raw(writer, self.raw_children.at(0))?;
1826        write_optional_named_line(writer, "a:lnL", self.left.as_ref())?;
1827        emit_raw(writer, self.raw_children.at(1))?;
1828        write_optional_named_line(writer, "a:lnR", self.right.as_ref())?;
1829        emit_raw(writer, self.raw_children.at(2))?;
1830        write_optional_named_line(writer, "a:lnT", self.top.as_ref())?;
1831        emit_raw(writer, self.raw_children.at(3))?;
1832        write_optional_named_line(writer, "a:lnB", self.bottom.as_ref())?;
1833        for slot in 4..=7 {
1834            emit_raw(writer, self.raw_children.at(slot))?;
1835        }
1836        if let Some(fill) = &self.fill {
1837            fill.write_xml(writer).map_err(drawing_error)?;
1838        }
1839        for slot in 8..=10 {
1840            emit_raw(writer, self.raw_children.at(slot))?;
1841        }
1842        writer.write_event(Event::End(BytesEnd::new("a:tcPr")))?;
1843        Ok(())
1844    }
1845}
1846
1847impl CT_TableStyleList {
1848    /// Parses a complete table-style list with any DrawingML prefix.
1849    pub fn from_xml(xml: &[u8]) -> Result<Self> {
1850        let mut reader = Reader::from_reader(xml);
1851        let mut buffer = Vec::new();
1852        loop {
1853            match reader.read_event_into(&mut buffer)? {
1854                Event::Start(start)
1855                    if matches_local_name(start.name().as_ref(), b"tblStyleLst") =>
1856                {
1857                    reject_conflicting_a_prefix(&start)?;
1858                    return Self::from_element(&mut reader, &start);
1859                }
1860                Event::Empty(start)
1861                    if matches_local_name(start.name().as_ref(), b"tblStyleLst") =>
1862                {
1863                    reject_conflicting_a_prefix(&start)?;
1864                    return Ok(Self {
1865                        default_style_id: decoded_attr(&start, b"def")?,
1866                        styles: Vec::new(),
1867                        raw_attributes: raw_attributes(&start, &[b"def"], true)?,
1868                        raw_children: OrderedRawChildren::default(),
1869                    });
1870                }
1871                Event::Start(start) | Event::Empty(start) => return Err(unexpected(&start)),
1872                Event::Eof => return Err(missing("a:tblStyleLst")),
1873                _ => {}
1874            }
1875            buffer.clear();
1876        }
1877    }
1878
1879    fn from_element(reader: &mut Reader<&[u8]>, start: &BytesStart<'_>) -> Result<Self> {
1880        let mut styles = Vec::new();
1881        let mut raw_children = OrderedRawChildren::default();
1882        let mut buffer = Vec::new();
1883        loop {
1884            match reader.read_event_into(&mut buffer)? {
1885                Event::Start(child) if matches_local_name(child.name().as_ref(), b"tblStyle") => {
1886                    let raw = capture_element(reader, &child)?;
1887                    styles.push(CT_TableStyle::from_xml(&raw)?);
1888                }
1889                Event::Empty(child) if matches_local_name(child.name().as_ref(), b"tblStyle") => {
1890                    let raw = capture_empty_element(&child)?;
1891                    styles.push(CT_TableStyle::from_xml(&raw)?);
1892                }
1893                Event::Start(child) => {
1894                    raw_children.push(styles.len(), capture_element(reader, &child)?)
1895                }
1896                Event::Empty(child) => {
1897                    raw_children.push(styles.len(), capture_empty_element(&child)?)
1898                }
1899                Event::End(end) if matches_local_name(end.name().as_ref(), b"tblStyleLst") => break,
1900                Event::Eof => return Err(missing("closing a:tblStyleLst")),
1901                _ => {}
1902            }
1903            buffer.clear();
1904        }
1905        Ok(Self {
1906            default_style_id: decoded_attr(start, b"def")?,
1907            styles,
1908            raw_attributes: raw_attributes(start, &[b"def"], true)?,
1909            raw_children,
1910        })
1911    }
1912
1913    /// Returns the explicitly selected style, then the list default.
1914    pub fn style(&self, style_id: Option<&str>) -> Option<&CT_TableStyle> {
1915        style_id
1916            .and_then(|style_id| self.styles.iter().find(|style| style.style_id == style_id))
1917            .or_else(|| {
1918                self.default_style_id.as_deref().and_then(|style_id| {
1919                    self.styles.iter().find(|style| style.style_id == style_id)
1920                })
1921            })
1922    }
1923
1924    /// Serialises with fixed `a:` prefixes and table-style schema order.
1925    pub fn to_xml(&self) -> Result<Vec<u8>> {
1926        let mut writer = Writer::new(Vec::new());
1927        let mut start = BytesStart::new("a:tblStyleLst");
1928        start.push_attribute(("xmlns:a", A_NS));
1929        if let Some(default_style_id) = &self.default_style_id {
1930            start.push_attribute(("def", default_style_id.as_str()));
1931        }
1932        push_attributes(&mut start, &self.raw_attributes);
1933        if self.styles.is_empty() && self.raw_children.is_empty() {
1934            writer.write_event(Event::Empty(start))?;
1935            return Ok(writer.into_inner());
1936        }
1937        writer.write_event(Event::Start(start))?;
1938        for boundary in 0..=self.styles.len() {
1939            emit_raw(&mut writer, self.raw_children.at(boundary))?;
1940            if let Some(style) = self.styles.get(boundary) {
1941                style.write_xml(&mut writer)?;
1942            }
1943        }
1944        writer.write_event(Event::End(BytesEnd::new("a:tblStyleLst")))?;
1945        Ok(writer.into_inner())
1946    }
1947}
1948
1949impl CT_TableStyle {
1950    /// Returns PowerPoint's definition of one of its 74 built-in table styles.
1951    ///
1952    /// A deck may name a built-in style by GUID in `a:tableStyleId` without
1953    /// defining it in `ppt/tableStyles.xml`, as python-pptx does, and
1954    /// PowerPoint then applies its own definition. Each definition is built
1955    /// from its family's rules and the variant's colour as `a:tblStyle` XML,
1956    /// which the ordinary parser reads. An unknown id returns `None`.
1957    pub fn builtin(style_id: &str) -> Option<Self> {
1958        let (_, family, accent) = builtin_table_styles()
1959            .into_iter()
1960            .find(|(id, ..)| *id == style_id)?;
1961        let xml = format!(
1962            r#"<a:tblStyle styleId="{style_id}" styleName="{}">{}</a:tblStyle>"#,
1963            family.style_name(accent),
1964            family.regions(accent)
1965        );
1966        let style = Self::from_xml(xml.as_bytes()).unwrap_or_else(|error| {
1967            panic!("built-in table style {style_id} is malformed: {error}")
1968        });
1969        Some(style)
1970    }
1971
1972    fn from_xml(xml: &[u8]) -> Result<Self> {
1973        let mut reader = Reader::from_reader(xml);
1974        let mut buffer = Vec::new();
1975        loop {
1976            match reader.read_event_into(&mut buffer)? {
1977                Event::Start(start) if matches_local_name(start.name().as_ref(), b"tblStyle") => {
1978                    return Self::from_element(&mut reader, &start);
1979                }
1980                Event::Empty(start) if matches_local_name(start.name().as_ref(), b"tblStyle") => {
1981                    return Self::from_start(&start);
1982                }
1983                Event::Start(start) | Event::Empty(start) => return Err(unexpected(&start)),
1984                Event::Eof => return Err(missing("a:tblStyle")),
1985                _ => {}
1986            }
1987            buffer.clear();
1988        }
1989    }
1990
1991    fn from_start(start: &BytesStart<'_>) -> Result<Self> {
1992        Ok(Self {
1993            style_id: required_string(start, b"styleId")?,
1994            style_name: required_string(start, b"styleName")?,
1995            raw_attributes: raw_attributes(start, &[b"styleId", b"styleName"], false)?,
1996            ..Self::default()
1997        })
1998    }
1999
2000    fn from_element(reader: &mut Reader<&[u8]>, start: &BytesStart<'_>) -> Result<Self> {
2001        let mut style = Self::from_start(start)?;
2002        let mut boundary = 0usize;
2003        let mut buffer = Vec::new();
2004        loop {
2005            match reader.read_event_into(&mut buffer)? {
2006                Event::Start(child) => {
2007                    let name = local_name(child.name().as_ref()).to_vec();
2008                    let raw = capture_element(reader, &child)?;
2009                    style.capture_child(&name, raw, &mut boundary)?;
2010                }
2011                Event::Empty(child) => {
2012                    let name = local_name(child.name().as_ref()).to_vec();
2013                    let raw = capture_empty_element(&child)?;
2014                    style.capture_child(&name, raw, &mut boundary)?;
2015                }
2016                Event::End(end) if matches_local_name(end.name().as_ref(), b"tblStyle") => break,
2017                Event::Eof => return Err(missing("closing a:tblStyle")),
2018                _ => {}
2019            }
2020            buffer.clear();
2021        }
2022        Ok(style)
2023    }
2024
2025    fn capture_child(&mut self, name: &[u8], raw: Vec<u8>, boundary: &mut usize) -> Result<()> {
2026        let Some(slot) = table_style_slot(name) else {
2027            self.raw_children.push(*boundary, raw);
2028            return Ok(());
2029        };
2030        if slot < *boundary {
2031            return Err(OxmlError::InvalidValue(
2032                "a:tblStyle children violate schema order".to_owned(),
2033            ));
2034        }
2035        if name == b"tblBg" {
2036            self.background = Some(CT_TableBackgroundStyle::from_xml(&raw)?);
2037            *boundary = slot + 1;
2038            return Ok(());
2039        }
2040        let destination = self.region_mut(name);
2041        if destination.is_some() {
2042            return Err(OxmlError::InvalidValue(format!(
2043                "duplicate a:{}",
2044                String::from_utf8_lossy(name)
2045            )));
2046        }
2047        *destination = Some(CT_TablePartStyle::from_xml(&raw, name)?);
2048        *boundary = slot + 1;
2049        Ok(())
2050    }
2051
2052    fn region_mut(&mut self, name: &[u8]) -> &mut Option<CT_TablePartStyle> {
2053        match name {
2054            b"wholeTbl" => &mut self.whole_table,
2055            b"band1H" => &mut self.band1_horizontal,
2056            b"band2H" => &mut self.band2_horizontal,
2057            b"band1V" => &mut self.band1_vertical,
2058            b"band2V" => &mut self.band2_vertical,
2059            b"firstCol" => &mut self.first_column,
2060            b"lastCol" => &mut self.last_column,
2061            b"firstRow" => &mut self.first_row,
2062            b"lastRow" => &mut self.last_row,
2063            b"nwCell" => &mut self.north_west_cell,
2064            b"neCell" => &mut self.north_east_cell,
2065            b"swCell" => &mut self.south_west_cell,
2066            b"seCell" => &mut self.south_east_cell,
2067            _ => unreachable!("modelled table region"),
2068        }
2069    }
2070
2071    fn write_xml<W: Write>(&self, writer: &mut Writer<W>) -> Result<()> {
2072        let mut start = BytesStart::new("a:tblStyle");
2073        start.push_attribute(("styleId", self.style_id.as_str()));
2074        start.push_attribute(("styleName", self.style_name.as_str()));
2075        push_attributes(&mut start, &self.raw_attributes);
2076        writer.write_event(Event::Start(start))?;
2077        emit_raw(writer, self.raw_children.at(0))?;
2078        if let Some(background) = &self.background {
2079            background.write_xml(writer)?;
2080        }
2081        let ordered = [
2082            ("a:wholeTbl", self.whole_table.as_ref()),
2083            ("a:band1H", self.band1_horizontal.as_ref()),
2084            ("a:band2H", self.band2_horizontal.as_ref()),
2085            ("a:band1V", self.band1_vertical.as_ref()),
2086            ("a:band2V", self.band2_vertical.as_ref()),
2087            ("a:lastCol", self.last_column.as_ref()),
2088            ("a:firstCol", self.first_column.as_ref()),
2089            ("a:lastRow", self.last_row.as_ref()),
2090            ("a:seCell", self.south_east_cell.as_ref()),
2091            ("a:swCell", self.south_west_cell.as_ref()),
2092            ("a:firstRow", self.first_row.as_ref()),
2093            ("a:neCell", self.north_east_cell.as_ref()),
2094            ("a:nwCell", self.north_west_cell.as_ref()),
2095        ];
2096        for (index, (tag, region)) in ordered.into_iter().enumerate() {
2097            emit_raw(writer, self.raw_children.at(index + 1))?;
2098            if let Some(region) = region {
2099                region.write_xml(writer, tag)?;
2100            }
2101        }
2102        emit_raw(writer, self.raw_children.at(14))?;
2103        emit_raw(writer, self.raw_children.at(15))?;
2104        writer.write_event(Event::End(BytesEnd::new("a:tblStyle")))?;
2105        Ok(())
2106    }
2107}
2108
2109impl CT_TableBackgroundStyle {
2110    fn from_xml(xml: &[u8]) -> Result<Self> {
2111        let mut reader = Reader::from_reader(xml);
2112        let mut buffer = Vec::new();
2113        loop {
2114            match reader.read_event_into(&mut buffer)? {
2115                Event::Start(start) if matches_local_name(start.name().as_ref(), b"tblBg") => {
2116                    let mut value = Self {
2117                        raw_attributes: raw_attributes(&start, &[], false)?,
2118                        ..Self::default()
2119                    };
2120                    let mut boundary = 0usize;
2121                    loop {
2122                        buffer.clear();
2123                        match reader.read_event_into(&mut buffer)? {
2124                            Event::Start(child) => {
2125                                let name = local_name(child.name().as_ref()).to_vec();
2126                                let raw = capture_element(&mut reader, &child)?;
2127                                value.capture_child(&name, raw, &mut boundary)?;
2128                            }
2129                            Event::Empty(child) => {
2130                                let name = local_name(child.name().as_ref()).to_vec();
2131                                let raw = capture_empty_element(&child)?;
2132                                value.capture_child(&name, raw, &mut boundary)?;
2133                            }
2134                            Event::End(end)
2135                                if matches_local_name(end.name().as_ref(), b"tblBg") =>
2136                            {
2137                                return Ok(value);
2138                            }
2139                            Event::Eof => return Err(missing("closing a:tblBg")),
2140                            _ => {}
2141                        }
2142                    }
2143                }
2144                Event::Empty(start) if matches_local_name(start.name().as_ref(), b"tblBg") => {
2145                    return Ok(Self {
2146                        raw_attributes: raw_attributes(&start, &[], false)?,
2147                        ..Self::default()
2148                    });
2149                }
2150                Event::Start(start) | Event::Empty(start) => return Err(unexpected(&start)),
2151                Event::Eof => return Err(missing("a:tblBg")),
2152                _ => {}
2153            }
2154            buffer.clear();
2155        }
2156    }
2157
2158    fn capture_child(&mut self, name: &[u8], raw: Vec<u8>, boundary: &mut usize) -> Result<()> {
2159        let slot = match name {
2160            b"fill" | b"fillRef" => Some(0),
2161            b"effect" | b"effectRef" => Some(1),
2162            _ => None,
2163        };
2164        ensure_schema_order(slot, *boundary, "a:tblBg")?;
2165        match name {
2166            b"fill" | b"fillRef" if self.fill.is_some() || self.fill_reference.is_some() => {
2167                return Err(OxmlError::InvalidValue(
2168                    "duplicate a:tblBg fill choice".to_owned(),
2169                ));
2170            }
2171            b"fill" => {
2172                if let Some(fill) = parse_fill_wrapper(&raw)? {
2173                    self.fill = Some(fill);
2174                } else {
2175                    self.unsupported.push("fill form".to_owned());
2176                    self.raw_children.push(0, raw);
2177                }
2178            }
2179            b"fillRef" => self.fill_reference = Some(parse_style_reference(&raw)?),
2180            b"effect" | b"effectRef" => {
2181                self.unsupported.push("effect".to_owned());
2182                self.raw_children.push(1, raw);
2183            }
2184            _ => self.raw_children.push(*boundary, raw),
2185        }
2186        if let Some(slot) = slot {
2187            *boundary = (*boundary).max(slot + 1);
2188        }
2189        Ok(())
2190    }
2191
2192    fn write_xml<W: Write>(&self, writer: &mut Writer<W>) -> Result<()> {
2193        let mut start = BytesStart::new("a:tblBg");
2194        push_attributes(&mut start, &self.raw_attributes);
2195        if self.fill.is_none() && self.fill_reference.is_none() && self.raw_children.is_empty() {
2196            writer.write_event(Event::Empty(start))?;
2197            return Ok(());
2198        }
2199        writer.write_event(Event::Start(start))?;
2200        emit_raw(writer, self.raw_children.at(0))?;
2201        if let Some(fill) = &self.fill {
2202            writer.write_event(Event::Start(BytesStart::new("a:fill")))?;
2203            fill.write_xml(writer).map_err(drawing_error)?;
2204            writer.write_event(Event::End(BytesEnd::new("a:fill")))?;
2205        }
2206        if let Some(reference) = &self.fill_reference {
2207            reference.write_xml(writer).map_err(drawing_error)?;
2208        }
2209        emit_raw(writer, self.raw_children.at(1))?;
2210        emit_raw(writer, self.raw_children.at(2))?;
2211        writer.write_event(Event::End(BytesEnd::new("a:tblBg")))?;
2212        Ok(())
2213    }
2214}
2215
2216impl CT_TablePartStyle {
2217    fn from_xml(xml: &[u8], expected: &[u8]) -> Result<Self> {
2218        let mut reader = Reader::from_reader(xml);
2219        let mut buffer = Vec::new();
2220        loop {
2221            match reader.read_event_into(&mut buffer)? {
2222                Event::Start(start) if matches_local_name(start.name().as_ref(), expected) => {
2223                    let mut value = Self {
2224                        raw_attributes: raw_attributes(&start, &[], false)?,
2225                        ..Self::default()
2226                    };
2227                    let mut boundary = 0usize;
2228                    loop {
2229                        buffer.clear();
2230                        match reader.read_event_into(&mut buffer)? {
2231                            Event::Start(child) => {
2232                                let name = local_name(child.name().as_ref()).to_vec();
2233                                let raw = capture_element(&mut reader, &child)?;
2234                                value.capture_child(&name, raw, &mut boundary)?;
2235                            }
2236                            Event::Empty(child) => {
2237                                let name = local_name(child.name().as_ref()).to_vec();
2238                                let raw = capture_empty_element(&child)?;
2239                                value.capture_child(&name, raw, &mut boundary)?;
2240                            }
2241                            Event::End(end)
2242                                if matches_local_name(end.name().as_ref(), expected) =>
2243                            {
2244                                return Ok(value);
2245                            }
2246                            Event::Eof => return Err(missing("closing table style region")),
2247                            _ => {}
2248                        }
2249                    }
2250                }
2251                Event::Empty(start) if matches_local_name(start.name().as_ref(), expected) => {
2252                    return Ok(Self {
2253                        raw_attributes: raw_attributes(&start, &[], false)?,
2254                        ..Self::default()
2255                    });
2256                }
2257                Event::Start(start) | Event::Empty(start) => return Err(unexpected(&start)),
2258                Event::Eof => return Err(missing("table style region")),
2259                _ => {}
2260            }
2261            buffer.clear();
2262        }
2263    }
2264
2265    fn capture_child(&mut self, name: &[u8], raw: Vec<u8>, boundary: &mut usize) -> Result<()> {
2266        match name {
2267            b"tcTxStyle" if self.text_style.is_none() && *boundary == 0 => {
2268                self.text_style = Some(CT_TableTextStyle::from_xml(&raw)?);
2269                *boundary = 1;
2270            }
2271            b"tcStyle" if self.cell_style.is_none() => {
2272                self.cell_style = Some(CT_TableCellStyle::from_xml(&raw)?);
2273                *boundary = 2;
2274            }
2275            b"tcTxStyle" | b"tcStyle" => {
2276                return Err(OxmlError::InvalidValue(
2277                    "table region style children violate schema order or are duplicated".to_owned(),
2278                ));
2279            }
2280            _ => self.raw_children.push(*boundary, raw),
2281        }
2282        Ok(())
2283    }
2284
2285    fn write_xml<W: Write>(&self, writer: &mut Writer<W>, tag: &str) -> Result<()> {
2286        let mut start = BytesStart::new(tag);
2287        push_attributes(&mut start, &self.raw_attributes);
2288        if self.text_style.is_none() && self.cell_style.is_none() && self.raw_children.is_empty() {
2289            writer.write_event(Event::Empty(start))?;
2290            return Ok(());
2291        }
2292        writer.write_event(Event::Start(start))?;
2293        emit_raw(writer, self.raw_children.at(0))?;
2294        if let Some(text_style) = &self.text_style {
2295            text_style.write_xml(writer)?;
2296        }
2297        emit_raw(writer, self.raw_children.at(1))?;
2298        if let Some(cell_style) = &self.cell_style {
2299            cell_style.write_xml(writer)?;
2300        }
2301        emit_raw(writer, self.raw_children.at(2))?;
2302        writer.write_event(Event::End(BytesEnd::new(tag)))?;
2303        Ok(())
2304    }
2305}
2306
2307impl CT_TableCellStyle {
2308    fn from_xml(xml: &[u8]) -> Result<Self> {
2309        let mut reader = Reader::from_reader(xml);
2310        let mut buffer = Vec::new();
2311        loop {
2312            match reader.read_event_into(&mut buffer)? {
2313                Event::Start(start) if matches_local_name(start.name().as_ref(), b"tcStyle") => {
2314                    let mut value = Self {
2315                        raw_attributes: raw_attributes(&start, &[], false)?,
2316                        ..Self::default()
2317                    };
2318                    let mut boundary = 0usize;
2319                    loop {
2320                        buffer.clear();
2321                        match reader.read_event_into(&mut buffer)? {
2322                            Event::Start(child) => {
2323                                let name = local_name(child.name().as_ref()).to_vec();
2324                                let raw = capture_element(&mut reader, &child)?;
2325                                value.capture_child(&name, raw, &mut boundary)?;
2326                            }
2327                            Event::Empty(child) => {
2328                                let name = local_name(child.name().as_ref()).to_vec();
2329                                let raw = capture_empty_element(&child)?;
2330                                value.capture_child(&name, raw, &mut boundary)?;
2331                            }
2332                            Event::End(end)
2333                                if matches_local_name(end.name().as_ref(), b"tcStyle") =>
2334                            {
2335                                return Ok(value);
2336                            }
2337                            Event::Eof => return Err(missing("closing a:tcStyle")),
2338                            _ => {}
2339                        }
2340                    }
2341                }
2342                Event::Empty(start) if matches_local_name(start.name().as_ref(), b"tcStyle") => {
2343                    return Ok(Self {
2344                        raw_attributes: raw_attributes(&start, &[], false)?,
2345                        ..Self::default()
2346                    });
2347                }
2348                Event::Start(start) | Event::Empty(start) => return Err(unexpected(&start)),
2349                Event::Eof => return Err(missing("a:tcStyle")),
2350                _ => {}
2351            }
2352            buffer.clear();
2353        }
2354    }
2355
2356    fn capture_child(&mut self, name: &[u8], raw: Vec<u8>, boundary: &mut usize) -> Result<()> {
2357        let slot = match name {
2358            b"tcBdr" => Some(0),
2359            b"fill" => Some(1),
2360            name if is_fill(name) => Some(1),
2361            b"fillRef" => Some(2),
2362            b"cell3D" => Some(3),
2363            _ => None,
2364        };
2365        ensure_schema_order(slot, *boundary, "a:tcStyle")?;
2366        match name {
2367            b"tcBdr" if self.borders.is_some() => {
2368                return Err(OxmlError::InvalidValue("duplicate a:tcBdr".to_owned()));
2369            }
2370            b"tcBdr" => self.borders = Some(CT_TableBorders::from_xml(&raw)?),
2371            b"fill" => {
2372                if self.fill.is_some() || self.fill_reference.is_some() {
2373                    return Err(OxmlError::InvalidValue(
2374                        "duplicate a:tcStyle fill choice".to_owned(),
2375                    ));
2376                }
2377                if let Some(fill) = parse_fill_wrapper(&raw)? {
2378                    self.fill = Some(fill);
2379                } else {
2380                    self.unsupported.push("fill form".to_owned());
2381                    self.raw_children.push(slot.unwrap(), raw);
2382                }
2383            }
2384            name if is_fill(name) => {
2385                if self.fill.is_some() || self.fill_reference.is_some() {
2386                    return Err(OxmlError::InvalidValue(
2387                        "duplicate a:tcStyle fill choice".to_owned(),
2388                    ));
2389                }
2390                self.fill = Some(parse_fill(&raw)?);
2391            }
2392            b"fillRef" => {
2393                if self.fill.is_some() || self.fill_reference.is_some() {
2394                    return Err(OxmlError::InvalidValue(
2395                        "duplicate a:tcStyle fill choice".to_owned(),
2396                    ));
2397                }
2398                self.fill_reference = Some(parse_style_reference(&raw)?);
2399            }
2400            b"cell3D" => {
2401                self.unsupported.push("3-D properties".to_owned());
2402                self.raw_children.push(slot.unwrap(), raw);
2403            }
2404            _ => self.raw_children.push(*boundary, raw),
2405        }
2406        if let Some(slot) = slot {
2407            *boundary = (*boundary).max(slot + 1);
2408        }
2409        Ok(())
2410    }
2411
2412    fn write_xml<W: Write>(&self, writer: &mut Writer<W>) -> Result<()> {
2413        let mut start = BytesStart::new("a:tcStyle");
2414        push_attributes(&mut start, &self.raw_attributes);
2415        let has_modelled =
2416            self.borders.is_some() || self.fill.is_some() || self.fill_reference.is_some();
2417        if !has_modelled && self.raw_children.is_empty() {
2418            writer.write_event(Event::Empty(start))?;
2419            return Ok(());
2420        }
2421        writer.write_event(Event::Start(start))?;
2422        emit_raw(writer, self.raw_children.at(0))?;
2423        if let Some(borders) = &self.borders {
2424            borders.write_xml(writer)?;
2425        }
2426        emit_raw(writer, self.raw_children.at(1))?;
2427        if let Some(fill) = &self.fill {
2428            writer.write_event(Event::Start(BytesStart::new("a:fill")))?;
2429            fill.write_xml(writer).map_err(drawing_error)?;
2430            writer.write_event(Event::End(BytesEnd::new("a:fill")))?;
2431        }
2432        emit_raw(writer, self.raw_children.at(2))?;
2433        if let Some(reference) = &self.fill_reference {
2434            reference.write_xml(writer).map_err(drawing_error)?;
2435        }
2436        emit_raw(writer, self.raw_children.at(3))?;
2437        emit_raw(writer, self.raw_children.at(4))?;
2438        writer.write_event(Event::End(BytesEnd::new("a:tcStyle")))?;
2439        Ok(())
2440    }
2441}
2442
2443impl CT_TableBorders {
2444    fn from_xml(xml: &[u8]) -> Result<Self> {
2445        let mut reader = Reader::from_reader(xml);
2446        let mut buffer = Vec::new();
2447        loop {
2448            match reader.read_event_into(&mut buffer)? {
2449                Event::Start(start) if matches_local_name(start.name().as_ref(), b"tcBdr") => {
2450                    let mut value = Self {
2451                        raw_attributes: raw_attributes(&start, &[], false)?,
2452                        ..Self::default()
2453                    };
2454                    let mut boundary = 0usize;
2455                    loop {
2456                        buffer.clear();
2457                        match reader.read_event_into(&mut buffer)? {
2458                            Event::Start(child) => {
2459                                let name = local_name(child.name().as_ref()).to_vec();
2460                                let raw = capture_element(&mut reader, &child)?;
2461                                value.capture_child(&name, raw, &mut boundary)?;
2462                            }
2463                            Event::Empty(child) => {
2464                                let name = local_name(child.name().as_ref()).to_vec();
2465                                let raw = capture_empty_element(&child)?;
2466                                value.capture_child(&name, raw, &mut boundary)?;
2467                            }
2468                            Event::End(end)
2469                                if matches_local_name(end.name().as_ref(), b"tcBdr") =>
2470                            {
2471                                return Ok(value);
2472                            }
2473                            Event::Eof => return Err(missing("closing a:tcBdr")),
2474                            _ => {}
2475                        }
2476                    }
2477                }
2478                Event::Empty(start) if matches_local_name(start.name().as_ref(), b"tcBdr") => {
2479                    return Ok(Self {
2480                        raw_attributes: raw_attributes(&start, &[], false)?,
2481                        ..Self::default()
2482                    });
2483                }
2484                Event::Start(start) | Event::Empty(start) => return Err(unexpected(&start)),
2485                Event::Eof => return Err(missing("a:tcBdr")),
2486                _ => {}
2487            }
2488            buffer.clear();
2489        }
2490    }
2491
2492    fn capture_child(&mut self, name: &[u8], raw: Vec<u8>, boundary: &mut usize) -> Result<()> {
2493        let slot = table_border_slot(name);
2494        ensure_schema_order(slot, *boundary, "a:tcBdr")?;
2495        match name {
2496            b"left" => self.capture_border(&raw, slot, "left")?,
2497            b"right" => self.capture_border(&raw, slot, "right")?,
2498            b"top" => self.capture_border(&raw, slot, "top")?,
2499            b"bottom" => self.capture_border(&raw, slot, "bottom")?,
2500            b"lnL" => self.capture_border(&raw, slot, "lnL")?,
2501            b"lnR" => self.capture_border(&raw, slot, "lnR")?,
2502            b"lnT" => self.capture_border(&raw, slot, "lnT")?,
2503            b"lnB" => self.capture_border(&raw, slot, "lnB")?,
2504            b"insideH" => self.capture_border(&raw, slot, "insideH")?,
2505            b"insideV" => self.capture_border(&raw, slot, "insideV")?,
2506            b"tl2br" | b"tr2bl" | b"lnTlToBr" | b"lnBlToTr" => {
2507                self.unsupported.push("diagonal border".to_owned());
2508                self.raw_children.push(slot.unwrap_or(*boundary), raw);
2509            }
2510            _ => self.raw_children.push(slot.unwrap_or(*boundary), raw),
2511        }
2512        if let Some(slot) = slot {
2513            *boundary = (*boundary).max(slot + 1);
2514        }
2515        Ok(())
2516    }
2517
2518    fn capture_border(&mut self, raw: &[u8], slot: Option<usize>, name: &str) -> Result<()> {
2519        let line = if name.starts_with("ln") {
2520            Some(parse_named_line(raw)?)
2521        } else {
2522            parse_border_wrapper(raw)?
2523        };
2524        let Some(line) = line else {
2525            self.unsupported.push("border form".to_owned());
2526            self.raw_children.push(slot.unwrap_or(0), raw.to_vec());
2527            return Ok(());
2528        };
2529        let target = match name {
2530            "left" | "lnL" => &mut self.left,
2531            "right" | "lnR" => &mut self.right,
2532            "top" | "lnT" => &mut self.top,
2533            "bottom" | "lnB" => &mut self.bottom,
2534            "insideH" => &mut self.inside_horizontal,
2535            "insideV" => &mut self.inside_vertical,
2536            _ => unreachable!("table border name"),
2537        };
2538        set_once(target, line, "a:tcBdr edge")
2539    }
2540
2541    fn has_values(&self) -> bool {
2542        self.left.is_some()
2543            || self.right.is_some()
2544            || self.top.is_some()
2545            || self.bottom.is_some()
2546            || self.inside_horizontal.is_some()
2547            || self.inside_vertical.is_some()
2548            || !self.raw_attributes.is_empty()
2549            || !self.raw_children.is_empty()
2550    }
2551
2552    fn write_xml<W: Write>(&self, writer: &mut Writer<W>) -> Result<()> {
2553        let mut start = BytesStart::new("a:tcBdr");
2554        push_attributes(&mut start, &self.raw_attributes);
2555        if !self.has_values() {
2556            writer.write_event(Event::Empty(start))?;
2557            return Ok(());
2558        }
2559        writer.write_event(Event::Start(start))?;
2560        let lines = [
2561            ("a:left", self.left.as_ref()),
2562            ("a:right", self.right.as_ref()),
2563            ("a:top", self.top.as_ref()),
2564            ("a:bottom", self.bottom.as_ref()),
2565            ("a:insideH", self.inside_horizontal.as_ref()),
2566            ("a:insideV", self.inside_vertical.as_ref()),
2567        ];
2568        for (slot, (tag, line)) in lines.into_iter().enumerate() {
2569            emit_raw(writer, self.raw_children.at(slot))?;
2570            write_optional_border_wrapper(writer, tag, line)?;
2571        }
2572        for slot in 6..=8 {
2573            emit_raw(writer, self.raw_children.at(slot))?;
2574        }
2575        writer.write_event(Event::End(BytesEnd::new("a:tcBdr")))?;
2576        Ok(())
2577    }
2578}
2579
2580impl CT_TableTextStyle {
2581    fn from_xml(xml: &[u8]) -> Result<Self> {
2582        let mut reader = Reader::from_reader(xml);
2583        let mut buffer = Vec::new();
2584        loop {
2585            match reader.read_event_into(&mut buffer)? {
2586                Event::Start(start) if matches_local_name(start.name().as_ref(), b"tcTxStyle") => {
2587                    let mut value = Self {
2588                        bold: bool_attr(&start, b"b")?,
2589                        italic: bool_attr(&start, b"i")?,
2590                        raw_attributes: raw_attributes(&start, &[b"b", b"i"], false)?,
2591                        ..Self::default()
2592                    };
2593                    let mut boundary = 0usize;
2594                    loop {
2595                        buffer.clear();
2596                        match reader.read_event_into(&mut buffer)? {
2597                            Event::Start(child) => {
2598                                let name = local_name(child.name().as_ref()).to_vec();
2599                                let raw = capture_element(&mut reader, &child)?;
2600                                value.capture_child(&name, raw, &mut boundary)?;
2601                            }
2602                            Event::Empty(child) => {
2603                                let name = local_name(child.name().as_ref()).to_vec();
2604                                let raw = capture_empty_element(&child)?;
2605                                value.capture_child(&name, raw, &mut boundary)?;
2606                            }
2607                            Event::End(end)
2608                                if matches_local_name(end.name().as_ref(), b"tcTxStyle") =>
2609                            {
2610                                return Ok(value);
2611                            }
2612                            Event::Eof => return Err(missing("closing a:tcTxStyle")),
2613                            _ => {}
2614                        }
2615                    }
2616                }
2617                Event::Empty(start) if matches_local_name(start.name().as_ref(), b"tcTxStyle") => {
2618                    return Ok(Self {
2619                        bold: bool_attr(&start, b"b")?,
2620                        italic: bool_attr(&start, b"i")?,
2621                        raw_attributes: raw_attributes(&start, &[b"b", b"i"], false)?,
2622                        ..Self::default()
2623                    });
2624                }
2625                Event::Start(start) | Event::Empty(start) => return Err(unexpected(&start)),
2626                Event::Eof => return Err(missing("a:tcTxStyle")),
2627                _ => {}
2628            }
2629            buffer.clear();
2630        }
2631    }
2632
2633    fn capture_child(&mut self, name: &[u8], raw: Vec<u8>, boundary: &mut usize) -> Result<()> {
2634        match name {
2635            b"fontRef" if self.font_reference.is_none() => {
2636                self.font_reference = Some(parse_style_reference(&raw)?);
2637                *boundary = 1;
2638            }
2639            name if is_color(name) && self.color.is_none() => {
2640                self.color = Some(parse_color(&raw)?);
2641                *boundary = 2;
2642            }
2643            b"fontRef" => {
2644                return Err(OxmlError::InvalidValue(
2645                    "duplicate a:tcTxStyle/a:fontRef".to_owned(),
2646                ));
2647            }
2648            name if is_color(name) => {
2649                return Err(OxmlError::InvalidValue(format!(
2650                    "duplicate a:tcTxStyle colour {}",
2651                    String::from_utf8_lossy(name)
2652                )));
2653            }
2654            _ => self.raw_children.push(*boundary, raw),
2655        }
2656        Ok(())
2657    }
2658
2659    fn write_xml<W: Write>(&self, writer: &mut Writer<W>) -> Result<()> {
2660        let mut start = BytesStart::new("a:tcTxStyle");
2661        push_optional_bool(&mut start, "b", self.bold);
2662        push_optional_bool(&mut start, "i", self.italic);
2663        push_attributes(&mut start, &self.raw_attributes);
2664        if self.font_reference.is_none() && self.color.is_none() && self.raw_children.is_empty() {
2665            writer.write_event(Event::Empty(start))?;
2666            return Ok(());
2667        }
2668        writer.write_event(Event::Start(start))?;
2669        emit_raw(writer, self.raw_children.at(0))?;
2670        if let Some(reference) = &self.font_reference {
2671            reference.write_xml(writer).map_err(drawing_error)?;
2672        }
2673        emit_raw(writer, self.raw_children.at(1))?;
2674        if let Some(color) = &self.color {
2675            color.to_xml(writer).map_err(drawing_error)?;
2676        }
2677        emit_raw(writer, self.raw_children.at(2))?;
2678        writer.write_event(Event::End(BytesEnd::new("a:tcTxStyle")))?;
2679        Ok(())
2680    }
2681}
2682
2683/// PowerPoint's built-in table styles in gallery order, with their family and
2684/// colour variant. Variant 0 is a family's plain style and 1 to 6 are Accent 1
2685/// to Accent 6. Dark Style 2 pairs accents: its variants 1, 3 and 5 are
2686/// Accent 1/Accent 2, Accent 3/Accent 4 and Accent 5/Accent 6.
2687fn builtin_table_styles() -> [(&'static str, BuiltinTableFamily, u8); 74] {
2688    use BuiltinTableFamily::*;
2689    [
2690        ("{2D5ABB26-0587-4C30-8999-92F81FD0307C}", NoGrid, 0),
2691        ("{3C2FFA5D-87B4-456A-9821-1D502468CF0F}", Themed1, 1),
2692        ("{284E427A-3D55-4303-BF80-6455036E1DE7}", Themed1, 2),
2693        ("{69C7853C-536D-4A76-A0AE-DD22124D55A5}", Themed1, 3),
2694        ("{775DCB02-9BB8-47FD-8907-85C794F793BA}", Themed1, 4),
2695        ("{35758FB7-9AC5-4552-8A53-C91805E547FA}", Themed1, 5),
2696        ("{08FB837D-C827-4EFA-A057-4D05807E0F7C}", Themed1, 6),
2697        ("{5940675A-B579-460E-94D1-54222C63F5DA}", TableGrid, 0),
2698        ("{D113A9D2-9D6B-4929-AA2D-F23B5EE8CBE7}", Themed2, 1),
2699        ("{18603FDC-E32A-4AB5-989C-0864C3EAD2B8}", Themed2, 2),
2700        ("{306799F8-075E-4A3A-A7F6-7FBC6576F1A4}", Themed2, 3),
2701        ("{E269D01E-BC32-4049-B463-5C60D7B0CCD2}", Themed2, 4),
2702        ("{327F97BB-C833-4FB7-BDE5-3F7075034690}", Themed2, 5),
2703        ("{638B1855-1B75-4FBE-930C-398BA8C253C6}", Themed2, 6),
2704        ("{9D7B26C5-4107-4FEC-AEDC-1716B250A1EF}", Light1, 0),
2705        ("{3B4B98B0-60AC-42C2-AFA5-B58CD77FA1E5}", Light1, 1),
2706        ("{0E3FDE45-AF77-4B5C-9715-49D594BDF05E}", Light1, 2),
2707        ("{C083E6E3-FA7D-4D7B-A595-EF9225AFEA82}", Light1, 3),
2708        ("{D27102A9-8310-4765-A935-A1911B00CA55}", Light1, 4),
2709        ("{5FD0F851-EC5A-4D38-B0AD-8093EC10F338}", Light1, 5),
2710        ("{68D230F3-CF80-4859-8CE7-A43EE81993B5}", Light1, 6),
2711        ("{7E9639D4-E3E2-4D34-9284-5A2195B3D0D7}", Light2, 0),
2712        ("{69012ECD-51FC-41F1-AA8D-1B2483CD663E}", Light2, 1),
2713        ("{72833802-FEF1-4C79-8D5D-14CF1EAF98D9}", Light2, 2),
2714        ("{F2DE63D5-997A-4646-A377-4702673A728D}", Light2, 3),
2715        ("{17292A2E-F333-43FB-9621-5CBBE7FDCDCB}", Light2, 4),
2716        ("{5A111915-BE36-4E01-A7E5-04B1672EAD32}", Light2, 5),
2717        ("{912C8C85-51F0-491E-9774-3900AFEF0FD7}", Light2, 6),
2718        ("{616DA210-FB5B-4158-B5E0-FEB733F419BA}", Light3, 0),
2719        ("{BC89EF96-8CEA-46FF-86C4-4CE0E7609802}", Light3, 1),
2720        ("{5DA37D80-6434-44D0-A028-1B22A696006F}", Light3, 2),
2721        ("{8799B23B-EC83-4686-B30A-512413B5E67A}", Light3, 3),
2722        ("{ED083AE6-46FA-4A59-8FB0-9F97EB10719F}", Light3, 4),
2723        ("{BDBED569-4797-4DF1-A0F4-6AAB3CD982D8}", Light3, 5),
2724        ("{E8B1032C-EA38-4F05-BA0D-38AFFFC7BED3}", Light3, 6),
2725        ("{793D81CF-94F2-401A-BA57-92F5A7B2D0C5}", Medium1, 0),
2726        ("{B301B821-A1FF-4177-AEE7-76D212191A09}", Medium1, 1),
2727        ("{9DCAF9ED-07DC-4A11-8D7F-57B35C25682E}", Medium1, 2),
2728        ("{1FECB4D8-DB02-4DC6-A0A2-4F2EBAE1DC90}", Medium1, 3),
2729        ("{1E171933-4619-4E11-9A3F-F7608DF75F80}", Medium1, 4),
2730        ("{FABFCF23-3B69-468F-B69F-88F6DE6A72F2}", Medium1, 5),
2731        ("{10A1B5D5-9B99-4C35-A422-299274C87663}", Medium1, 6),
2732        ("{073A0DAA-6AF3-43AB-8588-CEC1D06C72B9}", Medium2, 0),
2733        ("{5C22544A-7EE6-4342-B048-85BDC9FD1C3A}", Medium2, 1),
2734        ("{21E4AEA4-8DFA-4A89-87EB-49C32662AFE0}", Medium2, 2),
2735        ("{F5AB1C69-6EDB-4FF4-983F-18BD219EF322}", Medium2, 3),
2736        ("{00A15C55-8517-42AA-B614-E9B94910E393}", Medium2, 4),
2737        ("{7DF18680-E054-41AD-8BC1-D1AEF772440D}", Medium2, 5),
2738        ("{93296810-A885-4BE3-A3E7-6D5BEEA58F35}", Medium2, 6),
2739        ("{8EC20E35-A176-4012-BC5E-935CFFF8708E}", Medium3, 0),
2740        ("{6E25E649-3F16-4E02-A733-19D2CDBF48F0}", Medium3, 1),
2741        ("{85BE263C-DBD7-4A20-BB59-AAB30ACAA65A}", Medium3, 2),
2742        ("{EB344D84-9AFB-497E-A393-DC336BA19D2E}", Medium3, 3),
2743        ("{EB9631B5-78F2-41C9-869B-9F39066F8104}", Medium3, 4),
2744        ("{74C1A8A3-306A-4EB7-A6B1-4F7E0EB9C5D6}", Medium3, 5),
2745        ("{2A488322-F2BA-4B5B-9748-0D474271808F}", Medium3, 6),
2746        ("{D7AC3CCA-C797-4891-BE02-D94E43425B78}", Medium4, 0),
2747        ("{69CF1AB2-1976-4502-BF36-3FF5EA218861}", Medium4, 1),
2748        ("{8A107856-5554-42FB-B03E-39F5DBC370BA}", Medium4, 2),
2749        ("{0505E3EF-67EA-436B-97B2-0124C06EBD24}", Medium4, 3),
2750        ("{C4B1156A-380E-4F78-BDF5-A606A8083BF9}", Medium4, 4),
2751        ("{22838BEF-8BB2-4498-84A7-C5851F593DF1}", Medium4, 5),
2752        ("{16D9F66E-5EB9-4882-86FB-DCBF35E3C3E4}", Medium4, 6),
2753        ("{E8034E78-7F5D-4C2E-B375-FC64B27BC917}", Dark1, 0),
2754        ("{125E5076-3810-47DD-B79F-674D7AD40C01}", Dark1, 1),
2755        ("{37CE84F3-28C3-443E-9E96-99CF82512B78}", Dark1, 2),
2756        ("{D03447BB-5D67-496B-8E87-E561075AD55C}", Dark1, 3),
2757        ("{E929F9F4-4A8F-4326-A1B4-22849713DDAB}", Dark1, 4),
2758        ("{8FD4443E-F989-4FC4-A0C8-D5A2AF1F390B}", Dark1, 5),
2759        ("{AF606853-7671-496A-8E4F-DF71F8EC918B}", Dark1, 6),
2760        ("{5202B0CA-FC54-4496-8BCA-5EF66A818D29}", Dark2, 0),
2761        ("{0660B408-B3CF-4A94-85FC-2B1E0A45F4A2}", Dark2, 1),
2762        ("{91EBBBCC-DAD2-459C-BE2E-F6DE35CF9A28}", Dark2, 3),
2763        ("{46F890A9-2807-4EBB-B81D-B2AA78EC7F39}", Dark2, 5),
2764    ]
2765}
2766
2767/// A family of built-in table styles, whose rules place a variant colour.
2768#[derive(Clone, Copy)]
2769enum BuiltinTableFamily {
2770    NoGrid,
2771    TableGrid,
2772    Themed1,
2773    Themed2,
2774    Light1,
2775    Light2,
2776    Light3,
2777    Medium1,
2778    Medium2,
2779    Medium3,
2780    Medium4,
2781    Dark1,
2782    Dark2,
2783}
2784
2785impl BuiltinTableFamily {
2786    fn style_name(self, accent: u8) -> String {
2787        let family = match self {
2788            Self::NoGrid => "No Style, No Grid",
2789            Self::TableGrid => "No Style, Table Grid",
2790            Self::Themed1 => "Themed Style 1",
2791            Self::Themed2 => "Themed Style 2",
2792            Self::Light1 => "Light Style 1",
2793            Self::Light2 => "Light Style 2",
2794            Self::Light3 => "Light Style 3",
2795            Self::Medium1 => "Medium Style 1",
2796            Self::Medium2 => "Medium Style 2",
2797            Self::Medium3 => "Medium Style 3",
2798            Self::Medium4 => "Medium Style 4",
2799            Self::Dark1 => "Dark Style 1",
2800            Self::Dark2 => "Dark Style 2",
2801        };
2802        match (self, accent) {
2803            (_, 0) => family.to_owned(),
2804            (Self::Dark2, _) => format!("{family} - Accent {accent}/Accent {}", accent + 1),
2805            _ => format!("{family} - Accent {accent}"),
2806        }
2807    }
2808
2809    /// Returns the family's regions for one variant, in schema order.
2810    fn regions(self, accent: u8) -> String {
2811        // `c` is the variant colour. A plain style takes the text colour.
2812        let c = match (self, accent) {
2813            (Self::Light1 | Self::Light2 | Self::Light3, 0) => "tx1".to_owned(),
2814            (_, 0) => "dk1".to_owned(),
2815            _ => format!("accent{accent}"),
2816        };
2817        match self {
2818            Self::NoGrid => part("wholeTbl", &text("tx1"), &all_edges(NO_LINE), NO_FILL),
2819            Self::TableGrid => {
2820                let grid = line(1, &clr("tx1"));
2821                part("wholeTbl", &text("tx1"), &all_edges(&grid), NO_FILL)
2822            }
2823            Self::Themed1 => {
2824                let (thin, thick) = (line_ref(1, &clr(&c)), line_ref(2, &clr(&c)));
2825                let divider = line_ref(2, &clr("lt1"));
2826                let band = solid(&alpha(&c, 40));
2827                let last_col = edges(&thick, &thin, &thin, &thin, &thin, NO_LINE);
2828                let first_col = edges(&thin, &thick, &thin, &thin, &thin, NO_LINE);
2829                let last_row = edges(&thin, &thin, &thick, &thick, NO_LINE, NO_LINE);
2830                let first_row = edges(&thin, &thin, &thin, &divider, NO_LINE, NO_LINE);
2831                [
2832                    table_background(2, 1, &clr(&c)),
2833                    part("wholeTbl", &text("dk1"), &all_edges(&thin), NO_FILL),
2834                    part("band1H", "", &[], &band),
2835                    part("band2H", "", &[], ""),
2836                    part("band1V", "", &[("top", &thin), ("bottom", &thin)], &band),
2837                    part("band2V", "", &[], ""),
2838                    part("lastCol", BOLD, &last_col, ""),
2839                    part("firstCol", BOLD, &first_col, ""),
2840                    part("lastRow", BOLD, &last_row, NO_FILL),
2841                    part("firstRow", &bold("lt1"), &first_row, &solid(&clr(&c))),
2842                ]
2843                .concat()
2844            }
2845            Self::Themed2 => {
2846                let edge = line_ref(1, &tint(&c, 50));
2847                let divider = line_ref(2, &clr("lt1"));
2848                let heading = line_ref(3, &clr("lt1"));
2849                let band = solid(&alpha("lt1", 20));
2850                let whole = edges(&edge, &edge, &edge, &edge, NO_LINE, NO_LINE);
2851                [
2852                    table_background(3, 3, &clr(&c)),
2853                    part("wholeTbl", &text("lt1"), &whole, NO_FILL),
2854                    part("band1H", "", &[], &band),
2855                    part("band1V", "", &[], &band),
2856                    part("lastCol", BOLD, &[("left", &divider)], ""),
2857                    part("firstCol", BOLD, &[("right", &divider)], ""),
2858                    part("lastRow", BOLD, &[("top", &divider)], NO_FILL),
2859                    part("seCell", "", &[("left", NO_LINE), ("top", NO_LINE)], ""),
2860                    part("swCell", "", &[("right", NO_LINE), ("top", NO_LINE)], ""),
2861                    part("firstRow", BOLD, &[("bottom", &heading)], NO_FILL),
2862                    part("neCell", "", &[("bottom", NO_LINE)], ""),
2863                ]
2864                .concat()
2865            }
2866            Self::Light1 => {
2867                let rule = line(1, &clr(&c));
2868                let band = solid(&alpha(&c, 20));
2869                let whole = edges(NO_LINE, NO_LINE, &rule, &rule, NO_LINE, NO_LINE);
2870                [
2871                    part("wholeTbl", &text("tx1"), &whole, NO_FILL),
2872                    part("band1H", "", &[], &band),
2873                    part("band2H", "", &[], ""),
2874                    part("band1V", "", &[], &band),
2875                    part("lastCol", BOLD, &[], ""),
2876                    part("firstCol", BOLD, &[], ""),
2877                    part("lastRow", BOLD, &[("top", &rule)], NO_FILL),
2878                    part("firstRow", BOLD, &[("bottom", &rule)], NO_FILL),
2879                ]
2880                .concat()
2881            }
2882            Self::Light2 => {
2883                let edge = line_ref(1, &clr(&c));
2884                let whole = edges(&edge, &edge, &edge, &edge, NO_LINE, NO_LINE);
2885                let total = double_line(4, &clr(&c));
2886                [
2887                    part("wholeTbl", &text("tx1"), &whole, NO_FILL),
2888                    part("band1H", "", &[("top", &edge), ("bottom", &edge)], ""),
2889                    part("band1V", "", &[("left", &edge), ("right", &edge)], ""),
2890                    part("band2V", "", &[("left", &edge), ("right", &edge)], ""),
2891                    part("lastCol", BOLD, &[], ""),
2892                    part("firstCol", BOLD, &[], ""),
2893                    part("lastRow", BOLD, &[("top", &total)], ""),
2894                    part("firstRow", &bold("bg1"), &[], &fill_ref(1, &clr(&c))),
2895                ]
2896                .concat()
2897            }
2898            Self::Light3 => {
2899                let grid = line(1, &clr(&c));
2900                let band = solid(&alpha(&c, 20));
2901                [
2902                    part("wholeTbl", &text("tx1"), &all_edges(&grid), NO_FILL),
2903                    part("band1H", "", &[], &band),
2904                    part("band1V", "", &[], &band),
2905                    part("lastCol", BOLD, &[], ""),
2906                    part("firstCol", BOLD, &[], ""),
2907                    part(
2908                        "lastRow",
2909                        BOLD,
2910                        &[("top", &double_line(4, &clr(&c)))],
2911                        NO_FILL,
2912                    ),
2913                    part("firstRow", BOLD, &[("bottom", &line(2, &clr(&c)))], NO_FILL),
2914                ]
2915                .concat()
2916            }
2917            Self::Medium1 => {
2918                let rule = line(1, &clr(&c));
2919                let whole = edges(&rule, &rule, &rule, &rule, &rule, NO_LINE);
2920                let band = solid(&tint(&c, 20));
2921                let white = solid(&clr("lt1"));
2922                [
2923                    part("wholeTbl", &text("dk1"), &whole, &white),
2924                    part("band1H", "", &[], &band),
2925                    part("band1V", "", &[], &band),
2926                    part("lastCol", BOLD, &[], ""),
2927                    part("firstCol", BOLD, &[], ""),
2928                    part(
2929                        "lastRow",
2930                        BOLD,
2931                        &[("top", &double_line(4, &clr(&c)))],
2932                        &white,
2933                    ),
2934                    part("firstRow", &bold("lt1"), &[], &solid(&clr(&c))),
2935                ]
2936                .concat()
2937            }
2938            Self::Medium2 => {
2939                let grid = line(1, &clr("lt1"));
2940                let divider = line(3, &clr("lt1"));
2941                let body = solid(&tint(&c, 20));
2942                let band = solid(&tint(&c, 40));
2943                let heading = solid(&clr(&c));
2944                [
2945                    part("wholeTbl", &text("dk1"), &all_edges(&grid), &body),
2946                    part("band1H", "", &[], &band),
2947                    part("band2H", "", &[], ""),
2948                    part("band1V", "", &[], &band),
2949                    part("band2V", "", &[], ""),
2950                    part("lastCol", &bold("lt1"), &[], &heading),
2951                    part("firstCol", &bold("lt1"), &[], &heading),
2952                    part("lastRow", &bold("lt1"), &[("top", &divider)], &heading),
2953                    part("firstRow", &bold("lt1"), &[("bottom", &divider)], &heading),
2954                ]
2955                .concat()
2956            }
2957            Self::Medium3 => {
2958                // Only the heading fills take the variant colour.
2959                let rule = line(2, &clr("dk1"));
2960                let whole = edges(NO_LINE, NO_LINE, &rule, &rule, NO_LINE, NO_LINE);
2961                let band = solid(&tint("dk1", 20));
2962                let white = solid(&clr("lt1"));
2963                let heading = solid(&clr(&c));
2964                [
2965                    part("wholeTbl", &text("dk1"), &whole, &white),
2966                    part("band1H", "", &[], &band),
2967                    part("band1V", "", &[], &band),
2968                    part("lastCol", &bold("lt1"), &[], &heading),
2969                    part("firstCol", &bold("lt1"), &[], &heading),
2970                    part(
2971                        "lastRow",
2972                        BOLD,
2973                        &[("top", &double_line(4, &clr("dk1")))],
2974                        &white,
2975                    ),
2976                    part("seCell", &bold("dk1"), &[], ""),
2977                    part("swCell", &bold("dk1"), &[], ""),
2978                    part("firstRow", &bold("lt1"), &[("bottom", &rule)], &heading),
2979                ]
2980                .concat()
2981            }
2982            Self::Medium4 => {
2983                let grid = line(1, &clr(&c));
2984                let body = solid(&tint(&c, 20));
2985                let band = solid(&tint(&c, 40));
2986                [
2987                    part("wholeTbl", &text("dk1"), &all_edges(&grid), &body),
2988                    part("band1H", "", &[], &band),
2989                    part("band1V", "", &[], &band),
2990                    part("lastCol", BOLD, &[], ""),
2991                    part("firstCol", BOLD, &[], ""),
2992                    part("lastRow", BOLD, &[("top", &line(2, &clr(&c)))], &body),
2993                    part("firstRow", BOLD, &[], &body),
2994                ]
2995                .concat()
2996            }
2997            Self::Dark1 => {
2998                // The plain style tints black, the accent styles shade the accent.
2999                let plain = accent == 0;
3000                let body = solid(&if plain { tint("dk1", 20) } else { clr(&c) });
3001                let band = solid(&if plain {
3002                    tint("dk1", 40)
3003                } else {
3004                    shade(&c, 60)
3005                });
3006                let edge = solid(&if plain {
3007                    tint("dk1", 60)
3008                } else {
3009                    shade(&c, 60)
3010                });
3011                let total = solid(&if plain {
3012                    tint("dk1", 60)
3013                } else {
3014                    shade(&c, 40)
3015                });
3016                let divider = line(2, &clr("lt1"));
3017                [
3018                    part("wholeTbl", &text("lt1"), &all_edges(NO_LINE), &body),
3019                    part("band1H", "", &[], &band),
3020                    part("band1V", "", &[], &band),
3021                    part("lastCol", BOLD, &[("left", &divider)], &edge),
3022                    part("firstCol", BOLD, &[("right", &divider)], &edge),
3023                    part("lastRow", BOLD, &[("top", &divider)], &total),
3024                    part("seCell", "", &[("left", NO_LINE)], ""),
3025                    part("swCell", "", &[("right", NO_LINE)], ""),
3026                    part(
3027                        "firstRow",
3028                        BOLD,
3029                        &[("bottom", &divider)],
3030                        &solid(&clr("dk1")),
3031                    ),
3032                    part("neCell", "", &[("left", NO_LINE)], ""),
3033                    part("nwCell", "", &[("right", NO_LINE)], ""),
3034                ]
3035                .concat()
3036            }
3037            Self::Dark2 => {
3038                // The heading row takes the second accent of the pair.
3039                let heading = match accent {
3040                    0 => clr("dk1"),
3041                    _ => clr(&format!("accent{}", accent + 1)),
3042                };
3043                let body = solid(&tint(&c, 20));
3044                let band = solid(&tint(&c, 40));
3045                [
3046                    part("wholeTbl", &text("dk1"), &all_edges(NO_LINE), &body),
3047                    part("band1H", "", &[], &band),
3048                    part("band1V", "", &[], &band),
3049                    part("lastCol", BOLD, &[], ""),
3050                    part("firstCol", BOLD, &[], ""),
3051                    part(
3052                        "lastRow",
3053                        BOLD,
3054                        &[("top", &double_line(4, &clr("dk1")))],
3055                        &body,
3056                    ),
3057                    part("firstRow", &bold("lt1"), &[], &solid(&heading)),
3058                ]
3059                .concat()
3060            }
3061        }
3062    }
3063}
3064
3065const NO_LINE: &str = "<a:ln><a:noFill/></a:ln>";
3066const NO_FILL: &str = "<a:fill><a:noFill/></a:fill>";
3067const BOLD: &str = r#"<a:tcTxStyle b="on"/>"#;
3068
3069/// One table style region: its text style, its `a:tcBdr` edges in schema
3070/// order, and its fill.
3071fn part(tag: &str, text: &str, edges: &[(&str, &str)], fill: &str) -> String {
3072    let borders = edges
3073        .iter()
3074        .map(|(edge, line)| format!("<a:{edge}>{line}</a:{edge}>"))
3075        .collect::<String>();
3076    let borders = if borders.is_empty() {
3077        "<a:tcBdr/>".to_owned()
3078    } else {
3079        format!("<a:tcBdr>{borders}</a:tcBdr>")
3080    };
3081    format!("<a:{tag}>{text}<a:tcStyle>{borders}{fill}</a:tcStyle></a:{tag}>")
3082}
3083
3084/// The six `a:tcBdr` edges in schema order.
3085fn edges<'a>(
3086    left: &'a str,
3087    right: &'a str,
3088    top: &'a str,
3089    bottom: &'a str,
3090    inside_h: &'a str,
3091    inside_v: &'a str,
3092) -> [(&'static str, &'a str); 6] {
3093    [
3094        ("left", left),
3095        ("right", right),
3096        ("top", top),
3097        ("bottom", bottom),
3098        ("insideH", inside_h),
3099        ("insideV", inside_v),
3100    ]
3101}
3102
3103fn all_edges(line: &str) -> [(&'static str, &str); 6] {
3104    edges(line, line, line, line, line, line)
3105}
3106
3107/// Table text in the theme's minor font and a scheme colour.
3108fn text(color: &str) -> String {
3109    format!(r#"<a:tcTxStyle>{}</a:tcTxStyle>"#, minor_font(color))
3110}
3111
3112fn bold(color: &str) -> String {
3113    format!(r#"<a:tcTxStyle b="on">{}</a:tcTxStyle>"#, minor_font(color))
3114}
3115
3116fn minor_font(color: &str) -> String {
3117    let font = r#"<a:fontRef idx="minor"><a:scrgbClr r="0" g="0" b="0"/></a:fontRef>"#;
3118    format!("{font}{}", clr(color))
3119}
3120
3121fn clr(color: &str) -> String {
3122    format!(r#"<a:schemeClr val="{color}"/>"#)
3123}
3124
3125fn tint(color: &str, percent: u32) -> String {
3126    modified(color, "tint", percent)
3127}
3128
3129fn shade(color: &str, percent: u32) -> String {
3130    modified(color, "shade", percent)
3131}
3132
3133fn alpha(color: &str, percent: u32) -> String {
3134    modified(color, "alpha", percent)
3135}
3136
3137fn modified(color: &str, modifier: &str, percent: u32) -> String {
3138    let value = percent * 1_000;
3139    format!(r#"<a:schemeClr val="{color}"><a:{modifier} val="{value}"/></a:schemeClr>"#)
3140}
3141
3142/// A solid line whose width is in points.
3143fn line(points: u32, color: &str) -> String {
3144    let width = points * 12_700;
3145    format!(r#"<a:ln w="{width}" cmpd="sng"><a:solidFill>{color}</a:solidFill></a:ln>"#)
3146}
3147
3148fn double_line(points: u32, color: &str) -> String {
3149    let width = points * 12_700;
3150    format!(r#"<a:ln w="{width}" cmpd="dbl"><a:solidFill>{color}</a:solidFill></a:ln>"#)
3151}
3152
3153fn line_ref(index: u8, color: &str) -> String {
3154    format!(r#"<a:lnRef idx="{index}">{color}</a:lnRef>"#)
3155}
3156
3157fn solid(color: &str) -> String {
3158    format!("<a:fill><a:solidFill>{color}</a:solidFill></a:fill>")
3159}
3160
3161fn fill_ref(index: u8, color: &str) -> String {
3162    format!(r#"<a:fillRef idx="{index}">{color}</a:fillRef>"#)
3163}
3164
3165/// A `tblBg` of theme fill and effect references.
3166fn table_background(fill: u8, effect: u8, color: &str) -> String {
3167    let fill = format!(r#"<a:fillRef idx="{fill}">{color}</a:fillRef>"#);
3168    let effect = format!(r#"<a:effectRef idx="{effect}">{color}</a:effectRef>"#);
3169    format!("<a:tblBg>{fill}{effect}</a:tblBg>")
3170}
3171
3172fn table_style_slot(name: &[u8]) -> Option<usize> {
3173    match name {
3174        b"tblBg" => Some(0),
3175        b"wholeTbl" => Some(1),
3176        b"band1H" => Some(2),
3177        b"band2H" => Some(3),
3178        b"band1V" => Some(4),
3179        b"band2V" => Some(5),
3180        b"lastCol" => Some(6),
3181        b"firstCol" => Some(7),
3182        b"lastRow" => Some(8),
3183        b"seCell" => Some(9),
3184        b"swCell" => Some(10),
3185        b"firstRow" => Some(11),
3186        b"neCell" => Some(12),
3187        b"nwCell" => Some(13),
3188        _ => None,
3189    }
3190}
3191
3192fn table_border_slot(name: &[u8]) -> Option<usize> {
3193    match name {
3194        b"left" | b"lnL" => Some(0),
3195        b"right" | b"lnR" => Some(1),
3196        b"top" | b"lnT" => Some(2),
3197        b"bottom" | b"lnB" => Some(3),
3198        b"insideH" => Some(4),
3199        b"insideV" => Some(5),
3200        b"tl2br" | b"lnTlToBr" => Some(6),
3201        b"tr2bl" | b"lnBlToTr" => Some(7),
3202        _ => None,
3203    }
3204}
3205
3206fn cell_property_slot(name: &[u8]) -> Option<usize> {
3207    match name {
3208        b"lnL" => Some(0),
3209        b"lnR" => Some(1),
3210        b"lnT" => Some(2),
3211        b"lnB" => Some(3),
3212        b"lnTlToBr" => Some(4),
3213        b"lnBlToTr" => Some(5),
3214        b"cell3D" | b"effectLst" | b"effectDag" => Some(6),
3215        name if is_fill(name) => Some(7),
3216        b"headers" => Some(8),
3217        b"extLst" => Some(9),
3218        _ => None,
3219    }
3220}
3221
3222fn ensure_schema_order(slot: Option<usize>, boundary: usize, parent: &str) -> Result<()> {
3223    if slot.is_some_and(|slot| slot < boundary) {
3224        return Err(OxmlError::InvalidValue(format!(
3225            "{parent} children violate schema order"
3226        )));
3227    }
3228    Ok(())
3229}
3230
3231fn set_once<T>(target: &mut Option<T>, value: T, name: &str) -> Result<()> {
3232    if target.replace(value).is_some() {
3233        return Err(OxmlError::InvalidValue(format!("duplicate {name}")));
3234    }
3235    Ok(())
3236}
3237
3238fn is_fill(name: &[u8]) -> bool {
3239    matches!(
3240        name,
3241        b"noFill" | b"solidFill" | b"gradFill" | b"pattFill" | b"blipFill"
3242    )
3243}
3244
3245fn is_color(name: &[u8]) -> bool {
3246    matches!(
3247        name,
3248        b"scrgbClr" | b"srgbClr" | b"hslClr" | b"sysClr" | b"schemeClr" | b"prstClr"
3249    )
3250}
3251
3252fn parse_fill(xml: &[u8]) -> Result<Fill> {
3253    Fill::from_xml(xml).map_err(drawing_error)
3254}
3255
3256fn parse_fill_wrapper(xml: &[u8]) -> Result<Option<Fill>> {
3257    let mut reader = Reader::from_reader(xml);
3258    let mut buffer = Vec::new();
3259    let mut root_seen = false;
3260    let mut fill = None;
3261    loop {
3262        match reader.read_event_into(&mut buffer)? {
3263            Event::Start(start) if !root_seen => {
3264                root_seen = true;
3265                if start.attributes().next().is_some() {
3266                    return Ok(None);
3267                }
3268            }
3269            Event::Start(child) if root_seen && is_fill(local_name(child.name().as_ref())) => {
3270                if fill.is_some() {
3271                    return Ok(None);
3272                }
3273                fill = Some(parse_fill(&capture_element(&mut reader, &child)?)?);
3274            }
3275            Event::Empty(child) if root_seen && is_fill(local_name(child.name().as_ref())) => {
3276                if fill.is_some() {
3277                    return Ok(None);
3278                }
3279                fill = Some(parse_fill(&capture_empty_element(&child)?)?);
3280            }
3281            Event::Start(child) if root_seen => {
3282                let _ = capture_element(&mut reader, &child)?;
3283                return Ok(None);
3284            }
3285            Event::Empty(_) if root_seen => return Ok(None),
3286            Event::End(_) if root_seen => return Ok(fill),
3287            Event::Eof => return Err(missing("closing a:fill")),
3288            Event::Text(text) if !xml_text_is_whitespace(&text) => {
3289                return Ok(None);
3290            }
3291            _ => {}
3292        }
3293        buffer.clear();
3294    }
3295}
3296
3297fn parse_style_reference(xml: &[u8]) -> Result<StyleReference> {
3298    StyleReference::from_xml(xml).map_err(drawing_error)
3299}
3300
3301fn parse_color(xml: &[u8]) -> Result<crate::color::ColorChoice> {
3302    let mut reader = Reader::from_reader(xml);
3303    let mut buffer = Vec::new();
3304    loop {
3305        match reader.read_event_into(&mut buffer)? {
3306            Event::Start(start) => {
3307                return crate::color::ColorChoice::from_xml(&mut reader, &start)
3308                    .map_err(drawing_error);
3309            }
3310            Event::Empty(start) => {
3311                return crate::color::ColorChoice::from_empty_xml(&start).map_err(drawing_error);
3312            }
3313            Event::Eof => return Err(missing("DrawingML colour")),
3314            _ => {}
3315        }
3316        buffer.clear();
3317    }
3318}
3319
3320fn parse_named_line(xml: &[u8]) -> Result<CT_LineProperties> {
3321    let text = std::str::from_utf8(xml)?;
3322    let name_start = text.find('<').ok_or_else(|| missing("table border"))? + 1;
3323    let name_end = text[name_start..]
3324        .find(|character: char| character.is_whitespace() || matches!(character, '>' | '/'))
3325        .map(|offset| name_start + offset)
3326        .ok_or_else(|| missing("table border name"))?;
3327    let original_name = &text[name_start..name_end];
3328    let mut renamed = String::with_capacity(text.len() + 8);
3329    renamed.push_str(&text[..name_start]);
3330    renamed.push_str("a:ln");
3331    renamed.push_str(&text[name_end..]);
3332    let closing = format!("</{original_name}>");
3333    if renamed.ends_with(&closing) {
3334        renamed.truncate(renamed.len() - closing.len());
3335        renamed.push_str("</a:ln>");
3336    }
3337    CT_LineProperties::from_xml(renamed.as_bytes()).map_err(drawing_error)
3338}
3339
3340fn parse_border_wrapper(xml: &[u8]) -> Result<Option<CT_LineProperties>> {
3341    let mut reader = Reader::from_reader(xml);
3342    let mut buffer = Vec::new();
3343    let mut root_seen = false;
3344    let mut line = None;
3345    loop {
3346        match reader.read_event_into(&mut buffer)? {
3347            Event::Start(start) if !root_seen => {
3348                root_seen = true;
3349                if start.attributes().next().is_some() {
3350                    return Ok(None);
3351                }
3352            }
3353            Event::Start(child)
3354                if root_seen && matches_local_name(child.name().as_ref(), b"ln") =>
3355            {
3356                if line.is_some() {
3357                    return Ok(None);
3358                }
3359                line = Some(parse_named_line(&capture_element(&mut reader, &child)?)?);
3360            }
3361            Event::Empty(child)
3362                if root_seen && matches_local_name(child.name().as_ref(), b"ln") =>
3363            {
3364                if line.is_some() {
3365                    return Ok(None);
3366                }
3367                line = Some(parse_named_line(&capture_empty_element(&child)?)?);
3368            }
3369            Event::Start(child) if root_seen => {
3370                let _ = capture_element(&mut reader, &child)?;
3371                return Ok(None);
3372            }
3373            Event::Empty(_) if root_seen => return Ok(None),
3374            Event::End(_) if root_seen => return Ok(line),
3375            Event::Eof => return Err(missing("closing table border wrapper")),
3376            Event::Text(text) if !xml_text_is_whitespace(&text) => {
3377                return Ok(None);
3378            }
3379            _ => {}
3380        }
3381        buffer.clear();
3382    }
3383}
3384
3385fn write_optional_named_line<W: Write>(
3386    writer: &mut Writer<W>,
3387    tag: &str,
3388    line: Option<&CT_LineProperties>,
3389) -> Result<()> {
3390    let Some(line) = line else {
3391        return Ok(());
3392    };
3393    let xml = line.to_xml().map_err(drawing_error)?;
3394    let text = std::str::from_utf8(&xml)?;
3395    let mut renamed = text.replacen("<a:ln", &format!("<{tag}"), 1);
3396    if renamed.ends_with("</a:ln>") {
3397        renamed.truncate(renamed.len() - "</a:ln>".len());
3398        renamed.push_str("</");
3399        renamed.push_str(tag);
3400        renamed.push('>');
3401    }
3402    writer.get_mut().write_all(renamed.as_bytes())?;
3403    Ok(())
3404}
3405
3406fn write_optional_border_wrapper<W: Write>(
3407    writer: &mut Writer<W>,
3408    tag: &str,
3409    line: Option<&CT_LineProperties>,
3410) -> Result<()> {
3411    let Some(line) = line else {
3412        return Ok(());
3413    };
3414    writer.write_event(Event::Start(BytesStart::new(tag)))?;
3415    line.write_xml(writer).map_err(drawing_error)?;
3416    writer.write_event(Event::End(BytesEnd::new(tag)))?;
3417    Ok(())
3418}
3419
3420fn drawing_error(error: impl std::fmt::Display) -> OxmlError {
3421    OxmlError::InvalidValue(error.to_string())
3422}
3423
3424fn xml_text_is_whitespace(text: &BytesText<'_>) -> bool {
3425    let bytes: &[u8] = text.as_ref();
3426    bytes.iter().all(u8::is_ascii_whitespace)
3427}
3428
3429fn required_string(start: &BytesStart<'_>, name: &[u8]) -> Result<String> {
3430    decoded_attr(start, name)?.ok_or_else(|| {
3431        missing(&format!(
3432            "{}@{}",
3433            String::from_utf8_lossy(local_name(start.name().as_ref())),
3434            String::from_utf8_lossy(name)
3435        ))
3436    })
3437}
3438
3439fn optional_emu_attr(start: &BytesStart<'_>, name: &[u8]) -> Result<Option<Emu>> {
3440    decoded_attr(start, name)?
3441        .map(|value| {
3442            value.parse::<i64>().map(Emu).map_err(|error| {
3443                OxmlError::InvalidValue(format!(
3444                    "invalid {}: {error}",
3445                    String::from_utf8_lossy(name)
3446                ))
3447            })
3448        })
3449        .transpose()
3450}
3451
3452fn push_optional_emu(start: &mut BytesStart<'_>, name: &'static str, value: Option<Emu>) {
3453    if let Some(value) = value {
3454        start.push_attribute((name, Cow::Owned(value.0.to_string())));
3455    }
3456}
3457
3458fn push_optional_bool(start: &mut BytesStart<'_>, name: &'static str, value: Option<bool>) {
3459    if let Some(value) = value {
3460        start.push_attribute((name, if value { "1" } else { "0" }));
3461    }
3462}
3463
3464fn raw_attributes(
3465    start: &BytesStart<'_>,
3466    modelled: &[&[u8]],
3467    is_root: bool,
3468) -> Result<RawAttributes> {
3469    let mut raw = Vec::new();
3470    for attribute in start.attributes() {
3471        let attribute = attribute?;
3472        let key = attribute.key.as_ref();
3473        if modelled.contains(&key) || (is_root && key == b"xmlns:a") {
3474            continue;
3475        }
3476        raw.push((
3477            std::str::from_utf8(key)?.to_owned(),
3478            attribute
3479                .decoded_and_normalized_value(XmlVersion::Implicit1_0, start.decoder())?
3480                .into_owned(),
3481        ));
3482    }
3483    Ok(raw)
3484}
3485
3486fn decoded_attr(start: &BytesStart<'_>, name: &[u8]) -> Result<Option<String>> {
3487    for attribute in start.attributes() {
3488        let attribute = attribute?;
3489        if attribute.key.as_ref() == name {
3490            return Ok(Some(
3491                attribute
3492                    .decoded_and_normalized_value(XmlVersion::Implicit1_0, start.decoder())?
3493                    .into_owned(),
3494            ));
3495        }
3496    }
3497    Ok(None)
3498}
3499
3500fn required_i64(start: &BytesStart<'_>, name: &[u8]) -> Result<i64> {
3501    let value = decoded_attr(start, name)?.ok_or_else(|| {
3502        missing(&format!(
3503            "{}@{}",
3504            String::from_utf8_lossy(local_name(start.name().as_ref())),
3505            String::from_utf8_lossy(name)
3506        ))
3507    })?;
3508    value.parse::<i64>().map_err(|error| {
3509        OxmlError::InvalidValue(format!(
3510            "invalid {}@{} {value}: {error}",
3511            String::from_utf8_lossy(local_name(start.name().as_ref())),
3512            String::from_utf8_lossy(name)
3513        ))
3514    })
3515}
3516
3517fn positive_u32_attr(start: &BytesStart<'_>, name: &[u8]) -> Result<Option<u32>> {
3518    let Some(value) = decoded_attr(start, name)? else {
3519        return Ok(None);
3520    };
3521    let parsed = value.parse::<u32>().map_err(|error| {
3522        OxmlError::InvalidValue(format!(
3523            "invalid a:tc@{} {value}: {error}",
3524            String::from_utf8_lossy(name)
3525        ))
3526    })?;
3527    if parsed == 0 {
3528        return Err(OxmlError::InvalidValue(format!(
3529            "a:tc@{} must be positive",
3530            String::from_utf8_lossy(name)
3531        )));
3532    }
3533    Ok(Some(parsed))
3534}
3535
3536fn bool_attr(start: &BytesStart<'_>, name: &[u8]) -> Result<Option<bool>> {
3537    let Some(value) = decoded_attr(start, name)? else {
3538        return Ok(None);
3539    };
3540    match value.as_str() {
3541        "true" | "1" | "on" => Ok(Some(true)),
3542        "false" | "0" | "off" => Ok(Some(false)),
3543        _ => Err(OxmlError::InvalidValue(format!(
3544            "invalid {}@{} boolean: {value}",
3545            String::from_utf8_lossy(local_name(start.name().as_ref())),
3546            String::from_utf8_lossy(name)
3547        ))),
3548    }
3549}
3550
3551fn push_true(start: &mut BytesStart<'_>, name: &'static str, value: bool) {
3552    if value {
3553        start.push_attribute((name, "1"));
3554    }
3555}
3556
3557fn push_attributes(start: &mut BytesStart<'_>, attributes: &RawAttributes) {
3558    for (name, value) in attributes {
3559        start.push_attribute((name.as_str(), value.as_str()));
3560    }
3561}
3562
3563fn emit_raw<'a, W: Write>(
3564    writer: &mut Writer<W>,
3565    children: impl Iterator<Item = &'a [u8]>,
3566) -> Result<()> {
3567    for child in children {
3568        writer.get_mut().write_all(child)?;
3569    }
3570    Ok(())
3571}
3572
3573fn missing(element: &str) -> OxmlError {
3574    OxmlError::MissingElement(element.to_owned())
3575}
3576
3577fn unexpected(start: &BytesStart<'_>) -> OxmlError {
3578    OxmlError::UnexpectedElement(String::from_utf8_lossy(start.name().as_ref()).into_owned())
3579}
3580
3581#[cfg(test)]
3582mod tests {
3583    use std::path::PathBuf;
3584
3585    use oxml_opc::OpcPackage;
3586
3587    use std::collections::HashSet;
3588
3589    use super::{
3590        A_NS, CT_Table, CT_TableStyle, CT_TableStyleList, Emu, Fill, OxmlError, StyleReference,
3591        builtin_table_styles,
3592    };
3593    use crate::color::{ColorChoice, ColorTransform};
3594
3595    #[test]
3596    fn new_table_uses_truncating_dimensions_and_valid_cell_shells() {
3597        let table = CT_Table::new(2, 3, Emu(302), Emu(201)).unwrap();
3598        assert_eq!(table.grid.columns, vec![Emu(100), Emu(100), Emu(102)]);
3599        assert_eq!(
3600            table.rows.iter().map(|row| row.height).collect::<Vec<_>>(),
3601            vec![Emu(100), Emu(101)]
3602        );
3603        assert!(table.properties.as_ref().unwrap().first_row);
3604        assert!(table.properties.as_ref().unwrap().band_rows);
3605        assert!(table.rows.iter().all(|row| row.cells.len() == 3));
3606        assert!(table.rows.iter().flat_map(|row| &row.cells).all(|cell| {
3607            cell.text_body.as_ref().unwrap().paragraph_count() == 1 && cell.properties.is_some()
3608        }));
3609        assert_eq!(CT_Table::from_xml(&table.to_xml().unwrap()).unwrap(), table);
3610    }
3611
3612    #[test]
3613    fn new_table_rejects_invalid_dimensions_before_allocation() {
3614        for result in [
3615            CT_Table::new(0, 1, Emu(1), Emu(1)),
3616            CT_Table::new(1, 0, Emu(1), Emu(1)),
3617            CT_Table::new(1, 1, Emu(0), Emu(1)),
3618            CT_Table::new(1, 1, Emu(1), Emu(-1)),
3619            CT_Table::new(2, 1, Emu(1), Emu(1)),
3620            CT_Table::new(1, 2, Emu(1), Emu(1)),
3621        ] {
3622            assert!(result.is_err());
3623        }
3624    }
3625
3626    #[test]
3627    fn table_style_and_cell_properties_preserve_unmodelled_xml_byte_for_byte() {
3628        let xml = br#"<q:tblStyleLst xmlns:q="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:x="urn:producer" def="style"><x:before/><q:tblStyle styleId="style" styleName="Style"><q:wholeTbl><q:tcStyle><q:solidFill><q:srgbClr val="112233"/></q:solidFill><x:unsupported value="kept"/></q:tcStyle></q:wholeTbl></q:tblStyle><x:after/></q:tblStyleLst>"#;
3629
3630        let styles = CT_TableStyleList::from_xml(xml).expect("parse table style list");
3631        let written = styles.to_xml().expect("write table style list");
3632
3633        assert!(
3634            written
3635                .windows(b"<x:before/>".len())
3636                .any(|part| part == b"<x:before/>")
3637        );
3638        assert!(
3639            written
3640                .windows(b"<x:unsupported value=\"kept\"/>".len())
3641                .any(|part| part == b"<x:unsupported value=\"kept\"/>")
3642        );
3643        assert!(
3644            written
3645                .windows(b"<x:after/>".len())
3646                .any(|part| part == b"<x:after/>")
3647        );
3648        assert!(written.starts_with(
3649            br#"<a:tblStyleLst xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main""#
3650        ));
3651        assert_eq!(styles, CT_TableStyleList::from_xml(&written).unwrap());
3652
3653        let producer_style = br#"<a:tblStyleLst xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" def="style"><a:tblStyle styleId="style" styleName="Producer"><a:wholeTbl><a:tcTxStyle b="on"><a:fontRef idx="minor"><a:prstClr val="black"/></a:fontRef><a:schemeClr val="dk1"/></a:tcTxStyle><a:tcStyle><a:tcBdr><a:left><a:ln w="12700"><a:solidFill><a:schemeClr val="lt1"/></a:solidFill></a:ln></a:left><a:insideH><a:ln w="25400"><a:solidFill><a:schemeClr val="lt1"/></a:solidFill></a:ln></a:insideH></a:tcBdr><a:fill><a:solidFill><a:schemeClr val="accent1"/></a:solidFill></a:fill></a:tcStyle></a:wholeTbl></a:tblStyle></a:tblStyleLst>"#;
3654        let producer_style = CT_TableStyleList::from_xml(producer_style).unwrap();
3655        let whole = producer_style.styles[0].whole_table.as_ref().unwrap();
3656        assert_eq!(whole.text_style.as_ref().unwrap().bold, Some(true));
3657        assert!(whole.cell_style.as_ref().unwrap().fill.is_some());
3658        assert!(
3659            whole
3660                .cell_style
3661                .as_ref()
3662                .unwrap()
3663                .borders
3664                .as_ref()
3665                .unwrap()
3666                .left
3667                .is_some()
3668        );
3669        assert!(
3670            whole
3671                .cell_style
3672                .as_ref()
3673                .unwrap()
3674                .borders
3675                .as_ref()
3676                .unwrap()
3677                .inside_horizontal
3678                .is_some()
3679        );
3680        let producer_written = producer_style.to_xml().unwrap();
3681        assert_eq!(
3682            producer_style,
3683            CT_TableStyleList::from_xml(&producer_written).unwrap()
3684        );
3685
3686        let direct_edge_alias = br#"<a:tblStyleLst xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"><a:tblStyle styleId="alias" styleName="Alias"><a:wholeTbl><a:tcStyle><a:tcBdr><a:lnL w="12700"><a:solidFill><a:srgbClr val="000000"/></a:solidFill></a:lnL></a:tcBdr></a:tcStyle></a:wholeTbl></a:tblStyle></a:tblStyleLst>"#;
3687        let direct_edge_alias = CT_TableStyleList::from_xml(direct_edge_alias).unwrap();
3688        let borders = direct_edge_alias.styles[0]
3689            .whole_table
3690            .as_ref()
3691            .unwrap()
3692            .cell_style
3693            .as_ref()
3694            .unwrap()
3695            .borders
3696            .as_ref()
3697            .unwrap();
3698        assert!(borders.left.is_some());
3699        assert!(borders.unsupported.is_empty());
3700
3701        let table_xml = br#"<q:tbl xmlns:q="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:x="urn:producer"><q:tblGrid><q:gridCol w="127000"/></q:tblGrid><q:tr h="127000"><q:tc><q:tcPr marL="12700" marR="25400" marT="38100" marB="50800"><q:lnL w="12700"><q:solidFill><q:srgbClr val="000000"/></q:solidFill></q:lnL><q:lnTlToBr><x:diagonal/></q:lnTlToBr><q:solidFill><q:srgbClr val="ABCDEF"/></q:solidFill><x:after-fill/></q:tcPr></q:tc></q:tr></q:tbl>"#;
3702        let table = CT_Table::from_xml(table_xml).unwrap();
3703        let properties = table.rows[0].cells[0].properties.as_ref().unwrap();
3704        assert_eq!(properties.margin_left, Some(Emu(12_700)));
3705        assert_eq!(properties.margin_bottom, Some(Emu(50_800)));
3706        assert!(properties.left.is_some());
3707        assert!(properties.fill.is_some());
3708        assert_eq!(properties.unsupported, ["diagonal border"]);
3709        let table_written = table.to_xml().unwrap();
3710        assert!(
3711            table_written
3712                .windows(b"<x:diagonal/>".len())
3713                .any(|part| part == b"<x:diagonal/>")
3714        );
3715        assert!(
3716            table_written
3717                .windows(b"<x:after-fill/>".len())
3718                .any(|part| part == b"<x:after-fill/>")
3719        );
3720        assert_eq!(table, CT_Table::from_xml(&table_written).unwrap());
3721    }
3722
3723    #[test]
3724    fn every_corpus_table_style_list_parses_and_round_trips() {
3725        let corpus = std::env::var_os("RDOCX_PPTX_CORPUS_DIR")
3726            .map(PathBuf::from)
3727            .unwrap_or_else(|| PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../corpus/pptx"));
3728        if !corpus.is_dir() {
3729            assert_ne!(
3730                std::env::var_os("RDOCX_PPTX_CORPUS_REQUIRED").as_deref(),
3731                Some(std::ffi::OsStr::new("1")),
3732                "the required pinned corpus is missing at {}",
3733                corpus.display()
3734            );
3735            return;
3736        }
3737        for entry in std::fs::read_dir(&corpus).unwrap() {
3738            let path = entry.unwrap().path();
3739            if path.extension().and_then(|extension| extension.to_str()) != Some("pptx") {
3740                continue;
3741            }
3742            let package = OpcPackage::open(&path).unwrap();
3743            let Some(xml) = package.get_part("/ppt/tableStyles.xml") else {
3744                continue;
3745            };
3746            let styles = CT_TableStyleList::from_xml(xml)
3747                .unwrap_or_else(|error| panic!("{}: {error}", path.display()));
3748            let written = styles.to_xml().unwrap();
3749            assert_eq!(
3750                styles,
3751                CT_TableStyleList::from_xml(&written).unwrap(),
3752                "{}",
3753                path.display()
3754            );
3755        }
3756    }
3757
3758    #[test]
3759    fn table_properties_preserve_style_and_banding_flags() {
3760        let xml = br#"<a:tbl xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"><a:tblPr rtl="1" firstRow="true" firstCol="1" lastRow="true" lastCol="1" bandRow="true" bandCol="1"><a:tableStyleId>{5940675A-B579-460E-94D1-54222C63F5DA}</a:tableStyleId></a:tblPr><a:tblGrid><a:gridCol w="1"/></a:tblGrid><a:tr h="2"><a:tc/></a:tr></a:tbl>"#;
3761        let table = CT_Table::from_xml(xml).unwrap();
3762        let properties = table.properties.as_ref().unwrap();
3763        assert!(properties.right_to_left);
3764        assert!(properties.first_row && properties.first_column);
3765        assert!(properties.last_row && properties.last_column);
3766        assert!(properties.band_rows && properties.band_columns);
3767        assert_eq!(
3768            properties.style_id.as_deref(),
3769            Some("{5940675A-B579-460E-94D1-54222C63F5DA}")
3770        );
3771        assert_eq!(table, CT_Table::from_xml(&table.to_xml().unwrap()).unwrap());
3772    }
3773
3774    #[test]
3775    fn table_reader_is_prefix_tolerant_and_writer_uses_schema_order() {
3776        let xml = br#"<q:tbl xmlns:q="http://schemas.openxmlformats.org/drawingml/2006/main"><q:tblPr firstRow="1"/><q:tblGrid><q:gridCol w="100"/></q:tblGrid><q:tr h="200"><q:tc><q:txBody><q:bodyPr/><q:p/></q:txBody><q:tcPr/></q:tc></q:tr></q:tbl>"#;
3777        let table = CT_Table::from_xml(xml).unwrap();
3778        let written = table.to_xml().unwrap();
3779        let text = std::str::from_utf8(&written).unwrap();
3780        assert!(text.starts_with("<a:tbl xmlns:a="));
3781        assert_order(
3782            text,
3783            &[
3784                "<a:tblPr",
3785                "<a:tblGrid",
3786                "<a:tr",
3787                "<a:tc",
3788                "<a:txBody",
3789                "<a:tcPr",
3790            ],
3791        );
3792        let reparsed = CT_Table::from_xml(&written).unwrap();
3793        assert_eq!(table.grid, reparsed.grid);
3794        assert_eq!(table.rows[0], reparsed.rows[0]);
3795        assert_eq!(table, reparsed);
3796    }
3797
3798    #[test]
3799    fn unmodelled_table_and_cell_content_is_preserved_in_place() {
3800        let xml = br#"<q:tbl xmlns:q="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:x="urn:producer" x:table="kept"><x:before/><x:opaque xmlns:a="urn:producer"><a:data/></x:opaque><q:tblPr x:properties="kept"><x:fill/><q:tableStyleId>style</q:tableStyleId><x:effect/></q:tblPr><x:between/><q:tblGrid x:grid="kept"><x:grid-before/><q:gridCol w="100" x:column="kept"><x:column-child/></q:gridCol><x:grid-after/></q:tblGrid><x:before-row/><q:tr h="200" x:row="kept"><x:before-cell/><q:tc x:cell="kept"><x:before-text/><x:cell-opaque xmlns:a="urn:producer"><a:data/></x:cell-opaque><q:txBody><q:bodyPr/><q:p/></q:txBody><x:before-properties/><q:tcPr x:style="kept"><x:border/></q:tcPr><x:after-properties/></q:tc><x:after-cell/></q:tr><x:after/></q:tbl>"#;
3801        let table = CT_Table::from_xml(xml).unwrap();
3802        let written = table.to_xml().unwrap();
3803        let text = std::str::from_utf8(&written).unwrap();
3804        for raw in [
3805            r#"<x:before/>"#,
3806            r#"<x:opaque xmlns:a="urn:producer"><a:data/></x:opaque>"#,
3807            r#"<x:fill/>"#,
3808            r#"<x:effect/>"#,
3809            r#"<x:column-child/>"#,
3810            r#"<a:tcPr x:style="kept"><x:border/></a:tcPr>"#,
3811            r#"<x:cell-opaque xmlns:a="urn:producer"><a:data/></x:cell-opaque>"#,
3812            r#"<x:after-properties/>"#,
3813            r#"<x:after/>"#,
3814        ] {
3815            assert!(text.contains(raw), "missing {raw}");
3816        }
3817        assert_order(
3818            text,
3819            &[
3820                "<x:before-text",
3821                "<a:txBody",
3822                "<x:before-properties",
3823                "<a:tcPr",
3824                "<x:after-properties",
3825            ],
3826        );
3827        assert_eq!(table, CT_Table::from_xml(&written).unwrap());
3828    }
3829
3830    #[test]
3831    fn collection_edits_keep_surviving_metadata_and_trailing_raw_children() {
3832        let xml = br#"<a:tbl xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:x="urn:producer"><a:tblGrid><a:gridCol w="100" x:id="first"><x:first-child/></a:gridCol><a:gridCol w="200" x:id="second"><x:second-child/></a:gridCol><x:grid-tail/></a:tblGrid><a:tr h="300"><a:tc/><x:between-cells/><a:tc/><x:row-tail/></a:tr><x:between-rows/><a:tr h="400"><a:tc/></a:tr><x:table-tail/></a:tbl>"#;
3833        let mut table = CT_Table::from_xml(xml).unwrap();
3834        table.grid.columns.swap(0, 1);
3835        table.grid.columns.remove(1);
3836        table.rows[0].cells.remove(0);
3837        table.rows[0].cells[0].grid_span = 2;
3838        table.rows.remove(1);
3839
3840        let written = table.to_xml().unwrap();
3841        let text = std::str::from_utf8(&written).unwrap();
3842        assert!(text.contains(r#"<a:gridCol w="200" x:id="second"><x:second-child/></a:gridCol>"#));
3843        assert!(!text.contains(r#"x:id="first""#));
3844        for raw in [
3845            "<x:grid-tail/>",
3846            "<x:between-cells/>",
3847            "<x:row-tail/>",
3848            "<x:between-rows/>",
3849            "<x:table-tail/>",
3850        ] {
3851            assert!(text.contains(raw), "missing {raw}");
3852        }
3853        assert!(text.contains(
3854            r#"<a:tr h="300"><x:between-cells/><a:tc gridSpan="2"/><x:row-tail/></a:tr>"#
3855        ));
3856        let reparsed = CT_Table::from_xml(&written).unwrap();
3857        assert_eq!(table.grid, reparsed.grid);
3858        assert_eq!(table.rows[0], reparsed.rows[0]);
3859        assert_eq!(table, reparsed);
3860
3861        let mut width_edit = CT_Table::from_xml(xml).unwrap();
3862        width_edit.grid.columns[1] = Emu(250);
3863        let width_edit_xml = width_edit.to_xml().unwrap();
3864        assert!(
3865            std::str::from_utf8(&width_edit_xml)
3866                .unwrap()
3867                .contains(r#"<a:gridCol w="250" x:id="second"><x:second-child/></a:gridCol>"#)
3868        );
3869        assert_eq!(width_edit, CT_Table::from_xml(&width_edit_xml).unwrap());
3870    }
3871
3872    #[test]
3873    fn ambiguous_grid_edits_return_a_typed_serialization_error() {
3874        let xml = br#"<a:tbl xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:x="urn:producer"><a:tblGrid><a:gridCol w="100" x:id="first"/><a:gridCol w="200" x:id="second"/></a:tblGrid><a:tr h="300"><a:tc/></a:tr></a:tbl>"#;
3875
3876        let mut collision = CT_Table::from_xml(xml).unwrap();
3877        collision.grid.columns[0] = Emu(200);
3878        assert_ambiguous_grid_error(collision.to_xml().unwrap_err());
3879
3880        let mut combined_reorder_and_edit = CT_Table::from_xml(xml).unwrap();
3881        combined_reorder_and_edit.grid.columns.swap(0, 1);
3882        combined_reorder_and_edit.grid.columns[0] = Emu(100);
3883        assert_ambiguous_grid_error(combined_reorder_and_edit.to_xml().unwrap_err());
3884
3885        let mut duplicate_insertion = CT_Table::from_xml(xml).unwrap();
3886        duplicate_insertion.grid.columns.insert(0, Emu(100));
3887        assert_ambiguous_grid_error(duplicate_insertion.to_xml().unwrap_err());
3888
3889        let mut delete_and_edit = CT_Table::from_xml(xml).unwrap();
3890        delete_and_edit.grid.columns.remove(1);
3891        delete_and_edit.grid.columns[0] = Emu(150);
3892        assert_ambiguous_grid_error(delete_and_edit.to_xml().unwrap_err());
3893
3894        let mut insert_and_edit = CT_Table::from_xml(xml).unwrap();
3895        insert_and_edit.grid.columns[0] = Emu(150);
3896        insert_and_edit.grid.columns.insert(1, Emu(300));
3897        assert_ambiguous_grid_error(insert_and_edit.to_xml().unwrap_err());
3898    }
3899
3900    #[test]
3901    fn column_width_edits_keep_metadata_when_columns_share_a_width() {
3902        let xml = br#"<a:tbl xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:x="urn:producer"><a:tblGrid><a:gridCol w="100"><x:id val="1"/></a:gridCol><x:before-second/><a:gridCol w="100"><x:id val="2"/></a:gridCol><a:gridCol w="100"><x:id val="3"/></a:gridCol><x:grid-tail/></a:tblGrid><a:tr h="300"><a:tc/><a:tc/><a:tc/></a:tr></a:tbl>"#;
3903        let mut table = CT_Table::from_xml(xml).unwrap();
3904        table.set_column_width(1, Emu(250)).unwrap();
3905        table.set_column_width(2, Emu(100)).unwrap();
3906        table.set_column_width(0, Emu(250)).unwrap();
3907        assert!(table.set_column_width(3, Emu(1)).is_err());
3908
3909        let written = table.to_xml().unwrap();
3910        assert!(
3911            std::str::from_utf8(&written).unwrap().contains(
3912                r#"<a:tblGrid><a:gridCol w="250"><x:id val="1"/></a:gridCol><x:before-second/><a:gridCol w="250"><x:id val="2"/></a:gridCol><a:gridCol w="100"><x:id val="3"/></a:gridCol><x:grid-tail/></a:tblGrid>"#
3913            )
3914        );
3915        assert_eq!(table, CT_Table::from_xml(&written).unwrap());
3916    }
3917
3918    const PYTHON_PPTX_TABLE: &[u8] = br#"<a:tbl xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"><a:tblPr firstRow="1" bandRow="1"><a:tableStyleId>{5C22544A-7EE6-4342-B048-85BDC9FD1C3A}</a:tableStyleId></a:tblPr><a:tblGrid><a:gridCol w="2057400"/><a:gridCol w="1828800"/></a:tblGrid><a:tr h="548640"><a:tc><a:txBody><a:bodyPr/><a:lstStyle/><a:p/></a:txBody><a:tcPr><a:solidFill><a:srgbClr val="3E4A59"/></a:solidFill></a:tcPr></a:tc><a:tc><a:txBody><a:bodyPr/><a:lstStyle/><a:p><a:r><a:rPr sz="1500"/><a:t>A. Repair</a:t></a:r></a:p></a:txBody><a:tcPr><a:solidFill><a:srgbClr val="3E4A59"/></a:solidFill></a:tcPr></a:tc></a:tr><a:tr h="457200"><a:tc><a:txBody><a:bodyPr anchor="ctr"/><a:lstStyle/><a:p><a:pPr algn="r"/><a:r><a:rPr sz="1500" b="1"><a:hlinkClick r:id="rId9"/></a:rPr><a:t>Cost</a:t></a:r><a:r><a:rPr sz="900"/><a:t> (EUR)</a:t></a:r></a:p><a:p><a:r><a:t>second</a:t></a:r></a:p></a:txBody><a:tcPr marL="0"/></a:tc><a:tc><a:txBody><a:bodyPr/><a:lstStyle/><a:p><a:endParaRPr sz="1100"/></a:p></a:txBody><a:tcPr/></a:tc></a:tr></a:tbl>"#;
3919
3920    #[test]
3921    fn inserted_rows_and_columns_copy_neighbour_formatting_without_text() {
3922        let mut table = CT_Table::from_xml(PYTHON_PPTX_TABLE).unwrap();
3923        table.insert_row(2).unwrap();
3924        table.insert_row(0).unwrap();
3925        table.insert_column(2).unwrap();
3926        assert_eq!(
3927            table.rows.iter().map(|row| row.height).collect::<Vec<_>>(),
3928            [Emu(548_640), Emu(548_640), Emu(457_200), Emu(457_200)]
3929        );
3930        assert_eq!(
3931            table.grid.columns,
3932            [Emu(2_057_400), Emu(1_828_800), Emu(1_828_800)]
3933        );
3934        assert!(table.rows.iter().all(|row| row.cells.len() == 3));
3935
3936        let written = String::from_utf8(table.to_xml().unwrap()).unwrap();
3937        let rows = written.split("<a:tr ").skip(1).collect::<Vec<_>>();
3938        let header = r#"<a:tcPr><a:solidFill><a:srgbClr val="3E4A59"/></a:solidFill></a:tcPr>"#;
3939        assert_eq!(
3940            rows[0],
3941            format!(
3942                r#"h="548640"><a:tc><a:txBody><a:bodyPr/><a:lstStyle/><a:p/></a:txBody>{header}</a:tc><a:tc><a:txBody><a:bodyPr/><a:lstStyle/><a:p><a:endParaRPr sz="1500"/></a:p></a:txBody>{header}</a:tc><a:tc><a:txBody><a:bodyPr/><a:lstStyle/><a:p><a:endParaRPr sz="1500"/></a:p></a:txBody>{header}</a:tc></a:tr>"#
3943            )
3944        );
3945        assert!(rows[1].contains("A. Repair"));
3946        assert!(rows[2].contains("Cost") && rows[2].contains("second"));
3947        assert_eq!(
3948            rows[3],
3949            r#"h="457200"><a:tc><a:txBody><a:bodyPr anchor="ctr"/><a:lstStyle/><a:p><a:pPr algn="r"/><a:endParaRPr sz="1500" b="1"/></a:p></a:txBody><a:tcPr marL="0"/></a:tc><a:tc><a:txBody><a:bodyPr/><a:lstStyle/><a:p><a:endParaRPr sz="1100"/></a:p></a:txBody><a:tcPr/></a:tc><a:tc><a:txBody><a:bodyPr/><a:lstStyle/><a:p><a:endParaRPr sz="1100"/></a:p></a:txBody><a:tcPr/></a:tc></a:tr></a:tbl>"#
3950        );
3951        assert!(written.contains(
3952            r#"<a:tableStyleId>{5C22544A-7EE6-4342-B048-85BDC9FD1C3A}</a:tableStyleId>"#
3953        ));
3954        assert_eq!(written.matches("r:id=").count(), 1);
3955        assert_eq!(table, CT_Table::from_xml(written.as_bytes()).unwrap());
3956
3957        table.remove_row(3).unwrap();
3958        table.remove_row(0).unwrap();
3959        table.remove_column(2).unwrap();
3960        assert_eq!(
3961            table.to_xml().unwrap(),
3962            CT_Table::from_xml(PYTHON_PPTX_TABLE)
3963                .unwrap()
3964                .to_xml()
3965                .unwrap()
3966        );
3967    }
3968
3969    #[test]
3970    fn row_and_column_edits_refuse_what_would_break_the_grid() {
3971        let mut table = CT_Table::new(1, 1, Emu(100), Emu(100)).unwrap();
3972        let before = table.clone();
3973        for result in [
3974            table.remove_row(0),
3975            table.remove_column(0),
3976            table.insert_row(2),
3977            table.insert_column(2),
3978            table.remove_row(1),
3979            table.remove_column(1),
3980        ] {
3981            assert!(matches!(result, Err(OxmlError::InvalidValue(_))));
3982        }
3983        assert_eq!(table, before);
3984
3985        let mut emptied = CT_Table::new(1, 1, Emu(100), Emu(100)).unwrap();
3986        emptied.rows.clear();
3987        assert!(matches!(
3988            emptied.insert_row(0),
3989            Err(OxmlError::MissingElement(_))
3990        ));
3991        let mut emptied = CT_Table::new(1, 1, Emu(100), Emu(100)).unwrap();
3992        emptied.grid.columns.clear();
3993        emptied.rows[0].cells.clear();
3994        assert!(matches!(
3995            emptied.insert_column(0),
3996            Err(OxmlError::MissingElement(_))
3997        ));
3998
3999        let ragged = br#"<a:tbl xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"><a:tblGrid><a:gridCol w="100"/><a:gridCol w="100"/></a:tblGrid><a:tr h="100"><a:tc/></a:tr></a:tbl>"#;
4000        let overflowing = br#"<a:tbl xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"><a:tblGrid><a:gridCol w="100"/></a:tblGrid><a:tr h="100"><a:tc rowSpan="2"/></a:tr><a:tr h="100"><a:tc vMerge="1"/></a:tr><a:tr h="100"><a:tc gridSpan="2"/></a:tr></a:tbl>"#;
4001        for xml in [&ragged[..], &overflowing[..]] {
4002            let mut table = CT_Table::from_xml(xml).unwrap();
4003            assert!(table.insert_row(1).is_err());
4004            assert!(table.remove_column(0).is_err());
4005            assert_eq!(table, CT_Table::from_xml(xml).unwrap());
4006        }
4007    }
4008
4009    /// Draws each cell as its origin spans, `h` and `v` continuation flags,
4010    /// or `.` for an unmerged cell, row by row.
4011    fn merge_pattern(table: &CT_Table) -> Vec<String> {
4012        table
4013            .rows
4014            .iter()
4015            .map(|row| {
4016                row.cells
4017                    .iter()
4018                    .map(|cell| {
4019                        let flags = match (cell.horizontal_merge, cell.vertical_merge) {
4020                            (true, true) => "hv",
4021                            (true, false) => "h",
4022                            (false, true) => "v",
4023                            (false, false) if cell.row_span == 1 && cell.grid_span == 1 => ".",
4024                            (false, false) => "o",
4025                        };
4026                        format!("{flags}{}x{}", cell.row_span, cell.grid_span)
4027                    })
4028                    .collect::<Vec<_>>()
4029                    .join(" ")
4030            })
4031            .collect()
4032    }
4033
4034    fn merged_three_by_three() -> CT_Table {
4035        let mut table = CT_Table::new(3, 3, Emu(300), Emu(300)).unwrap();
4036        for (row, column, row_span, grid_span, horizontal, vertical) in [
4037            (0, 0, 2, 2, false, false),
4038            (0, 1, 2, 1, true, false),
4039            (1, 0, 1, 2, false, true),
4040            (1, 1, 1, 1, true, true),
4041        ] {
4042            let cell = &mut table.rows[row].cells[column];
4043            cell.row_span = row_span;
4044            cell.grid_span = grid_span;
4045            cell.horizontal_merge = horizontal;
4046            cell.vertical_merge = vertical;
4047            cell.text_body
4048                .as_mut()
4049                .unwrap()
4050                .set_text(&format!("{row}{column}"));
4051        }
4052        table.rows[0].cells[0]
4053            .properties
4054            .as_mut()
4055            .unwrap()
4056            .margin_left = Some(Emu(7));
4057        table
4058    }
4059
4060    #[test]
4061    fn row_and_column_edits_grow_shrink_and_move_merged_cells() {
4062        assert_eq!(
4063            merge_pattern(&merged_three_by_three()),
4064            ["o2x2 h2x1 .1x1", "v1x2 hv1x1 .1x1", ".1x1 .1x1 .1x1"]
4065        );
4066
4067        let mut inside = merged_three_by_three();
4068        inside.insert_row(1).unwrap();
4069        assert_eq!(
4070            merge_pattern(&inside),
4071            [
4072                "o3x2 h3x1 .1x1",
4073                "v1x2 hv1x1 .1x1",
4074                "v1x2 hv1x1 .1x1",
4075                ".1x1 .1x1 .1x1"
4076            ]
4077        );
4078        inside.insert_column(1).unwrap();
4079        assert_eq!(
4080            merge_pattern(&inside),
4081            [
4082                "o3x3 h3x1 h3x1 .1x1",
4083                "v1x3 hv1x1 hv1x1 .1x1",
4084                "v1x3 hv1x1 hv1x1 .1x1",
4085                ".1x1 .1x1 .1x1 .1x1"
4086            ]
4087        );
4088        assert_eq!(
4089            inside,
4090            CT_Table::from_xml(&inside.to_xml().unwrap()).unwrap()
4091        );
4092
4093        for index in [0, 2] {
4094            let mut outside = merged_three_by_three();
4095            outside.insert_row(index).unwrap();
4096            outside.insert_column(index).unwrap();
4097            let pattern = merge_pattern(&outside);
4098            assert_eq!(pattern.len(), 4);
4099            let without_new = pattern
4100                .iter()
4101                .enumerate()
4102                .filter(|(row, _)| *row != index)
4103                .map(|(_, row)| {
4104                    row.split(' ')
4105                        .enumerate()
4106                        .filter(|(column, _)| *column != index)
4107                        .map(|(_, cell)| cell)
4108                        .collect::<Vec<_>>()
4109                        .join(" ")
4110                })
4111                .collect::<Vec<_>>();
4112            assert_eq!(without_new, merge_pattern(&merged_three_by_three()));
4113            assert!(pattern[index].split(' ').all(|cell| cell == ".1x1"));
4114            assert!(
4115                pattern
4116                    .iter()
4117                    .all(|row| row.split(' ').nth(index) == Some(".1x1"))
4118            );
4119        }
4120
4121        let mut top_removed = merged_three_by_three();
4122        top_removed.remove_row(0).unwrap();
4123        assert_eq!(
4124            merge_pattern(&top_removed),
4125            ["o1x2 h1x1 .1x1", ".1x1 .1x1 .1x1"]
4126        );
4127        let origin = &top_removed.rows[0].cells[0];
4128        assert_eq!(origin.text_body.as_ref().unwrap().plain_text(), "00");
4129        assert_eq!(
4130            origin.properties.as_ref().unwrap().margin_left,
4131            Some(Emu(7))
4132        );
4133
4134        let mut covered_removed = merged_three_by_three();
4135        covered_removed.remove_row(1).unwrap();
4136        assert_eq!(
4137            merge_pattern(&covered_removed),
4138            ["o1x2 h1x1 .1x1", ".1x1 .1x1 .1x1"]
4139        );
4140
4141        let mut left_removed = merged_three_by_three();
4142        left_removed.remove_column(0).unwrap();
4143        assert_eq!(
4144            merge_pattern(&left_removed),
4145            ["o2x1 .1x1", "v1x1 .1x1", ".1x1 .1x1"]
4146        );
4147        let origin = &left_removed.rows[0].cells[0];
4148        assert_eq!(origin.text_body.as_ref().unwrap().plain_text(), "00");
4149        assert_eq!(
4150            origin.properties.as_ref().unwrap().margin_left,
4151            Some(Emu(7))
4152        );
4153        assert_eq!(left_removed.grid.columns, [Emu(100), Emu(100)]);
4154
4155        let mut collapsed = merged_three_by_three();
4156        collapsed.remove_row(1).unwrap();
4157        collapsed.remove_column(1).unwrap();
4158        assert_eq!(merge_pattern(&collapsed), [".1x1 .1x1", ".1x1 .1x1"]);
4159    }
4160
4161    #[test]
4162    fn row_and_column_edits_keep_unmodelled_row_and_grid_content_in_place() {
4163        let xml = br#"<a:tbl xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:x="urn:producer"><a:tblGrid><a:gridCol w="100"><x:id val="1"/></a:gridCol><x:before-second/><a:gridCol w="100"><x:id val="2"/></a:gridCol><x:grid-tail/></a:tblGrid><a:tr h="300"><a:tc/><a:tc x:cell="kept"/><x:row-id val="1"/></a:tr><x:between-rows/><a:tr h="400"><a:tc/><a:tc/><x:row-id val="2"/></a:tr><x:table-tail/></a:tbl>"#;
4164        let mut table = CT_Table::from_xml(xml).unwrap();
4165        table.insert_row(1).unwrap();
4166        table.insert_column(1).unwrap();
4167        table.set_column_width(1, Emu(150)).unwrap();
4168        let written = String::from_utf8(table.to_xml().unwrap()).unwrap();
4169        assert!(written.contains(
4170            r#"<a:tblGrid><a:gridCol w="100"><x:id val="1"/></a:gridCol><a:gridCol w="150"/><x:before-second/><a:gridCol w="100"><x:id val="2"/></a:gridCol><x:grid-tail/></a:tblGrid>"#
4171        ), "{written}");
4172        assert!(written.contains(
4173            r#"<a:tr h="300"><a:tc/><a:tc><a:txBody><a:bodyPr/><a:p/></a:txBody></a:tc><a:tc x:cell="kept"/><x:row-id val="1"/></a:tr><a:tr h="300"><a:tc><a:txBody><a:bodyPr/><a:p/></a:txBody></a:tc><a:tc><a:txBody><a:bodyPr/><a:p/></a:txBody></a:tc><a:tc><a:txBody><a:bodyPr/><a:p/></a:txBody></a:tc></a:tr><x:between-rows/><a:tr h="400">"#
4174        ), "{written}");
4175        assert!(written.ends_with(r#"<x:row-id val="2"/></a:tr><x:table-tail/></a:tbl>"#));
4176        assert_eq!(table, CT_Table::from_xml(written.as_bytes()).unwrap());
4177
4178        table.remove_column(0).unwrap();
4179        table.remove_row(0).unwrap();
4180        let written = String::from_utf8(table.to_xml().unwrap()).unwrap();
4181        assert!(written.contains(
4182            r#"<a:tblGrid><a:gridCol w="150"/><x:before-second/><a:gridCol w="100"><x:id val="2"/></a:gridCol><x:grid-tail/></a:tblGrid>"#
4183        ), "{written}");
4184        assert!(!written.contains(r#"<x:row-id val="1"/>"#));
4185        assert!(written.ends_with(r#"<x:row-id val="2"/></a:tr><x:table-tail/></a:tbl>"#));
4186        assert_eq!(table, CT_Table::from_xml(written.as_bytes()).unwrap());
4187    }
4188
4189    #[test]
4190    fn inherited_namespaces_make_extracted_table_output_self_contained() {
4191        let xml = br#"<q:tbl><q:tblGrid><q:gridCol w="100"/></q:tblGrid><q:tr h="200"><q:tc><x:extension/></q:tc></q:tr></q:tbl>"#;
4192        let inherited = vec![
4193            ("q".to_owned(), A_NS.to_owned()),
4194            ("x".to_owned(), "urn:producer".to_owned()),
4195        ];
4196        let table = CT_Table::from_xml_with_inherited_namespaces(xml, &inherited).unwrap();
4197        let written = table.to_xml().unwrap();
4198        let text = std::str::from_utf8(&written).unwrap();
4199        assert!(text.starts_with(&format!(r#"<a:tbl xmlns:a="{A_NS}""#)));
4200        assert!(text.contains(r#"xmlns:x="urn:producer""#));
4201        assert!(text.contains("<x:extension/>"));
4202        assert_eq!(table, CT_Table::from_xml(&written).unwrap());
4203
4204        let locally_shadowed = br#"<q:tbl xmlns:q="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"><q:tblGrid><q:gridCol w="100"/></q:tblGrid><q:tr h="200"><q:tc/></q:tr></q:tbl>"#;
4205        let producer_a = vec![("a".to_owned(), "urn:producer".to_owned())];
4206        assert!(
4207            CT_Table::from_xml_with_inherited_namespaces(locally_shadowed, &producer_a).is_ok()
4208        );
4209    }
4210
4211    #[test]
4212    fn inherited_namespace_prefixes_accept_xml_ncname_unicode() {
4213        let xml = r#"<q:tbl><q:tblGrid><q:gridCol w="100"/></q:tblGrid><q:tr h="200"><q:tc><é:extension/></q:tc></q:tr></q:tbl>"#;
4214        let inherited = vec![
4215            ("q".to_owned(), A_NS.to_owned()),
4216            ("é".to_owned(), "urn:producer".to_owned()),
4217        ];
4218        let table =
4219            CT_Table::from_xml_with_inherited_namespaces(xml.as_bytes(), &inherited).unwrap();
4220        let written = table.to_xml().unwrap();
4221        let text = std::str::from_utf8(&written).unwrap();
4222        assert!(text.contains(r#"xmlns:é="urn:producer""#));
4223        assert!(text.contains("<é:extension/>"));
4224        assert_eq!(table, CT_Table::from_xml(&written).unwrap());
4225    }
4226
4227    #[test]
4228    fn optional_child_edits_round_trip_as_equal_models() {
4229        let xml = br#"<a:tbl xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:x="urn:producer"><a:tblPr><x:before-style/><a:tableStyleId>style</a:tableStyleId><x:after-style/></a:tblPr><x:between-properties-grid/><a:tblGrid><a:gridCol w="100"/></a:tblGrid><a:tr h="200"><a:tc><x:before-text/><a:txBody><a:bodyPr/><a:p/></a:txBody><x:between-text-properties/><a:tcPr/></a:tc></a:tr></a:tbl>"#;
4230
4231        let mut no_properties = CT_Table::from_xml(xml).unwrap();
4232        no_properties.properties = None;
4233        let written = no_properties.to_xml().unwrap();
4234        assert_eq!(no_properties, CT_Table::from_xml(&written).unwrap());
4235
4236        let mut no_style = CT_Table::from_xml(xml).unwrap();
4237        no_style.properties.as_mut().unwrap().style_id = None;
4238        let written = no_style.to_xml().unwrap();
4239        let reparsed = CT_Table::from_xml(&written).unwrap();
4240        assert_eq!(
4241            no_style.properties.as_ref().unwrap(),
4242            reparsed.properties.as_ref().unwrap()
4243        );
4244        assert_eq!(no_style, reparsed);
4245
4246        let mut no_text = CT_Table::from_xml(xml).unwrap();
4247        no_text.rows[0].cells[0].text_body = None;
4248        let written = no_text.to_xml().unwrap();
4249        let reparsed = CT_Table::from_xml(&written).unwrap();
4250        assert_eq!(no_text.rows[0].cells[0], reparsed.rows[0].cells[0]);
4251        assert_eq!(no_text, reparsed);
4252    }
4253
4254    #[test]
4255    fn builtin_table_styles_resolve_all_74_powerpoint_ids_and_round_trip() {
4256        let mut list = CT_TableStyleList::from_xml(
4257            format!(r#"<a:tblStyleLst xmlns:a="{A_NS}" def="{{5C22544A-7EE6-4342-B048-85BDC9FD1C3A}}"/>"#)
4258                .as_bytes(),
4259        )
4260        .unwrap();
4261        for (style_id, ..) in builtin_table_styles() {
4262            let style = CT_TableStyle::builtin(style_id).unwrap();
4263            assert_eq!(style.style_id, style_id);
4264            assert!(style.whole_table.is_some(), "{style_id}");
4265            list.styles.push(style);
4266        }
4267        let names = list
4268            .styles
4269            .iter()
4270            .map(|style| style.style_name.as_str())
4271            .collect::<HashSet<_>>();
4272        assert_eq!(names.len(), 74);
4273        for name in [
4274            "No Style, No Grid",
4275            "Themed Style 2 - Accent 6",
4276            "Medium Style 2 - Accent 1",
4277            "Dark Style 2",
4278            "Dark Style 2 - Accent 5/Accent 6",
4279        ] {
4280            assert!(names.contains(name), "{name}");
4281        }
4282        assert_eq!(
4283            CT_TableStyleList::from_xml(&list.to_xml().unwrap()).unwrap(),
4284            list
4285        );
4286
4287        // ST_Guid is upper case in braces, and PowerPoint refuses a deck otherwise.
4288        for unknown in [
4289            "{5c22544a-7ee6-4342-b048-85bdc9fd1c3a}",
4290            "5C22544A-7EE6-4342-B048-85BDC9FD1C3A",
4291            "{00000000-0000-0000-0000-000000000000}",
4292        ] {
4293            assert!(CT_TableStyle::builtin(unknown).is_none(), "{unknown}");
4294        }
4295    }
4296
4297    #[test]
4298    fn builtin_table_style_families_place_their_variant_colour() {
4299        // Medium Style 2 - Accent 1, python-pptx's default: a white grid on accent tints.
4300        assert_regions(
4301            "{5C22544A-7EE6-4342-B048-85BDC9FD1C3A}",
4302            &[
4303                "wholeTbl text dk1",
4304                "wholeTbl fill accent1 tint 20%",
4305                "wholeTbl insideH 12700 lt1",
4306                "wholeTbl insideV 12700 lt1",
4307                "band1H fill accent1 tint 40%",
4308                "band1V fill accent1 tint 40%",
4309                "firstCol text bold lt1",
4310                "firstCol fill accent1",
4311                "lastCol fill accent1",
4312                "lastRow top 38100 lt1",
4313                "lastRow fill accent1",
4314                "firstRow text bold lt1",
4315                "firstRow bottom 38100 lt1",
4316                "firstRow fill accent1",
4317            ],
4318        );
4319        // A plain variant places dk1 where an accent variant places its accent.
4320        assert_regions(
4321            "{073A0DAA-6AF3-43AB-8588-CEC1D06C72B9}",
4322            &["wholeTbl fill dk1 tint 20%", "firstRow fill dk1"],
4323        );
4324        // Light Style 1 - Accent 2: accent rules and translucent bands under black text.
4325        assert_regions(
4326            "{0E3FDE45-AF77-4B5C-9715-49D594BDF05E}",
4327            &[
4328                "wholeTbl text tx1",
4329                "wholeTbl left none",
4330                "wholeTbl top 12700 accent2",
4331                "band1H fill accent2 alpha 20%",
4332                "firstRow text bold",
4333                "firstRow bottom 12700 accent2",
4334                "lastRow top 12700 accent2",
4335            ],
4336        );
4337        // Light Style 2 - Accent 1 fills its heading from the theme's first fill style.
4338        assert_regions(
4339            "{69012ECD-51FC-41F1-AA8D-1B2483CD663E}",
4340            &["firstRow text bold bg1", "firstRow fill ref 1 accent1"],
4341        );
4342        // Medium Style 3 - Accent 5 places its accent in the heading fills only.
4343        assert_regions(
4344            "{74C1A8A3-306A-4EB7-A6B1-4F7E0EB9C5D6}",
4345            &[
4346                "wholeTbl top 25400 dk1",
4347                "band1H fill dk1 tint 20%",
4348                "firstCol fill accent5",
4349                "firstRow bottom 25400 dk1",
4350                "firstRow fill accent5",
4351                "seCell text bold dk1",
4352            ],
4353        );
4354        // Medium Style 4 - Accent 6: an accent grid over accent tints.
4355        assert_regions(
4356            "{16D9F66E-5EB9-4882-86FB-DCBF35E3C3E4}",
4357            &[
4358                "wholeTbl insideV 12700 accent6",
4359                "wholeTbl fill accent6 tint 20%",
4360                "band1V fill accent6 tint 40%",
4361                "lastRow top 25400 accent6",
4362                "firstRow text bold",
4363            ],
4364        );
4365        // Dark Style 1 tints black when plain and shades the accent otherwise.
4366        assert_regions(
4367            "{E8034E78-7F5D-4C2E-B375-FC64B27BC917}",
4368            &[
4369                "wholeTbl text lt1",
4370                "wholeTbl fill dk1 tint 20%",
4371                "band1H fill dk1 tint 40%",
4372                "firstCol fill dk1 tint 60%",
4373                "lastRow fill dk1 tint 60%",
4374                "firstRow fill dk1",
4375            ],
4376        );
4377        assert_regions(
4378            "{D03447BB-5D67-496B-8E87-E561075AD55C}",
4379            &[
4380                "wholeTbl fill accent3",
4381                "band1H fill accent3 shade 60%",
4382                "firstCol right 25400 lt1",
4383                "lastRow fill accent3 shade 40%",
4384                "firstRow fill dk1",
4385                "nwCell right none",
4386            ],
4387        );
4388        // Dark Style 2 - Accent 3/Accent 4 heads a body of the first accent with the second.
4389        assert_regions(
4390            "{91EBBBCC-DAD2-459C-BE2E-F6DE35CF9A28}",
4391            &[
4392                "wholeTbl text dk1",
4393                "wholeTbl fill accent3 tint 20%",
4394                "band1H fill accent3 tint 40%",
4395                "lastRow top 50800 dk1",
4396                "firstRow text bold lt1",
4397                "firstRow fill accent4",
4398            ],
4399        );
4400        // Themed styles band with translucent colour over their table background.
4401        assert_regions(
4402            "{08FB837D-C827-4EFA-A057-4D05807E0F7C}",
4403            &[
4404                "band1H fill accent6 alpha 40%",
4405                "firstRow text bold lt1",
4406                "firstRow fill accent6",
4407            ],
4408        );
4409        for (style_id, index) in [
4410            ("{08FB837D-C827-4EFA-A057-4D05807E0F7C}", 2),
4411            ("{306799F8-075E-4A3A-A7F6-7FBC6576F1A4}", 3),
4412        ] {
4413            let style = CT_TableStyle::builtin(style_id).unwrap();
4414            let background = style.background.unwrap();
4415            let Some(StyleReference::Fill(reference)) = background.fill_reference else {
4416                panic!("{style_id}: expected a background fill reference");
4417            };
4418            assert_eq!(reference.index, index);
4419        }
4420        assert_regions(
4421            "{306799F8-075E-4A3A-A7F6-7FBC6576F1A4}",
4422            &[
4423                "wholeTbl text lt1",
4424                "band1H fill lt1 alpha 20%",
4425                "firstRow text bold",
4426            ],
4427        );
4428    }
4429
4430    #[test]
4431    fn table_background_models_its_fill_and_keeps_its_effect_in_place() {
4432        let xml = format!(
4433            r#"<q:tblStyleLst xmlns:q="{A_NS}" xmlns:x="urn:producer" def="s"><q:tblStyle styleId="s" styleName="S"><x:first/><q:tblBg><q:fillRef idx="2"><q:schemeClr val="accent1"/></q:fillRef><x:between/><q:effectRef idx="1"><q:schemeClr val="accent1"/></q:effectRef></q:tblBg><q:wholeTbl><q:tcStyle/></q:wholeTbl></q:tblStyle></q:tblStyleLst>"#
4434        );
4435        let styles = CT_TableStyleList::from_xml(xml.as_bytes()).unwrap();
4436        let background = styles.styles[0].background.as_ref().unwrap();
4437        let Some(StyleReference::Fill(reference)) = &background.fill_reference else {
4438            panic!("expected a fill reference");
4439        };
4440        assert_eq!(reference.index, 2);
4441        assert!(background.fill.is_none());
4442        assert_eq!(background.unsupported, ["effect"]);
4443
4444        let written = String::from_utf8(styles.to_xml().unwrap()).unwrap();
4445        assert_order(
4446            &written,
4447            &[
4448                "<x:first",
4449                "<a:tblBg>",
4450                r#"<a:fillRef idx="2">"#,
4451                "<x:between",
4452                r#"effectRef idx="1">"#,
4453                "</a:tblBg>",
4454                "<a:wholeTbl>",
4455            ],
4456        );
4457        assert_eq!(
4458            CT_TableStyleList::from_xml(written.as_bytes()).unwrap(),
4459            styles
4460        );
4461
4462        let solid = format!(
4463            r#"<a:tblStyleLst xmlns:a="{A_NS}"><a:tblStyle styleId="s" styleName="S"><a:tblBg><a:fill><a:solidFill><a:srgbClr val="112233"/></a:solidFill></a:fill></a:tblBg></a:tblStyle></a:tblStyleLst>"#
4464        );
4465        let styles = CT_TableStyleList::from_xml(solid.as_bytes()).unwrap();
4466        let background = styles.styles[0].background.as_ref().unwrap();
4467        assert!(matches!(background.fill, Some(Fill::Solid(_))));
4468        assert!(background.unsupported.is_empty());
4469        assert_eq!(
4470            CT_TableStyleList::from_xml(&styles.to_xml().unwrap()).unwrap(),
4471            styles
4472        );
4473    }
4474
4475    /// Checks phrases describing a built-in style's modelled region properties.
4476    fn assert_regions(style_id: &str, expected: &[&str]) {
4477        let style = CT_TableStyle::builtin(style_id).unwrap();
4478        let regions = [
4479            ("wholeTbl", &style.whole_table),
4480            ("band1H", &style.band1_horizontal),
4481            ("band2H", &style.band2_horizontal),
4482            ("band1V", &style.band1_vertical),
4483            ("band2V", &style.band2_vertical),
4484            ("lastCol", &style.last_column),
4485            ("firstCol", &style.first_column),
4486            ("lastRow", &style.last_row),
4487            ("seCell", &style.south_east_cell),
4488            ("swCell", &style.south_west_cell),
4489            ("firstRow", &style.first_row),
4490            ("neCell", &style.north_east_cell),
4491            ("nwCell", &style.north_west_cell),
4492        ];
4493        let mut phrases = Vec::new();
4494        for (name, region) in regions {
4495            let Some(region) = region else { continue };
4496            if let Some(text) = &region.text_style {
4497                let bold = if text.bold == Some(true) { " bold" } else { "" };
4498                let color = text
4499                    .color
4500                    .as_ref()
4501                    .map(|color| format!(" {}", scheme(color)));
4502                let color = color.unwrap_or_default();
4503                phrases.push(format!("{name} text{bold}{color}"));
4504            }
4505            let Some(cell) = &region.cell_style else {
4506                continue;
4507            };
4508            match (&cell.fill, &cell.fill_reference) {
4509                (Some(Fill::Solid(fill)), _) => {
4510                    phrases.push(format!(
4511                        "{name} fill {}",
4512                        scheme(fill.color.as_ref().unwrap())
4513                    ));
4514                }
4515                (Some(Fill::NoFill(_)), _) => phrases.push(format!("{name} fill none")),
4516                (_, Some(StyleReference::Fill(reference))) => phrases.push(format!(
4517                    "{name} fill ref {} {}",
4518                    reference.index,
4519                    scheme(reference.color.as_ref().unwrap())
4520                )),
4521                _ => {}
4522            }
4523            let Some(borders) = &cell.borders else {
4524                continue;
4525            };
4526            for (edge, line) in [
4527                ("left", &borders.left),
4528                ("right", &borders.right),
4529                ("top", &borders.top),
4530                ("bottom", &borders.bottom),
4531                ("insideH", &borders.inside_horizontal),
4532                ("insideV", &borders.inside_vertical),
4533            ] {
4534                match line.as_ref().map(|line| (line.width, &line.fill)) {
4535                    Some((Some(width), Some(Fill::Solid(fill)))) => phrases.push(format!(
4536                        "{name} {edge} {width} {}",
4537                        scheme(fill.color.as_ref().unwrap())
4538                    )),
4539                    Some((None, Some(Fill::NoFill(_)))) => {
4540                        phrases.push(format!("{name} {edge} none"));
4541                    }
4542                    _ => {}
4543                }
4544            }
4545        }
4546        for phrase in expected {
4547            assert!(
4548                phrases.iter().any(|candidate| candidate == phrase),
4549                "{style_id}: {phrase} not in {phrases:#?}"
4550            );
4551        }
4552    }
4553
4554    fn scheme(color: &ColorChoice) -> String {
4555        let ColorChoice::Scheme {
4556            value, transforms, ..
4557        } = color
4558        else {
4559            panic!("expected a scheme colour, got {color:?}");
4560        };
4561        transforms
4562            .iter()
4563            .fold(value.clone(), |text, transform| match transform {
4564                ColorTransform::Tint(value) => format!("{text} tint {}%", value.0 / 1000),
4565                ColorTransform::Shade(value) => format!("{text} shade {}%", value.0 / 1000),
4566                ColorTransform::Alpha(value) => format!("{text} alpha {}%", value.0 / 1000),
4567                other => panic!("unexpected transform {other:?}"),
4568            })
4569    }
4570
4571    fn assert_ambiguous_grid_error(error: OxmlError) {
4572        assert!(matches!(
4573            error,
4574            OxmlError::InvalidValue(message)
4575                if message == "edited table grid is ambiguous because preserved column metadata cannot be associated reliably"
4576        ));
4577    }
4578
4579    fn assert_order(text: &str, tags: &[&str]) {
4580        let mut previous = 0usize;
4581        for tag in tags {
4582            let position = text[previous..].find(tag).unwrap() + previous;
4583            assert!(position >= previous, "{tag}");
4584            previous = position + tag.len();
4585        }
4586    }
4587}