@shbernal/ts-xlsx 1.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 (223) hide show
  1. package/LICENSE +23 -0
  2. package/README.md +230 -0
  3. package/dist/core/address.d.ts +49 -0
  4. package/dist/core/address.js +125 -0
  5. package/dist/core/autofilter.d.ts +55 -0
  6. package/dist/core/autofilter.js +48 -0
  7. package/dist/core/cell.d.ts +132 -0
  8. package/dist/core/cell.js +249 -0
  9. package/dist/core/color-resolution.d.ts +58 -0
  10. package/dist/core/color-resolution.js +170 -0
  11. package/dist/core/column.d.ts +87 -0
  12. package/dist/core/column.js +167 -0
  13. package/dist/core/comment-thread.d.ts +116 -0
  14. package/dist/core/comment-thread.js +62 -0
  15. package/dist/core/conditional-formatting-overlay.d.ts +15 -0
  16. package/dist/core/conditional-formatting-overlay.js +24 -0
  17. package/dist/core/conditional-formatting.d.ts +70 -0
  18. package/dist/core/conditional-formatting.js +35 -0
  19. package/dist/core/containers.d.ts +2 -0
  20. package/dist/core/containers.js +25 -0
  21. package/dist/core/data-validation-overlay.d.ts +28 -0
  22. package/dist/core/data-validation-overlay.js +56 -0
  23. package/dist/core/data-validation.d.ts +38 -0
  24. package/dist/core/data-validation.js +13 -0
  25. package/dist/core/date.d.ts +25 -0
  26. package/dist/core/date.js +58 -0
  27. package/dist/core/formula.d.ts +49 -0
  28. package/dist/core/formula.js +319 -0
  29. package/dist/core/grid-edits.d.ts +22 -0
  30. package/dist/core/grid-edits.js +206 -0
  31. package/dist/core/image.d.ts +76 -0
  32. package/dist/core/image.js +65 -0
  33. package/dist/core/internal.d.ts +11 -0
  34. package/dist/core/internal.js +29 -0
  35. package/dist/core/merge.d.ts +13 -0
  36. package/dist/core/merge.js +26 -0
  37. package/dist/core/modern-functions.d.ts +1 -0
  38. package/dist/core/modern-functions.js +165 -0
  39. package/dist/core/page-setup.d.ts +94 -0
  40. package/dist/core/page-setup.js +6 -0
  41. package/dist/core/pivot-table.d.ts +113 -0
  42. package/dist/core/pivot-table.js +249 -0
  43. package/dist/core/preserved.d.ts +58 -0
  44. package/dist/core/preserved.js +5 -0
  45. package/dist/core/protection.d.ts +67 -0
  46. package/dist/core/protection.js +68 -0
  47. package/dist/core/range.d.ts +112 -0
  48. package/dist/core/range.js +283 -0
  49. package/dist/core/row.d.ts +73 -0
  50. package/dist/core/row.js +135 -0
  51. package/dist/core/style.d.ts +246 -0
  52. package/dist/core/style.js +125 -0
  53. package/dist/core/table-style.d.ts +67 -0
  54. package/dist/core/table-style.js +90 -0
  55. package/dist/core/table.d.ts +194 -0
  56. package/dist/core/table.js +291 -0
  57. package/dist/core/theme.d.ts +90 -0
  58. package/dist/core/theme.js +272 -0
  59. package/dist/core/value.d.ts +115 -0
  60. package/dist/core/value.js +144 -0
  61. package/dist/core/workbook-protection.d.ts +27 -0
  62. package/dist/core/workbook-protection.js +25 -0
  63. package/dist/core/workbook.d.ts +528 -0
  64. package/dist/core/workbook.js +732 -0
  65. package/dist/core/worksheet-model.d.ts +34 -0
  66. package/dist/core/worksheet-model.js +100 -0
  67. package/dist/core/worksheet.d.ts +647 -0
  68. package/dist/core/worksheet.js +1025 -0
  69. package/dist/customui/errors.d.ts +15 -0
  70. package/dist/customui/errors.js +15 -0
  71. package/dist/customui/index.d.ts +2 -0
  72. package/dist/customui/index.js +7 -0
  73. package/dist/customui/ribbon.d.ts +86 -0
  74. package/dist/customui/ribbon.js +180 -0
  75. package/dist/entries/core.d.ts +23 -0
  76. package/dist/entries/core.js +20 -0
  77. package/dist/entries/csv.d.ts +2 -0
  78. package/dist/entries/csv.js +6 -0
  79. package/dist/entries/customui.d.ts +1 -0
  80. package/dist/entries/customui.js +3 -0
  81. package/dist/entries/errors.d.ts +7 -0
  82. package/dist/entries/errors.js +20 -0
  83. package/dist/entries/vba.d.ts +2 -0
  84. package/dist/entries/vba.js +7 -0
  85. package/dist/entries/xlsb.d.ts +1 -0
  86. package/dist/entries/xlsb.js +7 -0
  87. package/dist/entries/xlsx.d.ts +5 -0
  88. package/dist/entries/xlsx.js +18 -0
  89. package/dist/errors.d.ts +65 -0
  90. package/dist/errors.js +66 -0
  91. package/dist/index.d.ts +7 -0
  92. package/dist/index.js +19 -0
  93. package/dist/io/csv/read.d.ts +15 -0
  94. package/dist/io/csv/read.js +113 -0
  95. package/dist/io/csv/write.d.ts +28 -0
  96. package/dist/io/csv/write.js +124 -0
  97. package/dist/io/opc/errors.d.ts +46 -0
  98. package/dist/io/opc/errors.js +49 -0
  99. package/dist/io/opc/inflate.d.ts +15 -0
  100. package/dist/io/opc/inflate.js +91 -0
  101. package/dist/io/opc/namespaces.d.ts +18 -0
  102. package/dist/io/opc/namespaces.js +18 -0
  103. package/dist/io/opc/part-paths.d.ts +4 -0
  104. package/dist/io/opc/part-paths.js +35 -0
  105. package/dist/io/opc/read-opc.d.ts +24 -0
  106. package/dist/io/opc/read-opc.js +153 -0
  107. package/dist/io/opc/read-options.d.ts +10 -0
  108. package/dist/io/opc/read-options.js +7 -0
  109. package/dist/io/opc/rels.d.ts +15 -0
  110. package/dist/io/opc/rels.js +28 -0
  111. package/dist/io/opc/sniff-format.d.ts +27 -0
  112. package/dist/io/opc/sniff-format.js +92 -0
  113. package/dist/io/style/xf-style.d.ts +47 -0
  114. package/dist/io/style/xf-style.js +95 -0
  115. package/dist/io/xlsb/errors.d.ts +15 -0
  116. package/dist/io/xlsb/errors.js +15 -0
  117. package/dist/io/xlsb/formula.d.ts +48 -0
  118. package/dist/io/xlsb/formula.js +398 -0
  119. package/dist/io/xlsb/primitives.d.ts +86 -0
  120. package/dist/io/xlsb/primitives.js +230 -0
  121. package/dist/io/xlsb/ptg-functions.d.ts +16 -0
  122. package/dist/io/xlsb/ptg-functions.js +659 -0
  123. package/dist/io/xlsb/read-shared-strings.d.ts +2 -0
  124. package/dist/io/xlsb/read-shared-strings.js +22 -0
  125. package/dist/io/xlsb/read-styles.d.ts +4 -0
  126. package/dist/io/xlsb/read-styles.js +386 -0
  127. package/dist/io/xlsb/read-worksheet.d.ts +9 -0
  128. package/dist/io/xlsb/read-worksheet.js +301 -0
  129. package/dist/io/xlsb/read.d.ts +20 -0
  130. package/dist/io/xlsb/read.js +199 -0
  131. package/dist/io/xlsb/record-stream.d.ts +14 -0
  132. package/dist/io/xlsb/record-stream.js +56 -0
  133. package/dist/io/xlsb/record-types.d.ts +55 -0
  134. package/dist/io/xlsb/record-types.js +73 -0
  135. package/dist/io/xlsx/cell-accumulator.d.ts +25 -0
  136. package/dist/io/xlsx/cell-accumulator.js +172 -0
  137. package/dist/io/xlsx/cell-value.d.ts +32 -0
  138. package/dist/io/xlsx/cell-value.js +85 -0
  139. package/dist/io/xlsx/comments.d.ts +74 -0
  140. package/dist/io/xlsx/comments.js +243 -0
  141. package/dist/io/xlsx/conditional-formatting.d.ts +32 -0
  142. package/dist/io/xlsx/conditional-formatting.js +464 -0
  143. package/dist/io/xlsx/data-validation.d.ts +20 -0
  144. package/dist/io/xlsx/data-validation.js +258 -0
  145. package/dist/io/xlsx/edit-vba.d.ts +23 -0
  146. package/dist/io/xlsx/edit-vba.js +139 -0
  147. package/dist/io/xlsx/errors.d.ts +17 -0
  148. package/dist/io/xlsx/errors.js +17 -0
  149. package/dist/io/xlsx/hyperlinks.d.ts +42 -0
  150. package/dist/io/xlsx/hyperlinks.js +139 -0
  151. package/dist/io/xlsx/images.d.ts +34 -0
  152. package/dist/io/xlsx/images.js +218 -0
  153. package/dist/io/xlsx/namespaces.d.ts +55 -0
  154. package/dist/io/xlsx/namespaces.js +55 -0
  155. package/dist/io/xlsx/package-plan.d.ts +103 -0
  156. package/dist/io/xlsx/package-plan.js +168 -0
  157. package/dist/io/xlsx/pivot-read.d.ts +5 -0
  158. package/dist/io/xlsx/pivot-read.js +134 -0
  159. package/dist/io/xlsx/pivot.d.ts +12 -0
  160. package/dist/io/xlsx/pivot.js +141 -0
  161. package/dist/io/xlsx/read-rows.d.ts +85 -0
  162. package/dist/io/xlsx/read-rows.js +286 -0
  163. package/dist/io/xlsx/read-styles.d.ts +8 -0
  164. package/dist/io/xlsx/read-styles.js +551 -0
  165. package/dist/io/xlsx/read-worksheet.d.ts +4 -0
  166. package/dist/io/xlsx/read-worksheet.js +528 -0
  167. package/dist/io/xlsx/read.d.ts +32 -0
  168. package/dist/io/xlsx/read.js +680 -0
  169. package/dist/io/xlsx/relationships.d.ts +33 -0
  170. package/dist/io/xlsx/relationships.js +47 -0
  171. package/dist/io/xlsx/rich-runs.d.ts +13 -0
  172. package/dist/io/xlsx/rich-runs.js +59 -0
  173. package/dist/io/xlsx/rich-text.d.ts +7 -0
  174. package/dist/io/xlsx/rich-text.js +23 -0
  175. package/dist/io/xlsx/shared-formulas.d.ts +6 -0
  176. package/dist/io/xlsx/shared-formulas.js +52 -0
  177. package/dist/io/xlsx/shared-strings-read.d.ts +2 -0
  178. package/dist/io/xlsx/shared-strings-read.js +70 -0
  179. package/dist/io/xlsx/shared-strings.d.ts +17 -0
  180. package/dist/io/xlsx/shared-strings.js +50 -0
  181. package/dist/io/xlsx/sheet-properties.d.ts +13 -0
  182. package/dist/io/xlsx/sheet-properties.js +211 -0
  183. package/dist/io/xlsx/styles.d.ts +138 -0
  184. package/dist/io/xlsx/styles.js +821 -0
  185. package/dist/io/xlsx/tables.d.ts +9 -0
  186. package/dist/io/xlsx/tables.js +208 -0
  187. package/dist/io/xlsx/threaded-comments.d.ts +102 -0
  188. package/dist/io/xlsx/threaded-comments.js +308 -0
  189. package/dist/io/xlsx/workbook-xml.d.ts +12 -0
  190. package/dist/io/xlsx/workbook-xml.js +353 -0
  191. package/dist/io/xlsx/worksheet-xml.d.ts +86 -0
  192. package/dist/io/xlsx/worksheet-xml.js +643 -0
  193. package/dist/io/xlsx/write-stream.d.ts +171 -0
  194. package/dist/io/xlsx/write-stream.js +399 -0
  195. package/dist/io/xlsx/write.d.ts +81 -0
  196. package/dist/io/xlsx/write.js +396 -0
  197. package/dist/io/xlsx/x14-ext.d.ts +9 -0
  198. package/dist/io/xlsx/x14-ext.js +12 -0
  199. package/dist/vba/cfb-writer.d.ts +19 -0
  200. package/dist/vba/cfb-writer.js +277 -0
  201. package/dist/vba/cfb.d.ts +16 -0
  202. package/dist/vba/cfb.js +324 -0
  203. package/dist/vba/codepage.d.ts +18 -0
  204. package/dist/vba/codepage.js +87 -0
  205. package/dist/vba/errors.d.ts +24 -0
  206. package/dist/vba/errors.js +24 -0
  207. package/dist/vba/index.d.ts +5 -0
  208. package/dist/vba/index.js +15 -0
  209. package/dist/vba/ms-ovba.d.ts +12 -0
  210. package/dist/vba/ms-ovba.js +176 -0
  211. package/dist/vba/project-editor.d.ts +62 -0
  212. package/dist/vba/project-editor.js +377 -0
  213. package/dist/vba/project.d.ts +45 -0
  214. package/dist/vba/project.js +160 -0
  215. package/dist/vba/vba-encoding.d.ts +14 -0
  216. package/dist/vba/vba-encoding.js +39 -0
  217. package/dist/xml/errors.d.ts +15 -0
  218. package/dist/xml/errors.js +15 -0
  219. package/dist/xml/xml-read.d.ts +107 -0
  220. package/dist/xml/xml-read.js +283 -0
  221. package/dist/xml/xml.d.ts +41 -0
  222. package/dist/xml/xml.js +86 -0
  223. package/package.json +117 -0
@@ -0,0 +1,15 @@
1
+ import type { CellValue } from '../../core/value.ts';
2
+ import { Workbook } from '../../core/workbook.ts';
3
+ export interface CsvReadOptions {
4
+ /** Field separator; defaults to a comma. A single character. */
5
+ readonly delimiter?: string;
6
+ /** Treat the first line as a header and drop it, leaving only data rows. */
7
+ readonly headers?: boolean;
8
+ /** Per-field transform replacing the default type coercion; receives the raw string and its
9
+ * 0-based column index. */
10
+ readonly map?: (value: string, index: number) => CellValue;
11
+ /** Name for the single worksheet produced; defaults to `"Sheet1"`. */
12
+ readonly sheetName?: string;
13
+ }
14
+ /** Parse CSV text (or UTF-8 bytes) into a workbook holding a single worksheet. */
15
+ export declare function readCsv(input: string | Uint8Array, options?: CsvReadOptions): Workbook;
@@ -0,0 +1,113 @@
1
+ // CSV parsing — flat delimited text back into a one-sheet workbook.
2
+ //
3
+ // The hard part of reading CSV is not splitting fields; it is deciding a field's *type* without
4
+ // corrupting data. The rules here are deliberate and lossless-by-default:
5
+ // - An empty field is the empty cell (`null`); a whitespace-only field is a string, never the
6
+ // number 0 that `Number(" ")` would silently produce.
7
+ // - A numeric-looking field becomes a number only when its magnitude is exactly representable
8
+ // (within the safe-integer range); an oversized id like a 20-digit account number is kept as
9
+ // its original string so no digits are lost.
10
+ // - Only a strictly-formatted ISO date (`YYYY-MM-DD`, optional time) becomes a Date; padded ids
11
+ // and dash-codes such as `2020-00001` or `1-3` stay strings.
12
+ // A caller can override coercion wholesale with `map` (e.g. the identity function to keep every
13
+ // field a raw string, preserving leading zeros).
14
+ import { Workbook } from "../../core/workbook.js";
15
+ import { AuthoringError } from "../../errors.js";
16
+ /** Parse CSV text (or UTF-8 bytes) into a workbook holding a single worksheet. */
17
+ export function readCsv(input, options = {}) {
18
+ const text = stripBom(typeof input === 'string' ? input : Buffer.from(input).toString('utf8'));
19
+ const delimiter = options.delimiter ?? ',';
20
+ if (delimiter.length !== 1) {
21
+ throw new AuthoringError(`CSV delimiter must be a single character, got ${JSON.stringify(delimiter)}`);
22
+ }
23
+ let rows = parseCsvRows(text, delimiter);
24
+ if (options.headers)
25
+ rows = rows.slice(1);
26
+ const coerce = options.map ?? defaultCsvCoerce;
27
+ const workbook = new Workbook();
28
+ const sheet = workbook.addWorksheet(options.sheetName ?? 'Sheet1');
29
+ sheet.addRows(rows.map((fields) => fields.map((field, index) => coerce(field, index))));
30
+ return workbook;
31
+ }
32
+ function stripBom(text) {
33
+ return text.charCodeAt(0) === 0xfeff ? text.slice(1) : text;
34
+ }
35
+ // A character-scan parser: quotes toggle literal mode, a doubled quote inside a quoted field is one
36
+ // quote, and a row ends on LF (a preceding CR is dropped, so CRLF and LF read the same). A final
37
+ // trailing newline does not yield a spurious empty row.
38
+ function parseCsvRows(text, delimiter) {
39
+ const rows = [];
40
+ let row = [];
41
+ let field = '';
42
+ let inQuotes = false;
43
+ for (let i = 0; i < text.length; i++) {
44
+ const ch = text.charAt(i);
45
+ if (inQuotes) {
46
+ if (ch === '"') {
47
+ if (text[i + 1] === '"') {
48
+ field += '"';
49
+ i++;
50
+ }
51
+ else {
52
+ inQuotes = false;
53
+ }
54
+ }
55
+ else {
56
+ field += ch;
57
+ }
58
+ continue;
59
+ }
60
+ if (ch === '"') {
61
+ inQuotes = true;
62
+ }
63
+ else if (ch === delimiter) {
64
+ row.push(field);
65
+ field = '';
66
+ }
67
+ else if (ch === '\n') {
68
+ row.push(field);
69
+ rows.push(row);
70
+ row = [];
71
+ field = '';
72
+ }
73
+ else if (ch !== '\r') {
74
+ field += ch;
75
+ }
76
+ }
77
+ row.push(field);
78
+ const trailingNewline = text.endsWith('\n') || text.endsWith('\r');
79
+ if (!(trailingNewline && row.length === 1 && row[0] === ''))
80
+ rows.push(row);
81
+ return rows;
82
+ }
83
+ const NUMERIC = /^[-+]?(\d+\.?\d*|\.\d+)([eE][-+]?\d+)?$/;
84
+ const ISO_DATE = /^(\d{4})-(\d{2})-(\d{2})(?:[T ](\d{2}):(\d{2})(?::(\d{2}))?)?$/;
85
+ function defaultCsvCoerce(field) {
86
+ if (field === '')
87
+ return null;
88
+ if (NUMERIC.test(field)) {
89
+ const value = Number(field);
90
+ // Beyond the safe-integer range a double silently loses digits; keep the original text instead.
91
+ return Number.isFinite(value) && Math.abs(value) <= Number.MAX_SAFE_INTEGER ? value : field;
92
+ }
93
+ const iso = ISO_DATE.exec(field);
94
+ if (iso) {
95
+ const date = isoToDate(iso);
96
+ if (date !== null)
97
+ return date;
98
+ }
99
+ return field;
100
+ }
101
+ function isoToDate(match) {
102
+ const year = Number(match[1]);
103
+ const month = Number(match[2]);
104
+ const day = Number(match[3]);
105
+ const hour = match[4] === undefined ? 0 : Number(match[4]);
106
+ const minute = match[5] === undefined ? 0 : Number(match[5]);
107
+ const second = match[6] === undefined ? 0 : Number(match[6]);
108
+ if (month < 1 || month > 12 || day < 1 || day > 31 || hour > 23 || minute > 59 || second > 59) {
109
+ return null;
110
+ }
111
+ const date = new Date(Date.UTC(year, month - 1, day, hour, minute, second));
112
+ return Number.isNaN(date.getTime()) ? null : date;
113
+ }
@@ -0,0 +1,28 @@
1
+ import type { CellValue } from '../../core/value.ts';
2
+ import type { Workbook } from '../../core/workbook.ts';
3
+ export interface CsvWriteOptions {
4
+ /** Which worksheet to write; defaults to the first. A name matching no sheet throws rather than
5
+ * silently emitting an empty file. */
6
+ readonly sheetName?: string;
7
+ /** Field separator; defaults to a comma. */
8
+ readonly delimiter?: string;
9
+ /** Line separator between rows; defaults to `"\n"`. */
10
+ readonly rowDelimiter?: string;
11
+ /** A token format (e.g. `"MM/DD/YYYY"`) for Date cells; without it a Date renders as a full
12
+ * ISO-8601 timestamp. */
13
+ readonly dateFormat?: string;
14
+ /** Render Date cells in UTC rather than the runner's local time. */
15
+ readonly dateUTC?: boolean;
16
+ /** Byte encoding for {@link writeCsv}; defaults to `"utf8"`. */
17
+ readonly encoding?: BufferEncoding;
18
+ /** Prepend a UTF-8 byte-order mark (applies only to UTF-8); defaults to `true` for UTF-8. */
19
+ readonly bom?: boolean;
20
+ /** Per-field transform replacing the default value rendering; receives the cell's value (`null`
21
+ * for an unpopulated column) and its 0-based column index. Quoting (commas, quotes, newlines) is
22
+ * still applied to the returned text. */
23
+ readonly map?: (value: CellValue, index: number) => string;
24
+ }
25
+ /** The logical CSV text of one worksheet — no BOM, no byte encoding. */
26
+ export declare function writeCsvText(workbook: Workbook, options?: CsvWriteOptions): string;
27
+ /** The CSV bytes of one worksheet in the requested encoding, with a UTF-8 BOM by default. */
28
+ export declare function writeCsv(workbook: Workbook, options?: CsvWriteOptions): Uint8Array;
@@ -0,0 +1,124 @@
1
+ // CSV serialization — the flat-text sibling of the XLSX writer.
2
+ //
3
+ // A worksheet is a rectangle of typed cells; CSV is that rectangle flattened to delimited
4
+ // text. The lossy direction (styles, formulas-as-formulas, multiple sheets) is inherent to the
5
+ // format, so this writer makes the honest choices explicit: one selected sheet, each row sized to
6
+ // its own populated extent (never clamped to a sibling row's width), a formula rendered as its
7
+ // cached result, a Date rendered by a caller-supplied format or a full ISO-8601 timestamp.
8
+ //
9
+ // `writeCsvText` yields the logical text; `writeCsv` encodes it to bytes and — for UTF-8, the
10
+ // default — prepends a byte-order mark so a consumer such as Excel detects the encoding and does
11
+ // not mangle non-ASCII on open. The BOM is a byte-level marker, not part of the logical text.
12
+ import { isErrorValue, isFormulaValue, isHyperlinkValue, isRichTextValue, isSharedFormulaValue, richTextToPlain, } from "../../core/value.js";
13
+ import { AuthoringError } from "../../errors.js";
14
+ const UTF8_BOM = Uint8Array.of(0xef, 0xbb, 0xbf);
15
+ /** The logical CSV text of one worksheet — no BOM, no byte encoding. */
16
+ export function writeCsvText(workbook, options = {}) {
17
+ const sheet = selectSheet(workbook, options.sheetName);
18
+ const delimiter = options.delimiter ?? ',';
19
+ const rowDelimiter = options.rowDelimiter ?? '\n';
20
+ const lines = [];
21
+ for (const { cells } of sheet.rows()) {
22
+ let width = 0;
23
+ const byColumn = new Map();
24
+ for (const cell of cells) {
25
+ byColumn.set(cell.col, cell);
26
+ if (cell.col > width)
27
+ width = cell.col;
28
+ }
29
+ const fields = [];
30
+ for (let column = 1; column <= width; column++) {
31
+ const value = byColumn.get(column)?.value ?? null;
32
+ const text = options.map ? options.map(value, column - 1) : csvFieldText(value, options);
33
+ fields.push(quoteField(text, delimiter));
34
+ }
35
+ lines.push(fields.join(delimiter));
36
+ }
37
+ return lines.join(rowDelimiter);
38
+ }
39
+ /** The CSV bytes of one worksheet in the requested encoding, with a UTF-8 BOM by default. */
40
+ export function writeCsv(workbook, options = {}) {
41
+ const text = writeCsvText(workbook, options);
42
+ const encoding = options.encoding ?? 'utf8';
43
+ const body = Buffer.from(text, encoding);
44
+ const wantBom = options.bom ?? encoding === 'utf8';
45
+ if (!wantBom || encoding !== 'utf8')
46
+ return Uint8Array.from(body);
47
+ const out = new Uint8Array(UTF8_BOM.length + body.length);
48
+ out.set(UTF8_BOM, 0);
49
+ out.set(body, UTF8_BOM.length);
50
+ return out;
51
+ }
52
+ function selectSheet(workbook, name) {
53
+ if (name === undefined) {
54
+ const first = workbook.worksheets[0];
55
+ if (first === undefined)
56
+ throw new AuthoringError('workbook has no worksheet to write as CSV');
57
+ return first;
58
+ }
59
+ const sheet = workbook.getWorksheet(name);
60
+ if (sheet === undefined)
61
+ throw new AuthoringError(`no worksheet named "${name}" to write as CSV`);
62
+ return sheet;
63
+ }
64
+ function csvFieldText(value, options) {
65
+ if (value === null)
66
+ return '';
67
+ if (typeof value === 'number')
68
+ return String(value);
69
+ if (typeof value === 'boolean')
70
+ return value ? 'TRUE' : 'FALSE';
71
+ if (typeof value === 'string')
72
+ return value;
73
+ if (value instanceof Date)
74
+ return formatDate(value, options.dateFormat, options.dateUTC ?? false);
75
+ if (isErrorValue(value))
76
+ return value.error;
77
+ if (isRichTextValue(value))
78
+ return richTextToPlain(value);
79
+ if (isHyperlinkValue(value)) {
80
+ return typeof value.text === 'string' ? value.text : richTextToPlain(value.text);
81
+ }
82
+ if (isFormulaValue(value) || isSharedFormulaValue(value)) {
83
+ return value.result === undefined ? '' : csvFieldText(value.result, options);
84
+ }
85
+ return '';
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
+ function quoteField(field, delimiter) {
117
+ if (field.includes(delimiter) ||
118
+ field.includes('"') ||
119
+ field.includes('\n') ||
120
+ field.includes('\r')) {
121
+ return `"${field.replace(/"/g, '""')}"`;
122
+ }
123
+ return field;
124
+ }
@@ -0,0 +1,46 @@
1
+ import { XlsxError } from '../../errors.ts';
2
+ /**
3
+ * Which unsupported input the reader recognised:
4
+ * - `'xls'` — a legacy BIFF `.xls` (an OLE2/CFB compound file), detected by its magic bytes.
5
+ * - `'xlsb'` — a binary BIFF12 `.xlsb`: the same OPC/ZIP container as `.xlsx`, but its office document
6
+ * is `xl/workbook.bin` rather than `xl/workbook.xml`. `readXlsx`/`readXlsb` read one; the entry points
7
+ * that cannot yet (the row streamer) report it under this format with their own message.
8
+ * - `'unknown'` — not a recognised spreadsheet at all: not a ZIP, or a ZIP carrying no OOXML workbook
9
+ * part (nor a `.xlsb` binary one).
10
+ */
11
+ export type UnsupportedFormat = 'xls' | 'xlsb' | 'unknown';
12
+ /**
13
+ * Thrown when input is not a readable `.xlsx` package. The single {@link format} field is the branch a
14
+ * caller keys on (rather than a subclass per format), so a `catch` can distinguish a legacy `.xls`, a
15
+ * binary `.xlsb`, and an unrecognised blob without string-matching the message.
16
+ *
17
+ * The message never carries a filesystem path or the underlying zip library's internals — the whole
18
+ * point of the type is that the classification, not a leaked lower-layer string, is what the caller sees.
19
+ *
20
+ * {@link format} stays the branch for *which* unsupported input this was; the inherited
21
+ * {@link XlsxError.code} answers the coarser question of what kind of failure it is.
22
+ */
23
+ export declare class UnsupportedFormatError extends XlsxError {
24
+ readonly name = "UnsupportedFormatError";
25
+ readonly code = "unsupported-format";
26
+ readonly format: UnsupportedFormat;
27
+ constructor(format: UnsupportedFormat, message?: string, options?: ErrorOptions);
28
+ }
29
+ /**
30
+ * Thrown when the input *is* a ZIP container but it cannot be unpacked: the archive is corrupt or
31
+ * truncated, or inflating it would push total uncompressed output past the caller's bound (which is
32
+ * how a zip bomb presents).
33
+ *
34
+ * The neighbouring {@link UnsupportedFormatError} says the input is a different *kind* of thing; this
35
+ * one says it is the right kind and we will not (or cannot) unpack it. Keeping them apart is what
36
+ * lets a caller answer "should I try another reader, or reject this file?" — and it is what replaced
37
+ * the message-prefix match the bomb refusal used to be recognised by.
38
+ *
39
+ * The zip library's own failure text never survives into either the message or `cause`: it can name
40
+ * internals — or an absolute filesystem path — from the layer below, and this type carries the
41
+ * classification precisely so no lower-layer string has to.
42
+ */
43
+ export declare class PackageReadError extends XlsxError {
44
+ readonly name = "PackageReadError";
45
+ readonly code = "malformed-input";
46
+ }
@@ -0,0 +1,49 @@
1
+ // Typed errors the reader raises when it is handed something that is not a readable OOXML `.xlsx`
2
+ // package. A spreadsheet library parses untrusted files, so the failure a caller sees must be a clear,
3
+ // programmatically-branchable signal — not a raw zip-internals string (which is opaque, and can leak an
4
+ // absolute filesystem path from the layer below). See the spec
5
+ // `docs/knowledge/specs/unsupported-input-format-typed-error.md`.
6
+ import { XlsxError } from "../../errors.js";
7
+ const DEFAULT_MESSAGE = {
8
+ xls: 'the legacy .xls binary format (BIFF/OLE2 compound file) is not supported; only OOXML .xlsx is read',
9
+ xlsb: 'this entry point does not read the binary .xlsb format (BIFF12)',
10
+ unknown: 'not a valid .xlsx package: no OOXML workbook part was found',
11
+ };
12
+ /**
13
+ * Thrown when input is not a readable `.xlsx` package. The single {@link format} field is the branch a
14
+ * caller keys on (rather than a subclass per format), so a `catch` can distinguish a legacy `.xls`, a
15
+ * binary `.xlsb`, and an unrecognised blob without string-matching the message.
16
+ *
17
+ * The message never carries a filesystem path or the underlying zip library's internals — the whole
18
+ * point of the type is that the classification, not a leaked lower-layer string, is what the caller sees.
19
+ *
20
+ * {@link format} stays the branch for *which* unsupported input this was; the inherited
21
+ * {@link XlsxError.code} answers the coarser question of what kind of failure it is.
22
+ */
23
+ export class UnsupportedFormatError extends XlsxError {
24
+ name = 'UnsupportedFormatError';
25
+ code = 'unsupported-format';
26
+ format;
27
+ constructor(format, message = DEFAULT_MESSAGE[format], options) {
28
+ super(message, options);
29
+ this.format = format;
30
+ }
31
+ }
32
+ /**
33
+ * Thrown when the input *is* a ZIP container but it cannot be unpacked: the archive is corrupt or
34
+ * truncated, or inflating it would push total uncompressed output past the caller's bound (which is
35
+ * how a zip bomb presents).
36
+ *
37
+ * The neighbouring {@link UnsupportedFormatError} says the input is a different *kind* of thing; this
38
+ * one says it is the right kind and we will not (or cannot) unpack it. Keeping them apart is what
39
+ * lets a caller answer "should I try another reader, or reject this file?" — and it is what replaced
40
+ * the message-prefix match the bomb refusal used to be recognised by.
41
+ *
42
+ * The zip library's own failure text never survives into either the message or `cause`: it can name
43
+ * internals — or an absolute filesystem path — from the layer below, and this type carries the
44
+ * classification precisely so no lower-layer string has to.
45
+ */
46
+ export class PackageReadError extends XlsxError {
47
+ name = 'PackageReadError';
48
+ code = 'malformed-input';
49
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Inflate every part of an `.xlsx` zip package, enforcing a hard ceiling on total
3
+ * decompressed output.
4
+ *
5
+ * @param data The raw zip bytes.
6
+ * @param cap Maximum total uncompressed output, in bytes, across all parts. Enforced
7
+ * against bytes actually produced, never against the archive's declared sizes.
8
+ * @returns A map of part path to inflated bytes.
9
+ * @throws {@link PackageReadError} if inflation would exceed `cap` — a probable zip bomb.
10
+ * @throws {Error} raised by the zip layer if the archive is malformed or a part uses an unsupported
11
+ * compression method. Callers reach this through {@link inflateSpreadsheetPackage}, which is where
12
+ * those are classified; the raw error is deliberately not re-typed here, because its text is the
13
+ * one thing that must not reach a caller unfiltered.
14
+ */
15
+ export declare function inflatePackage(data: Uint8Array, cap: number): Record<string, Uint8Array>;
@@ -0,0 +1,91 @@
1
+ // Bounded, streaming inflation of an OPC (`.xlsx`) zip package.
2
+ //
3
+ // Decompression is the reader's first hostile-input surface: a "zip bomb" ships a few
4
+ // kilobytes that inflate to gigabytes. The zip's own size headers cannot be trusted to
5
+ // bound this — they are attacker-controlled. A header that declares a *large* size makes a
6
+ // naïve reader preallocate that much (an amplifier: tiny input, huge allocation); a header
7
+ // that lies *small* makes a size-preallocating inflater silently truncate real data. So we
8
+ // consult the declared sizes for nothing. Instead the compressed archive is fed to fflate's
9
+ // streaming unzip in slices, the decompressor grows its output from the bytes it actually
10
+ // produces, and a running counter of real output aborts the moment it crosses the cap.
11
+ //
12
+ // Because DEFLATE cannot expand input by more than ~1032:1, feeding at most INPUT_SLICE
13
+ // compressed bytes before each counter check bounds the worst-case overshoot past the cap
14
+ // to one slice's expansion — not the whole (possibly enormous) stream.
15
+ import { Unzip, UnzipInflate } from 'fflate';
16
+ import { PackageReadError } from "./errors.js";
17
+ // Compressed input is pushed in slices this size so decompressed output arrives in
18
+ // increments the running counter can check. Small enough that the worst-case overshoot
19
+ // (slice × DEFLATE's ~1032:1 ceiling ≈ 16 MiB) is negligible against a sane cap; large
20
+ // enough that a legitimate multi-hundred-megabyte package still streams in cheaply.
21
+ const INPUT_SLICE = 1 << 14;
22
+ /**
23
+ * Inflate every part of an `.xlsx` zip package, enforcing a hard ceiling on total
24
+ * decompressed output.
25
+ *
26
+ * @param data The raw zip bytes.
27
+ * @param cap Maximum total uncompressed output, in bytes, across all parts. Enforced
28
+ * against bytes actually produced, never against the archive's declared sizes.
29
+ * @returns A map of part path to inflated bytes.
30
+ * @throws {@link PackageReadError} if inflation would exceed `cap` — a probable zip bomb.
31
+ * @throws {Error} raised by the zip layer if the archive is malformed or a part uses an unsupported
32
+ * compression method. Callers reach this through {@link inflateSpreadsheetPackage}, which is where
33
+ * those are classified; the raw error is deliberately not re-typed here, because its text is the
34
+ * one thing that must not reach a caller unfiltered.
35
+ */
36
+ export function inflatePackage(data, cap) {
37
+ const files = {};
38
+ let total = 0;
39
+ let failure;
40
+ const unzip = new Unzip((file) => {
41
+ const chunks = [];
42
+ let size = 0;
43
+ file.ondata = (error, chunk, final) => {
44
+ if (failure)
45
+ return;
46
+ if (error) {
47
+ failure = error;
48
+ return;
49
+ }
50
+ total += chunk.length;
51
+ if (total > cap) {
52
+ failure = new PackageReadError(`refusing to inflate: uncompressed output exceeds ${cap} bytes (possible zip bomb)`);
53
+ return;
54
+ }
55
+ chunks.push(chunk);
56
+ size += chunk.length;
57
+ if (final)
58
+ files[file.name] = join(chunks, size);
59
+ };
60
+ // `start()` reports an unsupported compression method through `ondata` above, then
61
+ // throws trying to build the missing decoder. Keep the reported error (its message
62
+ // names the method) and swallow the raw follow-on throw.
63
+ try {
64
+ file.start();
65
+ }
66
+ catch (error) {
67
+ failure ??= error instanceof Error ? error : new Error(String(error));
68
+ }
69
+ });
70
+ unzip.register(UnzipInflate);
71
+ let offset = 0;
72
+ do {
73
+ const end = Math.min(offset + INPUT_SLICE, data.length);
74
+ unzip.push(data.subarray(offset, end), end === data.length);
75
+ offset = end;
76
+ } while (offset < data.length && !failure);
77
+ if (failure)
78
+ throw failure;
79
+ return files;
80
+ }
81
+ function join(chunks, size) {
82
+ if (chunks.length === 1)
83
+ return chunks[0];
84
+ const out = new Uint8Array(size);
85
+ let at = 0;
86
+ for (const chunk of chunks) {
87
+ out.set(chunk, at);
88
+ at += chunk.length;
89
+ }
90
+ return out;
91
+ }
@@ -0,0 +1,18 @@
1
+ /**
2
+ * The namespace URIs the Open Packaging Conventions layer itself owns — the ones that describe a
3
+ * *package* rather than the spreadsheet inside it. Every OOXML package carries these regardless of
4
+ * which serialisation its office document uses, so an `.xlsb` and an `.xlsx` agree on them exactly.
5
+ *
6
+ * The SpreadsheetML vocabulary (the main namespace, DrawingML, the Microsoft extension URIs) is a
7
+ * property of the XML codec and lives in `../xlsx/namespaces.ts`.
8
+ */
9
+ /** The `xmlns` of every `.rels` package relationships part. */
10
+ export declare const PKG_RELS_NS = "http://schemas.openxmlformats.org/package/2006/relationships";
11
+ /** The `xmlns` of the package's `[Content_Types].xml`. */
12
+ export declare const CONTENT_TYPES_NS = "http://schemas.openxmlformats.org/package/2006/content-types";
13
+ /**
14
+ * Scopes both the relationship *type* URIs a `.rels` part declares and the `r:id` references a part's
15
+ * body carries. Part-level rather than package-level in ECMA-376's own split, but it is the vocabulary
16
+ * the relationship graph is written in, so it belongs with the graph and not with either codec.
17
+ */
18
+ export declare const RELATIONSHIPS_NS = "http://schemas.openxmlformats.org/officeDocument/2006/relationships";
@@ -0,0 +1,18 @@
1
+ /**
2
+ * The namespace URIs the Open Packaging Conventions layer itself owns — the ones that describe a
3
+ * *package* rather than the spreadsheet inside it. Every OOXML package carries these regardless of
4
+ * which serialisation its office document uses, so an `.xlsb` and an `.xlsx` agree on them exactly.
5
+ *
6
+ * The SpreadsheetML vocabulary (the main namespace, DrawingML, the Microsoft extension URIs) is a
7
+ * property of the XML codec and lives in `../xlsx/namespaces.ts`.
8
+ */
9
+ /** The `xmlns` of every `.rels` package relationships part. */
10
+ export const PKG_RELS_NS = 'http://schemas.openxmlformats.org/package/2006/relationships';
11
+ /** The `xmlns` of the package's `[Content_Types].xml`. */
12
+ export const CONTENT_TYPES_NS = 'http://schemas.openxmlformats.org/package/2006/content-types';
13
+ /**
14
+ * Scopes both the relationship *type* URIs a `.rels` part declares and the `r:id` references a part's
15
+ * body carries. Part-level rather than package-level in ECMA-376's own split, but it is the vocabulary
16
+ * the relationship graph is written in, so it belongs with the graph and not with either codec.
17
+ */
18
+ export const RELATIONSHIPS_NS = 'http://schemas.openxmlformats.org/officeDocument/2006/relationships';
@@ -0,0 +1,4 @@
1
+ export declare const THEME_PART_PATH = "xl/theme/theme1.xml";
2
+ export declare function extensionOf(partPath: string): string;
3
+ export declare function relsPathFor(partPath: string): string;
4
+ export declare function relativePartPath(fromPath: string, toPath: string): string;
@@ -0,0 +1,35 @@
1
+ // The path algebra of an OPC package: a part's extension, where its relationships live, and how one
2
+ // part names another. Pure string work over package-absolute paths, with no notion of what any part
3
+ // contains — so the XML and BIFF12 codecs, and the readers and writers within each, share it.
4
+ // Where the writer always puts the theme part. The workbook's theme relationship and the package's
5
+ // content-type override both name this path unconditionally, so a theme preserved from a source
6
+ // package that called its part something else is re-emitted here rather than at its original name.
7
+ export const THEME_PART_PATH = 'xl/theme/theme1.xml';
8
+ // The extension of a part path (`xl/media/image1.jpeg` → `jpeg`), or '' when it carries none.
9
+ export function extensionOf(partPath) {
10
+ const dot = partPath.lastIndexOf('.');
11
+ const slash = partPath.lastIndexOf('/');
12
+ return dot > slash ? partPath.slice(dot + 1) : '';
13
+ }
14
+ // The relationships part path for `dir/name.ext` → `dir/_rels/name.ext.rels`.
15
+ export function relsPathFor(partPath) {
16
+ const slash = partPath.lastIndexOf('/');
17
+ const dir = slash === -1 ? '' : partPath.slice(0, slash + 1);
18
+ const base = slash === -1 ? partPath : partPath.slice(slash + 1);
19
+ return `${dir}_rels/${base}.rels`;
20
+ }
21
+ // A relationship target expressed relative to the part that carries it: the `..` hops out of the
22
+ // referencing part's directory up to the common ancestor, then down to the target. Both paths are
23
+ // package-absolute (`xl/drawings/preservedP1.vml` → `xl/media/preservedP2.jpeg` → `../media/preservedP2.jpeg`).
24
+ export function relativePartPath(fromPath, toPath) {
25
+ const fromDir = fromPath.split('/').slice(0, -1);
26
+ const toSegments = toPath.split('/');
27
+ let common = 0;
28
+ while (common < fromDir.length &&
29
+ common < toSegments.length - 1 &&
30
+ fromDir[common] === toSegments[common]) {
31
+ common++;
32
+ }
33
+ const up = fromDir.length - common;
34
+ return [...Array(up).fill('..'), ...toSegments.slice(common)].join('/');
35
+ }
@@ -0,0 +1,24 @@
1
+ import type { PreservedPart } from '../../core/preserved.ts';
2
+ export interface PackageAccessors {
3
+ /** A part's decoded text, or undefined when the package holds no such part. Decodes lazily, so a
4
+ * part the reader never asks for is never stringified. */
5
+ partText(path: string): string | undefined;
6
+ /** A part's raw bytes, or undefined when the package holds no such part. */
7
+ partBytes(path: string): Uint8Array | undefined;
8
+ }
9
+ export declare function packageAccessors(files: Record<string, Uint8Array>): PackageAccessors;
10
+ export declare function relationshipTargetByType(xml: string, suffix: string): string | undefined;
11
+ export declare function relationshipTargetsByType(xml: string, suffix: string): string[];
12
+ export declare function sheetRelTarget(sheetPath: string, partText: (path: string) => string | undefined, type: string): string | undefined;
13
+ export declare function resolveRelativePart(basePart: string, target: string): string;
14
+ export declare function resolveWorkbookPart(target: string): string;
15
+ export declare function parseRelationships(xml: string): Map<string, string>;
16
+ export interface RelationshipRecord {
17
+ readonly id: string;
18
+ readonly type: string;
19
+ readonly target: string;
20
+ readonly external: boolean;
21
+ }
22
+ export declare function parseRelationshipRecords(xml: string): RelationshipRecord[];
23
+ export declare function contentTypeResolver(contentTypesXml: string): (path: string) => string;
24
+ export declare function capturePartClosure(entryPath: string, partText: (path: string) => string | undefined, partBytes: (path: string) => Uint8Array | undefined, contentTypeOf: (path: string) => string): readonly PreservedPart[] | undefined;