@opencraw/office-reader 0.0.2

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 (34) hide show
  1. package/README.md +201 -0
  2. package/dist/index.d.ts +1 -0
  3. package/dist/index.esm.js +6 -0
  4. package/dist/pptx.d.ts +1 -0
  5. package/dist/pptx.esm.js +5 -0
  6. package/dist/read-pptx.use-case.esm.js +591 -0
  7. package/dist/read-source.client.esm.js +289 -0
  8. package/dist/read-xlsx.use-case.esm.js +381 -0
  9. package/dist/src/index.d.ts +9 -0
  10. package/dist/src/ooxml-package/index.d.ts +7 -0
  11. package/dist/src/ooxml-package/ooxml-package.client.d.ts +47 -0
  12. package/dist/src/ooxml-package/relationships.mapper.d.ts +27 -0
  13. package/dist/src/ooxml-package/xml-walk.algorithm.d.ts +37 -0
  14. package/dist/src/presentation/chart.mapper.d.ts +13 -0
  15. package/dist/src/presentation/deck.model.d.ts +56 -0
  16. package/dist/src/presentation/index.d.ts +6 -0
  17. package/dist/src/presentation/notes.mapper.d.ts +9 -0
  18. package/dist/src/presentation/placeholder-geometry.mapper.d.ts +53 -0
  19. package/dist/src/presentation/read-pptx.use-case.d.ts +32 -0
  20. package/dist/src/presentation/slide.mapper.d.ts +24 -0
  21. package/dist/src/read-error/index.d.ts +3 -0
  22. package/dist/src/read-error/office-read.error.d.ts +29 -0
  23. package/dist/src/source-bytes/index.d.ts +3 -0
  24. package/dist/src/source-bytes/read-source.client.d.ts +18 -0
  25. package/dist/src/spreadsheet/cell-value.algorithm.d.ts +47 -0
  26. package/dist/src/spreadsheet/index.d.ts +6 -0
  27. package/dist/src/spreadsheet/number-formats.mapper.d.ts +10 -0
  28. package/dist/src/spreadsheet/read-xlsx.use-case.d.ts +27 -0
  29. package/dist/src/spreadsheet/shared-strings.mapper.d.ts +10 -0
  30. package/dist/src/spreadsheet/workbook.model.d.ts +35 -0
  31. package/dist/src/spreadsheet/worksheet.mapper.d.ts +23 -0
  32. package/dist/xlsx.d.ts +1 -0
  33. package/dist/xlsx.esm.js +5 -0
  34. package/package.json +77 -0
@@ -0,0 +1,289 @@
1
+ import { unzipSync, strFromU8 } from 'fflate';
2
+ import { Parser } from 'htmlparser2';
3
+
4
+ /** A file `office-reader` cannot read, with a `code` to branch on and a message that says what to do. */
5
+ class OfficeReadError extends Error {
6
+ code;
7
+ name = 'OfficeReadError';
8
+ constructor(code, message, options) {
9
+ super(message, options);
10
+ this.code = code;
11
+ }
12
+ }
13
+
14
+ const MIB = 1024 * 1024;
15
+ const DEFAULT_ENTRY_BYTES = 256 * MIB;
16
+ const DEFAULT_TOTAL_BYTES = 512 * MIB;
17
+ /** An Office Open XML package: a zip of XML parts, read part by part. */
18
+ class OoxmlPackage {
19
+ bytes;
20
+ entries;
21
+ limits;
22
+ /**
23
+ * Opens a package, refusing what is not one with the reason: a legacy or
24
+ * password-protected Office file (both are OLE compound files), an
25
+ * OpenDocument file, anything else that is not a zip.
26
+ *
27
+ * @param bytes - The file.
28
+ * @param limits - How much it may inflate to.
29
+ * @returns The package; no part is inflated yet.
30
+ * @throws OfficeReadError
31
+ */
32
+ static open(bytes, limits = {}) {
33
+ if (isCompoundFile(bytes)) {
34
+ if (holdsEncryptionInfo(bytes)) throw new OfficeReadError('encrypted', 'a password-protected Office file: remove the password and save it again');
35
+ throw new OfficeReadError('legacy-format', 'a legacy binary Office file (.xls, .ppt, .doc): save it as .xlsx or .pptx, or export it as PDF');
36
+ }
37
+ const entries = new Map();
38
+ try {
39
+ unzipSync(bytes, {
40
+ filter: file => {
41
+ entries.set(partKey(file.name), {
42
+ name: file.name,
43
+ size: file.originalSize
44
+ });
45
+ return false;
46
+ }
47
+ });
48
+ } catch (error) {
49
+ throw new OfficeReadError('not-zip', `not an Office file: not a readable zip (${error.message})`, {
50
+ cause: error
51
+ });
52
+ }
53
+ const opened = new OoxmlPackage(bytes, entries, {
54
+ entryBytes: limits.entryBytes ?? DEFAULT_ENTRY_BYTES,
55
+ totalBytes: limits.totalBytes ?? DEFAULT_TOTAL_BYTES
56
+ });
57
+ const mimetype = opened.has('mimetype') ? opened.text('mimetype').trim() : '';
58
+ if (mimetype.startsWith('application/vnd.oasis.opendocument')) throw new OfficeReadError('unsupported-format', `an OpenDocument file (${mimetype}): save it as .xlsx or .pptx`);
59
+ return opened;
60
+ }
61
+ declared = 0;
62
+ /**
63
+ * Entries by part key: part names are case-insensitive (OPC), and some
64
+ * generators store `xl\\sharedstrings.xml` for `xl/sharedStrings.xml`.
65
+ */
66
+ constructor(bytes, entries, limits) {
67
+ this.bytes = bytes;
68
+ this.entries = entries;
69
+ this.limits = limits;
70
+ }
71
+ /** The names of every part, as stored. */
72
+ get names() {
73
+ return Array.from(this.entries.values(), entry => entry.name);
74
+ }
75
+ has(name) {
76
+ return this.entries.has(partKey(name));
77
+ }
78
+ /**
79
+ * Inflates one part as UTF-8 text.
80
+ *
81
+ * @param name - The part, as stored (`xl/workbook.xml`).
82
+ * @returns The text; empty when the part is missing.
83
+ * @throws OfficeReadError: `too-large` past the limits, `malformed` when the part does not inflate.
84
+ */
85
+ text(name) {
86
+ const entry = this.entries.get(partKey(name));
87
+ if (entry === undefined) return '';
88
+ if (entry.size > this.limits.entryBytes) throw new OfficeReadError('too-large', `${name} declares ${entry.size} bytes, over the ${this.limits.entryBytes}-byte entry limit`);
89
+ this.declared += entry.size;
90
+ if (this.declared > this.limits.totalBytes) throw new OfficeReadError('too-large', `the parts read declare over ${this.limits.totalBytes} bytes together`);
91
+ let inflated;
92
+ try {
93
+ inflated = unzipSync(this.bytes, {
94
+ filter: file => file.name === entry.name
95
+ })[entry.name];
96
+ } catch (error) {
97
+ throw new OfficeReadError('malformed', `${name} does not inflate (${error.message}): the file is damaged`, {
98
+ cause: error
99
+ });
100
+ }
101
+ return inflated === undefined ? '' : strFromU8(inflated);
102
+ }
103
+ }
104
+ /** How a part is looked up: forward slashes, no leading slash, lower case. */
105
+ function partKey(name) {
106
+ return name.replaceAll('\\', '/').replace(/^\//, '').toLowerCase();
107
+ }
108
+ /** An OLE compound file (`D0 CF 11 E0 A1 B1 1A E1`): a legacy Office file, or an encrypted new one. */
109
+ function isCompoundFile(bytes) {
110
+ const signature = [0xD0, 0xCF, 0x11, 0xE0, 0xA1, 0xB1, 0x1A, 0xE1];
111
+ return signature.every((byte, index) => bytes[index] === byte);
112
+ }
113
+ /** Whether a compound file holds an `EncryptionInfo` stream: an encrypted .xlsx/.pptx, not a legacy file. */
114
+ function holdsEncryptionInfo(bytes) {
115
+ const name = [...'EncryptionInfo'].flatMap(char => [char.codePointAt(0) ?? 0, 0]);
116
+ const last = bytes.length - name.length;
117
+ for (let start = 0; start <= last; start += 1) {
118
+ if (name.every((byte, index) => bytes[start + index] === byte)) return true;
119
+ }
120
+ return false;
121
+ }
122
+
123
+ /**
124
+ * Walks an XML part as a stream of events, never building a tree, so a sheet
125
+ * of a million cells costs its text, not a DOM. htmlparser2 does no DTD
126
+ * processing: entities a document declares are not expanded (no "billion
127
+ * laughs") and no external entity is fetched (no XXE); only the XML built-ins
128
+ * and numeric references decode.
129
+ *
130
+ * Element names lose their prefix (`x:c`, `p:sp` → `c`, `sp`): generators
131
+ * choose prefixes freely. Attributes keep theirs; read a namespaced one with
132
+ * {@link namespacedAttribute}.
133
+ *
134
+ * @param xml - The part's text.
135
+ * @param handlers - What to do on each event.
136
+ */
137
+ function walkXml(xml, handlers) {
138
+ const parser = new Parser({
139
+ onopentag: (name, attributes) => handlers.open?.(localName(name), attributes),
140
+ ontext: text => handlers.text?.(text),
141
+ onclosetag: name => handlers.close?.(localName(name))
142
+ }, {
143
+ xmlMode: true,
144
+ decodeEntities: true
145
+ });
146
+ parser.write(xml);
147
+ parser.end();
148
+ }
149
+ /**
150
+ * An attribute in a namespace whatever its prefix (`r:id`, `ns1:id`).
151
+ *
152
+ * @param attributes - The element's attributes.
153
+ * @param name - The local name (`id`).
154
+ * @returns The value, or `undefined`.
155
+ */
156
+ function namespacedAttribute(attributes, name) {
157
+ const key = Object.keys(attributes).find(candidate => candidate.endsWith(`:${name}`) && !candidate.startsWith('xmlns'));
158
+ return key === undefined ? undefined : attributes[key];
159
+ }
160
+ /**
161
+ * Whether an XML boolean attribute is on (`1`, `true`, `on`).
162
+ *
163
+ * @param value - The attribute's value.
164
+ * @returns Whether it is set.
165
+ */
166
+ function isOn(value) {
167
+ return value !== undefined && ['1', 'true', 'on'].includes(value);
168
+ }
169
+ function localName(name) {
170
+ const colon = name.indexOf(':');
171
+ return colon === -1 ? name : name.slice(colon + 1);
172
+ }
173
+
174
+ /**
175
+ * The relationships of a part, targets resolved to part names. External
176
+ * targets (hyperlinks) are left out.
177
+ *
178
+ * @param pkg - The package.
179
+ * @param part - The part (`xl/workbook.xml`), or `''` for the package's own.
180
+ * @returns The relationships by id.
181
+ */
182
+ function relationshipsOf(pkg, part) {
183
+ const directory = part.includes('/') ? part.slice(0, part.lastIndexOf('/')) : '';
184
+ const file = part.slice(part.lastIndexOf('/') + 1);
185
+ const relationships = new Map();
186
+ walkXml(pkg.text(`${directory === '' ? '' : `${directory}/`}_rels/${file}.rels`), {
187
+ open: (name, attributes) => {
188
+ if (name !== 'Relationship' || attributes.TargetMode === 'External' || attributes.Id === undefined || attributes.Target === undefined) return;
189
+ relationships.set(attributes.Id, {
190
+ id: attributes.Id,
191
+ type: (attributes.Type ?? '').split('/').at(-1) ?? '',
192
+ target: resolvePart(directory, attributes.Target)
193
+ });
194
+ }
195
+ });
196
+ return relationships;
197
+ }
198
+ /**
199
+ * The first relationship of a type.
200
+ *
201
+ * @param relationships - A part's relationships.
202
+ * @param type - The type's last segment.
203
+ * @returns It, or `undefined`.
204
+ */
205
+ function relationshipOfType(relationships, type) {
206
+ for (const relationship of relationships.values()) {
207
+ if (relationship.type === type) return relationship;
208
+ }
209
+ return undefined;
210
+ }
211
+ /** A target relative to its source part's directory (`worksheets/sheet1.xml`, `../media/x.png`) or absolute (`/xl/…`). */
212
+ function resolvePart(directory, target) {
213
+ const segments = target.startsWith('/') ? [] : directory.split('/').filter(segment => segment !== '');
214
+ for (const segment of target.split('/')) {
215
+ if (segment === '..') segments.pop();else if (segment !== '.' && segment !== '') segments.push(segment);
216
+ }
217
+ return segments.join('/');
218
+ }
219
+
220
+ /**
221
+ * Reads a source into one `Uint8Array`. Only a path or a `file:` URL touches
222
+ * the file system, through a dynamic import, so the package runs in browsers,
223
+ * workers and edge runtimes too. Fetching is the caller's job: an `http:` URL
224
+ * is refused.
225
+ *
226
+ * @param source - The file.
227
+ * @returns Its bytes.
228
+ * @throws OfficeReadError (`bad-source`) for anything else.
229
+ */
230
+ async function readSource(source) {
231
+ if (typeof source === 'string') return readPath(source);
232
+ if (source instanceof URL) {
233
+ if (source.protocol !== 'file:') throw new OfficeReadError('bad-source', `${source.href}: office-reader reads files, not URLs; fetch it and pass the bytes`);
234
+ return readPath(source);
235
+ }
236
+ if (source instanceof Uint8Array) return source;
237
+ if (source instanceof ArrayBuffer) return new Uint8Array(source);
238
+ if (ArrayBuffer.isView(source)) return new Uint8Array(source.buffer, source.byteOffset, source.byteLength);
239
+ if (typeof Blob !== 'undefined' && source instanceof Blob) return new Uint8Array(await source.arrayBuffer());
240
+ if (isReadableStream(source)) return concat(await readStream(source));
241
+ if (isAsyncIterable(source)) return concat(await readIterable(source));
242
+ throw new OfficeReadError('bad-source', 'office-reader reads a path, a file: URL, bytes, a Blob, a ReadableStream or an async iterable of bytes');
243
+ }
244
+ async function readPath(path) {
245
+ const {
246
+ readFile
247
+ } = await import('node:fs/promises');
248
+ return new Uint8Array(await readFile(path));
249
+ }
250
+ function isReadableStream(value) {
251
+ return typeof value === 'object' && value !== null && typeof value.getReader === 'function';
252
+ }
253
+ function isAsyncIterable(value) {
254
+ return typeof value === 'object' && value !== null && typeof value[Symbol.asyncIterator] === 'function';
255
+ }
256
+ async function readStream(stream) {
257
+ const reader = stream.getReader();
258
+ const chunks = [];
259
+ for (;;) {
260
+ const {
261
+ done,
262
+ value
263
+ } = await reader.read();
264
+ if (done) return chunks;
265
+ chunks.push(chunkOf(value));
266
+ }
267
+ }
268
+ async function readIterable(iterable) {
269
+ const chunks = [];
270
+ for await (const chunk of iterable) chunks.push(chunkOf(chunk));
271
+ return chunks;
272
+ }
273
+ function chunkOf(chunk) {
274
+ if (chunk instanceof Uint8Array) return chunk;
275
+ throw new OfficeReadError('bad-source', 'a stream of text is not a file: read it as bytes (no encoding set)');
276
+ }
277
+ function concat(chunks) {
278
+ if (chunks.length === 1) return chunks[0];
279
+ const bytes = new Uint8Array(chunks.reduce((total, chunk) => total + chunk.length, 0));
280
+ let offset = 0;
281
+ for (const chunk of chunks) {
282
+ bytes.set(chunk, offset);
283
+ offset += chunk.length;
284
+ }
285
+ return bytes;
286
+ }
287
+
288
+ export { OfficeReadError as O, OoxmlPackage as a, relationshipOfType as b, relationshipsOf as c, isOn as i, namespacedAttribute as n, readSource as r, walkXml as w };
289
+ //# sourceMappingURL=read-source.client.esm.js.map
@@ -0,0 +1,381 @@
1
+ import { w as walkXml, i as isOn, a as OoxmlPackage, r as readSource, O as OfficeReadError, b as relationshipOfType, c as relationshipsOf, n as namespacedAttribute } from './read-source.client.esm.js';
2
+ import 'fflate';
3
+
4
+ /** Built-in number formats that show a date (14–17, 22 and the East Asian ones) or a time of day (18–21, 45–47). */
5
+ const BUILT_IN_DATES = new Set([14, 15, 16, 17, 22, 27, 28, 29, 30, 31, 34, 35, 36, 50, 51, 52, 53, 54, 57, 58]);
6
+ const BUILT_IN_TIMES = new Set([18, 19, 20, 21, 32, 33, 45, 46, 47, 55, 56]);
7
+ /**
8
+ * What each cell style (`s`, an index into `cellXfs`) makes of a number, from
9
+ * `styles.xml`: only dates and times need telling apart from numbers.
10
+ *
11
+ * @param stylesXml - The styles part; empty when the workbook has none.
12
+ * @returns The kind per style index.
13
+ */
14
+ function formatKinds(stylesXml) {
15
+ const codes = new Map();
16
+ const kinds = [];
17
+ let inCellXfs = false;
18
+ walkXml(stylesXml, {
19
+ open: (name, attributes) => {
20
+ if (name === 'numFmt') codes.set(Number(attributes.numFmtId), attributes.formatCode ?? '');else if (name === 'cellXfs') inCellXfs = true;else if (name === 'xf' && inCellXfs) kinds.push(kindOf(Number(attributes.numFmtId ?? 0), codes));
21
+ },
22
+ close: name => {
23
+ if (name === 'cellXfs') inCellXfs = false;
24
+ }
25
+ });
26
+ return kinds;
27
+ }
28
+ /**
29
+ * What a format code shows: a date when it has a day, month or year, a time
30
+ * of day when it has only hours, minutes or seconds. Quoted text, escaped
31
+ * characters, colours and locales (`[Red]`, `[$-409]`) are ignored; an
32
+ * elapsed time (`[h]:mm`) is a duration, so a number.
33
+ */
34
+ function kindOf(id, codes) {
35
+ const code = codes.get(id);
36
+ if (code === undefined) {
37
+ if (BUILT_IN_DATES.has(id)) return 'date';
38
+ return BUILT_IN_TIMES.has(id) ? 'time' : 'number';
39
+ }
40
+ const firstSection = code.split(';', 1)[0];
41
+ if (/\[(?:h+|m+|s+)\]/i.test(firstSection)) return 'number';
42
+ const bare = firstSection.replaceAll(/"[^"]*"|\\.|\[[^\]]*\]/g, '').replaceAll(/am\/pm|a\/p/gi, '');
43
+ if (/[dy]/i.test(bare) || hasMonth(bare)) return 'date';
44
+ return /[hs]/i.test(bare) ? 'time' : 'number';
45
+ }
46
+ /** Whether a run of `m` is a month: minutes sit after an hour or before seconds (`h:mm`, `mm:ss`). */
47
+ function hasMonth(code) {
48
+ for (const run of code.matchAll(/m+/gi)) {
49
+ const before = code.slice(0, run.index).trimEnd().at(-1)?.toLowerCase();
50
+ const after = code.slice(run.index + run[0].length).trimStart()[0]?.toLowerCase();
51
+ const minutes = before === 'h' || before === ':' || after === ':' || after === 's';
52
+ if (!minutes) return true;
53
+ }
54
+ return false;
55
+ }
56
+
57
+ /**
58
+ * The shared string table: the text of every `<si>`, rich-text runs joined.
59
+ * Phonetic guides (`<rPh>`, the furigana over Japanese text) are not part of
60
+ * the text and are left out.
61
+ *
62
+ * @param xml - The shared strings part; empty when the workbook has none.
63
+ * @returns The strings, by index.
64
+ */
65
+ function sharedStrings(xml) {
66
+ const strings = [];
67
+ let current;
68
+ let inText = false;
69
+ let phonetic = 0;
70
+ walkXml(xml, {
71
+ open: name => {
72
+ if (name === 'si') current = '';else if (name === 'rPh') phonetic += 1;else if (name === 't' && phonetic === 0) inText = true;
73
+ },
74
+ text: text => {
75
+ if (inText && current !== undefined) current += text;
76
+ },
77
+ close: name => {
78
+ switch (name) {
79
+ case 't':
80
+ {
81
+ inText = false;
82
+ break;
83
+ }
84
+ case 'rPh':
85
+ {
86
+ phonetic -= 1;
87
+ break;
88
+ }
89
+ case 'si':
90
+ {
91
+ strings.push(current ?? '');
92
+ current = undefined;
93
+ break;
94
+ }
95
+ // No default
96
+ }
97
+ }
98
+ });
99
+ return strings;
100
+ }
101
+
102
+ const MS_PER_DAY = 86_400_000;
103
+ /** 1970-01-01 as a 1900-system serial number. */
104
+ const UNIX_EPOCH_SERIAL = 25_569;
105
+ /** Days between the 1900 and the 1904 systems' day zero. */
106
+ const DAYS_1904 = 1462;
107
+ /**
108
+ * A stored cell as a typed value.
109
+ *
110
+ * @param cell - The cell.
111
+ * @param context - Shared strings and the date system.
112
+ * @returns The value; `null` for an empty cell.
113
+ */
114
+ function typedValue(cell, context) {
115
+ switch (cell.type) {
116
+ case 's':
117
+ {
118
+ return context.sharedStrings[Number(cell.value)] ?? '';
119
+ }
120
+ case 'str':
121
+ case 'inlineStr':
122
+ {
123
+ return cell.value;
124
+ }
125
+ case 'b':
126
+ {
127
+ return cell.value === '1' || cell.value === 'true';
128
+ }
129
+ case 'e':
130
+ {
131
+ return {
132
+ error: cell.value
133
+ };
134
+ }
135
+ case 'd':
136
+ {
137
+ const date = new Date(cell.value.endsWith('Z') || /[+-]\d\d:\d\d$/.test(cell.value) ? cell.value : `${cell.value}Z`);
138
+ return Number.isNaN(date.getTime()) ? cell.value : date;
139
+ }
140
+ default:
141
+ {
142
+ if (cell.value.trim() === '') return null;
143
+ const number = Number(cell.value);
144
+ if (Number.isNaN(number)) return cell.value;
145
+ return cell.format === 'number' ? number : dateOf(number, context.date1904);
146
+ }
147
+ }
148
+ }
149
+ /**
150
+ * A stored cell as canonical text: numbers in their shortest round-trip form
151
+ * (`78.6`, not `78.599999999999994`), dates as ISO (`2026-06-01`, or
152
+ * `2026-06-01T09:30:00` with a time, `09:30:00` for a time of day), booleans
153
+ * as `true`/`false`, errors as written, `''` when empty. Display formats are
154
+ * not applied: a percentage stays a fraction (`0.125`).
155
+ *
156
+ * @param cell - The cell.
157
+ * @param context - Shared strings and the date system.
158
+ * @returns The text.
159
+ */
160
+ function textValue(cell, context) {
161
+ const value = typedValue(cell, context);
162
+ if (value === null) return '';
163
+ if (value instanceof Date) return isoText(value, cell.format === 'time' && Number(cell.value) < 1);
164
+ if (typeof value === 'object') return value.error;
165
+ return String(value);
166
+ }
167
+ /**
168
+ * Reads a cell in the mode asked for.
169
+ *
170
+ * @param cell - The cell.
171
+ * @param context - Shared strings and the date system.
172
+ * @param mode - `typed` or `text`.
173
+ * @returns The value.
174
+ */
175
+ function cellValue(cell, context, mode) {
176
+ return mode === 'text' ? textValue(cell, context) : typedValue(cell, context);
177
+ }
178
+ /**
179
+ * A serial date as a `Date` holding the wall-clock time as UTC, rounded to the
180
+ * second. The 1900 system counts the 29th of February 1900 that never was
181
+ * (Lotus's bug, kept for compatibility): serials before it are a day early.
182
+ */
183
+ function dateOf(serial, date1904) {
184
+ let days = date1904 ? serial + DAYS_1904 : serial;
185
+ if (!date1904 && serial >= 1 && serial < 60) days += 1;
186
+ const seconds = Math.round((days - UNIX_EPOCH_SERIAL) * (MS_PER_DAY / 1000));
187
+ return new Date(seconds * 1000);
188
+ }
189
+ function isoText(date, timeOfDay) {
190
+ const iso = date.toISOString();
191
+ if (timeOfDay) return iso.slice(11, 19);
192
+ return iso.slice(11, 19) === '00:00:00' ? iso.slice(0, 10) : iso.slice(0, 19);
193
+ }
194
+
195
+ /**
196
+ * Reads a worksheet part as a stream: rows of cells (formulas give their
197
+ * cached value, never evaluated), hidden rows and merged ranges. A row or a
198
+ * cell without its reference (some generators leave `r` out) follows the one
199
+ * before it.
200
+ *
201
+ * @param xml - The worksheet part.
202
+ * @param context - Shared strings, number format kinds and the date system.
203
+ * @param mode - `typed` or `text`.
204
+ * @returns The content.
205
+ */
206
+ function readWorksheet(xml, context, mode) {
207
+ const rows = [];
208
+ const hiddenRows = [];
209
+ const merges = [];
210
+ let row = -1;
211
+ let column = -1;
212
+ let cell;
213
+ let capturing;
214
+ let phonetic = 0;
215
+ const finish = open => {
216
+ const stored = {
217
+ type: open.type,
218
+ value: open.value,
219
+ format: context.formats[open.style] ?? 'number'
220
+ };
221
+ rows[open.row] ??= [];
222
+ rows[open.row][open.column] = cellValue(stored, context, mode);
223
+ };
224
+ walkXml(xml, {
225
+ open: (name, attributes) => {
226
+ switch (name) {
227
+ case 'row':
228
+ {
229
+ row = attributes.r === undefined ? row + 1 : Number(attributes.r) - 1;
230
+ column = -1;
231
+ rows[row] ??= [];
232
+ if (isOn(attributes.hidden)) hiddenRows.push(row);
233
+ break;
234
+ }
235
+ case 'c':
236
+ {
237
+ const reference = attributes.r === undefined ? undefined : cellOf(attributes.r);
238
+ if (reference !== undefined) row = reference.row;
239
+ column = reference?.column ?? column + 1;
240
+ cell = {
241
+ row,
242
+ column,
243
+ type: attributes.t ?? 'n',
244
+ style: Number(attributes.s ?? 0),
245
+ value: ''
246
+ };
247
+ break;
248
+ }
249
+ case 'v':
250
+ {
251
+ if (cell !== undefined) capturing = 'value';
252
+ break;
253
+ }
254
+ case 'rPh':
255
+ {
256
+ phonetic += 1;
257
+ break;
258
+ }
259
+ case 't':
260
+ {
261
+ if (phonetic === 0 && cell?.type === 'inlineStr') capturing = 'inline';
262
+ break;
263
+ }
264
+ case 'mergeCell':
265
+ {
266
+ if (attributes.ref !== undefined) merges.push(attributes.ref);
267
+ break;
268
+ }
269
+ // No default
270
+ }
271
+ },
272
+ text: text => {
273
+ if (capturing !== undefined && cell !== undefined) cell.value += text;
274
+ },
275
+ close: name => {
276
+ if (name === 'v' || name === 't') {
277
+ capturing = undefined;
278
+ } else if (name === 'rPh') {
279
+ phonetic -= 1;
280
+ } else if (name === 'c' && cell !== undefined) {
281
+ finish(cell);
282
+ cell = undefined;
283
+ }
284
+ }
285
+ });
286
+ const empty = mode === 'text' ? '' : null;
287
+ return {
288
+ rows: Array.from(rows, cells => Array.from(cells ?? [], value => value ?? empty)),
289
+ hiddenRows,
290
+ merges
291
+ };
292
+ }
293
+ /** `AB12` as 0-based row and column. */
294
+ function cellOf(reference) {
295
+ const match = /^\$?([A-Z]+)\$?(\d+)$/i.exec(reference);
296
+ if (match === null) return undefined;
297
+ const letters = match[1].toUpperCase();
298
+ let column = 0;
299
+ for (const letter of letters) column = column * 26 + (letter.codePointAt(0) ?? 64) - 64;
300
+ return {
301
+ row: Number(match[2]) - 1,
302
+ column: column - 1
303
+ };
304
+ }
305
+
306
+ async function readXlsx(source, options = {}) {
307
+ const pkg = OoxmlPackage.open(await readSource(source), options.limits);
308
+ const workbookPart = mainPart(pkg);
309
+ let date1904 = false;
310
+ const entries = [];
311
+ let isWorkbook = false;
312
+ walkXml(pkg.text(workbookPart), {
313
+ open: (name, attributes) => {
314
+ switch (name) {
315
+ case 'workbook':
316
+ {
317
+ isWorkbook = true;
318
+ break;
319
+ }
320
+ case 'workbookPr':
321
+ {
322
+ date1904 = isOn(attributes.date1904);
323
+ break;
324
+ }
325
+ case 'sheet':
326
+ {
327
+ {
328
+ entries.push({
329
+ name: attributes.name ?? '',
330
+ hidden: attributes.state === 'hidden' || attributes.state === 'veryHidden',
331
+ id: namespacedAttribute(attributes, 'id')
332
+ });
333
+ // No default
334
+ }
335
+ break;
336
+ }
337
+ }
338
+ }
339
+ });
340
+ if (!isWorkbook) throw new OfficeReadError('not-xlsx', `not a spreadsheet: the package's main part is ${workbookPart === '' ? 'missing' : workbookPart}${workbookPart.endsWith('presentation.xml') ? ' (a presentation: read it with readPptx)' : ''}`);
341
+ const relationships = relationshipsOf(pkg, workbookPart);
342
+ const stringsPart = relationshipOfType(relationships, 'sharedStrings')?.target;
343
+ const stylesPart = relationshipOfType(relationships, 'styles')?.target;
344
+ const context = {
345
+ sharedStrings: stringsPart === undefined ? [] : sharedStrings(pkg.text(stringsPart)),
346
+ formats: stylesPart === undefined ? [] : formatKinds(pkg.text(stylesPart)),
347
+ date1904
348
+ };
349
+ const selected = selector(options.sheets);
350
+ const sheets = [];
351
+ for (const entry of entries) {
352
+ const part = entry.id === undefined ? undefined : relationships.get(entry.id);
353
+ // Chart sheets, dialog sheets and macro sheets hold no cells.
354
+ if (part?.type !== 'worksheet' || !selected(entry)) continue;
355
+ const content = readWorksheet(pkg.text(part.target), context, options.values ?? 'typed');
356
+ sheets.push({
357
+ name: entry.name,
358
+ hidden: entry.hidden,
359
+ ...content
360
+ });
361
+ }
362
+ return {
363
+ date1904,
364
+ sheets
365
+ };
366
+ }
367
+ /** The package's main part, from its root relationships (`xl/workbook.xml` by convention, not by rule). */
368
+ function mainPart(pkg) {
369
+ const main = relationshipOfType(relationshipsOf(pkg, ''), 'officeDocument')?.target;
370
+ if (main !== undefined) return main;
371
+ return pkg.has('xl/workbook.xml') ? 'xl/workbook.xml' : '';
372
+ }
373
+ function selector(filter) {
374
+ if (filter === undefined) return () => true;
375
+ if (typeof filter === 'string') return sheet => sheet.name === filter;
376
+ if (filter instanceof RegExp) return sheet => filter.test(sheet.name);
377
+ return filter;
378
+ }
379
+
380
+ export { readXlsx as r };
381
+ //# sourceMappingURL=read-xlsx.use-case.esm.js.map
@@ -0,0 +1,9 @@
1
+ export { readXlsx } from './spreadsheet/index.js';
2
+ export type { ReadXlsxOptions, CellValue, Sheet, Workbook, ValueMode, SheetFilter } from './spreadsheet/index.js';
3
+ export { readPptx } from './presentation/index.js';
4
+ export type { ReadPptxOptions, Deck, Slide, SlideShape, SlideChart, ChartSeries, SlideFilter } from './presentation/index.js';
5
+ export { OfficeReadError } from './read-error/index.js';
6
+ export type { OfficeReadErrorCode } from './read-error/index.js';
7
+ export type { OfficeSource } from './source-bytes/index.js';
8
+ export type { PackageLimits } from './ooxml-package/index.js';
9
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,7 @@
1
+ export { OoxmlPackage } from './ooxml-package.client.js';
2
+ export type { PackageLimits } from './ooxml-package.client.js';
3
+ export { walkXml, namespacedAttribute, isOn } from './xml-walk.algorithm.js';
4
+ export type { XmlHandlers } from './xml-walk.algorithm.js';
5
+ export { relationshipsOf, relationshipOfType } from './relationships.mapper.js';
6
+ export type { Relationship } from './relationships.mapper.js';
7
+ //# sourceMappingURL=index.d.ts.map