excel_ooxml/model.rs
1//! The minimal in-memory model of a SpreadsheetML workbook: a workbook is a sequence of sheets,
2//! each a sequence of rows, each a sequence of cells — matching how a real `.xlsx` structures
3//! `xl/workbook.xml`'s `<sheets>` list and each `xl/worksheets/sheetN.xml`'s `<sheetData>`
4//! (`CT_Workbook`/`CT_Worksheet`/`CT_Row`/`CT_Cell`).
5//!
6//! Text, numeric, boolean, date, formula, and rich-text cell values are modeled
7//! (`CellValue::Text`/`::Number`/`::Boolean`/`::Date`/`::Formula`/`::RichText`), and a cell may
8//! carry a [`CellFormat`] (number format, basic font, solid fill, alignment). A sheet may also
9//! carry [`ConditionalFormattingRule`]s, [`DataValidationRule`]s, and embedded charts
10//! ([`SheetChart`]). A workbook may also carry [`DefinedName`]s (named ranges/formulas/constants).
11
12/// A `.xlsx` workbook: a sequence of sheets, in the order they appear in Excel's own sheet tabs.
13#[derive(Debug, Default, Clone, PartialEq)]
14pub struct Workbook {
15 /// The sheets that make up this workbook, in order.
16 pub sheets: Vec<Sheet>,
17 /// Named ranges/formulas/constants defined on this workbook (`<definedNames>`/`<definedName>`,
18 /// `CT_DefinedNames`/`CT_DefinedName`), in order. Unlike a sheet, this lives at the workbook
19 /// level even for a name scoped to a single sheet (see [`DefinedName::local_sheet_id`]).
20 pub defined_names: Vec<DefinedName>,
21 /// Document metadata (`docProps/core.xml`/`docProps/app.xml`). Both parts are always written by
22 /// this crate (they're required for a strictly valid package), but before this point every
23 /// field in them was left empty/boilerplate.
24 pub properties: DocumentProperties,
25 /// Workbook *structure* protection (`<workbookProtection>`, `CT_WorkbookProtection`). Distinct
26 /// from [`Sheet::protection`]: this locks whether sheets can be
27 /// added/renamed/hidden/reordered/deleted, not cell editing within a sheet.
28 pub protection: Option<WorkbookProtection>,
29 /// Write-protection / "read-only recommended" (`<fileSharing>`, `CT_FileSharing`). Distinct
30 /// from [`protection`](Workbook::protection) (`<workbookProtection>`, point 26, which locks
31 /// *structure*): this instead controls the "This file should be opened as read-only" prompt
32 /// Excel shows when opening the file, and/or the legacy 16-bit write-reservation password some
33 /// older workflows still use. Confirmed via `sml.xsd`: `CT_Workbook`'s sequence puts
34 /// `fileSharing?` *before* `workbookPr?`/ `workbookProtection?`/`bookViews?`, i.e. as the very
35 /// first possible child of `<workbook>`.
36 pub write_protection: Option<WriteProtection>,
37 /// The 0-based index into [`sheets`](Workbook::sheets) of the sheet shown as active/selected
38 /// when the workbook is opened (`<bookViews><workbookView activeTab="..">`). `None` leaves
39 /// Excel's own default (the first sheet).
40 pub active_tab: Option<usize>,
41 /// References to other workbooks used by this workbook's formulas (`<externalReferences>` + one
42 /// `xl/externalLinks/externalLinkN.xml` part per entry, `CT_ExternalReference`). Storage only,
43 /// same posture as this crate's own formula text: nothing here resolves or evaluates a
44 /// reference into the other workbook's actual values.
45 pub external_links: Vec<ExternalWorkbookLink>,
46 /// A VBA project's raw compiled bytes (`xl/vbaProject.bin`), if this workbook carries macros.
47 /// Preserved *opaquely*: this crate can round-trip an existing macro-enabled workbook's
48 /// `vbaProject.bin` unchanged but cannot create, parse, or modify VBA source from scratch; the
49 /// OLE2/ CFB container format `vbaProject.bin` is internally structured in isn't decoded at
50 /// all. When `Some`, `Workbook::write_to` switches the workbook part's content type to the
51 /// macro-enabled variant (`..sheet.macroEnabled.main+xml`) — the caller is still responsible
52 /// for using a `.xlsm` file name, this crate has no notion of file extensions.
53 pub vba_project: Option<Vec<u8>>,
54 /// Workbook-wide calculation properties (`<calcPr>`, `CT_CalcPr`). `None` leaves Excel's own
55 /// default (automatic recalculation, no iterative calculation).
56 pub calculation: Option<CalculationProperties>,
57 /// Custom table style definitions (`xl/styles.xml`'s `<tableStyles>`), referenced by name from
58 /// any sheet's [`ExcelTable::table_style_name`]. Workbook-wide (styles.xml is a single shared
59 /// part), same posture as `defined_names` living here rather than per-sheet even for a
60 /// sheet-scoped name.
61 pub table_styles: Vec<TableStyle>,
62 /// Named cell styles (`xl/styles.xml`'s `<cellStyles>`/`<cellStyleXfs>`,
63 /// `CT_CellStyles`/`CT_CellStyleXfs`): the "Cell Styles" gallery Excel's Home ribbon shows
64 /// (`Good`/ `Bad`/`Neutral`, `Heading 1`-`4`, `Input`/`Output`/`Calculation`,
65 /// `Currency`/`Percent`/`Comma`..). Before this point every `cellXfs` entry this crate wrote
66 /// implicitly pointed at a single, hardcoded `"Normal"` `cellStyleXfs` entry (`xfId="0"`) — a
67 /// cell format that set [`CellFormat::named_style`] had no way to actually link back to one of
68 /// these named entries, so the association was silently lost on write. A [`CellFormat`] used
69 /// anywhere in this workbook (cell, row/column default..) that sets `named_style` to a name
70 /// found here gets that entry's index written as its `cellXfs` `xfId`, instead of the default
71 /// `0`; a name not found here silently falls back to `0` (`"Normal"`), same "best-effort, not
72 /// validated" posture as `ExcelTable::table_style_name` referencing a name not in this same
73 /// list. Workbook-wide (styles.xml is a single shared part), same posture as
74 /// `table_styles`/`defined_names` living here rather than per-sheet.
75 pub named_cell_styles: Vec<NamedCellStyle>,
76}
77
78/// A single worksheet (`xl/worksheets/sheetN.xml`, `CT_Worksheet`): a name (shown on Excel's sheet
79/// tab) plus a sequence of rows.
80#[derive(Debug, Default, Clone, PartialEq)]
81pub struct Sheet {
82 /// This sheet's name (`<sheet name="..">` in `xl/workbook.xml`), shown on Excel's own sheet
83 /// tab. Excel itself limits this to 31 characters and disallows a handful of characters (`: \ /
84 /// ? * [ ]`) — neither limit is enforced here, same "caller's responsibility" posture as
85 /// `word-ooxml`'s `PageSetup.orientation` not auto-swapping dimensions.
86 pub name: String,
87 /// The rows that make up this sheet's data, in order. A row's own 1-based row number (`<row
88 /// r="..">`) is simply this row's position in this `Vec` plus one — not a separate field — so
89 /// there is no way to represent a sparse sheet (e.g. data starting at row 5 with rows 1-4
90 /// genuinely absent) when *writing*; reading back a sparse sheet from a real `.xlsx` still
91 /// works (missing rows are filled in as empty), see `reader.rs`.
92 pub rows: Vec<Row>,
93 /// Conditional formatting rules applied to ranges of this sheet
94 /// (`<conditionalFormatting>`/`<cfRule>`, `CT_ConditionalFormatting`/ `CT_CfRule`), in order.
95 /// Each rule gets its own dedicated `<conditionalFormatting>` block when written (see
96 /// `writer.rs::write_conditional_formatting`) — real Excel groups multiple rules sharing the
97 /// same range under one block, but nothing in the schema requires it, so this crate doesn't
98 /// bother.
99 pub conditional_formatting_rules: Vec<ConditionalFormattingRule>,
100 /// Data validation rules applied to ranges of this sheet
101 /// (`<dataValidations>`/`<dataValidation>`, `CT_DataValidations`/ `CT_DataValidation`), in
102 /// order.
103 pub data_validation_rules: Vec<DataValidationRule>,
104 /// Merged cell ranges (`<mergeCells>`/`<mergeCell>`, `CT_MergeCells`), each written verbatim as
105 /// a range reference (e.g. `"A1:C1"`). This crate does not validate that ranges don't overlap
106 /// or that only the top-left cell of a merged range carries a value (both are the caller's
107 /// responsibility, same posture as
108 /// `ConditionalFormattingRule::range`/`DataValidationRule::range`).
109 pub merged_ranges: Vec<String>,
110 /// Per-column width/visibility/outline settings (`<cols>`/`<col>`, `CT_Cols`/`CT_Col`). Each
111 /// entry covers exactly one column (`min == max` in the written XML) — this crate doesn't
112 /// bother collapsing adjacent identical entries into a single `<col min="1" max="3"../>` range
113 /// the way real Excel does, same "less compact but schema-valid" posture as
114 /// `ConditionalFormattingRule`'s one-block-per-rule choice.
115 pub column_settings: Vec<ColumnSetting>,
116 /// The autofilter range (`<autoFilter ref="..">`, `CT_AutoFilter`), if any. Only the
117 /// dropdown-arrow range is modeled, not per-column filter criteria
118 /// (`CT_FilterColumn`/`CT_Filters`) — out of scope.
119 pub auto_filter_range: Option<String>,
120 /// Frozen (or split) panes (`<sheetView><pane../></sheetView>`, `CT_Pane`), if any. Only
121 /// "frozen" panes are modeled (the common case, e.g. freezing a header row/column), not "split"
122 /// panes (a draggable divider with no scroll-locking) — `CT_Pane`'s `state="split"` variant is
123 /// out of scope.
124 pub freeze_panes: Option<FreezePane>,
125 /// This sheet's print/page-setup settings (print area, repeated rows/columns, page orientation,
126 /// fit-to-page, header/footer text). `PrintSettings::default()` (everything `None`) writes no
127 /// print-related XML at all.
128 pub print_settings: PrintSettings,
129 /// This sheet's protection settings (`<sheetProtection>`, `CT_SheetProtection`), if protected.
130 pub protection: Option<SheetProtection>,
131 /// Per-cell hyperlinks (`<hyperlinks>`/`<hyperlink>`, `CT_Hyperlinks`/ `CT_Hyperlink`).
132 pub hyperlinks: Vec<CellHyperlink>,
133 /// Per-cell comments (legacy `xl/comments*.xml` + VML drawing, `CT_Comments`/`CT_Comment`).
134 pub comments: Vec<CellComment>,
135 /// A structured (`ListObject`) table on this sheet (`<tableParts>`/`<tablePart>` + a dedicated
136 /// `xl/tables/tableN.xml` part, `CT_Table`), if any. Only one table per sheet is modeled (this
137 /// crate's own scope choice, not an ECMA-376 limit — real Excel allows several) since a single
138 /// example is enough to prove the feature out; a second table on the same sheet is simply never
139 /// written.
140 pub table: Option<ExcelTable>,
141 /// This sheet's tab visibility (`<sheet state=".">` in `xl/workbook.xml` — a `CT_Sheet`
142 /// attribute, not part of the worksheet part itself) — extended to a genuine three-state
143 /// `SheetVisibility` (was a plain `bool` before this).
144 pub visibility: SheetVisibility,
145 /// This sheet's own display zoom percentage (`<sheetView zoomScale="..">`, e.g. `100` for
146 /// 100%). `None` leaves Excel's own default (100%).
147 pub zoom_scale: Option<u32>,
148 /// This sheet's tab color (`<sheetPr><tabColor rgb=".."/></sheetPr>`), as an 8-hex-digit ARGB
149 /// string. `None` leaves Excel's own default (no color, the plain gray/white tab).
150 pub tab_color: Option<String>,
151 /// Whether this sheet's view shows gridlines (`<sheetView showGridLines="..">`). `None` leaves
152 /// Excel's own default (shown).
153 pub show_grid_lines: Option<bool>,
154 /// Whether this sheet's view shows row/column headers (`<sheetView showRowColHeaders="..">`).
155 /// `None` leaves Excel's own default (shown).
156 pub show_row_col_headers: Option<bool>,
157 /// Whether this sheet's view shows zero values, or leaves those cells blank (`<sheetView
158 /// showZeros="..">`). `None` leaves Excel's own default (shown).
159 pub show_zeros: Option<bool>,
160 /// Whether this sheet's view is laid out right-to-left (`<sheetView rightToLeft="..">`). `None`
161 /// leaves Excel's own default (left-to-right).
162 pub right_to_left: Option<bool>,
163 /// The cell selected/active when this sheet is shown (`<selection activeCell=".." sqref="..">`,
164 /// both attributes written the same — this crate only models a single selected cell, not a
165 /// multi-cell selection). Distinct from [`Workbook::active_tab`]: this is the cell selected
166 /// *within* this sheet, not which sheet tab is shown. `None` leaves Excel's own default (`A1`).
167 pub active_cell: Option<String>,
168 /// This sheet's default column width, in Excel's own character-width units (`<sheetFormatPr
169 /// defaultColWidth="..">`). Distinct from [`ColumnSetting::width`](ColumnSetting): this is the
170 /// width used for every column *without* its own explicit [`ColumnSetting`]. `None` leaves
171 /// Excel's own default.
172 pub default_column_width: Option<f64>,
173 /// This sheet's default row height, in points (`<sheetFormatPr defaultRowHeight="..">`).
174 /// Distinct from [`Row::height`]: this is the height used for every row *without* its own
175 /// explicit height. `None` leaves Excel's own default (`15`, i.e. `defaultRowHeight` is always
176 /// written with a concrete value — `CT_SheetFormatPr::defaultRowHeight` is mandatory whenever
177 /// `<sheetFormatPr>` is written at all, unlike every other field on this type).
178 pub default_row_height: Option<f64>,
179 /// Manual page breaks after these 1-based row numbers (`<rowBreaks>`/`<brk>`, `CT_PageBreak`).
180 pub row_breaks: Vec<u32>,
181 /// Manual page breaks after these 1-based column numbers (`<colBreaks>`/`<brk>`).
182 pub column_breaks: Vec<u32>,
183 /// "What-if" scenarios (`<scenarios>`/`<scenario>`, `CT_Scenario`).
184 pub scenarios: Vec<Scenario>,
185 /// Per-column filter criteria for [`auto_filter_range`](Sheet::auto_filter_range)
186 /// (`<filterColumn>`/`<filters>`, `CT_FilterColumn`/`CT_Filters`). A no-op if
187 /// `auto_filter_range` is `None`. Only the simplest form (a fixed list of checked/visible
188 /// values) is modeled — `CT_Filters`'s custom criteria, top10, and date-grouping filter forms
189 /// are not.
190 pub auto_filter_columns: Vec<AutoFilterColumn>,
191 /// Suppressed error-checking indicators (the little green triangle in a cell's corner and its
192 /// "Ignore Error" right-click option), (`<ignoredErrors>`/`<ignoredError>`, `CT_IgnoredErrors`/
193 /// `CT_IgnoredError`). Each entry covers one range and one or more error categories; this crate
194 /// writes one `<ignoredError>` element per [`IgnoredError`] entry, same "less compact but
195 /// schema-valid" posture as `ColumnSetting`/ `ConditionalFormattingRule` not bothering to merge
196 /// adjacent/ overlapping ranges the way real Excel's UI sometimes does.
197 pub ignored_errors: Vec<IgnoredError>,
198 /// Multiple named protected ranges (`<protectedRanges>`/ `<protectedRange>`,
199 /// `CT_ProtectedRanges`/`CT_ProtectedRange`). Distinct from [`Sheet::protection`] (point 13,
200 /// `<sheetProtection>`): that locks/unlocks editing for the *whole* sheet at once (based on
201 /// each cell's own `locked` [`CellFormat`] flag), while this is Excel's older "Allow Users to
202 /// Edit Ranges" feature — one or more named ranges, each optionally its own password, that stay
203 /// editable while the rest of a protected sheet is locked. `CT_Worksheet`'s sequence puts
204 /// `protectedRanges` right after `sheetProtection`, before `scenarios` (confirmed via
205 /// `sml.xsd`).
206 pub protected_ranges: Vec<ProtectedRange>,
207 /// A remembered sort order for [`auto_filter_range`](Sheet::auto_filter_range)
208 /// (`<autoFilter><sortState>..</sortState></autoFilter>`). A no-op if `auto_filter_range` is
209 /// `None` (same posture as [`auto_filter_columns`](Sheet::auto_filter_columns)).
210 /// `CT_AutoFilter`'s own sequence puts `sortState` *after* `filterColumn*`, nested inside
211 /// `<autoFilter>` — distinct from [`ExcelTable::sort_state`], which is a sibling of a table's
212 /// own `<autoFilter>`, not a child of it (see that field's doc comment for the schema source).
213 pub sort_state: Option<SortState>,
214 /// Whether an outline/grouping's summary rows sit below their detail rows, or above
215 /// (`<sheetPr><outlinePr summaryBelow="..">`, `CT_SheetPr`/`CT_OutlinePr`). Relevant only when
216 /// [`Row::outline_level`]/column grouping is used. `None` leaves Excel's own default (`true`,
217 /// summary below); `Some(false)` matches the "summary above" layout common in accounting
218 /// templates. Confirmed via `sml.xsd`: `outlinePr` is `CT_SheetPr`'s second child, right after
219 /// `tabColor?`.
220 pub outline_summary_below: Option<bool>,
221 /// Whether an outline/grouping's summary columns sit to the right of their detail columns, or
222 /// to the left (`<outlinePr summaryRight="..">`) — same point 55, the column-axis counterpart
223 /// of `outline_summary_below`. `None` leaves Excel's own default (`true`, summary to the
224 /// right).
225 pub outline_summary_right: Option<bool>,
226 /// This sheet's DrawingML drawing — embedded pictures and/or charts, each anchored to a cell
227 /// range (`<drawing r:id="..">` referencing a dedicated `xl/drawings/drawingN.xml` part,
228 /// `CT_Drawing`/`xdr:wsDr`). Only one drawing part per sheet is modeled (this crate's own scope
229 /// choice, not an ECMA-376 limit — real Excel only ever has one anyway), same posture as
230 /// [`Sheet::table`]. `None` writes no `<drawing>` element and no drawing part at all.
231 pub drawing: Option<SheetDrawing>,
232}
233
234/// A single row of cells (`<row>`, `CT_Row`).
235#[derive(Debug, Default, Clone, PartialEq)]
236pub struct Row {
237 /// The cells that make up this row, in column order starting at column A. Like `Sheet.rows`, a
238 /// cell's column letter is derived from its position in this `Vec`, not stored separately —
239 /// writing a row always produces a dense `A`, `B`, `C`. sequence with no gaps. Reading back a
240 /// sparse row from a real `.xlsx` (Excel itself never writes a `<c>` element for a genuinely
241 /// empty cell) still works: any skipped column is filled in as `CellValue::Empty` so a cell's
242 /// index in this `Vec` always matches its real column, see `reader.rs`.
243 pub cells: Vec<Cell>,
244 /// This row's explicit height, in points (`<row ht=".." customHeight="1">`) — `None` uses
245 /// Excel's own default row height.
246 pub height: Option<f64>,
247 /// Whether this row is hidden (`<row hidden="1">`).
248 pub hidden: bool,
249 /// This row's outline (grouping) level (`<row outlineLevel="..">`, 0-7).
250 pub outline_level: u8,
251 /// Whether this row's outline group is shown collapsed (`<row collapsed="1">`, the "-"
252 /// summary-only state of the little outline button). Distinct from `outline_level` itself
253 /// (which rows belong to the group) — this is only the group's current expanded/collapsed
254 /// *display* state.
255 pub collapsed: bool,
256 /// This row's default cell format (`<row s="n" customFormat="1">`, an index into
257 /// `xl/styles.xml`'s `cellXfs`). Same "applies to any cell without its own explicit format,
258 /// storage only, caller keeps things straight" posture as [`ColumnSetting::style`] — see its
259 /// doc comment for the precedence note. `CT_Row`'s `customFormat` flag is always written
260 /// together with `s` when this is `Some` (confirmed via `sml.xsd`: `s` alone with
261 /// `customFormat` left at its `false` default means the row number's own displayed style, not
262 /// every cell in the row — real Excel always pairs the two when a row-level format is actually
263 /// set through its UI).
264 pub default_format: Option<CellFormat>,
265}
266
267/// A single cell (`<c>`, `CT_Cell`): a value plus an optional style.
268#[derive(Debug, Default, Clone, PartialEq)]
269pub struct Cell {
270 /// This cell's value.
271 pub value: CellValue,
272 /// This cell's style, if any. `None` means the cell uses Excel's own default style
273 /// (`xl/styles.xml`'s `cellXfs` index 0, the "Normal" style) — this crate never writes a
274 /// `<c>`'s `s` attribute for a cell with no format, matching how real Excel omits `s="0"` too
275 /// (it's the implicit default). Distinct `CellFormat`s across a workbook are deduplicated into
276 /// shared `cellXfs` entries by the writer, exactly like `CellValue::Text`'s shared-strings
277 /// deduplication — see `writer.rs::collect_cell_formats`.
278 pub format: Option<CellFormat>,
279}
280
281/// A cell's value.
282#[derive(Debug, Default, Clone, PartialEq)]
283pub enum CellValue {
284 /// No value — an empty cell. This crate never writes a `<c>` element for one (matching real
285 /// Excel output, which omits genuinely empty cells entirely) *unless* the cell carries a
286 /// [`CellFormat`], in which case a self-closing `<c r=".." s=".."/>` is written (a
287 /// styled-but-empty cell — real Excel does this too, e.g. for a pre-formatted but not yet
288 /// filled-in cell). This variant also exists so a sparse row read back from a real `.xlsx` can
289 /// still be represented densely, keeping a cell's `Vec` position equal to its real column. The
290 /// default value for a freshly-constructed `Cell`.
291 #[default]
292 Empty,
293 /// A text value (`<c t="s"><v>N</v></c>`, an index into the shared strings table
294 /// `xl/sharedStrings.xml` — `CT_Cell`'s `t="str"` in-cell-formula-result and `t="inlineStr"`
295 /// variants are not modeled, matching Excel's own default behavior of always deduplicating
296 /// repeated text through the shared strings table rather than writing it inline).
297 Text(String),
298 /// A numeric value (`<c><v>..</v></c>`, `CT_Cell`'s default/`"n"` type — a plain `f64`, written
299 /// with Rust's own default floating-point-to-string formatting; not a `CT_Cell`'s `t="b"`
300 /// (boolean) or `t="e"` (error) variant).
301 Number(f64),
302 /// A formula cell (`<c><f>..</f><v>..</v></c>`, `CT_CellFormula` + cached `CT_Cell/@v`).
303 /// Formula *evaluation* is out of scope — only the literal formula text and, if present, its
304 /// last-computed cached value are stored, never recomputed.
305 ///
306 /// A cell built via [`Cell::formula`] always has `Some` formula text; writing a cell with
307 /// `formula: None` falls back to writing just the cached value with no `<f>` element at all
308 /// (see `writer.rs::write_cell`). A real `.xlsx`'s `t="shared"` formula cells (both the group's
309 /// master and its followers) now read back as [`SharedFormula`](CellValue::SharedFormula)
310 /// instead of this variant — point 52, which replaced this variant's earlier "drop the
311 /// `si`/`ref`, keep only the cached value" limit with a dedicated, round-trippable
312 /// representation.
313 Formula {
314 /// The formula text, without a leading `=` (e.g. `"A2*B2"`).
315 formula: Option<String>,
316 /// The formula's last-computed value, if known.
317 cached_value: Option<f64>,
318 },
319 /// A boolean value (`<c t="b"><v>0|1</v></c>`). Fully round-trippable (unlike `Date`, see
320 /// below).
321 Boolean(bool),
322 /// A date/time value. **Write side only**: [`Cell::date`] converts an [`ExcelDateTime`] to the
323 /// serial-day number Excel actually stores (`<c><v>..</v></c>`, a plain numeric cell,
324 /// `CT_Cell`'s default type — SpreadsheetML has no dedicated date cell type, a date is *always*
325 /// just a number that happens to carry a date-shaped `numFmtId`), so writing one produces
326 /// exactly the same XML shape as `CellValue::Number`. Reading a workbook back never
327 /// reconstructs this variant, even for a cell this crate's own writer produced — a
328 /// date-formatted cell reads back as `CellValue::Number(serial)`, the caller's own
329 /// responsibility to reinterpret using the cell's `CellFormat::number_format`, same honest
330 /// "storage, not clever inference" posture as this crate's formula-evaluation scope limit. Pair
331 /// `Cell::date` with a `CellFormat::with_number_format` (e.g. `"dd/mm/yyyy"`) for Excel to
332 /// actually display it as a date rather than a raw serial number — this crate does not do that
333 /// automatically (see `Cell::date`'s doc comment), matching `word-ooxml`'s
334 /// `PageSetup.orientation` "caller's responsibility" precedent.
335 Date(ExcelDateTime),
336 /// A text value with per-run character formatting (`<si><r>..</r></si>` in
337 /// `xl/sharedStrings.xml` — the "rich text" shared string form, as opposed to
338 /// `CellValue::Text`'s plain `<si><t>..</t></si>` form). Fully round-trippable.
339 RichText(Vec<RichTextRun>),
340 /// An array (CSE, "Ctrl+Shift+Enter") formula (`<c><f t="array" ref=".">.</f><v>.</v></c>`,
341 /// `CT_CellFormula`'s `t="array"` variant) — distinct from `Formula`'s plain/shared forms. Real
342 /// Excel writes the literal `<f t="array" ref=".">` only on the array range's top-left (anchor)
343 /// cell — every other cell the range covers gets no `<f>` at all, just its own cached `<v>`
344 /// (this crate's own writer only ever builds a `Cell` holding this variant for the anchor cell
345 /// itself; the caller is responsible for the other cells in `range` carrying plain
346 /// `CellValue::Number`/`Empty` cells with the right cached values, same "caller keeps things in
347 /// sync" posture as `ExcelTable::columns` needing to match the header row's actual text).
348 /// Reading a real `.xlsx`'s array-formula continuation cells (which carry no `<f>` at all)
349 /// simply reads them back as whatever plain value type their own `<v>`/`t` attribute says —
350 /// this crate does not reconstruct that they were once part of an array's range.
351 ArrayFormula {
352 /// The formula text, without a leading `=`.
353 formula: String,
354 /// The array's full range this formula fills (e.g. `"B2:B10"`) — a single-cell array
355 /// formula still needs its own cell as the range (e.g. `"B2:B2"`).
356 range: String,
357 /// The formula's last-computed value for this (the anchor) cell, if known.
358 cached_value: Option<f64>,
359 },
360 /// One cell of a shared-formula group (`<c><f t="shared" si=".." ref="..">..</f><v>..</v></c>`
361 /// on the group's master cell, or `<c><f t="shared" si=".."/><v>..</v></c>` on a follower cell
362 /// — `CT_CellFormula`'s `t="shared"` variant). Confirmed against `sml.xsd`: `si` (the group's
363 /// shared index) is the only attribute present on *every* cell in the group; `ref` (the group's
364 /// full range) is only meaningful — and, matching real Excel's own convention, only ever
365 /// written — on the master cell, the one carrying the actual formula text. This crate does
366 /// **not** compute or shift relative references for follower cells (e.g. deriving `"A3*B3"`
367 /// from a master's `"A2*B2"`) — same "storage, not evaluation" posture as
368 /// [`Formula`](CellValue::Formula) itself; the caller supplies each follower's own
369 /// already-correct formula text (or `None`, matching what real Excel itself writes for a
370 /// follower — see [`Cell::shared_formula_follower`]).
371 SharedFormula {
372 /// The formula text, without a leading `=` — `Some` on the group's master cell, `None` on a
373 /// follower (real Excel writes a follower's `<f>` with no text content at all, just
374 /// `t`/`si`).
375 formula: Option<String>,
376 /// This formula group's shared index (`si`) — identical across every cell (master and every
377 /// follower) belonging to the same group; a workbook may have several independent groups,
378 /// each with its own index starting from `0`.
379 group_index: u32,
380 /// The group's full range (`ref`) — `Some` only on the master cell (the one written first,
381 /// real Excel omits it on every follower).
382 range: Option<String>,
383 /// This cell's own last-computed value, if known.
384 cached_value: Option<f64>,
385 },
386}
387
388/// A calendar date/time, used to build a [`CellValue::Date`] (`Cell::date`) — deliberately not tied
389/// to any external date/time crate (this project's dependency policy only forbids third-party
390/// *OOXML-implementing* crates, but a plain field struct is simpler still and avoids the question
391/// entirely). Only the proleptic Gregorian calendar and Excel's default "1900 date system" are
392/// supported — the legacy "1904 date system" (an old Mac Excel option, `<workbookPr
393/// date1904="1"/>`) is not modeled, same "one well-defined behavior, not every historical Excel
394/// quirk" posture as elsewhere in this crate.
395#[derive(Debug, Clone, Copy, PartialEq, Eq)]
396pub struct ExcelDateTime {
397 pub year: i32,
398 pub month: u32,
399 pub day: u32,
400 pub hour: u32,
401 pub minute: u32,
402 pub second: u32,
403}
404
405impl ExcelDateTime {
406 /// Creates a date with no time component (midnight).
407 pub fn date(year: i32, month: u32, day: u32) -> Self {
408 Self {
409 year,
410 month,
411 day,
412 hour: 0,
413 minute: 0,
414 second: 0,
415 }
416 }
417
418 /// Sets a time-of-day component and returns the date for chaining.
419 pub fn with_time(mut self, hour: u32, minute: u32, second: u32) -> Self {
420 self.hour = hour;
421 self.minute = minute;
422 self.second = second;
423 self
424 }
425}
426
427/// One run of a [`CellValue::RichText`] value: a span of text sharing the same character formatting
428/// (`<r><rPr>..</rPr><t>..</t></r>`, `CT_RElt`) — a deliberately scoped-down subset of
429/// `CT_RPrElt`'s full run-property surface (bold/italic/color/size only, same subset `CellFormat`'s
430/// own font fields already cover; underline/strike/ vertical-align/font-family are not modeled).
431#[derive(Debug, Clone, PartialEq)]
432pub struct RichTextRun {
433 pub text: String,
434 pub bold: bool,
435 pub italic: bool,
436 /// The run's font color, as an 8-hex-digit ARGB string.
437 pub font_color: Option<String>,
438 /// The run's font size, in points.
439 pub font_size: Option<f64>,
440 /// The run's font name/family (e.g. `"Calibri"`).
441 pub font_name: Option<String>,
442 /// Underline (plain single underline only, same posture as [`CellFormat::underline`]).
443 pub underline: bool,
444 /// Strikethrough.
445 pub strike: bool,
446}
447
448impl RichTextRun {
449 /// Creates a plain (no formatting) run.
450 pub fn new(text: impl Into<String>) -> Self {
451 Self {
452 text: text.into(),
453 bold: false,
454 italic: false,
455 font_color: None,
456 font_size: None,
457 font_name: None,
458 underline: false,
459 strike: false,
460 }
461 }
462
463 /// Sets bold and returns the run for chaining.
464 pub fn with_bold(mut self, bold: bool) -> Self {
465 self.bold = bold;
466 self
467 }
468
469 /// Sets italic and returns the run for chaining.
470 pub fn with_italic(mut self, italic: bool) -> Self {
471 self.italic = italic;
472 self
473 }
474
475 /// Sets the font color (8-hex-digit ARGB) and returns the run for chaining.
476 pub fn with_font_color(mut self, color: impl Into<String>) -> Self {
477 self.font_color = Some(color.into());
478 self
479 }
480
481 /// Sets the font size, in points, and returns the run for chaining.
482 pub fn with_font_size(mut self, size: f64) -> Self {
483 self.font_size = Some(size);
484 self
485 }
486
487 /// Sets the font name/family and returns the run for chaining.
488 pub fn with_font_name(mut self, font_name: impl Into<String>) -> Self {
489 self.font_name = Some(font_name.into());
490 self
491 }
492
493 /// Sets underline and returns the run for chaining.
494 pub fn with_underline(mut self, underline: bool) -> Self {
495 self.underline = underline;
496 self
497 }
498
499 /// Sets strikethrough and returns the run for chaining.
500 pub fn with_strike(mut self, strike: bool) -> Self {
501 self.strike = strike;
502 self
503 }
504}
505
506/// A cell's style: a number format, basic font attributes (bold/italic/size/color), a solid
507/// background fill color, borders, and horizontal/vertical alignment.
508///
509/// Not deduplicated on the model itself — the writer deduplicates identical `CellFormat`s into
510/// shared `xl/styles.xml` entries (`numFmts`/`fonts`/`fills`/`cellXfs`) when serializing, exactly
511/// like `Workbook`'s own shared-strings deduplication for repeated text (see
512/// `writer.rs::collect_cell_formats`/`build_styles`).
513#[derive(Debug, Default, Clone, PartialEq)]
514pub struct CellFormat {
515 /// A custom number format code (e.g. `"0.00"`, `"#,##0"`). Always registered as a *custom*
516 /// format (`numFmtId` >= 164) when written, even if the code happens to match one of Excel's
517 /// built-in format codes — this crate does not maintain the built-in-format lookup table
518 /// (`BuiltinFormats`), so it never reuses a built-in id. Reading back a real `.xlsx`'s built-in
519 /// number format (any `numFmtId` not declared in that file's own `<numFmts>`) is not modeled
520 /// either: it reads back as `None`.
521 pub number_format: Option<String>,
522 /// Bold font weight.
523 pub bold: bool,
524 /// Italic font style.
525 pub italic: bool,
526 /// Font size in points. `None` uses the workbook default (11pt) — note that writing *any* other
527 /// font attribute (bold, italic, or a color) also resolves this to the default 11pt in the
528 /// generated `xl/styles.xml` font entry (a single `<font>` element can't leave "unset"
529 /// attributes that fall back independently), so reading a round-tripped format back may see
530 /// `Some(11.0)` rather than `None` — a known, accepted round-trip nuance.
531 pub font_size: Option<f64>,
532 /// Font color, as an 8-hex-digit ARGB string (e.g. `"FFFF0000"` for opaque red). `None` uses
533 /// the workbook default (opaque black) — same round-trip nuance as `font_size` applies here too
534 /// (and to `font_name` below).
535 pub font_color: Option<String>,
536 /// Solid background fill color, as an 8-hex-digit ARGB string. `None` leaves the cell unfilled.
537 pub fill_color: Option<String>,
538 /// Horizontal alignment. `None` uses Excel's own default (`general` — text left-aligned,
539 /// numbers right-aligned).
540 pub horizontal_alignment: Option<HorizontalAlignment>,
541 /// Vertical alignment. `None` uses Excel's own default (`bottom`).
542 pub vertical_alignment: Option<VerticalAlignment>,
543 /// This cell's borders (top/bottom/left/right/diagonal). `Border::default()` (all
544 /// `None`/`false`) draws no border at all, matching every other `CellFormat` field's "unset
545 /// means Excel's own default" posture.
546 pub border: Border,
547 /// Font name/family (e.g. `"Calibri"`, `"Arial"`). `None` uses the workbook default (Excel's
548 /// own "Calibri"). Only a single Latin font name is modeled, not `CT_Font`'s separate
549 /// east-Asian/complex-script overrides (`family`/`scheme`/`charset` are not modeled either).
550 /// Same unset-resolves-to-the-default round-trip nuance as `font_size`/ `font_color` above
551 /// applies here too: writing any other font attribute also resolves this to `Some("Calibri")`
552 /// in the generated `<font>` entry, so an originally-`None` `font_name` may read back as
553 /// `Some("Calibri")` rather than `None`.
554 pub font_name: Option<String>,
555 /// Underline. Only a plain single underline is modeled (`CT_Font`'s `ST_UnderlineValues` has
556 /// several more: `double`/`singleAccounting`/ `doubleAccounting`) — same on/off posture
557 /// `RichTextRun`'s `bold`/`italic` already use (unlike `word-ooxml`'s much richer
558 /// `UnderlineStyle` enum).
559 pub underline: bool,
560 /// Strikethrough.
561 pub strike: bool,
562 /// Wraps text onto multiple lines within the cell (`wrapText`).
563 pub wrap_text: bool,
564 /// Indentation level (`indent`, roughly 3 character-widths per level).
565 pub indent: Option<u32>,
566 /// Text rotation, in Excel's own raw `ST_CellAlignment`/`textRotation` units — `0` to `90` for
567 /// counter-clockwise degrees, `91` to `180` for clockwise degrees (stored as `90 - angle`, e.g.
568 /// a plain `-45°` tilt is `135`), or the special value `255` for top-to-bottom vertical text.
569 /// Written and read back verbatim — converting a plain signed angle into this encoding is the
570 /// caller's responsibility, same "storage, not a conversion helper" posture as
571 /// `PrintSettings::header`'s raw `&L`/`&C`/`&R` codes.
572 pub text_rotation: Option<u16>,
573 /// Shrinks the font size to fit the cell's width instead of wrapping or overflowing
574 /// (`shrinkToFit`).
575 pub shrink_to_fit: bool,
576 /// A non-solid pattern fill (`<patternFill patternType="..">` with a foreground/background
577 /// color pair). Mutually exclusive with
578 /// [`fill_color`](CellFormat::fill_color)/[`gradient_fill`](CellFormat::gradient_fill) in
579 /// practice (Excel itself only ever uses one `<fill>` child); if more than one is set, the
580 /// writer prefers `gradient_fill`, then `pattern_fill`, then `fill_color`, see
581 /// `writer.rs::resolve_fill`.
582 pub pattern_fill: Option<PatternFill>,
583 /// A gradient fill (`<gradientFill>`). Only the linear (angle-based) gradient form is modeled,
584 /// not `CT_GradientFill`'s "path" (radial, from-center) form. See `pattern_fill`'s doc comment
585 /// for the precedence rule when more than one fill field is set.
586 pub gradient_fill: Option<GradientFill>,
587 /// Whether this cell is locked when the sheet is protected (`<protection locked=".">`,
588 /// `CT_CellProtection`) — independent of [`Sheet::protection`] actually being turned on: Excel
589 /// itself treats every cell as `locked` by default, so protection only has a visible effect
590 /// once [`Sheet::protection`] is `Some`. `None` leaves Excel's own default (locked).
591 pub locked: Option<bool>,
592 /// Whether this cell's formula is hidden from the formula bar when the sheet is protected
593 /// (`<protection hidden="..">`). `None` leaves Excel's own default (not hidden). Meaningless on
594 /// a non-formula cell, same as real Excel.
595 pub formula_hidden: Option<bool>,
596 /// Links this format to one of [`Workbook::named_cell_styles`] by name (resolved into that
597 /// entry's index, written as this format's `cellXfs` `xfId` attribute). This is what makes a
598 /// cell show up as e.g. "Good"/"Heading 1" in Excel's own Cell Styles gallery
599 /// (highlighted/selected) rather than merely happening to look the same. `None` (the default)
600 /// writes `xfId="0"`, i.e. based on the implicit `"Normal"` style, same as every `CellFormat`
601 /// before this point. A name not found in `Workbook::named_cell_styles` at write time silently
602 /// falls back to `0` too — see that field's own doc comment.
603 pub named_style: Option<String>,
604}
605
606/// A cell's horizontal alignment (`ST_HorizontalAlignment`) — only the three most common values are
607/// modeled; `general`/`fill`/`justify`/ `centerContinuous`/`distributed` are not (they read back as
608/// `None`).
609#[derive(Debug, Clone, Copy, PartialEq, Eq)]
610pub enum HorizontalAlignment {
611 Left,
612 Center,
613 Right,
614}
615
616/// A cell's vertical alignment (`ST_VerticalAlignment`) — only the three most common values are
617/// modeled; `justify`/`distributed` are not (they read back as `None`).
618#[derive(Debug, Clone, Copy, PartialEq, Eq)]
619pub enum VerticalAlignment {
620 Top,
621 Center,
622 Bottom,
623}
624
625/// A border line style (`ST_BorderStyle`) — only the handful of styles a real spreadsheet actually
626/// uses day to day are modeled; `hair`/`mediumDashed`/`dashDot`/`mediumDashDot`/`dashDotDot`/
627/// `mediumDashDotDot`/`slantDashDot` are not (out of scope, same scope-reduction posture as
628/// elsewhere in this crate).
629#[derive(Debug, Clone, Copy, PartialEq, Eq)]
630pub enum BorderStyle {
631 Thin,
632 Medium,
633 Thick,
634 Dashed,
635 Dotted,
636 Double,
637 Hair,
638}
639
640/// One edge of a [`Border`] (`<left>`/`<right>`/`<top>`/`<bottom>`/ `<diagonal>`, `CT_BorderPr`): a
641/// line style plus an optional color.
642#[derive(Debug, Clone, PartialEq)]
643pub struct BorderEdge {
644 pub style: BorderStyle,
645 /// The edge's color, as an 8-hex-digit ARGB string. `None` leaves the edge at Excel's own
646 /// default (opaque black).
647 pub color: Option<String>,
648}
649
650impl BorderEdge {
651 /// Creates a border edge with the given style, no explicit color (opaque black, Excel's own
652 /// default).
653 pub fn new(style: BorderStyle) -> Self {
654 Self { style, color: None }
655 }
656
657 /// Sets the edge's color (8-hex-digit ARGB) and returns it for chaining.
658 pub fn with_color(mut self, color: impl Into<String>) -> Self {
659 self.color = Some(color.into());
660 self
661 }
662}
663
664/// A cell's borders (`<border>`, `CT_Border`) — up to four edges plus an optional diagonal.
665/// `Border::default()` (every field `None`/`false`) draws no border at all.
666#[derive(Debug, Default, Clone, PartialEq)]
667pub struct Border {
668 pub top: Option<BorderEdge>,
669 pub bottom: Option<BorderEdge>,
670 pub left: Option<BorderEdge>,
671 pub right: Option<BorderEdge>,
672 /// The diagonal line's style/color — only drawn if [`diagonal_up`](Border::diagonal_up) and/or
673 /// [`diagonal_down`](Border::diagonal_down) is also set (`CT_Border`'s own
674 /// `diagonalUp`/`diagonalDown` attributes live on the parent `<border>` element, not
675 /// `<diagonal>` itself).
676 pub diagonal: Option<BorderEdge>,
677 /// Draws the diagonal from bottom-left to top-right.
678 pub diagonal_up: bool,
679 /// Draws the diagonal from top-left to bottom-right.
680 pub diagonal_down: bool,
681}
682
683impl Border {
684 /// Creates a `Border` with no edges at all.
685 pub fn new() -> Self {
686 Self::default()
687 }
688
689 /// Sets the top edge and returns the border for chaining.
690 pub fn with_top(mut self, edge: BorderEdge) -> Self {
691 self.top = Some(edge);
692 self
693 }
694
695 /// Sets the bottom edge and returns the border for chaining.
696 pub fn with_bottom(mut self, edge: BorderEdge) -> Self {
697 self.bottom = Some(edge);
698 self
699 }
700
701 /// Sets the left edge and returns the border for chaining.
702 pub fn with_left(mut self, edge: BorderEdge) -> Self {
703 self.left = Some(edge);
704 self
705 }
706
707 /// Sets the right edge and returns the border for chaining.
708 pub fn with_right(mut self, edge: BorderEdge) -> Self {
709 self.right = Some(edge);
710 self
711 }
712
713 /// Sets the diagonal edge and whether it runs bottom-left-to-top-right and/or
714 /// top-left-to-bottom-right, and returns the border for chaining.
715 pub fn with_diagonal(mut self, edge: BorderEdge, up: bool, down: bool) -> Self {
716 self.diagonal = Some(edge);
717 self.diagonal_up = up;
718 self.diagonal_down = down;
719 self
720 }
721
722 /// A convenience for the common case of the same style/color on all four sides (a full box
723 /// border) — equivalent to calling
724 /// [`with_top`](Border::with_top)/[`with_bottom`](Border::with_bottom)/
725 /// [`with_left`](Border::with_left)/[`with_right`](Border::with_right) with four identical
726 /// edges.
727 pub fn all(style: BorderStyle, color: impl Into<String>) -> Self {
728 let color = color.into();
729 Self {
730 top: Some(BorderEdge::new(style).with_color(color.clone())),
731 bottom: Some(BorderEdge::new(style).with_color(color.clone())),
732 left: Some(BorderEdge::new(style).with_color(color.clone())),
733 right: Some(BorderEdge::new(style).with_color(color)),
734 diagonal: None,
735 diagonal_up: false,
736 diagonal_down: false,
737 }
738 }
739}
740
741impl Workbook {
742 /// Creates an empty workbook (no sheets).
743 pub fn new() -> Self {
744 Self::default()
745 }
746
747 /// Appends a sheet and returns the workbook for chaining.
748 pub fn with_sheet(mut self, sheet: Sheet) -> Self {
749 self.sheets.push(sheet);
750 self
751 }
752
753 /// Appends an empty sheet with the given name and returns a mutable reference to it, to build
754 /// it up in place (e.g. `workbook.add_sheet("Feuil1").write_cell(1, 1, Cell::text("Hi"))`). A
755 /// `&mut self` counterpart to [`Self::with_sheet`], for building a workbook sheet-by-sheet in a
756 /// loop without the consuming builder's `workbook = workbook.with_sheet(..)` reassignment on
757 /// every iteration.
758 pub fn add_sheet(&mut self, name: impl Into<String>) -> &mut Sheet {
759 self.sheets.push(Sheet::new(name));
760 self.sheets.last_mut().expect("a sheet was just pushed")
761 }
762
763 /// Appends a defined name and returns the workbook for chaining.
764 pub fn with_defined_name(mut self, defined_name: DefinedName) -> Self {
765 self.defined_names.push(defined_name);
766 self
767 }
768
769 /// Sets this workbook's calculation properties and returns it for chaining.
770 pub fn with_calculation(mut self, calculation: CalculationProperties) -> Self {
771 self.calculation = Some(calculation);
772 self
773 }
774
775 /// Appends a custom table style definition and returns the workbook for chaining. See
776 /// [`ExcelTable::table_style_name`]'s doc comment for how a table references one of these by
777 /// name.
778 pub fn with_table_style(mut self, table_style: TableStyle) -> Self {
779 self.table_styles.push(table_style);
780 self
781 }
782
783 /// Sets this workbook's write-protection and returns it for chaining.
784 pub fn with_write_protection(mut self, write_protection: WriteProtection) -> Self {
785 self.write_protection = Some(write_protection);
786 self
787 }
788
789 /// Appends a named cell style definition and returns the workbook for chaining. See
790 /// [`CellFormat::named_style`]'s doc comment for how a format references one of these by name.
791 pub fn with_named_cell_style(mut self, named_cell_style: NamedCellStyle) -> Self {
792 self.named_cell_styles.push(named_cell_style);
793 self
794 }
795}
796
797/// A workbook's recalculation mode (`<calcPr calcMode="..">`, `ST_CalcMode`).
798#[derive(Debug, Clone, Copy, PartialEq, Eq)]
799pub enum CalculationMode {
800 /// Recalculates automatically whenever a dependency changes (Excel's own default).
801 Automatic,
802 /// Recalculates automatically except for data tables (`ST_CalcMode`'s `"autoNoTable"`) — data
803 /// tables (`TABLE()` what-if formulas) only recalculate on a manual trigger, everything else
804 /// stays automatic.
805 AutomaticExceptTables,
806 /// Only recalculates when the user explicitly triggers it (F9 or "Calculate Now").
807 Manual,
808}
809
810/// Workbook-wide calculation properties (`<calcPr>`, `CT_CalcPr`). This crate never evaluates
811/// formulas itself (see `CellValue::Formula`'s doc comment) — these settings are pure storage,
812/// telling *Excel* how to recalculate once the file is opened, same "storage, not evaluation"
813/// posture as a formula's own text.
814#[derive(Debug, Clone, Copy, PartialEq)]
815pub struct CalculationProperties {
816 /// The recalculation mode. `None` leaves Excel's own default (automatic).
817 pub mode: Option<CalculationMode>,
818 /// Enables iterative calculation, needed for formulas with intentional circular references
819 /// (`<calcPr iterate="1">`) — without this, Excel refuses to open a file containing a genuine
820 /// circular reference without warning the user first.
821 pub iterate: bool,
822}
823
824impl CalculationProperties {
825 /// Calculation properties with Excel's own defaults (automatic mode, no iterative calculation)
826 /// — only useful as a starting point for the `with_*` builders, since this is equivalent to
827 /// leaving `Workbook.calculation` as `None` entirely.
828 pub fn new() -> Self {
829 Self {
830 mode: None,
831 iterate: false,
832 }
833 }
834
835 /// Sets the recalculation mode and returns the properties for chaining.
836 pub fn with_mode(mut self, mode: CalculationMode) -> Self {
837 self.mode = Some(mode);
838 self
839 }
840
841 /// Enables iterative calculation and returns the properties for chaining.
842 pub fn with_iterate(mut self, iterate: bool) -> Self {
843 self.iterate = iterate;
844 self
845 }
846}
847
848impl Default for CalculationProperties {
849 fn default() -> Self {
850 Self::new()
851 }
852}
853
854impl Sheet {
855 /// Creates an empty sheet (no rows) with the given name.
856 pub fn new(name: impl Into<String>) -> Self {
857 Self {
858 name: name.into(),
859 rows: Vec::new(),
860 conditional_formatting_rules: Vec::new(),
861 data_validation_rules: Vec::new(),
862 merged_ranges: Vec::new(),
863 column_settings: Vec::new(),
864 auto_filter_range: None,
865 freeze_panes: None,
866 print_settings: PrintSettings::default(),
867 protection: None,
868 hyperlinks: Vec::new(),
869 comments: Vec::new(),
870 table: None,
871 visibility: SheetVisibility::Visible,
872 zoom_scale: None,
873 row_breaks: Vec::new(),
874 column_breaks: Vec::new(),
875 scenarios: Vec::new(),
876 auto_filter_columns: Vec::new(),
877 tab_color: None,
878 show_grid_lines: None,
879 show_row_col_headers: None,
880 show_zeros: None,
881 right_to_left: None,
882 active_cell: None,
883 default_column_width: None,
884 default_row_height: None,
885 ignored_errors: Vec::new(),
886 protected_ranges: Vec::new(),
887 sort_state: None,
888 outline_summary_below: None,
889 outline_summary_right: None,
890 drawing: None,
891 }
892 }
893
894 /// Appends a row and returns the sheet for chaining.
895 pub fn with_row(mut self, row: Row) -> Self {
896 self.rows.push(row);
897 self
898 }
899
900 /// Writes `cell` at the given 1-based `(row, col)` position — `(1, 1)` is `A1` — growing `rows`
901 /// and the target row's own `cells` with empty padding cells as needed to reach it, and returns
902 /// a mutable reference to the cell just written. Direct coordinate addressing, for the common
903 /// case of writing cells out of strict top-to-bottom, left-to-right order (or just one cell at
904 /// a time in a loop) without assembling a full [`Row`] by hand first. 1-based, to match this
905 /// crate's own existing convention — a row's or cell's position in its `Vec` plus one (see
906 /// [`Sheet::rows`]'s and [`Row::cells`]'s doc comments) — so a `Sheet` never mixes two
907 /// different numbering schemes depending on which method touched it.
908 ///
909 /// Padding cells are genuinely empty ([`Cell::new`]/[`Cell::default`]), not written out as
910 /// `<c>` elements at all unless later given a format — same posture as a sparse [`Row`] built
911 /// by hand.
912 ///
913 /// Accepts anything convertible to a [`Cell`] — a `Cell` itself (built via
914 /// [`Cell::text`]/[`Cell::formula`]/[`Cell::date`]/etc. for the cases below that need one made
915 /// explicitly), or, for the three unambiguous cases, the raw value directly: `&str`/`String` (→
916 /// [`CellValue::Text`]), `f64` (→ [`CellValue::Number`]), `bool` (→ [`CellValue::Boolean`]) —
917 /// the common case a caller writing cells one at a time reaches for most often. Formulas,
918 /// dates, rich text, and array formulas are deliberately *not* covered by an automatic
919 /// conversion — there's no unambiguous way to tell "this is a formula" from a plain string, or
920 /// "this number is a date" from a plain number, so those still go through their own `Cell::*`
921 /// constructor.
922 ///
923 /// # Panics
924 ///
925 /// Panics if `row` or `col` is `0` (1-based, so `0` is never a valid position).
926 pub fn write_cell(&mut self, row: u32, col: u32, cell: impl Into<Cell>) -> &mut Cell {
927 assert!(
928 row >= 1 && col >= 1,
929 "write_cell uses 1-based row/col indices, got ({row}, {col})"
930 );
931 let cell = cell.into();
932 let row_index = (row - 1) as usize;
933 let col_index = (col - 1) as usize;
934
935 if self.rows.len() <= row_index {
936 self.rows.resize_with(row_index + 1, Row::default);
937 }
938 let target_row = &mut self.rows[row_index];
939 if target_row.cells.len() <= col_index {
940 target_row.cells.resize_with(col_index + 1, Cell::default);
941 }
942 target_row.cells[col_index] = cell;
943 &mut target_row.cells[col_index]
944 }
945
946 /// Appends a conditional formatting rule and returns the sheet for chaining.
947 pub fn with_conditional_formatting_rule(mut self, rule: ConditionalFormattingRule) -> Self {
948 self.conditional_formatting_rules.push(rule);
949 self
950 }
951
952 /// Appends a data validation rule and returns the sheet for chaining.
953 pub fn with_data_validation_rule(mut self, rule: DataValidationRule) -> Self {
954 self.data_validation_rules.push(rule);
955 self
956 }
957
958 /// Appends a merged cell range (e.g. `"A1:C1"`) and returns the sheet for chaining.
959 pub fn with_merged_range(mut self, range: impl Into<String>) -> Self {
960 self.merged_ranges.push(range.into());
961 self
962 }
963
964 /// Appends a column setting and returns the sheet for chaining.
965 pub fn with_column_setting(mut self, setting: ColumnSetting) -> Self {
966 self.column_settings.push(setting);
967 self
968 }
969
970 /// Sets the autofilter range and returns the sheet for chaining.
971 pub fn with_auto_filter_range(mut self, range: impl Into<String>) -> Self {
972 self.auto_filter_range = Some(range.into());
973 self
974 }
975
976 /// Sets frozen panes and returns the sheet for chaining.
977 pub fn with_freeze_panes(mut self, freeze_panes: FreezePane) -> Self {
978 self.freeze_panes = Some(freeze_panes);
979 self
980 }
981
982 /// Sets this sheet's print settings and returns the sheet for chaining.
983 pub fn with_print_settings(mut self, print_settings: PrintSettings) -> Self {
984 self.print_settings = print_settings;
985 self
986 }
987
988 /// Sets this sheet's protection and returns the sheet for chaining.
989 pub fn with_protection(mut self, protection: SheetProtection) -> Self {
990 self.protection = Some(protection);
991 self
992 }
993
994 /// Appends a cell hyperlink and returns the sheet for chaining.
995 pub fn with_hyperlink(mut self, hyperlink: CellHyperlink) -> Self {
996 self.hyperlinks.push(hyperlink);
997 self
998 }
999
1000 /// Appends a cell comment and returns the sheet for chaining.
1001 pub fn with_comment(mut self, comment: CellComment) -> Self {
1002 self.comments.push(comment);
1003 self
1004 }
1005
1006 /// Sets this sheet's structured table and returns the sheet for chaining.
1007 pub fn with_table(mut self, table: ExcelTable) -> Self {
1008 self.table = Some(table);
1009 self
1010 }
1011
1012 /// Sets this sheet's drawing (embedded pictures/charts) and returns the sheet for chaining.
1013 pub fn with_drawing(mut self, drawing: SheetDrawing) -> Self {
1014 self.drawing = Some(drawing);
1015 self
1016 }
1017
1018 /// Sets this sheet's tab visibility and returns the sheet for chaining — replacing the old
1019 /// `with_hidden(bool)` (removed, pre-1.0 breaking change): use
1020 /// `with_visibility(SheetVisibility::Hidden)` for what used to be `with_hidden(true)`.
1021 pub fn with_visibility(mut self, visibility: SheetVisibility) -> Self {
1022 self.visibility = visibility;
1023 self
1024 }
1025
1026 /// Sets this sheet's display zoom percentage and returns the sheet for chaining.
1027 pub fn with_zoom_scale(mut self, zoom_scale: u32) -> Self {
1028 self.zoom_scale = Some(zoom_scale);
1029 self
1030 }
1031
1032 /// Sets this sheet's tab color (8-hex-digit ARGB) and returns the sheet for chaining.
1033 pub fn with_tab_color(mut self, color: impl Into<String>) -> Self {
1034 self.tab_color = Some(color.into());
1035 self
1036 }
1037
1038 /// Sets whether this sheet's view shows gridlines and returns the sheet for chaining.
1039 pub fn with_show_grid_lines(mut self, show: bool) -> Self {
1040 self.show_grid_lines = Some(show);
1041 self
1042 }
1043
1044 /// Sets whether this sheet's view shows row/column headers and returns the sheet for chaining.
1045 pub fn with_show_row_col_headers(mut self, show: bool) -> Self {
1046 self.show_row_col_headers = Some(show);
1047 self
1048 }
1049
1050 /// Sets whether this sheet's view shows zero values and returns the sheet for chaining.
1051 pub fn with_show_zeros(mut self, show: bool) -> Self {
1052 self.show_zeros = Some(show);
1053 self
1054 }
1055
1056 /// Sets whether this sheet's view is laid out right-to-left and returns the sheet for chaining.
1057 pub fn with_right_to_left(mut self, right_to_left: bool) -> Self {
1058 self.right_to_left = Some(right_to_left);
1059 self
1060 }
1061
1062 /// Sets the cell selected/active when this sheet is shown (e.g. `"B3"`) and returns the sheet
1063 /// for chaining.
1064 pub fn with_active_cell(mut self, cell: impl Into<String>) -> Self {
1065 self.active_cell = Some(cell.into());
1066 self
1067 }
1068
1069 /// Sets this sheet's default column width and returns the sheet for chaining.
1070 pub fn with_default_column_width(mut self, width: f64) -> Self {
1071 self.default_column_width = Some(width);
1072 self
1073 }
1074
1075 /// Sets this sheet's default row height and returns the sheet for chaining.
1076 pub fn with_default_row_height(mut self, height: f64) -> Self {
1077 self.default_row_height = Some(height);
1078 self
1079 }
1080
1081 /// Adds a manual page break after the given 1-based row number and returns the sheet for
1082 /// chaining.
1083 pub fn with_row_break(mut self, row: u32) -> Self {
1084 self.row_breaks.push(row);
1085 self
1086 }
1087
1088 /// Adds a manual page break after the given 1-based column number and returns the sheet for
1089 /// chaining.
1090 pub fn with_column_break(mut self, column: u32) -> Self {
1091 self.column_breaks.push(column);
1092 self
1093 }
1094
1095 /// Appends a "what-if" scenario and returns the sheet for chaining.
1096 pub fn with_scenario(mut self, scenario: Scenario) -> Self {
1097 self.scenarios.push(scenario);
1098 self
1099 }
1100
1101 /// Appends an autofilter column's criteria and returns the sheet for chaining.
1102 pub fn with_auto_filter_column(mut self, column: AutoFilterColumn) -> Self {
1103 self.auto_filter_columns.push(column);
1104 self
1105 }
1106
1107 /// Appends a suppressed error-checking entry and returns the sheet for chaining.
1108 pub fn with_ignored_error(mut self, ignored_error: IgnoredError) -> Self {
1109 self.ignored_errors.push(ignored_error);
1110 self
1111 }
1112
1113 /// Appends a named protected range and returns the sheet for chaining.
1114 pub fn with_protected_range(mut self, protected_range: ProtectedRange) -> Self {
1115 self.protected_ranges.push(protected_range);
1116 self
1117 }
1118
1119 /// Sets this sheet's (worksheet-level `<autoFilter>`) remembered sort order and returns the
1120 /// sheet for chaining. See [`Sheet::sort_state`]'s doc comment: a no-op unless
1121 /// [`with_auto_filter_range`](Sheet::with_auto_filter_range) is also used.
1122 pub fn with_sort_state(mut self, sort_state: SortState) -> Self {
1123 self.sort_state = Some(sort_state);
1124 self
1125 }
1126
1127 /// Sets whether outline/grouping summary rows sit below (`true`) or above (`false`) their
1128 /// detail rows, and returns the sheet for chaining.
1129 pub fn with_outline_summary_below(mut self, summary_below: bool) -> Self {
1130 self.outline_summary_below = Some(summary_below);
1131 self
1132 }
1133
1134 /// Sets whether outline/grouping summary columns sit to the right (`true`) or left (`false`) of
1135 /// their detail columns, and returns the sheet for chaining — point 55, the column-axis
1136 /// counterpart of `with_outline_summary_below`.
1137 pub fn with_outline_summary_right(mut self, summary_right: bool) -> Self {
1138 self.outline_summary_right = Some(summary_right);
1139 self
1140 }
1141}
1142
1143impl Row {
1144 /// Creates an empty row (no cells).
1145 pub fn new() -> Self {
1146 Self::default()
1147 }
1148
1149 /// Appends a cell and returns the row for chaining.
1150 pub fn with_cell(mut self, cell: Cell) -> Self {
1151 self.cells.push(cell);
1152 self
1153 }
1154
1155 /// Appends a text cell and returns the row for chaining — a convenience for the common case of
1156 /// not needing `Cell` directly.
1157 pub fn with_text(self, text: impl Into<String>) -> Self {
1158 self.with_cell(Cell::text(text))
1159 }
1160
1161 /// Appends a numeric cell and returns the row for chaining.
1162 pub fn with_number(self, number: f64) -> Self {
1163 self.with_cell(Cell::number(number))
1164 }
1165
1166 /// Appends a formula cell (no cached value) and returns the row for chaining.
1167 pub fn with_formula(self, formula: impl Into<String>) -> Self {
1168 self.with_cell(Cell::formula(formula))
1169 }
1170
1171 /// Sets this row's explicit height, in points, and returns it for chaining.
1172 pub fn with_height(mut self, height: f64) -> Self {
1173 self.height = Some(height);
1174 self
1175 }
1176
1177 /// Sets whether this row is hidden and returns it for chaining.
1178 pub fn with_hidden(mut self, hidden: bool) -> Self {
1179 self.hidden = hidden;
1180 self
1181 }
1182
1183 /// Sets this row's outline (grouping) level and returns it for chaining.
1184 pub fn with_outline_level(mut self, level: u8) -> Self {
1185 self.outline_level = level;
1186 self
1187 }
1188
1189 /// Sets whether this row's outline group is shown collapsed and returns it for chaining.
1190 pub fn with_collapsed(mut self, collapsed: bool) -> Self {
1191 self.collapsed = collapsed;
1192 self
1193 }
1194
1195 /// Sets this row's default cell format and returns it for chaining.
1196 pub fn with_style(mut self, style: CellFormat) -> Self {
1197 self.default_format = Some(style);
1198 self
1199 }
1200}
1201
1202impl Cell {
1203 /// Creates an empty cell.
1204 pub fn new() -> Self {
1205 Self::default()
1206 }
1207
1208 /// Creates a text cell.
1209 pub fn text(text: impl Into<String>) -> Self {
1210 Self {
1211 value: CellValue::Text(text.into()),
1212 format: None,
1213 }
1214 }
1215
1216 /// Creates a numeric cell.
1217 pub fn number(number: f64) -> Self {
1218 Self {
1219 value: CellValue::Number(number),
1220 format: None,
1221 }
1222 }
1223
1224 /// Creates a formula cell holding the given formula text (without a leading `=`) and no cached
1225 /// value yet — see [`Cell::with_cached_value`] to attach one. A formula with no cached value is
1226 /// still schema-valid (Excel simply recomputes it the next time the file is opened, its
1227 /// standard behavior for any formula it considers "dirty").
1228 pub fn formula(formula: impl Into<String>) -> Self {
1229 Self {
1230 value: CellValue::Formula {
1231 formula: Some(formula.into()),
1232 cached_value: None,
1233 },
1234 format: None,
1235 }
1236 }
1237
1238 /// Creates a boolean cell.
1239 pub fn boolean(value: bool) -> Self {
1240 Self {
1241 value: CellValue::Boolean(value),
1242 format: None,
1243 }
1244 }
1245
1246 /// Creates a date cell from an [`ExcelDateTime`] — see [`CellValue::Date`]'s doc comment: pair
1247 /// this with a `.with_format(CellFormat::new().with_number_format("dd/mm/yyyy"))` (or any other
1248 /// date-shaped format code) for Excel to actually display it as a date; without one, Excel
1249 /// shows the raw serial number.
1250 pub fn date(date: ExcelDateTime) -> Self {
1251 Self {
1252 value: CellValue::Date(date),
1253 format: None,
1254 }
1255 }
1256
1257 /// Creates a rich-text cell (a text value with per-run character formatting).
1258 pub fn rich_text(runs: Vec<RichTextRun>) -> Self {
1259 Self {
1260 value: CellValue::RichText(runs),
1261 format: None,
1262 }
1263 }
1264
1265 /// Creates an array (CSE) formula cell holding the given formula text (without a leading `=`)
1266 /// and range, and no cached value yet — see [`Cell::with_cached_value`] to attach one. This
1267 /// cell should be placed at the array range's top-left (anchor) position — see
1268 /// [`CellValue::ArrayFormula`]'s doc comment.
1269 pub fn array_formula(formula: impl Into<String>, range: impl Into<String>) -> Self {
1270 Self {
1271 value: CellValue::ArrayFormula {
1272 formula: formula.into(),
1273 range: range.into(),
1274 cached_value: None,
1275 },
1276 format: None,
1277 }
1278 }
1279
1280 /// Creates a shared-formula group's **master** cell — the one carrying the group's actual
1281 /// formula text and its full `range` — and no cached value yet — see
1282 /// [`Cell::with_cached_value`] to attach one. Pair with [`Cell::shared_formula_follower`] for
1283 /// the group's other cells, using the same `group_index`. See [`CellValue::SharedFormula`]'s
1284 /// doc comment: this crate does not compute each follower's own shifted formula text.
1285 pub fn shared_formula(
1286 formula: impl Into<String>,
1287 group_index: u32,
1288 range: impl Into<String>,
1289 ) -> Self {
1290 Self {
1291 value: CellValue::SharedFormula {
1292 formula: Some(formula.into()),
1293 group_index,
1294 range: Some(range.into()),
1295 cached_value: None,
1296 },
1297 format: None,
1298 }
1299 }
1300
1301 /// Creates a shared-formula group's **follower** cell — no formula text and no `range`
1302 /// (matching what real Excel itself writes for a follower, just `t="shared" si=".."`), only the
1303 /// group's shared `group_index`. See [`Cell::shared_formula`] for the group's master cell.
1304 pub fn shared_formula_follower(group_index: u32) -> Self {
1305 Self {
1306 value: CellValue::SharedFormula {
1307 formula: None,
1308 group_index,
1309 range: None,
1310 cached_value: None,
1311 },
1312 format: None,
1313 }
1314 }
1315
1316 /// Attaches a cached result value to a formula cell and returns the cell for chaining. A no-op
1317 /// if this cell doesn't hold
1318 /// `CellValue::Formula`/`CellValue::ArrayFormula`/`CellValue::SharedFormula` (e.g. called on a
1319 /// `Cell::number`).
1320 pub fn with_cached_value(mut self, value: f64) -> Self {
1321 match &mut self.value {
1322 CellValue::Formula { cached_value, .. }
1323 | CellValue::ArrayFormula { cached_value, .. }
1324 | CellValue::SharedFormula { cached_value, .. } => {
1325 *cached_value = Some(value);
1326 }
1327 _ => {}
1328 }
1329 self
1330 }
1331
1332 /// Attaches a cell style and returns the cell for chaining.
1333 pub fn with_format(mut self, format: CellFormat) -> Self {
1334 self.format = Some(format);
1335 self
1336 }
1337}
1338
1339/// Lets [`Sheet::write_cell`] accept a raw string directly (`sheet.write_cell(1, 1, "Bonjour")`)
1340/// instead of requiring `Cell::text("Bonjour")` — same rationale as the `f64`/`bool` impls below;
1341/// see [`Sheet::write_cell`]'s doc comment.
1342impl From<&str> for Cell {
1343 fn from(text: &str) -> Self {
1344 Cell::text(text)
1345 }
1346}
1347
1348/// See the `&str` impl just above.
1349impl From<String> for Cell {
1350 fn from(text: String) -> Self {
1351 Cell::text(text)
1352 }
1353}
1354
1355/// Lets [`Sheet::write_cell`] accept a raw `f64` directly (`sheet.write_cell(1, 1, 42.0)`) instead
1356/// of requiring `Cell::number(42.0)`.
1357impl From<f64> for Cell {
1358 fn from(number: f64) -> Self {
1359 Cell::number(number)
1360 }
1361}
1362
1363/// Lets [`Sheet::write_cell`] accept a raw `bool` directly (`sheet.write_cell(1, 1, true)`) instead
1364/// of requiring `Cell::boolean(true)`.
1365impl From<bool> for Cell {
1366 fn from(value: bool) -> Self {
1367 Cell::boolean(value)
1368 }
1369}
1370
1371impl CellFormat {
1372 /// Creates a default cell format (no overrides at all — equivalent to not setting a format,
1373 /// only useful as a starting point for the `with_*` builder methods).
1374 pub fn new() -> Self {
1375 Self::default()
1376 }
1377
1378 /// Sets a custom number format code.
1379 pub fn with_number_format(mut self, format: impl Into<String>) -> Self {
1380 self.number_format = Some(format.into());
1381 self
1382 }
1383
1384 /// Sets bold.
1385 pub fn with_bold(mut self, bold: bool) -> Self {
1386 self.bold = bold;
1387 self
1388 }
1389
1390 /// Sets italic.
1391 pub fn with_italic(mut self, italic: bool) -> Self {
1392 self.italic = italic;
1393 self
1394 }
1395
1396 /// Sets the font size, in points.
1397 pub fn with_font_size(mut self, size: f64) -> Self {
1398 self.font_size = Some(size);
1399 self
1400 }
1401
1402 /// Sets the font color (8-hex-digit ARGB, e.g. `"FFFF0000"`).
1403 pub fn with_font_color(mut self, color: impl Into<String>) -> Self {
1404 self.font_color = Some(color.into());
1405 self
1406 }
1407
1408 /// Sets the solid background fill color (8-hex-digit ARGB).
1409 pub fn with_fill_color(mut self, color: impl Into<String>) -> Self {
1410 self.fill_color = Some(color.into());
1411 self
1412 }
1413
1414 /// Sets horizontal alignment.
1415 pub fn with_horizontal_alignment(mut self, alignment: HorizontalAlignment) -> Self {
1416 self.horizontal_alignment = Some(alignment);
1417 self
1418 }
1419
1420 /// Sets vertical alignment.
1421 pub fn with_vertical_alignment(mut self, alignment: VerticalAlignment) -> Self {
1422 self.vertical_alignment = Some(alignment);
1423 self
1424 }
1425
1426 /// Sets this cell's borders.
1427 pub fn with_border(mut self, border: Border) -> Self {
1428 self.border = border;
1429 self
1430 }
1431
1432 /// Sets the font name/family.
1433 pub fn with_font_name(mut self, font_name: impl Into<String>) -> Self {
1434 self.font_name = Some(font_name.into());
1435 self
1436 }
1437
1438 /// Sets underline.
1439 pub fn with_underline(mut self, underline: bool) -> Self {
1440 self.underline = underline;
1441 self
1442 }
1443
1444 /// Sets strikethrough.
1445 pub fn with_strike(mut self, strike: bool) -> Self {
1446 self.strike = strike;
1447 self
1448 }
1449
1450 /// Sets whether text wraps onto multiple lines within the cell.
1451 pub fn with_wrap_text(mut self, wrap_text: bool) -> Self {
1452 self.wrap_text = wrap_text;
1453 self
1454 }
1455
1456 /// Sets the indentation level.
1457 pub fn with_indent(mut self, indent: u32) -> Self {
1458 self.indent = Some(indent);
1459 self
1460 }
1461
1462 /// Sets the raw Excel text rotation value (see [`text_rotation`](CellFormat::text_rotation)'s
1463 /// doc comment for the encoding).
1464 pub fn with_text_rotation(mut self, text_rotation: u16) -> Self {
1465 self.text_rotation = Some(text_rotation);
1466 self
1467 }
1468
1469 /// Sets whether the font size shrinks to fit the cell's width.
1470 pub fn with_shrink_to_fit(mut self, shrink_to_fit: bool) -> Self {
1471 self.shrink_to_fit = shrink_to_fit;
1472 self
1473 }
1474
1475 /// Sets a non-solid pattern fill.
1476 pub fn with_pattern_fill(mut self, pattern_fill: PatternFill) -> Self {
1477 self.pattern_fill = Some(pattern_fill);
1478 self
1479 }
1480
1481 /// Sets a gradient fill.
1482 pub fn with_gradient_fill(mut self, gradient_fill: GradientFill) -> Self {
1483 self.gradient_fill = Some(gradient_fill);
1484 self
1485 }
1486
1487 /// Sets whether this cell is locked when the sheet is protected.
1488 pub fn with_locked(mut self, locked: bool) -> Self {
1489 self.locked = Some(locked);
1490 self
1491 }
1492
1493 /// Sets whether this cell's formula is hidden when the sheet is protected.
1494 pub fn with_formula_hidden(mut self, formula_hidden: bool) -> Self {
1495 self.formula_hidden = Some(formula_hidden);
1496 self
1497 }
1498
1499 /// Links this format to a named cell style (by name, resolved against
1500 /// [`Workbook::named_cell_styles`] at write time) and returns it for chaining.
1501 pub fn with_named_style(mut self, name: impl Into<String>) -> Self {
1502 self.named_style = Some(name.into());
1503 self
1504 }
1505}
1506
1507/// A [`CellFormat::pattern_fill`]'s pattern (`ST_PatternType`) — the solid fill is deliberately
1508/// excluded here (that's [`CellFormat::fill_color`]'s job, the common case); only the non-solid
1509/// patterns are modeled.
1510#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1511pub enum PatternType {
1512 Gray125,
1513 Gray0625,
1514 DarkGray,
1515 MediumGray,
1516 LightGray,
1517 DarkHorizontal,
1518 DarkVertical,
1519 DarkDown,
1520 DarkUp,
1521 DarkGrid,
1522 DarkTrellis,
1523 LightHorizontal,
1524 LightVertical,
1525 LightDown,
1526 LightUp,
1527 LightGrid,
1528 LightTrellis,
1529}
1530
1531/// A cell's non-solid pattern fill (`<patternFill patternType="..">`, `CT_PatternFill`).
1532#[derive(Debug, Clone, PartialEq)]
1533pub struct PatternFill {
1534 pub pattern_type: PatternType,
1535 /// The pattern's foreground color (8-hex-digit ARGB) — the color the pattern's own lines/dots
1536 /// are drawn in.
1537 pub fg_color: Option<String>,
1538 /// The pattern's background color (8-hex-digit ARGB) — the color showing through between the
1539 /// pattern's lines/dots.
1540 pub bg_color: Option<String>,
1541}
1542
1543impl PatternFill {
1544 /// Creates a pattern fill with no colors set yet (Excel's own defaults — black foreground,
1545 /// white background — apply).
1546 pub fn new(pattern_type: PatternType) -> Self {
1547 Self {
1548 pattern_type,
1549 fg_color: None,
1550 bg_color: None,
1551 }
1552 }
1553
1554 /// Sets the foreground color and returns the fill for chaining.
1555 pub fn with_fg_color(mut self, color: impl Into<String>) -> Self {
1556 self.fg_color = Some(color.into());
1557 self
1558 }
1559
1560 /// Sets the background color and returns the fill for chaining.
1561 pub fn with_bg_color(mut self, color: impl Into<String>) -> Self {
1562 self.bg_color = Some(color.into());
1563 self
1564 }
1565}
1566
1567/// One color stop of a [`GradientFill`] (`<stop position="..">`, `CT_GradientStop`).
1568#[derive(Debug, Clone, PartialEq)]
1569pub struct GradientStop {
1570 /// The stop's position along the gradient, `0.0` to `1.0`.
1571 pub position: f64,
1572 /// 8-hex-digit ARGB color.
1573 pub color: String,
1574}
1575
1576impl GradientStop {
1577 pub fn new(position: f64, color: impl Into<String>) -> Self {
1578 Self {
1579 position,
1580 color: color.into(),
1581 }
1582 }
1583}
1584
1585/// A cell's linear gradient fill (`<gradientFill degree="..">`, `CT_GradientFill`'s `type="linear"`
1586/// form — the default, no `type` attribute written). The "path" (radial) gradient form is not
1587/// modeled.
1588#[derive(Debug, Clone, PartialEq)]
1589pub struct GradientFill {
1590 /// The gradient's angle, in degrees (`0` = left to right).
1591 pub angle: f64,
1592 /// The gradient's color stops, in order. Excel itself always expects at least two (a start and
1593 /// end color) — not enforced here.
1594 pub stops: Vec<GradientStop>,
1595}
1596
1597impl GradientFill {
1598 pub fn new(angle: f64, stops: Vec<GradientStop>) -> Self {
1599 Self { angle, stops }
1600 }
1601}
1602
1603/// A relational comparison operator, shared between [`ConditionalFormattingCondition::CellIs`] and
1604/// [`DataValidationKind`]'s `WholeNumber`/`Decimal` variants — ECMA-376 defines two separate simple
1605/// types for these (`ST_ConditionalFormattingOperator`/ `ST_DataValidationOperator`), but both
1606/// share exactly the same 8 relational values, so one Rust enum covers both. Neither schema's full
1607/// enumeration is modeled: `ST_ConditionalFormattingOperator`'s
1608/// `containsText`/`notContains`/`beginsWith`/`endsWith` are, despite sharing the same XML
1609/// attribute, really distinct rule *types* in Excel's own "Highlight Cells Rules" UI, not `cellIs`
1610/// variants — out of scope.
1611#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1612pub enum ComparisonOperator {
1613 LessThan,
1614 LessThanOrEqual,
1615 Equal,
1616 NotEqual,
1617 GreaterThanOrEqual,
1618 GreaterThan,
1619 Between,
1620 NotBetween,
1621}
1622
1623/// A conditional formatting rule (`<cfRule>`, `CT_CfRule`) applied over a cell range
1624/// (`<conditionalFormatting sqref="..">`, `CT_ConditionalFormatting`).
1625///
1626/// Only the two most common rule types are modeled — see [`ConditionalFormattingCondition`].
1627/// `CT_CfRule`'s many other types (`colorScale`, `dataBar`, `iconSet`, `top10`, `uniqueValues`,
1628/// `duplicateValues`, `containsText`, `timePeriod`, `aboveAverage`..) are not modeled — this
1629/// crate's scope here is deliberately narrow.
1630#[derive(Debug, Clone, PartialEq)]
1631pub struct ConditionalFormattingRule {
1632 /// The cell range this rule applies to (`sqref`, e.g. `"A1:A10"`).
1633 pub range: String,
1634 /// The rule's condition.
1635 pub condition: ConditionalFormattingCondition,
1636 /// The formatting applied to cells in `range` when `condition` is met — reuses [`CellFormat`],
1637 /// the same subset already modeled for plain cell styles, as the shape of the underlying
1638 /// differential format record (`<dxf>`, `CT_Dxf` — confirmed via ECMA-376 to be a sequence of
1639 /// the same kind of optional `font`/`numFmt`/`fill`/ `alignment`/`border`/`protection` children
1640 /// `CellFormat` already covers, minus borders/protection). Distinct `CellFormat`s across a
1641 /// workbook's conditional formatting rules are deduplicated into shared `xl/styles.xml`
1642 /// `<dxfs>` entries by the writer, see `writer.rs::collect_dxfs`.
1643 pub format: CellFormat,
1644}
1645
1646/// A conditional formatting rule's condition — only `cellIs` (a direct value comparison) and
1647/// `expression` (an arbitrary custom formula) are modeled, `CT_CfRule`'s two simplest and most
1648/// common types.
1649#[derive(Debug, Clone, PartialEq)]
1650pub enum ConditionalFormattingCondition {
1651 /// `type="cellIs"`: compares the cell's own value against one or two formulas using a
1652 /// relational operator (e.g. "greater than 0.5", or "between 10 and 20" when `formula2` is
1653 /// set).
1654 CellIs {
1655 operator: ComparisonOperator,
1656 /// The first (or only) comparison value/formula.
1657 formula1: String,
1658 /// The second comparison value/formula — only meaningful (and required by Excel) for
1659 /// `Between`/`NotBetween`.
1660 formula2: Option<String>,
1661 },
1662 /// `type="expression"`: an arbitrary formula, evaluated per cell in `range`, with the format
1663 /// applied when it evaluates to `TRUE`.
1664 Expression {
1665 /// The formula text (e.g. `"MOD(ROW(),2)=0"` for zebra striping).
1666 formula: String,
1667 },
1668 /// A 2- or 3-stop color scale (`<colorScale>`, `CT_ColorScale`). Unlike every other condition
1669 /// variant, a color scale ignores [`ConditionalFormattingRule::format`] entirely (its coloring
1670 /// comes from the stops themselves, not a `dxf`) — the writer never allocates a `dxfId` for
1671 /// this variant, see `writer.rs::write_conditional_formatting`.
1672 ColorScale(Vec<ColorScaleStop>),
1673 /// A data bar (`<dataBar>`, `CT_DataBar`) — min/max stops plus a single fill color. Like
1674 /// `ColorScale`, ignores `format`.
1675 DataBar {
1676 min: CfvoPosition,
1677 max: CfvoPosition,
1678 color: String,
1679 },
1680 /// A 3-icon set (`<iconSet>`, `CT_IconSet`) using Excel's own default equal-thirds percent
1681 /// thresholds (0/33/67) — only the icon set's visual style is configurable, not custom
1682 /// thresholds (out of scope). Like `ColorScale`, ignores `format`.
1683 IconSet(IconSetType),
1684 /// `type="top10"`: highlights the top (or bottom) `rank` items/percent of `range`.
1685 Top10 {
1686 rank: u32,
1687 /// Whether `rank` is a percentage rather than a raw count.
1688 percent: bool,
1689 /// Highlights the *bottom* `rank` instead of the top.
1690 bottom: bool,
1691 },
1692 /// `type="aboveAverage"`: highlights cells above (or below) the range's average.
1693 AboveAverage {
1694 /// Highlights above-average cells (`true`) or below-average ones (`false`).
1695 above: bool,
1696 /// Also highlights cells exactly equal to the average.
1697 equal_average: bool,
1698 },
1699 /// `type="duplicateValues"`: highlights cells whose value appears more than once in `range`.
1700 DuplicateValues,
1701 /// `type="uniqueValues"`: highlights cells whose value appears exactly once in `range`.
1702 UniqueValues,
1703 /// `type="containsText"`/`"notContains"`/`"beginsWith"`/`"endsWith"` (four distinct `CT_CfRule`
1704 /// types sharing the same shape, see [`TextComparisonOperator`]): highlights cells whose text
1705 /// matches `text` per `operator`. Unlike `ConditionalFormattingCondition::CellIs`, Excel
1706 /// auto-generates the underlying formula from `text` and the range's first cell — this crate
1707 /// reproduces that (see `writer.rs::write_conditional_formatting`), it isn't the caller's
1708 /// formula to write.
1709 ContainsText {
1710 operator: TextComparisonOperator,
1711 text: String,
1712 },
1713}
1714
1715/// A [`ConditionalFormattingCondition::ColorScale`] stop: a threshold position plus the color
1716/// applied there (colors between two stops are interpolated by Excel itself, not written to the
1717/// file).
1718#[derive(Debug, Clone, PartialEq)]
1719pub struct ColorScaleStop {
1720 pub position: CfvoPosition,
1721 /// 8-hex-digit ARGB color.
1722 pub color: String,
1723}
1724
1725impl ColorScaleStop {
1726 pub fn new(position: CfvoPosition, color: impl Into<String>) -> Self {
1727 Self {
1728 position,
1729 color: color.into(),
1730 }
1731 }
1732}
1733
1734/// A threshold position for a [`ColorScaleStop`]/`DataBar` min or max (`<cfvo type=".." val="..">`,
1735/// `CT_Cfvo`) — only the three most common `ST_CfvoType` values are modeled;
1736/// `formula`/`percentile`/`autoMin`/ `autoMax` are not.
1737#[derive(Debug, Clone, PartialEq)]
1738pub enum CfvoPosition {
1739 /// `type="min"`: the range's own minimum value — `Min`/`Max` carry no `val` attribute at all
1740 /// when written.
1741 Min,
1742 /// `type="max"`: the range's own maximum value.
1743 Max,
1744 /// `type="percent"`, e.g. `Percent(50.0)` for the 50th percentile point.
1745 Percent(f64),
1746 /// `type="num"`: a fixed literal value.
1747 Number(f64),
1748}
1749
1750/// A [`ConditionalFormattingCondition::IconSet`]'s visual style (`ST_IconSetType`) — only the
1751/// handful of 3-icon sets most commonly used are modeled.
1752#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1753pub enum IconSetType {
1754 ThreeTrafficLights,
1755 ThreeArrows,
1756 ThreeFlags,
1757 ThreeSymbols,
1758}
1759
1760/// The comparison performed by a [`ConditionalFormattingCondition::ContainsText`] rule — shares the
1761/// `cellIs`-adjacent `operator` attribute name with [`ComparisonOperator`], but is a distinct,
1762/// disjoint set of values (`ST_ConditionalFormattingOperator`'s text-comparison subset), so it's
1763/// its own enum rather than reusing `ComparisonOperator`.
1764#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1765pub enum TextComparisonOperator {
1766 Contains,
1767 NotContains,
1768 BeginsWith,
1769 EndsWith,
1770}
1771
1772impl ConditionalFormattingRule {
1773 /// Creates a `cellIs` rule.
1774 pub fn cell_is(
1775 range: impl Into<String>,
1776 operator: ComparisonOperator,
1777 formula1: impl Into<String>,
1778 format: CellFormat,
1779 ) -> Self {
1780 Self {
1781 range: range.into(),
1782 condition: ConditionalFormattingCondition::CellIs {
1783 operator,
1784 formula1: formula1.into(),
1785 formula2: None,
1786 },
1787 format,
1788 }
1789 }
1790
1791 /// Sets the second comparison formula (for `Between`/`NotBetween`) and returns the rule for
1792 /// chaining. A no-op if this rule isn't a `CellIs` condition.
1793 pub fn with_second_formula(mut self, formula2: impl Into<String>) -> Self {
1794 if let ConditionalFormattingCondition::CellIs {
1795 formula2: existing, ..
1796 } = &mut self.condition
1797 {
1798 *existing = Some(formula2.into());
1799 }
1800 self
1801 }
1802
1803 /// Creates an `expression` rule.
1804 pub fn expression(
1805 range: impl Into<String>,
1806 formula: impl Into<String>,
1807 format: CellFormat,
1808 ) -> Self {
1809 Self {
1810 range: range.into(),
1811 condition: ConditionalFormattingCondition::Expression {
1812 formula: formula.into(),
1813 },
1814 format,
1815 }
1816 }
1817
1818 /// Creates a color scale rule. `format` is ignored for this variant (see
1819 /// [`ConditionalFormattingCondition::ColorScale`]'s doc comment) — always
1820 /// `CellFormat::default()`.
1821 pub fn color_scale(range: impl Into<String>, stops: Vec<ColorScaleStop>) -> Self {
1822 Self {
1823 range: range.into(),
1824 condition: ConditionalFormattingCondition::ColorScale(stops),
1825 format: CellFormat::default(),
1826 }
1827 }
1828
1829 /// Creates a data bar rule. `format` is ignored, same as `color_scale`.
1830 pub fn data_bar(
1831 range: impl Into<String>,
1832 min: CfvoPosition,
1833 max: CfvoPosition,
1834 color: impl Into<String>,
1835 ) -> Self {
1836 Self {
1837 range: range.into(),
1838 condition: ConditionalFormattingCondition::DataBar {
1839 min,
1840 max,
1841 color: color.into(),
1842 },
1843 format: CellFormat::default(),
1844 }
1845 }
1846
1847 /// Creates a 3-icon-set rule with Excel's own default equal-thirds percent thresholds. `format`
1848 /// is ignored, same as `color_scale`.
1849 pub fn icon_set(range: impl Into<String>, icon_set: IconSetType) -> Self {
1850 Self {
1851 range: range.into(),
1852 condition: ConditionalFormattingCondition::IconSet(icon_set),
1853 format: CellFormat::default(),
1854 }
1855 }
1856
1857 /// Creates a `top10` rule (top `rank` items/percent).
1858 pub fn top10(range: impl Into<String>, rank: u32, percent: bool, format: CellFormat) -> Self {
1859 Self {
1860 range: range.into(),
1861 condition: ConditionalFormattingCondition::Top10 {
1862 rank,
1863 percent,
1864 bottom: false,
1865 },
1866 format,
1867 }
1868 }
1869
1870 /// Sets `top10`'s `bottom` flag (highlight the bottom `rank` instead) and returns the rule for
1871 /// chaining. A no-op for any other condition.
1872 pub fn with_bottom(mut self, bottom: bool) -> Self {
1873 if let ConditionalFormattingCondition::Top10 {
1874 bottom: existing, ..
1875 } = &mut self.condition
1876 {
1877 *existing = bottom;
1878 }
1879 self
1880 }
1881
1882 /// Creates an `aboveAverage` rule.
1883 pub fn above_average(range: impl Into<String>, above: bool, format: CellFormat) -> Self {
1884 Self {
1885 range: range.into(),
1886 condition: ConditionalFormattingCondition::AboveAverage {
1887 above,
1888 equal_average: false,
1889 },
1890 format,
1891 }
1892 }
1893
1894 /// Creates a `duplicateValues` rule.
1895 pub fn duplicate_values(range: impl Into<String>, format: CellFormat) -> Self {
1896 Self {
1897 range: range.into(),
1898 condition: ConditionalFormattingCondition::DuplicateValues,
1899 format,
1900 }
1901 }
1902
1903 /// Creates a `uniqueValues` rule.
1904 pub fn unique_values(range: impl Into<String>, format: CellFormat) -> Self {
1905 Self {
1906 range: range.into(),
1907 condition: ConditionalFormattingCondition::UniqueValues,
1908 format,
1909 }
1910 }
1911
1912 /// Creates a text-comparison rule (`containsText`/`notContains`/ `beginsWith`/`endsWith`).
1913 pub fn contains_text(
1914 range: impl Into<String>,
1915 operator: TextComparisonOperator,
1916 text: impl Into<String>,
1917 format: CellFormat,
1918 ) -> Self {
1919 Self {
1920 range: range.into(),
1921 condition: ConditionalFormattingCondition::ContainsText {
1922 operator,
1923 text: text.into(),
1924 },
1925 format,
1926 }
1927 }
1928}
1929
1930/// An input or error message attached to a [`DataValidationRule`] (`promptTitle`/`prompt` or
1931/// `errorTitle`/`error` on `CT_DataValidation`).
1932#[derive(Debug, Clone, PartialEq)]
1933pub struct Message {
1934 /// The message's title (shown in bold in Excel's popup/tooltip).
1935 pub title: Option<String>,
1936 /// The message's body text.
1937 pub text: String,
1938}
1939
1940impl Message {
1941 /// Creates a message with just body text, no title.
1942 pub fn new(text: impl Into<String>) -> Self {
1943 Self {
1944 title: None,
1945 text: text.into(),
1946 }
1947 }
1948
1949 /// Sets a title and returns the message for chaining.
1950 pub fn with_title(mut self, title: impl Into<String>) -> Self {
1951 self.title = Some(title.into());
1952 self
1953 }
1954}
1955
1956/// A data validation rule (`<dataValidation>`, `CT_DataValidation`) applied over a cell range
1957/// (`sqref`).
1958///
1959/// Only three of `ST_DataValidationType`'s eight values are modeled — see [`DataValidationKind`];
1960/// `date`/`time`/`textLength`/`custom` are not. `showDropDown`/`imeMode` are not modeled either
1961/// (the former is a rarely-touched, confusingly-inverted flag most libraries leave alone; the
1962/// latter is IME-input-method behavior, irrelevant outside East Asian input).
1963#[derive(Debug, Clone, PartialEq)]
1964pub struct DataValidationRule {
1965 /// The cell range this rule applies to (`sqref`).
1966 pub range: String,
1967 /// The validation performed.
1968 pub kind: DataValidationKind,
1969 /// Whether a blank cell is considered valid (`allowBlank`).
1970 pub allow_blank: bool,
1971 /// An input message shown when the cell is selected (`showInputMessage` is written
1972 /// automatically when this is `Some`).
1973 pub input_message: Option<Message>,
1974 /// An error message shown when an invalid value is entered (`showErrorMessage` is written
1975 /// automatically when this is `Some`). Always written with `errorStyle="stop"` (blocking) — the
1976 /// `warning`/ `information` (non-blocking) styles are not modeled.
1977 pub error_message: Option<Message>,
1978}
1979
1980/// The validation performed by a [`DataValidationRule`].
1981#[derive(Debug, Clone, PartialEq)]
1982pub enum DataValidationKind {
1983 /// `type="list"`: the cell's value must match one entry from a fixed list — Excel shows a
1984 /// dropdown. `source` is written verbatim as `formula1`, so it can be either a literal
1985 /// comma-separated list wrapped in quotes (e.g. `"\"Yes,No,Maybe\""`) or a reference to a range
1986 /// holding the valid values (e.g. `"$D$1:$D$3"`).
1987 List { source: String },
1988 /// `type="whole"`: the cell's value must be a whole number satisfying `operator` against
1989 /// `formula1` (and `formula2`, for `Between`/ `NotBetween`).
1990 WholeNumber {
1991 operator: ComparisonOperator,
1992 formula1: String,
1993 formula2: Option<String>,
1994 },
1995 /// `type="decimal"`: like `WholeNumber`, but the value may have a fractional part.
1996 Decimal {
1997 operator: ComparisonOperator,
1998 formula1: String,
1999 formula2: Option<String>,
2000 },
2001 /// `type="date"`: the cell's value must be a date satisfying `operator` against
2002 /// `formula1`/`formula2`. `formula1`/`formula2` are written verbatim, so the caller is
2003 /// responsible for passing an Excel date serial number (as text, e.g. `"45000"`) or a
2004 /// date-returning formula — same "storage, not a conversion helper" posture as everywhere else
2005 /// formula text is stored in this crate; see [`crate::ExcelDateTime`] plus
2006 /// `writer.rs::excel_serial_date` if a caller needs to compute one.
2007 Date {
2008 operator: ComparisonOperator,
2009 formula1: String,
2010 formula2: Option<String>,
2011 },
2012 /// `type="time"`: like `Date`, but for a time-of-day value (a fractional day, e.g. `"0.5"` for
2013 /// noon).
2014 Time {
2015 operator: ComparisonOperator,
2016 formula1: String,
2017 formula2: Option<String>,
2018 },
2019 /// `type="textLength"`: the cell's text length must satisfy `operator` against
2020 /// `formula1`/`formula2`.
2021 TextLength {
2022 operator: ComparisonOperator,
2023 formula1: String,
2024 formula2: Option<String>,
2025 },
2026 /// `type="custom"`: an arbitrary formula, evaluated per cell, the value is valid when it
2027 /// evaluates to `TRUE` — no `operator`, unlike every other variant.
2028 Custom { formula: String },
2029}
2030
2031impl DataValidationRule {
2032 /// Creates a `list` validation rule with no messages, blanks allowed.
2033 pub fn list(range: impl Into<String>, source: impl Into<String>) -> Self {
2034 Self {
2035 range: range.into(),
2036 kind: DataValidationKind::List {
2037 source: source.into(),
2038 },
2039 allow_blank: true,
2040 input_message: None,
2041 error_message: None,
2042 }
2043 }
2044
2045 /// Creates a `whole` validation rule with no messages, blanks allowed.
2046 pub fn whole_number(
2047 range: impl Into<String>,
2048 operator: ComparisonOperator,
2049 formula1: impl Into<String>,
2050 ) -> Self {
2051 Self {
2052 range: range.into(),
2053 kind: DataValidationKind::WholeNumber {
2054 operator,
2055 formula1: formula1.into(),
2056 formula2: None,
2057 },
2058 allow_blank: true,
2059 input_message: None,
2060 error_message: None,
2061 }
2062 }
2063
2064 /// Creates a `decimal` validation rule with no messages, blanks allowed.
2065 pub fn decimal(
2066 range: impl Into<String>,
2067 operator: ComparisonOperator,
2068 formula1: impl Into<String>,
2069 ) -> Self {
2070 Self {
2071 range: range.into(),
2072 kind: DataValidationKind::Decimal {
2073 operator,
2074 formula1: formula1.into(),
2075 formula2: None,
2076 },
2077 allow_blank: true,
2078 input_message: None,
2079 error_message: None,
2080 }
2081 }
2082
2083 /// Creates a `date` validation rule with no messages, blanks allowed.
2084 pub fn date(
2085 range: impl Into<String>,
2086 operator: ComparisonOperator,
2087 formula1: impl Into<String>,
2088 ) -> Self {
2089 Self {
2090 range: range.into(),
2091 kind: DataValidationKind::Date {
2092 operator,
2093 formula1: formula1.into(),
2094 formula2: None,
2095 },
2096 allow_blank: true,
2097 input_message: None,
2098 error_message: None,
2099 }
2100 }
2101
2102 /// Creates a `time` validation rule with no messages, blanks allowed.
2103 pub fn time(
2104 range: impl Into<String>,
2105 operator: ComparisonOperator,
2106 formula1: impl Into<String>,
2107 ) -> Self {
2108 Self {
2109 range: range.into(),
2110 kind: DataValidationKind::Time {
2111 operator,
2112 formula1: formula1.into(),
2113 formula2: None,
2114 },
2115 allow_blank: true,
2116 input_message: None,
2117 error_message: None,
2118 }
2119 }
2120
2121 /// Creates a `textLength` validation rule with no messages, blanks allowed.
2122 pub fn text_length(
2123 range: impl Into<String>,
2124 operator: ComparisonOperator,
2125 formula1: impl Into<String>,
2126 ) -> Self {
2127 Self {
2128 range: range.into(),
2129 kind: DataValidationKind::TextLength {
2130 operator,
2131 formula1: formula1.into(),
2132 formula2: None,
2133 },
2134 allow_blank: true,
2135 input_message: None,
2136 error_message: None,
2137 }
2138 }
2139
2140 /// Creates a `custom` validation rule with no messages, blanks allowed.
2141 pub fn custom(range: impl Into<String>, formula: impl Into<String>) -> Self {
2142 Self {
2143 range: range.into(),
2144 kind: DataValidationKind::Custom {
2145 formula: formula.into(),
2146 },
2147 allow_blank: true,
2148 input_message: None,
2149 error_message: None,
2150 }
2151 }
2152
2153 /// Sets the second comparison formula (for `Between`/`NotBetween`) on a
2154 /// `WholeNumber`/`Decimal`/`Date`/`Time`/`TextLength` rule and returns it for chaining. A no-op
2155 /// for a `List`/`Custom` rule.
2156 pub fn with_second_formula(mut self, formula2: impl Into<String>) -> Self {
2157 match &mut self.kind {
2158 DataValidationKind::WholeNumber {
2159 formula2: existing, ..
2160 }
2161 | DataValidationKind::Decimal {
2162 formula2: existing, ..
2163 }
2164 | DataValidationKind::Date {
2165 formula2: existing, ..
2166 }
2167 | DataValidationKind::Time {
2168 formula2: existing, ..
2169 }
2170 | DataValidationKind::TextLength {
2171 formula2: existing, ..
2172 } => {
2173 *existing = Some(formula2.into());
2174 }
2175 DataValidationKind::List { .. } | DataValidationKind::Custom { .. } => {}
2176 }
2177 self
2178 }
2179
2180 /// Sets whether a blank cell is considered valid and returns the rule for chaining.
2181 pub fn with_allow_blank(mut self, allow_blank: bool) -> Self {
2182 self.allow_blank = allow_blank;
2183 self
2184 }
2185
2186 /// Sets the input message and returns the rule for chaining.
2187 pub fn with_input_message(mut self, message: Message) -> Self {
2188 self.input_message = Some(message);
2189 self
2190 }
2191
2192 /// Sets the error message and returns the rule for chaining.
2193 pub fn with_error_message(mut self, message: Message) -> Self {
2194 self.error_message = Some(message);
2195 self
2196 }
2197}
2198
2199/// A named range/formula/constant (`<definedName>`, `CT_DefinedName`) — unlike
2200/// [`ConditionalFormattingRule`]/[`DataValidationRule`], this is a workbook-level construct, not
2201/// attached to any particular [`Sheet`], even when [`local_sheet_id`](DefinedName::local_sheet_id)
2202/// scopes it to one.
2203///
2204/// Only `name`/`refers_to`/`local_sheet_id`/`hidden`/`comment` are modeled. `CT_DefinedName`'s
2205/// remaining attributes (`function`/
2206/// `vbProcedure`/`xlm`/`functionGroupId`/`shortcutKey`/`publishToServer`/
2207/// `workbookParameter`/`customMenu`/`description`/`help`/`statusBar`) are all macro/add-in/UI-text
2208/// concerns with no real demand seen yet, same scope-reduction posture as elsewhere in this crate.
2209/// SpreadsheetML's reserved `_xlnm.*` built-in names (print area, print titles, filter database..)
2210/// aren't given any special handling — a caller can write one like any other name, but nothing here
2211/// validates or treats it specially.
2212#[derive(Debug, Clone, PartialEq)]
2213pub struct DefinedName {
2214 /// The name shown in Excel's Name Manager (`name`, required).
2215 pub name: String,
2216 /// The formula, range reference, or constant this name refers to (e.g. `"Feuil1!$A$1:$B$10"`),
2217 /// written as the element's own text content — unlike a conditional formatting/data validation
2218 /// rule's formula, `CT_DefinedName` has simple content (`ST_Formula`), not a dedicated child
2219 /// element.
2220 pub refers_to: String,
2221 /// If `Some`, scopes this name to a single sheet rather than the whole workbook
2222 /// (`localSheetId`, a 0-based index into [`Workbook.sheets`](Workbook::sheets) in declaration
2223 /// order) — a sheet-scoped name can be duplicated across different sheets' scopes, unlike a
2224 /// workbook-scoped one. `None` means workbook-wide scope, the common case.
2225 pub local_sheet_id: Option<usize>,
2226 /// Whether this name is hidden in Excel's Name Manager UI (`hidden`).
2227 pub hidden: bool,
2228 /// An optional comment shown for this name (`comment`).
2229 pub comment: Option<String>,
2230}
2231
2232impl DefinedName {
2233 /// Creates a workbook-scoped defined name, not hidden, no comment.
2234 pub fn new(name: impl Into<String>, refers_to: impl Into<String>) -> Self {
2235 Self {
2236 name: name.into(),
2237 refers_to: refers_to.into(),
2238 local_sheet_id: None,
2239 hidden: false,
2240 comment: None,
2241 }
2242 }
2243
2244 /// Scopes this name to a single sheet (by its 0-based position in
2245 /// [`Workbook.sheets`](Workbook::sheets)) and returns it for chaining.
2246 pub fn with_local_sheet_id(mut self, local_sheet_id: usize) -> Self {
2247 self.local_sheet_id = Some(local_sheet_id);
2248 self
2249 }
2250
2251 /// Sets whether this name is hidden in Excel's Name Manager and returns it for chaining.
2252 pub fn with_hidden(mut self, hidden: bool) -> Self {
2253 self.hidden = hidden;
2254 self
2255 }
2256
2257 /// Sets this name's comment and returns it for chaining.
2258 pub fn with_comment(mut self, comment: impl Into<String>) -> Self {
2259 self.comment = Some(comment.into());
2260 self
2261 }
2262}
2263
2264/// A sheet's tab visibility (`<sheet state=".">`, `ST_SheetState`) — replacing the plain `bool`
2265/// used since. `VeryHidden` (unlike `Hidden`) cannot be un-hidden from Excel's own "Unhide." dialog
2266/// — only from the VBA object model or by editing the file directly — real-world usage is normally
2267/// to hide implementation-detail sheets (lookup tables, staging data) from end users while still
2268/// letting formulas reference them.
2269#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
2270pub enum SheetVisibility {
2271 #[default]
2272 Visible,
2273 Hidden,
2274 VeryHidden,
2275}
2276
2277/// A single column's width/visibility/outline setting (`<col>`, `CT_Col`) — extended for
2278/// `collapsed`.
2279#[derive(Debug, Clone, PartialEq)]
2280pub struct ColumnSetting {
2281 /// The 0-based column index this setting applies to (e.g. `0` for column A) — written as `min
2282 /// == max == column + 1` (see `Sheet.column_settings`'s doc comment for why this crate doesn't
2283 /// bother collapsing adjacent columns into a single ranged `<col>`).
2284 pub column: usize,
2285 /// The column's width, in Excel's own character-width units. `None` leaves the column at
2286 /// Excel's default width.
2287 pub width: Option<f64>,
2288 /// Whether this column is hidden.
2289 pub hidden: bool,
2290 /// This column's outline (grouping) level (0-7).
2291 pub outline_level: u8,
2292 /// Whether this column's outline group is shown collapsed (the "-" summary-only state of the
2293 /// little outline button). Distinct from `outline_level` itself (which columns belong to the
2294 /// group) — this is only the group's current expanded/collapsed *display* state.
2295 pub collapsed: bool,
2296 /// This column's default cell format (`<col style="n">`, an index into `xl/styles.xml`'s
2297 /// `cellXfs`). Applied by real Excel to any cell in this column that doesn't carry its own
2298 /// explicit `<c s="..">` — e.g. a brand-new cell typed into an empty row of this column picks
2299 /// up this format automatically. Distinct from [`Cell::format`]: a cell's own format always
2300 /// wins over its column's default when both are set (same precedence as real Excel's own
2301 /// column-formatting feature); this crate does not enforce or resolve that precedence itself,
2302 /// it only stores and writes each side's format independently, same "storage, not resolution"
2303 /// posture as elsewhere in this crate. Deduplicated into shared `cellXfs` entries by the writer
2304 /// exactly like `Cell::format` — see `writer.rs::collect_cell_formats`.
2305 pub style: Option<CellFormat>,
2306}
2307
2308impl ColumnSetting {
2309 /// Creates a column setting with no width/hidden/outline/style override yet (only useful as a
2310 /// starting point for the `with_*` builders).
2311 pub fn new(column: usize) -> Self {
2312 Self {
2313 column,
2314 width: None,
2315 hidden: false,
2316 outline_level: 0,
2317 collapsed: false,
2318 style: None,
2319 }
2320 }
2321
2322 /// Sets the column's width and returns it for chaining.
2323 pub fn with_width(mut self, width: f64) -> Self {
2324 self.width = Some(width);
2325 self
2326 }
2327
2328 /// Sets the column's default cell format and returns it for chaining.
2329 pub fn with_style(mut self, style: CellFormat) -> Self {
2330 self.style = Some(style);
2331 self
2332 }
2333
2334 /// Sets whether the column is hidden and returns it for chaining.
2335 pub fn with_hidden(mut self, hidden: bool) -> Self {
2336 self.hidden = hidden;
2337 self
2338 }
2339
2340 /// Sets whether this column's outline group is shown collapsed and returns it for chaining.
2341 pub fn with_collapsed(mut self, collapsed: bool) -> Self {
2342 self.collapsed = collapsed;
2343 self
2344 }
2345
2346 /// Sets the column's outline (grouping) level and returns it for chaining.
2347 pub fn with_outline_level(mut self, level: u8) -> Self {
2348 self.outline_level = level;
2349 self
2350 }
2351}
2352
2353/// Frozen panes (`<pane state="frozen">`, `CT_Pane`). See `Sheet.freeze_panes`'s doc comment for
2354/// why only "frozen" (not "split") panes are modeled.
2355#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2356pub struct FreezePane {
2357 /// The number of columns frozen at the left (`0` = no column freeze).
2358 pub frozen_columns: usize,
2359 /// The number of rows frozen at the top (`0` = no row freeze).
2360 pub frozen_rows: usize,
2361}
2362
2363impl FreezePane {
2364 /// Creates a freeze-panes setting.
2365 pub fn new(frozen_columns: usize, frozen_rows: usize) -> Self {
2366 Self {
2367 frozen_columns,
2368 frozen_rows,
2369 }
2370 }
2371}
2372
2373/// A sheet's page orientation (`ST_Orientation`) — only `portrait`/ `landscape` are modeled,
2374/// `default` (a third schema value, treated by Excel as "portrait") is not, same posture as
2375/// `word-ooxml`'s own `PageOrientation`.
2376#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2377pub enum PageOrientation {
2378 Portrait,
2379 Landscape,
2380}
2381
2382/// A sheet's print/page-setup settings. `PrintSettings::default()` (everything `None`) writes no
2383/// print-related XML at all.
2384///
2385/// `print_area`/`repeat_rows`/`repeat_columns` are, despite looking like plain worksheet settings,
2386/// actually implemented by real Excel as reserved sheet-scoped defined names (`_xlnm.Print_Area`/
2387/// `_xlnm.Print_Titles`, confirmed via ECMA-376 §18.2.6's "Print_Area"/"Print_Titles" built-in name
2388/// table) rather than a `<pageSetup>` attribute — this crate's writer synthesizes those
2389/// `DefinedName` entries automatically from each sheet's `Sheet.print_settings` when writing
2390/// `xl/workbook.xml`'s `<definedNames>` (see `writer.rs::synthesized_print_defined_names`), the
2391/// caller never constructs them directly.
2392#[derive(Debug, Default, Clone, PartialEq)]
2393pub struct PrintSettings {
2394 /// The print area (e.g. `"$A$1:$F$30"`) — implemented via `_xlnm.Print_Area`.
2395 pub print_area: Option<String>,
2396 /// Rows to repeat on every printed page (e.g. `"$1:$2"`) — implemented via
2397 /// `_xlnm.Print_Titles`.
2398 pub repeat_rows: Option<String>,
2399 /// Columns to repeat on every printed page (e.g. `"$A:$B"`) — implemented via
2400 /// `_xlnm.Print_Titles` (combined with `repeat_rows` into a single name's `refers_to` when both
2401 /// are set, exactly like real Excel's own "Rows to repeat at top" + "Columns to repeat at left"
2402 /// combined into one `Print_Titles` name).
2403 pub repeat_columns: Option<String>,
2404 /// Scales the printout to fit within this many pages wide. Setting either this or
2405 /// `fit_to_height` switches on Excel's "Fit to" print scaling mode (`<sheetPr><pageSetUpPr
2406 /// fitToPage="1"/></sheetPr>`); leaving the other `None` means "as many pages as needed" in
2407 /// that dimension, matching Excel's own "Fit to: N page(s) wide by (blank) tall" UI behavior
2408 /// (written as `fitToWidth="0"`/`fitToHeight="0"`, `CT_PageSetup`'s own convention for
2409 /// "unconstrained").
2410 pub fit_to_width: Option<u32>,
2411 /// Scales the printout to fit within this many pages tall.
2412 pub fit_to_height: Option<u32>,
2413 /// Scales the printout by this percentage (`<pageSetup scale="..">`, e.g. `90` for 90%).
2414 /// Distinct from `fit_to_width`/`fit_to_height`: real Excel's "Page Setup" dialog offers either
2415 /// "Adjust to: N%" (`scale`) or "Fit to: W x H page(s)" (`fit_to_width`/`fit_to_height`) as two
2416 /// mutually exclusive radio-button modes (`<sheetPr><pageSetUpPr fitToPage>` selects which of
2417 /// the two `<pageSetup>` honors) — setting both here is the caller's own responsibility to
2418 /// avoid, not validated.
2419 pub scale: Option<u32>,
2420 /// Page orientation. `None` uses Excel's own default (portrait).
2421 pub orientation: Option<PageOrientation>,
2422 /// The page header text (`<oddHeader>`), written verbatim — Excel's own `&L`/`&C`/`&R` section
2423 /// codes and `&P`/`&N`/`&D`/. field codes are the caller's responsibility to include if wanted,
2424 /// same "storage, not a template engine" posture as a formula's text.
2425 pub header: Option<String>,
2426 /// The page footer text (`<oddFooter>`), written verbatim.
2427 pub footer: Option<String>,
2428 /// A different header on the first printed page (`<firstHeader>`), written verbatim. Setting
2429 /// this (or [`footer_first`](PrintSettings::footer_first)) switches on `<headerFooter
2430 /// differentFirst="1">`.
2431 pub header_first: Option<String>,
2432 /// A different footer on the first printed page (`<firstFooter>`).
2433 pub footer_first: Option<String>,
2434 /// A different header on even printed pages (`<evenHeader>`). Setting this (or
2435 /// [`footer_even`](PrintSettings::footer_even)) switches on `<headerFooter
2436 /// differentOddEven="1">`; the existing
2437 /// [`header`](PrintSettings::header)/[`footer`](PrintSettings::footer) fields become the
2438 /// *odd*-page header/footer once this is set, same as real Excel's own
2439 /// `<oddHeader>`/`<oddFooter>` meaning "odd, or every page if `differentOddEven` isn't set".
2440 pub header_even: Option<String>,
2441 /// A different footer on even printed pages (`<evenFooter>`).
2442 pub footer_even: Option<String>,
2443}
2444
2445impl PrintSettings {
2446 /// Creates print settings with every field unset (equivalent to `PrintSettings::default()`,
2447 /// only useful as a starting point for the `with_*` builders).
2448 pub fn new() -> Self {
2449 Self::default()
2450 }
2451
2452 /// Sets the print area and returns the settings for chaining.
2453 pub fn with_print_area(mut self, area: impl Into<String>) -> Self {
2454 self.print_area = Some(area.into());
2455 self
2456 }
2457
2458 /// Sets the rows to repeat on every printed page and returns the settings for chaining.
2459 pub fn with_repeat_rows(mut self, rows: impl Into<String>) -> Self {
2460 self.repeat_rows = Some(rows.into());
2461 self
2462 }
2463
2464 /// Sets the columns to repeat on every printed page and returns the settings for chaining.
2465 pub fn with_repeat_columns(mut self, columns: impl Into<String>) -> Self {
2466 self.repeat_columns = Some(columns.into());
2467 self
2468 }
2469
2470 /// Sets the fit-to-width page count and returns the settings for chaining.
2471 pub fn with_fit_to_width(mut self, pages: u32) -> Self {
2472 self.fit_to_width = Some(pages);
2473 self
2474 }
2475
2476 /// Sets the fit-to-height page count and returns the settings for chaining.
2477 pub fn with_fit_to_height(mut self, pages: u32) -> Self {
2478 self.fit_to_height = Some(pages);
2479 self
2480 }
2481
2482 /// Sets the print scale percentage and returns the settings for chaining.
2483 pub fn with_scale(mut self, percent: u32) -> Self {
2484 self.scale = Some(percent);
2485 self
2486 }
2487
2488 /// Sets the page orientation and returns the settings for chaining.
2489 pub fn with_orientation(mut self, orientation: PageOrientation) -> Self {
2490 self.orientation = Some(orientation);
2491 self
2492 }
2493
2494 /// Sets the page header text and returns the settings for chaining.
2495 pub fn with_header(mut self, header: impl Into<String>) -> Self {
2496 self.header = Some(header.into());
2497 self
2498 }
2499
2500 /// Sets the page footer text and returns the settings for chaining.
2501 pub fn with_footer(mut self, footer: impl Into<String>) -> Self {
2502 self.footer = Some(footer.into());
2503 self
2504 }
2505
2506 /// Sets the first page's header text and returns the settings for chaining.
2507 pub fn with_header_first(mut self, header: impl Into<String>) -> Self {
2508 self.header_first = Some(header.into());
2509 self
2510 }
2511
2512 /// Sets the first page's footer text and returns the settings for chaining.
2513 pub fn with_footer_first(mut self, footer: impl Into<String>) -> Self {
2514 self.footer_first = Some(footer.into());
2515 self
2516 }
2517
2518 /// Sets the even pages' header text and returns the settings for chaining.
2519 pub fn with_header_even(mut self, header: impl Into<String>) -> Self {
2520 self.header_even = Some(header.into());
2521 self
2522 }
2523
2524 /// Sets the even pages' footer text and returns the settings for chaining.
2525 pub fn with_footer_even(mut self, footer: impl Into<String>) -> Self {
2526 self.footer_even = Some(footer.into());
2527 self
2528 }
2529}
2530
2531/// A sheet's protection settings (`<sheetProtection>`, `CT_SheetProtection`). Only the legacy
2532/// 16-bit password hash (`password="XXXX"`, still fully understood by every version of Excel even
2533/// though the UI has written the newer SHA-512 `hashValue`/`saltValue`/`spinCount` form since Excel
2534/// 2013 — see `writer.rs::legacy_password_hash`'s doc comment) is modeled. Every other
2535/// `CT_SheetProtection` flag (`formatCells`/`insertRows`/`sort`/ `autoFilter`/`objects`/..) is left
2536/// at Excel's own schema default — this crate only ever writes `sheet="1"` and, if set, `password`,
2537/// matching what checking Excel's own "Protect Sheet" dialog with no extra options touched
2538/// produces.
2539#[derive(Debug, Clone, PartialEq)]
2540pub struct SheetProtection {
2541 /// The password, in plain text — hashed into the legacy 16-bit form on write; never stored or
2542 /// written in plain text anywhere in the output file. `None` protects the sheet with no
2543 /// password at all (Excel's "Protect Sheet" with a blank password field — still blocks
2544 /// accidental edits through the UI, but anyone can immediately remove the protection since
2545 /// there's nothing to check a password against).
2546 pub password: Option<String>,
2547}
2548
2549impl SheetProtection {
2550 /// Protects the sheet with no password.
2551 pub fn locked() -> Self {
2552 Self { password: None }
2553 }
2554
2555 /// Protects the sheet with a password.
2556 pub fn with_password(password: impl Into<String>) -> Self {
2557 Self {
2558 password: Some(password.into()),
2559 }
2560 }
2561}
2562
2563/// Workbook *structure* protection (`<workbookProtection>`, `CT_WorkbookProtection`). Locks whether
2564/// sheets can be added/renamed/hidden/reordered/deleted (`lockStructure`) and/or whether the
2565/// workbook's window can be moved/resized/closed (`lockWindows`) — distinct from
2566/// [`SheetProtection`], which locks cell editing within one sheet. Uses the same legacy 16-bit
2567/// password hash as `SheetProtection` (see `writer.rs::legacy_password_hash`'s doc comment for the
2568/// algorithm and its caveats) — implemented but not yet verified against a real Excel installation.
2569#[derive(Debug, Clone, PartialEq)]
2570pub struct WorkbookProtection {
2571 /// The password, in plain text — hashed on write, never recoverable on read (same one-way
2572 /// posture as `SheetProtection::password`).
2573 pub password: Option<String>,
2574 /// Locks the workbook's structure (adding/renaming/hiding/reordering/ deleting sheets).
2575 pub lock_structure: bool,
2576 /// Locks the workbook's window (moving/resizing/closing).
2577 pub lock_windows: bool,
2578}
2579
2580impl WorkbookProtection {
2581 /// Locks the workbook's structure, no password, windows not locked.
2582 pub fn locked() -> Self {
2583 Self {
2584 password: None,
2585 lock_structure: true,
2586 lock_windows: false,
2587 }
2588 }
2589
2590 /// Sets a password and returns the protection for chaining.
2591 pub fn with_password(mut self, password: impl Into<String>) -> Self {
2592 self.password = Some(password.into());
2593 self
2594 }
2595
2596 /// Sets whether the workbook's window is locked and returns the protection for chaining.
2597 pub fn with_lock_windows(mut self, lock_windows: bool) -> Self {
2598 self.lock_windows = lock_windows;
2599 self
2600 }
2601}
2602
2603/// Write-protection / "read-only recommended" (`<fileSharing>`, `CT_FileSharing`) — point 54, see
2604/// [`Workbook::write_protection`]'s doc comment for how this differs from [`WorkbookProtection`].
2605/// Confirmed against `sml.xsd`: `readOnlyRecommended` defaults to `false`; `userName` is a plain
2606/// display string (shown in Excel's own "reserved by.." prompt); `reservationPassword` uses the
2607/// same legacy 16-bit hash as `WorkbookProtection`/`SheetProtection` (see
2608/// `writer.rs::legacy_password_hash`'s doc comment) — the newer SHA-512
2609/// `algorithmName`/`hashValue`/`saltValue`/`spinCount` form is not modeled, same "one well-defined
2610/// mechanism" posture as `ProtectedRange`.
2611#[derive(Debug, Default, Clone, PartialEq)]
2612pub struct WriteProtection {
2613 /// Recommends opening the file as read-only (`readOnlyRecommended`) — the checkbox behind
2614 /// Excel's Save As > Tools > General Options > "Read-only recommended".
2615 pub read_only_recommended: bool,
2616 /// The name shown in Excel's write-reservation prompt (`userName`), e.g. the name of the user
2617 /// who last saved with a reservation password set.
2618 pub user_name: Option<String>,
2619 /// The write-reservation password, in plain text — hashed into the legacy 16-bit form on write,
2620 /// never recoverable on read (same one-way posture as `WorkbookProtection::password`). Excel
2621 /// prompts for this password (or "Read Only") when opening the file.
2622 pub password: Option<String>,
2623}
2624
2625impl WriteProtection {
2626 /// Recommends opening the file as read-only, no password/user name set.
2627 pub fn recommended() -> Self {
2628 Self {
2629 read_only_recommended: true,
2630 user_name: None,
2631 password: None,
2632 }
2633 }
2634
2635 /// Sets the write-reservation password and returns it for chaining.
2636 pub fn with_password(mut self, password: impl Into<String>) -> Self {
2637 self.password = Some(password.into());
2638 self
2639 }
2640
2641 /// Sets the user name shown in the write-reservation prompt and returns it for chaining.
2642 pub fn with_user_name(mut self, user_name: impl Into<String>) -> Self {
2643 self.user_name = Some(user_name.into());
2644 self
2645 }
2646}
2647
2648/// One named, independently-passworded protected range (`<protectedRange>`, `CT_ProtectedRange`) —
2649/// point 51, see [`Sheet::protected_ranges`]'s doc comment for how this differs from
2650/// [`SheetProtection`]. Confirmed against `sml.xsd`: `name`/`sqref` are the only required
2651/// attributes; `password` uses the same legacy 16-bit hash as
2652/// `SheetProtection`/`WorkbookProtection` (see `writer.rs::legacy_password_hash`'s doc comment) —
2653/// the newer SHA-512 `algorithmName`/`hashValue`/`saltValue`/`spinCount` form and the optional
2654/// `securityDescriptor` child/attribute (a Windows ACL-based alternative to a password) are not
2655/// modeled, same "one well-defined mechanism, not every historical Excel option" posture as
2656/// `SheetProtection`.
2657#[derive(Debug, Clone, PartialEq)]
2658pub struct ProtectedRange {
2659 /// This range's name, shown in Excel's "Allow Users to Edit Ranges" dialog (must be unique
2660 /// within the sheet, not validated here).
2661 pub name: String,
2662 /// The range this entry covers (`sqref`, e.g. `"B2:D10"`) — like [`IgnoredError::range`],
2663 /// several disjoint cells can be packed into one `sqref` (space-separated) if the caller wants
2664 /// a single named range to cover them.
2665 pub range: String,
2666 /// The password unlocking this range while the sheet is otherwise protected, in plain text —
2667 /// hashed into the legacy 16-bit form on write, never stored or written in plain text (same
2668 /// one-way posture as `SheetProtection::password`). `None` leaves the range editable by anyone
2669 /// once the sheet itself is protected (no password prompt), same as
2670 /// `SheetProtection::password`'s `None` case.
2671 pub password: Option<String>,
2672}
2673
2674impl ProtectedRange {
2675 /// Creates a named protected range with no password.
2676 pub fn new(name: impl Into<String>, range: impl Into<String>) -> Self {
2677 Self {
2678 name: name.into(),
2679 range: range.into(),
2680 password: None,
2681 }
2682 }
2683
2684 /// Sets a password and returns the range for chaining.
2685 pub fn with_password(mut self, password: impl Into<String>) -> Self {
2686 self.password = Some(password.into());
2687 self
2688 }
2689}
2690
2691/// A per-cell hyperlink (`<hyperlink>`, `CT_Hyperlink`).
2692#[derive(Debug, Clone, PartialEq)]
2693pub struct CellHyperlink {
2694 /// The cell this hyperlink is attached to (e.g. `"A1"`).
2695 pub cell: String,
2696 /// The link's target.
2697 pub target: HyperlinkTarget,
2698 /// An optional tooltip shown on hover (`tooltip`).
2699 pub tooltip: Option<String>,
2700}
2701
2702/// A [`CellHyperlink`]'s target.
2703#[derive(Debug, Clone, PartialEq)]
2704pub enum HyperlinkTarget {
2705 /// An external URL (`r:id`, resolved through this worksheet's own `.rels` part,
2706 /// `TargetMode="External"` — same shape as `word-ooxml`'s `Hyperlink::External`).
2707 External(String),
2708 /// A location within the same workbook (`location`, e.g. `"Feuil2!A1"` or a defined name) — no
2709 /// relationship needed, same shape as `word-ooxml`'s `Hyperlink::Internal`.
2710 Internal(String),
2711}
2712
2713impl CellHyperlink {
2714 /// Creates an external hyperlink on the given cell.
2715 pub fn external(cell: impl Into<String>, url: impl Into<String>) -> Self {
2716 Self {
2717 cell: cell.into(),
2718 target: HyperlinkTarget::External(url.into()),
2719 tooltip: None,
2720 }
2721 }
2722
2723 /// Creates an internal hyperlink (to a cell/defined name elsewhere in this workbook) on the
2724 /// given cell.
2725 pub fn internal(cell: impl Into<String>, location: impl Into<String>) -> Self {
2726 Self {
2727 cell: cell.into(),
2728 target: HyperlinkTarget::Internal(location.into()),
2729 tooltip: None,
2730 }
2731 }
2732
2733 /// Sets a tooltip and returns the hyperlink for chaining.
2734 pub fn with_tooltip(mut self, tooltip: impl Into<String>) -> Self {
2735 self.tooltip = Some(tooltip.into());
2736 self
2737 }
2738}
2739
2740/// A comment attached to a cell (legacy `xl/comments*.xml` + VML drawing, `CT_Comment`). Only
2741/// plain-text comments with a fixed author are modeled — `CT_Comment`'s rich-text (`CT_RElt` runs)
2742/// body and threaded-comment reply chains (`CT_ThreadedComment`, the modern Excel 365 "@mention"
2743/// comment form) are out of scope.
2744#[derive(Debug, Clone, PartialEq)]
2745pub struct CellComment {
2746 /// The cell this comment is attached to (e.g. `"B2"`).
2747 pub cell: String,
2748 /// The comment's author (shown in the comment box's first bold line by Excel's own convention,
2749 /// and stored as a separate `<author>` entry — not otherwise validated).
2750 pub author: String,
2751 /// The comment's body text — when [`rich_text`](CellComment::rich_text) is `Some`, this is
2752 /// simply the plain-text concatenation of every run's own text (kept in sync automatically by
2753 /// [`CellComment::with_rich_text`]), not a separate independent value.
2754 pub text: String,
2755 /// The comment's body as per-run formatted text (`<text><r>..</r></text>`, reusing
2756 /// [`RichTextRun`] — the same per-run bold/italic/color/size/ name/underline/strike model a
2757 /// rich-text cell's shared-string runs already use). `None` writes the plain single-run
2758 /// `<text><r><t>..</t></r></text>` form instead, using `text` verbatim.
2759 pub rich_text: Option<Vec<RichTextRun>>,
2760}
2761
2762impl CellComment {
2763 /// Creates a plain-text comment.
2764 pub fn new(
2765 cell: impl Into<String>,
2766 author: impl Into<String>,
2767 text: impl Into<String>,
2768 ) -> Self {
2769 Self {
2770 cell: cell.into(),
2771 author: author.into(),
2772 text: text.into(),
2773 rich_text: None,
2774 }
2775 }
2776
2777 /// Sets this comment's body as per-run formatted text and returns it for chaining. Also
2778 /// resolves `text` (the plain-text fallback) to the concatenation of every run's own text, so
2779 /// both fields stay consistent.
2780 pub fn with_rich_text(mut self, runs: Vec<RichTextRun>) -> Self {
2781 self.text = runs.iter().map(|run| run.text.as_str()).collect();
2782 self.rich_text = Some(runs);
2783 self
2784 }
2785}
2786
2787/// A custom table style definition (`<tableStyle>`, `CT_TableStyle`), declared workbook-wide in
2788/// `xl/styles.xml`'s `<tableStyles>` (`CT_TableStyles`) and referenced by name from an
2789/// [`ExcelTable`]'s own [`table_style_name`](ExcelTable::table_style_name). Confirmed against
2790/// `sml.xsd`: `CT_Stylesheet`'s own sequence puts `tableStyles` right after `dxfs`, before `colors`
2791/// (not written by this crate). This crate only ever writes `pivot="0"` (this style is for tables,
2792/// not pivot tables) and no `count` attribute (`CT_TableStyle`'s `count` is informational, Excel
2793/// recomputes it).
2794#[derive(Debug, Clone, PartialEq)]
2795pub struct TableStyle {
2796 /// This style's name, referenced by [`ExcelTable::table_style_name`] (must be unique within the
2797 /// workbook, not validated here — a name colliding with one of Excel's own dozens of built-in
2798 /// style names, e.g. `"TableStyleMedium2"`, silently shadows the built-in one for this
2799 /// workbook).
2800 pub name: String,
2801 /// The banding/region formats making up this style, in no particular required order (real Excel
2802 /// itself doesn't enforce one either).
2803 pub elements: Vec<TableStyleElement>,
2804}
2805
2806impl TableStyle {
2807 /// Creates a named table style with no elements yet.
2808 pub fn new(name: impl Into<String>) -> Self {
2809 Self {
2810 name: name.into(),
2811 elements: Vec::new(),
2812 }
2813 }
2814
2815 /// Appends a formatted element and returns the style for chaining.
2816 pub fn with_element(mut self, element: TableStyleElement) -> Self {
2817 self.elements.push(element);
2818 self
2819 }
2820}
2821
2822/// A named cell style (`<cellStyle>`/its paired `<cellStyleXfs>` `<xf>` entry,
2823/// `CT_CellStyle`/`CT_Xf`), declared workbook-wide in `xl/styles.xml` and referenced from any
2824/// [`CellFormat`] via [`CellFormat::named_style`]. This is Excel's "Cell Styles" gallery: built-in
2825/// entries like `Good`/`Bad`/ `Neutral`/`Heading 1`-`4`/`Input`/`Output`/`Calculation`/`Currency`/
2826/// `Percent`/`Comma`, or a caller's own custom-named style. Confirmed against `sml.xsd`:
2827/// `CT_CellStyle`'s only required attribute is `xfId` (this crate assigns it automatically, from
2828/// this entry's position in [`Workbook::named_cell_styles`], mirroring how a `dxfId`/table-style
2829/// `dxfId` is resolved rather than caller-supplied); `name`/`builtinId` are both optional but a
2830/// real style always sets `name` at least (an unnamed entry would show as blank in Excel's gallery,
2831/// so `name` is required here, not `Option`).
2832#[derive(Debug, Clone, PartialEq)]
2833pub struct NamedCellStyle {
2834 /// This style's name, shown in Excel's Cell Styles gallery and referenced by
2835 /// [`CellFormat::named_style`] (must be unique within the workbook, not validated here).
2836 pub name: String,
2837 /// The formatting this style applies (font/fill/border/number format/ alignment — the same
2838 /// [`CellFormat`] shape a regular cell format uses, written into its own dedicated
2839 /// `cellStyleXfs` entry rather than the shared `cellXfs` collection, mirroring how a `dxf`'s
2840 /// `CellFormat` is a separate index space too, see `writer.rs::collect_dxfs`'s doc comment).
2841 pub format: CellFormat,
2842 /// Identifies this as one of Excel's own built-in styles rather than a caller-defined one
2843 /// (`builtinId`, e.g. `0` = `"Normal"`, `3` = `"Good"`, `4` = `"Bad"`, `5` = `"Neutral"`,
2844 /// `7`-`10` = `"Heading 1"`-`"Heading 4"`. — `ST_CellStyleBuiltinId`'s full ~54-value table is
2845 /// not reproduced/validated here, this crate simply writes whatever `u32` the caller supplies
2846 /// verbatim). `None` for a purely custom style with no built-in equivalent.
2847 pub builtin_id: Option<u32>,
2848}
2849
2850impl NamedCellStyle {
2851 /// Creates a named cell style with the given name and format, no built-in id (a purely custom
2852 /// style).
2853 pub fn new(name: impl Into<String>, format: CellFormat) -> Self {
2854 Self {
2855 name: name.into(),
2856 format,
2857 builtin_id: None,
2858 }
2859 }
2860
2861 /// Sets the built-in id and returns the style for chaining.
2862 pub fn with_builtin_id(mut self, builtin_id: u32) -> Self {
2863 self.builtin_id = Some(builtin_id);
2864 self
2865 }
2866}
2867
2868/// One formatted region of a [`TableStyle`] (`<tableStyleElement>`, `CT_TableStyleElement`).
2869#[derive(Debug, Clone, PartialEq)]
2870pub struct TableStyleElement {
2871 /// Which region of the table this element formats.
2872 pub element_type: TableStyleElementType,
2873 /// The formatting applied to this region — written as a `<dxf>` entry in `xl/styles.xml`'s
2874 /// `<dxfs>` (the same shared, deduplicated collection a [`ConditionalFormattingRule`]'s own
2875 /// `format` uses, see `writer.rs::collect_dxfs`) and referenced here by `dxfId`, exactly like a
2876 /// conditional formatting rule.
2877 pub format: CellFormat,
2878}
2879
2880impl TableStyleElement {
2881 /// Creates a table style element.
2882 pub fn new(element_type: TableStyleElementType, format: CellFormat) -> Self {
2883 Self {
2884 element_type,
2885 format,
2886 }
2887 }
2888}
2889
2890/// Which region of a table a [`TableStyleElement`] formats (`ST_TableStyleType`) — only the most
2891/// commonly used values are modeled (whole-table default, header/total rows, and row/column
2892/// banding); `ST_TableStyleType`'s remaining values (`firstHeaderCell`/
2893/// `lastHeaderCell`/`firstTotalCell`/`lastTotalCell`/`firstSubtotalColumn`/
2894/// `secondSubtotalColumn`/`thirdSubtotalColumn`/`firstSubtotalRow`/
2895/// `secondSubtotalRow`/`thirdSubtotalRow`/`blankRow`/
2896/// `firstColumnSubheading`/`secondColumnSubheading`/`thirdColumnSubheading`/
2897/// `firstRowSubheading`/`secondRowSubheading`/`thirdRowSubheading`/
2898/// `pageFieldLabels`/`pageFieldValues`, confirmed via `sml.xsd`) cover niche PivotTable-report and
2899/// outline-subtotal layouts out of this crate's current scope.
2900#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2901pub enum TableStyleElementType {
2902 /// The whole table's default formatting (`"wholeTable"`).
2903 WholeTable,
2904 /// The header row (`"headerRow"`).
2905 HeaderRow,
2906 /// The totals row (`"totalRow"`).
2907 TotalRow,
2908 /// Odd data rows, 1-based from the header (`"firstRowStripe"`).
2909 FirstRowStripe,
2910 /// Even data rows (`"secondRowStripe"`).
2911 SecondRowStripe,
2912 /// Odd data columns (`"firstColumnStripe"`).
2913 FirstColumnStripe,
2914 /// Even data columns (`"secondColumnStripe"`).
2915 SecondColumnStripe,
2916 /// The table's first (leftmost) column (`"firstColumn"`).
2917 FirstColumn,
2918 /// The table's last (rightmost) column (`"lastColumn"`).
2919 LastColumn,
2920}
2921
2922/// A structured (`ListObject`) table (`<table>`, `CT_Table`) — extended for per-column
2923/// calculated-column formulas and totals-row functions, and for
2924/// [`table_style_name`](ExcelTable::table_style_name).
2925#[derive(Debug, Clone, PartialEq)]
2926pub struct ExcelTable {
2927 /// The table's name, shown in Excel's Name Manager/Table Design tab (must be unique within the
2928 /// workbook, not validated here).
2929 pub name: String,
2930 /// The table's full range, header row included (e.g. `"A1:D6"`).
2931 pub range: String,
2932 /// The table's columns, in order, matching the header row's actual cell text (not written into
2933 /// `<sheetData>` by this crate — the caller is responsible for the header row's own cell values
2934 /// matching each column's name, same "caller keeps the two in sync" posture as
2935 /// `ConditionalFormattingRule::range` referencing real cells).
2936 pub columns: Vec<TableColumn>,
2937 /// Whether the table shows a totals row as its last row.
2938 pub show_totals_row: bool,
2939 /// This table's remembered sort order (`<sortState>`, `CT_SortState`). Confirmed against
2940 /// `sml.xsd`: under `<table>` (`CT_Table`'s own sequence), `sortState` is a direct sibling of
2941 /// `autoFilter`, not nested inside it — distinct from [`Sheet::sort_state`], which *is* nested
2942 /// inside a plain worksheet-level `<autoFilter>` (`CT_AutoFilter`'s own sequence puts
2943 /// `sortState` after `filterColumn*`). Storage only: writing this records what Excel would show
2944 /// if the user reopened the table's sort dialog, it does not itself reorder `sheet.rows` — the
2945 /// caller remains responsible for the data actually being in that order, same "caller keeps
2946 /// things in sync" posture as `AutoFilterColumn`.
2947 pub sort_state: Option<SortState>,
2948 /// The named table style applied to this table (`<tableStyleInfo name="..">`). `None` keeps
2949 /// this crate's own long-standing default, `TableStyleMedium2` (Excel's built-in "Table Style
2950 /// Medium 2", the first entry in its table style gallery) — same behavior as before this point
2951 /// existed. `Some` can name either one of Excel's own dozens of built-in styles (e.g.
2952 /// `"TableStyleLight9"`) or a custom [`TableStyle`] declared in [`Workbook::table_styles`] —
2953 /// this crate does not itself validate that a `Some` name actually resolves to something Excel
2954 /// recognizes, same "caller's responsibility" posture as elsewhere (e.g. `Sheet.name`'s
2955 /// character-limit note).
2956 pub table_style_name: Option<String>,
2957}
2958
2959impl ExcelTable {
2960 /// Creates a table with no totals row, from a list of plain column names (no calculated-column
2961 /// formula, no totals-row function on any column yet) — the common case. Use
2962 /// [`ExcelTable::with_columns`] instead for full control over each [`TableColumn`].
2963 pub fn new<S: Into<String>>(
2964 name: impl Into<String>,
2965 range: impl Into<String>,
2966 columns: Vec<S>,
2967 ) -> Self {
2968 Self {
2969 name: name.into(),
2970 range: range.into(),
2971 columns: columns.into_iter().map(TableColumn::new).collect(),
2972 show_totals_row: false,
2973 sort_state: None,
2974 table_style_name: None,
2975 }
2976 }
2977
2978 /// Creates a table from a list of fully-specified [`TableColumn`]s.
2979 pub fn with_columns(
2980 name: impl Into<String>,
2981 range: impl Into<String>,
2982 columns: Vec<TableColumn>,
2983 ) -> Self {
2984 Self {
2985 name: name.into(),
2986 range: range.into(),
2987 columns,
2988 show_totals_row: false,
2989 sort_state: None,
2990 table_style_name: None,
2991 }
2992 }
2993
2994 /// Sets whether the table shows a totals row and returns it for chaining.
2995 pub fn with_totals_row(mut self, show: bool) -> Self {
2996 self.show_totals_row = show;
2997 self
2998 }
2999
3000 /// Sets this table's remembered sort order and returns it for chaining.
3001 pub fn with_sort_state(mut self, sort_state: SortState) -> Self {
3002 self.sort_state = Some(sort_state);
3003 self
3004 }
3005
3006 /// Sets this table's named style and returns it for chaining. See
3007 /// [`table_style_name`](ExcelTable::table_style_name)'s doc comment.
3008 pub fn with_table_style_name(mut self, name: impl Into<String>) -> Self {
3009 self.table_style_name = Some(name.into());
3010 self
3011 }
3012}
3013
3014/// One column of an [`ExcelTable`] (`<tableColumn>`, `CT_TableColumn`) — (plain `name` only),
3015/// extended for `calculated_formula`/ `totals_row_function`/`totals_row_label`.
3016#[derive(Debug, Clone, PartialEq)]
3017pub struct TableColumn {
3018 /// The column's name, matching the header row's actual cell text (see [`ExcelTable::columns`]'s
3019 /// doc comment).
3020 pub name: String,
3021 /// This column's calculated-column formula (`<calculatedColumnFormula>`), if any. Declaring a
3022 /// column calculated in the table definition only; this crate does **not** automatically
3023 /// propagate a `<f>` formula onto every data row's cell in this column — the caller remains
3024 /// responsible for each data row's own cell actually carrying a matching `CellValue::Formula`
3025 /// if the per-row formulas should appear too (storage only, same "caller keeps things in sync"
3026 /// posture as `ExcelTable::columns` itself).
3027 ///
3028 /// **Structured references must use the fully-qualified/expanded form, `TableName[[#This
3029 /// Row],[ColumnName]]` — not the shorthand `[@ColumnName]`.** Both are valid Excel syntax when
3030 /// *typed into the UI* (Excel silently normalizes one to the other), but a real Excel
3031 /// installation rejects and strips the whole `<table>` on open when the shorthand form is what
3032 /// ends up stored verbatim in `calculatedColumnFormula` (and/or a data row's own `<f>`) — a
3033 /// genuine Excel-authored file stores exactly this expanded form (`Table1[[#This
3034 /// Row],[ColumnName]]`) rather than `[@ColumnName]`. This crate does not rewrite or validate
3035 /// formula text — passing a shorthand structured reference here silently produces a file real
3036 /// Excel repairs on open.
3037 pub calculated_formula: Option<String>,
3038 /// This column's totals-row aggregate function (`<tableColumn totalsRowFunction="..">`), if
3039 /// any. Meaningless unless [`ExcelTable::show_totals_row`] is also `true`. Writes only the
3040 /// `totalsRowFunction=".."` attribute itself on `<tableColumn>` — unlike an earlier version of
3041 /// this crate, it does **not** auto-generate a matching
3042 /// `<totalsRowFormula>SUBTOTAL(n,[ColumnName])</totalsRowFormula>` *table-metadata child
3043 /// element* for a built-in function anymore: real Excel never writes that particular child
3044 /// element either (the function name alone determines what it computes internally).
3045 ///
3046 /// **This is a separate thing from the totals row's own cell in `sheetData`, which the caller
3047 /// remains responsible for** (same "caller keeps things in sync" posture as
3048 /// [`calculated_formula`](TableColumn::calculated_formula)): real Excel's own totals row is a
3049 /// genuine formula cell, not a plain cached number — confirmed against a real Excel-authored
3050 /// file, whose totals row cell for a `sum` column literally contains
3051 /// `<f>SUBTOTAL(109,TableName[ColumnName])</f>` with a cached `<v>`, and for `average`,
3052 /// `<f>SUBTOTAL(101,TableName[ColumnName])</f>`. The `10x`-prefixed `SUBTOTAL` function codes
3053 /// (ignoring manually-hidden rows) real Excel tables use are, in `TotalsRowFunction` order:
3054 /// `Average` → 101, `Count` → 102, `CountNums` → 103, `Max` → 104, `Min` → 105, `StdDev` → 107,
3055 /// `Var` → 110, `Sum` → 109 (`Custom` has no fixed code — the caller supplies
3056 /// [`totals_row_formula`](TableColumn::totals_row_formula) directly instead). A totals row cell
3057 /// left as a plain cached number instead of this formula is one of the concrete differences
3058 /// from real Excel output that causes a real Excel installation to reject and repair the file.
3059 /// Only [`TotalsRowFunction::Custom`] pairs with an explicit
3060 /// [`totals_row_formula`](TableColumn::totals_row_formula). Also see
3061 /// [`totals_row_label`](TableColumn::totals_row_label)'s doc comment: setting both on the same
3062 /// column, this field is silently dropped at write time.
3063 pub totals_row_function: Option<TotalsRowFunction>,
3064 /// This column's totals-row label (`<tableColumn totalsRowLabel="..">`), shown instead of a
3065 /// computed value — typically only meaningful on the *first* column of a totals row (e.g.
3066 /// `"Total"`). The same real Excel-authored reference file mentioned in `totals_row_function`'s
3067 /// doc comment also shows that the totals row's own cell for the label column carries the label
3068 /// text itself as a plain string value too (redundant with this attribute, but present) — the
3069 /// caller remains responsible for that cell's actual value matching. **Mutually exclusive with
3070 /// `totals_row_function` on the same column and enforced by the writer** (real Excel's own UI
3071 /// only ever offers one or the other per column): when both are set here, the label wins and
3072 /// `totals_row_function`/`totals_row_formula` are simply not written for that column.
3073 pub totals_row_label: Option<String>,
3074 /// This column's literal totals-row formula text (`<totalsRowFormula>`), only written when
3075 /// `totals_row_function` is [`TotalsRowFunction::Custom`] *and* `totals_row_label` is `None` —
3076 /// every other function variant writes no `<totalsRowFormula>` at all (see
3077 /// `totals_row_function`'s doc comment), and a label on the same column takes priority over
3078 /// both (see `totals_row_label`'s doc comment). If this formula uses a structured reference,
3079 /// see `calculated_formula`'s doc comment: use the expanded `TableName[[#This
3080 /// Row],[ColumnName]]` form, not `[@ColumnName]`.
3081 pub totals_row_formula: Option<String>,
3082}
3083
3084impl TableColumn {
3085 /// Creates a plain column with no calculated formula or totals-row function yet.
3086 pub fn new(name: impl Into<String>) -> Self {
3087 Self {
3088 name: name.into(),
3089 calculated_formula: None,
3090 totals_row_function: None,
3091 totals_row_label: None,
3092 totals_row_formula: None,
3093 }
3094 }
3095
3096 /// Sets this column's calculated-column formula and returns it for chaining.
3097 pub fn with_calculated_formula(mut self, formula: impl Into<String>) -> Self {
3098 self.calculated_formula = Some(formula.into());
3099 self
3100 }
3101
3102 /// Sets this column's totals-row aggregate function and returns it for chaining.
3103 pub fn with_totals_row_function(mut self, function: TotalsRowFunction) -> Self {
3104 self.totals_row_function = Some(function);
3105 self
3106 }
3107
3108 /// Sets this column's totals-row label and returns it for chaining.
3109 pub fn with_totals_row_label(mut self, label: impl Into<String>) -> Self {
3110 self.totals_row_label = Some(label.into());
3111 self
3112 }
3113
3114 /// Sets this column's literal totals-row formula text (only used when `totals_row_function` is
3115 /// [`TotalsRowFunction::Custom`]) and returns it for chaining.
3116 pub fn with_totals_row_formula(mut self, formula: impl Into<String>) -> Self {
3117 self.totals_row_formula = Some(formula.into());
3118 self
3119 }
3120}
3121
3122/// A remembered sort order (`<sortState>`, `CT_SortState`). Written in two different places
3123/// depending on context: as a child of a worksheet-level `<autoFilter>` ([`Sheet::sort_state`]) or
3124/// as a sibling of a table's own `<autoFilter>` ([`ExcelTable::sort_state`]) — see each field's own
3125/// doc comment for the exact schema positioning, confirmed via `sml.xsd`'s `CT_AutoFilter`/
3126/// `CT_Table` sequences. Only the simplest, most common shape is modeled: a plain value-based
3127/// ascending/descending sort per range (`ST_SortBy`'s `"value"`, the schema default) —
3128/// `sortBy="cellColor"/ "fontColor"/"icon"`, `customList`, and `dxfId`/`iconSet`/`iconId` (color/
3129/// icon-based custom sort criteria) are not modeled, same "the common case, not every historical
3130/// Excel option" posture as `SheetProtection`/`AutoFilterColumn`. Storage only — this crate does
3131/// not itself reorder any rows, see `ExcelTable.sort_state`'s doc comment.
3132#[derive(Debug, Clone, PartialEq)]
3133pub struct SortState {
3134 /// The overall range this sort applies to (`ref`, required) — e.g. the table's own data range,
3135 /// or the autofiltered range.
3136 pub range: String,
3137 /// Up to 64 sort keys, in priority order (`<sortCondition>`, `CT_SortState`'s own
3138 /// `maxOccurs="64"` limit — not enforced here, same "caller's responsibility" posture as
3139 /// elsewhere in this crate).
3140 pub conditions: Vec<SortCondition>,
3141}
3142
3143impl SortState {
3144 /// Creates a sort state over `range` with no sort keys yet.
3145 pub fn new(range: impl Into<String>) -> Self {
3146 Self {
3147 range: range.into(),
3148 conditions: Vec::new(),
3149 }
3150 }
3151
3152 /// Appends a sort key and returns the sort state for chaining.
3153 pub fn with_condition(mut self, condition: SortCondition) -> Self {
3154 self.conditions.push(condition);
3155 self
3156 }
3157}
3158
3159/// One sort key within a [`SortState`] (`<sortCondition>`, `CT_SortCondition`).
3160#[derive(Debug, Clone, PartialEq)]
3161pub struct SortCondition {
3162 /// The single column (or row, for a horizontal sort) this key sorts by, as a range reference
3163 /// (`ref`, required) — e.g. `"B2:B10"` for a sort keyed on column B's values.
3164 pub range: String,
3165 /// Whether this key sorts descending (`descending`, default ascending).
3166 pub descending: bool,
3167}
3168
3169impl SortCondition {
3170 /// Creates an ascending sort key over `range`.
3171 pub fn new(range: impl Into<String>) -> Self {
3172 Self {
3173 range: range.into(),
3174 descending: false,
3175 }
3176 }
3177
3178 /// Sets whether this key sorts descending and returns it for chaining.
3179 pub fn with_descending(mut self, descending: bool) -> Self {
3180 self.descending = descending;
3181 self
3182 }
3183}
3184
3185/// A table column's totals-row aggregate function (`ST_TotalsRowFunction`).
3186/// `ST_TotalsRowFunction`'s `"none"` value isn't modeled as its own variant, same convention as
3187/// elsewhere in this crate — `Option::None` on `TableColumn::totals_row_function` already expresses
3188/// it.
3189#[derive(Debug, Clone, Copy, PartialEq, Eq)]
3190pub enum TotalsRowFunction {
3191 Sum,
3192 Average,
3193 Count,
3194 CountNums,
3195 Max,
3196 Min,
3197 StdDev,
3198 Var,
3199 /// A totals-row function this crate doesn't auto-generate a formula for — pair with
3200 /// [`TableColumn::totals_row_formula`] set to the literal formula text the caller wants written
3201 /// verbatim.
3202 Custom,
3203}
3204
3205/// Document metadata (`docProps/core.xml`'s Dublin Core fields plus `docProps/app.xml`'s
3206/// `Company`/`Manager`/`HyperlinkBase`, and `docProps/custom.xml`'s user-defined properties) —
3207/// extended by. Before both `core.xml`/`app.xml` were always written but always empty/boilerplate
3208/// (see `writer.rs`'s `to_core_properties_xml`/ `to_app_properties_xml`). Only the handful of
3209/// fields a real document typically carries are modeled — `docProps/core.xml`'s
3210/// `lastModifiedBy`/`revision`/`created`/`modified`/`category`/`contentStatus` are still not.
3211#[derive(Debug, Default, Clone, PartialEq)]
3212pub struct DocumentProperties {
3213 /// `docProps/core.xml`'s `<dc:title>`.
3214 pub title: Option<String>,
3215 /// `docProps/core.xml`'s `<dc:creator>`.
3216 pub author: Option<String>,
3217 /// `docProps/core.xml`'s `<dc:subject>`.
3218 pub subject: Option<String>,
3219 /// `docProps/core.xml`'s `<cp:keywords>`.
3220 pub keywords: Option<String>,
3221 /// `docProps/app.xml`'s `<Company>`.
3222 pub company: Option<String>,
3223 /// `docProps/app.xml`'s `<Manager>`.
3224 pub manager: Option<String>,
3225 /// `docProps/app.xml`'s `<HyperlinkBase>` (the base URL relative hyperlinks in this workbook
3226 /// resolve against) — point 57.
3227 pub hyperlink_base: Option<String>,
3228 /// User-defined custom document properties (`docProps/custom.xml`,
3229 /// `CT_Properties`/`CT_Property`) — point 56, visible in Excel's File > Info > Properties >
3230 /// Advanced Properties > Custom tab. A `Vec` of `(name, value)` pairs, not a map: insertion
3231 /// order is preserved and determines the sequential `pid` each entry gets when written (`2`,
3232 /// `3`, `4`. — `0`/`1` are reserved by the format and never assigned), matching `word-ooxml`'s
3233 /// own `Document.custom_properties` convention exactly (including which 4
3234 /// [`CustomPropertyValue`] variants are modeled, for consistency across this workspace's
3235 /// crates). Only written if non-empty, same "optional part" posture as
3236 /// `table_styles`/`external_links` elsewhere in this crate.
3237 pub custom_properties: Vec<(String, CustomPropertyValue)>,
3238}
3239
3240/// A single custom document property's value (`docProps/custom.xml`, `CT_Property`'s value
3241/// `xsd:choice` — this crate models the same 4 of its many variant types as `word-ooxml`'s own
3242/// `CustomPropertyValue`: `vt:lpwstr` (`Text`), `vt:bool` (`Bool`), `vt:i4` (`Int`), `vt:r8`
3243/// (`Number`)). `CT_Property` also allows a date variant (`vt:filetime`) and several others
3244/// (vectors, blobs, currency..) not modeled here — same "simple case first, mirror the sibling
3245/// crate's own scope decision" posture as `word-ooxml`'s own doc comment for this exact type
3246/// explains.
3247#[derive(Debug, Clone, PartialEq)]
3248pub enum CustomPropertyValue {
3249 /// A text value (`vt:lpwstr`).
3250 Text(String),
3251 /// A boolean value (`vt:bool`).
3252 Bool(bool),
3253 /// A 32-bit signed integer value (`vt:i4`).
3254 Int(i32),
3255 /// A 64-bit floating-point value (`vt:r8`).
3256 Number(f64),
3257}
3258
3259impl DocumentProperties {
3260 /// Creates document properties with every field unset (equivalent to
3261 /// `DocumentProperties::default()`, only useful as a starting point for the `with_*` builders).
3262 pub fn new() -> Self {
3263 Self::default()
3264 }
3265
3266 /// Sets the title and returns the properties for chaining.
3267 pub fn with_title(mut self, title: impl Into<String>) -> Self {
3268 self.title = Some(title.into());
3269 self
3270 }
3271
3272 /// Sets the author and returns the properties for chaining.
3273 pub fn with_author(mut self, author: impl Into<String>) -> Self {
3274 self.author = Some(author.into());
3275 self
3276 }
3277
3278 /// Sets the subject and returns the properties for chaining.
3279 pub fn with_subject(mut self, subject: impl Into<String>) -> Self {
3280 self.subject = Some(subject.into());
3281 self
3282 }
3283
3284 /// Sets the keywords and returns the properties for chaining.
3285 pub fn with_keywords(mut self, keywords: impl Into<String>) -> Self {
3286 self.keywords = Some(keywords.into());
3287 self
3288 }
3289
3290 /// Sets the company and returns the properties for chaining.
3291 pub fn with_company(mut self, company: impl Into<String>) -> Self {
3292 self.company = Some(company.into());
3293 self
3294 }
3295
3296 /// Sets the manager and returns the properties for chaining.
3297 pub fn with_manager(mut self, manager: impl Into<String>) -> Self {
3298 self.manager = Some(manager.into());
3299 self
3300 }
3301
3302 /// Sets the hyperlink base and returns the properties for chaining — point 57.
3303 pub fn with_hyperlink_base(mut self, hyperlink_base: impl Into<String>) -> Self {
3304 self.hyperlink_base = Some(hyperlink_base.into());
3305 self
3306 }
3307
3308 /// Appends a custom document property and returns the properties for chaining. See
3309 /// `custom_properties`'s doc comment for why order matters (it determines each entry's `pid`).
3310 pub fn with_custom_property(
3311 mut self,
3312 name: impl Into<String>,
3313 value: CustomPropertyValue,
3314 ) -> Self {
3315 self.custom_properties.push((name.into(), value));
3316 self
3317 }
3318}
3319
3320/// A "what-if" scenario (`<scenario>`, `CT_Scenario`). Represents one named "what if these cells
3321/// held these values instead" snapshot, as managed through Excel's own Data > What-If Analysis >
3322/// Scenario Manager. This crate only stores the scenario's definition (name/inputs/comment); it
3323/// never evaluates the workbook under any scenario (same "storage, not evaluation" posture as
3324/// formulas).
3325#[derive(Debug, Clone, PartialEq)]
3326pub struct Scenario {
3327 /// The scenario's name, shown in Excel's Scenario Manager.
3328 pub name: String,
3329 /// The cells this scenario overrides, as `(cell reference, value text)` pairs
3330 /// (`<inputCells>`/`<inputCell r=".." val="..">`) — `val` is written verbatim as text, matching
3331 /// `CT_InputCells`'s own `xsd:string` typing (Excel re-parses it as a number itself).
3332 pub inputs: Vec<(String, String)>,
3333 /// An optional comment (`comment`).
3334 pub comment: Option<String>,
3335}
3336
3337impl Scenario {
3338 /// Creates a scenario with no inputs yet, no comment.
3339 pub fn new(name: impl Into<String>) -> Self {
3340 Self {
3341 name: name.into(),
3342 inputs: Vec::new(),
3343 comment: None,
3344 }
3345 }
3346
3347 /// Appends an input cell override and returns the scenario for chaining.
3348 pub fn with_input(mut self, cell: impl Into<String>, value: impl Into<String>) -> Self {
3349 self.inputs.push((cell.into(), value.into()));
3350 self
3351 }
3352
3353 /// Sets the scenario's comment and returns it for chaining.
3354 pub fn with_comment(mut self, comment: impl Into<String>) -> Self {
3355 self.comment = Some(comment.into());
3356 self
3357 }
3358}
3359
3360/// One column's filter criteria within a [`Sheet::auto_filter_range`]
3361/// (`<filterColumn>`/`<filters>`/`<filter>`, `CT_FilterColumn`/ `CT_Filters`/`CT_Filter`). Only the
3362/// simplest, most common filter form is modeled: a fixed list of checked/visible values —
3363/// `CT_Filters`'s `CT_CustomFilters` (e.g. "greater than"), `CT_Top10`, `CT_DynamicFilter` (e.g.
3364/// "this month"), and `CT_ColorFilter`/`CT_IconFilter` forms are not.
3365#[derive(Debug, Clone, PartialEq)]
3366pub struct AutoFilterColumn {
3367 /// This column's 0-based offset from [`Sheet::auto_filter_range`]'s first column (`colId`) —
3368 /// e.g. `0` for the filter range's own first column, regardless of that column's real sheet
3369 /// position.
3370 pub column_offset: u32,
3371 /// The values that remain visible when this filter is applied (`<filter val="..">` per entry),
3372 /// written verbatim as text.
3373 pub visible_values: Vec<String>,
3374}
3375
3376impl AutoFilterColumn {
3377 /// Creates a filter column with the given visible values.
3378 pub fn new(column_offset: u32, visible_values: Vec<String>) -> Self {
3379 Self {
3380 column_offset,
3381 visible_values,
3382 }
3383 }
3384}
3385
3386/// A reference to another workbook, used by this workbook's formulas (`<externalReference>` in
3387/// `xl/workbook.xml`'s `<externalReferences>`, plus a dedicated
3388/// `xl/externalLinks/externalLinkN.xml` part, `CT_ExternalReference`/`CT_ExternalLink`). Storage
3389/// only: this crate neither resolves the referenced workbook nor recomputes any formula against it
3390/// — a formula cell referencing this link (e.g. `"[1]Feuil1!A1"` or `"'[Budget.xlsx]Feuil1'!A1"`)
3391/// is written/read as plain formula text like any other, this type only accounts for the *link
3392/// itself* (the part Excel needs to know the reference is legitimate and to cache a fallback value
3393/// while the other file is closed).
3394#[derive(Debug, Clone, PartialEq)]
3395pub struct ExternalWorkbookLink {
3396 /// The other workbook's path/URL, written as an external (`TargetMode="External"`) relationship
3397 /// target on this link's own `xl/externalLinks/_rels/externalLinkN.xml.rels` — same shape as
3398 /// [`HyperlinkTarget::External`]. Can be an absolute path (`"file:///C:/Budget.xlsx"`), a
3399 /// relative one (`"Budget.xlsx"`), or a URL.
3400 pub target: String,
3401 /// The other workbook's own sheet names, in order (`<sheetNames>`/`<sheetName val="..">`) —
3402 /// Excel uses these to resolve `[1]SheetName!..` formula references while the other file is
3403 /// closed.
3404 pub sheet_names: Vec<String>,
3405 /// Named ranges/constants defined in the other workbook that this one's formulas reference, as
3406 /// `(name, refersTo text)` pairs (`<definedNames>`/`<definedName name=".." refersTo="..">`).
3407 pub defined_names: Vec<(String, String)>,
3408}
3409
3410impl ExternalWorkbookLink {
3411 /// Creates an external workbook link with no sheet names/defined names yet.
3412 pub fn new(target: impl Into<String>) -> Self {
3413 Self {
3414 target: target.into(),
3415 sheet_names: Vec::new(),
3416 defined_names: Vec::new(),
3417 }
3418 }
3419
3420 /// Appends a sheet name and returns the link for chaining.
3421 pub fn with_sheet_name(mut self, name: impl Into<String>) -> Self {
3422 self.sheet_names.push(name.into());
3423 self
3424 }
3425
3426 /// Appends a defined name reference and returns the link for chaining.
3427 pub fn with_defined_name(
3428 mut self,
3429 name: impl Into<String>,
3430 refers_to: impl Into<String>,
3431 ) -> Self {
3432 self.defined_names.push((name.into(), refers_to.into()));
3433 self
3434 }
3435}
3436
3437/// A suppressed error-checking indicator over one range (`<ignoredError>`, `CT_IgnoredError`).
3438/// Confirmed against `sml.xsd` (`CT_IgnoredErrors`/`CT_IgnoredError`, ECMA-376 SpreadsheetML):
3439/// `sqref` is the only required attribute, every error-category flag is an optional boolean
3440/// defaulting to `false`. This crate models every flag `CT_IgnoredError` defines; real Excel's own
3441/// "Ignore Error" right-click menu only ever sets one flag at a time per range, but nothing in the
3442/// schema forbids several flags on the same entry, so this type allows it too (caller's choice,
3443/// same "don't second-guess the caller" posture as `ConditionalFormattingRule`).
3444#[derive(Debug, Clone, PartialEq)]
3445pub struct IgnoredError {
3446 /// The range this entry covers (`sqref`, e.g. `"B2:B10"` or a single cell `"C4"`) — like
3447 /// `ConditionalFormattingRule::range`, several disjoint cells can be packed into one `sqref`
3448 /// (space-separated) if the caller wants a single entry to cover them; this crate writes the
3449 /// string verbatim, no validation.
3450 pub range: String,
3451 /// Suppresses the "number stored as text" warning — by far the most common real-world use of
3452 /// `<ignoredError>` (e.g. a column of ZIP codes or reference numbers intentionally kept as
3453 /// text).
3454 pub number_stored_as_text: bool,
3455 /// Suppresses the "formula omits adjacent cells" warning.
3456 pub formula_range: bool,
3457 /// Suppresses the "inconsistent formula" warning (a formula that differs from the pattern of
3458 /// its neighboring cells).
3459 pub formula: bool,
3460 /// Suppresses the "unlocked cell containing a formula" warning.
3461 pub unlocked_formula: bool,
3462 /// Suppresses the "formula refers to an empty cell" warning.
3463 pub empty_cell_reference: bool,
3464 /// Suppresses the "value fails a list data validation rule" warning.
3465 pub list_data_validation: bool,
3466 /// Suppresses the "table's calculated column formula is inconsistent" warning.
3467 pub calculated_column: bool,
3468 /// Suppresses the "text year is ambiguous" warning (a two-digit year entered in a
3469 /// text-formatted cell).
3470 pub two_digit_text_year: bool,
3471 /// Suppresses the "formula results in an error" warning (e.g. a deliberate `#N/A` or
3472 /// `#DIV/0!`).
3473 pub eval_error: bool,
3474}
3475
3476impl IgnoredError {
3477 /// Creates an ignored-error entry over `range` with every flag unset — use the `with_*`
3478 /// builders to enable the specific warning(s) to suppress.
3479 pub fn new(range: impl Into<String>) -> Self {
3480 Self {
3481 range: range.into(),
3482 number_stored_as_text: false,
3483 formula_range: false,
3484 formula: false,
3485 unlocked_formula: false,
3486 empty_cell_reference: false,
3487 list_data_validation: false,
3488 calculated_column: false,
3489 two_digit_text_year: false,
3490 eval_error: false,
3491 }
3492 }
3493
3494 /// Suppresses the "number stored as text" warning and returns the entry for chaining — the
3495 /// common case (see this type's own doc comment).
3496 pub fn with_number_stored_as_text(mut self, value: bool) -> Self {
3497 self.number_stored_as_text = value;
3498 self
3499 }
3500
3501 /// Suppresses the "formula omits adjacent cells" warning and returns the entry for chaining.
3502 pub fn with_formula_range(mut self, value: bool) -> Self {
3503 self.formula_range = value;
3504 self
3505 }
3506
3507 /// Suppresses the "inconsistent formula" warning and returns the entry for chaining.
3508 pub fn with_formula(mut self, value: bool) -> Self {
3509 self.formula = value;
3510 self
3511 }
3512
3513 /// Suppresses the "unlocked cell containing a formula" warning and returns the entry for
3514 /// chaining.
3515 pub fn with_unlocked_formula(mut self, value: bool) -> Self {
3516 self.unlocked_formula = value;
3517 self
3518 }
3519
3520 /// Suppresses the "formula refers to an empty cell" warning and returns the entry for chaining.
3521 pub fn with_empty_cell_reference(mut self, value: bool) -> Self {
3522 self.empty_cell_reference = value;
3523 self
3524 }
3525
3526 /// Suppresses the "value fails a list data validation rule" warning and returns the entry for
3527 /// chaining.
3528 pub fn with_list_data_validation(mut self, value: bool) -> Self {
3529 self.list_data_validation = value;
3530 self
3531 }
3532
3533 /// Suppresses the "table's calculated column formula is inconsistent" warning and returns the
3534 /// entry for chaining.
3535 pub fn with_calculated_column(mut self, value: bool) -> Self {
3536 self.calculated_column = value;
3537 self
3538 }
3539
3540 /// Suppresses the "text year is ambiguous" warning and returns the entry for chaining.
3541 pub fn with_two_digit_text_year(mut self, value: bool) -> Self {
3542 self.two_digit_text_year = value;
3543 self
3544 }
3545
3546 /// Suppresses the "formula results in an error" warning and returns the entry for chaining.
3547 pub fn with_eval_error(mut self, value: bool) -> Self {
3548 self.eval_error = value;
3549 self
3550 }
3551}
3552
3553// ============================================================================,: sheet drawing
3554// (embedded pictures/charts).
3555// ============================================================================
3556
3557/// One corner of a cell-anchored drawing range (`<xdr:from>`/`<xdr:to>`, `CT_Marker`) — a cell
3558/// reference (0-based column/row, matching this crate's own 0-based [`Row`]/column conventions
3559/// elsewhere) plus an EMU offset *within* that cell, letting a picture/chart start or end partway
3560/// through a cell rather than exactly on a gridline.
3561#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
3562pub struct CellAnchorPoint {
3563 pub column: u32,
3564 pub column_offset_emu: i64,
3565 pub row: u32,
3566 pub row_offset_emu: i64,
3567}
3568
3569impl CellAnchorPoint {
3570 /// A corner exactly on the gridline at `(column, row)`, no offset.
3571 pub fn new(column: u32, row: u32) -> Self {
3572 Self {
3573 column,
3574 row,
3575 column_offset_emu: 0,
3576 row_offset_emu: 0,
3577 }
3578 }
3579
3580 pub fn with_column_offset_emu(mut self, offset: i64) -> Self {
3581 self.column_offset_emu = offset;
3582 self
3583 }
3584
3585 pub fn with_row_offset_emu(mut self, offset: i64) -> Self {
3586 self.row_offset_emu = offset;
3587 self
3588 }
3589}
3590
3591/// A picture's raw image format — mirrors `word_ooxml::ImageFormat` exactly (same four formats,
3592/// same extension/content-type mapping), kept as its own type rather than shared across crates
3593/// since neither host crate depends on the other.
3594#[derive(Debug, Clone, Copy, PartialEq, Eq)]
3595pub enum PictureFormat {
3596 Png,
3597 Jpeg,
3598 Gif,
3599 Bmp,
3600}
3601
3602impl PictureFormat {
3603 pub(crate) fn extension(self) -> &'static str {
3604 match self {
3605 PictureFormat::Png => "png",
3606 PictureFormat::Jpeg => "jpeg",
3607 PictureFormat::Gif => "gif",
3608 PictureFormat::Bmp => "bmp",
3609 }
3610 }
3611
3612 pub(crate) fn content_type(self) -> &'static str {
3613 match self {
3614 PictureFormat::Png => "image/png",
3615 PictureFormat::Jpeg => "image/jpeg",
3616 PictureFormat::Gif => "image/gif",
3617 PictureFormat::Bmp => "image/bmp",
3618 }
3619 }
3620
3621 pub(crate) fn from_extension(extension: &str) -> Option<Self> {
3622 Some(match extension.to_ascii_lowercase().as_str() {
3623 "png" => PictureFormat::Png,
3624 "jpeg" | "jpg" => PictureFormat::Jpeg,
3625 "gif" => PictureFormat::Gif,
3626 "bmp" => PictureFormat::Bmp,
3627 _ => return None,
3628 })
3629 }
3630}
3631
3632/// A picture embedded in a sheet's drawing (`<xdr:pic>`, `CT_Picture` — the spreadsheet-drawing
3633/// flavor, distinct from `a:CT_Picture`), anchored by its enclosing [`DrawingAnchor`].
3634///
3635/// Reuses `drawing::ShapeProperties`/`drawing::BlipFill` for the picture's own formatting
3636/// (fill/line/rotation via `spPr`), the same DrawingML content model `word-ooxml`'s own picture
3637/// support is being generalized toward — this is genuinely shared content, not a parallel
3638/// reimplementation. Raw image bytes are supplied directly (`data`/`format`), same "no separate
3639/// loader type" posture as `word_ooxml::Image`.
3640///
3641/// Not modeled: cropping (`a:srcRect`), picture-specific effects beyond what `ShapeProperties`
3642/// itself carries.
3643#[derive(Debug, Clone, PartialEq)]
3644pub struct SheetPicture {
3645 pub data: Vec<u8>,
3646 pub format: PictureFormat,
3647 /// Alt text / non-visual name (`<xdr:cNvPr name="..">`).
3648 pub name: String,
3649 pub shape_properties: Option<drawing::ShapeProperties>,
3650}
3651
3652impl SheetPicture {
3653 pub fn new(data: impl Into<Vec<u8>>, format: PictureFormat) -> Self {
3654 Self {
3655 data: data.into(),
3656 format,
3657 name: String::new(),
3658 shape_properties: None,
3659 }
3660 }
3661
3662 pub fn with_name(mut self, name: impl Into<String>) -> Self {
3663 self.name = name.into();
3664 self
3665 }
3666
3667 pub fn with_shape_properties(mut self, properties: drawing::ShapeProperties) -> Self {
3668 self.shape_properties = Some(properties);
3669 self
3670 }
3671}
3672
3673/// A chart embedded in a sheet's drawing (`<xdr:graphicFrame>` referencing a dedicated
3674/// `xl/charts/chartN.xml` part via `<c:chart r:id="..">`), anchored by its enclosing
3675/// [`DrawingAnchor`]. Wraps the whole `chart::ChartSpace` this crate's `chart` dependency already
3676/// knows how to read/write in full — no Excel-specific chart modeling needed here, and no embedded
3677/// workbook needed either (an Excel chart reads its series directly from this same workbook's
3678/// cells, via each series' own cell-reference formulas already carried by
3679/// `chart::NumericDataSource`/`StringDataSource`).
3680#[derive(Debug, Clone, PartialEq)]
3681pub struct SheetChart {
3682 pub chart_space: chart::ChartSpace,
3683 /// Non-visual name (`<xdr:cNvPr name="..">`).
3684 pub name: String,
3685}
3686
3687impl SheetChart {
3688 pub fn new(chart_space: chart::ChartSpace) -> Self {
3689 Self {
3690 chart_space,
3691 name: String::new(),
3692 }
3693 }
3694
3695 pub fn with_name(mut self, name: impl Into<String>) -> Self {
3696 self.name = name.into();
3697 self
3698 }
3699}
3700
3701/// The object anchored by a [`DrawingAnchor`] — a picture or a chart.
3702#[derive(Debug, Clone, PartialEq)]
3703pub enum DrawingObject {
3704 /// Boxed: an embedded picture's own encoded bytes make it, in turn, the largest variant once
3705 /// `Chart` below was boxed (clippy's `large_enum_variant` moves on to whichever variant is now
3706 /// biggest).
3707 Picture(Box<SheetPicture>),
3708 /// Boxed for the same reason as `word-ooxml`'s `RunContent::Chart` and `powerpoint-ooxml`'s
3709 /// `Shape::Chart` — a chart's own nested `ChartSpace` otherwise forces every `DrawingObject`,
3710 /// picture or chart alike, to pay for the largest variant's size.
3711 Chart(Box<SheetChart>),
3712}
3713
3714/// One cell-anchored drawing object (`<xdr:twoCellAnchor>`, `CT_TwoCellAnchor`).
3715///
3716/// Only the two-cell anchor is modeled (`from`/`to` cell markers, `editAs` always the schema
3717/// default `"twoCell"`) — by far the common case real spreadsheet tools generate
3718/// (`CT_OneCellAnchor`/ `CT_AbsoluteAnchor` exist in the schema but are rarely if ever generated,
3719/// only tolerated on read). A wholly absent anchor kind on read is simply skipped by this crate's
3720/// forgiving reader, consistent with the rest of this workspace's posture.
3721#[derive(Debug, Clone, PartialEq)]
3722pub struct DrawingAnchor {
3723 pub from: CellAnchorPoint,
3724 pub to: CellAnchorPoint,
3725 pub object: DrawingObject,
3726}
3727
3728impl DrawingAnchor {
3729 pub fn new(from: CellAnchorPoint, to: CellAnchorPoint, object: DrawingObject) -> Self {
3730 Self { from, to, object }
3731 }
3732}
3733
3734/// A sheet's drawing (`xl/drawings/drawingN.xml`, `CT_Drawing`/root `<xdr:wsDr>`) — an unbounded
3735/// list of anchored pictures/charts, matching the schema's own `EG_Anchor` choice group repeated
3736/// `maxOccurs="unbounded"`.
3737#[derive(Debug, Clone, PartialEq, Default)]
3738pub struct SheetDrawing {
3739 pub anchors: Vec<DrawingAnchor>,
3740}
3741
3742impl SheetDrawing {
3743 pub fn new() -> Self {
3744 Self::default()
3745 }
3746
3747 pub fn with_anchor(mut self, anchor: DrawingAnchor) -> Self {
3748 self.anchors.push(anchor);
3749 self
3750 }
3751}