@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.
@@ -195,15 +195,27 @@ export declare class Body {
195
195
  * paragraph is marked deleted and every row's `w:trPr` takes a `w:del` instead.
196
196
  */
197
197
  clear(): void;
198
- /** Every tracked change in this body, in document order. */
198
+ /**
199
+ * Every tracked change in this body, in document order. Touching insertions or deletions by one
200
+ * author - a deleted paragraph's text and mark, a deletion across a mark - are one change, as
201
+ * Office JS lists them (CR-002 section 29, checks 22 and 23).
202
+ */
199
203
  getTrackedChanges(): TrackedChange[];
200
204
  /**
201
205
  * Accepts every tracked change, last first so that a paragraph join never disturbs one still
202
- * to do (docx4j `AcceptTrackedChanges` is the reference). Returns how many were accepted.
206
+ * to do (docx4j `AcceptTrackedChanges` is the reference). Returns how many were listed.
203
207
  */
204
208
  acceptAll(): number;
205
- /** Rejects every tracked change, last first. Returns how many were rejected. */
209
+ /** Rejects every tracked change, last first. Returns how many were listed. */
206
210
  rejectAll(): number;
211
+ /**
212
+ * Resolving one change can leave another to be listed afresh: accepting a move carries a deletion
213
+ * nested in its moved text out of it, and resolving a broken move dissolves the rest of it into a
214
+ * new `w:ins` or `w:del` (CR-002 section 29), so the list read first goes stale. The list is read
215
+ * again after each pass until nothing is left, as "all" promises (found by the editor's C5); a
216
+ * pass that resolves nothing ends it.
217
+ */
218
+ private resolveAll;
207
219
  /** Every `w:tr` in this body's tables, in document order (rows of nested tables included). */
208
220
  private rowElements;
209
221
  /** The part's XML (extension; Office JS getOoxml wraps it in a package). */
@@ -11,8 +11,8 @@ import { InlinePicture, addImage, writableWidthEmu } from './InlinePicture.mjs';
11
11
  import { contentOf } from './ooxml.mjs';
12
12
  import { expandReplacement } from './search.mjs';
13
13
  import { commentApi } from './comments.mjs';
14
- import { trackerOf, trackInsertedParagraph, trackInsertedTable } from './tracking.mjs';
15
- import { trackedChangesOfRow } from './TrackedChange.mjs';
14
+ import { trackerOf, trackInsertedBlock, markHolderInserted } from './tracking.mjs';
15
+ import { trackedChangesOfRow, trackedChangeTokensOfParagraph, groupTouching, BREAK, tablePropertiesChangeOf, cellChangeOf, bodySectionChangeOf, resolveUnlisted, holderInsertionAt } from './TrackedChange.mjs';
16
16
  import { listsOf, listLabelsOf } from './List.mjs';
17
17
  /**
18
18
  * A subset of Office JS `Word.Body` over any container with block-level content: the main
@@ -215,7 +215,8 @@ export class Body {
215
215
  }
216
216
  /** The package's change tracker while `changeTrackingMode` is on; undefined when it is off (CR-002 phase F). */
217
217
  get changeTracker() {
218
- return trackerOf(this.package_);
218
+ var _a;
219
+ return (_a = trackerOf(this.package_)) === null || _a === void 0 ? void 0 : _a.forPart(this.part);
219
220
  }
220
221
  insertParagraph(text, location) {
221
222
  const el = paragraphOf(text === '' ? [] : [textRun(text)]);
@@ -300,14 +301,12 @@ export class Body {
300
301
  for (const el of elements) {
301
302
  linkParents(el, owner);
302
303
  const tn = typeNameOf(el);
303
- if (tn === 'org_docx4j_wml.P') {
304
+ if (tn === 'org_docx4j_wml.P')
304
305
  this.assignParaId(el.value);
305
- if (tracker)
306
- trackInsertedParagraph(tracker, el.value);
307
- }
308
- else if (tn === 'org_docx4j_wml.Tbl' && tracker) {
309
- trackInsertedTable(tracker, el.value);
310
- }
306
+ // everything it holds is an insertion: the runs in its links and inline controls, the rows and
307
+ // nested tables of a table, a block control's content and the control itself (CR-002 section 37)
308
+ if (tracker)
309
+ trackInsertedBlock(tracker, el, container);
311
310
  }
312
311
  const first = elements[0];
313
312
  return typeNameOf(first) === 'org_docx4j_wml.P' ? new Paragraph(first, container, this) : { element: first, container };
@@ -378,57 +377,116 @@ export class Body {
378
377
  p.delete();
379
378
  }
380
379
  // --- change tracking (CR-002 phase F) ---------------------------------------------------
381
- /** Every tracked change in this body, in document order. */
380
+ /**
381
+ * Every tracked change in this body, in document order. Touching insertions or deletions by one
382
+ * author - a deleted paragraph's text and mark, a deletion across a mark - are one change, as
383
+ * Office JS lists them (CR-002 section 29, checks 22 and 23).
384
+ */
382
385
  getTrackedChanges() {
383
386
  const out = [];
384
387
  const visit = (items, into) => {
385
- var _a;
386
- for (const el of items) {
388
+ var _a, _b;
389
+ for (let i = 0; i < items.length; i++) {
390
+ const el = items[i];
387
391
  const tn = typeNameOf(el);
388
392
  if (tn === 'org_docx4j_wml.P') {
389
- into.push(...new Paragraph(el, items, this).getTrackedChanges());
393
+ into.push(...trackedChangeTokensOfParagraph(new Paragraph(el, items, this)));
394
+ continue;
395
+ }
396
+ // a block control or custom XML element that is itself an insertion (check 30): one piece,
397
+ // grouping with the insertions around and inside it, as Word lists them
398
+ const inserted = holderInsertionAt(items, i, undefined);
399
+ if (inserted) {
400
+ into.push(inserted);
401
+ visit((_a = childrenOf(items[i + 1].value)) !== null && _a !== void 0 ? _a : [], into);
402
+ i += 2;
390
403
  continue;
391
404
  }
392
405
  const v = el.value;
393
406
  if (typeof v !== 'object' || v === null)
394
407
  continue;
395
408
  if (tn === 'org_docx4j_wml.Tbl') {
409
+ // A table is no break of its own: an inserted or deleted row groups with the rows and the
410
+ // inserted text touching it, as Office JS lists them (check 31: a three-row table between two
411
+ // inserted paragraphs is one change; rows added at a table's end with a paragraph typed after
412
+ // it, one; a paragraph whose mark is plain before rows added at the start, two). Its property
413
+ // records are one change of their own where the table stands (CR-002 section 35), kept apart.
414
+ const tableChange = tablePropertiesChangeOf(el);
415
+ if (tableChange)
416
+ into.push(BREAK, tableChange, BREAK);
396
417
  for (const row of rowsOf(v)) {
397
418
  // A row that is itself a revision reports as one change, as Office JS does: the
398
419
  // markup its cells carry (Word marks every run and mark of a deleted or inserted row)
399
420
  // belongs to that change, which accepts or rejects it along with the row.
400
- const inner = [];
401
- for (const cell of cellsOf(row.element.value))
402
- visit((_a = childrenOf(cell.element.value)) !== null && _a !== void 0 ? _a : [], inner);
403
- const rowChanges = trackedChangesOfRow(row.element, row.container, inner);
404
- into.push(...(rowChanges.length > 0 ? rowChanges : inner));
421
+ const tokens = [];
422
+ for (const cell of cellsOf(row.element.value)) {
423
+ const cellChange = cellChangeOf(cell.element, cell.container, row.element.value);
424
+ if (cellChange)
425
+ tokens.push(cellChange, BREAK);
426
+ visit((_b = childrenOf(cell.element.value)) !== null && _b !== void 0 ? _b : [], tokens);
427
+ tokens.push(BREAK);
428
+ }
429
+ const inner = groupTouching(tokens);
430
+ const rowChanges = trackedChangesOfRow(row.element, row.container, inner, { element: el, container: items });
431
+ if (rowChanges.length > 0)
432
+ into.push(...rowChanges);
433
+ else {
434
+ into.push(BREAK);
435
+ for (const change of inner)
436
+ into.push(change, BREAK);
437
+ }
405
438
  }
406
439
  continue;
407
440
  }
441
+ // a block-level content control or custom XML element keeps what is inside it apart; a
442
+ // marker between paragraphs (a bookmark or range end) keeps nothing apart
408
443
  const children = childrenOf(v);
409
- if (children)
444
+ if (children) {
445
+ into.push(BREAK);
410
446
  visit(children, into);
447
+ into.push(BREAK);
448
+ }
411
449
  }
412
450
  };
413
451
  visit(this.content, out);
414
- return out;
452
+ // the body's last section, after everything in it
453
+ const section = bodySectionChangeOf(this.container);
454
+ if (section)
455
+ out.push(BREAK, section);
456
+ return groupTouching(out);
415
457
  }
416
458
  /**
417
459
  * Accepts every tracked change, last first so that a paragraph join never disturbs one still
418
- * to do (docx4j `AcceptTrackedChanges` is the reference). Returns how many were accepted.
460
+ * to do (docx4j `AcceptTrackedChanges` is the reference). Returns how many were listed.
419
461
  */
420
462
  acceptAll() {
421
- const changes = this.getTrackedChanges();
422
- for (let i = changes.length - 1; i >= 0; i--)
423
- changes[i].accept();
424
- return changes.length;
463
+ return this.resolveAll('accept');
425
464
  }
426
- /** Rejects every tracked change, last first. Returns how many were rejected. */
465
+ /** Rejects every tracked change, last first. Returns how many were listed. */
427
466
  rejectAll() {
428
- const changes = this.getTrackedChanges();
429
- for (let i = changes.length - 1; i >= 0; i--)
430
- changes[i].reject();
431
- return changes.length;
467
+ return this.resolveAll('reject');
468
+ }
469
+ /**
470
+ * Resolving one change can leave another to be listed afresh: accepting a move carries a deletion
471
+ * nested in its moved text out of it, and resolving a broken move dissolves the rest of it into a
472
+ * new `w:ins` or `w:del` (CR-002 section 29), so the list read first goes stale. The list is read
473
+ * again after each pass until nothing is left, as "all" promises (found by the editor's C5); a
474
+ * pass that resolves nothing ends it.
475
+ */
476
+ resolveAll(what) {
477
+ let changes = this.getTrackedChanges();
478
+ const count = changes.length;
479
+ while (changes.length > 0) {
480
+ for (let i = changes.length - 1; i >= 0; i--)
481
+ changes[i][what]();
482
+ const left = this.getTrackedChanges();
483
+ if (left.length >= changes.length)
484
+ break;
485
+ changes = left;
486
+ }
487
+ // and what is never listed: a cell merge, a numbering change, records that change nothing
488
+ resolveUnlisted(this.content, what);
489
+ return count;
432
490
  }
433
491
  /** Every `w:tr` in this body's tables, in document order (rows of nested tables included). */
434
492
  rowElements() {
@@ -690,6 +748,10 @@ export class Body {
690
748
  this.content.length = 0;
691
749
  this.content.push(sdt);
692
750
  linkParents(sdt, this.container);
751
+ // under tracking the control itself is the insertion, its content what it was (check 30)
752
+ const tracker = this.changeTracker;
753
+ if (tracker)
754
+ markHolderInserted(tracker, sdt, this.content, () => { });
693
755
  return new ContentControl(sdt, this.content, this);
694
756
  }
695
757
  }
@@ -2,7 +2,7 @@ import type * as wml from '@docx4j/generated-objects-ts/modules/org_docx4j_wml';
2
2
  import { Body } from './Body.mjs';
3
3
  import { Paragraph } from './Paragraph.mjs';
4
4
  import { Range } from './Range.mjs';
5
- import { type CommentContent, type CommentOptions, type CommentParts } from './comments.mjs';
5
+ import { type CommentContent, type CommentOptions, type CommentEntry, type CommentParts } from './comments.mjs';
6
6
  /**
7
7
  * A subset of Office JS `Word.Comment` over a `w:comment` and the markers that anchor it. A view:
8
8
  * the tree is the state, and a view may be created and discarded freely. Replies are `Comment`s
@@ -33,7 +33,12 @@ export declare class Comment {
33
33
  get initials(): string;
34
34
  /** The author's email from `w:people`, '' when the document records none. */
35
35
  get authorEmail(): string;
36
- /** `w:date`; undefined when the comment carries none. */
36
+ /**
37
+ * When the comment was made, as Word means it (CR-002 section 29, the revision dates' rule): its
38
+ * `w16cex:dateUtc` when the document has one for it, which is UTC; else `w:date`, which Word writes
39
+ * on the author's local wall clock whatever its `Z` says, read as local time. Undefined when the
40
+ * comment carries neither.
41
+ */
37
42
  get creationDate(): Date | undefined;
38
43
  /** The comment's paragraphs as a `Body` (extension: the same content API as the document's). */
39
44
  get commentBody(): Body;
@@ -80,9 +85,29 @@ export declare class Comment {
80
85
  * throws.
81
86
  */
82
87
  export declare function insertComment(range: Range, content: CommentContent, options?: CommentOptions): Promise<Comment>;
88
+ /**
89
+ * A comment's entry for markers the caller placed (CR-002 section 36, asked by the editor for its
90
+ * E4.e, which places a new comment's markers itself: a selection may span paragraphs, where
91
+ * `Range.insertComment` anchors in one). Everything `insertComment` writes but the markers: the
92
+ * `w:comment` with the given id, author, initials, date and paragraphs (the first with a
93
+ * `w14:paraId`); its `w15:commentEx` (`w15:done="0"`, and `w15:paraIdParent` for a reply); its
94
+ * `w16cid:commentId` where that part exists, and its `w16cex:commentExtensible` where that one does;
95
+ * and the author's `w15:person`. The comment parts are created as `insertComment` creates them, and
96
+ * the date follows section 29's rule (`w:date` on the local wall clock, `w16cex:dateUtc` the date
97
+ * given). The id must be unused in the comments part - markers naming it are expected, being the
98
+ * caller's - and is not checked against them; a comment with no markers is still a comment, which
99
+ * Word shows unanchored.
100
+ */
101
+ export declare function addCommentEntry(body: Body, entry: CommentEntry): Promise<Comment>;
83
102
  /**
84
103
  * `getComments()` on a body, a paragraph or a range: the comments anchored there in document
85
104
  * order, replies nested under their parent (a reply is in the array only when its parent is not).
86
105
  */
87
106
  export declare function commentsOf(scope: Body | Paragraph | Range): Promise<Comment[]>;
107
+ /**
108
+ * Comments whose anchored text went with a rejected insertion or an accepted deletion (check 32;
109
+ * `TrackedChange`'s `removeRevision`) had their markers removed then; their entries go here, with
110
+ * their replies, when the comments are next read or the package is saved (`WordprocessingMLPackage.saveTo`).
111
+ */
112
+ export declare function flushPendingCommentRemovals(body: Body): Promise<void>;
88
113
  export type { CommentParts, Author } from './comments.mjs';
@@ -8,7 +8,8 @@ import { Body } from './Body.mjs';
8
8
  import { Paragraph } from './Paragraph.mjs';
9
9
  import { Range } from './Range.mjs';
10
10
  import { runOf, paragraphOf, typeNameOf } from './tree.mjs';
11
- import { COMMENT_REFERENCE_STYLE, COMMENT_TEXT_STYLE, calendarToDate, commentExOf, commentIdsOf, commentPartsAccess, commentParaId, dateToCalendar, initialsOf, isDone, markersOf, markersOfParagraph, nextCommentId, paraIdTaken, randomHexId, removeMarkers, setCommentApi, } from './comments.mjs';
11
+ import { revisionDateOf, wordCalendarOf, copyRPr } from './tracking.mjs';
12
+ import { COMMENT_REFERENCE_STYLE, COMMENT_TEXT_STYLE, commentExOf, commentIdsOf, commentPartsAccess, commentParaId, initialsOf, isDone, markersOf, markersOfParagraph, nextCommentId, paraIdTaken, randomHexId, removeMarkers, setCommentApi, } from './comments.mjs';
12
13
  /**
13
14
  * A subset of Office JS `Word.Comment` over a `w:comment` and the markers that anchor it. A view:
14
15
  * the tree is the state, and a view may be created and discarded freely. Replies are `Comment`s
@@ -49,9 +50,21 @@ export class Comment {
49
50
  const ad = /^S::([^:]+)::/.exec(userId);
50
51
  return ad ? ad[1] : userId;
51
52
  }
52
- /** `w:date`; undefined when the comment carries none. */
53
+ /**
54
+ * When the comment was made, as Word means it (CR-002 section 29, the revision dates' rule): its
55
+ * `w16cex:dateUtc` when the document has one for it, which is UTC; else `w:date`, which Word writes
56
+ * on the author's local wall clock whatever its `Z` says, read as local time. Undefined when the
57
+ * comment carries neither.
58
+ */
53
59
  get creationDate() {
54
- return calendarToDate(this.element.date);
60
+ var _a;
61
+ for (const el of [...((_a = this.element.content) !== null && _a !== void 0 ? _a : [])].reverse()) {
62
+ const paraId = typeNameOf(el) === 'org_docx4j_wml.P' ? el.value.paraId : undefined;
63
+ const utc = paraId ? this.parts.datesUtc.get(paraId.toUpperCase()) : undefined;
64
+ if (utc)
65
+ return utc;
66
+ }
67
+ return revisionDateOf({ date: this.element.date });
55
68
  }
56
69
  /** The comment's paragraphs as a `Body` (extension: the same content API as the document's). */
57
70
  get commentBody() {
@@ -195,6 +208,7 @@ export class Comment {
195
208
  ids.splice(i, 1);
196
209
  if (durableId)
197
210
  (_c = this.parts.extensible) === null || _c === void 0 ? void 0 : _c.remove(durableId);
211
+ this.parts.datesUtc.delete(paraId.toUpperCase());
198
212
  }
199
213
  }
200
214
  }
@@ -226,9 +240,48 @@ export class Comment {
226
240
  function annotationRefRun() {
227
241
  return runOf([el.annotationRef(wmlFactory.createRAnnotationRef())], applyRunOptions({}, { style: COMMENT_REFERENCE_STYLE }));
228
242
  }
229
- /** The run that anchors a comment in the body: `w:commentReference` in the `CommentReference` style. */
230
- function referenceRun(id) {
231
- return runOf([el.commentReference(wmlFactory.createRCommentReference({ id }))], applyRunOptions({}, { style: COMMENT_REFERENCE_STYLE }));
243
+ /**
244
+ * The run that anchors a comment in the body: `w:commentReference` in the `CommentReference` style,
245
+ * over the run properties of the text it stands by when given (Word's reference run for a comment at
246
+ * a caret carried the text's `w:sz`, CR-002 section 36).
247
+ */
248
+ function referenceRun(id, textRPr) {
249
+ var _a;
250
+ const rPr = applyRunOptions((_a = copyRPr(textRPr)) !== null && _a !== void 0 ? _a : {}, { style: COMMENT_REFERENCE_STYLE });
251
+ return runOf([el.commentReference(wmlFactory.createRCommentReference({ id }))], rPr);
252
+ }
253
+ /** A comment marker that takes no room: a range start or end, or a run holding only a reference. */
254
+ function isCommentMarker(item) {
255
+ var _a, _b;
256
+ if (!item)
257
+ return false;
258
+ const name = (_a = item.name) === null || _a === void 0 ? void 0 : _a.localPart;
259
+ if (name === 'commentRangeStart' || name === 'commentRangeEnd')
260
+ return true;
261
+ if (typeNameOf(item) !== 'org_docx4j_wml.R')
262
+ return false;
263
+ const content = ((_b = item.value.content) !== null && _b !== void 0 ? _b : []);
264
+ return content.length > 0 && content.every((c) => { var _a; return ((_a = c.name) === null || _a === void 0 ? void 0 : _a.localPart) === 'commentReference'; });
265
+ }
266
+ /**
267
+ * Where an empty range's markers go when other comments' empty ranges already stand at the caret:
268
+ * inside them, right after their starts, as Word nests a second comment made at the same place
269
+ * (start 0, start 1, end 1, reference 1, end 0, reference 0; CR-002 section 36). A start counts only
270
+ * when its comment's end is at the caret too, so a comment over text that begins there is not entered.
271
+ */
272
+ function nestedIndex(owner, index) {
273
+ var _a;
274
+ let from = index;
275
+ while (isCommentMarker(owner[from - 1]))
276
+ from--;
277
+ let to = index;
278
+ while (isCommentMarker(owner[to]))
279
+ to++;
280
+ const endsHere = new Set(owner.slice(from, to).filter((e) => { var _a; return ((_a = e.name) === null || _a === void 0 ? void 0 : _a.localPart) === 'commentRangeEnd'; }).map((e) => String(e.value.id)));
281
+ let at = from;
282
+ while (at < to && ((_a = owner[at].name) === null || _a === void 0 ? void 0 : _a.localPart) === 'commentRangeStart' && endsHere.has(String(owner[at].value.id)))
283
+ at++;
284
+ return at > from ? at : index;
232
285
  }
233
286
  /** A paragraph of a comment: the `CommentText` style, opened by the annotation reference run. */
234
287
  function commentParagraph(text, first) {
@@ -248,11 +301,10 @@ function parentOf(element) {
248
301
  return element.value.PARENT;
249
302
  }
250
303
  /**
251
- * Where a marker for this segment's run goes: beside the run, but outside the `w:ins` or `w:del`
252
- * the run sits in, so that a comment made while change tracking is on is a comment and not an
253
- * insertion of its own (CR-002 section 13). Accepting or rejecting the revision then leaves the
254
- * comment where it is; the cost is that a comment on part of an insertion widens to the whole of
255
- * it, which is what Word shows anyway once the insertion is accepted.
304
+ * Where a marker for this segment's run goes when it is not put inside the revision the run sits
305
+ * in (`placeAround`): beside the run, but outside the `w:ins` or `w:del`, so that a comment made
306
+ * while change tracking is on is a comment and not an insertion of its own (CR-002 section 13).
307
+ * Accepting or rejecting the revision then leaves the comment where it is.
256
308
  */
257
309
  function markerSite(seg, fallback, after) {
258
310
  var _a;
@@ -271,23 +323,44 @@ function placeAround(range, id) {
271
323
  const covered = range.end > range.start ? paragraph.segments().filter((s) => s.end > range.start && s.start < range.end) : [];
272
324
  const start = el.commentRangeStart(wmlFactory.createCommentRangeStart({ id }));
273
325
  const end = el.commentRangeEnd(wmlFactory.createCommentRangeEnd({ id }));
274
- const reference = referenceRun(id);
275
326
  if (covered.length === 0) {
276
- // an empty range or an empty paragraph: the reference run only, which is all Word requires
327
+ // An empty range or an empty paragraph: both range markers with nothing between them, then the
328
+ // reference run in the text's run properties, and a later comment at the same place nested inside
329
+ // the earlier - what Word writes (measured by Jason 2026-09-29, CR-002 section 36).
277
330
  const segments = paragraph.segments();
278
331
  const after = segments.find((s) => s.start >= range.start);
279
332
  const before = [...segments].reverse().find((s) => s.end <= range.start);
333
+ const text = after !== null && after !== void 0 ? after : before;
280
334
  const site = after ? markerSite(after, paragraph.p, false) : before ? markerSite(before, paragraph.p, true) : undefined;
281
- if (site)
282
- insertAt(site.owner, site.index, [reference], site.parent);
283
- else
284
- insertAt(((_a = (_b = paragraph.p).content) !== null && _a !== void 0 ? _a : (_b.content = [])), paragraph.p.content.length, [reference], paragraph.p);
335
+ const owner = site ? site.owner : ((_a = (_b = paragraph.p).content) !== null && _a !== void 0 ? _a : (_b.content = []));
336
+ const index = nestedIndex(owner, site ? site.index : owner.length);
337
+ insertAt(owner, index, [start, end, referenceRun(id, text === null || text === void 0 ? void 0 : text.run.rPr)], site ? site.parent : paragraph.p);
285
338
  return;
286
339
  }
287
- const after = markerSite(covered[covered.length - 1], paragraph.p, true);
288
- insertAt(after.owner, after.index, [end, reference], after.parent);
289
- const before = markerSite(covered[0], paragraph.p, false);
290
- insertAt(before.owner, before.index, [start], before.parent);
340
+ const reference = referenceRun(id);
341
+ const first = covered[0];
342
+ const last = covered[covered.length - 1];
343
+ // A comment on part of an insertion, as Word writes it (the editor's measure-word2010.docx and
344
+ // measure-word15.docx, 2026-09-30; CR-002 section 38): the range start inside the w:ins before the
345
+ // first commented run, the w:ins closed at the range's end - the rest of it a second w:ins with a
346
+ // new id - and the end marker and the plain reference run after it. So the anchor stays narrow
347
+ // and the reference is no insertion. Before, both markers went outside the w:ins, which widened
348
+ // the anchor to the whole insertion. A deletion or a move keeps the markers outside (a deletion's
349
+ // form is not measured; a move's halves are not to be split).
350
+ const inInsertion = (seg) => { var _a; return ((_a = seg.revision) === null || _a === void 0 ? void 0 : _a.kind) === 'ins'; };
351
+ if (inInsertion(last)) {
352
+ paragraph.splitHolderAround(last.revision, last.runIndex + 1, [end, reference], () => paragraph.nextAnnotationId());
353
+ }
354
+ else {
355
+ const after = markerSite(last, paragraph.p, true);
356
+ insertAt(after.owner, after.index, [end, reference], after.parent);
357
+ }
358
+ if (inInsertion(first))
359
+ insertAt(first.runOwner, first.runIndex, [start], first.revision.value);
360
+ else {
361
+ const before = markerSite(first, paragraph.p, false);
362
+ insertAt(before.owner, before.index, [start], before.parent);
363
+ }
291
364
  }
292
365
  /** Writes the markers of a reply next to its parent's, as Word nests them. */
293
366
  function placeAfter(body, parentId, id) {
@@ -315,17 +388,19 @@ function placeAfter(body, parentId, id) {
315
388
  * paragraphs come checked, by `paragraphsOfContent` before the markers were placed.
316
389
  */
317
390
  function addComment(parts, body, id, paragraphs, parent, options) {
318
- var _a, _b, _c, _d, _e, _f, _g;
319
- var _h, _j;
391
+ var _a, _b, _c, _d, _e, _f;
392
+ var _g, _h;
320
393
  const author = (_a = options === null || options === void 0 ? void 0 : options.author) !== null && _a !== void 0 ? _a : parts.author;
394
+ // one instant for w:date (the local wall clock, as Word writes it) and w16cex:dateUtc (UTC)
395
+ const made = (_b = options === null || options === void 0 ? void 0 : options.date) !== null && _b !== void 0 ? _b : new Date();
321
396
  const paraId = randomHexId((candidate) => paraIdTaken(parts, body.container, candidate));
322
397
  paragraphs[0].value.paraId = paraId;
323
398
  const comment = wmlFactory.createCommentsComment({
324
- id, author: author.name, initials: (_b = options === null || options === void 0 ? void 0 : options.initials) !== null && _b !== void 0 ? _b : initialsOf(author),
325
- date: dateToCalendar((_c = options === null || options === void 0 ? void 0 : options.date) !== null && _c !== void 0 ? _c : new Date()),
399
+ id, author: author.name, initials: (_c = options === null || options === void 0 ? void 0 : options.initials) !== null && _c !== void 0 ? _c : initialsOf(author),
400
+ date: wordCalendarOf(made),
326
401
  content: paragraphs,
327
402
  });
328
- const comments = ((_d = (_h = parts.comments).comment) !== null && _d !== void 0 ? _d : (_h.comment = []));
403
+ const comments = ((_d = (_g = parts.comments).comment) !== null && _d !== void 0 ? _d : (_g.comment = []));
329
404
  comments.push(comment);
330
405
  linkParents(comment, parts.comments);
331
406
  const extended = parts.ensureCommentsEx();
@@ -337,9 +412,12 @@ function addComment(parts, body, id, paragraphs, parent, options) {
337
412
  if (parts.commentsIds) {
338
413
  const durableId = randomHexId((candidate) => { var _a; return ((_a = parts.commentsIds.commentId) !== null && _a !== void 0 ? _a : []).some((e) => e.durableId === candidate); });
339
414
  const commentId = w16cidFactory.createCTCommentId({ paraId, durableId });
340
- ((_f = (_j = parts.commentsIds).commentId) !== null && _f !== void 0 ? _f : (_j.commentId = [])).push(commentId);
415
+ ((_f = (_h = parts.commentsIds).commentId) !== null && _f !== void 0 ? _f : (_h.commentId = [])).push(commentId);
341
416
  linkParents(commentId, parts.commentsIds);
342
- (_g = parts.extensible) === null || _g === void 0 ? void 0 : _g.set(durableId, new Date());
417
+ if (parts.extensible) {
418
+ parts.extensible.set(durableId, made);
419
+ parts.datesUtc.set(paraId.toUpperCase(), made);
420
+ }
343
421
  }
344
422
  addPerson(parts, author.name, author.email);
345
423
  return new Comment(comment, parts, body);
@@ -381,6 +459,31 @@ export async function insertComment(range, content, options) {
381
459
  placeAround(range, id);
382
460
  return addComment(parts, body, id, paragraphs, undefined, options);
383
461
  }
462
+ /**
463
+ * A comment's entry for markers the caller placed (CR-002 section 36, asked by the editor for its
464
+ * E4.e, which places a new comment's markers itself: a selection may span paragraphs, where
465
+ * `Range.insertComment` anchors in one). Everything `insertComment` writes but the markers: the
466
+ * `w:comment` with the given id, author, initials, date and paragraphs (the first with a
467
+ * `w14:paraId`); its `w15:commentEx` (`w15:done="0"`, and `w15:paraIdParent` for a reply); its
468
+ * `w16cid:commentId` where that part exists, and its `w16cex:commentExtensible` where that one does;
469
+ * and the author's `w15:person`. The comment parts are created as `insertComment` creates them, and
470
+ * the date follows section 29's rule (`w:date` on the local wall clock, `w16cex:dateUtc` the date
471
+ * given). The id must be unused in the comments part - markers naming it are expected, being the
472
+ * caller's - and is not checked against them; a comment with no markers is still a comment, which
473
+ * Word shows unanchored.
474
+ */
475
+ export async function addCommentEntry(body, entry) {
476
+ var _a;
477
+ const paragraphs = paragraphsOfContent(entry.content);
478
+ const parts = await commentPartsAccess().create(body);
479
+ const id = entry.id;
480
+ if (!Number.isInteger(id) || id < 0)
481
+ throw new Docx4JException(`A comment id must be a non-negative integer, not ${id}`);
482
+ if (((_a = parts.comments.comment) !== null && _a !== void 0 ? _a : []).some((c) => c.id === id)) {
483
+ throw new Docx4JException(`The comment id ${id} is already in use in this document's comments part`);
484
+ }
485
+ return addComment(parts, body, id, paragraphs, entry.parent, entry);
486
+ }
384
487
  /**
385
488
  * The `w:id` for a new comment: the caller's when given, checked unused across the comments part and
386
489
  * the markers in the story, else the next free one. A clash throws rather than renumbering, as a
@@ -415,12 +518,59 @@ function paragraphsOfContent(content) {
415
518
  * order, replies nested under their parent (a reply is in the array only when its parent is not).
416
519
  */
417
520
  export async function commentsOf(scope) {
418
- var _a, _b;
419
521
  const paragraph = scope instanceof Range ? scope.paragraph : scope instanceof Paragraph ? scope : undefined;
420
522
  const body = paragraph ? paragraph.parentBody : scope;
421
523
  const parts = await commentPartsAccess().load(body);
422
524
  if (!parts)
423
525
  return [];
526
+ await flushPendingRemovals(body, parts);
527
+ const views = viewsOf(parts, body);
528
+ const ids = scope instanceof Range ? idsInRange(scope) : paragraph ? unique(markersOfParagraph(paragraph.element, paragraph.container).map((m) => m.id)) : commentIdsOf(body.container);
529
+ const inScope = new Set(ids);
530
+ const out = [];
531
+ for (const id of ids) {
532
+ const view = views.get(id);
533
+ if (!view)
534
+ continue;
535
+ if (view.parent && inScope.has(view.parent.id))
536
+ continue;
537
+ out.push(view);
538
+ }
539
+ return out;
540
+ }
541
+ /**
542
+ * Comments whose anchored text went with a rejected insertion or an accepted deletion (check 32;
543
+ * `TrackedChange`'s `removeRevision`) had their markers removed then; their entries go here, with
544
+ * their replies, when the comments are next read or the package is saved (`WordprocessingMLPackage.saveTo`).
545
+ */
546
+ export async function flushPendingCommentRemovals(body) {
547
+ var _a;
548
+ const pkg = body.package_;
549
+ if (!((_a = pkg === null || pkg === void 0 ? void 0 : pkg.pendingCommentRemovals) === null || _a === void 0 ? void 0 : _a.size))
550
+ return;
551
+ const parts = await commentPartsAccess().load(body);
552
+ if (!parts) {
553
+ pkg.pendingCommentRemovals.clear();
554
+ return;
555
+ }
556
+ await flushPendingRemovals(body, parts);
557
+ }
558
+ async function flushPendingRemovals(body, parts) {
559
+ const pkg = body.package_;
560
+ const pending = pkg === null || pkg === void 0 ? void 0 : pkg.pendingCommentRemovals;
561
+ if (!(pending === null || pending === void 0 ? void 0 : pending.size))
562
+ return;
563
+ const views = viewsOf(parts, body);
564
+ for (const id of [...pending]) {
565
+ const view = views.get(id);
566
+ if (view)
567
+ await view.delete();
568
+ pending.delete(id);
569
+ }
570
+ }
571
+ /** A `Comment` view per entry in the parts, replies linked to their parents. */
572
+ function viewsOf(parts, body) {
573
+ var _a, _b;
424
574
  const views = new Map();
425
575
  for (const comment of (_a = parts.comments.comment) !== null && _a !== void 0 ? _a : []) {
426
576
  if (typeof comment.id === 'number' && !views.has(comment.id))
@@ -438,18 +588,7 @@ export async function commentsOf(scope) {
438
588
  if (parent && parent !== view)
439
589
  parent.addReply(view);
440
590
  }
441
- const ids = scope instanceof Range ? idsInRange(scope) : paragraph ? unique(markersOfParagraph(paragraph.element, paragraph.container).map((m) => m.id)) : commentIdsOf(body.container);
442
- const inScope = new Set(ids);
443
- const out = [];
444
- for (const id of ids) {
445
- const view = views.get(id);
446
- if (!view)
447
- continue;
448
- if (view.parent && inScope.has(view.parent.id))
449
- continue;
450
- out.push(view);
451
- }
452
- return out;
591
+ return views;
453
592
  }
454
593
  /** The comments a range touches: a comment whose markers overlap [start, end]. */
455
594
  function idsInRange(range) {
@@ -94,6 +94,12 @@ export declare class ContentControl {
94
94
  private parentParagraph;
95
95
  /** The offsets a run-level control covers in its paragraph. */
96
96
  private runSpan;
97
+ /**
98
+ * Text into a run control that holds no run: a new run in its content, in a `w:ins` under
99
+ * tracking (Word: check 30, new-control-typed), placed before the control's trailing insertion
100
+ * marker when it is itself an insertion.
101
+ */
102
+ private insertIntoEmpty;
97
103
  /** The control's data binding (Office JS `xmlMapping`), backed by `w:dataBinding`. */
98
104
  get xmlMapping(): XmlMapping;
99
105
  /**