@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
|
@@ -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,11 +115,14 @@ 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;
|
|
123
|
+
private touchesSpan;
|
|
124
|
+
/** The text a hyperlink holds, as [start, end) in the paragraph, or undefined when it holds none. */
|
|
125
|
+
private spanOf;
|
|
121
126
|
/**
|
|
122
127
|
* The `Hyperlink` character style on every run of a hyperlink, in place of any character style
|
|
123
128
|
* and with the direct colour removed, or off again when the link goes: what Word does (CR-002
|
|
@@ -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,7 +272,30 @@ 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;
|
|
283
|
+
// A range that crosses the edge of a link takes the whole of that link into the new one; a range
|
|
284
|
+
// inside a link replaces it with a link over the range alone. Measured in Word through Office JS
|
|
285
|
+
// (test/README.md check 16, cases F and G; CR-002 section 25): "alpha beta" linked and then
|
|
286
|
+
// "beta gamma" is one link over "alpha beta gamma", where "alpha beta gamma" linked and then
|
|
287
|
+
// "beta" leaves "alpha " and " gamma" unlinked.
|
|
288
|
+
let start = this.start;
|
|
289
|
+
let end = this.end;
|
|
290
|
+
for (const holder of hyperlinkHolders(paragraph)) {
|
|
291
|
+
const span = this.spanOf(holder);
|
|
292
|
+
if (span === undefined || !this.touchesSpan(span))
|
|
293
|
+
continue;
|
|
294
|
+
if (span[0] <= this.start && this.end <= span[1])
|
|
295
|
+
continue;
|
|
296
|
+
start = Math.min(start, span[0]);
|
|
297
|
+
end = Math.max(end, span[1]);
|
|
298
|
+
}
|
|
274
299
|
this.removeHyperlinks();
|
|
275
300
|
if (value === '')
|
|
276
301
|
return;
|
|
@@ -287,13 +312,9 @@ export class Range {
|
|
|
287
312
|
holder.value.anchor = location;
|
|
288
313
|
if (address !== '')
|
|
289
314
|
holder.value.id = this.addHyperlinkRelationship(address);
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
}
|
|
294
|
-
paragraph.splitAt(this.start);
|
|
295
|
-
paragraph.splitAt(this.end);
|
|
296
|
-
const segments = paragraph.segments().filter((seg) => seg.start >= this.start && seg.end <= this.end);
|
|
315
|
+
paragraph.splitAt(start);
|
|
316
|
+
paragraph.splitAt(end);
|
|
317
|
+
const segments = paragraph.segments().filter((seg) => seg.start >= start && seg.end <= end);
|
|
297
318
|
if (segments.length === 0)
|
|
298
319
|
throw new Docx4JException('This range covers no run');
|
|
299
320
|
const owner = segments[0].runOwner;
|
|
@@ -311,6 +332,7 @@ export class Range {
|
|
|
311
332
|
owner.splice(at, items.length, holder);
|
|
312
333
|
linkParents(holder, (_a = segments[0].run.PARENT) !== null && _a !== void 0 ? _a : paragraph.p);
|
|
313
334
|
this.restyle(holder.value, true);
|
|
335
|
+
mergeTextRuns(holder.value.content, holder.value);
|
|
314
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]);
|
|
315
337
|
}
|
|
316
338
|
/** The first `w:hyperlink` this range touches, if any. */
|
|
@@ -337,11 +359,19 @@ export class Range {
|
|
|
337
359
|
}
|
|
338
360
|
}
|
|
339
361
|
/**
|
|
340
|
-
* Whether this range touches a hyperlink's text: overlaps it, or for an empty range lies in it
|
|
341
|
-
*
|
|
342
|
-
* 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.
|
|
343
365
|
*/
|
|
344
366
|
touches(holder) {
|
|
367
|
+
const span = this.spanOf(holder);
|
|
368
|
+
return span !== undefined && this.touchesSpan(span);
|
|
369
|
+
}
|
|
370
|
+
touchesSpan([start, end]) {
|
|
371
|
+
return this.start === this.end ? start <= this.start && this.start <= end : start < this.end && end > this.start;
|
|
372
|
+
}
|
|
373
|
+
/** The text a hyperlink holds, as [start, end) in the paragraph, or undefined when it holds none. */
|
|
374
|
+
spanOf(holder) {
|
|
345
375
|
let start;
|
|
346
376
|
let end = 0;
|
|
347
377
|
for (const segment of this.paragraph.segments()) {
|
|
@@ -350,9 +380,7 @@ export class Range {
|
|
|
350
380
|
start !== null && start !== void 0 ? start : (start = segment.start);
|
|
351
381
|
end = segment.end;
|
|
352
382
|
}
|
|
353
|
-
|
|
354
|
-
return false;
|
|
355
|
-
return this.start === this.end ? start <= this.start && this.start < end : start < this.end && end > this.start;
|
|
383
|
+
return start === undefined ? undefined : [start, end];
|
|
356
384
|
}
|
|
357
385
|
/**
|
|
358
386
|
* The `Hyperlink` character style on every run of a hyperlink, in place of any character style
|
|
@@ -371,6 +399,10 @@ export class Range {
|
|
|
371
399
|
if (tracking && !tracking.ownInsertions.has(run))
|
|
372
400
|
tracking.tracker.recordRPrChange(run.rPr);
|
|
373
401
|
applyRunOptions(run.rPr, linked ? { style: HYPERLINK_STYLE, color: '' } : { style: '' });
|
|
402
|
+
if (tracking)
|
|
403
|
+
tracking.tracker.settleRPrChange(run);
|
|
404
|
+
if (!run.rPr)
|
|
405
|
+
continue;
|
|
374
406
|
if (Object.keys(run.rPr).every((key) => key === 'TYPE_NAME'))
|
|
375
407
|
delete run.rPr;
|
|
376
408
|
else
|
|
@@ -391,8 +423,56 @@ export class Range {
|
|
|
391
423
|
return part.getRelationshipsPart(true).addExternalRelationship(Namespaces.HYPERLINK, address).id;
|
|
392
424
|
}
|
|
393
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
|
+
}
|
|
394
437
|
/** The character style Word gives the runs of a hyperlink it makes. */
|
|
395
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
|
+
}
|
|
396
476
|
/** Whether `object` is `ancestor` or below it, following `PARENT` no further than `stop`. */
|
|
397
477
|
function within(object, ancestor, stop) {
|
|
398
478
|
for (let o = object; o !== undefined && o !== stop; o = o.PARENT) {
|