@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.
@@ -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
- return;
84
- }
85
- pPr.pStyle = { val: id };
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
- var _a, _b, _c, _d, _e, _f, _g, _h;
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
- return;
152
- }
153
- pPr.jc = { val };
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 _e;
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
- // Insert: extend the w:t at `start` (the one containing it, else the one ending there, else the one starting there)
550
- const target = (_b = (_a = segs.find((s) => s.editable && s.start < start && start < s.end)) !== null && _a !== void 0 ? _a : segs.find((s) => s.editable && s.end === start)) !== null && _b !== void 0 ? _b : segs.find((s) => s.editable && s.start === start);
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 = (_c = runsOf(this.p)[0]) === null || _c === void 0 ? void 0 : _c.value.rPr;
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 = ((_d = (_e = this.p).content) !== null && _d !== void 0 ? _d : (_e.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
- * (from its first character up to, not including, its end). A hyperlink holding no text touches
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
- * (from its first character up to, not including, its end). A hyperlink holding no text touches
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 < end : start < this.end && end > 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
- /** `w:date`, when the markup carries one. */
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; '' for a paragraph mark, whose range is the paragraph. */
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
- /** The tracked changes of one paragraph, in document order. */
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;