@openjsxl/core 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -27,8 +27,10 @@ const wb = await openXlsx(await readFile('data.xlsx'))
27
27
  const sheet = wb.sheet('Sheet1')
28
28
 
29
29
  sheet.cell('A1') // { ref, type, value } — narrow on `type` for a typed value
30
+ sheet.style('B2') // { font?, fill?, border?, alignment?, numberFormat? } | undefined
30
31
  sheet.numberFormat('C1') // "mm-dd-yy" | undefined
31
32
  sheet.mergedCells // ["A1:B1", …]
33
+ sheet.freeze // { rows?, cols? } | undefined — plus columns, rowProperties, state, …
32
34
 
33
35
  // Constant-memory streaming for large sheets — one row at a time.
34
36
  for await (const row of streamSheetRows(await readFile('huge.xlsx'))) {
@@ -56,16 +58,21 @@ input.sheets[0].rows.push(['Pears', new Date('2024-02-01')])
56
58
  const updated = await writeXlsx(input)
57
59
  ```
58
60
 
59
- Output is deterministic; unrepresentable input (no sheets, bad/duplicate sheet name, non-finite
60
- number, invalid `Date`, XML-illegal characters) throws `XlsxError` with `code: 'invalid-input'`.
61
- The round trip is lossless for values, types, and sheet names/order.
61
+ Cells can carry styles (`{ value, style }` — the same shape `style(ref)` returns), and sheets
62
+ take `columns` (widths), `rowProperties` (heights), `freeze`, `merges`, `hyperlinks`, and a
63
+ visibility `state`. Output is deterministic; unrepresentable input (no sheets, bad/duplicate
64
+ sheet name, non-finite number, invalid `Date`, XML-illegal characters, malformed or overlapping
65
+ merges) throws `XlsxError` with `code: 'invalid-input'`. The round trip is lossless for values,
66
+ types, sheet names/order, styles, geometry, merges, hyperlinks, and visibility.
62
67
 
63
68
  ## Exports
64
69
 
65
70
  - **Reader:** `openXlsx`, `streamSheetRows`, `Workbook`, `Worksheet`, `ReadOptions`
66
- - **Writer:** `writeXlsx`, `workbookToInput`, `WorkbookInput`, `SheetInput`, `CellValue`, `WriteOptions`
71
+ - **Writer:** `writeXlsx`, `workbookToInput`, `WorkbookInput`, `SheetInput`, `CellInput`,
72
+ `StyledCell`, `CellValue`, `WriteOptions`
67
73
  - **Errors:** `XlsxError`, `XlsxErrorCode`
68
- - **Types:** `Row`, `Cell`, `CellType`, `Comment`, `Hyperlink`, `SheetInfo`, `CellRef`
74
+ - **Types:** `Row`, `Cell`, `CellType`, `CellStyle` (+ font/fill/border/alignment/color parts),
75
+ `ColumnProps`, `RowProps`, `FreezePane`, `Comment`, `Hyperlink`, `SheetInfo`, `SheetState`, `CellRef`
69
76
  - **A1 & dates:** `columnToIndex`, `indexToColumn`, `parseRef`, `formatRef`, `serialToDate`
70
77
 
71
78
  Full guide, design notes, and roadmap: <https://github.com/joaquimserafim/openjsxl>
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- type XlsxErrorCode = 'not-a-zip' | 'not-xlsx' | 'missing-part' | 'corrupt-zip' | 'unsupported' | 'no-such-sheet' | 'part-too-large' | 'invalid-input';
1
+ type XlsxErrorCode = "not-a-zip" | "not-xlsx" | "missing-part" | "corrupt-zip" | "unsupported" | "no-such-sheet" | "part-too-large" | "invalid-input";
2
2
  declare class XlsxError extends Error {
3
3
  /** Machine-readable discriminant; branch on this rather than the message. */
4
4
  readonly code: XlsxErrorCode;
@@ -21,37 +21,45 @@ declare function formatRef(ref: CellRef): string;
21
21
  declare function serialToDate(serial: number, date1904?: boolean): Date;
22
22
  declare function dateToSerial(date: Date, date1904?: boolean): number;
23
23
 
24
- type CellType = 'empty' | 'string' | 'number' | 'boolean' | 'date' | 'error';
24
+ type CellType = "empty" | "string" | "number" | "boolean" | "date" | "error";
25
25
  interface CellBase {
26
26
  /** A1 reference, e.g. "B2". */
27
27
  readonly ref: string;
28
28
  }
29
29
  type Cell = (CellBase & {
30
- readonly type: 'empty';
30
+ readonly type: "empty";
31
31
  readonly value: null;
32
32
  }) | (CellBase & {
33
- readonly type: 'string';
33
+ readonly type: "string";
34
34
  readonly value: string;
35
35
  }) | (CellBase & {
36
- readonly type: 'number';
36
+ readonly type: "number";
37
37
  readonly value: number;
38
38
  }) | (CellBase & {
39
- readonly type: 'boolean';
39
+ readonly type: "boolean";
40
40
  readonly value: boolean;
41
41
  }) | (CellBase & {
42
- readonly type: 'date';
42
+ readonly type: "date";
43
43
  readonly value: Date;
44
44
  }) | (CellBase & {
45
- readonly type: 'error';
45
+ readonly type: "error";
46
46
  readonly value: string;
47
47
  });
48
+ /**
49
+ * A sheet tab's visibility (the `state` attribute on `<sheet>`). `hidden` sheets can be re-shown
50
+ * from Excel's UI; `veryHidden` ones only through VBA or by editing the file. An absent or
51
+ * unrecognized state reads as `visible` (the spec's default).
52
+ */
53
+ type SheetState = "visible" | "hidden" | "veryHidden";
48
54
  interface SheetInfo {
49
55
  /** Sheet name as shown on Excel's tab. */
50
56
  readonly name: string;
51
57
  /** Workbook-relative part path, resolved via the relationship graph. */
52
58
  readonly path: string;
53
- /** false for hidden or very-hidden sheets. */
59
+ /** false for hidden or very-hidden sheets. Kept alongside {@link state} (which it derives from). */
54
60
  readonly visible: boolean;
61
+ /** The tab's visibility state (F4.6). `visible` is `state === "visible"`. */
62
+ readonly state: SheetState;
55
63
  }
56
64
  interface Comment {
57
65
  /** The cell the comment is anchored to, e.g. "B2". */
@@ -76,6 +84,114 @@ interface Hyperlink {
76
84
  /** Display-text override for the link, if any. */
77
85
  readonly display?: string;
78
86
  }
87
+ /** Width/visibility for a 1-based column range (`min`–`max` inclusive), from `<cols>`. */
88
+ interface ColumnProps {
89
+ readonly min: number;
90
+ readonly max: number;
91
+ /** Column width in characters of the default font (Excel's unit), 0 < width ≤ 255. */
92
+ readonly width?: number;
93
+ readonly hidden?: boolean;
94
+ }
95
+ /** Height/visibility of one row, from `<row ht hidden>`. */
96
+ interface RowProps {
97
+ /** Row height in points, 0 < height ≤ 409.5 (Excel's ceiling). */
98
+ readonly height?: number;
99
+ readonly hidden?: boolean;
100
+ }
101
+ /**
102
+ * A frozen pane: the top `rows` rows and/or leftmost `cols` columns stay visible while the rest
103
+ * scrolls. Split (non-frozen) panes are not modelled and read as no freeze.
104
+ */
105
+ interface FreezePane {
106
+ readonly rows?: number;
107
+ readonly cols?: number;
108
+ }
109
+ /**
110
+ * A color as OOXML stores it — kept RAW, never resolved. `rgb` is ARGB hex (e.g. `"FFFF0000"`);
111
+ * `theme` indexes the workbook theme's color scheme with an optional `tint` (−1…1); `indexed` is
112
+ * a legacy palette index; `auto` lets the consumer pick (usually black). Theme colors are NOT
113
+ * resolved to rgb on read: resolution needs a theme1.xml parser and is lossy on rewrite (a
114
+ * theme-aware consumer could no longer re-tint) — the raw form is what round-trips faithfully,
115
+ * and it is exactly what openpyxl stores too.
116
+ */
117
+ type Color = {
118
+ readonly rgb: string;
119
+ } | {
120
+ readonly theme: number;
121
+ readonly tint?: number;
122
+ } | {
123
+ readonly indexed: number;
124
+ } | {
125
+ readonly auto: true;
126
+ };
127
+ /**
128
+ * Underline style. The exotic accounting variants (`singleAccounting`/`doubleAccounting`)
129
+ * degrade to no underline on read and are rejected on write (deferred, documented).
130
+ */
131
+ type UnderlineStyle = "single" | "double";
132
+ interface FontStyle {
133
+ readonly name?: string;
134
+ /** Font size in points. */
135
+ readonly size?: number;
136
+ readonly bold?: boolean;
137
+ readonly italic?: boolean;
138
+ readonly underline?: UnderlineStyle;
139
+ readonly strike?: boolean;
140
+ readonly color?: Color;
141
+ }
142
+ /** Fill pattern kinds (ECMA-376 §18.18.55). `gray125` is the workbook-reserved fill 1. */
143
+ type PatternType = "none" | "solid" | "mediumGray" | "darkGray" | "lightGray" | "darkHorizontal" | "darkVertical" | "darkDown" | "darkUp" | "darkGrid" | "darkTrellis" | "lightHorizontal" | "lightVertical" | "lightDown" | "lightUp" | "lightGrid" | "lightTrellis" | "gray125" | "gray0625";
144
+ /**
145
+ * A pattern fill. For the everyday solid fill, the visible color is `fgColor` (OOXML's rule —
146
+ * `bgColor` shows only through pattern gaps). Gradient fills are not modelled (deferred): a
147
+ * gradient-filled cell reads as having no fill.
148
+ */
149
+ interface FillStyle {
150
+ readonly patternType: PatternType;
151
+ readonly fgColor?: Color;
152
+ readonly bgColor?: Color;
153
+ }
154
+ /** Border line styles (ECMA-376 §18.18.3). An edge with no style is simply absent. */
155
+ type BorderLineStyle = "thin" | "medium" | "thick" | "dashed" | "dotted" | "double" | "hair" | "mediumDashed" | "dashDot" | "mediumDashDot" | "dashDotDot" | "mediumDashDotDot" | "slantDashDot";
156
+ interface BorderEdge {
157
+ readonly style: BorderLineStyle;
158
+ readonly color?: Color;
159
+ }
160
+ /** Per-edge borders. Diagonal borders are not modelled (deferred). */
161
+ interface BorderStyle {
162
+ readonly top?: BorderEdge;
163
+ readonly right?: BorderEdge;
164
+ readonly bottom?: BorderEdge;
165
+ readonly left?: BorderEdge;
166
+ }
167
+ type HorizontalAlignment = "left" | "center" | "right" | "justify" | "fill" | "centerContinuous" | "distributed";
168
+ type VerticalAlignment = "top" | "center" | "bottom" | "justify" | "distributed";
169
+ interface Alignment {
170
+ readonly horizontal?: HorizontalAlignment;
171
+ readonly vertical?: VerticalAlignment;
172
+ readonly wrapText?: boolean;
173
+ readonly shrinkToFit?: boolean;
174
+ /** Indent level (whole units of about 3 spaces), 0–250. */
175
+ readonly indent?: number;
176
+ /**
177
+ * Text rotation in degrees, 0–180 (91–180 mean 1–90° downward, per the spec). The legacy
178
+ * marker 255 ("vertical stacked") is not modelled and degrades to no rotation.
179
+ */
180
+ readonly textRotation?: number;
181
+ }
182
+ /**
183
+ * The resolved style of one cell. Every component is optional; a cell whose effective format is
184
+ * the workbook default resolves to no style at all (`Worksheet.style(ref)` returns `undefined`).
185
+ * `numberFormat` is always the format CODE string (e.g. `"yyyy-mm-dd"`, `"0.00%"`) — ids are a
186
+ * file-internal detail and never appear in the API.
187
+ */
188
+ interface CellStyle {
189
+ readonly numberFormat?: string;
190
+ readonly font?: FontStyle;
191
+ readonly fill?: FillStyle;
192
+ readonly border?: BorderStyle;
193
+ readonly alignment?: Alignment;
194
+ }
79
195
 
80
196
  interface StyleTable {
81
197
  /** True when the cell format at this `s` index applies a date/time number format. */
@@ -86,6 +202,12 @@ interface StyleTable {
86
202
  * built-in with no portable code, or the index is out of range.
87
203
  */
88
204
  formatCode(styleIndex: number | undefined): string | undefined;
205
+ /**
206
+ * The full resolved style at this `s` index — number format code, font, fill, border, and
207
+ * alignment. `undefined` when the index is out of range or the xf resolves to the workbook
208
+ * default (no distinguishing component). Cached: repeated calls return the same object.
209
+ */
210
+ cellStyle(styleIndex: number | undefined): CellStyle | undefined;
89
211
  }
90
212
 
91
213
  interface DecodeContext {
@@ -102,7 +224,7 @@ interface Relationship {
102
224
  readonly type: string;
103
225
  /** Target exactly as written; resolve internal ones with resolveTarget. */
104
226
  readonly target: string;
105
- readonly targetMode: 'Internal' | 'External';
227
+ readonly targetMode: "Internal" | "External";
106
228
  }
107
229
 
108
230
  interface Row {
@@ -121,6 +243,8 @@ declare class Worksheet {
121
243
  get path(): string;
122
244
  /** false for hidden or very-hidden sheets. */
123
245
  get visible(): boolean;
246
+ /** The tab's visibility state (F4.6): `"visible"`, `"hidden"`, or `"veryHidden"`. */
247
+ get state(): SheetState;
124
248
  /**
125
249
  * Merged-cell ranges in A1 notation (e.g. `['A1:B1', 'A2:A4']`), in document order. Only the
126
250
  * top-left cell of a merge holds a value; the rest read as `empty`. Empty when none.
@@ -139,6 +263,15 @@ declare class Worksheet {
139
263
  * 0, usually `"General"`), mirroring how date detection defaults.
140
264
  */
141
265
  numberFormat(ref: string): string | undefined;
266
+ /**
267
+ * The resolved style of the cell at `ref` — number format code, font, fill, border, and
268
+ * alignment (F4.1). Resolution shares the same effective-style map as {@link numberFormat}
269
+ * (cell `s` → row `customFormat` default → column default), so the two always agree.
270
+ * `undefined` for an unstyled cell, an absent cell, or a workbook with no style table —
271
+ * "no style" and "the default style" are deliberately the same answer. Objects are cached
272
+ * per distinct format record: two cells sharing a format return the same object.
273
+ */
274
+ style(ref: string): CellStyle | undefined;
142
275
  /**
143
276
  * The sheet's declared used range in A1 notation (e.g. `"A1:E10"`, or a single cell), from
144
277
  * the worksheet's `<dimension>`. `undefined` when the producer omits it — it is an optional
@@ -150,6 +283,22 @@ declare class Worksheet {
150
283
  * resolved `author`, and plain `text`. Empty when the sheet has no comments part.
151
284
  */
152
285
  get comments(): readonly Comment[];
286
+ /**
287
+ * Column width/visibility declarations (`<cols>`), in document order (F4.5). Entries carrying
288
+ * only a column-default STYLE are not geometry and are omitted — `style(ref)` already resolves
289
+ * them. Empty when the sheet declares none.
290
+ */
291
+ get columns(): readonly ColumnProps[];
292
+ /**
293
+ * Per-row height/visibility, keyed by 1-based row index (F4.5). Only rows that declare a
294
+ * height or hidden flag appear. Empty map when none do.
295
+ */
296
+ get rowProperties(): ReadonlyMap<number, RowProps>;
297
+ /**
298
+ * The sheet's frozen pane, or `undefined` when nothing is frozen (F4.5). Split (non-frozen)
299
+ * panes are not modelled and read as `undefined`.
300
+ */
301
+ get freeze(): FreezePane | undefined;
153
302
  /** The cell at an A1 reference. Absent cells read as `empty` (Excel treats them blank). */
154
303
  cell(ref: string): Cell;
155
304
  /** Stream the populated rows in document order. Sparse: empty rows/cells are absent. */
@@ -185,11 +334,62 @@ declare function streamSheetRows(source: Uint8Array | ArrayBuffer, sheetName?: s
185
334
  * `null` / `undefined` (including array holes) → an empty cell, omitted from the output.
186
335
  */
187
336
  type CellValue = string | number | boolean | Date | null | undefined;
337
+ /**
338
+ * A cell with a {@link CellStyle} attached (F4.2). `value` is required but nullable — a styled
339
+ * BLANK cell (`{ value: null, style }`) is real and emits `<c r s/>`, which is how a border or
340
+ * fill lands on a cell with nothing in it. The style type is exactly what `Worksheet.style(ref)`
341
+ * returns, so read → modify → write carries styles as a pass-through.
342
+ */
343
+ interface StyledCell {
344
+ readonly value: CellValue;
345
+ readonly style?: CellStyle;
346
+ }
347
+ /**
348
+ * One cell in a row: a bare value, or a value with a style. Discrimination is total — `null` /
349
+ * `undefined` mean empty, `Date` instances are dates, and any OTHER object must be a
350
+ * {@link StyledCell} (an object without a `value` property throws `invalid-input`, catching stray
351
+ * objects loudly). Bare-value rows are untouched: pre-F4.2 input keeps its exact meaning AND its
352
+ * exact output bytes.
353
+ */
354
+ type CellInput = CellValue | StyledCell;
188
355
  interface SheetInput {
189
356
  /** Tab name. 1–31 chars, unique (case-insensitively), free of `\ / ? * [ ] :`. */
190
357
  readonly name: string;
191
- /** Rows top-to-bottom; each a left-to-right array of cell values. `rows[0][0]` is A1. */
192
- readonly rows: readonly (readonly CellValue[])[];
358
+ /**
359
+ * Rows top-to-bottom; each a left-to-right array of cells. `rows[0][0]` is A1. A hole or
360
+ * `undefined` row is an empty row (the bridge produces sparse arrays; hand-written input may
361
+ * too) — matching how `null`/`undefined`/holes inside a row mean empty cells.
362
+ */
363
+ readonly rows: readonly (readonly CellInput[] | undefined)[];
364
+ /**
365
+ * Column width/visibility declarations (F4.5) — the same shape `Worksheet.columns` returns.
366
+ * Ranges are 1-based and inclusive; entries need a `width` and/or `hidden: true`.
367
+ */
368
+ readonly columns?: readonly ColumnProps[];
369
+ /**
370
+ * Per-row height/visibility keyed by 1-based row index (F4.5) — the same records
371
+ * `Worksheet.rowProperties` holds. A row may have properties without having any cells.
372
+ */
373
+ readonly rowProperties?: Readonly<Record<number, RowProps>>;
374
+ /** Freeze the top `rows` rows and/or leftmost `cols` columns (F4.5). */
375
+ readonly freeze?: FreezePane;
376
+ /**
377
+ * Merged-cell ranges in canonical A1 form, top-left:bottom-right (e.g. `"A1:B2"`) — the same
378
+ * strings `Worksheet.mergedCells` returns (F4.6). Malformed, single-cell, out-of-grid, and
379
+ * overlapping ranges are rejected: Excel repair-prompts on them.
380
+ */
381
+ readonly merges?: readonly string[];
382
+ /**
383
+ * Hyperlinks on this sheet (F4.6) — the same records `Worksheet.hyperlinks` returns. Each needs
384
+ * a `ref` (cell or range) plus an external `target` and/or an in-workbook `location`; `tooltip`
385
+ * and `display` are optional. External targets get the sheet's relationships part.
386
+ */
387
+ readonly hyperlinks?: readonly Hyperlink[];
388
+ /**
389
+ * Tab visibility (F4.6). Defaults to `"visible"`; at least one sheet in the workbook must
390
+ * remain visible, or Excel refuses the file.
391
+ */
392
+ readonly state?: SheetState;
193
393
  }
194
394
  interface WorkbookInput {
195
395
  /** At least one sheet, in tab order. */
@@ -202,9 +402,11 @@ interface WriteOptions {
202
402
 
203
403
  /**
204
404
  * Convert an open {@link Workbook} into {@link WorkbookInput} for {@link writeXlsx}. Each populated
205
- * cell is placed at its own A1 reference, preserving sheet names and tab order. Rows/columns are
206
- * left sparse (array holes) — the writer treats a hole as an empty cell — so a workbook with a few
207
- * far-apart cells does not materialize a dense grid.
405
+ * cell is placed at its own A1 reference with its resolved style (if any), preserving sheet names
406
+ * and tab order. Rows/columns are left sparse (array holes) — the writer treats a hole as an empty
407
+ * cell — so a workbook with a few far-apart cells does not materialize a dense grid. An unstyled
408
+ * workbook yields bare values only: the exact pre-styles WorkbookInput, and the exact same bytes
409
+ * on rewrite.
208
410
  */
209
411
  declare function workbookToInput(workbook: Workbook): Promise<WorkbookInput>;
210
412
 
@@ -216,4 +418,4 @@ declare function workbookToInput(workbook: Workbook): Promise<WorkbookInput>;
216
418
  */
217
419
  declare function writeXlsx(workbook: WorkbookInput, options?: WriteOptions): Promise<Uint8Array>;
218
420
 
219
- export { type Cell, type CellRef, type CellType, type CellValue, type Comment, type Hyperlink, type ReadOptions, type Row, type SheetInfo, type SheetInput, Workbook, type WorkbookInput, Worksheet, type WriteOptions, XlsxError, type XlsxErrorCode, columnToIndex, dateToSerial, formatRef, indexToColumn, openXlsx, parseRef, serialToDate, streamSheetRows, workbookToInput, writeXlsx };
421
+ export { type Alignment, type BorderEdge, type BorderLineStyle, type BorderStyle, type Cell, type CellInput, type CellRef, type CellStyle, type CellType, type CellValue, type Color, type ColumnProps, type Comment, type FillStyle, type FontStyle, type FreezePane, type HorizontalAlignment, type Hyperlink, type PatternType, type ReadOptions, type Row, type RowProps, type SheetInfo, type SheetInput, type SheetState, type StyledCell, type UnderlineStyle, type VerticalAlignment, Workbook, type WorkbookInput, Worksheet, type WriteOptions, XlsxError, type XlsxErrorCode, columnToIndex, dateToSerial, formatRef, indexToColumn, openXlsx, parseRef, serialToDate, streamSheetRows, workbookToInput, writeXlsx };