easydoc_writer/executor/
table_executor.rs1use 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
19pub 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 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 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 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 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 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 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 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 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 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 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 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 if cell_idx < rpr_count {
175 num_fmt_inserts.push(format!("<w:numFmt w:val=\"{fmt}\"/>"));
176 }
177 }
178 }
179 let _ = cells; }
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 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 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 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
228fn 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
241fn to_docx_alignment(
243 alignment: easydoc_core::types::HorizontalAlignment,
244) -> docx_rs::AlignmentType {
245 #[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 _ => 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}