@sametalmdr/word-editor 0.2.0-alpha → 0.3.0-alpha
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/CHANGELOG.md +108 -70
- package/LICENSE +21 -21
- package/README.md +286 -247
- package/dist/canvas/box-clipboard.js +238 -238
- package/dist/canvas/box-selection.js +80 -80
- package/dist/canvas/canvas-editor.js +502 -502
- package/dist/canvas/controller.js +717 -717
- package/dist/canvas/geometry.js +131 -131
- package/dist/canvas/gesture.d.ts +0 -1
- package/dist/canvas/gesture.js +289 -289
- package/dist/canvas/handles.js +212 -212
- package/dist/canvas/menu.js +82 -82
- package/dist/canvas/mode-controller.js +81 -81
- package/dist/canvas/nudge.js +108 -108
- package/dist/canvas/overlay.js +149 -149
- package/dist/canvas/snap.js +132 -132
- package/dist/canvas/viewport.js +112 -112
- package/dist/canvas/z-order.js +171 -171
- package/dist/commands/alignment.js +150 -150
- package/dist/commands/box.js +272 -272
- package/dist/commands/clipboard.js +93 -93
- package/dist/commands/graphemes.js +112 -112
- package/dist/commands/marks.js +153 -153
- package/dist/commands/paragraph-props.js +176 -176
- package/dist/commands/size.js +368 -368
- package/dist/commands/text.js +291 -291
- package/dist/docx.d.ts +1 -0
- package/dist/docx.js +15 -13
- package/dist/editor/editing-host.js +434 -434
- package/dist/element.js +41 -41
- package/dist/fonts/OFL-Carlito.txt +94 -0
- package/dist/fonts/calibri-metrics.d.ts +27 -0
- package/dist/fonts/calibri-metrics.js +88 -0
- package/dist/history/history.js +433 -433
- package/dist/index.js +63 -63
- package/dist/input/beforeinput.js +224 -224
- package/dist/input/clipboard.js +107 -107
- package/dist/input/composition.js +341 -341
- package/dist/input/keymap.js +54 -54
- package/dist/io/ooxml/custgeom.js +72 -72
- package/dist/io/ooxml/drawingml.js +232 -232
- package/dist/io/ooxml/edits.js +67 -67
- package/dist/io/ooxml/export-splice.js +880 -880
- package/dist/io/ooxml/export.js +365 -365
- package/dist/io/ooxml/fill.d.ts +93 -8
- package/dist/io/ooxml/fill.js +618 -562
- package/dist/io/ooxml/fit.d.ts +252 -0
- package/dist/io/ooxml/fit.js +598 -0
- package/dist/io/ooxml/import-gate.js +200 -200
- package/dist/io/ooxml/import.js +691 -691
- package/dist/io/ooxml/inflate.js +126 -126
- package/dist/io/ooxml/media.js +87 -87
- package/dist/io/ooxml/ns.d.ts +2 -0
- package/dist/io/ooxml/ns.js +26 -21
- package/dist/io/ooxml/numbering.js +290 -290
- package/dist/io/ooxml/opc-write.js +171 -171
- package/dist/io/ooxml/provenance.js +93 -93
- package/dist/io/ooxml/recipe.js +708 -708
- package/dist/io/ooxml/relationships.js +59 -59
- package/dist/io/ooxml/run-fonts.d.ts +89 -0
- package/dist/io/ooxml/run-fonts.js +378 -0
- package/dist/io/ooxml/slots.d.ts +4 -0
- package/dist/io/ooxml/slots.js +582 -581
- package/dist/io/ooxml/styles-resolve.js +257 -257
- package/dist/io/ooxml/units.js +43 -43
- package/dist/io/ooxml/value.js +120 -120
- package/dist/io/ooxml/xml-scan.js +616 -616
- package/dist/io/ooxml/zip-container.js +228 -228
- package/dist/io/ooxml/zip-reject.js +106 -106
- package/dist/measure/placement.js +83 -83
- package/dist/measure/report.js +75 -75
- package/dist/model/ids.js +70 -70
- package/dist/model/normalize.js +146 -146
- package/dist/model/position.js +283 -283
- package/dist/model/transaction.js +209 -209
- package/dist/platform/capabilities.js +25 -25
- package/dist/react/index.js +21 -21
- package/dist/selection/controller.js +315 -315
- package/dist/selection/dom-map.js +334 -334
- package/dist/state/editor-state.js +109 -109
- package/dist/styles.css +66 -66
- package/package.json +94 -93
package/dist/io/ooxml/slots.js
CHANGED
|
@@ -1,581 +1,582 @@
|
|
|
1
|
-
// Template scan for the fill engine (FR2, ADR-0037): where the `{tag}`s are,
|
|
2
|
-
// which of them may be filled, and every reason one may not. Pure and
|
|
3
|
-
// synchronous — word/document.xml's scanned tree and bytes in, a plain result
|
|
4
|
-
// out. Never calls importDocx: the importer's gate (character styles,
|
|
5
|
-
// wp:inline, group transforms) has nothing to say about filling.
|
|
6
|
-
//
|
|
7
|
-
// THE RULES (ADR-0037 Karar 1–3, 7):
|
|
8
|
-
// - A tag is the WHOLE text of one `w:t`: `{` + [a-z][a-z0-9_]* + `}`, its
|
|
9
|
-
// raw bytes equal to that text, covered by a single text node.
|
|
10
|
-
// - The only context a tag may sit in:
|
|
11
|
-
// mc:AlternateContent > mc:Choice|mc:Fallback > … > w:txbxContent > w:p >
|
|
12
|
-
// w:r > w:t, the AlternateContent holding exactly one Choice (with one
|
|
13
|
-
// wp:anchor), one Fallback and one w:txbxContent per branch, not nested,
|
|
14
|
-
// in a paragraph with no field.
|
|
15
|
-
// - The two copies are compared STRUCTURALLY; only bookmark ids may differ.
|
|
16
|
-
// When they are equal, the k-th tag of the Choice copy is the twin of the
|
|
17
|
-
// k-th tag of the Fallback copy — no VML id, bookmark name or offset is
|
|
18
|
-
// ever used to pair them.
|
|
19
|
-
// - Grammar and context problems are reported from the Choice copy (and the
|
|
20
|
-
// body) only. A brace in the Fallback copy marks its AlternateContent for
|
|
21
|
-
// the copy comparison, which is what surfaces a Fallback-only difference.
|
|
22
|
-
|
|
23
|
-
import { getAttr, findChild, isElement } from "./xml-scan.js";
|
|
24
|
-
import { readSections, pageForAnchor } from "./sections.js";
|
|
25
|
-
import { W_NS, WP_NS, MC_NS, XML_NS } from "./ns.js";
|
|
26
|
-
|
|
27
|
-
const TAG_TEXT = /^\{([a-z][a-z0-9_]*)\}$/;
|
|
28
|
-
const TAG_ANYWHERE = /\{[a-z][a-z0-9_]*\}/;
|
|
29
|
-
const BRACED = /\{[^{}]*\}/;
|
|
30
|
-
|
|
31
|
-
// ST_OnOff's false spellings (ECMA-376 §22.9.2.7) — `<w:vanish w:val="0"/>` is not hidden.
|
|
32
|
-
const ON_OFF_FALSE = new Set(["0", "false", "off"]);
|
|
33
|
-
|
|
34
|
-
const textDecoder = new TextDecoder("utf-8", { fatal: true });
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* @typedef {object} TemplateProblem
|
|
38
|
-
* @property {string} code
|
|
39
|
-
* @property {string} message never contains a data value
|
|
40
|
-
* @property {string} [tag]
|
|
41
|
-
* @property {number | null} [docPrId]
|
|
42
|
-
* @property {number} [page]
|
|
43
|
-
* @property {string} [part]
|
|
44
|
-
* @property {string} [path]
|
|
45
|
-
*/
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* @typedef {object} TagSite one tag `w:t`, in one copy
|
|
49
|
-
* @property {string} name
|
|
50
|
-
* @property {import("./xml-scan.js").XmlElement} t
|
|
51
|
-
* @property {import("./xml-scan.js").XmlElement} run
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
* @
|
|
57
|
-
* @property {
|
|
58
|
-
* @property {number}
|
|
59
|
-
* @property {number}
|
|
60
|
-
* @property {
|
|
61
|
-
* @property {TagSite}
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
* @
|
|
67
|
-
* @property {
|
|
68
|
-
* @property {
|
|
69
|
-
* @property {
|
|
70
|
-
* @property {import("./xml-scan.js").XmlElement[]}
|
|
71
|
-
* @property {import("./xml-scan.js").XmlElement[]}
|
|
72
|
-
* @property {import("./xml-scan.js").XmlElement[]}
|
|
73
|
-
* @property {import("./xml-scan.js").XmlElement[]}
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
* @
|
|
79
|
-
* @property {
|
|
80
|
-
* @property {
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
* @
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
*
|
|
94
|
-
* @
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
*
|
|
107
|
-
*
|
|
108
|
-
*
|
|
109
|
-
*
|
|
110
|
-
*
|
|
111
|
-
* @param {
|
|
112
|
-
* @param {(
|
|
113
|
-
* @
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
* @param {
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
if (
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
ctx.
|
|
151
|
-
ctx.
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
*
|
|
179
|
-
*
|
|
180
|
-
* @
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
*
|
|
189
|
-
*
|
|
190
|
-
* @
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
*
|
|
208
|
-
*
|
|
209
|
-
*
|
|
210
|
-
*
|
|
211
|
-
*
|
|
212
|
-
* @
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
*
|
|
232
|
-
*
|
|
233
|
-
*
|
|
234
|
-
*
|
|
235
|
-
*
|
|
236
|
-
*
|
|
237
|
-
*
|
|
238
|
-
* @param {import("./xml-scan.js").XmlElement}
|
|
239
|
-
* @
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
* @param {import("./xml-scan.js").
|
|
249
|
-
* @
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
attr.
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
* @param {import("./xml-scan.js").XmlElement}
|
|
263
|
-
* @param {
|
|
264
|
-
* @
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
const
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
const
|
|
273
|
-
|
|
274
|
-
if (y
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
const
|
|
288
|
-
|
|
289
|
-
if (ca.kind
|
|
290
|
-
|
|
291
|
-
const
|
|
292
|
-
seen.
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
* @param {
|
|
305
|
-
* @
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
*
|
|
333
|
-
* @param {
|
|
334
|
-
* @param {
|
|
335
|
-
* @
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
return { code: "
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
if (ctx.
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
const
|
|
351
|
-
const
|
|
352
|
-
|
|
353
|
-
if (
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
return
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
* @
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
if (
|
|
371
|
-
if (
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
*
|
|
379
|
-
* @
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
*
|
|
391
|
-
*
|
|
392
|
-
*
|
|
393
|
-
* @
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
const
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
const
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
* @
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
const
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
* @
|
|
420
|
-
* @property {
|
|
421
|
-
* @property {TemplateProblem[]}
|
|
422
|
-
* @property {
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
* @param {
|
|
428
|
-
* @
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
const
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
const
|
|
483
|
-
const
|
|
484
|
-
|
|
485
|
-
if (ac.
|
|
486
|
-
if (ac.
|
|
487
|
-
if (ac.
|
|
488
|
-
if (ac.
|
|
489
|
-
if (
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
let
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
const
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
//
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
}
|
|
1
|
+
// Template scan for the fill engine (FR2, ADR-0037): where the `{tag}`s are,
|
|
2
|
+
// which of them may be filled, and every reason one may not. Pure and
|
|
3
|
+
// synchronous — word/document.xml's scanned tree and bytes in, a plain result
|
|
4
|
+
// out. Never calls importDocx: the importer's gate (character styles,
|
|
5
|
+
// wp:inline, group transforms) has nothing to say about filling.
|
|
6
|
+
//
|
|
7
|
+
// THE RULES (ADR-0037 Karar 1–3, 7):
|
|
8
|
+
// - A tag is the WHOLE text of one `w:t`: `{` + [a-z][a-z0-9_]* + `}`, its
|
|
9
|
+
// raw bytes equal to that text, covered by a single text node.
|
|
10
|
+
// - The only context a tag may sit in:
|
|
11
|
+
// mc:AlternateContent > mc:Choice|mc:Fallback > … > w:txbxContent > w:p >
|
|
12
|
+
// w:r > w:t, the AlternateContent holding exactly one Choice (with one
|
|
13
|
+
// wp:anchor), one Fallback and one w:txbxContent per branch, not nested,
|
|
14
|
+
// in a paragraph with no field.
|
|
15
|
+
// - The two copies are compared STRUCTURALLY; only bookmark ids may differ.
|
|
16
|
+
// When they are equal, the k-th tag of the Choice copy is the twin of the
|
|
17
|
+
// k-th tag of the Fallback copy — no VML id, bookmark name or offset is
|
|
18
|
+
// ever used to pair them.
|
|
19
|
+
// - Grammar and context problems are reported from the Choice copy (and the
|
|
20
|
+
// body) only. A brace in the Fallback copy marks its AlternateContent for
|
|
21
|
+
// the copy comparison, which is what surfaces a Fallback-only difference.
|
|
22
|
+
|
|
23
|
+
import { getAttr, findChild, isElement } from "./xml-scan.js";
|
|
24
|
+
import { readSections, pageForAnchor } from "./sections.js";
|
|
25
|
+
import { W_NS, WP_NS, MC_NS, XML_NS } from "./ns.js";
|
|
26
|
+
|
|
27
|
+
const TAG_TEXT = /^\{([a-z][a-z0-9_]*)\}$/;
|
|
28
|
+
const TAG_ANYWHERE = /\{[a-z][a-z0-9_]*\}/;
|
|
29
|
+
const BRACED = /\{[^{}]*\}/;
|
|
30
|
+
|
|
31
|
+
// ST_OnOff's false spellings (ECMA-376 §22.9.2.7) — `<w:vanish w:val="0"/>` is not hidden.
|
|
32
|
+
const ON_OFF_FALSE = new Set(["0", "false", "off"]);
|
|
33
|
+
|
|
34
|
+
const textDecoder = new TextDecoder("utf-8", { fatal: true });
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* @typedef {object} TemplateProblem
|
|
38
|
+
* @property {string} code
|
|
39
|
+
* @property {string} message never contains a data value
|
|
40
|
+
* @property {string} [tag]
|
|
41
|
+
* @property {number | null} [docPrId]
|
|
42
|
+
* @property {number} [page]
|
|
43
|
+
* @property {string} [part]
|
|
44
|
+
* @property {string} [path]
|
|
45
|
+
*/
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* @typedef {object} TagSite one tag `w:t`, in one copy
|
|
49
|
+
* @property {string} name
|
|
50
|
+
* @property {import("./xml-scan.js").XmlElement} t
|
|
51
|
+
* @property {import("./xml-scan.js").XmlElement} run
|
|
52
|
+
* @property {import("./xml-scan.js").XmlElement} paragraph the w:p holding the run (contextProblem checked it)
|
|
53
|
+
*/
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* @typedef {object} TagOccurrence one tag of a box's Choice copy, with its Fallback twin
|
|
57
|
+
* @property {string} name
|
|
58
|
+
* @property {number | null} docPrId
|
|
59
|
+
* @property {number} page
|
|
60
|
+
* @property {number} acIndex
|
|
61
|
+
* @property {TagSite} choice
|
|
62
|
+
* @property {TagSite} fallback
|
|
63
|
+
*/
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* @typedef {object} AlternateContentRecord
|
|
67
|
+
* @property {import("./xml-scan.js").XmlElement} el
|
|
68
|
+
* @property {number} index document (pre-)order among all AlternateContents
|
|
69
|
+
* @property {boolean} nested
|
|
70
|
+
* @property {import("./xml-scan.js").XmlElement[]} choices
|
|
71
|
+
* @property {import("./xml-scan.js").XmlElement[]} fallbacks
|
|
72
|
+
* @property {import("./xml-scan.js").XmlElement[]} choiceTx
|
|
73
|
+
* @property {import("./xml-scan.js").XmlElement[]} fallbackTx
|
|
74
|
+
* @property {import("./xml-scan.js").XmlElement[]} choiceAnchors wp:anchor / wp:inline in the Choice, outside any text box
|
|
75
|
+
*/
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* @typedef {object} WalkContext
|
|
79
|
+
* @property {AlternateContentRecord | null} ac
|
|
80
|
+
* @property {"choice" | "fallback" | null} branch
|
|
81
|
+
* @property {{el: import("./xml-scan.js").XmlElement, nested: boolean} | null} txbx
|
|
82
|
+
*/
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* @param {import("./xml-scan.js").XmlElement} el
|
|
86
|
+
* @returns {string}
|
|
87
|
+
*/
|
|
88
|
+
function qname(el) {
|
|
89
|
+
return el.prefix ? `${el.prefix}:${el.localName}` : el.localName;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* The decoded text of a `w:t`-like element: every text and CDATA child.
|
|
94
|
+
* @param {import("./xml-scan.js").XmlElement} el
|
|
95
|
+
* @returns {string}
|
|
96
|
+
*/
|
|
97
|
+
function elementText(el) {
|
|
98
|
+
let text = "";
|
|
99
|
+
for (const c of el.children) {
|
|
100
|
+
if (c.kind === "text" || c.kind === "cdata") text += c.value;
|
|
101
|
+
}
|
|
102
|
+
return text;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* One document-order walk over the whole tree — Choice AND Fallback, inside
|
|
107
|
+
* text boxes too — building the AlternateContent records and handing every
|
|
108
|
+
* `t` element to `onText` together with its ancestor stack (stack top = its
|
|
109
|
+
* parent), and every `w:br` to `onBreak`. No parent pointers exist in
|
|
110
|
+
* xml-scan's tree; the stack is the parent chain.
|
|
111
|
+
* @param {import("./xml-scan.js").XmlElement} root
|
|
112
|
+
* @param {(t: import("./xml-scan.js").XmlElement, ctx: WalkContext, stack: import("./xml-scan.js").XmlElement[]) => void} onText
|
|
113
|
+
* @param {(br: import("./xml-scan.js").XmlElement) => void} [onBreak]
|
|
114
|
+
* @returns {AlternateContentRecord[]}
|
|
115
|
+
*/
|
|
116
|
+
function walk(root, onText, onBreak = () => {}) {
|
|
117
|
+
/** @type {AlternateContentRecord[]} */
|
|
118
|
+
const acs = [];
|
|
119
|
+
/** @type {import("./xml-scan.js").XmlElement[]} */
|
|
120
|
+
const stack = [];
|
|
121
|
+
/**
|
|
122
|
+
* @param {import("./xml-scan.js").XmlElement} el
|
|
123
|
+
* @param {WalkContext} ctx
|
|
124
|
+
*/
|
|
125
|
+
function visit(el, ctx) {
|
|
126
|
+
stack.push(el);
|
|
127
|
+
for (const child of el.children) {
|
|
128
|
+
if (child.kind !== "element") continue;
|
|
129
|
+
if (isElement(child, MC_NS, "AlternateContent")) {
|
|
130
|
+
/** @type {AlternateContentRecord} */
|
|
131
|
+
const rec = {
|
|
132
|
+
el: child,
|
|
133
|
+
index: acs.length,
|
|
134
|
+
nested: ctx.ac !== null,
|
|
135
|
+
choices: [],
|
|
136
|
+
fallbacks: [],
|
|
137
|
+
choiceTx: [],
|
|
138
|
+
fallbackTx: [],
|
|
139
|
+
choiceAnchors: [],
|
|
140
|
+
};
|
|
141
|
+
acs.push(rec);
|
|
142
|
+
visit(child, { ac: rec, branch: null, txbx: null });
|
|
143
|
+
} else if (ctx.ac !== null && el === ctx.ac.el && isElement(child, MC_NS, "Choice")) {
|
|
144
|
+
ctx.ac.choices.push(child);
|
|
145
|
+
visit(child, { ...ctx, branch: "choice" });
|
|
146
|
+
} else if (ctx.ac !== null && el === ctx.ac.el && isElement(child, MC_NS, "Fallback")) {
|
|
147
|
+
ctx.ac.fallbacks.push(child);
|
|
148
|
+
visit(child, { ...ctx, branch: "fallback" });
|
|
149
|
+
} else if (
|
|
150
|
+
ctx.ac !== null &&
|
|
151
|
+
ctx.branch === "choice" &&
|
|
152
|
+
ctx.txbx === null &&
|
|
153
|
+
(isElement(child, WP_NS, "anchor") || isElement(child, WP_NS, "inline"))
|
|
154
|
+
) {
|
|
155
|
+
ctx.ac.choiceAnchors.push(child);
|
|
156
|
+
visit(child, ctx);
|
|
157
|
+
} else if (isElement(child, W_NS, "txbxContent")) {
|
|
158
|
+
const txbx = { el: child, nested: ctx.txbx !== null };
|
|
159
|
+
if (ctx.ac !== null && ctx.branch !== null && !txbx.nested) {
|
|
160
|
+
(ctx.branch === "choice" ? ctx.ac.choiceTx : ctx.ac.fallbackTx).push(child);
|
|
161
|
+
}
|
|
162
|
+
visit(child, { ...ctx, txbx });
|
|
163
|
+
} else if (child.localName === "t") {
|
|
164
|
+
onText(child, ctx, stack);
|
|
165
|
+
} else if (isElement(child, W_NS, "br")) {
|
|
166
|
+
onBreak(child);
|
|
167
|
+
} else {
|
|
168
|
+
visit(child, ctx);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
stack.pop();
|
|
172
|
+
}
|
|
173
|
+
visit(root, { ac: null, branch: null, txbx: null });
|
|
174
|
+
return acs;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Every AlternateContent's structure, in the same order `scanTemplate`
|
|
179
|
+
* numbers them — for checking the copies of a filled output.
|
|
180
|
+
* @param {import("./xml-scan.js").XmlElement} root
|
|
181
|
+
* @returns {AlternateContentRecord[]}
|
|
182
|
+
*/
|
|
183
|
+
export function collectAlternateContents(root) {
|
|
184
|
+
return walk(root, () => {});
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Every `w:t` of the document, in document order, with its decoded text —
|
|
189
|
+
* body, Choice and Fallback alike.
|
|
190
|
+
* @param {import("./xml-scan.js").XmlElement} root
|
|
191
|
+
* @returns {{el: import("./xml-scan.js").XmlElement, text: string}[]}
|
|
192
|
+
*/
|
|
193
|
+
export function textElements(root) {
|
|
194
|
+
/** @type {{el: import("./xml-scan.js").XmlElement, text: string}[]} */
|
|
195
|
+
const out = [];
|
|
196
|
+
walk(root, (t) => {
|
|
197
|
+
if (t.uri === W_NS) out.push({ el: t, text: elementText(t) });
|
|
198
|
+
});
|
|
199
|
+
return out;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* @typedef {{kind: "t", el: import("./xml-scan.js").XmlElement, text: string} | {kind: "br", el: import("./xml-scan.js").XmlElement}} TextToken
|
|
204
|
+
*/
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Every `w:t` (with its decoded text) and every `w:br` of the document, in
|
|
208
|
+
* document order — body, Choice and Fallback alike. The fill's read-back
|
|
209
|
+
* compares these streams (ADR-0039 Karar 4): a multi-line value is its lines'
|
|
210
|
+
* `w:t`s with a `w:br` between each two, so counting `w:t`s alone would
|
|
211
|
+
* accept an output whose lines were never separated.
|
|
212
|
+
* @param {import("./xml-scan.js").XmlElement} root
|
|
213
|
+
* @returns {TextToken[]}
|
|
214
|
+
*/
|
|
215
|
+
export function textTokens(root) {
|
|
216
|
+
/** @type {TextToken[]} */
|
|
217
|
+
const out = [];
|
|
218
|
+
walk(
|
|
219
|
+
root,
|
|
220
|
+
(t) => {
|
|
221
|
+
if (t.uri === W_NS) out.push({ kind: "t", el: t, text: elementText(t) });
|
|
222
|
+
},
|
|
223
|
+
(br) => {
|
|
224
|
+
out.push({ kind: "br", el: br });
|
|
225
|
+
},
|
|
226
|
+
);
|
|
227
|
+
return out;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Two `w:txbxContent` subtrees compared structurally: namespace + local name,
|
|
232
|
+
* attributes as a set (with values), decoded text and CDATA, child order.
|
|
233
|
+
* Comments count only by position — xml-scan keeps no comment text, and a
|
|
234
|
+
* processing instruction is not a node at all — so two copies whose comments
|
|
235
|
+
* say different things compare equal; neither is rendered. The one exemption is `w:id` on `w:bookmarkStart`/`w:bookmarkEnd`
|
|
236
|
+
* (29 corpus boxes differ only there). Returns the first difference's path,
|
|
237
|
+
* e.g. `w:p[1]/w:r[2]/w:rPr/w:rFonts@w:hAnsi`.
|
|
238
|
+
* @param {import("./xml-scan.js").XmlElement} a
|
|
239
|
+
* @param {import("./xml-scan.js").XmlElement} b
|
|
240
|
+
* @returns {{equal: true} | {equal: false, path: string}}
|
|
241
|
+
*/
|
|
242
|
+
export function compareCopies(a, b) {
|
|
243
|
+
const path = compareElements(a, b, qname(a));
|
|
244
|
+
return path === null ? { equal: true } : { equal: false, path };
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* @param {import("./xml-scan.js").XmlElement} el
|
|
249
|
+
* @param {import("./xml-scan.js").XmlAttribute} attr
|
|
250
|
+
* @returns {boolean}
|
|
251
|
+
*/
|
|
252
|
+
function isExemptAttribute(el, attr) {
|
|
253
|
+
return (
|
|
254
|
+
el.uri === W_NS &&
|
|
255
|
+
(el.localName === "bookmarkStart" || el.localName === "bookmarkEnd") &&
|
|
256
|
+
attr.uri === W_NS &&
|
|
257
|
+
attr.localName === "id"
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* @param {import("./xml-scan.js").XmlElement} a
|
|
263
|
+
* @param {import("./xml-scan.js").XmlElement} b
|
|
264
|
+
* @param {string} path
|
|
265
|
+
* @returns {string | null} null when equal
|
|
266
|
+
*/
|
|
267
|
+
function compareElements(a, b, path) {
|
|
268
|
+
if (a.uri !== b.uri || a.localName !== b.localName) return `${path} (${qname(a)} vs ${qname(b)})`;
|
|
269
|
+
const attrsA = a.attributes.filter((x) => !isExemptAttribute(a, x));
|
|
270
|
+
const attrsB = b.attributes.filter((x) => !isExemptAttribute(b, x));
|
|
271
|
+
for (const x of attrsA) {
|
|
272
|
+
const y = attrsB.find((o) => o.uri === x.uri && o.localName === x.localName);
|
|
273
|
+
const name = x.prefix ? `${x.prefix}:${x.localName}` : x.localName;
|
|
274
|
+
if (y === undefined) return `${path}@${name} (missing in the Fallback copy)`;
|
|
275
|
+
if (y.value !== x.value) return `${path}@${name} (values differ)`;
|
|
276
|
+
}
|
|
277
|
+
for (const y of attrsB) {
|
|
278
|
+
if (!attrsA.some((x) => x.uri === y.uri && x.localName === y.localName)) {
|
|
279
|
+
const name = y.prefix ? `${y.prefix}:${y.localName}` : y.localName;
|
|
280
|
+
return `${path}@${name} (missing in the Choice copy)`;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
if (a.children.length !== b.children.length) return `${path} (child count ${a.children.length} vs ${b.children.length})`;
|
|
284
|
+
/** @type {Map<string, number>} */
|
|
285
|
+
const seen = new Map();
|
|
286
|
+
for (let i = 0; i < a.children.length; i += 1) {
|
|
287
|
+
const ca = a.children[i];
|
|
288
|
+
const cb = b.children[i];
|
|
289
|
+
if (ca.kind !== cb.kind) return `${path}/node()[${i + 1}] (${ca.kind} vs ${cb.kind})`;
|
|
290
|
+
if (ca.kind === "element" && cb.kind === "element") {
|
|
291
|
+
const key = `${ca.uri} ${ca.localName}`;
|
|
292
|
+
const n = (seen.get(key) ?? 0) + 1;
|
|
293
|
+
seen.set(key, n);
|
|
294
|
+
const diff = compareElements(ca, cb, `${path}/${qname(ca)}[${n}]`);
|
|
295
|
+
if (diff !== null) return diff;
|
|
296
|
+
} else if ((ca.kind === "text" || ca.kind === "cdata") && (cb.kind === "text" || cb.kind === "cdata")) {
|
|
297
|
+
if (ca.value !== cb.value) return `${path}/text()[${i + 1}] (text differs)`;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
return null;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* @param {import("./xml-scan.js").XmlElement} t
|
|
305
|
+
* @param {Uint8Array} xmlBytes
|
|
306
|
+
* @returns {{kind: "none"} | {kind: "tag", name: string} | {kind: "problem", code: string, detail: string}}
|
|
307
|
+
*/
|
|
308
|
+
function classifyText(t, xmlBytes) {
|
|
309
|
+
const text = elementText(t);
|
|
310
|
+
if (!text.includes("{") && !text.includes("}")) return { kind: "none" };
|
|
311
|
+
const only = t.children.length === 1 ? t.children[0] : null;
|
|
312
|
+
if (only === null || only.kind !== "text" || only.start !== t.contentStart || only.end !== t.contentEnd) {
|
|
313
|
+
return { kind: "problem", code: "TAG_TEXT_NOT_PLAIN", detail: "its content is not one plain text node (comment, CDATA or processing instruction)" };
|
|
314
|
+
}
|
|
315
|
+
const match = TAG_TEXT.exec(text);
|
|
316
|
+
if (match !== null) {
|
|
317
|
+
if (textDecoder.decode(xmlBytes.subarray(t.contentStart, t.contentEnd)) !== text) {
|
|
318
|
+
return { kind: "problem", code: "TAG_ENCODED", detail: "the tag is written with a character reference; write the braces and name as plain text" };
|
|
319
|
+
}
|
|
320
|
+
return { kind: "tag", name: match[1] };
|
|
321
|
+
}
|
|
322
|
+
if (TAG_ANYWHERE.test(text)) {
|
|
323
|
+
return { kind: "problem", code: "TAG_NOT_ALONE", detail: "a tag must be the whole text of its w:t, with nothing before or after it" };
|
|
324
|
+
}
|
|
325
|
+
if (BRACED.test(text)) {
|
|
326
|
+
return { kind: "problem", code: "INVALID_TAG_NAME", detail: "a tag name is a lowercase ASCII letter followed by lowercase ASCII letters, digits or _" };
|
|
327
|
+
}
|
|
328
|
+
return { kind: "problem", code: "STRAY_BRACE", detail: "a brace that is not part of a whole-w:t tag (a tag split across runs looks like this)" };
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Why a tag `w:t` sits in a context the engine does not fill, or null.
|
|
333
|
+
* @param {import("./xml-scan.js").XmlElement} t
|
|
334
|
+
* @param {WalkContext} ctx
|
|
335
|
+
* @param {import("./xml-scan.js").XmlElement[]} stack stack top = the w:t's parent
|
|
336
|
+
* @returns {{code: string, detail: string} | null}
|
|
337
|
+
*/
|
|
338
|
+
function contextProblem(t, ctx, stack) {
|
|
339
|
+
if (t.uri !== W_NS) {
|
|
340
|
+
return { code: "TAG_CONTEXT_UNSUPPORTED", detail: `a brace in <${qname(t)}> (only w:t is filled)` };
|
|
341
|
+
}
|
|
342
|
+
if (ctx.txbx === null) {
|
|
343
|
+
if (ctx.ac === null) return { code: "TAG_OUTSIDE_TEXT_BOX", detail: "tags are filled only inside text boxes" };
|
|
344
|
+
return { code: "TAG_CONTEXT_UNSUPPORTED", detail: "a w:t inside mc:AlternateContent but outside any w:txbxContent" };
|
|
345
|
+
}
|
|
346
|
+
if (ctx.txbx.nested) return { code: "TAG_CONTEXT_UNSUPPORTED", detail: "a text box nested inside another text box" };
|
|
347
|
+
if (ctx.ac === null || ctx.branch === null) {
|
|
348
|
+
return { code: "TAG_CONTEXT_UNSUPPORTED", detail: "a text box outside mc:AlternateContent (VML-only or a bare w:drawing)" };
|
|
349
|
+
}
|
|
350
|
+
const run = stack[stack.length - 1];
|
|
351
|
+
const paragraph = stack[stack.length - 2];
|
|
352
|
+
const box = stack[stack.length - 3];
|
|
353
|
+
if (!isElement(run, W_NS, "r")) return { code: "TAG_CONTEXT_UNSUPPORTED", detail: `the w:t sits in <${qname(run)}>, not directly in a w:r` };
|
|
354
|
+
if (paragraph === undefined || !isElement(paragraph, W_NS, "p")) {
|
|
355
|
+
return { code: "TAG_CONTEXT_UNSUPPORTED", detail: `the run sits in <${paragraph === undefined ? "?" : qname(paragraph)}>, not directly in a w:p` };
|
|
356
|
+
}
|
|
357
|
+
if (box !== ctx.txbx.el) {
|
|
358
|
+
return { code: "TAG_CONTEXT_UNSUPPORTED", detail: `the paragraph sits in <${box === undefined ? "?" : qname(box)}>, not directly in the w:txbxContent` };
|
|
359
|
+
}
|
|
360
|
+
if (hasField(paragraph)) return { code: "TAG_CONTEXT_UNSUPPORTED", detail: "the paragraph holds a field (w:fldChar / w:instrText)" };
|
|
361
|
+
return null;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* @param {import("./xml-scan.js").XmlElement} el
|
|
366
|
+
* @returns {boolean}
|
|
367
|
+
*/
|
|
368
|
+
function hasField(el) {
|
|
369
|
+
for (const c of el.children) {
|
|
370
|
+
if (c.kind !== "element") continue;
|
|
371
|
+
if (isElement(c, W_NS, "fldChar") || isElement(c, W_NS, "instrText") || isElement(c, W_NS, "fldSimple")) return true;
|
|
372
|
+
if (hasField(c)) return true;
|
|
373
|
+
}
|
|
374
|
+
return false;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* Null when the tag `w:t`'s start tag can be kept or given `xml:space`, else why not.
|
|
379
|
+
* @param {import("./xml-scan.js").XmlElement} t
|
|
380
|
+
* @returns {string | null}
|
|
381
|
+
*/
|
|
382
|
+
function attributeProblem(t) {
|
|
383
|
+
if (t.attributes.length === 0) return null;
|
|
384
|
+
const only = t.attributes.length === 1 ? t.attributes[0] : null;
|
|
385
|
+
if (only !== null && only.uri === XML_NS && only.localName === "space" && only.value === "preserve") return null;
|
|
386
|
+
return `the tag's w:t carries attributes other than xml:space="preserve" (${t.attributes.map((x) => (x.prefix ? `${x.prefix}:` : "") + x.localName).join(", ")})`;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* The run's direct `w:rFonts` sets one of the ascii/hAnsi slots but not the
|
|
391
|
+
* other, or sets them to different fonts (FILL-E1: Turkish letters then draw
|
|
392
|
+
* in another font than the rest of the value).
|
|
393
|
+
* @param {import("./xml-scan.js").XmlElement} run
|
|
394
|
+
* @returns {boolean}
|
|
395
|
+
*/
|
|
396
|
+
function fontSlotsSplit(run) {
|
|
397
|
+
const rPr = findChild(run, W_NS, "rPr");
|
|
398
|
+
const rFonts = rPr && findChild(rPr, W_NS, "rFonts");
|
|
399
|
+
if (!rFonts) return false;
|
|
400
|
+
const ascii = getAttr(rFonts, W_NS, "asciiTheme") ?? getAttr(rFonts, W_NS, "ascii");
|
|
401
|
+
const hAnsi = getAttr(rFonts, W_NS, "hAnsiTheme") ?? getAttr(rFonts, W_NS, "hAnsi");
|
|
402
|
+
if (ascii === undefined && hAnsi === undefined) return false;
|
|
403
|
+
return ascii !== hAnsi;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* @param {import("./xml-scan.js").XmlElement} run
|
|
408
|
+
* @returns {boolean}
|
|
409
|
+
*/
|
|
410
|
+
function isHidden(run) {
|
|
411
|
+
const rPr = findChild(run, W_NS, "rPr");
|
|
412
|
+
const vanish = rPr && findChild(rPr, W_NS, "vanish");
|
|
413
|
+
if (!vanish) return false;
|
|
414
|
+
const val = getAttr(vanish, W_NS, "val");
|
|
415
|
+
return val === undefined || !ON_OFF_FALSE.has(val);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/**
|
|
419
|
+
* @typedef {object} ScanResult
|
|
420
|
+
* @property {TagOccurrence[]} occurrences document order (Choice copy)
|
|
421
|
+
* @property {TemplateProblem[]} problems
|
|
422
|
+
* @property {TemplateProblem[]} warnings
|
|
423
|
+
* @property {AlternateContentRecord[]} acs
|
|
424
|
+
*/
|
|
425
|
+
|
|
426
|
+
/**
|
|
427
|
+
* @param {import("./xml-scan.js").XmlElement} root scanXml(xmlBytes).root
|
|
428
|
+
* @param {Uint8Array} xmlBytes
|
|
429
|
+
* @returns {ScanResult}
|
|
430
|
+
*/
|
|
431
|
+
export function scanTemplate(root, xmlBytes) {
|
|
432
|
+
const { page, sectPrStarts } = readSections(root);
|
|
433
|
+
/** @type {{t: import("./xml-scan.js").XmlElement, code: string, detail: string, acIndex: number | null, tag?: string}[]} */
|
|
434
|
+
const pending = [];
|
|
435
|
+
/** @type {Map<number, {choice: TagSite[], fallback: TagSite[], braces: boolean}>} */
|
|
436
|
+
const byAc = new Map();
|
|
437
|
+
/** @param {number} index */
|
|
438
|
+
const acEntry = (index) => {
|
|
439
|
+
let entry = byAc.get(index);
|
|
440
|
+
if (entry === undefined) {
|
|
441
|
+
entry = { choice: [], fallback: [], braces: false };
|
|
442
|
+
byAc.set(index, entry);
|
|
443
|
+
}
|
|
444
|
+
return entry;
|
|
445
|
+
};
|
|
446
|
+
|
|
447
|
+
const acs = walk(root, (t, ctx, stack) => {
|
|
448
|
+
const verdict = classifyText(t, xmlBytes);
|
|
449
|
+
if (verdict.kind === "none") return;
|
|
450
|
+
const acIndex = ctx.ac === null ? null : ctx.ac.index;
|
|
451
|
+
if (acIndex !== null) acEntry(acIndex).braces = true;
|
|
452
|
+
const reportHere = ctx.branch !== "fallback";
|
|
453
|
+
if (verdict.kind === "problem") {
|
|
454
|
+
if (reportHere) pending.push({ t, code: verdict.code, detail: verdict.detail, acIndex });
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
457
|
+
const where = contextProblem(t, ctx, stack);
|
|
458
|
+
if (where !== null) {
|
|
459
|
+
if (reportHere) pending.push({ t, code: where.code, detail: where.detail, acIndex, tag: verdict.name });
|
|
460
|
+
return;
|
|
461
|
+
}
|
|
462
|
+
const attrs = attributeProblem(t);
|
|
463
|
+
if (attrs !== null) {
|
|
464
|
+
if (reportHere) pending.push({ t, code: "TAG_TEXT_ATTRIBUTES", detail: attrs, acIndex, tag: verdict.name });
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
467
|
+
const site = { name: verdict.name, t, run: stack[stack.length - 1], paragraph: stack[stack.length - 2] };
|
|
468
|
+
const entry = acEntry(/** @type {number} */ (acIndex));
|
|
469
|
+
(ctx.branch === "choice" ? entry.choice : entry.fallback).push(site);
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
/** @type {Map<number, {docPrId: number | null, page: number}>} */
|
|
473
|
+
const attribution = new Map();
|
|
474
|
+
/** @type {TemplateProblem[]} */
|
|
475
|
+
const acProblems = [];
|
|
476
|
+
/** @type {TemplateProblem[]} */
|
|
477
|
+
const warnings = [];
|
|
478
|
+
/** @type {TagOccurrence[]} */
|
|
479
|
+
const occurrences = [];
|
|
480
|
+
|
|
481
|
+
for (const [index, entry] of [...byAc.entries()].sort((x, y) => x[0] - y[0])) {
|
|
482
|
+
const ac = acs[index];
|
|
483
|
+
const anchor = ac.choiceAnchors.length === 1 ? ac.choiceAnchors[0] : null;
|
|
484
|
+
const shapeIssues = [];
|
|
485
|
+
if (ac.nested) shapeIssues.push("it is nested inside another mc:AlternateContent");
|
|
486
|
+
if (ac.choices.length !== 1) shapeIssues.push(`${ac.choices.length} mc:Choice`);
|
|
487
|
+
if (ac.fallbacks.length !== 1) shapeIssues.push(`${ac.fallbacks.length} mc:Fallback`);
|
|
488
|
+
if (ac.choiceTx.length !== 1) shapeIssues.push(`${ac.choiceTx.length} w:txbxContent in the Choice`);
|
|
489
|
+
if (ac.fallbackTx.length !== 1) shapeIssues.push(`${ac.fallbackTx.length} w:txbxContent in the Fallback`);
|
|
490
|
+
if (anchor === null) shapeIssues.push(`${ac.choiceAnchors.length} wp:anchor/wp:inline in the Choice`);
|
|
491
|
+
else if (!isElement(anchor, WP_NS, "anchor")) shapeIssues.push("the Choice holds wp:inline, not wp:anchor");
|
|
492
|
+
|
|
493
|
+
/** @type {number | null} */
|
|
494
|
+
let docPrId = null;
|
|
495
|
+
let pageNumber = pageForAnchor(sectPrStarts, ac.el.start, page.count);
|
|
496
|
+
if (anchor !== null) {
|
|
497
|
+
pageNumber = pageForAnchor(sectPrStarts, anchor.start, page.count);
|
|
498
|
+
const docPr = findChild(anchor, WP_NS, "docPr");
|
|
499
|
+
const id = docPr ? getAttr(docPr, "", "id") : undefined;
|
|
500
|
+
if (id !== undefined && /^[0-9]+$/.test(id)) docPrId = Number(id);
|
|
501
|
+
}
|
|
502
|
+
attribution.set(index, { docPrId, page: pageNumber });
|
|
503
|
+
|
|
504
|
+
if (shapeIssues.length > 0) {
|
|
505
|
+
acProblems.push({
|
|
506
|
+
code: "ALTERNATE_CONTENT_SHAPE",
|
|
507
|
+
message: `the text box holding a tag does not have the shape the engine fills: ${shapeIssues.join("; ")}`,
|
|
508
|
+
docPrId,
|
|
509
|
+
page: pageNumber,
|
|
510
|
+
});
|
|
511
|
+
continue;
|
|
512
|
+
}
|
|
513
|
+
const copies = compareCopies(ac.choiceTx[0], ac.fallbackTx[0]);
|
|
514
|
+
if (!copies.equal) {
|
|
515
|
+
acProblems.push({
|
|
516
|
+
code: "COPIES_DIFFER",
|
|
517
|
+
message: `the box's mc:Choice and mc:Fallback copies differ at ${copies.path}`,
|
|
518
|
+
docPrId,
|
|
519
|
+
page: pageNumber,
|
|
520
|
+
path: copies.path,
|
|
521
|
+
});
|
|
522
|
+
continue;
|
|
523
|
+
}
|
|
524
|
+
// Structurally equal trees can still spell a tag differently in raw bytes
|
|
525
|
+
// (a character reference in one copy) — compare the twin lists themselves.
|
|
526
|
+
const twinsMatch =
|
|
527
|
+
entry.choice.length === entry.fallback.length && entry.choice.every((site, i) => site.name === entry.fallback[i].name);
|
|
528
|
+
if (!twinsMatch) {
|
|
529
|
+
acProblems.push({
|
|
530
|
+
code: "COPIES_DIFFER",
|
|
531
|
+
message: "the box's mc:Choice and mc:Fallback copies do not hold the same tags",
|
|
532
|
+
docPrId,
|
|
533
|
+
page: pageNumber,
|
|
534
|
+
});
|
|
535
|
+
continue;
|
|
536
|
+
}
|
|
537
|
+
if (anchor !== null && docPrId === null) {
|
|
538
|
+
warnings.push({ code: "DOCPR_MISSING", message: "a text box holding a tag has no numeric wp:docPr id", page: pageNumber });
|
|
539
|
+
}
|
|
540
|
+
entry.choice.forEach((site, i) => {
|
|
541
|
+
occurrences.push({ name: site.name, docPrId, page: pageNumber, acIndex: index, choice: site, fallback: entry.fallback[i] });
|
|
542
|
+
});
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
/** @type {TemplateProblem[]} */
|
|
546
|
+
const problems = pending.map((p) => {
|
|
547
|
+
const where = p.acIndex !== null && attribution.has(p.acIndex)
|
|
548
|
+
? /** @type {{docPrId: number | null, page: number}} */ (attribution.get(p.acIndex))
|
|
549
|
+
: { docPrId: null, page: pageForAnchor(sectPrStarts, p.t.start, page.count) };
|
|
550
|
+
/** @type {TemplateProblem} */
|
|
551
|
+
const problem = { code: p.code, message: p.detail, docPrId: where.docPrId, page: where.page };
|
|
552
|
+
if (p.tag !== undefined) problem.tag = p.tag;
|
|
553
|
+
return problem;
|
|
554
|
+
});
|
|
555
|
+
problems.push(...acProblems);
|
|
556
|
+
|
|
557
|
+
/** @type {Map<number, number>} */
|
|
558
|
+
const docPrUse = new Map();
|
|
559
|
+
for (const index of new Set(occurrences.map((o) => o.acIndex))) {
|
|
560
|
+
const id = /** @type {{docPrId: number | null}} */ (attribution.get(index)).docPrId;
|
|
561
|
+
if (id !== null) docPrUse.set(id, (docPrUse.get(id) ?? 0) + 1);
|
|
562
|
+
}
|
|
563
|
+
for (const [id, n] of docPrUse) {
|
|
564
|
+
if (n > 1) warnings.push({ code: "DUPLICATE_DOCPR_ID", message: `${n} text boxes holding tags share wp:docPr id ${id}`, docPrId: id });
|
|
565
|
+
}
|
|
566
|
+
for (const o of occurrences) {
|
|
567
|
+
if (fontSlotsSplit(o.choice.run)) {
|
|
568
|
+
warnings.push({
|
|
569
|
+
code: "FONT_SLOTS_SPLIT",
|
|
570
|
+
message: "the tag's run sets the ascii and hAnsi font slots differently, so Turkish letters may draw in another font",
|
|
571
|
+
tag: o.name,
|
|
572
|
+
docPrId: o.docPrId,
|
|
573
|
+
page: o.page,
|
|
574
|
+
});
|
|
575
|
+
}
|
|
576
|
+
if (isHidden(o.choice.run)) {
|
|
577
|
+
warnings.push({ code: "HIDDEN_TAG_RUN", message: "the tag's run is hidden (w:vanish)", tag: o.name, docPrId: o.docPrId, page: o.page });
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
return { occurrences, problems, warnings, acs };
|
|
582
|
+
}
|