@qvac/harness 0.7.3 → 0.8.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 (106) hide show
  1. package/CHANGELOG.md +45 -1
  2. package/NOTICE.md +16 -0
  3. package/dist-lib/lib/capabilities/completion.d.ts +1 -0
  4. package/dist-lib/lib/capabilities/completion.js +28 -8
  5. package/dist-lib/lib/capabilities/tool-turns.d.ts +3 -0
  6. package/dist-lib/lib/capabilities/tool-turns.js +7 -0
  7. package/dist-lib/lib/context-window.js +2 -2
  8. package/dist-lib/lib/helpers/run-outcome.d.ts +4 -1
  9. package/dist-lib/lib/helpers/run-outcome.js +14 -8
  10. package/dist-lib/lib/helpers/tool-call-markup.js +4 -4
  11. package/dist-lib/lib/hooks/built-in/arg-repair.js +7 -0
  12. package/dist-lib/lib/office/engine/codes.d.ts +62 -0
  13. package/dist-lib/lib/office/engine/codes.js +61 -0
  14. package/dist-lib/lib/office/engine/docx/document.d.ts +216 -0
  15. package/dist-lib/lib/office/engine/docx/document.js +1152 -0
  16. package/dist-lib/lib/office/engine/docx/index.d.ts +7 -0
  17. package/dist-lib/lib/office/engine/docx/index.js +8 -0
  18. package/dist-lib/lib/office/engine/docx/styles.d.ts +31 -0
  19. package/dist-lib/lib/office/engine/docx/styles.js +173 -0
  20. package/dist-lib/lib/office/engine/docx/template.d.ts +18 -0
  21. package/dist-lib/lib/office/engine/docx/template.js +133 -0
  22. package/dist-lib/lib/office/engine/docx/text.d.ts +55 -0
  23. package/dist-lib/lib/office/engine/docx/text.js +538 -0
  24. package/dist-lib/lib/office/engine/drawingml/chart.d.ts +63 -0
  25. package/dist-lib/lib/office/engine/drawingml/chart.js +539 -0
  26. package/dist-lib/lib/office/engine/drawingml/text.d.ts +35 -0
  27. package/dist-lib/lib/office/engine/drawingml/text.js +352 -0
  28. package/dist-lib/lib/office/engine/errors.d.ts +6 -0
  29. package/dist-lib/lib/office/engine/errors.js +12 -0
  30. package/dist-lib/lib/office/engine/image.d.ts +8 -0
  31. package/dist-lib/lib/office/engine/image.js +48 -0
  32. package/dist-lib/lib/office/engine/index.d.ts +12 -0
  33. package/dist-lib/lib/office/engine/index.js +7 -0
  34. package/dist-lib/lib/office/engine/opc/content-types.d.ts +17 -0
  35. package/dist-lib/lib/office/engine/opc/content-types.js +74 -0
  36. package/dist-lib/lib/office/engine/opc/media.d.ts +13 -0
  37. package/dist-lib/lib/office/engine/opc/media.js +11 -0
  38. package/dist-lib/lib/office/engine/opc/package.d.ts +51 -0
  39. package/dist-lib/lib/office/engine/opc/package.js +324 -0
  40. package/dist-lib/lib/office/engine/opc/paths.d.ts +7 -0
  41. package/dist-lib/lib/office/engine/opc/paths.js +46 -0
  42. package/dist-lib/lib/office/engine/opc/properties.d.ts +7 -0
  43. package/dist-lib/lib/office/engine/opc/properties.js +75 -0
  44. package/dist-lib/lib/office/engine/opc/relationships.d.ts +23 -0
  45. package/dist-lib/lib/office/engine/opc/relationships.js +65 -0
  46. package/dist-lib/lib/office/engine/opc/zip.d.ts +15 -0
  47. package/dist-lib/lib/office/engine/opc/zip.js +264 -0
  48. package/dist-lib/lib/office/engine/pptx/fit.d.ts +12 -0
  49. package/dist-lib/lib/office/engine/pptx/fit.js +37 -0
  50. package/dist-lib/lib/office/engine/pptx/index.d.ts +8 -0
  51. package/dist-lib/lib/office/engine/pptx/index.js +8 -0
  52. package/dist-lib/lib/office/engine/pptx/inherit.d.ts +29 -0
  53. package/dist-lib/lib/office/engine/pptx/inherit.js +155 -0
  54. package/dist-lib/lib/office/engine/pptx/notes.d.ts +5 -0
  55. package/dist-lib/lib/office/engine/pptx/notes.js +85 -0
  56. package/dist-lib/lib/office/engine/pptx/presentation.d.ts +54 -0
  57. package/dist-lib/lib/office/engine/pptx/presentation.js +749 -0
  58. package/dist-lib/lib/office/engine/pptx/read.d.ts +9 -0
  59. package/dist-lib/lib/office/engine/pptx/read.js +66 -0
  60. package/dist-lib/lib/office/engine/pptx/shapes.d.ts +52 -0
  61. package/dist-lib/lib/office/engine/pptx/shapes.js +276 -0
  62. package/dist-lib/lib/office/engine/pptx/slides.d.ts +27 -0
  63. package/dist-lib/lib/office/engine/pptx/slides.js +123 -0
  64. package/dist-lib/lib/office/engine/pptx/template.d.ts +20 -0
  65. package/dist-lib/lib/office/engine/pptx/template.js +391 -0
  66. package/dist-lib/lib/office/engine/pptx/types.d.ts +120 -0
  67. package/dist-lib/lib/office/engine/pptx/types.js +1 -0
  68. package/dist-lib/lib/office/engine/units.d.ts +3 -0
  69. package/dist-lib/lib/office/engine/units.js +12 -0
  70. package/dist-lib/lib/office/engine/utf8.d.ts +2 -0
  71. package/dist-lib/lib/office/engine/utf8.js +103 -0
  72. package/dist-lib/lib/office/engine/xlsx/formula.d.ts +49 -0
  73. package/dist-lib/lib/office/engine/xlsx/formula.js +381 -0
  74. package/dist-lib/lib/office/engine/xlsx/index.d.ts +8 -0
  75. package/dist-lib/lib/office/engine/xlsx/index.js +8 -0
  76. package/dist-lib/lib/office/engine/xlsx/refs.d.ts +22 -0
  77. package/dist-lib/lib/office/engine/xlsx/refs.js +82 -0
  78. package/dist-lib/lib/office/engine/xlsx/reshape.d.ts +21 -0
  79. package/dist-lib/lib/office/engine/xlsx/reshape.js +424 -0
  80. package/dist-lib/lib/office/engine/xlsx/sheet.d.ts +108 -0
  81. package/dist-lib/lib/office/engine/xlsx/sheet.js +662 -0
  82. package/dist-lib/lib/office/engine/xlsx/strings.d.ts +15 -0
  83. package/dist-lib/lib/office/engine/xlsx/strings.js +93 -0
  84. package/dist-lib/lib/office/engine/xlsx/styles.d.ts +38 -0
  85. package/dist-lib/lib/office/engine/xlsx/styles.js +369 -0
  86. package/dist-lib/lib/office/engine/xlsx/template.d.ts +7 -0
  87. package/dist-lib/lib/office/engine/xlsx/template.js +54 -0
  88. package/dist-lib/lib/office/engine/xlsx/workbook.d.ts +161 -0
  89. package/dist-lib/lib/office/engine/xlsx/workbook.js +850 -0
  90. package/dist-lib/lib/office/engine/xlsx/write.d.ts +2 -0
  91. package/dist-lib/lib/office/engine/xlsx/write.js +42 -0
  92. package/dist-lib/lib/office/engine/xml.d.ts +45 -0
  93. package/dist-lib/lib/office/engine/xml.js +258 -0
  94. package/dist-lib/lib/tools/exec/scope.d.ts +2 -0
  95. package/dist-lib/lib/tools/exec/scope.js +33 -2
  96. package/dist-lib/lib/tools/pdf/build.d.ts +43 -0
  97. package/dist-lib/lib/tools/pdf/build.js +150 -0
  98. package/dist-lib/lib/tools/pdf/images.d.ts +7 -0
  99. package/dist-lib/lib/tools/pdf/images.js +82 -0
  100. package/dist-lib/lib/tools/pdf/request.d.ts +63 -0
  101. package/dist-lib/lib/tools/pdf/request.js +335 -0
  102. package/dist-lib/lib/tools/pdf.d.ts +6 -0
  103. package/dist-lib/lib/tools/pdf.js +302 -0
  104. package/dist-lib/lib/tools/skill.js +14 -0
  105. package/package.json +25 -1
  106. package/worker/harness-sidecar.mjs +3 -1
@@ -0,0 +1,1152 @@
1
+ import { Package, referencedIds } from '../opc/package.js';
2
+ import * as xml from '../xml.js';
3
+ import * as media from '../opc/media.js';
4
+ import { probe } from '../image.js';
5
+ import * as chart from '../drawingml/chart.js';
6
+ import * as text from './text.js';
7
+ import { Styles } from './styles.js';
8
+ import * as template from './template.js';
9
+ import * as properties from '../opc/properties.js';
10
+ import { DocsError } from '../errors.js';
11
+ import { isRecord } from '../../../guards.js';
12
+ const { w: W, r: R, wp: WP, a: A, pic: PIC } = xml.ns;
13
+ const STRICT_REL = 'http://purl.oclc.org/ooxml/officeDocument/relationships/officeDocument';
14
+ const REL_BASE = 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/';
15
+ const REL = {
16
+ officeDocument: REL_BASE + 'officeDocument',
17
+ styles: REL_BASE + 'styles',
18
+ header: REL_BASE + 'header',
19
+ footer: REL_BASE + 'footer',
20
+ chart: REL_BASE + 'chart',
21
+ image: REL_BASE + 'image',
22
+ hyperlink: REL_BASE + 'hyperlink',
23
+ footnotes: REL_BASE + 'footnotes',
24
+ endnotes: REL_BASE + 'endnotes',
25
+ comments: REL_BASE + 'comments'
26
+ };
27
+ const DROPPABLE = [REL.image, REL.chart, REL.hyperlink, REL.header, REL.footer];
28
+ const DRAWING_PARTS = [REL.header, REL.footer, REL.footnotes, REL.endnotes, REL.comments];
29
+ const HEADER_TYPES = {
30
+ header: 'application/vnd.openxmlformats-officedocument.wordprocessingml.header+xml',
31
+ footer: 'application/vnd.openxmlformats-officedocument.wordprocessingml.footer+xml'
32
+ };
33
+ const SECTION_ORDER = [
34
+ 'headerReference',
35
+ 'footerReference',
36
+ 'footnotePr',
37
+ 'endnotePr',
38
+ 'type',
39
+ 'pgSz',
40
+ 'pgMar',
41
+ 'paperSrc',
42
+ 'pgBorders',
43
+ 'lnNumType',
44
+ 'pgNumType',
45
+ 'cols',
46
+ 'formProt',
47
+ 'vAlign',
48
+ 'noEndnote',
49
+ 'titlePg',
50
+ 'textDirection',
51
+ 'bidi',
52
+ 'rtlGutter',
53
+ 'docGrid',
54
+ 'printerSettings',
55
+ 'sectPrChange'
56
+ ];
57
+ const MAIN_TYPES = new Set([
58
+ 'application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml',
59
+ 'application/vnd.openxmlformats-officedocument.wordprocessingml.template.main+xml',
60
+ 'application/vnd.ms-word.document.macroEnabled.main+xml',
61
+ 'application/vnd.ms-word.template.macroEnabledTemplate.main+xml'
62
+ ]);
63
+ const BLOCKS = new Set(['p', 'tbl', 'sdt']);
64
+ const WRAPPERS = new Set(['sdt', 'customXml']);
65
+ const CELL_BLOCKS = new Set(['p', 'tbl', 'sdt', 'customXml']);
66
+ const WRAPPER_PROPERTIES = new Set(['sdtPr', 'sdtEndPr', 'customXmlPr']);
67
+ const UNWRAPPED = ['sdt', 'customXml', 'ins', 'moveTo'];
68
+ const UNTRACKED = [
69
+ 'del',
70
+ 'moveFrom',
71
+ 'rPrChange',
72
+ 'pPrChange',
73
+ 'trPrChange',
74
+ 'tcPrChange',
75
+ 'tblPrExChange',
76
+ 'sectPrChange',
77
+ 'numberingChange',
78
+ 'cellIns',
79
+ 'cellDel',
80
+ 'cellMerge',
81
+ 'moveFromRangeStart',
82
+ 'moveFromRangeEnd',
83
+ 'moveToRangeStart',
84
+ 'moveToRangeEnd',
85
+ 'bookmarkStart',
86
+ 'bookmarkEnd',
87
+ 'commentRangeStart',
88
+ 'commentRangeEnd',
89
+ 'permStart',
90
+ 'permEnd'
91
+ ];
92
+ const NOTE_REFERENCES = ['commentReference', 'footnoteReference', 'endnoteReference'];
93
+ const MARKS = new Set(['trPr', 'rPr']);
94
+ const NOTE_TYPES = ['separator', 'continuationSeparator', 'continuationNotice'];
95
+ const PIC_URI = 'http://schemas.openxmlformats.org/drawingml/2006/picture';
96
+ const EMU_PER_TWIP = 635;
97
+ const EMU_PER_PIXEL = 9525;
98
+ function isBlock(el) {
99
+ return BLOCKS.has(el.localName ?? '');
100
+ }
101
+ function blocksOf(parent) {
102
+ return xml.children(parent, W).filter(isBlock);
103
+ }
104
+ function textOf(parent) {
105
+ return blocksOf(parent).map(text.blockText).join('\n').trim();
106
+ }
107
+ function withRemoved(result, removed) {
108
+ if (Object.keys(removed).length) {
109
+ result.removed = removed;
110
+ }
111
+ return result;
112
+ }
113
+ function validateRows(rows) {
114
+ if (!Array.isArray(rows) || rows.length === 0 || !rows.every(Array.isArray)) {
115
+ throw new DocsError('INVALID_TABLE', 'rows must be a non-empty array of rows, each an array of cells');
116
+ }
117
+ const columns = Math.max(...rows.map((r) => r.length));
118
+ if (columns === 0) {
119
+ throw new DocsError('INVALID_TABLE', 'the table has no columns');
120
+ }
121
+ if (rows.length > 5000 || columns > 63) {
122
+ throw new DocsError('INVALID_TABLE', 'a table holds at most 5000 rows and 63 columns');
123
+ }
124
+ return rows.map((row, r) => row.map((value, c) => {
125
+ if (value === null || typeof value === 'string')
126
+ return value;
127
+ if (typeof value === 'number' && Number.isFinite(value))
128
+ return value;
129
+ throw new DocsError('INVALID_TABLE', `cell ${r},${c} must be a string, a number or null`);
130
+ }));
131
+ }
132
+ function cellText(value) {
133
+ return value === null || value === undefined ? '' : String(value);
134
+ }
135
+ function borders(doc) {
136
+ const el = text.wEl(doc, 'tblBorders');
137
+ for (const side of ['top', 'left', 'bottom', 'right', 'insideH', 'insideV']) {
138
+ el.appendChild(text.wEl(doc, side, { val: 'single', sz: 4, space: 0, color: 'auto' }));
139
+ }
140
+ return el;
141
+ }
142
+ function inlineDrawing(graphic, frame) {
143
+ const doc = xml.owner(graphic);
144
+ const wp = xml.builder(doc, WP);
145
+ const a = xml.builder(doc, A);
146
+ const drawing = text.wEl(doc, 'drawing');
147
+ const inline = xml.append(drawing, wp('inline', { distT: 0, distB: 0, distL: 0, distR: 0 }));
148
+ inline.appendChild(wp('extent', { cx: frame.size.width, cy: frame.size.height }));
149
+ inline.appendChild(wp('effectExtent', { l: 0, t: 0, r: 0, b: 0 }));
150
+ inline.appendChild(wp('docPr', { id: frame.id, name: frame.name, descr: frame.description }));
151
+ xml
152
+ .append(inline, wp('cNvGraphicFramePr'))
153
+ .appendChild(a('graphicFrameLocks', { noChangeAspect: 1 }));
154
+ inline.appendChild(graphic);
155
+ return drawing;
156
+ }
157
+ function pictureGraphic(doc, frame) {
158
+ const a = xml.builder(doc, A);
159
+ const pic = xml.builder(doc, PIC);
160
+ const graphic = a('graphic');
161
+ const data = xml.append(graphic, a('graphicData', { uri: PIC_URI }));
162
+ const picture = xml.append(data, pic('pic'));
163
+ const nv = xml.append(picture, pic('nvPicPr'));
164
+ nv.appendChild(pic('cNvPr', { id: frame.id, name: frame.name, descr: frame.description }));
165
+ nv.appendChild(pic('cNvPicPr'));
166
+ const fill = xml.append(picture, pic('blipFill'));
167
+ const blip = xml.append(fill, a('blip'));
168
+ xml.setNsAttribute(blip, { namespace: R, local: 'embed' }, frame.rId);
169
+ xml.append(fill, a('stretch')).appendChild(a('fillRect'));
170
+ const spPr = xml.append(picture, pic('spPr'));
171
+ const xfrm = xml.append(spPr, a('xfrm'));
172
+ xfrm.appendChild(a('off', { x: 0, y: 0 }));
173
+ xfrm.appendChild(a('ext', { cx: frame.size.width, cy: frame.size.height }));
174
+ xml.append(spPr, a('prstGeom', { prst: 'rect' })).appendChild(a('avLst'));
175
+ return graphic;
176
+ }
177
+ function chartGraphic(doc, rId) {
178
+ const a = xml.builder(doc, A);
179
+ const graphic = a('graphic');
180
+ const data = xml.append(graphic, a('graphicData', { uri: chart.GRAPHIC_URI }));
181
+ const ref = xml.append(data, xml.named(doc, 'c:chart', chart.C));
182
+ ref.setAttributeNS(R, 'r:id', rId);
183
+ return graphic;
184
+ }
185
+ function isEmptyParagraph(p) {
186
+ return xml.children(p, W).every((n) => n.localName === 'pPr');
187
+ }
188
+ function contentOf(wrapper) {
189
+ return xml.child(wrapper, W, 'sdtContent') ?? wrapper;
190
+ }
191
+ function unwrapped(parent, local) {
192
+ const out = [];
193
+ for (const el of xml.children(parent, W)) {
194
+ if (el.localName === local) {
195
+ out.push(el);
196
+ }
197
+ else if (WRAPPERS.has(el.localName ?? '')) {
198
+ out.push(...unwrapped(contentOf(el), local));
199
+ }
200
+ }
201
+ return out;
202
+ }
203
+ function firstParagraph(parent) {
204
+ for (const el of xml.children(parent, W)) {
205
+ if (el.localName === 'p') {
206
+ return el;
207
+ }
208
+ const found = WRAPPERS.has(el.localName ?? '') ? firstParagraph(contentOf(el)) : null;
209
+ if (found) {
210
+ return found;
211
+ }
212
+ }
213
+ return null;
214
+ }
215
+ function clearCell(tc) {
216
+ const keep = firstParagraph(tc);
217
+ if (!keep) {
218
+ for (const el of xml.children(tc, W).filter((n) => CELL_BLOCKS.has(n.localName ?? ''))) {
219
+ xml.remove(el);
220
+ }
221
+ return xml.append(tc, text.wEl(xml.owner(tc), 'p'));
222
+ }
223
+ for (let node = keep; node !== tc && node.parentNode; node = node.parentNode) {
224
+ for (const el of xml.children(node.parentNode, W)) {
225
+ if (el !== node && CELL_BLOCKS.has(el.localName ?? '')) {
226
+ xml.remove(el);
227
+ }
228
+ }
229
+ }
230
+ clearPlaceholders(keep);
231
+ return keep;
232
+ }
233
+ function clearPlaceholders(from) {
234
+ for (let node = from.parentNode; node && !xml.isElement(node, W, 'tbl'); node = node.parentNode) {
235
+ const sdtPr = xml.isElement(node, W, 'sdt') ? xml.child(node, W, 'sdtPr') : null;
236
+ for (const el of sdtPr ? xml.children(sdtPr, W, 'showingPlcHdr') : []) {
237
+ xml.remove(el);
238
+ }
239
+ }
240
+ }
241
+ function unwrap(el) {
242
+ const parent = el.parentNode;
243
+ if (!parent) {
244
+ return;
245
+ }
246
+ for (const child of xml.children(contentOf(el))) {
247
+ if (!WRAPPER_PROPERTIES.has(child.localName ?? '')) {
248
+ parent.insertBefore(child, el);
249
+ }
250
+ }
251
+ xml.remove(el);
252
+ }
253
+ function dropHeaderMark(tr) {
254
+ const trPr = xml.child(tr, W, 'trPr');
255
+ for (const h of trPr ? xml.children(trPr, W, 'tblHeader') : []) {
256
+ xml.remove(h);
257
+ }
258
+ }
259
+ function insertRow(tr, { tbl, rows, at }) {
260
+ const before = rows[at - 1];
261
+ if (before?.parentNode) {
262
+ before.parentNode.insertBefore(tr, before.nextSibling);
263
+ }
264
+ else {
265
+ ;
266
+ (rows[0]?.parentNode ?? tbl).insertBefore(tr, rows[0] ?? null);
267
+ }
268
+ }
269
+ function plainRow(tr) {
270
+ for (let el = firstOf(tr, UNWRAPPED); el; el = firstOf(tr, UNWRAPPED)) {
271
+ if (MARKS.has(el.parentNode?.localName ?? '')) {
272
+ xml.remove(el);
273
+ }
274
+ else {
275
+ unwrap(el);
276
+ }
277
+ }
278
+ for (const local of UNTRACKED) {
279
+ for (const el of xml.descendants(tr, W, local)) {
280
+ xml.remove(el);
281
+ }
282
+ }
283
+ for (const r of xml.descendants(tr, W, 'r')) {
284
+ if (NOTE_REFERENCES.some((ref) => xml.child(r, W, ref))) {
285
+ xml.remove(r);
286
+ }
287
+ }
288
+ return tr;
289
+ }
290
+ function firstOf(el, locals) {
291
+ for (const local of locals) {
292
+ const found = xml.descendants(el, W, local)[0];
293
+ if (found) {
294
+ return found;
295
+ }
296
+ }
297
+ return null;
298
+ }
299
+ function removeEmptyWrappers(from, stop) {
300
+ let node = from;
301
+ while (node && node !== stop && xml.isElement(node, W)) {
302
+ const wrapper = node.localName === 'sdtContent' ? node.parentNode : node;
303
+ if (!xml.isElement(wrapper, W) || !WRAPPERS.has(wrapper.localName ?? '')) {
304
+ return;
305
+ }
306
+ const content = xml.children(contentOf(wrapper));
307
+ if (content.some((el) => !WRAPPER_PROPERTIES.has(el.localName ?? ''))) {
308
+ return;
309
+ }
310
+ node = wrapper.parentNode;
311
+ xml.remove(wrapper);
312
+ }
313
+ }
314
+ function checkPositive(values) {
315
+ for (const [key, value] of Object.entries(values)) {
316
+ if (value !== undefined &&
317
+ !(typeof value === 'number' && Number.isInteger(value) && value > 0)) {
318
+ throw new DocsError('INVALID_BOX', `${key} must be a positive whole number of EMU, got ${String(value)}`);
319
+ }
320
+ }
321
+ }
322
+ function mainPart(pkg) {
323
+ const main = pkg.related('/', REL.officeDocument)[0];
324
+ if (main && MAIN_TYPES.has(pkg.contentType(main) ?? '')) {
325
+ return main;
326
+ }
327
+ if (pkg.rels('/').byType(STRICT_REL).length) {
328
+ throw new DocsError('UNSUPPORTED_DOCX', 'the document uses Strict Open XML, which is not supported');
329
+ }
330
+ throw new DocsError('NOT_DOCX', 'the file is not a Word document');
331
+ }
332
+ export class Document {
333
+ package;
334
+ main;
335
+ styles;
336
+ orphans;
337
+ constructor(pkg) {
338
+ this.package = pkg;
339
+ this.main = mainPart(pkg);
340
+ const top = pkg.root(this.main);
341
+ if (top.namespaceURI !== W || top.localName !== 'document') {
342
+ throw new DocsError('UNSUPPORTED_DOCX', 'the document uses Strict Open XML, which is not supported');
343
+ }
344
+ this.styles = new Styles(pkg, pkg.related(this.main, REL.styles)[0] ?? null);
345
+ this.orphans = new Set(this.unreferenced());
346
+ }
347
+ static open(bytes, opts) {
348
+ return new Document(Package.open(bytes, opts));
349
+ }
350
+ static create(opts = {}) {
351
+ const pkg = Package.create();
352
+ template.build(pkg, opts);
353
+ return new Document(pkg);
354
+ }
355
+ get changed() {
356
+ return this.package.changed;
357
+ }
358
+ get blockCount() {
359
+ return this.blocks().length;
360
+ }
361
+ read(opts = {}) {
362
+ const all = this.blocks();
363
+ const from = opts.from ?? 0;
364
+ const count = opts.count ?? Math.max(all.length, 1);
365
+ if (!Number.isInteger(from) || from < 0 || (from >= all.length && all.length > 0)) {
366
+ throw new DocsError('INVALID_OPTIONS', `from must be a block index from 0 to ${Math.max(0, all.length - 1)}, got ${String(opts.from)}`);
367
+ }
368
+ if (!Number.isInteger(count) || count < 1) {
369
+ throw new DocsError('INVALID_OPTIONS', `count must be at least 1, got ${String(opts.count)}`);
370
+ }
371
+ return {
372
+ total: all.length,
373
+ blocks: all.slice(from, from + count).map((el, i) => this.describe(el, from + i)),
374
+ headers: this.partTexts(REL.header),
375
+ footers: this.partTexts(REL.footer),
376
+ footnotes: this.notes(),
377
+ comments: this.comments()
378
+ };
379
+ }
380
+ setHeader(value, opts = {}) {
381
+ return this.setHeaderFooter({ kind: 'header', type: headerType(opts) }, value);
382
+ }
383
+ setFooter(value, opts = {}) {
384
+ return this.setHeaderFooter({ kind: 'footer', type: headerType(opts) }, value);
385
+ }
386
+ paragraphStyles() {
387
+ return this.styles.names('paragraph');
388
+ }
389
+ setText(index, input) {
390
+ const p = this.paragraph(index);
391
+ const before = text.paragraphText(p);
392
+ const runs = text.normalizeRuns(input);
393
+ const plain = runs.every((run) => Object.keys(run).length === 1);
394
+ if (plain && runs.map((run) => run.text).join('') === before) {
395
+ return { changed: false };
396
+ }
397
+ text.assertUntracked(p, `block ${index}`);
398
+ const removed = text.replaceContent(p, input);
399
+ this.touch();
400
+ return withRemoved({ changed: true }, removed);
401
+ }
402
+ insertParagraphs(at, specs) {
403
+ const blocks = this.blocks();
404
+ this.position(at, blocks.length);
405
+ const list = this.specs(specs);
406
+ const before = this.templateBefore(blocks, at);
407
+ const { anchor, index, trailer } = this.anchor(blocks, at);
408
+ const body = this.body();
409
+ const blank = this.blankOnly(blocks);
410
+ for (const spec of list) {
411
+ body.insertBefore(this.build(spec, before), anchor);
412
+ }
413
+ if (blank) {
414
+ xml.remove(blank);
415
+ }
416
+ if (trailer) {
417
+ xml.remove(trailer);
418
+ }
419
+ this.touch();
420
+ const shift = blank && index > 0 ? 1 : 0;
421
+ return { index: index - shift, count: list.length };
422
+ }
423
+ appendParagraphs(specs) {
424
+ return this.insertParagraphs(this.blockCount, specs);
425
+ }
426
+ deleteBlocks(index, opts = {}) {
427
+ const count = opts.count ?? 1;
428
+ const blocks = this.blocks();
429
+ this.block(index);
430
+ if (!Number.isInteger(count) || count < 1 || index + count > blocks.length) {
431
+ throw new DocsError('BLOCK_NOT_FOUND', `count must be from 1 to ${blocks.length - index} when deleting from block ${index}`);
432
+ }
433
+ for (const el of blocks.slice(index, index + count)) {
434
+ xml.remove(el);
435
+ }
436
+ this.ensureTrailingParagraph();
437
+ this.touch();
438
+ return { blocks: this.blockCount };
439
+ }
440
+ sections() {
441
+ const blocks = this.blocks();
442
+ const out = [];
443
+ blocks.forEach((el, index) => {
444
+ const level = this.headingLevel(el);
445
+ if (level === null || level < 1) {
446
+ return;
447
+ }
448
+ out.push({
449
+ index,
450
+ level,
451
+ title: text.paragraphText(el),
452
+ end: this.sectionEnd(blocks, { index, level })
453
+ });
454
+ });
455
+ return out;
456
+ }
457
+ replaceSection(index, opts = {}) {
458
+ const blocks = this.blocks();
459
+ const level = this.heading(blocks, index);
460
+ const end = this.sectionEnd(blocks, { index, level });
461
+ const body = blocks.slice(index + 1, end);
462
+ const list = opts.body === undefined ? null : this.specs(opts.body);
463
+ if (opts.title !== undefined) {
464
+ text.assertUntracked(blocks[index], `heading ${index}`);
465
+ text.replaceContent(blocks[index], opts.title);
466
+ }
467
+ if (list) {
468
+ const model = body.find((el) => el.localName === 'p' && this.headingLevel(el) === null && !this.isList(el));
469
+ const anchor = blocks[end] ?? this.sectPr();
470
+ for (const spec of list) {
471
+ this.body().insertBefore(this.build(spec, model ?? null), anchor);
472
+ }
473
+ for (const el of body) {
474
+ xml.remove(el);
475
+ }
476
+ }
477
+ this.touch();
478
+ return { index, removed: list ? body.length : 0, added: list ? list.length : 0 };
479
+ }
480
+ deleteSection(index) {
481
+ const blocks = this.blocks();
482
+ const level = this.heading(blocks, index);
483
+ const end = this.sectionEnd(blocks, { index, level });
484
+ return this.deleteBlocks(index, { count: end - index });
485
+ }
486
+ formatParagraph(index, input) {
487
+ const p = this.paragraph(index);
488
+ const { align, ...run } = text.validateFormat(input);
489
+ if (align) {
490
+ text.setAlign(p, align);
491
+ }
492
+ if (Object.keys(run).length) {
493
+ for (const r of xml.descendants(p, W, 'r')) {
494
+ if (!xml.child(r, W, 't'))
495
+ continue;
496
+ let rPr = xml.child(r, W, 'rPr');
497
+ if (!rPr) {
498
+ rPr = text.wEl(xml.owner(r), 'rPr');
499
+ r.insertBefore(rPr, r.firstChild);
500
+ }
501
+ text.applyRunFormat(rPr, run);
502
+ }
503
+ }
504
+ this.touch();
505
+ return { changed: true };
506
+ }
507
+ format(index, input) {
508
+ return this.formatParagraph(index, input);
509
+ }
510
+ formatStyle(name, input) {
511
+ if (!this.styles.part) {
512
+ throw new DocsError('STYLE_NOT_FOUND', 'the document has no styles part');
513
+ }
514
+ return this.styles.format(name, input);
515
+ }
516
+ addTable(at, opts) {
517
+ const blocks = this.blocks();
518
+ this.position(at, blocks.length);
519
+ const rows = validateRows(isRecord(opts) ? opts.rows : undefined);
520
+ const columns = Math.max(...rows.map((r) => r.length));
521
+ const header = opts.header !== false;
522
+ const tbl = this.tableElement({ rows, columns, header, styleId: this.tableStyleId(opts.style) });
523
+ const { anchor, index } = this.anchor(blocks, at);
524
+ this.body().insertBefore(tbl, anchor);
525
+ this.ensureTrailingParagraph();
526
+ this.touch();
527
+ return { index, rows: rows.length, columns };
528
+ }
529
+ setCell(cell, input) {
530
+ const tc = this.cell(cell);
531
+ text.assertUntracked(tc, `cell ${cell.row},${cell.column}`);
532
+ const removed = text.replaceContent(clearCell(tc), input);
533
+ this.touch();
534
+ return withRemoved({ changed: true }, removed);
535
+ }
536
+ addRow(table, values, opts = {}) {
537
+ const tbl = this.table(table);
538
+ const rows = unwrapped(tbl, 'tr');
539
+ const at = opts.at ?? rows.length;
540
+ if (!Number.isInteger(at) || at < 0 || at > rows.length) {
541
+ throw new DocsError('ROW_NOT_FOUND', `at must be a row position from 0 to ${rows.length}, got ${String(opts.at)}`);
542
+ }
543
+ if (!Array.isArray(values)) {
544
+ throw new DocsError('INVALID_TABLE', 'values must be an array of cell texts');
545
+ }
546
+ const model = at > 0 && rows[at - 1] ? rows[at - 1] : rows[rows.length > 1 ? 1 : 0];
547
+ const tr = plainRow(xml.clone(model));
548
+ dropHeaderMark(tr);
549
+ const cells = xml.children(tr, W, 'tc');
550
+ if (values.length > cells.length) {
551
+ throw new DocsError('INVALID_TABLE', `the row has ${cells.length} cells but ${values.length} values were given`);
552
+ }
553
+ cells.forEach((tc, c) => fillCell(tc, cellText(values[c])));
554
+ insertRow(tr, { tbl, rows, at });
555
+ this.touch();
556
+ return { rows: rows.length + 1 };
557
+ }
558
+ deleteRow(ref) {
559
+ const tbl = this.table(ref.table);
560
+ const rows = unwrapped(tbl, 'tr');
561
+ const tr = rows[ref.row];
562
+ if (!tr) {
563
+ throw new DocsError('ROW_NOT_FOUND', `row ${ref.row} does not exist; the table has ${rows.length} (0 to ${rows.length - 1})`);
564
+ }
565
+ if (rows.length === 1) {
566
+ throw new DocsError('LAST_ROW', "this is the table's only row; delete the table with deleteBlocks instead");
567
+ }
568
+ if (xml.descendants(tr, W, 'vMerge').length) {
569
+ throw new DocsError('ROW_MERGED', `row ${ref.row} is part of a vertically merged cell; unmerge it in Word first`);
570
+ }
571
+ const parent = tr.parentNode;
572
+ xml.remove(tr);
573
+ removeEmptyWrappers(parent, tbl);
574
+ this.touch();
575
+ return { rows: rows.length - 1 };
576
+ }
577
+ addImage(at, bytes, opts = {}) {
578
+ const blocks = this.blocks();
579
+ this.position(at, blocks.length);
580
+ const size = this.imageSize(probe(bytes), opts);
581
+ const { rId } = media.addImage(this.package, bytes, { source: this.main, dir: 'media' });
582
+ const doc = this.doc();
583
+ const id = this.nextDocPrId();
584
+ const p = text.wEl(doc, 'p');
585
+ if (opts.align) {
586
+ const { align } = text.validateFormat({ align: opts.align });
587
+ if (align) {
588
+ text.setAlign(p, align);
589
+ }
590
+ }
591
+ const frame = { id, size, name: opts.name || `Picture ${id}`, description: opts.description };
592
+ xml
593
+ .append(p, text.wEl(doc, 'r'))
594
+ .appendChild(inlineDrawing(pictureGraphic(doc, { ...frame, rId }), frame));
595
+ return { index: this.place(p, { blocks, at }), ...size };
596
+ }
597
+ addChart(at, spec) {
598
+ const blocks = this.blocks();
599
+ this.position(at, blocks.length);
600
+ const width = spec.width ?? this.textWidth() * EMU_PER_TWIP;
601
+ const height = spec.height ?? Math.round(width * 0.6);
602
+ checkPositive({ width, height });
603
+ const part = this.package.freeName('/word/charts/chart%d.xml');
604
+ chart.createChart({ pkg: this.package, part }, spec);
605
+ const rId = this.package.rels(this.main).add(REL.chart, part);
606
+ const doc = this.doc();
607
+ const id = this.nextDocPrId();
608
+ const p = text.wEl(doc, 'p');
609
+ const frame = { id, size: { width, height }, name: spec.name || `Chart ${id}` };
610
+ xml.append(p, text.wEl(doc, 'r')).appendChild(inlineDrawing(chartGraphic(doc, rId), frame));
611
+ return { index: this.place(p, { blocks, at }), width, height };
612
+ }
613
+ setChartData(index, data, opts = {}) {
614
+ return chart.setChartData({ pkg: this.package, part: this.chartPart(index, opts.chart ?? 0) }, data);
615
+ }
616
+ setChartTitle(index, title, opts = {}) {
617
+ if (!(title === null || typeof title === 'string' || Array.isArray(title))) {
618
+ throw new DocsError('INVALID_TEXT', 'title must be text, or null to remove it');
619
+ }
620
+ return chart.setChartTitle({ pkg: this.package, part: this.chartPart(index, opts.chart ?? 0) }, title);
621
+ }
622
+ save(opts) {
623
+ if (this.package.has(this.main)) {
624
+ this.dropOrphaned();
625
+ }
626
+ properties.applySaveOptions(this.package, opts);
627
+ return this.package.save();
628
+ }
629
+ doc() {
630
+ return this.package.xml(this.main);
631
+ }
632
+ unreferenced() {
633
+ const used = referencedIds(this.package.root(this.main));
634
+ return this.package
635
+ .rels(this.main)
636
+ .all()
637
+ .filter((rel) => DROPPABLE.includes(rel.type) && !used.has(rel.id))
638
+ .map((rel) => rel.id);
639
+ }
640
+ dropOrphaned() {
641
+ const rels = this.package.rels(this.main);
642
+ const dropped = this.unreferenced().filter((id) => !this.orphans.has(id));
643
+ for (const id of dropped) {
644
+ rels.remove(id);
645
+ }
646
+ if (dropped.length) {
647
+ this.package.prune();
648
+ }
649
+ }
650
+ body() {
651
+ const body = xml.child(this.package.root(this.main), W, 'body');
652
+ if (!body) {
653
+ throw new DocsError('BROKEN_DOCX', 'the document has no body');
654
+ }
655
+ return body;
656
+ }
657
+ sectPr() {
658
+ return xml.child(this.body(), W, 'sectPr');
659
+ }
660
+ blocks() {
661
+ return blocksOf(this.body());
662
+ }
663
+ touch() {
664
+ this.package.touch(this.main);
665
+ }
666
+ block(index) {
667
+ const blocks = this.blocks();
668
+ const found = Number.isInteger(index) && index >= 0 ? blocks[index] : undefined;
669
+ if (!found) {
670
+ const range = blocks.length ? ` (0 to ${blocks.length - 1})` : '';
671
+ throw new DocsError('BLOCK_NOT_FOUND', `block ${index} does not exist; the document has ${blocks.length} blocks${range}`);
672
+ }
673
+ return found;
674
+ }
675
+ paragraph(index) {
676
+ const el = this.block(index);
677
+ if (el.localName === 'p') {
678
+ return el;
679
+ }
680
+ const kind = el.localName === 'tbl' ? 'a table; use setCell' : 'a content control and cannot be edited';
681
+ throw new DocsError('NOT_PARAGRAPH', `block ${index} is ${kind}`);
682
+ }
683
+ table(index) {
684
+ const el = this.block(index);
685
+ if (el.localName !== 'tbl') {
686
+ throw new DocsError('NOT_TABLE', `block ${index} is not a table`);
687
+ }
688
+ return el;
689
+ }
690
+ cell(ref) {
691
+ const { row, column } = ref;
692
+ const rows = unwrapped(this.table(ref.table), 'tr');
693
+ const tr = Number.isInteger(row) && row >= 0 ? rows[row] : undefined;
694
+ if (!tr) {
695
+ throw new DocsError('CELL_NOT_FOUND', `row ${row} does not exist; the table has ${rows.length} (0 to ${rows.length - 1})`);
696
+ }
697
+ const cells = unwrapped(tr, 'tc');
698
+ const tc = Number.isInteger(column) && column >= 0 ? cells[column] : undefined;
699
+ if (!tc) {
700
+ throw new DocsError('CELL_NOT_FOUND', `column ${column} does not exist; the row has ${cells.length} (0 to ${cells.length - 1})`);
701
+ }
702
+ return checkMerge(tc, ref);
703
+ }
704
+ anchor(blocks, at) {
705
+ if (at < blocks.length) {
706
+ return { anchor: blocks[at], index: at, trailer: null };
707
+ }
708
+ const last = blocks[blocks.length - 1];
709
+ const before = blocks[blocks.length - 2];
710
+ const trailer = !!last &&
711
+ !!before &&
712
+ before.localName === 'tbl' &&
713
+ last.localName === 'p' &&
714
+ isEmptyParagraph(last);
715
+ if (trailer) {
716
+ return { anchor: last, index: at - 1, trailer: last };
717
+ }
718
+ return { anchor: this.sectPr(), index: at, trailer: null };
719
+ }
720
+ place(p, where) {
721
+ const { anchor, index, trailer } = this.anchor(where.blocks, where.at);
722
+ this.body().insertBefore(p, anchor);
723
+ if (trailer) {
724
+ xml.remove(trailer);
725
+ }
726
+ this.touch();
727
+ return index;
728
+ }
729
+ position(at, length) {
730
+ if (!Number.isInteger(at) || at < 0 || at > length) {
731
+ throw new DocsError('BLOCK_NOT_FOUND', `at must be a position from 0 to ${length}, got ${at}`);
732
+ }
733
+ }
734
+ specs(specs) {
735
+ const list = Array.isArray(specs) ? specs : [specs];
736
+ if (list.length === 0) {
737
+ throw new DocsError('INVALID_TEXT', 'give at least one paragraph');
738
+ }
739
+ return list.map((spec) => {
740
+ if (isRecord(spec) && spec.style !== undefined) {
741
+ return { ...spec, styleId: this.styles.require(spec.style).id };
742
+ }
743
+ return spec;
744
+ });
745
+ }
746
+ build(spec, model) {
747
+ const p = text.buildParagraph(this.doc(), spec, model);
748
+ if (isRecord(spec) && spec.level !== undefined) {
749
+ this.setLevel(p, spec.level);
750
+ }
751
+ return p;
752
+ }
753
+ setLevel(p, level) {
754
+ if (!(typeof level === 'number' && Number.isInteger(level) && level >= 0 && level <= 8)) {
755
+ throw new DocsError('INVALID_TEXT', `level must be an integer from 0 to 8, got ${String(level)}`);
756
+ }
757
+ const pPr = text.ensure(p, 'pPr');
758
+ const own = xml.child(pPr, W, 'numPr');
759
+ const numId = text.wGet(own ? xml.child(own, W, 'numId') : null, 'val') ||
760
+ this.styles.numIdOf(this.styleIdOf(p));
761
+ if (!numId || numId === '0') {
762
+ throw new DocsError('INVALID_TEXT', 'level applies to list paragraphs; give a list style such as "List Bullet"');
763
+ }
764
+ if (own) {
765
+ xml.remove(own);
766
+ }
767
+ const doc = xml.owner(p);
768
+ const numPr = xml.insertOrdered(pPr, text.wEl(doc, 'numPr'), text.PARAGRAPH_ORDER);
769
+ numPr.appendChild(text.wEl(doc, 'ilvl', { val: level }));
770
+ numPr.appendChild(text.wEl(doc, 'numId', { val: numId }));
771
+ }
772
+ sectionList() {
773
+ return xml
774
+ .descendants(this.body(), W, 'sectPr')
775
+ .filter((el) => !xml.isElement(el.parentNode, W, 'sectPrChange'));
776
+ }
777
+ setHeaderFooter(target, value) {
778
+ const sections = this.sectionList();
779
+ if (sections.length === 0) {
780
+ throw new DocsError('BROKEN_DOCX', 'the document has no section properties');
781
+ }
782
+ const refs = sections.flatMap((sectPr) => xml
783
+ .children(sectPr, W, `${target.kind}Reference`)
784
+ .filter((r) => (text.wGet(r, 'type') || 'default') === target.type));
785
+ if (value === null) {
786
+ for (const ref of refs) {
787
+ xml.remove(ref);
788
+ }
789
+ this.touch();
790
+ return { changed: refs.length > 0 };
791
+ }
792
+ const paragraphs = headerParagraphs(value, target.kind);
793
+ const parts = this.headerParts(refs);
794
+ if (parts.length === 0) {
795
+ parts.push(this.createHeaderPart(target, sections[0]));
796
+ }
797
+ for (const part of parts) {
798
+ this.fillHeaderPart(part, paragraphs);
799
+ }
800
+ return { changed: true, parts: parts.length };
801
+ }
802
+ headerParts(refs) {
803
+ const rels = this.package.rels(this.main);
804
+ const parts = new Set();
805
+ for (const ref of refs) {
806
+ const rel = rels.get(ref.getAttributeNS(R, 'id'));
807
+ if (rel && !rel.external && rel.part && this.package.has(rel.part)) {
808
+ parts.add(rel.part);
809
+ }
810
+ }
811
+ return [...parts];
812
+ }
813
+ createHeaderPart(target, first) {
814
+ const pkg = this.package;
815
+ const part = pkg.freeName(`/word/${target.kind}%d.xml`);
816
+ const style = this.styles.resolve(target.kind === 'header' ? 'Header' : 'Footer');
817
+ const rootName = target.kind === 'header' ? 'hdr' : 'ftr';
818
+ const doc = xml.parse(`<w:${rootName} xmlns:w="${W}" xmlns:r="${R}"><w:p/></w:${rootName}>`);
819
+ const p = xml.child(xml.root(doc), W, 'p');
820
+ if (style && p) {
821
+ text.ensure(p, 'pPr').appendChild(text.wEl(doc, 'pStyle', { val: style.id }));
822
+ }
823
+ pkg.add(part, doc, HEADER_TYPES[target.kind]);
824
+ const rId = pkg.rels(this.main).add(target.kind === 'header' ? REL.header : REL.footer, part);
825
+ const main = this.doc();
826
+ const ref = xml.insertOrdered(first, text.wEl(main, `${target.kind}Reference`, { type: target.type }), SECTION_ORDER);
827
+ xml.setNsAttribute(ref, { namespace: R, local: 'id' }, rId);
828
+ if (target.type === 'first' && !xml.child(first, W, 'titlePg')) {
829
+ xml.insertOrdered(first, text.wEl(main, 'titlePg'), SECTION_ORDER);
830
+ }
831
+ this.touch();
832
+ return part;
833
+ }
834
+ fillHeaderPart(part, paragraphs) {
835
+ const doc = this.package.xml(part);
836
+ const top = xml.root(doc);
837
+ const blocks = blocksOf(top);
838
+ const model = blocks.find((el) => el.localName === 'p') ?? null;
839
+ for (const spec of paragraphs) {
840
+ top.insertBefore(text.buildParagraph(doc, spec, model), blocks[0] ?? null);
841
+ }
842
+ for (const el of blocks) {
843
+ xml.remove(el);
844
+ }
845
+ this.package.touch(part);
846
+ }
847
+ notes() {
848
+ const part = this.package.related(this.main, REL.footnotes)[0];
849
+ if (!part) {
850
+ return [];
851
+ }
852
+ return xml
853
+ .children(this.package.root(part), W, 'footnote')
854
+ .filter((el) => !NOTE_TYPES.includes(text.wGet(el, 'type')))
855
+ .map((el) => ({ id: text.wGet(el, 'id'), text: textOf(el) }));
856
+ }
857
+ comments() {
858
+ const part = this.package.related(this.main, REL.comments)[0];
859
+ if (!part) {
860
+ return [];
861
+ }
862
+ return xml.children(this.package.root(part), W, 'comment').map((el) => ({
863
+ id: text.wGet(el, 'id'),
864
+ author: text.wGet(el, 'author') || null,
865
+ text: textOf(el)
866
+ }));
867
+ }
868
+ templateBefore(blocks, at) {
869
+ for (let i = at - 1; i >= 0; i--) {
870
+ const el = blocks[i];
871
+ if (el.localName !== 'p' || this.headingLevel(el) !== null) {
872
+ return null;
873
+ }
874
+ if (text.paragraphText(el) === '' && !xml.descendants(el, W, 'r').length) {
875
+ continue;
876
+ }
877
+ return el;
878
+ }
879
+ return null;
880
+ }
881
+ blankOnly(blocks) {
882
+ if (blocks.length !== 1 || blocks[0].localName !== 'p')
883
+ return null;
884
+ return isEmptyParagraph(blocks[0]) ? blocks[0] : null;
885
+ }
886
+ ensureTrailingParagraph() {
887
+ const blocks = this.blocks();
888
+ const last = blocks[blocks.length - 1];
889
+ if (!last || last.localName !== 'p') {
890
+ this.body().insertBefore(text.wEl(this.doc(), 'p'), this.sectPr());
891
+ }
892
+ }
893
+ styleIdOf(p) {
894
+ const pPr = xml.child(p, W, 'pPr');
895
+ const pStyle = pPr ? xml.child(pPr, W, 'pStyle') : null;
896
+ return pStyle ? pStyle.getAttributeNS(W, 'val') : null;
897
+ }
898
+ headingLevel(el) {
899
+ if (el.localName !== 'p')
900
+ return null;
901
+ const pPr = xml.child(el, W, 'pPr');
902
+ const outline = pPr ? xml.child(pPr, W, 'outlineLvl') : null;
903
+ if (outline) {
904
+ const level = Number(text.wGet(outline, 'val'));
905
+ return level < 9 ? level + 1 : null;
906
+ }
907
+ return this.styles.headingLevel(this.styleIdOf(el));
908
+ }
909
+ isList(p) {
910
+ const pPr = xml.child(p, W, 'pPr');
911
+ const numPr = pPr ? xml.child(pPr, W, 'numPr') : null;
912
+ const numId = numPr ? text.wGet(xml.child(numPr, W, 'numId'), 'val') : '';
913
+ if (numId) {
914
+ return numId !== '0';
915
+ }
916
+ return this.styles.isList(this.styleIdOf(p));
917
+ }
918
+ heading(blocks, index) {
919
+ this.block(index);
920
+ const level = this.headingLevel(blocks[index]);
921
+ if (level === null || level < 1) {
922
+ const headings = this.sections().map((s) => `${s.index} "${s.title}"`);
923
+ throw new DocsError('NOT_HEADING', `block ${index} is not a heading; the headings are ${headings.join(', ') || 'none'}`);
924
+ }
925
+ return level;
926
+ }
927
+ sectionEnd(blocks, start) {
928
+ for (let i = start.index + 1; i < blocks.length; i++) {
929
+ const other = this.headingLevel(blocks[i]);
930
+ if (other !== null && other <= start.level) {
931
+ return i;
932
+ }
933
+ }
934
+ return blocks.length;
935
+ }
936
+ describe(el, index) {
937
+ if (el.localName === 'tbl') {
938
+ return { index, type: 'table', rows: tableRows(el) };
939
+ }
940
+ if (el.localName === 'sdt') {
941
+ return { index, type: 'other', text: text.blockText(el) };
942
+ }
943
+ const out = {
944
+ index,
945
+ type: 'paragraph',
946
+ style: this.styles.nameOf(this.styleIdOf(el)),
947
+ text: text.paragraphText(el)
948
+ };
949
+ const level = this.headingLevel(el);
950
+ if (level !== null) {
951
+ out.heading = level;
952
+ }
953
+ if (this.isList(el)) {
954
+ out.list = true;
955
+ out.level = listLevel(el);
956
+ }
957
+ describeExtras(el, out);
958
+ const charts = this.chartParts(el);
959
+ if (charts.length) {
960
+ out.charts = charts.map((part) => chart.readChart(this.package.xml(part)));
961
+ }
962
+ if (text.hasPageBreak(el)) {
963
+ out.pageBreak = true;
964
+ }
965
+ return out;
966
+ }
967
+ chartParts(el) {
968
+ const rels = this.package.rels(this.main);
969
+ const out = [];
970
+ for (const ref of xml.descendants(el, chart.C, 'chart')) {
971
+ const rel = rels.get(ref.getAttributeNS(R, 'id'));
972
+ if (rel && !rel.external && rel.part && this.package.has(rel.part)) {
973
+ out.push(rel.part);
974
+ }
975
+ }
976
+ return out;
977
+ }
978
+ chartPart(index, which) {
979
+ const parts = this.chartParts(this.block(index));
980
+ if (parts.length === 0) {
981
+ throw new DocsError('NOT_CHART', `block ${index} holds no chart`);
982
+ }
983
+ const part = parts[which];
984
+ if (!part) {
985
+ throw new DocsError('NOT_CHART', `block ${index} holds ${parts.length} chart(s); chart ${which} does not exist`);
986
+ }
987
+ return part;
988
+ }
989
+ partTexts(type) {
990
+ const out = [];
991
+ for (const part of this.package.related(this.main, type)) {
992
+ const value = textOf(this.package.root(part));
993
+ if (value && !out.includes(value)) {
994
+ out.push(value);
995
+ }
996
+ }
997
+ return out;
998
+ }
999
+ textWidth() {
1000
+ const sectPr = this.sectPr();
1001
+ const pgSz = sectPr ? xml.child(sectPr, W, 'pgSz') : null;
1002
+ const pgMar = sectPr ? xml.child(sectPr, W, 'pgMar') : null;
1003
+ const width = pgSz ? Number(text.wGet(pgSz, 'w')) : 12240;
1004
+ const left = pgMar ? Number(text.wGet(pgMar, 'left')) : 1440;
1005
+ const right = pgMar ? Number(text.wGet(pgMar, 'right')) : 1440;
1006
+ const usable = width - left - right;
1007
+ return usable > 0 ? usable : 9360;
1008
+ }
1009
+ imageSize(info, opts) {
1010
+ checkPositive({ width: opts.width, height: opts.height });
1011
+ const ratio = info.height / info.width;
1012
+ if (opts.width !== undefined && opts.height !== undefined) {
1013
+ return { width: opts.width, height: opts.height };
1014
+ }
1015
+ if (opts.width !== undefined) {
1016
+ return { width: opts.width, height: Math.round(opts.width * ratio) };
1017
+ }
1018
+ if (opts.height !== undefined) {
1019
+ return { width: Math.round(opts.height / ratio), height: opts.height };
1020
+ }
1021
+ const width = Math.min(info.width * EMU_PER_PIXEL, this.textWidth() * EMU_PER_TWIP);
1022
+ return { width, height: Math.round(width * ratio) };
1023
+ }
1024
+ nextDocPrId() {
1025
+ const parts = [
1026
+ this.main,
1027
+ ...DRAWING_PARTS.flatMap((type) => this.package.related(this.main, type))
1028
+ ];
1029
+ let max = 0;
1030
+ for (const part of parts) {
1031
+ for (const el of xml.descendants(this.package.xml(part), WP, 'docPr')) {
1032
+ max = Math.max(max, Number(el.getAttribute('id')) || 0);
1033
+ }
1034
+ }
1035
+ return max + 1;
1036
+ }
1037
+ tableStyleId(style) {
1038
+ if (style !== undefined) {
1039
+ return this.styles.require(style, 'table').id;
1040
+ }
1041
+ return this.styles.resolve('Table Grid', 'table')?.id ?? null;
1042
+ }
1043
+ tableElement(spec) {
1044
+ const doc = this.doc();
1045
+ const cellWidth = Math.floor(this.textWidth() / spec.columns);
1046
+ const tbl = text.wEl(doc, 'tbl');
1047
+ tbl.appendChild(tableProperties(doc, spec));
1048
+ const grid = xml.append(tbl, text.wEl(doc, 'tblGrid'));
1049
+ for (let c = 0; c < spec.columns; c++) {
1050
+ grid.appendChild(text.wEl(doc, 'gridCol', { w: cellWidth }));
1051
+ }
1052
+ spec.rows.forEach((row, r) => tbl.appendChild(tableRow(doc, { row, first: r === 0, spec, cellWidth })));
1053
+ return tbl;
1054
+ }
1055
+ }
1056
+ function tableProperties(doc, spec) {
1057
+ const tblPr = text.wEl(doc, 'tblPr');
1058
+ if (spec.styleId) {
1059
+ tblPr.appendChild(text.wEl(doc, 'tblStyle', { val: spec.styleId }));
1060
+ }
1061
+ tblPr.appendChild(text.wEl(doc, 'tblW', { w: 0, type: 'auto' }));
1062
+ if (!spec.styleId) {
1063
+ tblPr.appendChild(borders(doc));
1064
+ }
1065
+ tblPr.appendChild(text.wEl(doc, 'tblLook', {
1066
+ val: '04A0',
1067
+ firstRow: spec.header ? 1 : 0,
1068
+ lastRow: 0,
1069
+ firstColumn: 1,
1070
+ lastColumn: 0,
1071
+ noHBand: 0,
1072
+ noVBand: 1
1073
+ }));
1074
+ return tblPr;
1075
+ }
1076
+ function tableRow(doc, build) {
1077
+ const headerRow = build.spec.header && build.first;
1078
+ const tr = text.wEl(doc, 'tr');
1079
+ if (headerRow) {
1080
+ xml.append(tr, text.wEl(doc, 'trPr')).appendChild(text.wEl(doc, 'tblHeader'));
1081
+ }
1082
+ for (let c = 0; c < build.spec.columns; c++) {
1083
+ const tc = xml.append(tr, text.wEl(doc, 'tc'));
1084
+ xml
1085
+ .append(tc, text.wEl(doc, 'tcPr'))
1086
+ .appendChild(text.wEl(doc, 'tcW', { w: build.cellWidth, type: 'dxa' }));
1087
+ const value = cellText(build.row[c]);
1088
+ tc.appendChild(text.buildParagraph(doc, headerRow ? { runs: [{ text: value, bold: true }] } : value, null));
1089
+ }
1090
+ return tr;
1091
+ }
1092
+ function fillCell(tc, value) {
1093
+ const tcPr = xml.child(tc, W, 'tcPr');
1094
+ for (const v of tcPr ? xml.children(tcPr, W, 'vMerge') : []) {
1095
+ xml.remove(v);
1096
+ }
1097
+ text.replaceContent(clearCell(tc), value);
1098
+ }
1099
+ function checkMerge(tc, ref) {
1100
+ const tcPr = xml.child(tc, W, 'tcPr');
1101
+ const vMerge = tcPr ? xml.child(tcPr, W, 'vMerge') : null;
1102
+ if (vMerge && text.wGet(vMerge, 'val') !== 'restart') {
1103
+ throw new DocsError('CELL_MERGED', `cell ${ref.row},${ref.column} is covered by a merged cell above it; write that cell instead`);
1104
+ }
1105
+ return tc;
1106
+ }
1107
+ function headerParagraphs(value, kind) {
1108
+ if (typeof value === 'string') {
1109
+ return value.split('\n');
1110
+ }
1111
+ if (Array.isArray(value) && value.length)
1112
+ return value;
1113
+ throw new DocsError('INVALID_TEXT', `${kind} text must be a string or an array of paragraphs`);
1114
+ }
1115
+ function headerType(opts) {
1116
+ const type = opts.type ?? 'default';
1117
+ if (type !== 'default' && type !== 'first') {
1118
+ throw new DocsError('INVALID_OPTIONS', 'type must be "default" or "first"');
1119
+ }
1120
+ return type;
1121
+ }
1122
+ function tableRows(el) {
1123
+ return unwrapped(el, 'tr').map((tr) => unwrapped(tr, 'tc').map((tc) => xml
1124
+ .children(tc, W)
1125
+ .filter((n) => CELL_BLOCKS.has(n.localName ?? ''))
1126
+ .map(text.blockText)
1127
+ .join('\n')));
1128
+ }
1129
+ function listLevel(el) {
1130
+ const pPr = xml.child(el, W, 'pPr');
1131
+ const numPr = pPr ? xml.child(pPr, W, 'numPr') : null;
1132
+ const ilvl = numPr ? xml.child(numPr, W, 'ilvl') : null;
1133
+ return ilvl ? Number(text.wGet(ilvl, 'val')) : 0;
1134
+ }
1135
+ function describeExtras(el, out) {
1136
+ const footnotes = xml.descendants(el, W, 'footnoteReference').map((r) => text.wGet(r, 'id'));
1137
+ if (footnotes.length) {
1138
+ out.footnotes = footnotes;
1139
+ }
1140
+ const anchors = [
1141
+ ...xml.descendants(el, W, 'commentRangeStart'),
1142
+ ...xml.descendants(el, W, 'commentReference')
1143
+ ];
1144
+ const comments = [...new Set(anchors.map((r) => text.wGet(r, 'id')))];
1145
+ if (comments.length) {
1146
+ out.comments = comments;
1147
+ }
1148
+ const pictures = xml.descendants(el, PIC, 'pic').length;
1149
+ if (pictures) {
1150
+ out.images = pictures;
1151
+ }
1152
+ }