@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.
Files changed (136) 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.d.ts +10 -0
  5. package/dist/core/cell.js +4 -123
  6. package/dist/core/color-resolution.js +0 -60
  7. package/dist/core/column.d.ts +9 -1
  8. package/dist/core/column.js +0 -52
  9. package/dist/core/comment-thread.js +0 -45
  10. package/dist/core/conditional-formatting-overlay.js +0 -11
  11. package/dist/core/conditional-formatting.js +0 -10
  12. package/dist/core/containers.js +0 -15
  13. package/dist/core/data-validation-overlay.js +0 -25
  14. package/dist/core/data-validation.js +0 -7
  15. package/dist/core/date.js +0 -39
  16. package/dist/core/formula.js +0 -117
  17. package/dist/core/grid-edits.js +0 -38
  18. package/dist/core/image.js +0 -25
  19. package/dist/core/internal.js +0 -27
  20. package/dist/core/limits.d.ts +30 -0
  21. package/dist/core/limits.js +2 -0
  22. package/dist/core/merge.d.ts +18 -0
  23. package/dist/core/merge.js +22 -7
  24. package/dist/core/modern-functions.js +0 -17
  25. package/dist/core/page-setup.js +0 -5
  26. package/dist/core/pivot-table.d.ts +1 -1
  27. package/dist/core/pivot-table.js +0 -44
  28. package/dist/core/preserved.js +0 -4
  29. package/dist/core/protection.js +0 -26
  30. package/dist/core/range.js +0 -112
  31. package/dist/core/row-input.d.ts +24 -0
  32. package/dist/core/row-input.js +29 -0
  33. package/dist/core/row.d.ts +9 -1
  34. package/dist/core/row.js +0 -58
  35. package/dist/core/style.js +0 -26
  36. package/dist/core/table-style.js +0 -28
  37. package/dist/core/table.js +0 -97
  38. package/dist/core/text-metrics.d.ts +20 -0
  39. package/dist/core/text-metrics.js +10 -0
  40. package/dist/core/theme.js +0 -85
  41. package/dist/core/value.d.ts +40 -0
  42. package/dist/core/value.js +30 -48
  43. package/dist/core/workbook-protection.js +0 -13
  44. package/dist/core/workbook.d.ts +13 -0
  45. package/dist/core/workbook.js +11 -368
  46. package/dist/core/worksheet-model.js +2 -23
  47. package/dist/core/worksheet.d.ts +22 -0
  48. package/dist/core/worksheet.js +14 -485
  49. package/dist/customui/errors.js +0 -10
  50. package/dist/customui/index.js +0 -5
  51. package/dist/customui/ribbon.js +0 -43
  52. package/dist/entries/core.d.ts +3 -1
  53. package/dist/entries/core.js +3 -7
  54. package/dist/entries/csv.js +0 -4
  55. package/dist/entries/customui.js +0 -2
  56. package/dist/entries/errors.js +0 -13
  57. package/dist/entries/vba.js +0 -5
  58. package/dist/entries/xlsb.js +0 -6
  59. package/dist/entries/xlsx.js +0 -13
  60. package/dist/errors.d.ts +14 -0
  61. package/dist/errors.js +5 -56
  62. package/dist/index.js +0 -12
  63. package/dist/io/csv/read.js +0 -18
  64. package/dist/io/csv/write.js +5 -35
  65. package/dist/io/opc/errors.js +0 -30
  66. package/dist/io/opc/inflate.js +0 -35
  67. package/dist/io/opc/namespaces.js +0 -15
  68. package/dist/io/opc/part-paths.js +0 -11
  69. package/dist/io/opc/read-opc.js +0 -31
  70. package/dist/io/opc/read-options.js +0 -6
  71. package/dist/io/opc/rels.js +0 -13
  72. package/dist/io/opc/sniff-format.js +0 -50
  73. package/dist/io/style/xf-style.js +0 -31
  74. package/dist/io/xlsb/errors.js +0 -10
  75. package/dist/io/xlsb/formula.js +1 -99
  76. package/dist/io/xlsb/primitives.js +1 -83
  77. package/dist/io/xlsb/ptg-functions.js +0 -34
  78. package/dist/io/xlsb/read-shared-strings.js +0 -7
  79. package/dist/io/xlsb/read-styles.js +3 -66
  80. package/dist/io/xlsb/read-worksheet.js +4 -87
  81. package/dist/io/xlsb/read.js +2 -60
  82. package/dist/io/xlsb/record-stream.js +0 -28
  83. package/dist/io/xlsb/record-types.js +0 -19
  84. package/dist/io/xlsx/cell-accumulator.js +0 -32
  85. package/dist/io/xlsx/cell-value.js +0 -29
  86. package/dist/io/xlsx/color-xml.d.ts +6 -0
  87. package/dist/io/xlsx/color-xml.js +42 -0
  88. package/dist/io/xlsx/comments.js +0 -84
  89. package/dist/io/xlsx/conditional-formatting.d.ts +1 -1
  90. package/dist/io/xlsx/conditional-formatting.js +1 -97
  91. package/dist/io/xlsx/data-validation.js +0 -56
  92. package/dist/io/xlsx/edit-vba.js +0 -54
  93. package/dist/io/xlsx/errors.js +0 -12
  94. package/dist/io/xlsx/hyperlinks.js +0 -40
  95. package/dist/io/xlsx/images.js +0 -32
  96. package/dist/io/xlsx/namespaces.js +0 -42
  97. package/dist/io/xlsx/package-plan.js +0 -47
  98. package/dist/io/xlsx/pivot.js +0 -22
  99. package/dist/io/xlsx/{pivot-read.js → read-pivot.js} +0 -30
  100. package/dist/io/xlsx/read-rows.js +1 -83
  101. package/dist/io/xlsx/{shared-strings-read.js → read-shared-strings.js} +0 -11
  102. package/dist/io/xlsx/read-styles.d.ts +25 -1
  103. package/dist/io/xlsx/read-styles.js +61 -108
  104. package/dist/io/xlsx/read-worksheet.js +1 -88
  105. package/dist/io/xlsx/read.js +3 -211
  106. package/dist/io/xlsx/relationships.js +0 -11
  107. package/dist/io/xlsx/rich-runs.js +0 -16
  108. package/dist/io/xlsx/rich-text.js +0 -12
  109. package/dist/io/xlsx/shared-formulas.js +0 -7
  110. package/dist/io/xlsx/shared-strings.js +0 -21
  111. package/dist/io/xlsx/sheet-properties.js +1 -64
  112. package/dist/io/xlsx/styles.d.ts +1 -29
  113. package/dist/io/xlsx/styles.js +2 -366
  114. package/dist/io/xlsx/tables.js +5 -52
  115. package/dist/io/xlsx/threaded-comments.js +0 -103
  116. package/dist/io/xlsx/workbook-xml.js +0 -94
  117. package/dist/io/xlsx/worksheet-xml.js +0 -174
  118. package/dist/io/xlsx/write-stream.js +0 -137
  119. package/dist/io/xlsx/write.js +1 -134
  120. package/dist/io/xlsx/x14-ext.js +0 -8
  121. package/dist/vba/cfb-writer.js +12 -59
  122. package/dist/vba/cfb.js +2 -32
  123. package/dist/vba/codepage.js +1 -23
  124. package/dist/vba/errors.js +0 -15
  125. package/dist/vba/index.js +0 -10
  126. package/dist/vba/ms-ovba.js +3 -46
  127. package/dist/vba/project-editor.js +4 -89
  128. package/dist/vba/project.js +2 -34
  129. package/dist/vba/vba-encoding.js +1 -17
  130. package/dist/xml/errors.js +0 -10
  131. package/dist/xml/xml-read.js +3 -82
  132. package/dist/xml/xml.js +0 -39
  133. package/package.json +9 -10
  134. package/skills/ts-xlsx-upstream/SKILL.md +178 -0
  135. /package/dist/io/xlsx/{pivot-read.d.ts → read-pivot.d.ts} +0 -0
  136. /package/dist/io/xlsx/{shared-strings-read.d.ts → read-shared-strings.d.ts} +0 -0
@@ -1,18 +1,3 @@
1
- // Structurally edit an existing `.xlsm` at the *package* level: remove a module, or add a reference,
2
- // returning new package bytes with every other part preserved byte-for-byte. Only `xl/vbaProject.bin` is
3
- // rewritten (plus dropping a now-stale signature); worksheets, styles, drawings, and every other part
4
- // ride through untouched.
5
- //
6
- // This is the highest-fidelity way to tweak an existing macro project. The alternative — `readXlsx` →
7
- // the matching `Workbook` method → `writeXlsx` — rebuilds the whole package from the parsed model, which
8
- // re-serialises every part and so only preserves what the model captures. For a rich, real-world
9
- // workbook that round-trip can perturb parts Excel is strict about; splicing the original bytes cannot,
10
- // because it never re-authors anything but the macro project. Use this when the input is a real file
11
- // whose non-macro content must be preserved exactly.
12
- //
13
- // Authoring or editing module SOURCE is not here: Excel runs a module's compiled p-code, not its source,
14
- // so that needs the offline `tools/vba-compiler` (VBIDE), which can produce a whole edited `.xlsm`
15
- // directly (ADR 0019).
16
1
  import { strFromU8, strToU8, unzipSync, zipSync } from 'fflate';
17
2
  import { VbaAuthorError } from '../../vba/errors.js';
18
3
  import { addVbaReference, removeVbaModule, } from '../../vba/project-editor.js';
@@ -20,42 +5,14 @@ import { relsPathFor } from '../opc/part-paths.js';
20
5
  import { parseRelationshipRecords, relationshipTargetByType, resolveRelativePart, resolveWorkbookPart, } from '../opc/read-opc.js';
21
6
  const OFFICE_DOCUMENT_REL = 'officeDocument';
22
7
  const VBA_PROJECT_REL = 'vbaProject';
23
- // Every signature flavour Excel writes over a VBA project (legacy, agile, V3) shares this local-name
24
- // prefix; all become stale the instant the project's bytes change and must be dropped with it.
25
8
  const VBA_SIGNATURE_REL_INFIX = 'vbaProjectSignature';
26
- /**
27
- * Remove a standard module from an existing macro-enabled package's VBA project, returning new package
28
- * bytes. Every part but `xl/vbaProject.bin` is preserved byte-for-byte (see {@link removeVbaModule} for
29
- * what changes within it), and any digital signature over the old project is dropped because it cannot
30
- * validate the new bytes.
31
- *
32
- * @throws {VbaAuthorError} if the package carries no VBA project, `name` is not in the project, or names
33
- * a `document`/`designer` module.
34
- * @throws {VbaParseError} if the attached `vbaProject.bin` is malformed.
35
- */
36
9
  export function editXlsxVbaRemoveModule(xlsx, name) {
37
10
  return applyToVbaProjectPart(xlsx, (bin) => removeVbaModule(bin, name));
38
11
  }
39
- /**
40
- * Add a registered (COM type-library) reference to an existing macro-enabled package's VBA project,
41
- * returning new package bytes. Every part but `xl/vbaProject.bin` is preserved byte-for-byte (see
42
- * {@link addVbaReference} for what changes within it), and any digital signature over the old project is
43
- * dropped because it cannot validate the new bytes.
44
- *
45
- * @throws {VbaAuthorError} if the package carries no VBA project, or any field of `ref` is invalid (see
46
- * {@link VbaLibraryReference}).
47
- * @throws {VbaParseError} if the attached `vbaProject.bin` is malformed.
48
- */
49
12
  export function editXlsxVbaAddReference(xlsx, ref) {
50
13
  return applyToVbaProjectPart(xlsx, (bin) => addVbaReference(bin, ref));
51
14
  }
52
- // Shared plumbing for every package-level VBA edit: unzip, locate `xl/vbaProject.bin`, replace it with
53
- // whatever `apply` produces, drop a now-stale signature, and re-zip. `apply` is expected to validate
54
- // fail-closed itself (every project-editor primitive does), so a bad edit throws before `files` is
55
- // touched.
56
15
  function applyToVbaProjectPart(xlsx, apply) {
57
- // Widen off fflate's `Uint8Array<ArrayBuffer>` element type so spliced/re-serialised parts (whose
58
- // buffers are `ArrayBufferLike`) assign back into the map.
59
16
  const files = unzipSync(xlsx);
60
17
  const binPath = locateVbaProjectPart(files);
61
18
  const bin = binPath === undefined ? undefined : files[binPath];
@@ -66,9 +23,6 @@ function applyToVbaProjectPart(xlsx, apply) {
66
23
  dropStaleSignature(files, binPath);
67
24
  return zipSync(files);
68
25
  }
69
- // Resolve the package's `xl/vbaProject.bin` part the way the reader does: `_rels/.rels` → the
70
- // officeDocument (workbook) part → its `.rels` → the `vbaProject` relationship, each target resolved
71
- // relative to its referrer. undefined when the package declares no such relationship (a macro-free book).
72
26
  function locateVbaProjectPart(files) {
73
27
  const rootRels = textPart(files, '_rels/.rels');
74
28
  if (rootRels === undefined)
@@ -85,8 +39,6 @@ function locateVbaProjectPart(files) {
85
39
  return undefined;
86
40
  return resolveVbaTarget(workbookPath, vbaTarget);
87
41
  }
88
- // The workbook's vbaProject relationship uses a workbook-relative target (`vbaProject.bin`); resolve it
89
- // through the workbook-part rule so both that and an absolute `/xl/vbaProject.bin` land on the part path.
90
42
  function resolveVbaTarget(workbookPath, target) {
91
43
  if (target.startsWith('/'))
92
44
  return target.slice(1);
@@ -94,9 +46,6 @@ function resolveVbaTarget(workbookPath, target) {
94
46
  return resolveWorkbookPart(target);
95
47
  return resolveRelativePart(workbookPath, target);
96
48
  }
97
- // Editing the project invalidates any signature over it, so remove every signature part the project's
98
- // `.rels` reaches, the relationships that point at them, and their content-type overrides — leaving a
99
- // package that advertises no signature rather than a broken one (mirrors Workbook.vbaProjectBytes).
100
49
  function dropStaleSignature(files, binPath) {
101
50
  const binRelsPath = relsPathFor(binPath);
102
51
  const binRels = textPart(files, binRelsPath);
@@ -125,12 +74,9 @@ function textPart(files, path) {
125
74
  const bytes = files[path];
126
75
  return bytes === undefined ? undefined : strFromU8(bytes);
127
76
  }
128
- // Drop the `<Relationship>` element carrying a given Id, matching the element as a whole (self-closing
129
- // or paired) so attribute order does not matter.
130
77
  function removeRelationshipById(xml, id) {
131
78
  return xml.replace(/<Relationship\b[^>]*?\/>|<Relationship\b[\s\S]*?<\/Relationship>/g, (element) => (new RegExp(`\\bId="${escapeRegExp(id)}"`).test(element) ? '' : element));
132
79
  }
133
- // Drop the `<Override>` naming a given part path; PartName is the full, unambiguous package path.
134
80
  function removeContentTypeOverride(xml, partPath) {
135
81
  return xml.replace(/<Override\b[^>]*?\/>/g, (element) => element.includes(`PartName="/${partPath}"`) ? '' : element);
136
82
  }
@@ -1,16 +1,4 @@
1
1
  import { XlsxError } from '../../errors.js';
2
- /**
3
- * Thrown when an `.xlsx` package's XML content is well-formed but does not describe a workbook this
4
- * library can act on — a `xl/workbook.xml` that declares no worksheets, say.
5
- *
6
- * It sits between two neighbours. {@link XmlParseError} reports that the *markup* did not parse;
7
- * {@link UnsupportedFormatError} reports that the *container* is not one we read at all. This one
8
- * fires when both of those were fine and the document itself is nonetheless not a workbook.
9
- *
10
- * The reader is deliberately lenient about content it does not recognise — an unknown element is
11
- * skipped, not fatal — so this is rare by design, and reaching it means something a workbook cannot
12
- * do without being corrupt.
13
- */
14
2
  export class XlsxParseError extends XlsxError {
15
3
  name = 'XlsxParseError';
16
4
  code = 'malformed-input';
@@ -1,28 +1,13 @@
1
- // Cell hyperlinks — the sheet-level `<hyperlinks>` element, its external relationships, and the
2
- // reader that folds a link back onto its cell's value.
3
- //
4
- // A hyperlink is not stored inside the cell in OOXML: the `<c>` holds only the visible label (a
5
- // normal string value), while a separate `<hyperlink>` child of `<worksheet>` ties an A1 reference
6
- // to a destination. An EXTERNAL destination (a URL) is reached indirectly, through a sheet
7
- // relationship carrying `TargetMode="External"` that the `<hyperlink>` names by `r:id`. An INTERNAL
8
- // destination (a location inside the same workbook, which the author writes as a `#`-prefixed value)
9
- // is held directly in a `location` attribute with NO relationship — emitting an internal target as an
10
- // external relationship makes a strict consumer resolve both the rel and the location and render the
11
- // destination doubled.
12
1
  import { decodeRange } from '../../core/address.js';
13
2
  import { isHyperlinkValue, isRichTextValue } from '../../core/value.js';
14
3
  import { escapeAttr } from '../../xml/xml.js';
15
4
  import { localName, parseXml } from '../../xml/xml-read.js';
16
- /** Gather every hyperlink cell on a sheet, in row-major order. */
17
5
  export function collectHyperlinks(sheet) {
18
6
  const links = [];
19
7
  for (const { cells } of sheet.rows()) {
20
8
  for (const cell of cells) {
21
9
  const value = cell.value;
22
10
  if (isHyperlinkValue(value)) {
23
- // A link that spans a range carries its extent in `range`; the anchor cell (this one) is the
24
- // range's top-left. Emit that extent as `ref` so the clickable area survives, falling back to
25
- // the single cell for an ordinary link.
26
11
  links.push({
27
12
  ref: value.range ?? cell.address,
28
13
  target: value.hyperlink,
@@ -33,22 +18,15 @@ export function collectHyperlinks(sheet) {
33
18
  }
34
19
  return links;
35
20
  }
36
- /** Split collected links into internal (location, no rel) and external (relationship) forms, drawing
37
- * each external link's relationship id from the sheet's allocator so external ids follow every other
38
- * sheet-local relationship in canonical order. An internal ('#'-prefixed) link consumes no id. */
39
21
  export function planHyperlinks(links, rels) {
40
22
  return links.map((link) => {
41
23
  const tooltip = link.tooltip !== undefined ? { tooltip: link.tooltip } : {};
42
- // A '#'-prefixed target is an internal document location: held verbatim in `location`, with no
43
- // relationship. Everything else is an external URL reached through a relationship.
44
24
  if (link.target.startsWith('#')) {
45
25
  return { ref: link.ref, location: link.target.slice(1), ...tooltip };
46
26
  }
47
27
  return { ref: link.ref, relId: rels.next(), target: link.target, ...tooltip };
48
28
  });
49
29
  }
50
- /** The `<hyperlinks>` element, or '' when the sheet has none. Attribute order follows CT_Hyperlink:
51
- * `ref`, `r:id`, `location`, `tooltip`. */
52
30
  export function hyperlinksXml(links) {
53
31
  if (links.length === 0)
54
32
  return '';
@@ -62,7 +40,6 @@ export function hyperlinksXml(links) {
62
40
  .join('');
63
41
  return `<hyperlinks>${items}</hyperlinks>`;
64
42
  }
65
- /** Parse every `<hyperlink>` element out of a worksheet part. */
66
43
  export function parseSheetHyperlinks(xml) {
67
44
  const links = [];
68
45
  parseXml(xml, {
@@ -82,26 +59,17 @@ export function parseSheetHyperlinks(xml) {
82
59
  });
83
60
  return links;
84
61
  }
85
- /** Fold parsed hyperlinks onto a sheet's cells, wrapping each cell's existing value (its visible
86
- * label) into a {@link HyperlinkValue}. `rels` maps a relationship id to its target URL. */
87
62
  export function applyHyperlinks(sheet, links, rels) {
88
63
  for (const link of links) {
89
64
  const target = resolveTarget(link, rels);
90
65
  if (target === undefined)
91
66
  continue;
92
- // A hyperlink may span a range (`ref="D1:H1"`); Excel anchors the link at the range's top-left
93
- // cell. Decode once so a multi-cell link folds onto that anchor rather than asking the sheet for
94
- // a range address it cannot resolve. A ref that does not decode is skipped, not fatal.
95
67
  const decoded = decodeRefSafe(link.ref);
96
68
  if (decoded === undefined)
97
69
  continue;
98
70
  const cell = sheet.getCell(decoded.tl.address);
99
- // The visible label is the cell's own value: a plain string, or rich text when the label
100
- // carried per-run formatting. Any other value kind has no textual label, so it reads as empty.
101
71
  const cellValue = cell.value;
102
72
  const text = typeof cellValue === 'string' ? cellValue : isRichTextValue(cellValue) ? cellValue : '';
103
- // Record the extent only when the link genuinely spans more than the anchor, so an ordinary
104
- // single-cell link stays a plain value and the range survives verbatim for a multi-cell one.
105
73
  const spansRange = decoded.tl.address !== decoded.br.address;
106
74
  const value = {
107
75
  hyperlink: target,
@@ -112,10 +80,6 @@ export function applyHyperlinks(sheet, links, rels) {
112
80
  cell.value = value;
113
81
  }
114
82
  }
115
- // A hyperlink's `ref` decoded to a range (a single-cell ref decodes to a range whose corners
116
- // coincide), so the caller can both anchor on the top-left and tell whether the link spans further.
117
- // Returns undefined for a ref that does not decode, so a malformed hyperlink is dropped rather than
118
- // crashing the load.
119
83
  function decodeRefSafe(ref) {
120
84
  try {
121
85
  return decodeRange(ref);
@@ -129,11 +93,7 @@ function resolveTarget(link, rels) {
129
93
  const base = rels.get(link.rid);
130
94
  if (base === undefined)
131
95
  return undefined;
132
- // A foreign file may split an external URL's fragment into the `location` attribute, apart from
133
- // the relationship Target; rejoin them so the whole URL survives. Our own writer keeps the
134
- // fragment in the Target, so a link we wrote never carries both.
135
96
  return link.location !== undefined ? `${base}#${link.location}` : base;
136
97
  }
137
- // No relationship: an internal ('#'-prefixed) target held verbatim in `location`.
138
98
  return link.location !== undefined ? `#${link.location}` : undefined;
139
99
  }
@@ -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;