Skip to main content

ooxml_core/oxml/
table.rs

1//! 表格:`<a:tbl>` 嵌入 `<p:graphicFrame>`。
2//!
3//! OOXML 中"表格"由三层结构组成:
4//!
5//! ```text
6//! <p:graphicFrame>
7//!   <p:nvGraphicFramePr><p:cNvPr .../></p:nvGraphicFramePr>
8//!   <p:xfrm>...</p:xfrm>
9//!   <a:graphic>
10//!     <a:graphicData uri="...">
11//!       <a:tbl>                  ← 本模块关心的内容
12//!         <a:tblPr>...</a:tblPr>
13//!         <a:tblGrid>...</a:tblGrid>
14//!         <a:tr>...</a:tr>       ← 行
15//!           <a:tc>...</a:tc>     ← 单元格
16//!         ...
17//!       </a:tbl>
18//!     </a:graphicData>
19//!   </a:graphic>
20//! </p:graphicFrame>
21//! ```
22//!
23//! # 与 python-pptx 的对应
24//!
25//! - `pptx.table.Table` ←→ [`Table`];
26//! - `_Row` / `_Column` / `_Cell` ←→ [`Row`] / [`Col`] / [`Cell`]。
27//!
28//! # 当前限制
29//!
30//! - 列宽 / 行高单位均为 EMU(OOXML 标准)。
31//! - 表格样式通过 GUID 引用 PowerPoint 内置样式(见 [`TableStyle`]);
32//!   自定义 `<a:tblStyle>` 定义(tableStyles.xml)尚未支持。
33
34use crate::oxml::color::Color;
35use crate::oxml::txbody::TextBody;
36use crate::units::Emu;
37
38/// 表格列属性。
39#[derive(Clone, Debug, Default)]
40pub struct Col {
41    /// 列宽(EMU)。
42    pub width: Emu,
43}
44
45/// 表格行属性。
46#[derive(Clone, Debug, Default)]
47pub struct Row {
48    /// 行高(EMU)。
49    pub height: Emu,
50    /// 行内每个 cell 的内容。
51    pub cells: Vec<Cell>,
52    /// 标识行头/列头。
53    pub header: bool,
54}
55
56/// 单元格垂直对齐方式(`<a:tcPr anchor="...">`)。
57///
58/// 对应 python-pptx `MSO_ANCHOR` 枚举。
59#[derive(Clone, Debug, Default, PartialEq)]
60pub enum VerticalAnchor {
61    /// 顶部对齐(`anchor="t"`)。
62    Top,
63    /// 中部对齐(`anchor="ctr"`,默认)。
64    #[default]
65    Middle,
66    /// 底部对齐(`anchor="b"`)。
67    Bottom,
68}
69
70impl VerticalAnchor {
71    /// 转为 OOXML 属性值。
72    pub fn as_str(&self) -> &'static str {
73        match self {
74            VerticalAnchor::Top => "t",
75            VerticalAnchor::Middle => "ctr",
76            VerticalAnchor::Bottom => "b",
77        }
78    }
79}
80
81/// 单元格边框样式(简化版,仅支持实线/虚线/无边框)。
82///
83/// 对应 `<a:lnL>` / `<a:lnR>` / `<a:lnT>` / `<a:lnB>` 子元素。
84#[derive(Clone, Debug, Default)]
85pub struct CellBorder {
86    /// 边框颜色。`Color::None` 表示不写出颜色(使用主题继承)。
87    pub color: Color,
88    /// 边框宽度(EMU)。
89    pub width: Emu,
90    /// 是否无边框(写出 `<a:noFill/>`)。
91    pub no_fill: bool,
92}
93
94/// 单元格。
95#[derive(Clone, Debug, Default)]
96pub struct Cell {
97    /// 单元格文本体。
98    pub text: TextBody,
99    /// 单元格填充色。`Color::None` 表示不写出填充。
100    pub fill: Color,
101    /// 上下左右内边距(EMU)。
102    pub margin: (Option<Emu>, Option<Emu>, Option<Emu>, Option<Emu>), // top,left,bottom,right
103    /// 跨行数(`rowSpan` 属性,0 或 1 表示不跨行)。
104    pub row_span: u32,
105    /// 跨列数(`gridSpan` 属性,0 或 1 表示不跨列)。
106    pub grid_span: u32,
107    /// 水平合并虚拟单元格标记(`hMerge="1"`,被合并方写出)。
108    pub h_merge: bool,
109    /// 垂直合并虚拟单元格标记(`vMerge="1"`,被合并方写出)。
110    pub v_merge: bool,
111    /// 垂直对齐方式。
112    pub anchor: VerticalAnchor,
113    /// 左边框(`<a:lnL>`)。
114    pub border_left: Option<CellBorder>,
115    /// 右边框(`<a:lnR>`)。
116    pub border_right: Option<CellBorder>,
117    /// 上边框(`<a:lnT>`)。
118    pub border_top: Option<CellBorder>,
119    /// 下边框(`<a:lnB>`)。
120    pub border_bottom: Option<CellBorder>,
121}
122
123/// 表格布尔属性(`<a:tblLook>` 的属性集)。
124///
125/// 对应 python-pptx `_Table.tbl_look` 的行为,控制表格样式应用范围。
126#[derive(Clone, Debug)]
127pub struct TableLook {
128    /// 样式索引值(默认 `"04A0"`)。
129    pub val: String,
130    /// 首行特殊格式(`firstRow="1"`)。
131    pub first_row: bool,
132    /// 末行特殊格式(`lastRow="1"`)。
133    pub last_row: bool,
134    /// 首列特殊格式(`firstColumn="1"`)。
135    pub first_column: bool,
136    /// 末列特殊格式(`lastColumn="1"`)。
137    pub last_column: bool,
138    /// 水平条纹(`noHBand="0"` 表示启用)。
139    pub no_h_band: bool,
140    /// 垂直条纹(`noVBand="1"` 表示禁用)。
141    pub no_v_band: bool,
142}
143
144impl Default for TableLook {
145    fn default() -> Self {
146        // 默认值与 PowerPoint 创建的表格一致
147        Self {
148            val: "04A0".to_string(),
149            first_row: true,
150            last_row: false,
151            first_column: true,
152            last_column: false,
153            no_h_band: false,
154            no_v_band: true,
155        }
156    }
157}
158
159/// 表格样式引用(`<a:tableStyleId>{GUID}</a:tableStyleId>`)。
160///
161/// OOXML 中表格样式通过 GUID 引用 `tableStyles.xml` 中定义的 `<a:tblStyle>`,
162/// 或引用 PowerPoint 内置样式(内置样式的 GUID 是预定义的,无需在包内定义)。
163///
164/// # 与 python-pptx 的对应
165///
166/// - python-pptx `Table.apply_style(style_id)` ←→ [`TableStyle::new`] + [`Table::set_style`];
167/// - python-pptx 默认使用 `{5C22544A-7EE6-4342-B048-85BDC9FD1C3A}`(Medium Style 2 - Accent 1)。
168///
169/// # 内置样式
170///
171/// PowerPoint 内置表格样式的 GUID 是固定的,可通过 [`TableStyle::from_name`] 按名称查找。
172/// 常见样式包括 "No Style, Table Grid"、"Medium Style 2 - Accent 1" 等。
173///
174/// # 示例
175///
176/// ```
177/// use ooxml_core::oxml::table::TableStyle;
178///
179/// // 按名称查找内置样式
180/// let style = TableStyle::from_name("Medium Style 2 - Accent 1").unwrap();
181/// assert_eq!(style.style_id(), "{5C22544A-7EE6-4342-B048-85BDC9FD1C3A}");
182///
183/// // 直接用 GUID 构造
184/// let style2 = TableStyle::new("{5940675A-B579-460E-94D1-54222C63F5DA}");
185/// assert_eq!(style2.style_id(), "{5940675A-B579-460E-94D1-54222C63F5DA}");
186/// ```
187#[derive(Clone, Debug, PartialEq, Eq)]
188pub struct TableStyle {
189    /// 样式 GUID(如 `{5C22544A-7EE6-4342-B048-85BDC9FD1C3A}`)。
190    style_id: String,
191    /// 可选的样式名称(如 "Medium Style 2 - Accent 1")。
192    style_name: Option<String>,
193}
194
195impl TableStyle {
196    /// 用 GUID 构造表格样式引用。
197    ///
198    /// # 参数
199    /// - `style_id`:样式 GUID 字符串(应包含花括号,如 `{5C22544A-...}`)。
200    pub fn new(style_id: impl Into<String>) -> Self {
201        Self {
202            style_id: style_id.into(),
203            style_name: None,
204        }
205    }
206
207    /// 用 GUID + 名称构造表格样式引用。
208    ///
209    /// # 参数
210    /// - `style_id`:样式 GUID 字符串;
211    /// - `style_name`:人类可读的样式名称。
212    pub fn with_name(style_id: impl Into<String>, style_name: impl Into<String>) -> Self {
213        Self {
214            style_id: style_id.into(),
215            style_name: Some(style_name.into()),
216        }
217    }
218
219    /// 按名称查找 PowerPoint 内置表格样式。
220    ///
221    /// # 参数
222    /// - `name`:样式名称(如 "Medium Style 2 - Accent 1")。
223    ///
224    /// # 返回值
225    /// - 找到时返回 `Some(TableStyle)`;
226    /// - 名称不在内置注册表中时返回 `None`。
227    ///
228    /// # 内置样式列表
229    ///
230    /// 目前注册的内置样式(可扩展):
231    /// - "No Style, Table Grid"
232    /// - "No Style, No Grid"
233    /// - "Medium Style 2 - Accent 1"
234    /// - "Themed Style 1 - Accent 1"
235    pub fn from_name(name: &str) -> Option<Self> {
236        builtin_table_style_guid(name).map(|guid| Self::with_name(guid, name))
237    }
238
239    /// 取样式 GUID。
240    pub fn style_id(&self) -> &str {
241        &self.style_id
242    }
243
244    /// 取样式名称(可能为 `None`)。
245    pub fn style_name(&self) -> Option<&str> {
246        self.style_name.as_deref()
247    }
248}
249
250/// 查找 PowerPoint 内置表格样式的 GUID。
251///
252/// 返回 `None` 表示名称不在内置注册表中。
253/// 调用方可通过 [`TableStyle::new`] 直接用 GUID 构造。
254fn builtin_table_style_guid(name: &str) -> Option<&'static str> {
255    // PowerPoint 内置表格样式 GUID 注册表。
256    // 这些 GUID 是 OOXML 规范和 PowerPoint 预定义的,无需在 tableStyles.xml 中定义。
257    // 参考:ECMA-376 第 20.1.4.2.27 节 tblStyleLst 示例。
258    match name {
259        // 无样式系列
260        "No Style, Table Grid" => Some("{5940675A-B579-460E-94D1-54222C63F5DA}"),
261        "No Style, No Grid" => Some("{2D5ABB26-0587-4C30-8999-92F81FD0307C}"),
262        // Medium Style 2 系列(python-pptx 默认)
263        "Medium Style 2 - Accent 1" => Some("{5C22544A-7EE6-4342-B048-85BDC9FD1C3A}"),
264        // Themed Style 1 系列(OOXML 规范示例)
265        "Themed Style 1 - Accent 1" => Some("{3C2FFA5D-87B4-456A-9821-1D502468CF0F}"),
266        _ => None,
267    }
268}
269
270/// 表格。
271#[derive(Clone, Debug, Default)]
272pub struct Table {
273    pub cols: Vec<Col>,
274    pub rows: Vec<Row>,
275    /// 表格样式查找属性(控制 tblLook)。
276    pub tbl_look: TableLook,
277    /// 表格样式引用(`<a:tableStyleId>`)。
278    ///
279    /// `None` 时不写出 `<a:tableStyleId>` 元素。
280    pub table_style: Option<TableStyle>,
281}
282
283impl Table {
284    /// 写 XML(`<a:tbl>` 嵌在 `<a:graphicData uri="...">` 内)。
285    ///
286    /// # OOXML 元素顺序
287    ///
288    /// ```text
289    /// <a:tbl>
290    ///   <a:tblPr>...</a:tblPr>           ← 必须先写,且不含 `<a:tblGrid>`
291    ///     <a:tableStyleId>...</a:tableStyleId>  ← 可选,紧跟 tblPr 属性后
292    ///   <a:tblGrid>...</a:tblGrid>       ← 列定义(gridCol 列表)
293    ///   <a:tr>...</a:tr>                 ← 行(先 cell 后 close)
294    ///     <a:tc>                         ← 单元格
295    ///       <a:txBody>...</a:txBody>     ← 文本
296    ///       <a:tcPr>                     ← 单元格属性(边距/边框/对齐)
297    ///         <a:lnL/>/<a:lnR/>/<a:lnT/>/<a:lnB/>
298    ///       </a:tcPr>
299    ///     </a:tc>
300    ///   </a:tr>
301    ///   ...
302    /// </a:tbl>
303    /// ```
304    pub fn write_xml(&self, w: &mut super::writer::XmlWriter) {
305        w.open("a:tbl");
306        // tblPr:注意 `<a:tblGrid/>` **不能**塞在这里,必须紧跟其后另开块。
307        w.open("a:tblPr");
308        w.empty_with("a:tblW", &[("w", "0"), ("type", "auto")]);
309        // tblLook:从结构体字段生成属性,不再硬编码
310        let lk = &self.tbl_look;
311        w.empty_with(
312            "a:tblLook",
313            &[
314                ("val", lk.val.as_str()),
315                ("firstRow", if lk.first_row { "1" } else { "0" }),
316                ("lastRow", if lk.last_row { "1" } else { "0" }),
317                ("firstColumn", if lk.first_column { "1" } else { "0" }),
318                ("lastColumn", if lk.last_column { "1" } else { "0" }),
319                ("noHBand", if lk.no_h_band { "1" } else { "0" }),
320                ("noVBand", if lk.no_v_band { "1" } else { "0" }),
321            ],
322        );
323        // tableStyleId:写出表格样式 GUID(可选)
324        if let Some(style) = &self.table_style {
325            w.leaf("a:tableStyleId", style.style_id());
326        }
327        w.close("a:tblPr");
328        // tblGrid:每个 col 一行
329        w.open("a:tblGrid");
330        for c in &self.cols {
331            w.empty_with("a:gridCol", &[("w", &c.width.value().to_string())]);
332        }
333        w.close("a:tblGrid");
334        // rows
335        for r in &self.rows {
336            w.open_with("a:tr", &[("h", &r.height.value().to_string())]);
337            for c in &r.cells {
338                // 构建 tc 的属性列表(gridSpan/rowSpan/hMerge/vMerge)
339                let mut tc_attrs: Vec<(&str, String)> = Vec::new();
340                if c.grid_span > 1 {
341                    tc_attrs.push(("gridSpan", c.grid_span.to_string()));
342                }
343                if c.row_span > 1 {
344                    tc_attrs.push(("rowSpan", c.row_span.to_string()));
345                }
346                if c.h_merge {
347                    tc_attrs.push(("hMerge", "1".to_string()));
348                }
349                if c.v_merge {
350                    tc_attrs.push(("vMerge", "1".to_string()));
351                }
352                let attr_refs: Vec<(&str, &str)> =
353                    tc_attrs.iter().map(|(k, v)| (*k, v.as_str())).collect();
354                if attr_refs.is_empty() {
355                    w.open("a:tc");
356                } else {
357                    w.open_with("a:tc", &attr_refs);
358                }
359
360                // 文本体
361                if c.text.paragraphs.is_empty() {
362                    let tb = TextBody::new();
363                    tb.write_xml(w, "a:txBody");
364                } else {
365                    c.text.write_xml(w, "a:txBody");
366                }
367
368                // tcPr:单元格属性(边距、边框、垂直对齐、填充)
369                let has_margins = c.margin.0.is_some()
370                    || c.margin.1.is_some()
371                    || c.margin.2.is_some()
372                    || c.margin.3.is_some();
373                let has_borders = c.border_left.is_some()
374                    || c.border_right.is_some()
375                    || c.border_top.is_some()
376                    || c.border_bottom.is_some();
377                let has_fill = !matches!(c.fill, Color::None);
378                let has_anchor = c.anchor != VerticalAnchor::default();
379
380                if has_margins || has_borders || has_fill || has_anchor {
381                    // 提前取出所有要序列化的字符串,扩展到块末尾
382                    let mart_s = c.margin.0.map(|m| m.value().to_string());
383                    let marl_s = c.margin.1.map(|m| m.value().to_string());
384                    let marb_s = c.margin.2.map(|m| m.value().to_string());
385                    let marr_s = c.margin.3.map(|m| m.value().to_string());
386                    let mut tcpr_attrs: Vec<(&str, &str)> = Vec::new();
387                    if let Some(s) = &mart_s {
388                        tcpr_attrs.push(("marT", s));
389                    }
390                    if let Some(s) = &marl_s {
391                        tcpr_attrs.push(("marL", s));
392                    }
393                    if let Some(s) = &marb_s {
394                        tcpr_attrs.push(("marB", s));
395                    }
396                    if let Some(s) = &marr_s {
397                        tcpr_attrs.push(("marR", s));
398                    }
399                    if has_anchor {
400                        tcpr_attrs.push(("anchor", c.anchor.as_str()));
401                    }
402                    if tcpr_attrs.is_empty() {
403                        w.open("a:tcPr");
404                    } else {
405                        w.open_with("a:tcPr", &tcpr_attrs);
406                    }
407                    // 填充色(solidFill)
408                    if has_fill {
409                        c.fill.write_solid_fill(w);
410                    }
411                    // 边框:OOXML 顺序为 lnL → lnR → lnT → lnB
412                    write_cell_border(w, "a:lnL", c.border_left.as_ref());
413                    write_cell_border(w, "a:lnR", c.border_right.as_ref());
414                    write_cell_border(w, "a:lnT", c.border_top.as_ref());
415                    write_cell_border(w, "a:lnB", c.border_bottom.as_ref());
416                    w.close("a:tcPr");
417                } else if has_fill {
418                    // 仅有填充色时也要写出 tcPr
419                    w.open("a:tcPr");
420                    c.fill.write_solid_fill(w);
421                    w.close("a:tcPr");
422                }
423                w.close("a:tc");
424            }
425            w.close("a:tr");
426        }
427        w.close("a:tbl");
428    }
429
430    /// 设置表格样式(按内置样式名称)。
431    ///
432    /// 对标 python-pptx `Table.apply_style(style_id)`。
433    ///
434    /// # 参数
435    /// - `name`:内置样式名称(如 "Medium Style 2 - Accent 1")。
436    ///
437    /// # 返回值
438    /// - 成功设置返回 `true`;
439    /// - 名称不在内置注册表中返回 `false`(`table_style` 保持不变)。
440    ///
441    /// # 示例
442    /// ```
443    /// use ooxml_core::oxml::table::Table;
444    ///
445    /// let mut t = Table::default();
446    /// assert!(t.set_style("Medium Style 2 - Accent 1"));
447    /// assert_eq!(
448    ///     t.table_style.as_ref().unwrap().style_id(),
449    ///     "{5C22544A-7EE6-4342-B048-85BDC9FD1C3A}"
450    /// );
451    /// assert!(!t.set_style("Unknown Style"));
452    /// ```
453    pub fn set_style(&mut self, name: &str) -> bool {
454        if let Some(style) = TableStyle::from_name(name) {
455            self.table_style = Some(style);
456            true
457        } else {
458            false
459        }
460    }
461
462    /// 设置表格样式(按原始 GUID)。
463    ///
464    /// 用于设置不在内置注册表中的样式(如自定义 tableStyles.xml 中定义的样式)。
465    ///
466    /// # 参数
467    /// - `guid`:样式 GUID 字符串(如 `{5C22544A-7EE6-4342-B048-85BDC9FD1C3A}`)。
468    pub fn set_style_id(&mut self, guid: impl Into<String>) {
469        self.table_style = Some(TableStyle::new(guid));
470    }
471
472    /// 清除表格样式引用。
473    pub fn clear_style(&mut self) {
474        self.table_style = None;
475    }
476}
477
478/// 写出单元格边框子元素。
479///
480/// `tag` 为 `a:lnL` / `a:lnR` / `a:lnT` / `a:lnB` 之一。
481fn write_cell_border(w: &mut super::writer::XmlWriter, tag: &str, border: Option<&CellBorder>) {
482    if let Some(b) = border {
483        // 提前取出宽度字符串,扩展到函数末尾
484        let w_s = b.width.value().to_string();
485        if b.no_fill {
486            w.open_with(tag, &[("w", w_s.as_str())]);
487            w.empty("a:noFill");
488            w.close(tag);
489        } else if !matches!(b.color, Color::None) {
490            w.open_with(tag, &[("w", w_s.as_str())]);
491            b.color.write_solid_fill(w);
492            w.close(tag);
493        } else {
494            // 无颜色也无 noFill:仅写空标签
495            w.empty_with(tag, &[("w", w_s.as_str())]);
496        }
497    }
498}
499
500#[cfg(test)]
501mod tests {
502    use super::*;
503
504    /// `TableStyle::from_name` 正确查找内置样式。
505    #[test]
506    fn table_style_from_name_finds_builtin() {
507        let style = TableStyle::from_name("Medium Style 2 - Accent 1")
508            .expect("Medium Style 2 - Accent 1 应在注册表中");
509        assert_eq!(style.style_id(), "{5C22544A-7EE6-4342-B048-85BDC9FD1C3A}");
510        assert_eq!(style.style_name(), Some("Medium Style 2 - Accent 1"));
511
512        let style2 = TableStyle::from_name("No Style, Table Grid")
513            .expect("No Style, Table Grid 应在注册表中");
514        assert_eq!(style2.style_id(), "{5940675A-B579-460E-94D1-54222C63F5DA}");
515    }
516
517    /// `TableStyle::from_name` 对未知名称返回 `None`。
518    #[test]
519    fn table_style_from_name_unknown_returns_none() {
520        assert!(TableStyle::from_name("Nonexistent Style").is_none());
521    }
522
523    /// `TableStyle::new` 用 GUID 构造,`style_name` 为 `None`。
524    #[test]
525    fn table_style_new_has_no_name() {
526        let style = TableStyle::new("{5940675A-B579-460E-94D1-54222C63F5DA}");
527        assert_eq!(style.style_id(), "{5940675A-B579-460E-94D1-54222C63F5DA}");
528        assert_eq!(style.style_name(), None);
529    }
530
531    /// `Table::set_style` 成功设置内置样式。
532    #[test]
533    fn table_set_style_builtin() {
534        let mut t = Table::default();
535        assert!(t.set_style("Medium Style 2 - Accent 1"));
536        let style = t.table_style.as_ref().expect("style 应已设置");
537        assert_eq!(style.style_id(), "{5C22544A-7EE6-4342-B048-85BDC9FD1C3A}");
538        assert_eq!(style.style_name(), Some("Medium Style 2 - Accent 1"));
539    }
540
541    /// `Table::set_style` 对未知名称返回 `false` 且不修改原状态。
542    #[test]
543    fn table_set_style_unknown_returns_false() {
544        let mut t = Table::default();
545        t.set_style("Medium Style 2 - Accent 1");
546        assert!(!t.set_style("Unknown Style"));
547        // 原样式应保持不变
548        assert_eq!(
549            t.table_style.as_ref().unwrap().style_id(),
550            "{5C22544A-7EE6-4342-B048-85BDC9FD1C3A}"
551        );
552    }
553
554    /// `Table::set_style_id` 用原始 GUID 设置样式。
555    #[test]
556    fn table_set_style_id_raw_guid() {
557        let mut t = Table::default();
558        t.set_style_id("{5940675A-B579-460E-94D1-54222C63F5DA}");
559        let style = t.table_style.as_ref().expect("style 应已设置");
560        assert_eq!(style.style_id(), "{5940675A-B579-460E-94D1-54222C63F5DA}");
561        assert_eq!(style.style_name(), None);
562    }
563
564    /// `Table::clear_style` 清除样式引用。
565    #[test]
566    fn table_clear_style() {
567        let mut t = Table::default();
568        t.set_style("Medium Style 2 - Accent 1");
569        assert!(t.table_style.is_some());
570        t.clear_style();
571        assert!(t.table_style.is_none());
572    }
573
574    /// `Table::write_xml` 正确序列化 `<a:tableStyleId>` 元素。
575    #[test]
576    fn table_write_xml_with_style() {
577        let mut t = Table::default();
578        t.set_style("Medium Style 2 - Accent 1");
579        let mut w = crate::oxml::writer::XmlWriter::new();
580        t.write_xml(&mut w);
581        let xml = &w.buf;
582        assert!(xml.contains("<a:tableStyleId>"));
583        assert!(xml.contains("{5C22544A-7EE6-4342-B048-85BDC9FD1C3A}"));
584        assert!(xml.contains("</a:tableStyleId>"));
585        // tableStyleId 应在 tblPr 内
586        let pr_start = xml.find("<a:tblPr>").unwrap();
587        let pr_end = xml.find("</a:tblPr>").unwrap();
588        let style_pos = xml.find("<a:tableStyleId>").unwrap();
589        let style_end = xml.find("</a:tableStyleId>").unwrap();
590        assert!(style_pos > pr_start && style_end < pr_end);
591    }
592
593    /// `Table::write_xml` 无样式时不写出 `<a:tableStyleId>`。
594    #[test]
595    fn table_write_xml_without_style() {
596        let t = Table::default();
597        let mut w = crate::oxml::writer::XmlWriter::new();
598        t.write_xml(&mut w);
599        assert!(!w.buf.contains("tableStyleId"));
600    }
601}