@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
|
@@ -1,22 +1,3 @@
|
|
|
1
|
-
// Streaming row reader: yield a worksheet's rows one at a time, without ever building the whole
|
|
2
|
-
// {@link Workbook} model.
|
|
3
|
-
//
|
|
4
|
-
// `readXlsx` materialises every cell of every sheet as a live `Cell` object held in nested Maps —
|
|
5
|
-
// fine for editing, but for a large sheet read purely to extract its data it holds the entire grid
|
|
6
|
-
// in memory at once. This reader instead *pulls* the sheet's XML through `xmlEvents` and yields a
|
|
7
|
-
// plain {@link StreamedRow} at each `</row>`, retaining only the row currently in hand. Peak model
|
|
8
|
-
// memory is one row, not the sheet.
|
|
9
|
-
//
|
|
10
|
-
// Two entry points sit on the same scanner:
|
|
11
|
-
// - {@link readSheetRows} streams a single selected sheet's rows (the terse data-extraction case).
|
|
12
|
-
// - {@link readWorkbookStream} yields a {@link StreamedSheet} per worksheet in workbook order, so a
|
|
13
|
-
// caller can walk every sheet — each sheet's rows still stream one at a time.
|
|
14
|
-
//
|
|
15
|
-
// Scope of this slice: the package is still inflated whole (bounded by the running counter in
|
|
16
|
-
// `./inflate.ts`) and shared strings / styles are read as whole parts — both are legitimately
|
|
17
|
-
// document-sized and cheap. What this avoids is retaining N materialised cells. A later slice can
|
|
18
|
-
// make the inflate itself per-part lazy; the pull primitive this stands on (`xmlEvents`) is the
|
|
19
|
-
// same one that path will use.
|
|
20
1
|
import { MAX_COLUMN } from '../../core/address.js';
|
|
21
2
|
import { AuthoringError } from '../../errors.js';
|
|
22
3
|
import { boolStrict, closeEmptyElements, localName, xmlEvents } from '../../xml/xml-read.js';
|
|
@@ -25,50 +6,18 @@ import { inflateSpreadsheetPackage, unsupportedWorkbookPart } from '../opc/sniff
|
|
|
25
6
|
import { CellAccumulator } from './cell-accumulator.js';
|
|
26
7
|
import { XlsxParseError } from './errors.js';
|
|
27
8
|
import { DEFAULT_MAX_UNCOMPRESSED, parseRelationships, parseStyleTable, parseWorkbookSheets, resolveWorkbookPart, } from './read.js';
|
|
28
|
-
import { parseSharedStrings } from './shared-strings
|
|
29
|
-
/**
|
|
30
|
-
* Stream a worksheet's rows from an `.xlsx` package, yielding each in sheet order without building
|
|
31
|
-
* the workbook model. Only rows the sheet actually declares are yielded, and within a row only its
|
|
32
|
-
* non-empty cells — a blank or style-only cell contributes nothing, matching the intent of a data
|
|
33
|
-
* read.
|
|
34
|
-
*
|
|
35
|
-
* @param data The raw `.xlsx` bytes.
|
|
36
|
-
* @param options Sheet selector and the inflate bound (see {@link ReadSheetRowsOptions}).
|
|
37
|
-
* @throws {UnsupportedFormatError} if the input is not a readable `.xlsx` package (a legacy `.xls`, a
|
|
38
|
-
* binary `.xlsb`, or an unrecognised/non-ZIP blob — branch on `.format`).
|
|
39
|
-
* @throws {PackageReadError} if the input is a ZIP that cannot be unpacked — a corrupt or
|
|
40
|
-
* truncated archive, or one exceeding the inflate bound (a probable zip bomb).
|
|
41
|
-
* @throws {XlsxParseError} if the package's workbook part declares no worksheets.
|
|
42
|
-
* @throws {RangeError} / {@link AuthoringError} if `options.sheet` selects a position, or a name,
|
|
43
|
-
* that no worksheet has.
|
|
44
|
-
*/
|
|
9
|
+
import { parseSharedStrings } from './read-shared-strings.js';
|
|
45
10
|
export function* readSheetRows(data, options = {}) {
|
|
46
11
|
const pkg = openPackage(data, options.maxUncompressedBytes);
|
|
47
12
|
const chosen = pickSheet(pkg.sheets, options.sheet);
|
|
48
13
|
const sheetXml = pkg.sheetXml(chosen.relId);
|
|
49
|
-
// The sheet is named but its part is missing (a truncated or foreign package) — it has no rows.
|
|
50
14
|
if (sheetXml === undefined)
|
|
51
15
|
return;
|
|
52
16
|
yield* scanSheet(sheetXml, pkg.sharedStrings, pkg.xfStyles, new Set(), []);
|
|
53
17
|
}
|
|
54
|
-
/**
|
|
55
|
-
* Stream every worksheet of an `.xlsx` package in workbook order, without building the workbook
|
|
56
|
-
* model. Each yielded {@link StreamedSheet} carries the declared sheet name and lets the caller
|
|
57
|
-
* stream that sheet's rows and read its hidden-column and merge summaries — the streaming analogue
|
|
58
|
-
* of walking `readXlsx(data).worksheets`.
|
|
59
|
-
*
|
|
60
|
-
* @param data The raw `.xlsx` bytes.
|
|
61
|
-
* @param options The inflate bound (see {@link ReadXlsxOptions}).
|
|
62
|
-
* @throws {UnsupportedFormatError} if the input is not a readable `.xlsx` package (a legacy `.xls`, a
|
|
63
|
-
* binary `.xlsb`, or an unrecognised/non-ZIP blob — branch on `.format`).
|
|
64
|
-
* @throws {PackageReadError} if the input is a ZIP that cannot be unpacked — a corrupt or
|
|
65
|
-
* truncated archive, or one exceeding the inflate bound (a probable zip bomb).
|
|
66
|
-
*/
|
|
67
18
|
export function* readWorkbookStream(data, options = {}) {
|
|
68
19
|
const pkg = openPackage(data, options.maxUncompressedBytes);
|
|
69
20
|
for (const sheet of pkg.sheets) {
|
|
70
|
-
// A named sheet whose part is missing (truncated/foreign package) still surfaces — with no rows,
|
|
71
|
-
// no hidden columns, and no merges — rather than vanishing from the workbook's sheet list.
|
|
72
21
|
const xml = pkg.sheetXml(sheet.relId) ?? '';
|
|
73
22
|
yield new StreamedSheetReader(sheet.name, xml, pkg.sharedStrings, pkg.xfStyles);
|
|
74
23
|
}
|
|
@@ -77,9 +26,6 @@ function openPackage(data, maxUncompressedBytes) {
|
|
|
77
26
|
const cap = maxUncompressedBytes ?? DEFAULT_MAX_UNCOMPRESSED;
|
|
78
27
|
const { partText: text } = packageAccessors(inflateSpreadsheetPackage(data, cap));
|
|
79
28
|
const workbookXml = text('xl/workbook.xml');
|
|
80
|
-
// A binary `.xlsb` is a workbook this library *can* read — just not through here. Row streaming is
|
|
81
|
-
// built on the XML worksheet parser, so the binary cell table has no streaming path yet; say so,
|
|
82
|
-
// rather than reporting the format as unreadable when `readXlsx` would take the very same bytes.
|
|
83
29
|
if (workbookXml === undefined) {
|
|
84
30
|
throw unsupportedWorkbookPart(text, 'the binary .xlsb format (BIFF12) cannot be row-streamed yet; read it with readXlsx or readXlsb');
|
|
85
31
|
}
|
|
@@ -115,10 +61,6 @@ function pickSheet(sheets, selector) {
|
|
|
115
61
|
throw new AuthoringError(`no worksheet named ${JSON.stringify(selector)}`);
|
|
116
62
|
return sheet;
|
|
117
63
|
}
|
|
118
|
-
// A single worksheet exposed by readWorkbookStream. Its rows() re-scans on each call (a fresh pass,
|
|
119
|
-
// so it is safely re-iterable); the hidden-column and merge accessors reuse a completed scan or, if
|
|
120
|
-
// the rows were never drained, drive one of their own. The hidden/merge state is filled in by the
|
|
121
|
-
// same scanSheet pass that yields the rows.
|
|
122
64
|
class StreamedSheetReader {
|
|
123
65
|
name;
|
|
124
66
|
#xml;
|
|
@@ -148,37 +90,19 @@ class StreamedSheetReader {
|
|
|
148
90
|
this.#ensureScanned();
|
|
149
91
|
return this.#merges;
|
|
150
92
|
}
|
|
151
|
-
// Drain a scan purely for its summaries when the caller reads them without (or before) iterating
|
|
152
|
-
// rows. A completed row iteration already set #scanned, so this re-scans nothing in the common
|
|
153
|
-
// streaming idiom.
|
|
154
93
|
#ensureScanned() {
|
|
155
94
|
if (this.#scanned)
|
|
156
95
|
return;
|
|
157
96
|
for (const _row of this.rows()) {
|
|
158
|
-
// The rows themselves are irrelevant here — we only want the hidden/merge side effects.
|
|
159
97
|
}
|
|
160
98
|
}
|
|
161
99
|
}
|
|
162
|
-
// A formatted-but-empty `<c/>` is expanded to open+close so it finalises once on close, matching
|
|
163
|
-
// the buffered reader; the text-bearing `<f/>`/`<v/>`/`<t/>` are excluded so an empty one never
|
|
164
|
-
// commits (their close captures text, which an empty tag has none of).
|
|
165
100
|
const CELL_EMPTY_CLOSE = new Set(['c']);
|
|
166
|
-
// Pull the sheet XML through the event stream, yielding a StreamedRow at each `</row>`, while
|
|
167
|
-
// recording the sheet's hidden columns (from `<col hidden>`, before <sheetData>) and merged ranges
|
|
168
|
-
// (from `<mergeCells>`, after <sheetData>) into the caller-supplied collectors. The cell state
|
|
169
|
-
// mirrors the buffered reader's `parseWorksheet` (same self-closing-`<c/>` handling, same capture
|
|
170
|
-
// flags), but commits into a row buffer that is handed off and discarded per row rather than into a
|
|
171
|
-
// persistent Worksheet — that hand-off is what bounds retained memory to one row.
|
|
172
101
|
function* scanSheet(xml, sharedStrings, xfStyles, hiddenColumns, merges) {
|
|
173
102
|
let rowNumber = 0;
|
|
174
103
|
let lastRow = 0;
|
|
175
104
|
let rowHidden = false;
|
|
176
105
|
let cells = [];
|
|
177
|
-
// The in-flight `<c>`, gathered exactly as the buffered reader gathers it. This reader drives the
|
|
178
|
-
// same beginCell/setFormula/setValue/appendText methods, then takes only the cell's plain decoded
|
|
179
|
-
// value (via decode) — never the shared-formula / data-table resolution the buffered finalize adds,
|
|
180
|
-
// which a data read does not want. Rich `<r>` runs are deliberately not opened here, so a rich
|
|
181
|
-
// inline string flattens to its concatenated text as a streamed value always has.
|
|
182
106
|
const cell = new CellAccumulator();
|
|
183
107
|
let inInlineString = false;
|
|
184
108
|
let capture = false;
|
|
@@ -188,8 +112,6 @@ function* scanSheet(xml, sharedStrings, xfStyles, hiddenColumns, merges) {
|
|
|
188
112
|
return;
|
|
189
113
|
const style = cell.styleIndex >= 0 ? xfStyles[cell.styleIndex] : undefined;
|
|
190
114
|
const value = cell.decode(sharedStrings, style);
|
|
191
|
-
// A blank or purely style-only cell decodes to null; a data read wants only cells that carry
|
|
192
|
-
// something (a formula object, an empty string, a false, and a 0 all count — only null drops).
|
|
193
115
|
if (value !== null) {
|
|
194
116
|
const { col, ref } = cell;
|
|
195
117
|
cells.push(style ? { col, address: ref, value, style } : { col, address: ref, value });
|
|
@@ -243,7 +165,6 @@ function* scanSheet(xml, sharedStrings, xfStyles, hiddenColumns, merges) {
|
|
|
243
165
|
capture = false;
|
|
244
166
|
continue;
|
|
245
167
|
}
|
|
246
|
-
// close
|
|
247
168
|
const local = localName(event.name);
|
|
248
169
|
switch (local) {
|
|
249
170
|
case 'f':
|
|
@@ -270,9 +191,6 @@ function* scanSheet(xml, sharedStrings, xfStyles, hiddenColumns, merges) {
|
|
|
270
191
|
capture = false;
|
|
271
192
|
}
|
|
272
193
|
}
|
|
273
|
-
// Record the hidden columns a `<col min max hidden>` element declares. The span is clamped to the
|
|
274
|
-
// format's column ceiling and gathered into a Set, so even a hostile file full of full-width hidden
|
|
275
|
-
// spans can add at most MAX_COLUMN distinct entries — never an unbounded allocation.
|
|
276
194
|
function collectHiddenColumn(attrs, hiddenColumns) {
|
|
277
195
|
if (attrs.hidden !== '1' && attrs.hidden !== 'true')
|
|
278
196
|
return;
|
|
@@ -1,19 +1,9 @@
|
|
|
1
|
-
// Parser for the shared-string table (`xl/sharedStrings.xml`), the pool that `t="s"` cells index into.
|
|
2
|
-
// Split out of read.ts beside its sibling parsers (read-styles.ts, rich-runs.ts) so read.ts stays
|
|
3
|
-
// orchestration; the run structure it decodes is owned by RunAccumulator, shared with inline strings.
|
|
4
1
|
import { localName, parseXml } from '../../xml/xml-read.js';
|
|
5
2
|
import { RunAccumulator } from './rich-runs.js';
|
|
6
|
-
// Shared strings resolve `t="s"` cells. Each `<si>` is one entry: a plain `<si><t>…</t>` decodes to a
|
|
7
|
-
// string, while a rich `<si><r><rPr>…</rPr><t>…</t></r>…` decodes to a {@link RichTextValue} whose runs
|
|
8
|
-
// carry their per-run fonts — so rich text Excel pooled reads back formatted, not flattened to text.
|
|
9
|
-
// The run structure inside an `<si>` is identical to an inline string's `<is>`, so it is parsed the
|
|
10
|
-
// same way (see the inline-run accumulation in `parseWorksheet`).
|
|
11
3
|
export function parseSharedStrings(xml) {
|
|
12
4
|
if (xml === '')
|
|
13
5
|
return [];
|
|
14
6
|
const strings = [];
|
|
15
|
-
// Per-`<si>` accumulation: `plain` gathers a bare `<t>`; `runs` gathers `<r>` runs. An `<si>` is
|
|
16
|
-
// rich the moment it holds one `<r>`, at which point its runs — not `plain` — become the entry.
|
|
17
7
|
let plain = '';
|
|
18
8
|
const runs = new RunAccumulator();
|
|
19
9
|
let isRich = false;
|
|
@@ -52,7 +42,6 @@ export function parseSharedStrings(xml) {
|
|
|
52
42
|
const local = localName(name);
|
|
53
43
|
switch (local) {
|
|
54
44
|
case 't':
|
|
55
|
-
// A `<t>` inside a run is that run's text; a bare `<t>` directly in the `<si>` is plain.
|
|
56
45
|
if (!runs.appendText(text))
|
|
57
46
|
plain += text;
|
|
58
47
|
capture = false;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type Font } from '../../core/style.ts';
|
|
1
|
+
import { type Font, type TableStyleTable } from '../../core/style.ts';
|
|
2
2
|
import { type XmlAttributes } from '../../xml/xml-read.ts';
|
|
3
3
|
import { type StyleTable } from '../style/xf-style.ts';
|
|
4
4
|
export type FontDraft = {
|
|
@@ -6,3 +6,27 @@ export type FontDraft = {
|
|
|
6
6
|
};
|
|
7
7
|
export declare function parseStyleTable(xml: string): StyleTable;
|
|
8
8
|
export declare function applyFontChild(draft: FontDraft, local: string, attrs: XmlAttributes): void;
|
|
9
|
+
/**
|
|
10
|
+
* Extract the custom indexed-color palette (`<colors><indexedColors>`) from styles.xml as verbatim
|
|
11
|
+
* `<rgbColor rgb="…"/>` fragments, or an empty list when the file rides the default palette. Kept raw
|
|
12
|
+
* — rather than parsed into RGB and re-serialised — so the exact entries (count, order, casing) a
|
|
13
|
+
* source file declared survive a round-trip and every `indexed="…"` reference keeps its RGB.
|
|
14
|
+
*/
|
|
15
|
+
export declare function parseIndexedColors(stylesXml: string): string[];
|
|
16
|
+
/**
|
|
17
|
+
* Extract the most-recently-used colour swatches (`<colors><mruColors>`) from styles.xml as verbatim
|
|
18
|
+
* `<color .../>` fragments, or an empty list when the file declares none. Kept raw for the same reason
|
|
19
|
+
* the indexed palette is: the list is the author's own working set of colours and the model has no
|
|
20
|
+
* use for its contents, only for not losing them.
|
|
21
|
+
*/
|
|
22
|
+
export declare function parseMruColors(stylesXml: string): string[];
|
|
23
|
+
/**
|
|
24
|
+
* Extract the `<tableStyles>` block from styles.xml: each `<tableStyle>` definition verbatim, plus the
|
|
25
|
+
* container's nominated `defaultTableStyle`/`defaultPivotStyle`. See {@link TableStyleTable} for why
|
|
26
|
+
* the definitions stay raw while the two names are decoded.
|
|
27
|
+
*
|
|
28
|
+
* A file with no such block — or with the self-closing `count="0"` container Excel writes when it has
|
|
29
|
+
* only defaults to state — yields an empty {@link TableStyleTable.styles} and whichever names it did
|
|
30
|
+
* carry.
|
|
31
|
+
*/
|
|
32
|
+
export declare function parseTableStyles(stylesXml: string): TableStyleTable;
|
|
@@ -1,24 +1,9 @@
|
|
|
1
|
-
// The style-table reader: `xl/styles.xml` in, a flat table of resolved cell formats out. It is a
|
|
2
|
-
// single streaming pass over the shared sub-tables (`<numFmts>`, `<fills>`, `<fonts>`, `<borders>`)
|
|
3
|
-
// and the two xf tables (`<cellXfs>`, `<cellStyleXfs>`), flattening the id-indirection so a cell's
|
|
4
|
-
// `s` index maps straight to its facets. A construct it does not recognise is skipped, never guessed.
|
|
5
|
-
//
|
|
6
|
-
// Only the parsing is XML-specific. What an xf resolves *to* — `XfStyle`, the built-in number
|
|
7
|
-
// formats, applying an xf to a cell — is a property of the OOXML style model rather than of its
|
|
8
|
-
// spelling, and lives above both codecs in `../style/xf-style.ts`; the `.xlsb` style reader builds
|
|
9
|
-
// the same table from BIFF12 records.
|
|
10
1
|
import { assignStyleFacets, isBorderStyle, isFillPatternType, isFontScheme, isFontVerticalAlignment, isHorizontalAlignment, isNamedUnderlineStyle, isVerticalAlignment, } from '../../core/style.js';
|
|
11
|
-
import { boolPresent, boolStrict, closeEmptyElements, localName, xmlEvents, } from '../../xml/xml-read.js';
|
|
2
|
+
import { boolPresent, boolStrict, closeEmptyElements, localName, openElements, xmlEvents, } from '../../xml/xml-read.js';
|
|
12
3
|
import { numFmtCodeFor } from '../style/xf-style.js';
|
|
13
|
-
import { parseColor } from './
|
|
14
|
-
// The four sides plus the diagonal — the edge elements a <border> can hold, in the order the
|
|
15
|
-
// schema lists them. This one tuple drives the edge-name union, the membership set (which drives
|
|
16
|
-
// edge parsing without a per-name branch), and the "does any edge carry a style" scan below.
|
|
4
|
+
import { parseColor } from './color-xml.js';
|
|
17
5
|
const BORDER_EDGE_NAMES = ['left', 'right', 'top', 'bottom', 'diagonal'];
|
|
18
6
|
const BORDER_EDGES = new Set(BORDER_EDGE_NAMES);
|
|
19
|
-
// Style-table elements that commit on their close: a bare <font/>/<border/>/<patternFill/>/
|
|
20
|
-
// <gradientFill/>/<xf/> or a self-closing border edge is expanded to open+close so each commits
|
|
21
|
-
// exactly once in onClose, never in a duplicated (and easily-forgotten) self-closing branch.
|
|
22
7
|
const STYLE_EMPTY_CLOSES = new Set([
|
|
23
8
|
'font',
|
|
24
9
|
'border',
|
|
@@ -27,11 +12,6 @@ const STYLE_EMPTY_CLOSES = new Set([
|
|
|
27
12
|
'xf',
|
|
28
13
|
...BORDER_EDGES,
|
|
29
14
|
]);
|
|
30
|
-
// styles.xml is a shared table: <numFmts> defines custom format codes by id, <fills> lists
|
|
31
|
-
// the fills, and <cellXfs> lists the cell formats, each naming a fill and a number format by
|
|
32
|
-
// id. We flatten that indirection into one array — cellXfs index → resolved {fill, numFmt} —
|
|
33
|
-
// so a cell/row/column style index maps straight to its facets. The schema orders <numFmts>
|
|
34
|
-
// and <fills> before <cellXfs>, so both lookups are complete before an xf references them.
|
|
35
15
|
export function parseStyleTable(xml) {
|
|
36
16
|
if (xml === '')
|
|
37
17
|
return { cellXfs: [], namedStyles: [] };
|
|
@@ -40,16 +20,8 @@ export function parseStyleTable(xml) {
|
|
|
40
20
|
let borders = [];
|
|
41
21
|
let numFmtCodes = new Map();
|
|
42
22
|
const xfStyles = [];
|
|
43
|
-
// The named-style layer: <cellStyleXfs> holds the base formats a cell's xfId links to; <cellStyles>
|
|
44
|
-
// labels them by name/builtinId. Parsed in parallel with cellXfs, then zipped and merged below.
|
|
45
23
|
const namedXfs = [];
|
|
46
24
|
let cellStyleNames = [];
|
|
47
|
-
// One streaming pass, but each top-level sub-table drives its own focused sub-parser over the slice
|
|
48
|
-
// of events between its open and close. The schema orders the shared tables (<numFmts>, <fonts>,
|
|
49
|
-
// <fills>, <borders>) before the xf tables, so their results are complete before an <xf> resolves
|
|
50
|
-
// against them. Every recognised container name is plural and unique to the styleSheet root — none
|
|
51
|
-
// appears inside a <dxf>'s singular <font>/<fill>/<border> children — so skipping an unrecognised
|
|
52
|
-
// section here drops exactly what the old flat pass gated off with its `in*` flags.
|
|
53
25
|
const events = closeEmptyElements(xmlEvents(xml), STYLE_EMPTY_CLOSES);
|
|
54
26
|
let next = events.next();
|
|
55
27
|
while (next.done !== true) {
|
|
@@ -81,18 +53,12 @@ export function parseStyleTable(xml) {
|
|
|
81
53
|
}
|
|
82
54
|
next = events.next();
|
|
83
55
|
}
|
|
84
|
-
// Layer each cellXfs entry over the named style its xfId links to: a facet the direct format sets
|
|
85
|
-
// wins; one it leaves unset falls through to the named style. The xfId is carried through so the
|
|
86
|
-
// link survives a re-write. A draft only holds keys for facets it actually set, so the spread merge
|
|
87
|
-
// takes the named base and lets the direct entry override exactly what it names.
|
|
88
56
|
const cellXfs = xfStyles.map((xf) => {
|
|
89
57
|
if (xf.xfId === undefined)
|
|
90
58
|
return xf;
|
|
91
59
|
const named = namedXfs[xf.xfId];
|
|
92
60
|
return named === undefined ? xf : { ...named, ...xf };
|
|
93
61
|
});
|
|
94
|
-
// Zip the resolved cellStyleXfs facets with their cellStyles name/builtinId into the model's named
|
|
95
|
-
// styles, index for index (a cellStyle's xfId is its cellStyleXfs index).
|
|
96
62
|
const namedStyles = namedXfs.map((xf, index) => {
|
|
97
63
|
const label = cellStyleNames.find((entry) => entry.xfId === index);
|
|
98
64
|
const style = {};
|
|
@@ -103,15 +69,9 @@ export function parseStyleTable(xml) {
|
|
|
103
69
|
style.builtinId = label.builtinId;
|
|
104
70
|
return style;
|
|
105
71
|
});
|
|
106
|
-
// Font 0 is the workbook's declared default, so it is carried out whole as well as flattened onto
|
|
107
|
-
// the xfs that name it — see {@link StyleTable.defaultFont}.
|
|
108
72
|
const defaultFont = fonts[0];
|
|
109
73
|
return defaultFont === undefined ? { cellXfs, namedStyles } : { cellXfs, namedStyles, defaultFont };
|
|
110
74
|
}
|
|
111
|
-
// Pull events off the shared stream up to — and consuming — the close of `container`, yielding only
|
|
112
|
-
// those strictly inside it. A sub-table parser loops this to completion (never breaking), so it drives
|
|
113
|
-
// its own small state machine over exactly its section without closing the underlying generator, and
|
|
114
|
-
// the outer pass resumes at the element after the container's close.
|
|
115
75
|
function* until(events, container) {
|
|
116
76
|
let next = events.next();
|
|
117
77
|
while (next.done !== true) {
|
|
@@ -122,8 +82,6 @@ function* until(events, container) {
|
|
|
122
82
|
next = events.next();
|
|
123
83
|
}
|
|
124
84
|
}
|
|
125
|
-
// <numFmts> entries are self-closing, so they are read on open. A code with no id, or the General
|
|
126
|
-
// id 0, contributes nothing.
|
|
127
85
|
function parseNumFmts(events) {
|
|
128
86
|
const codes = new Map();
|
|
129
87
|
for (const event of until(events, 'numFmts')) {
|
|
@@ -142,7 +100,6 @@ function parseFonts(events) {
|
|
|
142
100
|
for (const event of until(events, 'fonts')) {
|
|
143
101
|
if (event.kind === 'open') {
|
|
144
102
|
const local = localName(event.name);
|
|
145
|
-
// A <font>'s children are self-closing, so they are read here on open.
|
|
146
103
|
if (local === 'font')
|
|
147
104
|
fontDraft = {};
|
|
148
105
|
else if (fontDraft !== null)
|
|
@@ -160,9 +117,6 @@ function parseFills(events) {
|
|
|
160
117
|
let pattern = '';
|
|
161
118
|
let fgColor;
|
|
162
119
|
let bgColor;
|
|
163
|
-
// A gradient fill accumulates from <gradientFill> open to close; its stops fill in as <stop>/<color>
|
|
164
|
-
// pairs arrive. `fillSlotAt` marks where in `fills` the current <fill> began, so its close can keep a
|
|
165
|
-
// slot even when the fill body was neither a pattern nor a gradient — index alignment is load-bearing.
|
|
166
120
|
let gradientDraft = null;
|
|
167
121
|
let fillSlotAt = -1;
|
|
168
122
|
for (const event of until(events, 'fills')) {
|
|
@@ -170,9 +124,6 @@ function parseFills(events) {
|
|
|
170
124
|
const attrs = event.attrs;
|
|
171
125
|
switch (localName(event.name)) {
|
|
172
126
|
case 'fill':
|
|
173
|
-
// Mark where this <fill> starts so its close can guarantee exactly one slot — a fill body
|
|
174
|
-
// that is neither <patternFill> nor <gradientFill> (or a gradient we could not parse) must
|
|
175
|
-
// still consume an id, or every later fill index shifts and cells mis-resolve their fill.
|
|
176
127
|
fillSlotAt = fills.length;
|
|
177
128
|
break;
|
|
178
129
|
case 'patternFill':
|
|
@@ -206,7 +157,6 @@ function parseFills(events) {
|
|
|
206
157
|
}
|
|
207
158
|
break;
|
|
208
159
|
case 'color':
|
|
209
|
-
// The colour of the open <stop>; committed to a GradientStop when the stop closes.
|
|
210
160
|
if (gradientDraft !== null)
|
|
211
161
|
gradientDraft.stopColor = parseColor(attrs);
|
|
212
162
|
break;
|
|
@@ -235,8 +185,6 @@ function parseFills(events) {
|
|
|
235
185
|
}
|
|
236
186
|
break;
|
|
237
187
|
case 'fill':
|
|
238
|
-
// Backstop the slot: if this <fill>'s body pushed nothing (unparsed/unknown content), keep an
|
|
239
|
-
// empty slot so id alignment holds and later fills still resolve to the right cells.
|
|
240
188
|
if (fills.length === fillSlotAt)
|
|
241
189
|
fills.push(undefined);
|
|
242
190
|
break;
|
|
@@ -248,7 +196,6 @@ function parseFills(events) {
|
|
|
248
196
|
function parseBorders(events) {
|
|
249
197
|
const borders = [];
|
|
250
198
|
let borderDraft = null;
|
|
251
|
-
// Which edge of the current border a nested <color> belongs to; null between edges.
|
|
252
199
|
let currentEdge = null;
|
|
253
200
|
for (const event of until(events, 'borders')) {
|
|
254
201
|
if (event.kind === 'open') {
|
|
@@ -263,9 +210,6 @@ function parseBorders(events) {
|
|
|
263
210
|
borderDraft.diagonalDown = true;
|
|
264
211
|
}
|
|
265
212
|
else if (borderDraft !== null) {
|
|
266
|
-
// A border's edges and their <color> children are all read on open (each is self-closing bar a
|
|
267
|
-
// coloured edge, whose colour child is itself self-closing). An edge whose style is absent or
|
|
268
|
-
// an unrecognised token is dropped — the side simply carries no border.
|
|
269
213
|
if (BORDER_EDGES.has(local)) {
|
|
270
214
|
if (attrs.style !== undefined && isBorderStyle(attrs.style)) {
|
|
271
215
|
currentEdge = local;
|
|
@@ -292,39 +236,28 @@ function parseBorders(events) {
|
|
|
292
236
|
}
|
|
293
237
|
}
|
|
294
238
|
else if (borderDraft !== null && BORDER_EDGES.has(local)) {
|
|
295
|
-
// A coloured edge closes after its <color> child; drop the edge context so a stray later
|
|
296
|
-
// <color> cannot attach to it.
|
|
297
239
|
currentEdge = null;
|
|
298
240
|
}
|
|
299
241
|
}
|
|
300
242
|
}
|
|
301
243
|
return borders;
|
|
302
244
|
}
|
|
303
|
-
// Both <cellXfs> and <cellStyleXfs> hold <xf> with identical structure; they differ only in which
|
|
304
|
-
// table the result lands in and whether an xfId link is meaningful (only cellXfs entries link to a
|
|
305
|
-
// named style). One parser serves both, told by `container` which it is reading.
|
|
306
245
|
function parseXfTable(events, container, deps) {
|
|
307
246
|
const xfs = [];
|
|
308
247
|
const captureXfId = container === 'cellXfs';
|
|
309
|
-
// The xf being read; held from open to close so its <alignment>/<protection> children can attach
|
|
310
|
-
// before it is committed. null outside an <xf>.
|
|
311
248
|
let pendingXf = null;
|
|
312
249
|
for (const event of until(events, container)) {
|
|
313
250
|
if (event.kind === 'open') {
|
|
314
251
|
const local = localName(event.name);
|
|
315
252
|
if (local === 'xf') {
|
|
316
|
-
// Hold the xf open until its close so an <alignment>/<protection> child can attach first; a
|
|
317
|
-
// self-closing <xf/> is expanded to a close, so it commits there too, child-free.
|
|
318
253
|
pendingXf = resolveXf(event.attrs, deps, captureXfId);
|
|
319
254
|
}
|
|
320
255
|
else if (pendingXf !== null && local === 'alignment') {
|
|
321
|
-
// An xf's <alignment> child arrives before the xf closes; attach it to the pending xf.
|
|
322
256
|
const alignment = parseAlignment(event.attrs);
|
|
323
257
|
if (alignment !== undefined)
|
|
324
258
|
pendingXf.alignment = alignment;
|
|
325
259
|
}
|
|
326
260
|
else if (pendingXf !== null && local === 'protection') {
|
|
327
|
-
// An xf's <protection> child likewise arrives before the xf closes.
|
|
328
261
|
const protection = parseProtection(event.attrs);
|
|
329
262
|
if (protection !== undefined)
|
|
330
263
|
pendingXf.protection = protection;
|
|
@@ -337,18 +270,12 @@ function parseXfTable(events, container, deps) {
|
|
|
337
270
|
}
|
|
338
271
|
return xfs;
|
|
339
272
|
}
|
|
340
|
-
// Resolve an <xf>'s facet ids against the shared sub-tables into a draft. `captureXfId` is set only
|
|
341
|
-
// for cellXfs entries, the sole table whose xfId links to a named style.
|
|
342
273
|
function resolveXf(attrs, deps, captureXfId) {
|
|
343
274
|
const fillId = Number(attrs.fillId);
|
|
344
275
|
const fill = Number.isInteger(fillId) ? deps.fills[fillId] : undefined;
|
|
345
276
|
const fontId = Number(attrs.fontId);
|
|
346
|
-
// Font id 0 is the workbook default font (a real Calibri-11-style face), not an absence — unlike
|
|
347
|
-
// border id 0, which is a genuinely empty border. So an xf naming font 0 resolves to that default
|
|
348
|
-
// face, giving every cell a concrete font to render.
|
|
349
277
|
const font = Number.isInteger(fontId) ? deps.fonts[fontId] : undefined;
|
|
350
278
|
const borderId = Number(attrs.borderId);
|
|
351
|
-
// Border id 0 is the empty default; only a custom border (id > 0) is an explicit one.
|
|
352
279
|
const border = Number.isInteger(borderId) && borderId > 0 ? deps.borders[borderId] : undefined;
|
|
353
280
|
const numFmt = resolveNumFmt(attrs.numFmtId, deps.numFmtCodes);
|
|
354
281
|
const draft = {};
|
|
@@ -360,12 +287,8 @@ function resolveXf(attrs, deps, captureXfId) {
|
|
|
360
287
|
draft.font = font;
|
|
361
288
|
if (border)
|
|
362
289
|
draft.border = border;
|
|
363
|
-
// The quote-prefix flag is an attribute on the xf itself (no shared sub-table); carry it only when
|
|
364
|
-
// set so an ordinary cell does not gain a spurious `quotePrefix: false`.
|
|
365
290
|
if (boolStrict(attrs.quotePrefix))
|
|
366
291
|
draft.quotePrefix = true;
|
|
367
|
-
// A cellXfs entry's xfId links it to a named style; capture it only when it points beyond the Normal
|
|
368
|
-
// default (0), so an ordinary cell carries no spurious named-style link.
|
|
369
292
|
if (captureXfId && attrs.xfId !== undefined) {
|
|
370
293
|
const xfId = Number(attrs.xfId);
|
|
371
294
|
if (Number.isInteger(xfId) && xfId > 0)
|
|
@@ -373,8 +296,6 @@ function resolveXf(attrs, deps, captureXfId) {
|
|
|
373
296
|
}
|
|
374
297
|
return draft;
|
|
375
298
|
}
|
|
376
|
-
// A <cellStyle> (inside <cellStyles>) names a cellStyleXfs entry by xfId; it is self-closing, so it
|
|
377
|
-
// is read on open.
|
|
378
299
|
function parseCellStyles(events) {
|
|
379
300
|
const names = [];
|
|
380
301
|
for (const event of until(events, 'cellStyles')) {
|
|
@@ -396,9 +317,6 @@ function parseCellStyles(events) {
|
|
|
396
317
|
}
|
|
397
318
|
return names;
|
|
398
319
|
}
|
|
399
|
-
// A <font> child element sets one facet on the draft. Boolean flags honour their `val`: a
|
|
400
|
-
// bare tag or val="1"/"true" is on, val="0"/"false" is off (an explicit-false flag is not
|
|
401
|
-
// truthy merely because the tag is present). An unrecognised child is ignored.
|
|
402
320
|
export function applyFontChild(draft, local, attrs) {
|
|
403
321
|
switch (local) {
|
|
404
322
|
case 'b':
|
|
@@ -414,10 +332,6 @@ export function applyFontChild(draft, local, attrs) {
|
|
|
414
332
|
draft.outline = boolPresent(attrs.val);
|
|
415
333
|
break;
|
|
416
334
|
case 'u':
|
|
417
|
-
// A bare <u/> is a single underline; a named style (single/double/…) carries through; but
|
|
418
|
-
// val="none" is the explicit ABSENCE of an underline, so it must read back falsy — not the
|
|
419
|
-
// truthy string "none" that a consumer's `if (font.underline)` would mistake for underlined. An
|
|
420
|
-
// unrecognised token keeps the "is underlined" fact but drops the unknown style (a plain true).
|
|
421
335
|
draft.underline =
|
|
422
336
|
attrs.val === undefined
|
|
423
337
|
? true
|
|
@@ -440,7 +354,6 @@ export function applyFontChild(draft, local, attrs) {
|
|
|
440
354
|
case 'color':
|
|
441
355
|
draft.color = parseColor(attrs);
|
|
442
356
|
break;
|
|
443
|
-
// `<name>` in a styles `<font>`, `<rFont>` in a rich-text run's `<rPr>` — the same font face.
|
|
444
357
|
case 'name':
|
|
445
358
|
case 'rFont':
|
|
446
359
|
if (attrs.val !== undefined)
|
|
@@ -466,15 +379,10 @@ export function applyFontChild(draft, local, attrs) {
|
|
|
466
379
|
break;
|
|
467
380
|
}
|
|
468
381
|
}
|
|
469
|
-
// An xf's numFmtId resolves against the custom codes first, then the built-in table; the
|
|
470
|
-
// General format (id 0) and any unrecognised id mean the cell carries no explicit format.
|
|
471
382
|
function resolveNumFmt(raw, custom) {
|
|
472
383
|
return raw === undefined ? undefined : numFmtCodeFor(Number(raw), custom);
|
|
473
384
|
}
|
|
474
385
|
function toFill(pattern, fgColor, bgColor) {
|
|
475
|
-
// `none` (and an absent patternType) is the absence of a fill; an unrecognised token is dropped the
|
|
476
|
-
// same way — like the border-edge style above — so a foreign pattern we do not model leaves the cell
|
|
477
|
-
// unfilled rather than propagating a token the writer would later re-emit unvalidated.
|
|
478
386
|
if (!isFillPatternType(pattern) || pattern === 'none')
|
|
479
387
|
return undefined;
|
|
480
388
|
return {
|
|
@@ -484,8 +392,6 @@ function toFill(pattern, fgColor, bgColor) {
|
|
|
484
392
|
...(bgColor ? { bgColor } : {}),
|
|
485
393
|
};
|
|
486
394
|
}
|
|
487
|
-
// Copy the numeric <gradientFill> attributes (degree; the path insets) onto a gradient draft, keeping
|
|
488
|
-
// only the finite ones so an absent or malformed attribute leaves the field its OOXML default (unset).
|
|
489
395
|
function assignGradientNumbers(fill, attrs) {
|
|
490
396
|
for (const key of ['degree', 'left', 'right', 'top', 'bottom']) {
|
|
491
397
|
const value = Number(attrs[key]);
|
|
@@ -493,22 +399,14 @@ function assignGradientNumbers(fill, attrs) {
|
|
|
493
399
|
fill[key] = value;
|
|
494
400
|
}
|
|
495
401
|
}
|
|
496
|
-
// An accumulated border with no styled edge and no diagonal direction is the empty default:
|
|
497
|
-
// it carries nothing, so it resolves to undefined rather than an all-empty Border object.
|
|
498
402
|
function borderToStyle(draft) {
|
|
499
403
|
const hasEdge = BORDER_EDGE_NAMES.some((edge) => draft[edge] !== undefined);
|
|
500
404
|
if (!hasEdge && draft.diagonalUp === undefined && draft.diagonalDown === undefined)
|
|
501
405
|
return undefined;
|
|
502
406
|
return draft;
|
|
503
407
|
}
|
|
504
|
-
// Read an <alignment> element's attributes into an Alignment, keeping only facets that differ
|
|
505
|
-
// from the default. Boolean flags honour their parsed value — wrapText="0" is off, so it must
|
|
506
|
-
// not fabricate a { wrapText: false } alignment — and an element carrying only defaults yields
|
|
507
|
-
// undefined rather than an empty alignment object.
|
|
508
408
|
function parseAlignment(attrs) {
|
|
509
409
|
const out = {};
|
|
510
|
-
// `general` is the default and reads back as no explicit horizontal alignment; an unrecognised
|
|
511
|
-
// token (like an out-of-enum vertical one) is dropped rather than trusted into the model.
|
|
512
410
|
if (attrs.horizontal !== undefined &&
|
|
513
411
|
attrs.horizontal !== 'general' &&
|
|
514
412
|
isHorizontalAlignment(attrs.horizontal)) {
|
|
@@ -537,10 +435,6 @@ function parseAlignment(attrs) {
|
|
|
537
435
|
}
|
|
538
436
|
return Object.keys(out).length > 0 ? out : undefined;
|
|
539
437
|
}
|
|
540
|
-
// Read a <protection> element into a Protection, keeping only facets that differ from the OOXML
|
|
541
|
-
// default. `locked` defaults to TRUE, so only an explicit `locked="0"` carries information (an
|
|
542
|
-
// unlocked cell) — a default or explicit-true cell must not read back as { locked: true }; `hidden`
|
|
543
|
-
// defaults to false, so only `hidden="1"` is carried. An element with only defaults yields undefined.
|
|
544
438
|
function parseProtection(attrs) {
|
|
545
439
|
const out = {};
|
|
546
440
|
if (attrs.locked === '0' || attrs.locked === 'false')
|
|
@@ -549,3 +443,62 @@ function parseProtection(attrs) {
|
|
|
549
443
|
out.hidden = true;
|
|
550
444
|
return Object.keys(out).length > 0 ? out : undefined;
|
|
551
445
|
}
|
|
446
|
+
export function parseIndexedColors(stylesXml) {
|
|
447
|
+
return elementFragments(stylesXml, 'indexedColors', 'rgbColor');
|
|
448
|
+
}
|
|
449
|
+
export function parseMruColors(stylesXml) {
|
|
450
|
+
return elementFragments(stylesXml, 'mruColors', 'color');
|
|
451
|
+
}
|
|
452
|
+
export function parseTableStyles(stylesXml) {
|
|
453
|
+
const styles = elementFragments(stylesXml, 'tableStyles', 'tableStyle');
|
|
454
|
+
const table = { styles };
|
|
455
|
+
for (const { attrs } of openElements(stylesXml, 'tableStyles')) {
|
|
456
|
+
if (attrs.defaultTableStyle !== undefined)
|
|
457
|
+
table.defaultTableStyle = attrs.defaultTableStyle;
|
|
458
|
+
if (attrs.defaultPivotStyle !== undefined)
|
|
459
|
+
table.defaultPivotStyle = attrs.defaultPivotStyle;
|
|
460
|
+
break;
|
|
461
|
+
}
|
|
462
|
+
const namespaces = fragmentNamespaces(stylesXml, styles);
|
|
463
|
+
if (namespaces.length > 0)
|
|
464
|
+
table.namespaces = namespaces;
|
|
465
|
+
return table;
|
|
466
|
+
}
|
|
467
|
+
function fragmentNamespaces(stylesXml, fragments) {
|
|
468
|
+
if (fragments.length === 0)
|
|
469
|
+
return [];
|
|
470
|
+
const declared = new Map();
|
|
471
|
+
const ignorable = new Set();
|
|
472
|
+
for (const { attrs } of openElements(stylesXml, 'styleSheet')) {
|
|
473
|
+
for (const [name, value] of Object.entries(attrs)) {
|
|
474
|
+
if (name.startsWith('xmlns:'))
|
|
475
|
+
declared.set(name.slice('xmlns:'.length), value);
|
|
476
|
+
}
|
|
477
|
+
for (const prefix of (attrs['mc:Ignorable'] ?? '').split(/\s+/)) {
|
|
478
|
+
if (prefix !== '')
|
|
479
|
+
ignorable.add(prefix);
|
|
480
|
+
}
|
|
481
|
+
break;
|
|
482
|
+
}
|
|
483
|
+
const used = new Set();
|
|
484
|
+
for (const fragment of fragments) {
|
|
485
|
+
for (const match of fragment.matchAll(/[\s</]([A-Za-z_][\w.-]*):[A-Za-z_]/g)) {
|
|
486
|
+
used.add(match[1]);
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
return [...used]
|
|
490
|
+
.filter((prefix) => declared.has(prefix))
|
|
491
|
+
.map((prefix) => ({
|
|
492
|
+
prefix,
|
|
493
|
+
uri: declared.get(prefix),
|
|
494
|
+
ignorable: ignorable.has(prefix),
|
|
495
|
+
}));
|
|
496
|
+
}
|
|
497
|
+
function elementFragments(xml, container, child) {
|
|
498
|
+
const block = new RegExp(`<${container}\\b[^>]*>([\\s\\S]*?)</${container}>`).exec(xml);
|
|
499
|
+
if (block === null)
|
|
500
|
+
return [];
|
|
501
|
+
const inner = block[1] ?? '';
|
|
502
|
+
const pattern = new RegExp(`<${child}\\b[^>]*/>|<${child}\\b[^>]*>[\\s\\S]*?</${child}>`, 'g');
|
|
503
|
+
return [...inner.matchAll(pattern)].map((m) => m[0] ?? '');
|
|
504
|
+
}
|