@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
|
@@ -1,47 +1,18 @@
|
|
|
1
|
-
// Serialising conditional formatting to the classic `<conditionalFormatting>` worksheet element.
|
|
2
|
-
//
|
|
3
|
-
// Each block names its target range(s) in a `sqref` attribute and holds one or more `<cfRule>`
|
|
4
|
-
// children. A rule's shape depends on its type: a `dataBar`/`colorScale`/`iconSet` carries a scale
|
|
5
|
-
// element (its `<cfvo>` anchors and colours), while a `cellIs`/`expression`/`top10`/… carries its
|
|
6
|
-
// operands as `<formula>` children and points at a differential style by `dxfId`. A rule the library
|
|
7
|
-
// does not model in depth still round-trips its attributes, so nothing is silently dropped on save.
|
|
8
|
-
//
|
|
9
|
-
// A data bar's richer facets — its gradient fill, its negative-value fill colour, its axis colour —
|
|
10
|
-
// have no home in the classic `<dataBar>` element; Excel stores them only in the 2009 x14 extension.
|
|
11
|
-
// So a data-bar rule carrying any of them is written twice: the classic element (its anchors and bar
|
|
12
|
-
// colour, understood by every consumer) plus an `<x14:dataBar>` in the worksheet `<extLst>` carrying
|
|
13
|
-
// the extras, the two linked by a shared id. The reader folds the extension back onto the classic
|
|
14
|
-
// rule, so the gradient flag and the two extra colours survive a round-trip rather than being dropped.
|
|
15
1
|
import { boolAttr, escapeAttr, escapeText, stripFormulaEquals } from '../../xml/xml.js';
|
|
16
2
|
import { boolStrict, coerceNumericLiteral, localName, parseXml } from '../../xml/xml-read.js';
|
|
17
|
-
|
|
18
|
-
// exactly as Excel writes them, so no worksheet-root xmlns is needed. `CF_EXT_URI` scopes the
|
|
19
|
-
// worksheet's x14 conditional formattings; `DATABAR_LINK_EXT_URI` scopes the `<x14:id>` link a
|
|
20
|
-
// classic cfRule carries to name its extension.
|
|
3
|
+
import { colorAttrs, parseColor } from './color-xml.js';
|
|
21
4
|
import { CF_EXT_URI, DATABAR_LINK_EXT_URI, XM_NS } from './namespaces.js';
|
|
22
|
-
import { colorAttrs, parseColor } from './styles.js';
|
|
23
5
|
import { x14Ext } from './x14-ext.js';
|
|
24
|
-
// Excel's default data bar when the author supplies none: a min/max anchor pair and its standard blue.
|
|
25
6
|
const DEFAULT_DATABAR_CFVO = [{ type: 'min' }, { type: 'max' }];
|
|
26
7
|
const DEFAULT_DATABAR_COLOR = { argb: 'FF638EC6' };
|
|
27
|
-
// A data bar needs the x14 extension only when it carries a facet the classic element cannot express.
|
|
28
|
-
// A plain data bar (anchors and bar colour alone) stays classic-only, so an unadorned rule never
|
|
29
|
-
// fabricates an empty extension block.
|
|
30
8
|
function needsDataBarExt(rule) {
|
|
31
9
|
return (rule.gradient !== undefined ||
|
|
32
10
|
rule.negativeFillColor !== undefined ||
|
|
33
11
|
rule.axisColor !== undefined);
|
|
34
12
|
}
|
|
35
|
-
// The synthetic id linking a classic cfRule to its x14 extension. Excel uses a random GUID; any unique
|
|
36
|
-
// token that matches on both ends works, so a deterministic per-sheet index keeps the output stable
|
|
37
|
-
// and testable.
|
|
38
13
|
function dataBarExtGuid(index) {
|
|
39
14
|
return `{00000000-0000-0000-0000-${String(index + 1).padStart(12, '0')}}`;
|
|
40
15
|
}
|
|
41
|
-
// Assign every extended data bar its link id once, keyed by rule identity. The classic pass (which
|
|
42
|
-
// emits the `<extLst>` link on the cfRule) and the extension pass (which emits the `<x14:cfRule id>`)
|
|
43
|
-
// both read the id from this map, so the two ends of a link agree by construction — not by the two
|
|
44
|
-
// passes happening to walk the rules in the same order. A rule absent from the map needs no extension.
|
|
45
16
|
function dataBarExtLinks(formattings) {
|
|
46
17
|
const links = new Map();
|
|
47
18
|
let index = 0;
|
|
@@ -53,16 +24,8 @@ function dataBarExtLinks(formattings) {
|
|
|
53
24
|
}
|
|
54
25
|
return links;
|
|
55
26
|
}
|
|
56
|
-
// The three built-in visual rules. Each renders a built-in visual and carries no differential
|
|
57
|
-
// style (so the write side skips the dxf), and each nests its `<color>` children differently (so
|
|
58
|
-
// the read side, {@link ScaleKind}, tracks which one it is inside). This tuple drives both.
|
|
59
27
|
const SCALE_KINDS = ['dataBar', 'colorScale', 'iconSet'];
|
|
60
28
|
const SCALE_TYPES = new Set(SCALE_KINDS);
|
|
61
|
-
/**
|
|
62
|
-
* Serialise every conditional formatting on a sheet into its `<conditionalFormatting>` blocks, in
|
|
63
|
-
* insertion order. Returns '' when the sheet has none. A rule missing a `priority` is assigned the
|
|
64
|
-
* next free one, so the output always satisfies Excel's requirement that every cfRule carry one.
|
|
65
|
-
*/
|
|
66
29
|
export function conditionalFormattingsXml(formattings, styles) {
|
|
67
30
|
if (formattings.length === 0)
|
|
68
31
|
return '';
|
|
@@ -70,13 +33,6 @@ export function conditionalFormattingsXml(formattings, styles) {
|
|
|
70
33
|
const extLinks = dataBarExtLinks(formattings);
|
|
71
34
|
return formattings.map((cf) => blockXml(cf, styles, priority, extLinks)).join('');
|
|
72
35
|
}
|
|
73
|
-
/**
|
|
74
|
-
* The worksheet `<extLst>` `<ext>` carrying the x14 data-bar extensions, or '' when no data bar needs
|
|
75
|
-
* one. Each extension's `<x14:cfRule id>` is read from the same {@link dataBarExtLinks} map the classic
|
|
76
|
-
* pass uses for the `<x14:id>` its cfRule carries, so the two ends of a link always match. Emitted bare
|
|
77
|
-
* (no `<extLst>` wrapper) so the worksheet serialiser can gather it into a single `<extLst>` beside the
|
|
78
|
-
* data-validation extension.
|
|
79
|
-
*/
|
|
80
36
|
export function conditionalFormattingsExtXml(formattings) {
|
|
81
37
|
const extLinks = dataBarExtLinks(formattings);
|
|
82
38
|
const items = [];
|
|
@@ -91,9 +47,6 @@ export function conditionalFormattingsExtXml(formattings) {
|
|
|
91
47
|
return '';
|
|
92
48
|
return x14Ext(CF_EXT_URI, `<x14:conditionalFormattings>${items.join('')}</x14:conditionalFormattings>`);
|
|
93
49
|
}
|
|
94
|
-
// One `<x14:conditionalFormatting>`: an `<x14:cfRule type="dataBar" id>` mirroring the classic anchors
|
|
95
|
-
// as `<x14:cfvo>` and adding the facets the classic element cannot carry (gradient, negative-fill and
|
|
96
|
-
// axis colours), with the target range in an `<xm:sqref>` child — the shape Excel writes.
|
|
97
50
|
function x14DataBarXml(ref, rule, guid) {
|
|
98
51
|
const cfvo = rule.cfvo && rule.cfvo.length > 0 ? rule.cfvo : DEFAULT_DATABAR_CFVO;
|
|
99
52
|
const anchors = cfvo.map(x14CfvoXml).join('');
|
|
@@ -107,15 +60,12 @@ function x14DataBarXml(ref, rule, guid) {
|
|
|
107
60
|
`<x14:dataBar${gradient}>${anchors}${negative}${axis}</x14:dataBar>` +
|
|
108
61
|
`</x14:cfRule><xm:sqref>${escapeText(ref)}</xm:sqref></x14:conditionalFormatting>`);
|
|
109
62
|
}
|
|
110
|
-
// An x14 scale anchor. A `min`/`max` carries no value and self-closes; the rest wrap their value in an
|
|
111
|
-
// `<xm:f>` (the extension form stores every anchor value as a formula).
|
|
112
63
|
function x14CfvoXml(cfvo) {
|
|
113
64
|
const type = escapeAttr(cfvo.type);
|
|
114
65
|
if (cfvo.value === undefined)
|
|
115
66
|
return `<x14:cfvo type="${type}"/>`;
|
|
116
67
|
return `<x14:cfvo type="${type}"><xm:f>${escapeText(String(cfvo.value))}</xm:f></x14:cfvo>`;
|
|
117
68
|
}
|
|
118
|
-
// The `<extLst>` a classic data-bar cfRule carries to name its x14 extension by shared id.
|
|
119
69
|
function cfRuleExtLinkXml(guid) {
|
|
120
70
|
return `<extLst>${x14Ext(DATABAR_LINK_EXT_URI, `<x14:id>${guid}</x14:id>`)}</extLst>`;
|
|
121
71
|
}
|
|
@@ -125,7 +75,6 @@ function blockXml(cf, styles, priority, extLinks) {
|
|
|
125
75
|
}
|
|
126
76
|
function ruleXml(rule, styles, priority, extLinks) {
|
|
127
77
|
const p = rule.priority ?? priority.next;
|
|
128
|
-
// Keep the running counter ahead of any explicit priority so later auto-assigned ones stay unique.
|
|
129
78
|
priority.next = Math.max(priority.next, p) + 1;
|
|
130
79
|
const attrs = [`type="${escapeAttr(rule.type)}"`];
|
|
131
80
|
const dxfId = resolveDxfId(rule, styles);
|
|
@@ -153,9 +102,6 @@ function ruleXml(rule, styles, priority, extLinks) {
|
|
|
153
102
|
if (rule.stdDev !== undefined)
|
|
154
103
|
attrs.push(`stdDev="${rule.stdDev}"`);
|
|
155
104
|
let body = SCALE_TYPES.has(rule.type) ? scaleXml(rule) : formulaeXml(rule.formulae);
|
|
156
|
-
// A data bar with x14-only facets links to its extension by the id assigned in dataBarExtLinks; the
|
|
157
|
-
// extension itself rides in the worksheet <extLst>. The link is the cfRule's last child, after the
|
|
158
|
-
// dataBar. A rule absent from the map carries no extension.
|
|
159
105
|
const extGuid = extLinks.get(rule);
|
|
160
106
|
if (extGuid !== undefined)
|
|
161
107
|
body += cfRuleExtLinkXml(extGuid);
|
|
@@ -163,9 +109,6 @@ function ruleXml(rule, styles, priority, extLinks) {
|
|
|
163
109
|
? `<cfRule ${attrs.join(' ')}/>`
|
|
164
110
|
: `<cfRule ${attrs.join(' ')}>${body}</cfRule>`;
|
|
165
111
|
}
|
|
166
|
-
// A rule points at a differential style either by a preserved index read from a file (`dxfId`) or by
|
|
167
|
-
// a style authored on the rule (interned here). The preserved index wins — it references the original
|
|
168
|
-
// file's dxf table, which the writer re-emits verbatim. Scale rules never carry one.
|
|
169
112
|
function resolveDxfId(rule, styles) {
|
|
170
113
|
if (SCALE_TYPES.has(rule.type))
|
|
171
114
|
return undefined;
|
|
@@ -187,19 +130,12 @@ function scaleXml(rule) {
|
|
|
187
130
|
return colorScaleXml(rule);
|
|
188
131
|
return iconSetXml(rule);
|
|
189
132
|
}
|
|
190
|
-
// A data bar states its low and high anchors and its bar colour. The minimal call (no cfvo, no colour)
|
|
191
|
-
// gains Excel's own defaults — a min/max anchor pair and the standard blue — rather than an invalid
|
|
192
|
-
// empty element. The gradient flag and the negative-fill/axis colours have no home in this classic
|
|
193
|
-
// element; they ride in the x14 extension (see {@link conditionalFormattingsExtXml}), linked from the
|
|
194
|
-
// cfRule that wraps this by a shared id.
|
|
195
133
|
function dataBarXml(rule) {
|
|
196
134
|
const cfvo = rule.cfvo && rule.cfvo.length > 0 ? rule.cfvo : DEFAULT_DATABAR_CFVO;
|
|
197
135
|
const color = rule.color ?? DEFAULT_DATABAR_COLOR;
|
|
198
136
|
const anchors = cfvo.map(cfvoXml).join('');
|
|
199
137
|
return `<dataBar>${anchors}<color ${colorAttrs(color)}/></dataBar>`;
|
|
200
138
|
}
|
|
201
|
-
// A colour scale pairs each anchor with a colour; a missing colour list falls back to none, still a
|
|
202
|
-
// well-formed (if plain) element.
|
|
203
139
|
function colorScaleXml(rule) {
|
|
204
140
|
const anchors = (rule.cfvo ?? []).map(cfvoXml).join('');
|
|
205
141
|
const colors = (rule.colors ?? []).map((c) => `<color ${colorAttrs(c)}/>`).join('');
|
|
@@ -210,20 +146,10 @@ function iconSetXml(rule) {
|
|
|
210
146
|
const anchors = (rule.cfvo ?? []).map(cfvoXml).join('');
|
|
211
147
|
return `<iconSet${name}>${anchors}</iconSet>`;
|
|
212
148
|
}
|
|
213
|
-
// One scale anchor. `min`/`max` carry no value; the rest state theirs in `val` (a formula anchor's
|
|
214
|
-
// value is its formula text, escaped like any attribute).
|
|
215
149
|
function cfvoXml(cfvo) {
|
|
216
150
|
const val = cfvo.value !== undefined ? ` val="${escapeAttr(String(cfvo.value))}"` : '';
|
|
217
151
|
return `<cfvo type="${escapeAttr(cfvo.type)}"${val}/>`;
|
|
218
152
|
}
|
|
219
|
-
/**
|
|
220
|
-
* Parse a worksheet's conditional formatting into the model. The classic `<conditionalFormatting>`
|
|
221
|
-
* blocks supply every rule; the x14 extension (`<x14:conditionalFormatting>` inside `<extLst>`) is
|
|
222
|
-
* read only to enrich a classic data bar with the facets the classic element cannot carry — the
|
|
223
|
-
* gradient flag and the negative-fill and axis colours — matched by the shared id the two ends link
|
|
224
|
-
* on. An extension rule with no classic counterpart (a rule that lives only in x14) is ignored, so it
|
|
225
|
-
* is never half-read into a broken classic rule.
|
|
226
|
-
*/
|
|
227
153
|
export function parseConditionalFormattings(xml) {
|
|
228
154
|
const blocks = [];
|
|
229
155
|
let block;
|
|
@@ -231,9 +157,6 @@ export function parseConditionalFormattings(xml) {
|
|
|
231
157
|
let scale;
|
|
232
158
|
let capturingFormula = false;
|
|
233
159
|
let formulaText = '';
|
|
234
|
-
// Classic data-bar rules that named an extension, paired with the id they linked on, plus the
|
|
235
|
-
// extensions gathered from the worksheet <extLst>. The two are married after the pass — the
|
|
236
|
-
// extension always follows the classic blocks in document order, so it is known by then.
|
|
237
160
|
const linked = [];
|
|
238
161
|
const extById = new Map();
|
|
239
162
|
let x14Ext;
|
|
@@ -244,8 +167,6 @@ export function parseConditionalFormattings(xml) {
|
|
|
244
167
|
onOpen(name, attrs, selfClosing) {
|
|
245
168
|
const ln = localName(name);
|
|
246
169
|
if (name.includes(':')) {
|
|
247
|
-
// The `<x14:id>` a classic data bar carries to name its extension: capture its text into the
|
|
248
|
-
// open draft. The rest are the worksheet extension's own elements.
|
|
249
170
|
if (ln === 'id' && draft !== undefined) {
|
|
250
171
|
capturingX14Id = true;
|
|
251
172
|
x14IdText = '';
|
|
@@ -255,7 +176,6 @@ export function parseConditionalFormattings(xml) {
|
|
|
255
176
|
x14ExtId = attrs.id;
|
|
256
177
|
}
|
|
257
178
|
else if (x14Ext !== undefined && ln === 'dataBar') {
|
|
258
|
-
// gradient defaults to true in the x14 schema, so an absent attribute reads as a gradient.
|
|
259
179
|
x14Ext.gradient = attrs.gradient !== '0';
|
|
260
180
|
}
|
|
261
181
|
else if (x14Ext !== undefined && ln === 'negativeFillColor') {
|
|
@@ -270,8 +190,6 @@ export function parseConditionalFormattings(xml) {
|
|
|
270
190
|
block = { ref: attrs.sqref ?? '', rules: [] };
|
|
271
191
|
}
|
|
272
192
|
else if (ln === 'cfRule' && block !== undefined) {
|
|
273
|
-
// A rule with no operands (e.g. duplicateValues) is a self-closing element that fires no
|
|
274
|
-
// close event, so it must be finalised here; one with children waits for its </cfRule>.
|
|
275
193
|
if (selfClosing) {
|
|
276
194
|
block.rules.push(finalizeRule(newDraft(attrs)));
|
|
277
195
|
}
|
|
@@ -360,12 +278,6 @@ export function parseConditionalFormattings(xml) {
|
|
|
360
278
|
function emptyExt() {
|
|
361
279
|
return { gradient: undefined, negativeFillColor: undefined, axisColor: undefined };
|
|
362
280
|
}
|
|
363
|
-
/**
|
|
364
|
-
* Extract the differential-style (`<dxf>`) fragments from styles.xml, each verbatim. Preserving the
|
|
365
|
-
* raw XML — rather than reparsing and re-serialising — is what keeps a foreign dxf's number format a
|
|
366
|
-
* real format code on re-write instead of a coerced `"[object Object]"`, and keeps every conditional
|
|
367
|
-
* formatting's `dxfId` index pointing at the same style it did in the source file.
|
|
368
|
-
*/
|
|
369
281
|
export function parseDxfs(stylesXml) {
|
|
370
282
|
const block = /<dxfs\b[^>]*>([\s\S]*?)<\/dxfs>/.exec(stylesXml);
|
|
371
283
|
if (block === null)
|
|
@@ -385,7 +297,6 @@ function newDraft(attrs) {
|
|
|
385
297
|
stdDev: parseFiniteAttr(attrs.stdDev),
|
|
386
298
|
percent: boolStrict(attrs.percent),
|
|
387
299
|
bottom: boolStrict(attrs.bottom),
|
|
388
|
-
// aboveAverage defaults to true in OOXML; only an explicit "0" means below-average.
|
|
389
300
|
aboveAverage: attrs.aboveAverage === undefined ? undefined : attrs.aboveAverage !== '0',
|
|
390
301
|
equalAverage: boolStrict(attrs.equalAverage),
|
|
391
302
|
dxfId: parseIndexAttr(attrs.dxfId),
|
|
@@ -397,18 +308,12 @@ function newDraft(attrs) {
|
|
|
397
308
|
x14Id: undefined,
|
|
398
309
|
};
|
|
399
310
|
}
|
|
400
|
-
// priority/rank/stdDev must be finite; a malformed value is dropped rather than propagated as NaN —
|
|
401
|
-
// `priority` in particular feeds the writer's running priority counter (see `ruleXml`), so one bad
|
|
402
|
-
// value would otherwise poison every later rule's auto-assigned priority on the same sheet.
|
|
403
311
|
function parseFiniteAttr(value) {
|
|
404
312
|
if (value === undefined)
|
|
405
313
|
return undefined;
|
|
406
314
|
const n = Number(value);
|
|
407
315
|
return Number.isFinite(n) ? n : undefined;
|
|
408
316
|
}
|
|
409
|
-
// dxfId is preserved as the raw string (not renumbered) so it keeps pointing at the same slot in the
|
|
410
|
-
// dxf table on re-write; it must still be a non-negative integer, so a malformed value is dropped
|
|
411
|
-
// rather than later coercing to `dxfId="NaN"` in {@link resolveDxfId}.
|
|
412
317
|
function parseIndexAttr(value) {
|
|
413
318
|
if (value === undefined)
|
|
414
319
|
return undefined;
|
|
@@ -457,7 +362,6 @@ function parseCfvo(attrs) {
|
|
|
457
362
|
const type = (attrs.type ?? 'num');
|
|
458
363
|
const cfvo = { type };
|
|
459
364
|
if (attrs.val !== undefined) {
|
|
460
|
-
// A `formula` anchor's value is an expression and stays a string; the rest are numeric.
|
|
461
365
|
cfvo.value = type === 'formula' ? attrs.val : coerceNumericLiteral(attrs.val);
|
|
462
366
|
}
|
|
463
367
|
return cfvo;
|
|
@@ -1,30 +1,8 @@
|
|
|
1
|
-
// Data validation I/O — the sheet-level `<dataValidations>` element and the reader that folds each
|
|
2
|
-
// rule back onto its range.
|
|
3
|
-
//
|
|
4
|
-
// A validation stores its operands in `<formula1>`/`<formula2>` child elements and its target in a
|
|
5
|
-
// `sqref` attribute. The operand text carries NO leading '=' (the '=' is an authoring convention, not
|
|
6
|
-
// part of the stored formula), so the writer strips one and the reader keeps whatever it finds. A
|
|
7
|
-
// numeric-typed rule's literal operand parses to a number; a cell reference, defined name, or list
|
|
8
|
-
// source keeps its string, so a reference is never coerced to NaN and lost.
|
|
9
|
-
//
|
|
10
|
-
// The extended `<x14:dataValidation>` form (2009 extension schema) carries the validations a legacy
|
|
11
|
-
// element cannot express — chiefly a list whose source lives on another sheet. It lives in the
|
|
12
|
-
// worksheet `<extLst>`, keeps its target in a `<xm:sqref>` child rather than a `sqref` attribute, and
|
|
13
|
-
// wraps each operand in an `<xm:f>` under `<x14:formula1>`/`<x14:formula2>`. A rule read from that
|
|
14
|
-
// form is tagged `extended` so it is written back there; the two forms are parsed and serialised by
|
|
15
|
-
// prefix so neither reader mistakes one for the other.
|
|
16
1
|
import { escapeAttr, escapeText, stripFormulaEquals } from '../../xml/xml.js';
|
|
17
2
|
import { boolStrict, coerceNumericLiteral, localName, parseXml } from '../../xml/xml-read.js';
|
|
18
|
-
// The x14/xm extension namespaces and `DATA_VALIDATION_EXT_URI` are declared inline on the elements
|
|
19
|
-
// that need them, exactly as Excel writes them, so the block is self-contained and the worksheet root
|
|
20
|
-
// needs no extra namespace declaration.
|
|
21
3
|
import { DATA_VALIDATION_EXT_URI, XM_NS } from './namespaces.js';
|
|
22
4
|
import { x14Ext } from './x14-ext.js';
|
|
23
|
-
// The typed validations whose literal operands are numbers; `list`/`custom` operands stay strings.
|
|
24
5
|
const TYPED = new Set(['whole', 'decimal', 'date', 'time', 'textLength']);
|
|
25
|
-
/** The standard `<dataValidations>` element for the rules stored in the legacy form, or '' when the
|
|
26
|
-
* sheet has none of them — so a sheet with only extended (or no) validations stays byte-clean here.
|
|
27
|
-
* The extended rules are emitted separately by {@link dataValidationsExtXml}. */
|
|
28
6
|
export function dataValidationsXml(entries) {
|
|
29
7
|
const standard = entries.filter((entry) => !entry.extended);
|
|
30
8
|
if (standard.length === 0)
|
|
@@ -32,9 +10,6 @@ export function dataValidationsXml(entries) {
|
|
|
32
10
|
const items = standard.map(({ sqref, rule }) => dataValidationXml(sqref, rule)).join('');
|
|
33
11
|
return `<dataValidations count="${standard.length}">${items}</dataValidations>`;
|
|
34
12
|
}
|
|
35
|
-
/** The `<ext>` carrying the extended (`<x14:dataValidation>`) rules, or '' when the sheet declares
|
|
36
|
-
* none. Emitted bare (no `<extLst>` wrapper) so the worksheet serialiser can gather it into a single
|
|
37
|
-
* `<extLst>` beside the conditional-formatting extension — a worksheet may carry at most one. */
|
|
38
13
|
export function dataValidationsExtXml(entries) {
|
|
39
14
|
const extended = entries.filter((entry) => entry.extended);
|
|
40
15
|
if (extended.length === 0)
|
|
@@ -42,10 +17,6 @@ export function dataValidationsExtXml(entries) {
|
|
|
42
17
|
const items = extended.map(({ sqref, rule }) => extendedDataValidationXml(sqref, rule)).join('');
|
|
43
18
|
return x14Ext(DATA_VALIDATION_EXT_URI, `<x14:dataValidations count="${extended.length}" xmlns:xm="${XM_NS}">${items}</x14:dataValidations>`);
|
|
44
19
|
}
|
|
45
|
-
// The shared attributes of a validation, in CT_DataValidation order: type, errorStyle, operator,
|
|
46
|
-
// allowBlank, showInputMessage, showErrorMessage, errorTitle, error, promptTitle, prompt. The target
|
|
47
|
-
// range differs between the two forms (a `sqref` attribute vs an `<xm:sqref>` child), so it is not
|
|
48
|
-
// part of this shared prefix.
|
|
49
20
|
function ruleAttrs(rule) {
|
|
50
21
|
return (` type="${rule.type}"` +
|
|
51
22
|
(rule.errorStyle !== undefined ? ` errorStyle="${rule.errorStyle}"` : '') +
|
|
@@ -58,25 +29,17 @@ function ruleAttrs(rule) {
|
|
|
58
29
|
(rule.promptTitle !== undefined ? ` promptTitle="${escapeAttr(rule.promptTitle)}"` : '') +
|
|
59
30
|
(rule.prompt !== undefined ? ` prompt="${escapeAttr(rule.prompt)}"` : ''));
|
|
60
31
|
}
|
|
61
|
-
// The standard element: shared attributes, then `sqref` last, then `<formula1>`/`<formula2>` bodies.
|
|
62
32
|
function dataValidationXml(sqref, rule) {
|
|
63
33
|
const [f1, f2] = operands(rule);
|
|
64
34
|
const body = (f1 !== undefined ? `<formula1>${escapeText(stripFormulaEquals(f1))}</formula1>` : '') +
|
|
65
35
|
(f2 !== undefined ? `<formula2>${escapeText(stripFormulaEquals(f2))}</formula2>` : '');
|
|
66
36
|
return `<dataValidation${ruleAttrs(rule)} sqref="${escapeAttr(sqref)}">${body}</dataValidation>`;
|
|
67
37
|
}
|
|
68
|
-
// A rule's two operands with any non-finite numeric bound dropped: a NaN/±Infinity operand (e.g. a
|
|
69
|
-
// date validation whose bound failed to coerce to a serial) has no OOXML representation, so it is
|
|
70
|
-
// omitted rather than serialised as the literal "NaN" — the same graceful degradation a non-finite
|
|
71
|
-
// cell value gets.
|
|
72
38
|
function operands(rule) {
|
|
73
39
|
const drop = (v) => typeof v === 'number' && !Number.isFinite(v) ? undefined : v;
|
|
74
40
|
const [f1, f2] = rule.formulae ?? [];
|
|
75
41
|
return [drop(f1), drop(f2)];
|
|
76
42
|
}
|
|
77
|
-
// The extended element: same shared attributes, but each operand wraps in `<x14:formula1><xm:f>…` and
|
|
78
|
-
// the target range is an `<xm:sqref>` child that follows the formulae. The `xr:uid` Excel adds is
|
|
79
|
-
// revision metadata it regenerates freely, so it is not modelled or re-emitted.
|
|
80
43
|
function extendedDataValidationXml(sqref, rule) {
|
|
81
44
|
const [f1, f2] = operands(rule);
|
|
82
45
|
const body = (f1 !== undefined
|
|
@@ -88,7 +51,6 @@ function extendedDataValidationXml(sqref, rule) {
|
|
|
88
51
|
`<xm:sqref>${escapeText(sqref)}</xm:sqref>`;
|
|
89
52
|
return `<x14:dataValidation${ruleAttrs(rule)}>${body}</x14:dataValidation>`;
|
|
90
53
|
}
|
|
91
|
-
/** Parse every standard `<dataValidation>` out of a worksheet part into range-bound rules. */
|
|
92
54
|
export function parseDataValidations(xml) {
|
|
93
55
|
const entries = [];
|
|
94
56
|
let current;
|
|
@@ -96,7 +58,6 @@ export function parseDataValidations(xml) {
|
|
|
96
58
|
parseXml(xml, {
|
|
97
59
|
onOpen(name, attrs) {
|
|
98
60
|
const ln = localName(name);
|
|
99
|
-
// Only the standard, unprefixed element — an `x14:dataValidation` is left for the extended path.
|
|
100
61
|
if (ln === 'dataValidation' && !name.includes(':')) {
|
|
101
62
|
current = { attrs, formulae: [] };
|
|
102
63
|
}
|
|
@@ -137,9 +98,6 @@ function buildEntry(attrs, formulae) {
|
|
|
137
98
|
const rule = buildRule(attrs, formulae);
|
|
138
99
|
return rule === undefined ? undefined : { sqref, rule };
|
|
139
100
|
}
|
|
140
|
-
// The rule carried by a validation element of either form: its attributes decide the type, operator,
|
|
141
|
-
// flags, and messages; its `<formula1>`/`<formula2>` operands become `formulae`. The target range is
|
|
142
|
-
// supplied separately by each form's caller, so it is not read here.
|
|
143
101
|
function buildRule(attrs, formulae) {
|
|
144
102
|
const { type } = attrs;
|
|
145
103
|
if (type === undefined)
|
|
@@ -149,8 +107,6 @@ function buildRule(attrs, formulae) {
|
|
|
149
107
|
rule.operator = attrs.operator;
|
|
150
108
|
}
|
|
151
109
|
else if (TYPED.has(type)) {
|
|
152
|
-
// Excel omits `operator="between"` because it is the default for a typed rule; restore it so a
|
|
153
|
-
// reader sees the operator the rule actually enforces.
|
|
154
110
|
rule.operator = 'between';
|
|
155
111
|
}
|
|
156
112
|
if (boolStrict(attrs.allowBlank))
|
|
@@ -169,9 +125,6 @@ function buildRule(attrs, formulae) {
|
|
|
169
125
|
rule.prompt = attrs.prompt;
|
|
170
126
|
if (attrs.promptTitle !== undefined)
|
|
171
127
|
rule.promptTitle = attrs.promptTitle;
|
|
172
|
-
// A `list`/`custom` operand is always a string (a source list or an expression); every other
|
|
173
|
-
// type's literal operand coerces to a number when it is one, so a numeric bound reads back as a
|
|
174
|
-
// number while a cell reference or defined name survives as its verbatim string.
|
|
175
128
|
const parsed = formulae
|
|
176
129
|
.filter((f) => f !== undefined)
|
|
177
130
|
.map((f) => (type === 'list' || type === 'custom' ? f : coerceNumericLiteral(f)));
|
|
@@ -179,15 +132,9 @@ function buildRule(attrs, formulae) {
|
|
|
179
132
|
rule.formulae = parsed;
|
|
180
133
|
return rule;
|
|
181
134
|
}
|
|
182
|
-
/** Parse every extended `<x14:dataValidation>` out of a worksheet's `<extLst>` into range-bound
|
|
183
|
-
* rules tagged `extended`, so a cross-sheet or whole-column list validation Excel stored only in the
|
|
184
|
-
* 2009 extension form is read back rather than dropped. The standard parser ignores these (they are
|
|
185
|
-
* prefixed); this one, symmetrically, handles only the prefixed elements. */
|
|
186
135
|
export function parseExtendedDataValidations(xml) {
|
|
187
136
|
const entries = [];
|
|
188
137
|
let current;
|
|
189
|
-
// Which operand an `<xm:f>` feeds (set by the enclosing `<x14:formula1>`/`<x14:formula2>`), and
|
|
190
|
-
// which child element's text is currently being gathered.
|
|
191
138
|
let slot;
|
|
192
139
|
let capture;
|
|
193
140
|
let text = '';
|
|
@@ -195,7 +142,6 @@ export function parseExtendedDataValidations(xml) {
|
|
|
195
142
|
onOpen(name, attrs) {
|
|
196
143
|
const ln = localName(name);
|
|
197
144
|
const prefixed = name.includes(':');
|
|
198
|
-
// A `<x14:dataValidation>`; its attributes (type, flags, messages) build the rule.
|
|
199
145
|
if (ln === 'dataValidation' && prefixed) {
|
|
200
146
|
current = { attrs, formulae: [], sqref: '' };
|
|
201
147
|
}
|
|
@@ -249,8 +195,6 @@ function buildExtendedEntry(attrs, formulae, sqref) {
|
|
|
249
195
|
const rule = buildRule(attrs, formulae);
|
|
250
196
|
return rule === undefined ? undefined : { sqref, rule, extended: true };
|
|
251
197
|
}
|
|
252
|
-
/** Fold parsed validations onto a sheet, each bound to its original range and carrying its form: an
|
|
253
|
-
* `extended` entry is re-attached as extended so a round-trip writes it back to the x14 block. */
|
|
254
198
|
export function applyDataValidations(sheet, entries) {
|
|
255
199
|
for (const { sqref, rule, extended } of entries) {
|
|
256
200
|
sheet.addDataValidation(sqref, rule, extended ? { extended: true } : {});
|
package/dist/io/xlsx/edit-vba.js
CHANGED
|
@@ -1,18 +1,3 @@
|
|
|
1
|
-
// Structurally edit an existing `.xlsm` at the *package* level: remove a module, or add a reference,
|
|
2
|
-
// returning new package bytes with every other part preserved byte-for-byte. Only `xl/vbaProject.bin` is
|
|
3
|
-
// rewritten (plus dropping a now-stale signature); worksheets, styles, drawings, and every other part
|
|
4
|
-
// ride through untouched.
|
|
5
|
-
//
|
|
6
|
-
// This is the highest-fidelity way to tweak an existing macro project. The alternative — `readXlsx` →
|
|
7
|
-
// the matching `Workbook` method → `writeXlsx` — rebuilds the whole package from the parsed model, which
|
|
8
|
-
// re-serialises every part and so only preserves what the model captures. For a rich, real-world
|
|
9
|
-
// workbook that round-trip can perturb parts Excel is strict about; splicing the original bytes cannot,
|
|
10
|
-
// because it never re-authors anything but the macro project. Use this when the input is a real file
|
|
11
|
-
// whose non-macro content must be preserved exactly.
|
|
12
|
-
//
|
|
13
|
-
// Authoring or editing module SOURCE is not here: Excel runs a module's compiled p-code, not its source,
|
|
14
|
-
// so that needs the offline `tools/vba-compiler` (VBIDE), which can produce a whole edited `.xlsm`
|
|
15
|
-
// directly (ADR 0019).
|
|
16
1
|
import { strFromU8, strToU8, unzipSync, zipSync } from 'fflate';
|
|
17
2
|
import { VbaAuthorError } from '../../vba/errors.js';
|
|
18
3
|
import { addVbaReference, removeVbaModule, } from '../../vba/project-editor.js';
|
|
@@ -20,42 +5,14 @@ import { relsPathFor } from '../opc/part-paths.js';
|
|
|
20
5
|
import { parseRelationshipRecords, relationshipTargetByType, resolveRelativePart, resolveWorkbookPart, } from '../opc/read-opc.js';
|
|
21
6
|
const OFFICE_DOCUMENT_REL = 'officeDocument';
|
|
22
7
|
const VBA_PROJECT_REL = 'vbaProject';
|
|
23
|
-
// Every signature flavour Excel writes over a VBA project (legacy, agile, V3) shares this local-name
|
|
24
|
-
// prefix; all become stale the instant the project's bytes change and must be dropped with it.
|
|
25
8
|
const VBA_SIGNATURE_REL_INFIX = 'vbaProjectSignature';
|
|
26
|
-
/**
|
|
27
|
-
* Remove a standard module from an existing macro-enabled package's VBA project, returning new package
|
|
28
|
-
* bytes. Every part but `xl/vbaProject.bin` is preserved byte-for-byte (see {@link removeVbaModule} for
|
|
29
|
-
* what changes within it), and any digital signature over the old project is dropped because it cannot
|
|
30
|
-
* validate the new bytes.
|
|
31
|
-
*
|
|
32
|
-
* @throws {VbaAuthorError} if the package carries no VBA project, `name` is not in the project, or names
|
|
33
|
-
* a `document`/`designer` module.
|
|
34
|
-
* @throws {VbaParseError} if the attached `vbaProject.bin` is malformed.
|
|
35
|
-
*/
|
|
36
9
|
export function editXlsxVbaRemoveModule(xlsx, name) {
|
|
37
10
|
return applyToVbaProjectPart(xlsx, (bin) => removeVbaModule(bin, name));
|
|
38
11
|
}
|
|
39
|
-
/**
|
|
40
|
-
* Add a registered (COM type-library) reference to an existing macro-enabled package's VBA project,
|
|
41
|
-
* returning new package bytes. Every part but `xl/vbaProject.bin` is preserved byte-for-byte (see
|
|
42
|
-
* {@link addVbaReference} for what changes within it), and any digital signature over the old project is
|
|
43
|
-
* dropped because it cannot validate the new bytes.
|
|
44
|
-
*
|
|
45
|
-
* @throws {VbaAuthorError} if the package carries no VBA project, or any field of `ref` is invalid (see
|
|
46
|
-
* {@link VbaLibraryReference}).
|
|
47
|
-
* @throws {VbaParseError} if the attached `vbaProject.bin` is malformed.
|
|
48
|
-
*/
|
|
49
12
|
export function editXlsxVbaAddReference(xlsx, ref) {
|
|
50
13
|
return applyToVbaProjectPart(xlsx, (bin) => addVbaReference(bin, ref));
|
|
51
14
|
}
|
|
52
|
-
// Shared plumbing for every package-level VBA edit: unzip, locate `xl/vbaProject.bin`, replace it with
|
|
53
|
-
// whatever `apply` produces, drop a now-stale signature, and re-zip. `apply` is expected to validate
|
|
54
|
-
// fail-closed itself (every project-editor primitive does), so a bad edit throws before `files` is
|
|
55
|
-
// touched.
|
|
56
15
|
function applyToVbaProjectPart(xlsx, apply) {
|
|
57
|
-
// Widen off fflate's `Uint8Array<ArrayBuffer>` element type so spliced/re-serialised parts (whose
|
|
58
|
-
// buffers are `ArrayBufferLike`) assign back into the map.
|
|
59
16
|
const files = unzipSync(xlsx);
|
|
60
17
|
const binPath = locateVbaProjectPart(files);
|
|
61
18
|
const bin = binPath === undefined ? undefined : files[binPath];
|
|
@@ -66,9 +23,6 @@ function applyToVbaProjectPart(xlsx, apply) {
|
|
|
66
23
|
dropStaleSignature(files, binPath);
|
|
67
24
|
return zipSync(files);
|
|
68
25
|
}
|
|
69
|
-
// Resolve the package's `xl/vbaProject.bin` part the way the reader does: `_rels/.rels` → the
|
|
70
|
-
// officeDocument (workbook) part → its `.rels` → the `vbaProject` relationship, each target resolved
|
|
71
|
-
// relative to its referrer. undefined when the package declares no such relationship (a macro-free book).
|
|
72
26
|
function locateVbaProjectPart(files) {
|
|
73
27
|
const rootRels = textPart(files, '_rels/.rels');
|
|
74
28
|
if (rootRels === undefined)
|
|
@@ -85,8 +39,6 @@ function locateVbaProjectPart(files) {
|
|
|
85
39
|
return undefined;
|
|
86
40
|
return resolveVbaTarget(workbookPath, vbaTarget);
|
|
87
41
|
}
|
|
88
|
-
// The workbook's vbaProject relationship uses a workbook-relative target (`vbaProject.bin`); resolve it
|
|
89
|
-
// through the workbook-part rule so both that and an absolute `/xl/vbaProject.bin` land on the part path.
|
|
90
42
|
function resolveVbaTarget(workbookPath, target) {
|
|
91
43
|
if (target.startsWith('/'))
|
|
92
44
|
return target.slice(1);
|
|
@@ -94,9 +46,6 @@ function resolveVbaTarget(workbookPath, target) {
|
|
|
94
46
|
return resolveWorkbookPart(target);
|
|
95
47
|
return resolveRelativePart(workbookPath, target);
|
|
96
48
|
}
|
|
97
|
-
// Editing the project invalidates any signature over it, so remove every signature part the project's
|
|
98
|
-
// `.rels` reaches, the relationships that point at them, and their content-type overrides — leaving a
|
|
99
|
-
// package that advertises no signature rather than a broken one (mirrors Workbook.vbaProjectBytes).
|
|
100
49
|
function dropStaleSignature(files, binPath) {
|
|
101
50
|
const binRelsPath = relsPathFor(binPath);
|
|
102
51
|
const binRels = textPart(files, binRelsPath);
|
|
@@ -125,12 +74,9 @@ function textPart(files, path) {
|
|
|
125
74
|
const bytes = files[path];
|
|
126
75
|
return bytes === undefined ? undefined : strFromU8(bytes);
|
|
127
76
|
}
|
|
128
|
-
// Drop the `<Relationship>` element carrying a given Id, matching the element as a whole (self-closing
|
|
129
|
-
// or paired) so attribute order does not matter.
|
|
130
77
|
function removeRelationshipById(xml, id) {
|
|
131
78
|
return xml.replace(/<Relationship\b[^>]*?\/>|<Relationship\b[\s\S]*?<\/Relationship>/g, (element) => (new RegExp(`\\bId="${escapeRegExp(id)}"`).test(element) ? '' : element));
|
|
132
79
|
}
|
|
133
|
-
// Drop the `<Override>` naming a given part path; PartName is the full, unambiguous package path.
|
|
134
80
|
function removeContentTypeOverride(xml, partPath) {
|
|
135
81
|
return xml.replace(/<Override\b[^>]*?\/>/g, (element) => element.includes(`PartName="/${partPath}"`) ? '' : element);
|
|
136
82
|
}
|
package/dist/io/xlsx/errors.js
CHANGED
|
@@ -1,16 +1,4 @@
|
|
|
1
1
|
import { XlsxError } from '../../errors.js';
|
|
2
|
-
/**
|
|
3
|
-
* Thrown when an `.xlsx` package's XML content is well-formed but does not describe a workbook this
|
|
4
|
-
* library can act on — a `xl/workbook.xml` that declares no worksheets, say.
|
|
5
|
-
*
|
|
6
|
-
* It sits between two neighbours. {@link XmlParseError} reports that the *markup* did not parse;
|
|
7
|
-
* {@link UnsupportedFormatError} reports that the *container* is not one we read at all. This one
|
|
8
|
-
* fires when both of those were fine and the document itself is nonetheless not a workbook.
|
|
9
|
-
*
|
|
10
|
-
* The reader is deliberately lenient about content it does not recognise — an unknown element is
|
|
11
|
-
* skipped, not fatal — so this is rare by design, and reaching it means something a workbook cannot
|
|
12
|
-
* do without being corrupt.
|
|
13
|
-
*/
|
|
14
2
|
export class XlsxParseError extends XlsxError {
|
|
15
3
|
name = 'XlsxParseError';
|
|
16
4
|
code = 'malformed-input';
|
|
@@ -1,28 +1,13 @@
|
|
|
1
|
-
// Cell hyperlinks — the sheet-level `<hyperlinks>` element, its external relationships, and the
|
|
2
|
-
// reader that folds a link back onto its cell's value.
|
|
3
|
-
//
|
|
4
|
-
// A hyperlink is not stored inside the cell in OOXML: the `<c>` holds only the visible label (a
|
|
5
|
-
// normal string value), while a separate `<hyperlink>` child of `<worksheet>` ties an A1 reference
|
|
6
|
-
// to a destination. An EXTERNAL destination (a URL) is reached indirectly, through a sheet
|
|
7
|
-
// relationship carrying `TargetMode="External"` that the `<hyperlink>` names by `r:id`. An INTERNAL
|
|
8
|
-
// destination (a location inside the same workbook, which the author writes as a `#`-prefixed value)
|
|
9
|
-
// is held directly in a `location` attribute with NO relationship — emitting an internal target as an
|
|
10
|
-
// external relationship makes a strict consumer resolve both the rel and the location and render the
|
|
11
|
-
// destination doubled.
|
|
12
1
|
import { decodeRange } from '../../core/address.js';
|
|
13
2
|
import { isHyperlinkValue, isRichTextValue } from '../../core/value.js';
|
|
14
3
|
import { escapeAttr } from '../../xml/xml.js';
|
|
15
4
|
import { localName, parseXml } from '../../xml/xml-read.js';
|
|
16
|
-
/** Gather every hyperlink cell on a sheet, in row-major order. */
|
|
17
5
|
export function collectHyperlinks(sheet) {
|
|
18
6
|
const links = [];
|
|
19
7
|
for (const { cells } of sheet.rows()) {
|
|
20
8
|
for (const cell of cells) {
|
|
21
9
|
const value = cell.value;
|
|
22
10
|
if (isHyperlinkValue(value)) {
|
|
23
|
-
// A link that spans a range carries its extent in `range`; the anchor cell (this one) is the
|
|
24
|
-
// range's top-left. Emit that extent as `ref` so the clickable area survives, falling back to
|
|
25
|
-
// the single cell for an ordinary link.
|
|
26
11
|
links.push({
|
|
27
12
|
ref: value.range ?? cell.address,
|
|
28
13
|
target: value.hyperlink,
|
|
@@ -33,22 +18,15 @@ export function collectHyperlinks(sheet) {
|
|
|
33
18
|
}
|
|
34
19
|
return links;
|
|
35
20
|
}
|
|
36
|
-
/** Split collected links into internal (location, no rel) and external (relationship) forms, drawing
|
|
37
|
-
* each external link's relationship id from the sheet's allocator so external ids follow every other
|
|
38
|
-
* sheet-local relationship in canonical order. An internal ('#'-prefixed) link consumes no id. */
|
|
39
21
|
export function planHyperlinks(links, rels) {
|
|
40
22
|
return links.map((link) => {
|
|
41
23
|
const tooltip = link.tooltip !== undefined ? { tooltip: link.tooltip } : {};
|
|
42
|
-
// A '#'-prefixed target is an internal document location: held verbatim in `location`, with no
|
|
43
|
-
// relationship. Everything else is an external URL reached through a relationship.
|
|
44
24
|
if (link.target.startsWith('#')) {
|
|
45
25
|
return { ref: link.ref, location: link.target.slice(1), ...tooltip };
|
|
46
26
|
}
|
|
47
27
|
return { ref: link.ref, relId: rels.next(), target: link.target, ...tooltip };
|
|
48
28
|
});
|
|
49
29
|
}
|
|
50
|
-
/** The `<hyperlinks>` element, or '' when the sheet has none. Attribute order follows CT_Hyperlink:
|
|
51
|
-
* `ref`, `r:id`, `location`, `tooltip`. */
|
|
52
30
|
export function hyperlinksXml(links) {
|
|
53
31
|
if (links.length === 0)
|
|
54
32
|
return '';
|
|
@@ -62,7 +40,6 @@ export function hyperlinksXml(links) {
|
|
|
62
40
|
.join('');
|
|
63
41
|
return `<hyperlinks>${items}</hyperlinks>`;
|
|
64
42
|
}
|
|
65
|
-
/** Parse every `<hyperlink>` element out of a worksheet part. */
|
|
66
43
|
export function parseSheetHyperlinks(xml) {
|
|
67
44
|
const links = [];
|
|
68
45
|
parseXml(xml, {
|
|
@@ -82,26 +59,17 @@ export function parseSheetHyperlinks(xml) {
|
|
|
82
59
|
});
|
|
83
60
|
return links;
|
|
84
61
|
}
|
|
85
|
-
/** Fold parsed hyperlinks onto a sheet's cells, wrapping each cell's existing value (its visible
|
|
86
|
-
* label) into a {@link HyperlinkValue}. `rels` maps a relationship id to its target URL. */
|
|
87
62
|
export function applyHyperlinks(sheet, links, rels) {
|
|
88
63
|
for (const link of links) {
|
|
89
64
|
const target = resolveTarget(link, rels);
|
|
90
65
|
if (target === undefined)
|
|
91
66
|
continue;
|
|
92
|
-
// A hyperlink may span a range (`ref="D1:H1"`); Excel anchors the link at the range's top-left
|
|
93
|
-
// cell. Decode once so a multi-cell link folds onto that anchor rather than asking the sheet for
|
|
94
|
-
// a range address it cannot resolve. A ref that does not decode is skipped, not fatal.
|
|
95
67
|
const decoded = decodeRefSafe(link.ref);
|
|
96
68
|
if (decoded === undefined)
|
|
97
69
|
continue;
|
|
98
70
|
const cell = sheet.getCell(decoded.tl.address);
|
|
99
|
-
// The visible label is the cell's own value: a plain string, or rich text when the label
|
|
100
|
-
// carried per-run formatting. Any other value kind has no textual label, so it reads as empty.
|
|
101
71
|
const cellValue = cell.value;
|
|
102
72
|
const text = typeof cellValue === 'string' ? cellValue : isRichTextValue(cellValue) ? cellValue : '';
|
|
103
|
-
// Record the extent only when the link genuinely spans more than the anchor, so an ordinary
|
|
104
|
-
// single-cell link stays a plain value and the range survives verbatim for a multi-cell one.
|
|
105
73
|
const spansRange = decoded.tl.address !== decoded.br.address;
|
|
106
74
|
const value = {
|
|
107
75
|
hyperlink: target,
|
|
@@ -112,10 +80,6 @@ export function applyHyperlinks(sheet, links, rels) {
|
|
|
112
80
|
cell.value = value;
|
|
113
81
|
}
|
|
114
82
|
}
|
|
115
|
-
// A hyperlink's `ref` decoded to a range (a single-cell ref decodes to a range whose corners
|
|
116
|
-
// coincide), so the caller can both anchor on the top-left and tell whether the link spans further.
|
|
117
|
-
// Returns undefined for a ref that does not decode, so a malformed hyperlink is dropped rather than
|
|
118
|
-
// crashing the load.
|
|
119
83
|
function decodeRefSafe(ref) {
|
|
120
84
|
try {
|
|
121
85
|
return decodeRange(ref);
|
|
@@ -129,11 +93,7 @@ function resolveTarget(link, rels) {
|
|
|
129
93
|
const base = rels.get(link.rid);
|
|
130
94
|
if (base === undefined)
|
|
131
95
|
return undefined;
|
|
132
|
-
// A foreign file may split an external URL's fragment into the `location` attribute, apart from
|
|
133
|
-
// the relationship Target; rejoin them so the whole URL survives. Our own writer keeps the
|
|
134
|
-
// fragment in the Target, so a link we wrote never carries both.
|
|
135
96
|
return link.location !== undefined ? `${base}#${link.location}` : base;
|
|
136
97
|
}
|
|
137
|
-
// No relationship: an internal ('#'-prefixed) target held verbatim in `location`.
|
|
138
98
|
return link.location !== undefined ? `#${link.location}` : undefined;
|
|
139
99
|
}
|