@docx4j/core-ts 0.2.2 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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). */
@@ -12,7 +12,7 @@ import { contentOf } from './ooxml.mjs';
12
12
  import { expandReplacement } from './search.mjs';
13
13
  import { commentApi } from './comments.mjs';
14
14
  import { trackerOf, trackInsertedParagraph, trackInsertedTable } from './tracking.mjs';
15
- import { trackedChangesOfRow } from './TrackedChange.mjs';
15
+ import { trackedChangesOfRow, trackedChangeTokensOfParagraph, groupTouching, BREAK, tablePropertiesChangeOf, cellChangeOf, bodySectionChangeOf, resolveUnlisted } 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)]);
@@ -378,7 +379,11 @@ export class Body {
378
379
  p.delete();
379
380
  }
380
381
  // --- change tracking (CR-002 phase F) ---------------------------------------------------
381
- /** Every tracked change in this body, in document order. */
382
+ /**
383
+ * Every tracked change in this body, in document order. Touching insertions or deletions by one
384
+ * author - a deleted paragraph's text and mark, a deletion across a mark - are one change, as
385
+ * Office JS lists them (CR-002 section 29, checks 22 and 23).
386
+ */
382
387
  getTrackedChanges() {
383
388
  const out = [];
384
389
  const visit = (items, into) => {
@@ -386,49 +391,86 @@ export class Body {
386
391
  for (const el of items) {
387
392
  const tn = typeNameOf(el);
388
393
  if (tn === 'org_docx4j_wml.P') {
389
- into.push(...new Paragraph(el, items, this).getTrackedChanges());
394
+ into.push(...trackedChangeTokensOfParagraph(new Paragraph(el, items, this)));
390
395
  continue;
391
396
  }
392
397
  const v = el.value;
393
398
  if (typeof v !== 'object' || v === null)
394
399
  continue;
395
400
  if (tn === 'org_docx4j_wml.Tbl') {
401
+ into.push(BREAK);
402
+ // the table's property records, one change where the table stands (CR-002 section 35)
403
+ const tableChange = tablePropertiesChangeOf(el);
404
+ if (tableChange)
405
+ into.push(tableChange, BREAK);
396
406
  for (const row of rowsOf(v)) {
397
407
  // A row that is itself a revision reports as one change, as Office JS does: the
398
408
  // markup its cells carry (Word marks every run and mark of a deleted or inserted row)
399
409
  // 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);
410
+ const tokens = [];
411
+ for (const cell of cellsOf(row.element.value)) {
412
+ const cellChange = cellChangeOf(cell.element, cell.container, row.element.value);
413
+ if (cellChange)
414
+ tokens.push(cellChange, BREAK);
415
+ visit((_a = childrenOf(cell.element.value)) !== null && _a !== void 0 ? _a : [], tokens);
416
+ tokens.push(BREAK);
417
+ }
418
+ const inner = groupTouching(tokens);
403
419
  const rowChanges = trackedChangesOfRow(row.element, row.container, inner);
404
- into.push(...(rowChanges.length > 0 ? rowChanges : inner));
420
+ for (const change of rowChanges.length > 0 ? rowChanges : inner)
421
+ into.push(change, BREAK);
405
422
  }
406
423
  continue;
407
424
  }
425
+ // a block-level content control or custom XML element keeps what is inside it apart; a
426
+ // marker between paragraphs (a bookmark or range end) keeps nothing apart
408
427
  const children = childrenOf(v);
409
- if (children)
428
+ if (children) {
429
+ into.push(BREAK);
410
430
  visit(children, into);
431
+ into.push(BREAK);
432
+ }
411
433
  }
412
434
  };
413
435
  visit(this.content, out);
414
- return out;
436
+ // the body's last section, after everything in it
437
+ const section = bodySectionChangeOf(this.container);
438
+ if (section)
439
+ out.push(BREAK, section);
440
+ return groupTouching(out);
415
441
  }
416
442
  /**
417
443
  * 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.
444
+ * to do (docx4j `AcceptTrackedChanges` is the reference). Returns how many were listed.
419
445
  */
420
446
  acceptAll() {
421
- const changes = this.getTrackedChanges();
422
- for (let i = changes.length - 1; i >= 0; i--)
423
- changes[i].accept();
424
- return changes.length;
447
+ return this.resolveAll('accept');
425
448
  }
426
- /** Rejects every tracked change, last first. Returns how many were rejected. */
449
+ /** Rejects every tracked change, last first. Returns how many were listed. */
427
450
  rejectAll() {
428
- const changes = this.getTrackedChanges();
429
- for (let i = changes.length - 1; i >= 0; i--)
430
- changes[i].reject();
431
- return changes.length;
451
+ return this.resolveAll('reject');
452
+ }
453
+ /**
454
+ * Resolving one change can leave another to be listed afresh: accepting a move carries a deletion
455
+ * nested in its moved text out of it, and resolving a broken move dissolves the rest of it into a
456
+ * new `w:ins` or `w:del` (CR-002 section 29), so the list read first goes stale. The list is read
457
+ * again after each pass until nothing is left, as "all" promises (found by the editor's C5); a
458
+ * pass that resolves nothing ends it.
459
+ */
460
+ resolveAll(what) {
461
+ let changes = this.getTrackedChanges();
462
+ const count = changes.length;
463
+ while (changes.length > 0) {
464
+ for (let i = changes.length - 1; i >= 0; i--)
465
+ changes[i][what]();
466
+ const left = this.getTrackedChanges();
467
+ if (left.length >= changes.length)
468
+ break;
469
+ changes = left;
470
+ }
471
+ // and what is never listed: a cell merge, a numbering change, records that change nothing
472
+ resolveUnlisted(this.content, what);
473
+ return count;
432
474
  }
433
475
  /** Every `w:tr` in this body's tables, in document order (rows of nested tables included). */
434
476
  rowElements() {
@@ -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,6 +85,20 @@ 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).
@@ -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) {
@@ -271,19 +324,21 @@ function placeAround(range, id) {
271
324
  const covered = range.end > range.start ? paragraph.segments().filter((s) => s.end > range.start && s.start < range.end) : [];
272
325
  const start = el.commentRangeStart(wmlFactory.createCommentRangeStart({ id }));
273
326
  const end = el.commentRangeEnd(wmlFactory.createCommentRangeEnd({ id }));
274
- const reference = referenceRun(id);
275
327
  if (covered.length === 0) {
276
- // an empty range or an empty paragraph: the reference run only, which is all Word requires
328
+ // An empty range or an empty paragraph: both range markers with nothing between them, then the
329
+ // reference run in the text's run properties, and a later comment at the same place nested inside
330
+ // the earlier - what Word writes (measured by Jason 2026-09-29, CR-002 section 36).
277
331
  const segments = paragraph.segments();
278
332
  const after = segments.find((s) => s.start >= range.start);
279
333
  const before = [...segments].reverse().find((s) => s.end <= range.start);
334
+ const text = after !== null && after !== void 0 ? after : before;
280
335
  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);
336
+ const owner = site ? site.owner : ((_a = (_b = paragraph.p).content) !== null && _a !== void 0 ? _a : (_b.content = []));
337
+ const index = nestedIndex(owner, site ? site.index : owner.length);
338
+ insertAt(owner, index, [start, end, referenceRun(id, text === null || text === void 0 ? void 0 : text.run.rPr)], site ? site.parent : paragraph.p);
285
339
  return;
286
340
  }
341
+ const reference = referenceRun(id);
287
342
  const after = markerSite(covered[covered.length - 1], paragraph.p, true);
288
343
  insertAt(after.owner, after.index, [end, reference], after.parent);
289
344
  const before = markerSite(covered[0], paragraph.p, false);
@@ -315,17 +370,19 @@ function placeAfter(body, parentId, id) {
315
370
  * paragraphs come checked, by `paragraphsOfContent` before the markers were placed.
316
371
  */
317
372
  function addComment(parts, body, id, paragraphs, parent, options) {
318
- var _a, _b, _c, _d, _e, _f, _g;
319
- var _h, _j;
373
+ var _a, _b, _c, _d, _e, _f;
374
+ var _g, _h;
320
375
  const author = (_a = options === null || options === void 0 ? void 0 : options.author) !== null && _a !== void 0 ? _a : parts.author;
376
+ // one instant for w:date (the local wall clock, as Word writes it) and w16cex:dateUtc (UTC)
377
+ const made = (_b = options === null || options === void 0 ? void 0 : options.date) !== null && _b !== void 0 ? _b : new Date();
321
378
  const paraId = randomHexId((candidate) => paraIdTaken(parts, body.container, candidate));
322
379
  paragraphs[0].value.paraId = paraId;
323
380
  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()),
381
+ id, author: author.name, initials: (_c = options === null || options === void 0 ? void 0 : options.initials) !== null && _c !== void 0 ? _c : initialsOf(author),
382
+ date: wordCalendarOf(made),
326
383
  content: paragraphs,
327
384
  });
328
- const comments = ((_d = (_h = parts.comments).comment) !== null && _d !== void 0 ? _d : (_h.comment = []));
385
+ const comments = ((_d = (_g = parts.comments).comment) !== null && _d !== void 0 ? _d : (_g.comment = []));
329
386
  comments.push(comment);
330
387
  linkParents(comment, parts.comments);
331
388
  const extended = parts.ensureCommentsEx();
@@ -337,9 +394,12 @@ function addComment(parts, body, id, paragraphs, parent, options) {
337
394
  if (parts.commentsIds) {
338
395
  const durableId = randomHexId((candidate) => { var _a; return ((_a = parts.commentsIds.commentId) !== null && _a !== void 0 ? _a : []).some((e) => e.durableId === candidate); });
339
396
  const commentId = w16cidFactory.createCTCommentId({ paraId, durableId });
340
- ((_f = (_j = parts.commentsIds).commentId) !== null && _f !== void 0 ? _f : (_j.commentId = [])).push(commentId);
397
+ ((_f = (_h = parts.commentsIds).commentId) !== null && _f !== void 0 ? _f : (_h.commentId = [])).push(commentId);
341
398
  linkParents(commentId, parts.commentsIds);
342
- (_g = parts.extensible) === null || _g === void 0 ? void 0 : _g.set(durableId, new Date());
399
+ if (parts.extensible) {
400
+ parts.extensible.set(durableId, made);
401
+ parts.datesUtc.set(paraId.toUpperCase(), made);
402
+ }
343
403
  }
344
404
  addPerson(parts, author.name, author.email);
345
405
  return new Comment(comment, parts, body);
@@ -381,6 +441,31 @@ export async function insertComment(range, content, options) {
381
441
  placeAround(range, id);
382
442
  return addComment(parts, body, id, paragraphs, undefined, options);
383
443
  }
444
+ /**
445
+ * A comment's entry for markers the caller placed (CR-002 section 36, asked by the editor for its
446
+ * E4.e, which places a new comment's markers itself: a selection may span paragraphs, where
447
+ * `Range.insertComment` anchors in one). Everything `insertComment` writes but the markers: the
448
+ * `w:comment` with the given id, author, initials, date and paragraphs (the first with a
449
+ * `w14:paraId`); its `w15:commentEx` (`w15:done="0"`, and `w15:paraIdParent` for a reply); its
450
+ * `w16cid:commentId` where that part exists, and its `w16cex:commentExtensible` where that one does;
451
+ * and the author's `w15:person`. The comment parts are created as `insertComment` creates them, and
452
+ * the date follows section 29's rule (`w:date` on the local wall clock, `w16cex:dateUtc` the date
453
+ * given). The id must be unused in the comments part - markers naming it are expected, being the
454
+ * caller's - and is not checked against them; a comment with no markers is still a comment, which
455
+ * Word shows unanchored.
456
+ */
457
+ export async function addCommentEntry(body, entry) {
458
+ var _a;
459
+ const paragraphs = paragraphsOfContent(entry.content);
460
+ const parts = await commentPartsAccess().create(body);
461
+ const id = entry.id;
462
+ if (!Number.isInteger(id) || id < 0)
463
+ throw new Docx4JException(`A comment id must be a non-negative integer, not ${id}`);
464
+ if (((_a = parts.comments.comment) !== null && _a !== void 0 ? _a : []).some((c) => c.id === id)) {
465
+ throw new Docx4JException(`The comment id ${id} is already in use in this document's comments part`);
466
+ }
467
+ return addComment(parts, body, id, paragraphs, entry.parent, entry);
468
+ }
384
469
  /**
385
470
  * The `w:id` for a new comment: the caller's when given, checked unused across the comments part and
386
471
  * the markers in the story, else the next free one. A clash throws rather than renumbering, as a
@@ -49,6 +49,8 @@ export class Font {
49
49
  if (tracking && !tracking.ownInsertions.has(h))
50
50
  tracking.tracker.recordRPrChange(h.rPr);
51
51
  applyRunOptions(h.rPr, opts);
52
+ if (tracking)
53
+ tracking.tracker.settleRPrChange(h);
52
54
  }
53
55
  }
54
56
  get bold() { return this.read().bold; }
@@ -511,6 +511,7 @@ export class ListItem {
511
511
  }
512
512
  numPr.ilvl = f.createPPrBaseNumPrIlvl({ val: value });
513
513
  linkParents(numPr.ilvl, numPr);
514
+ this.paragraph.settleFormatting();
514
515
  }
515
516
  /**
516
517
  * Office JS `listItem.listString`: the label Word paints in front of the paragraph.
@@ -628,6 +629,7 @@ export function attachToList(paragraph, listId, level = 0) {
628
629
  numPr.numId = f.createPPrBaseNumPrNumId({ val: Number(listId) });
629
630
  numPr.ilvl = f.createPPrBaseNumPrIlvl({ val: level });
630
631
  linkParents(numPr, paragraph.p.pPr);
632
+ paragraph.settleFormatting();
631
633
  return new List(String(listId), paragraph.parentBody);
632
634
  }
633
635
  /**
@@ -640,12 +642,13 @@ export function detachFromList(paragraph) {
640
642
  if (((_a = paragraph.p.pPr) === null || _a === void 0 ? void 0 : _a.numPr) !== undefined) {
641
643
  delete paragraph.pPr().numPr;
642
644
  }
643
- if (!isListItem(paragraph))
644
- return;
645
- const numPr = paragraph.numPr();
646
- numPr.numId = f.createPPrBaseNumPrNumId({ val: 0 });
647
- linkParents(numPr.numId, numPr);
648
- delete numPr.ilvl;
645
+ if (isListItem(paragraph)) {
646
+ const numPr = paragraph.numPr();
647
+ numPr.numId = f.createPPrBaseNumPrNumId({ val: 0 });
648
+ linkParents(numPr.numId, numPr);
649
+ delete numPr.ilvl;
650
+ }
651
+ paragraph.settleFormatting();
649
652
  }
650
653
  /**
651
654
  * Office JS `paragraph.startNewList()`: a `w:abstractNum` copied from docx4j's default
@@ -9,7 +9,7 @@ import { ContentControl, type ContentControlType } from './ContentControl.mjs';
9
9
  import { InlinePicture, type InlinePictureOptions } from './InlinePicture.mjs';
10
10
  import { type CommentContent, type CommentOptions } from './comments.mjs';
11
11
  import type { Comment } from './Comment.mjs';
12
- import { type ChangeTracker } from './tracking.mjs';
12
+ import { ChangeTracker } from './tracking.mjs';
13
13
  import { TrackedChange } from './TrackedChange.mjs';
14
14
  import { type List, type ListItem, type StartListOptions } from './List.mjs';
15
15
  /**
@@ -206,6 +206,20 @@ export declare class Paragraph {
206
206
  * @internal public so that the list views (CR-002 phase H) write `w:numPr` the same way.
207
207
  */
208
208
  pPr(): wml.PPr;
209
+ /**
210
+ * After a tracked write of the paragraph's properties: when the paragraph is as it was, its
211
+ * recorded properties go back as they were and the `w:pPrChange` goes, since Word keeps no
212
+ * formatting change that changes nothing, whoever changed it back (`test/README.md` check 26:
213
+ * centred then left again, by the same author and by another; Increase Indent then Decrease
214
+ * Indent; each left no `w:pPr`; CR-002 section 29). "As it was" is Office JS's paragraph
215
+ * formatting (alignment, indents, spacing, outline level) read resolved from both, the rest of the
216
+ * properties - the style, the list - written the same: Word's Ctrl+L and Decrease Indent removed
217
+ * the direct values where this package's setters write `w:jc="left"` and `w:ind w:left="0"`.
218
+ * Without the package's `PropertyResolver` only properties written the same count.
219
+ *
220
+ * @internal public so that the list views (CR-002 phase H) settle their `w:numPr` writes.
221
+ */
222
+ settleFormatting(): void;
209
223
  /**
210
224
  * The paragraph's `w:numPr`, created when absent, through {@link pPr} so that a tracked
211
225
  * write records `w:pPrChange` first.
@@ -286,6 +300,27 @@ export declare class Paragraph {
286
300
  * Returns the `w:del` elements it made, in document order.
287
301
  */
288
302
  private deleteText;
303
+ /**
304
+ * Splits the revision that holds the text at `at` into two - the second half a copy of its
305
+ * attributes under a new `w:id` - and puts `item` between them. Word splits a revision so when
306
+ * text is typed inside it: around the typist's `w:ins` with tracking on, around plain text with it
307
+ * off (`test/README.md` checks 20 and 21). False, with nothing changed but a run split at `at`,
308
+ * where the text there is not directly in the revision (in a hyperlink inside it, say): the caller
309
+ * then does what it did before.
310
+ */
311
+ private insertBetweenHalves;
312
+ /**
313
+ * Puts `item` beside a revision rather than in it: after it, or before it - and then before the
314
+ * range start of a move whose range opens just there, so that text typed at the start of moved
315
+ * text is not inside the move (`test/README.md` check 20 case 03).
316
+ */
317
+ private insertBesideRevision;
318
+ /**
319
+ * A fresh annotation id for markup this paragraph writes itself: the package's counter when there
320
+ * is a package that keeps one, since a document has one id space; else one above every id in the
321
+ * story.
322
+ */
323
+ private nextAnnotationId;
289
324
  /**
290
325
  * Text inserted at `at` as a `w:ins`. A run of this author's own that is already inside a
291
326
  * `w:ins` is extended rather than nested in another one, as Word does. `anchor` is the `w:del`