@docx4j/core-ts 0.2.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -1
- package/dist/model/content/Body.d.mts +15 -3
- package/dist/model/content/Body.mjs +62 -20
- package/dist/model/content/Comment.d.mts +21 -2
- package/dist/model/content/Comment.mjs +104 -19
- package/dist/model/content/Font.mjs +2 -0
- package/dist/model/content/List.mjs +9 -6
- package/dist/model/content/Paragraph.d.mts +36 -1
- package/dist/model/content/Paragraph.mjs +172 -34
- package/dist/model/content/Range.d.mts +9 -4
- package/dist/model/content/Range.mjs +95 -15
- package/dist/model/content/TrackedChange.d.mts +103 -6
- package/dist/model/content/TrackedChange.mjs +713 -20
- 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/search.d.mts +8 -0
- package/dist/model/content/search.mjs +22 -1
- package/dist/model/content/tableRevisions.d.mts +30 -0
- package/dist/model/content/tableRevisions.mjs +207 -0
- package/dist/model/content/tracking.d.mts +58 -8
- package/dist/model/content/tracking.mjs +116 -10
- package/dist/packages/WordprocessingMLPackage.d.mts +12 -0
- package/dist/packages/WordprocessingMLPackage.mjs +22 -0
- package/dist/parts/wml/comments.mjs +47 -0
- package/package.json +1 -1
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { textOf } from '@docx4j/generated-objects-ts/builders/wml';
|
|
2
2
|
import { Docx4JException } from '../../opc/exceptions.mjs';
|
|
3
|
-
import { typeNameOf, runItemsOf, revisionKindOf, linkParents, segmentsOf, textOfView, cellsOf, childrenOf } from './tree.mjs';
|
|
3
|
+
import { typeNameOf, runItemsOf, revisionKindOf, linkParents, segmentsOf, textOfView, cellsOf, rowsOf, childrenOf, W_NS } from './tree.mjs';
|
|
4
4
|
import { Range } from './Range.mjs';
|
|
5
5
|
import { rPrFromElements } from '@docx4j/generated-objects-ts/builders/wml';
|
|
6
|
-
import {
|
|
6
|
+
import { revisionDateOf, toRestoredText, toDeletedText, restoreRPr, restorePPr, pruneParagraphProperties } from './tracking.mjs';
|
|
7
|
+
import { tableRecordsOf, hasTableRecords, tableChangeText, rowText as tableRowText, acceptTable, rejectTable } from './tableRevisions.mjs';
|
|
8
|
+
import { deepCopy } from '@docx4j/generated-objects-ts';
|
|
7
9
|
/**
|
|
8
10
|
* A subset of Office JS `Word.TrackedChange`: `type`, `author`, `date`, `text`, `accept()`,
|
|
9
11
|
* `reject()` and `getRange()`, plus `element`, `id` and `target` as extensions. A view, like
|
|
@@ -23,15 +25,21 @@ export class TrackedChange {
|
|
|
23
25
|
case 'run': return t.revision === 'ins' || t.revision === 'moveTo' ? 'Added' : 'Deleted';
|
|
24
26
|
case 'mark': return t.mark === 'ins' ? 'Added' : 'Deleted';
|
|
25
27
|
case 'row': return t.row === 'ins' ? 'Added' : 'Deleted';
|
|
28
|
+
case 'group': return t.revision === 'ins' ? 'Added' : t.revision === 'del' ? 'Deleted' : 'Formatted';
|
|
29
|
+
case 'cell': return t.cell === 'ins' ? 'Added' : 'Deleted';
|
|
26
30
|
default: return 'Formatted';
|
|
27
31
|
}
|
|
28
32
|
}
|
|
29
33
|
get author() {
|
|
30
34
|
return this.target.value.author;
|
|
31
35
|
}
|
|
32
|
-
/**
|
|
36
|
+
/**
|
|
37
|
+
* When the change was made: `w16du:dateUtc` if the markup has it, else `w:date` read as Word writes
|
|
38
|
+
* it, local wall-clock time (`revisionDateOf`, CR-002 section 29); a group's is its first piece's,
|
|
39
|
+
* as Office JS reports it.
|
|
40
|
+
*/
|
|
33
41
|
get date() {
|
|
34
|
-
return
|
|
42
|
+
return revisionDateOf(this.target.value);
|
|
35
43
|
}
|
|
36
44
|
/** The annotation id (`w:id`; an extension). */
|
|
37
45
|
get id() {
|
|
@@ -39,13 +47,28 @@ export class TrackedChange {
|
|
|
39
47
|
}
|
|
40
48
|
/** The markup this change is over (an extension): the `w:ins` element, the `w:rPrChange`, ... */
|
|
41
49
|
get element() {
|
|
42
|
-
|
|
50
|
+
const t = this.target;
|
|
51
|
+
switch (t.kind) {
|
|
52
|
+
case 'run': return t.element;
|
|
53
|
+
case 'row': return t.tr;
|
|
54
|
+
case 'group': return t.pieces[0].element;
|
|
55
|
+
case 'tableProperties': return t.tbl;
|
|
56
|
+
case 'cell': return t.tc;
|
|
57
|
+
case 'sectionProperties': return t.sectPr;
|
|
58
|
+
default: return t.value;
|
|
59
|
+
}
|
|
43
60
|
}
|
|
44
|
-
/** The text the change covers;
|
|
61
|
+
/** The text the change covers; `\r` for a paragraph mark, as Office JS gives it (check 22). */
|
|
45
62
|
get text() {
|
|
46
63
|
var _a, _b;
|
|
47
64
|
const t = this.target;
|
|
48
65
|
switch (t.kind) {
|
|
66
|
+
case 'mark': return '\r';
|
|
67
|
+
case 'markProperties': return '\r';
|
|
68
|
+
case 'group': return t.pieces.map((piece) => piece.text).join('');
|
|
69
|
+
case 'tableProperties': return tableChangeText(t.tbl.value);
|
|
70
|
+
case 'cell': return tableRowText(t.tr);
|
|
71
|
+
case 'sectionProperties': return t.text;
|
|
49
72
|
case 'run': return segmentsOf(t.element.value, { view: t.revision === 'ins' || t.revision === 'moveTo' ? 'accepted' : 'original' }).map((s) => s.text).join('');
|
|
50
73
|
case 'runProperties': return textOf(t.run);
|
|
51
74
|
case 'paragraphProperties': return (_b = (_a = this.paragraph) === null || _a === void 0 ? void 0 : _a.text) !== null && _b !== void 0 ? _b : '';
|
|
@@ -65,6 +88,13 @@ export class TrackedChange {
|
|
|
65
88
|
const [start, end] = spanOf(p, t.element);
|
|
66
89
|
return new Range(p, start, end);
|
|
67
90
|
}
|
|
91
|
+
if (t.kind === 'group') {
|
|
92
|
+
// a range is one paragraph's: the group's text in the paragraph it starts in
|
|
93
|
+
const here = t.pieces.filter((piece) => { var _a; return (piece.target.kind === 'run' || piece.target.kind === 'runProperties') && ((_a = piece.paragraph) === null || _a === void 0 ? void 0 : _a.element) === p.element; }).map((piece) => piece.getRange());
|
|
94
|
+
if (here.length === 0)
|
|
95
|
+
return t.pieces[0].getRange();
|
|
96
|
+
return new Range(p, Math.min(...here.map((r) => r.start)), Math.max(...here.map((r) => r.end)));
|
|
97
|
+
}
|
|
68
98
|
if (t.kind === 'runProperties') {
|
|
69
99
|
const segs = p.segments().filter((s) => s.run === t.run);
|
|
70
100
|
if (segs.length === 0)
|
|
@@ -73,12 +103,17 @@ export class TrackedChange {
|
|
|
73
103
|
}
|
|
74
104
|
return p.getRange('Whole');
|
|
75
105
|
}
|
|
76
|
-
/**
|
|
106
|
+
/**
|
|
107
|
+
* Keeps the change: a `w:ins` is unwrapped, a `w:del` removed, a deleted mark joins the paragraphs.
|
|
108
|
+
* Either half of a move keeps the whole move, as Word's Review tab does (CR-002 section 29).
|
|
109
|
+
*/
|
|
77
110
|
accept() {
|
|
78
|
-
var _a, _b, _c;
|
|
111
|
+
var _a, _b, _c, _d, _e, _f;
|
|
79
112
|
const t = this.target;
|
|
80
113
|
switch (t.kind) {
|
|
81
114
|
case 'run':
|
|
115
|
+
if ((t.revision === 'moveFrom' || t.revision === 'moveTo') && (resolveMove(this, 'accept') || resolveRangelessHalf(this, 'accept')))
|
|
116
|
+
return;
|
|
82
117
|
if (t.revision === 'ins' || t.revision === 'moveTo')
|
|
83
118
|
unwrap(t);
|
|
84
119
|
else
|
|
@@ -104,19 +139,47 @@ export class TrackedChange {
|
|
|
104
139
|
// w:trPr/w:ins; an accepted deletion takes the row and everything in it away
|
|
105
140
|
if (t.row === 'ins') {
|
|
106
141
|
(_c = t.tr.value.trPr) === null || _c === void 0 ? true : delete _c.ins;
|
|
142
|
+
pruneRowProperties(t.tr.value);
|
|
107
143
|
applyInner(t.inner, 'accept');
|
|
108
144
|
}
|
|
109
145
|
else
|
|
110
146
|
remove(t.owner, t.tr);
|
|
111
147
|
return;
|
|
148
|
+
case 'group':
|
|
149
|
+
resolvePieces(t.pieces, 'accept');
|
|
150
|
+
return;
|
|
151
|
+
case 'tableProperties':
|
|
152
|
+
acceptTable(t.tbl.value);
|
|
153
|
+
return;
|
|
154
|
+
case 'cell':
|
|
155
|
+
// an inserted cell stays, a deleted one goes (check 27)
|
|
156
|
+
if (t.cell === 'ins')
|
|
157
|
+
(_d = t.tc.value.tcPr) === null || _d === void 0 ? true : delete _d.cellIns;
|
|
158
|
+
else
|
|
159
|
+
removeCell(t.tc, t.owner);
|
|
160
|
+
return;
|
|
161
|
+
case 'markProperties': {
|
|
162
|
+
const p = this.requireParagraph().p;
|
|
163
|
+
(_f = (_e = p.pPr) === null || _e === void 0 ? void 0 : _e.rPr) === null || _f === void 0 ? true : delete _f.rPrChange;
|
|
164
|
+
pruneParagraphProperties(p);
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
case 'sectionProperties':
|
|
168
|
+
delete t.sectPr.sectPrChange;
|
|
169
|
+
return;
|
|
112
170
|
}
|
|
113
171
|
}
|
|
114
|
-
/**
|
|
172
|
+
/**
|
|
173
|
+
* Puts back what was there: a `w:ins` is removed, a `w:del` restored, a `w:rPrChange` re-applied.
|
|
174
|
+
* Either half of a move rejects the whole move, as Word's Review tab does (CR-002 section 29).
|
|
175
|
+
*/
|
|
115
176
|
reject() {
|
|
116
|
-
var _a;
|
|
177
|
+
var _a, _b;
|
|
117
178
|
const t = this.target;
|
|
118
179
|
switch (t.kind) {
|
|
119
180
|
case 'run':
|
|
181
|
+
if ((t.revision === 'moveFrom' || t.revision === 'moveTo') && (resolveMove(this, 'reject') || resolveRangelessHalf(this, 'reject')))
|
|
182
|
+
return;
|
|
120
183
|
if (t.revision === 'ins' || t.revision === 'moveTo')
|
|
121
184
|
remove(t.owner, t.element);
|
|
122
185
|
else
|
|
@@ -132,8 +195,10 @@ export class TrackedChange {
|
|
|
132
195
|
restoreRPr(t.run, rPrFromElements(t.value.rPr));
|
|
133
196
|
return;
|
|
134
197
|
case 'paragraphProperties': {
|
|
135
|
-
const
|
|
136
|
-
restorePPr(p, t.value.pPr);
|
|
198
|
+
const paragraph = this.requireParagraph();
|
|
199
|
+
restorePPr(paragraph.p, t.value.pPr);
|
|
200
|
+
dropLevelIndent(paragraph);
|
|
201
|
+
writeRecordedLevel(paragraph.p);
|
|
137
202
|
return;
|
|
138
203
|
}
|
|
139
204
|
case 'row':
|
|
@@ -141,11 +206,30 @@ export class TrackedChange {
|
|
|
141
206
|
// the w:del unwrapped, the deleted marks dropped); a rejected insertion takes it away
|
|
142
207
|
if (t.row === 'del') {
|
|
143
208
|
(_a = t.tr.value.trPr) === null || _a === void 0 ? true : delete _a.del;
|
|
209
|
+
pruneRowProperties(t.tr.value);
|
|
144
210
|
applyInner(t.inner, 'reject');
|
|
145
211
|
}
|
|
146
212
|
else
|
|
147
213
|
remove(t.owner, t.tr);
|
|
148
214
|
return;
|
|
215
|
+
case 'group':
|
|
216
|
+
resolvePieces(t.pieces, 'reject');
|
|
217
|
+
return;
|
|
218
|
+
case 'tableProperties':
|
|
219
|
+
rejectTable(t.tbl.value);
|
|
220
|
+
return;
|
|
221
|
+
case 'cell':
|
|
222
|
+
if (t.cell === 'del')
|
|
223
|
+
(_b = t.tc.value.tcPr) === null || _b === void 0 ? true : delete _b.cellDel;
|
|
224
|
+
else
|
|
225
|
+
removeCell(t.tc, t.owner);
|
|
226
|
+
return;
|
|
227
|
+
case 'markProperties':
|
|
228
|
+
restoreMarkProperties(this.requireParagraph().p, t.value);
|
|
229
|
+
return;
|
|
230
|
+
case 'sectionProperties':
|
|
231
|
+
restoreSection(t.sectPr, t.value);
|
|
232
|
+
return;
|
|
149
233
|
}
|
|
150
234
|
}
|
|
151
235
|
requireParagraph() {
|
|
@@ -182,6 +266,70 @@ function applyInner(inner, what) {
|
|
|
182
266
|
for (let i = inner.length - 1; i >= 0; i--)
|
|
183
267
|
inner[i][what]();
|
|
184
268
|
}
|
|
269
|
+
/**
|
|
270
|
+
* Word records a list paragraph's indent in `w:pPrChange` even where the list level gives it, and on
|
|
271
|
+
* reject writes no indent back: it restored "The first numbered item." to its numbered list with no
|
|
272
|
+
* `w:ind`, where the change recorded `left=720 hanging=360`, the level's own (`test/README.md` check
|
|
273
|
+
* 18, `reject-all.docx`, section 9), and check 25's level-0 items with none where it recorded only
|
|
274
|
+
* `hanging=360`, part of the level's `left=720 hanging=360` (`25b-bullets-reject.docx`; CR-002
|
|
275
|
+
* section 29). So a restored `w:ind` whose every attribute is the level's is dropped, leaving the level
|
|
276
|
+
* to give it; one with an attribute of its own - a genuinely direct indent - is restored. The numbering
|
|
277
|
+
* part must have been read (`await pkg.getBody()` or `getPropertyResolver()` reads it); when it has
|
|
278
|
+
* not, the recorded indent is restored as it was.
|
|
279
|
+
*/
|
|
280
|
+
function dropLevelIndent(paragraph) {
|
|
281
|
+
var _a;
|
|
282
|
+
const pPr = paragraph.p.pPr;
|
|
283
|
+
const numPr = pPr === null || pPr === void 0 ? void 0 : pPr.numPr;
|
|
284
|
+
const ind = pPr === null || pPr === void 0 ? void 0 : pPr.ind;
|
|
285
|
+
if (!pPr || !numPr || !ind)
|
|
286
|
+
return;
|
|
287
|
+
const part = (_a = paragraph.parentBody.package_) === null || _a === void 0 ? void 0 : _a.numberingDefinitionsPart;
|
|
288
|
+
let level;
|
|
289
|
+
try {
|
|
290
|
+
level = part === null || part === void 0 ? void 0 : part.getInd(numPr);
|
|
291
|
+
}
|
|
292
|
+
catch {
|
|
293
|
+
return; // not read: nothing to compare with
|
|
294
|
+
}
|
|
295
|
+
if (!level || !indentWithin(ind, level))
|
|
296
|
+
return;
|
|
297
|
+
delete pPr.ind;
|
|
298
|
+
pruneParagraphProperties(paragraph.p);
|
|
299
|
+
}
|
|
300
|
+
/**
|
|
301
|
+
* A restored `w:numPr` that names a list but no level is at level 0, and Word writes the 0: it
|
|
302
|
+
* restored the level recorded, not the current one, when the two differed (check 25's
|
|
303
|
+
* `25e-bullet-demote.docx`, recorded `numId 1` alone and current `ilvl 1`, rejected to `ilvl 0,
|
|
304
|
+
* numId 1`), and wrote `w:ilvl="0"` every time the record had none (check 18's section 9, 25b, 25f;
|
|
305
|
+
* CR-002 section 29).
|
|
306
|
+
*/
|
|
307
|
+
function writeRecordedLevel(p) {
|
|
308
|
+
var _a;
|
|
309
|
+
const numPr = (_a = p.pPr) === null || _a === void 0 ? void 0 : _a.numPr;
|
|
310
|
+
if (!(numPr === null || numPr === void 0 ? void 0 : numPr.numId) || Number(numPr.numId.val) === 0 || numPr.ilvl)
|
|
311
|
+
return;
|
|
312
|
+
numPr.ilvl = { TYPE_NAME: 'org_docx4j_wml.PPrBase.NumPr.Ilvl', val: 0 };
|
|
313
|
+
linkParents(numPr.ilvl, numPr);
|
|
314
|
+
}
|
|
315
|
+
/** True when every attribute `ind` sets, `level` sets to the same value. */
|
|
316
|
+
function indentWithin(ind, level) {
|
|
317
|
+
const attributes = Object.entries(ind).filter(([key, value]) => key !== 'TYPE_NAME' && key !== 'PARENT' && value !== undefined);
|
|
318
|
+
return attributes.length > 0 && attributes.every(([key, value]) => String(level[key]) === String(value));
|
|
319
|
+
}
|
|
320
|
+
/**
|
|
321
|
+
* A group's pieces resolved: its runs last first, then its marks last first, so that a paragraph join
|
|
322
|
+
* never strands a piece still to do. Office JS and Word's Review tab resolve the group as one (checks
|
|
323
|
+
* 22 and 23), and so, piece by piece, does this.
|
|
324
|
+
*/
|
|
325
|
+
function resolvePieces(pieces, what) {
|
|
326
|
+
const runs = pieces.filter((piece) => piece.target.kind !== 'mark');
|
|
327
|
+
const marks = pieces.filter((piece) => piece.target.kind === 'mark');
|
|
328
|
+
for (let i = runs.length - 1; i >= 0; i--)
|
|
329
|
+
runs[i][what]();
|
|
330
|
+
for (let i = marks.length - 1; i >= 0; i--)
|
|
331
|
+
marks[i][what]();
|
|
332
|
+
}
|
|
185
333
|
function remove(owner, element) {
|
|
186
334
|
const i = owner.indexOf(element);
|
|
187
335
|
if (i >= 0)
|
|
@@ -260,8 +408,28 @@ export function joinWithNext(paragraph, fallbackToPrevious = false) {
|
|
|
260
408
|
p.pPr = nextP.pPr;
|
|
261
409
|
if (p.pPr)
|
|
262
410
|
linkParents(p.pPr, p);
|
|
411
|
+
// The mark that survives is the next paragraph's, and so is the paragraph's identity: Word keeps the
|
|
412
|
+
// second paragraph's w14:paraId when a mark goes (check 18's accept-all and reject-all, check 22's
|
|
413
|
+
// 22-accept-whole; found by the editor's C5). Its w14:textId and session ids come with it; Word
|
|
414
|
+
// writes a new textId, the text having changed, which Word will do again when it next saves.
|
|
415
|
+
for (const key of PARAGRAPH_ATTRIBUTES) {
|
|
416
|
+
const value = nextP[key];
|
|
417
|
+
if (value === undefined)
|
|
418
|
+
delete p[key];
|
|
419
|
+
else
|
|
420
|
+
p[key] = value;
|
|
421
|
+
}
|
|
263
422
|
container.splice(i + 1, 1);
|
|
264
423
|
}
|
|
424
|
+
/** A `w:p`'s own attributes, which go with its mark. */
|
|
425
|
+
const PARAGRAPH_ATTRIBUTES = ['paraId', 'textId', 'rsidDel', 'rsidP', 'rsidR', 'rsidRDefault', 'rsidRPr', 'noSpellErr'];
|
|
426
|
+
/** A row's `w:trPr` with nothing left in it goes, as Word writes none (check 18's accept-all and reject-all). */
|
|
427
|
+
function pruneRowProperties(tr) {
|
|
428
|
+
var _a;
|
|
429
|
+
const trPr = tr.trPr;
|
|
430
|
+
if (trPr && !trPr.ins && !trPr.del && !trPr.trPrChange && !((_a = trPr.cnfStyleOrDivIdOrGridBefore) === null || _a === void 0 ? void 0 : _a.length))
|
|
431
|
+
delete tr.trPr;
|
|
432
|
+
}
|
|
265
433
|
/** The span of a run-level revision in the paragraph's accepted text; collapsed for a deletion. */
|
|
266
434
|
function spanOf(paragraph, element) {
|
|
267
435
|
const segs = paragraph.segments();
|
|
@@ -298,45 +466,430 @@ function acceptedOffsetOf(p, target) {
|
|
|
298
466
|
visit(runItemsOf(p));
|
|
299
467
|
return found >= 0 ? found : pos;
|
|
300
468
|
}
|
|
469
|
+
const RANGE_MARKERS = {
|
|
470
|
+
moveFromRangeStart: { kind: 'start', side: 'from' }, moveFromRangeEnd: { kind: 'end', side: 'from' },
|
|
471
|
+
moveToRangeStart: { kind: 'start', side: 'to' }, moveToRangeEnd: { kind: 'end', side: 'to' },
|
|
472
|
+
};
|
|
473
|
+
/**
|
|
474
|
+
* Accepts or rejects the whole move a run-level half belongs to, as Word's Review tab does from
|
|
475
|
+
* either half (`test/README.md` check 18, Word 16.0.20326.20158; Office JS cannot list a move at
|
|
476
|
+
* all there, check 17). Word pairs the halves by the `w:name` on their range starts
|
|
477
|
+
* (`w:moveFromRangeStart`, `w:moveToRangeStart`); `w:moveFrom` and `w:moveTo` carry none, so a half
|
|
478
|
+
* belongs to the move whose range of its side is open where it stands. Accepting keeps the
|
|
479
|
+
* destination and removes the source, paragraph mark and all; rejecting the other way round; the
|
|
480
|
+
* four range markers go either way. Returns false for a half in no named range - markup without
|
|
481
|
+
* range markers, or a half already resolved - which the caller then treats on its own, as before.
|
|
482
|
+
*/
|
|
483
|
+
function resolveMove(change, action) {
|
|
484
|
+
var _a, _b, _c;
|
|
485
|
+
const t = change.target;
|
|
486
|
+
if (t.kind !== 'run' || !change.paragraph)
|
|
487
|
+
return false;
|
|
488
|
+
const story = storyOf(change.paragraph.p);
|
|
489
|
+
const pieces = movePiecesOf(story);
|
|
490
|
+
const half = pieces.find((piece) => piece.kind === 'half' && piece.element === t.element);
|
|
491
|
+
if (!half || half.kind !== 'half' || half.names.length === 0)
|
|
492
|
+
return false;
|
|
493
|
+
const name = half.names[0];
|
|
494
|
+
const ids = new Set(pieces.filter((piece) => piece.kind === 'start' && piece.name === name).map((piece) => piece.id));
|
|
495
|
+
const mine = pieces.filter((piece) => (piece.kind === 'start' && piece.name === name) || (piece.kind === 'end' && ids.has(piece.id))
|
|
496
|
+
|| ((piece.kind === 'half' || piece.kind === 'mark' || piece.kind === 'inner') && piece.names.includes(name)));
|
|
497
|
+
// A destination whose source is gone is kept whichever way it is resolved: Word rejected such a
|
|
498
|
+
// move and left its text, plain, where it stood (test/README.md check 20 case 12).
|
|
499
|
+
const orphan = !mine.some((piece) => piece.side === 'from');
|
|
500
|
+
const kept = action === 'accept' || orphan ? 'to' : 'from';
|
|
501
|
+
const broken = orphan || !mine.some((piece) => piece.side === 'to');
|
|
502
|
+
// the markers first: a range end between two paragraphs would stop the paragraph join below
|
|
503
|
+
for (const piece of mine)
|
|
504
|
+
if (piece.kind === 'start' || piece.kind === 'end')
|
|
505
|
+
remove(piece.owner, piece.element);
|
|
506
|
+
// Rejected, a move takes with it everything typed into its destination: Word's default when moved
|
|
507
|
+
// text was edited (the Tracked Moves Conflict Dialog's "keep original location text", check 20
|
|
508
|
+
// cases 01 and 04). Text typed before the range starts is outside the move and stays (case 03).
|
|
509
|
+
// What accepting does to text typed into the source was not measured, so it is left alone.
|
|
510
|
+
if (kept === 'from') {
|
|
511
|
+
for (const piece of mine)
|
|
512
|
+
if (piece.kind === 'inner' && piece.side === 'to')
|
|
513
|
+
remove(piece.owner, piece.element);
|
|
514
|
+
}
|
|
515
|
+
for (const piece of mine) {
|
|
516
|
+
if (piece.kind !== 'half')
|
|
517
|
+
continue;
|
|
518
|
+
const target = { kind: 'run', revision: piece.side === 'from' ? 'moveFrom' : 'moveTo', element: piece.element, owner: piece.owner, value: piece.element.value };
|
|
519
|
+
if (piece.side === kept) {
|
|
520
|
+
if (piece.side === 'from')
|
|
521
|
+
restoreDeleted(target);
|
|
522
|
+
else
|
|
523
|
+
unwrap(target);
|
|
524
|
+
}
|
|
525
|
+
else
|
|
526
|
+
remove(piece.owner, piece.element);
|
|
527
|
+
}
|
|
528
|
+
// the paragraph marks last, last first, so that a join never disturbs one still to do
|
|
529
|
+
const marks = mine.filter((piece) => piece.kind === 'mark');
|
|
530
|
+
for (let i = marks.length - 1; i >= 0; i--) {
|
|
531
|
+
const mark = marks[i];
|
|
532
|
+
const p = mark.paragraph.value;
|
|
533
|
+
if (mark.side === kept) {
|
|
534
|
+
const rPr = (_a = p.pPr) === null || _a === void 0 ? void 0 : _a.rPr;
|
|
535
|
+
if (rPr)
|
|
536
|
+
delete rPr[mark.side === 'from' ? 'moveFrom' : 'moveTo'];
|
|
537
|
+
pruneParagraphProperties(p);
|
|
538
|
+
}
|
|
539
|
+
else if (((_b = p.content) !== null && _b !== void 0 ? _b : []).length === 0) {
|
|
540
|
+
remove(mark.container, mark.paragraph); // the moved paragraph, emptied: gone, as in Word
|
|
541
|
+
}
|
|
542
|
+
else {
|
|
543
|
+
const rPr = (_c = p.pPr) === null || _c === void 0 ? void 0 : _c.rPr;
|
|
544
|
+
if (rPr)
|
|
545
|
+
delete rPr[mark.side === 'from' ? 'moveFrom' : 'moveTo'];
|
|
546
|
+
joinWithNext({ container: mark.container, element: mark.paragraph, p }, mark.side === 'to');
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
if (broken)
|
|
550
|
+
dissolveBrokenMoves(story);
|
|
551
|
+
return true;
|
|
552
|
+
}
|
|
553
|
+
/** The story a paragraph is in: the outermost array of block content above it (a body, a header, a note). */
|
|
554
|
+
function storyOf(p) {
|
|
555
|
+
let story;
|
|
556
|
+
for (let o = p; o !== undefined; o = o.PARENT) {
|
|
557
|
+
const content = o.content;
|
|
558
|
+
if (Array.isArray(content) && o.TYPE_NAME !== 'org_docx4j_wml.P')
|
|
559
|
+
story = content;
|
|
560
|
+
}
|
|
561
|
+
return story !== null && story !== void 0 ? story : [];
|
|
562
|
+
}
|
|
563
|
+
/** Every piece of move markup in a story, in document order, each half and mark with the names of the ranges open over it. */
|
|
564
|
+
function movePiecesOf(story) {
|
|
565
|
+
const out = [];
|
|
566
|
+
const open = { from: new Map(), to: new Map() };
|
|
567
|
+
const visit = (items, runLevel = false) => {
|
|
568
|
+
var _a, _b, _c, _d, _e, _f;
|
|
569
|
+
for (const el of items) {
|
|
570
|
+
const marker = RANGE_MARKERS[(_b = (_a = el.name) === null || _a === void 0 ? void 0 : _a.localPart) !== null && _b !== void 0 ? _b : ''];
|
|
571
|
+
const value = el.value;
|
|
572
|
+
if (marker && value) {
|
|
573
|
+
const id = Number(value.id);
|
|
574
|
+
if (marker.kind === 'start') {
|
|
575
|
+
open[marker.side].set(id, (_c = value.name) !== null && _c !== void 0 ? _c : '');
|
|
576
|
+
out.push({ kind: 'start', side: marker.side, id, name: (_d = value.name) !== null && _d !== void 0 ? _d : '', element: el, owner: items });
|
|
577
|
+
}
|
|
578
|
+
else {
|
|
579
|
+
open[marker.side].delete(id);
|
|
580
|
+
out.push({ kind: 'end', side: marker.side, id, element: el, owner: items });
|
|
581
|
+
}
|
|
582
|
+
continue;
|
|
583
|
+
}
|
|
584
|
+
const revision = revisionKindOf(el);
|
|
585
|
+
if (revision === 'moveFrom' || revision === 'moveTo') {
|
|
586
|
+
const side = revision === 'moveFrom' ? 'from' : 'to';
|
|
587
|
+
out.push({ kind: 'half', side, element: el, owner: items, names: [...open[side].values()] });
|
|
588
|
+
}
|
|
589
|
+
else if (runLevel) {
|
|
590
|
+
for (const side of ['from', 'to']) {
|
|
591
|
+
if (open[side].size > 0)
|
|
592
|
+
out.push({ kind: 'inner', side, element: el, owner: items, names: [...open[side].values()] });
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
const v = el.value;
|
|
596
|
+
if (typeof v !== 'object' || v === null)
|
|
597
|
+
continue;
|
|
598
|
+
const children = (_e = childrenOf(v)) !== null && _e !== void 0 ? _e : runItemsOf(v);
|
|
599
|
+
if (children)
|
|
600
|
+
visit(children, typeNameOf(el) === 'org_docx4j_wml.P');
|
|
601
|
+
if (typeNameOf(el) === 'org_docx4j_wml.P') {
|
|
602
|
+
const rPr = (_f = v.pPr) === null || _f === void 0 ? void 0 : _f.rPr;
|
|
603
|
+
for (const side of ['from', 'to']) {
|
|
604
|
+
if (rPr === null || rPr === void 0 ? void 0 : rPr[side === 'from' ? 'moveFrom' : 'moveTo']) {
|
|
605
|
+
out.push({ kind: 'mark', side, paragraph: el, container: items, names: [...open[side].values()] });
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
};
|
|
611
|
+
visit(story);
|
|
612
|
+
return out;
|
|
613
|
+
}
|
|
614
|
+
/**
|
|
615
|
+
* A move half in no named range - its range markers gone - resolved as Word resolved one (check 20
|
|
616
|
+
* cases 13 and 14, where Word showed it as an insertion or a deletion): a `w:moveTo` as an insertion,
|
|
617
|
+
* a `w:moveFrom` as a deletion, its paragraph mark with it, so that a paragraph whose text and mark
|
|
618
|
+
* both go goes whole. Then the rest of the broken pair is dissolved (`dissolveBrokenMoves`).
|
|
619
|
+
*/
|
|
620
|
+
function resolveRangelessHalf(change, action) {
|
|
621
|
+
var _a, _b, _c;
|
|
622
|
+
const t = change.target;
|
|
623
|
+
const paragraph = change.paragraph;
|
|
624
|
+
if (t.kind !== 'run' || (t.revision !== 'moveTo' && t.revision !== 'moveFrom') || !paragraph)
|
|
625
|
+
return false;
|
|
626
|
+
const story = storyOf(paragraph.p);
|
|
627
|
+
const pieces = movePiecesOf(story);
|
|
628
|
+
const half = pieces.find((piece) => piece.kind === 'half' && piece.element === t.element);
|
|
629
|
+
if (!half || half.kind !== 'half' || half.names.length > 0)
|
|
630
|
+
return false;
|
|
631
|
+
const side = half.side;
|
|
632
|
+
const key = side === 'from' ? 'moveFrom' : 'moveTo';
|
|
633
|
+
const keep = (side === 'to') === (action === 'accept'); // an insertion accepted, a deletion rejected
|
|
634
|
+
if (!keep)
|
|
635
|
+
remove(t.owner, t.element);
|
|
636
|
+
else if (side === 'to')
|
|
637
|
+
unwrap(t);
|
|
638
|
+
else
|
|
639
|
+
restoreDeleted(t);
|
|
640
|
+
const mark = pieces.find((piece) => piece.kind === 'mark' && piece.side === side && piece.paragraph === paragraph.element && piece.names.length === 0);
|
|
641
|
+
const p = paragraph.p;
|
|
642
|
+
if (mark && ((_b = (_a = p.pPr) === null || _a === void 0 ? void 0 : _a.rPr) === null || _b === void 0 ? void 0 : _b[key])) {
|
|
643
|
+
if (keep) {
|
|
644
|
+
delete p.pPr.rPr[key];
|
|
645
|
+
pruneParagraphProperties(p);
|
|
646
|
+
}
|
|
647
|
+
else if (((_c = p.content) !== null && _c !== void 0 ? _c : []).length === 0) {
|
|
648
|
+
remove(paragraph.container, paragraph.element);
|
|
649
|
+
}
|
|
650
|
+
else {
|
|
651
|
+
delete p.pPr.rPr[key];
|
|
652
|
+
joinWithNext(paragraph, side === 'to');
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
dissolveBrokenMoves(story);
|
|
656
|
+
return true;
|
|
657
|
+
}
|
|
658
|
+
/**
|
|
659
|
+
* Every move half and moved paragraph mark in the story that cannot be paired - in no named range,
|
|
660
|
+
* or in one whose `w:name` no range of the other side carries - turned into the plain `w:ins` or
|
|
661
|
+
* `w:del` it stands for, pending, its range markers kept. That is how Word saved each broken pair
|
|
662
|
+
* in check 20 (re-run, cases 12 to 14, saved unchanged) and what it left of the other side once one
|
|
663
|
+
* was accepted or rejected. It runs after a broken move is resolved, and only then, so a document's
|
|
664
|
+
* other moves are left as they are; but a broken pair's halves cannot be told from another's, so
|
|
665
|
+
* this takes them all.
|
|
666
|
+
*/
|
|
667
|
+
function dissolveBrokenMoves(story) {
|
|
668
|
+
var _a;
|
|
669
|
+
const pieces = movePiecesOf(story);
|
|
670
|
+
const named = { from: new Set(), to: new Set() };
|
|
671
|
+
for (const piece of pieces)
|
|
672
|
+
if (piece.kind === 'start')
|
|
673
|
+
named[piece.side].add(piece.name);
|
|
674
|
+
const unpaired = (side, names) => {
|
|
675
|
+
const other = named[side === 'from' ? 'to' : 'from'];
|
|
676
|
+
return names.every((name) => !other.has(name)); // true for no names too
|
|
677
|
+
};
|
|
678
|
+
for (const piece of pieces) {
|
|
679
|
+
if (piece.kind === 'half' && unpaired(piece.side, piece.names))
|
|
680
|
+
toPlainRevision(piece.element, piece.owner, piece.side === 'from' ? 'del' : 'ins');
|
|
681
|
+
if (piece.kind === 'mark' && unpaired(piece.side, piece.names)) {
|
|
682
|
+
const rPr = (_a = piece.paragraph.value.pPr) === null || _a === void 0 ? void 0 : _a.rPr;
|
|
683
|
+
if (piece.side === 'from' && (rPr === null || rPr === void 0 ? void 0 : rPr.moveFrom)) {
|
|
684
|
+
rPr.del = rPr.moveFrom;
|
|
685
|
+
delete rPr.moveFrom;
|
|
686
|
+
}
|
|
687
|
+
if (piece.side === 'to' && (rPr === null || rPr === void 0 ? void 0 : rPr.moveTo)) {
|
|
688
|
+
rPr.ins = rPr.moveTo;
|
|
689
|
+
delete rPr.moveTo;
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
/**
|
|
695
|
+
* A `w:moveFrom` turned into the plain `w:del` of the same runs, their text become deleted text, or a
|
|
696
|
+
* `w:moveTo` into the plain `w:ins`.
|
|
697
|
+
*/
|
|
698
|
+
function toPlainRevision(element, owner, kind) {
|
|
699
|
+
var _a;
|
|
700
|
+
const value = element.value;
|
|
701
|
+
const items = (_a = runItemsOf(element.value)) !== null && _a !== void 0 ? _a : [];
|
|
702
|
+
if (kind === 'del')
|
|
703
|
+
for (const item of items)
|
|
704
|
+
if (typeNameOf(item) === 'org_docx4j_wml.R')
|
|
705
|
+
toDeletedText(item.value);
|
|
706
|
+
const plain = {
|
|
707
|
+
name: { namespaceURI: W_NS, localPart: kind },
|
|
708
|
+
value: { TYPE_NAME: kind === 'del' ? 'org_docx4j_wml.RunDel' : 'org_docx4j_wml.RunIns', id: value.id, author: value.author, date: value.date, customXmlOrSmartTagOrSdt: items },
|
|
709
|
+
};
|
|
710
|
+
const i = owner.indexOf(element);
|
|
711
|
+
if (i < 0)
|
|
712
|
+
return;
|
|
713
|
+
owner.splice(i, 1, plain);
|
|
714
|
+
linkParents(items, plain.value);
|
|
715
|
+
linkParents(plain, value.PARENT);
|
|
716
|
+
}
|
|
301
717
|
// --- collecting ----------------------------------------------------------------------------
|
|
302
|
-
/**
|
|
718
|
+
/**
|
|
719
|
+
* The tracked changes of one paragraph, in document order, touching pieces grouped as Office JS
|
|
720
|
+
* groups them (`groupTouching`). Office JS lists the changes that start in the paragraph (check 22):
|
|
721
|
+
* pieces that carry on a change from the paragraph before - its inserted or deleted mark - are left
|
|
722
|
+
* to that paragraph, and a change running on into the next is cut at this paragraph's end.
|
|
723
|
+
*/
|
|
303
724
|
export function trackedChangesOfParagraph(paragraph) {
|
|
304
|
-
|
|
725
|
+
const tokens = trackedChangeTokensOfParagraph(paragraph);
|
|
726
|
+
const before = markBefore(paragraph);
|
|
727
|
+
if (before) {
|
|
728
|
+
for (let i = 0; i < tokens.length;) {
|
|
729
|
+
const token = tokens[i];
|
|
730
|
+
if (token === BREAK)
|
|
731
|
+
break;
|
|
732
|
+
const kind = groupable(token);
|
|
733
|
+
if (kind === undefined) {
|
|
734
|
+
i++;
|
|
735
|
+
continue;
|
|
736
|
+
}
|
|
737
|
+
if (kind !== before.kind || token.author !== before.author)
|
|
738
|
+
break;
|
|
739
|
+
tokens.splice(i, 1);
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
return groupTouching(tokens);
|
|
743
|
+
}
|
|
744
|
+
/** The last revision on the mark of the paragraph before this one in its container, if any. */
|
|
745
|
+
function markBefore(paragraph) {
|
|
746
|
+
var _a;
|
|
747
|
+
const i = paragraph.container.indexOf(paragraph.element);
|
|
748
|
+
const previous = i > 0 ? paragraph.container[i - 1] : undefined;
|
|
749
|
+
if (!previous || typeNameOf(previous) !== 'org_docx4j_wml.P')
|
|
750
|
+
return undefined;
|
|
751
|
+
const rPr = (_a = previous.value.pPr) === null || _a === void 0 ? void 0 : _a.rPr;
|
|
752
|
+
if (rPr === null || rPr === void 0 ? void 0 : rPr.del)
|
|
753
|
+
return { kind: 'del', author: rPr.del.author };
|
|
754
|
+
if (rPr === null || rPr === void 0 ? void 0 : rPr.ins)
|
|
755
|
+
return { kind: 'ins', author: rPr.ins.author };
|
|
756
|
+
if (rPr === null || rPr === void 0 ? void 0 : rPr.rPrChange)
|
|
757
|
+
return { kind: 'format', author: rPr.rPrChange.author };
|
|
758
|
+
return undefined;
|
|
759
|
+
}
|
|
760
|
+
/** Between two tracked changes: something unrevised - text, a paragraph mark, a table - that keeps them apart. */
|
|
761
|
+
export const BREAK = 'break';
|
|
762
|
+
/**
|
|
763
|
+
* A paragraph's single changes in document order, its mark last, with a `BREAK` wherever unrevised
|
|
764
|
+
* content stands between two of them: `groupTouching` makes the groups from these.
|
|
765
|
+
*/
|
|
766
|
+
export function trackedChangeTokensOfParagraph(paragraph) {
|
|
767
|
+
var _a, _b, _c, _d;
|
|
305
768
|
const out = [];
|
|
306
769
|
const p = paragraph.p;
|
|
307
770
|
const pPrChange = (_a = p.pPr) === null || _a === void 0 ? void 0 : _a.pPrChange;
|
|
308
771
|
if (pPrChange)
|
|
309
772
|
out.push(new TrackedChange({ kind: 'paragraphProperties', value: pPrChange }, paragraph));
|
|
310
|
-
|
|
773
|
+
tokensOfRunLevel(paragraph, ((_b = p.content) !== null && _b !== void 0 ? _b : []), out);
|
|
774
|
+
const rPr = (_c = p.pPr) === null || _c === void 0 ? void 0 : _c.rPr;
|
|
775
|
+
// a mark whose formatting changed is a piece, grouping with its runs' (check 27: "Before.\r")
|
|
776
|
+
if (rPr === null || rPr === void 0 ? void 0 : rPr.rPrChange)
|
|
777
|
+
out.push(new TrackedChange({ kind: 'markProperties', value: rPr.rPrChange }, paragraph));
|
|
311
778
|
if (rPr === null || rPr === void 0 ? void 0 : rPr.ins)
|
|
312
779
|
out.push(new TrackedChange({ kind: 'mark', mark: 'ins', value: rPr.ins }, paragraph));
|
|
313
780
|
if (rPr === null || rPr === void 0 ? void 0 : rPr.del)
|
|
314
781
|
out.push(new TrackedChange({ kind: 'mark', mark: 'del', value: rPr.del }, paragraph));
|
|
315
|
-
|
|
782
|
+
if (!(rPr === null || rPr === void 0 ? void 0 : rPr.ins) && !(rPr === null || rPr === void 0 ? void 0 : rPr.del) && !(rPr === null || rPr === void 0 ? void 0 : rPr.rPrChange))
|
|
783
|
+
out.push(BREAK);
|
|
784
|
+
const sectPr = (_d = p.pPr) === null || _d === void 0 ? void 0 : _d.sectPr;
|
|
785
|
+
if (sectPr === null || sectPr === void 0 ? void 0 : sectPr.sectPrChange) {
|
|
786
|
+
out.push(new TrackedChange({ kind: 'sectionProperties', sectPr, value: sectPr.sectPrChange, text: `${sectionText(paragraph.container, paragraph.container.indexOf(paragraph.element) + 1)}\f` }, paragraph));
|
|
787
|
+
}
|
|
316
788
|
return out;
|
|
317
789
|
}
|
|
318
|
-
|
|
790
|
+
/**
|
|
791
|
+
* The text of the section that ends at `end` (exclusive) in a container: its paragraphs' text, a `\r`
|
|
792
|
+
* between them, back to the previous section break.
|
|
793
|
+
*/
|
|
794
|
+
function sectionText(items, end) {
|
|
319
795
|
var _a;
|
|
796
|
+
let from = end - 1;
|
|
797
|
+
while (from > 0 && !(typeNameOf(items[from - 1]) === 'org_docx4j_wml.P' && ((_a = items[from - 1].value.pPr) === null || _a === void 0 ? void 0 : _a.sectPr)))
|
|
798
|
+
from--;
|
|
799
|
+
return items.slice(Math.max(from, 0), end).filter((el) => typeNameOf(el) === 'org_docx4j_wml.P')
|
|
800
|
+
.map((el) => textOfView(el.value, { view: 'accepted' })).join('\r');
|
|
801
|
+
}
|
|
802
|
+
function tokensOfRunLevel(paragraph, items, out) {
|
|
803
|
+
var _a, _b;
|
|
320
804
|
for (const el of items) {
|
|
321
805
|
const kind = revisionKindOf(el);
|
|
322
806
|
if (kind !== undefined) {
|
|
807
|
+
// moved text is content Office JS cannot list (checks 16, 17), and is kept apart
|
|
808
|
+
const move = kind === 'moveFrom' || kind === 'moveTo';
|
|
809
|
+
if (move)
|
|
810
|
+
out.push(BREAK);
|
|
323
811
|
out.push(new TrackedChange({ kind: 'run', revision: kind, element: el, owner: items, value: el.value }, paragraph));
|
|
812
|
+
if (move)
|
|
813
|
+
out.push(BREAK);
|
|
324
814
|
const nested = runItemsOf(el.value);
|
|
325
|
-
if (nested)
|
|
326
|
-
|
|
815
|
+
if (nested) {
|
|
816
|
+
// a revision inside a revision (a deletion of inserted text) was not measured: listed apart
|
|
817
|
+
const inner = [];
|
|
818
|
+
tokensOfRunLevel(paragraph, nested, inner);
|
|
819
|
+
if (inner.some((token) => token !== BREAK && token.target.kind === 'run'))
|
|
820
|
+
out.push(BREAK, ...inner, BREAK);
|
|
821
|
+
else
|
|
822
|
+
out.push(...inner.filter((token) => token !== BREAK));
|
|
823
|
+
}
|
|
327
824
|
continue;
|
|
328
825
|
}
|
|
329
826
|
if (typeNameOf(el) === 'org_docx4j_wml.R') {
|
|
827
|
+
// a run whose formatting changed is a piece of its own; one that did not keeps pieces apart
|
|
330
828
|
const run = el.value;
|
|
331
829
|
if ((_a = run.rPr) === null || _a === void 0 ? void 0 : _a.rPrChange)
|
|
332
830
|
out.push(new TrackedChange({ kind: 'runProperties', run, value: run.rPr.rPrChange }, paragraph));
|
|
831
|
+
else if (((_b = run.content) !== null && _b !== void 0 ? _b : []).length > 0)
|
|
832
|
+
out.push(BREAK);
|
|
333
833
|
continue;
|
|
334
834
|
}
|
|
335
835
|
const nested = runItemsOf(el.value);
|
|
336
836
|
if (nested)
|
|
337
|
-
|
|
837
|
+
tokensOfRunLevel(paragraph, nested, out);
|
|
338
838
|
}
|
|
339
839
|
}
|
|
840
|
+
/**
|
|
841
|
+
* What a piece groups as: a `w:ins` or `w:del` around runs, or an inserted or deleted mark; a run's
|
|
842
|
+
* `w:rPrChange` (check 28: bold on an italic run and a plain one, one change). Moves and a
|
|
843
|
+
* paragraph's `w:pPrChange` do not group.
|
|
844
|
+
*/
|
|
845
|
+
function groupable(change) {
|
|
846
|
+
const t = change.target;
|
|
847
|
+
if (t.kind === 'mark')
|
|
848
|
+
return t.mark;
|
|
849
|
+
if (t.kind === 'run' && (t.revision === 'ins' || t.revision === 'del'))
|
|
850
|
+
return t.revision;
|
|
851
|
+
if (t.kind === 'runProperties' || t.kind === 'markProperties')
|
|
852
|
+
return 'format';
|
|
853
|
+
return undefined;
|
|
854
|
+
}
|
|
855
|
+
/**
|
|
856
|
+
* Tokens made into the changes Office JS lists: consecutive pieces of one kind (inserted, deleted, or
|
|
857
|
+
* a run's formatting) by one author, nothing unrevised between them, are one change (checks 22, 23
|
|
858
|
+
* and 28: text and marks alike, dates regardless, formatting whatever it recorded; another author, or
|
|
859
|
+
* one kind against another, starts a new one). A change that does not group - a paragraph's
|
|
860
|
+
* properties, a move - is listed where it stands and does not break a group. A group of one piece is
|
|
861
|
+
* that piece.
|
|
862
|
+
*/
|
|
863
|
+
export function groupTouching(tokens) {
|
|
864
|
+
const out = [];
|
|
865
|
+
let open;
|
|
866
|
+
for (const token of tokens) {
|
|
867
|
+
if (token === BREAK) {
|
|
868
|
+
open = undefined;
|
|
869
|
+
continue;
|
|
870
|
+
}
|
|
871
|
+
const kind = groupable(token);
|
|
872
|
+
if (kind === undefined) {
|
|
873
|
+
out.push(token);
|
|
874
|
+
continue;
|
|
875
|
+
}
|
|
876
|
+
const last = open === null || open === void 0 ? void 0 : open[open.length - 1];
|
|
877
|
+
if (open && last && groupable(last) === kind && last.author === token.author) {
|
|
878
|
+
open.push(token);
|
|
879
|
+
continue;
|
|
880
|
+
}
|
|
881
|
+
open = [token];
|
|
882
|
+
out.push(open);
|
|
883
|
+
}
|
|
884
|
+
return out.map((item) => {
|
|
885
|
+
if (!Array.isArray(item))
|
|
886
|
+
return item;
|
|
887
|
+
const first = item[0];
|
|
888
|
+
if (item.length === 1)
|
|
889
|
+
return first;
|
|
890
|
+
return new TrackedChange({ kind: 'group', revision: groupable(first), pieces: item, value: first.target.value }, first.paragraph);
|
|
891
|
+
});
|
|
892
|
+
}
|
|
340
893
|
/**
|
|
341
894
|
* The `w:trPr/w:ins` and `w:trPr/w:del` revisions of a table row. `inner` is the revision markup
|
|
342
895
|
* of the row's own cells, which the row's change owns rather than reporting separately.
|
|
@@ -350,3 +903,143 @@ export function trackedChangesOfRow(tr, owner, inner = []) {
|
|
|
350
903
|
out.push(new TrackedChange({ kind: 'row', row: 'del', tr, owner, value: trPr.del, inner }, undefined));
|
|
351
904
|
return out;
|
|
352
905
|
}
|
|
906
|
+
// --- CR-002 section 35: tables, cells, marks and sections --------------------------------------------
|
|
907
|
+
/** The change a table's property records make, one per table, or undefined when they make none. */
|
|
908
|
+
export function tablePropertiesChangeOf(tbl) {
|
|
909
|
+
const records = tableRecordsOf(tbl.value);
|
|
910
|
+
return records ? new TrackedChange({ kind: 'tableProperties', tbl, value: records.value }, undefined) : undefined;
|
|
911
|
+
}
|
|
912
|
+
/** A cell's own revision, `w:cellIns` or `w:cellDel`, or undefined. */
|
|
913
|
+
export function cellChangeOf(tc, owner, tr) {
|
|
914
|
+
const tcPr = tc.value.tcPr;
|
|
915
|
+
if (tcPr === null || tcPr === void 0 ? void 0 : tcPr.cellIns)
|
|
916
|
+
return new TrackedChange({ kind: 'cell', cell: 'ins', tc, owner, tr, value: tcPr.cellIns }, undefined);
|
|
917
|
+
if (tcPr === null || tcPr === void 0 ? void 0 : tcPr.cellDel)
|
|
918
|
+
return new TrackedChange({ kind: 'cell', cell: 'del', tc, owner, tr, value: tcPr.cellDel }, undefined);
|
|
919
|
+
return undefined;
|
|
920
|
+
}
|
|
921
|
+
/** The change to the body's last section (`w:body/w:sectPr/w:sectPrChange`), its text the section's paragraphs'. */
|
|
922
|
+
export function bodySectionChangeOf(container) {
|
|
923
|
+
var _a;
|
|
924
|
+
const sectPr = container.sectPr;
|
|
925
|
+
if (!(sectPr === null || sectPr === void 0 ? void 0 : sectPr.sectPrChange))
|
|
926
|
+
return undefined;
|
|
927
|
+
const items = (_a = container.content) !== null && _a !== void 0 ? _a : [];
|
|
928
|
+
return new TrackedChange({ kind: 'sectionProperties', sectPr, value: sectPr.sectPrChange, text: sectionText(items, items.length) }, undefined);
|
|
929
|
+
}
|
|
930
|
+
/**
|
|
931
|
+
* A cell taken out of its row, the cell before it (else after it) widened over its grid columns, as
|
|
932
|
+
* Word did rejecting an inserted cell and accepting a deleted one (check 27: `w:gridSpan` 2 and the
|
|
933
|
+
* two widths added).
|
|
934
|
+
*/
|
|
935
|
+
function removeCell(tc, owner) {
|
|
936
|
+
var _a, _b, _c, _d, _e, _f, _g;
|
|
937
|
+
var _h;
|
|
938
|
+
const i = owner.indexOf(tc);
|
|
939
|
+
if (i < 0)
|
|
940
|
+
return;
|
|
941
|
+
const cells = owner.filter((el) => typeNameOf(el) === 'org_docx4j_wml.Tc');
|
|
942
|
+
const at = cells.indexOf(tc);
|
|
943
|
+
const neighbour = (_a = cells[at - 1]) !== null && _a !== void 0 ? _a : cells[at + 1];
|
|
944
|
+
owner.splice(i, 1);
|
|
945
|
+
if (!neighbour)
|
|
946
|
+
return;
|
|
947
|
+
const removed = tc.value.tcPr;
|
|
948
|
+
const kept = ((_b = (_h = neighbour.value).tcPr) !== null && _b !== void 0 ? _b : (_h.tcPr = { TYPE_NAME: 'org_docx4j_wml.TcPr' }));
|
|
949
|
+
const span = (tcPr) => { var _a, _b; return Number((_b = (_a = tcPr === null || tcPr === void 0 ? void 0 : tcPr.gridSpan) === null || _a === void 0 ? void 0 : _a.val) !== null && _b !== void 0 ? _b : 1) || 1; };
|
|
950
|
+
kept.gridSpan = { ...((_c = kept.gridSpan) !== null && _c !== void 0 ? _c : {}), val: span(kept) + span(removed) };
|
|
951
|
+
if (((_d = kept.tcW) === null || _d === void 0 ? void 0 : _d.type) === 'dxa' && ((_e = removed === null || removed === void 0 ? void 0 : removed.tcW) === null || _e === void 0 ? void 0 : _e.type) === 'dxa') {
|
|
952
|
+
kept.tcW = { ...kept.tcW, w: Number((_f = kept.tcW.w) !== null && _f !== void 0 ? _f : 0) + Number((_g = removed.tcW.w) !== null && _g !== void 0 ? _g : 0) };
|
|
953
|
+
}
|
|
954
|
+
linkParents(kept, neighbour.value);
|
|
955
|
+
}
|
|
956
|
+
/** A mark's formatting put back as its `w:rPrChange` recorded it, its own revisions (inserted, deleted, moved) kept. */
|
|
957
|
+
function restoreMarkProperties(p, change) {
|
|
958
|
+
var _a, _b, _c;
|
|
959
|
+
const rPr = (_a = p.pPr) === null || _a === void 0 ? void 0 : _a.rPr;
|
|
960
|
+
if (!rPr)
|
|
961
|
+
return;
|
|
962
|
+
const keep = ['ins', 'del', 'moveFrom', 'moveTo'];
|
|
963
|
+
const target = rPr;
|
|
964
|
+
for (const key of Object.keys(target))
|
|
965
|
+
if (key !== 'TYPE_NAME' && key !== 'PARENT' && !keep.includes(key))
|
|
966
|
+
delete target[key];
|
|
967
|
+
const original = rPrFromElements((_c = (_b = change.rPr) === null || _b === void 0 ? void 0 : _b.egrPrBase) !== null && _c !== void 0 ? _c : []);
|
|
968
|
+
for (const [key, value] of Object.entries(original))
|
|
969
|
+
if (key !== 'TYPE_NAME' && key !== 'PARENT')
|
|
970
|
+
target[key] = value;
|
|
971
|
+
if (p.pPr)
|
|
972
|
+
linkParents(rPr, p.pPr);
|
|
973
|
+
pruneParagraphProperties(p);
|
|
974
|
+
}
|
|
975
|
+
/**
|
|
976
|
+
* A section's properties put back as its `w:sectPrChange` recorded them. Word records only what
|
|
977
|
+
* changed, and rejecting laid those over the section, the rest left as it stood (check 18's saved
|
|
978
|
+
* file: the margins came back, the page size and columns stayed); a recorded `w:docGrid` of pitch 0
|
|
979
|
+
* stands for none, and Word wrote none (check 18, and check 27's section break).
|
|
980
|
+
*/
|
|
981
|
+
function restoreSection(sectPr, change) {
|
|
982
|
+
var _a, _b;
|
|
983
|
+
const target = sectPr;
|
|
984
|
+
for (const [key, value] of Object.entries((_a = change.sectPr) !== null && _a !== void 0 ? _a : {})) {
|
|
985
|
+
if (key !== 'TYPE_NAME' && key !== 'PARENT')
|
|
986
|
+
target[key] = deepCopy(value);
|
|
987
|
+
}
|
|
988
|
+
const grid = sectPr.docGrid;
|
|
989
|
+
if (grid && Number((_b = grid.linePitch) !== null && _b !== void 0 ? _b : 0) === 0 && grid.type === undefined && grid.charSpace === undefined)
|
|
990
|
+
delete sectPr.docGrid;
|
|
991
|
+
delete sectPr.sectPrChange;
|
|
992
|
+
linkParents(sectPr, sectPr.PARENT);
|
|
993
|
+
}
|
|
994
|
+
/**
|
|
995
|
+
* What `acceptAll` and `rejectAll` resolve that is never listed: a `w:cellMerge`, which Office JS
|
|
996
|
+
* cannot list (check 27: `getTrackedChanges` throws); a `w:numberingChange`, which Word no longer
|
|
997
|
+
* writes and drops when one is put in, and whose record is the old number's text, so that a reject
|
|
998
|
+
* too can only drop it; and a table's records that change nothing. A merge rejected gets the merge it
|
|
999
|
+
* recorded back (docx4j's reading; unmeasured).
|
|
1000
|
+
*/
|
|
1001
|
+
export function resolveUnlisted(items, what) {
|
|
1002
|
+
const visit = (list) => {
|
|
1003
|
+
var _a;
|
|
1004
|
+
for (const el of list !== null && list !== void 0 ? list : []) {
|
|
1005
|
+
const tn = typeNameOf(el);
|
|
1006
|
+
if (tn === 'org_docx4j_wml.P') {
|
|
1007
|
+
const numPr = (_a = el.value.pPr) === null || _a === void 0 ? void 0 : _a.numPr;
|
|
1008
|
+
if (numPr === null || numPr === void 0 ? void 0 : numPr.numberingChange)
|
|
1009
|
+
delete numPr.numberingChange;
|
|
1010
|
+
continue;
|
|
1011
|
+
}
|
|
1012
|
+
if (tn === 'org_docx4j_wml.Tbl') {
|
|
1013
|
+
const tbl = el.value;
|
|
1014
|
+
if (hasTableRecords(tbl) && !tableRecordsOf(tbl))
|
|
1015
|
+
acceptTable(tbl);
|
|
1016
|
+
for (const row of rowsOf(tbl)) {
|
|
1017
|
+
for (const cell of cellsOf(row.element.value)) {
|
|
1018
|
+
const tcPr = cell.element.value.tcPr;
|
|
1019
|
+
if (tcPr === null || tcPr === void 0 ? void 0 : tcPr.cellMerge)
|
|
1020
|
+
resolveCellMerge(tcPr, what);
|
|
1021
|
+
visit(childrenOf(cell.element.value));
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
continue;
|
|
1025
|
+
}
|
|
1026
|
+
const v = el.value;
|
|
1027
|
+
if (typeof v === 'object' && v !== null)
|
|
1028
|
+
visit(childrenOf(v));
|
|
1029
|
+
}
|
|
1030
|
+
};
|
|
1031
|
+
visit(items);
|
|
1032
|
+
}
|
|
1033
|
+
function resolveCellMerge(tcPr, what) {
|
|
1034
|
+
const change = tcPr.cellMerge;
|
|
1035
|
+
delete tcPr.cellMerge;
|
|
1036
|
+
if (what === 'accept')
|
|
1037
|
+
return;
|
|
1038
|
+
const orig = change.vMergeOrig;
|
|
1039
|
+
if (orig === 'rest')
|
|
1040
|
+
tcPr.vMerge = { val: 'restart' };
|
|
1041
|
+
else if (orig === 'cont')
|
|
1042
|
+
tcPr.vMerge = {};
|
|
1043
|
+
else
|
|
1044
|
+
delete tcPr.vMerge;
|
|
1045
|
+
}
|