@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.
- package/README.md +21 -0
- package/dist/core/address.js +0 -33
- package/dist/core/autofilter.js +0 -9
- package/dist/core/cell.js +0 -131
- package/dist/core/color-resolution.js +0 -60
- package/dist/core/column.js +0 -60
- 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.js +0 -45
- 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 -48
- 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.js +0 -66
- 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.js +0 -40
- package/dist/core/theme.js +0 -85
- package/dist/core/value.js +0 -94
- package/dist/core/workbook-protection.js +0 -13
- package/dist/core/workbook.js +0 -380
- package/dist/core/worksheet-model.js +2 -23
- package/dist/core/worksheet.js +7 -506
- 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.js +0 -6
- 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 +0 -18
- 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 +8 -9
- 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
package/dist/io/xlsb/read.js
CHANGED
|
@@ -1,18 +1,3 @@
|
|
|
1
|
-
// The buffered `.xlsb` reader: a binary OPC package in, a {@link Workbook} model out.
|
|
2
|
-
//
|
|
3
|
-
// An `.xlsb` is the *same* OPC/ZIP container and the *same* relationship graph as an `.xlsx` — the
|
|
4
|
-
// workbook part points at its sheets, its shared strings, and its style sheet through ordinary
|
|
5
|
-
// `.rels` XML. Only the office-document parts differ: `xl/workbook.bin`, `xl/worksheets/sheetN.bin`
|
|
6
|
-
// and friends are BIFF12 record streams instead of XML. So this module reuses the container layer
|
|
7
|
-
// wholesale (`../opc/`, the bounded inflater) and swaps only the part parsers, which is
|
|
8
|
-
// exactly the "two codecs over one model" shape the format note argues for.
|
|
9
|
-
//
|
|
10
|
-
// The model produced is the one `readXlsx` produces, not a parallel one: the same `Workbook`, the
|
|
11
|
-
// same `XfStyle` table, the same cells. That is what lets a caller convert between the two forms —
|
|
12
|
-
// and what the corpus asserts, by reading a workbook Excel saved in both forms and comparing.
|
|
13
|
-
//
|
|
14
|
-
// Not yet decoded (each its own slice of work, none silently wrong): rich-text runs, tables, pivots,
|
|
15
|
-
// and conditional formatting.
|
|
16
1
|
import { unmangleFunctions } from '../../core/formula.js';
|
|
17
2
|
import { INTERNAL } from '../../core/internal.js';
|
|
18
3
|
import { Workbook } from '../../core/workbook.js';
|
|
@@ -27,26 +12,11 @@ import { parseStyleTable } from './read-styles.js';
|
|
|
27
12
|
import { parseWorksheet } from './read-worksheet.js';
|
|
28
13
|
import { readRecords } from './record-stream.js';
|
|
29
14
|
import { BRT } from './record-types.js';
|
|
30
|
-
/** The office-document part every `.xlsb` package is entered through. */
|
|
31
15
|
export const XLSB_WORKBOOK_PART = 'xl/workbook.bin';
|
|
32
|
-
/**
|
|
33
|
-
* Read an `.xlsb` (binary BIFF12) package into a {@link Workbook}.
|
|
34
|
-
*
|
|
35
|
-
* @throws {UnsupportedFormatError} if the input is not an `.xlsb` package — a legacy `.xls`
|
|
36
|
-
* (`.format === 'xls'`), an XML `.xlsx` or unrecognised blob (`'unknown'`).
|
|
37
|
-
* @throws {XlsbParseError} if a binary part is malformed.
|
|
38
|
-
* @throws {PackageReadError} if the input is a ZIP that cannot be unpacked — a corrupt or
|
|
39
|
-
* truncated archive, or one exceeding the inflate bound (a probable zip bomb).
|
|
40
|
-
*/
|
|
41
16
|
export function readXlsb(data, options = {}) {
|
|
42
17
|
const cap = options.maxUncompressedBytes ?? DEFAULT_MAX_UNCOMPRESSED;
|
|
43
18
|
return readXlsbPackage(inflateSpreadsheetPackage(data, cap));
|
|
44
19
|
}
|
|
45
|
-
/**
|
|
46
|
-
* Build the model from an already-inflated `.xlsb` package. Separate from {@link readXlsb} so the
|
|
47
|
-
* `.xlsx` reader can hand over a package it has already inflated and classified, rather than
|
|
48
|
-
* inflating the same bytes twice.
|
|
49
|
-
*/
|
|
50
20
|
export function readXlsbPackage(files) {
|
|
51
21
|
const { partText, partBytes } = packageAccessors(files);
|
|
52
22
|
const workbookPart = partBytes(XLSB_WORKBOOK_PART);
|
|
@@ -57,12 +27,8 @@ export function readXlsbPackage(files) {
|
|
|
57
27
|
const sharedStrings = parseSharedStrings(partBytes('xl/sharedStrings.bin'));
|
|
58
28
|
const { cellXfs, namedStyles, defaultFont } = parseStyleTable(partBytes('xl/styles.bin'));
|
|
59
29
|
const workbook = new Workbook();
|
|
60
|
-
// As in the XML reader, the named-style layer is restored only when a file declares more than the
|
|
61
|
-
// Normal default, so an ordinary workbook keeps an empty table and writes just that default back.
|
|
62
30
|
if (namedStyles.length > 1)
|
|
63
31
|
workbook[INTERNAL].restoreNamedStyles(namedStyles);
|
|
64
|
-
// As in the XML reader, font 0 is the workbook's declared default and must survive a re-write; an
|
|
65
|
-
// assumed Calibri in its place changes every empty cell and every character-unit column width.
|
|
66
32
|
workbook[INTERNAL].restoreDefaultFont(defaultFont);
|
|
67
33
|
const declaration = readWorkbookPart(workbookPart);
|
|
68
34
|
const scope = {
|
|
@@ -82,17 +48,12 @@ export function readXlsbPackage(files) {
|
|
|
82
48
|
workbook.defineName(defined);
|
|
83
49
|
return workbook;
|
|
84
50
|
}
|
|
85
|
-
// One pass over `xl/workbook.bin`, gathering everything the rest of the read depends on: the sheet
|
|
86
|
-
// bundle, the externals block a 3-D reference resolves through, and the defined names.
|
|
87
51
|
function readWorkbookPart(part) {
|
|
88
52
|
const sheets = [];
|
|
89
53
|
const names = [];
|
|
90
54
|
let externSheets = [];
|
|
91
55
|
let inBundle = false;
|
|
92
56
|
let inExternals = false;
|
|
93
|
-
// A workbook with no external links declares exactly one supporting book: itself. Rather than
|
|
94
|
-
// enumerate every record type that could open another — and risk miscounting into a *wrong* sheet
|
|
95
|
-
// name — anything else inside the externals block disqualifies the whole table.
|
|
96
57
|
let supportingBooks = 0;
|
|
97
58
|
let selfSupBook;
|
|
98
59
|
for (const record of readRecords(part)) {
|
|
@@ -122,21 +83,17 @@ function readWorkbookPart(part) {
|
|
|
122
83
|
selfSupBook: supportingBooks === 1 ? selfSupBook : undefined,
|
|
123
84
|
};
|
|
124
85
|
}
|
|
125
|
-
// `BrtBundleSh` ([MS-XLSB] 2.4.303) — the binary spelling of `<sheet name state r:id/>`.
|
|
126
86
|
function readSheet(data) {
|
|
127
87
|
const reader = new RecordReader(data);
|
|
128
88
|
const state = SHEET_STATES[reader.u32()] ?? 'visible';
|
|
129
|
-
reader.skip(4);
|
|
89
|
+
reader.skip(4);
|
|
130
90
|
const relId = reader.nullableWideString();
|
|
131
91
|
return { name: reader.wideString(), relId, state };
|
|
132
92
|
}
|
|
133
|
-
// `BrtExternSheet` ([MS-XLSB] 2.4.677): the `Xti` table every 3-D reference indexes into.
|
|
134
93
|
function readExternSheets(data) {
|
|
135
94
|
const reader = new RecordReader(data);
|
|
136
95
|
const count = reader.u32();
|
|
137
96
|
const entries = [];
|
|
138
|
-
// Each entry is three 4-byte fields; checking the count against what the record holds keeps a forged
|
|
139
|
-
// one from driving the loop rather than the record's own length.
|
|
140
97
|
if (count * XTI_BYTES > reader.remaining)
|
|
141
98
|
return entries;
|
|
142
99
|
for (let index = 0; index < count; index++) {
|
|
@@ -145,12 +102,10 @@ function readExternSheets(data) {
|
|
|
145
102
|
return entries;
|
|
146
103
|
}
|
|
147
104
|
const XTI_BYTES = 12;
|
|
148
|
-
// `BrtName` ([MS-XLSB] 2.4.673). The target is left undecoded here: it is a token stream that may cite
|
|
149
|
-
// a sheet by index, and the sheet bundle is not necessarily complete at this point in the stream.
|
|
150
105
|
function readName(data) {
|
|
151
106
|
const reader = new RecordReader(data);
|
|
152
107
|
const flags = reader.u32();
|
|
153
|
-
reader.skip(1);
|
|
108
|
+
reader.skip(1);
|
|
154
109
|
const itab = reader.u32();
|
|
155
110
|
const name = reader.wideString();
|
|
156
111
|
const rgce = reader.bytes(reader.u32());
|
|
@@ -163,19 +118,8 @@ function readName(data) {
|
|
|
163
118
|
rgcb,
|
|
164
119
|
};
|
|
165
120
|
}
|
|
166
|
-
// `itab` for a workbook-global name; any other value is a zero-based sheet index.
|
|
167
121
|
const GLOBAL_NAME_SCOPE = 0xffffffff;
|
|
168
|
-
// `fFunc`: the name registers something callable rather than a range.
|
|
169
122
|
const NAME_IS_FUNCTION = 0x00000002;
|
|
170
|
-
// The workbook's defined names, as the model holds them.
|
|
171
|
-
//
|
|
172
|
-
// Two kinds of `BrtName` are dropped, both because the XML form does not persist them either — so
|
|
173
|
-
// carrying them through would make the two readings of one workbook disagree. A *function* name is
|
|
174
|
-
// Excel's registration of a callable (every post-2007 function gets one, `_xlfn.TEXTJOIN` and
|
|
175
|
-
// friends); its target is the placeholder `#NAME?`, not a range. And `_xlnm._FilterDatabase` is the
|
|
176
|
-
// built-in Excel derives from a sheet's autofilter, which the model reconstructs from the autofilter
|
|
177
|
-
// itself. A name whose target uses a token this reader cannot decode is dropped too, rather than
|
|
178
|
-
// surfaced with a target that is a guess.
|
|
179
123
|
function definedNames(declaration, scope) {
|
|
180
124
|
const names = [];
|
|
181
125
|
for (const declared of declaration.names) {
|
|
@@ -188,12 +132,10 @@ function definedNames(declaration, scope) {
|
|
|
188
132
|
names.push({
|
|
189
133
|
name: declared.name,
|
|
190
134
|
...(sheet === undefined ? {} : { scope: sheet }),
|
|
191
|
-
// Stripped back to the readable form, the same normalisation the XML reader applies.
|
|
192
135
|
refersTo: unmangleFunctions(refersTo),
|
|
193
136
|
});
|
|
194
137
|
}
|
|
195
138
|
return names;
|
|
196
139
|
}
|
|
197
140
|
const FILTER_DATABASE_NAME = '_xlnm._FilterDatabase';
|
|
198
|
-
// `hsState` ([MS-XLSB] 2.4.303), indexed by its stored value.
|
|
199
141
|
const SHEET_STATES = ['visible', 'hidden', 'veryHidden'];
|
|
@@ -1,24 +1,4 @@
|
|
|
1
|
-
// The BIFF12 record framing every `.bin` part of an `.xlsb` package is built from ([MS-XLSB] 2.1.4).
|
|
2
|
-
//
|
|
3
|
-
// A part is a bare concatenation of records — no header, no index, no terminator — each framed as a
|
|
4
|
-
// variable-length type, a variable-length size, and that many payload bytes. Both prefixes are
|
|
5
|
-
// 7-bits-per-byte with the high bit meaning "one more byte follows": the type is 1–2 bytes (so the
|
|
6
|
-
// single-byte space is reserved for the hot records — a cell is one byte of framing), the size is 1–4.
|
|
7
|
-
//
|
|
8
|
-
// **This is the reader's hostile-input frontier.** The declared size is attacker-controlled, so it is
|
|
9
|
-
// never allowed to drive an allocation: a record's payload is handed out as a `subarray` *view* onto
|
|
10
|
-
// the part the inflate bound already materialised and capped, and a size that would run past the end
|
|
11
|
-
// of the part is rejected outright rather than clamped. A lying length therefore costs nothing and
|
|
12
|
-
// fails closed, and the whole stream's memory is exactly the part's own bytes.
|
|
13
1
|
import { XlsbParseError } from './errors.js';
|
|
14
|
-
/**
|
|
15
|
-
* Decode a `.bin` part into its record sequence, in order.
|
|
16
|
-
*
|
|
17
|
-
* Lazy: a caller that stops early (having found what it needs) never frames the rest of the part.
|
|
18
|
-
*
|
|
19
|
-
* @throws {XlsbParseError} if a record header is truncated or a record's declared size runs past the
|
|
20
|
-
* end of the part.
|
|
21
|
-
*/
|
|
22
2
|
export function* readRecords(part) {
|
|
23
3
|
let offset = 0;
|
|
24
4
|
while (offset < part.length) {
|
|
@@ -26,8 +6,6 @@ export function* readRecords(part) {
|
|
|
26
6
|
let type = lowType & 0x7f;
|
|
27
7
|
if ((lowType & 0x80) !== 0)
|
|
28
8
|
type |= (byteAt(part, offset++) & 0x7f) << 7;
|
|
29
|
-
// Up to four size bytes, least-significant seven bits first. The fourth byte's continuation bit
|
|
30
|
-
// is specified as ignored, which the fixed bound expresses: the loop simply stops there.
|
|
31
9
|
let size = 0;
|
|
32
10
|
for (let index = 0; index < 4; index++) {
|
|
33
11
|
const piece = byteAt(part, offset++);
|
|
@@ -35,9 +13,6 @@ export function* readRecords(part) {
|
|
|
35
13
|
if ((piece & 0x80) === 0)
|
|
36
14
|
break;
|
|
37
15
|
}
|
|
38
|
-
// The one check that makes a forged size harmless: compare it against what the part *actually*
|
|
39
|
-
// holds, before anything is handed out. `part.length - offset` cannot be negative here — byteAt
|
|
40
|
-
// has already proven every header byte was in range.
|
|
41
16
|
if (size > part.length - offset) {
|
|
42
17
|
throw new XlsbParseError(`BIFF12 record ${type} declares ${size} bytes but only ${part.length - offset} remain in the part`);
|
|
43
18
|
}
|
|
@@ -45,9 +20,6 @@ export function* readRecords(part) {
|
|
|
45
20
|
offset += size;
|
|
46
21
|
}
|
|
47
22
|
}
|
|
48
|
-
// Every header byte goes through here, so a part that ends mid-header fails closed rather than
|
|
49
|
-
// folding `undefined` into the arithmetic (which `noUncheckedIndexedAccess` would otherwise let
|
|
50
|
-
// through as a silent NaN-shaped type or size).
|
|
51
23
|
function byteAt(part, index) {
|
|
52
24
|
const value = part[index];
|
|
53
25
|
if (value === undefined)
|
|
@@ -1,15 +1,4 @@
|
|
|
1
|
-
// The BIFF12 record types this reader consumes, by their [MS-XLSB] "Record Enumeration" (section 2.3)
|
|
2
|
-
// numbers. Named constants rather than magic numbers at the switch sites: a record number carries no
|
|
3
|
-
// hint of what it is, so `BRT.CellRk` is the difference between a readable parser and a table of
|
|
4
|
-
// unexplained integers.
|
|
5
|
-
//
|
|
6
|
-
// Deliberately partial. [MS-XLSB] enumerates ~760 record types; listing the ones we do not decode
|
|
7
|
-
// would claim coverage we do not have. A record whose type is absent here is skipped by the parsers,
|
|
8
|
-
// which is the correct behaviour for a forward-compatible reader — the format grows, and an unknown
|
|
9
|
-
// record is always safely framed (its size is in the stream) even when its meaning is not known.
|
|
10
|
-
/** BIFF12 record numbers, grouped by the part whose stream they appear in. */
|
|
11
1
|
export const BRT = {
|
|
12
|
-
// Cell table (worksheet part). The single-byte record space — these are the hot path.
|
|
13
2
|
RowHdr: 0,
|
|
14
3
|
CellBlank: 1,
|
|
15
4
|
CellRk: 2,
|
|
@@ -25,13 +14,9 @@ export const BRT = {
|
|
|
25
14
|
CellRString: 62,
|
|
26
15
|
ColInfo: 60,
|
|
27
16
|
MergeCell: 176,
|
|
28
|
-
// Shared-string table.
|
|
29
17
|
SSTItem: 19,
|
|
30
18
|
BeginSst: 159,
|
|
31
19
|
EndSst: 160,
|
|
32
|
-
// Style sheet: each collection is a Begin/End pair around its entries, and `XF` appears inside two
|
|
33
|
-
// of them (the named-style layer and the direct-format layer), so the style parser tracks which
|
|
34
|
-
// collection it is in rather than keying on the record number alone.
|
|
35
20
|
Fmt: 44,
|
|
36
21
|
Font: 43,
|
|
37
22
|
Fill: 45,
|
|
@@ -52,18 +37,14 @@ export const BRT = {
|
|
|
52
37
|
EndCellXFs: 618,
|
|
53
38
|
BeginStyles: 619,
|
|
54
39
|
EndStyles: 620,
|
|
55
|
-
// Workbook part.
|
|
56
40
|
BundleSh: 156,
|
|
57
41
|
BeginBundleShs: 143,
|
|
58
42
|
EndBundleShs: 144,
|
|
59
43
|
Name: 39,
|
|
60
|
-
// The externals block: which workbooks a formula can reach, and which sheets of them each `ixti` a
|
|
61
|
-
// 3-D reference carries names. `SupSelf` declares a supporting book that is *this* workbook.
|
|
62
44
|
BeginExternals: 353,
|
|
63
45
|
EndExternals: 354,
|
|
64
46
|
SupSelf: 357,
|
|
65
47
|
ExternSheet: 362,
|
|
66
|
-
// Worksheet structure.
|
|
67
48
|
WsProp: 147,
|
|
68
49
|
WsDim: 148,
|
|
69
50
|
WsFmtInfo: 485,
|
|
@@ -1,9 +1,3 @@
|
|
|
1
|
-
// Cell-gathering state machine for the worksheet-body reader. It owns the still-textual pieces of the
|
|
2
|
-
// one `<c>` currently being read — its address/type/style, formula, cached value, inline text, and
|
|
3
|
-
// rich-text runs — plus the shared-formula master map that spans the whole sheet. Each `<c>` resets
|
|
4
|
-
// the per-cell state on {@link beginCell} and commits it on {@link finalize}, so this class is the
|
|
5
|
-
// single owner of "what has this cell gathered so far" — to a cell what {@link RunAccumulator} is to a
|
|
6
|
-
// rich string. Value *decoding* stays in `cell-value.ts`; this class only gathers the raw pieces.
|
|
7
1
|
import { decodeAddress, encodeAddress } from '../../core/address.js';
|
|
8
2
|
import { translateFormula, unmangleFunctions } from '../../core/formula.js';
|
|
9
3
|
import { boolPresent } from '../../xml/xml-read.js';
|
|
@@ -17,8 +11,6 @@ export class CellAccumulator {
|
|
|
17
11
|
#col = -1;
|
|
18
12
|
#row = -1;
|
|
19
13
|
#formula = '';
|
|
20
|
-
// Shared-formula bookkeeping. A master `<f t="shared" ref si>TEXT</f>` seeds the group; every clone
|
|
21
|
-
// `<f t="shared" si/>` in the sheet references it by `si` and carries no text of its own.
|
|
22
14
|
#formulaShared = false;
|
|
23
15
|
#formulaSi = -1;
|
|
24
16
|
#sharedClone = false;
|
|
@@ -28,27 +20,19 @@ export class CellAccumulator {
|
|
|
28
20
|
#hasFormula = false;
|
|
29
21
|
#hasValue = false;
|
|
30
22
|
#runs = new RunAccumulator();
|
|
31
|
-
// Masters always precede their clones (Excel keeps the master top-left), so a clone resolves against
|
|
32
|
-
// a map filled as the sheet streams: the master's formula translated to the clone's position.
|
|
33
23
|
#masters = new Map();
|
|
34
|
-
/** This cell's `<c r>` address (`"B3"`), or '' when it carried none. */
|
|
35
24
|
get ref() {
|
|
36
25
|
return this.#ref;
|
|
37
26
|
}
|
|
38
|
-
/** This cell's own `<c s>` style index, or -1 when it carries none. */
|
|
39
27
|
get styleIndex() {
|
|
40
28
|
return this.#style;
|
|
41
29
|
}
|
|
42
|
-
/** This cell's 1-based column, or -1 when its address was absent or unparseable. */
|
|
43
30
|
get col() {
|
|
44
31
|
return this.#col;
|
|
45
32
|
}
|
|
46
|
-
/** The rich-text run accumulator, driven by the surrounding parser's `<r>`/`<rPr>` handling. */
|
|
47
33
|
get runs() {
|
|
48
34
|
return this.#runs;
|
|
49
35
|
}
|
|
50
|
-
// Begin a new `<c>`: record its address/type/style and clear every per-cell gathered field so the
|
|
51
|
-
// last cell's formula, value, runs, or shared/data-table declaration cannot bleed into this one.
|
|
52
36
|
beginCell(attrs) {
|
|
53
37
|
this.#ref = attrs.r ?? '';
|
|
54
38
|
this.#type = attrs.t ?? '';
|
|
@@ -66,9 +50,6 @@ export class CellAccumulator {
|
|
|
66
50
|
this.#sharedClone = false;
|
|
67
51
|
this.#dataTable = null;
|
|
68
52
|
}
|
|
69
|
-
// Begin an `<f>`: record its shared-formula grouping and any data-table declaration. A self-closing
|
|
70
|
-
// `<f t="shared" si/>` is a clone — it fires no close and carries no text — so mark it here to
|
|
71
|
-
// resolve against its master when the cell finalises.
|
|
72
53
|
beginFormula(attrs, selfClosing) {
|
|
73
54
|
this.#formulaShared = attrs.t === 'shared';
|
|
74
55
|
this.#formulaSi = attrs.si !== undefined ? Number(attrs.si) : -1;
|
|
@@ -92,22 +73,14 @@ export class CellAccumulator {
|
|
|
92
73
|
this.#valueText = text;
|
|
93
74
|
this.#hasValue = true;
|
|
94
75
|
}
|
|
95
|
-
// Begin an `<is>`: clear the inline string and its runs so a rich value built from a previous cell's
|
|
96
|
-
// runs keeps its own array.
|
|
97
76
|
beginInlineString() {
|
|
98
77
|
this.#inlineText = '';
|
|
99
78
|
this.#runs.reset();
|
|
100
79
|
}
|
|
101
|
-
// Route a `<t>`'s text: to the open run when one is active, otherwise to the inline string when the
|
|
102
|
-
// parser is inside an `<is>`. A run takes precedence — a run is also inside the inline string.
|
|
103
80
|
appendText(text, inInlineString) {
|
|
104
81
|
if (!this.#runs.appendText(text) && inInlineString)
|
|
105
82
|
this.#inlineText += text;
|
|
106
83
|
}
|
|
107
|
-
// Commit the gathered cell to the sheet with its already-resolved style (the caller applies the
|
|
108
|
-
// cell → row → column inheritance order). A data-table cell surfaces its declaration; a shared-formula
|
|
109
|
-
// master seeds the group before finalising, and a clone resolves to the master translated to its own
|
|
110
|
-
// position. Everything else decodes as an ordinary `<c>` payload. An address-less cell is a no-op.
|
|
111
84
|
finalize(sheet, sharedStrings, style) {
|
|
112
85
|
if (this.#ref === '')
|
|
113
86
|
return;
|
|
@@ -138,7 +111,6 @@ export class CellAccumulator {
|
|
|
138
111
|
const value = {
|
|
139
112
|
sharedFormula: encodeAddress(master.col, master.row),
|
|
140
113
|
formula: unmangleFunctions(translated),
|
|
141
|
-
// A clone's cached result honours the cell's date format the same way a plain formula's does.
|
|
142
114
|
...(this.#hasValue
|
|
143
115
|
? { result: decodeFormulaResult(this.#type, this.#valueText, style?.numFmt) }
|
|
144
116
|
: {}),
|
|
@@ -153,10 +125,6 @@ export class CellAccumulator {
|
|
|
153
125
|
applyXfToCell(cell, style);
|
|
154
126
|
cell.value = this.decode(sharedStrings, style);
|
|
155
127
|
}
|
|
156
|
-
// Decode the gathered pieces into a plain cell value, resolving the shared pool and date formats but
|
|
157
|
-
// NOT the shared-formula / data-table declarations {@link finalize} handles. This is what a data
|
|
158
|
-
// read (the streaming reader) wants: the cell's own value, with a shared-formula clone surfacing its
|
|
159
|
-
// cached result rather than a translated formula it will not evaluate.
|
|
160
128
|
decode(sharedStrings, style) {
|
|
161
129
|
const raw = {
|
|
162
130
|
type: this.#type,
|
|
@@ -1,34 +1,17 @@
|
|
|
1
|
-
// Decoding a worksheet cell's on-disk `<c>` payload into a model {@link CellValue}.
|
|
2
|
-
//
|
|
3
|
-
// This is the single value-decoding surface both readers share: the buffered reader
|
|
4
|
-
// (`./read.ts`) and the streaming row reader (`./read-rows.ts`). Keeping it in one place is
|
|
5
|
-
// what guarantees a cell read one row at a time decodes identically to the same cell read as
|
|
6
|
-
// part of a whole workbook — a divergence here would be a silent data bug in exactly one path.
|
|
7
1
|
import { isDateFormat, serialToDate } from '../../core/date.js';
|
|
8
2
|
import { unmangleFunctions } from '../../core/formula.js';
|
|
9
3
|
import { isErrorCode, } from '../../core/value.js';
|
|
10
4
|
import { boolStrict } from '../../xml/xml-read.js';
|
|
11
|
-
/**
|
|
12
|
-
* Decode a gathered cell into its model value. A formula cell becomes a `{formula, result?}`
|
|
13
|
-
* object (the on-disk `_xlfn.`/`_xlpm.` mangling stripped back to the readable name); a plain
|
|
14
|
-
* numeric cell under a date number format becomes a {@link Date}; everything else decodes by its
|
|
15
|
-
* `t` type. `numFmt` is the cell's resolved number-format code, used only for date detection.
|
|
16
|
-
*/
|
|
17
5
|
export function decodeCellContent(raw, sharedStrings, numFmt) {
|
|
18
6
|
if (raw.hasFormula) {
|
|
19
7
|
const stored = unmangleFunctions(raw.formula);
|
|
20
8
|
const result = raw.hasValue ? decodeFormulaResult(raw.type, raw.valueText, numFmt) : undefined;
|
|
21
9
|
return result === undefined ? { formula: stored } : { formula: stored, result };
|
|
22
10
|
}
|
|
23
|
-
// An inline string built from `<r>` runs is rich text — surface its runs rather than flattening
|
|
24
|
-
// them to the concatenated `inlineText` a plain string would decode to.
|
|
25
11
|
if (raw.type === 'inlineStr' && raw.richTextRuns !== undefined && raw.richTextRuns.length > 0) {
|
|
26
12
|
return { richText: raw.richTextRuns };
|
|
27
13
|
}
|
|
28
14
|
const value = decodeValue(raw.type, raw.valueText, raw.inlineText, raw.hasValue, sharedStrings);
|
|
29
|
-
// A number stored under a date format is a date serial — surface it as a Date so a written
|
|
30
|
-
// date round-trips as a date, not a bare number. Only plain numeric cells qualify; a string,
|
|
31
|
-
// boolean, or formula result under a date format keeps its own kind.
|
|
32
15
|
return typeof value === 'number' && numFmt !== undefined && isDateFormat(numFmt)
|
|
33
16
|
? serialToDate(value)
|
|
34
17
|
: value;
|
|
@@ -40,13 +23,8 @@ function decodeValue(type, valueText, inlineText, hasValue, sharedStrings) {
|
|
|
40
23
|
case 'str':
|
|
41
24
|
return valueText;
|
|
42
25
|
case 'd':
|
|
43
|
-
// A Strict-mode (ISO/IEC 29500 Strict) date cell stores an ISO 8601 value directly, not a
|
|
44
|
-
// serial. Parse it literally — an ISO date is UTC — so it reads as the date it states rather
|
|
45
|
-
// than a 1900-epoch serial the transitional decoder would fabricate from the text.
|
|
46
26
|
return valueText === '' ? null : new Date(valueText);
|
|
47
27
|
case 's': {
|
|
48
|
-
// A `t="s"` cell indexes the shared pool; the entry is a plain string or, when Excel pooled a
|
|
49
|
-
// rich value, a {@link RichTextValue} whose runs surface here rather than being flattened.
|
|
50
28
|
const index = Number(valueText);
|
|
51
29
|
return Number.isInteger(index) ? (sharedStrings[index] ?? '') : '';
|
|
52
30
|
}
|
|
@@ -58,19 +36,12 @@ function decodeValue(type, valueText, inlineText, hasValue, sharedStrings) {
|
|
|
58
36
|
return hasValue ? Number(valueText) : null;
|
|
59
37
|
}
|
|
60
38
|
}
|
|
61
|
-
/** Decode a formula's cached `<v>` result by its `t` type, coercing a numeric result under a date
|
|
62
|
-
* `numFmt` to a {@link Date} exactly as a bare numeric cell is — so a date-valued formula result
|
|
63
|
-
* (e.g. `TODAY()`) reads back as a Date, not a serial. Shared by the buffered reader's shared-formula
|
|
64
|
-
* clone resolution, which caches a result the same way a plain formula cell does. */
|
|
65
39
|
export function decodeFormulaResult(type, valueText, numFmt) {
|
|
66
40
|
const result = decodeResult(type, valueText);
|
|
67
41
|
return typeof result === 'number' && numFmt !== undefined && isDateFormat(numFmt)
|
|
68
42
|
? serialToDate(result)
|
|
69
43
|
: result;
|
|
70
44
|
}
|
|
71
|
-
// The formula-result subset of `decodeValue`: a cached result is only ever a string, boolean,
|
|
72
|
-
// error, or number — never a shared-string index, inline string, or Strict-mode date — so this
|
|
73
|
-
// handles just those cases rather than the full cell-value grammar.
|
|
74
45
|
function decodeResult(type, valueText) {
|
|
75
46
|
switch (type) {
|
|
76
47
|
case 'str':
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { Color } from '../../core/style.ts';
|
|
2
|
+
/** Serialise a {@link Color} as the attribute list a `<color>`-shaped element carries. */
|
|
3
|
+
export declare function colorAttrs(color: Color): string;
|
|
4
|
+
export declare function parseColor(attrs: {
|
|
5
|
+
readonly [k: string]: string;
|
|
6
|
+
}): Color;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { AuthoringError } from '../../errors.js';
|
|
2
|
+
function normalizeArgb(argb) {
|
|
3
|
+
const hex = argb.startsWith('#') ? argb.slice(1) : argb;
|
|
4
|
+
const rgb = hex.length === 6 ? `FF${hex}` : hex;
|
|
5
|
+
if (!/^[0-9a-fA-F]{8}$/.test(rgb)) {
|
|
6
|
+
throw new AuthoringError(`Invalid ARGB colour ${JSON.stringify(argb)}: expected 6 or 8 hexadecimal digits`);
|
|
7
|
+
}
|
|
8
|
+
return rgb;
|
|
9
|
+
}
|
|
10
|
+
export function colorAttrs(color) {
|
|
11
|
+
const parts = [];
|
|
12
|
+
if (color.argb !== undefined)
|
|
13
|
+
parts.push(`rgb="${normalizeArgb(color.argb)}"`);
|
|
14
|
+
if (color.theme !== undefined)
|
|
15
|
+
parts.push(`theme="${color.theme}"`);
|
|
16
|
+
if (color.tint !== undefined)
|
|
17
|
+
parts.push(`tint="${color.tint}"`);
|
|
18
|
+
if (color.indexed !== undefined)
|
|
19
|
+
parts.push(`indexed="${color.indexed}"`);
|
|
20
|
+
return parts.join(' ');
|
|
21
|
+
}
|
|
22
|
+
export function parseColor(attrs) {
|
|
23
|
+
const color = {};
|
|
24
|
+
if (attrs.rgb !== undefined)
|
|
25
|
+
color.argb = attrs.rgb;
|
|
26
|
+
if (attrs.theme !== undefined) {
|
|
27
|
+
const theme = Number(attrs.theme);
|
|
28
|
+
if (Number.isInteger(theme))
|
|
29
|
+
color.theme = theme;
|
|
30
|
+
}
|
|
31
|
+
if (attrs.tint !== undefined) {
|
|
32
|
+
const tint = Number(attrs.tint);
|
|
33
|
+
if (Number.isFinite(tint))
|
|
34
|
+
color.tint = tint;
|
|
35
|
+
}
|
|
36
|
+
if (attrs.indexed !== undefined) {
|
|
37
|
+
const indexed = Number(attrs.indexed);
|
|
38
|
+
if (Number.isInteger(indexed))
|
|
39
|
+
color.indexed = indexed;
|
|
40
|
+
}
|
|
41
|
+
return color;
|
|
42
|
+
}
|
package/dist/io/xlsx/comments.js
CHANGED
|
@@ -1,49 +1,13 @@
|
|
|
1
|
-
// Cell comments — the `xl/comments{n}.xml` part, its `xl/drawings/vmlDrawing{n}.vml` companion, and the
|
|
2
|
-
// reader that maps a comment back onto its cell.
|
|
3
|
-
//
|
|
4
|
-
// A comment is anchored to a cell by A1 reference and rendered by Excel as a floating box. The box's
|
|
5
|
-
// geometry lives in a legacy VML drawing (the pre-DrawingML shape format Excel still requires here);
|
|
6
|
-
// the text lives in the comments part. Both are emitted together — a comments part with no matching
|
|
7
|
-
// `<legacyDrawing>`/VML reads as text but renders nothing, so we never split them.
|
|
8
|
-
//
|
|
9
|
-
// Two different things share this one wire form:
|
|
10
|
-
// • a user's **note** (`cell.note`) — a single anonymous annotation, the whole of what the part held
|
|
11
|
-
// before 2018;
|
|
12
|
-
// • the legacy **fallback** Excel writes beside every modern threaded comment (see
|
|
13
|
-
// `threaded-comments.ts`), so a pre-2018 reader still sees the conversation. Its text is a fixed
|
|
14
|
-
// boilerplate wrapping a copy of the thread, and its author is a synthetic `tc={headId}` entry.
|
|
15
|
-
//
|
|
16
|
-
// That `tc=` author and the comment's `xr:uid` are how Excel binds a cell back to its thread — not
|
|
17
|
-
// decoration. Verified against desktop Excel: a package whose threadedComment part, persons registry,
|
|
18
|
-
// relationships and content types all survive intact still reads back as ordinary notes with zero
|
|
19
|
-
// threads once those two are lost. So the fallback is *derived from the thread model* on write and
|
|
20
|
-
// *suppressed on read*, rather than round-tripped as a plain note.
|
|
21
1
|
import { decodeAddress } from '../../core/address.js';
|
|
22
2
|
import { escapeAttr, escapeText, textElement, XML_DECLARATION } from '../../xml/xml.js';
|
|
23
3
|
import { localName, parseXml } from '../../xml/xml-read.js';
|
|
24
4
|
import { MARKUP_COMPATIBILITY_NS, REVISION_NS, SPREADSHEETML_NS } from './namespaces.js';
|
|
25
|
-
/**
|
|
26
|
-
* Gather every comment a sheet must write: its cells' notes, plus one legacy fallback per conversation
|
|
27
|
-
* in `threads`. A comment anchors to its cell regardless of the cell's value, so a note (or a thread) on
|
|
28
|
-
* an otherwise-empty cell is collected too.
|
|
29
|
-
*
|
|
30
|
-
* `threads` is the conversations the *package* will carry, not simply the ones the sheet holds — the
|
|
31
|
-
* caller decides, because a fallback beside a thread whose `threadedComment` part is missing is worse
|
|
32
|
-
* than no fallback at all: verified against desktop Excel, such a comment shows as neither a thread nor
|
|
33
|
-
* a note, so the text disappears entirely.
|
|
34
|
-
*
|
|
35
|
-
* Ordered by cell, row-major, the way Excel writes the list — so a fallback lands interleaved among the
|
|
36
|
-
* notes rather than appended after them, and the VML shapes follow the same order.
|
|
37
|
-
*/
|
|
38
5
|
export function collectComments(sheet, threads) {
|
|
39
6
|
const fallbacks = threadFallbacks(threads);
|
|
40
7
|
const anchored = new Set(fallbacks.map((fallback) => fallback.ref));
|
|
41
8
|
const comments = [...fallbacks];
|
|
42
9
|
for (const { cells } of sheet.rows()) {
|
|
43
10
|
for (const cell of cells) {
|
|
44
|
-
// Excel refuses to put a note and a thread on one cell, so a file carrying both (only a foreign
|
|
45
|
-
// generator or a hand-edit makes one) is written back as the thread alone: two comments on one ref
|
|
46
|
-
// is a shape Excel repairs by dropping both, which would lose the conversation as well as the note.
|
|
47
11
|
if (cell.note === undefined || anchored.has(cell.address))
|
|
48
12
|
continue;
|
|
49
13
|
comments.push({ ref: cell.address, row: cell.row, col: cell.col, text: cell.note });
|
|
@@ -51,8 +15,6 @@ export function collectComments(sheet, threads) {
|
|
|
51
15
|
}
|
|
52
16
|
return comments.sort((a, b) => a.row - b.row || a.col - b.col);
|
|
53
17
|
}
|
|
54
|
-
// One legacy fallback per conversation, keyed to the thread head whose id binds it. A thread with no
|
|
55
|
-
// messages has nothing to write and no id to bind by, so it contributes none.
|
|
56
18
|
function threadFallbacks(threads) {
|
|
57
19
|
const fallbacks = [];
|
|
58
20
|
for (const thread of threads) {
|
|
@@ -64,31 +26,15 @@ function threadFallbacks(threads) {
|
|
|
64
26
|
}
|
|
65
27
|
return fallbacks;
|
|
66
28
|
}
|
|
67
|
-
// The boilerplate Excel puts in front of every fallback, captured verbatim from an Excel-authored file.
|
|
68
|
-
// It is what a pre-2018 reader shows the user, so it is reproduced exactly rather than paraphrased.
|
|
69
29
|
const FALLBACK_PREAMBLE = '[Threaded comment]\n\nYour version of Excel allows you to read this threaded comment; however, any ' +
|
|
70
30
|
'edits to it will get removed if the file is opened in a newer version of Excel. Learn more: ' +
|
|
71
31
|
'https://go.microsoft.com/fwlink/?linkid=870924\n\n';
|
|
72
|
-
// A whole conversation flattened into the one comment a pre-2018 reader can render: the opening message
|
|
73
|
-
// under `Comment:`, then each reply under its own `Reply:`, every body indented four spaces. Verified
|
|
74
|
-
// against desktop Excel for a thread with three replies — `Reply:` repeats per reply rather than the
|
|
75
|
-
// replies being joined under one heading.
|
|
76
32
|
function fallbackText(thread) {
|
|
77
33
|
const [head, ...replies] = thread.comments;
|
|
78
34
|
const body = replies.map((reply) => `\nReply:\n ${reply.text}`).join('');
|
|
79
35
|
return `${FALLBACK_PREAMBLE}Comment:\n ${head?.text ?? ''}${body}`;
|
|
80
36
|
}
|
|
81
|
-
// `xr:uid` lives in the 2014 revision namespace, declared `mc:Ignorable` exactly as Excel declares it so
|
|
82
|
-
// a consumer that does not know the prefix skips the attribute instead of rejecting the part.
|
|
83
37
|
const REVISION_NS_ATTRS = ` xmlns:mc="${MARKUP_COMPATIBILITY_NS}" mc:Ignorable="xr" xmlns:xr="${REVISION_NS}"`;
|
|
84
|
-
/**
|
|
85
|
-
* The `xl/comments{n}.xml` part.
|
|
86
|
-
*
|
|
87
|
-
* Authors are laid out the way Excel lays them out: one synthetic `tc={headId}` entry per threaded
|
|
88
|
-
* conversation first, then a single anonymous author shared by every note (the model carries no note
|
|
89
|
-
* author). Each comment points at its own author by index, and a fallback additionally carries the
|
|
90
|
-
* `xr:uid` naming its thread — the pair that keeps Excel treating the cell as threaded.
|
|
91
|
-
*/
|
|
92
38
|
export function commentsXml(comments) {
|
|
93
39
|
const authorIdByThreadId = new Map();
|
|
94
40
|
for (const { threadId } of comments) {
|
|
@@ -116,7 +62,6 @@ export function commentsXml(comments) {
|
|
|
116
62
|
`<commentList>${list}</commentList>` +
|
|
117
63
|
'</comments>');
|
|
118
64
|
}
|
|
119
|
-
// VML namespaces and the one shape type (a text box) every comment reuses.
|
|
120
65
|
const VML_HEADER = '<xml xmlns:v="urn:schemas-microsoft-com:vml" ' +
|
|
121
66
|
'xmlns:o="urn:schemas-microsoft-com:office:office" ' +
|
|
122
67
|
'xmlns:x="urn:schemas-microsoft-com:office:excel">' +
|
|
@@ -124,11 +69,6 @@ const VML_HEADER = '<xml xmlns:v="urn:schemas-microsoft-com:vml" ' +
|
|
|
124
69
|
'<v:shapetype id="_x0000_t202" coordsize="21600,21600" o:spt="202" ' +
|
|
125
70
|
'path="m,l,21600r21600,l21600,xe"><v:stroke joinstyle="miter"/>' +
|
|
126
71
|
'<v:path gradientshapeok="t" o:connecttype="rect"/></v:shapetype>';
|
|
127
|
-
/** The `xl/drawings/vmlDrawing{n}.vml` companion: one hidden text-box shape per comment, in the same
|
|
128
|
-
* order as the comments part. Anchor coordinates place the box a couple of cells down-and-right of its
|
|
129
|
-
* owner; Excel refines them on open, so the values are a sensible starting geometry rather than a
|
|
130
|
-
* pixel-exact layout. A thread's fallback shape is `ObjectType="Note"` like any other — Excel draws the
|
|
131
|
-
* threaded-comment card itself and only needs the shape to exist. */
|
|
132
72
|
export function vmlDrawingXml(comments) {
|
|
133
73
|
const shapes = comments
|
|
134
74
|
.map((comment, i) => {
|
|
@@ -148,18 +88,8 @@ export function vmlDrawingXml(comments) {
|
|
|
148
88
|
.join('');
|
|
149
89
|
return `${VML_HEADER}${shapes}</xml>`;
|
|
150
90
|
}
|
|
151
|
-
// A comment names its author by index into `<authors>`, so an empty entry must still occupy its slot —
|
|
152
|
-
// presenting the self-closing `<author/>` an author-less file writes as an empty element gives it the
|
|
153
|
-
// close that pushes it. Without this every later index would shift by one and a note could inherit a
|
|
154
|
-
// thread's `tc=` author.
|
|
155
91
|
const COMMENT_EMPTY_CLOSES = new Set(['author']);
|
|
156
|
-
// The author string marking a comment as a thread's legacy fallback: `tc={headThreadId}`.
|
|
157
92
|
const THREAD_AUTHOR_PREFIX = 'tc=';
|
|
158
|
-
/**
|
|
159
|
-
* Parse a `comments{n}.xml` part into a map of A1 reference → comment. Text runs within one comment are
|
|
160
|
-
* concatenated; an author-name run is Excel's own convention and is not stripped, so a note reads back
|
|
161
|
-
* as exactly the text that was written.
|
|
162
|
-
*/
|
|
163
93
|
export function parseComments(xml) {
|
|
164
94
|
const comments = new Map();
|
|
165
95
|
const authors = [];
|
|
@@ -210,26 +140,12 @@ export function parseComments(xml) {
|
|
|
210
140
|
}, { closeEmptyElements: COMMENT_EMPTY_CLOSES });
|
|
211
141
|
return comments;
|
|
212
142
|
}
|
|
213
|
-
// A missing or non-numeric `authorId` indexes nothing, so `authors[NaN]` is undefined and the comment
|
|
214
|
-
// reads as a plain note — the safe direction, since mistaking a note for a fallback would delete it.
|
|
215
143
|
function threadIdOf(author) {
|
|
216
144
|
if (author === undefined || !author.startsWith(THREAD_AUTHOR_PREFIX))
|
|
217
145
|
return undefined;
|
|
218
146
|
const id = author.slice(THREAD_AUTHOR_PREFIX.length);
|
|
219
147
|
return id === '' ? undefined : id;
|
|
220
148
|
}
|
|
221
|
-
/**
|
|
222
|
-
* Apply a parsed comments part onto a sheet's cells as notes, addressing each by its A1 reference.
|
|
223
|
-
*
|
|
224
|
-
* A thread's legacy fallback is not a note and does not become one: its text is boilerplate wrapping a
|
|
225
|
-
* copy of the conversation, so surfacing it as `cell.note` hands the caller garbage — and on write it
|
|
226
|
-
* would be re-emitted as a plain note, destroying the `tc=`/`xr:uid` binding and leaving Excel unable to
|
|
227
|
-
* see the thread at all.
|
|
228
|
-
*
|
|
229
|
-
* Suppressed only for a conversation the reader actually holds: a file whose thread part is missing or
|
|
230
|
-
* damaged has nothing else left, so there the boilerplate is kept rather than the content lost. Call
|
|
231
|
-
* after the sheet's threads are restored, since that is what this reads to decide.
|
|
232
|
-
*/
|
|
233
149
|
export function applyNotes(sheet, comments) {
|
|
234
150
|
const headIds = new Set(sheet.commentThreads.flatMap((thread) => {
|
|
235
151
|
const head = thread.comments[0];
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ConditionalFormatting } from '../../core/conditional-formatting.ts';
|
|
2
|
-
import {
|
|
2
|
+
import type { StyleRegistry } from './styles.ts';
|
|
3
3
|
/**
|
|
4
4
|
* Serialise every conditional formatting on a sheet into its `<conditionalFormatting>` blocks, in
|
|
5
5
|
* insertion order. Returns '' when the sheet has none. A rule missing a `priority` is assigned the
|