@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/read.js
CHANGED
|
@@ -1,21 +1,3 @@
|
|
|
1
|
-
// The buffered `.xlsx` reader: an OPC zip package in, a Workbook model out.
|
|
2
|
-
//
|
|
3
|
-
// It reconstructs the part of the model the writer emits today — sheet names and order,
|
|
4
|
-
// cells holding a number, string, boolean, or formula, per-column width/visibility,
|
|
5
|
-
// per-row height/visibility, merged ranges, page margins, and cell styles (pattern fills,
|
|
6
|
-
// number formats, fonts, borders, alignment, and protection — per cell, or inherited from a
|
|
7
|
-
// formatted row/column). Shared-formula slaves and the richer value kinds land as the model
|
|
8
|
-
// grows; an unrecognised construct is skipped rather than guessed, so a foreign file reads
|
|
9
|
-
// without crashing even where a facet is not yet materialised.
|
|
10
|
-
//
|
|
11
|
-
// This module is the orchestrator: it wires the parsed package parts together — the OPC/rel
|
|
12
|
-
// resolution (`./read-opc.ts`), the style table (`./read-styles.ts`), and each worksheet body
|
|
13
|
-
// (`./read-worksheet.ts`) — and owns the sheet-part discovery (notes, images, tables, pivots) and
|
|
14
|
-
// preserved-reference capture that a faithful round-trip depends on.
|
|
15
|
-
//
|
|
16
|
-
// Untrusted input: inflate is bounded by a running byte counter (`./inflate.ts`) that caps
|
|
17
|
-
// actual decompressed output rather than trusting the archive's forgeable size headers, and
|
|
18
|
-
// the parser (ADR 0004) never expands entities.
|
|
19
1
|
import { decodeRange } from '../../core/address.js';
|
|
20
2
|
import { unmangleFunctions } from '../../core/formula.js';
|
|
21
3
|
import { INTERNAL } from '../../core/internal.js';
|
|
@@ -33,36 +15,15 @@ import { parseConditionalFormattings, parseDxfs } from './conditional-formatting
|
|
|
33
15
|
import { applyDataValidations, parseDataValidations, parseExtendedDataValidations, } from './data-validation.js';
|
|
34
16
|
import { applyHyperlinks, parseSheetHyperlinks } from './hyperlinks.js';
|
|
35
17
|
import { drawingHasUnmodeledContent, parseDrawing } from './images.js';
|
|
36
|
-
import { parsePivotTable } from './pivot
|
|
37
|
-
import {
|
|
18
|
+
import { parsePivotTable } from './read-pivot.js';
|
|
19
|
+
import { parseSharedStrings } from './read-shared-strings.js';
|
|
20
|
+
import { parseIndexedColors, parseMruColors, parseStyleTable, parseTableStyles, } from './read-styles.js';
|
|
38
21
|
import { parseWorksheet } from './read-worksheet.js';
|
|
39
|
-
import { parseSharedStrings } from './shared-strings-read.js';
|
|
40
|
-
import { parseIndexedColors, parseMruColors, parseTableStyles } from './styles.js';
|
|
41
22
|
import { parseTable } from './tables.js';
|
|
42
23
|
import { buildCommentThreads, parsePersons, parseThreadedComments } from './threaded-comments.js';
|
|
43
|
-
// Re-exported for the streaming reader (`./read-rows.ts`) and the public barrel, which import these
|
|
44
|
-
// from here: the split into per-part parsers is internal, so the reader's import surface is stable.
|
|
45
24
|
export { parseRelationships, resolveWorkbookPart } from '../opc/read-opc.js';
|
|
46
|
-
// The inflate bound and its option bag are shared with the `.xlsb` reader and the row streamer, so
|
|
47
|
-
// they are declared apart from all three; they stay reachable here because this is the entry point
|
|
48
|
-
// callers reach for.
|
|
49
25
|
export { DEFAULT_MAX_UNCOMPRESSED } from '../opc/read-options.js';
|
|
50
26
|
export { parseStyleTable } from './read-styles.js';
|
|
51
|
-
/**
|
|
52
|
-
* Read a spreadsheet package into a {@link Workbook}.
|
|
53
|
-
*
|
|
54
|
-
* Both OOXML serialisations are accepted: an XML `.xlsx`, and a binary `.xlsb` (BIFF12), which is the
|
|
55
|
-
* same OPC container with binary office-document parts. The two are auto-detected from the package
|
|
56
|
-
* itself rather than from a file extension, so a caller never branches on which form it holds — and
|
|
57
|
-
* the model produced is the same either way. See `../xlsb/read.ts` for what the binary path does not
|
|
58
|
-
* yet decode.
|
|
59
|
-
*
|
|
60
|
-
* @throws {UnsupportedFormatError} if the input is neither — a legacy `.xls` (`.format === 'xls'`) or
|
|
61
|
-
* an unrecognised/non-ZIP blob (`'unknown'`).
|
|
62
|
-
* @throws {XlsbParseError} if a binary `.xlsb` part is malformed.
|
|
63
|
-
* @throws {PackageReadError} if the input is a ZIP that cannot be unpacked — a corrupt or
|
|
64
|
-
* truncated archive, or one exceeding the inflate bound (a probable zip bomb).
|
|
65
|
-
*/
|
|
66
27
|
export function readXlsx(data, options = {}) {
|
|
67
28
|
const cap = options.maxUncompressedBytes ?? DEFAULT_MAX_UNCOMPRESSED;
|
|
68
29
|
const files = inflateSpreadsheetPackage(data, cap);
|
|
@@ -70,59 +31,31 @@ export function readXlsx(data, options = {}) {
|
|
|
70
31
|
const { partText } = pkg;
|
|
71
32
|
const workbookXml = partText('xl/workbook.xml');
|
|
72
33
|
if (workbookXml === undefined) {
|
|
73
|
-
// No XML office document. A binary one means this is an `.xlsb`, which reads through the BIFF12
|
|
74
|
-
// codec over the very same model — the package is already inflated, so it is handed over as-is.
|
|
75
34
|
if (files[XLSB_WORKBOOK_PART] !== undefined)
|
|
76
35
|
return readXlsbPackage(files);
|
|
77
36
|
throw new UnsupportedFormatError('unknown');
|
|
78
37
|
}
|
|
79
|
-
// A part's content type is needed to faithfully re-declare any part preserved verbatim for
|
|
80
|
-
// round-tripping (a vector-shape drawing, a header/footer image and its VML). Resolve it the way
|
|
81
|
-
// OPC does: an explicit `<Override>` for the exact part, else the `<Default>` for its extension.
|
|
82
38
|
const contentTypeOf = contentTypeResolver(partText('[Content_Types].xml') ?? '');
|
|
83
39
|
const workbookRelsXml = partText('xl/_rels/workbook.xml.rels') ?? '';
|
|
84
40
|
const rels = parseRelationships(workbookRelsXml);
|
|
85
41
|
const sharedStrings = parseSharedStrings(partText('xl/sharedStrings.xml') ?? '');
|
|
86
|
-
// The style table resolves a cell/row/column style index to its facets (fill, number
|
|
87
|
-
// format); a package without one (a hand-rolled foreign file) yields an empty table and
|
|
88
|
-
// every index reads as unstyled.
|
|
89
42
|
const stylesXml = partText('xl/styles.xml') ?? '';
|
|
90
43
|
const { cellXfs: xfStyles, namedStyles, defaultFont } = parseStyleTable(stylesXml);
|
|
91
44
|
const workbook = new Workbook();
|
|
92
|
-
// Preserve the differential-style table verbatim so conditional formatting's dxfId references stay
|
|
93
|
-
// valid — and a foreign dxf's number format stays a real format code — across a re-write.
|
|
94
45
|
workbook[INTERNAL].restoreDifferentialStyles(parseDxfs(stylesXml));
|
|
95
|
-
// Preserve a custom indexed-color palette verbatim so an `indexed="…"` colour reference keeps its
|
|
96
|
-
// intended RGB across a re-write instead of resolving to a different default-palette entry.
|
|
97
46
|
workbook[INTERNAL].restoreIndexedColors(parseIndexedColors(stylesXml));
|
|
98
|
-
// Preserve the author's "Recent Colors" swatches, which the model never reads but re-writing would
|
|
99
|
-
// otherwise discard.
|
|
100
47
|
workbook[INTERNAL].restoreMruColors(parseMruColors(stylesXml));
|
|
101
|
-
// Preserve the custom table-style definitions so a table referencing one by name still resolves to
|
|
102
|
-
// a real definition after a re-write instead of rendering unstyled.
|
|
103
48
|
workbook[INTERNAL].restoreTableStyles(parseTableStyles(stylesXml));
|
|
104
|
-
// Preserve the theme part so a branded colour/font scheme is not overwritten by the default theme
|
|
105
|
-
// the writer emits for a workbook that has none.
|
|
106
49
|
readWorkbookTheme(workbookRelsXml, pkg, contentTypeOf, workbook);
|
|
107
|
-
// Preserve the named cell-style layer only when a file declares one beyond the Normal default, so an
|
|
108
|
-
// ordinary workbook keeps an empty named-style table and emits just the default on write.
|
|
109
50
|
if (namedStyles.length > 1)
|
|
110
51
|
workbook[INTERNAL].restoreNamedStyles(namedStyles);
|
|
111
|
-
// Preserve the declared default font (font id 0) so a re-write emits the face the file itself named
|
|
112
|
-
// rather than an assumed Calibri — which would change every empty cell and the metric every
|
|
113
|
-
// character-unit column width is expressed in.
|
|
114
52
|
workbook[INTERNAL].restoreDefaultFont(defaultFont);
|
|
115
53
|
const core = partText('docProps/core.xml');
|
|
116
54
|
if (core !== undefined)
|
|
117
55
|
applyCoreProperties(workbook, core);
|
|
118
56
|
workbook.protection = parseWorkbookProtection(workbookXml);
|
|
119
57
|
applyWorkbookView(workbook.view, workbookXml);
|
|
120
|
-
// The threaded-comment author registry is workbook-level, and every conversation on every sheet
|
|
121
|
-
// resolves its authors and @mentions through it — so it is restored before the sheet loop that reads
|
|
122
|
-
// those conversations, not alongside the other workbook-level parts below.
|
|
123
58
|
readWorkbookPersons(workbookRelsXml, pkg, workbook);
|
|
124
|
-
// A picture used on more than one sheet is one media part; caching by media path keeps it a single
|
|
125
|
-
// workbook image so a re-write does not duplicate the bytes.
|
|
126
59
|
const imageIdByMediaPath = new Map();
|
|
127
60
|
const sheetOrder = [];
|
|
128
61
|
for (const { name, relId, state } of parseWorkbookSheets(workbookXml)) {
|
|
@@ -144,8 +77,6 @@ export function readXlsx(data, options = {}) {
|
|
|
144
77
|
for (const cf of parseConditionalFormattings(sheetXml))
|
|
145
78
|
sheet.addConditionalFormatting(cf);
|
|
146
79
|
}
|
|
147
|
-
// Threads before notes: a threaded cell's comments-part entry is the thread's legacy fallback, not
|
|
148
|
-
// a note, and `applyNotes` reads the sheet's restored threads to tell the two apart.
|
|
149
80
|
const threads = readSheetCommentThreads(path, pkg, workbook);
|
|
150
81
|
if (threads.length > 0)
|
|
151
82
|
sheet[INTERNAL].restoreCommentThreads(threads);
|
|
@@ -166,16 +97,11 @@ export function readXlsx(data, options = {}) {
|
|
|
166
97
|
}
|
|
167
98
|
readWorkbookPreservedReferences(workbookXml, pkg, contentTypeOf, workbook);
|
|
168
99
|
readRootPreservedReferences(pkg, contentTypeOf, workbook);
|
|
169
|
-
// Defined names follow the sheets: a scoped name's `localSheetId` indexes the sheet order, which
|
|
170
|
-
// is why the names are read only once every sheet is registered.
|
|
171
100
|
for (const name of parseWorkbookDefinedNames(workbookXml, sheetOrder)) {
|
|
172
101
|
workbook.defineName(name);
|
|
173
102
|
}
|
|
174
103
|
return workbook;
|
|
175
104
|
}
|
|
176
|
-
// A sheet's comments live in a comments part reached through the sheet's own relationships: the sheet
|
|
177
|
-
// declares a relationship of type `.../comments` whose target resolves (relative to the sheet's
|
|
178
|
-
// directory) to the comments part. A sheet with no rels part or no such relationship simply has none.
|
|
179
105
|
function readSheetComments(sheetPath, pkg) {
|
|
180
106
|
const commentsPath = sheetRelTarget(sheetPath, pkg.partText, 'comments');
|
|
181
107
|
if (commentsPath === undefined)
|
|
@@ -185,26 +111,12 @@ function readSheetComments(sheetPath, pkg) {
|
|
|
185
111
|
return undefined;
|
|
186
112
|
return parseComments(commentsXml);
|
|
187
113
|
}
|
|
188
|
-
// The workbook's threaded-comment identity registry: a relationship of type `.../person` names
|
|
189
|
-
// `xl/persons/person.xml`, whose entries every message's `personId` and every mention's
|
|
190
|
-
// `mentionpersonId` resolve through. A workbook with no threaded comments declares no such
|
|
191
|
-
// relationship and keeps an empty registry.
|
|
192
114
|
function readWorkbookPersons(workbookRelsXml, pkg, workbook) {
|
|
193
115
|
const target = relationshipTargetByType(workbookRelsXml, 'person');
|
|
194
116
|
const xml = target === undefined ? undefined : pkg.partText(resolveWorkbookPart(target));
|
|
195
117
|
if (xml !== undefined)
|
|
196
118
|
workbook[INTERNAL].restorePersons(parsePersons(xml));
|
|
197
119
|
}
|
|
198
|
-
// The workbook's theme part: the `<clrScheme>`/`<fontScheme>`/`<fmtScheme>` every `theme="n"` colour
|
|
199
|
-
// and every `scheme="major|minor"` font in the package resolves against. It is reached through the
|
|
200
|
-
// workbook's `.../theme` relationship rather than assumed at `xl/theme/theme1.xml`, because the target
|
|
201
|
-
// is rel-relative and a foreign package is free to name the part anything.
|
|
202
|
-
//
|
|
203
|
-
// Captured with its transitive part closure, not as a lone string: a theme can carry its own
|
|
204
|
-
// relationships (a picture used as a themed fill, wired by an `r:embed` into the theme's rels part),
|
|
205
|
-
// and re-emitting the theme body without them would leave that reference dangling — which Excel
|
|
206
|
-
// reports as a package needing repair. A package that declares no theme leaves the workbook on the
|
|
207
|
-
// library's default, which is also what a dangling relationship target degrades to.
|
|
208
120
|
function readWorkbookTheme(workbookRelsXml, pkg, contentTypeOf, workbook) {
|
|
209
121
|
const target = relationshipTargetByType(workbookRelsXml, 'theme');
|
|
210
122
|
if (target === undefined)
|
|
@@ -214,16 +126,6 @@ function readWorkbookTheme(workbookRelsXml, pkg, contentTypeOf, workbook) {
|
|
|
214
126
|
if (parts !== undefined)
|
|
215
127
|
workbook[INTERNAL].restoreThemePart({ entryPath, parts });
|
|
216
128
|
}
|
|
217
|
-
// A sheet's threaded conversations live in a `xl/threadedComments/threadedComment{n}.xml` part reached
|
|
218
|
-
// through a relationship of type `.../threadedComment` on the sheet's own rels — the same discovery
|
|
219
|
-
// shape as the notes part above, and deliberately separate from it: a thread and a legacy note are
|
|
220
|
-
// different features that happen to share a sheet. The messages are grouped into threads and their
|
|
221
|
-
// authors resolved against the workbook registry, so each thread lands self-contained.
|
|
222
|
-
//
|
|
223
|
-
// What lands here IS what a re-write emits: the thread part is re-serialised from these threads, and so is
|
|
224
|
-
// the legacy fallback `<comment>` that binds each cell to its conversation (see `comments.ts`). Anything
|
|
225
|
-
// this reader drops is therefore dropped from the file — which is why a message too damaged to place is
|
|
226
|
-
// still kept wherever it can be, and why the anchor is canonicalised here rather than trusted downstream.
|
|
227
129
|
function readSheetCommentThreads(sheetPath, pkg, workbook) {
|
|
228
130
|
const path = sheetRelTarget(sheetPath, pkg.partText, 'threadedComment');
|
|
229
131
|
const xml = path === undefined ? undefined : pkg.partText(path);
|
|
@@ -231,19 +133,10 @@ function readSheetCommentThreads(sheetPath, pkg, workbook) {
|
|
|
231
133
|
return [];
|
|
232
134
|
return buildCommentThreads(parseThreadedComments(xml), (id) => workbook.getPerson(id));
|
|
233
135
|
}
|
|
234
|
-
// A sheet's printer-settings blob is an opaque binary part linked from `<pageSetup r:id>`: the sheet
|
|
235
|
-
// declares a relationship of type `.../printerSettings` whose target resolves to a `.bin` part. We
|
|
236
|
-
// keep the raw bytes verbatim — the DEVMODE inside is platform-specific and the model never
|
|
237
|
-
// interprets it, only round-trips it so re-writing the file preserves the user's print configuration.
|
|
238
|
-
// A sheet with no rels part or no such relationship simply has none.
|
|
239
136
|
function readSheetPrinterSettings(sheetPath, pkg) {
|
|
240
137
|
const path = sheetRelTarget(sheetPath, pkg.partText, 'printerSettings');
|
|
241
138
|
return path === undefined ? undefined : pkg.partBytes(path);
|
|
242
139
|
}
|
|
243
|
-
// A sheet's anchored images live in a drawing part reached through the sheet's own relationships: a
|
|
244
|
-
// relationship of type `.../drawing` names the drawing part, whose own relationships map each
|
|
245
|
-
// picture's embed id to a media part under `xl/media/`. Each anchor becomes a workbook image (deduped
|
|
246
|
-
// by media path) placed back on the sheet at its two-cell anchor.
|
|
247
140
|
function readSheetImages(sheetPath, pkg, workbook, sheet, imageIdByMediaPath) {
|
|
248
141
|
const { partText, partBytes } = pkg;
|
|
249
142
|
const drawingPath = sheetRelTarget(sheetPath, partText, 'drawing');
|
|
@@ -252,10 +145,6 @@ function readSheetImages(sheetPath, pkg, workbook, sheet, imageIdByMediaPath) {
|
|
|
252
145
|
const drawingXml = partText(drawingPath);
|
|
253
146
|
if (drawingXml === undefined)
|
|
254
147
|
return;
|
|
255
|
-
// A drawing that also holds a chart or shape is preserved whole (see readSheetPreservedReferences),
|
|
256
|
-
// so its pictures must not be modeled here: modeling them would leave the sheet with images, which
|
|
257
|
-
// suppresses that preservation and drops the chart. Leaving `sheet.images` empty routes the entire
|
|
258
|
-
// drawing — pictures included — through byte-preservation, keeping every anchor faithful.
|
|
259
148
|
if (drawingHasUnmodeledContent(drawingXml))
|
|
260
149
|
return;
|
|
261
150
|
const drawingRels = parseRelationships(partText(relsPathFor(drawingPath)) ?? '');
|
|
@@ -282,11 +171,6 @@ function readSheetImages(sheetPath, pkg, workbook, sheet, imageIdByMediaPath) {
|
|
|
282
171
|
}
|
|
283
172
|
}
|
|
284
173
|
}
|
|
285
|
-
// A sheet background is a workbook image referenced by the worksheet's `<picture>` element through a
|
|
286
|
-
// sheet-local relationship of type `.../image`. Unlike an anchored image (whose image relationships
|
|
287
|
-
// live in the drawing part's own rels), the background's relationship sits directly on the sheet, so
|
|
288
|
-
// it is the sheet rels' sole image relationship. The bytes are deduped against images shared with a
|
|
289
|
-
// drawing, keeping one media part per picture across a re-write.
|
|
290
174
|
function readSheetBackground(sheetPath, pkg, workbook, sheet, imageIdByMediaPath) {
|
|
291
175
|
const mediaPath = sheetRelTarget(sheetPath, pkg.partText, 'image');
|
|
292
176
|
if (mediaPath === undefined)
|
|
@@ -301,16 +185,6 @@ function readSheetBackground(sheetPath, pkg, workbook, sheet, imageIdByMediaPath
|
|
|
301
185
|
}
|
|
302
186
|
sheet.addBackgroundImage(id);
|
|
303
187
|
}
|
|
304
|
-
// Capture the worksheet-level references to package content the model does not interpret, so a
|
|
305
|
-
// round-trip re-emits them verbatim instead of dropping them:
|
|
306
|
-
// • `<drawing>` — but only when the reader modeled no anchored image from it: either a drawing that
|
|
307
|
-
// holds no pictures at all (a chart or shape), or a mixed drawing whose pictures the reader
|
|
308
|
-
// declined to model precisely so the whole part (chart included) rides here verbatim. A drawing
|
|
309
|
-
// whose pictures were modeled is owned by the model and re-serialised from it; capturing it here
|
|
310
|
-
// too would double-emit those pictures.
|
|
311
|
-
// • `<legacyDrawingHF>` — a header/footer image's VML, which the model never interprets.
|
|
312
|
-
// Each reference's target part and the transitive closure of parts it reaches (a VML's image, a
|
|
313
|
-
// drawing's media) are captured with their bytes, content types, and relationships.
|
|
314
188
|
function readSheetPreservedReferences(sheetPath, sheetXml, pkg, contentTypeOf, sheet) {
|
|
315
189
|
const { partText, partBytes } = pkg;
|
|
316
190
|
const relsXml = partText(relsPathFor(sheetPath));
|
|
@@ -324,10 +198,6 @@ function readSheetPreservedReferences(sheetPath, sheetXml, pkg, contentTypeOf, s
|
|
|
324
198
|
if (parts !== undefined)
|
|
325
199
|
sheet[INTERNAL].addPreservedReference({ element, relType, entryPath, parts });
|
|
326
200
|
};
|
|
327
|
-
// Element-wired references: a `<drawing>`/`<legacyDrawingHF>` names its part by an `r:id` in the
|
|
328
|
-
// sheet body. A `<drawing>` is preserved only when the reader modeled no picture from it — a
|
|
329
|
-
// chart/shape-only drawing, or a mixed one the reader left unmodeled — since one whose pictures are
|
|
330
|
-
// modeled is re-serialised from the model.
|
|
331
201
|
const referenceElements = sheet.images.length === 0 ? ['drawing', 'legacyDrawingHF'] : ['legacyDrawingHF'];
|
|
332
202
|
for (const element of referenceElements) {
|
|
333
203
|
const relId = worksheetReferenceRelId(sheetXml, element);
|
|
@@ -335,9 +205,6 @@ function readSheetPreservedReferences(sheetPath, sheetXml, pkg, contentTypeOf, s
|
|
|
335
205
|
if (record !== undefined && !record.external)
|
|
336
206
|
capture(element, record.type, record.target);
|
|
337
207
|
}
|
|
338
|
-
// Relationship-wired references: a pivot table or slicer is reached through a sheet relationship
|
|
339
|
-
// with no worksheet child pointing at it — Excel discovers it by scanning the sheet's rels. Preserve
|
|
340
|
-
// each so the pivots/slicers a fill-and-save workflow does not touch are not dropped.
|
|
341
208
|
for (const record of records) {
|
|
342
209
|
if (record.external)
|
|
343
210
|
continue;
|
|
@@ -345,19 +212,9 @@ function readSheetPreservedReferences(sheetPath, sheetXml, pkg, contentTypeOf, s
|
|
|
345
212
|
capture(undefined, record.type, record.target);
|
|
346
213
|
}
|
|
347
214
|
}
|
|
348
|
-
// A sheet relationship the model does not consume but must round-trip: a pivot table or a slicer. Every
|
|
349
|
-
// other sheet rel kind (drawing, printerSettings, table, comments, threadedComment, hyperlinks, background
|
|
350
|
-
// image, the comment VML) is modeled and re-serialised from the model, so preserving it here would emit
|
|
351
|
-
// the part twice.
|
|
352
215
|
function isPreservedSheetRelType(type) {
|
|
353
216
|
return type.endsWith('/pivotTable') || type.endsWith('/slicer');
|
|
354
217
|
}
|
|
355
|
-
// Capture the workbook-level references to package content the model does not interpret — pivot
|
|
356
|
-
// caches (`pivotCacheDefinition`), slicer caches (`slicerCache`), and external links (`externalLink`,
|
|
357
|
-
// each a link to a source workbook) — so a round-trip re-emits them instead of dropping the pivots,
|
|
358
|
-
// slicers, and linked-workbook references they back. A pivot cache's `<pivotCaches>` registration (its
|
|
359
|
-
// `cacheId`) and an external link's `<externalReferences>` position (its `[n]` index) are captured
|
|
360
|
-
// alongside so the wiring a pivot table or a formula resolves through survives too.
|
|
361
218
|
function readWorkbookPreservedReferences(workbookXml, pkg, contentTypeOf, workbook) {
|
|
362
219
|
const { partText, partBytes } = pkg;
|
|
363
220
|
const relsXml = partText('xl/_rels/workbook.xml.rels');
|
|
@@ -383,11 +240,6 @@ function readWorkbookPreservedReferences(workbookXml, pkg, contentTypeOf, workbo
|
|
|
383
240
|
});
|
|
384
241
|
}
|
|
385
242
|
}
|
|
386
|
-
// Content wired from the package's own `_rels/.rels` that the writer does not regenerate from the
|
|
387
|
-
// model — the ribbon customUI parts, custom document properties, a thumbnail. The writer rebuilds the
|
|
388
|
-
// root rels for the parts it models (the workbook, and core/app properties), so every other root
|
|
389
|
-
// relationship's target would be dropped on write; capturing its closure here re-declares it verbatim.
|
|
390
|
-
// External targets and the three regenerated relationship types are skipped.
|
|
391
243
|
function readRootPreservedReferences(pkg, contentTypeOf, workbook) {
|
|
392
244
|
const { partText, partBytes } = pkg;
|
|
393
245
|
const relsXml = partText('_rels/.rels');
|
|
@@ -403,31 +255,17 @@ function readRootPreservedReferences(pkg, contentTypeOf, workbook) {
|
|
|
403
255
|
workbook[INTERNAL].addPreservedRootReference({ relType: record.type, entryPath, parts });
|
|
404
256
|
}
|
|
405
257
|
}
|
|
406
|
-
// The three root relationships the writer regenerates from the model on every write: the office
|
|
407
|
-
// document and the core/extended document properties. Every other root relationship is unmodeled and
|
|
408
|
-
// is preserved verbatim by {@link readRootPreservedReferences} rather than dropped.
|
|
409
258
|
function isRegeneratedRootRelType(type) {
|
|
410
259
|
return (type.endsWith('/officeDocument') ||
|
|
411
260
|
type.endsWith('/core-properties') ||
|
|
412
261
|
type.endsWith('/extended-properties'));
|
|
413
262
|
}
|
|
414
|
-
// A workbook relationship the model does not consume but must round-trip: a pivot cache, a slicer
|
|
415
|
-
// cache, an external link (the pointer to a linked source workbook), or a macro-enabled workbook's VBA
|
|
416
|
-
// project. Worksheets, styles, theme, shared strings, and the threaded-comment person registry are modeled
|
|
417
|
-
// and re-serialised from the model. Preserving vbaProject here — rather than silently dropping it, as an
|
|
418
|
-
// unrecognised relationship type otherwise would — is what keeps loading and re-saving a .xlsm from
|
|
419
|
-
// discarding its macros; the content-type override in workbook-xml.ts is the other half, so the re-emitted
|
|
420
|
-
// package still declares itself macro-enabled. Preserving externalLink is what keeps a formula's `[n]`
|
|
421
|
-
// external reference from dangling: the link part and its `<externalReferences>` registration are both
|
|
422
|
-
// re-emitted.
|
|
423
263
|
function isPreservedWorkbookRelType(type) {
|
|
424
264
|
return (type.endsWith('/pivotCacheDefinition') ||
|
|
425
265
|
type.endsWith('/slicerCache') ||
|
|
426
266
|
type.endsWith('/vbaProject') ||
|
|
427
267
|
type.endsWith('/externalLink'));
|
|
428
268
|
}
|
|
429
|
-
// Map each `<pivotCache>` registration in the workbook's `<pivotCaches>` to the relationship id that
|
|
430
|
-
// reaches its cache definition, so a preserved cache carries the `cacheId` a pivot table refers to.
|
|
431
269
|
function parsePivotCacheRegistrations(workbookXml) {
|
|
432
270
|
const byRelId = new Map();
|
|
433
271
|
for (const { attrs } of openElements(workbookXml, 'pivotCache')) {
|
|
@@ -437,10 +275,6 @@ function parsePivotCacheRegistrations(workbookXml) {
|
|
|
437
275
|
}
|
|
438
276
|
return byRelId;
|
|
439
277
|
}
|
|
440
|
-
// Map each `<externalReference>` in the workbook's `<externalReferences>` to its 0-based position, keyed
|
|
441
|
-
// by the relationship id it wires. That position is the `[n]` index a formula or defined name resolves
|
|
442
|
-
// an external cell through (`[1]Sheet!$A$1`), so preserving it lets the writer re-emit the block in the
|
|
443
|
-
// original order and keep every `[n]` pointing at the same linked workbook.
|
|
444
278
|
function parseExternalReferenceRegistrations(workbookXml) {
|
|
445
279
|
const byRelId = new Map();
|
|
446
280
|
let index = 0;
|
|
@@ -450,10 +284,6 @@ function parseExternalReferenceRegistrations(workbookXml) {
|
|
|
450
284
|
}
|
|
451
285
|
return byRelId;
|
|
452
286
|
}
|
|
453
|
-
// The `r:id` of the first `<drawing>` / `<legacyDrawingHF>` element in a worksheet, or undefined when
|
|
454
|
-
// the sheet declares none. The reference lives in the worksheet XML (not distinguishable by
|
|
455
|
-
// relationship Type — a header/footer VML and a comment VML share the `vmlDrawing` type), so the
|
|
456
|
-
// specific relationship is found by reading the element's `r:id` here.
|
|
457
287
|
function worksheetReferenceRelId(sheetXml, element) {
|
|
458
288
|
for (const { attrs } of openElements(sheetXml, element)) {
|
|
459
289
|
if (attrs['r:id'] !== undefined)
|
|
@@ -461,10 +291,6 @@ function worksheetReferenceRelId(sheetXml, element) {
|
|
|
461
291
|
}
|
|
462
292
|
return undefined;
|
|
463
293
|
}
|
|
464
|
-
// A sheet's tables live in `xl/tables/table{n}.xml` parts, each reached through a relationship of
|
|
465
|
-
// type `.../table` on the sheet's own rels. The writer emits one relationship per table; each part
|
|
466
|
-
// is parsed back into the model and re-registered in definition order. A part that fails to parse
|
|
467
|
-
// (missing name/ref/columns — Excel corruption) is skipped rather than crashing the whole read.
|
|
468
294
|
function readSheetTables(sheetPath, pkg, sheet) {
|
|
469
295
|
const relsXml = pkg.partText(relsPathFor(sheetPath));
|
|
470
296
|
if (relsXml === undefined)
|
|
@@ -479,13 +305,6 @@ function readSheetTables(sheetPath, pkg, sheet) {
|
|
|
479
305
|
}
|
|
480
306
|
dropMergesInsideTables(sheet);
|
|
481
307
|
}
|
|
482
|
-
// Reconstruct an inspectable model of each pivot table hosted on a sheet. A pivot is reached by a
|
|
483
|
-
// sheet relationship of type `.../pivotTable`; the pivot-table part carries its own relationship of
|
|
484
|
-
// type `.../pivotCacheDefinition` to the cache holding the field catalogue and source range. Both
|
|
485
|
-
// parts are parsed and combined into a read-only view registered on the sheet — separate from the
|
|
486
|
-
// byte-preservation that actually round-trips the pivot, so this never changes what is re-emitted.
|
|
487
|
-
// The read is lenient: a pivot whose cache is missing still yields a (partial) model rather than
|
|
488
|
-
// throwing, matching Excel's tolerance for a damaged package on load.
|
|
489
308
|
function readSheetPivotTables(sheetPath, pkg, sheet) {
|
|
490
309
|
const { partText } = pkg;
|
|
491
310
|
const relsXml = partText(relsPathFor(sheetPath));
|
|
@@ -503,10 +322,6 @@ function readSheetPivotTables(sheetPath, pkg, sheet) {
|
|
|
503
322
|
sheet[INTERNAL].addLoadedPivotTable(parsePivotTable(tableXml, cacheXml));
|
|
504
323
|
}
|
|
505
324
|
}
|
|
506
|
-
// Excel forbids a merged range inside a formatted table and repairs such a file on load by dropping
|
|
507
|
-
// the merge. A worksheet's merges are read before its tables, so a real file carrying that invalid
|
|
508
|
-
// geometry lands in the model intact; this applies the same repair once the tables are known, so a
|
|
509
|
-
// re-write does not surface the Excel-invalid geometry the writer (correctly) rejects.
|
|
510
325
|
function dropMergesInsideTables(sheet) {
|
|
511
326
|
const regions = sheet.tables.map((table) => table.region);
|
|
512
327
|
if (regions.length === 0)
|
|
@@ -536,10 +351,6 @@ export function parseWorkbookSheets(xml) {
|
|
|
536
351
|
}
|
|
537
352
|
return sheets;
|
|
538
353
|
}
|
|
539
|
-
// Read the workbook's structure/window protection (`<workbookProtection>`). The three lock flags are
|
|
540
|
-
// decoded as booleans (an absent or "0" attribute stays unlocked), and only the whitelisted
|
|
541
|
-
// password/agile-hash attributes are preserved verbatim — a hostile or unknown attribute is dropped
|
|
542
|
-
// rather than echoed back on write. Returns undefined when the workbook declares no protection.
|
|
543
354
|
export function parseWorkbookProtection(xml) {
|
|
544
355
|
let result;
|
|
545
356
|
parseXml(xml, {
|
|
@@ -566,14 +377,6 @@ export function parseWorkbookProtection(xml) {
|
|
|
566
377
|
});
|
|
567
378
|
return result;
|
|
568
379
|
}
|
|
569
|
-
// Restore the workbook's saved window state from `<bookViews><workbookView/>` onto the model's view,
|
|
570
|
-
// so a round-trip hands back the geometry and active tab the author left rather than stamping the
|
|
571
|
-
// library's defaults over them. Only the first `<workbookView>` is read — the model carries one view,
|
|
572
|
-
// which is all Excel writes and all a single consuming window can restore.
|
|
573
|
-
//
|
|
574
|
-
// Each attribute is applied only when the source carried a usable value; an absent or non-numeric one
|
|
575
|
-
// leaves the default in place, so a truncated or hostile element degrades to a valid window rather
|
|
576
|
-
// than a NaN geometry that would serialise as garbage.
|
|
577
380
|
export function applyWorkbookView(view, xml) {
|
|
578
381
|
for (const { attrs } of openElements(xml, 'workbookView')) {
|
|
579
382
|
applyViewNumber(attrs.xWindow, (value) => (view.x = value));
|
|
@@ -596,10 +399,6 @@ function applyViewNumber(raw, assign) {
|
|
|
596
399
|
if (Number.isFinite(value))
|
|
597
400
|
assign(Math.trunc(value));
|
|
598
401
|
}
|
|
599
|
-
// Reconstruct the workbook's defined names. Each `<definedName>` carries its name (and optional
|
|
600
|
-
// comment/hidden flag) as attributes and its refersTo formula as text content; a `localSheetId`
|
|
601
|
-
// maps back through the sheet order to the scope sheet's name. A name whose localSheetId is out of
|
|
602
|
-
// range (a foreign file referencing a sheet we did not load) is left global rather than dropped.
|
|
603
402
|
function parseWorkbookDefinedNames(xml, sheetOrder) {
|
|
604
403
|
const names = [];
|
|
605
404
|
let capture = false;
|
|
@@ -609,9 +408,6 @@ function parseWorkbookDefinedNames(xml, sheetOrder) {
|
|
|
609
408
|
onOpen(name, attrs) {
|
|
610
409
|
if (localName(name) !== 'definedName' || attrs.name === undefined)
|
|
611
410
|
return;
|
|
612
|
-
// `_xlnm._FilterDatabase` is the built-in Excel derives from a sheet's autofilter, not a
|
|
613
|
-
// user-defined name: it is reconstructed from the sheet's `<autoFilter>` element, so skip it
|
|
614
|
-
// here to keep it off `Workbook.definedNames` and out of a duplicating round-trip.
|
|
615
411
|
if (attrs.name === '_xlnm._FilterDatabase')
|
|
616
412
|
return;
|
|
617
413
|
capture = true;
|
|
@@ -633,8 +429,6 @@ function parseWorkbookDefinedNames(xml, sheetOrder) {
|
|
|
633
429
|
onClose(name) {
|
|
634
430
|
if (localName(name) !== 'definedName' || pending === undefined)
|
|
635
431
|
return;
|
|
636
|
-
// Strip the `_xlfn.`/`_xlpm.` prefixes back to the readable name, the same normalisation the
|
|
637
|
-
// reader applies to a cell formula, so the model never holds the on-disk mangling.
|
|
638
432
|
names.push({ ...pending, refersTo: unmangleFunctions(refersTo) });
|
|
639
433
|
capture = false;
|
|
640
434
|
pending = undefined;
|
|
@@ -642,8 +436,6 @@ function parseWorkbookDefinedNames(xml, sheetOrder) {
|
|
|
642
436
|
});
|
|
643
437
|
return names;
|
|
644
438
|
}
|
|
645
|
-
// Core document properties live in docProps/core.xml under mixed namespaces
|
|
646
|
-
// (dc:creator, cp:lastModifiedBy, dcterms:created/modified); local names disambiguate.
|
|
647
439
|
const CORE_PROPERTY_LOCAL_NAMES = new Set(['creator', 'lastModifiedBy', 'created', 'modified']);
|
|
648
440
|
function applyCoreProperties(workbook, xml) {
|
|
649
441
|
let capture = '';
|
|
@@ -1,10 +1,3 @@
|
|
|
1
|
-
// The relationship-type URIs and namespace groupings the writer's serialisers reference. The
|
|
2
|
-
// canonical namespace set lives in `namespaces.ts`; the writer-local groupings here (the props
|
|
3
|
-
// vocabularies, the content-types namespace, and the rel-type URIs derived from them) are plumbing
|
|
4
|
-
// every part of the writer references.
|
|
5
|
-
//
|
|
6
|
-
// The `.rels` part envelope these types are written into is container-level, not SpreadsheetML, and
|
|
7
|
-
// lives in `../opc/rels.ts`.
|
|
8
1
|
import { CONTENT_TYPES_NS, PKG_RELS_NS, RELATIONSHIPS_NS } from '../opc/namespaces.js';
|
|
9
2
|
import { SPREADSHEETML_NS } from './namespaces.js';
|
|
10
3
|
export const NS = {
|
|
@@ -19,8 +12,6 @@ export const NS = {
|
|
|
19
12
|
dcmitype: 'http://purl.org/dc/dcmitype/',
|
|
20
13
|
xsi: 'http://www.w3.org/2001/XMLSchema-instance',
|
|
21
14
|
};
|
|
22
|
-
// The Office extension relationships namespace the 2018 threaded-comment feature wires both of its parts
|
|
23
|
-
// through. Local to this table: nothing outside it needs the base URI.
|
|
24
15
|
const MS_OFFICE_2017_RELS_NS = 'http://schemas.microsoft.com/office/2017/10/relationships';
|
|
25
16
|
export const REL = {
|
|
26
17
|
worksheet: `${NS.docRels}/worksheet`,
|
|
@@ -40,8 +31,6 @@ export const REL = {
|
|
|
40
31
|
pivotTable: `${NS.docRels}/pivotTable`,
|
|
41
32
|
pivotCacheDefinition: `${NS.docRels}/pivotCacheDefinition`,
|
|
42
33
|
pivotCacheRecords: `${NS.docRels}/pivotCacheRecords`,
|
|
43
|
-
// Threaded comments are a Microsoft extension, so both types live under the 2017/10 Office
|
|
44
|
-
// relationships namespace rather than the standard officeDocument one.
|
|
45
34
|
threadedComment: `${MS_OFFICE_2017_RELS_NS}/threadedComment`,
|
|
46
35
|
person: `${MS_OFFICE_2017_RELS_NS}/person`,
|
|
47
36
|
};
|
|
@@ -1,52 +1,36 @@
|
|
|
1
|
-
// Rich-text run accumulation, shared by the two readers that parse `<r>` runs identically: an inline
|
|
2
|
-
// string's `<is>` in a worksheet body and a pooled `<si>` in sharedStrings.xml. One `<r>` opens a run
|
|
3
|
-
// (resetting its font and text so an unformatted run inherits nothing from the last), an `<rPr>` opens
|
|
4
|
-
// the run's font bundle whose self-closing children each set one facet, and a `<t>` appends the run's
|
|
5
|
-
// text; the run commits on `</r>`. The surrounding parser owns only whether it is inside an `<is>`/`<si>`
|
|
6
|
-
// and where a bare (non-run) `<t>` goes.
|
|
7
1
|
import { applyFontChild } from './read-styles.js';
|
|
8
2
|
export class RunAccumulator {
|
|
9
3
|
#runs = [];
|
|
10
4
|
#font = null;
|
|
11
5
|
#text = '';
|
|
12
6
|
#inRun = false;
|
|
13
|
-
// Discard accumulated runs, readying the accumulator for a fresh `<is>`/`<si>`. A new array is
|
|
14
|
-
// installed, so a value already built from a previous reset's runs keeps its own array.
|
|
15
7
|
reset() {
|
|
16
8
|
this.#runs = [];
|
|
17
9
|
this.#font = null;
|
|
18
10
|
this.#inRun = false;
|
|
19
11
|
}
|
|
20
|
-
/** The runs gathered so far. */
|
|
21
12
|
get runs() {
|
|
22
13
|
return this.#runs;
|
|
23
14
|
}
|
|
24
|
-
// Open a run: reset the per-run font and text so an unformatted run inherits nothing from the last.
|
|
25
15
|
beginRun() {
|
|
26
16
|
this.#inRun = true;
|
|
27
17
|
this.#font = null;
|
|
28
18
|
this.#text = '';
|
|
29
19
|
}
|
|
30
|
-
// Open the run's `<rPr>` font bundle; its children stream in via {@link applyProperty}.
|
|
31
20
|
beginProperties() {
|
|
32
21
|
if (this.#inRun)
|
|
33
22
|
this.#font = {};
|
|
34
23
|
}
|
|
35
|
-
// Apply one `<rPr>` child (`<b/>`, `<sz>`, `<color>`, `<rFont>`, …) to the open run's font; a no-op
|
|
36
|
-
// when no `<rPr>` is open.
|
|
37
24
|
applyProperty(local, attrs) {
|
|
38
25
|
if (this.#font !== null)
|
|
39
26
|
applyFontChild(this.#font, local, attrs);
|
|
40
27
|
}
|
|
41
|
-
// Append a `<t>`'s text to the open run. Returns false when no run is open, so the caller routes the
|
|
42
|
-
// text to its surrounding plain/inline string instead.
|
|
43
28
|
appendText(text) {
|
|
44
29
|
if (!this.#inRun)
|
|
45
30
|
return false;
|
|
46
31
|
this.#text += text;
|
|
47
32
|
return true;
|
|
48
33
|
}
|
|
49
|
-
// Commit the open run — with its font, only if that font set at least one facet — to the runs list.
|
|
50
34
|
endRun() {
|
|
51
35
|
if (!this.#inRun)
|
|
52
36
|
return;
|
|
@@ -1,17 +1,5 @@
|
|
|
1
|
-
// Rich-text runs — a cell value composed of independently-formatted text runs.
|
|
2
|
-
//
|
|
3
|
-
// OOXML stores rich text as a sequence of `<r>` runs, each an optional `<rPr>` (the run's font, in
|
|
4
|
-
// the CT_RPrElt shape — identical to a styles `<font>` except the face element is `<rFont>`, not
|
|
5
|
-
// `<name>`) followed by a `<t>` text element. The writer serialises a rich-text value inline
|
|
6
|
-
// (`t="inlineStr"`), never into the shared-strings table, matching how it writes every other string
|
|
7
|
-
// value; the reader reconstructs the runs while scanning the inline string.
|
|
8
1
|
import { textElement } from '../../xml/xml.js';
|
|
9
2
|
import { fontXml } from './styles.js';
|
|
10
|
-
/**
|
|
11
|
-
* Serialise a rich-text value's runs as the inner content of an `<is>` element. A zero-length run
|
|
12
|
-
* is dropped: an empty `<t/>` is schema-invalid — Excel flags the file as corrupt — and an empty
|
|
13
|
-
* run contributes nothing to the rendered text, so omitting it is loss-free.
|
|
14
|
-
*/
|
|
15
3
|
export function richTextRunsXml(runs) {
|
|
16
4
|
return runs
|
|
17
5
|
.filter((run) => run.text !== '')
|
|
@@ -1,13 +1,6 @@
|
|
|
1
|
-
// Shared-formula group planning: assigning each master/clone group a sheet-unique index and the
|
|
2
|
-
// master's `ref` range before the row loop serialises a single cell.
|
|
3
1
|
import { encodeAddress } from '../../core/address.js';
|
|
4
2
|
import { isFormulaValue, isSharedFormulaValue } from '../../core/value.js';
|
|
5
3
|
import { AuthoringError } from '../../errors.js';
|
|
6
|
-
// Plan a sheet's shared-formula groups: every clone cell (a {@link SharedFormulaValue}) names its
|
|
7
|
-
// master by address, so group the clones by master, assign each group a sheet-unique `si`, and record
|
|
8
|
-
// the `ref` range (master through the furthest clone) on the master. Excel requires the master to sit
|
|
9
|
-
// at the top-left of that range, so a clone above or left of its master — or a master with no formula
|
|
10
|
-
// (an orphan) — is rejected here, named, rather than emitted as a package Excel repairs on open.
|
|
11
4
|
export function planSharedFormulas(sheet) {
|
|
12
5
|
const groups = new Map();
|
|
13
6
|
for (const { cells } of sheet.rows()) {
|
|
@@ -1,29 +1,10 @@
|
|
|
1
|
-
// The shared-strings table: `xl/sharedStrings.xml`, the workbook-wide pool a `t="s"` cell indexes.
|
|
2
|
-
//
|
|
3
|
-
// When the writer runs with `useSharedStrings`, a plain string cell value is interned here — an
|
|
4
|
-
// identical string is stored once and every cell holding it references the single `<si>` entry by
|
|
5
|
-
// index — rather than repeating the text inline in each cell. This trades a second part and an
|
|
6
|
-
// indirection for a smaller package when strings repeat, the storage Excel itself prefers.
|
|
7
|
-
//
|
|
8
|
-
// Both plain strings and rich text are pooled: a plain value becomes a `<si><t>…</t></si>` entry, a
|
|
9
|
-
// rich value a `<si><r>…</r>…</si>` entry carrying its per-run formatting — the rich `<si>` runs Excel
|
|
10
|
-
// itself writes. A `t="s"` cell then indexes either, and the reader reconstructs the runs, so pooled
|
|
11
|
-
// rich text round-trips its formatting rather than flattening to text.
|
|
12
1
|
import { textElement, XML_DECLARATION } from '../../xml/xml.js';
|
|
13
2
|
import { SPREADSHEETML_NS } from './namespaces.js';
|
|
14
3
|
import { richTextRunsXml } from './rich-text.js';
|
|
15
|
-
/**
|
|
16
|
-
* Interns cell string values into the shared-strings pool. {@link intern} returns the index a
|
|
17
|
-
* `t="s"` cell writes as its `<v>`; identical entries collapse to one, so `count` (total references)
|
|
18
|
-
* and `uniqueCount` (distinct entries) diverge exactly as Excel records them. Each entry is stored as
|
|
19
|
-
* its rendered `<si>` inner XML, which is also its dedup key — a plain string (`<t>…`) and rich runs
|
|
20
|
-
* (`<r>…`) render to distinct markup, so the two kinds never collide in the pool.
|
|
21
|
-
*/
|
|
22
4
|
export class SharedStringTable {
|
|
23
5
|
#indexByEntry = new Map();
|
|
24
6
|
#entries = [];
|
|
25
7
|
#references = 0;
|
|
26
|
-
/** Intern a plain or rich string and return its `<si>` index, reusing the entry when it repeats. */
|
|
27
8
|
intern(value) {
|
|
28
9
|
this.#references += 1;
|
|
29
10
|
const inner = typeof value === 'string' ? textElement(value) : richTextRunsXml(value.richText);
|
|
@@ -35,11 +16,9 @@ export class SharedStringTable {
|
|
|
35
16
|
this.#entries.push(inner);
|
|
36
17
|
return index;
|
|
37
18
|
}
|
|
38
|
-
/** Whether no string has been interned — the writer omits the part entirely when so. */
|
|
39
19
|
get isEmpty() {
|
|
40
20
|
return this.#entries.length === 0;
|
|
41
21
|
}
|
|
42
|
-
/** Serialise the pool as the `xl/sharedStrings.xml` part. */
|
|
43
22
|
toXml() {
|
|
44
23
|
const items = this.#entries.map((inner) => `<si>${inner}</si>`).join('');
|
|
45
24
|
return (XML_DECLARATION +
|