@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
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { deepCopy, deepCopyAsSync } from '@docx4j/generated-objects-ts';
|
|
2
|
-
import
|
|
3
|
-
import {
|
|
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
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
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,
|
|
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
|
|
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
|
-
/**
|
|
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
|
-
|
|
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
|
|
192
|
-
* `w:ins
|
|
193
|
-
*
|
|
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
|
-
|
|
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
|
-
/**
|
|
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
|
-
|
|
203
|
-
|
|
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 <
|
|
206
|
-
|
|
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
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
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
|
|
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
|
|
220
|
-
for (const row of (
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
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,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
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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.
|
|
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",
|