@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,24 +1,9 @@
1
- // The style-table reader: `xl/styles.xml` in, a flat table of resolved cell formats out. It is a
2
- // single streaming pass over the shared sub-tables (`<numFmts>`, `<fills>`, `<fonts>`, `<borders>`)
3
- // and the two xf tables (`<cellXfs>`, `<cellStyleXfs>`), flattening the id-indirection so a cell's
4
- // `s` index maps straight to its facets. A construct it does not recognise is skipped, never guessed.
5
- //
6
- // Only the parsing is XML-specific. What an xf resolves *to* — `XfStyle`, the built-in number
7
- // formats, applying an xf to a cell — is a property of the OOXML style model rather than of its
8
- // spelling, and lives above both codecs in `../style/xf-style.ts`; the `.xlsb` style reader builds
9
- // the same table from BIFF12 records.
10
1
  import { assignStyleFacets, isBorderStyle, isFillPatternType, isFontScheme, isFontVerticalAlignment, isHorizontalAlignment, isNamedUnderlineStyle, isVerticalAlignment, } from '../../core/style.js';
11
- import { boolPresent, boolStrict, closeEmptyElements, localName, xmlEvents, } from '../../xml/xml-read.js';
2
+ import { boolPresent, boolStrict, closeEmptyElements, localName, openElements, xmlEvents, } from '../../xml/xml-read.js';
12
3
  import { numFmtCodeFor } from '../style/xf-style.js';
13
- import { parseColor } from './styles.js';
14
- // The four sides plus the diagonal — the edge elements a <border> can hold, in the order the
15
- // schema lists them. This one tuple drives the edge-name union, the membership set (which drives
16
- // edge parsing without a per-name branch), and the "does any edge carry a style" scan below.
4
+ import { parseColor } from './color-xml.js';
17
5
  const BORDER_EDGE_NAMES = ['left', 'right', 'top', 'bottom', 'diagonal'];
18
6
  const BORDER_EDGES = new Set(BORDER_EDGE_NAMES);
19
- // Style-table elements that commit on their close: a bare <font/>/<border/>/<patternFill/>/
20
- // <gradientFill/>/<xf/> or a self-closing border edge is expanded to open+close so each commits
21
- // exactly once in onClose, never in a duplicated (and easily-forgotten) self-closing branch.
22
7
  const STYLE_EMPTY_CLOSES = new Set([
23
8
  'font',
24
9
  'border',
@@ -27,11 +12,6 @@ const STYLE_EMPTY_CLOSES = new Set([
27
12
  'xf',
28
13
  ...BORDER_EDGES,
29
14
  ]);
30
- // styles.xml is a shared table: <numFmts> defines custom format codes by id, <fills> lists
31
- // the fills, and <cellXfs> lists the cell formats, each naming a fill and a number format by
32
- // id. We flatten that indirection into one array — cellXfs index → resolved {fill, numFmt} —
33
- // so a cell/row/column style index maps straight to its facets. The schema orders <numFmts>
34
- // and <fills> before <cellXfs>, so both lookups are complete before an xf references them.
35
15
  export function parseStyleTable(xml) {
36
16
  if (xml === '')
37
17
  return { cellXfs: [], namedStyles: [] };
@@ -40,16 +20,8 @@ export function parseStyleTable(xml) {
40
20
  let borders = [];
41
21
  let numFmtCodes = new Map();
42
22
  const xfStyles = [];
43
- // The named-style layer: <cellStyleXfs> holds the base formats a cell's xfId links to; <cellStyles>
44
- // labels them by name/builtinId. Parsed in parallel with cellXfs, then zipped and merged below.
45
23
  const namedXfs = [];
46
24
  let cellStyleNames = [];
47
- // One streaming pass, but each top-level sub-table drives its own focused sub-parser over the slice
48
- // of events between its open and close. The schema orders the shared tables (<numFmts>, <fonts>,
49
- // <fills>, <borders>) before the xf tables, so their results are complete before an <xf> resolves
50
- // against them. Every recognised container name is plural and unique to the styleSheet root — none
51
- // appears inside a <dxf>'s singular <font>/<fill>/<border> children — so skipping an unrecognised
52
- // section here drops exactly what the old flat pass gated off with its `in*` flags.
53
25
  const events = closeEmptyElements(xmlEvents(xml), STYLE_EMPTY_CLOSES);
54
26
  let next = events.next();
55
27
  while (next.done !== true) {
@@ -81,18 +53,12 @@ export function parseStyleTable(xml) {
81
53
  }
82
54
  next = events.next();
83
55
  }
84
- // Layer each cellXfs entry over the named style its xfId links to: a facet the direct format sets
85
- // wins; one it leaves unset falls through to the named style. The xfId is carried through so the
86
- // link survives a re-write. A draft only holds keys for facets it actually set, so the spread merge
87
- // takes the named base and lets the direct entry override exactly what it names.
88
56
  const cellXfs = xfStyles.map((xf) => {
89
57
  if (xf.xfId === undefined)
90
58
  return xf;
91
59
  const named = namedXfs[xf.xfId];
92
60
  return named === undefined ? xf : { ...named, ...xf };
93
61
  });
94
- // Zip the resolved cellStyleXfs facets with their cellStyles name/builtinId into the model's named
95
- // styles, index for index (a cellStyle's xfId is its cellStyleXfs index).
96
62
  const namedStyles = namedXfs.map((xf, index) => {
97
63
  const label = cellStyleNames.find((entry) => entry.xfId === index);
98
64
  const style = {};
@@ -103,15 +69,9 @@ export function parseStyleTable(xml) {
103
69
  style.builtinId = label.builtinId;
104
70
  return style;
105
71
  });
106
- // Font 0 is the workbook's declared default, so it is carried out whole as well as flattened onto
107
- // the xfs that name it — see {@link StyleTable.defaultFont}.
108
72
  const defaultFont = fonts[0];
109
73
  return defaultFont === undefined ? { cellXfs, namedStyles } : { cellXfs, namedStyles, defaultFont };
110
74
  }
111
- // Pull events off the shared stream up to — and consuming — the close of `container`, yielding only
112
- // those strictly inside it. A sub-table parser loops this to completion (never breaking), so it drives
113
- // its own small state machine over exactly its section without closing the underlying generator, and
114
- // the outer pass resumes at the element after the container's close.
115
75
  function* until(events, container) {
116
76
  let next = events.next();
117
77
  while (next.done !== true) {
@@ -122,8 +82,6 @@ function* until(events, container) {
122
82
  next = events.next();
123
83
  }
124
84
  }
125
- // <numFmts> entries are self-closing, so they are read on open. A code with no id, or the General
126
- // id 0, contributes nothing.
127
85
  function parseNumFmts(events) {
128
86
  const codes = new Map();
129
87
  for (const event of until(events, 'numFmts')) {
@@ -142,7 +100,6 @@ function parseFonts(events) {
142
100
  for (const event of until(events, 'fonts')) {
143
101
  if (event.kind === 'open') {
144
102
  const local = localName(event.name);
145
- // A <font>'s children are self-closing, so they are read here on open.
146
103
  if (local === 'font')
147
104
  fontDraft = {};
148
105
  else if (fontDraft !== null)
@@ -160,9 +117,6 @@ function parseFills(events) {
160
117
  let pattern = '';
161
118
  let fgColor;
162
119
  let bgColor;
163
- // A gradient fill accumulates from <gradientFill> open to close; its stops fill in as <stop>/<color>
164
- // pairs arrive. `fillSlotAt` marks where in `fills` the current <fill> began, so its close can keep a
165
- // slot even when the fill body was neither a pattern nor a gradient — index alignment is load-bearing.
166
120
  let gradientDraft = null;
167
121
  let fillSlotAt = -1;
168
122
  for (const event of until(events, 'fills')) {
@@ -170,9 +124,6 @@ function parseFills(events) {
170
124
  const attrs = event.attrs;
171
125
  switch (localName(event.name)) {
172
126
  case 'fill':
173
- // Mark where this <fill> starts so its close can guarantee exactly one slot — a fill body
174
- // that is neither <patternFill> nor <gradientFill> (or a gradient we could not parse) must
175
- // still consume an id, or every later fill index shifts and cells mis-resolve their fill.
176
127
  fillSlotAt = fills.length;
177
128
  break;
178
129
  case 'patternFill':
@@ -206,7 +157,6 @@ function parseFills(events) {
206
157
  }
207
158
  break;
208
159
  case 'color':
209
- // The colour of the open <stop>; committed to a GradientStop when the stop closes.
210
160
  if (gradientDraft !== null)
211
161
  gradientDraft.stopColor = parseColor(attrs);
212
162
  break;
@@ -235,8 +185,6 @@ function parseFills(events) {
235
185
  }
236
186
  break;
237
187
  case 'fill':
238
- // Backstop the slot: if this <fill>'s body pushed nothing (unparsed/unknown content), keep an
239
- // empty slot so id alignment holds and later fills still resolve to the right cells.
240
188
  if (fills.length === fillSlotAt)
241
189
  fills.push(undefined);
242
190
  break;
@@ -248,7 +196,6 @@ function parseFills(events) {
248
196
  function parseBorders(events) {
249
197
  const borders = [];
250
198
  let borderDraft = null;
251
- // Which edge of the current border a nested <color> belongs to; null between edges.
252
199
  let currentEdge = null;
253
200
  for (const event of until(events, 'borders')) {
254
201
  if (event.kind === 'open') {
@@ -263,9 +210,6 @@ function parseBorders(events) {
263
210
  borderDraft.diagonalDown = true;
264
211
  }
265
212
  else if (borderDraft !== null) {
266
- // A border's edges and their <color> children are all read on open (each is self-closing bar a
267
- // coloured edge, whose colour child is itself self-closing). An edge whose style is absent or
268
- // an unrecognised token is dropped — the side simply carries no border.
269
213
  if (BORDER_EDGES.has(local)) {
270
214
  if (attrs.style !== undefined && isBorderStyle(attrs.style)) {
271
215
  currentEdge = local;
@@ -292,39 +236,28 @@ function parseBorders(events) {
292
236
  }
293
237
  }
294
238
  else if (borderDraft !== null && BORDER_EDGES.has(local)) {
295
- // A coloured edge closes after its <color> child; drop the edge context so a stray later
296
- // <color> cannot attach to it.
297
239
  currentEdge = null;
298
240
  }
299
241
  }
300
242
  }
301
243
  return borders;
302
244
  }
303
- // Both <cellXfs> and <cellStyleXfs> hold <xf> with identical structure; they differ only in which
304
- // table the result lands in and whether an xfId link is meaningful (only cellXfs entries link to a
305
- // named style). One parser serves both, told by `container` which it is reading.
306
245
  function parseXfTable(events, container, deps) {
307
246
  const xfs = [];
308
247
  const captureXfId = container === 'cellXfs';
309
- // The xf being read; held from open to close so its <alignment>/<protection> children can attach
310
- // before it is committed. null outside an <xf>.
311
248
  let pendingXf = null;
312
249
  for (const event of until(events, container)) {
313
250
  if (event.kind === 'open') {
314
251
  const local = localName(event.name);
315
252
  if (local === 'xf') {
316
- // Hold the xf open until its close so an <alignment>/<protection> child can attach first; a
317
- // self-closing <xf/> is expanded to a close, so it commits there too, child-free.
318
253
  pendingXf = resolveXf(event.attrs, deps, captureXfId);
319
254
  }
320
255
  else if (pendingXf !== null && local === 'alignment') {
321
- // An xf's <alignment> child arrives before the xf closes; attach it to the pending xf.
322
256
  const alignment = parseAlignment(event.attrs);
323
257
  if (alignment !== undefined)
324
258
  pendingXf.alignment = alignment;
325
259
  }
326
260
  else if (pendingXf !== null && local === 'protection') {
327
- // An xf's <protection> child likewise arrives before the xf closes.
328
261
  const protection = parseProtection(event.attrs);
329
262
  if (protection !== undefined)
330
263
  pendingXf.protection = protection;
@@ -337,18 +270,12 @@ function parseXfTable(events, container, deps) {
337
270
  }
338
271
  return xfs;
339
272
  }
340
- // Resolve an <xf>'s facet ids against the shared sub-tables into a draft. `captureXfId` is set only
341
- // for cellXfs entries, the sole table whose xfId links to a named style.
342
273
  function resolveXf(attrs, deps, captureXfId) {
343
274
  const fillId = Number(attrs.fillId);
344
275
  const fill = Number.isInteger(fillId) ? deps.fills[fillId] : undefined;
345
276
  const fontId = Number(attrs.fontId);
346
- // Font id 0 is the workbook default font (a real Calibri-11-style face), not an absence — unlike
347
- // border id 0, which is a genuinely empty border. So an xf naming font 0 resolves to that default
348
- // face, giving every cell a concrete font to render.
349
277
  const font = Number.isInteger(fontId) ? deps.fonts[fontId] : undefined;
350
278
  const borderId = Number(attrs.borderId);
351
- // Border id 0 is the empty default; only a custom border (id > 0) is an explicit one.
352
279
  const border = Number.isInteger(borderId) && borderId > 0 ? deps.borders[borderId] : undefined;
353
280
  const numFmt = resolveNumFmt(attrs.numFmtId, deps.numFmtCodes);
354
281
  const draft = {};
@@ -360,12 +287,8 @@ function resolveXf(attrs, deps, captureXfId) {
360
287
  draft.font = font;
361
288
  if (border)
362
289
  draft.border = border;
363
- // The quote-prefix flag is an attribute on the xf itself (no shared sub-table); carry it only when
364
- // set so an ordinary cell does not gain a spurious `quotePrefix: false`.
365
290
  if (boolStrict(attrs.quotePrefix))
366
291
  draft.quotePrefix = true;
367
- // A cellXfs entry's xfId links it to a named style; capture it only when it points beyond the Normal
368
- // default (0), so an ordinary cell carries no spurious named-style link.
369
292
  if (captureXfId && attrs.xfId !== undefined) {
370
293
  const xfId = Number(attrs.xfId);
371
294
  if (Number.isInteger(xfId) && xfId > 0)
@@ -373,8 +296,6 @@ function resolveXf(attrs, deps, captureXfId) {
373
296
  }
374
297
  return draft;
375
298
  }
376
- // A <cellStyle> (inside <cellStyles>) names a cellStyleXfs entry by xfId; it is self-closing, so it
377
- // is read on open.
378
299
  function parseCellStyles(events) {
379
300
  const names = [];
380
301
  for (const event of until(events, 'cellStyles')) {
@@ -396,9 +317,6 @@ function parseCellStyles(events) {
396
317
  }
397
318
  return names;
398
319
  }
399
- // A <font> child element sets one facet on the draft. Boolean flags honour their `val`: a
400
- // bare tag or val="1"/"true" is on, val="0"/"false" is off (an explicit-false flag is not
401
- // truthy merely because the tag is present). An unrecognised child is ignored.
402
320
  export function applyFontChild(draft, local, attrs) {
403
321
  switch (local) {
404
322
  case 'b':
@@ -414,10 +332,6 @@ export function applyFontChild(draft, local, attrs) {
414
332
  draft.outline = boolPresent(attrs.val);
415
333
  break;
416
334
  case 'u':
417
- // A bare <u/> is a single underline; a named style (single/double/…) carries through; but
418
- // val="none" is the explicit ABSENCE of an underline, so it must read back falsy — not the
419
- // truthy string "none" that a consumer's `if (font.underline)` would mistake for underlined. An
420
- // unrecognised token keeps the "is underlined" fact but drops the unknown style (a plain true).
421
335
  draft.underline =
422
336
  attrs.val === undefined
423
337
  ? true
@@ -440,7 +354,6 @@ export function applyFontChild(draft, local, attrs) {
440
354
  case 'color':
441
355
  draft.color = parseColor(attrs);
442
356
  break;
443
- // `<name>` in a styles `<font>`, `<rFont>` in a rich-text run's `<rPr>` — the same font face.
444
357
  case 'name':
445
358
  case 'rFont':
446
359
  if (attrs.val !== undefined)
@@ -466,15 +379,10 @@ export function applyFontChild(draft, local, attrs) {
466
379
  break;
467
380
  }
468
381
  }
469
- // An xf's numFmtId resolves against the custom codes first, then the built-in table; the
470
- // General format (id 0) and any unrecognised id mean the cell carries no explicit format.
471
382
  function resolveNumFmt(raw, custom) {
472
383
  return raw === undefined ? undefined : numFmtCodeFor(Number(raw), custom);
473
384
  }
474
385
  function toFill(pattern, fgColor, bgColor) {
475
- // `none` (and an absent patternType) is the absence of a fill; an unrecognised token is dropped the
476
- // same way — like the border-edge style above — so a foreign pattern we do not model leaves the cell
477
- // unfilled rather than propagating a token the writer would later re-emit unvalidated.
478
386
  if (!isFillPatternType(pattern) || pattern === 'none')
479
387
  return undefined;
480
388
  return {
@@ -484,8 +392,6 @@ function toFill(pattern, fgColor, bgColor) {
484
392
  ...(bgColor ? { bgColor } : {}),
485
393
  };
486
394
  }
487
- // Copy the numeric <gradientFill> attributes (degree; the path insets) onto a gradient draft, keeping
488
- // only the finite ones so an absent or malformed attribute leaves the field its OOXML default (unset).
489
395
  function assignGradientNumbers(fill, attrs) {
490
396
  for (const key of ['degree', 'left', 'right', 'top', 'bottom']) {
491
397
  const value = Number(attrs[key]);
@@ -493,22 +399,14 @@ function assignGradientNumbers(fill, attrs) {
493
399
  fill[key] = value;
494
400
  }
495
401
  }
496
- // An accumulated border with no styled edge and no diagonal direction is the empty default:
497
- // it carries nothing, so it resolves to undefined rather than an all-empty Border object.
498
402
  function borderToStyle(draft) {
499
403
  const hasEdge = BORDER_EDGE_NAMES.some((edge) => draft[edge] !== undefined);
500
404
  if (!hasEdge && draft.diagonalUp === undefined && draft.diagonalDown === undefined)
501
405
  return undefined;
502
406
  return draft;
503
407
  }
504
- // Read an <alignment> element's attributes into an Alignment, keeping only facets that differ
505
- // from the default. Boolean flags honour their parsed value — wrapText="0" is off, so it must
506
- // not fabricate a { wrapText: false } alignment — and an element carrying only defaults yields
507
- // undefined rather than an empty alignment object.
508
408
  function parseAlignment(attrs) {
509
409
  const out = {};
510
- // `general` is the default and reads back as no explicit horizontal alignment; an unrecognised
511
- // token (like an out-of-enum vertical one) is dropped rather than trusted into the model.
512
410
  if (attrs.horizontal !== undefined &&
513
411
  attrs.horizontal !== 'general' &&
514
412
  isHorizontalAlignment(attrs.horizontal)) {
@@ -537,10 +435,6 @@ function parseAlignment(attrs) {
537
435
  }
538
436
  return Object.keys(out).length > 0 ? out : undefined;
539
437
  }
540
- // Read a <protection> element into a Protection, keeping only facets that differ from the OOXML
541
- // default. `locked` defaults to TRUE, so only an explicit `locked="0"` carries information (an
542
- // unlocked cell) — a default or explicit-true cell must not read back as { locked: true }; `hidden`
543
- // defaults to false, so only `hidden="1"` is carried. An element with only defaults yields undefined.
544
438
  function parseProtection(attrs) {
545
439
  const out = {};
546
440
  if (attrs.locked === '0' || attrs.locked === 'false')
@@ -549,3 +443,62 @@ function parseProtection(attrs) {
549
443
  out.hidden = true;
550
444
  return Object.keys(out).length > 0 ? out : undefined;
551
445
  }
446
+ export function parseIndexedColors(stylesXml) {
447
+ return elementFragments(stylesXml, 'indexedColors', 'rgbColor');
448
+ }
449
+ export function parseMruColors(stylesXml) {
450
+ return elementFragments(stylesXml, 'mruColors', 'color');
451
+ }
452
+ export function parseTableStyles(stylesXml) {
453
+ const styles = elementFragments(stylesXml, 'tableStyles', 'tableStyle');
454
+ const table = { styles };
455
+ for (const { attrs } of openElements(stylesXml, 'tableStyles')) {
456
+ if (attrs.defaultTableStyle !== undefined)
457
+ table.defaultTableStyle = attrs.defaultTableStyle;
458
+ if (attrs.defaultPivotStyle !== undefined)
459
+ table.defaultPivotStyle = attrs.defaultPivotStyle;
460
+ break;
461
+ }
462
+ const namespaces = fragmentNamespaces(stylesXml, styles);
463
+ if (namespaces.length > 0)
464
+ table.namespaces = namespaces;
465
+ return table;
466
+ }
467
+ function fragmentNamespaces(stylesXml, fragments) {
468
+ if (fragments.length === 0)
469
+ return [];
470
+ const declared = new Map();
471
+ const ignorable = new Set();
472
+ for (const { attrs } of openElements(stylesXml, 'styleSheet')) {
473
+ for (const [name, value] of Object.entries(attrs)) {
474
+ if (name.startsWith('xmlns:'))
475
+ declared.set(name.slice('xmlns:'.length), value);
476
+ }
477
+ for (const prefix of (attrs['mc:Ignorable'] ?? '').split(/\s+/)) {
478
+ if (prefix !== '')
479
+ ignorable.add(prefix);
480
+ }
481
+ break;
482
+ }
483
+ const used = new Set();
484
+ for (const fragment of fragments) {
485
+ for (const match of fragment.matchAll(/[\s</]([A-Za-z_][\w.-]*):[A-Za-z_]/g)) {
486
+ used.add(match[1]);
487
+ }
488
+ }
489
+ return [...used]
490
+ .filter((prefix) => declared.has(prefix))
491
+ .map((prefix) => ({
492
+ prefix,
493
+ uri: declared.get(prefix),
494
+ ignorable: ignorable.has(prefix),
495
+ }));
496
+ }
497
+ function elementFragments(xml, container, child) {
498
+ const block = new RegExp(`<${container}\\b[^>]*>([\\s\\S]*?)</${container}>`).exec(xml);
499
+ if (block === null)
500
+ return [];
501
+ const inner = block[1] ?? '';
502
+ const pattern = new RegExp(`<${child}\\b[^>]*/>|<${child}\\b[^>]*>[\\s\\S]*?</${child}>`, 'g');
503
+ return [...inner.matchAll(pattern)].map((m) => m[0] ?? '');
504
+ }