1use crate::shared_strings::write_text_node;
2use crate::writer::{Result, XmlWriter};
3use std::io::Write;
4use xlsbye_core::types::{
5 Cell, CellValue, ColumnInfo, MergeCells, PaneInfo, RangeRef, RichTextRun, RowInfo,
6 SelectionInfo, SheetFormatInfo, SheetViewInfo,
7};
8use xlsbye_core::xml_names::{RELATIONSHIPS_NS, SPREADSHEET_NS};
9
10#[derive(Debug, Clone, PartialEq)]
11pub struct SheetRow {
12 pub info: RowInfo,
13 pub cells: Vec<Cell>,
14}
15
16#[derive(Debug, Clone, PartialEq, Default)]
17pub struct SheetData {
18 pub dimension: Option<RangeRef>,
19 pub sheet_views: Vec<SheetViewInfo>,
20 pub sheet_format: Option<SheetFormatInfo>,
21 pub columns: Vec<ColumnInfo>,
22 pub rows: Vec<SheetRow>,
23 pub merge_cells: MergeCells,
24}
25
26pub fn write_worksheet(writer: impl Write, sheet_data: &SheetData) -> Result<()> {
27 let mut writer = XmlWriter::new(writer);
28 writer.write_xml_declaration()?;
29 writer.write_start_element_with_ns(
30 "worksheet",
31 [
32 ("", SPREADSHEET_NS),
33 ("r", RELATIONSHIPS_NS),
34 ("x14ac", "http://schemas.microsoft.com/office/spreadsheetml/2009/9/ac"),
35 ],
36 std::iter::empty::<(&str, &str)>(),
37 )?;
38
39 if let Some(dimension) = &sheet_data.dimension {
40 writer.write_empty_element("dimension", [("ref", range_ref_to_a1(dimension))])?;
41 }
42
43 if !sheet_data.sheet_views.is_empty() {
44 writer.write_start_element("sheetViews", std::iter::empty::<(&str, &str)>())?;
45 for sheet_view in &sheet_data.sheet_views {
46 write_sheet_view(&mut writer, sheet_view)?;
47 }
48 writer.write_end_element("sheetViews")?;
49 }
50
51 if let Some(sheet_format) = &sheet_data.sheet_format {
52 let mut attrs = vec![("defaultRowHeight".to_string(), trim_float(sheet_format.default_row_height))];
53 if sheet_format.outline_level_row != 0 {
54 attrs.push(("outlineLevelRow".to_string(), sheet_format.outline_level_row.to_string()));
55 }
56 if sheet_format.outline_level_col != 0 {
57 attrs.push(("outlineLevelCol".to_string(), sheet_format.outline_level_col.to_string()));
58 }
59 attrs.push(("x14ac:dyDescent".to_string(), "0.2".to_string()));
60 writer.write_empty_element("sheetFormatPr", attrs)?;
61 }
62
63 if !sheet_data.columns.is_empty() {
64 writer.write_start_element("cols", std::iter::empty::<(&str, &str)>())?;
65 for column in &sheet_data.columns {
66 let mut attrs = vec![
67 ("min".to_string(), column.min.to_string()),
68 ("max".to_string(), column.max.to_string()),
69 ("width".to_string(), column.width.to_string()),
70 ];
71 if column.style_index != 0 {
72 attrs.push(("style".to_string(), column.style_index.to_string()));
73 }
74 if column.hidden {
75 attrs.push(("hidden".to_string(), "1".to_string()));
76 }
77 if column.best_fit {
78 attrs.push(("bestFit".to_string(), "1".to_string()));
79 }
80 if column.custom_width {
81 attrs.push(("customWidth".to_string(), "1".to_string()));
82 }
83 if column.outline_level != 0 {
84 attrs.push(("outlineLevel".to_string(), column.outline_level.to_string()));
85 }
86 if column.collapsed {
87 attrs.push(("collapsed".to_string(), "1".to_string()));
88 }
89 writer.write_empty_element("col", attrs)?;
90 }
91 writer.write_end_element("cols")?;
92 }
93
94 writer.write_start_element("sheetData", std::iter::empty::<(&str, &str)>())?;
95 for row in &sheet_data.rows {
96 write_row(&mut writer, row, sheet_data.sheet_format.as_ref())?;
97 }
98 writer.write_end_element("sheetData")?;
99
100 if !sheet_data.merge_cells.is_empty() {
101 writer.write_start_element(
102 "mergeCells",
103 [("count", sheet_data.merge_cells.len().to_string())],
104 )?;
105 for merge in &sheet_data.merge_cells {
106 writer.write_empty_element("mergeCell", [("ref", range_ref_to_a1(&merge.range))])?;
107 }
108 writer.write_end_element("mergeCells")?;
109 }
110
111 writer.write_end_element("worksheet")?;
112 Ok(())
113}
114
115fn write_row<W: Write>(writer: &mut XmlWriter<W>, row: &SheetRow, sheet_format: Option<&SheetFormatInfo>) -> Result<()> {
116 let mut attrs = vec![("r".to_string(), row.info.row.to_string())];
117 if let Some((min_col, max_col)) = row.cells.first().zip(row.cells.last()).map(|(first, last)| (first.col, last.col)) {
118 attrs.push(("spans".to_string(), format!("{min_col}:{max_col}")));
119 }
120
121 let default_row_height = sheet_format.map(|format| format.default_row_height).unwrap_or(15.0);
122 if row.info.custom_height || (row.info.height - default_row_height).abs() > 0.0001 {
123 attrs.push(("ht".to_string(), trim_float(row.info.height)));
124 }
125 if row.info.custom_height {
126 attrs.push(("customHeight".to_string(), "1".to_string()));
127 }
128 if row.info.style_index != 0 {
129 attrs.push(("s".to_string(), row.info.style_index.to_string()));
130 attrs.push(("customFormat".to_string(), "1".to_string()));
131 }
132 if row.info.hidden {
133 attrs.push(("hidden".to_string(), "1".to_string()));
134 }
135 if row.info.outline_level != 0 {
136 attrs.push(("outlineLevel".to_string(), row.info.outline_level.to_string()));
137 }
138 if row.info.collapsed {
139 attrs.push(("collapsed".to_string(), "1".to_string()));
140 }
141 if row.info.thick_top {
142 attrs.push(("thickTop".to_string(), "1".to_string()));
143 }
144 if row.info.thick_bottom {
145 attrs.push(("thickBot".to_string(), "1".to_string()));
146 }
147 attrs.push(("x14ac:dyDescent".to_string(), "0.2".to_string()));
148
149 writer.write_start_element("row", attrs)?;
150 for cell in &row.cells {
151 write_cell(writer, row.info.row, cell)?;
152 }
153 writer.write_end_element("row")
154}
155
156fn trim_float(value: f64) -> String {
157 if value.fract() == 0.0 {
158 format!("{value:.0}")
159 } else {
160 let text = value.to_string();
161 text.trim_end_matches('0').trim_end_matches('.').to_string()
162 }
163}
164
165fn write_sheet_view<W: Write>(writer: &mut XmlWriter<W>, sheet_view: &SheetViewInfo) -> Result<()> {
166 let mut attrs = vec![("workbookViewId".to_string(), sheet_view.workbook_view_id.to_string())];
167 if !sheet_view.show_grid_lines {
168 attrs.push(("showGridLines".to_string(), "0".to_string()));
169 }
170 if sheet_view.tab_selected {
171 attrs.push(("tabSelected".to_string(), "1".to_string()));
172 }
173 if sheet_view.top_left_row != 0 || sheet_view.top_left_col != 0 {
174 attrs.push((
175 "topLeftCell".to_string(),
176 cell_ref_zero_based_to_a1(sheet_view.top_left_row, sheet_view.top_left_col),
177 ));
178 }
179 if let Some(zoom_scale) = sheet_view.zoom_scale {
180 attrs.push(("zoomScale".to_string(), zoom_scale.to_string()));
181 }
182 if let Some(zoom_scale_normal) = sheet_view.zoom_scale_normal {
183 attrs.push(("zoomScaleNormal".to_string(), zoom_scale_normal.to_string()));
184 }
185
186 writer.write_start_element("sheetView", attrs)?;
187 if let Some(pane) = &sheet_view.pane {
188 write_pane(writer, pane)?;
189 }
190 for selection in &sheet_view.selections {
191 write_selection(writer, selection)?;
192 }
193 writer.write_end_element("sheetView")
194}
195
196fn write_pane<W: Write>(writer: &mut XmlWriter<W>, pane: &PaneInfo) -> Result<()> {
197 let mut attrs = vec![
198 ("xSplit".to_string(), split_to_string(pane.x_split)),
199 ("ySplit".to_string(), split_to_string(pane.y_split)),
200 (
201 "topLeftCell".to_string(),
202 cell_ref_zero_based_to_a1(pane.top_left_row, pane.top_left_col),
203 ),
204 ("activePane".to_string(), pane.active_pane.clone()),
205 ("state".to_string(), pane.state.clone()),
206 ];
207 attrs.retain(|(_, value)| !value.is_empty());
208 writer.write_empty_element("pane", attrs)
209}
210
211fn write_selection<W: Write>(writer: &mut XmlWriter<W>, selection: &SelectionInfo) -> Result<()> {
212 let mut attrs = Vec::new();
213 if let Some(pane) = &selection.pane {
214 attrs.push(("pane".to_string(), pane.clone()));
215 }
216 attrs.push(("activeCell".to_string(), selection.active_cell.clone()));
217 attrs.push(("sqref".to_string(), selection.sqref.clone()));
218 writer.write_empty_element("selection", attrs)
219}
220
221fn split_to_string(value: f64) -> String {
222 if value.fract() == 0.0 {
223 format!("{value:.0}")
224 } else {
225 value.to_string()
226 }
227}
228
229fn cell_ref_zero_based_to_a1(row: u32, col: u32) -> String {
230 format!("{}{}", col_to_name(col + 1), row + 1)
231}
232
233fn write_cell<W: Write>(writer: &mut XmlWriter<W>, row: u32, cell: &Cell) -> Result<()> {
234 let mut attrs = vec![
235 ("r".to_string(), cell_ref_to_a1(row, cell.col)),
236 ("s".to_string(), cell.style_index.to_string()),
237 ];
238
239 let mut inline_string_runs: Option<&[RichTextRun]> = None;
240 let mut inline_string_text: Option<&str> = None;
241 let mut value_text: Option<String> = None;
242
243 match &cell.value {
244 CellValue::Blank => {}
245 CellValue::Bool(v) => {
246 attrs.push(("t".to_string(), "b".to_string()));
247 value_text = Some(if *v { "1".to_string() } else { "0".to_string() });
248 }
249 CellValue::Number(v) => {
250 value_text = Some(v.to_string());
251 }
252 CellValue::Error(err) => {
253 attrs.push(("t".to_string(), "e".to_string()));
254 value_text = Some(err.as_str().to_string());
255 }
256 CellValue::String(text) => {
257 if cell.formula.is_some() || cell.shared_formula_index.is_some() {
258 attrs.push(("t".to_string(), "str".to_string()));
259 value_text = Some(text.clone());
260 } else {
261 attrs.push(("t".to_string(), "inlineStr".to_string()));
262 inline_string_text = Some(text.as_str());
263 }
264 }
265 CellValue::SharedString(index) => {
266 attrs.push(("t".to_string(), "s".to_string()));
267 value_text = Some(index.to_string());
268 }
269 CellValue::RichString(runs) => {
270 attrs.push(("t".to_string(), "inlineStr".to_string()));
271 inline_string_runs = Some(runs);
272 }
273 }
274
275 if cell.formula.is_none()
276 && cell.shared_formula_index.is_none()
277 && inline_string_runs.is_none()
278 && inline_string_text.is_none()
279 && value_text.is_none()
280 {
281 writer.write_empty_element("c", attrs)?;
282 return Ok(());
283 }
284
285 writer.write_start_element("c", attrs)?;
286
287 if cell.formula.is_some() || cell.shared_formula_index.is_some() {
288 let mut formula_attrs = Vec::new();
289 if let Some(shared_index) = cell.shared_formula_index {
290 formula_attrs.push(("t".to_string(), "shared".to_string()));
291 formula_attrs.push(("si".to_string(), shared_index.to_string()));
292 if let Some(shared_ref) = &cell.shared_formula_ref {
293 formula_attrs.push(("ref".to_string(), range_ref_to_a1(shared_ref)));
294 }
295 }
296
297 if let Some(formula) = &cell.formula {
298 writer.write_text_element("f", formula_attrs, formula)?;
299 } else {
300 writer.write_empty_element("f", formula_attrs)?;
301 }
302 }
303
304 if let Some(text) = inline_string_text {
305 writer.write_start_element("is", std::iter::empty::<(&str, &str)>())?;
306 write_text_node(writer, "t", text)?;
307 writer.write_end_element("is")?;
308 } else if let Some(runs) = inline_string_runs {
309 writer.write_start_element("is", std::iter::empty::<(&str, &str)>())?;
310 for run in runs {
311 writer.write_start_element("r", std::iter::empty::<(&str, &str)>())?;
312 if let Some(font_index) = run.font_index {
313 writer.write_start_element("rPr", std::iter::empty::<(&str, &str)>())?;
314 writer.write_empty_element("rFont", [("val", format!("font{}", font_index))])?;
315 writer.write_end_element("rPr")?;
316 }
317 write_text_node(writer, "t", &run.text)?;
318 writer.write_end_element("r")?;
319 }
320 writer.write_end_element("is")?;
321 } else if let Some(v) = value_text {
322 writer.write_text_element("v", std::iter::empty::<(&str, &str)>(), &v)?;
323 }
324
325 writer.write_end_element("c")
326}
327
328fn range_ref_to_a1(range: &RangeRef) -> String {
329 format!(
330 "{}:{}",
331 cell_ref_to_a1(range.first_row, range.first_col),
332 cell_ref_to_a1(range.last_row, range.last_col)
333 )
334}
335
336fn cell_ref_to_a1(row: u32, col: u32) -> String {
337 format!("{}{}", col_to_name(col), row)
338}
339
340fn col_to_name(mut col: u32) -> String {
341 let mut letters = Vec::new();
342 while col > 0 {
343 let rem = ((col - 1) % 26) as u8;
344 letters.push((b'A' + rem) as char);
345 col = (col - 1) / 26;
346 }
347 letters.iter().rev().collect()
348}