@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.
- package/dist/model/content/Body.d.mts +15 -3
- package/dist/model/content/Body.mjs +93 -31
- package/dist/model/content/Comment.d.mts +27 -2
- package/dist/model/content/Comment.mjs +180 -41
- package/dist/model/content/ContentControl.d.mts +6 -0
- package/dist/model/content/ContentControl.mjs +51 -5
- package/dist/model/content/Font.mjs +2 -0
- package/dist/model/content/List.mjs +9 -6
- package/dist/model/content/Paragraph.d.mts +72 -2
- package/dist/model/content/Paragraph.mjs +308 -55
- package/dist/model/content/Range.d.mts +6 -4
- package/dist/model/content/Range.mjs +114 -13
- package/dist/model/content/Table.mjs +6 -8
- package/dist/model/content/TrackedChange.d.mts +125 -6
- package/dist/model/content/TrackedChange.mjs +847 -50
- package/dist/model/content/comments.d.mts +20 -3
- package/dist/model/content/comments.mjs +1 -9
- package/dist/model/content/index.d.mts +3 -3
- package/dist/model/content/index.mjs +2 -2
- package/dist/model/content/stylesFacade.mjs +5 -2
- package/dist/model/content/tableRevisions.d.mts +30 -0
- package/dist/model/content/tableRevisions.mjs +207 -0
- package/dist/model/content/tracking.d.mts +109 -13
- package/dist/model/content/tracking.mjs +267 -43
- package/dist/packages/WordprocessingMLPackage.d.mts +6 -0
- package/dist/packages/WordprocessingMLPackage.mjs +11 -0
- package/dist/parts/wml/commentStyles.d.mts +3 -0
- package/dist/parts/wml/commentStyles.mjs +25 -0
- package/dist/parts/wml/comments.mjs +51 -22
- package/package.json +1 -1
|
@@ -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
|
-
/**
|
|
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
|
|
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
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
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
|
-
/**
|
|
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 (
|
|
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)
|
|
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
|
|
401
|
-
for (const cell of cellsOf(row.element.value))
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
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
|
-
|
|
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
|
|
460
|
+
* to do (docx4j `AcceptTrackedChanges` is the reference). Returns how many were listed.
|
|
419
461
|
*/
|
|
420
462
|
acceptAll() {
|
|
421
|
-
|
|
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
|
|
465
|
+
/** Rejects every tracked change, last first. Returns how many were listed. */
|
|
427
466
|
rejectAll() {
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
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
|
-
/**
|
|
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 {
|
|
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
|
-
/**
|
|
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
|
-
|
|
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
|
-
/**
|
|
230
|
-
|
|
231
|
-
|
|
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
|
|
252
|
-
* the run
|
|
253
|
-
* insertion of its own (CR-002 section 13).
|
|
254
|
-
*
|
|
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
|
-
//
|
|
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
|
-
|
|
282
|
-
|
|
283
|
-
|
|
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
|
|
288
|
-
|
|
289
|
-
const
|
|
290
|
-
|
|
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
|
|
319
|
-
var
|
|
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: (
|
|
325
|
-
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 = (
|
|
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 = (
|
|
415
|
+
((_f = (_h = parts.commentsIds).commentId) !== null && _f !== void 0 ? _f : (_h.commentId = [])).push(commentId);
|
|
341
416
|
linkParents(commentId, parts.commentsIds);
|
|
342
|
-
(
|
|
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
|
-
|
|
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
|
/**
|