@shbernal/ts-xlsx 1.0.3 → 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.
- package/README.md +21 -0
- package/dist/core/address.js +0 -33
- package/dist/core/autofilter.js +0 -9
- package/dist/core/cell.d.ts +10 -0
- package/dist/core/cell.js +4 -123
- package/dist/core/color-resolution.js +0 -60
- package/dist/core/column.d.ts +9 -1
- package/dist/core/column.js +0 -52
- package/dist/core/comment-thread.js +0 -45
- package/dist/core/conditional-formatting-overlay.js +0 -11
- package/dist/core/conditional-formatting.js +0 -10
- package/dist/core/containers.js +0 -15
- package/dist/core/data-validation-overlay.js +0 -25
- package/dist/core/data-validation.js +0 -7
- package/dist/core/date.js +0 -39
- package/dist/core/formula.js +0 -117
- package/dist/core/grid-edits.js +0 -38
- package/dist/core/image.js +0 -25
- package/dist/core/internal.js +0 -27
- package/dist/core/limits.d.ts +30 -0
- package/dist/core/limits.js +2 -0
- package/dist/core/merge.d.ts +18 -0
- package/dist/core/merge.js +22 -7
- package/dist/core/modern-functions.js +0 -17
- package/dist/core/page-setup.js +0 -5
- package/dist/core/pivot-table.d.ts +1 -1
- package/dist/core/pivot-table.js +0 -44
- package/dist/core/preserved.js +0 -4
- package/dist/core/protection.js +0 -26
- package/dist/core/range.js +0 -112
- package/dist/core/row-input.d.ts +24 -0
- package/dist/core/row-input.js +29 -0
- package/dist/core/row.d.ts +9 -1
- package/dist/core/row.js +0 -58
- package/dist/core/style.js +0 -26
- package/dist/core/table-style.js +0 -28
- package/dist/core/table.js +0 -97
- package/dist/core/text-metrics.d.ts +20 -0
- package/dist/core/text-metrics.js +10 -0
- package/dist/core/theme.js +0 -85
- package/dist/core/value.d.ts +40 -0
- package/dist/core/value.js +30 -48
- package/dist/core/workbook-protection.js +0 -13
- package/dist/core/workbook.d.ts +13 -0
- package/dist/core/workbook.js +11 -368
- package/dist/core/worksheet-model.js +2 -23
- package/dist/core/worksheet.d.ts +22 -0
- package/dist/core/worksheet.js +14 -485
- package/dist/customui/errors.js +0 -10
- package/dist/customui/index.js +0 -5
- package/dist/customui/ribbon.js +0 -43
- package/dist/entries/core.d.ts +3 -1
- package/dist/entries/core.js +3 -7
- package/dist/entries/csv.js +0 -4
- package/dist/entries/customui.js +0 -2
- package/dist/entries/errors.js +0 -13
- package/dist/entries/vba.js +0 -5
- package/dist/entries/xlsb.js +0 -6
- package/dist/entries/xlsx.js +0 -13
- package/dist/errors.d.ts +14 -0
- package/dist/errors.js +5 -56
- package/dist/index.js +0 -12
- package/dist/io/csv/read.js +0 -18
- package/dist/io/csv/write.js +5 -35
- package/dist/io/opc/errors.js +0 -30
- package/dist/io/opc/inflate.js +0 -35
- package/dist/io/opc/namespaces.js +0 -15
- package/dist/io/opc/part-paths.js +0 -11
- package/dist/io/opc/read-opc.js +0 -31
- package/dist/io/opc/read-options.js +0 -6
- package/dist/io/opc/rels.js +0 -13
- package/dist/io/opc/sniff-format.js +0 -50
- package/dist/io/style/xf-style.js +0 -31
- package/dist/io/xlsb/errors.js +0 -10
- package/dist/io/xlsb/formula.js +1 -99
- package/dist/io/xlsb/primitives.js +1 -83
- package/dist/io/xlsb/ptg-functions.js +0 -34
- package/dist/io/xlsb/read-shared-strings.js +0 -7
- package/dist/io/xlsb/read-styles.js +3 -66
- package/dist/io/xlsb/read-worksheet.js +4 -87
- package/dist/io/xlsb/read.js +2 -60
- package/dist/io/xlsb/record-stream.js +0 -28
- package/dist/io/xlsb/record-types.js +0 -19
- package/dist/io/xlsx/cell-accumulator.js +0 -32
- package/dist/io/xlsx/cell-value.js +0 -29
- package/dist/io/xlsx/color-xml.d.ts +6 -0
- package/dist/io/xlsx/color-xml.js +42 -0
- package/dist/io/xlsx/comments.js +0 -84
- package/dist/io/xlsx/conditional-formatting.d.ts +1 -1
- package/dist/io/xlsx/conditional-formatting.js +1 -97
- package/dist/io/xlsx/data-validation.js +0 -56
- package/dist/io/xlsx/edit-vba.js +0 -54
- package/dist/io/xlsx/errors.js +0 -12
- package/dist/io/xlsx/hyperlinks.js +0 -40
- package/dist/io/xlsx/images.js +0 -32
- package/dist/io/xlsx/namespaces.js +0 -42
- package/dist/io/xlsx/package-plan.js +0 -47
- package/dist/io/xlsx/pivot.js +0 -22
- package/dist/io/xlsx/{pivot-read.js → read-pivot.js} +0 -30
- package/dist/io/xlsx/read-rows.js +1 -83
- package/dist/io/xlsx/{shared-strings-read.js → read-shared-strings.js} +0 -11
- package/dist/io/xlsx/read-styles.d.ts +25 -1
- package/dist/io/xlsx/read-styles.js +61 -108
- package/dist/io/xlsx/read-worksheet.js +1 -88
- package/dist/io/xlsx/read.js +3 -211
- package/dist/io/xlsx/relationships.js +0 -11
- package/dist/io/xlsx/rich-runs.js +0 -16
- package/dist/io/xlsx/rich-text.js +0 -12
- package/dist/io/xlsx/shared-formulas.js +0 -7
- package/dist/io/xlsx/shared-strings.js +0 -21
- package/dist/io/xlsx/sheet-properties.js +1 -64
- package/dist/io/xlsx/styles.d.ts +1 -29
- package/dist/io/xlsx/styles.js +2 -366
- package/dist/io/xlsx/tables.js +5 -52
- package/dist/io/xlsx/threaded-comments.js +0 -103
- package/dist/io/xlsx/workbook-xml.js +0 -94
- package/dist/io/xlsx/worksheet-xml.js +0 -174
- package/dist/io/xlsx/write-stream.js +0 -137
- package/dist/io/xlsx/write.js +1 -134
- package/dist/io/xlsx/x14-ext.js +0 -8
- package/dist/vba/cfb-writer.js +12 -59
- package/dist/vba/cfb.js +2 -32
- package/dist/vba/codepage.js +1 -23
- package/dist/vba/errors.js +0 -15
- package/dist/vba/index.js +0 -10
- package/dist/vba/ms-ovba.js +3 -46
- package/dist/vba/project-editor.js +4 -89
- package/dist/vba/project.js +2 -34
- package/dist/vba/vba-encoding.js +1 -17
- package/dist/xml/errors.js +0 -10
- package/dist/xml/xml-read.js +3 -82
- package/dist/xml/xml.js +0 -39
- package/package.json +9 -10
- package/skills/ts-xlsx-upstream/SKILL.md +178 -0
- /package/dist/io/xlsx/{pivot-read.d.ts → read-pivot.d.ts} +0 -0
- /package/dist/io/xlsx/{shared-strings-read.d.ts → read-shared-strings.d.ts} +0 -0
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { AuthoringError } from '../errors.js';
|
|
2
|
+
import { Cell } from './cell.js';
|
|
3
|
+
export function rowPlacements(values, columns) {
|
|
4
|
+
if (Array.isArray(values)) {
|
|
5
|
+
const placements = [];
|
|
6
|
+
values.forEach((value, index) => {
|
|
7
|
+
if (value !== undefined)
|
|
8
|
+
placements.push([index + 1, value]);
|
|
9
|
+
});
|
|
10
|
+
return placements;
|
|
11
|
+
}
|
|
12
|
+
return Object.entries(values).map(([key, value]) => [columnIndexByKey(columns, key), value]);
|
|
13
|
+
}
|
|
14
|
+
export function buildRowCells(number, values, columns) {
|
|
15
|
+
const row = new Map();
|
|
16
|
+
for (const [col, value] of rowPlacements(values, columns)) {
|
|
17
|
+
const cell = new Cell(number, col);
|
|
18
|
+
cell.value = value;
|
|
19
|
+
row.set(col, cell);
|
|
20
|
+
}
|
|
21
|
+
return row;
|
|
22
|
+
}
|
|
23
|
+
export function columnIndexByKey(columns, key) {
|
|
24
|
+
for (const [index, properties] of columns) {
|
|
25
|
+
if (properties.key === key)
|
|
26
|
+
return index;
|
|
27
|
+
}
|
|
28
|
+
throw new AuthoringError(`no column is keyed ${JSON.stringify(key)} — set getColumn(n).key first`);
|
|
29
|
+
}
|
package/dist/core/row.d.ts
CHANGED
|
@@ -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
|
-
/**
|
|
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
|
@@ -1,24 +1,8 @@
|
|
|
1
|
-
// A handle on one row of a worksheet: its formatting and its cells, reached by row number.
|
|
2
|
-
//
|
|
3
|
-
// A handle, not a record. `Worksheet` keeps the authoritative stores — the cell grid and the sparse
|
|
4
|
-
// map of per-row formatting — and a `Row` reads and writes straight through to them, so two handles
|
|
5
|
-
// on the same number always agree and neither can hold a stale copy. That is the whole reason this
|
|
6
|
-
// is not a snapshot: a row object that copied its cells out would be the shape of the merge-loss
|
|
7
|
-
// class of bug the model contract exists to prevent.
|
|
8
|
-
//
|
|
9
|
-
// Position is fixed at construction, exactly as `Cell` fixes `(row, col)`. `sheet.getRow(3)` means
|
|
10
|
-
// "row 3" and keeps meaning row 3 — a splice that moves content past it does not carry the handle
|
|
11
|
-
// along, any more than it re-points a `Cell`.
|
|
12
|
-
//
|
|
13
|
-
// Formatting is created on write, never on read. Asking for `sheet.getRow(500)` costs nothing and
|
|
14
|
-
// does not extend the used range; assigning `height` is what materialises the record.
|
|
15
1
|
import { columnToNumber, encodeAddress } from './address.js';
|
|
16
2
|
import { INTERNAL } from './internal.js';
|
|
17
3
|
export class Row {
|
|
18
4
|
#sheet;
|
|
19
|
-
/** 1-based row number. Fixed for this handle's lifetime. */
|
|
20
5
|
number;
|
|
21
|
-
/** @throws {RangeError} if the number is not a positive integer. */
|
|
22
6
|
constructor(sheet, number) {
|
|
23
7
|
if (!Number.isInteger(number) || number < 1) {
|
|
24
8
|
throw new RangeError(`row ${number} is out of bounds — rows start at 1`);
|
|
@@ -26,83 +10,46 @@ export class Row {
|
|
|
26
10
|
this.#sheet = sheet;
|
|
27
11
|
this.number = number;
|
|
28
12
|
}
|
|
29
|
-
/**
|
|
30
|
-
* The row's format record if it has one, else `undefined` — a read that never fabricates, so a
|
|
31
|
-
* serializer can ask every row it visits whether there are attributes to emit without giving each
|
|
32
|
-
* one an empty record. Read-only on purpose: {@link height} and its siblings are how a row is
|
|
33
|
-
* formatted, and they create the record on first write.
|
|
34
|
-
*/
|
|
35
13
|
get properties() {
|
|
36
14
|
return this.#sheet[INTERNAL].rowPropertiesOf(this.number);
|
|
37
15
|
}
|
|
38
|
-
/** Row height in points; `undefined` leaves the sheet default in force. */
|
|
39
16
|
get height() {
|
|
40
17
|
return this.#read('height');
|
|
41
18
|
}
|
|
42
19
|
set height(height) {
|
|
43
20
|
this.#write('height', height);
|
|
44
21
|
}
|
|
45
|
-
/** Whether the row is hidden. */
|
|
46
22
|
get hidden() {
|
|
47
23
|
return this.#read('hidden');
|
|
48
24
|
}
|
|
49
25
|
set hidden(hidden) {
|
|
50
26
|
this.#write('hidden', hidden);
|
|
51
27
|
}
|
|
52
|
-
/** Outline (grouping) depth; 0 or `undefined` means ungrouped. */
|
|
53
28
|
get outlineLevel() {
|
|
54
29
|
return this.#read('outlineLevel');
|
|
55
30
|
}
|
|
56
31
|
set outlineLevel(outlineLevel) {
|
|
57
32
|
this.#write('outlineLevel', outlineLevel);
|
|
58
33
|
}
|
|
59
|
-
/** Whether this row is the collapsed summary of an outline group. */
|
|
60
34
|
get collapsed() {
|
|
61
35
|
return this.#read('collapsed');
|
|
62
36
|
}
|
|
63
37
|
set collapsed(collapsed) {
|
|
64
38
|
this.#write('collapsed', collapsed);
|
|
65
39
|
}
|
|
66
|
-
/** Background fill for the row's cells that carry no fill of their own. */
|
|
67
40
|
get fill() {
|
|
68
41
|
return this.#read('fill');
|
|
69
42
|
}
|
|
70
43
|
set fill(fill) {
|
|
71
44
|
this.#write('fill', fill);
|
|
72
45
|
}
|
|
73
|
-
/**
|
|
74
|
-
* The cell at a column in this row, creating it on first access. The column is a 1-based index
|
|
75
|
-
* (`row.getCell(2)`) or its letters (`row.getCell('B')`).
|
|
76
|
-
*
|
|
77
|
-
* Resolves through merges exactly as {@link Worksheet.getCell} does: addressing a cell covered by
|
|
78
|
-
* a merged region yields that region's master.
|
|
79
|
-
*
|
|
80
|
-
* @throws {RangeError} if the column is not a positive integer.
|
|
81
|
-
* @throws {SyntaxError} if the column letters do not name a column.
|
|
82
|
-
*/
|
|
83
46
|
getCell(column) {
|
|
84
47
|
const index = typeof column === 'number' ? column : columnToNumber(column);
|
|
85
48
|
return this.#sheet.getCell(encodeAddress(index, this.number));
|
|
86
49
|
}
|
|
87
|
-
/**
|
|
88
|
-
* The row's materialised cells in ascending column order. Sparse: a column never written to has
|
|
89
|
-
* no cell here, and the array is a fresh snapshot of *which* cells exist — the cells themselves
|
|
90
|
-
* are the live ones.
|
|
91
|
-
*/
|
|
92
50
|
get cells() {
|
|
93
51
|
return this.#sheet[INTERNAL].rowCells(this.number);
|
|
94
52
|
}
|
|
95
|
-
/**
|
|
96
|
-
* The row's values by position, index 0 being column A. Sparse in the same way {@link cells} is:
|
|
97
|
-
* a column with no cell is a hole, which is what distinguishes "never written" from a cell
|
|
98
|
-
* holding `null`.
|
|
99
|
-
*
|
|
100
|
-
* Assigning places each value it names and leaves every other column untouched — a hole or an
|
|
101
|
-
* explicit `undefined` skips that column, and a shorter array does not clear the tail. These are
|
|
102
|
-
* {@link Worksheet.addRow}'s rules, deliberately: `values` is that same row shape addressed by
|
|
103
|
-
* number rather than appended. To *replace* a row, including clearing what it held, splice it —
|
|
104
|
-
* `sheet.spliceRows(n, 1, values)`.
|
|
105
|
-
*/
|
|
106
53
|
get values() {
|
|
107
54
|
const values = [];
|
|
108
55
|
for (const cell of this.cells)
|
|
@@ -118,11 +65,6 @@ export class Row {
|
|
|
118
65
|
#read(key) {
|
|
119
66
|
return this.#sheet[INTERNAL].rowPropertiesOf(this.number)?.[key];
|
|
120
67
|
}
|
|
121
|
-
// `undefined` clears rather than stores: `RowProperties` is declared with optional fields under
|
|
122
|
-
// `exactOptionalPropertyTypes`, so a present-but-undefined key is not the same shape as an absent
|
|
123
|
-
// one — and it would make a formatting-free row look formatted to anything reading `properties`.
|
|
124
|
-
// Clearing a row that has no record at all is a no-op, so a write of `undefined` never
|
|
125
|
-
// materialises one.
|
|
126
68
|
#write(key, value) {
|
|
127
69
|
if (value === undefined) {
|
|
128
70
|
const properties = this.#sheet[INTERNAL].rowPropertiesOf(this.number);
|
package/dist/core/style.js
CHANGED
|
@@ -1,8 +1,3 @@
|
|
|
1
|
-
// Cell style primitives.
|
|
2
|
-
//
|
|
3
|
-
// Styles are a large surface in OOXML (fonts, fills, borders, alignment, number
|
|
4
|
-
// formats, protection). The rewrite grows them corpus-first; this module models the
|
|
5
|
-
// facets landed so far — colours, fills, borders, fonts, alignment, and protection.
|
|
6
1
|
const NAMED_UNDERLINE_STYLES = new Set([
|
|
7
2
|
'none',
|
|
8
3
|
'single',
|
|
@@ -10,7 +5,6 @@ const NAMED_UNDERLINE_STYLES = new Set([
|
|
|
10
5
|
'singleAccounting',
|
|
11
6
|
'doubleAccounting',
|
|
12
7
|
]);
|
|
13
|
-
/** Narrow a raw `<u val>` token to a named {@link UnderlineStyle} (the non-boolean members). */
|
|
14
8
|
export function isNamedUnderlineStyle(value) {
|
|
15
9
|
return NAMED_UNDERLINE_STYLES.has(value);
|
|
16
10
|
}
|
|
@@ -35,7 +29,6 @@ const FILL_PATTERN_TYPES = new Set([
|
|
|
35
29
|
'lightGrid',
|
|
36
30
|
'lightTrellis',
|
|
37
31
|
]);
|
|
38
|
-
/** Narrow a raw `<patternFill patternType>` token to a known {@link FillPatternType}. */
|
|
39
32
|
export function isFillPatternType(value) {
|
|
40
33
|
return FILL_PATTERN_TYPES.has(value);
|
|
41
34
|
}
|
|
@@ -54,15 +47,12 @@ const BORDER_STYLES = new Set([
|
|
|
54
47
|
'mediumDashDotDot',
|
|
55
48
|
'slantDashDot',
|
|
56
49
|
]);
|
|
57
|
-
/** Narrow a raw border-edge `style` attribute to a known {@link BorderStyle}. */
|
|
58
50
|
export function isBorderStyle(value) {
|
|
59
51
|
return BORDER_STYLES.has(value);
|
|
60
52
|
}
|
|
61
|
-
/** Narrow a raw `<vertAlign val>` token to a known {@link FontVerticalAlignment}. */
|
|
62
53
|
export function isFontVerticalAlignment(value) {
|
|
63
54
|
return value === 'superscript' || value === 'subscript';
|
|
64
55
|
}
|
|
65
|
-
/** Narrow a raw `<scheme val>` token to a known {@link FontScheme}. */
|
|
66
56
|
export function isFontScheme(value) {
|
|
67
57
|
return value === 'minor' || value === 'major' || value === 'none';
|
|
68
58
|
}
|
|
@@ -76,7 +66,6 @@ const HORIZONTAL_ALIGNMENTS = new Set([
|
|
|
76
66
|
'centerContinuous',
|
|
77
67
|
'distributed',
|
|
78
68
|
]);
|
|
79
|
-
/** Narrow a raw `<alignment horizontal>` token to a known {@link HorizontalAlignment}. */
|
|
80
69
|
export function isHorizontalAlignment(value) {
|
|
81
70
|
return HORIZONTAL_ALIGNMENTS.has(value);
|
|
82
71
|
}
|
|
@@ -87,13 +76,9 @@ const VERTICAL_ALIGNMENTS = new Set([
|
|
|
87
76
|
'justify',
|
|
88
77
|
'distributed',
|
|
89
78
|
]);
|
|
90
|
-
/** Narrow a raw `<alignment vertical>` token to a known {@link VerticalAlignment}. */
|
|
91
79
|
export function isVerticalAlignment(value) {
|
|
92
80
|
return VERTICAL_ALIGNMENTS.has(value);
|
|
93
81
|
}
|
|
94
|
-
// One entry per facet, as a Record rather than a bare list so the compiler rejects it the moment a
|
|
95
|
-
// facet is added to CellStyle and forgotten here — which would otherwise let the facet-list-driven
|
|
96
|
-
// copies below (assignStyleFacets) silently skip the new facet, a merge-loss.
|
|
97
82
|
const CELL_STYLE_FACET_KEYS = {
|
|
98
83
|
fill: true,
|
|
99
84
|
numFmt: true,
|
|
@@ -102,22 +87,11 @@ const CELL_STYLE_FACET_KEYS = {
|
|
|
102
87
|
alignment: true,
|
|
103
88
|
protection: true,
|
|
104
89
|
};
|
|
105
|
-
/** The names of the {@link CellStyle} facets, for helpers that copy the tuple facet-by-facet. */
|
|
106
90
|
export const CELL_STYLE_FACETS = Object.keys(CELL_STYLE_FACET_KEYS);
|
|
107
|
-
/**
|
|
108
|
-
* Copy each present facet of `source` onto `target`, leaving facets `source` omits untouched — the
|
|
109
|
-
* plain-record counterpart to a cell's `applyCellStyle`, for the {@link CellStyle}-shaped targets a
|
|
110
|
-
* `Cell`'s setters don't reach (a column's cell-defaults, a named style being assembled on read).
|
|
111
|
-
* Driven by {@link CELL_STYLE_FACETS}, so a facet added to the tuple reaches these paths the moment
|
|
112
|
-
* it joins — the same single-point-of-change the cell path gets.
|
|
113
|
-
*/
|
|
114
91
|
export function assignStyleFacets(target, source) {
|
|
115
92
|
for (const facet of CELL_STYLE_FACETS)
|
|
116
93
|
copyFacet(target, source, facet);
|
|
117
94
|
}
|
|
118
|
-
// A single facet key at a time, so the write's key type is one member (not the whole union) and
|
|
119
|
-
// `target[key] = source[key]` typechecks without a cast — the correlated-key access TS can't verify
|
|
120
|
-
// when the key is a union.
|
|
121
95
|
function copyFacet(target, source, key) {
|
|
122
96
|
const value = source[key];
|
|
123
97
|
if (value !== undefined)
|
package/dist/core/table-style.js
CHANGED
|
@@ -1,20 +1,4 @@
|
|
|
1
|
-
// A custom table style: the named, reusable look a table (or a pivot) applies to itself by name.
|
|
2
|
-
//
|
|
3
|
-
// A table style is a list of *elements*, each naming one region of the table — the whole table, the
|
|
4
|
-
// header row, the first row stripe — and the differential formatting to lay over it. Excel's built-in
|
|
5
|
-
// gallery ("TableStyleMedium2" and its sixty siblings) is exactly this shape; a workbook that declares
|
|
6
|
-
// its own joins the gallery for that file, and a table reaches it by name through
|
|
7
|
-
// `TableStyleInfo.name`.
|
|
8
1
|
import { AuthoringError } from '../errors.js';
|
|
9
|
-
/**
|
|
10
|
-
* The regions a table style can format (`ST_TableStyleType`).
|
|
11
|
-
*
|
|
12
|
-
* The first thirteen apply to a **table**; the rest style a **pivot table**, which has regions a
|
|
13
|
-
* table does not have (subtotal rows, page-field labels, subheadings). Both live in the same
|
|
14
|
-
* enumeration and the same `<tableStyle>` element — what decides which regions a consumer honours is
|
|
15
|
-
* the style's own `table`/`pivot` flags, not the element names — so the type carries all of them
|
|
16
|
-
* rather than splitting into two enumerations that a caller would have to choose between up front.
|
|
17
|
-
*/
|
|
18
2
|
export const TABLE_STYLE_ELEMENT_TYPES = [
|
|
19
3
|
'wholeTable',
|
|
20
4
|
'headerRow',
|
|
@@ -45,7 +29,6 @@ export const TABLE_STYLE_ELEMENT_TYPES = [
|
|
|
45
29
|
'pageFieldLabels',
|
|
46
30
|
'pageFieldValues',
|
|
47
31
|
];
|
|
48
|
-
/** The four element types banded across several rows or columns — the only ones {@link TableStyleElement.size} means anything on. */
|
|
49
32
|
export const STRIPE_ELEMENT_TYPES = new Set([
|
|
50
33
|
'firstRowStripe',
|
|
51
34
|
'secondRowStripe',
|
|
@@ -55,17 +38,6 @@ export const STRIPE_ELEMENT_TYPES = new Set([
|
|
|
55
38
|
export function isTableStyleElementType(value) {
|
|
56
39
|
return TABLE_STYLE_ELEMENT_TYPES.includes(value);
|
|
57
40
|
}
|
|
58
|
-
/**
|
|
59
|
-
* Reject a table style the writer would otherwise emit as valid-but-inert XML.
|
|
60
|
-
*
|
|
61
|
-
* Both failures here are of the same kind: Excel accepts the file and quietly does nothing with the
|
|
62
|
-
* part the caller cared about. An empty name means no table can ever reference the style, and a
|
|
63
|
-
* `size` outside a stripe is ignored — neither shows up as a repair prompt or a schema error, so the
|
|
64
|
-
* only place to catch them is the call that made them.
|
|
65
|
-
*
|
|
66
|
-
* @throws {AuthoringError} if the name is empty, or a non-stripe element carries a `size`, or a `size` is not
|
|
67
|
-
* a positive integer.
|
|
68
|
-
*/
|
|
69
41
|
export function checkTableStyle(style) {
|
|
70
42
|
if (style.name === '') {
|
|
71
43
|
throw new AuthoringError('a table style needs a name: a table references its style by name');
|
package/dist/core/table.js
CHANGED
|
@@ -1,20 +1,5 @@
|
|
|
1
|
-
// A worksheet table (OOXML `<table>`): a named, structured range with typed columns,
|
|
2
|
-
// an optional header row, and an optional totals row.
|
|
3
|
-
//
|
|
4
|
-
// The model stores the anchor and the column/row counts, not a pre-baked range string —
|
|
5
|
-
// the occupied geometry is derived, so it stays correct as an empty table (header row
|
|
6
|
-
// only), a headerless table (data rows only), or a totals-bearing table. The writer is
|
|
7
|
-
// the OOXML gatekeeper for serialization; this model owns the invariants Excel enforces
|
|
8
|
-
// on the *shape* itself: a legal name, at least one column, and at least one row.
|
|
9
1
|
import { AuthoringError } from '../errors.js';
|
|
10
2
|
import { decodeAddress, encodeAddress } from './address.js';
|
|
11
|
-
/** Copy a style, keeping only its defined fields off the literal so `exactOptionalPropertyTypes`
|
|
12
|
-
* never sees a fabricated `key: undefined` — an absent attribute must stay absent across a copy.
|
|
13
|
-
*
|
|
14
|
-
* The sentinel name `"None"` (Excel's table-style gallery entry for *no* style) is normalised to an
|
|
15
|
-
* absent name: OOXML expresses "unstyled" as a `<tableStyleInfo>` with no `name` attribute, so a
|
|
16
|
-
* literal `name="None"` would reference a style that does not exist and make the file suspect. The
|
|
17
|
-
* banding flags set alongside it are untouched. */
|
|
18
3
|
function cloneStyleInfo(style) {
|
|
19
4
|
const clone = {};
|
|
20
5
|
if (style.name !== undefined && style.name !== 'None')
|
|
@@ -29,16 +14,6 @@ function cloneStyleInfo(style) {
|
|
|
29
14
|
clone.showColumnStripes = style.showColumnStripes;
|
|
30
15
|
return clone;
|
|
31
16
|
}
|
|
32
|
-
/**
|
|
33
|
-
* OOXML's totals-row function names (`ST_TotalsRowFunction`) to the `SUBTOTAL` first-argument code
|
|
34
|
-
* Excel writes into a materialised totals cell. The `10x` band ignores manually hidden rows — the
|
|
35
|
-
* behaviour Excel's totals row uses. The one inversion trap: `count` is COUNTA (103, non-empty) while
|
|
36
|
-
* `countNums` is COUNT (102, numbers only). `none` (no aggregate) has no built-in code, so a column
|
|
37
|
-
* carrying it is absent here and its totals cell is left unmaterialised — Excel accepts the blank
|
|
38
|
-
* cell. `custom` is likewise absent: its aggregate is not a `SUBTOTAL` but the arbitrary formula
|
|
39
|
-
* stored in {@link TableColumn.totalsRowFormula}, which the reader/writer round-trip and the
|
|
40
|
-
* materialiser writes into the cell verbatim.
|
|
41
|
-
*/
|
|
42
17
|
export const TOTALS_ROW_SUBTOTAL_CODE = {
|
|
43
18
|
average: 101,
|
|
44
19
|
countNums: 102,
|
|
@@ -61,24 +36,10 @@ const TOTALS_ROW_FUNCTIONS = new Set([
|
|
|
61
36
|
'custom',
|
|
62
37
|
'none',
|
|
63
38
|
]);
|
|
64
|
-
/** Narrow a raw `totalsRowFunction` attribute to a known {@link TotalsRowFunction}. */
|
|
65
39
|
export function isTotalsRowFunction(value) {
|
|
66
40
|
return TOTALS_ROW_FUNCTIONS.has(value);
|
|
67
41
|
}
|
|
68
|
-
// Excel's table-name grammar: start with a letter, underscore, or backslash; every later
|
|
69
|
-
// character a letter, digit, period, or underscore. Unicode letters/digits are allowed.
|
|
70
|
-
// Excel additionally forbids a name that *is* a cell reference (`A1`, `R1C1`); we defer
|
|
71
|
-
// that rule deliberately — the regression corpus treats cell-reference-shaped names like
|
|
72
|
-
// `T1` as valid table names, so enforcing the collision rule here would reject a fixture
|
|
73
|
-
// the contract accepts.
|
|
74
42
|
const IDENTIFIER = /^[\p{L}\\_][\p{L}\p{N}._]*$/u;
|
|
75
|
-
/**
|
|
76
|
-
* Return copies of `columns` with every name made unique (case-insensitively): the first occurrence
|
|
77
|
-
* keeps its name; a later clash gains the smallest numeric suffix that resolves it (`foo`, `foo2`,
|
|
78
|
-
* `foo3`, …). OOXML requires unique column names within a table — Excel treats a collision as
|
|
79
|
-
* corruption — so this is applied both when a table is authored and when one is read from a file,
|
|
80
|
-
* keeping the two paths identical rather than rejecting a name list the reader would accept.
|
|
81
|
-
*/
|
|
82
43
|
export function disambiguateColumnNames(columns) {
|
|
83
44
|
const seen = new Set();
|
|
84
45
|
return columns.map((column) => {
|
|
@@ -107,17 +68,10 @@ export class Table {
|
|
|
107
68
|
totalsRowShown;
|
|
108
69
|
autoFilter;
|
|
109
70
|
style;
|
|
110
|
-
// The anchor and data-row count move when a row/column splice shifts or resizes the table, so
|
|
111
|
-
// they are mutable behind the class's controlled `shiftRows`/`shiftColumns` methods.
|
|
112
71
|
#anchorCol;
|
|
113
72
|
#anchorRow;
|
|
114
73
|
#dataRowCount;
|
|
115
|
-
// Set by the worksheet that registers this table so an appended row can be written into the grid.
|
|
116
|
-
// A table constructed standalone (a unit test, a bare model) has none — appending values then
|
|
117
|
-
// throws rather than silently dropping them.
|
|
118
74
|
#writeCell;
|
|
119
|
-
// Supplied alongside #writeCell by the registering worksheet. A totals-row table appends by
|
|
120
|
-
// inserting a grid row above the totals; a standalone table has neither hook.
|
|
121
75
|
#insertRow;
|
|
122
76
|
constructor(options, writeCell, insertRow) {
|
|
123
77
|
validateTableName(options.name);
|
|
@@ -138,8 +92,6 @@ export class Table {
|
|
|
138
92
|
this.totalsRow = options.totalsRow ?? false;
|
|
139
93
|
this.totalsRowShown = options.totalsRowShown;
|
|
140
94
|
this.style = options.style === undefined ? undefined : cloneStyleInfo(options.style);
|
|
141
|
-
// A header table gains an autoFilter by default (Excel's behaviour when a table is inserted);
|
|
142
|
-
// a headerless table can never carry one — an autoFilter has no header row to anchor to.
|
|
143
95
|
this.autoFilter = this.headerRow && (options.autoFilter ?? true);
|
|
144
96
|
this.#anchorCol = col;
|
|
145
97
|
this.#anchorRow = row;
|
|
@@ -153,34 +105,18 @@ export class Table {
|
|
|
153
105
|
get columnCount() {
|
|
154
106
|
return this.columns.length;
|
|
155
107
|
}
|
|
156
|
-
/** The number of data rows (excludes the header and totals rows). Always defined — a table loaded
|
|
157
|
-
* from a file derives it from the stored range, so reading the height never throws. */
|
|
158
108
|
get rowCount() {
|
|
159
109
|
return this.#dataRowCount;
|
|
160
110
|
}
|
|
161
|
-
/**
|
|
162
|
-
* Append a data row to the bottom of the table, growing its range by one row and writing `values`
|
|
163
|
-
* left-to-right across its columns. A loaded table exposes its rows the same as a freshly-authored
|
|
164
|
-
* one, so this works identically whether the table was built in memory or read from a file.
|
|
165
|
-
*
|
|
166
|
-
* A table carrying a totals row appends above it: the new data row lands where the totals row sat,
|
|
167
|
-
* and the totals row (with any sheet content below) shifts down by one — exactly what inserting a
|
|
168
|
-
* worksheet row does. That relocation lives in the grid, so a totals-row table not attached to a
|
|
169
|
-
* worksheet throws, as does passing `values` on any detached table — there is nowhere to put them.
|
|
170
|
-
*/
|
|
171
111
|
addRow(values = []) {
|
|
172
112
|
if (values.length > this.columnCount) {
|
|
173
113
|
throw new RangeError(`row has ${values.length} values but table "${this.name}" has ${this.columnCount} columns`);
|
|
174
114
|
}
|
|
175
|
-
// The append point is the row directly below the last data row: the totals row when one exists,
|
|
176
|
-
// otherwise the first free row under the table.
|
|
177
115
|
const target = this.#anchorRow + (this.headerRow ? 1 : 0) + this.#dataRowCount;
|
|
178
116
|
if (this.totalsRow) {
|
|
179
117
|
if (this.#insertRow === undefined) {
|
|
180
118
|
throw new AuthoringError(`table "${this.name}" is not attached to a worksheet — cannot relocate its totals row to append a data row`);
|
|
181
119
|
}
|
|
182
|
-
// Opening a grid slot at the totals row shifts the totals down and grows this table by one
|
|
183
|
-
// through the sheet's own table re-pinning, so #dataRowCount is not bumped again here.
|
|
184
120
|
this.#insertRow(target);
|
|
185
121
|
values.forEach((value, index) => {
|
|
186
122
|
this.#writeCell?.(target, this.#anchorCol + index, value, this.columns[index]?.style);
|
|
@@ -197,15 +133,7 @@ export class Table {
|
|
|
197
133
|
}
|
|
198
134
|
this.#dataRowCount += 1;
|
|
199
135
|
}
|
|
200
|
-
/**
|
|
201
|
-
* Re-pin the table through a row splice: `count` rows removed at the 1-based `start`, then rows
|
|
202
|
-
* inserted so surviving rows below shift by `delta`. A splice entirely above the table moves its
|
|
203
|
-
* whole range by `delta`; one landing inside grows or shrinks the data rows to absorb the change;
|
|
204
|
-
* one that deletes the table's every row removes it. Returns `false` when the table no longer has
|
|
205
|
-
* a row to occupy (the caller drops it), `true` when it survives.
|
|
206
|
-
*/
|
|
207
136
|
shiftRows(start, count, delta) {
|
|
208
|
-
// A table whose every row lies within the deleted span has nothing left to occupy.
|
|
209
137
|
if (this.#anchorRow >= start && this.#bottom < start + count)
|
|
210
138
|
return false;
|
|
211
139
|
const shift = (v) => (v < start ? v : v >= start + count ? v + delta : start);
|
|
@@ -220,23 +148,11 @@ export class Table {
|
|
|
220
148
|
this.#dataRowCount = dataRows;
|
|
221
149
|
return true;
|
|
222
150
|
}
|
|
223
|
-
/**
|
|
224
|
-
* Re-pin the table through a column splice. A splice entirely to the table's left moves its anchor
|
|
225
|
-
* by `delta`; one to its right leaves it untouched. A splice landing inside the table's columns is
|
|
226
|
-
* structural surgery on named columns with no unambiguous answer, so the table's columns are left
|
|
227
|
-
* as-is (anchor unchanged) rather than fabricated or dropped. Always returns `true`.
|
|
228
|
-
*/
|
|
229
151
|
shiftColumns(start, count, delta) {
|
|
230
152
|
if (this.#anchorCol >= start + count)
|
|
231
153
|
this.#anchorCol += delta;
|
|
232
154
|
return true;
|
|
233
155
|
}
|
|
234
|
-
/**
|
|
235
|
-
* The options that reconstruct this table — the anchor as a single-cell ref (not the derived
|
|
236
|
-
* full range), the columns, and the data-row count with the header/totals flags. Feeding this
|
|
237
|
-
* back to the constructor yields an equivalent table, so a worksheet model can carry a table
|
|
238
|
-
* losslessly across an export/import round-trip.
|
|
239
|
-
*/
|
|
240
156
|
get options() {
|
|
241
157
|
const options = {
|
|
242
158
|
name: this.name,
|
|
@@ -248,34 +164,21 @@ export class Table {
|
|
|
248
164
|
totalsRow: this.totalsRow,
|
|
249
165
|
autoFilter: this.autoFilter,
|
|
250
166
|
};
|
|
251
|
-
// Kept off the literal so `undefined` (attribute absent) stays absent, not an explicit
|
|
252
|
-
// `totalsRowShown: undefined` — the round-trip must not fabricate the flag.
|
|
253
167
|
if (this.totalsRowShown !== undefined)
|
|
254
168
|
options.totalsRowShown = this.totalsRowShown;
|
|
255
169
|
if (this.style !== undefined)
|
|
256
170
|
options.style = cloneStyleInfo(this.style);
|
|
257
171
|
return options;
|
|
258
172
|
}
|
|
259
|
-
/** The full A1 range the table occupies: header (if any) + data rows + totals (if any). Distinct
|
|
260
|
-
* from {@link TableOptions.ref} (and {@link options}'s own `ref`), which is only the single-cell
|
|
261
|
-
* anchor a table is constructed from — this is the anchor plus the columns/rows it has grown to
|
|
262
|
-
* cover. */
|
|
263
173
|
get range() {
|
|
264
174
|
return `${encodeAddress(this.#anchorCol, this.#anchorRow)}:${encodeAddress(this.#right, this.#bottom)}`;
|
|
265
175
|
}
|
|
266
|
-
/**
|
|
267
|
-
* The autoFilter range — the header row plus the data rows, never the totals row — or
|
|
268
|
-
* `undefined` when the table has no autoFilter: either it is headerless (an autoFilter has
|
|
269
|
-
* nothing to anchor to and Excel treats its presence as corruption) or its {@link autoFilter}
|
|
270
|
-
* flag is off (a table read without one must not gain one on round-trip).
|
|
271
|
-
*/
|
|
272
176
|
get autoFilterRef() {
|
|
273
177
|
if (!this.autoFilter)
|
|
274
178
|
return undefined;
|
|
275
179
|
const bottom = this.#anchorRow + this.#dataRowCount;
|
|
276
180
|
return `${encodeAddress(this.#anchorCol, this.#anchorRow)}:${encodeAddress(this.#right, bottom)}`;
|
|
277
181
|
}
|
|
278
|
-
/** The occupied rectangle, for conflict checks such as overlapping merges. */
|
|
279
182
|
get region() {
|
|
280
183
|
return { top: this.#anchorRow, left: this.#anchorCol, bottom: this.#bottom, right: this.#right };
|
|
281
184
|
}
|
|
@@ -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,10 @@
|
|
|
1
|
+
export function estimateWrappedLines(text, width) {
|
|
2
|
+
if (!Number.isFinite(width) || width <= 0) {
|
|
3
|
+
throw new RangeError(`wrap width ${width} is not a positive finite number of character units`);
|
|
4
|
+
}
|
|
5
|
+
let lines = 0;
|
|
6
|
+
for (const segment of text.split(/\r\n?|\n/)) {
|
|
7
|
+
lines += Math.max(1, Math.ceil(segment.length / width));
|
|
8
|
+
}
|
|
9
|
+
return lines;
|
|
10
|
+
}
|