@shbernal/ts-xlsx 1.0.3 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +21 -0
- package/dist/core/address.js +0 -33
- package/dist/core/autofilter.js +0 -9
- package/dist/core/cell.d.ts +10 -0
- package/dist/core/cell.js +4 -123
- package/dist/core/color-resolution.js +0 -60
- package/dist/core/column.d.ts +9 -1
- package/dist/core/column.js +0 -52
- package/dist/core/comment-thread.js +0 -45
- package/dist/core/conditional-formatting-overlay.js +0 -11
- package/dist/core/conditional-formatting.js +0 -10
- package/dist/core/containers.js +0 -15
- package/dist/core/data-validation-overlay.js +0 -25
- package/dist/core/data-validation.js +0 -7
- package/dist/core/date.js +0 -39
- package/dist/core/formula.js +0 -117
- package/dist/core/grid-edits.js +0 -38
- package/dist/core/image.js +0 -25
- package/dist/core/internal.js +0 -27
- package/dist/core/limits.d.ts +30 -0
- package/dist/core/limits.js +2 -0
- package/dist/core/merge.d.ts +18 -0
- package/dist/core/merge.js +22 -7
- package/dist/core/modern-functions.js +0 -17
- package/dist/core/page-setup.js +0 -5
- package/dist/core/pivot-table.d.ts +1 -1
- package/dist/core/pivot-table.js +0 -44
- package/dist/core/preserved.js +0 -4
- package/dist/core/protection.js +0 -26
- package/dist/core/range.js +0 -112
- package/dist/core/row-input.d.ts +24 -0
- package/dist/core/row-input.js +29 -0
- package/dist/core/row.d.ts +9 -1
- package/dist/core/row.js +0 -58
- package/dist/core/style.js +0 -26
- package/dist/core/table-style.js +0 -28
- package/dist/core/table.js +0 -97
- package/dist/core/text-metrics.d.ts +20 -0
- package/dist/core/text-metrics.js +10 -0
- package/dist/core/theme.js +0 -85
- package/dist/core/value.d.ts +40 -0
- package/dist/core/value.js +30 -48
- package/dist/core/workbook-protection.js +0 -13
- package/dist/core/workbook.d.ts +13 -0
- package/dist/core/workbook.js +11 -368
- package/dist/core/worksheet-model.js +2 -23
- package/dist/core/worksheet.d.ts +22 -0
- package/dist/core/worksheet.js +14 -485
- package/dist/customui/errors.js +0 -10
- package/dist/customui/index.js +0 -5
- package/dist/customui/ribbon.js +0 -43
- package/dist/entries/core.d.ts +3 -1
- package/dist/entries/core.js +3 -7
- package/dist/entries/csv.js +0 -4
- package/dist/entries/customui.js +0 -2
- package/dist/entries/errors.js +0 -13
- package/dist/entries/vba.js +0 -5
- package/dist/entries/xlsb.js +0 -6
- package/dist/entries/xlsx.js +0 -13
- package/dist/errors.d.ts +14 -0
- package/dist/errors.js +5 -56
- package/dist/index.js +0 -12
- package/dist/io/csv/read.js +0 -18
- package/dist/io/csv/write.js +5 -35
- package/dist/io/opc/errors.js +0 -30
- package/dist/io/opc/inflate.js +0 -35
- package/dist/io/opc/namespaces.js +0 -15
- package/dist/io/opc/part-paths.js +0 -11
- package/dist/io/opc/read-opc.js +0 -31
- package/dist/io/opc/read-options.js +0 -6
- package/dist/io/opc/rels.js +0 -13
- package/dist/io/opc/sniff-format.js +0 -50
- package/dist/io/style/xf-style.js +0 -31
- package/dist/io/xlsb/errors.js +0 -10
- package/dist/io/xlsb/formula.js +1 -99
- package/dist/io/xlsb/primitives.js +1 -83
- package/dist/io/xlsb/ptg-functions.js +0 -34
- package/dist/io/xlsb/read-shared-strings.js +0 -7
- package/dist/io/xlsb/read-styles.js +3 -66
- package/dist/io/xlsb/read-worksheet.js +4 -87
- package/dist/io/xlsb/read.js +2 -60
- package/dist/io/xlsb/record-stream.js +0 -28
- package/dist/io/xlsb/record-types.js +0 -19
- package/dist/io/xlsx/cell-accumulator.js +0 -32
- package/dist/io/xlsx/cell-value.js +0 -29
- package/dist/io/xlsx/color-xml.d.ts +6 -0
- package/dist/io/xlsx/color-xml.js +42 -0
- package/dist/io/xlsx/comments.js +0 -84
- package/dist/io/xlsx/conditional-formatting.d.ts +1 -1
- package/dist/io/xlsx/conditional-formatting.js +1 -97
- package/dist/io/xlsx/data-validation.js +0 -56
- package/dist/io/xlsx/edit-vba.js +0 -54
- package/dist/io/xlsx/errors.js +0 -12
- package/dist/io/xlsx/hyperlinks.js +0 -40
- package/dist/io/xlsx/images.js +0 -32
- package/dist/io/xlsx/namespaces.js +0 -42
- package/dist/io/xlsx/package-plan.js +0 -47
- package/dist/io/xlsx/pivot.js +0 -22
- package/dist/io/xlsx/{pivot-read.js → read-pivot.js} +0 -30
- package/dist/io/xlsx/read-rows.js +1 -83
- package/dist/io/xlsx/{shared-strings-read.js → read-shared-strings.js} +0 -11
- package/dist/io/xlsx/read-styles.d.ts +25 -1
- package/dist/io/xlsx/read-styles.js +61 -108
- package/dist/io/xlsx/read-worksheet.js +1 -88
- package/dist/io/xlsx/read.js +3 -211
- package/dist/io/xlsx/relationships.js +0 -11
- package/dist/io/xlsx/rich-runs.js +0 -16
- package/dist/io/xlsx/rich-text.js +0 -12
- package/dist/io/xlsx/shared-formulas.js +0 -7
- package/dist/io/xlsx/shared-strings.js +0 -21
- package/dist/io/xlsx/sheet-properties.js +1 -64
- package/dist/io/xlsx/styles.d.ts +1 -29
- package/dist/io/xlsx/styles.js +2 -366
- package/dist/io/xlsx/tables.js +5 -52
- package/dist/io/xlsx/threaded-comments.js +0 -103
- package/dist/io/xlsx/workbook-xml.js +0 -94
- package/dist/io/xlsx/worksheet-xml.js +0 -174
- package/dist/io/xlsx/write-stream.js +0 -137
- package/dist/io/xlsx/write.js +1 -134
- package/dist/io/xlsx/x14-ext.js +0 -8
- package/dist/vba/cfb-writer.js +12 -59
- package/dist/vba/cfb.js +2 -32
- package/dist/vba/codepage.js +1 -23
- package/dist/vba/errors.js +0 -15
- package/dist/vba/index.js +0 -10
- package/dist/vba/ms-ovba.js +3 -46
- package/dist/vba/project-editor.js +4 -89
- package/dist/vba/project.js +2 -34
- package/dist/vba/vba-encoding.js +1 -17
- package/dist/xml/errors.js +0 -10
- package/dist/xml/xml-read.js +3 -82
- package/dist/xml/xml.js +0 -39
- package/package.json +9 -10
- package/skills/ts-xlsx-upstream/SKILL.md +178 -0
- /package/dist/io/xlsx/{pivot-read.d.ts → read-pivot.d.ts} +0 -0
- /package/dist/io/xlsx/{shared-strings-read.d.ts → read-shared-strings.d.ts} +0 -0
package/dist/io/xlsx/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
|
}
|
package/dist/io/xlsx/images.js
CHANGED
|
@@ -1,7 +1,3 @@
|
|
|
1
|
-
// Anchored images on the wire: the `xl/drawings/drawing{n}.xml` part (a DrawingML two-cell anchor
|
|
2
|
-
// per image), the drawing's own relationships to the `xl/media/` bytes, and the reader that turns a
|
|
3
|
-
// drawing back into anchors. The image bytes themselves are opaque here — the writer copies them
|
|
4
|
-
// verbatim into a media part and the reader hands them back untouched.
|
|
5
1
|
import { isOneCellAnchor, } from '../../core/image.js';
|
|
6
2
|
import { XML_DECLARATION } from '../../xml/xml.js';
|
|
7
3
|
import { localName, parseXml } from '../../xml/xml-read.js';
|
|
@@ -9,8 +5,6 @@ import { RELATIONSHIPS_NS } from '../opc/namespaces.js';
|
|
|
9
5
|
import { relationship, relationshipsPart } from '../opc/rels.js';
|
|
10
6
|
import { DRAWINGML_NS, XDR_NS } from './namespaces.js';
|
|
11
7
|
const IMAGE_REL_TYPE = `${RELATIONSHIPS_NS}/image`;
|
|
12
|
-
// The content type Excel expects for each image kind, keyed by lower-case extension. An unlisted
|
|
13
|
-
// extension falls back to `image/<ext>`, which is what a well-behaved consumer infers anyway.
|
|
14
8
|
const IMAGE_CONTENT_TYPES = {
|
|
15
9
|
png: 'image/png',
|
|
16
10
|
jpg: 'image/jpeg',
|
|
@@ -23,12 +17,10 @@ const IMAGE_CONTENT_TYPES = {
|
|
|
23
17
|
wmf: 'image/x-wmf',
|
|
24
18
|
svg: 'image/svg+xml',
|
|
25
19
|
};
|
|
26
|
-
/** The content type for a media part's `<Default Extension>` entry in `[Content_Types].xml`. */
|
|
27
20
|
export function imageContentType(extension) {
|
|
28
21
|
const ext = extension.toLowerCase();
|
|
29
22
|
return IMAGE_CONTENT_TYPES[ext] ?? `image/${ext}`;
|
|
30
23
|
}
|
|
31
|
-
/** The `xl/drawings/drawing{n}.xml` part: one anchor per image, two-cell or one-cell by its shape. */
|
|
32
24
|
export function drawingXml(images) {
|
|
33
25
|
const anchors = images.map((image, i) => anchorXml(image, i + 1)).join('');
|
|
34
26
|
return (XML_DECLARATION +
|
|
@@ -42,10 +34,6 @@ function anchorXml(image, id) {
|
|
|
42
34
|
? oneCellAnchorXml(anchor.from, anchor.ext, anchor.rotation, image.embedId, id)
|
|
43
35
|
: twoCellAnchorXml(anchor.from, anchor.to, anchor.editAs ?? 'oneCell', anchor.rotation, image.embedId, id);
|
|
44
36
|
}
|
|
45
|
-
// A picture anchored between two grid points. The geometry lives entirely in <xdr:from>/<xdr:to>, so
|
|
46
|
-
// the picture carries no absolute <a:xfrm> — a zeroed one would override the anchor and collapse the
|
|
47
|
-
// image to nothing in strict viewers (LibreOffice), while a non-zero one would fight the anchor. A
|
|
48
|
-
// rotation is the one transform kept: it can't be derived from the anchor, so it rides a rot-only xfrm.
|
|
49
37
|
function twoCellAnchorXml(from, to, editAs, rotation, embedId, id) {
|
|
50
38
|
return (`<xdr:twoCellAnchor editAs="${editAs}">` +
|
|
51
39
|
`<xdr:from>${anchorPointXml(from)}</xdr:from>` +
|
|
@@ -54,8 +42,6 @@ function twoCellAnchorXml(from, to, editAs, rotation, embedId, id) {
|
|
|
54
42
|
'<xdr:clientData/>' +
|
|
55
43
|
'</xdr:twoCellAnchor>');
|
|
56
44
|
}
|
|
57
|
-
// A picture pinned at one grid point with a fixed EMU extent. editAs is a two-cell-only attribute and
|
|
58
|
-
// the schema forbids it here, so a one-cell anchor never carries one.
|
|
59
45
|
function oneCellAnchorXml(from, ext, rotation, embedId, id) {
|
|
60
46
|
return ('<xdr:oneCellAnchor>' +
|
|
61
47
|
`<xdr:from>${anchorPointXml(from)}</xdr:from>` +
|
|
@@ -78,8 +64,6 @@ function anchorPointXml(point) {
|
|
|
78
64
|
return (`<xdr:col>${point.col}</xdr:col><xdr:colOff>${point.colOff ?? 0}</xdr:colOff>` +
|
|
79
65
|
`<xdr:row>${point.row}</xdr:row><xdr:rowOff>${point.rowOff ?? 0}</xdr:rowOff>`);
|
|
80
66
|
}
|
|
81
|
-
/** The drawing's `_rels/drawing{n}.xml.rels`: one image relationship per anchor, in `embedId` order
|
|
82
|
-
* (`rId1`, `rId2`, …), each pointing at the media part the anchor shows. */
|
|
83
67
|
export function drawingRelsXml(mediaTargets) {
|
|
84
68
|
return relationshipsPart(mediaTargets.map((target, i) => relationship(`rId${i + 1}`, IMAGE_REL_TYPE, target)));
|
|
85
69
|
}
|
|
@@ -87,9 +71,6 @@ function blankPoint() {
|
|
|
87
71
|
return { col: 0, row: 0, colOff: 0, rowOff: 0 };
|
|
88
72
|
}
|
|
89
73
|
const EDIT_AS = new Set(['oneCell', 'twoCell', 'absolute']);
|
|
90
|
-
/** Parse a drawing part into its image anchors (both `<xdr:twoCellAnchor>` and `<xdr:oneCellAnchor>`).
|
|
91
|
-
* Anchors that are not pictures (a chart, a shape) carry no `<a:blip r:embed>` and are skipped, so a
|
|
92
|
-
* mixed drawing yields only its images. */
|
|
93
74
|
export function parseDrawing(xml) {
|
|
94
75
|
const anchors = [];
|
|
95
76
|
let from = null;
|
|
@@ -98,12 +79,8 @@ export function parseDrawing(xml) {
|
|
|
98
79
|
let editAs;
|
|
99
80
|
let rotation;
|
|
100
81
|
let embed;
|
|
101
|
-
// The point (<xdr:from> or <xdr:to>) whose coordinate children are currently streaming in.
|
|
102
82
|
let target = null;
|
|
103
|
-
// Depth inside <xdr:pic>, so the anchor-level <xdr:ext> is not confused with the <a:ext> nested in
|
|
104
|
-
// a picture's spPr transform (both have local name "ext").
|
|
105
83
|
let picDepth = 0;
|
|
106
|
-
// Which coordinate child is open, so its text lands on the right field; '' between children.
|
|
107
84
|
let coord = '';
|
|
108
85
|
let text = '';
|
|
109
86
|
parseXml(xml, {
|
|
@@ -122,7 +99,6 @@ export function parseDrawing(xml) {
|
|
|
122
99
|
picDepth++;
|
|
123
100
|
}
|
|
124
101
|
else if (local === 'xfrm' && picDepth > 0) {
|
|
125
|
-
// The picture's own rotation — the one spPr transform that can't be derived from the anchor.
|
|
126
102
|
const rot = Number(attrs.rot);
|
|
127
103
|
if (Number.isFinite(rot) && rot !== 0)
|
|
128
104
|
rotation = rot;
|
|
@@ -186,15 +162,7 @@ export function parseDrawing(xml) {
|
|
|
186
162
|
});
|
|
187
163
|
return anchors;
|
|
188
164
|
}
|
|
189
|
-
// Anchor content a drawing can hold that the image model does not interpret: a chart
|
|
190
|
-
// (`<xdr:graphicFrame>`), a shape or text box (`<xdr:sp>`), a connector (`<xdr:cxnSp>`), or a group
|
|
191
|
-
// (`<xdr:grpSp>`). A drawing carrying any of these is preserved whole rather than modeled, so it is
|
|
192
|
-
// not re-serialised from its pictures alone (which would silently drop the chart/shape).
|
|
193
165
|
const UNMODELED_DRAWING_CONTENT = new Set(['graphicFrame', 'sp', 'cxnSp', 'grpSp']);
|
|
194
|
-
/** Whether a drawing part holds anchor content beyond plain pictures — a chart, shape, connector, or
|
|
195
|
-
* group. Excel packs every one of a sheet's anchors into a single drawing part, so a sheet with both a
|
|
196
|
-
* picture and a chart yields a mixed drawing; modeling only its pictures and re-serialising from them
|
|
197
|
-
* would drop the chart. The reader uses this to fall back to whole-drawing byte-preservation instead. */
|
|
198
166
|
export function drawingHasUnmodeledContent(xml) {
|
|
199
167
|
let found = false;
|
|
200
168
|
parseXml(xml, {
|
|
@@ -1,53 +1,11 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Canonical SpreadsheetML namespace URIs and well-known extension GUIDs.
|
|
3
|
-
*
|
|
4
|
-
* These are wire-format constants: Excel keys its parsing off the exact URI or
|
|
5
|
-
* GUID, so a producer must reproduce each one byte-for-byte. Centralizing them
|
|
6
|
-
* keeps the writer and reader from drifting apart and retires the
|
|
7
|
-
* `NS_MAIN`/`MAIN_NS`/`main` naming fork that had grown across the io modules.
|
|
8
|
-
*
|
|
9
|
-
* The package-level URIs (`.rels`, content types, the relationship vocabulary) describe the
|
|
10
|
-
* container rather than the spreadsheet inside it, and live in `../opc/namespaces.ts`.
|
|
11
|
-
*/
|
|
12
|
-
/**
|
|
13
|
-
* SpreadsheetML main namespace — the default `xmlns` of the workbook,
|
|
14
|
-
* worksheet, styles, sharedStrings, comments, table and pivot parts.
|
|
15
|
-
*/
|
|
16
1
|
export const SPREADSHEETML_NS = 'http://schemas.openxmlformats.org/spreadsheetml/2006/main';
|
|
17
|
-
/**
|
|
18
|
-
* Markup-compatibility namespace (`mc:`), whose `mc:Ignorable` attribute lists the prefixes a
|
|
19
|
-
* consumer that does not know them may skip rather than reject the part over.
|
|
20
|
-
*/
|
|
21
2
|
export const MARKUP_COMPATIBILITY_NS = 'http://schemas.openxmlformats.org/markup-compatibility/2006';
|
|
22
|
-
/**
|
|
23
|
-
* The 2014 revision namespace (`xr:`), which scopes the `xr:uid` Excel stamps on a comment. Declared
|
|
24
|
-
* `mc:Ignorable` wherever it appears, so a consumer that ignores it still reads the part.
|
|
25
|
-
*/
|
|
26
3
|
export const REVISION_NS = 'http://schemas.microsoft.com/office/spreadsheetml/2014/revision';
|
|
27
|
-
/**
|
|
28
|
-
* The 2018 threaded-comments namespace, shared by both parts of the feature — a sheet's
|
|
29
|
-
* `threadedComment{n}.xml` and the workbook's `person.xml`. Note the plural `threadedcomments`, all
|
|
30
|
-
* lower-case: Excel matches the URI exactly and reads neither part under any other spelling.
|
|
31
|
-
*/
|
|
32
4
|
export const THREADED_COMMENTS_NS = 'http://schemas.microsoft.com/office/spreadsheetml/2018/threadedcomments';
|
|
33
|
-
/** DrawingML shared graphics namespace (`a:`). */
|
|
34
5
|
export const DRAWINGML_NS = 'http://schemas.openxmlformats.org/drawingml/2006/main';
|
|
35
|
-
/** Spreadsheet-drawing anchor namespace (`xdr:`) used by the worksheet drawing part. */
|
|
36
6
|
export const XDR_NS = 'http://schemas.openxmlformats.org/drawingml/2006/spreadsheetDrawing';
|
|
37
|
-
/**
|
|
38
|
-
* The 2009 Microsoft extension namespace. `x14` scopes the feature elements
|
|
39
|
-
* Excel tucks inside `<ext>` blocks (conditional formatting, data validation,
|
|
40
|
-
* slicers); it is declared inline on those elements exactly as Excel writes
|
|
41
|
-
* them, so a worksheet root never needs an extra namespace declaration.
|
|
42
|
-
*/
|
|
43
7
|
export const X14_NS = 'http://schemas.microsoft.com/office/spreadsheetml/2009/9/main';
|
|
44
|
-
/** Scopes the `<xm:sqref>`/`<xm:f>` references the x14 feature elements carry. */
|
|
45
8
|
export const XM_NS = 'http://schemas.microsoft.com/office/excel/2006/main';
|
|
46
|
-
/**
|
|
47
|
-
* Well-known `<ext uri=…>` GUIDs. Each `<ext>` block is opaque to a consumer
|
|
48
|
-
* that does not recognize its GUID, so a producer must emit these exact values
|
|
49
|
-
* for Excel to rediscover the feature.
|
|
50
|
-
*/
|
|
51
9
|
export const CF_EXT_URI = '{78C0D931-6437-407d-A8EE-F0AAD7539E65}';
|
|
52
10
|
export const DATABAR_LINK_EXT_URI = '{B025F937-C7B1-47D3-B67F-A62EFF666E3E}';
|
|
53
11
|
export const DATA_VALIDATION_EXT_URI = '{CCE6A557-97BC-4b89-ADB6-D9C93CAAB3DF}';
|
|
@@ -1,27 +1,12 @@
|
|
|
1
|
-
// The plan layer of the writer: pure graph resolution that turns a Workbook model into the numbered,
|
|
2
|
-
// cross-referenced set of parts an `.xlsx` package needs — media, preserved (verbatim-carried) parts,
|
|
3
|
-
// and the sheet-/workbook-local relationship ids that wire them — before any XML is serialised.
|
|
4
1
|
import { AuthoringError } from '../../errors.js';
|
|
5
2
|
import { extensionOf, relativePartPath, relsPathFor, THEME_PART_PATH } from '../opc/part-paths.js';
|
|
6
3
|
import { preservedRelsXml } from '../opc/rels.js';
|
|
7
|
-
// A sheet's relationship-id allocator: hands out `rId1`, `rId2`, … in the one canonical order the
|
|
8
|
-
// package wires a sheet's parts (tables, drawing, comments, threaded comments, printer settings, external
|
|
9
|
-
// hyperlinks, background, preserved references, pivot tables). Every sheet-local id is drawn from here in
|
|
10
|
-
// sequence, so no plan step re-derives its starting offset by summing the counts of the steps before
|
|
11
|
-
// it — the arithmetic that, open-coded once per step with subtly different prefixes, could silently
|
|
12
|
-
// hand two parts the same id and corrupt the package. Monotonic by construction, so collisions cannot
|
|
13
|
-
// arise however the steps grow. One fresh allocator per sheet; the ids it yields are sheet-local.
|
|
14
4
|
export class SheetRelIds {
|
|
15
5
|
#next = 1;
|
|
16
|
-
/** The next relationship id (`rId1`, `rId2`, …), advancing the counter. */
|
|
17
6
|
next() {
|
|
18
7
|
return `rId${this.#next++}`;
|
|
19
8
|
}
|
|
20
9
|
}
|
|
21
|
-
// Gather the workbook images actually referenced by some sheet — either anchored in a drawing or set
|
|
22
|
-
// as a sheet background (an unreferenced image is not written) — number them in first-use order, and
|
|
23
|
-
// record the extensions in play. A sheet referencing an id with no registered image is a programming
|
|
24
|
-
// error the writer surfaces rather than emitting a dangling relationship.
|
|
25
10
|
export function planMedia(workbook, sheets) {
|
|
26
11
|
const usedIds = [];
|
|
27
12
|
const seen = new Set();
|
|
@@ -52,35 +37,14 @@ export function planMedia(workbook, sheets) {
|
|
|
52
37
|
});
|
|
53
38
|
return { parts, numberById, extensions: [...extensions] };
|
|
54
39
|
}
|
|
55
|
-
// Resolve every sheet's verbatim-preserved worksheet references (a vector-shape drawing, a
|
|
56
|
-
// header/footer image) into the parts to emit and the per-sheet reference data that wires them. Each
|
|
57
|
-
// reference's captured part closure is re-numbered onto collision-proof `preservedP{n}` paths — so
|
|
58
|
-
// preserved content never clobbers a generated drawing/VML/media part — with the closure's internal
|
|
59
|
-
// relationships rewritten to the new sibling paths. Part numbering is the only cross-sheet concern
|
|
60
|
-
// here; each reference's sheet-local relationship id is assigned by the caller from the sheet's
|
|
61
|
-
// {@link SheetRelIds} allocator, so this function stays free of the sheet-local id arithmetic.
|
|
62
40
|
export function planPreservedParts(workbook, generatedDrawingCount, generatedMediaCount) {
|
|
63
41
|
const sheets = workbook.worksheets;
|
|
64
|
-
// The writer generates drawings, VML, and media of its own, so a preserved part of one of those
|
|
65
|
-
// kinds is re-numbered past the generated ones (a preserved drawing never clobbers an anchored
|
|
66
|
-
// drawing, a preserved VML never clobbers a comment's VML). Comment VML is numbered by sheet index,
|
|
67
|
-
// so `sheets.length` bounds it. Every other kind (pivot tables, caches, slicers, charts) the writer
|
|
68
|
-
// never generates, so those keep their original path — see {@link preservedPartPath}.
|
|
69
42
|
const numbering = {
|
|
70
43
|
drawing: generatedDrawingCount,
|
|
71
44
|
vml: sheets.length,
|
|
72
45
|
media: generatedMediaCount,
|
|
73
46
|
};
|
|
74
|
-
// One package-wide remap and one emitted-parts map: a part reached through more than one reference
|
|
75
|
-
// (a pivot cache reached both from its pivot table and from the workbook) is numbered once and
|
|
76
|
-
// emitted once, so overlapping closures collapse instead of duplicating parts.
|
|
77
47
|
const remap = new Map();
|
|
78
|
-
// A preserved theme rides the same closure machinery as every other verbatim part — it can carry
|
|
79
|
-
// relationships of its own (a picture used as a themed fill) that need the same renumbering and
|
|
80
|
-
// rewiring. Its entry is pinned to the fixed theme path rather than left to {@link preservedPartPath},
|
|
81
|
-
// because the workbook's theme relationship and the content-type override name that path
|
|
82
|
-
// unconditionally; a source package that called its part `theme2.xml` would otherwise land it
|
|
83
|
-
// somewhere neither points.
|
|
84
48
|
const theme = workbook.themePart;
|
|
85
49
|
if (theme !== undefined)
|
|
86
50
|
remap.set(theme.entryPath, THEME_PART_PATH);
|
|
@@ -103,8 +67,6 @@ export function planPreservedParts(workbook, generatedDrawingCount, generatedMed
|
|
|
103
67
|
if (emitted.has(newPath))
|
|
104
68
|
continue;
|
|
105
69
|
const rels = part.rels.flatMap((rel) => {
|
|
106
|
-
// An external relationship (a linked workbook) is emitted verbatim — its target is outside the
|
|
107
|
-
// package, so it is neither in the remap nor expressed relative to the new path.
|
|
108
70
|
if (rel.external) {
|
|
109
71
|
return [{ id: rel.id, type: rel.type, target: rel.targetPath, external: true }];
|
|
110
72
|
}
|
|
@@ -113,10 +75,6 @@ export function planPreservedParts(workbook, generatedDrawingCount, generatedMed
|
|
|
113
75
|
? []
|
|
114
76
|
: [{ id: rel.id, type: rel.type, target: relativePartPath(newPath, target) }];
|
|
115
77
|
});
|
|
116
|
-
// The one preserved part whose *bytes* can change: a theme the caller authored over is
|
|
117
|
-
// regenerated from the source part (see `Workbook.authoredThemeXml`) rather than carried
|
|
118
|
-
// verbatim, so the format scheme, the unauthored slots' encoding, and the relationships below
|
|
119
|
-
// all still ride through — only the authored elements differ.
|
|
120
78
|
const authoredTheme = newPath === THEME_PART_PATH ? workbook.authoredThemeXml() : undefined;
|
|
121
79
|
emitted.set(newPath, {
|
|
122
80
|
path: newPath,
|
|
@@ -150,11 +108,6 @@ export function planPreservedParts(workbook, generatedDrawingCount, generatedMed
|
|
|
150
108
|
themeEmitted: theme !== undefined,
|
|
151
109
|
};
|
|
152
110
|
}
|
|
153
|
-
// The path a preserved part is emitted at. A kind the writer generates of its own — a drawing, a VML,
|
|
154
|
-
// a media image — is re-numbered past the generated parts of that kind (see {@link planPreservedParts})
|
|
155
|
-
// so it never clobbers one. Every other kind (a pivot table, a pivot/slicer cache, a slicer, a chart)
|
|
156
|
-
// the writer never generates, so it keeps its original path — leaving the package's standard part
|
|
157
|
-
// names intact and letting overlapping closures agree on a single path for a shared part.
|
|
158
111
|
function preservedPartPath(originalPath, numbering) {
|
|
159
112
|
const ext = extensionOf(originalPath);
|
|
160
113
|
if (ext.toLowerCase() === 'vml')
|
package/dist/io/xlsx/pivot.js
CHANGED
|
@@ -1,16 +1,7 @@
|
|
|
1
|
-
// OOXML rendering for a modeled pivot table: the three parts a pivot needs — the cache definition
|
|
2
|
-
// (field catalogue), the cache records (a copy of the source rows), and the pivot table definition
|
|
3
|
-
// (the layout). The semantic computation lives in `core/pivot-table.ts`; this file only serialises.
|
|
4
|
-
//
|
|
5
|
-
// Every value that reaches an attribute is run through `escapeAttr`, so source strings carrying XML
|
|
6
|
-
// specials (`Smith & Co`, `<West>`, `It's "best"`) become well-formed markup rather than corrupting
|
|
7
|
-
// the package — the whole point of the shared-item escaping this module guarantees.
|
|
8
1
|
import { encodeAddress } from '../../core/address.js';
|
|
9
2
|
import { escapeAttr, XML_DECLARATION } from '../../xml/xml.js';
|
|
10
3
|
import { RELATIONSHIPS_NS } from '../opc/namespaces.js';
|
|
11
4
|
import { SPREADSHEETML_NS } from './namespaces.js';
|
|
12
|
-
// Excel's default caption prefix for each aggregation ("Sum of Amount", "Average of Amount"). A
|
|
13
|
-
// metric's name is also its `subtotal` value, which is why the record key equals the enum member.
|
|
14
5
|
const METRIC_CAPTIONS = {
|
|
15
6
|
sum: 'Sum',
|
|
16
7
|
count: 'Count',
|
|
@@ -24,10 +15,6 @@ const METRIC_CAPTIONS = {
|
|
|
24
15
|
var: 'Var',
|
|
25
16
|
varp: 'Varp',
|
|
26
17
|
};
|
|
27
|
-
/** The `pivotCacheDefinition` part: the source reference and the field catalogue. `r:id="rId1"`
|
|
28
|
-
* names the cache-records part through this cache's own rels part. `refreshOnLoad="1"` tells Excel to
|
|
29
|
-
* rebuild the pivot body from the cache on open, so the layout stays correct without us computing it
|
|
30
|
-
* to the pixel. `recordCount` is the number of source data rows. */
|
|
31
18
|
export function pivotCacheDefinitionXml(table) {
|
|
32
19
|
const fields = table.cacheFields
|
|
33
20
|
.map((field) => {
|
|
@@ -59,8 +46,6 @@ export function pivotCacheDefinitionXml(table) {
|
|
|
59
46
|
`<cacheFields count="${table.cacheFields.length}">${fields}</cacheFields>` +
|
|
60
47
|
`</pivotCacheDefinition>`);
|
|
61
48
|
}
|
|
62
|
-
/** The `pivotCacheRecords` part: one `<r>` per source data row, each cell either an index into an
|
|
63
|
-
* axis field's shared-items catalogue (`<x>`) or an inline value (`<n>`/`<s>`/`<m>`). */
|
|
64
49
|
export function pivotCacheRecordsXml(table) {
|
|
65
50
|
const rows = table.records
|
|
66
51
|
.map((record) => `<r>${record.map(recordCellXml).join('')}</r>`)
|
|
@@ -70,16 +55,11 @@ export function pivotCacheRecordsXml(table) {
|
|
|
70
55
|
rows +
|
|
71
56
|
`</pivotCacheRecords>`);
|
|
72
57
|
}
|
|
73
|
-
/** The `pivotTableDefinition` part placed on the destination sheet: the field layout that binds the
|
|
74
|
-
* cache (by `cacheId`) to the row/column axes and the summed value field. */
|
|
75
58
|
export function pivotTableXml(table, name, cacheId) {
|
|
76
59
|
const rowField = table.rowFields[0];
|
|
77
60
|
const columnField = table.columnFields[0];
|
|
78
61
|
const rowGroups = table.cacheFields[rowField]?.sharedItems?.length ?? 1;
|
|
79
62
|
const columnGroups = table.cacheFields[columnField]?.sharedItems?.length ?? 1;
|
|
80
|
-
// A generous bounding box on the destination sheet: a row-label column plus one column per column
|
|
81
|
-
// group plus a grand-total column; two header rows plus one row per row group plus a grand total.
|
|
82
|
-
// Excel recomputes the exact extent from the cache on refresh, so this only has to be valid.
|
|
83
63
|
const location = `A1:${encodeAddress(2 + columnGroups, 3 + rowGroups)}`;
|
|
84
64
|
const pivotFields = table.cacheFields
|
|
85
65
|
.map((field, index) => {
|
|
@@ -116,8 +96,6 @@ export function pivotTableXml(table, name, cacheId) {
|
|
|
116
96
|
`showRowStripes="0" showColStripes="0" showLastColumn="1"/>` +
|
|
117
97
|
`</pivotTableDefinition>`);
|
|
118
98
|
}
|
|
119
|
-
/** The `<dataField>` that names the aggregated column and selects its function. `sum` is Excel's
|
|
120
|
-
* implicit default, so its `subtotal` attribute is omitted; every other metric names itself. */
|
|
121
99
|
function dataFieldXml(table) {
|
|
122
100
|
const caption = `${METRIC_CAPTIONS[table.metric]} of ${table.valueFieldName}`;
|
|
123
101
|
const subtotal = table.metric === 'sum' ? '' : ` subtotal="${table.metric}"`;
|
|
@@ -1,21 +1,5 @@
|
|
|
1
|
-
// Reading a pivot table back from its OOXML parts — the inverse of `pivot.ts`.
|
|
2
|
-
//
|
|
3
|
-
// A written pivot round-trips today by byte-preservation: the reader keeps the pivot parts verbatim
|
|
4
|
-
// and re-emits them. That keeps the file faithful, but leaves the pivot opaque to the model — a
|
|
5
|
-
// `.model` copy cannot carry a pivot it cannot see. This module reconstructs the *semantic* shape of
|
|
6
|
-
// a pivot (its source, field roles, value field, and aggregation) from the `pivotTableDefinition`
|
|
7
|
-
// and its `pivotCacheDefinition`, so a loaded pivot becomes inspectable data rather than an opaque
|
|
8
|
-
// blob.
|
|
9
|
-
//
|
|
10
|
-
// Read leniently: these parts describe a file that already exists, so a missing or unrecognised
|
|
11
|
-
// attribute yields a sensible default rather than a throw — the strict invariants belong on the
|
|
12
|
-
// authoring path (`core/pivot-table.ts`), not here. A hostile part therefore degrades to an
|
|
13
|
-
// incomplete model; it never crashes the reader.
|
|
14
1
|
import { pivotMetricFromSubtotal, } from '../../core/pivot-table.js';
|
|
15
2
|
import { localName, parseXml } from '../../xml/xml-read.js';
|
|
16
|
-
/** Reconstruct a pivot's semantic model from its two definition parts. The records part is not
|
|
17
|
-
* consulted: the cache's field catalogue and the table's field roles fully describe the pivot's
|
|
18
|
-
* shape, and the aggregated values are Excel's to compute on refresh. */
|
|
19
3
|
export function parsePivotTable(tableXml, cacheXml) {
|
|
20
4
|
const { fields, source } = parsePivotCacheDefinition(cacheXml);
|
|
21
5
|
const def = parsePivotTableDefinition(tableXml);
|
|
@@ -32,13 +16,8 @@ export function parsePivotTable(tableXml, cacheXml) {
|
|
|
32
16
|
metric: def.metric,
|
|
33
17
|
};
|
|
34
18
|
}
|
|
35
|
-
/** The field catalogue and worksheet source from a `pivotCacheDefinition`. Fields are collected in
|
|
36
|
-
* document order — the order a table's `fld`/`x` indices address them by. */
|
|
37
19
|
function parsePivotCacheDefinition(cacheXml) {
|
|
38
20
|
const fields = [];
|
|
39
|
-
// A worksheet source is the assumed default until proven otherwise: it is what our writer emits and
|
|
40
|
-
// the overwhelmingly common shape, and its `<worksheetSource>` child fills in the coordinates. A
|
|
41
|
-
// `<cacheSource type>` we recognise overrides the kind; an unrecognised one degrades to `unknown`.
|
|
42
21
|
let source = { kind: 'worksheet', sheet: '', ref: '' };
|
|
43
22
|
parseXml(cacheXml, {
|
|
44
23
|
onOpen(name, attrs) {
|
|
@@ -56,10 +35,6 @@ function parsePivotCacheDefinition(cacheXml) {
|
|
|
56
35
|
});
|
|
57
36
|
return { fields, source };
|
|
58
37
|
}
|
|
59
|
-
/** The layout half of a pivot: its name, cache id, axis field roles, and the single value field.
|
|
60
|
-
* `<field x>` appears identically inside `<rowFields>` and `<colFields>`, so the current container is
|
|
61
|
-
* tracked to route each into the right axis. Only the first `<dataField>` is modeled — the authoring
|
|
62
|
-
* model supports one value field — and any further ones are ignored rather than rejected. */
|
|
63
38
|
function parsePivotTableDefinition(tableXml) {
|
|
64
39
|
let name = '';
|
|
65
40
|
let cacheId = '';
|
|
@@ -116,16 +91,11 @@ const SOURCE_KINDS = new Set([
|
|
|
116
91
|
'consolidation',
|
|
117
92
|
'scenario',
|
|
118
93
|
]);
|
|
119
|
-
/** Map a `<cacheSource type>` to a known kind. Absent reads as `worksheet` (the spec default and what
|
|
120
|
-
* our writer emits); an unrecognised value reads as `unknown` rather than throwing, keeping the read
|
|
121
|
-
* lenient while still telling a consumer the declared source is not one we model. */
|
|
122
94
|
function sourceKind(type) {
|
|
123
95
|
if (type === undefined)
|
|
124
96
|
return 'worksheet';
|
|
125
97
|
return SOURCE_KINDS.has(type) ? type : 'unknown';
|
|
126
98
|
}
|
|
127
|
-
/** Parse a non-negative field index attribute, or -1 when it is absent or not a whole number — a
|
|
128
|
-
* hostile `x="../etc"` can never become a wild array index this way. */
|
|
129
99
|
function toIndex(value) {
|
|
130
100
|
if (value === undefined)
|
|
131
101
|
return -1;
|