@docx4j/core-ts 0.2.2 → 0.3.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.
@@ -1,9 +1,11 @@
1
1
  import { textOf } from '@docx4j/generated-objects-ts/builders/wml';
2
2
  import { Docx4JException } from '../../opc/exceptions.mjs';
3
- import { typeNameOf, runItemsOf, revisionKindOf, linkParents, segmentsOf, textOfView, cellsOf, childrenOf } from './tree.mjs';
3
+ import { typeNameOf, runItemsOf, revisionKindOf, linkParents, segmentsOf, textOfView, cellsOf, rowsOf, childrenOf, W_NS } from './tree.mjs';
4
4
  import { Range } from './Range.mjs';
5
5
  import { rPrFromElements } from '@docx4j/generated-objects-ts/builders/wml';
6
- import { dateOf, toRestoredText, restoreRPr, restorePPr, pruneParagraphProperties } from './tracking.mjs';
6
+ import { revisionDateOf, toRestoredText, toDeletedText, restoreRPr, restorePPr, pruneParagraphProperties } from './tracking.mjs';
7
+ import { tableRecordsOf, hasTableRecords, tableChangeText, rowText as tableRowText, acceptTable, rejectTable } from './tableRevisions.mjs';
8
+ import { deepCopy } from '@docx4j/generated-objects-ts';
7
9
  /**
8
10
  * A subset of Office JS `Word.TrackedChange`: `type`, `author`, `date`, `text`, `accept()`,
9
11
  * `reject()` and `getRange()`, plus `element`, `id` and `target` as extensions. A view, like
@@ -23,15 +25,21 @@ export class TrackedChange {
23
25
  case 'run': return t.revision === 'ins' || t.revision === 'moveTo' ? 'Added' : 'Deleted';
24
26
  case 'mark': return t.mark === 'ins' ? 'Added' : 'Deleted';
25
27
  case 'row': return t.row === 'ins' ? 'Added' : 'Deleted';
28
+ case 'group': return t.revision === 'ins' ? 'Added' : t.revision === 'del' ? 'Deleted' : 'Formatted';
29
+ case 'cell': return t.cell === 'ins' ? 'Added' : 'Deleted';
26
30
  default: return 'Formatted';
27
31
  }
28
32
  }
29
33
  get author() {
30
34
  return this.target.value.author;
31
35
  }
32
- /** `w:date`, when the markup carries one. */
36
+ /**
37
+ * When the change was made: `w16du:dateUtc` if the markup has it, else `w:date` read as Word writes
38
+ * it, local wall-clock time (`revisionDateOf`, CR-002 section 29); a group's is its first piece's,
39
+ * as Office JS reports it.
40
+ */
33
41
  get date() {
34
- return dateOf(this.target.value.date);
42
+ return revisionDateOf(this.target.value);
35
43
  }
36
44
  /** The annotation id (`w:id`; an extension). */
37
45
  get id() {
@@ -39,13 +47,28 @@ export class TrackedChange {
39
47
  }
40
48
  /** The markup this change is over (an extension): the `w:ins` element, the `w:rPrChange`, ... */
41
49
  get element() {
42
- return this.target.kind === 'run' ? this.target.element : this.target.kind === 'row' ? this.target.tr : this.target.value;
50
+ const t = this.target;
51
+ switch (t.kind) {
52
+ case 'run': return t.element;
53
+ case 'row': return t.tr;
54
+ case 'group': return t.pieces[0].element;
55
+ case 'tableProperties': return t.tbl;
56
+ case 'cell': return t.tc;
57
+ case 'sectionProperties': return t.sectPr;
58
+ default: return t.value;
59
+ }
43
60
  }
44
- /** The text the change covers; '' for a paragraph mark, whose range is the paragraph. */
61
+ /** The text the change covers; `\r` for a paragraph mark, as Office JS gives it (check 22). */
45
62
  get text() {
46
63
  var _a, _b;
47
64
  const t = this.target;
48
65
  switch (t.kind) {
66
+ case 'mark': return '\r';
67
+ case 'markProperties': return '\r';
68
+ case 'group': return t.pieces.map((piece) => piece.text).join('');
69
+ case 'tableProperties': return tableChangeText(t.tbl.value);
70
+ case 'cell': return tableRowText(t.tr);
71
+ case 'sectionProperties': return t.text;
49
72
  case 'run': return segmentsOf(t.element.value, { view: t.revision === 'ins' || t.revision === 'moveTo' ? 'accepted' : 'original' }).map((s) => s.text).join('');
50
73
  case 'runProperties': return textOf(t.run);
51
74
  case 'paragraphProperties': return (_b = (_a = this.paragraph) === null || _a === void 0 ? void 0 : _a.text) !== null && _b !== void 0 ? _b : '';
@@ -65,6 +88,13 @@ export class TrackedChange {
65
88
  const [start, end] = spanOf(p, t.element);
66
89
  return new Range(p, start, end);
67
90
  }
91
+ if (t.kind === 'group') {
92
+ // a range is one paragraph's: the group's text in the paragraph it starts in
93
+ 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());
94
+ if (here.length === 0)
95
+ return t.pieces[0].getRange();
96
+ return new Range(p, Math.min(...here.map((r) => r.start)), Math.max(...here.map((r) => r.end)));
97
+ }
68
98
  if (t.kind === 'runProperties') {
69
99
  const segs = p.segments().filter((s) => s.run === t.run);
70
100
  if (segs.length === 0)
@@ -78,11 +108,11 @@ export class TrackedChange {
78
108
  * Either half of a move keeps the whole move, as Word's Review tab does (CR-002 section 29).
79
109
  */
80
110
  accept() {
81
- var _a, _b, _c;
111
+ var _a, _b, _c, _d, _e, _f;
82
112
  const t = this.target;
83
113
  switch (t.kind) {
84
114
  case 'run':
85
- if ((t.revision === 'moveFrom' || t.revision === 'moveTo') && resolveMove(this, 'accept'))
115
+ if ((t.revision === 'moveFrom' || t.revision === 'moveTo') && (resolveMove(this, 'accept') || resolveRangelessHalf(this, 'accept')))
86
116
  return;
87
117
  if (t.revision === 'ins' || t.revision === 'moveTo')
88
118
  unwrap(t);
@@ -109,11 +139,34 @@ export class TrackedChange {
109
139
  // w:trPr/w:ins; an accepted deletion takes the row and everything in it away
110
140
  if (t.row === 'ins') {
111
141
  (_c = t.tr.value.trPr) === null || _c === void 0 ? true : delete _c.ins;
142
+ pruneRowProperties(t.tr.value);
112
143
  applyInner(t.inner, 'accept');
113
144
  }
114
145
  else
115
146
  remove(t.owner, t.tr);
116
147
  return;
148
+ case 'group':
149
+ resolvePieces(t.pieces, 'accept');
150
+ return;
151
+ case 'tableProperties':
152
+ acceptTable(t.tbl.value);
153
+ return;
154
+ case 'cell':
155
+ // an inserted cell stays, a deleted one goes (check 27)
156
+ if (t.cell === 'ins')
157
+ (_d = t.tc.value.tcPr) === null || _d === void 0 ? true : delete _d.cellIns;
158
+ else
159
+ removeCell(t.tc, t.owner);
160
+ return;
161
+ case 'markProperties': {
162
+ const p = this.requireParagraph().p;
163
+ (_f = (_e = p.pPr) === null || _e === void 0 ? void 0 : _e.rPr) === null || _f === void 0 ? true : delete _f.rPrChange;
164
+ pruneParagraphProperties(p);
165
+ return;
166
+ }
167
+ case 'sectionProperties':
168
+ delete t.sectPr.sectPrChange;
169
+ return;
117
170
  }
118
171
  }
119
172
  /**
@@ -121,11 +174,11 @@ export class TrackedChange {
121
174
  * Either half of a move rejects the whole move, as Word's Review tab does (CR-002 section 29).
122
175
  */
123
176
  reject() {
124
- var _a;
177
+ var _a, _b;
125
178
  const t = this.target;
126
179
  switch (t.kind) {
127
180
  case 'run':
128
- if ((t.revision === 'moveFrom' || t.revision === 'moveTo') && resolveMove(this, 'reject'))
181
+ if ((t.revision === 'moveFrom' || t.revision === 'moveTo') && (resolveMove(this, 'reject') || resolveRangelessHalf(this, 'reject')))
129
182
  return;
130
183
  if (t.revision === 'ins' || t.revision === 'moveTo')
131
184
  remove(t.owner, t.element);
@@ -142,8 +195,10 @@ export class TrackedChange {
142
195
  restoreRPr(t.run, rPrFromElements(t.value.rPr));
143
196
  return;
144
197
  case 'paragraphProperties': {
145
- const p = this.requireParagraph().p;
146
- restorePPr(p, t.value.pPr);
198
+ const paragraph = this.requireParagraph();
199
+ restorePPr(paragraph.p, t.value.pPr);
200
+ dropLevelIndent(paragraph);
201
+ writeRecordedLevel(paragraph.p);
147
202
  return;
148
203
  }
149
204
  case 'row':
@@ -151,11 +206,30 @@ export class TrackedChange {
151
206
  // the w:del unwrapped, the deleted marks dropped); a rejected insertion takes it away
152
207
  if (t.row === 'del') {
153
208
  (_a = t.tr.value.trPr) === null || _a === void 0 ? true : delete _a.del;
209
+ pruneRowProperties(t.tr.value);
154
210
  applyInner(t.inner, 'reject');
155
211
  }
156
212
  else
157
213
  remove(t.owner, t.tr);
158
214
  return;
215
+ case 'group':
216
+ resolvePieces(t.pieces, 'reject');
217
+ return;
218
+ case 'tableProperties':
219
+ rejectTable(t.tbl.value);
220
+ return;
221
+ case 'cell':
222
+ if (t.cell === 'del')
223
+ (_b = t.tc.value.tcPr) === null || _b === void 0 ? true : delete _b.cellDel;
224
+ else
225
+ removeCell(t.tc, t.owner);
226
+ return;
227
+ case 'markProperties':
228
+ restoreMarkProperties(this.requireParagraph().p, t.value);
229
+ return;
230
+ case 'sectionProperties':
231
+ restoreSection(t.sectPr, t.value);
232
+ return;
159
233
  }
160
234
  }
161
235
  requireParagraph() {
@@ -192,6 +266,70 @@ function applyInner(inner, what) {
192
266
  for (let i = inner.length - 1; i >= 0; i--)
193
267
  inner[i][what]();
194
268
  }
269
+ /**
270
+ * Word records a list paragraph's indent in `w:pPrChange` even where the list level gives it, and on
271
+ * reject writes no indent back: it restored "The first numbered item." to its numbered list with no
272
+ * `w:ind`, where the change recorded `left=720 hanging=360`, the level's own (`test/README.md` check
273
+ * 18, `reject-all.docx`, section 9), and check 25's level-0 items with none where it recorded only
274
+ * `hanging=360`, part of the level's `left=720 hanging=360` (`25b-bullets-reject.docx`; CR-002
275
+ * section 29). So a restored `w:ind` whose every attribute is the level's is dropped, leaving the level
276
+ * to give it; one with an attribute of its own - a genuinely direct indent - is restored. The numbering
277
+ * part must have been read (`await pkg.getBody()` or `getPropertyResolver()` reads it); when it has
278
+ * not, the recorded indent is restored as it was.
279
+ */
280
+ function dropLevelIndent(paragraph) {
281
+ var _a;
282
+ const pPr = paragraph.p.pPr;
283
+ const numPr = pPr === null || pPr === void 0 ? void 0 : pPr.numPr;
284
+ const ind = pPr === null || pPr === void 0 ? void 0 : pPr.ind;
285
+ if (!pPr || !numPr || !ind)
286
+ return;
287
+ const part = (_a = paragraph.parentBody.package_) === null || _a === void 0 ? void 0 : _a.numberingDefinitionsPart;
288
+ let level;
289
+ try {
290
+ level = part === null || part === void 0 ? void 0 : part.getInd(numPr);
291
+ }
292
+ catch {
293
+ return; // not read: nothing to compare with
294
+ }
295
+ if (!level || !indentWithin(ind, level))
296
+ return;
297
+ delete pPr.ind;
298
+ pruneParagraphProperties(paragraph.p);
299
+ }
300
+ /**
301
+ * A restored `w:numPr` that names a list but no level is at level 0, and Word writes the 0: it
302
+ * restored the level recorded, not the current one, when the two differed (check 25's
303
+ * `25e-bullet-demote.docx`, recorded `numId 1` alone and current `ilvl 1`, rejected to `ilvl 0,
304
+ * numId 1`), and wrote `w:ilvl="0"` every time the record had none (check 18's section 9, 25b, 25f;
305
+ * CR-002 section 29).
306
+ */
307
+ function writeRecordedLevel(p) {
308
+ var _a;
309
+ const numPr = (_a = p.pPr) === null || _a === void 0 ? void 0 : _a.numPr;
310
+ if (!(numPr === null || numPr === void 0 ? void 0 : numPr.numId) || Number(numPr.numId.val) === 0 || numPr.ilvl)
311
+ return;
312
+ numPr.ilvl = { TYPE_NAME: 'org_docx4j_wml.PPrBase.NumPr.Ilvl', val: 0 };
313
+ linkParents(numPr.ilvl, numPr);
314
+ }
315
+ /** True when every attribute `ind` sets, `level` sets to the same value. */
316
+ function indentWithin(ind, level) {
317
+ const attributes = Object.entries(ind).filter(([key, value]) => key !== 'TYPE_NAME' && key !== 'PARENT' && value !== undefined);
318
+ return attributes.length > 0 && attributes.every(([key, value]) => String(level[key]) === String(value));
319
+ }
320
+ /**
321
+ * A group's pieces resolved: its runs last first, then its marks last first, so that a paragraph join
322
+ * never strands a piece still to do. Office JS and Word's Review tab resolve the group as one (checks
323
+ * 22 and 23), and so, piece by piece, does this.
324
+ */
325
+ function resolvePieces(pieces, what) {
326
+ const runs = pieces.filter((piece) => piece.target.kind !== 'mark');
327
+ const marks = pieces.filter((piece) => piece.target.kind === 'mark');
328
+ for (let i = runs.length - 1; i >= 0; i--)
329
+ runs[i][what]();
330
+ for (let i = marks.length - 1; i >= 0; i--)
331
+ marks[i][what]();
332
+ }
195
333
  function remove(owner, element) {
196
334
  const i = owner.indexOf(element);
197
335
  if (i >= 0)
@@ -270,8 +408,28 @@ export function joinWithNext(paragraph, fallbackToPrevious = false) {
270
408
  p.pPr = nextP.pPr;
271
409
  if (p.pPr)
272
410
  linkParents(p.pPr, p);
411
+ // The mark that survives is the next paragraph's, and so is the paragraph's identity: Word keeps the
412
+ // second paragraph's w14:paraId when a mark goes (check 18's accept-all and reject-all, check 22's
413
+ // 22-accept-whole; found by the editor's C5). Its w14:textId and session ids come with it; Word
414
+ // writes a new textId, the text having changed, which Word will do again when it next saves.
415
+ for (const key of PARAGRAPH_ATTRIBUTES) {
416
+ const value = nextP[key];
417
+ if (value === undefined)
418
+ delete p[key];
419
+ else
420
+ p[key] = value;
421
+ }
273
422
  container.splice(i + 1, 1);
274
423
  }
424
+ /** A `w:p`'s own attributes, which go with its mark. */
425
+ const PARAGRAPH_ATTRIBUTES = ['paraId', 'textId', 'rsidDel', 'rsidP', 'rsidR', 'rsidRDefault', 'rsidRPr', 'noSpellErr'];
426
+ /** A row's `w:trPr` with nothing left in it goes, as Word writes none (check 18's accept-all and reject-all). */
427
+ function pruneRowProperties(tr) {
428
+ var _a;
429
+ const trPr = tr.trPr;
430
+ if (trPr && !trPr.ins && !trPr.del && !trPr.trPrChange && !((_a = trPr.cnfStyleOrDivIdOrGridBefore) === null || _a === void 0 ? void 0 : _a.length))
431
+ delete tr.trPr;
432
+ }
275
433
  /** The span of a run-level revision in the paragraph's accepted text; collapsed for a deletion. */
276
434
  function spanOf(paragraph, element) {
277
435
  const segs = paragraph.segments();
@@ -327,19 +485,33 @@ function resolveMove(change, action) {
327
485
  const t = change.target;
328
486
  if (t.kind !== 'run' || !change.paragraph)
329
487
  return false;
330
- const pieces = movePiecesOf(storyOf(change.paragraph.p));
488
+ const story = storyOf(change.paragraph.p);
489
+ const pieces = movePiecesOf(story);
331
490
  const half = pieces.find((piece) => piece.kind === 'half' && piece.element === t.element);
332
491
  if (!half || half.kind !== 'half' || half.names.length === 0)
333
492
  return false;
334
493
  const name = half.names[0];
335
494
  const ids = new Set(pieces.filter((piece) => piece.kind === 'start' && piece.name === name).map((piece) => piece.id));
336
495
  const mine = pieces.filter((piece) => (piece.kind === 'start' && piece.name === name) || (piece.kind === 'end' && ids.has(piece.id))
337
- || ((piece.kind === 'half' || piece.kind === 'mark') && piece.names.includes(name)));
338
- const kept = action === 'accept' ? 'to' : 'from';
496
+ || ((piece.kind === 'half' || piece.kind === 'mark' || piece.kind === 'inner') && piece.names.includes(name)));
497
+ // A destination whose source is gone is kept whichever way it is resolved: Word rejected such a
498
+ // move and left its text, plain, where it stood (test/README.md check 20 case 12).
499
+ const orphan = !mine.some((piece) => piece.side === 'from');
500
+ const kept = action === 'accept' || orphan ? 'to' : 'from';
501
+ const broken = orphan || !mine.some((piece) => piece.side === 'to');
339
502
  // the markers first: a range end between two paragraphs would stop the paragraph join below
340
503
  for (const piece of mine)
341
504
  if (piece.kind === 'start' || piece.kind === 'end')
342
505
  remove(piece.owner, piece.element);
506
+ // Rejected, a move takes with it everything typed into its destination: Word's default when moved
507
+ // text was edited (the Tracked Moves Conflict Dialog's "keep original location text", check 20
508
+ // cases 01 and 04). Text typed before the range starts is outside the move and stays (case 03).
509
+ // What accepting does to text typed into the source was not measured, so it is left alone.
510
+ if (kept === 'from') {
511
+ for (const piece of mine)
512
+ if (piece.kind === 'inner' && piece.side === 'to')
513
+ remove(piece.owner, piece.element);
514
+ }
343
515
  for (const piece of mine) {
344
516
  if (piece.kind !== 'half')
345
517
  continue;
@@ -374,6 +546,8 @@ function resolveMove(change, action) {
374
546
  joinWithNext({ container: mark.container, element: mark.paragraph, p }, mark.side === 'to');
375
547
  }
376
548
  }
549
+ if (broken)
550
+ dissolveBrokenMoves(story);
377
551
  return true;
378
552
  }
379
553
  /** The story a paragraph is in: the outermost array of block content above it (a body, a header, a note). */
@@ -390,7 +564,7 @@ function storyOf(p) {
390
564
  function movePiecesOf(story) {
391
565
  const out = [];
392
566
  const open = { from: new Map(), to: new Map() };
393
- const visit = (items) => {
567
+ const visit = (items, runLevel = false) => {
394
568
  var _a, _b, _c, _d, _e, _f;
395
569
  for (const el of items) {
396
570
  const marker = RANGE_MARKERS[(_b = (_a = el.name) === null || _a === void 0 ? void 0 : _a.localPart) !== null && _b !== void 0 ? _b : ''];
@@ -412,12 +586,18 @@ function movePiecesOf(story) {
412
586
  const side = revision === 'moveFrom' ? 'from' : 'to';
413
587
  out.push({ kind: 'half', side, element: el, owner: items, names: [...open[side].values()] });
414
588
  }
589
+ else if (runLevel) {
590
+ for (const side of ['from', 'to']) {
591
+ if (open[side].size > 0)
592
+ out.push({ kind: 'inner', side, element: el, owner: items, names: [...open[side].values()] });
593
+ }
594
+ }
415
595
  const v = el.value;
416
596
  if (typeof v !== 'object' || v === null)
417
597
  continue;
418
598
  const children = (_e = childrenOf(v)) !== null && _e !== void 0 ? _e : runItemsOf(v);
419
599
  if (children)
420
- visit(children);
600
+ visit(children, typeNameOf(el) === 'org_docx4j_wml.P');
421
601
  if (typeNameOf(el) === 'org_docx4j_wml.P') {
422
602
  const rPr = (_f = v.pPr) === null || _f === void 0 ? void 0 : _f.rPr;
423
603
  for (const side of ['from', 'to']) {
@@ -431,44 +611,284 @@ function movePiecesOf(story) {
431
611
  visit(story);
432
612
  return out;
433
613
  }
614
+ /**
615
+ * A move half in no named range - its range markers gone - resolved as Word resolved one (check 20
616
+ * cases 13 and 14, where Word showed it as an insertion or a deletion): a `w:moveTo` as an insertion,
617
+ * a `w:moveFrom` as a deletion, its paragraph mark with it, so that a paragraph whose text and mark
618
+ * both go goes whole. Then the rest of the broken pair is dissolved (`dissolveBrokenMoves`).
619
+ */
620
+ function resolveRangelessHalf(change, action) {
621
+ var _a, _b, _c;
622
+ const t = change.target;
623
+ const paragraph = change.paragraph;
624
+ if (t.kind !== 'run' || (t.revision !== 'moveTo' && t.revision !== 'moveFrom') || !paragraph)
625
+ return false;
626
+ const story = storyOf(paragraph.p);
627
+ const pieces = movePiecesOf(story);
628
+ const half = pieces.find((piece) => piece.kind === 'half' && piece.element === t.element);
629
+ if (!half || half.kind !== 'half' || half.names.length > 0)
630
+ return false;
631
+ const side = half.side;
632
+ const key = side === 'from' ? 'moveFrom' : 'moveTo';
633
+ const keep = (side === 'to') === (action === 'accept'); // an insertion accepted, a deletion rejected
634
+ if (!keep)
635
+ remove(t.owner, t.element);
636
+ else if (side === 'to')
637
+ unwrap(t);
638
+ else
639
+ restoreDeleted(t);
640
+ const mark = pieces.find((piece) => piece.kind === 'mark' && piece.side === side && piece.paragraph === paragraph.element && piece.names.length === 0);
641
+ const p = paragraph.p;
642
+ if (mark && ((_b = (_a = p.pPr) === null || _a === void 0 ? void 0 : _a.rPr) === null || _b === void 0 ? void 0 : _b[key])) {
643
+ if (keep) {
644
+ delete p.pPr.rPr[key];
645
+ pruneParagraphProperties(p);
646
+ }
647
+ else if (((_c = p.content) !== null && _c !== void 0 ? _c : []).length === 0) {
648
+ remove(paragraph.container, paragraph.element);
649
+ }
650
+ else {
651
+ delete p.pPr.rPr[key];
652
+ joinWithNext(paragraph, side === 'to');
653
+ }
654
+ }
655
+ dissolveBrokenMoves(story);
656
+ return true;
657
+ }
658
+ /**
659
+ * Every move half and moved paragraph mark in the story that cannot be paired - in no named range,
660
+ * or in one whose `w:name` no range of the other side carries - turned into the plain `w:ins` or
661
+ * `w:del` it stands for, pending, its range markers kept. That is how Word saved each broken pair
662
+ * in check 20 (re-run, cases 12 to 14, saved unchanged) and what it left of the other side once one
663
+ * was accepted or rejected. It runs after a broken move is resolved, and only then, so a document's
664
+ * other moves are left as they are; but a broken pair's halves cannot be told from another's, so
665
+ * this takes them all.
666
+ */
667
+ function dissolveBrokenMoves(story) {
668
+ var _a;
669
+ const pieces = movePiecesOf(story);
670
+ const named = { from: new Set(), to: new Set() };
671
+ for (const piece of pieces)
672
+ if (piece.kind === 'start')
673
+ named[piece.side].add(piece.name);
674
+ const unpaired = (side, names) => {
675
+ const other = named[side === 'from' ? 'to' : 'from'];
676
+ return names.every((name) => !other.has(name)); // true for no names too
677
+ };
678
+ for (const piece of pieces) {
679
+ if (piece.kind === 'half' && unpaired(piece.side, piece.names))
680
+ toPlainRevision(piece.element, piece.owner, piece.side === 'from' ? 'del' : 'ins');
681
+ if (piece.kind === 'mark' && unpaired(piece.side, piece.names)) {
682
+ const rPr = (_a = piece.paragraph.value.pPr) === null || _a === void 0 ? void 0 : _a.rPr;
683
+ if (piece.side === 'from' && (rPr === null || rPr === void 0 ? void 0 : rPr.moveFrom)) {
684
+ rPr.del = rPr.moveFrom;
685
+ delete rPr.moveFrom;
686
+ }
687
+ if (piece.side === 'to' && (rPr === null || rPr === void 0 ? void 0 : rPr.moveTo)) {
688
+ rPr.ins = rPr.moveTo;
689
+ delete rPr.moveTo;
690
+ }
691
+ }
692
+ }
693
+ }
694
+ /**
695
+ * A `w:moveFrom` turned into the plain `w:del` of the same runs, their text become deleted text, or a
696
+ * `w:moveTo` into the plain `w:ins`.
697
+ */
698
+ function toPlainRevision(element, owner, kind) {
699
+ var _a;
700
+ const value = element.value;
701
+ const items = (_a = runItemsOf(element.value)) !== null && _a !== void 0 ? _a : [];
702
+ if (kind === 'del')
703
+ for (const item of items)
704
+ if (typeNameOf(item) === 'org_docx4j_wml.R')
705
+ toDeletedText(item.value);
706
+ const plain = {
707
+ name: { namespaceURI: W_NS, localPart: kind },
708
+ value: { TYPE_NAME: kind === 'del' ? 'org_docx4j_wml.RunDel' : 'org_docx4j_wml.RunIns', id: value.id, author: value.author, date: value.date, customXmlOrSmartTagOrSdt: items },
709
+ };
710
+ const i = owner.indexOf(element);
711
+ if (i < 0)
712
+ return;
713
+ owner.splice(i, 1, plain);
714
+ linkParents(items, plain.value);
715
+ linkParents(plain, value.PARENT);
716
+ }
434
717
  // --- collecting ----------------------------------------------------------------------------
435
- /** The tracked changes of one paragraph, in document order. */
718
+ /**
719
+ * The tracked changes of one paragraph, in document order, touching pieces grouped as Office JS
720
+ * groups them (`groupTouching`). Office JS lists the changes that start in the paragraph (check 22):
721
+ * pieces that carry on a change from the paragraph before - its inserted or deleted mark - are left
722
+ * to that paragraph, and a change running on into the next is cut at this paragraph's end.
723
+ */
436
724
  export function trackedChangesOfParagraph(paragraph) {
437
- var _a, _b, _c;
725
+ const tokens = trackedChangeTokensOfParagraph(paragraph);
726
+ const before = markBefore(paragraph);
727
+ if (before) {
728
+ for (let i = 0; i < tokens.length;) {
729
+ const token = tokens[i];
730
+ if (token === BREAK)
731
+ break;
732
+ const kind = groupable(token);
733
+ if (kind === undefined) {
734
+ i++;
735
+ continue;
736
+ }
737
+ if (kind !== before.kind || token.author !== before.author)
738
+ break;
739
+ tokens.splice(i, 1);
740
+ }
741
+ }
742
+ return groupTouching(tokens);
743
+ }
744
+ /** The last revision on the mark of the paragraph before this one in its container, if any. */
745
+ function markBefore(paragraph) {
746
+ var _a;
747
+ const i = paragraph.container.indexOf(paragraph.element);
748
+ const previous = i > 0 ? paragraph.container[i - 1] : undefined;
749
+ if (!previous || typeNameOf(previous) !== 'org_docx4j_wml.P')
750
+ return undefined;
751
+ const rPr = (_a = previous.value.pPr) === null || _a === void 0 ? void 0 : _a.rPr;
752
+ if (rPr === null || rPr === void 0 ? void 0 : rPr.del)
753
+ return { kind: 'del', author: rPr.del.author };
754
+ if (rPr === null || rPr === void 0 ? void 0 : rPr.ins)
755
+ return { kind: 'ins', author: rPr.ins.author };
756
+ if (rPr === null || rPr === void 0 ? void 0 : rPr.rPrChange)
757
+ return { kind: 'format', author: rPr.rPrChange.author };
758
+ return undefined;
759
+ }
760
+ /** Between two tracked changes: something unrevised - text, a paragraph mark, a table - that keeps them apart. */
761
+ export const BREAK = 'break';
762
+ /**
763
+ * A paragraph's single changes in document order, its mark last, with a `BREAK` wherever unrevised
764
+ * content stands between two of them: `groupTouching` makes the groups from these.
765
+ */
766
+ export function trackedChangeTokensOfParagraph(paragraph) {
767
+ var _a, _b, _c, _d;
438
768
  const out = [];
439
769
  const p = paragraph.p;
440
770
  const pPrChange = (_a = p.pPr) === null || _a === void 0 ? void 0 : _a.pPrChange;
441
771
  if (pPrChange)
442
772
  out.push(new TrackedChange({ kind: 'paragraphProperties', value: pPrChange }, paragraph));
443
- const rPr = (_b = p.pPr) === null || _b === void 0 ? void 0 : _b.rPr;
773
+ tokensOfRunLevel(paragraph, ((_b = p.content) !== null && _b !== void 0 ? _b : []), out);
774
+ const rPr = (_c = p.pPr) === null || _c === void 0 ? void 0 : _c.rPr;
775
+ // a mark whose formatting changed is a piece, grouping with its runs' (check 27: "Before.\r")
776
+ if (rPr === null || rPr === void 0 ? void 0 : rPr.rPrChange)
777
+ out.push(new TrackedChange({ kind: 'markProperties', value: rPr.rPrChange }, paragraph));
444
778
  if (rPr === null || rPr === void 0 ? void 0 : rPr.ins)
445
779
  out.push(new TrackedChange({ kind: 'mark', mark: 'ins', value: rPr.ins }, paragraph));
446
780
  if (rPr === null || rPr === void 0 ? void 0 : rPr.del)
447
781
  out.push(new TrackedChange({ kind: 'mark', mark: 'del', value: rPr.del }, paragraph));
448
- collectRunLevel(paragraph, ((_c = p.content) !== null && _c !== void 0 ? _c : []), out);
782
+ if (!(rPr === null || rPr === void 0 ? void 0 : rPr.ins) && !(rPr === null || rPr === void 0 ? void 0 : rPr.del) && !(rPr === null || rPr === void 0 ? void 0 : rPr.rPrChange))
783
+ out.push(BREAK);
784
+ const sectPr = (_d = p.pPr) === null || _d === void 0 ? void 0 : _d.sectPr;
785
+ if (sectPr === null || sectPr === void 0 ? void 0 : sectPr.sectPrChange) {
786
+ out.push(new TrackedChange({ kind: 'sectionProperties', sectPr, value: sectPr.sectPrChange, text: `${sectionText(paragraph.container, paragraph.container.indexOf(paragraph.element) + 1)}\f` }, paragraph));
787
+ }
449
788
  return out;
450
789
  }
451
- function collectRunLevel(paragraph, items, out) {
790
+ /**
791
+ * The text of the section that ends at `end` (exclusive) in a container: its paragraphs' text, a `\r`
792
+ * between them, back to the previous section break.
793
+ */
794
+ function sectionText(items, end) {
452
795
  var _a;
796
+ let from = end - 1;
797
+ while (from > 0 && !(typeNameOf(items[from - 1]) === 'org_docx4j_wml.P' && ((_a = items[from - 1].value.pPr) === null || _a === void 0 ? void 0 : _a.sectPr)))
798
+ from--;
799
+ return items.slice(Math.max(from, 0), end).filter((el) => typeNameOf(el) === 'org_docx4j_wml.P')
800
+ .map((el) => textOfView(el.value, { view: 'accepted' })).join('\r');
801
+ }
802
+ function tokensOfRunLevel(paragraph, items, out) {
803
+ var _a, _b;
453
804
  for (const el of items) {
454
805
  const kind = revisionKindOf(el);
455
806
  if (kind !== undefined) {
807
+ // moved text is content Office JS cannot list (checks 16, 17), and is kept apart
808
+ const move = kind === 'moveFrom' || kind === 'moveTo';
809
+ if (move)
810
+ out.push(BREAK);
456
811
  out.push(new TrackedChange({ kind: 'run', revision: kind, element: el, owner: items, value: el.value }, paragraph));
812
+ if (move)
813
+ out.push(BREAK);
457
814
  const nested = runItemsOf(el.value);
458
- if (nested)
459
- collectRunLevel(paragraph, nested, out);
815
+ if (nested) {
816
+ // a revision inside a revision (a deletion of inserted text) was not measured: listed apart
817
+ const inner = [];
818
+ tokensOfRunLevel(paragraph, nested, inner);
819
+ if (inner.some((token) => token !== BREAK && token.target.kind === 'run'))
820
+ out.push(BREAK, ...inner, BREAK);
821
+ else
822
+ out.push(...inner.filter((token) => token !== BREAK));
823
+ }
460
824
  continue;
461
825
  }
462
826
  if (typeNameOf(el) === 'org_docx4j_wml.R') {
827
+ // a run whose formatting changed is a piece of its own; one that did not keeps pieces apart
463
828
  const run = el.value;
464
829
  if ((_a = run.rPr) === null || _a === void 0 ? void 0 : _a.rPrChange)
465
830
  out.push(new TrackedChange({ kind: 'runProperties', run, value: run.rPr.rPrChange }, paragraph));
831
+ else if (((_b = run.content) !== null && _b !== void 0 ? _b : []).length > 0)
832
+ out.push(BREAK);
466
833
  continue;
467
834
  }
468
835
  const nested = runItemsOf(el.value);
469
836
  if (nested)
470
- collectRunLevel(paragraph, nested, out);
837
+ tokensOfRunLevel(paragraph, nested, out);
838
+ }
839
+ }
840
+ /**
841
+ * What a piece groups as: a `w:ins` or `w:del` around runs, or an inserted or deleted mark; a run's
842
+ * `w:rPrChange` (check 28: bold on an italic run and a plain one, one change). Moves and a
843
+ * paragraph's `w:pPrChange` do not group.
844
+ */
845
+ function groupable(change) {
846
+ const t = change.target;
847
+ if (t.kind === 'mark')
848
+ return t.mark;
849
+ if (t.kind === 'run' && (t.revision === 'ins' || t.revision === 'del'))
850
+ return t.revision;
851
+ if (t.kind === 'runProperties' || t.kind === 'markProperties')
852
+ return 'format';
853
+ return undefined;
854
+ }
855
+ /**
856
+ * Tokens made into the changes Office JS lists: consecutive pieces of one kind (inserted, deleted, or
857
+ * a run's formatting) by one author, nothing unrevised between them, are one change (checks 22, 23
858
+ * and 28: text and marks alike, dates regardless, formatting whatever it recorded; another author, or
859
+ * one kind against another, starts a new one). A change that does not group - a paragraph's
860
+ * properties, a move - is listed where it stands and does not break a group. A group of one piece is
861
+ * that piece.
862
+ */
863
+ export function groupTouching(tokens) {
864
+ const out = [];
865
+ let open;
866
+ for (const token of tokens) {
867
+ if (token === BREAK) {
868
+ open = undefined;
869
+ continue;
870
+ }
871
+ const kind = groupable(token);
872
+ if (kind === undefined) {
873
+ out.push(token);
874
+ continue;
875
+ }
876
+ const last = open === null || open === void 0 ? void 0 : open[open.length - 1];
877
+ if (open && last && groupable(last) === kind && last.author === token.author) {
878
+ open.push(token);
879
+ continue;
880
+ }
881
+ open = [token];
882
+ out.push(open);
471
883
  }
884
+ return out.map((item) => {
885
+ if (!Array.isArray(item))
886
+ return item;
887
+ const first = item[0];
888
+ if (item.length === 1)
889
+ return first;
890
+ return new TrackedChange({ kind: 'group', revision: groupable(first), pieces: item, value: first.target.value }, first.paragraph);
891
+ });
472
892
  }
473
893
  /**
474
894
  * The `w:trPr/w:ins` and `w:trPr/w:del` revisions of a table row. `inner` is the revision markup
@@ -483,3 +903,143 @@ export function trackedChangesOfRow(tr, owner, inner = []) {
483
903
  out.push(new TrackedChange({ kind: 'row', row: 'del', tr, owner, value: trPr.del, inner }, undefined));
484
904
  return out;
485
905
  }
906
+ // --- CR-002 section 35: tables, cells, marks and sections --------------------------------------------
907
+ /** The change a table's property records make, one per table, or undefined when they make none. */
908
+ export function tablePropertiesChangeOf(tbl) {
909
+ const records = tableRecordsOf(tbl.value);
910
+ return records ? new TrackedChange({ kind: 'tableProperties', tbl, value: records.value }, undefined) : undefined;
911
+ }
912
+ /** A cell's own revision, `w:cellIns` or `w:cellDel`, or undefined. */
913
+ export function cellChangeOf(tc, owner, tr) {
914
+ const tcPr = tc.value.tcPr;
915
+ if (tcPr === null || tcPr === void 0 ? void 0 : tcPr.cellIns)
916
+ return new TrackedChange({ kind: 'cell', cell: 'ins', tc, owner, tr, value: tcPr.cellIns }, undefined);
917
+ if (tcPr === null || tcPr === void 0 ? void 0 : tcPr.cellDel)
918
+ return new TrackedChange({ kind: 'cell', cell: 'del', tc, owner, tr, value: tcPr.cellDel }, undefined);
919
+ return undefined;
920
+ }
921
+ /** The change to the body's last section (`w:body/w:sectPr/w:sectPrChange`), its text the section's paragraphs'. */
922
+ export function bodySectionChangeOf(container) {
923
+ var _a;
924
+ const sectPr = container.sectPr;
925
+ if (!(sectPr === null || sectPr === void 0 ? void 0 : sectPr.sectPrChange))
926
+ return undefined;
927
+ const items = (_a = container.content) !== null && _a !== void 0 ? _a : [];
928
+ return new TrackedChange({ kind: 'sectionProperties', sectPr, value: sectPr.sectPrChange, text: sectionText(items, items.length) }, undefined);
929
+ }
930
+ /**
931
+ * A cell taken out of its row, the cell before it (else after it) widened over its grid columns, as
932
+ * Word did rejecting an inserted cell and accepting a deleted one (check 27: `w:gridSpan` 2 and the
933
+ * two widths added).
934
+ */
935
+ function removeCell(tc, owner) {
936
+ var _a, _b, _c, _d, _e, _f, _g;
937
+ var _h;
938
+ const i = owner.indexOf(tc);
939
+ if (i < 0)
940
+ return;
941
+ const cells = owner.filter((el) => typeNameOf(el) === 'org_docx4j_wml.Tc');
942
+ const at = cells.indexOf(tc);
943
+ const neighbour = (_a = cells[at - 1]) !== null && _a !== void 0 ? _a : cells[at + 1];
944
+ owner.splice(i, 1);
945
+ if (!neighbour)
946
+ return;
947
+ const removed = tc.value.tcPr;
948
+ const kept = ((_b = (_h = neighbour.value).tcPr) !== null && _b !== void 0 ? _b : (_h.tcPr = { TYPE_NAME: 'org_docx4j_wml.TcPr' }));
949
+ const span = (tcPr) => { var _a, _b; return Number((_b = (_a = tcPr === null || tcPr === void 0 ? void 0 : tcPr.gridSpan) === null || _a === void 0 ? void 0 : _a.val) !== null && _b !== void 0 ? _b : 1) || 1; };
950
+ kept.gridSpan = { ...((_c = kept.gridSpan) !== null && _c !== void 0 ? _c : {}), val: span(kept) + span(removed) };
951
+ if (((_d = kept.tcW) === null || _d === void 0 ? void 0 : _d.type) === 'dxa' && ((_e = removed === null || removed === void 0 ? void 0 : removed.tcW) === null || _e === void 0 ? void 0 : _e.type) === 'dxa') {
952
+ kept.tcW = { ...kept.tcW, w: Number((_f = kept.tcW.w) !== null && _f !== void 0 ? _f : 0) + Number((_g = removed.tcW.w) !== null && _g !== void 0 ? _g : 0) };
953
+ }
954
+ linkParents(kept, neighbour.value);
955
+ }
956
+ /** A mark's formatting put back as its `w:rPrChange` recorded it, its own revisions (inserted, deleted, moved) kept. */
957
+ function restoreMarkProperties(p, change) {
958
+ var _a, _b, _c;
959
+ const rPr = (_a = p.pPr) === null || _a === void 0 ? void 0 : _a.rPr;
960
+ if (!rPr)
961
+ return;
962
+ const keep = ['ins', 'del', 'moveFrom', 'moveTo'];
963
+ const target = rPr;
964
+ for (const key of Object.keys(target))
965
+ if (key !== 'TYPE_NAME' && key !== 'PARENT' && !keep.includes(key))
966
+ delete target[key];
967
+ const original = rPrFromElements((_c = (_b = change.rPr) === null || _b === void 0 ? void 0 : _b.egrPrBase) !== null && _c !== void 0 ? _c : []);
968
+ for (const [key, value] of Object.entries(original))
969
+ if (key !== 'TYPE_NAME' && key !== 'PARENT')
970
+ target[key] = value;
971
+ if (p.pPr)
972
+ linkParents(rPr, p.pPr);
973
+ pruneParagraphProperties(p);
974
+ }
975
+ /**
976
+ * A section's properties put back as its `w:sectPrChange` recorded them. Word records only what
977
+ * changed, and rejecting laid those over the section, the rest left as it stood (check 18's saved
978
+ * file: the margins came back, the page size and columns stayed); a recorded `w:docGrid` of pitch 0
979
+ * stands for none, and Word wrote none (check 18, and check 27's section break).
980
+ */
981
+ function restoreSection(sectPr, change) {
982
+ var _a, _b;
983
+ const target = sectPr;
984
+ for (const [key, value] of Object.entries((_a = change.sectPr) !== null && _a !== void 0 ? _a : {})) {
985
+ if (key !== 'TYPE_NAME' && key !== 'PARENT')
986
+ target[key] = deepCopy(value);
987
+ }
988
+ const grid = sectPr.docGrid;
989
+ if (grid && Number((_b = grid.linePitch) !== null && _b !== void 0 ? _b : 0) === 0 && grid.type === undefined && grid.charSpace === undefined)
990
+ delete sectPr.docGrid;
991
+ delete sectPr.sectPrChange;
992
+ linkParents(sectPr, sectPr.PARENT);
993
+ }
994
+ /**
995
+ * What `acceptAll` and `rejectAll` resolve that is never listed: a `w:cellMerge`, which Office JS
996
+ * cannot list (check 27: `getTrackedChanges` throws); a `w:numberingChange`, which Word no longer
997
+ * writes and drops when one is put in, and whose record is the old number's text, so that a reject
998
+ * too can only drop it; and a table's records that change nothing. A merge rejected gets the merge it
999
+ * recorded back (docx4j's reading; unmeasured).
1000
+ */
1001
+ export function resolveUnlisted(items, what) {
1002
+ const visit = (list) => {
1003
+ var _a;
1004
+ for (const el of list !== null && list !== void 0 ? list : []) {
1005
+ const tn = typeNameOf(el);
1006
+ if (tn === 'org_docx4j_wml.P') {
1007
+ const numPr = (_a = el.value.pPr) === null || _a === void 0 ? void 0 : _a.numPr;
1008
+ if (numPr === null || numPr === void 0 ? void 0 : numPr.numberingChange)
1009
+ delete numPr.numberingChange;
1010
+ continue;
1011
+ }
1012
+ if (tn === 'org_docx4j_wml.Tbl') {
1013
+ const tbl = el.value;
1014
+ if (hasTableRecords(tbl) && !tableRecordsOf(tbl))
1015
+ acceptTable(tbl);
1016
+ for (const row of rowsOf(tbl)) {
1017
+ for (const cell of cellsOf(row.element.value)) {
1018
+ const tcPr = cell.element.value.tcPr;
1019
+ if (tcPr === null || tcPr === void 0 ? void 0 : tcPr.cellMerge)
1020
+ resolveCellMerge(tcPr, what);
1021
+ visit(childrenOf(cell.element.value));
1022
+ }
1023
+ }
1024
+ continue;
1025
+ }
1026
+ const v = el.value;
1027
+ if (typeof v === 'object' && v !== null)
1028
+ visit(childrenOf(v));
1029
+ }
1030
+ };
1031
+ visit(items);
1032
+ }
1033
+ function resolveCellMerge(tcPr, what) {
1034
+ const change = tcPr.cellMerge;
1035
+ delete tcPr.cellMerge;
1036
+ if (what === 'accept')
1037
+ return;
1038
+ const orig = change.vMergeOrig;
1039
+ if (orig === 'rest')
1040
+ tcPr.vMerge = { val: 'restart' };
1041
+ else if (orig === 'cont')
1042
+ tcPr.vMerge = {};
1043
+ else
1044
+ delete tcPr.vMerge;
1045
+ }