@docx4j/core-ts 0.2.2 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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, wrapInsertedRuns, markHolderInserted } 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,35 @@ 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 if (this.inheritedAlignment() === v)
156
+ delete pPr.jc;
157
+ else
158
+ pPr.jc = { val };
159
+ this.settleFormatting();
160
+ }
161
+ /**
162
+ * The alignment the paragraph resolves to with no `w:jc` of its own: its style's, else the
163
+ * defaults'. Word's Ctrl+L on a centred paragraph whose style is left-aligned removes the `w:jc`
164
+ * rather than writing `w:jc="left"`, with tracking on (check 26) and off (the editor's
165
+ * `measure-word2010.docx` and `measure-word15.docx`, 2026-09-30; CR-002 section 38), so a value the
166
+ * paragraph already inherits is not written. Measured for left over a left-aligned style; taken to
167
+ * hold for any value. Undefined without a resolver, where the value is written as asked.
168
+ */
169
+ inheritedAlignment() {
170
+ var _a;
171
+ const resolver = (_a = this.parentBody.package_) === null || _a === void 0 ? void 0 : _a.propertyResolverOrUndefined;
172
+ if (!resolver)
173
+ return undefined;
174
+ const { jc: _jc, pPrChange: _change, PARENT: _parent, ...rest } = this.p.pPr;
175
+ return alignmentOf(resolver.getEffectivePPr(rest), true);
154
176
  }
155
177
  /** Indents and spacing in points, as Office JS; the effective values (CR-001 Phase B step 2). */
156
178
  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); }
179
+ set leftIndent(pt) { this.ind().left = Math.round(pt * TWIPS_PER_POINT); this.settleFormatting(); }
158
180
  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); }
181
+ set rightIndent(pt) { this.ind().right = Math.round(pt * TWIPS_PER_POINT); this.settleFormatting(); }
160
182
  /** First-line indent; negative for a hanging indent, as Office JS. */
161
183
  get firstLineIndent() {
162
184
  return firstLineIndentOf(this.effectivePPr);
@@ -171,22 +193,23 @@ export class Paragraph {
171
193
  ind.firstLine = Math.round(pt * TWIPS_PER_POINT);
172
194
  delete ind.hanging;
173
195
  }
196
+ this.settleFormatting();
174
197
  }
175
198
  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); }
199
+ set spaceBefore(pt) { this.spacing().before = Math.round(pt * TWIPS_PER_POINT); this.settleFormatting(); }
177
200
  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); }
201
+ set spaceAfter(pt) { this.spacing().after = Math.round(pt * TWIPS_PER_POINT); this.settleFormatting(); }
179
202
  /** Line spacing in points: w:line/240 lines of 12pt when the rule is auto, else w:line twips; 0 when not set. */
180
203
  get lineSpacing() {
181
204
  return lineSpacingOf(this.effectivePPr);
182
205
  }
183
- set lineSpacing(pt) { const s = this.spacing(); s.line = Math.round(pt * TWIPS_PER_POINT); s.lineRule = 'exact'; }
206
+ set lineSpacing(pt) { const s = this.spacing(); s.line = Math.round(pt * TWIPS_PER_POINT); s.lineRule = 'exact'; this.settleFormatting(); }
184
207
  /** w:outlineLvl + 1 (1 to 9); 10 for body text, as Office JS. A heading style supplies it. */
185
208
  get outlineLevel() { return outlineLevelOf(this.effectivePPr); }
186
209
  set outlineLevel(level) { const pPr = this.pPr(); if (level >= 10 || level < 1)
187
210
  delete pPr.outlineLvl;
188
211
  else
189
- pPr.outlineLvl = { val: level - 1 }; }
212
+ pPr.outlineLvl = { val: level - 1 }; this.settleFormatting(); }
190
213
  /**
191
214
  * The runs' formatting: reads the first run's *effective* properties, writes direct
192
215
  * formatting to all (extension: `Font` is over runs, not the paragraph mark).
@@ -403,6 +426,42 @@ export class Paragraph {
403
426
  return;
404
427
  }
405
428
  tracker.markParagraphDeleted(this.p);
429
+ this.giveMarkPropertiesToNext(tracker);
430
+ }
431
+ /**
432
+ * A paragraph mark deleted under tracking gives the next paragraph this paragraph's properties,
433
+ * as a `w:pPrChange` on it recording its own: Word, at Delete at the end of a Heading 2 with a
434
+ * Normal paragraph after it, wrote the next paragraph `w:pStyle Heading2` with
435
+ * `<w:pPrChange><w:pPr/></w:pPrChange>`, and No Markup reads the joined line as a heading
436
+ * throughout (the editor's `measure-word15.docx`, 2026-09-30; CR-002 section 38). The Delete
437
+ * counterpart of check 29's Enter. Nothing where the two paragraphs' properties are the same
438
+ * (check 22's deleted marks). Accepting the mark then joins into a paragraph with these
439
+ * properties, and rejecting puts the next paragraph's back.
440
+ */
441
+ giveMarkPropertiesToNext(tracker) {
442
+ const i = this.index;
443
+ const next = i >= 0 ? this.container[i + 1] : undefined;
444
+ if (!next || typeNameOf(next) !== 'org_docx4j_wml.P')
445
+ return;
446
+ const base = (pPr) => {
447
+ const out = {};
448
+ for (const [key, value] of Object.entries(pPr !== null && pPr !== void 0 ? pPr : {})) {
449
+ if (key !== 'TYPE_NAME' && key !== 'PARENT' && key !== 'rPr' && key !== 'sectPr' && key !== 'pPrChange' && value !== undefined)
450
+ out[key] = value;
451
+ }
452
+ return out;
453
+ };
454
+ const source = base(this.p.pPr);
455
+ const nextP = next.value;
456
+ if (canonical(source) === canonical(base(nextP.pPr)))
457
+ return;
458
+ const target = new Paragraph(next, this.container, this.parentBody).pPr();
459
+ tracker.recordPPrChange(target);
460
+ for (const key of Object.keys(base(target)))
461
+ delete target[key];
462
+ for (const [key, value] of Object.entries(source))
463
+ target[key] = deepCopy(value);
464
+ linkParents(target, nextP);
406
465
  }
407
466
  /**
408
467
  * The paragraph's content and its mark marked deleted, the paragraph element itself kept:
@@ -446,6 +505,40 @@ export class Paragraph {
446
505
  (_b = this.changeTracker) === null || _b === void 0 ? void 0 : _b.recordPPrChange(pPr);
447
506
  return pPr;
448
507
  }
508
+ /**
509
+ * After a tracked write of the paragraph's properties: when the paragraph is as it was, its
510
+ * recorded properties go back as they were and the `w:pPrChange` goes, since Word keeps no
511
+ * formatting change that changes nothing, whoever changed it back (`test/README.md` check 26:
512
+ * centred then left again, by the same author and by another; Increase Indent then Decrease
513
+ * Indent; each left no `w:pPr`; CR-002 section 29). "As it was" is Office JS's paragraph
514
+ * formatting (alignment, indents, spacing, outline level) read resolved from both, the rest of the
515
+ * properties - the style, the list - written the same: Word's Ctrl+L and Decrease Indent removed
516
+ * the direct values where this package's setters write `w:jc="left"` and `w:ind w:left="0"`.
517
+ * Without the package's `PropertyResolver` only properties written the same count.
518
+ *
519
+ * @internal public so that the list views (CR-002 phase H) settle their `w:numPr` writes.
520
+ */
521
+ settleFormatting() {
522
+ var _a, _b;
523
+ const pPr = this.p.pPr;
524
+ const recorded = (_a = pPr === null || pPr === void 0 ? void 0 : pPr.pPrChange) === null || _a === void 0 ? void 0 : _a.pPr;
525
+ if (!pPr || !pPr.pPrChange || !this.changeTracker)
526
+ return;
527
+ const current = {};
528
+ for (const [key, value] of Object.entries(pPr))
529
+ if (key !== 'pPrChange' && key !== 'rPr' && key !== 'sectPr')
530
+ current[key] = value;
531
+ const before = { ...(recorded !== null && recorded !== void 0 ? recorded : {}) };
532
+ let same = canonical(current) === canonical(before);
533
+ if (!same) {
534
+ const rest = (props) => canonical(Object.fromEntries(Object.entries(props).filter(([key]) => !FORMATTING_MEMBERS.has(key))));
535
+ const resolver = (_b = this.parentBody.package_) === null || _b === void 0 ? void 0 : _b.propertyResolverOrUndefined;
536
+ same = resolver !== undefined && rest(current) === rest(before)
537
+ && canonical(formattingOf(resolver.getEffectivePPr(current), true)) === canonical(formattingOf(resolver.getEffectivePPr(before), true));
538
+ }
539
+ if (same)
540
+ restorePPr(this.p, recorded);
541
+ }
449
542
  /**
450
543
  * The paragraph's `w:numPr`, created when absent, through {@link pPr} so that a tracked
451
544
  * write records `w:pPrChange` first.
@@ -494,9 +587,9 @@ export class Paragraph {
494
587
  * is removed, a run left empty is removed. Text is inserted into the w:t at `start`,
495
588
  * else into a new run with the formatting of the nearest run.
496
589
  */
497
- splice(start, end, text) {
498
- var _a, _b, _c, _d;
499
- var _e;
590
+ splice(start, end, text, prefer = 'before') {
591
+ var _a, _b, _c, _d, _e, _f;
592
+ var _g;
500
593
  const length = this.text.length;
501
594
  start = Math.max(0, Math.min(start, length));
502
595
  end = Math.max(start, Math.min(end, length));
@@ -505,11 +598,18 @@ export class Paragraph {
505
598
  // a replacement is the w:del first and the w:ins after it, as Word writes one
506
599
  const deletions = end > start ? this.deleteText(tracker, start, end) : [];
507
600
  if (text.length > 0)
508
- this.insertTracked(tracker, start, text, deletions[deletions.length - 1]);
601
+ this.insertTracked(tracker, start, text, deletions[deletions.length - 1], prefer);
509
602
  return new Range(this, start, start + text.length);
510
603
  }
511
604
  let segs = this.segments();
512
605
  let inserted = false;
606
+ // A replacement that begins inside a revision is a deletion and then the new text typed there, so
607
+ // the new text is plain and splits the revision, as typed text is (test/README.md check 21), rather
608
+ // than joining the revision it replaced part of.
609
+ if (end > start && text.length > 0 && ((_a = segs.find((s) => s.start <= start && start < s.end)) === null || _a === void 0 ? void 0 : _a.revision)) {
610
+ this.splice(start, end, '');
611
+ return this.splice(start, start, text);
612
+ }
513
613
  if (end > start) {
514
614
  // Replace: the new text takes the place (and formatting) of the first replaced character, as Word does.
515
615
  const first = segs.find((s) => s.start <= start && start < s.end);
@@ -546,8 +646,27 @@ export class Paragraph {
546
646
  segs = this.segments();
547
647
  }
548
648
  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);
649
+ // Text typed with tracking off is plain wherever it lands, as Word writes it (test/README.md
650
+ // check 21): inside a revision it splits the revision, and next to one it goes beside it, not
651
+ // in it. Otherwise it extends the w:t at `start` (the one containing it, else the one ending
652
+ // there, else the one starting there).
653
+ const containing = segs.find((s) => s.editable && s.start < start && start < s.end);
654
+ if ((containing === null || containing === void 0 ? void 0 : containing.revision) && this.insertBetweenHalves(start, runOf([textItem(text)], copyRPr(containing.run.rPr)), () => this.nextAnnotationId())) {
655
+ return new Range(this, start, start + text.length);
656
+ }
657
+ const ending = segs.find((s) => s.editable && s.end === start);
658
+ const starting = segs.find((s) => s.editable && s.start === start);
659
+ // `prefer` says which run takes text put exactly between two: the one ending there (typed text,
660
+ // `Paragraph.insertText`) or the one starting there (`Range.insertText('Before')`, which Office
661
+ // JS puts inside the link the range is on: check 32)
662
+ const [near, far] = prefer === 'after' ? [starting, ending] : [ending, starting];
663
+ const target = (_d = (_c = (_b = (containing && !containing.revision ? containing : undefined)) !== null && _b !== void 0 ? _b : (near && !near.revision ? near : undefined)) !== null && _c !== void 0 ? _c : (far && !far.revision ? far : undefined)) !== null && _d !== void 0 ? _d : containing; // a revision that could not be split: extended, as before
664
+ const edge = target ? undefined : near !== null && near !== void 0 ? near : far;
665
+ if (edge === null || edge === void 0 ? void 0 : edge.revision) {
666
+ const run = runOf([textItem(text)], copyRPr(edge.run.rPr));
667
+ this.insertBesideRevision(edge.revision, run, edge === ending ? 'after' : 'before');
668
+ return new Range(this, start, start + text.length);
669
+ }
551
670
  if (target) {
552
671
  const t = target.item.value;
553
672
  const at = start - target.start;
@@ -557,16 +676,16 @@ export class Paragraph {
557
676
  // no w:t to extend: a new run next to the segment at `start`, with that run's formatting
558
677
  const before = [...segs].reverse().find((s) => s.end <= start);
559
678
  const after = segs.find((s) => s.start >= start);
560
- 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;
679
+ const neighbour = prefer === 'after' ? after !== null && after !== void 0 ? after : before : before !== null && before !== void 0 ? before : after;
680
+ const firstRun = (_e = runsOf(this.p)[0]) === null || _e === void 0 ? void 0 : _e.value.rPr;
562
681
  const rPr = (neighbour === null || neighbour === void 0 ? void 0 : neighbour.run.rPr) ? deepCopy(neighbour.run.rPr) : firstRun ? deepCopy(firstRun) : undefined;
563
682
  const run = runOf([textItem(text)], rPr);
564
- if (before)
683
+ if (neighbour === before && before)
565
684
  before.runOwner.splice(before.runIndex + 1, 0, run);
566
685
  else if (after)
567
686
  after.runOwner.splice(after.runIndex, 0, run);
568
687
  else {
569
- const content = ((_d = (_e = this.p).content) !== null && _d !== void 0 ? _d : (_e.content = []));
688
+ const content = ((_f = (_g = this.p).content) !== null && _f !== void 0 ? _f : (_g.content = []));
570
689
  if (start === 0)
571
690
  content.unshift(run);
572
691
  else
@@ -582,19 +701,33 @@ export class Paragraph {
582
701
  * run there when the offset falls inside one. PARENT is linked on what is inserted.
583
702
  */
584
703
  insertItemsAt(offset, items) {
585
- var _a, _b, _c;
586
- var _d;
704
+ var _a, _b, _c, _d;
705
+ var _e;
587
706
  if (items.length === 0)
588
707
  return;
708
+ const tracker = this.changeTracker;
709
+ if (tracker) {
710
+ // With tracking on, what goes in is an insertion: its runs in w:ins, in a link, a control or a
711
+ // field too, placed as typed text is - beside another's revision, or between its halves, never
712
+ // in it. Before, `insertOoxml` into a paragraph went in untracked (CR-002 section 37).
713
+ wrapInsertedRuns(tracker, items, this.p);
714
+ this.splitAt(offset);
715
+ const segs = this.segments();
716
+ const before = [...segs].reverse().find((s) => s.end <= offset);
717
+ const after = segs.find((s) => s.start >= offset);
718
+ tracker.assertEditable((_a = (before !== null && before !== void 0 ? before : after)) === null || _a === void 0 ? void 0 : _a.revision);
719
+ this.placeInsertion(offset, items, before, after, () => tracker.nextId());
720
+ return;
721
+ }
589
722
  this.splitAt(offset);
590
723
  const segs = this.segments();
591
724
  const after = segs.find((s) => s.start >= offset);
592
725
  const before = [...segs].reverse().find((s) => s.end <= offset);
593
726
  const neighbour = after !== null && after !== void 0 ? after : before;
594
- const list = neighbour ? neighbour.runOwner : ((_a = (_d = this.p).content) !== null && _a !== void 0 ? _a : (_d.content = []));
727
+ const list = neighbour ? neighbour.runOwner : ((_b = (_e = this.p).content) !== null && _b !== void 0 ? _b : (_e.content = []));
595
728
  const at = neighbour ? (after ? neighbour.runIndex : neighbour.runIndex + 1) : (offset === 0 ? 0 : list.length);
596
729
  list.splice(at, 0, ...items);
597
- const owner = (_c = (_b = neighbour === null || neighbour === void 0 ? void 0 : neighbour.run) === null || _b === void 0 ? void 0 : _b.PARENT) !== null && _c !== void 0 ? _c : this.p;
730
+ const owner = (_d = (_c = neighbour === null || neighbour === void 0 ? void 0 : neighbour.run) === null || _c === void 0 ? void 0 : _c.PARENT) !== null && _d !== void 0 ? _d : this.p;
598
731
  for (const item of items)
599
732
  linkParents(item, owner);
600
733
  }
@@ -693,6 +826,10 @@ export class Paragraph {
693
826
  const sdt = sdtOf([this.element], { kind: sdtKindFor(kind), id: nextSdtId(controlIdScope(this.parentBody)), form: 'block' });
694
827
  this.container.splice(at, 1, sdt);
695
828
  linkParents(sdt, (_a = this.p.PARENT) !== null && _a !== void 0 ? _a : this.parentBody.container);
829
+ // under tracking the control itself is the insertion, the paragraph what it was (check 30)
830
+ const tracker = this.changeTracker;
831
+ if (tracker)
832
+ markHolderInserted(tracker, sdt, this.container, () => { });
696
833
  return new ContentControl(sdt, this.container, this.parentBody);
697
834
  }
698
835
  removeEmptyRuns() {
@@ -726,7 +863,7 @@ export class Paragraph {
726
863
  * Returns the `w:del` elements it made, in document order.
727
864
  */
728
865
  deleteText(tracker, start, end) {
729
- var _a, _b, _c, _d, _e, _f;
866
+ var _a, _b, _c, _d, _e;
730
867
  const targets = this.isolate(start, end);
731
868
  for (const seg of targets)
732
869
  tracker.assertEditable(seg.revision);
@@ -772,21 +909,114 @@ export class Paragraph {
772
909
  const del = tracker.del(runs);
773
910
  owner.splice(at, 0, del);
774
911
  linkParents(del, ((_c = (_b = (_a = group[0].revision) === null || _a === void 0 ? void 0 : _a.value) !== null && _b !== void 0 ? _b : group[0].run.value.PARENT) !== null && _c !== void 0 ? _c : this.p));
775
- made.unshift({ owner, element: del, parent: ((_f = (_e = (_d = group[0].revision) === null || _d === void 0 ? void 0 : _d.value) !== null && _e !== void 0 ? _e : del.value.PARENT) !== null && _f !== void 0 ? _f : this.p) });
912
+ const revision = group[0].revision;
913
+ const anchor = { owner, element: del, parent: ((_e = (_d = revision === null || revision === void 0 ? void 0 : revision.value) !== null && _d !== void 0 ? _d : del.value.PARENT) !== null && _e !== void 0 ? _e : this.p) };
914
+ if (revision && revision.items === owner)
915
+ anchor.revision = revision;
916
+ made.unshift(anchor);
776
917
  }
777
918
  return made;
778
919
  }
920
+ /**
921
+ * Splits the revision that holds the text at `at` into two - the second half a copy of its
922
+ * attributes under a new `w:id` - and puts `item` between them. Word splits a revision so when
923
+ * text is typed inside it: around the typist's `w:ins` with tracking on, around plain text with it
924
+ * off (`test/README.md` checks 20 and 21). False, with nothing changed but a run split at `at`,
925
+ * where the text there is not directly in the revision (in a hyperlink inside it, say): the caller
926
+ * then does what it did before.
927
+ */
928
+ insertBetweenHalves(at, item, nextId) {
929
+ this.splitAt(at);
930
+ const after = this.segments().find((s) => s.start >= at && s.revision);
931
+ const holder = after === null || after === void 0 ? void 0 : after.revision;
932
+ const index = holder && after ? holder.items.indexOf(after.runOwner[after.runIndex]) : -1;
933
+ if (!holder || index <= 0)
934
+ return false;
935
+ return this.splitHolderAround(holder, index, item, nextId);
936
+ }
937
+ /**
938
+ * Moves a revision's items from `index` on into a copy of it - its attributes, a new `w:id` - and
939
+ * puts `item` between the two; with nothing from `index` on, `item` just goes after the revision.
940
+ * False, with nothing changed, where the revision's items cannot be told apart from its attributes.
941
+ * Internal: `Range.insertContentControl` uses it to put a new control between the halves of the
942
+ * insertion it wraps part of (check 30).
943
+ */
944
+ splitHolderAround(holder, index, item, nextId) {
945
+ var _a;
946
+ const value = holder.value;
947
+ const itemsKey = Object.keys(value).find((key) => value[key] === holder.items);
948
+ if (!itemsKey)
949
+ return false;
950
+ const parent = (_a = holder.value.PARENT) !== null && _a !== void 0 ? _a : this.p;
951
+ const at = holder.owner.indexOf(holder.element) + 1;
952
+ const items = Array.isArray(item) ? item : [item];
953
+ if (index >= holder.items.length) {
954
+ holder.owner.splice(at, 0, ...items);
955
+ linkParents(items, parent);
956
+ return true;
957
+ }
958
+ const copy = {};
959
+ for (const [key, v] of Object.entries(value))
960
+ if (key !== itemsKey)
961
+ copy[key] = v;
962
+ copy.id = nextId();
963
+ copy[itemsKey] = holder.items.splice(index);
964
+ const second = { name: holder.element.name, value: copy };
965
+ holder.owner.splice(at, 0, ...items, second);
966
+ linkParents(copy[itemsKey], copy);
967
+ linkParents([...items, second], parent);
968
+ return true;
969
+ }
970
+ /**
971
+ * Puts `item` beside a revision rather than in it: after it, or before it - and then before the
972
+ * range start of a move whose range opens just there, so that text typed at the start of moved
973
+ * text is not inside the move (`test/README.md` check 20 case 03).
974
+ */
975
+ insertBesideRevision(holder, item, side) {
976
+ var _a, _b, _c;
977
+ let index = holder.owner.indexOf(holder.element) + (side === 'after' ? 1 : 0);
978
+ if (side === 'before' && (holder.kind === 'moveTo' || holder.kind === 'moveFrom')) {
979
+ const marker = holder.kind === 'moveTo' ? 'moveToRangeStart' : 'moveFromRangeStart';
980
+ 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)
981
+ index--;
982
+ }
983
+ const items = Array.isArray(item) ? item : [item];
984
+ holder.owner.splice(index, 0, ...items);
985
+ linkParents(items, (_c = holder.value.PARENT) !== null && _c !== void 0 ? _c : this.p);
986
+ }
987
+ /**
988
+ * A fresh annotation id for markup this paragraph writes itself: the package's counter when there
989
+ * is a package that keeps one, since a document has one id space; else one above every id in the
990
+ * story.
991
+ */
992
+ /** @internal `Comment` uses it to split an insertion at a comment's end. */
993
+ nextAnnotationId() {
994
+ const pkg = this.parentBody.package_;
995
+ if (pkg && typeof pkg.markupRoots === 'function' && 'annotationIdFloor' in pkg) {
996
+ return new ChangeTracker(pkg, 'TrackAll').nextId();
997
+ }
998
+ let root = this.p;
999
+ for (let o = this.p; o !== undefined; o = o.PARENT)
1000
+ root = o;
1001
+ return highestAnnotationId(root) + 1;
1002
+ }
779
1003
  /**
780
1004
  * Text inserted at `at` as a `w:ins`. A run of this author's own that is already inside a
781
1005
  * `w:ins` is extended rather than nested in another one, as Word does. `anchor` is the `w:del`
782
1006
  * of a replacement, which the insertion must follow.
783
1007
  */
784
- insertTracked(tracker, at, text, anchor) {
785
- var _a, _b, _c, _d, _e;
786
- var _f;
1008
+ insertTracked(tracker, at, text, anchor, prefer = 'before') {
1009
+ var _a, _b, _c;
787
1010
  if (anchor) {
788
1011
  const rPr = copyRPr((_a = runsOf(anchor.element.value, { view: 'original' })[0]) === null || _a === void 0 ? void 0 : _a.value.rPr);
789
1012
  const ins = tracker.ins([runOf([textItem(text)], rPr)]);
1013
+ // A replacement inside another's revision - an insertion, moved text - leaves its w:del in
1014
+ // that revision; the new text is not put in it too, but the revision split after the deletion
1015
+ // and the text between the halves, as for anything typed in it (check 21; the editor's finding
1016
+ // on 0.3.0, CR-002 section 37).
1017
+ const holder = anchor.revision;
1018
+ if (holder && this.splitHolderAround(holder, holder.items.indexOf(anchor.element) + 1, ins, () => tracker.nextId()))
1019
+ return;
790
1020
  anchor.owner.splice(anchor.owner.indexOf(anchor.element) + 1, 0, ins);
791
1021
  linkParents(ins, anchor.parent);
792
1022
  return;
@@ -795,7 +1025,8 @@ export class Paragraph {
795
1025
  const segs = this.segments();
796
1026
  const before = [...segs].reverse().find((s) => s.end <= at);
797
1027
  const after = segs.find((s) => s.start >= at);
798
- for (const [seg, side] of [[before, 'after'], [after, 'before']]) {
1028
+ const sides = [[before, 'after'], [after, 'before']];
1029
+ for (const [seg, side] of prefer === 'after' ? [...sides].reverse() : sides) {
799
1030
  if (!seg || !tracker.ownInsertion(seg.revision))
800
1031
  continue;
801
1032
  if (seg.editable) {
@@ -809,24 +1040,46 @@ export class Paragraph {
809
1040
  }
810
1041
  return;
811
1042
  }
812
- const neighbour = before !== null && before !== void 0 ? before : after;
1043
+ const neighbour = prefer === 'after' ? after !== null && after !== void 0 ? after : before : before !== null && before !== void 0 ? before : after;
813
1044
  tracker.assertEditable(neighbour === null || neighbour === void 0 ? void 0 : neighbour.revision);
814
1045
  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
1046
  const ins = tracker.ins([runOf([textItem(text)], rPr)]);
1047
+ this.placeInsertion(at, [ins], before, after, () => tracker.nextId(), prefer);
1048
+ }
1049
+ /**
1050
+ * An insertion put at `at`, `before` and `after` being the segments either side of it once the runs
1051
+ * are split there: never inside another's revision.
1052
+ */
1053
+ placeInsertion(at, items, before, after, nextId, prefer = 'before') {
1054
+ var _a, _b;
1055
+ var _c;
1056
+ // Typed inside another's revision - a w:ins, a w:moveTo - the revision is split and the typist's
1057
+ // w:ins goes between its halves, as Word writes it (test/README.md check 20 case 01, check 21's
1058
+ // first run); before, it went to the revision's edge, putting the text in the wrong place.
1059
+ 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
1060
+ && this.insertBetweenHalves(at, items, nextId)) {
1061
+ return;
1062
+ }
1063
+ // at the start of a revision, beside it - and outside a move's range that opens there (case 03)
1064
+ if (!before && (after === null || after === void 0 ? void 0 : after.revision)) {
1065
+ this.insertBesideRevision(after.revision, items, 'before');
1066
+ return;
1067
+ }
1068
+ const neighbour = prefer === 'after' ? after !== null && after !== void 0 ? after : before : before !== null && before !== void 0 ? before : after;
816
1069
  if (neighbour) {
817
1070
  const owner = neighbour.revision ? neighbour.revision.owner : neighbour.runOwner;
818
1071
  const item = neighbour.revision ? neighbour.revision.element : neighbour.runOwner[neighbour.runIndex];
819
1072
  const parent = (neighbour.revision ? neighbour.revision.value : neighbour.run);
820
- owner.splice(owner.indexOf(item) + (neighbour === before ? 1 : 0), 0, ins);
821
- linkParents(ins, (_d = parent.PARENT) !== null && _d !== void 0 ? _d : this.p);
1073
+ owner.splice(owner.indexOf(item) + (neighbour === before ? 1 : 0), 0, ...items);
1074
+ linkParents(items, (_a = parent.PARENT) !== null && _a !== void 0 ? _a : this.p);
822
1075
  }
823
1076
  else {
824
- const content = ((_e = (_f = this.p).content) !== null && _e !== void 0 ? _e : (_f.content = []));
1077
+ const content = ((_b = (_c = this.p).content) !== null && _b !== void 0 ? _b : (_c.content = []));
825
1078
  if (at === 0)
826
- content.unshift(ins);
1079
+ content.unshift(...items);
827
1080
  else
828
- content.push(ins);
829
- linkParents(ins, this.p);
1081
+ content.push(...items);
1082
+ linkParents(items, this.p);
830
1083
  }
831
1084
  }
832
1085
  /**
@@ -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;