@docx4j/core-ts 0.3.0 → 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,10 @@
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, rowsOf, childrenOf, W_NS } 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 { revisionDateOf, toRestoredText, toDeletedText, restoreRPr, restorePPr, pruneParagraphProperties } from './tracking.mjs';
7
+ import { revisionDateOf, toRestoredText, toDeletedText, restoreRPr, restorePPr, pruneParagraphProperties, HOLDER_TYPES, holderItemsOf } from './tracking.mjs';
7
8
  import { tableRecordsOf, hasTableRecords, tableChangeText, rowText as tableRowText, acceptTable, rejectTable } from './tableRevisions.mjs';
8
9
  import { deepCopy } from '@docx4j/generated-objects-ts';
9
10
  /**
@@ -27,6 +28,7 @@ export class TrackedChange {
27
28
  case 'row': return t.row === 'ins' ? 'Added' : 'Deleted';
28
29
  case 'group': return t.revision === 'ins' ? 'Added' : t.revision === 'del' ? 'Deleted' : 'Formatted';
29
30
  case 'cell': return t.cell === 'ins' ? 'Added' : 'Deleted';
31
+ case 'holder': return 'Added';
30
32
  default: return 'Formatted';
31
33
  }
32
34
  }
@@ -55,6 +57,7 @@ export class TrackedChange {
55
57
  case 'tableProperties': return t.tbl;
56
58
  case 'cell': return t.tc;
57
59
  case 'sectionProperties': return t.sectPr;
60
+ case 'holder': return t.holder;
58
61
  default: return t.value;
59
62
  }
60
63
  }
@@ -117,7 +120,7 @@ export class TrackedChange {
117
120
  if (t.revision === 'ins' || t.revision === 'moveTo')
118
121
  unwrap(t);
119
122
  else
120
- remove(t.owner, t.element);
123
+ removeRevision(t, this.paragraph);
121
124
  return;
122
125
  case 'mark':
123
126
  if (t.mark === 'ins')
@@ -143,7 +146,7 @@ export class TrackedChange {
143
146
  applyInner(t.inner, 'accept');
144
147
  }
145
148
  else
146
- remove(t.owner, t.tr);
149
+ removeRow(t);
147
150
  return;
148
151
  case 'group':
149
152
  resolvePieces(t.pieces, 'accept');
@@ -167,6 +170,10 @@ export class TrackedChange {
167
170
  case 'sectionProperties':
168
171
  delete t.sectPr.sectPrChange;
169
172
  return;
173
+ case 'holder':
174
+ // the element stays, the markers go (check 30: accept all leaves the control, no marker)
175
+ removeHolderMarkers(t);
176
+ return;
170
177
  }
171
178
  }
172
179
  /**
@@ -181,7 +188,7 @@ export class TrackedChange {
181
188
  if ((t.revision === 'moveFrom' || t.revision === 'moveTo') && (resolveMove(this, 'reject') || resolveRangelessHalf(this, 'reject')))
182
189
  return;
183
190
  if (t.revision === 'ins' || t.revision === 'moveTo')
184
- remove(t.owner, t.element);
191
+ removeRevision(t, this.paragraph);
185
192
  else
186
193
  restoreDeleted(t);
187
194
  return;
@@ -210,7 +217,7 @@ export class TrackedChange {
210
217
  applyInner(t.inner, 'reject');
211
218
  }
212
219
  else
213
- remove(t.owner, t.tr);
220
+ removeRow(t);
214
221
  return;
215
222
  case 'group':
216
223
  resolvePieces(t.pieces, 'reject');
@@ -230,6 +237,12 @@ export class TrackedChange {
230
237
  case 'sectionProperties':
231
238
  restoreSection(t.sectPr, t.value);
232
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);
245
+ return;
233
246
  }
234
247
  }
235
248
  requireParagraph() {
@@ -245,21 +258,37 @@ export class TrackedChange {
245
258
  * is all `w:delText`).
246
259
  */
247
260
  function rowText(tr, view) {
248
- const lines = [];
249
- const visit = (items) => {
250
- for (const el of items !== null && items !== void 0 ? items : []) {
251
- if (typeNameOf(el) === 'org_docx4j_wml.P') {
252
- lines.push(textOfView(el.value, { view }));
253
- 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
+ }
254
287
  }
255
- const v = el.value;
256
- if (typeof v === 'object' && v !== null)
257
- visit(childrenOf(v));
258
288
  }
289
+ return out;
259
290
  };
260
- for (const cell of cellsOf(tr))
261
- visit(childrenOf(cell.element.value));
262
- return lines.join('\n');
291
+ return cellsOf(tr).map((cell) => cellText(cell.element.value)).join('\t') + '\r\n';
263
292
  }
264
293
  /** The changes inside a row, last first, so that a paragraph join never disturbs one still to do. */
265
294
  function applyInner(inner, what) {
@@ -330,10 +359,167 @@ function resolvePieces(pieces, what) {
330
359
  for (let i = marks.length - 1; i >= 0; i--)
331
360
  marks[i][what]();
332
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
+ }
333
375
  function remove(owner, element) {
334
- const i = owner.indexOf(element);
376
+ const list = currentOwner(owner, element);
377
+ const i = list.indexOf(element);
335
378
  if (i >= 0)
336
- 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);
337
523
  }
338
524
  /** A `w:ins` or `w:moveTo` accepted: its runs take its place. */
339
525
  function unwrap(t) {
@@ -377,7 +563,7 @@ function dropMark(paragraph, which) {
377
563
  */
378
564
  export function joinWithNext(paragraph, fallbackToPrevious = false) {
379
565
  var _a, _b, _c, _d, _e;
380
- const container = paragraph.container;
566
+ const container = currentOwner(paragraph.container, paragraph.element);
381
567
  const i = container.indexOf(paragraph.element);
382
568
  const next = i >= 0 ? container[i + 1] : undefined;
383
569
  const p = paragraph.p;
@@ -800,8 +986,17 @@ function sectionText(items, end) {
800
986
  .map((el) => textOfView(el.value, { view: 'accepted' })).join('\r');
801
987
  }
802
988
  function tokensOfRunLevel(paragraph, items, out) {
803
- var _a, _b;
804
- for (const el of items) {
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
+ }
805
1000
  const kind = revisionKindOf(el);
806
1001
  if (kind !== undefined) {
807
1002
  // moved text is content Office JS cannot list (checks 16, 17), and is kept apart
@@ -824,17 +1019,27 @@ function tokensOfRunLevel(paragraph, items, out) {
824
1019
  continue;
825
1020
  }
826
1021
  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
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)
828
1025
  const run = el.value;
829
- 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)
830
1027
  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)
1028
+ else if (((_c = run.content) !== null && _c !== void 0 ? _c : []).some((item) => itemTextOf(item) !== undefined))
832
1029
  out.push(BREAK);
833
1030
  continue;
834
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
835
1037
  const nested = runItemsOf(el.value);
836
- if (nested)
1038
+ if (nested) {
1039
+ out.push(BREAK);
837
1040
  tokensOfRunLevel(paragraph, nested, out);
1041
+ out.push(BREAK);
1042
+ }
838
1043
  }
839
1044
  }
840
1045
  /**
@@ -850,6 +1055,13 @@ function groupable(change) {
850
1055
  return t.revision;
851
1056
  if (t.kind === 'runProperties' || t.kind === 'markProperties')
852
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;
853
1065
  return undefined;
854
1066
  }
855
1067
  /**
@@ -894,13 +1106,13 @@ export function groupTouching(tokens) {
894
1106
  * The `w:trPr/w:ins` and `w:trPr/w:del` revisions of a table row. `inner` is the revision markup
895
1107
  * of the row's own cells, which the row's change owns rather than reporting separately.
896
1108
  */
897
- export function trackedChangesOfRow(tr, owner, inner = []) {
1109
+ export function trackedChangesOfRow(tr, owner, inner = [], table) {
898
1110
  const trPr = tr.value.trPr;
899
1111
  const out = [];
900
1112
  if (trPr === null || trPr === void 0 ? void 0 : trPr.ins)
901
- 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));
902
1114
  if (trPr === null || trPr === void 0 ? void 0 : trPr.del)
903
- 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));
904
1116
  return out;
905
1117
  }
906
1118
  // --- CR-002 section 35: tables, cells, marks and sections --------------------------------------------
@@ -996,17 +1208,26 @@ function restoreSection(sectPr, change) {
996
1208
  * cannot list (check 27: `getTrackedChanges` throws); a `w:numberingChange`, which Word no longer
997
1209
  * writes and drops when one is put in, and whose record is the old number's text, so that a reject
998
1210
  * 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).
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.
1000
1213
  */
1001
1214
  export function resolveUnlisted(items, what) {
1002
1215
  const visit = (list) => {
1003
- var _a;
1004
- for (const el of list !== null && list !== void 0 ? 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) {
1005
1225
  const tn = typeNameOf(el);
1006
1226
  if (tn === 'org_docx4j_wml.P') {
1007
- const numPr = (_a = el.value.pPr) === null || _a === void 0 ? void 0 : _a.numPr;
1227
+ const numPr = (_b = el.value.pPr) === null || _b === void 0 ? void 0 : _b.numPr;
1008
1228
  if (numPr === null || numPr === void 0 ? void 0 : numPr.numberingChange)
1009
1229
  delete numPr.numberingChange;
1230
+ sweepMarkers(el.value.content);
1010
1231
  continue;
1011
1232
  }
1012
1233
  if (tn === 'org_docx4j_wml.Tbl') {
@@ -1030,6 +1251,22 @@ export function resolveUnlisted(items, what) {
1030
1251
  };
1031
1252
  visit(items);
1032
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
+ }
1033
1270
  function resolveCellMerge(tcPr, what) {
1034
1271
  const change = tcPr.cellMerge;
1035
1272
  delete tcPr.cellMerge;
@@ -13,7 +13,7 @@ export { contentOf, isFlatOpc, rewriteRelationshipIds, type OoxmlOptions } from
13
13
  export { type SearchOptions, searchPattern, findAll, matchesOf, expandReplacement } from './search.mjs';
14
14
  export { segmentsOf, runsOf, runItemsOf, childrenOf, rowsOf, cellsOf, BLOCK_LEVEL_TYPES, SDT_TYPES, runOf, paragraphOf, textOfView, revisionKindOf, type TextSegment, type Located, type TextView, type TextViewOptions, type RevisionKind, type RevisionHolder } from './tree.mjs';
15
15
  export { TrackedChange, trackedChangesOfParagraph, trackedChangesOfRow, joinWithNext, type TrackedChangeType, type TrackedChangeTarget } from './TrackedChange.mjs';
16
- export { ChangeTracker, trackerOf, calendarOf, dateOf, wordCalendarOf, revisionDateOf, copyRPr, markDeleted, markInserted, highestAnnotationId, MARKUP_TYPES, toDeletedText, toRestoredText, restoreRPr, restorePPr, trackInsertedParagraph, trackInsertedTable, wrapNewRuns, paraRPrOf, rowPrOf, pruneParagraphProperties, type ChangeTrackingMode, type TrackingHost, type TrackingScope, type TrackedPart, } from './tracking.mjs';
16
+ export { ChangeTracker, trackerOf, calendarOf, dateOf, wordCalendarOf, revisionDateOf, copyRPr, markDeleted, markInserted, highestAnnotationId, MARKUP_TYPES, toDeletedText, toRestoredText, restoreRPr, restorePPr, trackInsertedParagraph, trackInsertedTable, trackInsertedBlocks, wrapNewRuns, wrapInsertedRuns, paraRPrOf, rowPrOf, pruneParagraphProperties, type ChangeTrackingMode, type TrackingHost, type TrackingScope, type TrackedPart, } from './tracking.mjs';
17
17
  export { wml, wmlOne, p, r, t, br, tab, tbl, tr, tc, sdt, sdtPr, nextSdtId, sdtProperty, sdtKindOf, inlinePicture, rPrToElements, rPrFromElements, textOf, walk, walkAll, find, linkParents, applyRunOptions, readRunOptions, UNDERLINE_TO_WML, isElement, typeNameOf, W_NS, type Element, type Wrapper, type WmlOptions, type Raw, type Interpolated, type RunOptions, type RunFormatting, type ParagraphOptions, type TableOptions, type CellOptions, type SdtKind, type SdtForm, type SdtOptions } from '@docx4j/generated-objects-ts/builders/wml';
18
18
  export { StylesFacade, type StylePackageLike } from './stylesFacade.mjs';
19
19
  export { bindSnapshot, boundsOf, pathOf, type Bound, type BoundSnapshot } from './binder.mjs';
@@ -16,7 +16,7 @@ export { searchPattern, findAll, matchesOf, expandReplacement } from './search.m
16
16
  export { segmentsOf, runsOf, runItemsOf, childrenOf, rowsOf, cellsOf, BLOCK_LEVEL_TYPES, SDT_TYPES, runOf, paragraphOf, textOfView, revisionKindOf } from './tree.mjs';
17
17
  // change tracking (CR-002 phase F)
18
18
  export { TrackedChange, trackedChangesOfParagraph, trackedChangesOfRow, joinWithNext } from './TrackedChange.mjs';
19
- export { ChangeTracker, trackerOf, calendarOf, dateOf, wordCalendarOf, revisionDateOf, copyRPr, markDeleted, markInserted, highestAnnotationId, MARKUP_TYPES, toDeletedText, toRestoredText, restoreRPr, restorePPr, trackInsertedParagraph, trackInsertedTable, wrapNewRuns, paraRPrOf, rowPrOf, pruneParagraphProperties, } from './tracking.mjs';
19
+ export { ChangeTracker, trackerOf, calendarOf, dateOf, wordCalendarOf, revisionDateOf, copyRPr, markDeleted, markInserted, highestAnnotationId, MARKUP_TYPES, toDeletedText, toRestoredText, restoreRPr, restorePPr, trackInsertedParagraph, trackInsertedTable, trackInsertedBlocks, wrapNewRuns, wrapInsertedRuns, paraRPrOf, rowPrOf, pruneParagraphProperties, } from './tracking.mjs';
20
20
  // The objects package's builders, re-exported so that one import serves the content API and the tree it works on.
21
21
  export { wml, wmlOne, p, r, t, br, tab, tbl, tr, tc, sdt, sdtPr, nextSdtId, sdtProperty, sdtKindOf, inlinePicture, rPrToElements, rPrFromElements, textOf, walk, walkAll, find, linkParents, applyRunOptions, readRunOptions, UNDERLINE_TO_WML, isElement, typeNameOf, W_NS } from '@docx4j/generated-objects-ts/builders/wml';
22
22
  export { StylesFacade } from './stylesFacade.mjs';
@@ -3,6 +3,7 @@ import { linkParents } from './tree.mjs';
3
3
  import { parseXml } from '../../xml/dom.mjs';
4
4
  import { DEFAULT_STYLES_XML } from '../../parts/wml/defaultStyles.mjs';
5
5
  import { SPLICEABLE_STYLES_XML } from '../../parts/wml/spliceableStyles.mjs';
6
+ import { commentStyles } from '../../parts/wml/commentStyles.mjs';
6
7
  import { Docx4JException } from '../../opc/exceptions.mjs';
7
8
  /**
8
9
  * Where a definition is looked up, unmarshalled once and never handed out (a copy is spliced in):
@@ -15,7 +16,9 @@ function styleSources() {
15
16
  return (sources !== null && sources !== void 0 ? sources : (sources = (async () => {
16
17
  const read = async (xml) => { var _a; return (_a = (await unmarshalNode(parseXml(xml))).value.style) !== null && _a !== void 0 ? _a : []; };
17
18
  const defaults = await read(DEFAULT_STYLES_XML);
18
- return { defaults, all: [...defaults, ...(await read(SPLICEABLE_STYLES_XML))] };
19
+ // and Word's two comment styles, which the comment parts add when they are made, for a caller
20
+ // that makes the comment itself (CR-002 section 37)
21
+ return { defaults, all: [...defaults, ...(await read(SPLICEABLE_STYLES_XML)), ...commentStyles()] };
19
22
  })()));
20
23
  }
21
24
  /**
@@ -99,7 +102,7 @@ function closureOf(wanted, present, source) {
99
102
  seen.add(id);
100
103
  const template = source.find((s) => s.styleId === id);
101
104
  if (template === undefined) {
102
- throw new Docx4JException(`No definition to splice for the style ${id}: neither docx4j's default styles nor the nine it comments out carry one. Define it yourself on the styles part.`);
105
+ throw new Docx4JException(`No definition to splice for the style ${id}: neither docx4j's default styles, the nine it comments out nor the two comment styles carry one. Define it yourself on the styles part.`);
103
106
  }
104
107
  // what it is based on and linked to must be there too, and before it reads better in the part
105
108
  for (const dependency of [(_a = template.basedOn) === null || _a === void 0 ? void 0 : _a.val, (_b = template.link) === null || _b === void 0 ? void 0 : _b.val]) {
@@ -174,16 +174,62 @@ export declare class ChangeTracker {
174
174
  markRowDeleted(tr: wml.Tr): void;
175
175
  }
176
176
  /**
177
- * A whole paragraph inserted: the mark is marked inserted and every run of it goes into a
178
- * `w:ins`. Word, splitting a paragraph, marks the *first* paragraph's mark instead and leaves
179
- * the new one the original mark; marking the new paragraph's own mark is the same document once
177
+ * A whole paragraph inserted: the mark is marked inserted (unless it already is) and every run of it
178
+ * goes into a `w:ins` - the runs in its hyperlinks, inline content controls, fields and smart tags as
179
+ * well (`wrapNewRuns`). Word, splitting a paragraph, marks the *first* paragraph's mark instead and
180
+ * leaves the new one the original mark; marking the new paragraph's own mark is the same document once
180
181
  * accepted or rejected and keeps the edit to the element that was added (CR-002 section 13).
181
182
  */
182
183
  export declare function trackInsertedParagraph(tracker: ChangeTracker, p: wml.P): void;
183
- /** Every top-level run of the paragraph that is not already in a revision, wrapped in one `w:ins` per group. */
184
+ /**
185
+ * Every run of the paragraph not already in a revision, wrapped in one `w:ins` per group of touching
186
+ * runs - at the top level and inside the run-level holders, where Word's form decides (`test/README.md`
187
+ * check 30, CR-002 section 37 item 7): in a hyperlink or a simple field the `w:ins` goes inside, since
188
+ * neither can be in one (`CT_RunTrackChange` holds `EG_ContentRunContent` only); a smart tag goes
189
+ * *into* the `w:ins` with the runs beside it, its own runs plain; an inline content control or custom
190
+ * XML element keeps the `w:ins` inside and is itself marked inserted (`markHolderInserted`). Before,
191
+ * only the top level was wrapped, so that rejecting an inserted paragraph left a link's text behind
192
+ * (the editor's finding on 0.3.0, CR-002 section 37 item 4).
193
+ */
184
194
  export declare function wrapNewRuns(tracker: ChangeTracker, p: wml.P): void;
185
- /** A whole table inserted: every row is marked inserted and every paragraph in it is an insertion. */
195
+ /**
196
+ * `wrapNewRuns` over run-level items that are about to go into a paragraph (`Paragraph.insertItemsAt`):
197
+ * the runs among them, and those in their holders, wrapped in `w:ins`; a content control or custom
198
+ * XML element among them marked inserted. `parent` is what the items' array belongs to.
199
+ */
200
+ export declare function wrapInsertedRuns(tracker: ChangeTracker, items: Element[] | undefined, parent: object): void;
201
+ /** A whole table inserted: every row is marked inserted and everything in its cells is an insertion (`trackInsertedBlocks`). */
186
202
  export declare function trackInsertedTable(tracker: ChangeTracker, tbl: wml.Tbl): void;
203
+ /**
204
+ * Block content inserted, whatever it holds: a paragraph (`trackInsertedParagraph`), a table
205
+ * (`trackInsertedTable`: rows inside row-level content controls, nested tables in its cells), and a
206
+ * block-level content control's or custom XML element's content, recursively, the element itself
207
+ * marked inserted (`markHolderInserted`; `owner` is the array holding `block`, which takes the
208
+ * markers). What `Body.insertElement` and `Table.addRows` track new content by (CR-002 section 37).
209
+ */
210
+ export declare function trackInsertedBlock(tracker: ChangeTracker, block: Element, owner: Element[]): void;
211
+ /** `trackInsertedBlock` over every item of a list, which is their owner. */
212
+ export declare function trackInsertedBlocks(tracker: ChangeTracker, items: Element[] | undefined): void;
213
+ /**
214
+ * The elements Word marks inserted as a whole with `w:customXmlInsRangeStart` / `End`: a content
215
+ * control or a custom XML element (check 30 measured the control at run and block level; a smart tag
216
+ * goes into the `w:ins` instead, and a hyperlink or field has no form of its own). Row- and cell-level
217
+ * ones are listed for the reader: `trackInsertedTable` walks through them to the rows and cells, and
218
+ * does not mark them.
219
+ */
220
+ export declare const HOLDER_TYPES: ReadonlySet<string>;
221
+ /** The list a holder's content is in (`w:sdtContent`'s for a control, its own for custom XML), created when absent. */
222
+ export declare function holderItemsOf(value: object): Element[] | undefined;
223
+ /**
224
+ * A content control or custom XML element inserted under tracking, as Word writes it (check 30): a
225
+ * `w:customXmlInsRangeStart` before the element with its `w:customXmlInsRangeEnd` first inside its
226
+ * content, and a second start last inside with its end after the element. Rejecting takes the element
227
+ * away and leaves its content where it stood (`TrackedChange`'s `holder` target); without the markers
228
+ * an element holding only an insertion reads as one that was there before and was typed into, which
229
+ * rejecting keeps (check 30, existing-control-typed). `inside` tracks the content between the two
230
+ * pairs, so that the ids run in document order as Word's do.
231
+ */
232
+ export declare function markHolderInserted(tracker: ChangeTracker, holder: Element, owner: Element[], inside: () => void): void;
187
233
  /** The paragraph mark's run properties (`w:pPr/w:rPr`), created when absent. */
188
234
  export declare function paraRPrOf(p: wml.P): wml.ParaRPr;
189
235
  /** The row's properties (`w:trPr`), created when absent. */