@opencraw/office-reader 0.0.2 → 0.0.3
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.
- package/README.md +52 -5
- package/dist/docx.d.ts +1 -0
- package/dist/docx.esm.js +5 -0
- package/dist/index.esm.js +1 -0
- package/dist/read-docx.use-case.esm.js +594 -0
- package/dist/read-pptx.use-case.esm.js +1 -1
- package/dist/read-source.client.esm.js +21 -3
- package/dist/read-xlsx.use-case.esm.js +1 -1
- package/dist/src/document/body.mapper.d.ts +29 -0
- package/dist/src/document/index.d.ts +6 -0
- package/dist/src/document/numbering.mapper.d.ts +9 -0
- package/dist/src/document/read-docx.use-case.d.ts +23 -0
- package/dist/src/document/styles.mapper.d.ts +20 -0
- package/dist/src/document/word-document.model.d.ts +44 -0
- package/dist/src/index.d.ts +2 -0
- package/dist/src/ooxml-package/index.d.ts +1 -1
- package/dist/src/ooxml-package/relationships.mapper.d.ts +8 -0
- package/dist/src/read-error/office-read.error.d.ts +2 -0
- package/package.json +9 -2
package/README.md
CHANGED
|
@@ -4,7 +4,9 @@ Reads Office files into plain objects:
|
|
|
4
4
|
|
|
5
5
|
- **workbooks** (`.xlsx`, `.xlsm`) as sheets of cells, with their merged ranges and hidden rows;
|
|
6
6
|
- **presentations** (`.pptx`, `.pptm`, `.ppsx`) as slides of positioned text boxes, tables, chart data and
|
|
7
|
-
speaker notes
|
|
7
|
+
speaker notes;
|
|
8
|
+
- **documents** (`.docx`, `.docm`, `.dotx`) as paragraphs with their heading and list levels and links, tables
|
|
9
|
+
with their merged cells, headers, footers and notes.
|
|
8
10
|
|
|
9
11
|
- **What the file holds, faithfully.** Formulas give their cached value, and nothing is evaluated. Dates are
|
|
10
12
|
dates, in both the 1900 and 1904 systems. Merged ranges and hidden rows and sheets are reported, not
|
|
@@ -110,10 +112,10 @@ says what to do:
|
|
|
110
112
|
|
|
111
113
|
| `code` | The file is |
|
|
112
114
|
|---|---|
|
|
113
|
-
| `legacy-format` | a legacy binary `.xls`, `.ppt` or `.doc`: save it as `.xlsx` / `.pptx`, or export it as PDF |
|
|
115
|
+
| `legacy-format` | a legacy binary `.xls`, `.ppt` or `.doc`: save it as `.xlsx` / `.pptx` / `.docx`, or export it as PDF |
|
|
114
116
|
| `encrypted` | password-protected |
|
|
115
|
-
| `unsupported-format` | an OpenDocument `.ods` / `.odp` |
|
|
116
|
-
| `not-xlsx` / `not-pptx` | a zip package of
|
|
117
|
+
| `unsupported-format` | an OpenDocument `.ods` / `.odp` / `.odt` |
|
|
118
|
+
| `not-xlsx` / `not-pptx` / `not-docx` | a zip package of another kind (a `.pptx` given to `readXlsx`, say) |
|
|
117
119
|
| `not-zip` | not a zip at all |
|
|
118
120
|
| `too-large` | past the `limits` |
|
|
119
121
|
| `malformed` | damaged: a part does not inflate |
|
|
@@ -171,6 +173,50 @@ Tested against the 100 presentations of Apache POI's test corpus. It reads 90 of
|
|
|
171
173
|
28 charts), and every placeholder gets a position. The other 10 are fuzzer cases and a truncated zip, all
|
|
172
174
|
refused with an `OfficeReadError`.
|
|
173
175
|
|
|
176
|
+
## Read a Word document
|
|
177
|
+
|
|
178
|
+
```ts
|
|
179
|
+
import { readDocx } from '@opencraw/office-reader/docx'
|
|
180
|
+
|
|
181
|
+
const document = await readDocx('./circolare.docx')
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
```ts
|
|
185
|
+
// { title: 'Circolare giugno',
|
|
186
|
+
// body: [
|
|
187
|
+
// { kind: 'paragraph', text: 'Circolare incentivi giugno 2026', style: 'Title', heading: 1 },
|
|
188
|
+
// { kind: 'paragraph', text: 'Condizioni', style: 'Heading1', heading: 1 },
|
|
189
|
+
// { kind: 'paragraph', text: 'Solo rottamazione', style: 'ListBullet', list: { level: 0, ordered: false } },
|
|
190
|
+
// { kind: 'paragraph', text: 'Prezzi', style: 'Titolo2', heading: 2 },
|
|
191
|
+
// { kind: 'table', name: 'table 1', hidden: false, hiddenRows: [],
|
|
192
|
+
// rows: [['Modello', 'Prezzo', '', 'Sconto'], ['', 'Listino', 'Netto', ''], ['Pandina', '15.950', '13.955', '12,5%'], …],
|
|
193
|
+
// merges: ['B1:C1', 'A1:A2', 'D1:D2'] },
|
|
194
|
+
// { kind: 'paragraph', text: 'Listino completo: listino giugno', links: [{ text: 'listino giugno', href: 'https://…/listino.pdf' }] },
|
|
195
|
+
// … ],
|
|
196
|
+
// headers: [[{ kind: 'paragraph', text: 'Stellantis Italia – riservato' }]],
|
|
197
|
+
// footers: [[…]],
|
|
198
|
+
// notes: [{ kind: 'footnote', id: '1', text: 'Prezzi chiavi in mano, IPT esclusa.' }] }
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
`readDocx(source, options?)` takes the same sources as `readXlsx`. Options: `extras` (default `true`; `false`
|
|
202
|
+
skips headers, footers and notes) and `limits`.
|
|
203
|
+
|
|
204
|
+
- **Headings:** a paragraph whose style is named `heading N` (the built-in names stay English in every
|
|
205
|
+
language: an Italian `Titolo2` is still named `heading 2`), has an outline level (its own or its style's,
|
|
206
|
+
through `basedOn`), or is `Title` (level 1).
|
|
207
|
+
- **Lists:** numbering from the paragraph or its style (`List Bullet`); `ordered` says whether the level is
|
|
208
|
+
numbered (`1.`, `a)`, `i.`) or bulleted.
|
|
209
|
+
- **Tables:** grids like a workbook's sheets. A cell spanning columns (`gridSpan`) fills the columns after it
|
|
210
|
+
with `''`; a cell merged down (`vMerge`) leaves `''` below it; both are listed in `merges`. A table inside a
|
|
211
|
+
cell is a block of its own, and its text is in the cell too.
|
|
212
|
+
- **Text:** tabs as `\t`, line breaks as `\n`. Tracked insertions read as text, deletions do not. Field codes
|
|
213
|
+
are left out, their results kept. A text box's paragraphs follow the paragraph that holds it, once (Word
|
|
214
|
+
also writes a fallback copy for old readers, which is skipped).
|
|
215
|
+
- **Links:** external links by URL, internal ones as `#bookmark`.
|
|
216
|
+
|
|
217
|
+
Tested against the 130 documents of Apache POI's test corpus. It reads 115 of them (1,500 paragraphs, 5,115
|
|
218
|
+
tables, 352 links) and refuses the other 15, fuzzer cases and truncated zips, with an `OfficeReadError`.
|
|
219
|
+
|
|
174
220
|
## How it compares
|
|
175
221
|
|
|
176
222
|
| | office-reader | SheetJS (`xlsx` on npm) | ExcelJS | read-excel-file |
|
|
@@ -196,6 +242,7 @@ Tested against the 367 workbooks of Apache POI's test corpus, real files and fuz
|
|
|
196
242
|
|
|
197
243
|
- **Writing files.**
|
|
198
244
|
- **Evaluating formulas, and applying display formats.** A percentage stays `0.125` and a price stays `15950`.
|
|
199
|
-
- **Legacy `.xls`, `.ppt`, `.xlsb` and OpenDocument `.ods` / `.odp`.** These are refused with a code.
|
|
245
|
+
- **Legacy `.xls`, `.ppt`, `.doc`, `.xlsb` and OpenDocument `.ods` / `.odp` / `.odt`.** These are refused with a code.
|
|
200
246
|
- **In workbooks:** charts, pivot tables, images, comments and data validation.
|
|
201
247
|
- **In presentations:** SmartArt text, text inside images (no OCR), animations and themes.
|
|
248
|
+
- **In documents:** comments, formatting (bold, colours, fonts), images, equations, and the text of charts.
|
package/dist/docx.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./src/document/index.js";
|
package/dist/docx.esm.js
ADDED
package/dist/index.esm.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { O as OfficeReadError } from './read-source.client.esm.js';
|
|
2
|
+
export { r as readDocx } from './read-docx.use-case.esm.js';
|
|
2
3
|
export { r as readPptx } from './read-pptx.use-case.esm.js';
|
|
3
4
|
export { r as readXlsx } from './read-xlsx.use-case.esm.js';
|
|
4
5
|
import 'fflate';
|
|
@@ -0,0 +1,594 @@
|
|
|
1
|
+
import { w as walkXml, n as namespacedAttribute, a as OoxmlPackage, r as readSource, b as relationshipsOf, c as relationshipOfType, O as OfficeReadError, h as hyperlinksOf } from './read-source.client.esm.js';
|
|
2
|
+
import 'fflate';
|
|
3
|
+
|
|
4
|
+
/** Elements whose content is not part of the document's text: deleted runs, field codes, the fallback copy of alternate content. */
|
|
5
|
+
const SKIPPED = new Set(['del', 'moveFrom', 'instrText', 'delInstrText', 'Fallback']);
|
|
6
|
+
/**
|
|
7
|
+
* Reads the blocks of a WordprocessingML part (the body, a header, a note):
|
|
8
|
+
* paragraphs with their style, heading level, list level and links, and
|
|
9
|
+
* tables as grids with merged cells (`gridSpan` across, `vMerge` down) as
|
|
10
|
+
* ranges. Tracked insertions read as text, deletions do not. A paragraph in a
|
|
11
|
+
* text box comes after the paragraph that holds the box; a table inside a
|
|
12
|
+
* cell is a block of its own, and its text also joins the cell's.
|
|
13
|
+
*
|
|
14
|
+
* Footnotes and endnotes (`footnotes.xml`, `endnotes.xml`) come back as
|
|
15
|
+
* notes, one per note, their paragraphs joined.
|
|
16
|
+
*
|
|
17
|
+
* @param xml - The part.
|
|
18
|
+
* @param context - Styles, numbering, links.
|
|
19
|
+
* @returns The blocks, in document order, and the notes.
|
|
20
|
+
*/
|
|
21
|
+
function readBlocks(xml, context) {
|
|
22
|
+
const blocks = [];
|
|
23
|
+
const notes = [];
|
|
24
|
+
let note;
|
|
25
|
+
let inProperties = false;
|
|
26
|
+
const paragraphs = [];
|
|
27
|
+
const tables = [];
|
|
28
|
+
// Where a paragraph that closes goes: a table cell, or the list of blocks (the body, a text box).
|
|
29
|
+
const containers = ['blocks'];
|
|
30
|
+
let skipping = 0;
|
|
31
|
+
let inText = false;
|
|
32
|
+
let inNumbering = false;
|
|
33
|
+
let tableCount = 0;
|
|
34
|
+
const append = text => {
|
|
35
|
+
const paragraph = paragraphs.at(-1);
|
|
36
|
+
if (paragraph !== undefined && skipping === 0) paragraph.text += text;
|
|
37
|
+
};
|
|
38
|
+
walkXml(xml, {
|
|
39
|
+
open: (name, attributes) => {
|
|
40
|
+
if (SKIPPED.has(name)) {
|
|
41
|
+
skipping += 1;
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
if (skipping > 0) return;
|
|
45
|
+
const paragraph = paragraphs.at(-1);
|
|
46
|
+
const table = tables.at(-1);
|
|
47
|
+
const value = namespacedAttribute(attributes, 'val');
|
|
48
|
+
switch (name) {
|
|
49
|
+
case 'p':
|
|
50
|
+
{
|
|
51
|
+
paragraphs.push({
|
|
52
|
+
text: '',
|
|
53
|
+
links: [],
|
|
54
|
+
boxed: []
|
|
55
|
+
});
|
|
56
|
+
break;
|
|
57
|
+
}
|
|
58
|
+
case 'pPr':
|
|
59
|
+
{
|
|
60
|
+
inProperties = true;
|
|
61
|
+
break;
|
|
62
|
+
}
|
|
63
|
+
case 'footnote':
|
|
64
|
+
case 'endnote':
|
|
65
|
+
{
|
|
66
|
+
note = {
|
|
67
|
+
kind: name,
|
|
68
|
+
id: namespacedAttribute(attributes, 'id') ?? '',
|
|
69
|
+
start: blocks.length
|
|
70
|
+
};
|
|
71
|
+
break;
|
|
72
|
+
}
|
|
73
|
+
case 'pStyle':
|
|
74
|
+
{
|
|
75
|
+
if (paragraph !== undefined) paragraph.style = value;
|
|
76
|
+
break;
|
|
77
|
+
}
|
|
78
|
+
case 'outlineLvl':
|
|
79
|
+
{
|
|
80
|
+
if (paragraph !== undefined) paragraph.outline = Number(value);
|
|
81
|
+
break;
|
|
82
|
+
}
|
|
83
|
+
case 'numPr':
|
|
84
|
+
{
|
|
85
|
+
inNumbering = true;
|
|
86
|
+
break;
|
|
87
|
+
}
|
|
88
|
+
case 'numId':
|
|
89
|
+
{
|
|
90
|
+
if (inNumbering && paragraph !== undefined) paragraph.numId = value;
|
|
91
|
+
break;
|
|
92
|
+
}
|
|
93
|
+
case 'ilvl':
|
|
94
|
+
{
|
|
95
|
+
if (inNumbering && paragraph !== undefined) paragraph.level = Number(value);
|
|
96
|
+
break;
|
|
97
|
+
}
|
|
98
|
+
case 't':
|
|
99
|
+
{
|
|
100
|
+
inText = true;
|
|
101
|
+
break;
|
|
102
|
+
}
|
|
103
|
+
case 'tab':
|
|
104
|
+
case 'ptab':
|
|
105
|
+
{
|
|
106
|
+
// A tab stop in the paragraph's properties is not a tab in its text.
|
|
107
|
+
if (!inProperties) append('\t');
|
|
108
|
+
break;
|
|
109
|
+
}
|
|
110
|
+
case 'br':
|
|
111
|
+
case 'cr':
|
|
112
|
+
{
|
|
113
|
+
append('\n');
|
|
114
|
+
break;
|
|
115
|
+
}
|
|
116
|
+
case 'noBreakHyphen':
|
|
117
|
+
{
|
|
118
|
+
append('-');
|
|
119
|
+
break;
|
|
120
|
+
}
|
|
121
|
+
case 'hyperlink':
|
|
122
|
+
{
|
|
123
|
+
const id = namespacedAttribute(attributes, 'id');
|
|
124
|
+
const anchor = namespacedAttribute(attributes, 'anchor');
|
|
125
|
+
const href = id === undefined ? anchor === undefined ? undefined : `#${anchor}` : context.links.get(id);
|
|
126
|
+
if (paragraph !== undefined && href !== undefined) paragraph.link = {
|
|
127
|
+
href,
|
|
128
|
+
from: paragraph.text.length
|
|
129
|
+
};
|
|
130
|
+
break;
|
|
131
|
+
}
|
|
132
|
+
case 'txbxContent':
|
|
133
|
+
{
|
|
134
|
+
containers.push('blocks');
|
|
135
|
+
break;
|
|
136
|
+
}
|
|
137
|
+
case 'tbl':
|
|
138
|
+
{
|
|
139
|
+
tables.push({
|
|
140
|
+
rows: [],
|
|
141
|
+
merges: [],
|
|
142
|
+
vertical: new Map()
|
|
143
|
+
});
|
|
144
|
+
break;
|
|
145
|
+
}
|
|
146
|
+
case 'tr':
|
|
147
|
+
{
|
|
148
|
+
if (table !== undefined) table.row = [];
|
|
149
|
+
break;
|
|
150
|
+
}
|
|
151
|
+
case 'gridBefore':
|
|
152
|
+
{
|
|
153
|
+
if (table?.row !== undefined) table.row.push(...Array.from({
|
|
154
|
+
length: Number(value ?? 0)
|
|
155
|
+
}, () => ''));
|
|
156
|
+
break;
|
|
157
|
+
}
|
|
158
|
+
case 'tc':
|
|
159
|
+
{
|
|
160
|
+
if (table !== undefined) table.cell = {
|
|
161
|
+
paragraphs: [],
|
|
162
|
+
span: 1
|
|
163
|
+
};
|
|
164
|
+
containers.push('cell');
|
|
165
|
+
break;
|
|
166
|
+
}
|
|
167
|
+
case 'gridSpan':
|
|
168
|
+
{
|
|
169
|
+
if (table?.cell !== undefined) table.cell.span = Math.max(1, Number(value ?? 1));
|
|
170
|
+
break;
|
|
171
|
+
}
|
|
172
|
+
case 'vMerge':
|
|
173
|
+
{
|
|
174
|
+
if (table?.cell !== undefined) table.cell.vMerge = value === 'restart' ? 'restart' : 'continue';
|
|
175
|
+
break;
|
|
176
|
+
}
|
|
177
|
+
// No default
|
|
178
|
+
}
|
|
179
|
+
},
|
|
180
|
+
text: text => {
|
|
181
|
+
if (inText) append(text);
|
|
182
|
+
},
|
|
183
|
+
close: name => {
|
|
184
|
+
if (SKIPPED.has(name)) {
|
|
185
|
+
skipping -= 1;
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
if (skipping > 0) return;
|
|
189
|
+
switch (name) {
|
|
190
|
+
case 't':
|
|
191
|
+
{
|
|
192
|
+
inText = false;
|
|
193
|
+
break;
|
|
194
|
+
}
|
|
195
|
+
case 'numPr':
|
|
196
|
+
{
|
|
197
|
+
inNumbering = false;
|
|
198
|
+
break;
|
|
199
|
+
}
|
|
200
|
+
case 'pPr':
|
|
201
|
+
{
|
|
202
|
+
inProperties = false;
|
|
203
|
+
break;
|
|
204
|
+
}
|
|
205
|
+
case 'footnote':
|
|
206
|
+
case 'endnote':
|
|
207
|
+
{
|
|
208
|
+
if (note === undefined) break;
|
|
209
|
+
const text = blocks.splice(note.start).map(block => block.kind === 'paragraph' ? block.text : block.rows.map(row => row.join(' ')).join('\n')).join('\n');
|
|
210
|
+
if (text !== '') notes.push({
|
|
211
|
+
kind: note.kind,
|
|
212
|
+
id: note.id,
|
|
213
|
+
text
|
|
214
|
+
});
|
|
215
|
+
note = undefined;
|
|
216
|
+
break;
|
|
217
|
+
}
|
|
218
|
+
case 'hyperlink':
|
|
219
|
+
{
|
|
220
|
+
const paragraph = paragraphs.at(-1);
|
|
221
|
+
if (paragraph?.link !== undefined) {
|
|
222
|
+
const text = paragraph.text.slice(paragraph.link.from).trim();
|
|
223
|
+
if (text !== '') paragraph.links.push({
|
|
224
|
+
text,
|
|
225
|
+
href: paragraph.link.href
|
|
226
|
+
});
|
|
227
|
+
paragraph.link = undefined;
|
|
228
|
+
}
|
|
229
|
+
break;
|
|
230
|
+
}
|
|
231
|
+
case 'p':
|
|
232
|
+
{
|
|
233
|
+
const open = paragraphs.pop();
|
|
234
|
+
if (open === undefined) break;
|
|
235
|
+
const block = paragraphBlock(open, context);
|
|
236
|
+
const emitted = [...(block === undefined ? [] : [block]), ...open.boxed];
|
|
237
|
+
const host = paragraphs.at(-1);
|
|
238
|
+
if (containers.at(-1) === 'cell') tables.at(-1)?.cell?.paragraphs.push(open.text, ...open.boxed.map(boxed => boxed.kind === 'paragraph' ? boxed.text : ''));
|
|
239
|
+
// A paragraph in a text box waits for the paragraph holding the box.
|
|
240
|
+
else if (host === undefined) blocks.push(...emitted);else host.boxed.push(...emitted);
|
|
241
|
+
break;
|
|
242
|
+
}
|
|
243
|
+
case 'txbxContent':
|
|
244
|
+
{
|
|
245
|
+
containers.pop();
|
|
246
|
+
break;
|
|
247
|
+
}
|
|
248
|
+
case 'tc':
|
|
249
|
+
{
|
|
250
|
+
containers.pop();
|
|
251
|
+
const table = tables.at(-1);
|
|
252
|
+
if (table?.cell !== undefined && table.row !== undefined) closeCell(table, table.cell, table.row);
|
|
253
|
+
if (table !== undefined) table.cell = undefined;
|
|
254
|
+
break;
|
|
255
|
+
}
|
|
256
|
+
case 'tr':
|
|
257
|
+
{
|
|
258
|
+
const table = tables.at(-1);
|
|
259
|
+
if (table?.row !== undefined) table.rows.push(table.row);
|
|
260
|
+
if (table !== undefined) table.row = undefined;
|
|
261
|
+
break;
|
|
262
|
+
}
|
|
263
|
+
case 'tbl':
|
|
264
|
+
{
|
|
265
|
+
const table = tables.pop();
|
|
266
|
+
if (table === undefined) break;
|
|
267
|
+
for (const [column, open] of table.vertical) closeVertical(table, column, open);
|
|
268
|
+
tableCount += 1;
|
|
269
|
+
const block = {
|
|
270
|
+
kind: 'table',
|
|
271
|
+
name: `table ${tableCount}`,
|
|
272
|
+
hidden: false,
|
|
273
|
+
rows: table.rows,
|
|
274
|
+
hiddenRows: [],
|
|
275
|
+
merges: table.merges
|
|
276
|
+
};
|
|
277
|
+
blocks.push(block);
|
|
278
|
+
// A table inside a cell: its text belongs to the cell too.
|
|
279
|
+
tables.at(-1)?.cell?.paragraphs.push(table.rows.map(row => row.filter(cell => cell !== '').join(' ')).join('\n'));
|
|
280
|
+
break;
|
|
281
|
+
}
|
|
282
|
+
// No default
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
});
|
|
286
|
+
return {
|
|
287
|
+
blocks,
|
|
288
|
+
notes
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
function paragraphBlock(open, context) {
|
|
292
|
+
const text = open.text.replaceAll(/[ \t]+\n/g, '\n').trim();
|
|
293
|
+
if (text === '') return undefined;
|
|
294
|
+
const heading = open.outline !== undefined && open.outline < 9 ? open.outline + 1 : context.styles.heading(open.style);
|
|
295
|
+
const numbering = open.numId === undefined ? context.styles.list(open.style) : {
|
|
296
|
+
numId: open.numId,
|
|
297
|
+
level: open.level ?? 0
|
|
298
|
+
};
|
|
299
|
+
const list = heading !== undefined || numbering === undefined || numbering.numId === '0' ? undefined : {
|
|
300
|
+
level: numbering.level,
|
|
301
|
+
ordered: context.ordered(numbering.numId, numbering.level)
|
|
302
|
+
};
|
|
303
|
+
return {
|
|
304
|
+
kind: 'paragraph',
|
|
305
|
+
text,
|
|
306
|
+
...(open.style !== undefined && {
|
|
307
|
+
style: open.style
|
|
308
|
+
}),
|
|
309
|
+
...(heading !== undefined && {
|
|
310
|
+
heading
|
|
311
|
+
}),
|
|
312
|
+
...(list !== undefined && {
|
|
313
|
+
list
|
|
314
|
+
}),
|
|
315
|
+
...(open.links.length > 0 && {
|
|
316
|
+
links: open.links
|
|
317
|
+
})
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
/** Places a closed cell in its row: its text, then a blank for each extra grid column it spans; tracks merges. */
|
|
321
|
+
function closeCell(table, cell, row) {
|
|
322
|
+
const column = row.length;
|
|
323
|
+
const rowIndex = table.rows.length;
|
|
324
|
+
const open = table.vertical.get(column);
|
|
325
|
+
if (open !== undefined && cell.vMerge === 'continue') {
|
|
326
|
+
open.end = rowIndex;
|
|
327
|
+
row.push(...Array.from({
|
|
328
|
+
length: cell.span
|
|
329
|
+
}, () => ''));
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
if (open !== undefined) closeVertical(table, column, open);
|
|
333
|
+
row.push(cell.paragraphs.join('\n').trim(), ...Array.from({
|
|
334
|
+
length: cell.span - 1
|
|
335
|
+
}, () => ''));
|
|
336
|
+
if (cell.vMerge === 'restart') table.vertical.set(column, {
|
|
337
|
+
row: rowIndex,
|
|
338
|
+
end: rowIndex,
|
|
339
|
+
span: cell.span
|
|
340
|
+
});else if (cell.span > 1) table.merges.push(rangeOf(rowIndex, column, 1, cell.span));
|
|
341
|
+
}
|
|
342
|
+
function closeVertical(table, column, open) {
|
|
343
|
+
table.vertical.delete(column);
|
|
344
|
+
if (open.end > open.row || open.span > 1) table.merges.push(rangeOf(open.row, column, open.end - open.row + 1, open.span));
|
|
345
|
+
}
|
|
346
|
+
/** A merged range as an A1 reference (`A1:D1`). */
|
|
347
|
+
function rangeOf(row, column, rowSpan, columnSpan) {
|
|
348
|
+
return `${columnLetter(column)}${row + 1}:${columnLetter(column + columnSpan - 1)}${row + rowSpan}`;
|
|
349
|
+
}
|
|
350
|
+
function columnLetter(index) {
|
|
351
|
+
let letters = '';
|
|
352
|
+
for (let rest = index + 1; rest > 0; rest = Math.floor((rest - 1) / 26)) letters = String.fromCodePoint(65 + (rest - 1) % 26) + letters;
|
|
353
|
+
return letters;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/** Formats that do not number: a bullet, or nothing. */
|
|
357
|
+
const UNNUMBERED = new Set(['bullet', 'none']);
|
|
358
|
+
/**
|
|
359
|
+
* Reads `numbering.xml`: for a list (`numId`) at a level, whether its items
|
|
360
|
+
* are numbered (`1.`, `a)`, `i.`) or bulleted.
|
|
361
|
+
*
|
|
362
|
+
* @param xml - The numbering part; empty when the document has none.
|
|
363
|
+
* @returns Whether a list level is ordered.
|
|
364
|
+
*/
|
|
365
|
+
function readNumbering(xml) {
|
|
366
|
+
const formats = new Map();
|
|
367
|
+
const abstractOf = new Map();
|
|
368
|
+
let abstract;
|
|
369
|
+
let level;
|
|
370
|
+
let num;
|
|
371
|
+
walkXml(xml, {
|
|
372
|
+
open: (name, attributes) => {
|
|
373
|
+
switch (name) {
|
|
374
|
+
case 'abstractNum':
|
|
375
|
+
{
|
|
376
|
+
abstract = new Map();
|
|
377
|
+
formats.set(namespacedAttribute(attributes, 'abstractNumId') ?? '', abstract);
|
|
378
|
+
break;
|
|
379
|
+
}
|
|
380
|
+
case 'lvl':
|
|
381
|
+
{
|
|
382
|
+
level = Number(namespacedAttribute(attributes, 'ilvl') ?? 0);
|
|
383
|
+
break;
|
|
384
|
+
}
|
|
385
|
+
case 'numFmt':
|
|
386
|
+
{
|
|
387
|
+
if (abstract !== undefined && level !== undefined) abstract.set(level, namespacedAttribute(attributes, 'val') ?? 'decimal');
|
|
388
|
+
break;
|
|
389
|
+
}
|
|
390
|
+
case 'num':
|
|
391
|
+
{
|
|
392
|
+
num = namespacedAttribute(attributes, 'numId');
|
|
393
|
+
break;
|
|
394
|
+
}
|
|
395
|
+
case 'abstractNumId':
|
|
396
|
+
{
|
|
397
|
+
if (num !== undefined) abstractOf.set(num, namespacedAttribute(attributes, 'val') ?? '');
|
|
398
|
+
break;
|
|
399
|
+
}
|
|
400
|
+
// No default
|
|
401
|
+
}
|
|
402
|
+
},
|
|
403
|
+
close: name => {
|
|
404
|
+
switch (name) {
|
|
405
|
+
case 'abstractNum':
|
|
406
|
+
{
|
|
407
|
+
abstract = undefined;
|
|
408
|
+
break;
|
|
409
|
+
}
|
|
410
|
+
case 'lvl':
|
|
411
|
+
{
|
|
412
|
+
level = undefined;
|
|
413
|
+
break;
|
|
414
|
+
}
|
|
415
|
+
case 'num':
|
|
416
|
+
{
|
|
417
|
+
{
|
|
418
|
+
num = undefined;
|
|
419
|
+
// No default
|
|
420
|
+
}
|
|
421
|
+
break;
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
});
|
|
426
|
+
return (numId, at) => {
|
|
427
|
+
const format = formats.get(abstractOf.get(numId) ?? '')?.get(at) ?? 'bullet';
|
|
428
|
+
return !UNNUMBERED.has(format);
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* Reads the paragraph styles of `styles.xml`. A heading is a style named
|
|
434
|
+
* `heading N` (the built-in names stay English whatever the document's
|
|
435
|
+
* language: an Italian `Titolo1` is still named `heading 1`), a style with an
|
|
436
|
+
* outline level, or `Title`; either can be inherited through `basedOn`. A
|
|
437
|
+
* style can also carry list numbering (`List Bullet`).
|
|
438
|
+
*
|
|
439
|
+
* @param xml - The styles part; empty when the document has none.
|
|
440
|
+
* @returns The lookups.
|
|
441
|
+
*/
|
|
442
|
+
function readStyles(xml) {
|
|
443
|
+
const styles = new Map();
|
|
444
|
+
let current;
|
|
445
|
+
let inNumbering = false;
|
|
446
|
+
walkXml(xml, {
|
|
447
|
+
open: (name, attributes) => {
|
|
448
|
+
const value = namespacedAttribute(attributes, 'val');
|
|
449
|
+
if (name === 'style') {
|
|
450
|
+
current = namespacedAttribute(attributes, 'type') === 'paragraph' ? {
|
|
451
|
+
name: ''
|
|
452
|
+
} : undefined;
|
|
453
|
+
if (current !== undefined) styles.set(namespacedAttribute(attributes, 'styleId') ?? '', current);
|
|
454
|
+
return;
|
|
455
|
+
}
|
|
456
|
+
if (current === undefined) return;
|
|
457
|
+
switch (name) {
|
|
458
|
+
case 'name':
|
|
459
|
+
{
|
|
460
|
+
current.name = (value ?? '').toLowerCase();
|
|
461
|
+
break;
|
|
462
|
+
}
|
|
463
|
+
case 'basedOn':
|
|
464
|
+
{
|
|
465
|
+
current.basedOn = value;
|
|
466
|
+
break;
|
|
467
|
+
}
|
|
468
|
+
case 'outlineLvl':
|
|
469
|
+
{
|
|
470
|
+
current.outline = Number(value);
|
|
471
|
+
break;
|
|
472
|
+
}
|
|
473
|
+
case 'numPr':
|
|
474
|
+
{
|
|
475
|
+
inNumbering = true;
|
|
476
|
+
break;
|
|
477
|
+
}
|
|
478
|
+
default:
|
|
479
|
+
{
|
|
480
|
+
if (inNumbering && name === 'numId') current.numId = value;else if (inNumbering && name === 'ilvl') current.level = Number(value);
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
},
|
|
484
|
+
close: name => {
|
|
485
|
+
if (name === 'numPr') inNumbering = false;else if (name === 'style') current = undefined;
|
|
486
|
+
}
|
|
487
|
+
});
|
|
488
|
+
const chain = styleId => {
|
|
489
|
+
const seen = [];
|
|
490
|
+
for (let id = styleId, facts = id === undefined ? undefined : styles.get(id); facts !== undefined && seen.length < 20; id = facts.basedOn, facts = id === undefined ? undefined : styles.get(id)) seen.push(facts);
|
|
491
|
+
return seen;
|
|
492
|
+
};
|
|
493
|
+
return {
|
|
494
|
+
heading: styleId => {
|
|
495
|
+
for (const facts of chain(styleId)) {
|
|
496
|
+
const named = /^heading ([1-9])$/.exec(facts.name);
|
|
497
|
+
if (named !== null) return Number(named[1]);
|
|
498
|
+
if (facts.name === 'title') return 1;
|
|
499
|
+
if (facts.outline !== undefined && facts.outline < 9) return facts.outline + 1;
|
|
500
|
+
}
|
|
501
|
+
return;
|
|
502
|
+
},
|
|
503
|
+
list: styleId => {
|
|
504
|
+
const facts = chain(styleId).find(entry => entry.numId !== undefined);
|
|
505
|
+
return facts?.numId === undefined ? undefined : {
|
|
506
|
+
numId: facts.numId,
|
|
507
|
+
level: facts.level ?? 0
|
|
508
|
+
};
|
|
509
|
+
}
|
|
510
|
+
};
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
/**
|
|
514
|
+
* Reads a `.docx` Word document (also `.docm`, `.dotx`): its body as
|
|
515
|
+
* paragraphs (with heading and list levels, and links) and tables (with
|
|
516
|
+
* merged cells), its headers and footers, its footnotes and endnotes, and its
|
|
517
|
+
* title.
|
|
518
|
+
*
|
|
519
|
+
* @param source - A path, bytes, a Blob or a stream (see {@link OfficeSource}).
|
|
520
|
+
* @param options - Whether to read headers, footers and notes; the limits.
|
|
521
|
+
* @returns The document.
|
|
522
|
+
* @throws OfficeReadError for a file that is not a readable Word document: `legacy-format` (`.doc`), `encrypted`, `unsupported-format` (`.odt`), `not-docx`, `too-large`…
|
|
523
|
+
*/
|
|
524
|
+
async function readDocx(source, options = {}) {
|
|
525
|
+
const pkg = OoxmlPackage.open(await readSource(source), options.limits);
|
|
526
|
+
const packageRelationships = relationshipsOf(pkg, '');
|
|
527
|
+
const main = relationshipOfType(packageRelationships, 'officeDocument')?.target ?? (pkg.has('word/document.xml') ? 'word/document.xml' : '');
|
|
528
|
+
const xml = main === '' ? '' : pkg.text(main);
|
|
529
|
+
if (!/<(?:\w+:)?document[\s>]/.test(xml.slice(0, 4096)) || !/<(?:\w+:)?body[\s>]/.test(xml)) {
|
|
530
|
+
const other = main.endsWith('workbook.xml') ? ' (a spreadsheet: read it with readXlsx)' : main.endsWith('presentation.xml') ? ' (a presentation: read it with readPptx)' : '';
|
|
531
|
+
throw new OfficeReadError('not-docx', `not a Word document: the package's main part is ${main === '' ? 'missing' : main}${other}`);
|
|
532
|
+
}
|
|
533
|
+
const relationships = relationshipsOf(pkg, main);
|
|
534
|
+
const part = type => {
|
|
535
|
+
const target = relationshipOfType(relationships, type)?.target;
|
|
536
|
+
return target === undefined ? '' : pkg.text(target);
|
|
537
|
+
};
|
|
538
|
+
const styles = readStyles(part('styles'));
|
|
539
|
+
const ordered = readNumbering(part('numbering'));
|
|
540
|
+
const context = partName => ({
|
|
541
|
+
styles,
|
|
542
|
+
ordered,
|
|
543
|
+
links: hyperlinksOf(pkg, partName)
|
|
544
|
+
});
|
|
545
|
+
const {
|
|
546
|
+
blocks: body
|
|
547
|
+
} = readBlocks(xml, context(main));
|
|
548
|
+
const headers = [];
|
|
549
|
+
const footers = [];
|
|
550
|
+
const notes = [];
|
|
551
|
+
if (options.extras !== false) {
|
|
552
|
+
for (const relationship of relationships.values()) {
|
|
553
|
+
if (relationship.type === 'header' || relationship.type === 'footer') {
|
|
554
|
+
const {
|
|
555
|
+
blocks
|
|
556
|
+
} = readBlocks(pkg.text(relationship.target), context(relationship.target));
|
|
557
|
+
if (blocks.length > 0) (relationship.type === 'header' ? headers : footers).push(blocks);
|
|
558
|
+
} else if (relationship.type === 'footnotes' || relationship.type === 'endnotes') {
|
|
559
|
+
notes.push(...readBlocks(pkg.text(relationship.target), context(relationship.target)).notes);
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
const title = titleOf(pkg.text(relationshipOfType(packageRelationships, 'core-properties')?.target ?? 'docProps/core.xml'));
|
|
564
|
+
return {
|
|
565
|
+
...(title !== undefined && {
|
|
566
|
+
title
|
|
567
|
+
}),
|
|
568
|
+
body,
|
|
569
|
+
headers,
|
|
570
|
+
footers,
|
|
571
|
+
notes
|
|
572
|
+
};
|
|
573
|
+
}
|
|
574
|
+
/** The `dc:title` of the core properties part, when set. */
|
|
575
|
+
function titleOf(xml) {
|
|
576
|
+
let inTitle = false;
|
|
577
|
+
let title = '';
|
|
578
|
+
walkXml(xml, {
|
|
579
|
+
open: name => {
|
|
580
|
+
inTitle = name === 'title';
|
|
581
|
+
},
|
|
582
|
+
text: text => {
|
|
583
|
+
if (inTitle) title += text;
|
|
584
|
+
},
|
|
585
|
+
close: () => {
|
|
586
|
+
inTitle = false;
|
|
587
|
+
}
|
|
588
|
+
});
|
|
589
|
+
title = title.trim();
|
|
590
|
+
return title === '' ? undefined : title;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
export { readDocx as r };
|
|
594
|
+
//# sourceMappingURL=read-docx.use-case.esm.js.map
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { w as walkXml, n as namespacedAttribute, i as isOn, a as OoxmlPackage, r as readSource,
|
|
1
|
+
import { w as walkXml, n as namespacedAttribute, i as isOn, a as OoxmlPackage, r as readSource, c as relationshipOfType, b as relationshipsOf, O as OfficeReadError } from './read-source.client.esm.js';
|
|
2
2
|
import 'fflate';
|
|
3
3
|
|
|
4
4
|
/**
|
|
@@ -32,7 +32,7 @@ class OoxmlPackage {
|
|
|
32
32
|
static open(bytes, limits = {}) {
|
|
33
33
|
if (isCompoundFile(bytes)) {
|
|
34
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 .
|
|
35
|
+
throw new OfficeReadError('legacy-format', 'a legacy binary Office file (.xls, .ppt, .doc): save it as .xlsx, .pptx or .docx, or export it as PDF');
|
|
36
36
|
}
|
|
37
37
|
const entries = new Map();
|
|
38
38
|
try {
|
|
@@ -55,7 +55,7 @@ class OoxmlPackage {
|
|
|
55
55
|
totalBytes: limits.totalBytes ?? DEFAULT_TOTAL_BYTES
|
|
56
56
|
});
|
|
57
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 .
|
|
58
|
+
if (mimetype.startsWith('application/vnd.oasis.opendocument')) throw new OfficeReadError('unsupported-format', `an OpenDocument file (${mimetype}): save it as .xlsx, .pptx or .docx`);
|
|
59
59
|
return opened;
|
|
60
60
|
}
|
|
61
61
|
declared = 0;
|
|
@@ -195,6 +195,24 @@ function relationshipsOf(pkg, part) {
|
|
|
195
195
|
});
|
|
196
196
|
return relationships;
|
|
197
197
|
}
|
|
198
|
+
/**
|
|
199
|
+
* A part's hyperlinks: the external targets its relationships point at, by id.
|
|
200
|
+
*
|
|
201
|
+
* @param pkg - The package.
|
|
202
|
+
* @param part - The part (`word/document.xml`).
|
|
203
|
+
* @returns The URLs by relationship id.
|
|
204
|
+
*/
|
|
205
|
+
function hyperlinksOf(pkg, part) {
|
|
206
|
+
const directory = part.includes('/') ? part.slice(0, part.lastIndexOf('/')) : '';
|
|
207
|
+
const file = part.slice(part.lastIndexOf('/') + 1);
|
|
208
|
+
const links = new Map();
|
|
209
|
+
walkXml(pkg.text(`${directory === '' ? '' : `${directory}/`}_rels/${file}.rels`), {
|
|
210
|
+
open: (name, attributes) => {
|
|
211
|
+
if (name === 'Relationship' && attributes.TargetMode === 'External' && attributes.Id !== undefined && attributes.Target !== undefined) links.set(attributes.Id, attributes.Target);
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
return links;
|
|
215
|
+
}
|
|
198
216
|
/**
|
|
199
217
|
* The first relationship of a type.
|
|
200
218
|
*
|
|
@@ -285,5 +303,5 @@ function concat(chunks) {
|
|
|
285
303
|
return bytes;
|
|
286
304
|
}
|
|
287
305
|
|
|
288
|
-
export { OfficeReadError as O, OoxmlPackage as a,
|
|
306
|
+
export { OfficeReadError as O, OoxmlPackage as a, relationshipsOf as b, relationshipOfType as c, hyperlinksOf as h, isOn as i, namespacedAttribute as n, readSource as r, walkXml as w };
|
|
289
307
|
//# sourceMappingURL=read-source.client.esm.js.map
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { w as walkXml, i as isOn, a as OoxmlPackage, r as readSource, O as OfficeReadError,
|
|
1
|
+
import { w as walkXml, i as isOn, a as OoxmlPackage, r as readSource, O as OfficeReadError, c as relationshipOfType, b as relationshipsOf, n as namespacedAttribute } from './read-source.client.esm.js';
|
|
2
2
|
import 'fflate';
|
|
3
3
|
|
|
4
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). */
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { DocumentBlock, DocumentNote } from './word-document.model.js';
|
|
2
|
+
import type { ParagraphStyles } from './styles.mapper.js';
|
|
3
|
+
/** What reading a part needs from the rest of the document. */
|
|
4
|
+
export interface BodyContext {
|
|
5
|
+
styles: ParagraphStyles;
|
|
6
|
+
ordered: (numId: string, level: number) => boolean;
|
|
7
|
+
/** The part's relationships: a hyperlink's `r:id` to its URL. */
|
|
8
|
+
links: ReadonlyMap<string, string>;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Reads the blocks of a WordprocessingML part (the body, a header, a note):
|
|
12
|
+
* paragraphs with their style, heading level, list level and links, and
|
|
13
|
+
* tables as grids with merged cells (`gridSpan` across, `vMerge` down) as
|
|
14
|
+
* ranges. Tracked insertions read as text, deletions do not. A paragraph in a
|
|
15
|
+
* text box comes after the paragraph that holds the box; a table inside a
|
|
16
|
+
* cell is a block of its own, and its text also joins the cell's.
|
|
17
|
+
*
|
|
18
|
+
* Footnotes and endnotes (`footnotes.xml`, `endnotes.xml`) come back as
|
|
19
|
+
* notes, one per note, their paragraphs joined.
|
|
20
|
+
*
|
|
21
|
+
* @param xml - The part.
|
|
22
|
+
* @param context - Styles, numbering, links.
|
|
23
|
+
* @returns The blocks, in document order, and the notes.
|
|
24
|
+
*/
|
|
25
|
+
export declare function readBlocks(xml: string, context: BodyContext): {
|
|
26
|
+
blocks: DocumentBlock[];
|
|
27
|
+
notes: DocumentNote[];
|
|
28
|
+
};
|
|
29
|
+
//# sourceMappingURL=body.mapper.d.ts.map
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { readDocx } from './read-docx.use-case.js';
|
|
2
|
+
export type { ReadDocxOptions } from './read-docx.use-case.js';
|
|
3
|
+
export type { WordDocument, DocumentBlock, Paragraph, DocumentTable, DocumentNote, DocumentLink } from './word-document.model.js';
|
|
4
|
+
export { OfficeReadError } from '../read-error/index.js';
|
|
5
|
+
export type { OfficeReadErrorCode } from '../read-error/index.js';
|
|
6
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reads `numbering.xml`: for a list (`numId`) at a level, whether its items
|
|
3
|
+
* are numbered (`1.`, `a)`, `i.`) or bulleted.
|
|
4
|
+
*
|
|
5
|
+
* @param xml - The numbering part; empty when the document has none.
|
|
6
|
+
* @returns Whether a list level is ordered.
|
|
7
|
+
*/
|
|
8
|
+
export declare function readNumbering(xml: string): (numId: string, level: number) => boolean;
|
|
9
|
+
//# sourceMappingURL=numbering.mapper.d.ts.map
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { PackageLimits } from '../ooxml-package/index.js';
|
|
2
|
+
import type { OfficeSource } from '../source-bytes/index.js';
|
|
3
|
+
import type { WordDocument } from './word-document.model.js';
|
|
4
|
+
/** How to read a Word document. */
|
|
5
|
+
export interface ReadDocxOptions {
|
|
6
|
+
/** Read headers, footers and notes; default true. */
|
|
7
|
+
extras?: boolean;
|
|
8
|
+
/** How much the file may inflate to. */
|
|
9
|
+
limits?: PackageLimits;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Reads a `.docx` Word document (also `.docm`, `.dotx`): its body as
|
|
13
|
+
* paragraphs (with heading and list levels, and links) and tables (with
|
|
14
|
+
* merged cells), its headers and footers, its footnotes and endnotes, and its
|
|
15
|
+
* title.
|
|
16
|
+
*
|
|
17
|
+
* @param source - A path, bytes, a Blob or a stream (see {@link OfficeSource}).
|
|
18
|
+
* @param options - Whether to read headers, footers and notes; the limits.
|
|
19
|
+
* @returns The document.
|
|
20
|
+
* @throws OfficeReadError for a file that is not a readable Word document: `legacy-format` (`.doc`), `encrypted`, `unsupported-format` (`.odt`), `not-docx`, `too-large`…
|
|
21
|
+
*/
|
|
22
|
+
export declare function readDocx(source: OfficeSource, options?: ReadDocxOptions): Promise<WordDocument>;
|
|
23
|
+
//# sourceMappingURL=read-docx.use-case.d.ts.map
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/** How a paragraph style reads: as a heading of some level, as a list, or neither. */
|
|
2
|
+
export interface ParagraphStyles {
|
|
3
|
+
heading: (styleId: string | undefined) => number | undefined;
|
|
4
|
+
list: (styleId: string | undefined) => {
|
|
5
|
+
numId: string;
|
|
6
|
+
level: number;
|
|
7
|
+
} | undefined;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Reads the paragraph styles of `styles.xml`. A heading is a style named
|
|
11
|
+
* `heading N` (the built-in names stay English whatever the document's
|
|
12
|
+
* language: an Italian `Titolo1` is still named `heading 1`), a style with an
|
|
13
|
+
* outline level, or `Title`; either can be inherited through `basedOn`. A
|
|
14
|
+
* style can also carry list numbering (`List Bullet`).
|
|
15
|
+
*
|
|
16
|
+
* @param xml - The styles part; empty when the document has none.
|
|
17
|
+
* @returns The lookups.
|
|
18
|
+
*/
|
|
19
|
+
export declare function readStyles(xml: string): ParagraphStyles;
|
|
20
|
+
//# sourceMappingURL=styles.mapper.d.ts.map
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { Sheet } from '../spreadsheet/index.js';
|
|
2
|
+
/** A run of text that links somewhere: a URL, or a bookmark in the document (`#name`). */
|
|
3
|
+
export interface DocumentLink {
|
|
4
|
+
text: string;
|
|
5
|
+
href: string;
|
|
6
|
+
}
|
|
7
|
+
/** A paragraph, with what Word says it is: a heading, a list item, or plain text. */
|
|
8
|
+
export interface Paragraph {
|
|
9
|
+
kind: 'paragraph';
|
|
10
|
+
text: string;
|
|
11
|
+
/** The paragraph style's id (`Heading1`, `ListBullet`, a custom `Prezzo`). */
|
|
12
|
+
style?: string;
|
|
13
|
+
/** 1 to 9 for a heading (a `Heading N` style, a style with an outline level, or `Title` as 1). */
|
|
14
|
+
heading?: number;
|
|
15
|
+
/** For a list item: its level from 0, and whether it is numbered. */
|
|
16
|
+
list?: {
|
|
17
|
+
level: number;
|
|
18
|
+
ordered: boolean;
|
|
19
|
+
};
|
|
20
|
+
links?: DocumentLink[];
|
|
21
|
+
}
|
|
22
|
+
/** A table as a grid: rows of cell texts, merged cells as A1 ranges whose value sits in the top-left cell. */
|
|
23
|
+
export type DocumentTable = {
|
|
24
|
+
kind: 'table';
|
|
25
|
+
} & Sheet<string>;
|
|
26
|
+
export type DocumentBlock = Paragraph | DocumentTable;
|
|
27
|
+
/** A footnote or an endnote. */
|
|
28
|
+
export interface DocumentNote {
|
|
29
|
+
kind: 'footnote' | 'endnote';
|
|
30
|
+
id: string;
|
|
31
|
+
text: string;
|
|
32
|
+
}
|
|
33
|
+
/** What a Word document holds. */
|
|
34
|
+
export interface WordDocument {
|
|
35
|
+
/** The title in the document's properties, when it has one. */
|
|
36
|
+
title?: string;
|
|
37
|
+
/** The body, in order: paragraphs and tables. Text boxes come after the paragraph they sit in. */
|
|
38
|
+
body: DocumentBlock[];
|
|
39
|
+
/** Each header part's blocks (first page, odd, even pages…). */
|
|
40
|
+
headers: DocumentBlock[][];
|
|
41
|
+
footers: DocumentBlock[][];
|
|
42
|
+
notes: DocumentNote[];
|
|
43
|
+
}
|
|
44
|
+
//# sourceMappingURL=word-document.model.d.ts.map
|
package/dist/src/index.d.ts
CHANGED
|
@@ -2,6 +2,8 @@ export { readXlsx } from './spreadsheet/index.js';
|
|
|
2
2
|
export type { ReadXlsxOptions, CellValue, Sheet, Workbook, ValueMode, SheetFilter } from './spreadsheet/index.js';
|
|
3
3
|
export { readPptx } from './presentation/index.js';
|
|
4
4
|
export type { ReadPptxOptions, Deck, Slide, SlideShape, SlideChart, ChartSeries, SlideFilter } from './presentation/index.js';
|
|
5
|
+
export { readDocx } from './document/index.js';
|
|
6
|
+
export type { ReadDocxOptions, WordDocument, DocumentBlock, Paragraph, DocumentTable, DocumentNote, DocumentLink } from './document/index.js';
|
|
5
7
|
export { OfficeReadError } from './read-error/index.js';
|
|
6
8
|
export type { OfficeReadErrorCode } from './read-error/index.js';
|
|
7
9
|
export type { OfficeSource } from './source-bytes/index.js';
|
|
@@ -2,6 +2,6 @@ export { OoxmlPackage } from './ooxml-package.client.js';
|
|
|
2
2
|
export type { PackageLimits } from './ooxml-package.client.js';
|
|
3
3
|
export { walkXml, namespacedAttribute, isOn } from './xml-walk.algorithm.js';
|
|
4
4
|
export type { XmlHandlers } from './xml-walk.algorithm.js';
|
|
5
|
-
export { relationshipsOf, relationshipOfType } from './relationships.mapper.js';
|
|
5
|
+
export { relationshipsOf, relationshipOfType, hyperlinksOf } from './relationships.mapper.js';
|
|
6
6
|
export type { Relationship } from './relationships.mapper.js';
|
|
7
7
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -16,6 +16,14 @@ export interface Relationship {
|
|
|
16
16
|
* @returns The relationships by id.
|
|
17
17
|
*/
|
|
18
18
|
export declare function relationshipsOf(pkg: OoxmlPackage, part: string): Map<string, Relationship>;
|
|
19
|
+
/**
|
|
20
|
+
* A part's hyperlinks: the external targets its relationships point at, by id.
|
|
21
|
+
*
|
|
22
|
+
* @param pkg - The package.
|
|
23
|
+
* @param part - The part (`word/document.xml`).
|
|
24
|
+
* @returns The URLs by relationship id.
|
|
25
|
+
*/
|
|
26
|
+
export declare function hyperlinksOf(pkg: OoxmlPackage, part: string): Map<string, string>;
|
|
19
27
|
/**
|
|
20
28
|
* The first relationship of a type.
|
|
21
29
|
*
|
|
@@ -16,6 +16,8 @@ export type OfficeReadErrorCode =
|
|
|
16
16
|
'not-xlsx' |
|
|
17
17
|
/** A zip package that is not a presentation. */
|
|
18
18
|
'not-pptx' |
|
|
19
|
+
/** A zip package that is not a Word document. */
|
|
20
|
+
'not-docx' |
|
|
19
21
|
/** A part the package needs is missing or malformed. */
|
|
20
22
|
'malformed';
|
|
21
23
|
/** A file `office-reader` cannot read, with a `code` to branch on and a message that says what to do. */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@opencraw/office-reader",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.3",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.esm.js",
|
|
6
6
|
"module": "./dist/index.esm.js",
|
|
@@ -21,6 +21,11 @@
|
|
|
21
21
|
"types": "./dist/src/presentation/index.d.ts",
|
|
22
22
|
"import": "./dist/pptx.esm.js",
|
|
23
23
|
"default": "./dist/pptx.esm.js"
|
|
24
|
+
},
|
|
25
|
+
"./docx": {
|
|
26
|
+
"types": "./dist/src/document/index.d.ts",
|
|
27
|
+
"import": "./dist/docx.esm.js",
|
|
28
|
+
"default": "./dist/docx.esm.js"
|
|
24
29
|
}
|
|
25
30
|
},
|
|
26
31
|
"files": [
|
|
@@ -36,7 +41,7 @@
|
|
|
36
41
|
"publishConfig": {
|
|
37
42
|
"access": "public"
|
|
38
43
|
},
|
|
39
|
-
"description": "Reads Office files (.xlsx workbooks, .pptx presentations) into plain objects: cells with merged ranges and hidden rows, slides with positioned text, tables and chart data. No Node built-ins beyond reading a path: runs in Node, browsers, workers and edge runtimes.",
|
|
44
|
+
"description": "Reads Office files (.xlsx workbooks, .pptx presentations, .docx documents) into plain objects: cells with merged ranges and hidden rows, slides with positioned text, tables and chart data, document paragraphs with headings, lists and tables. No Node built-ins beyond reading a path: runs in Node, browsers, workers and edge runtimes.",
|
|
40
45
|
"license": "MIT",
|
|
41
46
|
"repository": {
|
|
42
47
|
"type": "git",
|
|
@@ -50,6 +55,8 @@
|
|
|
50
55
|
"pptx",
|
|
51
56
|
"powerpoint",
|
|
52
57
|
"presentation",
|
|
58
|
+
"docx",
|
|
59
|
+
"word",
|
|
53
60
|
"office",
|
|
54
61
|
"ooxml",
|
|
55
62
|
"reader",
|