@shbernal/ts-xlsx 1.1.0 → 1.2.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 (127) hide show
  1. package/README.md +21 -0
  2. package/dist/core/address.js +0 -33
  3. package/dist/core/autofilter.js +0 -9
  4. package/dist/core/cell.js +0 -131
  5. package/dist/core/color-resolution.js +0 -60
  6. package/dist/core/column.js +0 -60
  7. package/dist/core/comment-thread.js +0 -45
  8. package/dist/core/conditional-formatting-overlay.js +0 -11
  9. package/dist/core/conditional-formatting.js +0 -10
  10. package/dist/core/containers.js +0 -15
  11. package/dist/core/data-validation-overlay.js +0 -25
  12. package/dist/core/data-validation.js +0 -7
  13. package/dist/core/date.js +0 -39
  14. package/dist/core/formula.js +0 -117
  15. package/dist/core/grid-edits.js +0 -38
  16. package/dist/core/image.js +0 -25
  17. package/dist/core/internal.js +0 -27
  18. package/dist/core/limits.js +0 -45
  19. package/dist/core/merge.d.ts +18 -0
  20. package/dist/core/merge.js +22 -7
  21. package/dist/core/modern-functions.js +0 -17
  22. package/dist/core/page-setup.js +0 -5
  23. package/dist/core/pivot-table.d.ts +1 -1
  24. package/dist/core/pivot-table.js +0 -48
  25. package/dist/core/preserved.js +0 -4
  26. package/dist/core/protection.js +0 -26
  27. package/dist/core/range.js +0 -112
  28. package/dist/core/row-input.d.ts +24 -0
  29. package/dist/core/row-input.js +29 -0
  30. package/dist/core/row.js +0 -66
  31. package/dist/core/style.js +0 -26
  32. package/dist/core/table-style.js +0 -28
  33. package/dist/core/table.js +0 -97
  34. package/dist/core/text-metrics.js +0 -40
  35. package/dist/core/theme.js +0 -85
  36. package/dist/core/value.js +0 -94
  37. package/dist/core/workbook-protection.js +0 -13
  38. package/dist/core/workbook.js +0 -380
  39. package/dist/core/worksheet-model.js +2 -23
  40. package/dist/core/worksheet.js +7 -506
  41. package/dist/customui/errors.js +0 -10
  42. package/dist/customui/index.js +0 -5
  43. package/dist/customui/ribbon.js +0 -43
  44. package/dist/entries/core.js +0 -6
  45. package/dist/entries/csv.js +0 -4
  46. package/dist/entries/customui.js +0 -2
  47. package/dist/entries/errors.js +0 -13
  48. package/dist/entries/vba.js +0 -5
  49. package/dist/entries/xlsb.js +0 -6
  50. package/dist/entries/xlsx.js +0 -13
  51. package/dist/errors.d.ts +14 -0
  52. package/dist/errors.js +5 -56
  53. package/dist/index.js +0 -12
  54. package/dist/io/csv/read.js +0 -18
  55. package/dist/io/csv/write.js +0 -18
  56. package/dist/io/opc/errors.js +0 -30
  57. package/dist/io/opc/inflate.js +0 -35
  58. package/dist/io/opc/namespaces.js +0 -15
  59. package/dist/io/opc/part-paths.js +0 -11
  60. package/dist/io/opc/read-opc.js +0 -31
  61. package/dist/io/opc/read-options.js +0 -6
  62. package/dist/io/opc/rels.js +0 -13
  63. package/dist/io/opc/sniff-format.js +0 -50
  64. package/dist/io/style/xf-style.js +0 -31
  65. package/dist/io/xlsb/errors.js +0 -10
  66. package/dist/io/xlsb/formula.js +1 -99
  67. package/dist/io/xlsb/primitives.js +1 -83
  68. package/dist/io/xlsb/ptg-functions.js +0 -34
  69. package/dist/io/xlsb/read-shared-strings.js +0 -7
  70. package/dist/io/xlsb/read-styles.js +3 -66
  71. package/dist/io/xlsb/read-worksheet.js +4 -87
  72. package/dist/io/xlsb/read.js +2 -60
  73. package/dist/io/xlsb/record-stream.js +0 -28
  74. package/dist/io/xlsb/record-types.js +0 -19
  75. package/dist/io/xlsx/cell-accumulator.js +0 -32
  76. package/dist/io/xlsx/cell-value.js +0 -29
  77. package/dist/io/xlsx/color-xml.d.ts +6 -0
  78. package/dist/io/xlsx/color-xml.js +42 -0
  79. package/dist/io/xlsx/comments.js +0 -84
  80. package/dist/io/xlsx/conditional-formatting.d.ts +1 -1
  81. package/dist/io/xlsx/conditional-formatting.js +1 -97
  82. package/dist/io/xlsx/data-validation.js +0 -56
  83. package/dist/io/xlsx/edit-vba.js +0 -54
  84. package/dist/io/xlsx/errors.js +0 -12
  85. package/dist/io/xlsx/hyperlinks.js +0 -40
  86. package/dist/io/xlsx/images.js +0 -32
  87. package/dist/io/xlsx/namespaces.js +0 -42
  88. package/dist/io/xlsx/package-plan.js +0 -47
  89. package/dist/io/xlsx/pivot.js +0 -22
  90. package/dist/io/xlsx/{pivot-read.js → read-pivot.js} +0 -30
  91. package/dist/io/xlsx/read-rows.js +1 -83
  92. package/dist/io/xlsx/{shared-strings-read.js → read-shared-strings.js} +0 -11
  93. package/dist/io/xlsx/read-styles.d.ts +25 -1
  94. package/dist/io/xlsx/read-styles.js +61 -108
  95. package/dist/io/xlsx/read-worksheet.js +1 -88
  96. package/dist/io/xlsx/read.js +3 -211
  97. package/dist/io/xlsx/relationships.js +0 -11
  98. package/dist/io/xlsx/rich-runs.js +0 -16
  99. package/dist/io/xlsx/rich-text.js +0 -12
  100. package/dist/io/xlsx/shared-formulas.js +0 -7
  101. package/dist/io/xlsx/shared-strings.js +0 -21
  102. package/dist/io/xlsx/sheet-properties.js +1 -64
  103. package/dist/io/xlsx/styles.d.ts +1 -29
  104. package/dist/io/xlsx/styles.js +2 -366
  105. package/dist/io/xlsx/tables.js +5 -52
  106. package/dist/io/xlsx/threaded-comments.js +0 -103
  107. package/dist/io/xlsx/workbook-xml.js +0 -94
  108. package/dist/io/xlsx/worksheet-xml.js +0 -174
  109. package/dist/io/xlsx/write-stream.js +0 -137
  110. package/dist/io/xlsx/write.js +1 -134
  111. package/dist/io/xlsx/x14-ext.js +0 -8
  112. package/dist/vba/cfb-writer.js +12 -59
  113. package/dist/vba/cfb.js +2 -32
  114. package/dist/vba/codepage.js +1 -23
  115. package/dist/vba/errors.js +0 -15
  116. package/dist/vba/index.js +0 -10
  117. package/dist/vba/ms-ovba.js +3 -46
  118. package/dist/vba/project-editor.js +4 -89
  119. package/dist/vba/project.js +2 -34
  120. package/dist/vba/vba-encoding.js +1 -17
  121. package/dist/xml/errors.js +0 -10
  122. package/dist/xml/xml-read.js +3 -82
  123. package/dist/xml/xml.js +0 -39
  124. package/package.json +8 -9
  125. package/skills/ts-xlsx-upstream/SKILL.md +178 -0
  126. /package/dist/io/xlsx/{pivot-read.d.ts → read-pivot.d.ts} +0 -0
  127. /package/dist/io/xlsx/{shared-strings-read.d.ts → read-shared-strings.d.ts} +0 -0
@@ -1,47 +1,2 @@
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
1
  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
2
  export const MAX_COLUMN_WIDTH = 255;
@@ -1,3 +1,4 @@
1
+ import type { Cell } from './cell.ts';
1
2
  /** A merged region as inclusive 1-based grid bounds. */
2
3
  export interface MergeRect {
3
4
  readonly top: number;
@@ -7,6 +8,23 @@ export interface MergeRect {
7
8
  }
8
9
  /** Whether two inclusive grid rectangles share at least one cell. */
9
10
  export declare function rectsOverlap(a: MergeRect, b: MergeRect): boolean;
11
+ /**
12
+ * Resolve a position to the master (top-left) of the merged region covering it, or to itself when no
13
+ * region does. First covering region wins; `Worksheet.mergeCells` rejects overlaps, so at most one
14
+ * region ever applies. Only fully-bounded rects participate — an unbounded whole-row/column merge
15
+ * carries no rect and so resolves nothing.
16
+ */
17
+ export declare function masterOf(rects: readonly MergeRect[], row: number, col: number): {
18
+ row: number;
19
+ col: number;
20
+ };
21
+ /**
22
+ * Drop any value already sitting in a merge's covered non-anchor cells, keeping only the top-left
23
+ * anchor — the collapse Excel performs on merge. A leftover covered value would serialise as a
24
+ * populated `<c>` under the range's `<mergeCell>` ref, the geometry that trips Excel's repair prompt.
25
+ * Styles are untouched: a border spanning the merged region rides the covered cells.
26
+ */
27
+ export declare function clearCoveredValues(rows: Map<number, Map<number, Cell>>, rect: MergeRect): void;
10
28
  /** Decode an OOXML `sqref` (one or more space-separated ranges) into containment rectangles. A whole
11
29
  * column or row leaves one axis unbounded, so its missing edges open to `Infinity` rather than
12
30
  * clamping — a cell anywhere down the column still resolves inside it. */
@@ -1,14 +1,29 @@
1
- // Inclusive grid rectangles and the geometry a worksheet uses to reason about merged regions and the
2
- // `sqref` ranges that overlays (data validations, conditional formats) apply to: overlap detection and
3
- // decoding an OOXML `sqref` into containment rectangles.
4
1
  import { decodeRange } from './address.js';
5
- /** Whether two inclusive grid rectangles share at least one cell. */
6
2
  export function rectsOverlap(a, b) {
7
3
  return a.left <= b.right && b.left <= a.right && a.top <= b.bottom && b.top <= a.bottom;
8
4
  }
9
- /** Decode an OOXML `sqref` (one or more space-separated ranges) into containment rectangles. A whole
10
- * column or row leaves one axis unbounded, so its missing edges open to `Infinity` rather than
11
- * clamping a cell anywhere down the column still resolves inside it. */
5
+ export function masterOf(rects, row, col) {
6
+ for (const rect of rects) {
7
+ if (row >= rect.top && row <= rect.bottom && col >= rect.left && col <= rect.right) {
8
+ return { row: rect.top, col: rect.left };
9
+ }
10
+ }
11
+ return { row, col };
12
+ }
13
+ export function clearCoveredValues(rows, rect) {
14
+ for (let row = rect.top; row <= rect.bottom; row++) {
15
+ const cols = rows.get(row);
16
+ if (cols === undefined)
17
+ continue;
18
+ for (let col = rect.left; col <= rect.right; col++) {
19
+ if (row === rect.top && col === rect.left)
20
+ continue;
21
+ const covered = cols.get(col);
22
+ if (covered !== undefined)
23
+ covered.value = null;
24
+ }
25
+ }
26
+ }
12
27
  export function decodeSqrefRects(sqref) {
13
28
  const rects = [];
14
29
  for (const part of sqref.split(/\s+/)) {
@@ -1,11 +1,4 @@
1
- // The post-2007 functions Excel persists with an `_xlfn.` prefix, keyed by their uppercased name.
2
- // OOXML froze its function grammar around Excel 2007; everything Microsoft added since is unknown to
3
- // older readers under its bare name, so the writer stores it prefixed and the reader strips the prefix
4
- // back — the mangling in `formula.ts` is the single place that applies and removes it. That mangler
5
- // treats '.' as part of a function name, so both the plain modern functions and the dotted 2010
6
- // statistical rename family below are matched as whole names and prefixed.
7
1
  export const MODERN_FUNCTIONS = new Set([
8
- // Dynamic arrays (Excel 365)
9
2
  'FILTER',
10
3
  'SORT',
11
4
  'SORTBY',
@@ -14,7 +7,6 @@ export const MODERN_FUNCTIONS = new Set([
14
7
  'RANDARRAY',
15
8
  'XLOOKUP',
16
9
  'XMATCH',
17
- // LAMBDA and its helpers
18
10
  'LAMBDA',
19
11
  'LET',
20
12
  'BYROW',
@@ -24,7 +16,6 @@ export const MODERN_FUNCTIONS = new Set([
24
16
  'REDUCE',
25
17
  'SCAN',
26
18
  'ISOMITTED',
27
- // Array shaping (Excel 365)
28
19
  'VSTACK',
29
20
  'HSTACK',
30
21
  'TOROW',
@@ -36,7 +27,6 @@ export const MODERN_FUNCTIONS = new Set([
36
27
  'EXPAND',
37
28
  'CHOOSEROWS',
38
29
  'CHOOSECOLS',
39
- // Text (Excel 2019 / 365)
40
30
  'TEXTJOIN',
41
31
  'CONCAT',
42
32
  'TEXTBEFORE',
@@ -44,14 +34,10 @@ export const MODERN_FUNCTIONS = new Set([
44
34
  'TEXTSPLIT',
45
35
  'ARRAYTOTEXT',
46
36
  'VALUETOTEXT',
47
- // Logical and conditional aggregation (Excel 2016 / 2019)
48
37
  'IFS',
49
38
  'SWITCH',
50
39
  'MAXIFS',
51
40
  'MINIFS',
52
- // Other bare-name functions added after the frozen grammar (Excel 2010 / 2013) — trigonometric,
53
- // bitwise, engineering, information, and math/financial additions. Their names carry no '.', so
54
- // they need no tokenizer work; they simply have to be recognised as modern to earn the prefix.
55
41
  'AGGREGATE',
56
42
  'ACOT',
57
43
  'ACOTH',
@@ -99,9 +85,6 @@ export const MODERN_FUNCTIONS = new Set([
99
85
  'XOR',
100
86
  'PDURATION',
101
87
  'RRI',
102
- // The Excel 2010 statistical-consistency rename family and the handful of other post-2007
103
- // functions whose canonical names contain a '.'. They carry the same `_xlfn.` prefix; the whole
104
- // dotted name is stored, e.g. `_xlfn.NORM.DIST`, `_xlfn.T.DIST.2T`.
105
88
  'BETA.DIST',
106
89
  'BETA.INV',
107
90
  'BINOM.DIST',
@@ -1,6 +1 @@
1
- // The print/page-layout data shapes a worksheet carries: how it scales and orients on paper, which
2
- // print toggles are set, where manual page breaks fall, the margins, and the header/footer text. Each
3
- // is a pure data shape mapping onto an OOXML print element (`<pageSetup>`, `<printOptions>`, `<brk>`,
4
- // `<pageMargins>`, `<headerFooter>`); the model stores only what an author or source file set, so an
5
- // unset field is omitted and a round-trip never fabricates one.
6
1
  export {};
@@ -29,7 +29,7 @@ export interface ParsedPivotSource {
29
29
  readonly ref: string;
30
30
  }
31
31
  /** The semantic model reconstructed from a loaded pivot's `pivotTableDefinition` and its
32
- * `pivotCacheDefinition` (see `io/xlsx/pivot-read.ts`). Field roles are indices into {@link fields};
32
+ * `pivotCacheDefinition` (see `io/xlsx/read-pivot.ts`). Field roles are indices into {@link fields};
33
33
  * {@link metric} is the aggregation the value field applies. This mirrors the authoring model's shape
34
34
  * without requiring the source sheet it was built from, so a pivot loaded from a package is
35
35
  * inspectable data rather than an opaque preserved blob. It is a read-only view: the writer emits a
@@ -1,14 +1,3 @@
1
- // A pivot table authored from a model.
2
- //
3
- // A pivot summarises a source range: its distinct field values become row/column axes and a value
4
- // field is aggregated across them. OOXML splits that into three parts — a `pivotCacheDefinition`
5
- // (the field catalogue), a `pivotCacheRecords` (a copy of the source rows, with axis-field cells
6
- // swapped for indices into the catalogue), and a `pivotTableDefinition` (the layout on the
7
- // destination sheet). This module owns the *semantic* computation of all three; the OOXML rendering
8
- // lives in `io/xlsx/pivot.ts`.
9
- //
10
- // The source data is captured when the pivot is added — the model reads the source sheet's cells
11
- // once, here, so the pivot is a stable snapshot independent of later edits to the source.
12
1
  import { AuthoringError, InternalError } from '../errors.js';
13
2
  import { encodeAddress } from './address.js';
14
3
  import { isErrorValue, isFormulaValue, isHyperlinkValue, isRichTextValue, isSharedFormulaValue, richTextToPlain, } from './value.js';
@@ -25,36 +14,21 @@ const PIVOT_METRICS = new Set([
25
14
  'var',
26
15
  'varp',
27
16
  ]);
28
- /** Narrow a raw `subtotal` attribute (or any string) to a known {@link PivotMetric}. */
29
17
  export function isPivotMetric(value) {
30
18
  return PIVOT_METRICS.has(value);
31
19
  }
32
- /** Map an OOXML `<dataField subtotal="…">` value back to its metric. The attribute is absent for
33
- * `sum` (Excel's implicit default), so `undefined` reads as `sum`; an unrecognised value also reads
34
- * as `sum` rather than throwing, because reconstructing an existing file is a lenient operation —
35
- * the strict rejection of unknown metrics belongs on the authoring path, not the read path. */
36
20
  export function pivotMetricFromSubtotal(subtotal) {
37
21
  if (subtotal === undefined)
38
22
  return 'sum';
39
23
  return isPivotMetric(subtotal) ? subtotal : 'sum';
40
24
  }
41
25
  const BLANK = { kind: 'blank' };
42
- /**
43
- * A pivot table built over a source sheet's data. Construction reads the source once and computes
44
- * the full cache (fields + records) and the axis-field wiring the renderer needs; nothing here
45
- * touches XML.
46
- *
47
- * Supported shape: exactly one value field aggregated by `sum`, at least one row field and one
48
- * column field. An unsupported request throws at authoring time rather than emitting a corrupt file.
49
- */
50
26
  export class PivotTable {
51
27
  metric;
52
28
  sourceSheetName;
53
- /** The `A1:C4` source range: the header row through the last data row, across the field columns. */
54
29
  sourceRef;
55
30
  cacheFields;
56
31
  records;
57
- /** Indices into {@link cacheFields} of the row-axis, column-axis, and value fields. */
58
32
  rowFields;
59
33
  columnFields;
60
34
  valueField;
@@ -70,7 +44,6 @@ export class PivotTable {
70
44
  if (columnCount < 1 || lastRow < 2) {
71
45
  throw new AuthoringError('a pivot source needs a header row and at least one data row');
72
46
  }
73
- // Every non-blank header cell in row 1 defines a field, in ascending column order.
74
47
  const fields = [];
75
48
  for (let col = 1; col <= columnCount; col++) {
76
49
  const name = textOf(scalarOf(source.getCell(encodeAddress(col, 1)).value));
@@ -98,7 +71,6 @@ export class PivotTable {
98
71
  this.columnFields = options.columns.map((name) => resolve('column', name));
99
72
  this.valueField = resolve('value', valueName);
100
73
  const axisFields = new Set([...this.rowFields, ...this.columnFields]);
101
- // fields is non-empty (guarded above); the source span runs from its first to its last column.
102
74
  const firstField = fields[0];
103
75
  const lastField = fields[fields.length - 1];
104
76
  if (firstField === undefined || lastField === undefined) {
@@ -106,8 +78,6 @@ export class PivotTable {
106
78
  }
107
79
  this.sourceSheetName = source.name;
108
80
  this.sourceRef = `${encodeAddress(firstField.col, 1)}:${encodeAddress(lastField.col, lastRow)}`;
109
- // Read the source body once, field by field, so the same scan feeds both the shared-items
110
- // catalogues and the records that reference them.
111
81
  const dataRowCount = lastRow - 1;
112
82
  const columnScalars = fields.map((field) => {
113
83
  const scalars = [];
@@ -146,7 +116,6 @@ export class PivotTable {
146
116
  const catalogue = catalogues[fieldIndex];
147
117
  if (!catalogue)
148
118
  return scalar;
149
- // Every scalar was catalogued in the shared-items pass above, so this always hits.
150
119
  const index = catalogue.get(itemKey(scalar));
151
120
  if (index === undefined)
152
121
  throw new InternalError('pivot record references an uncatalogued item');
@@ -155,7 +124,6 @@ export class PivotTable {
155
124
  }
156
125
  this.records = records;
157
126
  }
158
- /** The value field's header name, used to label the aggregated data column ("Sum of Amount"). */
159
127
  get valueFieldName() {
160
128
  const field = this.cacheFields[this.valueField];
161
129
  if (field === undefined) {
@@ -165,9 +133,6 @@ export class PivotTable {
165
133
  return field.name;
166
134
  }
167
135
  }
168
- // columnScalars[fieldIndex] is always present: it was built by mapping the same `fields` array this
169
- // index is drawn from. Centralised here so the invariant is asserted once rather than cast away at
170
- // each of its two call sites.
171
136
  function scalarsForField(columnScalars, fieldIndex) {
172
137
  const scalars = columnScalars[fieldIndex];
173
138
  if (scalars === undefined) {
@@ -175,7 +140,6 @@ function scalarsForField(columnScalars, fieldIndex) {
175
140
  }
176
141
  return scalars;
177
142
  }
178
- /** A stable dedup key for a shared item: kind-tagged so the number `1` and the string `"1"` differ. */
179
143
  function itemKey(item) {
180
144
  switch (item.kind) {
181
145
  case 'string':
@@ -186,8 +150,6 @@ function itemKey(item) {
186
150
  return 'b';
187
151
  }
188
152
  }
189
- /** The numeric summary for an inline field, or null when any present value is non-numeric (a string
190
- * present means the field is not a pure numeric column; blanks alone do not disqualify it). */
191
153
  function numericSummary(scalars) {
192
154
  let min = Infinity;
193
155
  let max = -Infinity;
@@ -208,19 +170,9 @@ function numericSummary(scalars) {
208
170
  }
209
171
  return sawNumber ? { allInteger, min, max } : null;
210
172
  }
211
- /** The string form of a shared item, used for header names (a blank header contributes no field). */
212
173
  function textOf(item) {
213
174
  return item.kind === 'blank' ? '' : String(item.value);
214
175
  }
215
- /**
216
- * Reduce any cell value to the scalar a pivot cache can hold: a number, a string, or a blank. Only
217
- * finite numbers stay numeric (a NaN would corrupt the cache); every other kind is flattened to its
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.
223
- */
224
176
  function scalarOf(value) {
225
177
  if (value === null)
226
178
  return BLANK;
@@ -1,5 +1 @@
1
- // Package content the model does not interpret, captured verbatim so a round-trip re-emits it intact.
2
- // A preserved reference names a worksheet- or workbook-level relationship (a chart, vector drawing,
3
- // slicer, pivot table) the model does not model; its transitive closure of parts is held as raw bytes
4
- // with their content types and rewired relationships, and re-emitted unchanged on write.
5
1
  export {};
@@ -1,22 +1,4 @@
1
- // Worksheet-level protection: the `<sheetProtection>` state that makes the per-cell
2
- // `locked`/`hidden` flags enforceable. A cell's protection does nothing until the sheet
3
- // itself is protected; this module models that sheet-level switch and the optional
4
- // password credential that guards lifting it.
5
- //
6
- // The option surface is stated in the AUTHOR's terms — each flag answers "may a user do
7
- // this while the sheet is protected?" (`sort: true` = sorting stays available). OOXML
8
- // encodes the inverse ("1" LOCKS an operation, "0"/omission PERMITS it) and its per-
9
- // attribute defaults differ; that encoding table is {@link SHEET_PROTECTION_FLAGS} below,
10
- // shared by the writer and reader, while the translation that consumes it lives in the io layer.
11
1
  import { createHash, randomBytes } from 'node:crypto';
12
- /**
13
- * The OOXML encoding table for the protection flags: each `<sheetProtection>` attribute paired
14
- * with whether that operation is *forbidden by default* once a sheet is protected. Both directions
15
- * key off this one list — the writer turns an author allow-flag into an attribute (omitting values
16
- * equal to the default), the reader turns an attribute back into an allow-flag — so serialization
17
- * and deserialization can never fall out of step. Most editing operations default to forbidden
18
- * under protection; selecting cells and the object/scenario operations default to permitted.
19
- */
20
2
  export const SHEET_PROTECTION_FLAGS = [
21
3
  { key: 'formatCells', defaultForbidden: true },
22
4
  { key: 'formatColumns', defaultForbidden: true },
@@ -34,18 +16,10 @@ export const SHEET_PROTECTION_FLAGS = [
34
16
  { key: 'selectLockedCells', defaultForbidden: false },
35
17
  { key: 'selectUnlockedCells', defaultForbidden: false },
36
18
  ];
37
- // OOXML's agile hashing (ECMA-376 / MS-OFFCRYPTO): the password is UTF-16LE, prefixed with
38
- // the salt for the first hash, then re-hashed `spinCount` times with a little-endian uint32
39
- // iteration counter mixed in. SHA-512 is the modern choice Excel writes.
40
19
  const ALGORITHM_NAME = 'SHA-512';
41
20
  const HASH = 'sha512';
42
21
  const DEFAULT_SPIN_COUNT = 100000;
43
22
  const SALT_BYTES = 16;
44
- /**
45
- * Derive a fresh {@link SheetProtectionCredential} for a password. Each call generates a new
46
- * random salt, so protecting two sheets with the same password yields different credentials —
47
- * the salt is real randomness, not a stub.
48
- */
49
23
  export function deriveCredential(password, spinCount = DEFAULT_SPIN_COUNT) {
50
24
  const salt = randomBytes(SALT_BYTES);
51
25
  const secret = Buffer.from(password, 'utf16le');
@@ -1,44 +1,12 @@
1
- // A handle on a rectangular block of cells, reached by an A1 range reference or by its corners.
2
- //
3
- // The third axis handle, after `Row` and `Column`, and the same contract as both: a live view over
4
- // the worksheet's stores, bounds fixed at construction, nothing materialised on read. See
5
- // `core/row.ts` for why these are handles rather than records.
6
- //
7
- // **The rectangle is bounded, always.** `A:A` and `1:1` are legal range references and are rejected
8
- // here, because a whole-axis "range" is not this shape of thing: styling one means stamping a
9
- // million cells, whereas OOXML expresses it natively as a single `<col style>` or `<row s>` — which
10
- // is what `getColumn`/`getRow` already write, in constant space. Refusing them is what lets a range
11
- // materialise its cells eagerly without a cost cliff hiding behind an innocuous-looking call.
12
1
  import { decodeRange, encodeAddress, MAX_COLUMN, MAX_ROW } from './address.js';
13
2
  import { applyCellStyle } from './cell.js';
14
3
  import { CELL_STYLE_FACETS, } from './style.js';
15
- /**
16
- * A rectangular block of a worksheet's cells: `sheet.getRange('B2:D5')`.
17
- *
18
- * Cheap and stateless — constructing one creates no cells and does not extend the used range.
19
- * {@link addresses} walks the block without materialising anything; {@link cells} reports only what
20
- * already exists.
21
- *
22
- * Bounds are **inclusive first/last**, never start-and-count. That is the convention every
23
- * range-shaped accessor in this library follows, so the three axes cannot disagree about what a
24
- * pair of numbers means.
25
- */
26
4
  export class Range {
27
5
  #sheet;
28
- /** 1-based row of the block's top edge. Fixed for this handle's lifetime. */
29
6
  top;
30
- /** 1-based column of the block's left edge. Fixed for this handle's lifetime. */
31
7
  left;
32
- /** 1-based row of the block's bottom edge, inclusive. */
33
8
  bottom;
34
- /** 1-based column of the block's right edge, inclusive. */
35
9
  right;
36
- /**
37
- * Build a handle from inclusive corners, in any order — `(5, 4, 2, 2)` and `(2, 2, 5, 4)` name the
38
- * same block, exactly as `D5:B2` and `B2:D5` do. Prefer {@link Worksheet.getRange}.
39
- *
40
- * @throws {RangeError} if a corner is not a positive integer or falls outside the sheet's bounds.
41
- */
42
10
  constructor(sheet, top, left, bottom, right) {
43
11
  checkBound('row', top, MAX_ROW);
44
12
  checkBound('row', bottom, MAX_ROW);
@@ -50,35 +18,24 @@ export class Range {
50
18
  this.left = Math.min(left, right);
51
19
  this.right = Math.max(left, right);
52
20
  }
53
- /** The worksheet this block belongs to. */
54
21
  get sheet() {
55
22
  return this.#sheet;
56
23
  }
57
- /** Canonical `tl:br` A1 form — `"B2:D5"`. A one-cell block still reads as `"B2:B2"`. */
58
24
  get address() {
59
25
  return `${encodeAddress(this.left, this.top)}:${encodeAddress(this.right, this.bottom)}`;
60
26
  }
61
- /** Rows spanned, inclusive of both edges. */
62
27
  get rowCount() {
63
28
  return this.bottom - this.top + 1;
64
29
  }
65
- /** Columns spanned, inclusive of both edges. */
66
30
  get columnCount() {
67
31
  return this.right - this.left + 1;
68
32
  }
69
- /** Cells the block covers — `rowCount * columnCount`, whether or not they exist yet. */
70
33
  get cellCount() {
71
34
  return this.rowCount * this.columnCount;
72
35
  }
73
- /** Whether a 1-based position falls inside the block. */
74
36
  contains(row, col) {
75
37
  return row >= this.top && row <= this.bottom && col >= this.left && col <= this.right;
76
38
  }
77
- /**
78
- * Every address the block covers, row-major (`B2`, `C2`, `D2`, `B3`, …). Materialises nothing, so
79
- * this is the cheap way to walk a large block — and, being a generator, it can be abandoned
80
- * part-way without having built the whole list.
81
- */
82
39
  *addresses() {
83
40
  for (let row = this.top; row <= this.bottom; row++) {
84
41
  for (let col = this.left; col <= this.right; col++) {
@@ -86,11 +43,6 @@ export class Range {
86
43
  }
87
44
  }
88
45
  }
89
- /**
90
- * The block's **materialised** cells, row-major. Sparse: a position nothing has ever written to is
91
- * simply absent, which is what distinguishes "never written" from a cell holding `null`. Reading
92
- * this creates nothing — mirroring {@link Column.cells}.
93
- */
94
46
  get cells() {
95
47
  const cells = [];
96
48
  for (let row = this.top; row <= this.bottom; row++) {
@@ -101,25 +53,6 @@ export class Range {
101
53
  }
102
54
  return cells;
103
55
  }
104
- /**
105
- * The block's style, facet by facet — the counterpart of {@link Cell.style} over a rectangle, with
106
- * the same semantics in both directions.
107
- *
108
- * **Reading** reports a facet only when *every* position in the block carries a structurally
109
- * identical one, and `undefined` when they differ or any position is still empty. A block styled
110
- * through this handle therefore reads back what was written; a block whose cells disagree says so
111
- * rather than picking a corner's answer and passing it off as the whole.
112
- *
113
- * **Writing** lays each facet the payload names onto every cell, leaving facets it omits untouched
114
- * — exactly what `cell.style = {...}` does, so this composes with prior styling instead of clearing
115
- * it. Use {@link clearStyle} first for a wholesale replace.
116
- *
117
- * Writing **materialises** every position in the block, because a styled-but-valueless cell is the
118
- * only way an empty cell renders with a fill: skipping the holes would leave gaps in a header band.
119
- * The cost is bounded by construction — a range is always a bounded rectangle, and whole-axis
120
- * styling belongs to {@link Worksheet.getColumn}/{@link Worksheet.getRow} instead. {@link cellCount}
121
- * is the exact number of cells a write will create.
122
- */
123
56
  get style() {
124
57
  const style = {};
125
58
  for (const facet of CELL_STYLE_FACETS)
@@ -130,83 +63,61 @@ export class Range {
130
63
  for (const cell of this.#materialise())
131
64
  applyCellStyle(cell, style);
132
65
  }
133
- /**
134
- * Strip every style facet from every cell in the block, leaving values untouched. Assigning
135
- * {@link style} composes, so this is how a wholesale replace is said: `clearStyle()` then assign.
136
- * Materialises nothing — a cell that does not exist carries no style to clear.
137
- */
138
66
  clearStyle() {
139
67
  for (const cell of this.cells) {
140
68
  for (const facet of CELL_STYLE_FACETS)
141
69
  setFacet(cell, facet, undefined);
142
70
  }
143
71
  }
144
- /** Fill applied to every cell in the block; `undefined` when they do not all agree. */
145
72
  get fill() {
146
73
  return this.#sharedFacet('fill');
147
74
  }
148
75
  set fill(fill) {
149
76
  this.#writeFacet('fill', fill);
150
77
  }
151
- /** Number format applied to every cell in the block; `undefined` when they do not all agree. */
152
78
  get numFmt() {
153
79
  return this.#sharedFacet('numFmt');
154
80
  }
155
81
  set numFmt(numFmt) {
156
82
  this.#writeFacet('numFmt', numFmt);
157
83
  }
158
- /** Font applied to every cell in the block; `undefined` when they do not all agree. */
159
84
  get font() {
160
85
  return this.#sharedFacet('font');
161
86
  }
162
87
  set font(font) {
163
88
  this.#writeFacet('font', font);
164
89
  }
165
- /** Border applied to every cell in the block; `undefined` when they do not all agree. */
166
90
  get border() {
167
91
  return this.#sharedFacet('border');
168
92
  }
169
93
  set border(border) {
170
94
  this.#writeFacet('border', border);
171
95
  }
172
- /** Alignment applied to every cell in the block; `undefined` when they do not all agree. */
173
96
  get alignment() {
174
97
  return this.#sharedFacet('alignment');
175
98
  }
176
99
  set alignment(alignment) {
177
100
  this.#writeFacet('alignment', alignment);
178
101
  }
179
- /** Protection flags applied to every cell in the block; `undefined` when they do not all agree. */
180
102
  get protection() {
181
103
  return this.#sharedFacet('protection');
182
104
  }
183
105
  set protection(protection) {
184
106
  this.#writeFacet('protection', protection);
185
107
  }
186
- // Every cell in the block, created where it does not exist yet. An address covered by a merge
187
- // resolves to that region's master, so a block overlapping a merge restyles the master rather than
188
- // stranding a style on a covered cell the serializer would then have to drop.
189
108
  #materialise() {
190
109
  return [...this.addresses()].map((address) => this.#sheet.getCell(address));
191
110
  }
192
- // Assigning a facet replaces that facet on every cell — the block-wide reading of `cell.fill = x`.
193
- // Clearing one (`undefined`) touches only the cells that exist: there is nothing to clear on a hole,
194
- // and materialising the block to write nothing onto it would be pure cost.
195
111
  #writeFacet(facet, value) {
196
112
  const cells = value === undefined ? this.cells : this.#materialise();
197
113
  for (const cell of cells)
198
114
  setFacet(cell, facet, value);
199
115
  }
200
- // One facet of the block-wide style onto the record being assembled. Narrowed to a single key for
201
- // the same reason {@link setFacet} is — see there.
202
116
  #collectShared(target, facet) {
203
117
  const shared = this.#sharedFacet(facet);
204
118
  if (shared !== undefined)
205
119
  target[facet] = shared;
206
120
  }
207
- // A facet's value when every position in the block carries a structurally identical one, else
208
- // undefined. A hole counts as "no facet", so a partly-styled block is reported as disagreeing —
209
- // which it does, since the empty positions render unstyled.
210
121
  #sharedFacet(facet) {
211
122
  let first;
212
123
  let firstKey;
@@ -230,22 +141,10 @@ export class Range {
230
141
  return first;
231
142
  }
232
143
  }
233
- // Write one facet through a `Cell`'s own setter. Narrowed to a single key — rather than assigning
234
- // `cell[facet]` with `facet` still a union — for the same reason `style.ts`'s `copyFacet` is: the
235
- // compiler cannot correlate a union key with its value type across two object types, and a cast here
236
- // would be the one place a facet could be written to the wrong slot without anything noticing.
237
144
  function setFacet(cell, facet, value) {
238
145
  const target = cell;
239
146
  target[facet] = value;
240
147
  }
241
- // A canonical string for a facet value, so two structurally identical records compare equal whatever
242
- // order their keys were written in. Sound here and nowhere near a general deep-equal: every facet is
243
- // a plain data record of strings, numbers, booleans and nested records — no functions, no cycles, no
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.
249
148
  function facetKey(value) {
250
149
  if (value === undefined)
251
150
  return '\u0000';
@@ -261,22 +160,11 @@ function checkBound(axis, value, max) {
261
160
  throw new RangeError(`${axis} ${value} is out of bounds — the sheet ends at ${max}`);
262
161
  }
263
162
  }
264
- /**
265
- * Resolve an A1 range reference against a worksheet into a {@link Range}.
266
- *
267
- * A reference carrying a sheet prefix is accepted only when it names *this* sheet (case-insensitively,
268
- * as sheet lookup is everywhere else): a range handed out by `sheetA` cannot be a block of `sheetB`,
269
- * and quietly ignoring the prefix would let a pasted `Sheet2!B2:D5` style the wrong sheet.
270
- *
271
- * @throws {SyntaxError} if the reference is unparseable, names another sheet, or leaves an axis
272
- * unbounded (`A:A`, `1:1`).
273
- */
274
163
  export function rangeFrom(sheet, reference) {
275
164
  const { top, left, bottom, right, sheetName, dimensions } = decodeRange(reference);
276
165
  if (sheetName !== undefined && sheetName.toLowerCase() !== sheet.name.toLowerCase()) {
277
166
  throw new SyntaxError(`"${reference}" names worksheet "${sheetName}", not "${sheet.name}" — a range belongs to the sheet it came from`);
278
167
  }
279
- // An unbounded *row* axis is what `A:A` has: it names whole columns, every row of them.
280
168
  if (top === undefined || bottom === undefined) {
281
169
  throw new SyntaxError(`"${reference}" spans whole columns (${dimensions}) — style them through getColumn(n), which says the same thing in one attribute instead of ${MAX_ROW} cells`);
282
170
  }
@@ -0,0 +1,24 @@
1
+ import { Cell } from './cell.ts';
2
+ import type { CellValue } from './value.ts';
3
+ import type { ColumnProperties, RowInput } from './worksheet.ts';
4
+ /**
5
+ * Resolve a `RowInput` to the (1-based column, value) placements it names. A positional array maps
6
+ * each value to its column from A, skipping a hole or an explicit `undefined` so that column is left
7
+ * untouched; a keyed object maps each value under the column carrying the matching key.
8
+ *
9
+ * `Array.isArray`, not `instanceof Array`: a row built in another realm (a vm context, a browser
10
+ * iframe) is still an array but fails the identity check, and would then be walked as a keyed object —
11
+ * placing nothing.
12
+ */
13
+ export declare function rowPlacements(values: RowInput, columns: ReadonlyMap<number, ColumnProperties>): Array<[number, CellValue]>;
14
+ /**
15
+ * Build the detached cell row an insert introduces: a fresh cell per placement, positioned at `number`,
16
+ * keyed by column. The grid-edit machinery then splices this map into place.
17
+ */
18
+ export declare function buildRowCells(number: number, values: RowInput, columns: ReadonlyMap<number, ColumnProperties>): Map<number, Cell>;
19
+ /**
20
+ * The 1-based index of the column carrying `key` (see {@link ColumnProperties.key}).
21
+ *
22
+ * @throws {AuthoringError} if no column declares that key.
23
+ */
24
+ export declare function columnIndexByKey(columns: ReadonlyMap<number, ColumnProperties>, key: string): number;