@shbernal/ts-xlsx 3.0.0 → 3.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 (277) hide show
  1. package/README.md +34 -24
  2. package/dist/bytes.d.ts +10 -2
  3. package/dist/bytes.js +22 -0
  4. package/dist/core/address.d.ts +96 -2
  5. package/dist/core/address.js +87 -33
  6. package/dist/core/array-formula-ranges.d.ts +33 -0
  7. package/dist/core/array-formula-ranges.js +62 -0
  8. package/dist/core/autofilter.d.ts +4 -3
  9. package/dist/core/autofilter.js +31 -30
  10. package/dist/core/axis-handle.d.ts +2 -0
  11. package/dist/core/axis-handle.js +5 -2
  12. package/dist/core/cell.d.ts +4 -3
  13. package/dist/core/cell.js +4 -6
  14. package/dist/core/clone.d.ts +29 -0
  15. package/dist/core/clone.js +24 -0
  16. package/dist/core/color-resolution.js +4 -6
  17. package/dist/core/column.d.ts +1 -0
  18. package/dist/core/column.js +6 -2
  19. package/dist/core/comment-thread.d.ts +7 -3
  20. package/dist/core/comment-thread.js +2 -1
  21. package/dist/core/conditional-formatting-overlay.d.ts +11 -1
  22. package/dist/core/conditional-formatting-overlay.js +30 -3
  23. package/dist/core/conditional-formatting.d.ts +107 -11
  24. package/dist/core/conditional-formatting.js +76 -19
  25. package/dist/core/containers.d.ts +12 -0
  26. package/dist/core/containers.js +5 -0
  27. package/dist/core/data-validation-overlay.d.ts +7 -1
  28. package/dist/core/data-validation-overlay.js +19 -4
  29. package/dist/core/data-validation.d.ts +22 -0
  30. package/dist/core/data-validation.js +30 -4
  31. package/dist/core/date-format.d.ts +20 -0
  32. package/dist/core/date-format.js +170 -0
  33. package/dist/core/date.d.ts +43 -8
  34. package/dist/core/date.js +19 -3
  35. package/dist/core/formula-references.d.ts +26 -0
  36. package/dist/core/formula-references.js +173 -0
  37. package/dist/core/formula-scan.d.ts +2 -0
  38. package/dist/core/formula-scan.js +58 -0
  39. package/dist/core/formula.d.ts +76 -17
  40. package/dist/core/formula.js +87 -80
  41. package/dist/core/function-values.d.ts +4 -0
  42. package/dist/core/function-values.js +58 -0
  43. package/dist/core/future-functions.d.ts +3 -0
  44. package/dist/core/{modern-functions.js → future-functions.js} +119 -88
  45. package/dist/core/grid-edits.d.ts +30 -3
  46. package/dist/core/grid-edits.js +248 -81
  47. package/dist/core/grid-shift.d.ts +58 -9
  48. package/dist/core/grid-shift.js +26 -4
  49. package/dist/core/hyperlink.d.ts +39 -0
  50. package/dist/core/hyperlink.js +88 -0
  51. package/dist/core/image.d.ts +49 -14
  52. package/dist/core/image.js +16 -5
  53. package/dist/core/internal.d.ts +10 -2
  54. package/dist/core/limits.d.ts +25 -0
  55. package/dist/core/limits.js +4 -0
  56. package/dist/core/merge.d.ts +18 -4
  57. package/dist/core/merge.js +33 -20
  58. package/dist/core/page-setup.d.ts +26 -11
  59. package/dist/core/pivot-table.d.ts +49 -10
  60. package/dist/core/pivot-table.js +112 -60
  61. package/dist/core/protection.d.ts +66 -4
  62. package/dist/core/protection.js +6 -11
  63. package/dist/core/range.js +34 -30
  64. package/dist/core/row-input.js +2 -2
  65. package/dist/core/row.d.ts +1 -0
  66. package/dist/core/row.js +6 -2
  67. package/dist/core/structured-reference.d.ts +7 -0
  68. package/dist/core/structured-reference.js +7 -0
  69. package/dist/core/style.d.ts +85 -70
  70. package/dist/core/style.js +92 -28
  71. package/dist/core/table-style.d.ts +1 -1
  72. package/dist/core/table-style.js +4 -4
  73. package/dist/core/table.d.ts +64 -16
  74. package/dist/core/table.js +87 -40
  75. package/dist/core/theme.d.ts +2 -0
  76. package/dist/core/theme.js +9 -5
  77. package/dist/core/value.d.ts +60 -28
  78. package/dist/core/value.js +48 -43
  79. package/dist/core/workbook-media.d.ts +34 -0
  80. package/dist/core/workbook-media.js +44 -0
  81. package/dist/core/workbook-styles.d.ts +83 -1
  82. package/dist/core/workbook-styles.js +4 -0
  83. package/dist/core/workbook-theme.js +5 -3
  84. package/dist/core/workbook-vba.js +3 -2
  85. package/dist/core/workbook.d.ts +41 -3
  86. package/dist/core/workbook.js +46 -41
  87. package/dist/core/worksheet-comments.d.ts +2 -1
  88. package/dist/core/worksheet-comments.js +20 -13
  89. package/dist/core/worksheet-merges.d.ts +49 -0
  90. package/dist/core/worksheet-merges.js +65 -0
  91. package/dist/core/worksheet-model.d.ts +10 -2
  92. package/dist/core/worksheet-model.js +28 -9
  93. package/dist/core/worksheet-pictures.d.ts +3 -3
  94. package/dist/core/worksheet-pictures.js +7 -6
  95. package/dist/core/worksheet.d.ts +165 -27
  96. package/dist/core/worksheet.js +206 -89
  97. package/dist/customui/ribbon.js +2 -1
  98. package/dist/entries/core.d.ts +21 -14
  99. package/dist/entries/core.js +14 -7
  100. package/dist/entries/errors.d.ts +1 -0
  101. package/dist/entries/errors.js +1 -0
  102. package/dist/entries/xlsx.d.ts +1 -1
  103. package/dist/errors.d.ts +64 -0
  104. package/dist/errors.js +22 -0
  105. package/dist/hex.d.ts +8 -0
  106. package/dist/hex.js +3 -0
  107. package/dist/io/cell-metadata/metadata.d.ts +42 -0
  108. package/dist/io/cell-metadata/metadata.js +35 -0
  109. package/dist/io/cell-metadata/rich-values.d.ts +11 -0
  110. package/dist/io/cell-metadata/rich-values.js +80 -0
  111. package/dist/io/csv/delimiter.d.ts +21 -0
  112. package/dist/io/csv/delimiter.js +18 -0
  113. package/dist/io/csv/errors.d.ts +14 -0
  114. package/dist/io/csv/errors.js +5 -0
  115. package/dist/io/csv/read.d.ts +6 -1
  116. package/dist/io/csv/read.js +51 -22
  117. package/dist/io/csv/write.d.ts +19 -5
  118. package/dist/io/csv/write.js +24 -44
  119. package/dist/io/opc/inflate.js +14 -0
  120. package/dist/io/opc/namespaces.d.ts +16 -0
  121. package/dist/io/opc/namespaces.js +4 -0
  122. package/dist/io/opc/part-paths.d.ts +9 -0
  123. package/dist/io/opc/part-paths.js +5 -5
  124. package/dist/io/opc/read-opc.d.ts +58 -14
  125. package/dist/io/opc/read-opc.js +49 -21
  126. package/dist/io/opc/read-options.d.ts +5 -1
  127. package/dist/io/opc/sniff-format.d.ts +3 -3
  128. package/dist/io/opc/sniff-format.js +2 -2
  129. package/dist/io/opc/strict-relationships.d.ts +2 -0
  130. package/dist/io/opc/strict-relationships.js +12 -0
  131. package/dist/io/read-policy/column-budget.d.ts +26 -0
  132. package/dist/io/read-policy/column-budget.js +18 -0
  133. package/dist/io/read-policy/read-repair.d.ts +74 -0
  134. package/dist/io/read-policy/read-repair.js +119 -0
  135. package/dist/io/style/cell-style-resolution.d.ts +25 -0
  136. package/dist/io/style/cell-style-resolution.js +30 -0
  137. package/dist/io/style/xf-style.d.ts +23 -1
  138. package/dist/io/style/xf-style.js +8 -0
  139. package/dist/io/xlsb/formula.d.ts +2 -2
  140. package/dist/io/xlsb/formula.js +52 -35
  141. package/dist/io/xlsb/primitives.js +8 -14
  142. package/dist/io/xlsb/ptg-functions.d.ts +19 -10
  143. package/dist/io/xlsb/ptg-functions.js +495 -596
  144. package/dist/io/xlsb/read-metadata.d.ts +7 -0
  145. package/dist/io/xlsb/read-metadata.js +69 -0
  146. package/dist/io/xlsb/read-styles.js +45 -81
  147. package/dist/io/xlsb/read-worksheet.d.ts +26 -1
  148. package/dist/io/xlsb/read-worksheet.js +148 -106
  149. package/dist/io/xlsb/read.d.ts +6 -3
  150. package/dist/io/xlsb/read.js +70 -31
  151. package/dist/io/xlsb/record-stream.d.ts +30 -0
  152. package/dist/io/xlsb/record-stream.js +20 -0
  153. package/dist/io/xlsb/record-types.d.ts +14 -5
  154. package/dist/io/xlsb/record-types.js +14 -5
  155. package/dist/io/xlsb/sheet-protection.d.ts +20 -0
  156. package/dist/io/xlsb/sheet-protection.js +60 -0
  157. package/dist/io/xlsx/cell-accumulator.d.ts +24 -9
  158. package/dist/io/xlsx/cell-accumulator.js +74 -21
  159. package/dist/io/xlsx/cell-metadata.d.ts +39 -0
  160. package/dist/io/xlsx/cell-metadata.js +162 -0
  161. package/dist/io/xlsx/cell-value.d.ts +19 -5
  162. package/dist/io/xlsx/cell-value.js +45 -30
  163. package/dist/io/xlsx/color-xml.js +3 -2
  164. package/dist/io/xlsx/column-span.d.ts +11 -0
  165. package/dist/io/xlsx/column-span.js +9 -0
  166. package/dist/io/xlsx/comments.d.ts +9 -30
  167. package/dist/io/xlsx/comments.js +25 -83
  168. package/dist/io/xlsx/conditional-formatting.d.ts +43 -23
  169. package/dist/io/xlsx/conditional-formatting.js +437 -138
  170. package/dist/io/xlsx/data-validation.d.ts +12 -7
  171. package/dist/io/xlsx/data-validation.js +67 -53
  172. package/dist/io/xlsx/edit-vba.d.ts +7 -2
  173. package/dist/io/xlsx/edit-vba.js +52 -52
  174. package/dist/io/xlsx/font-xml.d.ts +7 -0
  175. package/dist/io/xlsx/font-xml.js +104 -0
  176. package/dist/io/xlsx/hyperlinks.d.ts +19 -21
  177. package/dist/io/xlsx/hyperlinks.js +12 -32
  178. package/dist/io/xlsx/images.d.ts +49 -16
  179. package/dist/io/xlsx/images.js +149 -50
  180. package/dist/io/xlsx/namespaces.d.ts +24 -0
  181. package/dist/io/xlsx/namespaces.js +11 -0
  182. package/dist/io/xlsx/package-plan.d.ts +60 -15
  183. package/dist/io/xlsx/package-plan.js +83 -18
  184. package/dist/io/xlsx/part-names.d.ts +6 -8
  185. package/dist/io/xlsx/part-names.js +3 -7
  186. package/dist/io/xlsx/pivot.d.ts +5 -5
  187. package/dist/io/xlsx/pivot.js +26 -24
  188. package/dist/io/xlsx/preserved-splices.d.ts +6 -0
  189. package/dist/io/xlsx/preserved-splices.js +74 -0
  190. package/dist/io/xlsx/read-comments.d.ts +29 -0
  191. package/dist/io/xlsx/read-comments.js +75 -0
  192. package/dist/io/xlsx/read-parts.d.ts +59 -0
  193. package/dist/io/xlsx/read-parts.js +259 -0
  194. package/dist/io/xlsx/read-pivot.js +11 -4
  195. package/dist/io/xlsx/{rich-runs.d.ts → read-rich-runs.d.ts} +8 -5
  196. package/dist/io/xlsx/{rich-runs.js → read-rich-runs.js} +24 -4
  197. package/dist/io/xlsx/read-rows.d.ts +35 -11
  198. package/dist/io/xlsx/read-rows.js +73 -49
  199. package/dist/io/xlsx/read-shared-strings.js +3 -2
  200. package/dist/io/xlsx/read-styles.d.ts +0 -30
  201. package/dist/io/xlsx/read-styles.js +45 -105
  202. package/dist/io/xlsx/read-workbook-xml.d.ts +42 -0
  203. package/dist/io/xlsx/read-workbook-xml.js +175 -0
  204. package/dist/io/xlsx/read-worksheet.d.ts +9 -3
  205. package/dist/io/xlsx/read-worksheet.js +70 -177
  206. package/dist/io/xlsx/read.d.ts +3 -14
  207. package/dist/io/xlsx/read.js +74 -411
  208. package/dist/io/xlsx/relationships.d.ts +3 -0
  209. package/dist/io/xlsx/relationships.js +4 -0
  210. package/dist/io/xlsx/rich-text.js +1 -1
  211. package/dist/io/xlsx/row-position.d.ts +20 -0
  212. package/dist/io/xlsx/row-position.js +11 -0
  213. package/dist/io/xlsx/row-xml.d.ts +139 -0
  214. package/dist/io/xlsx/row-xml.js +222 -0
  215. package/dist/io/xlsx/shared-formulas.js +2 -2
  216. package/dist/io/xlsx/sheet-properties.d.ts +10 -1
  217. package/dist/io/xlsx/sheet-properties.js +182 -20
  218. package/dist/io/xlsx/strict-parts.d.ts +7 -0
  219. package/dist/io/xlsx/strict-parts.js +171 -0
  220. package/dist/io/xlsx/style-elements.d.ts +41 -0
  221. package/dist/io/xlsx/style-elements.js +136 -0
  222. package/dist/io/xlsx/styles.d.ts +17 -3
  223. package/dist/io/xlsx/styles.js +13 -209
  224. package/dist/io/xlsx/tables.d.ts +10 -2
  225. package/dist/io/xlsx/tables.js +55 -52
  226. package/dist/io/xlsx/theme-xml.d.ts +32 -4
  227. package/dist/io/xlsx/theme-xml.js +106 -71
  228. package/dist/io/xlsx/threaded-comments.d.ts +14 -11
  229. package/dist/io/xlsx/threaded-comments.js +16 -8
  230. package/dist/io/xlsx/workbook-xml.d.ts +45 -6
  231. package/dist/io/xlsx/workbook-xml.js +62 -54
  232. package/dist/io/xlsx/worksheet-xml.d.ts +26 -70
  233. package/dist/io/xlsx/worksheet-xml.js +69 -282
  234. package/dist/io/xlsx/write-stream.d.ts +90 -19
  235. package/dist/io/xlsx/write-stream.js +80 -25
  236. package/dist/io/xlsx/write.d.ts +12 -6
  237. package/dist/io/xlsx/write.js +226 -123
  238. package/dist/rel-type.d.ts +25 -0
  239. package/dist/rel-type.js +9 -0
  240. package/dist/token-set.d.ts +13 -0
  241. package/dist/token-set.js +4 -1
  242. package/dist/vba/bytes.d.ts +16 -5
  243. package/dist/vba/bytes.js +27 -6
  244. package/dist/vba/cfb-format.d.ts +55 -0
  245. package/dist/vba/cfb-format.js +35 -0
  246. package/dist/vba/cfb-writer.d.ts +8 -0
  247. package/dist/vba/cfb-writer.js +40 -40
  248. package/dist/vba/cfb.d.ts +21 -4
  249. package/dist/vba/cfb.js +124 -90
  250. package/dist/vba/codepage.js +2 -1
  251. package/dist/vba/dir-records.d.ts +6 -0
  252. package/dist/vba/dir-records.js +10 -2
  253. package/dist/vba/errors.d.ts +11 -4
  254. package/dist/vba/index.d.ts +0 -2
  255. package/dist/vba/index.js +0 -2
  256. package/dist/vba/ms-ovba.d.ts +6 -2
  257. package/dist/vba/ms-ovba.js +66 -22
  258. package/dist/vba/project-editor.js +129 -119
  259. package/dist/vba/project.d.ts +15 -1
  260. package/dist/vba/project.js +38 -13
  261. package/dist/vba/vba-encoding.d.ts +4 -10
  262. package/dist/vba/vba-encoding.js +3 -17
  263. package/dist/xml/xml-attrs.d.ts +37 -0
  264. package/dist/xml/xml-attrs.js +30 -0
  265. package/dist/xml/xml-chars.d.ts +40 -0
  266. package/dist/xml/xml-chars.js +12 -0
  267. package/dist/xml/xml-namespaces.d.ts +31 -0
  268. package/dist/xml/xml-namespaces.js +50 -0
  269. package/dist/xml/xml-read.d.ts +101 -11
  270. package/dist/xml/xml-read.js +150 -18
  271. package/dist/xml/xml-scan.d.ts +25 -34
  272. package/dist/xml/xml-scan.js +69 -46
  273. package/dist/xml/xml.d.ts +59 -26
  274. package/dist/xml/xml.js +45 -23
  275. package/package.json +15 -9
  276. package/skills/ts-xlsx-upstream/SKILL.md +21 -13
  277. package/dist/core/modern-functions.d.ts +0 -1
@@ -1,19 +1,28 @@
1
- import { AuthoringError } from '../errors.js';
1
+ import { AuthoringError, quoted } from '../errors.js';
2
2
  import { tokenSet } from '../token-set.js';
3
- import { decodeCellRef, encodeAddress } from './address.js';
3
+ import { decodeCellRef, encodeAddress, encodeRect, MAX_COLUMN, MAX_ROW, nameReadsAsReference, numberToColumn, } from './address.js';
4
+ import { cloneWith } from './clone.js';
4
5
  import { isDeletedSpan, shiftIndex } from './grid-shift.js';
6
+ import { INTERNAL } from './internal.js';
7
+ import { MAX_TABLE_NAME_LENGTH, TABLE_NAME_PATTERN } from './limits.js';
8
+ import { structuredColumnReference } from './structured-reference.js';
9
+ const STYLE_INFO_CLONE = {
10
+ name: (name) => (name === 'None' ? undefined : name),
11
+ showFirstColumn: 'value',
12
+ showLastColumn: 'value',
13
+ showRowStripes: 'value',
14
+ showColumnStripes: 'value',
15
+ };
16
+ export const TABLE_STYLE_FLAGS = [
17
+ 'showFirstColumn',
18
+ 'showLastColumn',
19
+ 'showRowStripes',
20
+ 'showColumnStripes',
21
+ ];
5
22
  function cloneStyleInfo(style) {
6
- const clone = {};
7
- if (style.name !== undefined && style.name !== 'None')
8
- clone.name = style.name;
9
- if (style.showFirstColumn !== undefined)
10
- clone.showFirstColumn = style.showFirstColumn;
11
- if (style.showLastColumn !== undefined)
12
- clone.showLastColumn = style.showLastColumn;
13
- if (style.showRowStripes !== undefined)
14
- clone.showRowStripes = style.showRowStripes;
15
- if (style.showColumnStripes !== undefined)
16
- clone.showColumnStripes = style.showColumnStripes;
23
+ const clone = cloneWith(style, STYLE_INFO_CLONE);
24
+ if (clone.name === undefined)
25
+ delete clone.name;
17
26
  return clone;
18
27
  }
19
28
  export const TOTALS_ROW_SUBTOTAL_CODE = {
@@ -38,7 +47,6 @@ export const isTotalsRowFunction = tokenSet({
38
47
  custom: true,
39
48
  none: true,
40
49
  });
41
- const IDENTIFIER = /^[\p{L}\\_][\p{L}\p{N}._]*$/u;
42
50
  function disambiguateColumnNames(columns) {
43
51
  const seen = new Set();
44
52
  return columns.map((column) => {
@@ -50,18 +58,20 @@ function disambiguateColumnNames(columns) {
50
58
  });
51
59
  }
52
60
  function validateTableName(name) {
53
- if (name.length === 0 || name.length > 255) {
54
- throw new RangeError(`table name ${JSON.stringify(name)} must be between 1 and 255 characters`);
61
+ if (name.length === 0 || name.length > MAX_TABLE_NAME_LENGTH) {
62
+ throw new RangeError(`table name ${quoted(name)} must be between 1 and ${MAX_TABLE_NAME_LENGTH} characters`);
55
63
  }
56
- if (!IDENTIFIER.test(name)) {
57
- throw new SyntaxError(`table name ${JSON.stringify(name)} is not a valid Excel identifier: it must start with a letter, ` +
64
+ if (!TABLE_NAME_PATTERN.test(name)) {
65
+ throw new SyntaxError(`table name ${quoted(name)} is not a valid Excel identifier: it must start with a letter, ` +
58
66
  'underscore, or backslash and contain only letters, digits, periods, and underscores');
59
67
  }
68
+ if (nameReadsAsReference(name)) {
69
+ throw new SyntaxError(`table name ${quoted(name)} reads as a cell reference, an R1C1 reference or a boolean, ` +
70
+ 'which Excel does not accept as a name');
71
+ }
60
72
  }
61
73
  export class Table {
62
74
  name;
63
- displayName;
64
- columns;
65
75
  headerRow;
66
76
  totalsRow;
67
77
  totalsRowShown;
@@ -70,28 +80,46 @@ export class Table {
70
80
  #anchorCol;
71
81
  #anchorRow;
72
82
  #dataRowCount;
83
+ #columns;
73
84
  #grid;
85
+ [INTERNAL] = {
86
+ rewriteFormulas: (rewrite) => {
87
+ this.#columns = this.#columns.map((column) => {
88
+ const { calculatedColumnFormula: calculated, totalsRowFormula: totals } = column;
89
+ const nextCalculated = calculated === undefined ? undefined : rewrite(calculated);
90
+ const nextTotals = totals === undefined ? undefined : rewrite(totals);
91
+ if (nextCalculated === calculated && nextTotals === totals)
92
+ return column;
93
+ return {
94
+ ...column,
95
+ ...(nextCalculated === undefined ? {} : { calculatedColumnFormula: nextCalculated }),
96
+ ...(nextTotals === undefined ? {} : { totalsRowFormula: nextTotals }),
97
+ };
98
+ });
99
+ },
100
+ };
74
101
  constructor(options, grid) {
75
102
  validateTableName(options.name);
76
103
  if (options.columns.length === 0) {
77
- throw new AuthoringError(`table "${options.name}" must declare at least one column`);
104
+ throw new AuthoringError(`table ${quoted(options.name)} must declare at least one column`);
78
105
  }
79
106
  if (!Number.isInteger(options.rowCount) || options.rowCount < 0) {
80
- throw new RangeError(`table "${options.name}" has an invalid data-row count (${options.rowCount})`);
107
+ throw new RangeError(`table ${quoted(options.name)} has an invalid data-row count (${options.rowCount})`);
81
108
  }
82
109
  let anchor;
83
110
  try {
84
111
  anchor = decodeCellRef(options.ref);
85
112
  }
86
113
  catch (cause) {
87
- throw new SyntaxError(`table ref "${options.ref}" must anchor at a single cell (e.g. "A1")`, {
114
+ if (cause instanceof RangeError)
115
+ throw cause;
116
+ throw new SyntaxError(`table ref ${quoted(options.ref)} must anchor at a single cell (e.g. "A1")`, {
88
117
  cause,
89
118
  });
90
119
  }
91
120
  const { col, row } = anchor;
92
121
  this.name = options.name;
93
- this.displayName = options.displayName ?? options.name;
94
- this.columns = disambiguateColumnNames(options.columns);
122
+ this.#columns = disambiguateColumnNames(options.columns);
95
123
  this.headerRow = options.headerRow ?? true;
96
124
  this.totalsRow = options.totalsRow ?? false;
97
125
  this.totalsRowShown = options.totalsRowShown;
@@ -102,11 +130,22 @@ export class Table {
102
130
  this.#dataRowCount = options.rowCount;
103
131
  this.#grid = grid;
104
132
  if (this.#rowSpan < 1) {
105
- throw new AuthoringError(`table "${this.name}" has no rows: it needs a header row or at least one data row`);
133
+ throw new AuthoringError(`table ${quoted(this.name)} has no rows: it needs a header row or at least one data row`);
134
+ }
135
+ if (this.#right > MAX_COLUMN) {
136
+ throw new RangeError(`table ${quoted(this.name)} spans ${this.columns.length} columns from ` +
137
+ `${numberToColumn(this.#anchorCol)}, past the last column (${numberToColumn(MAX_COLUMN)})`);
138
+ }
139
+ if (this.#bottom > MAX_ROW) {
140
+ throw new RangeError(`table ${quoted(this.name)} spans ${this.#rowSpan} rows from ${this.#anchorRow}, ` +
141
+ `past the last row (${MAX_ROW})`);
106
142
  }
107
143
  if (grid !== undefined)
108
144
  this.#materializeFrame(grid);
109
145
  }
146
+ get columns() {
147
+ return this.#columns;
148
+ }
110
149
  get columnCount() {
111
150
  return this.columns.length;
112
151
  }
@@ -115,13 +154,13 @@ export class Table {
115
154
  }
116
155
  addRow(values = []) {
117
156
  if (values.length > this.columnCount) {
118
- throw new RangeError(`row has ${values.length} values but table "${this.name}" has ${this.columnCount} columns`);
157
+ throw new RangeError(`row has ${values.length} values but table ${quoted(this.name)} has ${this.columnCount} columns`);
119
158
  }
120
159
  const target = this.#anchorRow + (this.headerRow ? 1 : 0) + this.#dataRowCount;
121
160
  const grid = this.#grid;
122
161
  if (this.totalsRow) {
123
162
  if (grid === undefined) {
124
- throw new AuthoringError(`table "${this.name}" is not attached to a worksheet: cannot relocate its totals row to append a data row`);
163
+ throw new AuthoringError(`table ${quoted(this.name)} is not attached to a worksheet: cannot relocate its totals row to append a data row`);
125
164
  }
126
165
  grid.insertRow(target);
127
166
  this.#writeRow(grid, target, values);
@@ -129,7 +168,7 @@ export class Table {
129
168
  }
130
169
  if (values.length > 0) {
131
170
  if (grid === undefined) {
132
- throw new AuthoringError(`table "${this.name}" is not attached to a worksheet: cannot write appended row values`);
171
+ throw new AuthoringError(`table ${quoted(this.name)} is not attached to a worksheet: cannot write appended row values`);
133
172
  }
134
173
  this.#writeRow(grid, target, values);
135
174
  }
@@ -165,7 +204,7 @@ export class Table {
165
204
  const code = TOTALS_ROW_SUBTOTAL_CODE[column.totalsRowFunction];
166
205
  if (code !== undefined) {
167
206
  grid.writeCell(bottom, col, {
168
- formula: `SUBTOTAL(${code},${this.name}[${column.name}])`,
207
+ formula: `SUBTOTAL(${code},${structuredColumnReference(this.name, column.name)})`,
169
208
  });
170
209
  }
171
210
  else if (column.totalsRowFunction === 'custom' && column.totalsRowFormula !== undefined) {
@@ -174,11 +213,11 @@ export class Table {
174
213
  });
175
214
  }
176
215
  }
177
- shiftRows(start, count, delta) {
178
- if (isDeletedSpan(this.#anchorRow, this.#bottom, start, count))
216
+ shiftRows(splice) {
217
+ if (isDeletedSpan(this.#anchorRow, this.#bottom, splice))
179
218
  return false;
180
- const top = shiftIndex(this.#anchorRow, start, count, delta, 'row');
181
- const bottom = shiftIndex(this.#bottom, start, count, delta, 'row');
219
+ const top = shiftIndex(this.#anchorRow, splice);
220
+ const bottom = shiftIndex(this.#bottom, splice);
182
221
  const span = bottom - top + 1;
183
222
  const fixedRows = (this.headerRow ? 1 : 0) + (this.totalsRow ? 1 : 0);
184
223
  const dataRows = span - fixedRows;
@@ -188,15 +227,18 @@ export class Table {
188
227
  this.#dataRowCount = dataRows;
189
228
  return true;
190
229
  }
191
- shiftColumns(start, count, delta) {
192
- if (this.#anchorCol >= start + count)
193
- this.#anchorCol += delta;
230
+ shiftColumns(splice) {
231
+ if (isDeletedSpan(this.#anchorCol, this.#right, splice))
232
+ return false;
233
+ const anchor = shiftIndex(this.#anchorCol, splice);
234
+ if (anchor + this.columns.length - 1 > MAX_COLUMN)
235
+ return false;
236
+ this.#anchorCol = anchor;
194
237
  return true;
195
238
  }
196
239
  get options() {
197
240
  const options = {
198
241
  name: this.name,
199
- displayName: this.displayName,
200
242
  ref: encodeAddress(this.#anchorCol, this.#anchorRow),
201
243
  columns: this.columns.map((column) => ({ ...column })),
202
244
  rowCount: this.#dataRowCount,
@@ -211,13 +253,18 @@ export class Table {
211
253
  return options;
212
254
  }
213
255
  get range() {
214
- return `${encodeAddress(this.#anchorCol, this.#anchorRow)}:${encodeAddress(this.#right, this.#bottom)}`;
256
+ return encodeRect({
257
+ top: this.#anchorRow,
258
+ left: this.#anchorCol,
259
+ bottom: this.#bottom,
260
+ right: this.#right,
261
+ });
215
262
  }
216
263
  get autoFilterRef() {
217
264
  if (!this.autoFilter)
218
265
  return undefined;
219
266
  const bottom = this.#anchorRow + this.#dataRowCount;
220
- return `${encodeAddress(this.#anchorCol, this.#anchorRow)}:${encodeAddress(this.#right, bottom)}`;
267
+ return encodeRect({ top: this.#anchorRow, left: this.#anchorCol, bottom, right: this.#right });
221
268
  }
222
269
  get region() {
223
270
  return { top: this.#anchorRow, left: this.#anchorCol, bottom: this.#bottom, right: this.#right };
@@ -48,6 +48,8 @@ export interface ThemeOverrides {
48
48
  readonly colors?: Readonly<Partial<Record<ThemeColorSlot, string>>> | undefined;
49
49
  readonly fonts?: ThemeFontScheme | undefined;
50
50
  }
51
+ /** Whether a value is the bare 6-hex RGB a theme colour slot holds, in either casing. */
52
+ export declare function isThemeRgb(value: string): boolean;
51
53
  /** Reduce an authored theme colour to the bare 6-hex RGB DrawingML wants.
52
54
  *
53
55
  * A theme colour is a bare 6-hex RGB: DrawingML has no alpha channel on `<a:srgbClr val>`. The two
@@ -1,3 +1,5 @@
1
+ import { quoted } from '../errors.js';
2
+ import { parseArgb } from './style.js';
1
3
  export const THEME_COLOR_SLOTS = [
2
4
  'lt1',
3
5
  'dk1',
@@ -35,11 +37,13 @@ export const DEFAULT_THEME_FONTS = {
35
37
  major: 'Calibri Light',
36
38
  minor: OFFICE_BODY_FACE,
37
39
  };
40
+ export function isThemeRgb(value) {
41
+ return /^[0-9A-Fa-f]{6}$/.test(value);
42
+ }
38
43
  export function normalizeThemeColor(value) {
39
- const hex = value.startsWith('#') ? value.slice(1) : value;
40
- const rgb = hex.length === 8 ? hex.slice(2) : hex;
41
- if (!/^[0-9a-fA-F]{6}$/.test(rgb)) {
42
- throw new SyntaxError(`Invalid theme colour ${JSON.stringify(value)}: expected 6 hexadecimal digits (RRGGBB)`);
44
+ const argb = parseArgb(value);
45
+ if (argb === undefined) {
46
+ throw new SyntaxError(`invalid theme colour ${quoted(value)}: expected RRGGBB or AARRGGBB hexadecimal digits, optionally after '#'`);
43
47
  }
44
- return rgb.toUpperCase();
48
+ return argb.slice(2).toUpperCase();
45
49
  }
@@ -1,5 +1,5 @@
1
1
  import type { Font } from './style.ts';
2
- /** The observable kind of a cell's value. Both formula shapes report as `Formula`. */
2
+ /** The observable kind of a cell's value. Every formula kind reports as `Formula`. */
3
3
  export declare const ValueType: {
4
4
  readonly Null: 'null';
5
5
  readonly Number: 'number';
@@ -9,12 +9,29 @@ export declare const ValueType: {
9
9
  readonly Error: 'error';
10
10
  readonly Formula: 'formula';
11
11
  readonly RichText: 'richText';
12
- readonly Hyperlink: 'hyperlink';
13
12
  };
14
13
  export type ValueType = (typeof ValueType)[keyof typeof ValueType];
15
- /** The canonical Excel error literals a cell (or formula result) can carry. */
16
- export declare const ERROR_CODES: readonly ['#N/A', '#REF!', '#NAME?', '#DIV/0!', '#NULL!', '#VALUE!', '#NUM!', '#SPILL!', '#CALC!', '#GETTING_DATA'];
14
+ /**
15
+ * The errors a cell (or formula result) can hold.
16
+ *
17
+ * They are stored two ways. The classic seven, `#GETTING_DATA` and `#BUSY!` are literals: a typed cell
18
+ * holds the spelling and Excel reads it back as that error. Excel has no literal for `#SPILL!`,
19
+ * `#CONNECT!`, `#BLOCKED!`, `#UNKNOWN!`, `#FIELD!` and `#CALC!`, and stores each as `#VALUE!` beside a
20
+ * rich value naming the real one, which is how they are read and written here. `#PYTHON!`, `#EXTERNAL!`
21
+ * and `#TIMEOUT!` are not here: Excel reads no literal spelling of them, and no rich value was seen to
22
+ * read back as one of them unambiguously (Excel 16.0 build 20326).
23
+ */
24
+ export declare const ERROR_CODES: readonly ['#N/A', '#REF!', '#NAME?', '#DIV/0!', '#NULL!', '#VALUE!', '#NUM!', '#GETTING_DATA', '#BUSY!', '#SPILL!', '#CONNECT!', '#BLOCKED!', '#UNKNOWN!', '#FIELD!', '#CALC!'];
17
25
  export type ErrorCode = (typeof ERROR_CODES)[number];
26
+ /**
27
+ * The error a spreadsheet puts in place of a reference that has nowhere left to point.
28
+ *
29
+ * Named here rather than in the two formula codecs that produce it. Both of them -- the A1 renderer
30
+ * in `core/formula.ts` and the BIFF12 one in `io/xlsb/formula.ts` -- had a private literal of their
31
+ * own, spelled the same and named differently, for a value {@link ERROR_CODES} already publishes and
32
+ * `isErrorCode` already recognises.
33
+ */
34
+ export declare const REF_ERROR: ErrorCode;
18
35
  /** An in-cell error, e.g. `{error: '#REF!'}`. */
19
36
  export interface ErrorValue {
20
37
  readonly error: ErrorCode;
@@ -28,16 +45,6 @@ export interface RichTextRun {
28
45
  export interface RichTextValue {
29
46
  readonly richText: readonly RichTextRun[];
30
47
  }
31
- /** A hyperlink cell: a URL plus the text (plain or rich) shown in the cell. */
32
- export interface HyperlinkValue {
33
- readonly hyperlink: string;
34
- readonly text: string | RichTextValue;
35
- readonly tooltip?: string;
36
- /** The clickable extent (`'D1:H1'`) when the link spans a range whose top-left corner is this
37
- * cell. Absent for an ordinary single-cell link. The destination and label live on the top-left
38
- * cell; `range` records how far Excel highlights the clickable area so it survives a round-trip. */
39
- readonly range?: string;
40
- }
41
48
  /** The cached result a formula carries: any scalar, a date, or an error. */
42
49
  export type FormulaResult = number | string | boolean | Date | ErrorValue;
43
50
  /** A cell whose value is computed by its own formula. */
@@ -59,6 +66,26 @@ export interface SharedFormulaValue {
59
66
  readonly formula?: string;
60
67
  readonly result?: FormulaResult;
61
68
  }
69
+ /**
70
+ * A cell holding an array formula (`<f t="array">`): one formula whose result fills {@link ref}, a
71
+ * range starting at this cell. The other cells of the range hold only the values the formula
72
+ * produced, which is how Excel stores them, so they are plain values here too.
73
+ *
74
+ * Excel stores two kinds this way. A legacy array formula, entered with Ctrl+Shift+Enter, shows in
75
+ * braces and fills the range it was entered over. A dynamic-array formula is {@link dynamic}: it
76
+ * spills, `ref` is the range its last calculation filled, and no braces are shown. Excel keeps that
77
+ * mark in the workbook's cell metadata rather than on the formula, and a formula that loses it opens
78
+ * as a legacy array formula that no longer spills.
79
+ */
80
+ export interface ArrayFormulaValue {
81
+ readonly shareType: 'array';
82
+ readonly formula: string;
83
+ /** The range the result fills, starting at this cell: `'B1:B3'`, or `'B1'` for one cell. */
84
+ readonly ref: string;
85
+ /** Whether Excel evaluates the formula as a dynamic array rather than a Ctrl+Shift+Enter one. */
86
+ readonly dynamic?: boolean;
87
+ readonly result?: FormulaResult;
88
+ }
62
89
  /**
63
90
  * A cell computed by a What-If-Analysis data table (`<f t="dataTable">`), the OOXML formula kind that
64
91
  * fills a range by re-evaluating a model against a grid of substituted input cells. The library does
@@ -77,25 +104,36 @@ export interface DataTableFormulaValue {
77
104
  readonly r1?: string;
78
105
  /** The second (column) input-cell reference, present for a 2-D table. */
79
106
  readonly r2?: string;
107
+ /**
108
+ * Whether the cell {@link r1} named has been deleted. Excel keeps the reference as it was written and
109
+ * sets this flag, and the table then shows `#REF!`; a row or column delete that takes the input cell
110
+ * does the same here.
111
+ */
112
+ readonly r1Deleted?: boolean;
113
+ /** Whether the cell {@link r2} named has been deleted, as {@link r1Deleted} is for {@link r1}. */
114
+ readonly r2Deleted?: boolean;
80
115
  readonly result?: FormulaResult;
81
116
  }
82
117
  /** Everything a cell's value can be. `null` is the empty cell. */
83
- export type CellValue = null | number | string | boolean | Date | ErrorValue | FormulaValue | SharedFormulaValue | DataTableFormulaValue | RichTextValue | HyperlinkValue;
118
+ export type CellValue = null | number | string | boolean | Date | ErrorValue | FormulaValue | SharedFormulaValue | ArrayFormulaValue | DataTableFormulaValue | RichTextValue;
84
119
  /**
85
120
  * Whether a value is an in-cell error ({@link ErrorValue}). The narrowing counterpart of
86
121
  * `detectValueType(value) === ValueType.Error`: use this one when the branch goes on to read
87
- * `.error`, and {@link detectValueType} when it dispatches over all nine kinds at once.
122
+ * `.error`, and {@link detectValueType} when it dispatches over all eight kinds at once.
88
123
  */
89
124
  export declare function isErrorValue(value: CellValue): value is ErrorValue;
90
125
  /**
91
- * Whether a value is a cell's own formula ({@link FormulaValue}): a master, or a formula
92
- * belonging to no shared group. A shared-formula clone is **not** one of these; see
93
- * {@link isSharedFormulaValue}. Both report as `ValueType.Formula`, so a caller that means "any
94
- * formula-shaped cell" wants {@link detectValueType}, not this.
126
+ * Whether a value is a cell's own plain formula ({@link FormulaValue}): a shared-formula master, or a
127
+ * formula belonging to no group. A shared-formula clone is **not** one of these, and nor is an array
128
+ * formula; see {@link isSharedFormulaValue} and {@link isArrayFormulaValue}. Every formula kind reports
129
+ * as `ValueType.Formula`, so a caller that means "any formula-shaped cell" wants
130
+ * {@link detectValueType}, not this.
95
131
  */
96
132
  export declare function isFormulaValue(value: CellValue): value is FormulaValue;
97
133
  /** Whether a value is a clone participating in a shared formula ({@link SharedFormulaValue}). */
98
134
  export declare function isSharedFormulaValue(value: CellValue): value is SharedFormulaValue;
135
+ /** Whether a value is an array formula, legacy or dynamic ({@link ArrayFormulaValue}). */
136
+ export declare function isArrayFormulaValue(value: CellValue): value is ArrayFormulaValue;
99
137
  /** Whether a value is a What-If-Analysis data-table formula ({@link DataTableFormulaValue}). */
100
138
  export declare function isDataTableFormulaValue(value: CellValue): value is DataTableFormulaValue;
101
139
  /**
@@ -103,11 +141,6 @@ export declare function isDataTableFormulaValue(value: CellValue): value is Data
103
141
  * make before {@link richTextToPlain}, which accepts nothing else.
104
142
  */
105
143
  export declare function isRichTextValue(value: CellValue): value is RichTextValue;
106
- /**
107
- * Whether a value is a hyperlink ({@link HyperlinkValue}). Note that its `text` is itself either
108
- * a string or a {@link RichTextValue}, so reading the label out means one more narrowing.
109
- */
110
- export declare function isHyperlinkValue(value: CellValue): value is HyperlinkValue;
111
144
  /**
112
145
  * Flatten a rich-text value to its plain text by concatenating every run's text in order. This is the
113
146
  * text a consumer that cannot render per-run formatting (a CSV field, a pivot cache entry) sees, and
@@ -128,9 +161,8 @@ export declare function richTextToPlain(value: RichTextValue): string;
128
161
  * - a `Date` → a full ISO-8601 timestamp
129
162
  * - an error → its literal, e.g. `"#REF!"`, the same string the grid shows
130
163
  * - rich text → every run concatenated ({@link richTextToPlain})
131
- * - a hyperlinkits label, never its destination
132
- * - any of the three formula kinds the text of the *cached result*, and `""` when the cell
133
- * carries no cached result: the formula source is not text the sheet ever displayed
164
+ * - any formula kind the text of the *cached result*, and `""` when the cell carries no cached
165
+ * result: the formula source is not text the sheet ever displayed
134
166
  */
135
167
  export declare function cellValueToText(value: CellValue): string;
136
168
  /**
@@ -7,7 +7,6 @@ export const ValueType = {
7
7
  Error: 'error',
8
8
  Formula: 'formula',
9
9
  RichText: 'richText',
10
- Hyperlink: 'hyperlink',
11
10
  };
12
11
  export const ERROR_CODES = [
13
12
  '#N/A',
@@ -17,10 +16,16 @@ export const ERROR_CODES = [
17
16
  '#NULL!',
18
17
  '#VALUE!',
19
18
  '#NUM!',
19
+ '#GETTING_DATA',
20
+ '#BUSY!',
20
21
  '#SPILL!',
22
+ '#CONNECT!',
23
+ '#BLOCKED!',
24
+ '#UNKNOWN!',
25
+ '#FIELD!',
21
26
  '#CALC!',
22
- '#GETTING_DATA',
23
27
  ];
28
+ export const REF_ERROR = '#REF!';
24
29
  const ERROR_SET = new Set(ERROR_CODES);
25
30
  function hasKey(value, key) {
26
31
  return typeof value === 'object' && value !== null && key in value;
@@ -29,76 +34,76 @@ export function isErrorValue(value) {
29
34
  return hasKey(value, 'error');
30
35
  }
31
36
  export function isFormulaValue(value) {
32
- return hasKey(value, 'formula') && !('sharedFormula' in value);
37
+ return hasKey(value, 'formula') && !('sharedFormula' in value) && !('shareType' in value);
33
38
  }
34
39
  export function isSharedFormulaValue(value) {
35
40
  return hasKey(value, 'sharedFormula');
36
41
  }
42
+ export function isArrayFormulaValue(value) {
43
+ return hasKey(value, 'shareType') && value.shareType === 'array';
44
+ }
37
45
  export function isDataTableFormulaValue(value) {
38
46
  return hasKey(value, 'shareType') && value.shareType === 'dataTable';
39
47
  }
40
48
  export function isRichTextValue(value) {
41
49
  return hasKey(value, 'richText');
42
50
  }
43
- export function isHyperlinkValue(value) {
44
- return hasKey(value, 'hyperlink');
45
- }
46
51
  export function richTextToPlain(value) {
47
52
  return value.richText.map((run) => run.text).join('');
48
53
  }
49
54
  export function cellValueToText(value) {
50
- if (value === null)
51
- return '';
52
- switch (typeof value) {
53
- case 'number':
54
- return String(value);
55
- case 'string':
56
- return value;
57
- case 'boolean':
58
- return value ? 'TRUE' : 'FALSE';
59
- default:
60
- break;
61
- }
62
- if (value instanceof Date)
63
- return Number.isNaN(value.getTime()) ? '' : value.toISOString();
64
- if (isHyperlinkValue(value)) {
65
- return typeof value.text === 'string' ? value.text : richTextToPlain(value.text);
66
- }
67
- if (isFormulaValue(value) || isSharedFormulaValue(value) || isDataTableFormulaValue(value)) {
68
- return value.result === undefined ? '' : cellValueToText(value.result);
69
- }
70
- if (isRichTextValue(value))
71
- return richTextToPlain(value);
72
- if (isErrorValue(value))
73
- return value.error;
74
- return unsupportedValue(value);
55
+ return classify(value, TO_TEXT);
75
56
  }
76
57
  export function detectValueType(value) {
58
+ return classify(value, TO_TYPE);
59
+ }
60
+ function classify(value, visit) {
77
61
  if (value === null)
78
- return ValueType.Null;
62
+ return visit.null(value);
79
63
  switch (typeof value) {
80
64
  case 'number':
81
- return ValueType.Number;
65
+ return visit.number(value);
82
66
  case 'string':
83
- return ValueType.String;
67
+ return visit.string(value);
84
68
  case 'boolean':
85
- return ValueType.Boolean;
69
+ return visit.boolean(value);
86
70
  default:
87
71
  break;
88
72
  }
89
73
  if (value instanceof Date)
90
- return ValueType.Date;
91
- if (isHyperlinkValue(value))
92
- return ValueType.Hyperlink;
93
- if (isFormulaValue(value) || isSharedFormulaValue(value) || isDataTableFormulaValue(value)) {
94
- return ValueType.Formula;
74
+ return visit.date(value);
75
+ if (isFormulaValue(value) ||
76
+ isSharedFormulaValue(value) ||
77
+ isArrayFormulaValue(value) ||
78
+ isDataTableFormulaValue(value)) {
79
+ return visit.formula(value);
95
80
  }
96
81
  if (isRichTextValue(value))
97
- return ValueType.RichText;
82
+ return visit.richText(value);
98
83
  if (isErrorValue(value))
99
- return ValueType.Error;
84
+ return visit.error(value);
100
85
  return unsupportedValue(value);
101
86
  }
87
+ const TO_TYPE = {
88
+ null: () => ValueType.Null,
89
+ number: () => ValueType.Number,
90
+ string: () => ValueType.String,
91
+ boolean: () => ValueType.Boolean,
92
+ date: () => ValueType.Date,
93
+ error: () => ValueType.Error,
94
+ formula: () => ValueType.Formula,
95
+ richText: () => ValueType.RichText,
96
+ };
97
+ const TO_TEXT = {
98
+ null: () => '',
99
+ number: (value) => String(value),
100
+ string: (value) => value,
101
+ boolean: (value) => (value ? 'TRUE' : 'FALSE'),
102
+ date: (value) => (Number.isNaN(value.getTime()) ? '' : value.toISOString()),
103
+ error: (value) => value.error,
104
+ formula: (value) => (value.result === undefined ? '' : cellValueToText(value.result)),
105
+ richText: richTextToPlain,
106
+ };
102
107
  function unsupportedValue(value) {
103
108
  throw new TypeError(`unsupported cell value: ${describe(value)}`);
104
109
  }
@@ -115,7 +120,7 @@ export function coerceCellValue(value) {
115
120
  if (value === undefined)
116
121
  return null;
117
122
  detectValueType(value);
118
- if (isFormulaValue(value)) {
123
+ if (isFormulaValue(value) || isArrayFormulaValue(value)) {
119
124
  const formula = stripLeadingEquals(value.formula);
120
125
  return formula === value.formula ? value : { ...value, formula };
121
126
  }
@@ -0,0 +1,34 @@
1
+ import { type WorkbookImage } from './image.ts';
2
+ export declare class WorkbookMedia {
3
+ #private;
4
+ /** The registered images, indexed by the id {@link register} returned. Live, not a copy. */
5
+ get all(): readonly WorkbookImage[];
6
+ /** Look up a registered image by its id, or `undefined` if no image carries that id. */
7
+ get(id: number): WorkbookImage | undefined;
8
+ /**
9
+ * {@link get} for a caller that has no answer for an absent id.
10
+ *
11
+ * An unregistered id is what a sheet belonging to *another* workbook looks like from here, which
12
+ * is why the message names the sheet rather than the id alone. Emitting a package with a drawing
13
+ * pointing at media nobody registered is the silently-broken-image failure this refuses to start.
14
+ *
15
+ * @throws {AuthoringError} if no image carries that id.
16
+ */
17
+ require(id: number, forSheetName: string): WorkbookImage;
18
+ /** Store a picture's bytes and return the id that names them. */
19
+ register(extension: string | undefined, buffer: Uint8Array): number;
20
+ /**
21
+ * Register a picture arriving from elsewhere, re-using an identical one already held.
22
+ *
23
+ * The extension is re-normalised rather than trusted: a hand-built {@link WorkbookImage} may carry
24
+ * `".PNG"` where the registry holds `"png"`, and two spellings of one kind must not read as two
25
+ * pictures.
26
+ *
27
+ * Through the content index rather than a scan. Comparing byte-by-byte against every held picture
28
+ * made importing n distinct images cost n squared byte comparisons: fifty 1 MB pictures carried
29
+ * between workbooks compared about 2.5 GB. The *rule* is unchanged, and `imageContentKey` states
30
+ * why identity here is content and never object identity. The index is built in reverse so the
31
+ * FIRST id registered under a key wins, which is the answer the scan gave.
32
+ */
33
+ registerExisting(image: WorkbookImage): number;
34
+ }
@@ -0,0 +1,44 @@
1
+ import { AuthoringError, quoted } from '../errors.js';
2
+ import { imageContentKey, normalizeImageExtension } from './image.js';
3
+ export class WorkbookMedia {
4
+ #images = [];
5
+ #byContent;
6
+ get all() {
7
+ return this.#images;
8
+ }
9
+ get(id) {
10
+ return this.#images[id];
11
+ }
12
+ require(id, forSheetName) {
13
+ const image = this.#images[id];
14
+ if (image === undefined) {
15
+ throw new AuthoringError(`worksheet ${quoted(forSheetName)} shows image id ${id}, which is not registered on this ` +
16
+ "workbook: a sheet's images can only be exported by the workbook that holds them");
17
+ }
18
+ return image;
19
+ }
20
+ register(extension, buffer) {
21
+ const image = {
22
+ extension: normalizeImageExtension(extension, buffer),
23
+ data: buffer,
24
+ };
25
+ this.#images.push(image);
26
+ const id = this.#images.length - 1;
27
+ const index = this.#byContent;
28
+ if (index !== undefined) {
29
+ const key = imageContentKey(image);
30
+ if (!index.has(key))
31
+ index.set(key, id);
32
+ }
33
+ return id;
34
+ }
35
+ registerExisting(image) {
36
+ const candidate = {
37
+ extension: normalizeImageExtension(image.extension, image.data),
38
+ data: image.data,
39
+ };
40
+ this.#byContent ??= new Map(this.#images.map((held, id) => [imageContentKey(held), id]).reverse());
41
+ return (this.#byContent.get(imageContentKey(candidate)) ??
42
+ this.register(candidate.extension, candidate.data));
43
+ }
44
+ }