Skip to main content

easydoc_writer/executor/
table_executor.rs

1//! 快速表格写入执行器 -- 将 `Vec<T>` 直接渲染为 DOCX 表格。
2//!
3//! 从 [`TableColumn`] schema 应用逐列属性(`width`、`wrap`、`format`、`align`),
4//! 使生成的 OOXML 忠实表示 `#[derive(DocxRow)]` 产生的列元数据。
5//!
6//! 对应 Java: `com.alibaba.excel.write.ExcelBuilderImpl` 中的表格写入逻辑
7
8use std::fs::File;
9use std::io::{Cursor, Seek, Write};
10use std::path::PathBuf;
11
12use docx_rs::Docx;
13use easydoc_core::metadata::TableColumn;
14use easydoc_core::style::TableStyle;
15use easydoc_core::{DocError, DocxRow, Result};
16
17use crate::util::{insert_many_after_nth, parse_width};
18
19/// Executor for one-shot table writes.
20pub struct TableWriteExecutor<'a, T: DocxRow> {
21    path: PathBuf,
22    data: &'a [T],
23    title: Option<String>,
24    style: TableStyle,
25    need_header: bool,
26}
27
28impl<'a, T: DocxRow> TableWriteExecutor<'a, T> {
29    /// Creates a new table write executor.
30    pub(crate) fn new(
31        path: PathBuf,
32        data: &'a [T],
33        title: Option<String>,
34        style: TableStyle,
35        need_header: bool,
36    ) -> Self {
37        Self {
38            path,
39            data,
40            title,
41            style,
42            need_header,
43        }
44    }
45
46    /// Builds the `docx_rs` document from stored data.
47    fn build_docx(&self) -> Result<Docx> {
48        let mut docx = Docx::new();
49
50        if let Some(ref title) = self.title {
51            docx = docx.add_paragraph(
52                docx_rs::Paragraph::new()
53                    .add_run(docx_rs::Run::new().add_text(title.as_str()).bold().size(28)),
54            );
55        }
56
57        // Schema columns sorted by index so they align with `to_row()` cell order.
58        let mut schema: Vec<&TableColumn> = T::schema().iter().collect();
59        schema.sort_by_key(|c| c.index);
60        let schema = schema;
61
62        let mut rows: Vec<docx_rs::TableRow> = Vec::new();
63
64        // ---- Header row ----
65        if self.need_header {
66            let header_cells: Vec<docx_rs::TableCell> = schema
67                .iter()
68                .filter(|c| !c.ignored)
69                .map(|col| {
70                    let mut run = docx_rs::Run::new().add_text(col.name.as_str());
71                    if self.style.header_font.bold {
72                        run = run.bold();
73                    }
74                    let mut cell = docx_rs::TableCell::new()
75                        .add_paragraph(docx_rs::Paragraph::new().add_run(run));
76                    cell = apply_cell_width(cell, col);
77                    cell
78                })
79                .collect();
80            rows.push(docx_rs::TableRow::new(header_cells));
81        }
82
83        // ---- Data rows ----
84        for item in self.data {
85            let cells = item.to_row()?;
86            let visible_cols: Vec<&&TableColumn> = schema.iter().filter(|c| !c.ignored).collect();
87
88            let data_cells: Vec<docx_rs::TableCell> = cells
89                .iter()
90                .zip(visible_cols.iter())
91                .map(|(cell, col)| {
92                    let text = doc_value_str(&cell.value);
93                    let mut para =
94                        docx_rs::Paragraph::new().add_run(docx_rs::Run::new().add_text(text));
95
96                    // Apply column-level or cell-level alignment.
97                    let alignment = col.align.or(cell.alignment);
98                    if let Some(align) = alignment {
99                        para = para.align(to_docx_alignment(align));
100                    }
101
102                    let mut tc = docx_rs::TableCell::new().add_paragraph(para);
103                    tc = apply_cell_width(tc, col);
104                    tc
105                })
106                .collect();
107            rows.push(docx_rs::TableRow::new(data_cells));
108        }
109
110        docx = docx.add_table(docx_rs::Table::new(rows));
111        Ok(docx)
112    }
113
114    /// Post-processes the raw document XML to inject `noWrap` and `numFmt`
115    /// attributes that `docx-rs` does not natively support.
116    fn apply_xml_extras(&self, document_xml: &mut Vec<u8>) -> Result<()> {
117        let mut schema: Vec<&TableColumn> = T::schema().iter().collect();
118        schema.sort_by_key(|c| c.index);
119
120        let visible: Vec<&TableColumn> = schema.iter().filter(|c| !c.ignored).copied().collect();
121        let num_visible = visible.len();
122
123        // Fast path: skip XML post-processing entirely when no columns need
124        // noWrap or numFmt injection (avoids O(cells) string allocations).
125        let needs_no_wrap = visible.iter().any(|c| !c.wrap);
126        let needs_num_fmt = visible.iter().any(|c| c.format.is_some());
127        if !needs_no_wrap && !needs_num_fmt {
128            return Ok(());
129        }
130
131        let xml = String::from_utf8_lossy(document_xml).to_string();
132        let mut modified = xml;
133
134        // Apply wrap (noWrap) to all visible cells -- header + data.
135        // 线性优化:一次扫描收集所有需要插入的 noWrap 片段,
136        // 单次批量插入(避免逐 cell 调用 insert_after_nth 的 O(n²) 扫描)。
137        let total_cells = if self.need_header {
138            num_visible * (1 + self.data.len())
139        } else {
140            num_visible * self.data.len()
141        };
142
143        let tcw_count = modified.matches("<w:tcW").count();
144        let rpr_count = modified.matches("<w:pPr><w:rPr").count();
145
146        let mut no_wrap_inserts: Vec<String> = Vec::new();
147        for cell_idx in 0..total_cells {
148            let col_idx = cell_idx % num_visible;
149            let col = visible[col_idx];
150            if !col.wrap {
151                no_wrap_inserts.push("<w:noWrap/>".to_owned());
152            }
153        }
154        if !no_wrap_inserts.is_empty() {
155            // 优先在 <w:tcW ... /> 后插入;数量不足时回退到 <w:tcPr>。
156            let pattern = if tcw_count >= no_wrap_inserts.len() {
157                "<w:tcW"
158            } else {
159                "<w:tcPr"
160            };
161            modified = insert_many_after_nth(&modified, pattern, &no_wrap_inserts);
162        }
163
164        // Apply numFmt to data cells only (skip header cells).
165        let data_offset = if self.need_header { num_visible } else { 0 };
166        let mut num_fmt_inserts: Vec<String> = Vec::new();
167        for (i, item) in self.data.iter().enumerate() {
168            let cells = item.to_row()?;
169            for (j, col) in visible.iter().enumerate() {
170                if let Some(ref fmt) = col.format {
171                    let cell_idx = data_offset + i * num_visible + j;
172                    // 与 insert_num_fmt 的语义一致:仅在对应位置存在
173                    // <w:pPr><w:rPr> 时才插入(先收集,统一判断)
174                    if cell_idx < rpr_count {
175                        num_fmt_inserts.push(format!("<w:numFmt w:val=\"{fmt}\"/>"));
176                    }
177                }
178            }
179            let _ = cells; // keep ownership for potential future use
180        }
181        if !num_fmt_inserts.is_empty() {
182            modified = insert_many_after_nth(&modified, "<w:pPr><w:rPr", &num_fmt_inserts);
183        }
184
185        *document_xml = modified.into_bytes();
186        Ok(())
187    }
188
189    /// Executes the write to disk.
190    pub fn execute(self) -> Result<()> {
191        let file = File::create(&self.path)?;
192        let docx = self.build_docx()?;
193        let mut xml_docx = docx.build();
194        self.apply_xml_extras(&mut xml_docx.document)?;
195        xml_docx
196            .pack(file)
197            .map_err(|e| DocError::Zip(e.to_string()))?;
198        Ok(())
199    }
200
201    /// Executes the write to a generic writer.
202    ///
203    /// Corresponds to Hutool's `flush(OutputStream)` pattern.
204    pub fn execute_to_writer<W: Write + Seek>(self, writer: W) -> Result<()> {
205        let docx = self.build_docx()?;
206        let mut xml_docx = docx.build();
207        self.apply_xml_extras(&mut xml_docx.document)?;
208        xml_docx
209            .pack(writer)
210            .map_err(|e| DocError::Zip(e.to_string()))?;
211        Ok(())
212    }
213
214    /// Executes the write and returns bytes.
215    pub fn execute_to_bytes(self) -> Result<Vec<u8>> {
216        let mut buf = Vec::new();
217        let cursor = Cursor::new(&mut buf);
218        let docx = self.build_docx()?;
219        let mut xml_docx = docx.build();
220        self.apply_xml_extras(&mut xml_docx.document)?;
221        xml_docx
222            .pack(cursor)
223            .map_err(|e| DocError::Zip(e.to_string()))?;
224        Ok(buf)
225    }
226}
227
228/// Applies the column's `width` attribute to a `docx_rs::TableCell`.
229///
230/// Uses [`parse_width`] to convert the CSS-like width string to OOXML twips
231/// or percentage units.  No-op when the column has no width set.
232fn apply_cell_width(cell: docx_rs::TableCell, col: &TableColumn) -> docx_rs::TableCell {
233    if let Some(ref w) = col.width
234        && let Some(parsed) = parse_width(w)
235    {
236        return cell.width(parsed.value, parsed.width_type);
237    }
238    cell
239}
240
241/// Converts our domain [`HorizontalAlignment`] to the `docx-rs` equivalent.
242fn to_docx_alignment(
243    alignment: easydoc_core::types::HorizontalAlignment,
244) -> docx_rs::AlignmentType {
245    // `_` 通配与显式分支体相同是 #[non_exhaustive] 的必然结果
246    #[allow(clippy::match_same_arms)]
247    match alignment {
248        easydoc_core::types::HorizontalAlignment::Left => docx_rs::AlignmentType::Left,
249        easydoc_core::types::HorizontalAlignment::Center => docx_rs::AlignmentType::Center,
250        easydoc_core::types::HorizontalAlignment::Right => docx_rs::AlignmentType::Right,
251        easydoc_core::types::HorizontalAlignment::Both => docx_rs::AlignmentType::Both,
252        // #[non_exhaustive]:未来新增的对齐方式默认左对齐
253        _ => docx_rs::AlignmentType::Left,
254    }
255}
256
257fn doc_value_str(value: &easydoc_core::DocValue) -> String {
258    match value {
259        easydoc_core::DocValue::String(s) => s.clone(),
260        easydoc_core::DocValue::Int(n) => n.to_string(),
261        easydoc_core::DocValue::Float(n) => n.to_string(),
262        easydoc_core::DocValue::Bool(b) => b.to_string(),
263        easydoc_core::DocValue::Empty => String::new(),
264        other => format!("{other:?}"),
265    }
266}