@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
package/dist/io/xlsx/tables.js
CHANGED
|
@@ -1,11 +1,3 @@
|
|
|
1
|
-
// Worksheet tables (OOXML `<table>` parts, `xl/tables/table{n}.xml`) — both directions. The writer
|
|
2
|
-
// (`tableXml`) turns a `Table` into its part; the reader (`parseTable`) is its inverse, turning a
|
|
3
|
-
// stored part back into the `TableOptions` a worksheet re-registers.
|
|
4
|
-
//
|
|
5
|
-
// The part stores the table's *full* occupied range (`ref="A1:B3"`), whereas the model anchors at a
|
|
6
|
-
// single top-left cell plus a data-row count. The two are equivalent: the data-row count is the
|
|
7
|
-
// range height minus the header row (present unless `headerRowCount="0"`) and the totals row (present
|
|
8
|
-
// only when `totalsRowCount` is positive), so reconstructing one from the other is lossless.
|
|
9
1
|
import { decodeRange, encodeAddress } from '../../core/address.js';
|
|
10
2
|
import { isTotalsRowFunction, } from '../../core/table.js';
|
|
11
3
|
import { boolAttr, escapeAttr, escapeText, XML_DECLARATION } from '../../xml/xml.js';
|
|
@@ -14,12 +6,7 @@ import { NS } from './relationships.js';
|
|
|
14
6
|
export function tableXml(table, id) {
|
|
15
7
|
const name = escapeAttr(table.name);
|
|
16
8
|
const displayName = escapeAttr(table.displayName);
|
|
17
|
-
// headerRowCount defaults to 1 in OOXML, so only a headerless table needs it stated.
|
|
18
9
|
const headerRowCount = table.headerRow ? '' : ' headerRowCount="0"';
|
|
19
|
-
// A present totals row implies it is shown, so it only needs the count. Without a totals row the
|
|
20
|
-
// model's tri-state totalsRowShown decides: emit the flag Excel recorded, or nothing when the
|
|
21
|
-
// source omitted it — injecting `totalsRowShown="0"` onto a table that lacked the attribute is
|
|
22
|
-
// exactly the spurious change that makes Excel treat an otherwise-valid table as corrupt.
|
|
23
10
|
let totals;
|
|
24
11
|
if (table.totalsRow) {
|
|
25
12
|
totals = ' totalsRowCount="1"';
|
|
@@ -40,12 +27,8 @@ export function tableXml(table, id) {
|
|
|
40
27
|
tableStyleInfoXml(table.style) +
|
|
41
28
|
'</table>');
|
|
42
29
|
}
|
|
43
|
-
// Excel's default table appearance, written for a table that carries no style of its own.
|
|
44
30
|
const DEFAULT_TABLE_STYLE = '<tableStyleInfo name="TableStyleMedium2" showFirstColumn="0" showLastColumn="0" ' +
|
|
45
31
|
'showRowStripes="1" showColumnStripes="0"/>';
|
|
46
|
-
// Emit `<tableStyleInfo>` from the model's style, or the default when none was captured. Each
|
|
47
|
-
// attribute is written only when the model holds it, so a style read without (say) a `name` — or a
|
|
48
|
-
// part that omitted a banding flag — re-emits exactly as it arrived rather than gaining an attribute.
|
|
49
32
|
function tableStyleInfoXml(style) {
|
|
50
33
|
if (style === undefined)
|
|
51
34
|
return DEFAULT_TABLE_STYLE;
|
|
@@ -67,46 +50,30 @@ function tableColumnXml(column, id) {
|
|
|
67
50
|
if (column.totalsRowFunction !== undefined) {
|
|
68
51
|
attrs += ` totalsRowFunction="${escapeAttr(column.totalsRowFunction)}"`;
|
|
69
52
|
}
|
|
70
|
-
// A `custom` total is carried by a `<totalsRowFormula>` child rather than a built-in function, so
|
|
71
|
-
// the element is non-self-closing when one is present. The formula is stored without a leading `=`,
|
|
72
|
-
// matching how Excel writes it.
|
|
73
53
|
if (column.totalsRowFormula !== undefined) {
|
|
74
54
|
return `<tableColumn ${attrs}><totalsRowFormula>${escapeText(column.totalsRowFormula)}</totalsRowFormula></tableColumn>`;
|
|
75
55
|
}
|
|
76
56
|
return `<tableColumn ${attrs}/>`;
|
|
77
57
|
}
|
|
78
|
-
// OOXML booleans spell false as "0" or "false"; every other spelling (including "1"/"true") is true.
|
|
79
58
|
function parseOoxmlBool(value) {
|
|
80
59
|
return value !== '0' && value !== 'false';
|
|
81
60
|
}
|
|
82
|
-
/**
|
|
83
|
-
* Parse a `<table>` part into the options that reconstruct it, or `undefined` when the XML is not a
|
|
84
|
-
* usable table (no name, no ref, or no columns — Excel treats such a part as corrupt, so we drop it
|
|
85
|
-
* rather than fabricate a degenerate table). Duplicate column names are not resolved here — the
|
|
86
|
-
* {@link Table} constructor disambiguates them, so authoring and loading share one implementation.
|
|
87
|
-
*/
|
|
88
61
|
export function parseTable(xml) {
|
|
89
62
|
let name;
|
|
90
63
|
let displayName;
|
|
91
64
|
let ref;
|
|
92
|
-
let headerRowCount = 1;
|
|
93
|
-
let totalsRowCount = 0;
|
|
94
|
-
let totalsRowShown;
|
|
95
|
-
let style;
|
|
96
|
-
let hasAutoFilter = false;
|
|
65
|
+
let headerRowCount = 1;
|
|
66
|
+
let totalsRowCount = 0;
|
|
67
|
+
let totalsRowShown;
|
|
68
|
+
let style;
|
|
69
|
+
let hasAutoFilter = false;
|
|
97
70
|
const columns = [];
|
|
98
|
-
// A `<totalsRowFormula>` is a text child of the current `<tableColumn>`, so it is captured across
|
|
99
|
-
// open/text/close rather than from an attribute. `calculatedColumnFormula` is a sibling child of
|
|
100
|
-
// the same type (CT_TableFormula), so guard on the exact element to avoid capturing its text.
|
|
101
71
|
let inTotalsFormula = false;
|
|
102
72
|
let totalsFormula = '';
|
|
103
73
|
parseXml(xml, {
|
|
104
74
|
onOpen(elementName, attrs) {
|
|
105
75
|
switch (localName(elementName)) {
|
|
106
76
|
case 'table':
|
|
107
|
-
// OOXML makes `displayName` the required identifier and `name` an optional alias; the
|
|
108
|
-
// model inverts the roles (`name` is the formula identifier, `displayName` the label),
|
|
109
|
-
// so read each from its own attribute and fall back across the pair when one is absent.
|
|
110
77
|
name = attrs.name ?? attrs.displayName;
|
|
111
78
|
displayName = attrs.displayName ?? attrs.name;
|
|
112
79
|
ref = attrs.ref;
|
|
@@ -114,8 +81,6 @@ export function parseTable(xml) {
|
|
|
114
81
|
headerRowCount = Number(attrs.headerRowCount);
|
|
115
82
|
if (attrs.totalsRowCount !== undefined)
|
|
116
83
|
totalsRowCount = Number(attrs.totalsRowCount);
|
|
117
|
-
// Capture the flag verbatim so it re-emits exactly (or, absent, stays absent) rather
|
|
118
|
-
// than being normalised.
|
|
119
84
|
if (attrs.totalsRowShown !== undefined)
|
|
120
85
|
totalsRowShown = parseOoxmlBool(attrs.totalsRowShown);
|
|
121
86
|
break;
|
|
@@ -123,8 +88,6 @@ export function parseTable(xml) {
|
|
|
123
88
|
hasAutoFilter = true;
|
|
124
89
|
break;
|
|
125
90
|
case 'tableStyleInfo': {
|
|
126
|
-
// Keep each attribute off the literal so an absent one stays absent (not `key: undefined`),
|
|
127
|
-
// preserving the round-trip — the writer re-emits only the attributes we actually saw.
|
|
128
91
|
const captured = {};
|
|
129
92
|
if (attrs.name !== undefined)
|
|
130
93
|
captured.name = attrs.name;
|
|
@@ -146,9 +109,6 @@ export function parseTable(xml) {
|
|
|
146
109
|
const column = { name: attrs.name };
|
|
147
110
|
if (attrs.totalsRowLabel !== undefined)
|
|
148
111
|
column.totalsRowLabel = attrs.totalsRowLabel;
|
|
149
|
-
// An unrecognised totalsRowFunction is dropped rather than trusted in verbatim — the token
|
|
150
|
-
// is a closed OOXML enumeration, so a foreign value is malformed input, not a future Excel
|
|
151
|
-
// addition to accommodate.
|
|
152
112
|
if (attrs.totalsRowFunction !== undefined &&
|
|
153
113
|
isTotalsRowFunction(attrs.totalsRowFunction)) {
|
|
154
114
|
column.totalsRowFunction = attrs.totalsRowFunction;
|
|
@@ -170,9 +130,6 @@ export function parseTable(xml) {
|
|
|
170
130
|
if (localName(elementName) !== 'totalsRowFormula')
|
|
171
131
|
return;
|
|
172
132
|
inTotalsFormula = false;
|
|
173
|
-
// Attach to the column currently being parsed — the last one pushed. Excel writes the child
|
|
174
|
-
// only for `totalsRowFunction="custom"`, so a formula on any other column is meaningless, but
|
|
175
|
-
// preserving whatever the part carried keeps the round-trip faithful rather than second-guessing.
|
|
176
133
|
const column = columns[columns.length - 1];
|
|
177
134
|
if (column !== undefined)
|
|
178
135
|
column.totalsRowFormula = totalsFormula;
|
|
@@ -194,12 +151,8 @@ export function parseTable(xml) {
|
|
|
194
151
|
rowCount: Math.max(0, dataRows),
|
|
195
152
|
headerRow,
|
|
196
153
|
totalsRow,
|
|
197
|
-
// Reconstruct the autoFilter state explicitly from the part: a header table read without an
|
|
198
|
-
// `<autoFilter>` must not have one fabricated on the next write.
|
|
199
154
|
autoFilter: hasAutoFilter,
|
|
200
155
|
};
|
|
201
|
-
// Kept off the literal so an absent attribute stays absent (not `totalsRowShown: undefined`),
|
|
202
|
-
// preserving the round-trip: a table that never stated the flag must not gain one.
|
|
203
156
|
if (totalsRowShown !== undefined)
|
|
204
157
|
options.totalsRowShown = totalsRowShown;
|
|
205
158
|
if (style !== undefined)
|
|
@@ -1,44 +1,12 @@
|
|
|
1
|
-
// Modern threaded comments — the review-style conversations Excel has written since 2018, read from and
|
|
2
|
-
// written to their two parts: `xl/threadedComments/threadedComment{n}.xml` (per sheet) and
|
|
3
|
-
// `xl/persons/person.xml` (per workbook, the author registry).
|
|
4
|
-
//
|
|
5
|
-
// These are a Microsoft extension, not base ECMA-376, and they are a *separate* feature from legacy
|
|
6
|
-
// notes (`comments{n}.xml`, see `comments.ts`) rather than a newer spelling of them. A cell carries
|
|
7
|
-
// one or the other, never both — Excel refuses to add a note to a threaded cell and vice versa.
|
|
8
|
-
//
|
|
9
|
-
// A message identifies its author by `personId` into the registry and its thread by `id`/`parentId`:
|
|
10
|
-
// the first message of a thread has no `parentId`, every reply carries the head's `id`. Only the head
|
|
11
|
-
// carries `done`, so a thread's resolved state is its head's — a reply never says.
|
|
12
|
-
//
|
|
13
|
-
// These parsers describe a file that already exists, so they read leniently: an unrecognised or
|
|
14
|
-
// missing attribute yields a sensible default rather than a throw, and a message too incomplete to
|
|
15
|
-
// anchor is skipped instead of crashing the read. Optional wire attributes stay optional in the
|
|
16
|
-
// parsed shape rather than collapsing to `''`, so "the file did not say" never masquerades as a value
|
|
17
|
-
// the file contained.
|
|
18
|
-
//
|
|
19
|
-
// {@link buildCommentThreads} turns the flat message list into the model's threads, resolving each
|
|
20
|
-
// author and mention against the workbook's person registry. That grouping is deliberately not the
|
|
21
|
-
// parsers' job: they stay faithful to the part, one function per wire form.
|
|
22
|
-
//
|
|
23
|
-
// The writers are the exact inverse and carry no clock and no id generator: every guid and every
|
|
24
|
-
// timestamp is written from the model, verbatim, so the same workbook always serialises to the same
|
|
25
|
-
// bytes. A conversation Excel wrote round-trips as itself; one authored in the model carries whatever
|
|
26
|
-
// ids and dates the caller supplied.
|
|
27
1
|
import { decodeAddress } from '../../core/address.js';
|
|
28
2
|
import { MENTION_OFFSET_MAX, } from '../../core/comment-thread.js';
|
|
29
3
|
import { escapeAttr, escapeText, XML_DECLARATION } from '../../xml/xml.js';
|
|
30
4
|
import { boolStrict, localName, parseXml } from '../../xml/xml-read.js';
|
|
31
5
|
import { THREADED_COMMENTS_NS } from './namespaces.js';
|
|
32
|
-
/**
|
|
33
|
-
* Parse `xl/persons/person.xml` into its registered authors, in document order. Order carries no
|
|
34
|
-
* meaning — Excel re-sorts the list by person id when it saves — so nothing may depend on it. An
|
|
35
|
-
* entry without an `id` is skipped: no message could reference it.
|
|
36
|
-
*/
|
|
37
6
|
export function parsePersons(xml) {
|
|
38
7
|
const persons = [];
|
|
39
8
|
parseXml(xml, {
|
|
40
9
|
onOpen(name, attrs) {
|
|
41
|
-
// `personList` shares the prefix, so match the exact local name rather than a `startsWith`.
|
|
42
10
|
if (localName(name) !== 'person')
|
|
43
11
|
return;
|
|
44
12
|
const { id, displayName, userId, providerId } = attrs;
|
|
@@ -54,15 +22,7 @@ export function parsePersons(xml) {
|
|
|
54
22
|
});
|
|
55
23
|
return persons;
|
|
56
24
|
}
|
|
57
|
-
// Committing a message from its close handling alone would silently drop `<threadedComment …/>`,
|
|
58
|
-
// which fires no close of its own; presenting the self-closing form as an empty element gives it one.
|
|
59
25
|
const THREADED_COMMENT_EMPTY_CLOSES = new Set(['threadedComment']);
|
|
60
|
-
/**
|
|
61
|
-
* Parse a `threadedComment{n}.xml` part into its messages, in document order — thread order, with
|
|
62
|
-
* each thread's replies following its head. Grouping into threads is {@link buildCommentThreads}'s
|
|
63
|
-
* job; this stays faithful to the part. A message without a `ref` or `id` cannot be anchored or
|
|
64
|
-
* replied to and is skipped.
|
|
65
|
-
*/
|
|
66
26
|
export function parseThreadedComments(xml) {
|
|
67
27
|
const messages = [];
|
|
68
28
|
let open;
|
|
@@ -124,21 +84,12 @@ function threadedCommentFrom(attrs, text, mentions) {
|
|
|
124
84
|
...(parentId !== undefined ? { parentId } : {}),
|
|
125
85
|
};
|
|
126
86
|
}
|
|
127
|
-
// A mention without a target person or a usable span cannot be resolved or rendered, so it is dropped
|
|
128
|
-
// rather than carried as a mention over nothing — a `length` of 0 would be an invisible chip, and a
|
|
129
|
-
// negative or non-numeric offset would place it outside the text it is supposed to cover.
|
|
130
|
-
//
|
|
131
|
-
// The upper bound is what makes this a hostile-input guard rather than a tidiness check: an offset the
|
|
132
|
-
// wire cannot express would be re-emitted by the writer as an invalid attribute — and JavaScript spells a
|
|
133
|
-
// large enough number in exponent form (`1e+21`), which no schema accepts at all — costing the reader's
|
|
134
|
-
// leniency the entire conversation when Excel repairs the part. Dropping the chip costs a highlight.
|
|
135
87
|
function mentionFrom(attrs) {
|
|
136
88
|
const personId = attrs.mentionpersonId;
|
|
137
89
|
const startIndex = integerAttribute(attrs.startIndex);
|
|
138
90
|
const length = integerAttribute(attrs.length);
|
|
139
91
|
if (personId === undefined || startIndex === undefined || length === undefined)
|
|
140
92
|
return undefined;
|
|
141
|
-
// A negative offset points outside the text and a zero length spans nothing, so neither could render.
|
|
142
93
|
if (startIndex < 0 || length <= 0)
|
|
143
94
|
return undefined;
|
|
144
95
|
if (startIndex > MENTION_OFFSET_MAX || length > MENTION_OFFSET_MAX)
|
|
@@ -150,26 +101,14 @@ function mentionFrom(attrs) {
|
|
|
150
101
|
...(attrs.mentionId !== undefined ? { mentionId: attrs.mentionId } : {}),
|
|
151
102
|
};
|
|
152
103
|
}
|
|
153
|
-
// An integer attribute, or undefined when the file did not write a usable one. Blank is rejected before
|
|
154
|
-
// `Number` sees it, since `Number('')` is 0 — an empty attribute must not read as offset zero.
|
|
155
104
|
function integerAttribute(raw) {
|
|
156
105
|
if (raw === undefined || raw.trim() === '')
|
|
157
106
|
return undefined;
|
|
158
107
|
const value = Number(raw);
|
|
159
108
|
return Number.isInteger(value) ? value : undefined;
|
|
160
109
|
}
|
|
161
|
-
/**
|
|
162
|
-
* Group a part's messages into the model's threads, resolving every author and mention against the
|
|
163
|
-
* workbook's person registry (`personById`, typically `Workbook.getPerson`).
|
|
164
|
-
*
|
|
165
|
-
* Document order is thread order with each thread's replies following its head, so one pass suffices: a
|
|
166
|
-
* message with no `parentId` opens a thread, and a reply joins the thread its `parentId` names. A reply
|
|
167
|
-
* whose parent is unknown — a dangling `parentId` no Excel file produces — opens a thread of its own
|
|
168
|
-
* rather than being dropped, so a foreign generator's damage costs structure, never content.
|
|
169
|
-
*/
|
|
170
110
|
export function buildCommentThreads(messages, personById) {
|
|
171
111
|
const threads = [];
|
|
172
|
-
// The head's own message list, so a reply appends to the thread already published in `threads`.
|
|
173
112
|
const commentsByHeadId = new Map();
|
|
174
113
|
for (const message of messages) {
|
|
175
114
|
const comment = commentFrom(message, personById);
|
|
@@ -183,15 +122,10 @@ export function buildCommentThreads(messages, personById) {
|
|
|
183
122
|
continue;
|
|
184
123
|
const comments = [comment];
|
|
185
124
|
commentsByHeadId.set(message.id, comments);
|
|
186
|
-
// Resolved is the head's flag: a reply never carries `done`, so it never contradicts its thread.
|
|
187
125
|
threads.push({ ref, resolved: message.done, comments });
|
|
188
126
|
}
|
|
189
127
|
return threads;
|
|
190
128
|
}
|
|
191
|
-
// The canonical A1 form of a thread's anchor, or undefined when the file wrote something that cannot
|
|
192
|
-
// anchor one — a range, a bare row or column, or outright garbage. Canonicalising here is what lets
|
|
193
|
-
// every later consumer compare anchors as plain strings (`$B$2` and `B2` are one cell) and keeps a
|
|
194
|
-
// foreign file's malformed reference out of the writer, which anchors the thread's legacy fallback by it.
|
|
195
129
|
function anchorRef(reference) {
|
|
196
130
|
let decoded;
|
|
197
131
|
try {
|
|
@@ -223,18 +157,6 @@ function mentionOf(mention, personById) {
|
|
|
223
157
|
...(mention.mentionId !== undefined ? { mentionId: mention.mentionId } : {}),
|
|
224
158
|
};
|
|
225
159
|
}
|
|
226
|
-
/**
|
|
227
|
-
* Serialise one sheet's conversations into its `xl/threadedComments/threadedComment{n}.xml` part.
|
|
228
|
-
*
|
|
229
|
-
* Messages are written flat, in thread order with each thread's replies after its head — the shape
|
|
230
|
-
* {@link parseThreadedComments} reads back. The head/reply distinction the model holds as array position
|
|
231
|
-
* becomes `parentId` on every reply but the head, and `done="1"` goes on the head alone: only the head
|
|
232
|
-
* carries the flag on the wire, so a reply can never contradict the thread it belongs to. An open thread
|
|
233
|
-
* omits `done` entirely rather than writing `done="0"`, exactly as Excel does.
|
|
234
|
-
*
|
|
235
|
-
* A thread with no messages writes nothing: it has neither text to say nor a head id for its replies and
|
|
236
|
-
* its legacy fallback to hang off.
|
|
237
|
-
*/
|
|
238
160
|
export function threadedCommentsXml(threads) {
|
|
239
161
|
const messages = threads.flatMap((thread) => {
|
|
240
162
|
const [head, ...replies] = thread.comments;
|
|
@@ -248,11 +170,6 @@ export function threadedCommentsXml(threads) {
|
|
|
248
170
|
return (XML_DECLARATION +
|
|
249
171
|
`<ThreadedComments xmlns="${THREADED_COMMENTS_NS}">${messages.join('')}</ThreadedComments>`);
|
|
250
172
|
}
|
|
251
|
-
// One `<threadedComment>`. `tail` is the attribute that distinguishes the message's role — `done` for a
|
|
252
|
-
// resolved head, `parentId` for a reply, nothing for an open head. A `dT` or `personId` the model never
|
|
253
|
-
// held is omitted rather than written empty, so "the file did not say" stays distinguishable from "the
|
|
254
|
-
// file said nothing". Every value is escaped: an authored message's text and a foreign file's ids alike
|
|
255
|
-
// are untrusted, and an unescaped `"` would end the attribute and reshape the part.
|
|
256
173
|
function threadedCommentXml(ref, comment, tail) {
|
|
257
174
|
const date = comment.date === undefined ? '' : ` dT="${escapeAttr(comment.date)}"`;
|
|
258
175
|
const person = comment.personId === undefined ? '' : ` personId="${escapeAttr(comment.personId)}"`;
|
|
@@ -261,21 +178,10 @@ function threadedCommentXml(ref, comment, tail) {
|
|
|
261
178
|
mentionsXml(comment.mentions) +
|
|
262
179
|
'</threadedComment>');
|
|
263
180
|
}
|
|
264
|
-
// The `<mentions>` block, which follows `<text>` in the message. All four `<mention>` attributes are
|
|
265
|
-
// required — verified by dropping each in turn and getting `Sch_MissRequiredAttribute` — so a mention the
|
|
266
|
-
// model holds without a `mentionId`, or with a span the wire cannot express, cannot be written at all. It
|
|
267
|
-
// is dropped rather than given an invented id or a clamped span: the `@name` stays in the text and only
|
|
268
|
-
// the chip is lost, whereas an invalid part risks Excel repairing the whole conversation away.
|
|
269
|
-
//
|
|
270
|
-
// The bounds check is deliberately here as well as in the reader. Both the authoring verb and the parser
|
|
271
|
-
// already refuse an out-of-range offset, so nothing should reach this — but `restoreCommentThreads` takes
|
|
272
|
-
// a model wholesale, and a serialiser that *cannot* emit `length="1e+21"` beats one that merely is not
|
|
273
|
-
// currently handed one.
|
|
274
181
|
function mentionsXml(mentions) {
|
|
275
182
|
const entries = mentions.flatMap(({ personId, mentionId, startIndex, length }) => {
|
|
276
183
|
if (mentionId === undefined)
|
|
277
184
|
return [];
|
|
278
|
-
// A zero-length span is in range but renders nothing, so it is no more writable than one out of range.
|
|
279
185
|
if (!writableOffset(startIndex) || !writableOffset(length) || length === 0)
|
|
280
186
|
return [];
|
|
281
187
|
return [
|
|
@@ -285,18 +191,9 @@ function mentionsXml(mentions) {
|
|
|
285
191
|
});
|
|
286
192
|
return entries.length === 0 ? '' : `<mentions>${entries.join('')}</mentions>`;
|
|
287
193
|
}
|
|
288
|
-
// An offset the wire can express: a whole number within the schema's UInt32 ceiling.
|
|
289
194
|
function writableOffset(value) {
|
|
290
195
|
return Number.isInteger(value) && value >= 0 && value <= MENTION_OFFSET_MAX;
|
|
291
196
|
}
|
|
292
|
-
/**
|
|
293
|
-
* Serialise the workbook's identity registry into `xl/persons/person.xml` — singular and unnumbered,
|
|
294
|
-
* unlike the per-sheet thread parts.
|
|
295
|
-
*
|
|
296
|
-
* Entries are written in registry order, which carries no meaning: Excel re-sorts the list by person id
|
|
297
|
-
* whenever it saves, so this only has to be deterministic, not canonical. `userId`/`providerId` are
|
|
298
|
-
* written when the model holds them; a registry read from a file holds whatever that file stated.
|
|
299
|
-
*/
|
|
300
197
|
export function personsXml(persons) {
|
|
301
198
|
const entries = persons.map((person) => {
|
|
302
199
|
const userId = person.userId === undefined ? '' : ` userId="${escapeAttr(person.userId)}"`;
|
|
@@ -1,6 +1,3 @@
|
|
|
1
|
-
// Workbook-level serialisation: the package's `[Content_Types].xml`, its root and workbook `.rels`
|
|
2
|
-
// parts, `xl/workbook.xml` (sheets, defined names, calc/protection settings, pivot-cache and slicer
|
|
3
|
-
// registrations), and the `docProps` core/app property parts.
|
|
4
1
|
import { mangleFormula, quoteSheetName } from '../../core/formula.js';
|
|
5
2
|
import { WORKBOOK_PROTECTION_CREDENTIAL_ATTRS } from '../../core/workbook-protection.js';
|
|
6
3
|
import { escapeAttr, escapeText, XML_DECLARATION } from '../../xml/xml.js';
|
|
@@ -27,15 +24,10 @@ const CT = {
|
|
|
27
24
|
pivotTable: 'application/vnd.openxmlformats-officedocument.spreadsheetml.pivotTable+xml',
|
|
28
25
|
pivotCacheDefinition: 'application/vnd.openxmlformats-officedocument.spreadsheetml.pivotCacheDefinition+xml',
|
|
29
26
|
pivotCacheRecords: 'application/vnd.openxmlformats-officedocument.spreadsheetml.pivotCacheRecords+xml',
|
|
30
|
-
// Both threaded-comment parts are Microsoft extensions, hence the `vnd.ms-excel` vendor prefix rather
|
|
31
|
-
// than the `openxmlformats-officedocument` one every standard part carries.
|
|
32
27
|
threadedComments: 'application/vnd.ms-excel.threadedcomments+xml',
|
|
33
28
|
person: 'application/vnd.ms-excel.person+xml',
|
|
34
29
|
};
|
|
35
30
|
export function contentTypesXml(sheetCount, tables, commentNumbers, drawingNumbers, printerSettingsNumbers, mediaExtensions, hasSharedStrings, preservedParts, pivots, preservedWorkbookRefs, threadedCommentNumbers, hasPersons) {
|
|
36
|
-
// One extension→default-content-type map both halves read: the defaults render it, the overrides
|
|
37
|
-
// correct any preserved part whose own type differs from its extension's default. Sharing it is what
|
|
38
|
-
// keeps a `<Default>` and its `<Override>`s from ever disagreeing.
|
|
39
31
|
const extensionDefaults = buildExtensionDefaults(commentNumbers, printerSettingsNumbers, mediaExtensions, preservedParts);
|
|
40
32
|
return (XML_DECLARATION +
|
|
41
33
|
`<Types xmlns="${NS.contentTypes}">` +
|
|
@@ -43,12 +35,6 @@ export function contentTypesXml(sheetCount, tables, commentNumbers, drawingNumbe
|
|
|
43
35
|
contentTypeOverrides(sheetCount, tables, drawingNumbers, commentNumbers, hasSharedStrings, preservedParts, pivots, preservedWorkbookRefs, extensionDefaults, threadedCommentNumbers, hasPersons) +
|
|
44
36
|
'</Types>');
|
|
45
37
|
}
|
|
46
|
-
// The content type declared at the `<Default>` level for each file extension the package carries, in
|
|
47
|
-
// emission order: the built-in rels/xml pair, the vml/bin/image kinds the writer itself generates,
|
|
48
|
-
// then one default per *new* extension a preserved binary part introduces (first part of an extension
|
|
49
|
-
// wins). Keys are lower-cased for case-insensitive extension matching; each value keeps the extension
|
|
50
|
-
// token as it will be emitted so rendering stays byte-stable. A preserved `.xml` part is deliberately
|
|
51
|
-
// left out — it always carries its own type as a per-part override, never a generic `xml` default.
|
|
52
38
|
function buildExtensionDefaults(commentNumbers, printerSettingsNumbers, mediaExtensions, preservedParts) {
|
|
53
39
|
const defaults = new Map();
|
|
54
40
|
const add = (extension, contentType) => {
|
|
@@ -72,36 +58,16 @@ function buildExtensionDefaults(commentNumbers, printerSettingsNumbers, mediaExt
|
|
|
72
58
|
}
|
|
73
59
|
return defaults;
|
|
74
60
|
}
|
|
75
|
-
// A macro-enabled workbook's own VBA project is round-tripped as a preserved workbook reference
|
|
76
|
-
// (see `isPreservedWorkbookRelType` in read.ts), so its presence is what distinguishes a re-emitted
|
|
77
|
-
// .xlsm from a plain .xlsx: `xl/workbook.xml` must declare the macro-enabled content type or Excel
|
|
78
|
-
// flags the package as needing repair on open.
|
|
79
61
|
function isMacroEnabled(preservedWorkbookRefs) {
|
|
80
62
|
return preservedWorkbookRefs.some((ref) => ref.relType.endsWith('/vbaProject'));
|
|
81
63
|
}
|
|
82
|
-
// The extension-level `<Default>` declarations, rendered from the shared extension-default map (see
|
|
83
|
-
// {@link buildExtensionDefaults}) in insertion order. The raw bytes of a VML, a printer-settings blob,
|
|
84
|
-
// or an image carry no XML content type of their own, so each is declared once per extension rather
|
|
85
|
-
// than per part; a preserved binary part whose type differs from its extension's default is corrected
|
|
86
|
-
// by a per-part `<Override>` in {@link contentTypeOverrides}.
|
|
87
64
|
function contentTypeDefaults(extensionDefaults) {
|
|
88
65
|
return [...extensionDefaults.values()]
|
|
89
66
|
.map(({ extension, contentType }) => defaultType(extension, contentType))
|
|
90
67
|
.join('');
|
|
91
68
|
}
|
|
92
|
-
// The per-part `<Override>` declarations, in canonical package order: workbook, worksheets, tables,
|
|
93
|
-
// drawings, comments, the threaded-comment parts, each pivot's three parts, theme, styles, the optional
|
|
94
|
-
// shared strings and threaded-comment person registry, the doc-props pair, then any preserved part whose
|
|
95
|
-
// content type its extension's `<Default>` does not already carry — every `.xml` part (the generic `xml`
|
|
96
|
-
// default never matches a real part type) and any binary part sharing an extension with a differently-typed
|
|
97
|
-
// sibling (a `vbaProjectSignature.bin` next to a `vbaProject.bin`), which a lone extension default would
|
|
98
|
-
// otherwise mis-type.
|
|
99
69
|
function contentTypeOverrides(sheetCount, tables, drawingNumbers, commentNumbers, hasSharedStrings, preservedParts, pivots, preservedWorkbookRefs, extensionDefaults, threadedCommentNumbers, hasPersons) {
|
|
100
70
|
const preservedOverrides = preservedParts
|
|
101
|
-
// A preserved theme lands at the fixed theme path, whose override is already in the list below —
|
|
102
|
-
// and OPC forbids declaring the same PartName twice. The fixed declaration is the right one to
|
|
103
|
-
// keep: the part reached the model through a `.../theme` relationship, so its type is the theme
|
|
104
|
-
// type whatever the source package happened to declare.
|
|
105
71
|
.filter((part) => part.path !== THEME_PART_PATH)
|
|
106
72
|
.filter((part) => extensionDefaults.get(extensionOf(part.path).toLowerCase())?.contentType !==
|
|
107
73
|
part.contentType)
|
|
@@ -128,15 +94,9 @@ function contentTypeOverrides(sheetCount, tables, drawingNumbers, commentNumbers
|
|
|
128
94
|
function override(partName, contentType) {
|
|
129
95
|
return `<Override PartName="${partName}" ContentType="${contentType}"/>`;
|
|
130
96
|
}
|
|
131
|
-
// A `<Default>` content-type declaration binding a file extension to the type every part with that
|
|
132
|
-
// extension carries — the extension-level counterpart to {@link override}'s per-part declaration.
|
|
133
97
|
function defaultType(extension, contentType) {
|
|
134
98
|
return `<Default Extension="${extension}" ContentType="${contentType}"/>`;
|
|
135
99
|
}
|
|
136
|
-
// The package root relationships: the three the writer regenerates from the model (the office
|
|
137
|
-
// document and the core/app properties), followed by any preserved root references — customUI ribbon
|
|
138
|
-
// parts, custom properties, a thumbnail — re-declared with fresh ids past the fixed three so a
|
|
139
|
-
// round-trip keeps content wired from `_rels/.rels` that the model does not otherwise emit.
|
|
140
100
|
export function rootRelsXml(rootRefs) {
|
|
141
101
|
return relationshipsPart([
|
|
142
102
|
relationship('rId1', REL.officeDocument, 'xl/workbook.xml'),
|
|
@@ -165,12 +125,6 @@ export function workbookXml(workbook, preservedRels, pivots) {
|
|
|
165
125
|
workbookExtLstXml(preservedRels) +
|
|
166
126
|
'</workbook>');
|
|
167
127
|
}
|
|
168
|
-
// `<bookViews>` follows `<workbookProtection>` and precedes `<sheets>` in CT_Workbook order. Unlike
|
|
169
|
-
// every other optional block here it is emitted unconditionally: a consumer restores the document
|
|
170
|
-
// window from this rect and lays the sheet's panes out inside it, so a package without one leaves a
|
|
171
|
-
// frozen split measured against an uninitialised window (see `DEFAULT_WORKBOOK_VIEW`). The
|
|
172
|
-
// visibility and minimised flags are written only when they differ from the schema default, and
|
|
173
|
-
// `activeTab` only for a non-first sheet — exactly the shape Excel writes for an ordinary window.
|
|
174
128
|
function bookViewsXml(workbook) {
|
|
175
129
|
const view = workbook.view;
|
|
176
130
|
const activeTab = workbook.activeTabIndex;
|
|
@@ -182,12 +136,6 @@ function bookViewsXml(workbook) {
|
|
|
182
136
|
(activeTab === 0 ? '' : ` activeTab="${activeTab}"`);
|
|
183
137
|
return `<bookViews><workbookView${attrs}/></bookViews>`;
|
|
184
138
|
}
|
|
185
|
-
// The `<externalReferences>` element registers each preserved external link (a link to a source
|
|
186
|
-
// workbook), wired to the relationship reaching its `externalLink` part. It follows `<sheets>` in
|
|
187
|
-
// CT_Workbook order and precedes `<definedNames>`; its child order is the `[n]` index a formula or
|
|
188
|
-
// defined name resolves an external cell through, so entries are emitted by their captured
|
|
189
|
-
// `externalReferenceIndex` — keeping every `[1]`, `[2]`, … pointing at the same linked workbook it did
|
|
190
|
-
// before the round-trip. '' when no external link was preserved.
|
|
191
139
|
function externalReferencesXml(preservedRels) {
|
|
192
140
|
const links = preservedRels
|
|
193
141
|
.filter((ref) => ref.externalReferenceIndex !== undefined)
|
|
@@ -197,10 +145,6 @@ function externalReferencesXml(preservedRels) {
|
|
|
197
145
|
const entries = links.map((ref) => `<externalReference r:id="${ref.relId}"/>`).join('');
|
|
198
146
|
return `<externalReferences>${entries}</externalReferences>`;
|
|
199
147
|
}
|
|
200
|
-
// The workbook-body `<x14:slicerCaches>` extension that registers each preserved slicer cache, wired
|
|
201
|
-
// to the relationship reaching its cache part. Slicer caches (unlike pivot caches, which register in
|
|
202
|
-
// `<pivotCaches>`) live only in this extension block, so re-emitting it is what lets Excel rediscover
|
|
203
|
-
// the slicers. `<extLst>` is the final child of CT_Workbook. '' when no slicer cache was preserved.
|
|
204
148
|
function workbookExtLstXml(preservedRels) {
|
|
205
149
|
const caches = preservedRels.filter((ref) => ref.relType.endsWith('/slicerCache'));
|
|
206
150
|
if (caches.length === 0)
|
|
@@ -208,11 +152,6 @@ function workbookExtLstXml(preservedRels) {
|
|
|
208
152
|
const entries = caches.map((ref) => `<x14:slicerCache r:id="${ref.relId}"/>`).join('');
|
|
209
153
|
return `<extLst>${x14Ext(SLICER_CACHES_EXT_URI, `<x14:slicerCaches>${entries}</x14:slicerCaches>`)}</extLst>`;
|
|
210
154
|
}
|
|
211
|
-
// The `<pivotCaches>` element registers each pivot cache under the `cacheId` a pivot table resolves
|
|
212
|
-
// its cache through, wired to the relationship that reaches the cache definition. It follows
|
|
213
|
-
// `<calcPr>` in CT_Workbook order and carries both preserved caches (passed through from a read file)
|
|
214
|
-
// and caches the writer generated for modeled pivot tables. A slicer cache (no `cacheId`) is
|
|
215
|
-
// registered in a workbook extension block, not here, so it is skipped.
|
|
216
155
|
function pivotCachesXml(preservedRels, pivots) {
|
|
217
156
|
const preserved = preservedRels
|
|
218
157
|
.filter((ref) => ref.pivotCacheId !== undefined)
|
|
@@ -223,10 +162,6 @@ function pivotCachesXml(preservedRels, pivots) {
|
|
|
223
162
|
return '';
|
|
224
163
|
return `<pivotCaches>${entries.join('')}</pivotCaches>`;
|
|
225
164
|
}
|
|
226
|
-
// `<workbookProtection>` precedes `<sheets>` in CT_Workbook order. It re-emits the workbook's
|
|
227
|
-
// structure/window lock flags (each written only when true, so an unlocked aspect stays absent) and
|
|
228
|
-
// the preserved password/agile-hash credential attributes verbatim. Emitted only when the workbook
|
|
229
|
-
// actually declares protection — the flags or a credential — so an unprotected workbook stays clean.
|
|
230
165
|
function workbookProtectionXml(workbook) {
|
|
231
166
|
const p = workbook.protection;
|
|
232
167
|
if (p === undefined)
|
|
@@ -247,24 +182,10 @@ function workbookProtectionXml(workbook) {
|
|
|
247
182
|
return '';
|
|
248
183
|
return `<workbookProtection ${attrs.join(' ')}/>`;
|
|
249
184
|
}
|
|
250
|
-
// The `calcId` desktop Excel stamps into `<calcPr>` — the build number of the calc engine that last
|
|
251
|
-
// evaluated the workbook. Consumers only compare it to decide whether cached results are stale; we
|
|
252
|
-
// emit a fixed recent value alongside `fullCalcOnLoad`, which already forces a full recalculation.
|
|
253
185
|
const EXCEL_CALC_ID = '171027';
|
|
254
|
-
// `<calcPr>` follows `<definedNames>` in CT_Workbook order and carries the calculation settings.
|
|
255
|
-
// Today the model exposes a single one: `fullCalcOnLoad`, which tells the consumer to recalculate
|
|
256
|
-
// every formula on open instead of trusting the cached results. Emitted only when set, so an
|
|
257
|
-
// unmarked workbook keeps the element (and its `calcId`) out of the file entirely.
|
|
258
186
|
function calcPrXml(workbook) {
|
|
259
187
|
return workbook.fullCalcOnLoad ? `<calcPr calcId="${EXCEL_CALC_ID}" fullCalcOnLoad="1"/>` : '';
|
|
260
188
|
}
|
|
261
|
-
// The `<definedNames>` block follows `<sheets>` in the schema. A sheet-scoped name carries a
|
|
262
|
-
// `localSheetId` — the 0-based position of its sheet among the `<sheet>` entries, NOT the sheet's
|
|
263
|
-
// own id — so the index is resolved against the worksheet order here. The refersTo formula is the
|
|
264
|
-
// element's text content, run through the same `_xlfn.` function mangling the writer applies to a
|
|
265
|
-
// cell formula so a name defined as a modern function (a LAMBDA, an XLOOKUP-based name) is stored
|
|
266
|
-
// under the prefix Excel requires; a plain reference has no function call and passes through
|
|
267
|
-
// untouched. Only names that are actually set emit anything.
|
|
268
189
|
function definedNamesXml(workbook) {
|
|
269
190
|
const sheets = workbook.worksheets;
|
|
270
191
|
const userEntries = workbook.definedNames.map((name) => {
|
|
@@ -276,10 +197,6 @@ function definedNamesXml(workbook) {
|
|
|
276
197
|
return (`<definedName name="${escapeAttr(name.name)}"${scopeAttr}${commentAttr}${hiddenAttr}>` +
|
|
277
198
|
`${escapeText(mangleFormula(name.refersTo))}</definedName>`);
|
|
278
199
|
});
|
|
279
|
-
// Every sheet-level autofilter contributes the hidden, sheet-scoped `_FilterDatabase` built-in that
|
|
280
|
-
// Excel derives from its range. The reader drops these on load and rebuilds them from the sheet's
|
|
281
|
-
// `<autoFilter>`, so `Worksheet.autoFilter` stays the single source of truth and a round-trip never
|
|
282
|
-
// duplicates them.
|
|
283
200
|
const filterEntries = sheets.flatMap((sheet, index) => sheet.autoFilter === undefined
|
|
284
201
|
? []
|
|
285
202
|
: [
|
|
@@ -291,17 +208,10 @@ function definedNamesXml(workbook) {
|
|
|
291
208
|
return '';
|
|
292
209
|
return `<definedNames>${entries.join('')}</definedNames>`;
|
|
293
210
|
}
|
|
294
|
-
// Build the sheet-qualified, fully-absolute reference a `_FilterDatabase` name carries
|
|
295
|
-
// (`'Sheet 1'!$A$1:$C$10`) from a sheet name and its already-canonical `A1:C10` autofilter range.
|
|
296
211
|
function filterDatabaseRefersTo(sheetName, range) {
|
|
297
212
|
const absolute = range.replace(/([A-Z]+)(\d+)/g, '$$$1$$$2');
|
|
298
213
|
return `${quoteSheetName(sheetName)}!${absolute}`;
|
|
299
214
|
}
|
|
300
|
-
// The relationships the workbook part always carries after its per-sheet rels: `styles.xml` and
|
|
301
|
-
// `theme/theme1.xml`. Their count anchors every downstream rel id — `sharedStrings.xml` and the
|
|
302
|
-
// threaded-comment person registry (when present), then the preserved/pivot caches — so `write.ts`
|
|
303
|
-
// derives its `workbookRelBase` from this same constant rather than repeating the literal and risking
|
|
304
|
-
// drift.
|
|
305
215
|
export const FIXED_WORKBOOK_REL_COUNT = 2;
|
|
306
216
|
export function workbookRelsXml(sheetCount, hasSharedStrings, personsRelId, preservedRels, pivots) {
|
|
307
217
|
return relationshipsPart([
|
|
@@ -313,14 +223,10 @@ export function workbookRelsXml(sheetCount, hasSharedStrings, personsRelId, pres
|
|
|
313
223
|
relationship(`rId${sheetCount + FIXED_WORKBOOK_REL_COUNT + 1}`, REL.sharedStrings, 'sharedStrings.xml'),
|
|
314
224
|
]
|
|
315
225
|
: []),
|
|
316
|
-
// The threaded-comment identity registry every conversation on every sheet resolves its authors and
|
|
317
|
-
// @mentions through. Workbook-level and singular, so this one relationship serves all the sheets.
|
|
318
226
|
...(personsRelId === null
|
|
319
227
|
? []
|
|
320
228
|
: [relationship(personsRelId, REL.person, 'persons/person.xml')]),
|
|
321
|
-
// A preserved cache's target is package-absolute; express it relative to the workbook part.
|
|
322
229
|
...preservedRels.map((ref) => relationship(ref.relId, ref.relType, escapeAttr(relativePartPath('xl/workbook.xml', ref.entryPath)))),
|
|
323
|
-
// A generated pivot cache's workbook relationship reaches its cache definition part.
|
|
324
230
|
...pivots.map((pivot) => relationship(pivot.workbookRelId, REL.pivotCacheDefinition, `pivotCache/pivotCacheDefinition${pivot.number}.xml`)),
|
|
325
231
|
]);
|
|
326
232
|
}
|