@docx4j/core-ts 0.2.2 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +6 -4
- package/dist/model/content/Range.mjs +68 -10
- package/dist/model/content/TrackedChange.d.mts +94 -3
- package/dist/model/content/TrackedChange.mjs +585 -25
- 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/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/parts/wml/comments.mjs +47 -0
- package/package.json +1 -1
|
@@ -19,10 +19,26 @@ import { controlIdScope, sdtKindFor } from '../customxml/insert.mjs';
|
|
|
19
19
|
import { InlinePicture, addImage, writableWidthEmu } from './InlinePicture.mjs';
|
|
20
20
|
import { contentOf } from './ooxml.mjs';
|
|
21
21
|
import { commentApi } from './comments.mjs';
|
|
22
|
-
import { copyRPr, markDeleted, toDeletedText } from './tracking.mjs';
|
|
22
|
+
import { ChangeTracker, copyRPr, markDeleted, toDeletedText, highestAnnotationId, canonical, restorePPr } from './tracking.mjs';
|
|
23
23
|
import { trackedChangesOfParagraph } from './TrackedChange.mjs';
|
|
24
24
|
import { isListItem, listOf, listOrNullObjectOf, listItemOf, listItemOrNullObjectOf, startNewList, attachToList, detachFromList, } from './List.mjs';
|
|
25
25
|
const TWIPS_PER_POINT = 20;
|
|
26
|
+
/** Office JS's paragraph formatting read from a `w:pPr`, resolved (`effective`) or direct. */
|
|
27
|
+
function formattingOf(pPr, effective) {
|
|
28
|
+
var _a, _b, _c, _d, _e, _f, _g, _h;
|
|
29
|
+
return {
|
|
30
|
+
alignment: alignmentOf(pPr, effective),
|
|
31
|
+
leftIndent: ((_b = (_a = pPr === null || pPr === void 0 ? void 0 : pPr.ind) === null || _a === void 0 ? void 0 : _a.left) !== null && _b !== void 0 ? _b : 0) / TWIPS_PER_POINT,
|
|
32
|
+
rightIndent: ((_d = (_c = pPr === null || pPr === void 0 ? void 0 : pPr.ind) === null || _c === void 0 ? void 0 : _c.right) !== null && _d !== void 0 ? _d : 0) / TWIPS_PER_POINT,
|
|
33
|
+
firstLineIndent: firstLineIndentOf(pPr),
|
|
34
|
+
spaceBefore: ((_f = (_e = pPr === null || pPr === void 0 ? void 0 : pPr.spacing) === null || _e === void 0 ? void 0 : _e.before) !== null && _f !== void 0 ? _f : 0) / TWIPS_PER_POINT,
|
|
35
|
+
spaceAfter: ((_h = (_g = pPr === null || pPr === void 0 ? void 0 : pPr.spacing) === null || _g === void 0 ? void 0 : _g.after) !== null && _h !== void 0 ? _h : 0) / TWIPS_PER_POINT,
|
|
36
|
+
lineSpacing: lineSpacingOf(pPr),
|
|
37
|
+
outlineLevel: outlineLevelOf(pPr),
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
/** The `w:pPr` members `formattingOf` reads; everything else is compared as it is written. */
|
|
41
|
+
const FORMATTING_MEMBERS = new Set(['jc', 'ind', 'spacing', 'outlineLvl']);
|
|
26
42
|
/**
|
|
27
43
|
* A subset of Office JS `Word.Paragraph` over a `w:p` in a body, cell, header, footer or
|
|
28
44
|
* content control. A light view: it holds the element and the array that contains it; nothing
|
|
@@ -78,11 +94,11 @@ export class Paragraph {
|
|
|
78
94
|
set styleId(id) {
|
|
79
95
|
// through pPr(), so that a tracked write records w:pPrChange first (CR-002 phase F)
|
|
80
96
|
const pPr = this.pPr();
|
|
81
|
-
if (id === '' || id === 'Normal')
|
|
97
|
+
if (id === '' || id === 'Normal')
|
|
82
98
|
delete pPr.pStyle;
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
99
|
+
else
|
|
100
|
+
pPr.pStyle = { val: id };
|
|
101
|
+
this.settleFormatting();
|
|
86
102
|
}
|
|
87
103
|
/**
|
|
88
104
|
* The style's display name ('Heading 1', 'My Style'), as Office JS: from the styles part's
|
|
@@ -125,19 +141,7 @@ export class Paragraph {
|
|
|
125
141
|
* Phase B step 2 (an absent property reads 0, 10 or `'Unknown'`).
|
|
126
142
|
*/
|
|
127
143
|
formatting(options) {
|
|
128
|
-
|
|
129
|
-
const pPr = this.readPPr(options);
|
|
130
|
-
const effective = (options === null || options === void 0 ? void 0 : options.direct) !== true;
|
|
131
|
-
return {
|
|
132
|
-
alignment: alignmentOf(pPr, effective),
|
|
133
|
-
leftIndent: ((_b = (_a = pPr === null || pPr === void 0 ? void 0 : pPr.ind) === null || _a === void 0 ? void 0 : _a.left) !== null && _b !== void 0 ? _b : 0) / TWIPS_PER_POINT,
|
|
134
|
-
rightIndent: ((_d = (_c = pPr === null || pPr === void 0 ? void 0 : pPr.ind) === null || _c === void 0 ? void 0 : _c.right) !== null && _d !== void 0 ? _d : 0) / TWIPS_PER_POINT,
|
|
135
|
-
firstLineIndent: firstLineIndentOf(pPr),
|
|
136
|
-
spaceBefore: ((_f = (_e = pPr === null || pPr === void 0 ? void 0 : pPr.spacing) === null || _e === void 0 ? void 0 : _e.before) !== null && _f !== void 0 ? _f : 0) / TWIPS_PER_POINT,
|
|
137
|
-
spaceAfter: ((_h = (_g = pPr === null || pPr === void 0 ? void 0 : pPr.spacing) === null || _g === void 0 ? void 0 : _g.after) !== null && _h !== void 0 ? _h : 0) / TWIPS_PER_POINT,
|
|
138
|
-
lineSpacing: lineSpacingOf(pPr),
|
|
139
|
-
outlineLevel: outlineLevelOf(pPr),
|
|
140
|
-
};
|
|
144
|
+
return formattingOf(this.readPPr(options), (options === null || options === void 0 ? void 0 : options.direct) !== true);
|
|
141
145
|
}
|
|
142
146
|
/** The effective alignment; an absent `w:jc` after resolution is `'Left'`, as Office JS. */
|
|
143
147
|
get alignment() {
|
|
@@ -146,17 +150,17 @@ export class Paragraph {
|
|
|
146
150
|
set alignment(v) {
|
|
147
151
|
const pPr = this.pPr();
|
|
148
152
|
const val = v === 'Left' ? 'left' : v === 'Centered' ? 'center' : v === 'Right' ? 'right' : v === 'Justified' ? 'both' : undefined;
|
|
149
|
-
if (val === undefined)
|
|
153
|
+
if (val === undefined)
|
|
150
154
|
delete pPr.jc;
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
155
|
+
else
|
|
156
|
+
pPr.jc = { val };
|
|
157
|
+
this.settleFormatting();
|
|
154
158
|
}
|
|
155
159
|
/** Indents and spacing in points, as Office JS; the effective values (CR-001 Phase B step 2). */
|
|
156
160
|
get leftIndent() { var _a, _b; return ((_b = (_a = this.effectivePPr.ind) === null || _a === void 0 ? void 0 : _a.left) !== null && _b !== void 0 ? _b : 0) / TWIPS_PER_POINT; }
|
|
157
|
-
set leftIndent(pt) { this.ind().left = Math.round(pt * TWIPS_PER_POINT); }
|
|
161
|
+
set leftIndent(pt) { this.ind().left = Math.round(pt * TWIPS_PER_POINT); this.settleFormatting(); }
|
|
158
162
|
get rightIndent() { var _a, _b; return ((_b = (_a = this.effectivePPr.ind) === null || _a === void 0 ? void 0 : _a.right) !== null && _b !== void 0 ? _b : 0) / TWIPS_PER_POINT; }
|
|
159
|
-
set rightIndent(pt) { this.ind().right = Math.round(pt * TWIPS_PER_POINT); }
|
|
163
|
+
set rightIndent(pt) { this.ind().right = Math.round(pt * TWIPS_PER_POINT); this.settleFormatting(); }
|
|
160
164
|
/** First-line indent; negative for a hanging indent, as Office JS. */
|
|
161
165
|
get firstLineIndent() {
|
|
162
166
|
return firstLineIndentOf(this.effectivePPr);
|
|
@@ -171,22 +175,23 @@ export class Paragraph {
|
|
|
171
175
|
ind.firstLine = Math.round(pt * TWIPS_PER_POINT);
|
|
172
176
|
delete ind.hanging;
|
|
173
177
|
}
|
|
178
|
+
this.settleFormatting();
|
|
174
179
|
}
|
|
175
180
|
get spaceBefore() { var _a, _b; return ((_b = (_a = this.effectivePPr.spacing) === null || _a === void 0 ? void 0 : _a.before) !== null && _b !== void 0 ? _b : 0) / TWIPS_PER_POINT; }
|
|
176
|
-
set spaceBefore(pt) { this.spacing().before = Math.round(pt * TWIPS_PER_POINT); }
|
|
181
|
+
set spaceBefore(pt) { this.spacing().before = Math.round(pt * TWIPS_PER_POINT); this.settleFormatting(); }
|
|
177
182
|
get spaceAfter() { var _a, _b; return ((_b = (_a = this.effectivePPr.spacing) === null || _a === void 0 ? void 0 : _a.after) !== null && _b !== void 0 ? _b : 0) / TWIPS_PER_POINT; }
|
|
178
|
-
set spaceAfter(pt) { this.spacing().after = Math.round(pt * TWIPS_PER_POINT); }
|
|
183
|
+
set spaceAfter(pt) { this.spacing().after = Math.round(pt * TWIPS_PER_POINT); this.settleFormatting(); }
|
|
179
184
|
/** Line spacing in points: w:line/240 lines of 12pt when the rule is auto, else w:line twips; 0 when not set. */
|
|
180
185
|
get lineSpacing() {
|
|
181
186
|
return lineSpacingOf(this.effectivePPr);
|
|
182
187
|
}
|
|
183
|
-
set lineSpacing(pt) { const s = this.spacing(); s.line = Math.round(pt * TWIPS_PER_POINT); s.lineRule = 'exact'; }
|
|
188
|
+
set lineSpacing(pt) { const s = this.spacing(); s.line = Math.round(pt * TWIPS_PER_POINT); s.lineRule = 'exact'; this.settleFormatting(); }
|
|
184
189
|
/** w:outlineLvl + 1 (1 to 9); 10 for body text, as Office JS. A heading style supplies it. */
|
|
185
190
|
get outlineLevel() { return outlineLevelOf(this.effectivePPr); }
|
|
186
191
|
set outlineLevel(level) { const pPr = this.pPr(); if (level >= 10 || level < 1)
|
|
187
192
|
delete pPr.outlineLvl;
|
|
188
193
|
else
|
|
189
|
-
pPr.outlineLvl = { val: level - 1 }; }
|
|
194
|
+
pPr.outlineLvl = { val: level - 1 }; this.settleFormatting(); }
|
|
190
195
|
/**
|
|
191
196
|
* The runs' formatting: reads the first run's *effective* properties, writes direct
|
|
192
197
|
* formatting to all (extension: `Font` is over runs, not the paragraph mark).
|
|
@@ -446,6 +451,40 @@ export class Paragraph {
|
|
|
446
451
|
(_b = this.changeTracker) === null || _b === void 0 ? void 0 : _b.recordPPrChange(pPr);
|
|
447
452
|
return pPr;
|
|
448
453
|
}
|
|
454
|
+
/**
|
|
455
|
+
* After a tracked write of the paragraph's properties: when the paragraph is as it was, its
|
|
456
|
+
* recorded properties go back as they were and the `w:pPrChange` goes, since Word keeps no
|
|
457
|
+
* formatting change that changes nothing, whoever changed it back (`test/README.md` check 26:
|
|
458
|
+
* centred then left again, by the same author and by another; Increase Indent then Decrease
|
|
459
|
+
* Indent; each left no `w:pPr`; CR-002 section 29). "As it was" is Office JS's paragraph
|
|
460
|
+
* formatting (alignment, indents, spacing, outline level) read resolved from both, the rest of the
|
|
461
|
+
* properties - the style, the list - written the same: Word's Ctrl+L and Decrease Indent removed
|
|
462
|
+
* the direct values where this package's setters write `w:jc="left"` and `w:ind w:left="0"`.
|
|
463
|
+
* Without the package's `PropertyResolver` only properties written the same count.
|
|
464
|
+
*
|
|
465
|
+
* @internal public so that the list views (CR-002 phase H) settle their `w:numPr` writes.
|
|
466
|
+
*/
|
|
467
|
+
settleFormatting() {
|
|
468
|
+
var _a, _b;
|
|
469
|
+
const pPr = this.p.pPr;
|
|
470
|
+
const recorded = (_a = pPr === null || pPr === void 0 ? void 0 : pPr.pPrChange) === null || _a === void 0 ? void 0 : _a.pPr;
|
|
471
|
+
if (!pPr || !pPr.pPrChange || !this.changeTracker)
|
|
472
|
+
return;
|
|
473
|
+
const current = {};
|
|
474
|
+
for (const [key, value] of Object.entries(pPr))
|
|
475
|
+
if (key !== 'pPrChange' && key !== 'rPr' && key !== 'sectPr')
|
|
476
|
+
current[key] = value;
|
|
477
|
+
const before = { ...(recorded !== null && recorded !== void 0 ? recorded : {}) };
|
|
478
|
+
let same = canonical(current) === canonical(before);
|
|
479
|
+
if (!same) {
|
|
480
|
+
const rest = (props) => canonical(Object.fromEntries(Object.entries(props).filter(([key]) => !FORMATTING_MEMBERS.has(key))));
|
|
481
|
+
const resolver = (_b = this.parentBody.package_) === null || _b === void 0 ? void 0 : _b.propertyResolverOrUndefined;
|
|
482
|
+
same = resolver !== undefined && rest(current) === rest(before)
|
|
483
|
+
&& canonical(formattingOf(resolver.getEffectivePPr(current), true)) === canonical(formattingOf(resolver.getEffectivePPr(before), true));
|
|
484
|
+
}
|
|
485
|
+
if (same)
|
|
486
|
+
restorePPr(this.p, recorded);
|
|
487
|
+
}
|
|
449
488
|
/**
|
|
450
489
|
* The paragraph's `w:numPr`, created when absent, through {@link pPr} so that a tracked
|
|
451
490
|
* write records `w:pPrChange` first.
|
|
@@ -495,8 +534,8 @@ export class Paragraph {
|
|
|
495
534
|
* else into a new run with the formatting of the nearest run.
|
|
496
535
|
*/
|
|
497
536
|
splice(start, end, text) {
|
|
498
|
-
var _a, _b, _c, _d;
|
|
499
|
-
var
|
|
537
|
+
var _a, _b, _c, _d, _e, _f;
|
|
538
|
+
var _g;
|
|
500
539
|
const length = this.text.length;
|
|
501
540
|
start = Math.max(0, Math.min(start, length));
|
|
502
541
|
end = Math.max(start, Math.min(end, length));
|
|
@@ -510,6 +549,13 @@ export class Paragraph {
|
|
|
510
549
|
}
|
|
511
550
|
let segs = this.segments();
|
|
512
551
|
let inserted = false;
|
|
552
|
+
// A replacement that begins inside a revision is a deletion and then the new text typed there, so
|
|
553
|
+
// the new text is plain and splits the revision, as typed text is (test/README.md check 21), rather
|
|
554
|
+
// than joining the revision it replaced part of.
|
|
555
|
+
if (end > start && text.length > 0 && ((_a = segs.find((s) => s.start <= start && start < s.end)) === null || _a === void 0 ? void 0 : _a.revision)) {
|
|
556
|
+
this.splice(start, end, '');
|
|
557
|
+
return this.splice(start, start, text);
|
|
558
|
+
}
|
|
513
559
|
if (end > start) {
|
|
514
560
|
// Replace: the new text takes the place (and formatting) of the first replaced character, as Word does.
|
|
515
561
|
const first = segs.find((s) => s.start <= start && start < s.end);
|
|
@@ -546,8 +592,23 @@ export class Paragraph {
|
|
|
546
592
|
segs = this.segments();
|
|
547
593
|
}
|
|
548
594
|
if (text.length > 0) {
|
|
549
|
-
//
|
|
550
|
-
|
|
595
|
+
// Text typed with tracking off is plain wherever it lands, as Word writes it (test/README.md
|
|
596
|
+
// check 21): inside a revision it splits the revision, and next to one it goes beside it, not
|
|
597
|
+
// in it. Otherwise it extends the w:t at `start` (the one containing it, else the one ending
|
|
598
|
+
// there, else the one starting there).
|
|
599
|
+
const containing = segs.find((s) => s.editable && s.start < start && start < s.end);
|
|
600
|
+
if ((containing === null || containing === void 0 ? void 0 : containing.revision) && this.insertBetweenHalves(start, runOf([textItem(text)], copyRPr(containing.run.rPr)), () => this.nextAnnotationId())) {
|
|
601
|
+
return new Range(this, start, start + text.length);
|
|
602
|
+
}
|
|
603
|
+
const ending = segs.find((s) => s.editable && s.end === start);
|
|
604
|
+
const starting = segs.find((s) => s.editable && s.start === start);
|
|
605
|
+
const target = (_d = (_c = (_b = (containing && !containing.revision ? containing : undefined)) !== null && _b !== void 0 ? _b : (ending && !ending.revision ? ending : undefined)) !== null && _c !== void 0 ? _c : (starting && !starting.revision ? starting : undefined)) !== null && _d !== void 0 ? _d : containing; // a revision that could not be split: extended, as before
|
|
606
|
+
const edge = target ? undefined : ending !== null && ending !== void 0 ? ending : starting;
|
|
607
|
+
if (edge === null || edge === void 0 ? void 0 : edge.revision) {
|
|
608
|
+
const run = runOf([textItem(text)], copyRPr(edge.run.rPr));
|
|
609
|
+
this.insertBesideRevision(edge.revision, run, edge === ending ? 'after' : 'before');
|
|
610
|
+
return new Range(this, start, start + text.length);
|
|
611
|
+
}
|
|
551
612
|
if (target) {
|
|
552
613
|
const t = target.item.value;
|
|
553
614
|
const at = start - target.start;
|
|
@@ -558,7 +619,7 @@ export class Paragraph {
|
|
|
558
619
|
const before = [...segs].reverse().find((s) => s.end <= start);
|
|
559
620
|
const after = segs.find((s) => s.start >= start);
|
|
560
621
|
const neighbour = before !== null && before !== void 0 ? before : after;
|
|
561
|
-
const firstRun = (
|
|
622
|
+
const firstRun = (_e = runsOf(this.p)[0]) === null || _e === void 0 ? void 0 : _e.value.rPr;
|
|
562
623
|
const rPr = (neighbour === null || neighbour === void 0 ? void 0 : neighbour.run.rPr) ? deepCopy(neighbour.run.rPr) : firstRun ? deepCopy(firstRun) : undefined;
|
|
563
624
|
const run = runOf([textItem(text)], rPr);
|
|
564
625
|
if (before)
|
|
@@ -566,7 +627,7 @@ export class Paragraph {
|
|
|
566
627
|
else if (after)
|
|
567
628
|
after.runOwner.splice(after.runIndex, 0, run);
|
|
568
629
|
else {
|
|
569
|
-
const content = ((
|
|
630
|
+
const content = ((_f = (_g = this.p).content) !== null && _f !== void 0 ? _f : (_g.content = []));
|
|
570
631
|
if (start === 0)
|
|
571
632
|
content.unshift(run);
|
|
572
633
|
else
|
|
@@ -776,6 +837,71 @@ export class Paragraph {
|
|
|
776
837
|
}
|
|
777
838
|
return made;
|
|
778
839
|
}
|
|
840
|
+
/**
|
|
841
|
+
* Splits the revision that holds the text at `at` into two - the second half a copy of its
|
|
842
|
+
* attributes under a new `w:id` - and puts `item` between them. Word splits a revision so when
|
|
843
|
+
* text is typed inside it: around the typist's `w:ins` with tracking on, around plain text with it
|
|
844
|
+
* off (`test/README.md` checks 20 and 21). False, with nothing changed but a run split at `at`,
|
|
845
|
+
* where the text there is not directly in the revision (in a hyperlink inside it, say): the caller
|
|
846
|
+
* then does what it did before.
|
|
847
|
+
*/
|
|
848
|
+
insertBetweenHalves(at, item, nextId) {
|
|
849
|
+
var _a;
|
|
850
|
+
this.splitAt(at);
|
|
851
|
+
const after = this.segments().find((s) => s.start >= at && s.revision);
|
|
852
|
+
const holder = after === null || after === void 0 ? void 0 : after.revision;
|
|
853
|
+
const index = holder && after ? holder.items.indexOf(after.runOwner[after.runIndex]) : -1;
|
|
854
|
+
if (!holder || index <= 0)
|
|
855
|
+
return false;
|
|
856
|
+
const value = holder.value;
|
|
857
|
+
const itemsKey = Object.keys(value).find((key) => value[key] === holder.items);
|
|
858
|
+
if (!itemsKey)
|
|
859
|
+
return false;
|
|
860
|
+
const copy = {};
|
|
861
|
+
for (const [key, v] of Object.entries(value))
|
|
862
|
+
if (key !== itemsKey)
|
|
863
|
+
copy[key] = v;
|
|
864
|
+
copy.id = nextId();
|
|
865
|
+
copy[itemsKey] = holder.items.splice(index);
|
|
866
|
+
const second = { name: holder.element.name, value: copy };
|
|
867
|
+
const parent = (_a = holder.value.PARENT) !== null && _a !== void 0 ? _a : this.p;
|
|
868
|
+
const at2 = holder.owner.indexOf(holder.element) + 1;
|
|
869
|
+
holder.owner.splice(at2, 0, item, second);
|
|
870
|
+
linkParents(copy[itemsKey], copy);
|
|
871
|
+
linkParents([item, second], parent);
|
|
872
|
+
return true;
|
|
873
|
+
}
|
|
874
|
+
/**
|
|
875
|
+
* Puts `item` beside a revision rather than in it: after it, or before it - and then before the
|
|
876
|
+
* range start of a move whose range opens just there, so that text typed at the start of moved
|
|
877
|
+
* text is not inside the move (`test/README.md` check 20 case 03).
|
|
878
|
+
*/
|
|
879
|
+
insertBesideRevision(holder, item, side) {
|
|
880
|
+
var _a, _b, _c;
|
|
881
|
+
let index = holder.owner.indexOf(holder.element) + (side === 'after' ? 1 : 0);
|
|
882
|
+
if (side === 'before' && (holder.kind === 'moveTo' || holder.kind === 'moveFrom')) {
|
|
883
|
+
const marker = holder.kind === 'moveTo' ? 'moveToRangeStart' : 'moveFromRangeStart';
|
|
884
|
+
while (index > 0 && ((_b = (_a = holder.owner[index - 1]) === null || _a === void 0 ? void 0 : _a.name) === null || _b === void 0 ? void 0 : _b.localPart) === marker)
|
|
885
|
+
index--;
|
|
886
|
+
}
|
|
887
|
+
holder.owner.splice(index, 0, item);
|
|
888
|
+
linkParents(item, (_c = holder.value.PARENT) !== null && _c !== void 0 ? _c : this.p);
|
|
889
|
+
}
|
|
890
|
+
/**
|
|
891
|
+
* A fresh annotation id for markup this paragraph writes itself: the package's counter when there
|
|
892
|
+
* is a package that keeps one, since a document has one id space; else one above every id in the
|
|
893
|
+
* story.
|
|
894
|
+
*/
|
|
895
|
+
nextAnnotationId() {
|
|
896
|
+
const pkg = this.parentBody.package_;
|
|
897
|
+
if (pkg && typeof pkg.markupRoots === 'function' && 'annotationIdFloor' in pkg) {
|
|
898
|
+
return new ChangeTracker(pkg, 'TrackAll').nextId();
|
|
899
|
+
}
|
|
900
|
+
let root = this.p;
|
|
901
|
+
for (let o = this.p; o !== undefined; o = o.PARENT)
|
|
902
|
+
root = o;
|
|
903
|
+
return highestAnnotationId(root) + 1;
|
|
904
|
+
}
|
|
779
905
|
/**
|
|
780
906
|
* Text inserted at `at` as a `w:ins`. A run of this author's own that is already inside a
|
|
781
907
|
* `w:ins` is extended rather than nested in another one, as Word does. `anchor` is the `w:del`
|
|
@@ -813,6 +939,18 @@ export class Paragraph {
|
|
|
813
939
|
tracker.assertEditable(neighbour === null || neighbour === void 0 ? void 0 : neighbour.revision);
|
|
814
940
|
const rPr = copyRPr((_b = neighbour === null || neighbour === void 0 ? void 0 : neighbour.run.rPr) !== null && _b !== void 0 ? _b : (_c = runsOf(this.p)[0]) === null || _c === void 0 ? void 0 : _c.value.rPr);
|
|
815
941
|
const ins = tracker.ins([runOf([textItem(text)], rPr)]);
|
|
942
|
+
// Typed inside another's revision - a w:ins, a w:moveTo - the revision is split and the typist's
|
|
943
|
+
// w:ins goes between its halves, as Word writes it (test/README.md check 20 case 01, check 21's
|
|
944
|
+
// first run); before, it went to the revision's edge, putting the text in the wrong place.
|
|
945
|
+
if ((before === null || before === void 0 ? void 0 : before.revision) && (after === null || after === void 0 ? void 0 : after.revision) && before.revision.element === after.revision.element
|
|
946
|
+
&& this.insertBetweenHalves(at, ins, () => tracker.nextId())) {
|
|
947
|
+
return;
|
|
948
|
+
}
|
|
949
|
+
// at the start of a revision, beside it - and outside a move's range that opens there (case 03)
|
|
950
|
+
if (!before && (after === null || after === void 0 ? void 0 : after.revision)) {
|
|
951
|
+
this.insertBesideRevision(after.revision, ins, 'before');
|
|
952
|
+
return;
|
|
953
|
+
}
|
|
816
954
|
if (neighbour) {
|
|
817
955
|
const owner = neighbour.revision ? neighbour.revision.owner : neighbour.runOwner;
|
|
818
956
|
const item = neighbour.revision ? neighbour.revision.element : neighbour.runOwner[neighbour.runIndex];
|
|
@@ -91,7 +91,9 @@ export declare class Range {
|
|
|
91
91
|
* `w:hyperlink`, splitting runs at the boundaries as `font` and `insertContentControl` do;
|
|
92
92
|
* setting `""` removes them and wraps nothing. An external address becomes a relationship of
|
|
93
93
|
* this range's part, which is why the part must exist (CR-002 section 20). Only the hyperlinks
|
|
94
|
-
* the range touches are removed; before 0.2.1 every one in the paragraph was (section 25).
|
|
94
|
+
* the range touches are removed; before 0.2.1 every one in the paragraph was (section 25). An
|
|
95
|
+
* empty range is refused with a `GeneralException`, as Office JS refuses it (section 25, check 19):
|
|
96
|
+
* to change a link under a caret, set it on the link's whole range.
|
|
95
97
|
*
|
|
96
98
|
* The runs are styled as Word styles them, measured through Office JS (`test/README.md` check
|
|
97
99
|
* 15, CR-002 section 25): each run a link wraps takes the `Hyperlink` character style, over any
|
|
@@ -113,9 +115,9 @@ export declare class Range {
|
|
|
113
115
|
*/
|
|
114
116
|
private removeHyperlinks;
|
|
115
117
|
/**
|
|
116
|
-
* Whether this range touches a hyperlink's text: overlaps it, or for an empty range lies in it
|
|
117
|
-
*
|
|
118
|
-
* nothing.
|
|
118
|
+
* Whether this range touches a hyperlink's text: overlaps it, or for an empty range lies in it or
|
|
119
|
+
* at either end of it - Office JS reads a caret just after a link as in that link (check 19, case
|
|
120
|
+
* K; the start edge was not measured). A hyperlink holding no text touches nothing.
|
|
119
121
|
*/
|
|
120
122
|
private touches;
|
|
121
123
|
private touchesSpan;
|
|
@@ -3,7 +3,7 @@ import { runFontSelectorOf } from '../fonts/lookup.mjs';
|
|
|
3
3
|
import { typeNameOf, linkParents, runItemsOf, runsOf } from './tree.mjs';
|
|
4
4
|
import { Docx4JException } from '../../opc/exceptions.mjs';
|
|
5
5
|
import { ContentControl } from './ContentControl.mjs';
|
|
6
|
-
import { sdt as sdtOf, nextSdtId, applyRunOptions } from '@docx4j/generated-objects-ts/builders/wml';
|
|
6
|
+
import { sdt as sdtOf, nextSdtId, applyRunOptions, t as textElement } from '@docx4j/generated-objects-ts/builders/wml';
|
|
7
7
|
import { hyperlink as hyperlinkOf } from '@docx4j/generated-objects-ts/el/org_docx4j_wml';
|
|
8
8
|
import { Namespaces } from '../../parts/Namespaces.mjs';
|
|
9
9
|
import { controlIdScope, sdtKindFor } from '../customxml/insert.mjs';
|
|
@@ -248,7 +248,9 @@ export class Range {
|
|
|
248
248
|
* `w:hyperlink`, splitting runs at the boundaries as `font` and `insertContentControl` do;
|
|
249
249
|
* setting `""` removes them and wraps nothing. An external address becomes a relationship of
|
|
250
250
|
* this range's part, which is why the part must exist (CR-002 section 20). Only the hyperlinks
|
|
251
|
-
* the range touches are removed; before 0.2.1 every one in the paragraph was (section 25).
|
|
251
|
+
* the range touches are removed; before 0.2.1 every one in the paragraph was (section 25). An
|
|
252
|
+
* empty range is refused with a `GeneralException`, as Office JS refuses it (section 25, check 19):
|
|
253
|
+
* to change a link under a caret, set it on the link's whole range.
|
|
252
254
|
*
|
|
253
255
|
* The runs are styled as Word styles them, measured through Office JS (`test/README.md` check
|
|
254
256
|
* 15, CR-002 section 25): each run a link wraps takes the `Hyperlink` character style, over any
|
|
@@ -270,6 +272,13 @@ export class Range {
|
|
|
270
272
|
}
|
|
271
273
|
set hyperlink(value) {
|
|
272
274
|
var _a, _b, _c;
|
|
275
|
+
// Office JS refuses a hyperlink set on an empty range - inside a link, in plain text, just after a
|
|
276
|
+
// link, and "" alike - with GeneralException, and changes nothing (test/README.md check 19, Word
|
|
277
|
+
// 16.0.20326.20158; CR-002 section 25). Before, the setter removed the link under a caret and
|
|
278
|
+
// left an empty w:hyperlink there.
|
|
279
|
+
if (this.start === this.end) {
|
|
280
|
+
throw new GeneralException('A hyperlink cannot be set on an empty range, as in Office JS: select the text to link, or the whole of a link to change it');
|
|
281
|
+
}
|
|
273
282
|
const paragraph = this.paragraph;
|
|
274
283
|
// A range that crosses the edge of a link takes the whole of that link into the new one; a range
|
|
275
284
|
// inside a link replaces it with a link over the range alone. Measured in Word through Office JS
|
|
@@ -303,10 +312,6 @@ export class Range {
|
|
|
303
312
|
holder.value.anchor = location;
|
|
304
313
|
if (address !== '')
|
|
305
314
|
holder.value.id = this.addHyperlinkRelationship(address);
|
|
306
|
-
if (start === end) {
|
|
307
|
-
paragraph.insertItemsAt(start, [holder]);
|
|
308
|
-
return;
|
|
309
|
-
}
|
|
310
315
|
paragraph.splitAt(start);
|
|
311
316
|
paragraph.splitAt(end);
|
|
312
317
|
const segments = paragraph.segments().filter((seg) => seg.start >= start && seg.end <= end);
|
|
@@ -327,6 +332,7 @@ export class Range {
|
|
|
327
332
|
owner.splice(at, items.length, holder);
|
|
328
333
|
linkParents(holder, (_a = segments[0].run.PARENT) !== null && _a !== void 0 ? _a : paragraph.p);
|
|
329
334
|
this.restyle(holder.value, true);
|
|
335
|
+
mergeTextRuns(holder.value.content, holder.value);
|
|
330
336
|
(_c = (_b = paragraph.parentBody.package_) === null || _b === void 0 ? void 0 : _b.requireStyles) === null || _c === void 0 ? void 0 : _c.call(_b, [HYPERLINK_STYLE]);
|
|
331
337
|
}
|
|
332
338
|
/** The first `w:hyperlink` this range touches, if any. */
|
|
@@ -353,16 +359,16 @@ export class Range {
|
|
|
353
359
|
}
|
|
354
360
|
}
|
|
355
361
|
/**
|
|
356
|
-
* Whether this range touches a hyperlink's text: overlaps it, or for an empty range lies in it
|
|
357
|
-
*
|
|
358
|
-
* nothing.
|
|
362
|
+
* Whether this range touches a hyperlink's text: overlaps it, or for an empty range lies in it or
|
|
363
|
+
* at either end of it - Office JS reads a caret just after a link as in that link (check 19, case
|
|
364
|
+
* K; the start edge was not measured). A hyperlink holding no text touches nothing.
|
|
359
365
|
*/
|
|
360
366
|
touches(holder) {
|
|
361
367
|
const span = this.spanOf(holder);
|
|
362
368
|
return span !== undefined && this.touchesSpan(span);
|
|
363
369
|
}
|
|
364
370
|
touchesSpan([start, end]) {
|
|
365
|
-
return this.start === this.end ? start <= this.start && this.start
|
|
371
|
+
return this.start === this.end ? start <= this.start && this.start <= end : start < this.end && end > this.start;
|
|
366
372
|
}
|
|
367
373
|
/** The text a hyperlink holds, as [start, end) in the paragraph, or undefined when it holds none. */
|
|
368
374
|
spanOf(holder) {
|
|
@@ -393,6 +399,10 @@ export class Range {
|
|
|
393
399
|
if (tracking && !tracking.ownInsertions.has(run))
|
|
394
400
|
tracking.tracker.recordRPrChange(run.rPr);
|
|
395
401
|
applyRunOptions(run.rPr, linked ? { style: HYPERLINK_STYLE, color: '' } : { style: '' });
|
|
402
|
+
if (tracking)
|
|
403
|
+
tracking.tracker.settleRPrChange(run);
|
|
404
|
+
if (!run.rPr)
|
|
405
|
+
continue;
|
|
396
406
|
if (Object.keys(run.rPr).every((key) => key === 'TYPE_NAME'))
|
|
397
407
|
delete run.rPr;
|
|
398
408
|
else
|
|
@@ -413,8 +423,56 @@ export class Range {
|
|
|
413
423
|
return part.getRelationshipsPart(true).addExternalRelationship(Namespaces.HYPERLINK, address).id;
|
|
414
424
|
}
|
|
415
425
|
}
|
|
426
|
+
/**
|
|
427
|
+
* What Office JS throws where the host refuses an operation it has no answer for: its `code`, so that
|
|
428
|
+
* add-in code branching on `error.code` behaves the same here (check 19).
|
|
429
|
+
*/
|
|
430
|
+
class GeneralException extends Docx4JException {
|
|
431
|
+
constructor(message) {
|
|
432
|
+
super(message);
|
|
433
|
+
this.code = 'GeneralException';
|
|
434
|
+
this.name = 'GeneralException';
|
|
435
|
+
}
|
|
436
|
+
}
|
|
416
437
|
/** The character style Word gives the runs of a hyperlink it makes. */
|
|
417
438
|
const HYPERLINK_STYLE = 'Hyperlink';
|
|
439
|
+
/**
|
|
440
|
+
* Adjacent runs holding nothing but text and the same properties become one, as Word writes a link
|
|
441
|
+
* it makes: linking "beta gamma" over the end of a link on "alpha beta" gives Word one run
|
|
442
|
+
* "alpha beta gamma" (`test/README.md` check 16, case F), where splitting and restyling left two.
|
|
443
|
+
* Runs whose properties differ at all - a `w:rPrChange` under tracking among them - stay apart.
|
|
444
|
+
*/
|
|
445
|
+
function mergeTextRuns(items, parent) {
|
|
446
|
+
var _a, _b;
|
|
447
|
+
for (let i = items.length - 1; i > 0; i--) {
|
|
448
|
+
const [a, b] = [items[i - 1], items[i]];
|
|
449
|
+
if (!isTextRun(a) || !isTextRun(b))
|
|
450
|
+
continue;
|
|
451
|
+
const [ra, rb] = [a.value, b.value];
|
|
452
|
+
if (canonical(ra.rPr) !== canonical(rb.rPr))
|
|
453
|
+
continue;
|
|
454
|
+
const text = [...((_a = ra.content) !== null && _a !== void 0 ? _a : []), ...((_b = rb.content) !== null && _b !== void 0 ? _b : [])].map((item) => { var _a; return (_a = item.value.value) !== null && _a !== void 0 ? _a : ''; }).join('');
|
|
455
|
+
ra.content = [textElement(text)];
|
|
456
|
+
linkParents(ra.content, ra);
|
|
457
|
+
items.splice(i, 1);
|
|
458
|
+
}
|
|
459
|
+
linkParents(items, parent);
|
|
460
|
+
}
|
|
461
|
+
function isTextRun(item) {
|
|
462
|
+
var _a;
|
|
463
|
+
return typeNameOf(item) === 'org_docx4j_wml.R'
|
|
464
|
+
&& ((_a = item.value.content) !== null && _a !== void 0 ? _a : []).every((c) => { var _a; return ((_a = c.name) === null || _a === void 0 ? void 0 : _a.localPart) === 't'; });
|
|
465
|
+
}
|
|
466
|
+
/** A value as text with its keys in order and `PARENT` left out, to compare run properties by content. */
|
|
467
|
+
function canonical(value) {
|
|
468
|
+
return JSON.stringify(value, (key, v) => {
|
|
469
|
+
if (key === 'PARENT')
|
|
470
|
+
return undefined;
|
|
471
|
+
if (v === null || typeof v !== 'object' || Array.isArray(v))
|
|
472
|
+
return v;
|
|
473
|
+
return Object.fromEntries(Object.keys(v).sort().map((k) => [k, v[k]]));
|
|
474
|
+
});
|
|
475
|
+
}
|
|
418
476
|
/** Whether `object` is `ancestor` or below it, following `PARENT` no further than `stop`. */
|
|
419
477
|
function within(object, ancestor, stop) {
|
|
420
478
|
for (let o = object; o !== undefined && o !== stop; o = o.PARENT) {
|
|
@@ -44,6 +44,54 @@ export type TrackedChangeTarget =
|
|
|
44
44
|
owner: Element[];
|
|
45
45
|
value: wml.CTTrackChange;
|
|
46
46
|
inner: TrackedChange[];
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Touching insertions, deletions or formatting changes by one author - `w:ins` or `w:del` around
|
|
50
|
+
* runs and inserted or deleted paragraph marks, or runs' `w:rPrChange`, with nothing unrevised
|
|
51
|
+
* between them - which Office JS lists as one change (CR-002 section 29, checks 22, 23 and 28): a
|
|
52
|
+
* deleted paragraph's text and mark, a deletion across a mark, bold put on two runs of different
|
|
53
|
+
* formatting. `pieces` are the single changes, in document order; `value` is the first's.
|
|
54
|
+
*/
|
|
55
|
+
| {
|
|
56
|
+
kind: 'group';
|
|
57
|
+
revision: 'ins' | 'del' | 'format';
|
|
58
|
+
pieces: TrackedChange[];
|
|
59
|
+
value: wml.CTTrackChange;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* A table's property records - `w:tblPrChange`, `w:tblGridChange`, `w:tblPrExChange`, and every
|
|
63
|
+
* `w:trPrChange` and `w:tcPrChange` in it - which Word writes as one set for one change and Office
|
|
64
|
+
* JS lists as one `Formatted` change (CR-002 section 35, checks 27 and 28). `value` is the table's
|
|
65
|
+
* record, else the first row's or cell's.
|
|
66
|
+
*/
|
|
67
|
+
| {
|
|
68
|
+
kind: 'tableProperties';
|
|
69
|
+
tbl: Element<wml.Tbl>;
|
|
70
|
+
value: wml.CTTrackChange;
|
|
71
|
+
}
|
|
72
|
+
/** `w:tcPr/w:cellIns` or `w:tcPr/w:cellDel`: an inserted or deleted cell (section 35, check 27). */
|
|
73
|
+
| {
|
|
74
|
+
kind: 'cell';
|
|
75
|
+
cell: 'ins' | 'del';
|
|
76
|
+
tc: Element<wml.Tc>;
|
|
77
|
+
owner: Element[];
|
|
78
|
+
tr: wml.Tr;
|
|
79
|
+
value: wml.CTTrackChange;
|
|
80
|
+
}
|
|
81
|
+
/** `w:pPr/w:rPr/w:rPrChange`: a paragraph mark's formatting, which groups with its runs' (check 27). */
|
|
82
|
+
| {
|
|
83
|
+
kind: 'markProperties';
|
|
84
|
+
value: wml.ParaRPrChange;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* `w:sectPr/w:sectPrChange`: a section's properties, at a paragraph's section break or the body's
|
|
88
|
+
* last section. `text` is the section's, as Office JS reports it (check 27: the text, then `\f`).
|
|
89
|
+
*/
|
|
90
|
+
| {
|
|
91
|
+
kind: 'sectionProperties';
|
|
92
|
+
sectPr: wml.SectPr;
|
|
93
|
+
value: wml.CTSectPrChange;
|
|
94
|
+
text: string;
|
|
47
95
|
};
|
|
48
96
|
/**
|
|
49
97
|
* A subset of Office JS `Word.TrackedChange`: `type`, `author`, `date`, `text`, `accept()`,
|
|
@@ -60,13 +108,17 @@ export declare class TrackedChange {
|
|
|
60
108
|
/** Office JS `Word.ChangeTrackingState`. */
|
|
61
109
|
get type(): TrackedChangeType;
|
|
62
110
|
get author(): string;
|
|
63
|
-
/**
|
|
111
|
+
/**
|
|
112
|
+
* When the change was made: `w16du:dateUtc` if the markup has it, else `w:date` read as Word writes
|
|
113
|
+
* it, local wall-clock time (`revisionDateOf`, CR-002 section 29); a group's is its first piece's,
|
|
114
|
+
* as Office JS reports it.
|
|
115
|
+
*/
|
|
64
116
|
get date(): Date | undefined;
|
|
65
117
|
/** The annotation id (`w:id`; an extension). */
|
|
66
118
|
get id(): number;
|
|
67
119
|
/** The markup this change is over (an extension): the `w:ins` element, the `w:rPrChange`, ... */
|
|
68
120
|
get element(): object;
|
|
69
|
-
/** The text the change covers;
|
|
121
|
+
/** The text the change covers; `\r` for a paragraph mark, as Office JS gives it (check 22). */
|
|
70
122
|
get text(): string;
|
|
71
123
|
/** The range the change covers; a deletion is a collapsed range where it sits in the accepted text. */
|
|
72
124
|
getRange(): Range | undefined;
|
|
@@ -93,10 +145,49 @@ export declare class TrackedChange {
|
|
|
93
145
|
* mark is simply dropped.
|
|
94
146
|
*/
|
|
95
147
|
export declare function joinWithNext(paragraph: Pick<Paragraph, 'container' | 'element' | 'p'>, fallbackToPrevious?: boolean): void;
|
|
96
|
-
/**
|
|
148
|
+
/**
|
|
149
|
+
* The tracked changes of one paragraph, in document order, touching pieces grouped as Office JS
|
|
150
|
+
* groups them (`groupTouching`). Office JS lists the changes that start in the paragraph (check 22):
|
|
151
|
+
* pieces that carry on a change from the paragraph before - its inserted or deleted mark - are left
|
|
152
|
+
* to that paragraph, and a change running on into the next is cut at this paragraph's end.
|
|
153
|
+
*/
|
|
97
154
|
export declare function trackedChangesOfParagraph(paragraph: Paragraph): TrackedChange[];
|
|
155
|
+
/** Between two tracked changes: something unrevised - text, a paragraph mark, a table - that keeps them apart. */
|
|
156
|
+
export declare const BREAK: "break";
|
|
157
|
+
export type TrackedChangeToken = TrackedChange | typeof BREAK;
|
|
158
|
+
/**
|
|
159
|
+
* A paragraph's single changes in document order, its mark last, with a `BREAK` wherever unrevised
|
|
160
|
+
* content stands between two of them: `groupTouching` makes the groups from these.
|
|
161
|
+
*/
|
|
162
|
+
export declare function trackedChangeTokensOfParagraph(paragraph: Paragraph): TrackedChangeToken[];
|
|
163
|
+
/**
|
|
164
|
+
* Tokens made into the changes Office JS lists: consecutive pieces of one kind (inserted, deleted, or
|
|
165
|
+
* a run's formatting) by one author, nothing unrevised between them, are one change (checks 22, 23
|
|
166
|
+
* and 28: text and marks alike, dates regardless, formatting whatever it recorded; another author, or
|
|
167
|
+
* one kind against another, starts a new one). A change that does not group - a paragraph's
|
|
168
|
+
* properties, a move - is listed where it stands and does not break a group. A group of one piece is
|
|
169
|
+
* that piece.
|
|
170
|
+
*/
|
|
171
|
+
export declare function groupTouching(tokens: TrackedChangeToken[]): TrackedChange[];
|
|
98
172
|
/**
|
|
99
173
|
* The `w:trPr/w:ins` and `w:trPr/w:del` revisions of a table row. `inner` is the revision markup
|
|
100
174
|
* of the row's own cells, which the row's change owns rather than reporting separately.
|
|
101
175
|
*/
|
|
102
176
|
export declare function trackedChangesOfRow(tr: Element<wml.Tr>, owner: Element[], inner?: TrackedChange[]): TrackedChange[];
|
|
177
|
+
/** The change a table's property records make, one per table, or undefined when they make none. */
|
|
178
|
+
export declare function tablePropertiesChangeOf(tbl: Element<wml.Tbl>): TrackedChange | undefined;
|
|
179
|
+
/** A cell's own revision, `w:cellIns` or `w:cellDel`, or undefined. */
|
|
180
|
+
export declare function cellChangeOf(tc: Element<wml.Tc>, owner: Element[], tr: wml.Tr): TrackedChange | undefined;
|
|
181
|
+
/** The change to the body's last section (`w:body/w:sectPr/w:sectPrChange`), its text the section's paragraphs'. */
|
|
182
|
+
export declare function bodySectionChangeOf(container: {
|
|
183
|
+
content?: Element[];
|
|
184
|
+
sectPr?: wml.SectPr;
|
|
185
|
+
}): TrackedChange | undefined;
|
|
186
|
+
/**
|
|
187
|
+
* What `acceptAll` and `rejectAll` resolve that is never listed: a `w:cellMerge`, which Office JS
|
|
188
|
+
* cannot list (check 27: `getTrackedChanges` throws); a `w:numberingChange`, which Word no longer
|
|
189
|
+
* writes and drops when one is put in, and whose record is the old number's text, so that a reject
|
|
190
|
+
* too can only drop it; and a table's records that change nothing. A merge rejected gets the merge it
|
|
191
|
+
* recorded back (docx4j's reading; unmeasured).
|
|
192
|
+
*/
|
|
193
|
+
export declare function resolveUnlisted(items: Element[], what: 'accept' | 'reject'): void;
|