@docx4j/core-ts 0.2.2 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,8 +1,9 @@
1
1
  import { deepCopy, deepCopyAsSync } from '@docx4j/generated-objects-ts';
2
- import { createCTTrackChange, createCTRPrChange, createCTRPrChangeRPr, createCTPPrChange, createDelText, createPPr, createParaRPr, createTrPr, } from '@docx4j/generated-objects-ts/factory/org_docx4j_wml';
3
- import { rPrToElements } from '@docx4j/generated-objects-ts/builders/wml';
2
+ import * as el from '@docx4j/generated-objects-ts/el/org_docx4j_wml';
3
+ import { createCTTrackChange, createCTRPrChange, createCTRPrChangeRPr, createCTPPrChange, createCTMarkup, createDelText, createPPr, createParaRPr, createTrPr, } from '@docx4j/generated-objects-ts/factory/org_docx4j_wml';
4
+ import { rPrToElements, rPrFromElements } from '@docx4j/generated-objects-ts/builders/wml';
4
5
  import { Docx4JException } from '../../opc/exceptions.mjs';
5
- import { W_NS, linkParents, walk, typeNameOf, revisionKindOf } from './tree.mjs';
6
+ import { W_NS, linkParents, walk, typeNameOf, revisionKindOf, childrenOf, rowsOf, cellsOf } from './tree.mjs';
6
7
  /**
7
8
  * `w:id` on these is the one annotation id space (ECMA-376 17.13.5.4): revisions, bookmarks,
8
9
  * comment range marks and permissions share it. `w:comment/@w:id` (docx4j `Comments.Comment`,
@@ -39,24 +40,25 @@ export function trackerOf(pkg) {
39
40
  return pkg === null || pkg === void 0 ? void 0 : pkg.changeTracker;
40
41
  }
41
42
  /**
42
- * Now, with the seconds and milliseconds dropped, which is what Word stamps a revision with.
43
- * Not cosmetic: Word groups adjacent revisions by author **and** timestamp, so a per-second stamp
44
- * makes one change of every keystroke run and an editor's typing unreviewable (CR-002 section 28.2).
45
- * A caller that sets `trackedChangeDate` gets exactly the date it asked for, truncated or not.
43
+ * Now, with the seconds and milliseconds dropped, which is what Word stamps a revision with
44
+ * (CR-002 section 28.2). A caller that sets `trackedChangeDate` gets exactly the date it asked for,
45
+ * truncated or not. (Section 28.2 gave a second reason, that Word groups adjacent revisions by their
46
+ * timestamp; check 23 found otherwise for Office JS's list, where an hour apart is still one change,
47
+ * CR-002 section 29.)
46
48
  */
47
49
  export function nowToTheMinute() {
48
50
  const now = new Date();
49
51
  now.setUTCSeconds(0, 0);
50
52
  return now;
51
53
  }
52
- /** An `XmlCalendar` in UTC, as Word writes `w:date` (an xsd:dateTime ending in `Z`). */
54
+ /** An `XmlCalendar` in UTC, ending in `Z`: `w16du:dateUtc`, or any xsd:dateTime meant as UTC. */
53
55
  export function calendarOf(date) {
54
56
  return {
55
57
  year: date.getUTCFullYear(), month: date.getUTCMonth() + 1, day: date.getUTCDate(),
56
58
  hour: date.getUTCHours(), minute: date.getUTCMinutes(), second: date.getUTCSeconds(), timezone: 0,
57
59
  };
58
60
  }
59
- /** The `Date` of a `w:date`, or undefined when there is none. */
61
+ /** The `Date` of an xsd:dateTime read as it says - a missing offset as UTC - or undefined when there is none. */
60
62
  export function dateOf(cal) {
61
63
  var _a, _b, _c, _d, _e, _f;
62
64
  if (!cal || cal.year === undefined)
@@ -64,6 +66,63 @@ export function dateOf(cal) {
64
66
  const ms = Date.UTC(cal.year, ((_a = cal.month) !== null && _a !== void 0 ? _a : 1) - 1, (_b = cal.day) !== null && _b !== void 0 ? _b : 1, (_c = cal.hour) !== null && _c !== void 0 ? _c : 0, (_d = cal.minute) !== null && _d !== void 0 ? _d : 0, (_e = cal.second) !== null && _e !== void 0 ? _e : 0);
65
67
  return new Date(ms - ((_f = cal.timezone) !== null && _f !== void 0 ? _f : 0) * 60000);
66
68
  }
69
+ /**
70
+ * A revision's `w:date` as Word writes it: the local wall-clock time, with a `Z` that does not mean
71
+ * UTC (CR-002 section 29, check 23: Office JS read `01:00:00Z` as 15:00 UTC the day before, at
72
+ * UTC+10; Word's own files carry the true UTC beside it in `w16du:dateUtc`). "Local" is this
73
+ * process's time zone, as it is the add-in's for Office JS.
74
+ */
75
+ export function wordCalendarOf(date) {
76
+ return {
77
+ year: date.getFullYear(), month: date.getMonth() + 1, day: date.getDate(),
78
+ hour: date.getHours(), minute: date.getMinutes(), second: date.getSeconds(), timezone: 0,
79
+ };
80
+ }
81
+ /**
82
+ * A revision's date as Word means it: `w16du:dateUtc` when there is one, which is UTC; else
83
+ * `w:date` read as local wall-clock time, its `Z` notwithstanding, as Office JS reads it (check 23).
84
+ * A `w:date` with a non-zero offset, which Word does not write, is read as it says.
85
+ */
86
+ export function revisionDateOf(value) {
87
+ var _a, _b, _c, _d, _e, _f;
88
+ if (((_a = value.dateUtc) === null || _a === void 0 ? void 0 : _a.year) !== undefined)
89
+ return dateOf(value.dateUtc);
90
+ const cal = value.date;
91
+ if (!cal || cal.year === undefined)
92
+ return undefined;
93
+ if (cal.timezone !== undefined && cal.timezone !== 0)
94
+ return dateOf(cal);
95
+ return new Date(cal.year, ((_b = cal.month) !== null && _b !== void 0 ? _b : 1) - 1, (_c = cal.day) !== null && _c !== void 0 ? _c : 1, (_d = cal.hour) !== null && _d !== void 0 ? _d : 0, (_e = cal.minute) !== null && _e !== void 0 ? _e : 0, (_f = cal.second) !== null && _f !== void 0 ? _f : 0);
96
+ }
97
+ /** Properties compared as values: keys sorted, `PARENT`, `TYPE_NAME` and absent values left out. */
98
+ export function canonical(value) {
99
+ return JSON.stringify(value, (key, v) => {
100
+ if (key === 'PARENT' || key === 'TYPE_NAME' || v === undefined)
101
+ return undefined;
102
+ if (v && typeof v === 'object' && !Array.isArray(v))
103
+ return Object.fromEntries(Object.keys(v).sort().map((k) => [k, v[k]]));
104
+ return v;
105
+ });
106
+ }
107
+ /**
108
+ * Makes sure the part's root lists `w16du` in `mc:Ignorable`, appending it when absent, so that
109
+ * `w16du:dateUtc` may be written in it; the facade then declares the prefix. This is docx4j's
110
+ * pattern - the writer of the content adds its prefix (`Paginate.declareW14Ignorable`) - and Jason's
111
+ * decision (CR-002 section 29); objects CR-008 would do it for every writer. False when there is no
112
+ * unmarshalled root to put it on. Every part a `Body` is over (document, header, footer, notes,
113
+ * comments) has a root that binds `mc:Ignorable`.
114
+ */
115
+ function declareDateUtc(part) {
116
+ var _a;
117
+ const contents = part === null || part === void 0 ? void 0 : part.contents;
118
+ if (!contents || typeof contents !== 'object')
119
+ return false;
120
+ const root = (contents.value && typeof contents.value === 'object' ? contents.value : contents);
121
+ const prefixes = ((_a = root.ignorable) !== null && _a !== void 0 ? _a : '').split(/\s+/).filter((prefix) => prefix.length > 0);
122
+ if (!prefixes.includes('w16du'))
123
+ root.ignorable = [...prefixes, 'w16du'].join(' ');
124
+ return true;
125
+ }
67
126
  function element(localPart, value) {
68
127
  return { name: { namespaceURI: W_NS, localPart }, value };
69
128
  }
@@ -76,9 +135,27 @@ function element(localPart, value) {
76
135
  * The name follows Office JS's vocabulary (`Word.ChangeTrackingMode`).
77
136
  */
78
137
  export class ChangeTracker {
79
- constructor(host, mode) {
138
+ constructor(host, mode,
139
+ /** The part this tracker writes into, when known: it decides whether `w16du:dateUtc` is written. */
140
+ part) {
80
141
  this.host = host;
81
142
  this.mode = mode;
143
+ this.part = part;
144
+ this.perPart = new WeakMap();
145
+ }
146
+ /**
147
+ * This tracker for revisions written into one part (a `Body` asks for its own). The id counter is
148
+ * the host's, so every tracker over a document draws ids from one space.
149
+ */
150
+ forPart(part) {
151
+ if (!part || part === this.part)
152
+ return this;
153
+ let tracker = this.perPart.get(part);
154
+ if (!tracker) {
155
+ tracker = new ChangeTracker(this.host, this.mode, part);
156
+ this.perPart.set(part, tracker);
157
+ }
158
+ return tracker;
82
159
  }
83
160
  /** The `w:author` a new revision carries: the package author's name. */
84
161
  get author() {
@@ -106,10 +183,20 @@ export class ChangeTracker {
106
183
  host.annotationIdCounter = counter;
107
184
  return counter;
108
185
  }
109
- /** The `w:id`, `w:author` and `w:date` a new revision carries. */
186
+ /**
187
+ * The `w:id`, `w:author` and `w:date` a new revision carries, `w:date` in Word's local wall-clock
188
+ * convention (`wordCalendarOf`), and `w16du:dateUtc`, the true UTC, as Word writes them both. The
189
+ * part's root is made to list `w16du` in `mc:Ignorable` first (`declareDateUtc`): a consumer that
190
+ * does not know `w16du` must reject it unless it is declared ignorable. A tracker bound to no part -
191
+ * the package's own - writes `w:date` alone (CR-002 section 29).
192
+ */
110
193
  markup() {
111
194
  var _a;
112
- return { id: this.nextId(), author: this.author, date: calendarOf((_a = this.host.trackedChangeDate) !== null && _a !== void 0 ? _a : nowToTheMinute()) };
195
+ const when = (_a = this.host.trackedChangeDate) !== null && _a !== void 0 ? _a : nowToTheMinute();
196
+ const markup = { id: this.nextId(), author: this.author, date: wordCalendarOf(when) };
197
+ if (declareDateUtc(this.part))
198
+ markup.dateUtc = calendarOf(when);
199
+ return markup;
113
200
  }
114
201
  /** A `w:ins` / `w:del` attribute set: the id, this package's author and the date. */
115
202
  trackChange() {
@@ -147,6 +234,26 @@ export class ChangeTracker {
147
234
  rPr.rPrChange = createCTRPrChange({ ...this.markup(), rPr: original });
148
235
  linkParents(rPr.rPrChange, rPr);
149
236
  }
237
+ /**
238
+ * After a tracked formatting write: the run's `w:rPrChange` goes when its properties are back to
239
+ * what it recorded, since Word keeps no formatting change that changes nothing, whoever changed the
240
+ * formatting back (`test/README.md` check 24: bold then unbold by the same author, and by another; a
241
+ * character style applied then cleared; CR-002 section 29). An `w:rPr` then left empty goes too, as
242
+ * Word writes none.
243
+ */
244
+ settleRPrChange(run) {
245
+ var _a, _b;
246
+ const rPr = run.rPr;
247
+ const change = rPr === null || rPr === void 0 ? void 0 : rPr.rPrChange;
248
+ if (!rPr || !change)
249
+ return;
250
+ const { rPrChange: _recorded, ...current } = rPr;
251
+ if (canonical(current) !== canonical(rPrFromElements((_b = (_a = change.rPr) === null || _a === void 0 ? void 0 : _a.egrPrBase) !== null && _b !== void 0 ? _b : [])))
252
+ return;
253
+ delete rPr.rPrChange;
254
+ if (Object.keys(rPr).every((key) => key === 'TYPE_NAME'))
255
+ delete run.rPr;
256
+ }
150
257
  /** Records the paragraph's properties as they are now in `w:pPrChange`, once. */
151
258
  recordPPrChange(pPr) {
152
259
  if (pPr.pPrChange)
@@ -188,49 +295,166 @@ export class ChangeTracker {
188
295
  }
189
296
  }
190
297
  /**
191
- * A whole paragraph inserted: the mark is marked inserted and every run of it goes into a
192
- * `w:ins`. Word, splitting a paragraph, marks the *first* paragraph's mark instead and leaves
193
- * the new one the original mark; marking the new paragraph's own mark is the same document once
298
+ * A whole paragraph inserted: the mark is marked inserted (unless it already is) and every run of it
299
+ * goes into a `w:ins` - the runs in its hyperlinks, inline content controls, fields and smart tags as
300
+ * well (`wrapNewRuns`). Word, splitting a paragraph, marks the *first* paragraph's mark instead and
301
+ * leaves the new one the original mark; marking the new paragraph's own mark is the same document once
194
302
  * accepted or rejected and keeps the edit to the element that was added (CR-002 section 13).
195
303
  */
196
304
  export function trackInsertedParagraph(tracker, p) {
197
- tracker.markParagraphInserted(p);
305
+ var _a, _b;
306
+ if (!((_b = (_a = p.pPr) === null || _a === void 0 ? void 0 : _a.rPr) === null || _b === void 0 ? void 0 : _b.ins))
307
+ tracker.markParagraphInserted(p);
198
308
  wrapNewRuns(tracker, p);
199
309
  }
200
- /** Every top-level run of the paragraph that is not already in a revision, wrapped in one `w:ins` per group. */
310
+ /**
311
+ * Every run of the paragraph not already in a revision, wrapped in one `w:ins` per group of touching
312
+ * runs - at the top level and inside the run-level holders, where Word's form decides (`test/README.md`
313
+ * check 30, CR-002 section 37 item 7): in a hyperlink or a simple field the `w:ins` goes inside, since
314
+ * neither can be in one (`CT_RunTrackChange` holds `EG_ContentRunContent` only); a smart tag goes
315
+ * *into* the `w:ins` with the runs beside it, its own runs plain; an inline content control or custom
316
+ * XML element keeps the `w:ins` inside and is itself marked inserted (`markHolderInserted`). Before,
317
+ * only the top level was wrapped, so that rejecting an inserted paragraph left a link's text behind
318
+ * (the editor's finding on 0.3.0, CR-002 section 37 item 4).
319
+ */
201
320
  export function wrapNewRuns(tracker, p) {
202
- const content = p.content;
203
- if (!content)
321
+ wrapInsertedRuns(tracker, p.content, p);
322
+ }
323
+ /** True for what goes into a `w:ins` beside the runs: a run, or a smart tag (Word's form, check 30). */
324
+ function wrapsAsRun(item) {
325
+ const tn = typeNameOf(item);
326
+ return (tn === 'org_docx4j_wml.R' || tn === 'org_docx4j_wml.CTSmartTagRun') && revisionKindOf(item) === undefined;
327
+ }
328
+ /**
329
+ * `wrapNewRuns` over run-level items that are about to go into a paragraph (`Paragraph.insertItemsAt`):
330
+ * the runs among them, and those in their holders, wrapped in `w:ins`; a content control or custom
331
+ * XML element among them marked inserted. `parent` is what the items' array belongs to.
332
+ */
333
+ export function wrapInsertedRuns(tracker, items, parent) {
334
+ var _a, _b;
335
+ if (!items)
204
336
  return;
205
- for (let i = 0; i < content.length; i++) {
206
- if (typeNameOf(content[i]) !== 'org_docx4j_wml.R' || revisionKindOf(content[i]) !== undefined)
337
+ for (let i = 0; i < items.length; i++) {
338
+ const item = items[i];
339
+ if (revisionKindOf(item) !== undefined)
340
+ continue; // already a revision
341
+ if (wrapsAsRun(item)) {
342
+ let j = i;
343
+ while (j < items.length && wrapsAsRun(items[j]))
344
+ j++;
345
+ const runs = items.splice(i, j - i);
346
+ const ins = tracker.ins(runs);
347
+ items.splice(i, 0, ins);
348
+ linkParents(ins, parent);
349
+ continue;
350
+ }
351
+ const value = item.value;
352
+ if (typeof value !== 'object' || value === null)
207
353
  continue;
208
- let j = i;
209
- while (j < content.length && typeNameOf(content[j]) === 'org_docx4j_wml.R' && revisionKindOf(content[j]) === undefined)
210
- j++;
211
- const runs = content.splice(i, j - i);
212
- const ins = tracker.ins(runs);
213
- content.splice(i, 0, ins);
214
- linkParents(ins, p);
354
+ const holder = (_a = value.sdtContent) !== null && _a !== void 0 ? _a : value;
355
+ if (HOLDER_TYPES.has((_b = typeNameOf(item)) !== null && _b !== void 0 ? _b : '')) {
356
+ const before = items.length;
357
+ markHolderInserted(tracker, item, items, () => wrapInsertedRuns(tracker, childrenOf(value), holder));
358
+ i += items.length - before;
359
+ continue;
360
+ }
361
+ wrapInsertedRuns(tracker, childrenOf(value), holder);
215
362
  }
216
363
  }
217
- /** A whole table inserted: every row is marked inserted and every paragraph in it is an insertion. */
364
+ /** A whole table inserted: every row is marked inserted and everything in its cells is an insertion (`trackInsertedBlocks`). */
218
365
  export function trackInsertedTable(tracker, tbl) {
219
- var _a, _b, _c;
220
- for (const row of ((_a = tbl.content) !== null && _a !== void 0 ? _a : [])) {
221
- if (typeNameOf(row) !== 'org_docx4j_wml.Tr')
222
- continue;
223
- const tr = row.value;
224
- tracker.markRowInserted(tr);
225
- for (const cell of ((_b = tr.content) !== null && _b !== void 0 ? _b : [])) {
226
- if (typeNameOf(cell) !== 'org_docx4j_wml.Tc')
227
- continue;
228
- for (const block of ((_c = cell.value.content) !== null && _c !== void 0 ? _c : [])) {
229
- if (typeNameOf(block) === 'org_docx4j_wml.P')
230
- trackInsertedParagraph(tracker, block.value);
231
- }
232
- }
366
+ var _a;
367
+ for (const row of rowsOf(tbl)) {
368
+ const tr = row.element.value;
369
+ if (!((_a = tr.trPr) === null || _a === void 0 ? void 0 : _a.ins))
370
+ tracker.markRowInserted(tr);
371
+ for (const cell of cellsOf(tr))
372
+ trackInsertedBlocks(tracker, childrenOf(cell.element.value));
373
+ }
374
+ }
375
+ /**
376
+ * Block content inserted, whatever it holds: a paragraph (`trackInsertedParagraph`), a table
377
+ * (`trackInsertedTable`: rows inside row-level content controls, nested tables in its cells), and a
378
+ * block-level content control's or custom XML element's content, recursively, the element itself
379
+ * marked inserted (`markHolderInserted`; `owner` is the array holding `block`, which takes the
380
+ * markers). What `Body.insertElement` and `Table.addRows` track new content by (CR-002 section 37).
381
+ */
382
+ export function trackInsertedBlock(tracker, block, owner) {
383
+ const tn = typeNameOf(block);
384
+ if (tn === 'org_docx4j_wml.P')
385
+ trackInsertedParagraph(tracker, block.value);
386
+ else if (tn === 'org_docx4j_wml.Tbl')
387
+ trackInsertedTable(tracker, block.value);
388
+ else if (typeof block.value !== 'object' || block.value === null)
389
+ return;
390
+ else if (HOLDER_TYPES.has(tn !== null && tn !== void 0 ? tn : ''))
391
+ markHolderInserted(tracker, block, owner, () => trackInsertedBlocks(tracker, childrenOf(block.value)));
392
+ else
393
+ trackInsertedBlocks(tracker, childrenOf(block.value));
394
+ }
395
+ /** `trackInsertedBlock` over every item of a list, which is their owner. */
396
+ export function trackInsertedBlocks(tracker, items) {
397
+ if (!items)
398
+ return;
399
+ for (let i = 0; i < items.length; i++) {
400
+ const before = items.length;
401
+ trackInsertedBlock(tracker, items[i], items);
402
+ i += items.length - before; // past the markers just put in
403
+ }
404
+ }
405
+ /**
406
+ * The elements Word marks inserted as a whole with `w:customXmlInsRangeStart` / `End`: a content
407
+ * control or a custom XML element (check 30 measured the control at run and block level; a smart tag
408
+ * goes into the `w:ins` instead, and a hyperlink or field has no form of its own). Row- and cell-level
409
+ * ones are listed for the reader: `trackInsertedTable` walks through them to the rows and cells, and
410
+ * does not mark them.
411
+ */
412
+ export const HOLDER_TYPES = new Set([
413
+ 'org_docx4j_wml.SdtRun', 'org_docx4j_wml.SdtBlock', 'org_docx4j_wml.CTSdtRow', 'org_docx4j_wml.CTSdtCell',
414
+ 'org_docx4j_wml.CTCustomXmlRun', 'org_docx4j_wml.CTCustomXmlBlock', 'org_docx4j_wml.CTCustomXmlRow', 'org_docx4j_wml.CTCustomXmlCell',
415
+ ]);
416
+ /** The list a holder's content is in (`w:sdtContent`'s for a control, its own for custom XML), created when absent. */
417
+ export function holderItemsOf(value) {
418
+ var _a, _b;
419
+ var _c;
420
+ const v = value;
421
+ if (v.sdtContent)
422
+ return ((_a = (_c = v.sdtContent).content) !== null && _a !== void 0 ? _a : (_c.content = []));
423
+ if ('element' in v)
424
+ return ((_b = v.content) !== null && _b !== void 0 ? _b : (v.content = [])); // w:customXml carries w:element
425
+ return undefined;
426
+ }
427
+ /**
428
+ * A content control or custom XML element inserted under tracking, as Word writes it (check 30): a
429
+ * `w:customXmlInsRangeStart` before the element with its `w:customXmlInsRangeEnd` first inside its
430
+ * content, and a second start last inside with its end after the element. Rejecting takes the element
431
+ * away and leaves its content where it stood (`TrackedChange`'s `holder` target); without the markers
432
+ * an element holding only an insertion reads as one that was there before and was typed into, which
433
+ * rejecting keeps (check 30, existing-control-typed). `inside` tracks the content between the two
434
+ * pairs, so that the ids run in document order as Word's do.
435
+ */
436
+ export function markHolderInserted(tracker, holder, owner, inside) {
437
+ var _a;
438
+ const at = owner.indexOf(holder);
439
+ const items = holderItemsOf(holder.value);
440
+ if (at < 0 || !items) {
441
+ inside();
442
+ return;
233
443
  }
444
+ const value = holder.value;
445
+ const first = tracker.markup();
446
+ const outerStart = el.customXmlInsRangeStart(createCTTrackChange(first));
447
+ const innerEnd = el.customXmlInsRangeEnd(createCTMarkup({ id: first.id }));
448
+ owner.splice(at, 0, outerStart);
449
+ items.unshift(innerEnd);
450
+ inside();
451
+ const second = tracker.markup();
452
+ const innerStart = el.customXmlInsRangeStart(createCTTrackChange(second));
453
+ const outerEnd = el.customXmlInsRangeEnd(createCTMarkup({ id: second.id }));
454
+ items.push(innerStart);
455
+ owner.splice(owner.indexOf(holder) + 1, 0, outerEnd);
456
+ linkParents([outerStart, outerEnd], value.PARENT);
457
+ linkParents([innerEnd, innerStart], (_a = value.sdtContent) !== null && _a !== void 0 ? _a : value);
234
458
  }
235
459
  /** The paragraph mark's run properties (`w:pPr/w:rPr`), created when absent. */
236
460
  export function paraRPrOf(p) {
@@ -81,6 +81,12 @@ export declare class WordprocessingMLPackage extends OpcPackage implements Track
81
81
  * `styles.ensure` (CR-002 section 25). `saveTo` ensures them; nothing is read before then.
82
82
  */
83
83
  private requiredStyles;
84
+ /**
85
+ * Comments whose anchored text went with a rejected insertion or an accepted deletion: their
86
+ * markers are gone, their entries go from the comment parts when the comments are next read or
87
+ * the package is saved (check 32; `TrackedChange`). An extension.
88
+ */
89
+ readonly pendingCommentRemovals: Set<number>;
84
90
  /**
85
91
  * The parts `seedAnnotationIds()` has read, with what it read: `true` for a part still holding its
86
92
  * source, whose bytes cannot change, and otherwise the bytes that were set, which stay the same
@@ -15,6 +15,7 @@ import { DefaultXPathEngine } from '../model/customxml/xpath.mjs';
15
15
  import { ChangeTracker, highestAnnotationId } from '../model/content/tracking.mjs';
16
16
  import { NumberingFacade } from '../model/content/List.mjs';
17
17
  import { StylesFacade } from '../model/content/stylesFacade.mjs';
18
+ import { flushPendingCommentRemovals } from '../model/content/Comment.mjs';
18
19
  import { PropertyResolver } from '../model/properties/PropertyResolver.mjs';
19
20
  import { PropertyResolverNotCreatedException } from '../opc/exceptions.mjs';
20
21
  import { XmlPart } from '../parts/XmlPart.mjs';
@@ -81,6 +82,12 @@ export class WordprocessingMLPackage extends OpcPackage {
81
82
  * `styles.ensure` (CR-002 section 25). `saveTo` ensures them; nothing is read before then.
82
83
  */
83
84
  this.requiredStyles = new Set();
85
+ /**
86
+ * Comments whose anchored text went with a rejected insertion or an accepted deletion: their
87
+ * markers are gone, their entries go from the comment parts when the comments are next read or
88
+ * the package is saved (check 32; `TrackedChange`). An extension.
89
+ */
90
+ this.pendingCommentRemovals = new Set();
84
91
  /**
85
92
  * The parts `seedAnnotationIds()` has read, with what it read: `true` for a part still holding its
86
93
  * source, whose bytes cannot change, and otherwise the bytes that were set, which stay the same
@@ -497,6 +504,8 @@ export class WordprocessingMLPackage extends OpcPackage {
497
504
  return;
498
505
  target.requireStyles(this.requiredStyles);
499
506
  target.author = { ...this.author };
507
+ for (const id of this.pendingCommentRemovals)
508
+ target.pendingCommentRemovals.add(id);
500
509
  target.trackedChangeDate = this.trackedChangeDate;
501
510
  target.fonts.defaultTheme = this.fonts.defaultTheme;
502
511
  target.xpathEngine = this.xpathEngine;
@@ -505,6 +514,8 @@ export class WordprocessingMLPackage extends OpcPackage {
505
514
  var _a;
506
515
  if (this.trackingPending)
507
516
  await this.setChangeTrackingMode((_a = this.trackingMode) !== null && _a !== void 0 ? _a : 'Off');
517
+ if (this.pendingCommentRemovals.size > 0)
518
+ await flushPendingCommentRemovals(await this.getBody());
508
519
  if (this.requiredStyles.size > 0) {
509
520
  await this.styles.ensure([...this.requiredStyles]);
510
521
  this.requiredStyles.clear();
@@ -0,0 +1,3 @@
1
+ import type * as wml from '@docx4j/generated-objects-ts/modules/org_docx4j_wml';
2
+ /** Fresh copies of the two definitions, each call. */
3
+ export declare function commentStyles(): wml.Style[];
@@ -0,0 +1,25 @@
1
+ import * as wmlFactory from '@docx4j/generated-objects-ts/factory/org_docx4j_wml';
2
+ import { COMMENT_REFERENCE_STYLE, COMMENT_TEXT_STYLE } from '../../model/content/comments.mjs';
3
+ /** Fresh copies of the two definitions, each call. */
4
+ export function commentStyles() {
5
+ return [
6
+ wmlFactory.createStyle({
7
+ type: 'paragraph', styleId: COMMENT_TEXT_STYLE,
8
+ name: wmlFactory.createStyleName({ val: 'annotation text' }),
9
+ basedOn: wmlFactory.createStyleBasedOn({ val: 'Normal' }),
10
+ uiPriority: wmlFactory.createStyleUiPriority({ val: 99 }),
11
+ semiHidden: wmlFactory.createBooleanDefaultTrue(),
12
+ unhideWhenUsed: wmlFactory.createBooleanDefaultTrue(),
13
+ rPr: wmlFactory.createRPr({ sz: wmlFactory.createHpsMeasure({ val: 20 }), szCs: wmlFactory.createHpsMeasure({ val: 20 }) }),
14
+ }),
15
+ wmlFactory.createStyle({
16
+ type: 'character', styleId: COMMENT_REFERENCE_STYLE,
17
+ name: wmlFactory.createStyleName({ val: 'annotation reference' }),
18
+ basedOn: wmlFactory.createStyleBasedOn({ val: 'DefaultParagraphFont' }),
19
+ uiPriority: wmlFactory.createStyleUiPriority({ val: 99 }),
20
+ semiHidden: wmlFactory.createBooleanDefaultTrue(),
21
+ unhideWhenUsed: wmlFactory.createBooleanDefaultTrue(),
22
+ rPr: wmlFactory.createRPr({ sz: wmlFactory.createHpsMeasure({ val: 16 }), szCs: wmlFactory.createHpsMeasure({ val: 16 }) }),
23
+ }),
24
+ ];
25
+ }
@@ -3,10 +3,13 @@ import * as w15Factory from '@docx4j/generated-objects-ts/factory/org_docx4j_w15
3
3
  import * as w16cidFactory from '@docx4j/generated-objects-ts/factory/org_docx4j_w16cid';
4
4
  import { linkParents } from '@docx4j/generated-objects-ts/builders/wml';
5
5
  import { Docx4JException } from '../../opc/exceptions.mjs';
6
+ import { parseXml } from '../../xml/dom.mjs';
6
7
  import { MainDocumentPart, CommentsPart, CommentsExtendedPart, CommentsIdsPart, PeoplePart } from './index.mjs';
7
8
  import { COMMENT_REFERENCE_STYLE, COMMENT_TEXT_STYLE, setCommentPartsAccess, dateToCalendar, } from '../../model/content/comments.mjs';
9
+ import { dateOf } from '../../model/content/tracking.mjs';
8
10
  // Loaded for its side effect: the Comment views register themselves with the content API.
9
11
  import '../../model/content/Comment.mjs';
12
+ import { commentStyles } from './commentStyles.mjs';
10
13
  /** The comment parts of one document. */
11
14
  class DocumentCommentParts {
12
15
  constructor(main, commentsPart, comments, author) {
@@ -14,6 +17,7 @@ class DocumentCommentParts {
14
17
  this.commentsPart = commentsPart;
15
18
  this.comments = comments;
16
19
  this.author = author;
20
+ this.datesUtc = new Map();
17
21
  this.loadedAll = false;
18
22
  }
19
23
  ensureCommentsEx() {
@@ -66,8 +70,52 @@ async function partsFor(main, commentsPart, author) {
66
70
  parts.commentsEx = await main.commentsExtendedPart.getContents();
67
71
  if (main.peoplePart)
68
72
  parts.people = await main.peoplePart.getContents();
73
+ await readDatesUtc(main, parts.datesUtc);
69
74
  return parts;
70
75
  }
76
+ const W16CID_NS = 'http://schemas.microsoft.com/office/word/2016/wordml/cid';
77
+ const W16CEX_NS = 'http://schemas.microsoft.com/office/word/2018/wordml/cex';
78
+ /**
79
+ * Each comment's `w16cex:dateUtc`, by the paragraph id `w16cid:commentId` pairs its durable id with.
80
+ * From the parts' contents when they are unmarshalled, else from their XML, which leaves them
81
+ * untouched: no view needs them otherwise, and an unmarshalled part is re-marshalled on save.
82
+ */
83
+ async function readDatesUtc(main, into) {
84
+ var _a, _b, _c;
85
+ const idsPart = main.commentsIdsPart;
86
+ const extensiblePart = main.commentsExtensiblePart;
87
+ if (!idsPart || !extensiblePart)
88
+ return;
89
+ const paraIdOf = new Map();
90
+ if (idsPart.isUnmarshalled) {
91
+ for (const e of (_a = idsPart.contents.commentId) !== null && _a !== void 0 ? _a : [])
92
+ if (e.durableId && e.paraId)
93
+ paraIdOf.set(e.durableId.toUpperCase(), e.paraId.toUpperCase());
94
+ }
95
+ else {
96
+ for (const e of Array.from(parseXml(await idsPart.getXml()).getElementsByTagNameNS(W16CID_NS, 'commentId'))) {
97
+ const durableId = e.getAttributeNS(W16CID_NS, 'durableId');
98
+ const paraId = e.getAttributeNS(W16CID_NS, 'paraId');
99
+ if (durableId && paraId)
100
+ paraIdOf.set(durableId.toUpperCase(), paraId.toUpperCase());
101
+ }
102
+ }
103
+ const add = (durableId, date) => {
104
+ const paraId = durableId ? paraIdOf.get(durableId.toUpperCase()) : undefined;
105
+ if (paraId && date && !Number.isNaN(date.getTime()))
106
+ into.set(paraId, date);
107
+ };
108
+ if (extensiblePart.isUnmarshalled) {
109
+ for (const e of (_b = extensiblePart.contents.commentExtensible) !== null && _b !== void 0 ? _b : [])
110
+ add(e.durableId, dateOf(e.dateUtc));
111
+ }
112
+ else {
113
+ for (const e of Array.from(parseXml(await extensiblePart.getXml()).getElementsByTagNameNS(W16CEX_NS, 'commentExtensible'))) {
114
+ const value = e.getAttributeNS(W16CEX_NS, 'dateUtc');
115
+ add((_c = e.getAttributeNS(W16CEX_NS, 'durableId')) !== null && _c !== void 0 ? _c : undefined, value ? new Date(value) : undefined);
116
+ }
117
+ }
118
+ }
71
119
  async function ensurePeople(main) {
72
120
  if (main.peoplePart)
73
121
  return main.peoplePart.getContents();
@@ -133,28 +181,9 @@ async function ensureCommentStyles(main) {
133
181
  linkParents(style, styles);
134
182
  (_a = pkg === null || pkg === void 0 ? void 0 : pkg.refreshPropertyResolver) === null || _a === void 0 ? void 0 : _a.call(pkg);
135
183
  };
136
- if (!list.some((s) => s.styleId === COMMENT_TEXT_STYLE)) {
137
- add(wmlFactory.createStyle({
138
- type: 'paragraph', styleId: COMMENT_TEXT_STYLE,
139
- name: wmlFactory.createStyleName({ val: 'annotation text' }),
140
- basedOn: wmlFactory.createStyleBasedOn({ val: 'Normal' }),
141
- uiPriority: wmlFactory.createStyleUiPriority({ val: 99 }),
142
- semiHidden: wmlFactory.createBooleanDefaultTrue(),
143
- unhideWhenUsed: wmlFactory.createBooleanDefaultTrue(),
144
- rPr: wmlFactory.createRPr({ sz: wmlFactory.createHpsMeasure({ val: 20 }), szCs: wmlFactory.createHpsMeasure({ val: 20 }) }),
145
- }));
146
- }
147
- if (!list.some((s) => s.styleId === COMMENT_REFERENCE_STYLE)) {
148
- add(wmlFactory.createStyle({
149
- type: 'character', styleId: COMMENT_REFERENCE_STYLE,
150
- name: wmlFactory.createStyleName({ val: 'annotation reference' }),
151
- basedOn: wmlFactory.createStyleBasedOn({ val: 'DefaultParagraphFont' }),
152
- uiPriority: wmlFactory.createStyleUiPriority({ val: 99 }),
153
- semiHidden: wmlFactory.createBooleanDefaultTrue(),
154
- unhideWhenUsed: wmlFactory.createBooleanDefaultTrue(),
155
- rPr: wmlFactory.createRPr({ sz: wmlFactory.createHpsMeasure({ val: 16 }), szCs: wmlFactory.createHpsMeasure({ val: 16 }) }),
156
- }));
157
- }
184
+ for (const style of commentStyles())
185
+ if (!list.some((s) => s.styleId === style.styleId))
186
+ add(style);
158
187
  }
159
188
  setCommentPartsAccess({
160
189
  async load(body) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@docx4j/core-ts",
3
- "version": "0.2.2",
3
+ "version": "0.3.1",
4
4
  "description": "docx4j-core for TypeScript: Open Packaging (docx/pptx/xlsx zip and flat OPC), typed parts and relationships, style, numbering and font resolution over @docx4j/generated-objects-ts. Node, browsers, Word add-ins.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.mjs",