@shbernal/ts-xlsx 2.0.0 → 2.1.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 (106) hide show
  1. package/README.md +20 -10
  2. package/dist/bytes.d.ts +24 -0
  3. package/dist/bytes.js +34 -0
  4. package/dist/core/address.d.ts +26 -4
  5. package/dist/core/address.js +21 -9
  6. package/dist/core/autofilter.d.ts +11 -0
  7. package/dist/core/autofilter.js +35 -10
  8. package/dist/core/color-resolution.d.ts +1 -1
  9. package/dist/core/color-resolution.js +7 -8
  10. package/dist/core/conditional-formatting-overlay.d.ts +5 -0
  11. package/dist/core/conditional-formatting-overlay.js +11 -0
  12. package/dist/core/conditional-formatting.d.ts +45 -5
  13. package/dist/core/conditional-formatting.js +88 -0
  14. package/dist/core/data-validation-overlay.d.ts +11 -0
  15. package/dist/core/data-validation-overlay.js +21 -2
  16. package/dist/core/data-validation.d.ts +9 -2
  17. package/dist/core/data-validation.js +34 -0
  18. package/dist/core/grid-edits.d.ts +16 -0
  19. package/dist/core/grid-edits.js +26 -8
  20. package/dist/core/grid-shift.d.ts +19 -0
  21. package/dist/core/grid-shift.js +8 -0
  22. package/dist/core/image.d.ts +2 -0
  23. package/dist/core/image.js +4 -0
  24. package/dist/core/merge.d.ts +12 -1
  25. package/dist/core/merge.js +36 -2
  26. package/dist/core/page-setup.d.ts +16 -2
  27. package/dist/core/page-setup.js +8 -1
  28. package/dist/core/pivot-table.d.ts +2 -0
  29. package/dist/core/pivot-table.js +25 -16
  30. package/dist/core/protection.d.ts +3 -0
  31. package/dist/core/protection.js +20 -14
  32. package/dist/core/row-input.d.ts +7 -0
  33. package/dist/core/row-input.js +10 -8
  34. package/dist/core/style.d.ts +27 -0
  35. package/dist/core/style.js +82 -67
  36. package/dist/core/table.d.ts +25 -11
  37. package/dist/core/table.js +67 -31
  38. package/dist/core/theme.d.ts +3 -1
  39. package/dist/core/theme.js +4 -0
  40. package/dist/core/workbook.d.ts +2 -2
  41. package/dist/core/worksheet-comments.d.ts +8 -0
  42. package/dist/core/worksheet-comments.js +18 -0
  43. package/dist/core/worksheet.d.ts +15 -2
  44. package/dist/core/worksheet.js +28 -48
  45. package/dist/customui/ribbon.js +24 -23
  46. package/dist/entries/core.d.ts +3 -3
  47. package/dist/entries/csv.d.ts +1 -1
  48. package/dist/entries/node-unavailable.d.ts +15 -0
  49. package/dist/entries/node-unavailable.js +20 -0
  50. package/dist/entries/node.d.ts +1 -0
  51. package/dist/entries/node.js +1 -0
  52. package/dist/entries/xlsx.d.ts +0 -1
  53. package/dist/entries/xlsx.js +0 -1
  54. package/dist/io/csv/read.js +1 -1
  55. package/dist/io/csv/write.d.ts +12 -2
  56. package/dist/io/csv/write.js +17 -2
  57. package/dist/io/opc/inflate.js +2 -12
  58. package/dist/io/opc/rels.d.ts +11 -0
  59. package/dist/io/opc/rels.js +3 -2
  60. package/dist/io/style/xf-style.d.ts +7 -2
  61. package/dist/io/style/xf-style.js +6 -1
  62. package/dist/io/xlsx/cell-accumulator.d.ts +1 -3
  63. package/dist/io/xlsx/cell-accumulator.js +20 -58
  64. package/dist/io/xlsx/cell-value.d.ts +1 -1
  65. package/dist/io/xlsx/cell-value.js +7 -4
  66. package/dist/io/xlsx/color-xml.d.ts +1 -1
  67. package/dist/io/xlsx/color-xml.js +7 -6
  68. package/dist/io/xlsx/comments.js +4 -2
  69. package/dist/io/xlsx/conditional-formatting.d.ts +1 -1
  70. package/dist/io/xlsx/conditional-formatting.js +60 -45
  71. package/dist/io/xlsx/data-validation.d.ts +1 -1
  72. package/dist/io/xlsx/data-validation.js +35 -33
  73. package/dist/io/xlsx/hyperlinks.js +6 -14
  74. package/dist/io/xlsx/images.js +19 -22
  75. package/dist/io/xlsx/read-pivot.js +2 -8
  76. package/dist/io/xlsx/read-shared-strings.js +8 -47
  77. package/dist/io/xlsx/read-styles.js +2 -1
  78. package/dist/io/xlsx/read-worksheet.js +14 -16
  79. package/dist/io/xlsx/read.d.ts +1 -1
  80. package/dist/io/xlsx/read.js +44 -74
  81. package/dist/io/xlsx/rich-runs.d.ts +41 -7
  82. package/dist/io/xlsx/rich-runs.js +94 -30
  83. package/dist/io/xlsx/sheet-properties.d.ts +1 -1
  84. package/dist/io/xlsx/sheet-properties.js +13 -9
  85. package/dist/io/xlsx/styles.d.ts +3 -15
  86. package/dist/io/xlsx/styles.js +63 -88
  87. package/dist/io/xlsx/tables.js +13 -13
  88. package/dist/io/xlsx/theme-xml.js +7 -4
  89. package/dist/io/xlsx/threaded-comments.js +19 -19
  90. package/dist/io/xlsx/workbook-xml.js +14 -8
  91. package/dist/io/xlsx/worksheet-xml.js +34 -37
  92. package/dist/io/xlsx/write-stream.d.ts +8 -1
  93. package/dist/io/xlsx/write-stream.js +10 -11
  94. package/dist/sha512.d.ts +2 -0
  95. package/dist/sha512.js +146 -0
  96. package/dist/vba/bytes.d.ts +1 -2
  97. package/dist/vba/bytes.js +1 -12
  98. package/dist/vba/dir-records.d.ts +53 -0
  99. package/dist/vba/dir-records.js +28 -0
  100. package/dist/vba/project-editor.js +5 -34
  101. package/dist/vba/project.js +2 -18
  102. package/dist/xml/xml-read.d.ts +60 -0
  103. package/dist/xml/xml-read.js +47 -0
  104. package/dist/xml/xml.d.ts +43 -1
  105. package/dist/xml/xml.js +17 -5
  106. package/package.json +21 -4
@@ -1,8 +1,7 @@
1
1
  import { isBorderStyle, isFillPatternType, isFontScheme, isFontVerticalAlignment, isHorizontalAlignment, isNamedUnderlineStyle, isVerticalAlignment, } from '../../core/style.js';
2
2
  import { TABLE_STYLE_ELEMENT_TYPES } from '../../core/table-style.js';
3
- import { AuthoringError } from '../../errors.js';
4
3
  import { decodeEntities } from '../../xml/xml-read.js';
5
- import { assertRepresentable, escapeAttr, XML_DECLARATION } from '../../xml/xml.js';
4
+ import { assertRepresentable, checkedToken, escapeAttr, numberText, XML_DECLARATION, } from '../../xml/xml.js';
6
5
  import { colorAttrs } from './color-xml.js';
7
6
  import { MARKUP_COMPATIBILITY_NS, SPREADSHEETML_NS } from './namespaces.js';
8
7
  const RESERVED_FILL_COUNT = 2;
@@ -21,6 +20,36 @@ const DEFAULT_FORMAT = {
21
20
  quotePrefix: false,
22
21
  xfId: 0,
23
22
  };
23
+ class InternTable {
24
+ #entries = [];
25
+ #idByKey = new Map();
26
+ #base;
27
+ constructor(base) {
28
+ this.#base = base;
29
+ }
30
+ get entries() {
31
+ return this.#entries;
32
+ }
33
+ get size() {
34
+ return this.#entries.length;
35
+ }
36
+ intern(key, entry) {
37
+ const existing = this.#idByKey.get(key);
38
+ if (existing !== undefined)
39
+ return existing;
40
+ const id = this.#base + this.#entries.length;
41
+ this.#entries.push(entry);
42
+ this.#idByKey.set(key, id);
43
+ return id;
44
+ }
45
+ seed(entry) {
46
+ const id = this.#base + this.#entries.length;
47
+ this.#entries.push(entry);
48
+ if (!this.#idByKey.has(entry))
49
+ this.#idByKey.set(entry, id);
50
+ return id;
51
+ }
52
+ }
24
53
  export class StyleRegistry {
25
54
  #defaultFontBody;
26
55
  #font0Bodies;
@@ -31,22 +60,17 @@ export class StyleRegistry {
31
60
  ? [this.#defaultFontBody]
32
61
  : [this.#defaultFontBody, fontXml(options.declaredDefaultFont)]);
33
62
  }
34
- #fillXml = [];
35
- #fillIdBySignature = new Map();
36
- #numFmtCodes = [];
37
- #numFmtIdByCode = new Map();
38
- #fontXml = [];
39
- #fontIdBySignature = new Map();
40
- #borderXml = [];
41
- #borderIdBySignature = new Map();
63
+ #fills = new InternTable(RESERVED_FILL_COUNT);
64
+ #numFmts = new InternTable(CUSTOM_NUMFMT_BASE);
65
+ #fonts = new InternTable(RESERVED_FONT_COUNT);
66
+ #borders = new InternTable(RESERVED_BORDER_COUNT);
42
67
  #formats = [DEFAULT_FORMAT];
43
68
  #xfIndexBySignature = new Map();
44
69
  #cellStyleXfs = [DEFAULT_FORMAT];
45
70
  #cellStyleNames = [
46
71
  { name: 'Normal', builtinId: 0, xfId: 0 },
47
72
  ];
48
- #dxfXml = [];
49
- #dxfIndexByFragment = new Map();
73
+ #dxfs = new InternTable(0);
50
74
  #indexedColors = [];
51
75
  #mruColors = [];
52
76
  #tableStyles = { styles: [] };
@@ -94,12 +118,8 @@ export class StyleRegistry {
94
118
  });
95
119
  }
96
120
  seedDifferentialStyles(fragments) {
97
- for (const fragment of fragments) {
98
- const index = this.#dxfXml.length;
99
- this.#dxfXml.push(fragment);
100
- if (!this.#dxfIndexByFragment.has(fragment))
101
- this.#dxfIndexByFragment.set(fragment, index);
102
- }
121
+ for (const fragment of fragments)
122
+ this.#dxfs.seed(fragment);
103
123
  }
104
124
  seedIndexedColors(fragments) {
105
125
  this.#indexedColors.length = 0;
@@ -128,69 +148,36 @@ export class StyleRegistry {
128
148
  }
129
149
  differentialStyleId(style) {
130
150
  const fragment = dxfXml(style);
131
- let index = this.#dxfIndexByFragment.get(fragment);
132
- if (index === undefined) {
133
- index = this.#dxfXml.length;
134
- this.#dxfXml.push(fragment);
135
- this.#dxfIndexByFragment.set(fragment, index);
136
- }
137
- return index;
151
+ return this.#dxfs.intern(fragment, fragment);
138
152
  }
139
153
  #internFill(fill) {
140
- const signature = fillSignature(fill);
141
- let id = this.#fillIdBySignature.get(signature);
142
- if (id === undefined) {
143
- id = RESERVED_FILL_COUNT + this.#fillXml.length;
144
- this.#fillXml.push(patternFillXml(fill, { solidBgFallback: true }));
145
- this.#fillIdBySignature.set(signature, id);
146
- }
147
- return id;
154
+ return this.#fills.intern(fillSignature(fill), patternFillXml(fill, { solidBgFallback: true }));
148
155
  }
149
156
  #internNumFmt(code) {
150
- let id = this.#numFmtIdByCode.get(code);
151
- if (id === undefined) {
152
- id = CUSTOM_NUMFMT_BASE + this.#numFmtCodes.length;
153
- this.#numFmtCodes.push(code);
154
- this.#numFmtIdByCode.set(code, id);
155
- }
156
- return id;
157
+ return this.#numFmts.intern(code, code);
157
158
  }
158
159
  #internFont(font) {
159
160
  const xml = fontXml(font);
160
161
  if (xml === '' || this.#font0Bodies.has(xml))
161
162
  return 0;
162
- let id = this.#fontIdBySignature.get(xml);
163
- if (id === undefined) {
164
- id = RESERVED_FONT_COUNT + this.#fontXml.length;
165
- this.#fontXml.push(`<font>${xml}</font>`);
166
- this.#fontIdBySignature.set(xml, id);
167
- }
168
- return id;
163
+ return this.#fonts.intern(xml, `<font>${xml}</font>`);
169
164
  }
170
165
  #internBorder(border) {
171
166
  const xml = borderXml(border);
172
- if (xml === DEFAULT_BORDER)
173
- return 0;
174
- let id = this.#borderIdBySignature.get(xml);
175
- if (id === undefined) {
176
- id = RESERVED_BORDER_COUNT + this.#borderXml.length;
177
- this.#borderXml.push(xml);
178
- this.#borderIdBySignature.set(xml, id);
179
- }
180
- return id;
167
+ return xml === DEFAULT_BORDER ? 0 : this.#borders.intern(xml, xml);
181
168
  }
182
169
  toXml() {
183
- const fillCount = RESERVED_FILL_COUNT + this.#fillXml.length;
170
+ const fillCount = RESERVED_FILL_COUNT + this.#fills.size;
184
171
  const fills = '<fill><patternFill patternType="none"/></fill>' +
185
172
  '<fill><patternFill patternType="gray125"/></fill>' +
186
- this.#fillXml.join('');
173
+ this.#fills.entries.join('');
187
174
  const cellXfs = this.#formats.map((format) => xfXml(format, format.xfId)).join('');
188
175
  const cellStyleXfs = this.#cellStyleXfs.map((format) => xfXml(format, null)).join('');
189
176
  const cellStyles = this.#cellStyleNames.map(cellStyleTag).join('');
190
- const fontCount = RESERVED_FONT_COUNT + this.#fontXml.length;
191
- const fonts = `<font>${this.#defaultFontBody}</font>${this.#fontXml.join('')}`;
192
- const borderCount = RESERVED_BORDER_COUNT + this.#borderXml.length;
193
- const borders = DEFAULT_BORDER + this.#borderXml.join('');
177
+ const fontCount = RESERVED_FONT_COUNT + this.#fonts.size;
178
+ const fonts = `<font>${this.#defaultFontBody}</font>${this.#fonts.entries.join('')}`;
179
+ const borderCount = RESERVED_BORDER_COUNT + this.#borders.size;
180
+ const borders = DEFAULT_BORDER + this.#borders.entries.join('');
194
181
  return (XML_DECLARATION +
195
182
  `<styleSheet xmlns="${SPREADSHEETML_NS}"${this.#foreignNamespaceAttrs()}>` +
196
183
  this.#numFmtsXml() +
@@ -245,17 +232,17 @@ export class StyleRegistry {
245
232
  return `<colors>${indexed}${mru}</colors>`;
246
233
  }
247
234
  #dxfsXml() {
248
- if (this.#dxfXml.length === 0)
235
+ if (this.#dxfs.size === 0)
249
236
  return '<dxfs count="0"/>';
250
- return `<dxfs count="${this.#dxfXml.length}">${this.#dxfXml.join('')}</dxfs>`;
237
+ return `<dxfs count="${this.#dxfs.size}">${this.#dxfs.entries.join('')}</dxfs>`;
251
238
  }
252
239
  #numFmtsXml() {
253
- if (this.#numFmtCodes.length === 0)
240
+ if (this.#numFmts.size === 0)
254
241
  return '';
255
- const entries = this.#numFmtCodes
242
+ const entries = this.#numFmts.entries
256
243
  .map((code, i) => `<numFmt numFmtId="${CUSTOM_NUMFMT_BASE + i}" formatCode="${escapeFormatCode(code)}"/>`)
257
244
  .join('');
258
- return `<numFmts count="${this.#numFmtCodes.length}">${entries}</numFmts>`;
245
+ return `<numFmts count="${this.#numFmts.size}">${entries}</numFmts>`;
259
246
  }
260
247
  }
261
248
  function isDefaultFormat(format) {
@@ -292,12 +279,6 @@ function cellStyleTag(entry) {
292
279
  const builtin = entry.builtinId === undefined ? '' : ` builtinId="${entry.builtinId}"`;
293
280
  return `<cellStyle name="${escapeAttr(entry.name)}" xfId="${entry.xfId}"${builtin}/>`;
294
281
  }
295
- function checkedToken(value, isValid, kind) {
296
- if (!isValid(value)) {
297
- throw new AuthoringError(`Invalid ${kind} ${JSON.stringify(value)}: not a value the OOXML enumeration allows`);
298
- }
299
- return value;
300
- }
301
282
  function alignmentAttrs(alignment) {
302
283
  const parts = [];
303
284
  if (alignment.horizontal !== undefined && alignment.horizontal !== 'general') {
@@ -307,17 +288,17 @@ function alignmentAttrs(alignment) {
307
288
  parts.push(`vertical="${checkedToken(alignment.vertical, isVerticalAlignment, 'vertical alignment')}"`);
308
289
  }
309
290
  if (alignment.textRotation !== undefined && alignment.textRotation !== 0) {
310
- parts.push(`textRotation="${numberAttr(alignment.textRotation)}"`);
291
+ parts.push(`textRotation="${numberText(alignment.textRotation)}"`);
311
292
  }
312
293
  if (alignment.wrapText)
313
294
  parts.push('wrapText="1"');
314
295
  if (alignment.indent !== undefined && alignment.indent !== 0) {
315
- parts.push(`indent="${numberAttr(alignment.indent)}"`);
296
+ parts.push(`indent="${numberText(alignment.indent)}"`);
316
297
  }
317
298
  if (alignment.shrinkToFit)
318
299
  parts.push('shrinkToFit="1"');
319
300
  if (alignment.readingOrder !== undefined && alignment.readingOrder !== 0) {
320
- parts.push(`readingOrder="${numberAttr(alignment.readingOrder)}"`);
301
+ parts.push(`readingOrder="${numberText(alignment.readingOrder)}"`);
321
302
  }
322
303
  return parts.join(' ');
323
304
  }
@@ -349,15 +330,15 @@ export function fontXml(font, nameTag = 'name') {
349
330
  parts.push(`<vertAlign val="${checkedToken(font.vertAlign, isFontVerticalAlignment, 'font vertical alignment')}"/>`);
350
331
  }
351
332
  if (font.size !== undefined)
352
- parts.push(`<sz val="${numberAttr(font.size)}"/>`);
333
+ parts.push(`<sz val="${numberText(font.size)}"/>`);
353
334
  if (font.color !== undefined)
354
335
  parts.push(`<color ${colorAttrs(font.color)}/>`);
355
336
  if (font.name !== undefined)
356
337
  parts.push(`<${nameTag} val="${escapeAttr(font.name)}"/>`);
357
338
  if (font.family !== undefined)
358
- parts.push(`<family val="${numberAttr(font.family)}"/>`);
339
+ parts.push(`<family val="${numberText(font.family)}"/>`);
359
340
  if (font.charset !== undefined)
360
- parts.push(`<charset val="${numberAttr(font.charset)}"/>`);
341
+ parts.push(`<charset val="${numberText(font.charset)}"/>`);
361
342
  if (font.scheme !== undefined && font.scheme !== 'none')
362
343
  parts.push(`<scheme val="${checkedToken(font.scheme, isFontScheme, 'font scheme')}"/>`);
363
344
  return parts.join('');
@@ -380,18 +361,18 @@ function dxfXml(style) {
380
361
  }
381
362
  function gradientFillXml(fill) {
382
363
  const attrs = (fill.gradient === 'path' ? ' type="path"' : '') +
383
- (fill.degree ? ` degree="${numberAttr(fill.degree)}"` : '') +
364
+ (fill.degree ? ` degree="${numberText(fill.degree)}"` : '') +
384
365
  insetAttr('left', fill.left) +
385
366
  insetAttr('right', fill.right) +
386
367
  insetAttr('top', fill.top) +
387
368
  insetAttr('bottom', fill.bottom);
388
369
  const stops = fill.stops
389
- .map((stop) => `<stop position="${numberAttr(stop.position)}"><color ${colorAttrs(stop.color)}/></stop>`)
370
+ .map((stop) => `<stop position="${numberText(stop.position)}"><color ${colorAttrs(stop.color)}/></stop>`)
390
371
  .join('');
391
372
  return `<gradientFill${attrs}>${stops}</gradientFill>`;
392
373
  }
393
374
  function insetAttr(name, value) {
394
- return value ? ` ${name}="${numberAttr(value)}"` : '';
375
+ return value ? ` ${name}="${numberText(value)}"` : '';
395
376
  }
396
377
  function underlineXml(underline) {
397
378
  if (underline === undefined || underline === false || underline === 'none')
@@ -400,12 +381,6 @@ function underlineXml(underline) {
400
381
  return '<u/>';
401
382
  return `<u val="${checkedToken(underline, isNamedUnderlineStyle, 'underline style')}"/>`;
402
383
  }
403
- function numberAttr(value) {
404
- if (!Number.isFinite(value)) {
405
- throw new AuthoringError(`cannot serialise a non-finite font metric (${value})`);
406
- }
407
- return String(value);
408
- }
409
384
  function borderXml(border) {
410
385
  const attrs = (border.diagonalUp ? ' diagonalUp="1"' : '') + (border.diagonalDown ? ' diagonalDown="1"' : '');
411
386
  return (`<border${attrs}>` +
@@ -1,6 +1,6 @@
1
- import { decodeRange, encodeAddress } from '../../core/address.js';
1
+ import { encodeAddress, tryDecodeRange } from '../../core/address.js';
2
2
  import { isTotalsRowFunction, } from '../../core/table.js';
3
- import { boolPresent, localName, numInteger, parseXml } from '../../xml/xml-read.js';
3
+ import { boolPresent, localName, numInteger, parseXml, TextCapture } from '../../xml/xml-read.js';
4
4
  import { boolAttr, escapeAttr, escapeText, XML_DECLARATION } from '../../xml/xml.js';
5
5
  import { NS } from './relationships.js';
6
6
  export function tableXml(table, id) {
@@ -65,10 +65,9 @@ export function parseTable(xml) {
65
65
  let style;
66
66
  let hasAutoFilter = false;
67
67
  const columns = [];
68
- let inTotalsFormula = false;
69
- let totalsFormula = '';
68
+ const totalsFormula = new TextCapture('totalsRowFormula');
70
69
  parseXml(xml, {
71
- onOpen(elementName, attrs) {
70
+ onOpen(elementName, attrs, selfClosing) {
72
71
  switch (localName(elementName)) {
73
72
  case 'table':
74
73
  name = attrs.name ?? attrs.displayName;
@@ -112,27 +111,28 @@ export function parseTable(xml) {
112
111
  break;
113
112
  }
114
113
  case 'totalsRowFormula':
115
- inTotalsFormula = true;
116
- totalsFormula = '';
114
+ totalsFormula.open('totalsRowFormula', selfClosing);
117
115
  break;
118
116
  }
119
117
  },
120
118
  onText(text) {
121
- if (inTotalsFormula)
122
- totalsFormula += text;
119
+ totalsFormula.text(text);
123
120
  },
124
121
  onClose(elementName) {
125
- if (localName(elementName) !== 'totalsRowFormula')
122
+ const formula = totalsFormula.close(localName(elementName));
123
+ if (formula === undefined)
126
124
  return;
127
- inTotalsFormula = false;
128
125
  const column = columns[columns.length - 1];
129
126
  if (column !== undefined)
130
- column.totalsRowFormula = totalsFormula;
127
+ column.totalsRowFormula = formula;
131
128
  },
132
129
  });
133
130
  if (name === undefined || ref === undefined || columns.length === 0)
134
131
  return undefined;
135
- const { top, left, bottom } = decodeRange(ref);
132
+ const decoded = tryDecodeRange(ref);
133
+ if (decoded === undefined)
134
+ return undefined;
135
+ const { top, left, bottom } = decoded;
136
136
  if (top === undefined || left === undefined || bottom === undefined)
137
137
  return undefined;
138
138
  const headerRow = headerRowCount !== 0;
@@ -1,4 +1,4 @@
1
- import { DEFAULT_THEME_COLOR_SCHEME, normalizeThemeColor, } from '../../core/theme.js';
1
+ import { DEFAULT_THEME_COLOR_SCHEME, isThemeColorSlot, normalizeThemeColor, } from '../../core/theme.js';
2
2
  import { decodeEntities } from '../../xml/xml-read.js';
3
3
  import { escapeAttr } from '../../xml/xml.js';
4
4
  const SCHEME_SLOT = new RegExp('<a:(dk1|lt1|dk2|lt2|accent[1-6]|hlink|folHlink)\\b[^>]*>' + '\\s*<a:(srgbClr|sysClr)\\b([^>]*)>', 'g');
@@ -9,6 +9,8 @@ export function parseThemeColorScheme(themeXml) {
9
9
  const scheme = {};
10
10
  for (const match of (block[1] ?? '').matchAll(SCHEME_SLOT)) {
11
11
  const slot = match[1];
12
+ if (slot === undefined || !isThemeColorSlot(slot))
13
+ continue;
12
14
  const attrs = match[3] ?? '';
13
15
  const source = match[2] === 'sysClr' ? /\blastClr="([^"]*)"/ : /\bval="([^"]*)"/;
14
16
  const value = source.exec(attrs)?.[1];
@@ -58,8 +60,7 @@ export function applyThemeOverrides(baseXml, overrides) {
58
60
  const authored = colors[slot];
59
61
  const inner = authored !== undefined
60
62
  ? `<a:srgbClr val="${normalizeThemeColor(authored)}"/>`
61
- : (sourceElements[slot] ??
62
- `<a:srgbClr val="${DEFAULT_THEME_COLOR_SCHEME[slot]}"/>`);
63
+ : (sourceElements[slot] ?? `<a:srgbClr val="${DEFAULT_THEME_COLOR_SCHEME[slot]}"/>`);
63
64
  return `<a:${slot}>${inner}</a:${slot}>`;
64
65
  }).join('');
65
66
  xml = replaceBlockBody(xml, 'clrScheme', body);
@@ -78,7 +79,9 @@ function parseThemeColorElements(themeXml) {
78
79
  const elements = {};
79
80
  const pattern = /<a:(dk1|lt1|dk2|lt2|accent[1-6]|hlink|folHlink)>([\s\S]*?)<\/a:\1>|<a:(dk1|lt1|dk2|lt2|accent[1-6]|hlink|folHlink)\/>/g;
80
81
  for (const match of (block[1] ?? '').matchAll(pattern)) {
81
- const slot = (match[1] ?? match[3]);
82
+ const slot = match[1] ?? match[3];
83
+ if (slot === undefined || !isThemeColorSlot(slot))
84
+ continue;
82
85
  const inner = match[2];
83
86
  if (inner !== undefined && inner !== '')
84
87
  elements[slot] = inner;
@@ -1,7 +1,7 @@
1
1
  import { encodeAddress, tryDecodeCellRef } from '../../core/address.js';
2
2
  import { MENTION_OFFSET_MAX, } from '../../core/comment-thread.js';
3
- import { boolStrict, decodeSpreadsheetText, localName, numInteger, parseXml, } from '../../xml/xml-read.js';
4
- import { escapeAttr, escapeSpreadsheetText, XML_DECLARATION } from '../../xml/xml.js';
3
+ import { boolStrict, decodeSpreadsheetText, localName, numInteger, parseXml, TextCapture, } from '../../xml/xml-read.js';
4
+ import { escapeAttr, escapeSpreadsheetText, textAttr, XML_DECLARATION } from '../../xml/xml.js';
5
5
  import { THREADED_COMMENTS_NS } from './namespaces.js';
6
6
  export function parsePersons(xml) {
7
7
  const persons = [];
@@ -27,33 +27,33 @@ export function parseThreadedComments(xml) {
27
27
  const messages = [];
28
28
  let open;
29
29
  let mentions = [];
30
- let inText = false;
31
30
  let text = '';
31
+ const capture = new TextCapture('text');
32
32
  parseXml(xml, {
33
- onOpen(name, attrs) {
33
+ onOpen(name, attrs, selfClosing) {
34
34
  const local = localName(name);
35
35
  if (local === 'threadedComment') {
36
36
  open = attrs;
37
37
  text = '';
38
38
  mentions = [];
39
39
  }
40
- else if (local === 'text' && open !== undefined) {
41
- inText = true;
42
- }
43
- else if (local === 'mention' && open !== undefined) {
44
- const mention = mentionFrom(attrs);
45
- if (mention !== undefined)
46
- mentions.push(mention);
40
+ else if (open !== undefined) {
41
+ capture.open(local, selfClosing);
42
+ if (local === 'mention') {
43
+ const mention = mentionFrom(attrs);
44
+ if (mention !== undefined)
45
+ mentions.push(mention);
46
+ }
47
47
  }
48
48
  },
49
49
  onText(chunk) {
50
- if (inText)
51
- text += chunk;
50
+ capture.text(chunk);
52
51
  },
53
52
  onClose(name) {
54
53
  const local = localName(name);
55
- if (local === 'text') {
56
- inText = false;
54
+ const body = capture.close(local);
55
+ if (body !== undefined) {
56
+ text = body;
57
57
  }
58
58
  else if (local === 'threadedComment') {
59
59
  if (open !== undefined) {
@@ -159,8 +159,8 @@ export function threadedCommentsXml(threads) {
159
159
  `<ThreadedComments xmlns="${THREADED_COMMENTS_NS}">${messages.join('')}</ThreadedComments>`);
160
160
  }
161
161
  function threadedCommentXml(ref, comment, tail) {
162
- const date = comment.date === undefined ? '' : ` dT="${escapeAttr(comment.date)}"`;
163
- const person = comment.personId === undefined ? '' : ` personId="${escapeAttr(comment.personId)}"`;
162
+ const date = textAttr('dT', comment.date);
163
+ const person = textAttr('personId', comment.personId);
164
164
  return (`<threadedComment ref="${escapeAttr(ref)}"${date}${person} id="${escapeAttr(comment.id)}"${tail}>` +
165
165
  `<text>${escapeSpreadsheetText(comment.text)}</text>` +
166
166
  mentionsXml(comment.mentions) +
@@ -184,8 +184,8 @@ function writableOffset(value) {
184
184
  }
185
185
  export function personsXml(persons) {
186
186
  const entries = persons.map((person) => {
187
- const userId = person.userId === undefined ? '' : ` userId="${escapeAttr(person.userId)}"`;
188
- const providerId = person.providerId === undefined ? '' : ` providerId="${escapeAttr(person.providerId)}"`;
187
+ const userId = textAttr('userId', person.userId);
188
+ const providerId = textAttr('providerId', person.providerId);
189
189
  return (`<person displayName="${escapeAttr(person.displayName)}"` +
190
190
  ` id="${escapeAttr(person.id)}"${userId}${providerId}/>`);
191
191
  });
@@ -1,6 +1,7 @@
1
1
  import { mangleFormula, quoteSheetName } from '../../core/formula.js';
2
2
  import { WORKBOOK_PROTECTION_CREDENTIAL_ATTRS } from '../../core/workbook-protection.js';
3
- import { escapeAttr, escapeText, XML_DECLARATION } from '../../xml/xml.js';
3
+ import { isVisibility } from '../../core/worksheet.js';
4
+ import { checkedToken, escapeAttr, escapeText, numAttr, textAttr, XML_DECLARATION, } from '../../xml/xml.js';
4
5
  import { extensionOf, relativePartPath, THEME_PART_PATH } from '../opc/part-paths.js';
5
6
  import { relationship, relationshipsPart } from '../opc/rels.js';
6
7
  import { imageContentType } from './images.js';
@@ -102,14 +103,16 @@ export function rootRelsXml(rootRefs) {
102
103
  relationship('rId1', REL.officeDocument, 'xl/workbook.xml'),
103
104
  relationship('rId2', REL.coreProps, 'docProps/core.xml'),
104
105
  relationship('rId3', REL.extProps, 'docProps/app.xml'),
105
- ...rootRefs.map((ref, i) => relationship(`rId${4 + i}`, ref.relType, escapeAttr(ref.entryPath))),
106
+ ...rootRefs.map((ref, i) => relationship(`rId${4 + i}`, ref.relType, ref.entryPath)),
106
107
  ]);
107
108
  }
108
109
  export function workbookXml(workbook, preservedRels, pivots) {
109
110
  const sheets = workbook.worksheets;
110
111
  const entries = sheets
111
112
  .map((sheet, i) => {
112
- const state = sheet.state === 'visible' ? '' : ` state="${sheet.state}"`;
113
+ const state = sheet.state === 'visible'
114
+ ? ''
115
+ : ` state="${checkedToken(sheet.state, isVisibility, 'sheet visibility')}"`;
113
116
  return `<sheet name="${escapeAttr(sheet.name)}" sheetId="${sheet.id}"${state} r:id="rId${i + 1}"/>`;
114
117
  })
115
118
  .join('');
@@ -128,11 +131,14 @@ export function workbookXml(workbook, preservedRels, pivots) {
128
131
  function bookViewsXml(workbook) {
129
132
  const view = workbook.view;
130
133
  const activeTab = workbook.activeTabIndex;
131
- const attrs = (view.visibility !== undefined && view.visibility !== 'visible'
132
- ? ` visibility="${view.visibility}"`
133
- : '') +
134
+ const attrs = (view.visibility === undefined || view.visibility === 'visible'
135
+ ? ''
136
+ : ` visibility="${checkedToken(view.visibility, isVisibility, 'window visibility')}"`) +
134
137
  (view.minimized ? ' minimized="1"' : '') +
135
- ` xWindow="${view.x}" yWindow="${view.y}" windowWidth="${view.width}" windowHeight="${view.height}"` +
138
+ numAttr('xWindow', view.x) +
139
+ numAttr('yWindow', view.y) +
140
+ numAttr('windowWidth', view.width) +
141
+ numAttr('windowHeight', view.height) +
136
142
  (activeTab === 0 ? '' : ` activeTab="${activeTab}"`);
137
143
  return `<bookViews><workbookView${attrs}/></bookViews>`;
138
144
  }
@@ -192,7 +198,7 @@ function definedNamesXml(workbook) {
192
198
  const scopeAttr = name.scope === undefined
193
199
  ? ''
194
200
  : ` localSheetId="${sheets.findIndex((sheet) => sheet.name === name.scope)}"`;
195
- const commentAttr = name.comment === undefined ? '' : ` comment="${escapeAttr(name.comment)}"`;
201
+ const commentAttr = textAttr('comment', name.comment);
196
202
  const hiddenAttr = name.hidden ? ' hidden="1"' : '';
197
203
  return (`<definedName name="${escapeAttr(name.name)}"${scopeAttr}${commentAttr}${hiddenAttr}>` +
198
204
  `${escapeText(mangleFormula(name.refersTo))}</definedName>`);
@@ -2,9 +2,10 @@ import { decodeRange, encodeAddress } from '../../core/address.js';
2
2
  import { DEFAULT_DATE_NUMFMT, dateToSerial } from '../../core/date.js';
3
3
  import { mangleFormula } from '../../core/formula.js';
4
4
  import { NAMED_STYLE_ID } from '../../core/internal.js';
5
+ import { CELL_STYLE_FACETS, pickStyleFacets } from '../../core/style.js';
5
6
  import { detectValueType, isDataTableFormulaValue, isErrorValue, isFormulaValue, isHyperlinkValue, isRichTextValue, isSharedFormulaValue, } from '../../core/value.js';
6
7
  import { AuthoringError, InternalError } from '../../errors.js';
7
- import { escapeAttr, escapeSpreadsheetText, escapeText, numberText, textElement, XML_DECLARATION, } from '../../xml/xml.js';
8
+ import { assertWritableNumber, escapeAttr, escapeSpreadsheetText, escapeText, numberText, textAttr, textElement, XML_DECLARATION, } from '../../xml/xml.js';
8
9
  import { relativePartPath } from '../opc/part-paths.js';
9
10
  import { relationship, relationshipsPart } from '../opc/rels.js';
10
11
  import { conditionalFormattingsExtXml, conditionalFormattingsXml } from './conditional-formatting.js';
@@ -136,16 +137,20 @@ export function renderRow(entry, ctx) {
136
137
  return { xml: `<row r="${number}"${attrs}>${cellsXml}</row>`, minCol, maxCol };
137
138
  }
138
139
  function composeCellStyle(cell, rowFill, colDef) {
139
- return {
140
- fill: cell.fill ?? rowFill ?? colDef?.fill,
141
- numFmt: cell.numFmt ?? colDef?.numFmt ?? dateDefaultNumFmt(cell.value),
142
- font: cell.font ?? colDef?.font,
143
- border: cell.border ?? colDef?.border,
144
- alignment: cell.alignment ?? colDef?.alignment,
145
- protection: cell.protection ?? colDef?.protection,
146
- quotePrefix: cell.quotePrefix,
147
- xfId: cell[NAMED_STYLE_ID],
148
- };
140
+ const style = {};
141
+ for (const facet of CELL_STYLE_FACETS)
142
+ inheritFacet(style, facet, cell, colDef);
143
+ style.fill = cell.fill ?? rowFill ?? colDef?.fill;
144
+ style.numFmt = cell.numFmt ?? colDef?.numFmt ?? dateDefaultNumFmt(cell.value);
145
+ if (cell.quotePrefix !== undefined)
146
+ style.quotePrefix = cell.quotePrefix;
147
+ const xfId = cell[NAMED_STYLE_ID];
148
+ if (xfId !== undefined)
149
+ style.xfId = xfId;
150
+ return style;
151
+ }
152
+ function inheritFacet(target, key, cell, colDef) {
153
+ target[key] = cell[key] ?? colDef?.[key];
149
154
  }
150
155
  function worksheetExtLstXml(sheet, slicerRelIds) {
151
156
  const exts = [
@@ -228,7 +233,7 @@ export function worksheetRelsXml(tables, drawing, comments, threadedComments, pr
228
233
  ...preservedReferences.map((reference) => relationship(reference.relId, reference.relType, escapeAttr(relativePartPath('xl/worksheets/sheet1.xml', reference.entryPath)))),
229
234
  ...hyperlinks
230
235
  .filter((link) => link.relId !== undefined && link.target !== undefined)
231
- .map((link) => relationship(link.relId, REL.hyperlink, escapeAttr(link.target), {
236
+ .map((link) => relationship(link.relId, REL.hyperlink, link.target, {
232
237
  external: true,
233
238
  })),
234
239
  ];
@@ -243,12 +248,16 @@ function sheetFormatPr(properties, outlineLevel) {
243
248
  }
244
249
  if (properties.defaultRowHeight !== undefined)
245
250
  attrs += ' customHeight="1"';
246
- if (outlineLevel.col > 0)
247
- attrs += ` outlineLevelCol="${outlineLevel.col}"`;
248
- if (outlineLevel.row > 0)
249
- attrs += ` outlineLevelRow="${outlineLevel.row}"`;
251
+ attrs += outlineAttr('outlineLevelCol', outlineLevel.col);
252
+ attrs += outlineAttr('outlineLevelRow', outlineLevel.row);
250
253
  return `<sheetFormatPr${attrs}/>`;
251
254
  }
255
+ function outlineAttr(name, level) {
256
+ if (level === undefined)
257
+ return '';
258
+ const text = numberText(level);
259
+ return level > 0 ? ` ${name}="${text}"` : '';
260
+ }
252
261
  function maxColumnOutlineLevel(sheet) {
253
262
  let max = 0;
254
263
  for (const column of sheet.columns())
@@ -285,22 +294,16 @@ function colBody(properties, styles) {
285
294
  attrs += ' hidden="1"';
286
295
  meaningful = true;
287
296
  }
288
- if (properties.outlineLevel !== undefined && properties.outlineLevel > 0) {
289
- attrs += ` outlineLevel="${properties.outlineLevel}"`;
297
+ const outline = outlineAttr('outlineLevel', properties.outlineLevel);
298
+ if (outline !== '') {
299
+ attrs += outline;
290
300
  meaningful = true;
291
301
  }
292
302
  if (properties.collapsed) {
293
303
  attrs += ' collapsed="1"';
294
304
  meaningful = true;
295
305
  }
296
- const style = styles.styleId({
297
- fill: properties.fill,
298
- numFmt: properties.numFmt,
299
- font: properties.font,
300
- border: properties.border,
301
- alignment: properties.alignment,
302
- protection: properties.protection,
303
- });
306
+ const style = styles.styleId(pickStyleFacets(properties));
304
307
  if (style !== 0) {
305
308
  attrs += ` style="${style}"`;
306
309
  meaningful = true;
@@ -315,9 +318,7 @@ function rowAttrs(properties, styles, collapsedSummary) {
315
318
  attrs += ` ht="${numberText(properties.height)}" customHeight="1"`;
316
319
  if (properties.hidden)
317
320
  attrs += ' hidden="1"';
318
- if (properties.outlineLevel !== undefined && properties.outlineLevel > 0) {
319
- attrs += ` outlineLevel="${properties.outlineLevel}"`;
320
- }
321
+ attrs += outlineAttr('outlineLevel', properties.outlineLevel);
321
322
  if (properties.collapsed || collapsedSummary)
322
323
  attrs += ' collapsed="1"';
323
324
  const style = styles.styleId({ fill: properties.fill });
@@ -331,6 +332,7 @@ function scanRowOutline(sheet) {
331
332
  let maxLevel = 0;
332
333
  for (const { number, properties } of sheet.rows()) {
333
334
  const rowLevel = properties?.outlineLevel ?? 0;
335
+ assertWritableNumber(rowLevel);
334
336
  level.set(number, rowLevel);
335
337
  hidden.set(number, properties?.hidden ?? false);
336
338
  if (rowLevel > maxLevel)
@@ -408,12 +410,7 @@ function cellXml(cell, style, shared, sharedStrings) {
408
410
  throw new InternalError(`writing a ${detectValueType(value)} cell value has no arm: every CellValue kind is handled above`);
409
411
  }
410
412
  function hasOwnStyle(cell) {
411
- return (cell.fill !== undefined ||
412
- cell.numFmt !== undefined ||
413
- cell.font !== undefined ||
414
- cell.border !== undefined ||
415
- cell.alignment !== undefined ||
416
- cell.protection !== undefined ||
413
+ return (CELL_STYLE_FACETS.some((facet) => cell[facet] !== undefined) ||
417
414
  cell.quotePrefix === true ||
418
415
  cell[NAMED_STYLE_ID] !== undefined);
419
416
  }
@@ -430,8 +427,8 @@ function cellFormulaXml(ref, s, value, shared) {
430
427
  const attrs = `ref="${escapeAttr(value.ref)}"` +
431
428
  ` dt2D="${value.dataTable2D ? 1 : 0}"` +
432
429
  ` dtr="${value.dataTableRow ? 1 : 0}"` +
433
- (value.r1 !== undefined ? ` r1="${escapeAttr(value.r1)}"` : '') +
434
- (value.r2 !== undefined ? ` r2="${escapeAttr(value.r2)}"` : '');
430
+ textAttr('r1', value.r1) +
431
+ textAttr('r2', value.r2);
435
432
  return formulaBodyXml(ref, s, `<f t="dataTable" ${attrs}/>`, value.result);
436
433
  }
437
434
  if (isFormulaValue(value)) {