@docx4j/core-ts 0.4.0 → 0.5.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 (49) hide show
  1. package/README.md +5 -2
  2. package/dist/html/convert.mjs +38 -14
  3. package/dist/model/content/Body.d.mts +13 -1
  4. package/dist/model/content/Body.mjs +90 -19
  5. package/dist/model/content/Comment.mjs +7 -0
  6. package/dist/model/content/InlinePicture.d.mts +71 -7
  7. package/dist/model/content/InlinePicture.mjs +201 -28
  8. package/dist/model/content/List.mjs +10 -1
  9. package/dist/model/content/Paragraph.d.mts +28 -5
  10. package/dist/model/content/Paragraph.mjs +77 -37
  11. package/dist/model/content/Range.d.mts +18 -2
  12. package/dist/model/content/Range.mjs +194 -64
  13. package/dist/model/content/TrackedChange.d.mts +129 -3
  14. package/dist/model/content/TrackedChange.mjs +577 -12
  15. package/dist/model/content/binder.d.mts +2 -1
  16. package/dist/model/content/binder.mjs +48 -1
  17. package/dist/model/content/index.d.mts +2 -2
  18. package/dist/model/content/index.mjs +1 -1
  19. package/dist/model/content/relationships.d.mts +30 -0
  20. package/dist/model/content/relationships.mjs +133 -0
  21. package/dist/model/content/tree.d.mts +52 -2
  22. package/dist/model/content/tree.mjs +48 -3
  23. package/dist/model/customxml/pictures.d.mts +5 -2
  24. package/dist/model/customxml/pictures.mjs +13 -7
  25. package/dist/model/fonts/families.generated.mjs +2 -2
  26. package/dist/model/fonts/registry.mjs +7 -1
  27. package/dist/model/fonts/substitutions.generated.mjs +2 -2
  28. package/dist/model/fonts/themes.generated.mjs +1 -1
  29. package/dist/model/listnumbering/definitions.mjs +18 -0
  30. package/dist/model/properties/PropertyResolver.d.mts +27 -6
  31. package/dist/model/properties/PropertyResolver.mjs +83 -16
  32. package/dist/model/properties/catalogue.d.mts +15 -1
  33. package/dist/model/properties/catalogue.mjs +80 -27
  34. package/dist/model/properties/table.d.mts +16 -2
  35. package/dist/model/properties/table.mjs +13 -3
  36. package/dist/opc/Load.d.mts +9 -2
  37. package/dist/opc/index.d.mts +2 -2
  38. package/dist/opc/index.mjs +2 -2
  39. package/dist/opc/mce/preprocessor.d.mts +62 -6
  40. package/dist/opc/mce/preprocessor.mjs +142 -35
  41. package/dist/opc/mce/understood.d.mts +10 -0
  42. package/dist/opc/mce/understood.mjs +25 -3
  43. package/dist/packages/OpcPackage.d.mts +7 -0
  44. package/dist/packages/OpcPackage.mjs +15 -3
  45. package/dist/packages/WordprocessingMLPackage.d.mts +35 -2
  46. package/dist/packages/WordprocessingMLPackage.mjs +98 -2
  47. package/dist/parts/XmlPart.d.mts +26 -5
  48. package/dist/parts/XmlPart.mjs +67 -18
  49. package/package.json +2 -2
@@ -7,6 +7,7 @@ import { rPrFromElements } from '@docx4j/generated-objects-ts/builders/wml';
7
7
  import { revisionDateOf, toRestoredText, toDeletedText, restoreRPr, restorePPr, pruneParagraphProperties, HOLDER_TYPES, holderItemsOf } from './tracking.mjs';
8
8
  import { tableRecordsOf, hasTableRecords, tableChangeText, rowText as tableRowText, acceptTable, rejectTable } from './tableRevisions.mjs';
9
9
  import { deepCopy } from '@docx4j/generated-objects-ts';
10
+ import { relationshipIdsIn, dropRelationshipsUnused, queueRelationshipRemovals } from './relationships.mjs';
10
11
  /**
11
12
  * A subset of Office JS `Word.TrackedChange`: `type`, `author`, `date`, `text`, `accept()`,
12
13
  * `reject()` and `getRange()`, plus `element`, `id` and `target` as extensions. A view, like
@@ -15,9 +16,12 @@ import { deepCopy } from '@docx4j/generated-objects-ts';
15
16
  export class TrackedChange {
16
17
  constructor(target,
17
18
  /** The paragraph the change is in; undefined for a row revision. */
18
- paragraph) {
19
+ paragraph,
20
+ /** The body a change without a paragraph (a row, a cell) is in, for the notes it may take away. */
21
+ body) {
19
22
  this.target = target;
20
23
  this.paragraph = paragraph;
24
+ this.body = body;
21
25
  }
22
26
  /** Office JS `Word.ChangeTrackingState`. */
23
27
  get type() {
@@ -29,6 +33,9 @@ export class TrackedChange {
29
33
  case 'group': return t.revision === 'ins' ? 'Added' : t.revision === 'del' ? 'Deleted' : 'Formatted';
30
34
  case 'cell': return t.cell === 'ins' ? 'Added' : 'Deleted';
31
35
  case 'holder': return 'Added';
36
+ case 'field': return 'Added';
37
+ case 'mathRun':
38
+ case 'mathStructure': return t.revision === 'ins' ? 'Added' : 'Deleted';
32
39
  default: return 'Formatted';
33
40
  }
34
41
  }
@@ -49,6 +56,7 @@ export class TrackedChange {
49
56
  }
50
57
  /** The markup this change is over (an extension): the `w:ins` element, the `w:rPrChange`, ... */
51
58
  get element() {
59
+ var _a;
52
60
  const t = this.target;
53
61
  switch (t.kind) {
54
62
  case 'run': return t.element;
@@ -58,12 +66,15 @@ export class TrackedChange {
58
66
  case 'cell': return t.tc;
59
67
  case 'sectionProperties': return t.sectPr;
60
68
  case 'holder': return t.holder;
69
+ case 'field': return t.span[0];
70
+ case 'mathRun': return t.item;
71
+ case 'mathStructure': return (_a = t.structure) !== null && _a !== void 0 ? _a : t.ctrlPr;
61
72
  default: return t.value;
62
73
  }
63
74
  }
64
75
  /** The text the change covers; `\r` for a paragraph mark, as Office JS gives it (check 22). */
65
76
  get text() {
66
- var _a, _b;
77
+ var _a, _b, _c;
67
78
  const t = this.target;
68
79
  switch (t.kind) {
69
80
  case 'mark': return '\r';
@@ -74,10 +85,14 @@ export class TrackedChange {
74
85
  case 'sectionProperties': return t.text;
75
86
  case 'run': return segmentsOf(t.element.value, { view: t.revision === 'ins' || t.revision === 'moveTo' ? 'accepted' : 'original' }).map((s) => s.text).join('');
76
87
  case 'runProperties': return textOf(t.run);
88
+ // the field's result: its instruction is no text, as Office JS reports it (check 30: "1")
89
+ case 'field': return segmentsOf({ content: t.span }, { view: 'accepted' }).map((s) => s.text).join('');
77
90
  case 'paragraphProperties': return (_b = (_a = this.paragraph) === null || _a === void 0 ? void 0 : _a.text) !== null && _b !== void 0 ? _b : '';
78
91
  // a deleted row's content is `w:delText` inside `w:del`, which the accepted view skips:
79
92
  // its text is what the row said before, as for a run deletion
80
93
  case 'row': return rowText(t.tr.value, t.row === 'del' ? 'original' : 'accepted');
94
+ // an equation's text: its m:t, as docx4j gives it; a structure's own marker covers no text
95
+ case 'mathRun': return ((_c = t.value.rest) !== null && _c !== void 0 ? _c : []).map((item) => { var _a; return (_a = mathTextOf(item)) !== null && _a !== void 0 ? _a : ''; }).join('');
81
96
  default: return '';
82
97
  }
83
98
  }
@@ -93,17 +108,26 @@ export class TrackedChange {
93
108
  }
94
109
  if (t.kind === 'group') {
95
110
  // a range is one paragraph's: the group's text in the paragraph it starts in
96
- const here = t.pieces.filter((piece) => { var _a; return (piece.target.kind === 'run' || piece.target.kind === 'runProperties') && ((_a = piece.paragraph) === null || _a === void 0 ? void 0 : _a.element) === p.element; }).map((piece) => piece.getRange());
111
+ const here = t.pieces.filter((piece) => { var _a; return (piece.target.kind === 'run' || piece.target.kind === 'runProperties' || piece.target.kind === 'field') && ((_a = piece.paragraph) === null || _a === void 0 ? void 0 : _a.element) === p.element; }).map((piece) => piece.getRange());
97
112
  if (here.length === 0)
98
113
  return t.pieces[0].getRange();
99
114
  return new Range(p, Math.min(...here.map((r) => r.start)), Math.max(...here.map((r) => r.end)));
100
115
  }
116
+ if (t.kind === 'field') {
117
+ const start = acceptedOffsetOf(p.p, t.span[0]);
118
+ return new Range(p, start, start + this.text.length);
119
+ }
101
120
  if (t.kind === 'runProperties') {
102
121
  const segs = p.segments().filter((s) => s.run === t.run);
103
122
  if (segs.length === 0)
104
123
  return p.getRange('Start');
105
124
  return new Range(p, segs[0].start, segs[segs.length - 1].end);
106
125
  }
126
+ // an equation's text is not in the paragraph's: collapsed where the equation stands
127
+ if ((t.kind === 'mathRun' || t.kind === 'mathStructure') && t.equation) {
128
+ const at = acceptedOffsetOf(p.p, t.equation);
129
+ return new Range(p, at, at);
130
+ }
107
131
  return p.getRange('Whole');
108
132
  }
109
133
  /**
@@ -111,6 +135,9 @@ export class TrackedChange {
111
135
  * Either half of a move keeps the whole move, as Word's Review tab does (CR-002 section 29).
112
136
  */
113
137
  accept() {
138
+ resolving(this.package_, this.body_, () => this.acceptHere());
139
+ }
140
+ acceptHere() {
114
141
  var _a, _b, _c, _d, _e, _f;
115
142
  const t = this.target;
116
143
  switch (t.kind) {
@@ -174,6 +201,18 @@ export class TrackedChange {
174
201
  // the element stays, the markers go (check 30: accept all leaves the control, no marker)
175
202
  removeHolderMarkers(t);
176
203
  return;
204
+ case 'field':
205
+ for (const el of t.span) {
206
+ if (revisionKindOf(el) === 'ins')
207
+ unwrap({ kind: 'run', revision: 'ins', element: el, owner: currentOwner(t.owner, el), value: el.value });
208
+ }
209
+ return;
210
+ case 'mathRun':
211
+ resolveMathRun(t, t.revision === 'ins');
212
+ return;
213
+ case 'mathStructure':
214
+ resolveMathStructure(t, t.revision === 'ins');
215
+ return;
177
216
  }
178
217
  }
179
218
  /**
@@ -181,6 +220,9 @@ export class TrackedChange {
181
220
  * Either half of a move rejects the whole move, as Word's Review tab does (CR-002 section 29).
182
221
  */
183
222
  reject() {
223
+ resolving(this.package_, this.body_, () => this.rejectHere());
224
+ }
225
+ rejectHere() {
184
226
  var _a, _b;
185
227
  const t = this.target;
186
228
  switch (t.kind) {
@@ -243,8 +285,27 @@ export class TrackedChange {
243
285
  // control, and an existing control, which has no markers, stays)
244
286
  unwrapHolder(t);
245
287
  return;
288
+ case 'field':
289
+ removeField(t, this.paragraph);
290
+ return;
291
+ case 'mathRun':
292
+ resolveMathRun(t, t.revision === 'del');
293
+ return;
294
+ case 'mathStructure':
295
+ resolveMathStructure(t, t.revision === 'del');
296
+ return;
246
297
  }
247
298
  }
299
+ /** The body the change is in. */
300
+ get body_() {
301
+ var _a, _b;
302
+ return (_b = (_a = this.paragraph) === null || _a === void 0 ? void 0 : _a.parentBody) !== null && _b !== void 0 ? _b : this.body;
303
+ }
304
+ /** The package the change is in, which keeps the notes whose references it took away. */
305
+ get package_() {
306
+ var _a;
307
+ return (_a = this.body_) === null || _a === void 0 ? void 0 : _a.package_;
308
+ }
248
309
  requireParagraph() {
249
310
  if (!this.paragraph)
250
311
  throw new Docx4JException('This tracked change has no paragraph');
@@ -375,8 +436,287 @@ function currentOwner(owner, element) {
375
436
  function remove(owner, element) {
376
437
  const list = currentOwner(owner, element);
377
438
  const i = list.indexOf(element);
378
- if (i >= 0)
379
- list.splice(i, 1);
439
+ if (i < 0)
440
+ return;
441
+ discarding(element);
442
+ list.splice(i, 1);
443
+ }
444
+ let resolution;
445
+ /**
446
+ * Runs `resolve`, then takes away what went with the content it removed:
447
+ * - **Notes**: Word took a footnote from the notes part when its reference went with the content
448
+ * around it, accepting a deletion and rejecting an insertion alike (the editor's `review-for-word`
449
+ * saves, Word 15 and Word 2010); the entries go when the package is saved
450
+ * (`WordprocessingMLPackage.pendingNoteRemovals`).
451
+ * - **Bookmarks**: Word removed a bookmark whole when its end went with a deletion it accepted, and
452
+ * when the insertion it was put around went with a reject (docx4j CR-043's probe, C11 and C11b, Word
453
+ * 16.0.20326). docx4j's rule from the two (its CR-043 section 3.2) is followed here: a bookmark with a
454
+ * marker in removed content goes whole, its other marker too, even with content left beside it; so
455
+ * does one with content between its markers that is left with none, a deletion it was around accepted
456
+ * among them (U15); and one that was empty before, as `_GoBack` is, stays. Those three are not measured.
457
+ * - **Relationships and parts**: Word's saves kept no part the resolved content alone referenced. A
458
+ * picture deleted and accepted, or inserted and rejected, took its image part (docx4j CR-043's probe,
459
+ * C05 and C04), and a section break deleted and accepted took the headers and footers of the section
460
+ * it ended (C06: six of each became three). docx4j's rule (its CR-043 section 3.2, D7) is followed: a
461
+ * relationship of the change's part that the removed content referenced, through any `r:` attribute,
462
+ * goes when nothing left in the part references it; its part goes when no relationship in the package
463
+ * targets it any more, and what that part alone related to goes with it. The kinds beyond pictures,
464
+ * headers and footers (charts, OLE objects, `v:imagedata`) are docx4j's reading, not measured.
465
+ * A change resolved inside another's (a row's cells, a group's pieces, `acceptAll`'s changes) shares the
466
+ * outer resolution, so the bookmarks are read twice for the lot, and not at all when nothing is removed.
467
+ */
468
+ export function resolving(pkg, body, resolve) {
469
+ var _a, _b, _c;
470
+ const notes = pkg === null || pkg === void 0 ? void 0 : pkg.pendingNoteRemovals;
471
+ const root = storyRootOf(body);
472
+ const outer = resolution;
473
+ if (outer) {
474
+ (_a = outer.root) !== null && _a !== void 0 ? _a : (outer.root = root);
475
+ (_b = outer.part) !== null && _b !== void 0 ? _b : (outer.part = body === null || body === void 0 ? void 0 : body.part);
476
+ const previous = outer.notes;
477
+ outer.notes = notes !== null && notes !== void 0 ? notes : previous;
478
+ try {
479
+ resolve();
480
+ }
481
+ finally {
482
+ outer.notes = previous;
483
+ }
484
+ return;
485
+ }
486
+ resolution = { notes, root, part: body === null || body === void 0 ? void 0 : body.part };
487
+ try {
488
+ resolve();
489
+ const done = resolution;
490
+ if (done.root && done.before)
491
+ dropBookmarksBroken(done.root, done.before);
492
+ if (done.part && ((_c = done.relationships) === null || _c === void 0 ? void 0 : _c.size))
493
+ dropRelationshipsUnused(done.part, done.relationships);
494
+ }
495
+ finally {
496
+ resolution = undefined;
497
+ }
498
+ }
499
+ /** The tree a body's bookmarks are read in: its part's, so a marker in another cell or paragraph is found. */
500
+ function storyRootOf(body) {
501
+ var _a;
502
+ if (!body)
503
+ return undefined;
504
+ return ((_a = body.part) === null || _a === void 0 ? void 0 : _a.isUnmarshalled) ? body.part.contents : body.container;
505
+ }
506
+ /**
507
+ * Called before content is removed (or a paragraph mark joins two): records its notes, the relationships
508
+ * it references, and the bookmarks as they stand. `properties` is a paragraph's `w:pPr` that a join drops,
509
+ * whose `w:sectPr` references its section's headers and footers.
510
+ */
511
+ function discarding(element, properties) {
512
+ var _a, _b;
513
+ if (!resolution)
514
+ return;
515
+ if (resolution.root)
516
+ (_a = resolution.before) !== null && _a !== void 0 ? _a : (resolution.before = bookmarksIn(resolution.root));
517
+ if (element && resolution.notes)
518
+ noteReferencesIn(element, resolution.notes);
519
+ if (resolution.part && (element || properties)) {
520
+ const into = ((_b = resolution.relationships) !== null && _b !== void 0 ? _b : (resolution.relationships = new Set()));
521
+ if (element)
522
+ relationshipIdsIn(element, into);
523
+ if (properties)
524
+ relationshipIdsIn(properties, into);
525
+ }
526
+ }
527
+ const MATH_NS = 'http://schemas.openxmlformats.org/officeDocument/2006/math';
528
+ /** Run content that is something in the text: a character, a drawing, a field character, a note's reference. */
529
+ const FILLS = new Set(['tab', 'br', 'cr', 'noBreakHyphen', 'softHyphen', 'sym', 'ptab', 'drawing', 'pict', 'object',
530
+ 'fldChar', 'footnoteReference', 'endnoteReference', 'commentReference', 'footnoteRef', 'endnoteRef', 'annotationRef',
531
+ 'separator', 'continuationSeparator', 'pgNum', 'dayShort', 'monthShort', 'yearShort', 'dayLong', 'monthLong', 'yearLong',
532
+ 'contentPart', 'ruby']);
533
+ /** Of those, the ones that can hold a story of their own (a text box), and so bookmarks: walked into. */
534
+ const HOLDS_CONTENT = new Set(['drawing', 'pict', 'object', 'ruby', 'contentPart']);
535
+ const TEXTS = new Set(['t', 'delText', 'instrText', 'delInstrText']);
536
+ /** Properties, which hold no bookmark and no content: not walked. */
537
+ const PROPERTIES = new Set(['PARENT', 'TYPE_NAME', 'name', 'otherAttributes', 'pPr', 'rPr', 'tblPr', 'tblPrEx', 'tblGrid', 'trPr', 'tcPr', 'sectPr', 'sdtPr', 'sdtEndPr']);
538
+ /**
539
+ * Every bookmark in a tree, by id, in document order. Deleted and inserted content both count as
540
+ * content, and so does a paragraph's mark, so a bookmark over a paragraph boundary is not empty.
541
+ */
542
+ function bookmarksIn(root) {
543
+ const marks = new Map();
544
+ let open = [];
545
+ const fill = () => { for (const state of open)
546
+ state.filled = true; open = []; };
547
+ const visit = (o) => {
548
+ var _a, _b, _c, _d;
549
+ if (typeof o !== 'object' || o === null)
550
+ return;
551
+ if (Array.isArray(o)) {
552
+ for (const item of o)
553
+ visit(item);
554
+ return;
555
+ }
556
+ const el = o;
557
+ const local = (_a = el.name) === null || _a === void 0 ? void 0 : _a.localPart;
558
+ if (local === 'bookmarkStart' || local === 'bookmarkEnd') {
559
+ const id = String((_b = el.value) === null || _b === void 0 ? void 0 : _b.id);
560
+ let state = marks.get(id);
561
+ if (!state)
562
+ marks.set(id, state = { start: false, end: false, filled: false });
563
+ if (local === 'bookmarkStart') {
564
+ state.start = true;
565
+ open.push(state);
566
+ }
567
+ else {
568
+ state.end = true;
569
+ open = open.filter((other) => other !== state);
570
+ }
571
+ return;
572
+ }
573
+ if (local !== undefined && (el.name.namespaceURI === W_NS || el.name.namespaceURI === MATH_NS)) {
574
+ if (TEXTS.has(local)) {
575
+ if (String((_d = (_c = el.value) === null || _c === void 0 ? void 0 : _c.value) !== null && _d !== void 0 ? _d : '') !== '')
576
+ fill();
577
+ return;
578
+ }
579
+ if (FILLS.has(local)) {
580
+ fill();
581
+ if (!HOLDS_CONTENT.has(local))
582
+ return;
583
+ }
584
+ }
585
+ for (const key in o)
586
+ if (!PROPERTIES.has(key))
587
+ visit(o[key]);
588
+ if (o.TYPE_NAME === 'org_docx4j_wml.P')
589
+ fill();
590
+ };
591
+ visit(root);
592
+ return marks;
593
+ }
594
+ /**
595
+ * The bookmarks whole before and broken now - a marker gone, or emptied - taken away, both markers.
596
+ * One broken already, or gone whole with the content it was in, is left as it is.
597
+ */
598
+ function dropBookmarksBroken(root, before) {
599
+ if (before.size === 0)
600
+ return;
601
+ const after = bookmarksIn(root);
602
+ const drop = new Set();
603
+ for (const [id, was] of before) {
604
+ if (!was.start || !was.end)
605
+ continue;
606
+ const now = after.get(id);
607
+ if (!now)
608
+ continue;
609
+ if (!now.start || !now.end || (was.filled && !now.filled))
610
+ drop.add(id);
611
+ }
612
+ if (drop.size === 0)
613
+ return;
614
+ const visit = (o) => {
615
+ var _a, _b;
616
+ if (typeof o !== 'object' || o === null)
617
+ return;
618
+ if (Array.isArray(o)) {
619
+ for (let i = o.length - 1; i >= 0; i--) {
620
+ const el = o[i];
621
+ const local = (_a = el === null || el === void 0 ? void 0 : el.name) === null || _a === void 0 ? void 0 : _a.localPart;
622
+ if ((local === 'bookmarkStart' || local === 'bookmarkEnd') && drop.has(String((_b = el.value) === null || _b === void 0 ? void 0 : _b.id)))
623
+ o.splice(i, 1);
624
+ else
625
+ visit(el);
626
+ }
627
+ return;
628
+ }
629
+ for (const key in o)
630
+ if (!PROPERTIES.has(key))
631
+ visit(o[key]);
632
+ };
633
+ visit(root);
634
+ }
635
+ /** Adds the ids of the `w:footnoteReference` and `w:endnoteReference` elements anywhere in a tree to `into`. */
636
+ function noteReferencesIn(tree, into) {
637
+ const visit = (o) => {
638
+ var _a, _b;
639
+ if (typeof o !== 'object' || o === null)
640
+ return;
641
+ if (Array.isArray(o)) {
642
+ for (const item of o)
643
+ visit(item);
644
+ return;
645
+ }
646
+ const local = (_a = o.name) === null || _a === void 0 ? void 0 : _a.localPart;
647
+ if (local === 'footnoteReference' || local === 'endnoteReference') {
648
+ const id = Number((_b = (o.value)) === null || _b === void 0 ? void 0 : _b.id);
649
+ if (Number.isInteger(id))
650
+ into[local === 'footnoteReference' ? 'footnote' : 'endnote'].add(id);
651
+ return;
652
+ }
653
+ for (const [key, value] of Object.entries(o))
654
+ if (key !== 'PARENT')
655
+ visit(value);
656
+ };
657
+ visit(tree);
658
+ }
659
+ /**
660
+ * The notes `pending` holds taken out of the notes parts, each unless a reference to it still stands in
661
+ * the main document (an accepted move keeps the reference its source had, say); then `pending` is
662
+ * empty. A note's entry goes, Word's separators never: their ids are never referenced. The notes parts
663
+ * are read only when something is pending, so a save with none leaves them untouched. Word's own save
664
+ * also renumbers the notes left (the editor's `review-for-word` saves); their ids stay here.
665
+ *
666
+ * What a removed note held goes on with it, as docx4j's `Removals.flush` takes it (CR-002 section 50,
667
+ * docx4j's reading, not measured): the relationships its content referenced (a picture's image) are
668
+ * queued on `host` for the notes part, to go when nothing else uses them, and a comment whose reference
669
+ * was in it is queued for removal, its anchor being gone.
670
+ */
671
+ export async function flushPendingNoteRemovals(main, pending, host) {
672
+ if (pending.footnote.size === 0 && pending.endnote.size === 0)
673
+ return;
674
+ const standing = { footnote: new Set(), endnote: new Set() };
675
+ noteReferencesIn(await main.getContents(), standing);
676
+ const prune = (part, notes, ids, kept) => {
677
+ if (!notes)
678
+ return;
679
+ const released = new Set();
680
+ for (let i = notes.length - 1; i >= 0; i--) {
681
+ const note = notes[i];
682
+ if ((note.type === undefined || note.type === 'normal') && ids.has(Number(note.id)) && !kept.has(Number(note.id))) {
683
+ relationshipIdsIn(note.content, released);
684
+ if (host === null || host === void 0 ? void 0 : host.pendingCommentRemovals)
685
+ commentReferencesIn(note.content, host.pendingCommentRemovals);
686
+ notes.splice(i, 1);
687
+ }
688
+ }
689
+ queueRelationshipRemovals(host === null || host === void 0 ? void 0 : host.pendingRelationshipRemovals, part, released);
690
+ };
691
+ if (pending.footnote.size > 0 && main.footnotesPart)
692
+ prune(main.footnotesPart, (await main.footnotesPart.getContents()).footnote, pending.footnote, standing.footnote);
693
+ if (pending.endnote.size > 0 && main.endnotesPart)
694
+ prune(main.endnotesPart, (await main.endnotesPart.getContents()).endnote, pending.endnote, standing.endnote);
695
+ pending.footnote.clear();
696
+ pending.endnote.clear();
697
+ }
698
+ /** Adds the ids of the `w:commentReference` elements anywhere in a tree to `into`. */
699
+ function commentReferencesIn(tree, into) {
700
+ const visit = (o) => {
701
+ var _a, _b;
702
+ if (typeof o !== 'object' || o === null)
703
+ return;
704
+ if (Array.isArray(o)) {
705
+ for (const item of o)
706
+ visit(item);
707
+ return;
708
+ }
709
+ if (((_a = o.name) === null || _a === void 0 ? void 0 : _a.localPart) === 'commentReference') {
710
+ const id = Number((_b = (o.value)) === null || _b === void 0 ? void 0 : _b.id);
711
+ if (Number.isInteger(id))
712
+ into.add(id);
713
+ return;
714
+ }
715
+ for (const [key, value] of Object.entries(o))
716
+ if (key !== 'PARENT')
717
+ visit(value);
718
+ };
719
+ visit(tree);
380
720
  }
381
721
  /** A holder's four insertion markers taken away; the element stays. */
382
722
  function removeHolderMarkers(t) {
@@ -425,6 +765,87 @@ export function holderInsertionAt(items, i, paragraph) {
425
765
  return undefined;
426
766
  return new TrackedChange({ kind: 'holder', holder, owner: items, outer: [start, outerEnd], inner: [innerEnd, innerStart], value: start.value }, paragraph);
427
767
  }
768
+ /**
769
+ * An inserted field rejected (Word's form, check 30 `field-inserted`): the `w:ins` pieces and the plain
770
+ * runs between them go, last first so that a comment's range is judged once the field's text is gone;
771
+ * a bookmark, a comment's range or a proofing mark between them stays where it stood.
772
+ */
773
+ function removeField(t, paragraph) {
774
+ for (let i = t.span.length - 1; i >= 0; i--) {
775
+ const el = t.span[i];
776
+ if (revisionKindOf(el) === 'ins')
777
+ removeRevision({ kind: 'run', revision: 'ins', element: el, owner: currentOwner(t.owner, el), value: el.value }, paragraph);
778
+ else if (typeNameOf(el) === 'org_docx4j_wml.R')
779
+ remove(t.owner, el);
780
+ }
781
+ }
782
+ /**
783
+ * Word's form for a field inserted under tracking, when `items[i]` starts it: a `w:ins` that opens a
784
+ * field (a `w:fldChar` begin it does not end), then plain runs - the separator and the result - and
785
+ * markers, then a `w:ins` by the same author in which that field ends (check 30, `field-inserted`:
786
+ * begin and instruction in one, the end run in the other). Another revision, a run whose formatting
787
+ * changed, a run holder, or a field that ends in a plain run is not this form, and is listed as before.
788
+ * A field wholly inside one `w:ins` needs nothing of this: that `w:ins` already takes it whole.
789
+ */
790
+ export function insertedFieldAt(items, i, paragraph) {
791
+ var _a;
792
+ const first = items[i];
793
+ if (revisionKindOf(first) !== 'ins')
794
+ return undefined;
795
+ const author = first.value.author;
796
+ let depth = 0;
797
+ // the fldChars of an element's runs in order: 'closed' when the field the span opened ends
798
+ const scan = (el) => {
799
+ var _a, _b, _c;
800
+ const runs = typeNameOf(el) === 'org_docx4j_wml.R' ? [el] : (_a = runItemsOf(el.value)) !== null && _a !== void 0 ? _a : [];
801
+ for (const run of runs) {
802
+ if (typeNameOf(run) !== 'org_docx4j_wml.R')
803
+ continue;
804
+ for (const item of (_b = run.value.content) !== null && _b !== void 0 ? _b : []) {
805
+ if (((_c = item.name) === null || _c === void 0 ? void 0 : _c.localPart) !== 'fldChar')
806
+ continue;
807
+ const type = item.value.fldCharType;
808
+ if (type === 'begin')
809
+ depth++;
810
+ else if (type === 'end') {
811
+ depth--;
812
+ if (depth < 0)
813
+ return 'other';
814
+ if (depth === 0)
815
+ return 'closed';
816
+ }
817
+ }
818
+ }
819
+ return 'open';
820
+ };
821
+ if (scan(first) !== 'open' || depth === 0)
822
+ return undefined;
823
+ for (let j = i + 1; j < items.length; j++) {
824
+ const el = items[j];
825
+ const kind = revisionKindOf(el);
826
+ if (kind === 'ins') {
827
+ if (el.value.author !== author)
828
+ return undefined;
829
+ const state = scan(el);
830
+ if (state === 'other')
831
+ return undefined;
832
+ if (state === 'closed')
833
+ return new TrackedChange({ kind: 'field', span: items.slice(i, j + 1), owner: items, value: first.value }, paragraph);
834
+ continue;
835
+ }
836
+ if (kind !== undefined)
837
+ return undefined;
838
+ const v = el.value;
839
+ if (typeNameOf(el) === 'org_docx4j_wml.R') {
840
+ if (((_a = v.rPr) === null || _a === void 0 ? void 0 : _a.rPrChange) || scan(el) !== 'open')
841
+ return undefined;
842
+ continue;
843
+ }
844
+ if (typeof v === 'object' && v !== null && runItemsOf(v))
845
+ return undefined;
846
+ }
847
+ return undefined;
848
+ }
428
849
  /**
429
850
  * A row taken away, and its table with it when it was the last: a table has no revision of its own,
430
851
  * so one whose rows were all inserted was inserted, and one whose rows were all deleted was deleted.
@@ -471,6 +892,7 @@ function removeRevision(t, paragraph) {
471
892
  if (owner.some((other, e) => { var _a; return e > i && ((_a = other.name) === null || _a === void 0 ? void 0 : _a.localPart) === 'commentRangeEnd' && idOf(other) === id; }))
472
893
  outside.add(id);
473
894
  }
895
+ discarding(t.element);
474
896
  owner.splice(i, 1, ...kept);
475
897
  linkParents(kept, t.element.value.PARENT);
476
898
  const body = paragraph === null || paragraph === void 0 ? void 0 : paragraph.parentBody;
@@ -563,6 +985,7 @@ function dropMark(paragraph, which) {
563
985
  */
564
986
  export function joinWithNext(paragraph, fallbackToPrevious = false) {
565
987
  var _a, _b, _c, _d, _e;
988
+ discarding();
566
989
  const container = currentOwner(paragraph.container, paragraph.element);
567
990
  const i = container.indexOf(paragraph.element);
568
991
  const next = i >= 0 ? container[i + 1] : undefined;
@@ -575,6 +998,7 @@ export function joinWithNext(paragraph, fallbackToPrevious = false) {
575
998
  for (const item of (_b = p.content) !== null && _b !== void 0 ? _b : [])
576
999
  items.push(item);
577
1000
  linkParents(items, into);
1001
+ discarding(undefined, p.pPr);
578
1002
  container.splice(i, 1);
579
1003
  return;
580
1004
  }
@@ -591,6 +1015,7 @@ export function joinWithNext(paragraph, fallbackToPrevious = false) {
591
1015
  for (const item of (_e = nextP.content) !== null && _e !== void 0 ? _e : [])
592
1016
  content.push(item);
593
1017
  linkParents(content, p);
1018
+ discarding(undefined, p.pPr); // a section break's sectPr goes with it
594
1019
  p.pPr = nextP.pPr;
595
1020
  if (p.pPr)
596
1021
  linkParents(p.pPr, p);
@@ -652,6 +1077,131 @@ function acceptedOffsetOf(p, target) {
652
1077
  visit(runItemsOf(p));
653
1078
  return found >= 0 ? found : pos;
654
1079
  }
1080
+ // --- math (CR-002 section 51) ------------------------------------------------------------------
1081
+ /** True for a value of the math model (`m:oMath`, `m:r`, `m:sSup`, ...). */
1082
+ function isMathValue(value) {
1083
+ const tn = value === null || value === void 0 ? void 0 : value.TYPE_NAME;
1084
+ return typeof tn === 'string' && tn.startsWith('org_docx4j_math.');
1085
+ }
1086
+ /** The text of an `m:t` (or a `w:t`) in an `m:r`; undefined for anything else. */
1087
+ function mathTextOf(item) {
1088
+ var _a, _b, _c;
1089
+ if (((_a = item === null || item === void 0 ? void 0 : item.name) === null || _a === void 0 ? void 0 : _a.localPart) !== 't')
1090
+ return undefined;
1091
+ return String((_c = (_b = item.value) === null || _b === void 0 ? void 0 : _b.value) !== null && _c !== void 0 ? _c : '');
1092
+ }
1093
+ /**
1094
+ * A math run's revision resolved: `keep` (an insertion accepted, a deletion rejected) puts its content in
1095
+ * its place in the `m:r`; otherwise it goes, and the `m:r` with it when no text is left in it (docx4j's
1096
+ * reading of Word's Accept All and Reject All of `tracked-changes-equations.docx`).
1097
+ */
1098
+ function resolveMathRun(t, keep) {
1099
+ var _a, _b;
1100
+ const content = ((_a = t.run.value.content) !== null && _a !== void 0 ? _a : []);
1101
+ const i = content.indexOf(t.item);
1102
+ if (i < 0)
1103
+ return;
1104
+ if (keep) {
1105
+ const moved = ((_b = t.value.rest) !== null && _b !== void 0 ? _b : []);
1106
+ content.splice(i, 1, ...moved);
1107
+ linkParents(moved, t.run.value);
1108
+ return;
1109
+ }
1110
+ discarding(t.item);
1111
+ content.splice(i, 1);
1112
+ if (!content.some((item) => { var _a; return mathTextOf(item) !== undefined || ((_a = item.value) === null || _a === void 0 ? void 0 : _a.TYPE_NAME) === 'org_docx4j_wml.CTMathRunTrackChange'; })) {
1113
+ remove(t.runOwner, t.run);
1114
+ }
1115
+ }
1116
+ /**
1117
+ * A structure's insertion or deletion resolved: `keep` drops the marker, its recorded run properties
1118
+ * becoming the control's when it has none, as Word's saves have them; otherwise the structure goes
1119
+ * whole from its list. An argument's own marker has no structure, and is dropped either way.
1120
+ */
1121
+ function resolveMathStructure(t, keep) {
1122
+ var _a;
1123
+ if (!keep && t.structure && t.owner) {
1124
+ remove(t.owner, t.structure);
1125
+ return;
1126
+ }
1127
+ const ctrlPr = t.ctrlPr;
1128
+ if (!ctrlPr.rPr && t.value.rPr) {
1129
+ ctrlPr.rPr = rPrFromElements((_a = t.value.rPr.egrPrBase) !== null && _a !== void 0 ? _a : []);
1130
+ linkParents(ctrlPr.rPr, ctrlPr);
1131
+ }
1132
+ if (t.revision === 'ins')
1133
+ delete ctrlPr.ins;
1134
+ else
1135
+ delete ctrlPr.del;
1136
+ }
1137
+ /** The equation a math piece is in: the outermost level's element, the one in the paragraph. */
1138
+ function equationOf(stack) {
1139
+ var _a;
1140
+ const top = (_a = stack[0]) === null || _a === void 0 ? void 0 : _a.item;
1141
+ return top && typeof top === 'object' && 'name' in top ? { equation: top } : {};
1142
+ }
1143
+ /**
1144
+ * An equation's single changes, in document order, docx4j's walk (its CR-043 `Lister.math`): a
1145
+ * revision inside an `m:r`; a structure's, in its `m:ctrlPr`, which comes before the structure's
1146
+ * arguments; and a run-level revision holding math. A math run with text and no revision keeps the
1147
+ * pieces either side apart, as plain text does.
1148
+ */
1149
+ function mathTokens(paragraph, value, item, owner, stack, out) {
1150
+ var _a;
1151
+ const tn = value.TYPE_NAME;
1152
+ if (tn === 'org_docx4j_math.CTR') {
1153
+ for (const c of ((_a = value.content) !== null && _a !== void 0 ? _a : [])) {
1154
+ const cv = c === null || c === void 0 ? void 0 : c.value;
1155
+ if ((cv === null || cv === void 0 ? void 0 : cv.TYPE_NAME) === 'org_docx4j_wml.CTMathRunTrackChange' && owner) {
1156
+ out.push(new TrackedChange({ kind: 'mathRun', revision: c.name.localPart === 'del' ? 'del' : 'ins', item: c, run: item, runOwner: owner, value: cv, ...equationOf(stack) }, paragraph));
1157
+ }
1158
+ else if (mathTextOf(c) !== undefined)
1159
+ out.push(BREAK);
1160
+ }
1161
+ return;
1162
+ }
1163
+ if (tn === 'org_docx4j_math.CTCtrlPr') {
1164
+ const ctrlPr = value;
1165
+ if (!ctrlPr.ins && !ctrlPr.del)
1166
+ return;
1167
+ // in m:sSupPr in m:sSup, the structure is two up; an argument's (m:e's) own marks none
1168
+ const parent = stack[stack.length - 1];
1169
+ const grand = parent && parent.value.TYPE_NAME !== 'org_docx4j_math.CTOMathArg' ? stack[stack.length - 2] : undefined;
1170
+ const structure = { ...((grand === null || grand === void 0 ? void 0 : grand.owner) ? { structure: grand.item, owner: grand.owner } : {}), ...equationOf(stack) };
1171
+ if (ctrlPr.ins)
1172
+ out.push(new TrackedChange({ kind: 'mathStructure', revision: 'ins', ctrlPr, value: ctrlPr.ins, ...structure }, paragraph));
1173
+ if (ctrlPr.del)
1174
+ out.push(new TrackedChange({ kind: 'mathStructure', revision: 'del', ctrlPr, value: ctrlPr.del, ...structure }, paragraph));
1175
+ return;
1176
+ }
1177
+ stack.push({ value, item, owner });
1178
+ for (const key of Object.keys(value)) {
1179
+ if (key === 'PARENT' || key === 'TYPE_NAME')
1180
+ continue;
1181
+ const field = value[key];
1182
+ if (typeof field !== 'object' || field === null)
1183
+ continue;
1184
+ if (Array.isArray(field)) {
1185
+ const list = field;
1186
+ for (const c of list) {
1187
+ if (typeof c !== 'object' || c === null)
1188
+ continue;
1189
+ const kind = revisionKindOf(c);
1190
+ if (kind !== undefined) {
1191
+ // a w:ins or w:del holding m:r: as runs (schema-legal; docx4j's U7, in no fixture)
1192
+ out.push(new TrackedChange({ kind: 'run', revision: kind, element: c, owner: list, value: c.value }, paragraph));
1193
+ continue;
1194
+ }
1195
+ const cv = c !== null && typeof c === 'object' && 'value' in c && 'name' in c ? c.value : c;
1196
+ if (isMathValue(cv))
1197
+ mathTokens(paragraph, cv, c, list, stack, out);
1198
+ }
1199
+ }
1200
+ else if (isMathValue(field))
1201
+ mathTokens(paragraph, field, field, undefined, stack, out);
1202
+ }
1203
+ stack.pop();
1204
+ }
655
1205
  const RANGE_MARKERS = {
656
1206
  moveFromRangeStart: { kind: 'start', side: 'from' }, moveFromRangeEnd: { kind: 'end', side: 'from' },
657
1207
  moveToRangeStart: { kind: 'start', side: 'to' }, moveToRangeEnd: { kind: 'end', side: 'to' },
@@ -997,6 +1547,18 @@ function tokensOfRunLevel(paragraph, items, out) {
997
1547
  i += 2;
998
1548
  continue;
999
1549
  }
1550
+ // a field Word inserted, its separator and result plain between two insertions: one piece
1551
+ const field = insertedFieldAt(items, i, paragraph);
1552
+ if (field && field.target.kind === 'field') {
1553
+ out.push(field);
1554
+ i += field.target.span.length - 1;
1555
+ continue;
1556
+ }
1557
+ // an equation: the revisions in its runs and on its structures (section 51)
1558
+ if (isMathValue(el.value)) {
1559
+ mathTokens(paragraph, el.value, el, items, [], out);
1560
+ continue;
1561
+ }
1000
1562
  const kind = revisionKindOf(el);
1001
1563
  if (kind !== undefined) {
1002
1564
  // moved text is content Office JS cannot list (checks 16, 17), and is kept apart
@@ -1055,8 +1617,10 @@ function groupable(change) {
1055
1617
  return t.revision;
1056
1618
  if (t.kind === 'runProperties' || t.kind === 'markProperties')
1057
1619
  return 'format';
1058
- if (t.kind === 'holder')
1620
+ if (t.kind === 'holder' || t.kind === 'field')
1059
1621
  return 'ins';
1622
+ if (t.kind === 'mathRun' || t.kind === 'mathStructure')
1623
+ return t.revision;
1060
1624
  // an inserted or deleted row groups with rows and text touching it: Office JS lists an inserted
1061
1625
  // table of three rows, with the inserted paragraphs before and after it, as one change, and rows
1062
1626
  // added to a table that was there as one change per touching run of them (check 31)
@@ -1106,13 +1670,13 @@ export function groupTouching(tokens) {
1106
1670
  * The `w:trPr/w:ins` and `w:trPr/w:del` revisions of a table row. `inner` is the revision markup
1107
1671
  * of the row's own cells, which the row's change owns rather than reporting separately.
1108
1672
  */
1109
- export function trackedChangesOfRow(tr, owner, inner = [], table) {
1673
+ export function trackedChangesOfRow(tr, owner, inner = [], table, body) {
1110
1674
  const trPr = tr.value.trPr;
1111
1675
  const out = [];
1112
1676
  if (trPr === null || trPr === void 0 ? void 0 : trPr.ins)
1113
- out.push(new TrackedChange({ kind: 'row', row: 'ins', tr, owner, value: trPr.ins, inner, table }, undefined));
1677
+ out.push(new TrackedChange({ kind: 'row', row: 'ins', tr, owner, value: trPr.ins, inner, table }, undefined, body));
1114
1678
  if (trPr === null || trPr === void 0 ? void 0 : trPr.del)
1115
- out.push(new TrackedChange({ kind: 'row', row: 'del', tr, owner, value: trPr.del, inner, table }, undefined));
1679
+ out.push(new TrackedChange({ kind: 'row', row: 'del', tr, owner, value: trPr.del, inner, table }, undefined, body));
1116
1680
  return out;
1117
1681
  }
1118
1682
  // --- CR-002 section 35: tables, cells, marks and sections --------------------------------------------
@@ -1122,12 +1686,12 @@ export function tablePropertiesChangeOf(tbl) {
1122
1686
  return records ? new TrackedChange({ kind: 'tableProperties', tbl, value: records.value }, undefined) : undefined;
1123
1687
  }
1124
1688
  /** A cell's own revision, `w:cellIns` or `w:cellDel`, or undefined. */
1125
- export function cellChangeOf(tc, owner, tr) {
1689
+ export function cellChangeOf(tc, owner, tr, body) {
1126
1690
  const tcPr = tc.value.tcPr;
1127
1691
  if (tcPr === null || tcPr === void 0 ? void 0 : tcPr.cellIns)
1128
- return new TrackedChange({ kind: 'cell', cell: 'ins', tc, owner, tr, value: tcPr.cellIns }, undefined);
1692
+ return new TrackedChange({ kind: 'cell', cell: 'ins', tc, owner, tr, value: tcPr.cellIns }, undefined, body);
1129
1693
  if (tcPr === null || tcPr === void 0 ? void 0 : tcPr.cellDel)
1130
- return new TrackedChange({ kind: 'cell', cell: 'del', tc, owner, tr, value: tcPr.cellDel }, undefined);
1694
+ return new TrackedChange({ kind: 'cell', cell: 'del', tc, owner, tr, value: tcPr.cellDel }, undefined, body);
1131
1695
  return undefined;
1132
1696
  }
1133
1697
  /** The change to the body's last section (`w:body/w:sectPr/w:sectPrChange`), its text the section's paragraphs'. */
@@ -1153,6 +1717,7 @@ function removeCell(tc, owner) {
1153
1717
  const cells = owner.filter((el) => typeNameOf(el) === 'org_docx4j_wml.Tc');
1154
1718
  const at = cells.indexOf(tc);
1155
1719
  const neighbour = (_a = cells[at - 1]) !== null && _a !== void 0 ? _a : cells[at + 1];
1720
+ discarding(tc);
1156
1721
  owner.splice(i, 1);
1157
1722
  if (!neighbour)
1158
1723
  return;