@docx4j/core-ts 0.3.0 → 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,7 +19,7 @@ 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 { ChangeTracker, copyRPr, markDeleted, toDeletedText, highestAnnotationId, canonical, restorePPr } 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;
@@ -152,10 +152,28 @@ export class Paragraph {
152
152
  const val = v === 'Left' ? 'left' : v === 'Centered' ? 'center' : v === 'Right' ? 'right' : v === 'Justified' ? 'both' : undefined;
153
153
  if (val === undefined)
154
154
  delete pPr.jc;
155
+ else if (this.inheritedAlignment() === v)
156
+ delete pPr.jc;
155
157
  else
156
158
  pPr.jc = { val };
157
159
  this.settleFormatting();
158
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);
176
+ }
159
177
  /** Indents and spacing in points, as Office JS; the effective values (CR-001 Phase B step 2). */
160
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; }
161
179
  set leftIndent(pt) { this.ind().left = Math.round(pt * TWIPS_PER_POINT); this.settleFormatting(); }
@@ -408,6 +426,42 @@ export class Paragraph {
408
426
  return;
409
427
  }
410
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);
411
465
  }
412
466
  /**
413
467
  * The paragraph's content and its mark marked deleted, the paragraph element itself kept:
@@ -533,7 +587,7 @@ export class Paragraph {
533
587
  * is removed, a run left empty is removed. Text is inserted into the w:t at `start`,
534
588
  * else into a new run with the formatting of the nearest run.
535
589
  */
536
- splice(start, end, text) {
590
+ splice(start, end, text, prefer = 'before') {
537
591
  var _a, _b, _c, _d, _e, _f;
538
592
  var _g;
539
593
  const length = this.text.length;
@@ -544,7 +598,7 @@ export class Paragraph {
544
598
  // a replacement is the w:del first and the w:ins after it, as Word writes one
545
599
  const deletions = end > start ? this.deleteText(tracker, start, end) : [];
546
600
  if (text.length > 0)
547
- this.insertTracked(tracker, start, text, deletions[deletions.length - 1]);
601
+ this.insertTracked(tracker, start, text, deletions[deletions.length - 1], prefer);
548
602
  return new Range(this, start, start + text.length);
549
603
  }
550
604
  let segs = this.segments();
@@ -602,8 +656,12 @@ export class Paragraph {
602
656
  }
603
657
  const ending = segs.find((s) => s.editable && s.end === start);
604
658
  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;
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;
607
665
  if (edge === null || edge === void 0 ? void 0 : edge.revision) {
608
666
  const run = runOf([textItem(text)], copyRPr(edge.run.rPr));
609
667
  this.insertBesideRevision(edge.revision, run, edge === ending ? 'after' : 'before');
@@ -618,11 +676,11 @@ export class Paragraph {
618
676
  // no w:t to extend: a new run next to the segment at `start`, with that run's formatting
619
677
  const before = [...segs].reverse().find((s) => s.end <= start);
620
678
  const after = segs.find((s) => s.start >= start);
621
- const neighbour = before !== null && before !== void 0 ? before : after;
679
+ const neighbour = prefer === 'after' ? after !== null && after !== void 0 ? after : before : before !== null && before !== void 0 ? before : after;
622
680
  const firstRun = (_e = runsOf(this.p)[0]) === null || _e === void 0 ? void 0 : _e.value.rPr;
623
681
  const rPr = (neighbour === null || neighbour === void 0 ? void 0 : neighbour.run.rPr) ? deepCopy(neighbour.run.rPr) : firstRun ? deepCopy(firstRun) : undefined;
624
682
  const run = runOf([textItem(text)], rPr);
625
- if (before)
683
+ if (neighbour === before && before)
626
684
  before.runOwner.splice(before.runIndex + 1, 0, run);
627
685
  else if (after)
628
686
  after.runOwner.splice(after.runIndex, 0, run);
@@ -643,19 +701,33 @@ export class Paragraph {
643
701
  * run there when the offset falls inside one. PARENT is linked on what is inserted.
644
702
  */
645
703
  insertItemsAt(offset, items) {
646
- var _a, _b, _c;
647
- var _d;
704
+ var _a, _b, _c, _d;
705
+ var _e;
648
706
  if (items.length === 0)
649
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
+ }
650
722
  this.splitAt(offset);
651
723
  const segs = this.segments();
652
724
  const after = segs.find((s) => s.start >= offset);
653
725
  const before = [...segs].reverse().find((s) => s.end <= offset);
654
726
  const neighbour = after !== null && after !== void 0 ? after : before;
655
- 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 = []));
656
728
  const at = neighbour ? (after ? neighbour.runIndex : neighbour.runIndex + 1) : (offset === 0 ? 0 : list.length);
657
729
  list.splice(at, 0, ...items);
658
- 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;
659
731
  for (const item of items)
660
732
  linkParents(item, owner);
661
733
  }
@@ -754,6 +826,10 @@ export class Paragraph {
754
826
  const sdt = sdtOf([this.element], { kind: sdtKindFor(kind), id: nextSdtId(controlIdScope(this.parentBody)), form: 'block' });
755
827
  this.container.splice(at, 1, sdt);
756
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, () => { });
757
833
  return new ContentControl(sdt, this.container, this.parentBody);
758
834
  }
759
835
  removeEmptyRuns() {
@@ -787,7 +863,7 @@ export class Paragraph {
787
863
  * Returns the `w:del` elements it made, in document order.
788
864
  */
789
865
  deleteText(tracker, start, end) {
790
- var _a, _b, _c, _d, _e, _f;
866
+ var _a, _b, _c, _d, _e;
791
867
  const targets = this.isolate(start, end);
792
868
  for (const seg of targets)
793
869
  tracker.assertEditable(seg.revision);
@@ -833,7 +909,11 @@ export class Paragraph {
833
909
  const del = tracker.del(runs);
834
910
  owner.splice(at, 0, del);
835
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));
836
- 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);
837
917
  }
838
918
  return made;
839
919
  }
@@ -846,17 +926,35 @@ export class Paragraph {
846
926
  * then does what it did before.
847
927
  */
848
928
  insertBetweenHalves(at, item, nextId) {
849
- var _a;
850
929
  this.splitAt(at);
851
930
  const after = this.segments().find((s) => s.start >= at && s.revision);
852
931
  const holder = after === null || after === void 0 ? void 0 : after.revision;
853
932
  const index = holder && after ? holder.items.indexOf(after.runOwner[after.runIndex]) : -1;
854
933
  if (!holder || index <= 0)
855
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;
856
946
  const value = holder.value;
857
947
  const itemsKey = Object.keys(value).find((key) => value[key] === holder.items);
858
948
  if (!itemsKey)
859
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
+ }
860
958
  const copy = {};
861
959
  for (const [key, v] of Object.entries(value))
862
960
  if (key !== itemsKey)
@@ -864,11 +962,9 @@ export class Paragraph {
864
962
  copy.id = nextId();
865
963
  copy[itemsKey] = holder.items.splice(index);
866
964
  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);
965
+ holder.owner.splice(at, 0, ...items, second);
870
966
  linkParents(copy[itemsKey], copy);
871
- linkParents([item, second], parent);
967
+ linkParents([...items, second], parent);
872
968
  return true;
873
969
  }
874
970
  /**
@@ -884,14 +980,16 @@ export class Paragraph {
884
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)
885
981
  index--;
886
982
  }
887
- holder.owner.splice(index, 0, item);
888
- linkParents(item, (_c = holder.value.PARENT) !== null && _c !== void 0 ? _c : this.p);
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);
889
986
  }
890
987
  /**
891
988
  * A fresh annotation id for markup this paragraph writes itself: the package's counter when there
892
989
  * is a package that keeps one, since a document has one id space; else one above every id in the
893
990
  * story.
894
991
  */
992
+ /** @internal `Comment` uses it to split an insertion at a comment's end. */
895
993
  nextAnnotationId() {
896
994
  const pkg = this.parentBody.package_;
897
995
  if (pkg && typeof pkg.markupRoots === 'function' && 'annotationIdFloor' in pkg) {
@@ -907,12 +1005,18 @@ export class Paragraph {
907
1005
  * `w:ins` is extended rather than nested in another one, as Word does. `anchor` is the `w:del`
908
1006
  * of a replacement, which the insertion must follow.
909
1007
  */
910
- insertTracked(tracker, at, text, anchor) {
911
- var _a, _b, _c, _d, _e;
912
- var _f;
1008
+ insertTracked(tracker, at, text, anchor, prefer = 'before') {
1009
+ var _a, _b, _c;
913
1010
  if (anchor) {
914
1011
  const rPr = copyRPr((_a = runsOf(anchor.element.value, { view: 'original' })[0]) === null || _a === void 0 ? void 0 : _a.value.rPr);
915
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;
916
1020
  anchor.owner.splice(anchor.owner.indexOf(anchor.element) + 1, 0, ins);
917
1021
  linkParents(ins, anchor.parent);
918
1022
  return;
@@ -921,7 +1025,8 @@ export class Paragraph {
921
1025
  const segs = this.segments();
922
1026
  const before = [...segs].reverse().find((s) => s.end <= at);
923
1027
  const after = segs.find((s) => s.start >= at);
924
- 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) {
925
1030
  if (!seg || !tracker.ownInsertion(seg.revision))
926
1031
  continue;
927
1032
  if (seg.editable) {
@@ -935,36 +1040,46 @@ export class Paragraph {
935
1040
  }
936
1041
  return;
937
1042
  }
938
- 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;
939
1044
  tracker.assertEditable(neighbour === null || neighbour === void 0 ? void 0 : neighbour.revision);
940
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);
941
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;
942
1056
  // Typed inside another's revision - a w:ins, a w:moveTo - the revision is split and the typist's
943
1057
  // w:ins goes between its halves, as Word writes it (test/README.md check 20 case 01, check 21's
944
1058
  // first run); before, it went to the revision's edge, putting the text in the wrong place.
945
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
946
- && this.insertBetweenHalves(at, ins, () => tracker.nextId())) {
1060
+ && this.insertBetweenHalves(at, items, nextId)) {
947
1061
  return;
948
1062
  }
949
1063
  // at the start of a revision, beside it - and outside a move's range that opens there (case 03)
950
1064
  if (!before && (after === null || after === void 0 ? void 0 : after.revision)) {
951
- this.insertBesideRevision(after.revision, ins, 'before');
1065
+ this.insertBesideRevision(after.revision, items, 'before');
952
1066
  return;
953
1067
  }
1068
+ const neighbour = prefer === 'after' ? after !== null && after !== void 0 ? after : before : before !== null && before !== void 0 ? before : after;
954
1069
  if (neighbour) {
955
1070
  const owner = neighbour.revision ? neighbour.revision.owner : neighbour.runOwner;
956
1071
  const item = neighbour.revision ? neighbour.revision.element : neighbour.runOwner[neighbour.runIndex];
957
1072
  const parent = (neighbour.revision ? neighbour.revision.value : neighbour.run);
958
- owner.splice(owner.indexOf(item) + (neighbour === before ? 1 : 0), 0, ins);
959
- 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);
960
1075
  }
961
1076
  else {
962
- 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 = []));
963
1078
  if (at === 0)
964
- content.unshift(ins);
1079
+ content.unshift(...items);
965
1080
  else
966
- content.push(ins);
967
- linkParents(ins, this.p);
1081
+ content.push(...items);
1082
+ linkParents(items, this.p);
968
1083
  }
969
1084
  }
970
1085
  /**
@@ -8,6 +8,7 @@ import { hyperlink as hyperlinkOf } from '@docx4j/generated-objects-ts/el/org_do
8
8
  import { Namespaces } from '../../parts/Namespaces.mjs';
9
9
  import { controlIdScope, sdtKindFor } from '../customxml/insert.mjs';
10
10
  import { contentOf } from './ooxml.mjs';
11
+ import { markHolderInserted } from './tracking.mjs';
11
12
  import { searchPattern, findAll, matchesOf, expandReplacement } from './search.mjs';
12
13
  import { commentApi } from './comments.mjs';
13
14
  /**
@@ -93,8 +94,10 @@ export class Range {
93
94
  let r;
94
95
  if (location === 'Replace')
95
96
  r = this.paragraph.splice(this.start, this.end, text);
97
+ // text put before the range joins the run the range starts in - inside the link the range is on,
98
+ // as Office JS (check 32: insertText('Before') on a link's text went into the link, as 'After' did)
96
99
  else if (location === 'Before' || location === 'Start')
97
- r = this.paragraph.splice(this.start, this.start, text);
100
+ r = this.paragraph.splice(this.start, this.start, text, 'after');
98
101
  else
99
102
  r = this.paragraph.splice(this.end, this.end, text);
100
103
  // this range keeps covering the same text: a replacement resizes it, text before it shifts it, text after it leaves it
@@ -206,7 +209,7 @@ export class Range {
206
209
  * span gets an empty control at its position. Returns the control.
207
210
  */
208
211
  insertContentControl(kind) {
209
- var _a;
212
+ var _a, _b;
210
213
  const paragraph = this.paragraph;
211
214
  const body = paragraph.parentBody;
212
215
  // The control is built empty first: `sdt` refuses a kind that cannot be run-level (a repeating
@@ -232,9 +235,49 @@ export class Range {
232
235
  items.push(element);
233
236
  }
234
237
  const at = owner.indexOf(items[0]);
238
+ const tracker = paragraph.changeTracker;
239
+ const revision = segments[0].revision;
240
+ if (tracker && revision) {
241
+ // The runs are in a revision: the control goes between its halves, not in it, and the runs go
242
+ // into the control in a revision of the same kind and attributes with a new id - Word's form for
243
+ // inserted text wrapped in a new control (check 30, inserted-text-wrapped: the w:ins inside the
244
+ // control, the control's own markers outside it).
245
+ const index = revision.items.indexOf(items[0]);
246
+ revision.items.splice(index, items.length);
247
+ const value = revision.value;
248
+ const itemsKey = Object.keys(value).find((key) => value[key] === revision.items);
249
+ const inner = {};
250
+ for (const [key, v] of Object.entries(value))
251
+ if (key !== itemsKey && key !== 'PARENT')
252
+ inner[key] = v;
253
+ inner.id = tracker.nextId();
254
+ inner[itemsKey] = items;
255
+ const wrapped = { name: revision.element.name, value: inner };
256
+ linkParents(items, inner);
257
+ sdt.value.sdtContent.content = [wrapped];
258
+ linkParents(wrapped, sdt.value.sdtContent);
259
+ const holderOwner = revision.owner;
260
+ const parent = (_a = revision.value.PARENT) !== null && _a !== void 0 ? _a : paragraph.p;
261
+ if (revision.items.length === 0) {
262
+ holderOwner.splice(holderOwner.indexOf(revision.element), 1, sdt); // the whole revision wrapped
263
+ linkParents(sdt, parent);
264
+ }
265
+ else if (index === 0) {
266
+ holderOwner.splice(holderOwner.indexOf(revision.element), 0, sdt); // its start wrapped
267
+ linkParents(sdt, parent);
268
+ }
269
+ else if (!paragraph.splitHolderAround(revision, index, sdt, () => tracker.nextId())) {
270
+ throw new Docx4JException('This range is in a revision that cannot be split around a content control');
271
+ }
272
+ markHolderInserted(tracker, sdt, holderOwner, () => { });
273
+ return new ContentControl(sdt, holderOwner, body);
274
+ }
235
275
  sdt.value.sdtContent.content = items;
236
276
  owner.splice(at, items.length, sdt);
237
- linkParents(sdt, (_a = segments[0].run.PARENT) !== null && _a !== void 0 ? _a : paragraph.p);
277
+ linkParents(sdt, (_b = segments[0].run.PARENT) !== null && _b !== void 0 ? _b : paragraph.p);
278
+ // under tracking the control itself is the insertion, the runs in it what they were (check 30)
279
+ if (tracker)
280
+ markHolderInserted(tracker, sdt, owner, () => { });
238
281
  return new ContentControl(sdt, owner, body);
239
282
  }
240
283
  /**
@@ -3,7 +3,7 @@ import { tr as rowOf } from '@docx4j/generated-objects-ts/builders/wml';
3
3
  import { Docx4JException } from '../../opc/exceptions.mjs';
4
4
  import { typeNameOf, childrenOf, linkParents, textOf, rowsOf, cellsOf } from './tree.mjs';
5
5
  import { builtInOf, idOfBuiltIn, styleNameOf, styleIdOf } from './styles.mjs';
6
- import { trackInsertedParagraph } from './tracking.mjs';
6
+ import { trackInsertedBlocks } from './tracking.mjs';
7
7
  /** The default table width in twips (A4 with 2.54 cm margins), as the objects package's `tbl` builder uses. */
8
8
  const DEFAULT_WIDTH = 9026;
9
9
  const TWIPS_PER_POINT = 20;
@@ -418,12 +418,10 @@ function markRowsInserted(tracker, rows) {
418
418
  if (!tracker)
419
419
  return;
420
420
  for (const row of rows) {
421
- tracker.markRowInserted(row.value);
422
- for (const cell of cellsOf(row.value)) {
423
- for (const block of (_a = childrenOf(cell.element.value)) !== null && _a !== void 0 ? _a : []) {
424
- if (typeNameOf(block) === 'org_docx4j_wml.P')
425
- trackInsertedParagraph(tracker, block.value);
426
- }
427
- }
421
+ if (!((_a = row.value.trPr) === null || _a === void 0 ? void 0 : _a.ins))
422
+ tracker.markRowInserted(row.value);
423
+ // nested tables and block controls in a new row's cells too (CR-002 section 37)
424
+ for (const cell of cellsOf(row.value))
425
+ trackInsertedBlocks(tracker, childrenOf(cell.element.value));
428
426
  }
429
427
  }
@@ -1,5 +1,5 @@
1
1
  import type * as wml from '@docx4j/generated-objects-ts/modules/org_docx4j_wml';
2
- import { type Element, type RevisionKind } from './tree.mjs';
2
+ import { type Element, type Located, type RevisionKind } from './tree.mjs';
3
3
  import { Range } from './Range.mjs';
4
4
  import type { Paragraph } from './Paragraph.mjs';
5
5
  /** Office JS `Word.ChangeTrackingState` (`Unknown` is not produced here). */
@@ -35,7 +35,8 @@ export type TrackedChangeTarget =
35
35
  * `w:trPr/w:ins` or `w:trPr/w:del`: a table row. `inner` is the revision markup the row's own
36
36
  * cells carry (the `w:ins` or `w:del` around their runs and on their paragraph marks), which
37
37
  * `Body.getTrackedChanges` reports through the row rather than separately, as Office JS does:
38
- * one change per row. Accepting or rejecting the row applies them too.
38
+ * one change per row. Accepting or rejecting the row applies them too. `table` is the table the
39
+ * row is in, which goes with its last row.
39
40
  */
40
41
  | {
41
42
  kind: 'row';
@@ -44,6 +45,7 @@ export type TrackedChangeTarget =
44
45
  owner: Element[];
45
46
  value: wml.CTTrackChange;
46
47
  inner: TrackedChange[];
48
+ table?: Located<wml.Tbl>;
47
49
  }
48
50
  /**
49
51
  * Touching insertions, deletions or formatting changes by one author - `w:ins` or `w:del` around
@@ -92,6 +94,24 @@ export type TrackedChangeTarget =
92
94
  sectPr: wml.SectPr;
93
95
  value: wml.CTSectPrChange;
94
96
  text: string;
97
+ }
98
+ /**
99
+ * A content control or custom XML element that is itself an insertion: `w:customXmlInsRangeStart`
100
+ * and `w:customXmlInsRangeEnd` around it, the end first inside its content, and a second pair the
101
+ * other way round at its end - Word's form for a control put in under tracking (check 30, CR-002
102
+ * section 37 item 7), which groups with the insertions around and inside it. `holder` is the
103
+ * `w:sdt` or `w:customXml`, `owner` the list holding it, `outer` the pair in that list and `inner`
104
+ * the pair in its content. Rejecting takes the element away and leaves its content where it
105
+ * stood; accepting takes the markers away. Its text is its content's, which the pieces inside
106
+ * report.
107
+ */
108
+ | {
109
+ kind: 'holder';
110
+ holder: Element;
111
+ owner: Element[];
112
+ outer: [Element, Element];
113
+ inner: [Element, Element];
114
+ value: wml.CTTrackChange;
95
115
  };
96
116
  /**
97
117
  * A subset of Office JS `Word.TrackedChange`: `type`, `author`, `date`, `text`, `accept()`,
@@ -134,6 +154,13 @@ export declare class TrackedChange {
134
154
  reject(): void;
135
155
  private requireParagraph;
136
156
  }
157
+ /**
158
+ * The insertion of a content control or custom XML element, when `items[i]` is a
159
+ * `w:customXmlInsRangeStart` and what follows is Word's form for one (check 30): the element, whose
160
+ * content starts with that marker's `w:customXmlInsRangeEnd` and ends with a second start, and that
161
+ * one's end right after the element. Anything else is not one (a marker on its own is left alone).
162
+ */
163
+ export declare function holderInsertionAt(items: Element[], i: number, paragraph: Paragraph | undefined): TrackedChange | undefined;
137
164
  /**
138
165
  * docx4j AcceptTrackedChanges: the paragraph takes the next one's content and properties, so the
139
166
  * mark that survives is the next one's. Called when a deleted mark is accepted and when an
@@ -173,7 +200,7 @@ export declare function groupTouching(tokens: TrackedChangeToken[]): TrackedChan
173
200
  * The `w:trPr/w:ins` and `w:trPr/w:del` revisions of a table row. `inner` is the revision markup
174
201
  * of the row's own cells, which the row's change owns rather than reporting separately.
175
202
  */
176
- export declare function trackedChangesOfRow(tr: Element<wml.Tr>, owner: Element[], inner?: TrackedChange[]): TrackedChange[];
203
+ export declare function trackedChangesOfRow(tr: Element<wml.Tr>, owner: Element[], inner?: TrackedChange[], table?: Located<wml.Tbl>): TrackedChange[];
177
204
  /** The change a table's property records make, one per table, or undefined when they make none. */
178
205
  export declare function tablePropertiesChangeOf(tbl: Element<wml.Tbl>): TrackedChange | undefined;
179
206
  /** A cell's own revision, `w:cellIns` or `w:cellDel`, or undefined. */
@@ -188,6 +215,7 @@ export declare function bodySectionChangeOf(container: {
188
215
  * cannot list (check 27: `getTrackedChanges` throws); a `w:numberingChange`, which Word no longer
189
216
  * writes and drops when one is put in, and whose record is the old number's text, so that a reject
190
217
  * 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).
218
+ * recorded back (docx4j's reading; unmeasured). A `w:customXmlInsRangeStart` or `End` left without
219
+ * the form `holderInsertionAt` lists marks nothing that can be found, and goes either way.
192
220
  */
193
221
  export declare function resolveUnlisted(items: Element[], what: 'accept' | 'reject'): void;