@shbernal/ts-xlsx 1.1.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (127) hide show
  1. package/README.md +21 -0
  2. package/dist/core/address.js +0 -33
  3. package/dist/core/autofilter.js +0 -9
  4. package/dist/core/cell.js +0 -131
  5. package/dist/core/color-resolution.js +0 -60
  6. package/dist/core/column.js +0 -60
  7. package/dist/core/comment-thread.js +0 -45
  8. package/dist/core/conditional-formatting-overlay.js +0 -11
  9. package/dist/core/conditional-formatting.js +0 -10
  10. package/dist/core/containers.js +0 -15
  11. package/dist/core/data-validation-overlay.js +0 -25
  12. package/dist/core/data-validation.js +0 -7
  13. package/dist/core/date.js +0 -39
  14. package/dist/core/formula.js +0 -117
  15. package/dist/core/grid-edits.js +0 -38
  16. package/dist/core/image.js +0 -25
  17. package/dist/core/internal.js +0 -27
  18. package/dist/core/limits.js +0 -45
  19. package/dist/core/merge.d.ts +18 -0
  20. package/dist/core/merge.js +22 -7
  21. package/dist/core/modern-functions.js +0 -17
  22. package/dist/core/page-setup.js +0 -5
  23. package/dist/core/pivot-table.d.ts +1 -1
  24. package/dist/core/pivot-table.js +0 -48
  25. package/dist/core/preserved.js +0 -4
  26. package/dist/core/protection.js +0 -26
  27. package/dist/core/range.js +0 -112
  28. package/dist/core/row-input.d.ts +24 -0
  29. package/dist/core/row-input.js +29 -0
  30. package/dist/core/row.js +0 -66
  31. package/dist/core/style.js +0 -26
  32. package/dist/core/table-style.js +0 -28
  33. package/dist/core/table.js +0 -97
  34. package/dist/core/text-metrics.js +0 -40
  35. package/dist/core/theme.js +0 -85
  36. package/dist/core/value.js +0 -94
  37. package/dist/core/workbook-protection.js +0 -13
  38. package/dist/core/workbook.js +0 -380
  39. package/dist/core/worksheet-model.js +2 -23
  40. package/dist/core/worksheet.js +7 -506
  41. package/dist/customui/errors.js +0 -10
  42. package/dist/customui/index.js +0 -5
  43. package/dist/customui/ribbon.js +0 -43
  44. package/dist/entries/core.js +0 -6
  45. package/dist/entries/csv.js +0 -4
  46. package/dist/entries/customui.js +0 -2
  47. package/dist/entries/errors.js +0 -13
  48. package/dist/entries/vba.js +0 -5
  49. package/dist/entries/xlsb.js +0 -6
  50. package/dist/entries/xlsx.js +0 -13
  51. package/dist/errors.d.ts +14 -0
  52. package/dist/errors.js +5 -56
  53. package/dist/index.js +0 -12
  54. package/dist/io/csv/read.js +0 -18
  55. package/dist/io/csv/write.js +0 -18
  56. package/dist/io/opc/errors.js +0 -30
  57. package/dist/io/opc/inflate.js +0 -35
  58. package/dist/io/opc/namespaces.js +0 -15
  59. package/dist/io/opc/part-paths.js +0 -11
  60. package/dist/io/opc/read-opc.js +0 -31
  61. package/dist/io/opc/read-options.js +0 -6
  62. package/dist/io/opc/rels.js +0 -13
  63. package/dist/io/opc/sniff-format.js +0 -50
  64. package/dist/io/style/xf-style.js +0 -31
  65. package/dist/io/xlsb/errors.js +0 -10
  66. package/dist/io/xlsb/formula.js +1 -99
  67. package/dist/io/xlsb/primitives.js +1 -83
  68. package/dist/io/xlsb/ptg-functions.js +0 -34
  69. package/dist/io/xlsb/read-shared-strings.js +0 -7
  70. package/dist/io/xlsb/read-styles.js +3 -66
  71. package/dist/io/xlsb/read-worksheet.js +4 -87
  72. package/dist/io/xlsb/read.js +2 -60
  73. package/dist/io/xlsb/record-stream.js +0 -28
  74. package/dist/io/xlsb/record-types.js +0 -19
  75. package/dist/io/xlsx/cell-accumulator.js +0 -32
  76. package/dist/io/xlsx/cell-value.js +0 -29
  77. package/dist/io/xlsx/color-xml.d.ts +6 -0
  78. package/dist/io/xlsx/color-xml.js +42 -0
  79. package/dist/io/xlsx/comments.js +0 -84
  80. package/dist/io/xlsx/conditional-formatting.d.ts +1 -1
  81. package/dist/io/xlsx/conditional-formatting.js +1 -97
  82. package/dist/io/xlsx/data-validation.js +0 -56
  83. package/dist/io/xlsx/edit-vba.js +0 -54
  84. package/dist/io/xlsx/errors.js +0 -12
  85. package/dist/io/xlsx/hyperlinks.js +0 -40
  86. package/dist/io/xlsx/images.js +0 -32
  87. package/dist/io/xlsx/namespaces.js +0 -42
  88. package/dist/io/xlsx/package-plan.js +0 -47
  89. package/dist/io/xlsx/pivot.js +0 -22
  90. package/dist/io/xlsx/{pivot-read.js → read-pivot.js} +0 -30
  91. package/dist/io/xlsx/read-rows.js +1 -83
  92. package/dist/io/xlsx/{shared-strings-read.js → read-shared-strings.js} +0 -11
  93. package/dist/io/xlsx/read-styles.d.ts +25 -1
  94. package/dist/io/xlsx/read-styles.js +61 -108
  95. package/dist/io/xlsx/read-worksheet.js +1 -88
  96. package/dist/io/xlsx/read.js +3 -211
  97. package/dist/io/xlsx/relationships.js +0 -11
  98. package/dist/io/xlsx/rich-runs.js +0 -16
  99. package/dist/io/xlsx/rich-text.js +0 -12
  100. package/dist/io/xlsx/shared-formulas.js +0 -7
  101. package/dist/io/xlsx/shared-strings.js +0 -21
  102. package/dist/io/xlsx/sheet-properties.js +1 -64
  103. package/dist/io/xlsx/styles.d.ts +1 -29
  104. package/dist/io/xlsx/styles.js +2 -366
  105. package/dist/io/xlsx/tables.js +5 -52
  106. package/dist/io/xlsx/threaded-comments.js +0 -103
  107. package/dist/io/xlsx/workbook-xml.js +0 -94
  108. package/dist/io/xlsx/worksheet-xml.js +0 -174
  109. package/dist/io/xlsx/write-stream.js +0 -137
  110. package/dist/io/xlsx/write.js +1 -134
  111. package/dist/io/xlsx/x14-ext.js +0 -8
  112. package/dist/vba/cfb-writer.js +12 -59
  113. package/dist/vba/cfb.js +2 -32
  114. package/dist/vba/codepage.js +1 -23
  115. package/dist/vba/errors.js +0 -15
  116. package/dist/vba/index.js +0 -10
  117. package/dist/vba/ms-ovba.js +3 -46
  118. package/dist/vba/project-editor.js +4 -89
  119. package/dist/vba/project.js +2 -34
  120. package/dist/vba/vba-encoding.js +1 -17
  121. package/dist/xml/errors.js +0 -10
  122. package/dist/xml/xml-read.js +3 -82
  123. package/dist/xml/xml.js +0 -39
  124. package/package.json +8 -9
  125. package/skills/ts-xlsx-upstream/SKILL.md +178 -0
  126. /package/dist/io/xlsx/{pivot-read.d.ts → read-pivot.d.ts} +0 -0
  127. /package/dist/io/xlsx/{shared-strings-read.d.ts → read-shared-strings.d.ts} +0 -0
@@ -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')
@@ -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;
@@ -1,22 +1,3 @@
1
- // Streaming row reader: yield a worksheet's rows one at a time, without ever building the whole
2
- // {@link Workbook} model.
3
- //
4
- // `readXlsx` materialises every cell of every sheet as a live `Cell` object held in nested Maps —
5
- // fine for editing, but for a large sheet read purely to extract its data it holds the entire grid
6
- // in memory at once. This reader instead *pulls* the sheet's XML through `xmlEvents` and yields a
7
- // plain {@link StreamedRow} at each `</row>`, retaining only the row currently in hand. Peak model
8
- // memory is one row, not the sheet.
9
- //
10
- // Two entry points sit on the same scanner:
11
- // - {@link readSheetRows} streams a single selected sheet's rows (the terse data-extraction case).
12
- // - {@link readWorkbookStream} yields a {@link StreamedSheet} per worksheet in workbook order, so a
13
- // caller can walk every sheet — each sheet's rows still stream one at a time.
14
- //
15
- // Scope of this slice: the package is still inflated whole (bounded by the running counter in
16
- // `./inflate.ts`) and shared strings / styles are read as whole parts — both are legitimately
17
- // document-sized and cheap. What this avoids is retaining N materialised cells. A later slice can
18
- // make the inflate itself per-part lazy; the pull primitive this stands on (`xmlEvents`) is the
19
- // same one that path will use.
20
1
  import { MAX_COLUMN } from '../../core/address.js';
21
2
  import { AuthoringError } from '../../errors.js';
22
3
  import { boolStrict, closeEmptyElements, localName, xmlEvents } from '../../xml/xml-read.js';
@@ -25,50 +6,18 @@ import { inflateSpreadsheetPackage, unsupportedWorkbookPart } from '../opc/sniff
25
6
  import { CellAccumulator } from './cell-accumulator.js';
26
7
  import { XlsxParseError } from './errors.js';
27
8
  import { DEFAULT_MAX_UNCOMPRESSED, parseRelationships, parseStyleTable, parseWorkbookSheets, resolveWorkbookPart, } from './read.js';
28
- import { parseSharedStrings } from './shared-strings-read.js';
29
- /**
30
- * Stream a worksheet's rows from an `.xlsx` package, yielding each in sheet order without building
31
- * the workbook model. Only rows the sheet actually declares are yielded, and within a row only its
32
- * non-empty cells — a blank or style-only cell contributes nothing, matching the intent of a data
33
- * read.
34
- *
35
- * @param data The raw `.xlsx` bytes.
36
- * @param options Sheet selector and the inflate bound (see {@link ReadSheetRowsOptions}).
37
- * @throws {UnsupportedFormatError} if the input is not a readable `.xlsx` package (a legacy `.xls`, a
38
- * binary `.xlsb`, or an unrecognised/non-ZIP blob — branch on `.format`).
39
- * @throws {PackageReadError} if the input is a ZIP that cannot be unpacked — a corrupt or
40
- * truncated archive, or one exceeding the inflate bound (a probable zip bomb).
41
- * @throws {XlsxParseError} if the package's workbook part declares no worksheets.
42
- * @throws {RangeError} / {@link AuthoringError} if `options.sheet` selects a position, or a name,
43
- * that no worksheet has.
44
- */
9
+ import { parseSharedStrings } from './read-shared-strings.js';
45
10
  export function* readSheetRows(data, options = {}) {
46
11
  const pkg = openPackage(data, options.maxUncompressedBytes);
47
12
  const chosen = pickSheet(pkg.sheets, options.sheet);
48
13
  const sheetXml = pkg.sheetXml(chosen.relId);
49
- // The sheet is named but its part is missing (a truncated or foreign package) — it has no rows.
50
14
  if (sheetXml === undefined)
51
15
  return;
52
16
  yield* scanSheet(sheetXml, pkg.sharedStrings, pkg.xfStyles, new Set(), []);
53
17
  }
54
- /**
55
- * Stream every worksheet of an `.xlsx` package in workbook order, without building the workbook
56
- * model. Each yielded {@link StreamedSheet} carries the declared sheet name and lets the caller
57
- * stream that sheet's rows and read its hidden-column and merge summaries — the streaming analogue
58
- * of walking `readXlsx(data).worksheets`.
59
- *
60
- * @param data The raw `.xlsx` bytes.
61
- * @param options The inflate bound (see {@link ReadXlsxOptions}).
62
- * @throws {UnsupportedFormatError} if the input is not a readable `.xlsx` package (a legacy `.xls`, a
63
- * binary `.xlsb`, or an unrecognised/non-ZIP blob — branch on `.format`).
64
- * @throws {PackageReadError} if the input is a ZIP that cannot be unpacked — a corrupt or
65
- * truncated archive, or one exceeding the inflate bound (a probable zip bomb).
66
- */
67
18
  export function* readWorkbookStream(data, options = {}) {
68
19
  const pkg = openPackage(data, options.maxUncompressedBytes);
69
20
  for (const sheet of pkg.sheets) {
70
- // A named sheet whose part is missing (truncated/foreign package) still surfaces — with no rows,
71
- // no hidden columns, and no merges — rather than vanishing from the workbook's sheet list.
72
21
  const xml = pkg.sheetXml(sheet.relId) ?? '';
73
22
  yield new StreamedSheetReader(sheet.name, xml, pkg.sharedStrings, pkg.xfStyles);
74
23
  }
@@ -77,9 +26,6 @@ function openPackage(data, maxUncompressedBytes) {
77
26
  const cap = maxUncompressedBytes ?? DEFAULT_MAX_UNCOMPRESSED;
78
27
  const { partText: text } = packageAccessors(inflateSpreadsheetPackage(data, cap));
79
28
  const workbookXml = text('xl/workbook.xml');
80
- // A binary `.xlsb` is a workbook this library *can* read — just not through here. Row streaming is
81
- // built on the XML worksheet parser, so the binary cell table has no streaming path yet; say so,
82
- // rather than reporting the format as unreadable when `readXlsx` would take the very same bytes.
83
29
  if (workbookXml === undefined) {
84
30
  throw unsupportedWorkbookPart(text, 'the binary .xlsb format (BIFF12) cannot be row-streamed yet; read it with readXlsx or readXlsb');
85
31
  }
@@ -115,10 +61,6 @@ function pickSheet(sheets, selector) {
115
61
  throw new AuthoringError(`no worksheet named ${JSON.stringify(selector)}`);
116
62
  return sheet;
117
63
  }
118
- // A single worksheet exposed by readWorkbookStream. Its rows() re-scans on each call (a fresh pass,
119
- // so it is safely re-iterable); the hidden-column and merge accessors reuse a completed scan or, if
120
- // the rows were never drained, drive one of their own. The hidden/merge state is filled in by the
121
- // same scanSheet pass that yields the rows.
122
64
  class StreamedSheetReader {
123
65
  name;
124
66
  #xml;
@@ -148,37 +90,19 @@ class StreamedSheetReader {
148
90
  this.#ensureScanned();
149
91
  return this.#merges;
150
92
  }
151
- // Drain a scan purely for its summaries when the caller reads them without (or before) iterating
152
- // rows. A completed row iteration already set #scanned, so this re-scans nothing in the common
153
- // streaming idiom.
154
93
  #ensureScanned() {
155
94
  if (this.#scanned)
156
95
  return;
157
96
  for (const _row of this.rows()) {
158
- // The rows themselves are irrelevant here — we only want the hidden/merge side effects.
159
97
  }
160
98
  }
161
99
  }
162
- // A formatted-but-empty `<c/>` is expanded to open+close so it finalises once on close, matching
163
- // the buffered reader; the text-bearing `<f/>`/`<v/>`/`<t/>` are excluded so an empty one never
164
- // commits (their close captures text, which an empty tag has none of).
165
100
  const CELL_EMPTY_CLOSE = new Set(['c']);
166
- // Pull the sheet XML through the event stream, yielding a StreamedRow at each `</row>`, while
167
- // recording the sheet's hidden columns (from `<col hidden>`, before <sheetData>) and merged ranges
168
- // (from `<mergeCells>`, after <sheetData>) into the caller-supplied collectors. The cell state
169
- // mirrors the buffered reader's `parseWorksheet` (same self-closing-`<c/>` handling, same capture
170
- // flags), but commits into a row buffer that is handed off and discarded per row rather than into a
171
- // persistent Worksheet — that hand-off is what bounds retained memory to one row.
172
101
  function* scanSheet(xml, sharedStrings, xfStyles, hiddenColumns, merges) {
173
102
  let rowNumber = 0;
174
103
  let lastRow = 0;
175
104
  let rowHidden = false;
176
105
  let cells = [];
177
- // The in-flight `<c>`, gathered exactly as the buffered reader gathers it. This reader drives the
178
- // same beginCell/setFormula/setValue/appendText methods, then takes only the cell's plain decoded
179
- // value (via decode) — never the shared-formula / data-table resolution the buffered finalize adds,
180
- // which a data read does not want. Rich `<r>` runs are deliberately not opened here, so a rich
181
- // inline string flattens to its concatenated text as a streamed value always has.
182
106
  const cell = new CellAccumulator();
183
107
  let inInlineString = false;
184
108
  let capture = false;
@@ -188,8 +112,6 @@ function* scanSheet(xml, sharedStrings, xfStyles, hiddenColumns, merges) {
188
112
  return;
189
113
  const style = cell.styleIndex >= 0 ? xfStyles[cell.styleIndex] : undefined;
190
114
  const value = cell.decode(sharedStrings, style);
191
- // A blank or purely style-only cell decodes to null; a data read wants only cells that carry
192
- // something (a formula object, an empty string, a false, and a 0 all count — only null drops).
193
115
  if (value !== null) {
194
116
  const { col, ref } = cell;
195
117
  cells.push(style ? { col, address: ref, value, style } : { col, address: ref, value });
@@ -243,7 +165,6 @@ function* scanSheet(xml, sharedStrings, xfStyles, hiddenColumns, merges) {
243
165
  capture = false;
244
166
  continue;
245
167
  }
246
- // close
247
168
  const local = localName(event.name);
248
169
  switch (local) {
249
170
  case 'f':
@@ -270,9 +191,6 @@ function* scanSheet(xml, sharedStrings, xfStyles, hiddenColumns, merges) {
270
191
  capture = false;
271
192
  }
272
193
  }
273
- // Record the hidden columns a `<col min max hidden>` element declares. The span is clamped to the
274
- // format's column ceiling and gathered into a Set, so even a hostile file full of full-width hidden
275
- // spans can add at most MAX_COLUMN distinct entries — never an unbounded allocation.
276
194
  function collectHiddenColumn(attrs, hiddenColumns) {
277
195
  if (attrs.hidden !== '1' && attrs.hidden !== 'true')
278
196
  return;
@@ -1,19 +1,9 @@
1
- // Parser for the shared-string table (`xl/sharedStrings.xml`), the pool that `t="s"` cells index into.
2
- // Split out of read.ts beside its sibling parsers (read-styles.ts, rich-runs.ts) so read.ts stays
3
- // orchestration; the run structure it decodes is owned by RunAccumulator, shared with inline strings.
4
1
  import { localName, parseXml } from '../../xml/xml-read.js';
5
2
  import { RunAccumulator } from './rich-runs.js';
6
- // Shared strings resolve `t="s"` cells. Each `<si>` is one entry: a plain `<si><t>…</t>` decodes to a
7
- // string, while a rich `<si><r><rPr>…</rPr><t>…</t></r>…` decodes to a {@link RichTextValue} whose runs
8
- // carry their per-run fonts — so rich text Excel pooled reads back formatted, not flattened to text.
9
- // The run structure inside an `<si>` is identical to an inline string's `<is>`, so it is parsed the
10
- // same way (see the inline-run accumulation in `parseWorksheet`).
11
3
  export function parseSharedStrings(xml) {
12
4
  if (xml === '')
13
5
  return [];
14
6
  const strings = [];
15
- // Per-`<si>` accumulation: `plain` gathers a bare `<t>`; `runs` gathers `<r>` runs. An `<si>` is
16
- // rich the moment it holds one `<r>`, at which point its runs — not `plain` — become the entry.
17
7
  let plain = '';
18
8
  const runs = new RunAccumulator();
19
9
  let isRich = false;
@@ -52,7 +42,6 @@ export function parseSharedStrings(xml) {
52
42
  const local = localName(name);
53
43
  switch (local) {
54
44
  case 't':
55
- // A `<t>` inside a run is that run's text; a bare `<t>` directly in the `<si>` is plain.
56
45
  if (!runs.appendText(text))
57
46
  plain += text;
58
47
  capture = false;
@@ -1,4 +1,4 @@
1
- import { type Font } from '../../core/style.ts';
1
+ import { type Font, type TableStyleTable } from '../../core/style.ts';
2
2
  import { type XmlAttributes } from '../../xml/xml-read.ts';
3
3
  import { type StyleTable } from '../style/xf-style.ts';
4
4
  export type FontDraft = {
@@ -6,3 +6,27 @@ export type FontDraft = {
6
6
  };
7
7
  export declare function parseStyleTable(xml: string): StyleTable;
8
8
  export declare function applyFontChild(draft: FontDraft, local: string, attrs: XmlAttributes): void;
9
+ /**
10
+ * Extract the custom indexed-color palette (`<colors><indexedColors>`) from styles.xml as verbatim
11
+ * `<rgbColor rgb="…"/>` fragments, or an empty list when the file rides the default palette. Kept raw
12
+ * — rather than parsed into RGB and re-serialised — so the exact entries (count, order, casing) a
13
+ * source file declared survive a round-trip and every `indexed="…"` reference keeps its RGB.
14
+ */
15
+ export declare function parseIndexedColors(stylesXml: string): string[];
16
+ /**
17
+ * Extract the most-recently-used colour swatches (`<colors><mruColors>`) from styles.xml as verbatim
18
+ * `<color .../>` fragments, or an empty list when the file declares none. Kept raw for the same reason
19
+ * the indexed palette is: the list is the author's own working set of colours and the model has no
20
+ * use for its contents, only for not losing them.
21
+ */
22
+ export declare function parseMruColors(stylesXml: string): string[];
23
+ /**
24
+ * Extract the `<tableStyles>` block from styles.xml: each `<tableStyle>` definition verbatim, plus the
25
+ * container's nominated `defaultTableStyle`/`defaultPivotStyle`. See {@link TableStyleTable} for why
26
+ * the definitions stay raw while the two names are decoded.
27
+ *
28
+ * A file with no such block — or with the self-closing `count="0"` container Excel writes when it has
29
+ * only defaults to state — yields an empty {@link TableStyleTable.styles} and whichever names it did
30
+ * carry.
31
+ */
32
+ export declare function parseTableStyles(stylesXml: string): TableStyleTable;