@shbernal/ts-xlsx 1.0.2 → 1.1.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.
Files changed (114) hide show
  1. package/README.md +6 -0
  2. package/dist/core/autofilter.js +2 -2
  3. package/dist/core/cell.d.ts +10 -0
  4. package/dist/core/cell.js +16 -4
  5. package/dist/core/color-resolution.js +1 -1
  6. package/dist/core/column.d.ts +9 -1
  7. package/dist/core/column.js +11 -3
  8. package/dist/core/conditional-formatting-overlay.js +1 -1
  9. package/dist/core/data-validation-overlay.js +2 -2
  10. package/dist/core/formula.js +2 -2
  11. package/dist/core/grid-edits.js +5 -5
  12. package/dist/core/limits.d.ts +30 -0
  13. package/dist/core/limits.js +47 -0
  14. package/dist/core/merge.js +1 -1
  15. package/dist/core/pivot-table.js +7 -3
  16. package/dist/core/range.js +8 -4
  17. package/dist/core/row.d.ts +9 -1
  18. package/dist/core/row.js +11 -3
  19. package/dist/core/table-style.d.ts +2 -2
  20. package/dist/core/table-style.js +2 -2
  21. package/dist/core/table.js +2 -2
  22. package/dist/core/text-metrics.d.ts +20 -0
  23. package/dist/core/text-metrics.js +50 -0
  24. package/dist/core/theme.d.ts +1 -1
  25. package/dist/core/theme.js +1 -1
  26. package/dist/core/value.d.ts +50 -10
  27. package/dist/core/value.js +76 -0
  28. package/dist/core/workbook-protection.d.ts +1 -1
  29. package/dist/core/workbook.d.ts +24 -11
  30. package/dist/core/workbook.js +45 -22
  31. package/dist/core/worksheet-model.d.ts +1 -1
  32. package/dist/core/worksheet-model.js +5 -5
  33. package/dist/core/worksheet.d.ts +24 -2
  34. package/dist/core/worksheet.js +49 -21
  35. package/dist/customui/errors.js +1 -1
  36. package/dist/customui/index.js +2 -2
  37. package/dist/customui/ribbon.d.ts +1 -1
  38. package/dist/customui/ribbon.js +3 -3
  39. package/dist/entries/core.d.ts +3 -1
  40. package/dist/entries/core.js +16 -14
  41. package/dist/entries/csv.js +2 -2
  42. package/dist/entries/customui.js +1 -1
  43. package/dist/entries/errors.js +7 -7
  44. package/dist/entries/vba.js +2 -2
  45. package/dist/entries/xlsb.js +1 -1
  46. package/dist/entries/xlsx.js +5 -5
  47. package/dist/index.js +7 -7
  48. package/dist/io/csv/read.js +2 -2
  49. package/dist/io/csv/write.js +12 -24
  50. package/dist/io/opc/errors.js +1 -1
  51. package/dist/io/opc/inflate.d.ts +1 -1
  52. package/dist/io/opc/inflate.js +2 -2
  53. package/dist/io/opc/read-opc.js +2 -2
  54. package/dist/io/opc/rels.js +2 -2
  55. package/dist/io/opc/sniff-format.js +2 -2
  56. package/dist/io/style/xf-style.js +2 -2
  57. package/dist/io/xlsb/errors.js +1 -1
  58. package/dist/io/xlsb/formula.js +4 -4
  59. package/dist/io/xlsb/primitives.js +2 -2
  60. package/dist/io/xlsb/read-shared-strings.js +3 -3
  61. package/dist/io/xlsb/read-styles.js +5 -5
  62. package/dist/io/xlsb/read-worksheet.js +9 -9
  63. package/dist/io/xlsb/read.d.ts +1 -1
  64. package/dist/io/xlsb/read.js +15 -15
  65. package/dist/io/xlsb/record-stream.js +1 -1
  66. package/dist/io/xlsx/cell-accumulator.js +6 -6
  67. package/dist/io/xlsx/cell-value.js +4 -4
  68. package/dist/io/xlsx/comments.js +4 -4
  69. package/dist/io/xlsx/conditional-formatting.js +5 -5
  70. package/dist/io/xlsx/data-validation.js +4 -4
  71. package/dist/io/xlsx/edit-vba.js +4 -4
  72. package/dist/io/xlsx/errors.js +1 -1
  73. package/dist/io/xlsx/hyperlinks.js +4 -4
  74. package/dist/io/xlsx/images.js +6 -6
  75. package/dist/io/xlsx/package-plan.js +3 -3
  76. package/dist/io/xlsx/pivot-read.js +2 -2
  77. package/dist/io/xlsx/pivot.js +4 -4
  78. package/dist/io/xlsx/read-rows.d.ts +3 -3
  79. package/dist/io/xlsx/read-rows.js +12 -12
  80. package/dist/io/xlsx/read-styles.js +4 -4
  81. package/dist/io/xlsx/read-worksheet.js +8 -8
  82. package/dist/io/xlsx/read.d.ts +1 -1
  83. package/dist/io/xlsx/read.js +28 -28
  84. package/dist/io/xlsx/relationships.d.ts +6 -6
  85. package/dist/io/xlsx/relationships.js +2 -2
  86. package/dist/io/xlsx/rich-runs.js +1 -1
  87. package/dist/io/xlsx/rich-text.js +2 -2
  88. package/dist/io/xlsx/shared-formulas.js +3 -3
  89. package/dist/io/xlsx/shared-strings-read.js +2 -2
  90. package/dist/io/xlsx/shared-strings.js +3 -3
  91. package/dist/io/xlsx/sheet-properties.js +4 -4
  92. package/dist/io/xlsx/styles.js +6 -6
  93. package/dist/io/xlsx/tables.js +5 -5
  94. package/dist/io/xlsx/threaded-comments.js +5 -5
  95. package/dist/io/xlsx/workbook-xml.js +9 -9
  96. package/dist/io/xlsx/worksheet-xml.js +18 -18
  97. package/dist/io/xlsx/write-stream.d.ts +1 -1
  98. package/dist/io/xlsx/write-stream.js +7 -7
  99. package/dist/io/xlsx/write.d.ts +3 -3
  100. package/dist/io/xlsx/write.js +20 -20
  101. package/dist/io/xlsx/x14-ext.js +1 -1
  102. package/dist/vba/cfb-writer.js +1 -1
  103. package/dist/vba/cfb.js +1 -1
  104. package/dist/vba/codepage.js +1 -1
  105. package/dist/vba/errors.js +1 -1
  106. package/dist/vba/index.js +5 -5
  107. package/dist/vba/ms-ovba.js +1 -1
  108. package/dist/vba/project-editor.js +7 -7
  109. package/dist/vba/project.js +4 -4
  110. package/dist/vba/vba-encoding.js +1 -1
  111. package/dist/xml/errors.js +1 -1
  112. package/dist/xml/xml-read.js +1 -1
  113. package/dist/xml/xml.js +1 -1
  114. package/package.json +4 -3
package/README.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # ts-xlsx
2
2
 
3
+ [![npm](https://img.shields.io/npm/v/%40shbernal%2Fts-xlsx)](https://www.npmjs.com/package/@shbernal/ts-xlsx)
4
+ [![weekly downloads](https://img.shields.io/npm/dw/%40shbernal%2Fts-xlsx.svg?label=npm%20downloads&logo=npm)](https://www.npmjs.com/package/@shbernal/ts-xlsx)
5
+ [![total downloads](https://img.shields.io/npm/dt/%40shbernal%2Fts-xlsx.svg?label=npm%20total%20downloads&logo=npm)](https://www.npmjs.com/package/@shbernal/ts-xlsx)
6
+ [![CI](https://github.com/shbernal/ts-xlsx/actions/workflows/build.yml/badge.svg)](https://github.com/shbernal/ts-xlsx/actions/workflows/build.yml)
7
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
8
+
3
9
  A TypeScript-first library for reading and writing spreadsheet documents
4
10
  (`.xlsx` / OOXML, and CSV) — synchronous, `Uint8Array`-native, and dependency-lean.
5
11
 
@@ -1,5 +1,5 @@
1
- import { AuthoringError } from "../errors.js";
2
- import { decodeRange } from "./address.js";
1
+ import { AuthoringError } from '../errors.js';
2
+ import { decodeRange } from './address.js';
3
3
  const CUSTOM_FILTER_OPERATORS = new Set([
4
4
  'equal',
5
5
  'notEqual',
@@ -22,6 +22,16 @@ export declare class Cell {
22
22
  set value(value: CellValue | undefined);
23
23
  /** The observable {@link ValueType} of the current value. */
24
24
  get type(): ValueType;
25
+ /**
26
+ * The cell's value as plain text ({@link cellValueToText}), `""` when it is empty — so a reader
27
+ * that only wants strings never has to narrow the value union itself.
28
+ *
29
+ * Read-only, because text is a *rendering* of the value and not a second place to store one:
30
+ * writing `"3"` here could only mean the string `"3"`, which is exactly `value = '3'` and reads
31
+ * nothing like it. The number format is not applied either — the style is not the cell's value,
32
+ * so a currency cell's text carries no currency sign.
33
+ */
34
+ get text(): string;
25
35
  /**
26
36
  * Assign rich text whose runs **inherit this cell's font**, so a run needs to state only what it
27
37
  * changes: `setRichText([{text: 'Note:', font: {bold: true}}, {text: ' the rest'}])` keeps the
package/dist/core/cell.js CHANGED
@@ -4,10 +4,10 @@
4
4
  // *numbers* (legacy shipped a type declaration calling them strings, which broke
5
5
  // strict consumers). The value is the only mutable state here; assigning it routes
6
6
  // through the value model so the cell's `type` is always consistent with what it holds.
7
- import { encodeAddress } from "./address.js";
8
- import { NAMED_STYLE_ID } from "./internal.js";
9
- import { assignStyleFacets, } from "./style.js";
10
- import { coerceCellValue, detectValueType, } from "./value.js";
7
+ import { encodeAddress } from './address.js';
8
+ import { NAMED_STYLE_ID } from './internal.js';
9
+ import { assignStyleFacets, } from './style.js';
10
+ import { cellValueToText, coerceCellValue, detectValueType, } from './value.js';
11
11
  /**
12
12
  * A single cell owns its value and every style facet outright. Each facet below — fill, number format,
13
13
  * font, border, alignment, protection, quote-prefix, and note — is held in the cell's own field and
@@ -54,6 +54,18 @@ export class Cell {
54
54
  get type() {
55
55
  return detectValueType(this.#value);
56
56
  }
57
+ /**
58
+ * The cell's value as plain text ({@link cellValueToText}), `""` when it is empty — so a reader
59
+ * that only wants strings never has to narrow the value union itself.
60
+ *
61
+ * Read-only, because text is a *rendering* of the value and not a second place to store one:
62
+ * writing `"3"` here could only mean the string `"3"`, which is exactly `value = '3'` and reads
63
+ * nothing like it. The number format is not applied either — the style is not the cell's value,
64
+ * so a currency cell's text carries no currency sign.
65
+ */
66
+ get text() {
67
+ return cellValueToText(this.#value);
68
+ }
57
69
  /**
58
70
  * Assign rich text whose runs **inherit this cell's font**, so a run needs to state only what it
59
71
  * changes: `setRichText([{text: 'Note:', font: {bold: true}}, {text: ' the rest'}])` keeps the
@@ -9,7 +9,7 @@
9
9
  // keeps the encoding its file used, so a round-trip re-emits `theme="4" tint="0.4"` rather than
10
10
  // rewriting every cell as a literal ARGB — which would bloat the styles table, break the link to the
11
11
  // theme (recolouring the workbook would stop working), and change what the file means.
12
- import { DEFAULT_THEME_COLOR_SCHEME, THEME_COLOR_SLOTS } from "./theme.js";
12
+ import { DEFAULT_THEME_COLOR_SCHEME, THEME_COLOR_SLOTS } from './theme.js';
13
13
  /**
14
14
  * The built-in indexed colour palette (ECMA-376 §18.8.27), by index. Entries 0–7 duplicate 8–15 —
15
15
  * redundancy the spec preserves for backwards compatibility with the legacy formats this palette came
@@ -23,7 +23,15 @@ export declare class Column {
23
23
  */
24
24
  get key(): string | undefined;
25
25
  set key(key: string | undefined);
26
- /** Column width in character units; `undefined` leaves the sheet default in force. */
26
+ /**
27
+ * Column width in character units — digits of the workbook default font's maximum digit width,
28
+ * so what one unit measures moves with that font. `undefined` leaves the sheet default in force.
29
+ *
30
+ * Not bounded here, for the same reason {@link Row.height} is not, and for a stronger one:
31
+ * {@link MAX_COLUMN_WIDTH} bounds what Excel accepts being set, not what a file may hold. Excel
32
+ * reads a wider column back unchanged and re-saves it verbatim, so a bound here would refuse a
33
+ * width Excel itself preserves.
34
+ */
27
35
  get width(): number | undefined;
28
36
  set width(width: number | undefined);
29
37
  /** Whether the column is hidden. */
@@ -7,8 +7,8 @@
7
7
  // A column carries more than a row does: besides the geometry (width, visibility, outline) it holds
8
8
  // the six `CellStyle` facets as *defaults* for its cells, which is why the mirror below is twice the
9
9
  // length of the row's.
10
- import { encodeAddress, numberToColumn } from "./address.js";
11
- import { INTERNAL } from "./internal.js";
10
+ import { encodeAddress, numberToColumn } from './address.js';
11
+ import { INTERNAL } from './internal.js';
12
12
  export class Column {
13
13
  #sheet;
14
14
  /** 1-based column index. Fixed for this handle's lifetime. */
@@ -44,7 +44,15 @@ export class Column {
44
44
  set key(key) {
45
45
  this.#write('key', key);
46
46
  }
47
- /** Column width in character units; `undefined` leaves the sheet default in force. */
47
+ /**
48
+ * Column width in character units — digits of the workbook default font's maximum digit width,
49
+ * so what one unit measures moves with that font. `undefined` leaves the sheet default in force.
50
+ *
51
+ * Not bounded here, for the same reason {@link Row.height} is not, and for a stronger one:
52
+ * {@link MAX_COLUMN_WIDTH} bounds what Excel accepts being set, not what a file may hold. Excel
53
+ * reads a wider column back unchanged and re-saves it verbatim, so a bound here would refuse a
54
+ * width Excel itself preserves.
55
+ */
48
56
  get width() {
49
57
  return this.#read('width');
50
58
  }
@@ -1,7 +1,7 @@
1
1
  // The conditional-formatting overlay a Worksheet owns: an insertion-ordered, defensively-copied list
2
2
  // of range-bound rule sets. Kept as its own class, the sibling to {@link DataValidationOverlay}, so
3
3
  // Worksheet delegates the collection's storage and cloning rather than managing the array itself.
4
- import { cloneConditionalFormatting } from "./conditional-formatting.js";
4
+ import { cloneConditionalFormatting } from './conditional-formatting.js';
5
5
  export class ConditionalFormattingOverlay {
6
6
  #entries = [];
7
7
  /**
@@ -3,8 +3,8 @@
3
3
  // as its own class — the sibling to {@link GridEdits} that owns splice arithmetic — rather than inline
4
4
  // on Worksheet, since a validation's storage (a rule plus its decoded ranges) is a self-contained unit
5
5
  // Worksheet only ever adds to, reads, or clears wholesale.
6
- import { cloneDataValidation, } from "./data-validation.js";
7
- import { decodeSqrefRects } from "./merge.js";
6
+ import { cloneDataValidation, } from './data-validation.js';
7
+ import { decodeSqrefRects } from './merge.js';
8
8
  export class DataValidationOverlay {
9
9
  #entries = [];
10
10
  #rects = [];
@@ -20,8 +20,8 @@
20
20
  // verbatim and hands each code run between them to a transform. `mangleParams` is the deliberate
21
21
  // exception: LET/LAMBDA parameter scope opens and closes at paren boundaries, state `scanFormula`'s
22
22
  // per-run transform cannot carry, so it runs its own forward walk — still deferring to `skipOpaque`.
23
- import { columnToNumber, numberToColumn } from "./address.js";
24
- import { MODERN_FUNCTIONS } from "./modern-functions.js";
23
+ import { columnToNumber, numberToColumn } from './address.js';
24
+ import { MODERN_FUNCTIONS } from './modern-functions.js';
25
25
  const XLFN = '_xlfn.';
26
26
  const XLPM = '_xlpm.';
27
27
  /**
@@ -4,11 +4,11 @@
4
4
  // mechanics: it holds the sheet's storage containers by reference and mutates them in place, and
5
5
  // touches none of the public cell API. Worksheet builds the cells an insert introduces, then hands
6
6
  // the pre-built rows (or the raw column values) here for the shift.
7
- import { decodeAddress, decodeRange, encodeAddress } from "./address.js";
8
- import { Cell, copyCellContent } from "./cell.js";
9
- import { replaceContents } from "./containers.js";
10
- import { isOneCellAnchor } from "./image.js";
11
- import { isSharedFormulaValue } from "./value.js";
7
+ import { decodeAddress, decodeRange, encodeAddress } from './address.js';
8
+ import { Cell, copyCellContent } from './cell.js';
9
+ import { replaceContents } from './containers.js';
10
+ import { isOneCellAnchor } from './image.js';
11
+ import { isSharedFormulaValue } from './value.js';
12
12
  // The shift rule shared by every re-anchoring pass: a coordinate before the edit stays put, one at or
13
13
  // after the edited span shifts by `delta`, and one inside a deleted span clamps to the cut line (`start`).
14
14
  function shiftIndex(v, start, count, delta) {
@@ -0,0 +1,30 @@
1
+ /**
2
+ * The tallest row Excel accepts being set to, in points: it refuses 409.6 and takes 409.5. A row
3
+ * asked to hold more wrapped text than this cannot grow to fit it, and the overflow is simply not
4
+ * shown.
5
+ *
6
+ * A file may state more, and stating more loses the value rather than the file. Excel opens such a
7
+ * package without complaint and silently clamps the row — to 409.6, a tick *above* what it lets
8
+ * you assign, being 8192 twentieths of a point and so the width of the field it is read into — and
9
+ * writes 409.6 back on its next save. Check against this constant to keep a stated height from
10
+ * quietly becoming a different one.
11
+ */
12
+ export declare const MAX_ROW_HEIGHT = 409.5;
13
+ /**
14
+ * The widest column Excel accepts being set to, in character units of the workbook's default font.
15
+ * Excel refuses 255.4, so unlike the row-height ceiling this one is exactly integral.
16
+ *
17
+ * It is also the weaker of the two ceilings: it binds assignment only, and not a file at all.
18
+ * Excel honours a `width` of 1000 read from a package, renders the column at it, and round-trips
19
+ * it verbatim through its own save — where an over-limit row height is clamped away. So a width
20
+ * above this is a column no Excel user could have produced by dragging, not a value at risk.
21
+ *
22
+ * Character units, not points or pixels: a width is a count of digits of the default font's
23
+ * *maximum digit width*, which is why there is no companion `DEFAULT_COLUMN_WIDTH` constant here.
24
+ * The width a column takes when it states none is a function of that font — the familiar 8.43 holds
25
+ * for Calibri 11 and not for a workbook whose normal style says otherwise (Excel reports 8.09 for
26
+ * Aptos Narrow 11). `sheet.properties.defaultColWidth` is what a file declares, and
27
+ * docs/knowledge/specs/default-font-must-not-be-assumed-for-column-widths.md is why assuming a
28
+ * value for it is a bug rather than a shortcut.
29
+ */
30
+ export declare const MAX_COLUMN_WIDTH = 255;
@@ -0,0 +1,47 @@
1
+ // Excel's limits on the grid's *geometry* — the other half of the bounds `address.ts` states.
2
+ //
3
+ // `MAX_ROW`/`MAX_COLUMN` bound where a cell can be; these bound how big a line can be *set*. The
4
+ // difference that matters is who enforces them: the addressing bounds are structural (a reference
5
+ // past XFD is not a reference), so the model refuses them outright, while these are limits on
6
+ // *assignment* in Excel's own UI and object model. They are not limits on what a package may
7
+ // carry. The schema types `ht` and `width` as a bare `xsd:double` with no ceiling on either, and
8
+ // Excel opens an over-limit file clean — no repair prompt, no repair log — so a reader that threw
9
+ // on one would refuse a file Excel accepts. Nothing here is enforced, therefore, on either the
10
+ // read or the write path; these are for a caller that wants the size it states to be the size
11
+ // Excel uses, and the doc comments on `Row.height`/`Column.width` point here for that reason.
12
+ //
13
+ // What Excel does with an over-limit file is not symmetric, and the difference is why only one of
14
+ // these two numbers describes a value that survives: a row is silently clamped, a column is not.
15
+ // Both ceilings and both file behaviours were measured rather than quoted — Microsoft's own
16
+ // specifications page rounds the row-height ceiling to "409 points", and Excel takes 409.5. See
17
+ // docs/knowledge/specs/grid-geometry-limits-are-excels-not-the-schemas.md for the probes.
18
+ /**
19
+ * The tallest row Excel accepts being set to, in points: it refuses 409.6 and takes 409.5. A row
20
+ * asked to hold more wrapped text than this cannot grow to fit it, and the overflow is simply not
21
+ * shown.
22
+ *
23
+ * A file may state more, and stating more loses the value rather than the file. Excel opens such a
24
+ * package without complaint and silently clamps the row — to 409.6, a tick *above* what it lets
25
+ * you assign, being 8192 twentieths of a point and so the width of the field it is read into — and
26
+ * writes 409.6 back on its next save. Check against this constant to keep a stated height from
27
+ * quietly becoming a different one.
28
+ */
29
+ export const MAX_ROW_HEIGHT = 409.5;
30
+ /**
31
+ * The widest column Excel accepts being set to, in character units of the workbook's default font.
32
+ * Excel refuses 255.4, so unlike the row-height ceiling this one is exactly integral.
33
+ *
34
+ * It is also the weaker of the two ceilings: it binds assignment only, and not a file at all.
35
+ * Excel honours a `width` of 1000 read from a package, renders the column at it, and round-trips
36
+ * it verbatim through its own save — where an over-limit row height is clamped away. So a width
37
+ * above this is a column no Excel user could have produced by dragging, not a value at risk.
38
+ *
39
+ * Character units, not points or pixels: a width is a count of digits of the default font's
40
+ * *maximum digit width*, which is why there is no companion `DEFAULT_COLUMN_WIDTH` constant here.
41
+ * The width a column takes when it states none is a function of that font — the familiar 8.43 holds
42
+ * for Calibri 11 and not for a workbook whose normal style says otherwise (Excel reports 8.09 for
43
+ * Aptos Narrow 11). `sheet.properties.defaultColWidth` is what a file declares, and
44
+ * docs/knowledge/specs/default-font-must-not-be-assumed-for-column-widths.md is why assuming a
45
+ * value for it is a bug rather than a shortcut.
46
+ */
47
+ export const MAX_COLUMN_WIDTH = 255;
@@ -1,7 +1,7 @@
1
1
  // Inclusive grid rectangles and the geometry a worksheet uses to reason about merged regions and the
2
2
  // `sqref` ranges that overlays (data validations, conditional formats) apply to: overlap detection and
3
3
  // decoding an OOXML `sqref` into containment rectangles.
4
- import { decodeRange } from "./address.js";
4
+ import { decodeRange } from './address.js';
5
5
  /** Whether two inclusive grid rectangles share at least one cell. */
6
6
  export function rectsOverlap(a, b) {
7
7
  return a.left <= b.right && b.left <= a.right && a.top <= b.bottom && b.top <= a.bottom;
@@ -9,9 +9,9 @@
9
9
  //
10
10
  // The source data is captured when the pivot is added — the model reads the source sheet's cells
11
11
  // once, here, so the pivot is a stable snapshot independent of later edits to the source.
12
- import { AuthoringError, InternalError } from "../errors.js";
13
- import { encodeAddress } from "./address.js";
14
- import { isErrorValue, isFormulaValue, isHyperlinkValue, isRichTextValue, isSharedFormulaValue, richTextToPlain, } from "./value.js";
12
+ import { AuthoringError, InternalError } from '../errors.js';
13
+ import { encodeAddress } from './address.js';
14
+ import { isErrorValue, isFormulaValue, isHyperlinkValue, isRichTextValue, isSharedFormulaValue, richTextToPlain, } from './value.js';
15
15
  const PIVOT_METRICS = new Set([
16
16
  'sum',
17
17
  'count',
@@ -216,6 +216,10 @@ function textOf(item) {
216
216
  * Reduce any cell value to the scalar a pivot cache can hold: a number, a string, or a blank. Only
217
217
  * finite numbers stay numeric (a NaN would corrupt the cache); every other kind is flattened to its
218
218
  * displayed text so hostile or exotic source content can never throw or leak an object into the XML.
219
+ *
220
+ * Deliberately not `cellValueToText`, close as the two look: this classifies rather than renders
221
+ * (a number must reach the cache *as a number*), and its no-throw promise is the opposite of that
222
+ * function's, which rejects a value outside the union rather than quietly caching a blank.
219
223
  */
220
224
  function scalarOf(value) {
221
225
  if (value === null)
@@ -9,9 +9,9 @@
9
9
  // million cells, whereas OOXML expresses it natively as a single `<col style>` or `<row s>` — which
10
10
  // is what `getColumn`/`getRow` already write, in constant space. Refusing them is what lets a range
11
11
  // materialise its cells eagerly without a cost cliff hiding behind an innocuous-looking call.
12
- import { decodeRange, encodeAddress, MAX_COLUMN, MAX_ROW } from "./address.js";
13
- import { applyCellStyle } from "./cell.js";
14
- import { CELL_STYLE_FACETS, } from "./style.js";
12
+ import { decodeRange, encodeAddress, MAX_COLUMN, MAX_ROW } from './address.js';
13
+ import { applyCellStyle } from './cell.js';
14
+ import { CELL_STYLE_FACETS, } from './style.js';
15
15
  /**
16
16
  * A rectangular block of a worksheet's cells: `sheet.getRange('B2:D5')`.
17
17
  *
@@ -242,9 +242,13 @@ function setFacet(cell, facet, value) {
242
242
  // order their keys were written in. Sound here and nowhere near a general deep-equal: every facet is
243
243
  // a plain data record of strings, numbers, booleans and nested records — no functions, no cycles, no
244
244
  // class instances — which is exactly the shape JSON round-trips faithfully.
245
+ //
246
+ // `undefined` needs a sentinel because `JSON.stringify` answers it with `undefined` rather than a
247
+ // string. NUL is the one that cannot collide: a defined value stringifies to `"`, `{`, `[`, a digit,
248
+ // or a bare `true`/`false`/`null`, and a NUL *inside* a string comes back as a six-char escape.
245
249
  function facetKey(value) {
246
250
  if (value === undefined)
247
- return '';
251
+ return '\u0000';
248
252
  return JSON.stringify(value, (_key, inner) => inner !== null && typeof inner === 'object' && !Array.isArray(inner)
249
253
  ? Object.fromEntries(Object.entries(inner).sort(([a], [b]) => (a < b ? -1 : 1)))
250
254
  : inner);
@@ -15,7 +15,15 @@ export declare class Row {
15
15
  * formatted, and they create the record on first write.
16
16
  */
17
17
  get properties(): Readonly<RowProperties> | undefined;
18
- /** Row height in points; `undefined` leaves the sheet default in force. */
18
+ /**
19
+ * Row height in points; `undefined` leaves the sheet default in force.
20
+ *
21
+ * Not bounded here, deliberately: {@link MAX_ROW_HEIGHT} is what Excel accepts *being set*, but
22
+ * the schema puts no ceiling on `ht` and this setter is also how the reader loads a foreign
23
+ * file, so refusing a taller row would mean refusing a file Excel opens clean. Check against the
24
+ * constant when authoring — Excel silently clamps a taller row on read, so a height above it is
25
+ * one you state and do not get.
26
+ */
19
27
  get height(): number | undefined;
20
28
  set height(height: number | undefined);
21
29
  /** Whether the row is hidden. */
package/dist/core/row.js CHANGED
@@ -12,8 +12,8 @@
12
12
  //
13
13
  // Formatting is created on write, never on read. Asking for `sheet.getRow(500)` costs nothing and
14
14
  // does not extend the used range; assigning `height` is what materialises the record.
15
- import { columnToNumber, encodeAddress } from "./address.js";
16
- import { INTERNAL } from "./internal.js";
15
+ import { columnToNumber, encodeAddress } from './address.js';
16
+ import { INTERNAL } from './internal.js';
17
17
  export class Row {
18
18
  #sheet;
19
19
  /** 1-based row number. Fixed for this handle's lifetime. */
@@ -35,7 +35,15 @@ export class Row {
35
35
  get properties() {
36
36
  return this.#sheet[INTERNAL].rowPropertiesOf(this.number);
37
37
  }
38
- /** Row height in points; `undefined` leaves the sheet default in force. */
38
+ /**
39
+ * Row height in points; `undefined` leaves the sheet default in force.
40
+ *
41
+ * Not bounded here, deliberately: {@link MAX_ROW_HEIGHT} is what Excel accepts *being set*, but
42
+ * the schema puts no ceiling on `ht` and this setter is also how the reader loads a foreign
43
+ * file, so refusing a taller row would mean refusing a file Excel opens clean. Check against the
44
+ * constant when authoring — Excel silently clamps a taller row on read, so a height above it is
45
+ * one you state and do not get.
46
+ */
39
47
  get height() {
40
48
  return this.#read('height');
41
49
  }
@@ -8,7 +8,7 @@ import type { DifferentialStyle } from './style.ts';
8
8
  * the style's own `table`/`pivot` flags, not the element names — so the type carries all of them
9
9
  * rather than splitting into two enumerations that a caller would have to choose between up front.
10
10
  */
11
- export declare const TABLE_STYLE_ELEMENT_TYPES: readonly ["wholeTable", "headerRow", "totalRow", "firstColumn", "lastColumn", "firstRowStripe", "secondRowStripe", "firstColumnStripe", "secondColumnStripe", "firstHeaderCell", "lastHeaderCell", "firstTotalCell", "lastTotalCell", "firstSubtotalColumn", "secondSubtotalColumn", "thirdSubtotalColumn", "firstSubtotalRow", "secondSubtotalRow", "thirdSubtotalRow", "blankRow", "firstColumnSubheading", "secondColumnSubheading", "thirdColumnSubheading", "firstRowSubheading", "secondRowSubheading", "thirdRowSubheading", "pageFieldLabels", "pageFieldValues"];
11
+ export declare const TABLE_STYLE_ELEMENT_TYPES: readonly ['wholeTable', 'headerRow', 'totalRow', 'firstColumn', 'lastColumn', 'firstRowStripe', 'secondRowStripe', 'firstColumnStripe', 'secondColumnStripe', 'firstHeaderCell', 'lastHeaderCell', 'firstTotalCell', 'lastTotalCell', 'firstSubtotalColumn', 'secondSubtotalColumn', 'thirdSubtotalColumn', 'firstSubtotalRow', 'secondSubtotalRow', 'thirdSubtotalRow', 'blankRow', 'firstColumnSubheading', 'secondColumnSubheading', 'thirdColumnSubheading', 'firstRowSubheading', 'secondRowSubheading', 'thirdRowSubheading', 'pageFieldLabels', 'pageFieldValues'];
12
12
  /** One region of a table or pivot that a table style can format. */
13
13
  export type TableStyleElementType = (typeof TABLE_STYLE_ELEMENT_TYPES)[number];
14
14
  /** The four element types banded across several rows or columns — the only ones {@link TableStyleElement.size} means anything on. */
@@ -61,7 +61,7 @@ export interface TableStyle {
61
61
  * `size` outside a stripe is ignored — neither shows up as a repair prompt or a schema error, so the
62
62
  * only place to catch them is the call that made them.
63
63
  *
64
- * @throws {@link AuthoringError} if the name is empty, or a non-stripe element carries a `size`, or a `size` is not
64
+ * @throws {AuthoringError} if the name is empty, or a non-stripe element carries a `size`, or a `size` is not
65
65
  * a positive integer.
66
66
  */
67
67
  export declare function checkTableStyle(style: TableStyle): void;
@@ -5,7 +5,7 @@
5
5
  // gallery ("TableStyleMedium2" and its sixty siblings) is exactly this shape; a workbook that declares
6
6
  // its own joins the gallery for that file, and a table reaches it by name through
7
7
  // `TableStyleInfo.name`.
8
- import { AuthoringError } from "../errors.js";
8
+ import { AuthoringError } from '../errors.js';
9
9
  /**
10
10
  * The regions a table style can format (`ST_TableStyleType`).
11
11
  *
@@ -63,7 +63,7 @@ export function isTableStyleElementType(value) {
63
63
  * `size` outside a stripe is ignored — neither shows up as a repair prompt or a schema error, so the
64
64
  * only place to catch them is the call that made them.
65
65
  *
66
- * @throws {@link AuthoringError} if the name is empty, or a non-stripe element carries a `size`, or a `size` is not
66
+ * @throws {AuthoringError} if the name is empty, or a non-stripe element carries a `size`, or a `size` is not
67
67
  * a positive integer.
68
68
  */
69
69
  export function checkTableStyle(style) {
@@ -6,8 +6,8 @@
6
6
  // only), a headerless table (data rows only), or a totals-bearing table. The writer is
7
7
  // the OOXML gatekeeper for serialization; this model owns the invariants Excel enforces
8
8
  // on the *shape* itself: a legal name, at least one column, and at least one row.
9
- import { AuthoringError } from "../errors.js";
10
- import { decodeAddress, encodeAddress } from "./address.js";
9
+ import { AuthoringError } from '../errors.js';
10
+ import { decodeAddress, encodeAddress } from './address.js';
11
11
  /** Copy a style, keeping only its defined fields off the literal so `exactOptionalPropertyTypes`
12
12
  * never sees a fabricated `key: undefined` — an absent attribute must stay absent across a copy.
13
13
  *
@@ -0,0 +1,20 @@
1
+ /**
2
+ * The number of lines `text` occupies when wrapped at `width` character units - the width unit a
3
+ * column states, so `estimateWrappedLines(cell, sheet.getColumn(2).width ?? 8.43)` is the shape of
4
+ * the call.
5
+ *
6
+ * A hard break opens a line of its own and what follows wraps independently, matching how Excel
7
+ * lays a wrapped cell out. The empty string is one line, not zero: a cell always occupies its row.
8
+ *
9
+ * An estimate, and only ever that. It counts characters, so it is exact for a monospaced face that
10
+ * wraps mid-word and approximate for every other - a run of `W`s wraps sooner on screen than this
11
+ * predicts, a run of `i`s later. Against Excel it reads a shade *low*, because Excel breaks at word
12
+ * boundaries and its usable width is about 0.64 character units under the stated one: measured at
13
+ * 5 lines where Excel laid out 6, 25 where Excel laid out 26. This exists so that a writer can
14
+ * state *a* height rather than leave one to the application that opens the file, and being within a
15
+ * line of the truth is what that needs.
16
+ *
17
+ * @throws {RangeError} if `width` is not a positive finite number - a column of zero width wraps
18
+ * nothing, and silently answering `Infinity` or `NaN` would put that straight into a row height.
19
+ */
20
+ export declare function estimateWrappedLines(text: string, width: number): number;
@@ -0,0 +1,50 @@
1
+ // How many lines a wrapped cell takes - the one measurement a writer needs and the format does
2
+ // not record.
3
+ //
4
+ // A row that states no height records no geometry, so the height is whoever opens the file's
5
+ // answer. Excel Desktop's answer is an auto-fit computed on open - promptly and correctly, but
6
+ // saturating at MAX_ROW_HEIGHT, so past ~28 lines it stops answering the question - and no answer
7
+ // at all is what a consumer that does not implement that auto-fit gets, this library's reader
8
+ // included. Stating a height is how the geometry stops depending on the reader, and to state one
9
+ // you have to know the line count. Measured in
10
+ // docs/knowledge/specs/rows-with-no-stated-height-are-autofitted-on-open.md, which also buries the
11
+ // claim this comment used to make, that such a sheet opens with blank unpainted bands. It does not.
12
+ //
13
+ // This counts *characters against a character-unit width*. It does not measure glyphs, and it is
14
+ // not the deferred font-metric question that
15
+ // docs/knowledge/specs/default-font-must-not-be-assumed-for-column-widths.md leaves open: a column
16
+ // width is already expressed in character units, so counting characters is dimensionally honest
17
+ // and needs no metric table. It is also, for the same reason, an approximation for any
18
+ // proportional face - `WWW` and `iii` are one character unit each here and are not on screen.
19
+ /**
20
+ * The number of lines `text` occupies when wrapped at `width` character units - the width unit a
21
+ * column states, so `estimateWrappedLines(cell, sheet.getColumn(2).width ?? 8.43)` is the shape of
22
+ * the call.
23
+ *
24
+ * A hard break opens a line of its own and what follows wraps independently, matching how Excel
25
+ * lays a wrapped cell out. The empty string is one line, not zero: a cell always occupies its row.
26
+ *
27
+ * An estimate, and only ever that. It counts characters, so it is exact for a monospaced face that
28
+ * wraps mid-word and approximate for every other - a run of `W`s wraps sooner on screen than this
29
+ * predicts, a run of `i`s later. Against Excel it reads a shade *low*, because Excel breaks at word
30
+ * boundaries and its usable width is about 0.64 character units under the stated one: measured at
31
+ * 5 lines where Excel laid out 6, 25 where Excel laid out 26. This exists so that a writer can
32
+ * state *a* height rather than leave one to the application that opens the file, and being within a
33
+ * line of the truth is what that needs.
34
+ *
35
+ * @throws {RangeError} if `width` is not a positive finite number - a column of zero width wraps
36
+ * nothing, and silently answering `Infinity` or `NaN` would put that straight into a row height.
37
+ */
38
+ export function estimateWrappedLines(text, width) {
39
+ if (!Number.isFinite(width) || width <= 0) {
40
+ throw new RangeError(`wrap width ${width} is not a positive finite number of character units`);
41
+ }
42
+ let lines = 0;
43
+ // A lone \r is not a break Excel writes, but a value assembled by a caller on Windows may carry
44
+ // one; counting it as a break costs nothing and mis-counting it as a character would show up as
45
+ // a row one line short.
46
+ for (const segment of text.split(/\r\n?|\n/)) {
47
+ lines += Math.max(1, Math.ceil(segment.length / width));
48
+ }
49
+ return lines;
50
+ }
@@ -12,7 +12,7 @@
12
12
  * `test/corpus/fixtures/excel-oracle/theme-color-index-order.json`. The stylesheet's own default font
13
13
  * is the everyday witness: it carries `<color theme="1"/>` and renders black, which is `dk1`.
14
14
  */
15
- export declare const THEME_COLOR_SLOTS: readonly ["lt1", "dk1", "lt2", "dk2", "accent1", "accent2", "accent3", "accent4", "accent5", "accent6", "hlink", "folHlink"];
15
+ export declare const THEME_COLOR_SLOTS: readonly ['lt1', 'dk1', 'lt2', 'dk2', 'accent1', 'accent2', 'accent3', 'accent4', 'accent5', 'accent6', 'hlink', 'folHlink'];
16
16
  /** One slot of a theme's colour scheme. */
17
17
  export type ThemeColorSlot = (typeof THEME_COLOR_SLOTS)[number];
18
18
  /**
@@ -4,7 +4,7 @@
4
4
  // module reads just the `<a:clrScheme>` out of it, because that is the only piece a colour reference
5
5
  // needs. Everything else in a theme (the format scheme's gradients, line and effect styles) is
6
6
  // nobody's business here.
7
- import { AuthoringError } from "../errors.js";
7
+ import { AuthoringError } from '../errors.js';
8
8
  /**
9
9
  * The twelve colour-scheme slots **in the order a `theme="n"` attribute indexes them**.
10
10
  *
@@ -1,19 +1,19 @@
1
1
  import type { Font } from './style.ts';
2
2
  /** The observable kind of a cell's value. Both formula shapes report as `Formula`. */
3
3
  export declare const ValueType: {
4
- readonly Null: "null";
5
- readonly Number: "number";
6
- readonly String: "string";
7
- readonly Boolean: "boolean";
8
- readonly Date: "date";
9
- readonly Error: "error";
10
- readonly Formula: "formula";
11
- readonly RichText: "richText";
12
- readonly Hyperlink: "hyperlink";
4
+ readonly Null: 'null';
5
+ readonly Number: 'number';
6
+ readonly String: 'string';
7
+ readonly Boolean: 'boolean';
8
+ readonly Date: 'date';
9
+ readonly Error: 'error';
10
+ readonly Formula: 'formula';
11
+ readonly RichText: 'richText';
12
+ readonly Hyperlink: 'hyperlink';
13
13
  };
14
14
  export type ValueType = (typeof ValueType)[keyof typeof ValueType];
15
15
  /** The canonical Excel error literals a cell (or formula result) can carry. */
16
- export declare const ERROR_CODES: readonly ["#N/A", "#REF!", "#NAME?", "#DIV/0!", "#NULL!", "#VALUE!", "#NUM!", "#SPILL!", "#CALC!", "#GETTING_DATA"];
16
+ export declare const ERROR_CODES: readonly ['#N/A', '#REF!', '#NAME?', '#DIV/0!', '#NULL!', '#VALUE!', '#NUM!', '#SPILL!', '#CALC!', '#GETTING_DATA'];
17
17
  export type ErrorCode = (typeof ERROR_CODES)[number];
18
18
  /** An in-cell error, e.g. `{error: '#REF!'}`. */
19
19
  export interface ErrorValue {
@@ -81,11 +81,32 @@ export interface DataTableFormulaValue {
81
81
  }
82
82
  /** Everything a cell's value can be. `null` is the empty cell. */
83
83
  export type CellValue = null | number | string | boolean | Date | ErrorValue | FormulaValue | SharedFormulaValue | DataTableFormulaValue | RichTextValue | HyperlinkValue;
84
+ /**
85
+ * Whether a value is an in-cell error ({@link ErrorValue}). The narrowing counterpart of
86
+ * `detectValueType(value) === ValueType.Error`: use this one when the branch goes on to read
87
+ * `.error`, and {@link detectValueType} when it dispatches over all nine kinds at once.
88
+ */
84
89
  export declare function isErrorValue(value: CellValue): value is ErrorValue;
90
+ /**
91
+ * Whether a value is a cell's own formula ({@link FormulaValue}) — a master, or a formula
92
+ * belonging to no shared group. A shared-formula clone is **not** one of these; see
93
+ * {@link isSharedFormulaValue}. Both report as `ValueType.Formula`, so a caller that means "any
94
+ * formula-shaped cell" wants {@link detectValueType}, not this.
95
+ */
85
96
  export declare function isFormulaValue(value: CellValue): value is FormulaValue;
97
+ /** Whether a value is a clone participating in a shared formula ({@link SharedFormulaValue}). */
86
98
  export declare function isSharedFormulaValue(value: CellValue): value is SharedFormulaValue;
99
+ /** Whether a value is a What-If-Analysis data-table formula ({@link DataTableFormulaValue}). */
87
100
  export declare function isDataTableFormulaValue(value: CellValue): value is DataTableFormulaValue;
101
+ /**
102
+ * Whether a value is composed of formatted runs ({@link RichTextValue}). This is the test to
103
+ * make before {@link richTextToPlain}, which accepts nothing else.
104
+ */
88
105
  export declare function isRichTextValue(value: CellValue): value is RichTextValue;
106
+ /**
107
+ * Whether a value is a hyperlink ({@link HyperlinkValue}). Note that its `text` is itself either
108
+ * a string or a {@link RichTextValue}, so reading the label out means one more narrowing.
109
+ */
89
110
  export declare function isHyperlinkValue(value: CellValue): value is HyperlinkValue;
90
111
  /**
91
112
  * Flatten a rich-text value to its plain text by concatenating every run's text in order. This is the
@@ -93,6 +114,25 @@ export declare function isHyperlinkValue(value: CellValue): value is HyperlinkVa
93
114
  * the string a rich cell reads as when its formatting is discarded.
94
115
  */
95
116
  export declare function richTextToPlain(value: RichTextValue): string;
117
+ /**
118
+ * The plain text of any cell value — total over {@link CellValue}, so a caller reading a sheet
119
+ * whose cells it did not write never has to switch on the union itself.
120
+ *
121
+ * This is the *value's* text, not the cell's *display* text: a number renders as JavaScript
122
+ * renders it, with no number format applied (`0.1 + 0.2` is `"0.30000000000000004"`, a currency
123
+ * cell has no currency sign), because the format lives on the style and this function is given
124
+ * only the value. What each kind yields:
125
+ *
126
+ * - the empty cell (`null`) and an invalid `Date` → `""`, the two ways a cell has no text
127
+ * - a boolean → `"TRUE"` / `"FALSE"`, Excel's own literals rather than JavaScript's
128
+ * - a `Date` → a full ISO-8601 timestamp
129
+ * - an error → its literal, e.g. `"#REF!"` — the same string the grid shows
130
+ * - rich text → every run concatenated ({@link richTextToPlain})
131
+ * - a hyperlink → its label, never its destination
132
+ * - any of the three formula kinds → the text of the *cached result*, and `""` when the cell
133
+ * carries no cached result: the formula source is not text the sheet ever displayed
134
+ */
135
+ export declare function cellValueToText(value: CellValue): string;
96
136
  /**
97
137
  * Classify a value into its observable {@link ValueType}. This is total over
98
138
  * {@link CellValue}: every legal value has exactly one type. A `Date` is a date even