Skip to main content

xl3_core/
manifest.rs

1//! Style manifest model exchanged with the JS shell.
2//!
3//! Phase 2 Task 2.2 (PLAN.md §5): exceljs in xl3 (TS) already
4//! parses every OOXML cell style; rather than ask `xl3-core` to
5//! re-parse styles.xml in depth, the TS side ships a normalised
6//! JSON shape and we apply it on the output.
7//!
8//! Boundary discipline (CLAUDE.md): this module is plain Rust with
9//! no `wasm_bindgen` / `JsValue` dependency. The JSON decoder lives
10//! in `xl3-wasm`, which constructs a `StyleManifest` and hands it to
11//! the renderer.
12
13use std::collections::HashMap;
14
15/// One-shot bundle of all the styling information `xl3-core` needs
16/// to reproduce an exceljs-parsed workbook's look.
17#[derive(Debug, Default, Clone)]
18pub struct StyleManifest {
19    /// Deduplicated style table. Cells reference entries by index;
20    /// matches OOXML `cellXfs` semantics.
21    pub styles: Vec<StyleSpec>,
22    /// Per-sheet, per-template-cell style index. Keys are sheet
23    /// names; inner keys are zero-based `(row, col)` matching the
24    /// planner's position math.
25    pub cells: HashMap<String, HashMap<(u32, u32), usize>>,
26    /// Merge ranges per sheet, A1 form (`"A1:B2"`). Applied to the
27    /// rendered worksheet before saving.
28    pub merges: HashMap<String, Vec<String>>,
29    /// Per-sheet column widths in OOXML cw units. Only the
30    /// non-default columns are listed; the rest inherit
31    /// rust_xlsxwriter's defaults.
32    pub columns: HashMap<String, Vec<ColumnWidth>>,
33}
34
35#[derive(Debug, Clone, Copy)]
36pub struct ColumnWidth {
37    pub col: u32,
38    pub width: f64,
39}
40
41#[derive(Debug, Default, Clone)]
42pub struct StyleSpec {
43    pub font: Option<FontSpec>,
44    pub num_fmt: Option<String>,
45    pub alignment: Option<AlignmentSpec>,
46    pub fill: Option<FillSpec>,
47}
48
49#[derive(Debug, Default, Clone)]
50pub struct FontSpec {
51    pub name: Option<String>,
52    pub size: Option<f64>,
53    pub bold: bool,
54    pub italic: bool,
55    pub underline: bool,
56    /// ARGB hex (`"FF000000"`), `None` for the theme default.
57    pub color: Option<String>,
58}
59
60#[derive(Debug, Default, Clone, Copy)]
61pub struct AlignmentSpec {
62    pub horizontal: Option<HorizontalAlign>,
63    pub vertical: Option<VerticalAlign>,
64    pub wrap_text: bool,
65    pub indent: u8,
66}
67
68#[derive(Debug, Clone, Copy, PartialEq, Eq)]
69pub enum HorizontalAlign {
70    Left,
71    Center,
72    Right,
73    Justify,
74}
75
76#[derive(Debug, Clone, Copy, PartialEq, Eq)]
77pub enum VerticalAlign {
78    Top,
79    Middle,
80    Bottom,
81}
82
83#[derive(Debug, Clone)]
84pub struct FillSpec {
85    /// Pattern type. Phase 2 supports `Solid` only — other patterns
86    /// (gray125, etc.) are deferred along with conditional formatting.
87    pub pattern: FillPattern,
88    /// ARGB hex (`"FFFFFF00"` for yellow).
89    pub color: String,
90}
91
92#[derive(Debug, Clone, Copy, PartialEq, Eq)]
93pub enum FillPattern {
94    Solid,
95}
96
97impl StyleManifest {
98    /// Look up a template cell's style index. `None` when the
99    /// (sheet, row, col) triple wasn't in the manifest — the
100    /// caller falls back to defaults.
101    pub fn cell_style(&self, sheet: &str, row: u32, col: u32) -> Option<&StyleSpec> {
102        let idx = *self.cells.get(sheet)?.get(&(row, col))?;
103        self.styles.get(idx)
104    }
105
106    pub fn sheet_merges(&self, sheet: &str) -> &[String] {
107        self.merges
108            .get(sheet)
109            .map(|v| v.as_slice())
110            .unwrap_or(&[])
111    }
112
113    pub fn sheet_columns(&self, sheet: &str) -> &[ColumnWidth] {
114        self.columns
115            .get(sheet)
116            .map(|v| v.as_slice())
117            .unwrap_or(&[])
118    }
119}