@docx4j/core-ts 0.2.2 → 0.3.1

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,12 @@
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, itemTextOf } from './tree.mjs';
4
+ import { removeMarkers } from './comments.mjs';
4
5
  import { Range } from './Range.mjs';
5
6
  import { rPrFromElements } from '@docx4j/generated-objects-ts/builders/wml';
6
- import { dateOf, toRestoredText, restoreRPr, restorePPr, pruneParagraphProperties } from './tracking.mjs';
7
+ import { revisionDateOf, toRestoredText, toDeletedText, restoreRPr, restorePPr, pruneParagraphProperties, HOLDER_TYPES, holderItemsOf } from './tracking.mjs';
8
+ import { tableRecordsOf, hasTableRecords, tableChangeText, rowText as tableRowText, acceptTable, rejectTable } from './tableRevisions.mjs';
9
+ import { deepCopy } from '@docx4j/generated-objects-ts';
7
10
  /**
8
11
  * A subset of Office JS `Word.TrackedChange`: `type`, `author`, `date`, `text`, `accept()`,
9
12
  * `reject()` and `getRange()`, plus `element`, `id` and `target` as extensions. A view, like
@@ -23,15 +26,22 @@ export class TrackedChange {
23
26
  case 'run': return t.revision === 'ins' || t.revision === 'moveTo' ? 'Added' : 'Deleted';
24
27
  case 'mark': return t.mark === 'ins' ? 'Added' : 'Deleted';
25
28
  case 'row': return t.row === 'ins' ? 'Added' : 'Deleted';
29
+ case 'group': return t.revision === 'ins' ? 'Added' : t.revision === 'del' ? 'Deleted' : 'Formatted';
30
+ case 'cell': return t.cell === 'ins' ? 'Added' : 'Deleted';
31
+ case 'holder': return 'Added';
26
32
  default: return 'Formatted';
27
33
  }
28
34
  }
29
35
  get author() {
30
36
  return this.target.value.author;
31
37
  }
32
- /** `w:date`, when the markup carries one. */
38
+ /**
39
+ * When the change was made: `w16du:dateUtc` if the markup has it, else `w:date` read as Word writes
40
+ * it, local wall-clock time (`revisionDateOf`, CR-002 section 29); a group's is its first piece's,
41
+ * as Office JS reports it.
42
+ */
33
43
  get date() {
34
- return dateOf(this.target.value.date);
44
+ return revisionDateOf(this.target.value);
35
45
  }
36
46
  /** The annotation id (`w:id`; an extension). */
37
47
  get id() {
@@ -39,13 +49,29 @@ export class TrackedChange {
39
49
  }
40
50
  /** The markup this change is over (an extension): the `w:ins` element, the `w:rPrChange`, ... */
41
51
  get element() {
42
- return this.target.kind === 'run' ? this.target.element : this.target.kind === 'row' ? this.target.tr : this.target.value;
52
+ const t = this.target;
53
+ switch (t.kind) {
54
+ case 'run': return t.element;
55
+ case 'row': return t.tr;
56
+ case 'group': return t.pieces[0].element;
57
+ case 'tableProperties': return t.tbl;
58
+ case 'cell': return t.tc;
59
+ case 'sectionProperties': return t.sectPr;
60
+ case 'holder': return t.holder;
61
+ default: return t.value;
62
+ }
43
63
  }
44
- /** The text the change covers; '' for a paragraph mark, whose range is the paragraph. */
64
+ /** The text the change covers; `\r` for a paragraph mark, as Office JS gives it (check 22). */
45
65
  get text() {
46
66
  var _a, _b;
47
67
  const t = this.target;
48
68
  switch (t.kind) {
69
+ case 'mark': return '\r';
70
+ case 'markProperties': return '\r';
71
+ case 'group': return t.pieces.map((piece) => piece.text).join('');
72
+ case 'tableProperties': return tableChangeText(t.tbl.value);
73
+ case 'cell': return tableRowText(t.tr);
74
+ case 'sectionProperties': return t.text;
49
75
  case 'run': return segmentsOf(t.element.value, { view: t.revision === 'ins' || t.revision === 'moveTo' ? 'accepted' : 'original' }).map((s) => s.text).join('');
50
76
  case 'runProperties': return textOf(t.run);
51
77
  case 'paragraphProperties': return (_b = (_a = this.paragraph) === null || _a === void 0 ? void 0 : _a.text) !== null && _b !== void 0 ? _b : '';
@@ -65,6 +91,13 @@ export class TrackedChange {
65
91
  const [start, end] = spanOf(p, t.element);
66
92
  return new Range(p, start, end);
67
93
  }
94
+ if (t.kind === 'group') {
95
+ // 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());
97
+ if (here.length === 0)
98
+ return t.pieces[0].getRange();
99
+ return new Range(p, Math.min(...here.map((r) => r.start)), Math.max(...here.map((r) => r.end)));
100
+ }
68
101
  if (t.kind === 'runProperties') {
69
102
  const segs = p.segments().filter((s) => s.run === t.run);
70
103
  if (segs.length === 0)
@@ -78,16 +111,16 @@ export class TrackedChange {
78
111
  * Either half of a move keeps the whole move, as Word's Review tab does (CR-002 section 29).
79
112
  */
80
113
  accept() {
81
- var _a, _b, _c;
114
+ var _a, _b, _c, _d, _e, _f;
82
115
  const t = this.target;
83
116
  switch (t.kind) {
84
117
  case 'run':
85
- if ((t.revision === 'moveFrom' || t.revision === 'moveTo') && resolveMove(this, 'accept'))
118
+ if ((t.revision === 'moveFrom' || t.revision === 'moveTo') && (resolveMove(this, 'accept') || resolveRangelessHalf(this, 'accept')))
86
119
  return;
87
120
  if (t.revision === 'ins' || t.revision === 'moveTo')
88
121
  unwrap(t);
89
122
  else
90
- remove(t.owner, t.element);
123
+ removeRevision(t, this.paragraph);
91
124
  return;
92
125
  case 'mark':
93
126
  if (t.mark === 'ins')
@@ -109,10 +142,37 @@ export class TrackedChange {
109
142
  // w:trPr/w:ins; an accepted deletion takes the row and everything in it away
110
143
  if (t.row === 'ins') {
111
144
  (_c = t.tr.value.trPr) === null || _c === void 0 ? true : delete _c.ins;
145
+ pruneRowProperties(t.tr.value);
112
146
  applyInner(t.inner, 'accept');
113
147
  }
114
148
  else
115
- remove(t.owner, t.tr);
149
+ removeRow(t);
150
+ return;
151
+ case 'group':
152
+ resolvePieces(t.pieces, 'accept');
153
+ return;
154
+ case 'tableProperties':
155
+ acceptTable(t.tbl.value);
156
+ return;
157
+ case 'cell':
158
+ // an inserted cell stays, a deleted one goes (check 27)
159
+ if (t.cell === 'ins')
160
+ (_d = t.tc.value.tcPr) === null || _d === void 0 ? true : delete _d.cellIns;
161
+ else
162
+ removeCell(t.tc, t.owner);
163
+ return;
164
+ case 'markProperties': {
165
+ const p = this.requireParagraph().p;
166
+ (_f = (_e = p.pPr) === null || _e === void 0 ? void 0 : _e.rPr) === null || _f === void 0 ? true : delete _f.rPrChange;
167
+ pruneParagraphProperties(p);
168
+ return;
169
+ }
170
+ case 'sectionProperties':
171
+ delete t.sectPr.sectPrChange;
172
+ return;
173
+ case 'holder':
174
+ // the element stays, the markers go (check 30: accept all leaves the control, no marker)
175
+ removeHolderMarkers(t);
116
176
  return;
117
177
  }
118
178
  }
@@ -121,14 +181,14 @@ export class TrackedChange {
121
181
  * Either half of a move rejects the whole move, as Word's Review tab does (CR-002 section 29).
122
182
  */
123
183
  reject() {
124
- var _a;
184
+ var _a, _b;
125
185
  const t = this.target;
126
186
  switch (t.kind) {
127
187
  case 'run':
128
- if ((t.revision === 'moveFrom' || t.revision === 'moveTo') && resolveMove(this, 'reject'))
188
+ if ((t.revision === 'moveFrom' || t.revision === 'moveTo') && (resolveMove(this, 'reject') || resolveRangelessHalf(this, 'reject')))
129
189
  return;
130
190
  if (t.revision === 'ins' || t.revision === 'moveTo')
131
- remove(t.owner, t.element);
191
+ removeRevision(t, this.paragraph);
132
192
  else
133
193
  restoreDeleted(t);
134
194
  return;
@@ -142,8 +202,10 @@ export class TrackedChange {
142
202
  restoreRPr(t.run, rPrFromElements(t.value.rPr));
143
203
  return;
144
204
  case 'paragraphProperties': {
145
- const p = this.requireParagraph().p;
146
- restorePPr(p, t.value.pPr);
205
+ const paragraph = this.requireParagraph();
206
+ restorePPr(paragraph.p, t.value.pPr);
207
+ dropLevelIndent(paragraph);
208
+ writeRecordedLevel(paragraph.p);
147
209
  return;
148
210
  }
149
211
  case 'row':
@@ -151,10 +213,35 @@ export class TrackedChange {
151
213
  // the w:del unwrapped, the deleted marks dropped); a rejected insertion takes it away
152
214
  if (t.row === 'del') {
153
215
  (_a = t.tr.value.trPr) === null || _a === void 0 ? true : delete _a.del;
216
+ pruneRowProperties(t.tr.value);
154
217
  applyInner(t.inner, 'reject');
155
218
  }
156
219
  else
157
- remove(t.owner, t.tr);
220
+ removeRow(t);
221
+ return;
222
+ case 'group':
223
+ resolvePieces(t.pieces, 'reject');
224
+ return;
225
+ case 'tableProperties':
226
+ rejectTable(t.tbl.value);
227
+ return;
228
+ case 'cell':
229
+ if (t.cell === 'del')
230
+ (_b = t.tc.value.tcPr) === null || _b === void 0 ? true : delete _b.cellDel;
231
+ else
232
+ removeCell(t.tc, t.owner);
233
+ return;
234
+ case 'markProperties':
235
+ restoreMarkProperties(this.requireParagraph().p, t.value);
236
+ return;
237
+ case 'sectionProperties':
238
+ restoreSection(t.sectPr, t.value);
239
+ return;
240
+ case 'holder':
241
+ // the element goes, its content stays where it stood: what is left of it after the pieces
242
+ // inside were rejected (nothing, for content inserted with it; check 30: reject all removes the
243
+ // control, and an existing control, which has no markers, stays)
244
+ unwrapHolder(t);
158
245
  return;
159
246
  }
160
247
  }
@@ -171,31 +258,268 @@ export class TrackedChange {
171
258
  * is all `w:delText`).
172
259
  */
173
260
  function rowText(tr, view) {
174
- const lines = [];
175
- const visit = (items) => {
176
- for (const el of items !== null && items !== void 0 ? items : []) {
177
- if (typeNameOf(el) === 'org_docx4j_wml.P') {
178
- lines.push(textOfView(el.value, { view }));
179
- continue;
261
+ // Office JS's text for a row: its cells with a tab between them, then `\r\n`; a cell's paragraphs
262
+ // with `\r` between them, a nested table's rows each with their own `\r\n` and no separator of their
263
+ // own (checks 30 and 31: "row one\r\n", "nested\r\nouter cell\r\n")
264
+ const cellText = (tc) => {
265
+ var _a, _b;
266
+ let out = '';
267
+ let afterParagraph = false;
268
+ for (const el of (_a = childrenOf(tc)) !== null && _a !== void 0 ? _a : []) {
269
+ const tn = typeNameOf(el);
270
+ if (tn === 'org_docx4j_wml.P') {
271
+ out += (afterParagraph ? '\r' : '') + textOfView(el.value, { view });
272
+ afterParagraph = true;
273
+ }
274
+ else if (tn === 'org_docx4j_wml.Tbl') {
275
+ for (const row of rowsOf(el.value))
276
+ out += rowText(row.element.value, view);
277
+ afterParagraph = false;
278
+ }
279
+ else if (typeof el.value === 'object' && el.value !== null) {
280
+ // a block control or custom XML element: what it holds, as if it were not there
281
+ for (const inner of (_b = childrenOf(el.value)) !== null && _b !== void 0 ? _b : []) {
282
+ if (typeNameOf(inner) === 'org_docx4j_wml.P') {
283
+ out += (afterParagraph ? '\r' : '') + textOfView(inner.value, { view });
284
+ afterParagraph = true;
285
+ }
286
+ }
180
287
  }
181
- const v = el.value;
182
- if (typeof v === 'object' && v !== null)
183
- visit(childrenOf(v));
184
288
  }
289
+ return out;
185
290
  };
186
- for (const cell of cellsOf(tr))
187
- visit(childrenOf(cell.element.value));
188
- return lines.join('\n');
291
+ return cellsOf(tr).map((cell) => cellText(cell.element.value)).join('\t') + '\r\n';
189
292
  }
190
293
  /** The changes inside a row, last first, so that a paragraph join never disturbs one still to do. */
191
294
  function applyInner(inner, what) {
192
295
  for (let i = inner.length - 1; i >= 0; i--)
193
296
  inner[i][what]();
194
297
  }
298
+ /**
299
+ * Word records a list paragraph's indent in `w:pPrChange` even where the list level gives it, and on
300
+ * reject writes no indent back: it restored "The first numbered item." to its numbered list with no
301
+ * `w:ind`, where the change recorded `left=720 hanging=360`, the level's own (`test/README.md` check
302
+ * 18, `reject-all.docx`, section 9), and check 25's level-0 items with none where it recorded only
303
+ * `hanging=360`, part of the level's `left=720 hanging=360` (`25b-bullets-reject.docx`; CR-002
304
+ * section 29). So a restored `w:ind` whose every attribute is the level's is dropped, leaving the level
305
+ * to give it; one with an attribute of its own - a genuinely direct indent - is restored. The numbering
306
+ * part must have been read (`await pkg.getBody()` or `getPropertyResolver()` reads it); when it has
307
+ * not, the recorded indent is restored as it was.
308
+ */
309
+ function dropLevelIndent(paragraph) {
310
+ var _a;
311
+ const pPr = paragraph.p.pPr;
312
+ const numPr = pPr === null || pPr === void 0 ? void 0 : pPr.numPr;
313
+ const ind = pPr === null || pPr === void 0 ? void 0 : pPr.ind;
314
+ if (!pPr || !numPr || !ind)
315
+ return;
316
+ const part = (_a = paragraph.parentBody.package_) === null || _a === void 0 ? void 0 : _a.numberingDefinitionsPart;
317
+ let level;
318
+ try {
319
+ level = part === null || part === void 0 ? void 0 : part.getInd(numPr);
320
+ }
321
+ catch {
322
+ return; // not read: nothing to compare with
323
+ }
324
+ if (!level || !indentWithin(ind, level))
325
+ return;
326
+ delete pPr.ind;
327
+ pruneParagraphProperties(paragraph.p);
328
+ }
329
+ /**
330
+ * A restored `w:numPr` that names a list but no level is at level 0, and Word writes the 0: it
331
+ * restored the level recorded, not the current one, when the two differed (check 25's
332
+ * `25e-bullet-demote.docx`, recorded `numId 1` alone and current `ilvl 1`, rejected to `ilvl 0,
333
+ * numId 1`), and wrote `w:ilvl="0"` every time the record had none (check 18's section 9, 25b, 25f;
334
+ * CR-002 section 29).
335
+ */
336
+ function writeRecordedLevel(p) {
337
+ var _a;
338
+ const numPr = (_a = p.pPr) === null || _a === void 0 ? void 0 : _a.numPr;
339
+ if (!(numPr === null || numPr === void 0 ? void 0 : numPr.numId) || Number(numPr.numId.val) === 0 || numPr.ilvl)
340
+ return;
341
+ numPr.ilvl = { TYPE_NAME: 'org_docx4j_wml.PPrBase.NumPr.Ilvl', val: 0 };
342
+ linkParents(numPr.ilvl, numPr);
343
+ }
344
+ /** True when every attribute `ind` sets, `level` sets to the same value. */
345
+ function indentWithin(ind, level) {
346
+ const attributes = Object.entries(ind).filter(([key, value]) => key !== 'TYPE_NAME' && key !== 'PARENT' && value !== undefined);
347
+ return attributes.length > 0 && attributes.every(([key, value]) => String(level[key]) === String(value));
348
+ }
349
+ /**
350
+ * A group's pieces resolved: its runs last first, then its marks last first, so that a paragraph join
351
+ * never strands a piece still to do. Office JS and Word's Review tab resolve the group as one (checks
352
+ * 22 and 23), and so, piece by piece, does this.
353
+ */
354
+ function resolvePieces(pieces, what) {
355
+ const runs = pieces.filter((piece) => piece.target.kind !== 'mark');
356
+ const marks = pieces.filter((piece) => piece.target.kind === 'mark');
357
+ for (let i = runs.length - 1; i >= 0; i--)
358
+ runs[i][what]();
359
+ for (let i = marks.length - 1; i >= 0; i--)
360
+ marks[i][what]();
361
+ }
362
+ /**
363
+ * The list an element is in now: `owner` as the change was listed, else the list its `PARENT` keeps -
364
+ * a piece listed inside a holder that was unwrapped since (its content moved out into the owner,
365
+ * check 30) is still found.
366
+ */
367
+ function currentOwner(owner, element) {
368
+ var _a, _b;
369
+ if (owner.includes(element))
370
+ return owner;
371
+ const parent = (_a = element.value) === null || _a === void 0 ? void 0 : _a.PARENT;
372
+ const actual = parent ? (_b = runItemsOf(parent)) !== null && _b !== void 0 ? _b : childrenOf(parent) : undefined;
373
+ return (actual === null || actual === void 0 ? void 0 : actual.includes(element)) ? actual : owner;
374
+ }
195
375
  function remove(owner, element) {
196
- const i = owner.indexOf(element);
376
+ const list = currentOwner(owner, element);
377
+ const i = list.indexOf(element);
197
378
  if (i >= 0)
198
- owner.splice(i, 1);
379
+ list.splice(i, 1);
380
+ }
381
+ /** A holder's four insertion markers taken away; the element stays. */
382
+ function removeHolderMarkers(t) {
383
+ var _a;
384
+ remove(t.owner, t.outer[0]);
385
+ remove(t.owner, t.outer[1]);
386
+ const items = (_a = holderItemsOf(t.holder.value)) !== null && _a !== void 0 ? _a : [];
387
+ remove(items, t.inner[0]);
388
+ remove(items, t.inner[1]);
389
+ }
390
+ /** A holder's insertion rejected: the markers go, and its content takes its place in the owner. */
391
+ function unwrapHolder(t) {
392
+ var _a;
393
+ removeHolderMarkers(t);
394
+ const owner = currentOwner(t.owner, t.holder);
395
+ const i = owner.indexOf(t.holder);
396
+ if (i < 0)
397
+ return;
398
+ const items = (_a = holderItemsOf(t.holder.value)) !== null && _a !== void 0 ? _a : [];
399
+ const moved = items.splice(0); // the holder keeps nothing
400
+ owner.splice(i, 1, ...moved);
401
+ linkParents(moved, t.holder.value.PARENT);
402
+ }
403
+ /**
404
+ * The insertion of a content control or custom XML element, when `items[i]` is a
405
+ * `w:customXmlInsRangeStart` and what follows is Word's form for one (check 30): the element, whose
406
+ * content starts with that marker's `w:customXmlInsRangeEnd` and ends with a second start, and that
407
+ * one's end right after the element. Anything else is not one (a marker on its own is left alone).
408
+ */
409
+ export function holderInsertionAt(items, i, paragraph) {
410
+ var _a, _b, _c, _d, _e, _f;
411
+ const start = items[i];
412
+ if (((_a = start.name) === null || _a === void 0 ? void 0 : _a.localPart) !== 'customXmlInsRangeStart')
413
+ return undefined;
414
+ const holder = items[i + 1];
415
+ const outerEnd = items[i + 2];
416
+ if (!holder || !outerEnd || !HOLDER_TYPES.has((_b = typeNameOf(holder)) !== null && _b !== void 0 ? _b : '') || ((_c = outerEnd.name) === null || _c === void 0 ? void 0 : _c.localPart) !== 'customXmlInsRangeEnd')
417
+ return undefined;
418
+ const inner = (_d = childrenOf(holder.value)) !== null && _d !== void 0 ? _d : [];
419
+ const innerEnd = inner[0];
420
+ const innerStart = inner[inner.length - 1];
421
+ if (inner.length < 2 || !innerEnd || !innerStart || ((_e = innerEnd.name) === null || _e === void 0 ? void 0 : _e.localPart) !== 'customXmlInsRangeEnd' || ((_f = innerStart.name) === null || _f === void 0 ? void 0 : _f.localPart) !== 'customXmlInsRangeStart')
422
+ return undefined;
423
+ const idOf = (marker) => marker.value.id;
424
+ if (idOf(innerEnd) !== idOf(start) || idOf(outerEnd) !== idOf(innerStart))
425
+ return undefined;
426
+ return new TrackedChange({ kind: 'holder', holder, owner: items, outer: [start, outerEnd], inner: [innerEnd, innerStart], value: start.value }, paragraph);
427
+ }
428
+ /**
429
+ * A row taken away, and its table with it when it was the last: a table has no revision of its own,
430
+ * so one whose rows were all inserted was inserted, and one whose rows were all deleted was deleted.
431
+ * A `w:tbl` left with no rows is not a table any more (CR-002 section 37).
432
+ */
433
+ function removeRow(t) {
434
+ remove(t.owner, t.tr);
435
+ if (t.table && rowsOf(t.table.element.value).length === 0)
436
+ remove(t.table.container, t.table.element);
437
+ }
438
+ /**
439
+ * A revision taken away with its runs - a rejected insertion, an accepted deletion - and the comments
440
+ * on it, by where their markers were (CR-002 section 38 item 1):
441
+ * - Word writes a comment's range start inside the `w:ins` of the text it is on; when that text goes,
442
+ * current Word removes the comment with it - markers, reference run and entry (check 32, run
443
+ * 2026-10-02: rejecting the insertion, rejecting all, and accepting the deletion in the same shape,
444
+ * each left no comment). A comment whose start comes out of the revision with no text left in its
445
+ * range is removed here: the markers and the reference now, the entries in the comment parts when
446
+ * the comments are next read or the package saved (`pendingCommentRemovals`).
447
+ * - A comment whose markers lie outside the revision (this package's own form before 0.3.1, and any
448
+ * file's) is kept when its text goes: Word 2010 and Word 15, rejecting all in the editor's
449
+ * `review-for-word.docx`, left its reference run alone, the range markers dropped and the entry kept
450
+ * (the editor's finding of 2026-10-02). The range markers go here, the reference and the entry stay.
451
+ * A comment with text left in its range keeps its markers where the text was (not measured), and
452
+ * current Word's answer for the outside form, and the older Words' for the inside one, are not measured.
453
+ */
454
+ function removeRevision(t, paragraph) {
455
+ var _a, _b, _c;
456
+ const owner = currentOwner(t.owner, t.element);
457
+ const i = owner.indexOf(t.element);
458
+ if (i < 0)
459
+ return;
460
+ const kept = ((_a = runItemsOf(t.element.value)) !== null && _a !== void 0 ? _a : []).filter((item) => { var _a, _b; return ((_a = item.name) === null || _a === void 0 ? void 0 : _a.localPart) === 'commentRangeStart' || ((_b = item.name) === null || _b === void 0 ? void 0 : _b.localPart) === 'commentRangeEnd'; });
461
+ // the comments whose range held the revision: a start inside it (Word's form), or one before it
462
+ // with its end after it (the engine's own, the markers beside the runs)
463
+ const idOf = (el) => el.value.id;
464
+ const inside = new Set(kept.filter((m) => { var _a; return ((_a = m.name) === null || _a === void 0 ? void 0 : _a.localPart) === 'commentRangeStart'; }).map(idOf));
465
+ const outside = new Set();
466
+ for (let s = 0; s < i; s++) {
467
+ const el = owner[s];
468
+ if (((_b = el.name) === null || _b === void 0 ? void 0 : _b.localPart) !== 'commentRangeStart')
469
+ continue;
470
+ const id = idOf(el);
471
+ 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
+ outside.add(id);
473
+ }
474
+ owner.splice(i, 1, ...kept);
475
+ linkParents(kept, t.element.value.PARENT);
476
+ const body = paragraph === null || paragraph === void 0 ? void 0 : paragraph.parentBody;
477
+ if (!body)
478
+ return;
479
+ for (const id of [...inside, ...outside]) {
480
+ const start = owner.find((el) => { var _a; return ((_a = el.name) === null || _a === void 0 ? void 0 : _a.localPart) === 'commentRangeStart' && idOf(el) === id; });
481
+ if (!start || !rangeIsEmpty(owner, start, id))
482
+ continue;
483
+ if (inside.has(id)) {
484
+ removeMarkers(body.container, id);
485
+ const pkg = body.package_;
486
+ (_c = pkg === null || pkg === void 0 ? void 0 : pkg.pendingCommentRemovals) === null || _c === void 0 ? void 0 : _c.add(id);
487
+ }
488
+ else {
489
+ // the range markers only: the reference run and the entry stay (Word 2010 and 15)
490
+ const end = owner.find((el) => { var _a; return ((_a = el.name) === null || _a === void 0 ? void 0 : _a.localPart) === 'commentRangeEnd' && idOf(el) === id; });
491
+ for (const marker of [end, start])
492
+ if (marker)
493
+ owner.splice(owner.indexOf(marker), 1);
494
+ }
495
+ }
496
+ }
497
+ /** True when nothing with text stands between a comment's range start and its end in the same list. */
498
+ function rangeIsEmpty(owner, start, id) {
499
+ var _a;
500
+ const from = owner.indexOf(start);
501
+ if (from < 0)
502
+ return false;
503
+ for (let i = from + 1; i < owner.length; i++) {
504
+ const el = owner[i];
505
+ if (((_a = el.name) === null || _a === void 0 ? void 0 : _a.localPart) === 'commentRangeEnd' && el.value.id === id)
506
+ return true;
507
+ if (hasText(el))
508
+ return false;
509
+ }
510
+ return false; // the end is elsewhere: text may be
511
+ }
512
+ function hasText(el) {
513
+ var _a, _b;
514
+ const v = el.value;
515
+ if (typeof v !== 'object' || v === null)
516
+ return false;
517
+ if (typeNameOf(el) === 'org_docx4j_wml.R')
518
+ return ((_a = v.content) !== null && _a !== void 0 ? _a : []).some((item) => itemTextOf(item) !== undefined);
519
+ const kind = revisionKindOf(el);
520
+ if (kind === 'del' || kind === 'moveFrom')
521
+ return false; // deleted text is no text in the accepted view
522
+ return ((_b = runItemsOf(v)) !== null && _b !== void 0 ? _b : []).some(hasText);
199
523
  }
200
524
  /** A `w:ins` or `w:moveTo` accepted: its runs take its place. */
201
525
  function unwrap(t) {
@@ -239,7 +563,7 @@ function dropMark(paragraph, which) {
239
563
  */
240
564
  export function joinWithNext(paragraph, fallbackToPrevious = false) {
241
565
  var _a, _b, _c, _d, _e;
242
- const container = paragraph.container;
566
+ const container = currentOwner(paragraph.container, paragraph.element);
243
567
  const i = container.indexOf(paragraph.element);
244
568
  const next = i >= 0 ? container[i + 1] : undefined;
245
569
  const p = paragraph.p;
@@ -270,8 +594,28 @@ export function joinWithNext(paragraph, fallbackToPrevious = false) {
270
594
  p.pPr = nextP.pPr;
271
595
  if (p.pPr)
272
596
  linkParents(p.pPr, p);
597
+ // The mark that survives is the next paragraph's, and so is the paragraph's identity: Word keeps the
598
+ // second paragraph's w14:paraId when a mark goes (check 18's accept-all and reject-all, check 22's
599
+ // 22-accept-whole; found by the editor's C5). Its w14:textId and session ids come with it; Word
600
+ // writes a new textId, the text having changed, which Word will do again when it next saves.
601
+ for (const key of PARAGRAPH_ATTRIBUTES) {
602
+ const value = nextP[key];
603
+ if (value === undefined)
604
+ delete p[key];
605
+ else
606
+ p[key] = value;
607
+ }
273
608
  container.splice(i + 1, 1);
274
609
  }
610
+ /** A `w:p`'s own attributes, which go with its mark. */
611
+ const PARAGRAPH_ATTRIBUTES = ['paraId', 'textId', 'rsidDel', 'rsidP', 'rsidR', 'rsidRDefault', 'rsidRPr', 'noSpellErr'];
612
+ /** A row's `w:trPr` with nothing left in it goes, as Word writes none (check 18's accept-all and reject-all). */
613
+ function pruneRowProperties(tr) {
614
+ var _a;
615
+ const trPr = tr.trPr;
616
+ if (trPr && !trPr.ins && !trPr.del && !trPr.trPrChange && !((_a = trPr.cnfStyleOrDivIdOrGridBefore) === null || _a === void 0 ? void 0 : _a.length))
617
+ delete tr.trPr;
618
+ }
275
619
  /** The span of a run-level revision in the paragraph's accepted text; collapsed for a deletion. */
276
620
  function spanOf(paragraph, element) {
277
621
  const segs = paragraph.segments();
@@ -327,19 +671,33 @@ function resolveMove(change, action) {
327
671
  const t = change.target;
328
672
  if (t.kind !== 'run' || !change.paragraph)
329
673
  return false;
330
- const pieces = movePiecesOf(storyOf(change.paragraph.p));
674
+ const story = storyOf(change.paragraph.p);
675
+ const pieces = movePiecesOf(story);
331
676
  const half = pieces.find((piece) => piece.kind === 'half' && piece.element === t.element);
332
677
  if (!half || half.kind !== 'half' || half.names.length === 0)
333
678
  return false;
334
679
  const name = half.names[0];
335
680
  const ids = new Set(pieces.filter((piece) => piece.kind === 'start' && piece.name === name).map((piece) => piece.id));
336
681
  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';
682
+ || ((piece.kind === 'half' || piece.kind === 'mark' || piece.kind === 'inner') && piece.names.includes(name)));
683
+ // A destination whose source is gone is kept whichever way it is resolved: Word rejected such a
684
+ // move and left its text, plain, where it stood (test/README.md check 20 case 12).
685
+ const orphan = !mine.some((piece) => piece.side === 'from');
686
+ const kept = action === 'accept' || orphan ? 'to' : 'from';
687
+ const broken = orphan || !mine.some((piece) => piece.side === 'to');
339
688
  // the markers first: a range end between two paragraphs would stop the paragraph join below
340
689
  for (const piece of mine)
341
690
  if (piece.kind === 'start' || piece.kind === 'end')
342
691
  remove(piece.owner, piece.element);
692
+ // Rejected, a move takes with it everything typed into its destination: Word's default when moved
693
+ // text was edited (the Tracked Moves Conflict Dialog's "keep original location text", check 20
694
+ // cases 01 and 04). Text typed before the range starts is outside the move and stays (case 03).
695
+ // What accepting does to text typed into the source was not measured, so it is left alone.
696
+ if (kept === 'from') {
697
+ for (const piece of mine)
698
+ if (piece.kind === 'inner' && piece.side === 'to')
699
+ remove(piece.owner, piece.element);
700
+ }
343
701
  for (const piece of mine) {
344
702
  if (piece.kind !== 'half')
345
703
  continue;
@@ -374,6 +732,8 @@ function resolveMove(change, action) {
374
732
  joinWithNext({ container: mark.container, element: mark.paragraph, p }, mark.side === 'to');
375
733
  }
376
734
  }
735
+ if (broken)
736
+ dissolveBrokenMoves(story);
377
737
  return true;
378
738
  }
379
739
  /** The story a paragraph is in: the outermost array of block content above it (a body, a header, a note). */
@@ -390,7 +750,7 @@ function storyOf(p) {
390
750
  function movePiecesOf(story) {
391
751
  const out = [];
392
752
  const open = { from: new Map(), to: new Map() };
393
- const visit = (items) => {
753
+ const visit = (items, runLevel = false) => {
394
754
  var _a, _b, _c, _d, _e, _f;
395
755
  for (const el of items) {
396
756
  const marker = RANGE_MARKERS[(_b = (_a = el.name) === null || _a === void 0 ? void 0 : _a.localPart) !== null && _b !== void 0 ? _b : ''];
@@ -412,12 +772,18 @@ function movePiecesOf(story) {
412
772
  const side = revision === 'moveFrom' ? 'from' : 'to';
413
773
  out.push({ kind: 'half', side, element: el, owner: items, names: [...open[side].values()] });
414
774
  }
775
+ else if (runLevel) {
776
+ for (const side of ['from', 'to']) {
777
+ if (open[side].size > 0)
778
+ out.push({ kind: 'inner', side, element: el, owner: items, names: [...open[side].values()] });
779
+ }
780
+ }
415
781
  const v = el.value;
416
782
  if (typeof v !== 'object' || v === null)
417
783
  continue;
418
784
  const children = (_e = childrenOf(v)) !== null && _e !== void 0 ? _e : runItemsOf(v);
419
785
  if (children)
420
- visit(children);
786
+ visit(children, typeNameOf(el) === 'org_docx4j_wml.P');
421
787
  if (typeNameOf(el) === 'org_docx4j_wml.P') {
422
788
  const rPr = (_f = v.pPr) === null || _f === void 0 ? void 0 : _f.rPr;
423
789
  for (const side of ['from', 'to']) {
@@ -431,55 +797,486 @@ function movePiecesOf(story) {
431
797
  visit(story);
432
798
  return out;
433
799
  }
800
+ /**
801
+ * A move half in no named range - its range markers gone - resolved as Word resolved one (check 20
802
+ * cases 13 and 14, where Word showed it as an insertion or a deletion): a `w:moveTo` as an insertion,
803
+ * a `w:moveFrom` as a deletion, its paragraph mark with it, so that a paragraph whose text and mark
804
+ * both go goes whole. Then the rest of the broken pair is dissolved (`dissolveBrokenMoves`).
805
+ */
806
+ function resolveRangelessHalf(change, action) {
807
+ var _a, _b, _c;
808
+ const t = change.target;
809
+ const paragraph = change.paragraph;
810
+ if (t.kind !== 'run' || (t.revision !== 'moveTo' && t.revision !== 'moveFrom') || !paragraph)
811
+ return false;
812
+ const story = storyOf(paragraph.p);
813
+ const pieces = movePiecesOf(story);
814
+ const half = pieces.find((piece) => piece.kind === 'half' && piece.element === t.element);
815
+ if (!half || half.kind !== 'half' || half.names.length > 0)
816
+ return false;
817
+ const side = half.side;
818
+ const key = side === 'from' ? 'moveFrom' : 'moveTo';
819
+ const keep = (side === 'to') === (action === 'accept'); // an insertion accepted, a deletion rejected
820
+ if (!keep)
821
+ remove(t.owner, t.element);
822
+ else if (side === 'to')
823
+ unwrap(t);
824
+ else
825
+ restoreDeleted(t);
826
+ const mark = pieces.find((piece) => piece.kind === 'mark' && piece.side === side && piece.paragraph === paragraph.element && piece.names.length === 0);
827
+ const p = paragraph.p;
828
+ if (mark && ((_b = (_a = p.pPr) === null || _a === void 0 ? void 0 : _a.rPr) === null || _b === void 0 ? void 0 : _b[key])) {
829
+ if (keep) {
830
+ delete p.pPr.rPr[key];
831
+ pruneParagraphProperties(p);
832
+ }
833
+ else if (((_c = p.content) !== null && _c !== void 0 ? _c : []).length === 0) {
834
+ remove(paragraph.container, paragraph.element);
835
+ }
836
+ else {
837
+ delete p.pPr.rPr[key];
838
+ joinWithNext(paragraph, side === 'to');
839
+ }
840
+ }
841
+ dissolveBrokenMoves(story);
842
+ return true;
843
+ }
844
+ /**
845
+ * Every move half and moved paragraph mark in the story that cannot be paired - in no named range,
846
+ * or in one whose `w:name` no range of the other side carries - turned into the plain `w:ins` or
847
+ * `w:del` it stands for, pending, its range markers kept. That is how Word saved each broken pair
848
+ * in check 20 (re-run, cases 12 to 14, saved unchanged) and what it left of the other side once one
849
+ * was accepted or rejected. It runs after a broken move is resolved, and only then, so a document's
850
+ * other moves are left as they are; but a broken pair's halves cannot be told from another's, so
851
+ * this takes them all.
852
+ */
853
+ function dissolveBrokenMoves(story) {
854
+ var _a;
855
+ const pieces = movePiecesOf(story);
856
+ const named = { from: new Set(), to: new Set() };
857
+ for (const piece of pieces)
858
+ if (piece.kind === 'start')
859
+ named[piece.side].add(piece.name);
860
+ const unpaired = (side, names) => {
861
+ const other = named[side === 'from' ? 'to' : 'from'];
862
+ return names.every((name) => !other.has(name)); // true for no names too
863
+ };
864
+ for (const piece of pieces) {
865
+ if (piece.kind === 'half' && unpaired(piece.side, piece.names))
866
+ toPlainRevision(piece.element, piece.owner, piece.side === 'from' ? 'del' : 'ins');
867
+ if (piece.kind === 'mark' && unpaired(piece.side, piece.names)) {
868
+ const rPr = (_a = piece.paragraph.value.pPr) === null || _a === void 0 ? void 0 : _a.rPr;
869
+ if (piece.side === 'from' && (rPr === null || rPr === void 0 ? void 0 : rPr.moveFrom)) {
870
+ rPr.del = rPr.moveFrom;
871
+ delete rPr.moveFrom;
872
+ }
873
+ if (piece.side === 'to' && (rPr === null || rPr === void 0 ? void 0 : rPr.moveTo)) {
874
+ rPr.ins = rPr.moveTo;
875
+ delete rPr.moveTo;
876
+ }
877
+ }
878
+ }
879
+ }
880
+ /**
881
+ * A `w:moveFrom` turned into the plain `w:del` of the same runs, their text become deleted text, or a
882
+ * `w:moveTo` into the plain `w:ins`.
883
+ */
884
+ function toPlainRevision(element, owner, kind) {
885
+ var _a;
886
+ const value = element.value;
887
+ const items = (_a = runItemsOf(element.value)) !== null && _a !== void 0 ? _a : [];
888
+ if (kind === 'del')
889
+ for (const item of items)
890
+ if (typeNameOf(item) === 'org_docx4j_wml.R')
891
+ toDeletedText(item.value);
892
+ const plain = {
893
+ name: { namespaceURI: W_NS, localPart: kind },
894
+ value: { TYPE_NAME: kind === 'del' ? 'org_docx4j_wml.RunDel' : 'org_docx4j_wml.RunIns', id: value.id, author: value.author, date: value.date, customXmlOrSmartTagOrSdt: items },
895
+ };
896
+ const i = owner.indexOf(element);
897
+ if (i < 0)
898
+ return;
899
+ owner.splice(i, 1, plain);
900
+ linkParents(items, plain.value);
901
+ linkParents(plain, value.PARENT);
902
+ }
434
903
  // --- collecting ----------------------------------------------------------------------------
435
- /** The tracked changes of one paragraph, in document order. */
904
+ /**
905
+ * The tracked changes of one paragraph, in document order, touching pieces grouped as Office JS
906
+ * groups them (`groupTouching`). Office JS lists the changes that start in the paragraph (check 22):
907
+ * pieces that carry on a change from the paragraph before - its inserted or deleted mark - are left
908
+ * to that paragraph, and a change running on into the next is cut at this paragraph's end.
909
+ */
436
910
  export function trackedChangesOfParagraph(paragraph) {
437
- var _a, _b, _c;
911
+ const tokens = trackedChangeTokensOfParagraph(paragraph);
912
+ const before = markBefore(paragraph);
913
+ if (before) {
914
+ for (let i = 0; i < tokens.length;) {
915
+ const token = tokens[i];
916
+ if (token === BREAK)
917
+ break;
918
+ const kind = groupable(token);
919
+ if (kind === undefined) {
920
+ i++;
921
+ continue;
922
+ }
923
+ if (kind !== before.kind || token.author !== before.author)
924
+ break;
925
+ tokens.splice(i, 1);
926
+ }
927
+ }
928
+ return groupTouching(tokens);
929
+ }
930
+ /** The last revision on the mark of the paragraph before this one in its container, if any. */
931
+ function markBefore(paragraph) {
932
+ var _a;
933
+ const i = paragraph.container.indexOf(paragraph.element);
934
+ const previous = i > 0 ? paragraph.container[i - 1] : undefined;
935
+ if (!previous || typeNameOf(previous) !== 'org_docx4j_wml.P')
936
+ return undefined;
937
+ const rPr = (_a = previous.value.pPr) === null || _a === void 0 ? void 0 : _a.rPr;
938
+ if (rPr === null || rPr === void 0 ? void 0 : rPr.del)
939
+ return { kind: 'del', author: rPr.del.author };
940
+ if (rPr === null || rPr === void 0 ? void 0 : rPr.ins)
941
+ return { kind: 'ins', author: rPr.ins.author };
942
+ if (rPr === null || rPr === void 0 ? void 0 : rPr.rPrChange)
943
+ return { kind: 'format', author: rPr.rPrChange.author };
944
+ return undefined;
945
+ }
946
+ /** Between two tracked changes: something unrevised - text, a paragraph mark, a table - that keeps them apart. */
947
+ export const BREAK = 'break';
948
+ /**
949
+ * A paragraph's single changes in document order, its mark last, with a `BREAK` wherever unrevised
950
+ * content stands between two of them: `groupTouching` makes the groups from these.
951
+ */
952
+ export function trackedChangeTokensOfParagraph(paragraph) {
953
+ var _a, _b, _c, _d;
438
954
  const out = [];
439
955
  const p = paragraph.p;
440
956
  const pPrChange = (_a = p.pPr) === null || _a === void 0 ? void 0 : _a.pPrChange;
441
957
  if (pPrChange)
442
958
  out.push(new TrackedChange({ kind: 'paragraphProperties', value: pPrChange }, paragraph));
443
- const rPr = (_b = p.pPr) === null || _b === void 0 ? void 0 : _b.rPr;
959
+ tokensOfRunLevel(paragraph, ((_b = p.content) !== null && _b !== void 0 ? _b : []), out);
960
+ const rPr = (_c = p.pPr) === null || _c === void 0 ? void 0 : _c.rPr;
961
+ // a mark whose formatting changed is a piece, grouping with its runs' (check 27: "Before.\r")
962
+ if (rPr === null || rPr === void 0 ? void 0 : rPr.rPrChange)
963
+ out.push(new TrackedChange({ kind: 'markProperties', value: rPr.rPrChange }, paragraph));
444
964
  if (rPr === null || rPr === void 0 ? void 0 : rPr.ins)
445
965
  out.push(new TrackedChange({ kind: 'mark', mark: 'ins', value: rPr.ins }, paragraph));
446
966
  if (rPr === null || rPr === void 0 ? void 0 : rPr.del)
447
967
  out.push(new TrackedChange({ kind: 'mark', mark: 'del', value: rPr.del }, paragraph));
448
- collectRunLevel(paragraph, ((_c = p.content) !== null && _c !== void 0 ? _c : []), out);
968
+ 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))
969
+ out.push(BREAK);
970
+ const sectPr = (_d = p.pPr) === null || _d === void 0 ? void 0 : _d.sectPr;
971
+ if (sectPr === null || sectPr === void 0 ? void 0 : sectPr.sectPrChange) {
972
+ out.push(new TrackedChange({ kind: 'sectionProperties', sectPr, value: sectPr.sectPrChange, text: `${sectionText(paragraph.container, paragraph.container.indexOf(paragraph.element) + 1)}\f` }, paragraph));
973
+ }
449
974
  return out;
450
975
  }
451
- function collectRunLevel(paragraph, items, out) {
976
+ /**
977
+ * The text of the section that ends at `end` (exclusive) in a container: its paragraphs' text, a `\r`
978
+ * between them, back to the previous section break.
979
+ */
980
+ function sectionText(items, end) {
452
981
  var _a;
453
- for (const el of items) {
982
+ let from = end - 1;
983
+ 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)))
984
+ from--;
985
+ return items.slice(Math.max(from, 0), end).filter((el) => typeNameOf(el) === 'org_docx4j_wml.P')
986
+ .map((el) => textOfView(el.value, { view: 'accepted' })).join('\r');
987
+ }
988
+ function tokensOfRunLevel(paragraph, items, out) {
989
+ var _a, _b, _c;
990
+ for (let i = 0; i < items.length; i++) {
991
+ const el = items[i];
992
+ // an inline control or custom XML element that is itself an insertion: a piece, then what is in it
993
+ const inserted = holderInsertionAt(items, i, paragraph);
994
+ if (inserted) {
995
+ out.push(inserted);
996
+ tokensOfRunLevel(paragraph, (_a = childrenOf(items[i + 1].value)) !== null && _a !== void 0 ? _a : [], out);
997
+ i += 2;
998
+ continue;
999
+ }
454
1000
  const kind = revisionKindOf(el);
455
1001
  if (kind !== undefined) {
1002
+ // moved text is content Office JS cannot list (checks 16, 17), and is kept apart
1003
+ const move = kind === 'moveFrom' || kind === 'moveTo';
1004
+ if (move)
1005
+ out.push(BREAK);
456
1006
  out.push(new TrackedChange({ kind: 'run', revision: kind, element: el, owner: items, value: el.value }, paragraph));
1007
+ if (move)
1008
+ out.push(BREAK);
457
1009
  const nested = runItemsOf(el.value);
458
- if (nested)
459
- collectRunLevel(paragraph, nested, out);
1010
+ if (nested) {
1011
+ // a revision inside a revision (a deletion of inserted text) was not measured: listed apart
1012
+ const inner = [];
1013
+ tokensOfRunLevel(paragraph, nested, inner);
1014
+ if (inner.some((token) => token !== BREAK && token.target.kind === 'run'))
1015
+ out.push(BREAK, ...inner, BREAK);
1016
+ else
1017
+ out.push(...inner.filter((token) => token !== BREAK));
1018
+ }
460
1019
  continue;
461
1020
  }
462
1021
  if (typeNameOf(el) === 'org_docx4j_wml.R') {
1022
+ // a run whose formatting changed is a piece of its own; one that did not keeps pieces apart -
1023
+ // if it has text: a run holding only a comment reference does not (check 32: one author's two
1024
+ // w:ins with the reference run between them listed as one change)
463
1025
  const run = el.value;
464
- if ((_a = run.rPr) === null || _a === void 0 ? void 0 : _a.rPrChange)
1026
+ if ((_b = run.rPr) === null || _b === void 0 ? void 0 : _b.rPrChange)
465
1027
  out.push(new TrackedChange({ kind: 'runProperties', run, value: run.rPr.rPrChange }, paragraph));
1028
+ else if (((_c = run.content) !== null && _c !== void 0 ? _c : []).some((item) => itemTextOf(item) !== undefined))
1029
+ out.push(BREAK);
466
1030
  continue;
467
1031
  }
1032
+ // a run holder that is not itself an insertion - a hyperlink, a field, a control or custom XML
1033
+ // element that was there - keeps the insertions inside it apart from those outside: Office JS
1034
+ // listed "before ", "link" and " after" as three changes over a w:ins in a w:hyperlink, and the same
1035
+ // over a w:fldSimple and an unmarked inline w:sdt (check 30, part C), against one change over an
1036
+ // inserted control (above) and over Word's own one-w:ins forms
468
1037
  const nested = runItemsOf(el.value);
469
- if (nested)
470
- collectRunLevel(paragraph, nested, out);
1038
+ if (nested) {
1039
+ out.push(BREAK);
1040
+ tokensOfRunLevel(paragraph, nested, out);
1041
+ out.push(BREAK);
1042
+ }
1043
+ }
1044
+ }
1045
+ /**
1046
+ * What a piece groups as: a `w:ins` or `w:del` around runs, or an inserted or deleted mark; a run's
1047
+ * `w:rPrChange` (check 28: bold on an italic run and a plain one, one change). Moves and a
1048
+ * paragraph's `w:pPrChange` do not group.
1049
+ */
1050
+ function groupable(change) {
1051
+ const t = change.target;
1052
+ if (t.kind === 'mark')
1053
+ return t.mark;
1054
+ if (t.kind === 'run' && (t.revision === 'ins' || t.revision === 'del'))
1055
+ return t.revision;
1056
+ if (t.kind === 'runProperties' || t.kind === 'markProperties')
1057
+ return 'format';
1058
+ if (t.kind === 'holder')
1059
+ return 'ins';
1060
+ // an inserted or deleted row groups with rows and text touching it: Office JS lists an inserted
1061
+ // table of three rows, with the inserted paragraphs before and after it, as one change, and rows
1062
+ // added to a table that was there as one change per touching run of them (check 31)
1063
+ if (t.kind === 'row')
1064
+ return t.row;
1065
+ return undefined;
1066
+ }
1067
+ /**
1068
+ * Tokens made into the changes Office JS lists: consecutive pieces of one kind (inserted, deleted, or
1069
+ * a run's formatting) by one author, nothing unrevised between them, are one change (checks 22, 23
1070
+ * and 28: text and marks alike, dates regardless, formatting whatever it recorded; another author, or
1071
+ * one kind against another, starts a new one). A change that does not group - a paragraph's
1072
+ * properties, a move - is listed where it stands and does not break a group. A group of one piece is
1073
+ * that piece.
1074
+ */
1075
+ export function groupTouching(tokens) {
1076
+ const out = [];
1077
+ let open;
1078
+ for (const token of tokens) {
1079
+ if (token === BREAK) {
1080
+ open = undefined;
1081
+ continue;
1082
+ }
1083
+ const kind = groupable(token);
1084
+ if (kind === undefined) {
1085
+ out.push(token);
1086
+ continue;
1087
+ }
1088
+ const last = open === null || open === void 0 ? void 0 : open[open.length - 1];
1089
+ if (open && last && groupable(last) === kind && last.author === token.author) {
1090
+ open.push(token);
1091
+ continue;
1092
+ }
1093
+ open = [token];
1094
+ out.push(open);
471
1095
  }
1096
+ return out.map((item) => {
1097
+ if (!Array.isArray(item))
1098
+ return item;
1099
+ const first = item[0];
1100
+ if (item.length === 1)
1101
+ return first;
1102
+ return new TrackedChange({ kind: 'group', revision: groupable(first), pieces: item, value: first.target.value }, first.paragraph);
1103
+ });
472
1104
  }
473
1105
  /**
474
1106
  * The `w:trPr/w:ins` and `w:trPr/w:del` revisions of a table row. `inner` is the revision markup
475
1107
  * of the row's own cells, which the row's change owns rather than reporting separately.
476
1108
  */
477
- export function trackedChangesOfRow(tr, owner, inner = []) {
1109
+ export function trackedChangesOfRow(tr, owner, inner = [], table) {
478
1110
  const trPr = tr.value.trPr;
479
1111
  const out = [];
480
1112
  if (trPr === null || trPr === void 0 ? void 0 : trPr.ins)
481
- out.push(new TrackedChange({ kind: 'row', row: 'ins', tr, owner, value: trPr.ins, inner }, undefined));
1113
+ out.push(new TrackedChange({ kind: 'row', row: 'ins', tr, owner, value: trPr.ins, inner, table }, undefined));
482
1114
  if (trPr === null || trPr === void 0 ? void 0 : trPr.del)
483
- out.push(new TrackedChange({ kind: 'row', row: 'del', tr, owner, value: trPr.del, inner }, undefined));
1115
+ out.push(new TrackedChange({ kind: 'row', row: 'del', tr, owner, value: trPr.del, inner, table }, undefined));
484
1116
  return out;
485
1117
  }
1118
+ // --- CR-002 section 35: tables, cells, marks and sections --------------------------------------------
1119
+ /** The change a table's property records make, one per table, or undefined when they make none. */
1120
+ export function tablePropertiesChangeOf(tbl) {
1121
+ const records = tableRecordsOf(tbl.value);
1122
+ return records ? new TrackedChange({ kind: 'tableProperties', tbl, value: records.value }, undefined) : undefined;
1123
+ }
1124
+ /** A cell's own revision, `w:cellIns` or `w:cellDel`, or undefined. */
1125
+ export function cellChangeOf(tc, owner, tr) {
1126
+ const tcPr = tc.value.tcPr;
1127
+ 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);
1129
+ 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);
1131
+ return undefined;
1132
+ }
1133
+ /** The change to the body's last section (`w:body/w:sectPr/w:sectPrChange`), its text the section's paragraphs'. */
1134
+ export function bodySectionChangeOf(container) {
1135
+ var _a;
1136
+ const sectPr = container.sectPr;
1137
+ if (!(sectPr === null || sectPr === void 0 ? void 0 : sectPr.sectPrChange))
1138
+ return undefined;
1139
+ const items = (_a = container.content) !== null && _a !== void 0 ? _a : [];
1140
+ return new TrackedChange({ kind: 'sectionProperties', sectPr, value: sectPr.sectPrChange, text: sectionText(items, items.length) }, undefined);
1141
+ }
1142
+ /**
1143
+ * A cell taken out of its row, the cell before it (else after it) widened over its grid columns, as
1144
+ * Word did rejecting an inserted cell and accepting a deleted one (check 27: `w:gridSpan` 2 and the
1145
+ * two widths added).
1146
+ */
1147
+ function removeCell(tc, owner) {
1148
+ var _a, _b, _c, _d, _e, _f, _g;
1149
+ var _h;
1150
+ const i = owner.indexOf(tc);
1151
+ if (i < 0)
1152
+ return;
1153
+ const cells = owner.filter((el) => typeNameOf(el) === 'org_docx4j_wml.Tc');
1154
+ const at = cells.indexOf(tc);
1155
+ const neighbour = (_a = cells[at - 1]) !== null && _a !== void 0 ? _a : cells[at + 1];
1156
+ owner.splice(i, 1);
1157
+ if (!neighbour)
1158
+ return;
1159
+ const removed = tc.value.tcPr;
1160
+ const kept = ((_b = (_h = neighbour.value).tcPr) !== null && _b !== void 0 ? _b : (_h.tcPr = { TYPE_NAME: 'org_docx4j_wml.TcPr' }));
1161
+ 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; };
1162
+ kept.gridSpan = { ...((_c = kept.gridSpan) !== null && _c !== void 0 ? _c : {}), val: span(kept) + span(removed) };
1163
+ 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') {
1164
+ 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) };
1165
+ }
1166
+ linkParents(kept, neighbour.value);
1167
+ }
1168
+ /** A mark's formatting put back as its `w:rPrChange` recorded it, its own revisions (inserted, deleted, moved) kept. */
1169
+ function restoreMarkProperties(p, change) {
1170
+ var _a, _b, _c;
1171
+ const rPr = (_a = p.pPr) === null || _a === void 0 ? void 0 : _a.rPr;
1172
+ if (!rPr)
1173
+ return;
1174
+ const keep = ['ins', 'del', 'moveFrom', 'moveTo'];
1175
+ const target = rPr;
1176
+ for (const key of Object.keys(target))
1177
+ if (key !== 'TYPE_NAME' && key !== 'PARENT' && !keep.includes(key))
1178
+ delete target[key];
1179
+ const original = rPrFromElements((_c = (_b = change.rPr) === null || _b === void 0 ? void 0 : _b.egrPrBase) !== null && _c !== void 0 ? _c : []);
1180
+ for (const [key, value] of Object.entries(original))
1181
+ if (key !== 'TYPE_NAME' && key !== 'PARENT')
1182
+ target[key] = value;
1183
+ if (p.pPr)
1184
+ linkParents(rPr, p.pPr);
1185
+ pruneParagraphProperties(p);
1186
+ }
1187
+ /**
1188
+ * A section's properties put back as its `w:sectPrChange` recorded them. Word records only what
1189
+ * changed, and rejecting laid those over the section, the rest left as it stood (check 18's saved
1190
+ * file: the margins came back, the page size and columns stayed); a recorded `w:docGrid` of pitch 0
1191
+ * stands for none, and Word wrote none (check 18, and check 27's section break).
1192
+ */
1193
+ function restoreSection(sectPr, change) {
1194
+ var _a, _b;
1195
+ const target = sectPr;
1196
+ for (const [key, value] of Object.entries((_a = change.sectPr) !== null && _a !== void 0 ? _a : {})) {
1197
+ if (key !== 'TYPE_NAME' && key !== 'PARENT')
1198
+ target[key] = deepCopy(value);
1199
+ }
1200
+ const grid = sectPr.docGrid;
1201
+ if (grid && Number((_b = grid.linePitch) !== null && _b !== void 0 ? _b : 0) === 0 && grid.type === undefined && grid.charSpace === undefined)
1202
+ delete sectPr.docGrid;
1203
+ delete sectPr.sectPrChange;
1204
+ linkParents(sectPr, sectPr.PARENT);
1205
+ }
1206
+ /**
1207
+ * What `acceptAll` and `rejectAll` resolve that is never listed: a `w:cellMerge`, which Office JS
1208
+ * cannot list (check 27: `getTrackedChanges` throws); a `w:numberingChange`, which Word no longer
1209
+ * writes and drops when one is put in, and whose record is the old number's text, so that a reject
1210
+ * too can only drop it; and a table's records that change nothing. A merge rejected gets the merge it
1211
+ * recorded back (docx4j's reading; unmeasured). A `w:customXmlInsRangeStart` or `End` left without
1212
+ * the form `holderInsertionAt` lists marks nothing that can be found, and goes either way.
1213
+ */
1214
+ export function resolveUnlisted(items, what) {
1215
+ const visit = (list) => {
1216
+ var _a, _b;
1217
+ if (!list)
1218
+ return;
1219
+ for (let i = list.length - 1; i >= 0; i--) {
1220
+ const name = (_a = list[i].name) === null || _a === void 0 ? void 0 : _a.localPart;
1221
+ if (name === 'customXmlInsRangeStart' || name === 'customXmlInsRangeEnd')
1222
+ list.splice(i, 1);
1223
+ }
1224
+ for (const el of list) {
1225
+ const tn = typeNameOf(el);
1226
+ if (tn === 'org_docx4j_wml.P') {
1227
+ const numPr = (_b = el.value.pPr) === null || _b === void 0 ? void 0 : _b.numPr;
1228
+ if (numPr === null || numPr === void 0 ? void 0 : numPr.numberingChange)
1229
+ delete numPr.numberingChange;
1230
+ sweepMarkers(el.value.content);
1231
+ continue;
1232
+ }
1233
+ if (tn === 'org_docx4j_wml.Tbl') {
1234
+ const tbl = el.value;
1235
+ if (hasTableRecords(tbl) && !tableRecordsOf(tbl))
1236
+ acceptTable(tbl);
1237
+ for (const row of rowsOf(tbl)) {
1238
+ for (const cell of cellsOf(row.element.value)) {
1239
+ const tcPr = cell.element.value.tcPr;
1240
+ if (tcPr === null || tcPr === void 0 ? void 0 : tcPr.cellMerge)
1241
+ resolveCellMerge(tcPr, what);
1242
+ visit(childrenOf(cell.element.value));
1243
+ }
1244
+ }
1245
+ continue;
1246
+ }
1247
+ const v = el.value;
1248
+ if (typeof v === 'object' && v !== null)
1249
+ visit(childrenOf(v));
1250
+ }
1251
+ };
1252
+ visit(items);
1253
+ }
1254
+ /** Stray `w:customXmlInsRangeStart` / `End` markers taken out of run-level content, through its holders and revisions. */
1255
+ function sweepMarkers(items) {
1256
+ var _a;
1257
+ if (!items)
1258
+ return;
1259
+ for (let i = items.length - 1; i >= 0; i--) {
1260
+ const name = (_a = items[i].name) === null || _a === void 0 ? void 0 : _a.localPart;
1261
+ if (name === 'customXmlInsRangeStart' || name === 'customXmlInsRangeEnd') {
1262
+ items.splice(i, 1);
1263
+ continue;
1264
+ }
1265
+ const v = items[i].value;
1266
+ if (typeof v === 'object' && v !== null)
1267
+ sweepMarkers(runItemsOf(v));
1268
+ }
1269
+ }
1270
+ function resolveCellMerge(tcPr, what) {
1271
+ const change = tcPr.cellMerge;
1272
+ delete tcPr.cellMerge;
1273
+ if (what === 'accept')
1274
+ return;
1275
+ const orig = change.vMergeOrig;
1276
+ if (orig === 'rest')
1277
+ tcPr.vMerge = { val: 'restart' };
1278
+ else if (orig === 'cont')
1279
+ tcPr.vMerge = {};
1280
+ else
1281
+ delete tcPr.vMerge;
1282
+ }