@shbernal/ts-xlsx 3.0.0 → 3.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 (213) hide show
  1. package/README.md +4 -1
  2. package/dist/bytes.d.ts +7 -0
  3. package/dist/bytes.js +22 -0
  4. package/dist/core/address.d.ts +60 -2
  5. package/dist/core/address.js +40 -24
  6. package/dist/core/autofilter.d.ts +4 -3
  7. package/dist/core/autofilter.js +31 -30
  8. package/dist/core/axis-handle.d.ts +2 -0
  9. package/dist/core/axis-handle.js +5 -2
  10. package/dist/core/cell.d.ts +4 -3
  11. package/dist/core/cell.js +4 -6
  12. package/dist/core/clone.d.ts +29 -0
  13. package/dist/core/clone.js +24 -0
  14. package/dist/core/color-resolution.js +2 -4
  15. package/dist/core/column.d.ts +1 -0
  16. package/dist/core/column.js +3 -0
  17. package/dist/core/comment-thread.js +2 -1
  18. package/dist/core/conditional-formatting-overlay.d.ts +2 -1
  19. package/dist/core/conditional-formatting-overlay.js +2 -2
  20. package/dist/core/conditional-formatting.d.ts +9 -1
  21. package/dist/core/conditional-formatting.js +34 -18
  22. package/dist/core/containers.d.ts +12 -0
  23. package/dist/core/containers.js +5 -0
  24. package/dist/core/data-validation-overlay.d.ts +2 -1
  25. package/dist/core/data-validation-overlay.js +4 -4
  26. package/dist/core/data-validation.d.ts +6 -0
  27. package/dist/core/data-validation.js +15 -4
  28. package/dist/core/date-format.d.ts +20 -0
  29. package/dist/core/date-format.js +160 -0
  30. package/dist/core/date.d.ts +43 -8
  31. package/dist/core/date.js +18 -2
  32. package/dist/core/formula.d.ts +26 -0
  33. package/dist/core/formula.js +20 -4
  34. package/dist/core/grid-edits.d.ts +2 -3
  35. package/dist/core/grid-edits.js +69 -70
  36. package/dist/core/grid-shift.d.ts +58 -9
  37. package/dist/core/grid-shift.js +26 -4
  38. package/dist/core/image.d.ts +13 -9
  39. package/dist/core/image.js +7 -5
  40. package/dist/core/internal.d.ts +8 -2
  41. package/dist/core/limits.d.ts +23 -0
  42. package/dist/core/limits.js +4 -0
  43. package/dist/core/merge.d.ts +5 -4
  44. package/dist/core/merge.js +18 -19
  45. package/dist/core/pivot-table.js +63 -45
  46. package/dist/core/protection.js +2 -11
  47. package/dist/core/range.js +34 -30
  48. package/dist/core/row-input.js +2 -2
  49. package/dist/core/row.d.ts +1 -0
  50. package/dist/core/row.js +3 -0
  51. package/dist/core/style.d.ts +85 -70
  52. package/dist/core/style.js +92 -28
  53. package/dist/core/table-style.d.ts +1 -1
  54. package/dist/core/table-style.js +4 -4
  55. package/dist/core/table.d.ts +27 -9
  56. package/dist/core/table.js +51 -34
  57. package/dist/core/theme.js +6 -5
  58. package/dist/core/value.d.ts +9 -0
  59. package/dist/core/value.js +36 -34
  60. package/dist/core/workbook-media.d.ts +34 -0
  61. package/dist/core/workbook-media.js +44 -0
  62. package/dist/core/workbook-styles.d.ts +67 -1
  63. package/dist/core/workbook-vba.js +3 -2
  64. package/dist/core/workbook.d.ts +23 -2
  65. package/dist/core/workbook.js +24 -40
  66. package/dist/core/worksheet-comments.d.ts +2 -1
  67. package/dist/core/worksheet-comments.js +20 -13
  68. package/dist/core/worksheet-merges.d.ts +44 -0
  69. package/dist/core/worksheet-merges.js +59 -0
  70. package/dist/core/worksheet-model.d.ts +9 -1
  71. package/dist/core/worksheet-model.js +24 -9
  72. package/dist/core/worksheet.d.ts +55 -6
  73. package/dist/core/worksheet.js +36 -44
  74. package/dist/customui/ribbon.js +2 -1
  75. package/dist/entries/core.d.ts +18 -13
  76. package/dist/entries/core.js +12 -6
  77. package/dist/entries/xlsx.d.ts +1 -1
  78. package/dist/errors.d.ts +64 -0
  79. package/dist/errors.js +22 -0
  80. package/dist/hex.d.ts +8 -0
  81. package/dist/hex.js +3 -0
  82. package/dist/io/csv/delimiter.d.ts +9 -0
  83. package/dist/io/csv/delimiter.js +6 -0
  84. package/dist/io/csv/read.js +11 -4
  85. package/dist/io/csv/write.d.ts +9 -2
  86. package/dist/io/csv/write.js +9 -38
  87. package/dist/io/opc/inflate.js +14 -0
  88. package/dist/io/opc/namespaces.d.ts +16 -0
  89. package/dist/io/opc/namespaces.js +4 -0
  90. package/dist/io/opc/part-paths.d.ts +9 -0
  91. package/dist/io/opc/part-paths.js +1 -1
  92. package/dist/io/opc/read-opc.d.ts +54 -14
  93. package/dist/io/opc/read-opc.js +42 -20
  94. package/dist/io/opc/read-options.d.ts +5 -1
  95. package/dist/io/opc/sniff-format.d.ts +3 -3
  96. package/dist/io/opc/sniff-format.js +2 -2
  97. package/dist/io/style/xf-style.d.ts +23 -1
  98. package/dist/io/style/xf-style.js +8 -0
  99. package/dist/io/xlsb/formula.d.ts +2 -2
  100. package/dist/io/xlsb/formula.js +49 -34
  101. package/dist/io/xlsb/primitives.js +8 -14
  102. package/dist/io/xlsb/ptg-functions.d.ts +19 -10
  103. package/dist/io/xlsb/ptg-functions.js +385 -596
  104. package/dist/io/xlsb/read-styles.js +45 -81
  105. package/dist/io/xlsb/read-worksheet.d.ts +20 -1
  106. package/dist/io/xlsb/read-worksheet.js +73 -89
  107. package/dist/io/xlsb/read.d.ts +6 -3
  108. package/dist/io/xlsb/read.js +44 -23
  109. package/dist/io/xlsb/record-stream.d.ts +30 -0
  110. package/dist/io/xlsb/record-stream.js +20 -0
  111. package/dist/io/xlsb/record-types.d.ts +1 -5
  112. package/dist/io/xlsb/record-types.js +1 -5
  113. package/dist/io/xlsx/cell-accumulator.d.ts +8 -1
  114. package/dist/io/xlsx/cell-accumulator.js +32 -9
  115. package/dist/io/xlsx/cell-style-resolution.d.ts +25 -0
  116. package/dist/io/xlsx/cell-style-resolution.js +29 -0
  117. package/dist/io/xlsx/cell-value.d.ts +5 -3
  118. package/dist/io/xlsx/cell-value.js +13 -15
  119. package/dist/io/xlsx/color-xml.js +3 -2
  120. package/dist/io/xlsx/column-budget.d.ts +26 -0
  121. package/dist/io/xlsx/column-budget.js +22 -0
  122. package/dist/io/xlsx/comments.d.ts +6 -2
  123. package/dist/io/xlsx/comments.js +36 -32
  124. package/dist/io/xlsx/conditional-formatting.d.ts +6 -3
  125. package/dist/io/xlsx/conditional-formatting.js +30 -26
  126. package/dist/io/xlsx/data-validation.js +29 -27
  127. package/dist/io/xlsx/edit-vba.d.ts +7 -2
  128. package/dist/io/xlsx/edit-vba.js +14 -21
  129. package/dist/io/xlsx/font-xml.d.ts +7 -0
  130. package/dist/io/xlsx/font-xml.js +104 -0
  131. package/dist/io/xlsx/hyperlinks.d.ts +37 -5
  132. package/dist/io/xlsx/hyperlinks.js +22 -13
  133. package/dist/io/xlsx/images.js +11 -13
  134. package/dist/io/xlsx/namespaces.d.ts +16 -0
  135. package/dist/io/xlsx/namespaces.js +7 -0
  136. package/dist/io/xlsx/package-plan.d.ts +14 -2
  137. package/dist/io/xlsx/package-plan.js +39 -11
  138. package/dist/io/xlsx/pivot.js +8 -7
  139. package/dist/io/xlsx/read-parts.d.ts +59 -0
  140. package/dist/io/xlsx/read-parts.js +252 -0
  141. package/dist/io/xlsx/read-pivot.js +2 -1
  142. package/dist/io/xlsx/read-repair.d.ts +26 -0
  143. package/dist/io/xlsx/read-repair.js +39 -0
  144. package/dist/io/xlsx/{rich-runs.js → read-rich-runs.js} +6 -3
  145. package/dist/io/xlsx/read-rows.d.ts +18 -5
  146. package/dist/io/xlsx/read-rows.js +45 -31
  147. package/dist/io/xlsx/read-shared-strings.js +3 -2
  148. package/dist/io/xlsx/read-styles.d.ts +1 -6
  149. package/dist/io/xlsx/read-styles.js +6 -71
  150. package/dist/io/xlsx/read-workbook-xml.d.ts +36 -0
  151. package/dist/io/xlsx/read-workbook-xml.js +170 -0
  152. package/dist/io/xlsx/read-worksheet.d.ts +2 -1
  153. package/dist/io/xlsx/read-worksheet.js +35 -164
  154. package/dist/io/xlsx/read.d.ts +5 -12
  155. package/dist/io/xlsx/read.js +73 -406
  156. package/dist/io/xlsx/rich-text.js +1 -1
  157. package/dist/io/xlsx/row-position.d.ts +18 -0
  158. package/dist/io/xlsx/row-position.js +11 -0
  159. package/dist/io/xlsx/row-xml.d.ts +128 -0
  160. package/dist/io/xlsx/row-xml.js +203 -0
  161. package/dist/io/xlsx/shared-formulas.js +2 -2
  162. package/dist/io/xlsx/sheet-properties.d.ts +10 -1
  163. package/dist/io/xlsx/sheet-properties.js +145 -9
  164. package/dist/io/xlsx/style-elements.d.ts +43 -0
  165. package/dist/io/xlsx/style-elements.js +156 -0
  166. package/dist/io/xlsx/styles.d.ts +12 -3
  167. package/dist/io/xlsx/styles.js +5 -207
  168. package/dist/io/xlsx/tables.js +10 -14
  169. package/dist/io/xlsx/theme-xml.d.ts +26 -4
  170. package/dist/io/xlsx/theme-xml.js +107 -69
  171. package/dist/io/xlsx/threaded-comments.d.ts +8 -10
  172. package/dist/io/xlsx/threaded-comments.js +2 -1
  173. package/dist/io/xlsx/workbook-xml.d.ts +54 -4
  174. package/dist/io/xlsx/workbook-xml.js +60 -34
  175. package/dist/io/xlsx/worksheet-xml.d.ts +23 -70
  176. package/dist/io/xlsx/worksheet-xml.js +48 -229
  177. package/dist/io/xlsx/write-stream.d.ts +65 -16
  178. package/dist/io/xlsx/write-stream.js +50 -20
  179. package/dist/io/xlsx/write.d.ts +1 -2
  180. package/dist/io/xlsx/write.js +114 -74
  181. package/dist/rel-type.d.ts +16 -0
  182. package/dist/rel-type.js +6 -0
  183. package/dist/token-set.d.ts +13 -0
  184. package/dist/token-set.js +4 -1
  185. package/dist/vba/bytes.d.ts +14 -5
  186. package/dist/vba/bytes.js +16 -6
  187. package/dist/vba/cfb-format.d.ts +8 -0
  188. package/dist/vba/cfb-format.js +1 -0
  189. package/dist/vba/cfb-writer.d.ts +9 -0
  190. package/dist/vba/cfb-writer.js +12 -9
  191. package/dist/vba/cfb.d.ts +13 -1
  192. package/dist/vba/cfb.js +73 -51
  193. package/dist/vba/codepage.js +2 -1
  194. package/dist/vba/errors.d.ts +7 -0
  195. package/dist/vba/ms-ovba.js +51 -16
  196. package/dist/vba/project-editor.js +35 -40
  197. package/dist/vba/project.d.ts +11 -1
  198. package/dist/vba/project.js +27 -8
  199. package/dist/vba/vba-encoding.js +4 -5
  200. package/dist/xml/xml-attrs.d.ts +37 -0
  201. package/dist/xml/xml-attrs.js +30 -0
  202. package/dist/xml/xml-chars.d.ts +40 -0
  203. package/dist/xml/xml-chars.js +12 -0
  204. package/dist/xml/xml-namespaces.d.ts +31 -0
  205. package/dist/xml/xml-namespaces.js +50 -0
  206. package/dist/xml/xml-read.d.ts +72 -10
  207. package/dist/xml/xml-read.js +94 -18
  208. package/dist/xml/xml-scan.d.ts +15 -37
  209. package/dist/xml/xml-scan.js +12 -40
  210. package/dist/xml/xml.d.ts +28 -27
  211. package/dist/xml/xml.js +24 -24
  212. package/package.json +12 -7
  213. /package/dist/io/xlsx/{rich-runs.d.ts → read-rich-runs.d.ts} +0 -0
@@ -1,9 +1,12 @@
1
+ import { formatSerialDate } from '../../core/date-format.js';
1
2
  import { cellValueToText, isDataTableFormulaValue, isFormulaValue, isSharedFormulaValue, } from '../../core/value.js';
2
- import { AuthoringError } from '../../errors.js';
3
+ import { AuthoringError, unrepresentable } from '../../errors.js';
4
+ import { assertDelimiter } from './delimiter.js';
3
5
  const UTF8_BOM = Uint8Array.of(0xef, 0xbb, 0xbf);
4
6
  export function writeCsvText(workbook, options = {}) {
5
7
  const sheet = selectSheet(workbook, options.sheetName);
6
8
  const delimiter = options.delimiter ?? ',';
9
+ assertDelimiter(delimiter);
7
10
  const rowDelimiter = options.rowDelimiter ?? '\n';
8
11
  const lines = [];
9
12
  for (const { cells } of sheet.rows()) {
@@ -58,14 +61,11 @@ function encode(text, encoding) {
58
61
  return bytes;
59
62
  }
60
63
  function assertEncodable(text) {
61
- const found = LONE_SURROGATE.exec(text);
62
- if (found === null)
63
- return;
64
- const codePoint = text.codePointAt(found.index).toString(16).toUpperCase();
65
- throw new AuthoringError(`cannot write U+${codePoint} at offset ${found.index} of the CSV text: it is an unpaired ` +
66
- 'surrogate, which UTF-8 cannot encode and CSV has no escape for');
64
+ const error = unrepresentable(text, SURROGATES, 'it is an unpaired surrogate, which UTF-8 cannot encode and CSV has no escape for');
65
+ if (error !== undefined)
66
+ throw error;
67
67
  }
68
- const LONE_SURROGATE = /[\u{D800}-\u{DFFF}]/u;
68
+ const SURROGATES = /[\u{D800}-\u{DFFF}]/u;
69
69
  function selectSheet(workbook, name) {
70
70
  if (name === undefined) {
71
71
  const first = workbook.worksheets[0];
@@ -77,42 +77,13 @@ function selectSheet(workbook, name) {
77
77
  }
78
78
  function csvFieldText(value, options) {
79
79
  if (value instanceof Date && options.dateFormat !== undefined) {
80
- return formatDate(value, options.dateFormat, options.dateUTC ?? false);
80
+ return formatSerialDate(value, options.dateFormat, options.dateUTC ?? false);
81
81
  }
82
82
  if (isFormulaValue(value) || isSharedFormulaValue(value) || isDataTableFormulaValue(value)) {
83
83
  return value.result === undefined ? '' : csvFieldText(value.result, options);
84
84
  }
85
85
  return cellValueToText(value);
86
86
  }
87
- const DATE_TOKENS = /YYYY|YY|MM|DD|HH|mm|ss|M|D|H|m|s/g;
88
- function formatDate(date, format, utc) {
89
- if (Number.isNaN(date.getTime()))
90
- return '';
91
- if (format === undefined)
92
- return date.toISOString();
93
- const year = utc ? date.getUTCFullYear() : date.getFullYear();
94
- const month = (utc ? date.getUTCMonth() : date.getMonth()) + 1;
95
- const day = utc ? date.getUTCDate() : date.getDate();
96
- const hour = utc ? date.getUTCHours() : date.getHours();
97
- const minute = utc ? date.getUTCMinutes() : date.getMinutes();
98
- const second = utc ? date.getUTCSeconds() : date.getSeconds();
99
- const pad = (n) => String(n).padStart(2, '0');
100
- const tokens = {
101
- YYYY: String(year),
102
- YY: pad(year % 100),
103
- MM: pad(month),
104
- M: String(month),
105
- DD: pad(day),
106
- D: String(day),
107
- HH: pad(hour),
108
- H: String(hour),
109
- mm: pad(minute),
110
- m: String(minute),
111
- ss: pad(second),
112
- s: String(second),
113
- };
114
- return format.replace(DATE_TOKENS, (token) => tokens[token] ?? token);
115
- }
116
87
  function quoteField(field, delimiter) {
117
88
  if (field.includes(delimiter) ||
118
89
  field.includes('"') ||
@@ -1,5 +1,6 @@
1
1
  import { Unzip, UnzipInflate } from 'fflate';
2
2
  import { concat } from '../../bytes.js';
3
+ import { quoted } from '../../errors.js';
3
4
  import { PackageReadError } from './errors.js';
4
5
  const INPUT_SLICE = 1 << 14;
5
6
  export function inflatePackage(data, cap) {
@@ -7,6 +8,12 @@ export function inflatePackage(data, cap) {
7
8
  let total = 0;
8
9
  let failure;
9
10
  const unzip = new Unzip((file) => {
11
+ if (!isPartName(file.name)) {
12
+ failure ??= new PackageReadError(`illegal part name ${quoted(file.name)} in package`);
13
+ }
14
+ else if (file.name in files) {
15
+ failure ??= new PackageReadError(`duplicate part ${quoted(file.name)} in package`);
16
+ }
10
17
  const chunks = [];
11
18
  let size = 0;
12
19
  file.ondata = (error, chunk, final) => {
@@ -44,3 +51,10 @@ export function inflatePackage(data, cap) {
44
51
  throw failure;
45
52
  return files;
46
53
  }
54
+ function isPartName(name) {
55
+ if (name === '' || name.startsWith('/') || name.includes('\\'))
56
+ return false;
57
+ if (/^[A-Za-z]:/.test(name))
58
+ return false;
59
+ return !name.split('/').includes('..');
60
+ }
@@ -6,6 +6,8 @@
6
6
  * The SpreadsheetML vocabulary (the main namespace, DrawingML, the Microsoft extension URIs) is a
7
7
  * property of the XML codec and lives in `../xlsx/namespaces.ts`.
8
8
  */
9
+ import type { NamespaceScope } from '../../xml/xml-namespaces.ts';
10
+ import { type XmlAttributes } from '../../xml/xml-scan.ts';
9
11
  /** The `xmlns` of every `.rels` package relationships part. */
10
12
  export declare const PKG_RELS_NS = "http://schemas.openxmlformats.org/package/2006/relationships";
11
13
  /** The `xmlns` of the package's `[Content_Types].xml`. */
@@ -16,3 +18,17 @@ export declare const CONTENT_TYPES_NS = "http://schemas.openxmlformats.org/packa
16
18
  * the relationship graph is written in, so it belongs with the graph and not with either codec.
17
19
  */
18
20
  export declare const RELATIONSHIPS_NS = "http://schemas.openxmlformats.org/officeDocument/2006/relationships";
21
+ /**
22
+ * A relationship attribute (`r:id`, `r:embed`) resolved by namespace rather than by prefix.
23
+ *
24
+ * A workbook may bind {@link RELATIONSHIPS_NS} to any prefix it likes, and real toolchains do: reading
25
+ * the literal `attrs['r:id']` lost every sheet relationship in such a file, so the sheets loaded
26
+ * permanently empty with no error.
27
+ *
28
+ * Falls back to the conventional `r:` spelling when the prefix is not bound anywhere in scope. That is
29
+ * not laxity for its own sake: a part carrying `r:embed` without declaring `r` is malformed XML that
30
+ * this scanner does not reject, it was read before, and refusing it now would lose a file to a fix
31
+ * meant to gain files. Resolution by namespace comes first, so a file that binds `r:` to something
32
+ * else is read correctly rather than by its prefix.
33
+ */
34
+ export declare function relAttr(scope: NamespaceScope, attrs: XmlAttributes, local: string): string | undefined;
@@ -1,3 +1,7 @@
1
+ import {} from '../../xml/xml-scan.js';
1
2
  export const PKG_RELS_NS = 'http://schemas.openxmlformats.org/package/2006/relationships';
2
3
  export const CONTENT_TYPES_NS = 'http://schemas.openxmlformats.org/package/2006/content-types';
3
4
  export const RELATIONSHIPS_NS = 'http://schemas.openxmlformats.org/officeDocument/2006/relationships';
5
+ export function relAttr(scope, attrs, local) {
6
+ return scope.attr(attrs, RELATIONSHIPS_NS, local) ?? attrs[`r:${local}`];
7
+ }
@@ -1,4 +1,13 @@
1
1
  export declare const THEME_PART_PATH = "xl/theme/theme1.xml";
2
+ /**
3
+ * The extension of a part path (`xl/media/image1.JPEG` → `jpeg`), or `''` when it carries none.
4
+ *
5
+ * Lower-cased here rather than by each caller. OPC extensions are case-insensitive, so an extension is
6
+ * only ever used as a key or compared against a literal, and every caller but one folded the case
7
+ * itself; the one that did not handed a raw `JPEG` to `addImage`, which folds later, so the same media
8
+ * type could reach `[Content_Types].xml` as two `<Default Extension>` entries. Folding once at the
9
+ * source is what makes the missed fold unrepresentable rather than merely unlikely.
10
+ */
2
11
  export declare function extensionOf(partPath: string): string;
3
12
  export declare function relsPathFor(partPath: string): string;
4
13
  export declare function relativePartPath(fromPath: string, toPath: string): string;
@@ -2,7 +2,7 @@ export const THEME_PART_PATH = 'xl/theme/theme1.xml';
2
2
  export function extensionOf(partPath) {
3
3
  const dot = partPath.lastIndexOf('.');
4
4
  const slash = partPath.lastIndexOf('/');
5
- return dot > slash ? partPath.slice(dot + 1) : '';
5
+ return dot > slash ? partPath.slice(dot + 1).toLowerCase() : '';
6
6
  }
7
7
  export function relsPathFor(partPath) {
8
8
  const slash = partPath.lastIndexOf('/');
@@ -7,21 +7,30 @@ export interface PackageAccessors {
7
7
  partBytes: (path: string) => Uint8Array | undefined;
8
8
  }
9
9
  /** A spreadsheet package opened for reading: inflated under the read bound, its parts bound to
10
- * accessors, and its XML office document read if it has one. */
10
+ * accessors, its office document located through the package's own relationship graph, and that
11
+ * document read if it is XML. */
11
12
  export interface OpenedSpreadsheet {
12
13
  /** The inflated parts, for a reader that hands the whole package on to another codec. */
13
14
  readonly files: Record<string, Uint8Array>;
14
15
  readonly pkg: PackageAccessors;
15
- /** `xl/workbook.xml`, or undefined when the package carries no XML office document (a `.xlsb`
16
- * carries `xl/workbook.bin` instead). Each entry point answers that case for itself: they
17
- * genuinely want different answers, and the difference is documented where they diverge. */
16
+ /**
17
+ * Where this package keeps its office document, resolved through `_rels/.rels`. Conventionally
18
+ * `xl/workbook.xml` (or `xl/workbook.bin` for a `.xlsb`), but the convention is Excel's habit
19
+ * rather than the format's rule, and a package is free to name the part anything its root
20
+ * relationship points at.
21
+ */
22
+ readonly documentPath: string;
23
+ /** The office document's text, or undefined when the package's document is not XML (a `.xlsb`
24
+ * carries a BIFF12 part instead). Each entry point answers that case for itself: they genuinely
25
+ * want different answers, and the difference is documented where they diverge. */
18
26
  readonly workbookXml: string | undefined;
19
27
  }
20
28
  /**
21
- * Open a spreadsheet package: the six-step preamble every reader shares, and in particular the two
29
+ * Open a spreadsheet package: the six-step preamble every reader shares, and in particular the three
22
30
  * decisions worth having exactly one of. The inflate bound is a security decision (an unbounded
23
- * inflate is a zip bomb) and "does this package carry an XML office document" is the dispatch
24
- * decision between the two codecs, so neither should be restated once per entry point.
31
+ * inflate is a zip bomb), *where the office document lives* is a question only the package can
32
+ * answer, and "is that document XML" is the dispatch decision between the two codecs. None should be
33
+ * restated once per entry point.
25
34
  *
26
35
  * It deliberately stops short of the shared strings and the style table: those are codec-specific
27
36
  * (`xl/sharedStrings.xml` against BIFF12's own record stream), and lifting them here would put
@@ -29,8 +38,6 @@ export interface OpenedSpreadsheet {
29
38
  */
30
39
  export declare function openSpreadsheetPackage(data: Uint8Array, maxUncompressedBytes: number | undefined): OpenedSpreadsheet;
31
40
  export declare function packageAccessors(files: Record<string, Uint8Array>): PackageAccessors;
32
- export declare function relationshipTargetByType(xml: string, suffix: string): string | undefined;
33
- export declare function relationshipTargetsByType(xml: string, suffix: string): string[];
34
41
  export interface RelationshipRecord {
35
42
  readonly id: string;
36
43
  readonly type: string;
@@ -46,14 +53,47 @@ export interface PartRelationships {
46
53
  byId(id: string): RelationshipRecord | undefined;
47
54
  /** Resolve one of this part's targets against the part's own directory. */
48
55
  pathOf(target: string): string;
49
- /** The package part reached through the first relationship whose Type ends with `/<suffix>`, or
56
+ /** The package part reached through the first relationship of this class ({@link isRelType}), or
50
57
  * undefined when the part declares none: the single-part lookup (notes, printer settings, drawing,
51
58
  * background) in one call. */
52
- targetPath(suffix: string): string | undefined;
53
- /** Every package part reached through a relationship of this type, in declaration order. For a part
59
+ targetPath(name: string): string | undefined;
60
+ /** Every package part reached through a relationship of this class, in declaration order. For a part
54
61
  * class one sheet may reference more than once (tables, pivot tables). */
55
- targetPaths(suffix: string): string[];
62
+ targetPaths(name: string): string[];
63
+ /**
64
+ * The text of the single part reached through a relationship of this type, or `undefined` when the
65
+ * part declares no such relationship *or* names one the package does not contain.
66
+ *
67
+ * Those two are deliberately one answer. A relationship pointing at an absent part is a damaged
68
+ * package, and every reader here answers a damaged package the same way Excel does: the feature is
69
+ * simply not there, rather than the load failing over it. Collapsing `targetPath` then `partText`
70
+ * into one call is what lets that contract be stated once instead of at each of the seven places
71
+ * that used to spell it out.
72
+ */
73
+ relatedText(name: string): string | undefined;
74
+ /** As {@link relatedText}, for a part whose content is opaque bytes (a printer-settings blob, an
75
+ * image) rather than XML. */
76
+ relatedBytes(name: string): Uint8Array | undefined;
77
+ /**
78
+ * The text of a part reached through the relationship that names it, falling back to
79
+ * `conventionalPath` only when the part declares no such relationship, and to `''` when neither
80
+ * resolves.
81
+ *
82
+ * The order is the whole point, and it decides more than where a lookup lands. A package is free
83
+ * to name any part anything -- the workbook part included -- so the part's own relationships are
84
+ * what say where its pool and its stylesheet live. Resolved conventional-path-first, a renamed
85
+ * pool reads as no pooled strings at all, and a renamed stylesheet silently changes cell *types*,
86
+ * because the date test reads `numFmt` off the resolved style to tell `45000` from a date. The
87
+ * conventional path stays as the fallback for a package whose rels are damaged.
88
+ *
89
+ * The `''` is a real package state, not a lookup that failed quietly: a workbook may carry no pool
90
+ * and no stylesheet, and every reader of these parses the empty text into the empty table it would
91
+ * have built anyway. Stated here so the two readers that resolve these parts cannot answer the
92
+ * same package differently -- a streamed read must not decode a cell differently from a buffered
93
+ * one.
94
+ */
95
+ relatedTextOrPath(name: string, conventionalPath: string): string;
56
96
  }
57
- export declare function readPartRelationships(partPath: string, partText: (path: string) => string | undefined): PartRelationships;
97
+ export declare function readPartRelationships(partPath: string, partText: (path: string) => string | undefined, partBytes?: (path: string) => Uint8Array | undefined): PartRelationships;
58
98
  export declare function contentTypeResolver(contentTypesXml: string): (path: string) => string;
59
99
  export declare function capturePartClosure(entryPath: string, partText: (path: string) => string | undefined, partBytes: (path: string) => Uint8Array | undefined, contentTypeOf: (path: string) => string): readonly PreservedPart[] | undefined;
@@ -1,4 +1,5 @@
1
1
  import { strFromU8 } from 'fflate';
2
+ import { isRelType } from '../../rel-type.js';
2
3
  import { openElements } from '../../xml/xml-read.js';
3
4
  import { extensionOf, relsPathFor, resolveRelativePart } from './part-paths.js';
4
5
  import { DEFAULT_MAX_UNCOMPRESSED } from './read-options.js';
@@ -6,25 +7,33 @@ import { inflateSpreadsheetPackage } from './sniff-format.js';
6
7
  export function openSpreadsheetPackage(data, maxUncompressedBytes) {
7
8
  const files = inflateSpreadsheetPackage(data, maxUncompressedBytes ?? DEFAULT_MAX_UNCOMPRESSED);
8
9
  const pkg = packageAccessors(files);
9
- return { files, pkg, workbookXml: pkg.partText('xl/workbook.xml') };
10
+ const documentPath = readPartRelationships('', pkg.partText).targetPath(OFFICE_DOCUMENT_REL) ??
11
+ conventionalDocument(pkg);
12
+ const workbookXml = extensionOf(documentPath) === 'xml' ? pkg.partText(documentPath) : undefined;
13
+ return { files, pkg, documentPath, workbookXml };
14
+ }
15
+ const OFFICE_DOCUMENT_REL = 'officeDocument';
16
+ const CONVENTIONAL_DOCUMENTS = ['xl/workbook.xml', 'xl/workbook.bin'];
17
+ function conventionalDocument(pkg) {
18
+ return (CONVENTIONAL_DOCUMENTS.find((path) => pkg.partBytes(path) !== undefined) ??
19
+ CONVENTIONAL_DOCUMENTS[0]);
10
20
  }
11
21
  export function packageAccessors(files) {
22
+ const folded = new Map();
23
+ for (const [path, bytes] of Object.entries(files)) {
24
+ const key = path.toLowerCase();
25
+ if (!folded.has(key))
26
+ folded.set(key, bytes);
27
+ }
28
+ const lookup = (path) => files[path] ?? folded.get(path.toLowerCase());
12
29
  return {
13
30
  partText: (path) => {
14
- const bytes = files[path];
31
+ const bytes = lookup(path);
15
32
  return bytes === undefined ? undefined : strFromU8(bytes);
16
33
  },
17
- partBytes: (path) => files[path],
34
+ partBytes: lookup,
18
35
  };
19
36
  }
20
- export function relationshipTargetByType(xml, suffix) {
21
- return relationshipTargetsByType(xml, suffix)[0];
22
- }
23
- export function relationshipTargetsByType(xml, suffix) {
24
- return parseRelationshipRecords(xml)
25
- .filter((record) => record.type.endsWith(`/${suffix}`))
26
- .map((record) => record.target);
27
- }
28
37
  export function parseRelationshipRecords(xml) {
29
38
  const records = [];
30
39
  for (const { attrs } of openElements(xml, 'Relationship')) {
@@ -39,17 +48,30 @@ export function parseRelationshipRecords(xml) {
39
48
  }
40
49
  return records;
41
50
  }
42
- export function readPartRelationships(partPath, partText) {
51
+ export function readPartRelationships(partPath, partText, partBytes) {
43
52
  const records = parseRelationshipRecords(partText(relsPathFor(partPath)) ?? '');
44
53
  const byId = new Map(records.map((record) => [record.id, record]));
45
54
  const pathOf = (target) => resolveRelativePart(partPath, target);
46
- const targetsOf = (suffix) => records.filter((record) => record.type.endsWith(`/${suffix}`)).map((record) => record.target);
55
+ const targetsOf = (name) => records.filter((record) => isRelType(record.type, name)).map((record) => record.target);
56
+ const targetPath = (name) => targetsOf(name).map(pathOf)[0];
47
57
  return {
48
58
  records,
49
59
  byId: (id) => byId.get(id),
50
60
  pathOf,
51
- targetPath: (suffix) => targetsOf(suffix).map(pathOf)[0],
52
- targetPaths: (suffix) => targetsOf(suffix).map(pathOf),
61
+ targetPath,
62
+ targetPaths: (name) => targetsOf(name).map(pathOf),
63
+ relatedText: (name) => {
64
+ const path = targetPath(name);
65
+ return path === undefined ? undefined : partText(path);
66
+ },
67
+ relatedBytes: (name) => {
68
+ const path = targetPath(name);
69
+ return path === undefined ? undefined : partBytes?.(path);
70
+ },
71
+ relatedTextOrPath: (name, conventionalPath) => {
72
+ const path = targetPath(name);
73
+ return (path === undefined ? undefined : partText(path)) ?? partText(conventionalPath) ?? '';
74
+ },
53
75
  };
54
76
  }
55
77
  export function contentTypeResolver(contentTypesXml) {
@@ -57,7 +79,7 @@ export function contentTypeResolver(contentTypesXml) {
57
79
  const defaults = new Map();
58
80
  for (const { local, attrs } of openElements(contentTypesXml, 'Override', 'Default')) {
59
81
  if (local === 'Override' && attrs.PartName !== undefined && attrs.ContentType !== undefined) {
60
- overrides.set(attrs.PartName, attrs.ContentType);
82
+ overrides.set(attrs.PartName.toLowerCase(), attrs.ContentType);
61
83
  }
62
84
  else if (local === 'Default' &&
63
85
  attrs.Extension !== undefined &&
@@ -65,16 +87,16 @@ export function contentTypeResolver(contentTypesXml) {
65
87
  defaults.set(attrs.Extension.toLowerCase(), attrs.ContentType);
66
88
  }
67
89
  }
68
- return (path) => overrides.get(`/${path}`) ??
69
- defaults.get(extensionOf(path).toLowerCase()) ??
90
+ return (path) => overrides.get(`/${path}`.toLowerCase()) ??
91
+ defaults.get(extensionOf(path)) ??
70
92
  'application/octet-stream';
71
93
  }
72
94
  export function capturePartClosure(entryPath, partText, partBytes, contentTypeOf) {
73
95
  const parts = [];
74
96
  const visited = new Set();
75
97
  const queue = [entryPath];
76
- while (queue.length > 0) {
77
- const path = queue.shift();
98
+ for (let next = 0; next < queue.length; next++) {
99
+ const path = queue[next];
78
100
  if (path === undefined || visited.has(path))
79
101
  continue;
80
102
  visited.add(path);
@@ -1,4 +1,8 @@
1
- export interface ReadXlsxOptions {
1
+ /**
2
+ * How a reader is allowed to open an OPC package. One knob today: the ceiling on what inflating it
3
+ * may produce, which every reader shares because every reader inflates the same container.
4
+ */
5
+ export interface ReadPackageOptions {
2
6
  /**
3
7
  * Maximum total uncompressed output, in bytes, produced while inflating the package.
4
8
  * The bound is enforced by a running counter as bytes are decompressed, never read from
@@ -17,11 +17,11 @@ export declare function sniffContainer(data: Uint8Array): Container;
17
17
  */
18
18
  export declare function inflateSpreadsheetPackage(data: Uint8Array, cap: number): Record<string, Uint8Array>;
19
19
  /**
20
- * The typed error for an inflated package that carries no `xl/workbook.xml`: a `.xlsb` if its binary
21
- * `xl/workbook.bin` office document is present, otherwise an unrecognised (non-workbook) ZIP.
20
+ * The typed error for an inflated package whose office document is not XML: a `.xlsb` if that
21
+ * document is present as binary, otherwise an unrecognised (non-workbook) ZIP.
22
22
  *
23
23
  * The `.xlsb` branch takes the caller's own explanation, because whether a binary workbook is
24
24
  * readable now depends on *which* entry point was asked: `readXlsx` reads one, the row streamer
25
25
  * cannot yet. A single baked-in "not supported" message would be wrong for one of them.
26
26
  */
27
- export declare function unsupportedWorkbookPart(partText: (path: string) => string | undefined, xlsbMessage: string): UnsupportedFormatError;
27
+ export declare function unsupportedWorkbookPart(partBytes: (path: string) => Uint8Array | undefined, documentPath: string, xlsbMessage: string): UnsupportedFormatError;
@@ -34,8 +34,8 @@ export function inflateSpreadsheetPackage(data, cap) {
34
34
  throw new PackageReadError('not a readable .xlsx package: the ZIP container is corrupt or truncated and could not be inflated');
35
35
  }
36
36
  }
37
- export function unsupportedWorkbookPart(partText, xlsbMessage) {
38
- if (partText('xl/workbook.bin') !== undefined) {
37
+ export function unsupportedWorkbookPart(partBytes, documentPath, xlsbMessage) {
38
+ if (partBytes(documentPath) !== undefined) {
39
39
  return new UnsupportedFormatError('xlsb', xlsbMessage);
40
40
  }
41
41
  return new UnsupportedFormatError('unknown');
@@ -1,5 +1,6 @@
1
1
  import { type Cell } from '../../core/cell.ts';
2
- import { type Border, type CellStyle, type Fill, type Font, type NamedCellStyle, type TableStyleTable } from '../../core/style.ts';
2
+ import { type Border, type CellStyle, type Fill, type Font, type Protection } from '../../core/style.ts';
3
+ import type { NamedCellStyle, TableStyleTable } from '../../core/workbook-styles.ts';
3
4
  /**
4
5
  * What an xf resolves to: the {@link CellStyle} facet tuple, plus the two flags an xf carries that
5
6
  * are not facets. Absent facets stay undefined, matching the contract that an unset facet is simply
@@ -74,6 +75,27 @@ export declare function numFmtCodeFor(id: number, custom: ReadonlyMap<number, st
74
75
  * from, and the two cannot drift on what "applying a style" means.
75
76
  */
76
77
  export declare function applyXfToCell(cell: Cell, style: XfStyle | undefined): void;
78
+ /**
79
+ * A cell's protection facets from the two flags either serialisation states, keeping only what
80
+ * carries information.
81
+ *
82
+ * `locked` defaults to TRUE in OOXML, so an explicitly *unlocked* cell is the state worth recording
83
+ * and a locked one merely restates the default; `hidden` defaults to false, so only a set flag does.
84
+ * An xf that states neither yields no protection at all rather than an empty object, which is what
85
+ * keeps the two readings of one workbook identical.
86
+ *
87
+ * Here rather than once per codec because that is exactly what it was: `parseProtection` in the XML
88
+ * reader and `readProtection` in the binary one, two spellings of two default rules, agreeing by
89
+ * review. The inputs differ (an attribute is tri-state, a bit is not) and the rule does not, so the
90
+ * codecs keep the parsing and share the rule.
91
+ *
92
+ * @param locked the `locked` attribute's tri-state reading, or the bit's boolean; `undefined` where
93
+ * the file states nothing.
94
+ */
95
+ export declare function protectionFrom(flags: {
96
+ readonly locked?: boolean | undefined;
97
+ readonly hidden?: boolean | undefined;
98
+ }): Protection | undefined;
77
99
  /**
78
100
  * A `<cellStyle>` / `BrtStyle` label: the name and builtinId that title one `cellStyleXfs` entry,
79
101
  * keyed to that entry's index.
@@ -69,6 +69,14 @@ export function applyXfToCell(cell, style) {
69
69
  if (style.xfId !== undefined)
70
70
  cell[NAMED_STYLE_ID] = style.xfId;
71
71
  }
72
+ export function protectionFrom(flags) {
73
+ const out = {};
74
+ if (flags.locked === false)
75
+ out.locked = false;
76
+ if (flags.hidden === true)
77
+ out.hidden = true;
78
+ return Object.keys(out).length > 0 ? out : undefined;
79
+ }
72
80
  export function resolveStyleTable(tables) {
73
81
  const { directXfs, namedXfs, labels, fonts } = tables;
74
82
  const cellXfs = directXfs.map((xf) => {
@@ -34,8 +34,8 @@ export interface FormulaAnchor {
34
34
  * @param rgce the token stream.
35
35
  * @param rgcb the trailing extra-data block: the array constants, and the cell ranges a precomputed
36
36
  * range token refers to. Its entries are consumed in token order.
37
- * @returns the formula text, or `undefined` if the stream uses a token this reader does not decode.
38
- * @throws {XlsbParseError} if a token runs past the end of the stream (a malformed formula).
37
+ * @returns the formula text, or `undefined` if the stream uses a token this reader does not decode,
38
+ * or runs off the end of its own record.
39
39
  */
40
40
  export declare function decodeFormula(rgce: Uint8Array, rgcb: Uint8Array, scope: FormulaScope): string | undefined;
41
41
  /**