@shbernal/ts-xlsx 1.0.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/LICENSE +23 -0
- package/README.md +230 -0
- package/dist/core/address.d.ts +49 -0
- package/dist/core/address.js +125 -0
- package/dist/core/autofilter.d.ts +55 -0
- package/dist/core/autofilter.js +48 -0
- package/dist/core/cell.d.ts +132 -0
- package/dist/core/cell.js +249 -0
- package/dist/core/color-resolution.d.ts +58 -0
- package/dist/core/color-resolution.js +170 -0
- package/dist/core/column.d.ts +87 -0
- package/dist/core/column.js +167 -0
- package/dist/core/comment-thread.d.ts +116 -0
- package/dist/core/comment-thread.js +62 -0
- package/dist/core/conditional-formatting-overlay.d.ts +15 -0
- package/dist/core/conditional-formatting-overlay.js +24 -0
- package/dist/core/conditional-formatting.d.ts +70 -0
- package/dist/core/conditional-formatting.js +35 -0
- package/dist/core/containers.d.ts +2 -0
- package/dist/core/containers.js +25 -0
- package/dist/core/data-validation-overlay.d.ts +28 -0
- package/dist/core/data-validation-overlay.js +56 -0
- package/dist/core/data-validation.d.ts +38 -0
- package/dist/core/data-validation.js +13 -0
- package/dist/core/date.d.ts +25 -0
- package/dist/core/date.js +58 -0
- package/dist/core/formula.d.ts +49 -0
- package/dist/core/formula.js +319 -0
- package/dist/core/grid-edits.d.ts +22 -0
- package/dist/core/grid-edits.js +206 -0
- package/dist/core/image.d.ts +76 -0
- package/dist/core/image.js +65 -0
- package/dist/core/internal.d.ts +11 -0
- package/dist/core/internal.js +29 -0
- package/dist/core/merge.d.ts +13 -0
- package/dist/core/merge.js +26 -0
- package/dist/core/modern-functions.d.ts +1 -0
- package/dist/core/modern-functions.js +165 -0
- package/dist/core/page-setup.d.ts +94 -0
- package/dist/core/page-setup.js +6 -0
- package/dist/core/pivot-table.d.ts +113 -0
- package/dist/core/pivot-table.js +249 -0
- package/dist/core/preserved.d.ts +58 -0
- package/dist/core/preserved.js +5 -0
- package/dist/core/protection.d.ts +67 -0
- package/dist/core/protection.js +68 -0
- package/dist/core/range.d.ts +112 -0
- package/dist/core/range.js +283 -0
- package/dist/core/row.d.ts +73 -0
- package/dist/core/row.js +135 -0
- package/dist/core/style.d.ts +246 -0
- package/dist/core/style.js +125 -0
- package/dist/core/table-style.d.ts +67 -0
- package/dist/core/table-style.js +90 -0
- package/dist/core/table.d.ts +194 -0
- package/dist/core/table.js +291 -0
- package/dist/core/theme.d.ts +90 -0
- package/dist/core/theme.js +272 -0
- package/dist/core/value.d.ts +115 -0
- package/dist/core/value.js +144 -0
- package/dist/core/workbook-protection.d.ts +27 -0
- package/dist/core/workbook-protection.js +25 -0
- package/dist/core/workbook.d.ts +528 -0
- package/dist/core/workbook.js +732 -0
- package/dist/core/worksheet-model.d.ts +34 -0
- package/dist/core/worksheet-model.js +100 -0
- package/dist/core/worksheet.d.ts +647 -0
- package/dist/core/worksheet.js +1025 -0
- package/dist/customui/errors.d.ts +15 -0
- package/dist/customui/errors.js +15 -0
- package/dist/customui/index.d.ts +2 -0
- package/dist/customui/index.js +7 -0
- package/dist/customui/ribbon.d.ts +86 -0
- package/dist/customui/ribbon.js +180 -0
- package/dist/entries/core.d.ts +23 -0
- package/dist/entries/core.js +20 -0
- package/dist/entries/csv.d.ts +2 -0
- package/dist/entries/csv.js +6 -0
- package/dist/entries/customui.d.ts +1 -0
- package/dist/entries/customui.js +3 -0
- package/dist/entries/errors.d.ts +7 -0
- package/dist/entries/errors.js +20 -0
- package/dist/entries/vba.d.ts +2 -0
- package/dist/entries/vba.js +7 -0
- package/dist/entries/xlsb.d.ts +1 -0
- package/dist/entries/xlsb.js +7 -0
- package/dist/entries/xlsx.d.ts +5 -0
- package/dist/entries/xlsx.js +18 -0
- package/dist/errors.d.ts +65 -0
- package/dist/errors.js +66 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +19 -0
- package/dist/io/csv/read.d.ts +15 -0
- package/dist/io/csv/read.js +113 -0
- package/dist/io/csv/write.d.ts +28 -0
- package/dist/io/csv/write.js +124 -0
- package/dist/io/opc/errors.d.ts +46 -0
- package/dist/io/opc/errors.js +49 -0
- package/dist/io/opc/inflate.d.ts +15 -0
- package/dist/io/opc/inflate.js +91 -0
- package/dist/io/opc/namespaces.d.ts +18 -0
- package/dist/io/opc/namespaces.js +18 -0
- package/dist/io/opc/part-paths.d.ts +4 -0
- package/dist/io/opc/part-paths.js +35 -0
- package/dist/io/opc/read-opc.d.ts +24 -0
- package/dist/io/opc/read-opc.js +153 -0
- package/dist/io/opc/read-options.d.ts +10 -0
- package/dist/io/opc/read-options.js +7 -0
- package/dist/io/opc/rels.d.ts +15 -0
- package/dist/io/opc/rels.js +28 -0
- package/dist/io/opc/sniff-format.d.ts +27 -0
- package/dist/io/opc/sniff-format.js +92 -0
- package/dist/io/style/xf-style.d.ts +47 -0
- package/dist/io/style/xf-style.js +95 -0
- package/dist/io/xlsb/errors.d.ts +15 -0
- package/dist/io/xlsb/errors.js +15 -0
- package/dist/io/xlsb/formula.d.ts +48 -0
- package/dist/io/xlsb/formula.js +398 -0
- package/dist/io/xlsb/primitives.d.ts +86 -0
- package/dist/io/xlsb/primitives.js +230 -0
- package/dist/io/xlsb/ptg-functions.d.ts +16 -0
- package/dist/io/xlsb/ptg-functions.js +659 -0
- package/dist/io/xlsb/read-shared-strings.d.ts +2 -0
- package/dist/io/xlsb/read-shared-strings.js +22 -0
- package/dist/io/xlsb/read-styles.d.ts +4 -0
- package/dist/io/xlsb/read-styles.js +386 -0
- package/dist/io/xlsb/read-worksheet.d.ts +9 -0
- package/dist/io/xlsb/read-worksheet.js +301 -0
- package/dist/io/xlsb/read.d.ts +20 -0
- package/dist/io/xlsb/read.js +199 -0
- package/dist/io/xlsb/record-stream.d.ts +14 -0
- package/dist/io/xlsb/record-stream.js +56 -0
- package/dist/io/xlsb/record-types.d.ts +55 -0
- package/dist/io/xlsb/record-types.js +73 -0
- package/dist/io/xlsx/cell-accumulator.d.ts +25 -0
- package/dist/io/xlsx/cell-accumulator.js +172 -0
- package/dist/io/xlsx/cell-value.d.ts +32 -0
- package/dist/io/xlsx/cell-value.js +85 -0
- package/dist/io/xlsx/comments.d.ts +74 -0
- package/dist/io/xlsx/comments.js +243 -0
- package/dist/io/xlsx/conditional-formatting.d.ts +32 -0
- package/dist/io/xlsx/conditional-formatting.js +464 -0
- package/dist/io/xlsx/data-validation.d.ts +20 -0
- package/dist/io/xlsx/data-validation.js +258 -0
- package/dist/io/xlsx/edit-vba.d.ts +23 -0
- package/dist/io/xlsx/edit-vba.js +139 -0
- package/dist/io/xlsx/errors.d.ts +17 -0
- package/dist/io/xlsx/errors.js +17 -0
- package/dist/io/xlsx/hyperlinks.d.ts +42 -0
- package/dist/io/xlsx/hyperlinks.js +139 -0
- package/dist/io/xlsx/images.d.ts +34 -0
- package/dist/io/xlsx/images.js +218 -0
- package/dist/io/xlsx/namespaces.d.ts +55 -0
- package/dist/io/xlsx/namespaces.js +55 -0
- package/dist/io/xlsx/package-plan.d.ts +103 -0
- package/dist/io/xlsx/package-plan.js +168 -0
- package/dist/io/xlsx/pivot-read.d.ts +5 -0
- package/dist/io/xlsx/pivot-read.js +134 -0
- package/dist/io/xlsx/pivot.d.ts +12 -0
- package/dist/io/xlsx/pivot.js +141 -0
- package/dist/io/xlsx/read-rows.d.ts +85 -0
- package/dist/io/xlsx/read-rows.js +286 -0
- package/dist/io/xlsx/read-styles.d.ts +8 -0
- package/dist/io/xlsx/read-styles.js +551 -0
- package/dist/io/xlsx/read-worksheet.d.ts +4 -0
- package/dist/io/xlsx/read-worksheet.js +528 -0
- package/dist/io/xlsx/read.d.ts +32 -0
- package/dist/io/xlsx/read.js +680 -0
- package/dist/io/xlsx/relationships.d.ts +33 -0
- package/dist/io/xlsx/relationships.js +47 -0
- package/dist/io/xlsx/rich-runs.d.ts +13 -0
- package/dist/io/xlsx/rich-runs.js +59 -0
- package/dist/io/xlsx/rich-text.d.ts +7 -0
- package/dist/io/xlsx/rich-text.js +23 -0
- package/dist/io/xlsx/shared-formulas.d.ts +6 -0
- package/dist/io/xlsx/shared-formulas.js +52 -0
- package/dist/io/xlsx/shared-strings-read.d.ts +2 -0
- package/dist/io/xlsx/shared-strings-read.js +70 -0
- package/dist/io/xlsx/shared-strings.d.ts +17 -0
- package/dist/io/xlsx/shared-strings.js +50 -0
- package/dist/io/xlsx/sheet-properties.d.ts +13 -0
- package/dist/io/xlsx/sheet-properties.js +211 -0
- package/dist/io/xlsx/styles.d.ts +138 -0
- package/dist/io/xlsx/styles.js +821 -0
- package/dist/io/xlsx/tables.d.ts +9 -0
- package/dist/io/xlsx/tables.js +208 -0
- package/dist/io/xlsx/threaded-comments.d.ts +102 -0
- package/dist/io/xlsx/threaded-comments.js +308 -0
- package/dist/io/xlsx/workbook-xml.d.ts +12 -0
- package/dist/io/xlsx/workbook-xml.js +353 -0
- package/dist/io/xlsx/worksheet-xml.d.ts +86 -0
- package/dist/io/xlsx/worksheet-xml.js +643 -0
- package/dist/io/xlsx/write-stream.d.ts +171 -0
- package/dist/io/xlsx/write-stream.js +399 -0
- package/dist/io/xlsx/write.d.ts +81 -0
- package/dist/io/xlsx/write.js +396 -0
- package/dist/io/xlsx/x14-ext.d.ts +9 -0
- package/dist/io/xlsx/x14-ext.js +12 -0
- package/dist/vba/cfb-writer.d.ts +19 -0
- package/dist/vba/cfb-writer.js +277 -0
- package/dist/vba/cfb.d.ts +16 -0
- package/dist/vba/cfb.js +324 -0
- package/dist/vba/codepage.d.ts +18 -0
- package/dist/vba/codepage.js +87 -0
- package/dist/vba/errors.d.ts +24 -0
- package/dist/vba/errors.js +24 -0
- package/dist/vba/index.d.ts +5 -0
- package/dist/vba/index.js +15 -0
- package/dist/vba/ms-ovba.d.ts +12 -0
- package/dist/vba/ms-ovba.js +176 -0
- package/dist/vba/project-editor.d.ts +62 -0
- package/dist/vba/project-editor.js +377 -0
- package/dist/vba/project.d.ts +45 -0
- package/dist/vba/project.js +160 -0
- package/dist/vba/vba-encoding.d.ts +14 -0
- package/dist/vba/vba-encoding.js +39 -0
- package/dist/xml/errors.d.ts +15 -0
- package/dist/xml/errors.js +15 -0
- package/dist/xml/xml-read.d.ts +107 -0
- package/dist/xml/xml-read.js +283 -0
- package/dist/xml/xml.d.ts +41 -0
- package/dist/xml/xml.js +86 -0
- package/package.json +117 -0
|
@@ -0,0 +1,258 @@
|
|
|
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
|
+
import { escapeAttr, escapeText, stripFormulaEquals } from "../../xml/xml.js";
|
|
17
|
+
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
|
+
import { DATA_VALIDATION_EXT_URI, XM_NS } from "./namespaces.js";
|
|
22
|
+
import { x14Ext } from "./x14-ext.js";
|
|
23
|
+
// The typed validations whose literal operands are numbers; `list`/`custom` operands stay strings.
|
|
24
|
+
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
|
+
export function dataValidationsXml(entries) {
|
|
29
|
+
const standard = entries.filter((entry) => !entry.extended);
|
|
30
|
+
if (standard.length === 0)
|
|
31
|
+
return '';
|
|
32
|
+
const items = standard.map(({ sqref, rule }) => dataValidationXml(sqref, rule)).join('');
|
|
33
|
+
return `<dataValidations count="${standard.length}">${items}</dataValidations>`;
|
|
34
|
+
}
|
|
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
|
+
export function dataValidationsExtXml(entries) {
|
|
39
|
+
const extended = entries.filter((entry) => entry.extended);
|
|
40
|
+
if (extended.length === 0)
|
|
41
|
+
return '';
|
|
42
|
+
const items = extended.map(({ sqref, rule }) => extendedDataValidationXml(sqref, rule)).join('');
|
|
43
|
+
return x14Ext(DATA_VALIDATION_EXT_URI, `<x14:dataValidations count="${extended.length}" xmlns:xm="${XM_NS}">${items}</x14:dataValidations>`);
|
|
44
|
+
}
|
|
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
|
+
function ruleAttrs(rule) {
|
|
50
|
+
return (` type="${rule.type}"` +
|
|
51
|
+
(rule.errorStyle !== undefined ? ` errorStyle="${rule.errorStyle}"` : '') +
|
|
52
|
+
(rule.operator !== undefined ? ` operator="${rule.operator}"` : '') +
|
|
53
|
+
(rule.allowBlank ? ' allowBlank="1"' : '') +
|
|
54
|
+
(rule.showInputMessage ? ' showInputMessage="1"' : '') +
|
|
55
|
+
(rule.showErrorMessage ? ' showErrorMessage="1"' : '') +
|
|
56
|
+
(rule.errorTitle !== undefined ? ` errorTitle="${escapeAttr(rule.errorTitle)}"` : '') +
|
|
57
|
+
(rule.error !== undefined ? ` error="${escapeAttr(rule.error)}"` : '') +
|
|
58
|
+
(rule.promptTitle !== undefined ? ` promptTitle="${escapeAttr(rule.promptTitle)}"` : '') +
|
|
59
|
+
(rule.prompt !== undefined ? ` prompt="${escapeAttr(rule.prompt)}"` : ''));
|
|
60
|
+
}
|
|
61
|
+
// The standard element: shared attributes, then `sqref` last, then `<formula1>`/`<formula2>` bodies.
|
|
62
|
+
function dataValidationXml(sqref, rule) {
|
|
63
|
+
const [f1, f2] = operands(rule);
|
|
64
|
+
const body = (f1 !== undefined ? `<formula1>${escapeText(stripFormulaEquals(f1))}</formula1>` : '') +
|
|
65
|
+
(f2 !== undefined ? `<formula2>${escapeText(stripFormulaEquals(f2))}</formula2>` : '');
|
|
66
|
+
return `<dataValidation${ruleAttrs(rule)} sqref="${escapeAttr(sqref)}">${body}</dataValidation>`;
|
|
67
|
+
}
|
|
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
|
+
function operands(rule) {
|
|
73
|
+
const drop = (v) => typeof v === 'number' && !Number.isFinite(v) ? undefined : v;
|
|
74
|
+
const [f1, f2] = rule.formulae ?? [];
|
|
75
|
+
return [drop(f1), drop(f2)];
|
|
76
|
+
}
|
|
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
|
+
function extendedDataValidationXml(sqref, rule) {
|
|
81
|
+
const [f1, f2] = operands(rule);
|
|
82
|
+
const body = (f1 !== undefined
|
|
83
|
+
? `<x14:formula1><xm:f>${escapeText(stripFormulaEquals(f1))}</xm:f></x14:formula1>`
|
|
84
|
+
: '') +
|
|
85
|
+
(f2 !== undefined
|
|
86
|
+
? `<x14:formula2><xm:f>${escapeText(stripFormulaEquals(f2))}</xm:f></x14:formula2>`
|
|
87
|
+
: '') +
|
|
88
|
+
`<xm:sqref>${escapeText(sqref)}</xm:sqref>`;
|
|
89
|
+
return `<x14:dataValidation${ruleAttrs(rule)}>${body}</x14:dataValidation>`;
|
|
90
|
+
}
|
|
91
|
+
/** Parse every standard `<dataValidation>` out of a worksheet part into range-bound rules. */
|
|
92
|
+
export function parseDataValidations(xml) {
|
|
93
|
+
const entries = [];
|
|
94
|
+
let current;
|
|
95
|
+
let slot;
|
|
96
|
+
parseXml(xml, {
|
|
97
|
+
onOpen(name, attrs) {
|
|
98
|
+
const ln = localName(name);
|
|
99
|
+
// Only the standard, unprefixed element — an `x14:dataValidation` is left for the extended path.
|
|
100
|
+
if (ln === 'dataValidation' && !name.includes(':')) {
|
|
101
|
+
current = { attrs, formulae: [] };
|
|
102
|
+
}
|
|
103
|
+
else if (current !== undefined && ln === 'formula1') {
|
|
104
|
+
slot = 0;
|
|
105
|
+
current.formulae[0] = '';
|
|
106
|
+
}
|
|
107
|
+
else if (current !== undefined && ln === 'formula2') {
|
|
108
|
+
slot = 1;
|
|
109
|
+
current.formulae[1] = '';
|
|
110
|
+
}
|
|
111
|
+
},
|
|
112
|
+
onText(text) {
|
|
113
|
+
if (current !== undefined && slot !== undefined) {
|
|
114
|
+
current.formulae[slot] = (current.formulae[slot] ?? '') + text;
|
|
115
|
+
}
|
|
116
|
+
},
|
|
117
|
+
onClose(name) {
|
|
118
|
+
const ln = localName(name);
|
|
119
|
+
if (ln === 'formula1' || ln === 'formula2') {
|
|
120
|
+
slot = undefined;
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
if (ln === 'dataValidation' && current !== undefined) {
|
|
124
|
+
const built = buildEntry(current.attrs, current.formulae);
|
|
125
|
+
if (built !== undefined)
|
|
126
|
+
entries.push(built);
|
|
127
|
+
current = undefined;
|
|
128
|
+
}
|
|
129
|
+
},
|
|
130
|
+
});
|
|
131
|
+
return entries;
|
|
132
|
+
}
|
|
133
|
+
function buildEntry(attrs, formulae) {
|
|
134
|
+
const { sqref } = attrs;
|
|
135
|
+
if (sqref === undefined)
|
|
136
|
+
return undefined;
|
|
137
|
+
const rule = buildRule(attrs, formulae);
|
|
138
|
+
return rule === undefined ? undefined : { sqref, rule };
|
|
139
|
+
}
|
|
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
|
+
function buildRule(attrs, formulae) {
|
|
144
|
+
const { type } = attrs;
|
|
145
|
+
if (type === undefined)
|
|
146
|
+
return undefined;
|
|
147
|
+
const rule = { type: type };
|
|
148
|
+
if (attrs.operator !== undefined) {
|
|
149
|
+
rule.operator = attrs.operator;
|
|
150
|
+
}
|
|
151
|
+
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
|
+
rule.operator = 'between';
|
|
155
|
+
}
|
|
156
|
+
if (boolStrict(attrs.allowBlank))
|
|
157
|
+
rule.allowBlank = true;
|
|
158
|
+
if (boolStrict(attrs.showInputMessage))
|
|
159
|
+
rule.showInputMessage = true;
|
|
160
|
+
if (boolStrict(attrs.showErrorMessage))
|
|
161
|
+
rule.showErrorMessage = true;
|
|
162
|
+
if (attrs.errorStyle !== undefined)
|
|
163
|
+
rule.errorStyle = attrs.errorStyle;
|
|
164
|
+
if (attrs.error !== undefined)
|
|
165
|
+
rule.error = attrs.error;
|
|
166
|
+
if (attrs.errorTitle !== undefined)
|
|
167
|
+
rule.errorTitle = attrs.errorTitle;
|
|
168
|
+
if (attrs.prompt !== undefined)
|
|
169
|
+
rule.prompt = attrs.prompt;
|
|
170
|
+
if (attrs.promptTitle !== undefined)
|
|
171
|
+
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
|
+
const parsed = formulae
|
|
176
|
+
.filter((f) => f !== undefined)
|
|
177
|
+
.map((f) => (type === 'list' || type === 'custom' ? f : coerceNumericLiteral(f)));
|
|
178
|
+
if (parsed.length > 0)
|
|
179
|
+
rule.formulae = parsed;
|
|
180
|
+
return rule;
|
|
181
|
+
}
|
|
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
|
+
export function parseExtendedDataValidations(xml) {
|
|
187
|
+
const entries = [];
|
|
188
|
+
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
|
+
let slot;
|
|
192
|
+
let capture;
|
|
193
|
+
let text = '';
|
|
194
|
+
parseXml(xml, {
|
|
195
|
+
onOpen(name, attrs) {
|
|
196
|
+
const ln = localName(name);
|
|
197
|
+
const prefixed = name.includes(':');
|
|
198
|
+
// A `<x14:dataValidation>`; its attributes (type, flags, messages) build the rule.
|
|
199
|
+
if (ln === 'dataValidation' && prefixed) {
|
|
200
|
+
current = { attrs, formulae: [], sqref: '' };
|
|
201
|
+
}
|
|
202
|
+
else if (current !== undefined && prefixed && ln === 'formula1') {
|
|
203
|
+
slot = 0;
|
|
204
|
+
}
|
|
205
|
+
else if (current !== undefined && prefixed && ln === 'formula2') {
|
|
206
|
+
slot = 1;
|
|
207
|
+
}
|
|
208
|
+
else if (current !== undefined && ln === 'f') {
|
|
209
|
+
capture = 'formula';
|
|
210
|
+
text = '';
|
|
211
|
+
}
|
|
212
|
+
else if (current !== undefined && ln === 'sqref') {
|
|
213
|
+
capture = 'sqref';
|
|
214
|
+
text = '';
|
|
215
|
+
}
|
|
216
|
+
},
|
|
217
|
+
onText(chunk) {
|
|
218
|
+
if (capture !== undefined)
|
|
219
|
+
text += chunk;
|
|
220
|
+
},
|
|
221
|
+
onClose(name) {
|
|
222
|
+
const ln = localName(name);
|
|
223
|
+
if (ln === 'f' && capture === 'formula') {
|
|
224
|
+
if (current !== undefined && slot !== undefined)
|
|
225
|
+
current.formulae[slot] = text;
|
|
226
|
+
capture = undefined;
|
|
227
|
+
}
|
|
228
|
+
else if (ln === 'sqref' && capture === 'sqref') {
|
|
229
|
+
if (current !== undefined)
|
|
230
|
+
current.sqref = text;
|
|
231
|
+
capture = undefined;
|
|
232
|
+
}
|
|
233
|
+
else if ((ln === 'formula1' || ln === 'formula2') && name.includes(':')) {
|
|
234
|
+
slot = undefined;
|
|
235
|
+
}
|
|
236
|
+
else if (ln === 'dataValidation' && name.includes(':') && current !== undefined) {
|
|
237
|
+
const built = buildExtendedEntry(current.attrs, current.formulae, current.sqref);
|
|
238
|
+
if (built !== undefined)
|
|
239
|
+
entries.push(built);
|
|
240
|
+
current = undefined;
|
|
241
|
+
}
|
|
242
|
+
},
|
|
243
|
+
});
|
|
244
|
+
return entries;
|
|
245
|
+
}
|
|
246
|
+
function buildExtendedEntry(attrs, formulae, sqref) {
|
|
247
|
+
if (sqref === '')
|
|
248
|
+
return undefined;
|
|
249
|
+
const rule = buildRule(attrs, formulae);
|
|
250
|
+
return rule === undefined ? undefined : { sqref, rule, extended: true };
|
|
251
|
+
}
|
|
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
|
+
export function applyDataValidations(sheet, entries) {
|
|
255
|
+
for (const { sqref, rule, extended } of entries) {
|
|
256
|
+
sheet.addDataValidation(sqref, rule, extended ? { extended: true } : {});
|
|
257
|
+
}
|
|
258
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { type VbaLibraryReference } from '../../vba/project-editor.ts';
|
|
2
|
+
/**
|
|
3
|
+
* Remove a standard module from an existing macro-enabled package's VBA project, returning new package
|
|
4
|
+
* bytes. Every part but `xl/vbaProject.bin` is preserved byte-for-byte (see {@link removeVbaModule} for
|
|
5
|
+
* what changes within it), and any digital signature over the old project is dropped because it cannot
|
|
6
|
+
* validate the new bytes.
|
|
7
|
+
*
|
|
8
|
+
* @throws {VbaAuthorError} if the package carries no VBA project, `name` is not in the project, or names
|
|
9
|
+
* a `document`/`designer` module.
|
|
10
|
+
* @throws {VbaParseError} if the attached `vbaProject.bin` is malformed.
|
|
11
|
+
*/
|
|
12
|
+
export declare function editXlsxVbaRemoveModule(xlsx: Uint8Array, name: string): Uint8Array;
|
|
13
|
+
/**
|
|
14
|
+
* Add a registered (COM type-library) reference to an existing macro-enabled package's VBA project,
|
|
15
|
+
* returning new package bytes. Every part but `xl/vbaProject.bin` is preserved byte-for-byte (see
|
|
16
|
+
* {@link addVbaReference} for what changes within it), and any digital signature over the old project is
|
|
17
|
+
* dropped because it cannot validate the new bytes.
|
|
18
|
+
*
|
|
19
|
+
* @throws {VbaAuthorError} if the package carries no VBA project, or any field of `ref` is invalid (see
|
|
20
|
+
* {@link VbaLibraryReference}).
|
|
21
|
+
* @throws {VbaParseError} if the attached `vbaProject.bin` is malformed.
|
|
22
|
+
*/
|
|
23
|
+
export declare function editXlsxVbaAddReference(xlsx: Uint8Array, ref: VbaLibraryReference): Uint8Array;
|
|
@@ -0,0 +1,139 @@
|
|
|
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
|
+
import { strFromU8, strToU8, unzipSync, zipSync } from 'fflate';
|
|
17
|
+
import { VbaAuthorError } from "../../vba/errors.js";
|
|
18
|
+
import { addVbaReference, removeVbaModule, } from "../../vba/project-editor.js";
|
|
19
|
+
import { relsPathFor } from "../opc/part-paths.js";
|
|
20
|
+
import { parseRelationshipRecords, relationshipTargetByType, resolveRelativePart, resolveWorkbookPart, } from "../opc/read-opc.js";
|
|
21
|
+
const OFFICE_DOCUMENT_REL = 'officeDocument';
|
|
22
|
+
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
|
+
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
|
+
export function editXlsxVbaRemoveModule(xlsx, name) {
|
|
37
|
+
return applyToVbaProjectPart(xlsx, (bin) => removeVbaModule(bin, name));
|
|
38
|
+
}
|
|
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
|
+
export function editXlsxVbaAddReference(xlsx, ref) {
|
|
50
|
+
return applyToVbaProjectPart(xlsx, (bin) => addVbaReference(bin, ref));
|
|
51
|
+
}
|
|
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
|
+
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
|
+
const files = unzipSync(xlsx);
|
|
60
|
+
const binPath = locateVbaProjectPart(files);
|
|
61
|
+
const bin = binPath === undefined ? undefined : files[binPath];
|
|
62
|
+
if (binPath === undefined || bin === undefined) {
|
|
63
|
+
throw new VbaAuthorError('package has no VBA project to edit');
|
|
64
|
+
}
|
|
65
|
+
files[binPath] = apply(bin);
|
|
66
|
+
dropStaleSignature(files, binPath);
|
|
67
|
+
return zipSync(files);
|
|
68
|
+
}
|
|
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
|
+
function locateVbaProjectPart(files) {
|
|
73
|
+
const rootRels = textPart(files, '_rels/.rels');
|
|
74
|
+
if (rootRels === undefined)
|
|
75
|
+
return undefined;
|
|
76
|
+
const workbookTarget = relationshipTargetByType(rootRels, OFFICE_DOCUMENT_REL);
|
|
77
|
+
if (workbookTarget === undefined)
|
|
78
|
+
return undefined;
|
|
79
|
+
const workbookPath = resolveRelativePart('', workbookTarget);
|
|
80
|
+
const workbookRels = textPart(files, relsPathFor(workbookPath));
|
|
81
|
+
if (workbookRels === undefined)
|
|
82
|
+
return undefined;
|
|
83
|
+
const vbaTarget = relationshipTargetByType(workbookRels, VBA_PROJECT_REL);
|
|
84
|
+
if (vbaTarget === undefined)
|
|
85
|
+
return undefined;
|
|
86
|
+
return resolveVbaTarget(workbookPath, vbaTarget);
|
|
87
|
+
}
|
|
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
|
+
function resolveVbaTarget(workbookPath, target) {
|
|
91
|
+
if (target.startsWith('/'))
|
|
92
|
+
return target.slice(1);
|
|
93
|
+
if (workbookPath.startsWith('xl/'))
|
|
94
|
+
return resolveWorkbookPart(target);
|
|
95
|
+
return resolveRelativePart(workbookPath, target);
|
|
96
|
+
}
|
|
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
|
+
function dropStaleSignature(files, binPath) {
|
|
101
|
+
const binRelsPath = relsPathFor(binPath);
|
|
102
|
+
const binRels = textPart(files, binRelsPath);
|
|
103
|
+
if (binRels === undefined)
|
|
104
|
+
return;
|
|
105
|
+
const signatureRels = parseRelationshipRecords(binRels).filter((rel) => !rel.external && rel.type.includes(VBA_SIGNATURE_REL_INFIX));
|
|
106
|
+
if (signatureRels.length === 0)
|
|
107
|
+
return;
|
|
108
|
+
let contentTypes = textPart(files, '[Content_Types].xml');
|
|
109
|
+
let rels = binRels;
|
|
110
|
+
for (const rel of signatureRels) {
|
|
111
|
+
const partPath = resolveRelativePart(binPath, rel.target);
|
|
112
|
+
delete files[partPath];
|
|
113
|
+
rels = removeRelationshipById(rels, rel.id);
|
|
114
|
+
if (contentTypes !== undefined)
|
|
115
|
+
contentTypes = removeContentTypeOverride(contentTypes, partPath);
|
|
116
|
+
}
|
|
117
|
+
if (parseRelationshipRecords(rels).length === 0)
|
|
118
|
+
delete files[binRelsPath];
|
|
119
|
+
else
|
|
120
|
+
files[binRelsPath] = strToU8(rels);
|
|
121
|
+
if (contentTypes !== undefined)
|
|
122
|
+
files['[Content_Types].xml'] = strToU8(contentTypes);
|
|
123
|
+
}
|
|
124
|
+
function textPart(files, path) {
|
|
125
|
+
const bytes = files[path];
|
|
126
|
+
return bytes === undefined ? undefined : strFromU8(bytes);
|
|
127
|
+
}
|
|
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
|
+
function removeRelationshipById(xml, id) {
|
|
131
|
+
return xml.replace(/<Relationship\b[^>]*?\/>|<Relationship\b[\s\S]*?<\/Relationship>/g, (element) => (new RegExp(`\\bId="${escapeRegExp(id)}"`).test(element) ? '' : element));
|
|
132
|
+
}
|
|
133
|
+
// Drop the `<Override>` naming a given part path; PartName is the full, unambiguous package path.
|
|
134
|
+
function removeContentTypeOverride(xml, partPath) {
|
|
135
|
+
return xml.replace(/<Override\b[^>]*?\/>/g, (element) => element.includes(`PartName="/${partPath}"`) ? '' : element);
|
|
136
|
+
}
|
|
137
|
+
function escapeRegExp(value) {
|
|
138
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
139
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { XlsxError } from '../../errors.ts';
|
|
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
|
+
export declare class XlsxParseError extends XlsxError {
|
|
15
|
+
readonly name = "XlsxParseError";
|
|
16
|
+
readonly code = "malformed-input";
|
|
17
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
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
|
+
export class XlsxParseError extends XlsxError {
|
|
15
|
+
name = 'XlsxParseError';
|
|
16
|
+
code = 'malformed-input';
|
|
17
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { Worksheet } from '../../core/worksheet.ts';
|
|
2
|
+
import type { SheetRelIds } from './package-plan.ts';
|
|
3
|
+
/** A hyperlink gathered from a sheet for serialisation: the cell it sits on, its target, and an
|
|
4
|
+
* optional tooltip. The visible label is the cell's own value and is serialised as that value. */
|
|
5
|
+
export interface CollectedHyperlink {
|
|
6
|
+
readonly ref: string;
|
|
7
|
+
readonly target: string;
|
|
8
|
+
readonly tooltip?: string;
|
|
9
|
+
}
|
|
10
|
+
/** A hyperlink resolved for serialisation. An external target carries a `relId` (the sheet
|
|
11
|
+
* relationship holding the URL) plus that `target`; an internal target carries a `location` (the
|
|
12
|
+
* in-workbook reference). Exactly one of `relId`/`location` is ever set. */
|
|
13
|
+
export interface HyperlinkPlan {
|
|
14
|
+
readonly ref: string;
|
|
15
|
+
readonly relId?: string;
|
|
16
|
+
readonly target?: string;
|
|
17
|
+
readonly location?: string;
|
|
18
|
+
readonly tooltip?: string;
|
|
19
|
+
}
|
|
20
|
+
/** Gather every hyperlink cell on a sheet, in row-major order. */
|
|
21
|
+
export declare function collectHyperlinks(sheet: Worksheet): CollectedHyperlink[];
|
|
22
|
+
/** Split collected links into internal (location, no rel) and external (relationship) forms, drawing
|
|
23
|
+
* each external link's relationship id from the sheet's allocator so external ids follow every other
|
|
24
|
+
* sheet-local relationship in canonical order. An internal ('#'-prefixed) link consumes no id. */
|
|
25
|
+
export declare function planHyperlinks(links: readonly CollectedHyperlink[], rels: SheetRelIds): HyperlinkPlan[];
|
|
26
|
+
/** The `<hyperlinks>` element, or '' when the sheet has none. Attribute order follows CT_Hyperlink:
|
|
27
|
+
* `ref`, `r:id`, `location`, `tooltip`. */
|
|
28
|
+
export declare function hyperlinksXml(links: readonly HyperlinkPlan[]): string;
|
|
29
|
+
/** A hyperlink parsed from a sheet: its cell reference plus whichever of `rid`/`location`/`tooltip`
|
|
30
|
+
* the `<hyperlink>` element carried. */
|
|
31
|
+
interface ParsedHyperlink {
|
|
32
|
+
readonly ref: string;
|
|
33
|
+
readonly rid?: string;
|
|
34
|
+
readonly location?: string;
|
|
35
|
+
readonly tooltip?: string;
|
|
36
|
+
}
|
|
37
|
+
/** Parse every `<hyperlink>` element out of a worksheet part. */
|
|
38
|
+
export declare function parseSheetHyperlinks(xml: string): ParsedHyperlink[];
|
|
39
|
+
/** Fold parsed hyperlinks onto a sheet's cells, wrapping each cell's existing value (its visible
|
|
40
|
+
* label) into a {@link HyperlinkValue}. `rels` maps a relationship id to its target URL. */
|
|
41
|
+
export declare function applyHyperlinks(sheet: Worksheet, links: readonly ParsedHyperlink[], rels: Map<string, string>): void;
|
|
42
|
+
export {};
|
|
@@ -0,0 +1,139 @@
|
|
|
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
|
+
import { decodeRange } from "../../core/address.js";
|
|
13
|
+
import { isHyperlinkValue, isRichTextValue } from "../../core/value.js";
|
|
14
|
+
import { escapeAttr } from "../../xml/xml.js";
|
|
15
|
+
import { localName, parseXml } from "../../xml/xml-read.js";
|
|
16
|
+
/** Gather every hyperlink cell on a sheet, in row-major order. */
|
|
17
|
+
export function collectHyperlinks(sheet) {
|
|
18
|
+
const links = [];
|
|
19
|
+
for (const { cells } of sheet.rows()) {
|
|
20
|
+
for (const cell of cells) {
|
|
21
|
+
const value = cell.value;
|
|
22
|
+
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
|
+
links.push({
|
|
27
|
+
ref: value.range ?? cell.address,
|
|
28
|
+
target: value.hyperlink,
|
|
29
|
+
...(value.tooltip !== undefined ? { tooltip: value.tooltip } : {}),
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return links;
|
|
35
|
+
}
|
|
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
|
+
export function planHyperlinks(links, rels) {
|
|
40
|
+
return links.map((link) => {
|
|
41
|
+
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
|
+
if (link.target.startsWith('#')) {
|
|
45
|
+
return { ref: link.ref, location: link.target.slice(1), ...tooltip };
|
|
46
|
+
}
|
|
47
|
+
return { ref: link.ref, relId: rels.next(), target: link.target, ...tooltip };
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
/** The `<hyperlinks>` element, or '' when the sheet has none. Attribute order follows CT_Hyperlink:
|
|
51
|
+
* `ref`, `r:id`, `location`, `tooltip`. */
|
|
52
|
+
export function hyperlinksXml(links) {
|
|
53
|
+
if (links.length === 0)
|
|
54
|
+
return '';
|
|
55
|
+
const items = links
|
|
56
|
+
.map((link) => {
|
|
57
|
+
const rid = link.relId !== undefined ? ` r:id="${link.relId}"` : '';
|
|
58
|
+
const location = link.location !== undefined ? ` location="${escapeAttr(link.location)}"` : '';
|
|
59
|
+
const tooltip = link.tooltip !== undefined ? ` tooltip="${escapeAttr(link.tooltip)}"` : '';
|
|
60
|
+
return `<hyperlink ref="${escapeAttr(link.ref)}"${rid}${location}${tooltip}/>`;
|
|
61
|
+
})
|
|
62
|
+
.join('');
|
|
63
|
+
return `<hyperlinks>${items}</hyperlinks>`;
|
|
64
|
+
}
|
|
65
|
+
/** Parse every `<hyperlink>` element out of a worksheet part. */
|
|
66
|
+
export function parseSheetHyperlinks(xml) {
|
|
67
|
+
const links = [];
|
|
68
|
+
parseXml(xml, {
|
|
69
|
+
onOpen(name, attrs) {
|
|
70
|
+
if (localName(name) !== 'hyperlink')
|
|
71
|
+
return;
|
|
72
|
+
const ref = attrs.ref;
|
|
73
|
+
if (ref === undefined)
|
|
74
|
+
return;
|
|
75
|
+
links.push({
|
|
76
|
+
ref,
|
|
77
|
+
...(attrs['r:id'] !== undefined ? { rid: attrs['r:id'] } : {}),
|
|
78
|
+
...(attrs.location !== undefined ? { location: attrs.location } : {}),
|
|
79
|
+
...(attrs.tooltip !== undefined ? { tooltip: attrs.tooltip } : {}),
|
|
80
|
+
});
|
|
81
|
+
},
|
|
82
|
+
});
|
|
83
|
+
return links;
|
|
84
|
+
}
|
|
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
|
+
export function applyHyperlinks(sheet, links, rels) {
|
|
88
|
+
for (const link of links) {
|
|
89
|
+
const target = resolveTarget(link, rels);
|
|
90
|
+
if (target === undefined)
|
|
91
|
+
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
|
+
const decoded = decodeRefSafe(link.ref);
|
|
96
|
+
if (decoded === undefined)
|
|
97
|
+
continue;
|
|
98
|
+
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
|
+
const cellValue = cell.value;
|
|
102
|
+
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
|
+
const spansRange = decoded.tl.address !== decoded.br.address;
|
|
106
|
+
const value = {
|
|
107
|
+
hyperlink: target,
|
|
108
|
+
text,
|
|
109
|
+
...(link.tooltip !== undefined ? { tooltip: link.tooltip } : {}),
|
|
110
|
+
...(spansRange ? { range: link.ref } : {}),
|
|
111
|
+
};
|
|
112
|
+
cell.value = value;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
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
|
+
function decodeRefSafe(ref) {
|
|
120
|
+
try {
|
|
121
|
+
return decodeRange(ref);
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
return undefined;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
function resolveTarget(link, rels) {
|
|
128
|
+
if (link.rid !== undefined) {
|
|
129
|
+
const base = rels.get(link.rid);
|
|
130
|
+
if (base === undefined)
|
|
131
|
+
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
|
+
return link.location !== undefined ? `${base}#${link.location}` : base;
|
|
136
|
+
}
|
|
137
|
+
// No relationship: an internal ('#'-prefixed) target held verbatim in `location`.
|
|
138
|
+
return link.location !== undefined ? `#${link.location}` : undefined;
|
|
139
|
+
}
|