@shbernal/ts-xlsx 2.0.0 → 3.0.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 (147) 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 +12 -1
  7. package/dist/core/autofilter.js +35 -12
  8. package/dist/core/axis-handle.d.ts +8 -0
  9. package/dist/core/axis-handle.js +14 -0
  10. package/dist/core/cell.d.ts +24 -0
  11. package/dist/core/cell.js +9 -1
  12. package/dist/core/color-resolution.d.ts +1 -1
  13. package/dist/core/color-resolution.js +7 -8
  14. package/dist/core/column.d.ts +4 -1
  15. package/dist/core/column.js +31 -35
  16. package/dist/core/conditional-formatting-overlay.d.ts +5 -0
  17. package/dist/core/conditional-formatting-overlay.js +11 -0
  18. package/dist/core/conditional-formatting.d.ts +45 -5
  19. package/dist/core/conditional-formatting.js +74 -0
  20. package/dist/core/data-validation-overlay.d.ts +11 -0
  21. package/dist/core/data-validation-overlay.js +21 -2
  22. package/dist/core/data-validation.d.ts +9 -2
  23. package/dist/core/data-validation.js +26 -0
  24. package/dist/core/grid-edits.d.ts +16 -0
  25. package/dist/core/grid-edits.js +26 -8
  26. package/dist/core/grid-shift.d.ts +19 -0
  27. package/dist/core/grid-shift.js +8 -0
  28. package/dist/core/image.d.ts +2 -0
  29. package/dist/core/image.js +2 -0
  30. package/dist/core/merge-index.d.ts +31 -0
  31. package/dist/core/merge-index.js +60 -0
  32. package/dist/core/merge.d.ts +12 -11
  33. package/dist/core/merge.js +36 -10
  34. package/dist/core/page-setup.d.ts +86 -2
  35. package/dist/core/page-setup.js +34 -1
  36. package/dist/core/pivot-table.d.ts +2 -0
  37. package/dist/core/pivot-table.js +23 -18
  38. package/dist/core/protection.d.ts +3 -0
  39. package/dist/core/protection.js +20 -14
  40. package/dist/core/row-input.d.ts +7 -0
  41. package/dist/core/row-input.js +10 -8
  42. package/dist/core/row.d.ts +4 -1
  43. package/dist/core/row.js +19 -23
  44. package/dist/core/style.d.ts +78 -7
  45. package/dist/core/style.js +90 -80
  46. package/dist/core/table.d.ts +26 -12
  47. package/dist/core/table.js +67 -33
  48. package/dist/core/theme.d.ts +3 -1
  49. package/dist/core/theme.js +4 -0
  50. package/dist/core/used-extent.d.ts +44 -0
  51. package/dist/core/used-extent.js +102 -0
  52. package/dist/core/workbook.d.ts +2 -2
  53. package/dist/core/worksheet-comments.d.ts +8 -0
  54. package/dist/core/worksheet-comments.js +18 -0
  55. package/dist/core/worksheet.d.ts +31 -7
  56. package/dist/core/worksheet.js +72 -86
  57. package/dist/customui/ribbon.js +23 -24
  58. package/dist/entries/core.d.ts +3 -3
  59. package/dist/entries/csv.d.ts +1 -1
  60. package/dist/entries/node-unavailable.d.ts +15 -0
  61. package/dist/entries/node-unavailable.js +20 -0
  62. package/dist/entries/node.d.ts +1 -0
  63. package/dist/entries/node.js +1 -0
  64. package/dist/entries/xlsx.d.ts +0 -1
  65. package/dist/entries/xlsx.js +0 -1
  66. package/dist/io/csv/read.js +1 -1
  67. package/dist/io/csv/write.d.ts +12 -2
  68. package/dist/io/csv/write.js +17 -2
  69. package/dist/io/opc/inflate.js +3 -13
  70. package/dist/io/opc/part-paths.d.ts +1 -0
  71. package/dist/io/opc/part-paths.js +15 -0
  72. package/dist/io/opc/read-opc.d.ts +22 -3
  73. package/dist/io/opc/read-opc.js +11 -39
  74. package/dist/io/opc/rels.d.ts +12 -6
  75. package/dist/io/opc/rels.js +3 -5
  76. package/dist/io/style/xf-style.d.ts +33 -4
  77. package/dist/io/style/xf-style.js +12 -1
  78. package/dist/io/xlsb/read-styles.js +6 -3
  79. package/dist/io/xlsb/read.d.ts +1 -1
  80. package/dist/io/xlsb/read.js +5 -8
  81. package/dist/io/xlsx/cell-accumulator.d.ts +2 -4
  82. package/dist/io/xlsx/cell-accumulator.js +20 -58
  83. package/dist/io/xlsx/cell-value.d.ts +1 -1
  84. package/dist/io/xlsx/cell-value.js +7 -4
  85. package/dist/io/xlsx/color-xml.d.ts +1 -1
  86. package/dist/io/xlsx/color-xml.js +8 -7
  87. package/dist/io/xlsx/comments.js +6 -3
  88. package/dist/io/xlsx/conditional-formatting.d.ts +4 -3
  89. package/dist/io/xlsx/conditional-formatting.js +81 -65
  90. package/dist/io/xlsx/data-validation.d.ts +10 -8
  91. package/dist/io/xlsx/data-validation.js +44 -41
  92. package/dist/io/xlsx/edit-vba.js +4 -11
  93. package/dist/io/xlsx/hyperlinks.d.ts +4 -2
  94. package/dist/io/xlsx/hyperlinks.js +26 -31
  95. package/dist/io/xlsx/images.js +33 -35
  96. package/dist/io/xlsx/package-plan.d.ts +9 -1
  97. package/dist/io/xlsx/package-plan.js +29 -16
  98. package/dist/io/xlsx/part-names.d.ts +41 -0
  99. package/dist/io/xlsx/part-names.js +46 -0
  100. package/dist/io/xlsx/read-pivot.js +4 -9
  101. package/dist/io/xlsx/read-rows.js +12 -13
  102. package/dist/io/xlsx/read-shared-strings.js +9 -47
  103. package/dist/io/xlsx/read-styles.d.ts +1 -1
  104. package/dist/io/xlsx/read-styles.js +65 -43
  105. package/dist/io/xlsx/read-worksheet.d.ts +10 -1
  106. package/dist/io/xlsx/read-worksheet.js +48 -63
  107. package/dist/io/xlsx/read.d.ts +3 -4
  108. package/dist/io/xlsx/read.js +124 -146
  109. package/dist/io/xlsx/rich-runs.d.ts +41 -7
  110. package/dist/io/xlsx/rich-runs.js +95 -30
  111. package/dist/io/xlsx/sheet-properties.d.ts +1 -1
  112. package/dist/io/xlsx/sheet-properties.js +23 -26
  113. package/dist/io/xlsx/styles.d.ts +3 -15
  114. package/dist/io/xlsx/styles.js +83 -105
  115. package/dist/io/xlsx/tables.js +14 -13
  116. package/dist/io/xlsx/theme-xml.js +8 -5
  117. package/dist/io/xlsx/threaded-comments.js +20 -19
  118. package/dist/io/xlsx/workbook-xml.js +43 -36
  119. package/dist/io/xlsx/worksheet-xml.d.ts +1 -1
  120. package/dist/io/xlsx/worksheet-xml.js +47 -52
  121. package/dist/io/xlsx/write-stream.d.ts +14 -1
  122. package/dist/io/xlsx/write-stream.js +40 -33
  123. package/dist/io/xlsx/write.d.ts +5 -0
  124. package/dist/io/xlsx/write.js +155 -131
  125. package/dist/sha512.d.ts +2 -0
  126. package/dist/sha512.js +146 -0
  127. package/dist/token-set.d.ts +15 -0
  128. package/dist/token-set.js +4 -0
  129. package/dist/vba/bytes.d.ts +1 -2
  130. package/dist/vba/bytes.js +1 -12
  131. package/dist/vba/cfb-format.d.ts +24 -0
  132. package/dist/vba/cfb-format.js +12 -0
  133. package/dist/vba/cfb-writer.js +17 -17
  134. package/dist/vba/cfb.js +1 -7
  135. package/dist/vba/dir-records.d.ts +53 -0
  136. package/dist/vba/dir-records.js +28 -0
  137. package/dist/vba/ms-ovba.js +32 -11
  138. package/dist/vba/project-editor.js +5 -34
  139. package/dist/vba/project.js +15 -31
  140. package/dist/vba/vba-encoding.js +1 -1
  141. package/dist/xml/xml-read.d.ts +117 -72
  142. package/dist/xml/xml-read.js +123 -169
  143. package/dist/xml/xml-scan.d.ts +100 -0
  144. package/dist/xml/xml-scan.js +208 -0
  145. package/dist/xml/xml.d.ts +44 -2
  146. package/dist/xml/xml.js +17 -5
  147. package/package.json +22 -6
@@ -1,6 +1,7 @@
1
1
  import { encodeAddress } from '../../core/address.js';
2
+ import { HEADER_FOOTER_ELEMENTS, MARGIN_SIDES, PAGE_SETUP_FACETS, PRINT_OPTION_FLAGS, } from '../../core/page-setup.js';
2
3
  import { SHEET_PROTECTION_FLAGS } from '../../core/protection.js';
3
- import { attr, boolAttr, escapeAttr, escapeSpreadsheetText, numberText } from '../../xml/xml.js';
4
+ import { boolAttr, checkedToken, escapeAttr, escapeSpreadsheetText, numAttr, numberText, } from '../../xml/xml.js';
4
5
  import { colorAttrs } from './color-xml.js';
5
6
  export function sheetViewsXml(view, active) {
6
7
  const selected = active ? ' tabSelected="1"' : '';
@@ -79,16 +80,8 @@ function filterCriteriaXml(criteria) {
79
80
  .join('');
80
81
  return `<customFilters${andAttr}>${predicates}</customFilters>`;
81
82
  }
82
- const HF_CHILDREN = [
83
- { tag: 'oddHeader', key: 'oddHeader' },
84
- { tag: 'oddFooter', key: 'oddFooter' },
85
- { tag: 'evenHeader', key: 'evenHeader' },
86
- { tag: 'evenFooter', key: 'evenFooter' },
87
- { tag: 'firstHeader', key: 'firstHeader' },
88
- { tag: 'firstFooter', key: 'firstFooter' },
89
- ];
90
83
  export function headerFooterXml(hf) {
91
- const children = HF_CHILDREN.filter(({ key }) => hf[key] !== undefined);
84
+ const children = HEADER_FOOTER_ELEMENTS.filter((key) => hf[key] !== undefined);
92
85
  if (children.length === 0)
93
86
  return '';
94
87
  const differentOddEven = hf.evenHeader !== undefined || hf.evenFooter !== undefined;
@@ -99,7 +92,7 @@ export function headerFooterXml(hf) {
99
92
  if (differentFirst)
100
93
  attrs += ' differentFirst="1"';
101
94
  const body = children
102
- .map(({ tag, key }) => `<${tag}>${escapeSpreadsheetText(hf[key])}</${tag}>`)
95
+ .map((key) => `<${key}>${escapeSpreadsheetText(hf[key] ?? '')}</${key}>`)
103
96
  .join('');
104
97
  return `<headerFooter${attrs}>${body}</headerFooter>`;
105
98
  }
@@ -111,13 +104,8 @@ const DEFAULT_MARGINS = {
111
104
  header: 0.3,
112
105
  footer: 0.3,
113
106
  };
114
- const MARGIN_SIDES = ['left', 'right', 'top', 'bottom', 'header', 'footer'];
115
107
  export function printOptionsXml(printOptions) {
116
- const attrs = boolAttr('horizontalCentered', printOptions.horizontalCentered) +
117
- boolAttr('verticalCentered', printOptions.verticalCentered) +
118
- boolAttr('headings', printOptions.headings) +
119
- boolAttr('gridLines', printOptions.gridLines) +
120
- boolAttr('gridLinesSet', printOptions.gridLinesSet);
108
+ const attrs = PRINT_OPTION_FLAGS.map((flag) => boolAttr(flag, printOptions[flag])).join('');
121
109
  return attrs === '' ? '' : `<printOptions${attrs}/>`;
122
110
  }
123
111
  export function pageMarginsXml(margins) {
@@ -127,13 +115,23 @@ export function pageMarginsXml(margins) {
127
115
  return `<pageMargins ${attrs}/>`;
128
116
  }
129
117
  export function pageSetupXml(pageSetup, printerSettingsRelId) {
130
- const attrs = attr('paperSize', pageSetup.paperSize) +
131
- attr('scale', pageSetup.scale) +
132
- attr('fitToWidth', pageSetup.fitToWidth) +
133
- attr('fitToHeight', pageSetup.fitToHeight) +
134
- (pageSetup.pageOrder !== undefined ? ` pageOrder="${pageSetup.pageOrder}"` : '') +
135
- (pageSetup.orientation !== undefined ? ` orientation="${pageSetup.orientation}"` : '') +
136
- (printerSettingsRelId !== null ? ` r:id="${printerSettingsRelId}"` : '');
118
+ let attrs = '';
119
+ for (const facet of PAGE_SETUP_FACETS) {
120
+ switch (facet.kind) {
121
+ case 'count':
122
+ attrs += numAttr(facet.key, pageSetup[facet.key]);
123
+ break;
124
+ case 'token': {
125
+ const value = pageSetup[facet.key];
126
+ if (value !== undefined) {
127
+ attrs += ` ${facet.key}="${checkedToken(value, facet.isValid, facet.label)}"`;
128
+ }
129
+ break;
130
+ }
131
+ }
132
+ }
133
+ if (printerSettingsRelId !== null)
134
+ attrs += ` r:id="${printerSettingsRelId}"`;
137
135
  return attrs === '' ? '' : `<pageSetup${attrs}/>`;
138
136
  }
139
137
  export function pageBreaksXml(breaks, element) {
@@ -141,8 +139,7 @@ export function pageBreaksXml(breaks, element) {
141
139
  return '';
142
140
  const brks = breaks
143
141
  .map((brk) => {
144
- const maxAttr = brk.max !== undefined ? ` max="${brk.max}"` : '';
145
- return `<brk id="${brk.id}"${maxAttr} man="1"/>`;
142
+ return `<brk${numAttr('id', brk.id)}${numAttr('max', brk.max)} man="1"/>`;
146
143
  })
147
144
  .join('');
148
145
  return `<${element} count="${breaks.length}" manualBreakCount="${breaks.length}">${brks}</${element}>`;
@@ -1,18 +1,6 @@
1
- import { type Alignment, type Border, type DifferentialStyle, type Fill, type Font, type NamedCellStyle, type Protection, type TableStyleTable } from '../../core/style.ts';
1
+ import { type DifferentialStyle, type Font, type NamedCellStyle, type TableStyleTable } from '../../core/style.ts';
2
2
  import { type TableStyle } from '../../core/table-style.ts';
3
- /** A cell's style facets as the writer composes them: cell overrides atop row/column defaults. */
4
- export interface CellStyle {
5
- readonly fill?: Fill | undefined;
6
- readonly numFmt?: string | undefined;
7
- readonly font?: Font | undefined;
8
- readonly border?: Border | undefined;
9
- readonly alignment?: Alignment | undefined;
10
- readonly protection?: Protection | undefined;
11
- /** The quote-prefix flag: an attribute on the xf, not a shared sub-table entry. */
12
- readonly quotePrefix?: boolean | undefined;
13
- /** The `xfId` link into `cellStyleXfs`: the named cell style this format inherits from (0 = Normal). */
14
- readonly xfId?: number | undefined;
15
- }
3
+ import type { XfStyle } from '../style/xf-style.ts';
16
4
  /** What a {@link StyleRegistry} needs from its workbook before any style is interned. */
17
5
  export interface StyleRegistryOptions {
18
6
  /**
@@ -41,7 +29,7 @@ export declare class StyleRegistry {
41
29
  * The `<cellXfs>` index for a composed cell/row/column style. A style with no facet needs
42
30
  * no entry and resolves to the default xf 0, so its owner emits no `s` attribute at all.
43
31
  */
44
- styleId(style: CellStyle): number;
32
+ styleId(style: XfStyle): number;
45
33
  /**
46
34
  * Seed the named cell styles (`<cellStyleXfs>`/`<cellStyles>`) read from a file, in place of the
47
35
  * lone default, interning each style's facets into the shared sub-tables so its `fillId`/`fontId`/…
@@ -1,8 +1,7 @@
1
- import { isBorderStyle, isFillPatternType, isFontScheme, isFontVerticalAlignment, isHorizontalAlignment, isNamedUnderlineStyle, isVerticalAlignment, } from '../../core/style.js';
1
+ import { ALIGNMENT_FACETS, isBorderStyle, isFillPatternType, isFontScheme, isFontVerticalAlignment, isNamedUnderlineStyle, } from '../../core/style.js';
2
2
  import { TABLE_STYLE_ELEMENT_TYPES } from '../../core/table-style.js';
3
- import { AuthoringError } from '../../errors.js';
4
- import { decodeEntities } from '../../xml/xml-read.js';
5
- import { assertRepresentable, escapeAttr, XML_DECLARATION } from '../../xml/xml.js';
3
+ import { decodeEntities } from '../../xml/xml-scan.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,32 +279,29 @@ 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
- if (alignment.horizontal !== undefined && alignment.horizontal !== 'general') {
304
- parts.push(`horizontal="${checkedToken(alignment.horizontal, isHorizontalAlignment, 'horizontal alignment')}"`);
305
- }
306
- if (alignment.vertical !== undefined) {
307
- parts.push(`vertical="${checkedToken(alignment.vertical, isVerticalAlignment, 'vertical alignment')}"`);
308
- }
309
- if (alignment.textRotation !== undefined && alignment.textRotation !== 0) {
310
- parts.push(`textRotation="${numberAttr(alignment.textRotation)}"`);
311
- }
312
- if (alignment.wrapText)
313
- parts.push('wrapText="1"');
314
- if (alignment.indent !== undefined && alignment.indent !== 0) {
315
- parts.push(`indent="${numberAttr(alignment.indent)}"`);
316
- }
317
- if (alignment.shrinkToFit)
318
- parts.push('shrinkToFit="1"');
319
- if (alignment.readingOrder !== undefined && alignment.readingOrder !== 0) {
320
- parts.push(`readingOrder="${numberAttr(alignment.readingOrder)}"`);
284
+ for (const facet of ALIGNMENT_FACETS) {
285
+ switch (facet.kind) {
286
+ case 'token': {
287
+ const value = alignment[facet.key];
288
+ if (value === undefined || value === facet.omit)
289
+ break;
290
+ parts.push(`${facet.key}="${checkedToken(value, facet.isValid, facet.label)}"`);
291
+ break;
292
+ }
293
+ case 'number': {
294
+ const value = alignment[facet.key];
295
+ if (value === undefined || value === 0)
296
+ break;
297
+ parts.push(`${facet.key}="${numberText(value)}"`);
298
+ break;
299
+ }
300
+ case 'flag':
301
+ if (alignment[facet.key])
302
+ parts.push(`${facet.key}="1"`);
303
+ break;
304
+ }
321
305
  }
322
306
  return parts.join(' ');
323
307
  }
@@ -349,15 +333,15 @@ export function fontXml(font, nameTag = 'name') {
349
333
  parts.push(`<vertAlign val="${checkedToken(font.vertAlign, isFontVerticalAlignment, 'font vertical alignment')}"/>`);
350
334
  }
351
335
  if (font.size !== undefined)
352
- parts.push(`<sz val="${numberAttr(font.size)}"/>`);
336
+ parts.push(`<sz val="${numberText(font.size)}"/>`);
353
337
  if (font.color !== undefined)
354
338
  parts.push(`<color ${colorAttrs(font.color)}/>`);
355
339
  if (font.name !== undefined)
356
340
  parts.push(`<${nameTag} val="${escapeAttr(font.name)}"/>`);
357
341
  if (font.family !== undefined)
358
- parts.push(`<family val="${numberAttr(font.family)}"/>`);
342
+ parts.push(`<family val="${numberText(font.family)}"/>`);
359
343
  if (font.charset !== undefined)
360
- parts.push(`<charset val="${numberAttr(font.charset)}"/>`);
344
+ parts.push(`<charset val="${numberText(font.charset)}"/>`);
361
345
  if (font.scheme !== undefined && font.scheme !== 'none')
362
346
  parts.push(`<scheme val="${checkedToken(font.scheme, isFontScheme, 'font scheme')}"/>`);
363
347
  return parts.join('');
@@ -380,18 +364,18 @@ function dxfXml(style) {
380
364
  }
381
365
  function gradientFillXml(fill) {
382
366
  const attrs = (fill.gradient === 'path' ? ' type="path"' : '') +
383
- (fill.degree ? ` degree="${numberAttr(fill.degree)}"` : '') +
367
+ (fill.degree ? ` degree="${numberText(fill.degree)}"` : '') +
384
368
  insetAttr('left', fill.left) +
385
369
  insetAttr('right', fill.right) +
386
370
  insetAttr('top', fill.top) +
387
371
  insetAttr('bottom', fill.bottom);
388
372
  const stops = fill.stops
389
- .map((stop) => `<stop position="${numberAttr(stop.position)}"><color ${colorAttrs(stop.color)}/></stop>`)
373
+ .map((stop) => `<stop position="${numberText(stop.position)}"><color ${colorAttrs(stop.color)}/></stop>`)
390
374
  .join('');
391
375
  return `<gradientFill${attrs}>${stops}</gradientFill>`;
392
376
  }
393
377
  function insetAttr(name, value) {
394
- return value ? ` ${name}="${numberAttr(value)}"` : '';
378
+ return value ? ` ${name}="${numberText(value)}"` : '';
395
379
  }
396
380
  function underlineXml(underline) {
397
381
  if (underline === undefined || underline === false || underline === 'none')
@@ -400,12 +384,6 @@ function underlineXml(underline) {
400
384
  return '<u/>';
401
385
  return `<u val="${checkedToken(underline, isNamedUnderlineStyle, 'underline style')}"/>`;
402
386
  }
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
387
  function borderXml(border) {
410
388
  const attrs = (border.diagonalUp ? ' diagonalUp="1"' : '') + (border.diagonalDown ? ' diagonalDown="1"' : '');
411
389
  return (`<border${attrs}>` +
@@ -1,6 +1,7 @@
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 { parseXml, TextCapture } from '../../xml/xml-read.js';
4
+ import { boolPresent, localName, numInteger } from '../../xml/xml-scan.js';
4
5
  import { boolAttr, escapeAttr, escapeText, XML_DECLARATION } from '../../xml/xml.js';
5
6
  import { NS } from './relationships.js';
6
7
  export function tableXml(table, id) {
@@ -65,10 +66,9 @@ export function parseTable(xml) {
65
66
  let style;
66
67
  let hasAutoFilter = false;
67
68
  const columns = [];
68
- let inTotalsFormula = false;
69
- let totalsFormula = '';
69
+ const totalsFormula = new TextCapture('totalsRowFormula');
70
70
  parseXml(xml, {
71
- onOpen(elementName, attrs) {
71
+ onOpen(elementName, attrs, selfClosing) {
72
72
  switch (localName(elementName)) {
73
73
  case 'table':
74
74
  name = attrs.name ?? attrs.displayName;
@@ -112,27 +112,28 @@ export function parseTable(xml) {
112
112
  break;
113
113
  }
114
114
  case 'totalsRowFormula':
115
- inTotalsFormula = true;
116
- totalsFormula = '';
115
+ totalsFormula.open('totalsRowFormula', selfClosing);
117
116
  break;
118
117
  }
119
118
  },
120
119
  onText(text) {
121
- if (inTotalsFormula)
122
- totalsFormula += text;
120
+ totalsFormula.text(text);
123
121
  },
124
122
  onClose(elementName) {
125
- if (localName(elementName) !== 'totalsRowFormula')
123
+ const formula = totalsFormula.close(localName(elementName));
124
+ if (formula === undefined)
126
125
  return;
127
- inTotalsFormula = false;
128
126
  const column = columns[columns.length - 1];
129
127
  if (column !== undefined)
130
- column.totalsRowFormula = totalsFormula;
128
+ column.totalsRowFormula = formula;
131
129
  },
132
130
  });
133
131
  if (name === undefined || ref === undefined || columns.length === 0)
134
132
  return undefined;
135
- const { top, left, bottom } = decodeRange(ref);
133
+ const decoded = tryDecodeRange(ref);
134
+ if (decoded === undefined)
135
+ return undefined;
136
+ const { top, left, bottom } = decoded;
136
137
  if (top === undefined || left === undefined || bottom === undefined)
137
138
  return undefined;
138
139
  const headerRow = headerRowCount !== 0;
@@ -1,5 +1,5 @@
1
- import { DEFAULT_THEME_COLOR_SCHEME, normalizeThemeColor, } from '../../core/theme.js';
2
- import { decodeEntities } from '../../xml/xml-read.js';
1
+ import { DEFAULT_THEME_COLOR_SCHEME, isThemeColorSlot, normalizeThemeColor, } from '../../core/theme.js';
2
+ import { decodeEntities } from '../../xml/xml-scan.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');
5
5
  export function parseThemeColorScheme(themeXml) {
@@ -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,8 @@
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 { parseXml, TextCapture } from '../../xml/xml-read.js';
4
+ import { boolStrict, decodeSpreadsheetText, localName, numInteger, } from '../../xml/xml-scan.js';
5
+ import { escapeAttr, escapeSpreadsheetText, textAttr, XML_DECLARATION } from '../../xml/xml.js';
5
6
  import { THREADED_COMMENTS_NS } from './namespaces.js';
6
7
  export function parsePersons(xml) {
7
8
  const persons = [];
@@ -27,33 +28,33 @@ export function parseThreadedComments(xml) {
27
28
  const messages = [];
28
29
  let open;
29
30
  let mentions = [];
30
- let inText = false;
31
31
  let text = '';
32
+ const capture = new TextCapture('text');
32
33
  parseXml(xml, {
33
- onOpen(name, attrs) {
34
+ onOpen(name, attrs, selfClosing) {
34
35
  const local = localName(name);
35
36
  if (local === 'threadedComment') {
36
37
  open = attrs;
37
38
  text = '';
38
39
  mentions = [];
39
40
  }
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);
41
+ else if (open !== undefined) {
42
+ capture.open(local, selfClosing);
43
+ if (local === 'mention') {
44
+ const mention = mentionFrom(attrs);
45
+ if (mention !== undefined)
46
+ mentions.push(mention);
47
+ }
47
48
  }
48
49
  },
49
50
  onText(chunk) {
50
- if (inText)
51
- text += chunk;
51
+ capture.text(chunk);
52
52
  },
53
53
  onClose(name) {
54
54
  const local = localName(name);
55
- if (local === 'text') {
56
- inText = false;
55
+ const body = capture.close(local);
56
+ if (body !== undefined) {
57
+ text = body;
57
58
  }
58
59
  else if (local === 'threadedComment') {
59
60
  if (open !== undefined) {
@@ -159,8 +160,8 @@ export function threadedCommentsXml(threads) {
159
160
  `<ThreadedComments xmlns="${THREADED_COMMENTS_NS}">${messages.join('')}</ThreadedComments>`);
160
161
  }
161
162
  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)}"`;
163
+ const date = textAttr('dT', comment.date);
164
+ const person = textAttr('personId', comment.personId);
164
165
  return (`<threadedComment ref="${escapeAttr(ref)}"${date}${person} id="${escapeAttr(comment.id)}"${tail}>` +
165
166
  `<text>${escapeSpreadsheetText(comment.text)}</text>` +
166
167
  mentionsXml(comment.mentions) +
@@ -184,8 +185,8 @@ function writableOffset(value) {
184
185
  }
185
186
  export function personsXml(persons) {
186
187
  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)}"`;
188
+ const userId = textAttr('userId', person.userId);
189
+ const providerId = textAttr('providerId', person.providerId);
189
190
  return (`<person displayName="${escapeAttr(person.displayName)}"` +
190
191
  ` id="${escapeAttr(person.id)}"${userId}${providerId}/>`);
191
192
  });