@docx4j/core-ts 0.3.0 → 0.3.2

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.
Files changed (42) hide show
  1. package/dist/model/content/Body.mjs +40 -19
  2. package/dist/model/content/Comment.d.mts +6 -0
  3. package/dist/model/content/Comment.mjs +76 -22
  4. package/dist/model/content/ContentControl.d.mts +11 -1
  5. package/dist/model/content/ContentControl.mjs +69 -10
  6. package/dist/model/content/Paragraph.d.mts +37 -2
  7. package/dist/model/content/Paragraph.mjs +140 -38
  8. package/dist/model/content/Range.mjs +49 -5
  9. package/dist/model/content/Table.mjs +6 -8
  10. package/dist/model/content/TrackedChange.d.mts +32 -4
  11. package/dist/model/content/TrackedChange.mjs +271 -34
  12. package/dist/model/content/index.d.mts +1 -1
  13. package/dist/model/content/index.mjs +1 -1
  14. package/dist/model/content/stylesFacade.mjs +9 -3
  15. package/dist/model/content/tracking.d.mts +65 -5
  16. package/dist/model/content/tracking.mjs +170 -43
  17. package/dist/model/customxml/XmlMapping.d.mts +20 -4
  18. package/dist/model/customxml/XmlMapping.mjs +44 -6
  19. package/dist/model/customxml/bindings.d.mts +9 -2
  20. package/dist/model/customxml/bindings.mjs +18 -2
  21. package/dist/model/customxml/index.d.mts +1 -1
  22. package/dist/model/customxml/index.mjs +1 -1
  23. package/dist/model/customxml/insert.d.mts +10 -0
  24. package/dist/model/customxml/insert.mjs +13 -0
  25. package/dist/model/fonts/RunFontSelector.d.mts +8 -1
  26. package/dist/model/fonts/RunFontSelector.mjs +6 -5
  27. package/dist/model/properties/PropertyResolver.d.mts +110 -10
  28. package/dist/model/properties/PropertyResolver.mjs +300 -31
  29. package/dist/model/properties/index.d.mts +3 -0
  30. package/dist/model/properties/index.mjs +2 -0
  31. package/dist/model/properties/table.d.mts +155 -0
  32. package/dist/model/properties/table.mjs +315 -0
  33. package/dist/model/properties/tableStyleConditions.d.mts +101 -0
  34. package/dist/model/properties/tableStyleConditions.mjs +296 -0
  35. package/dist/packages/WordprocessingMLPackage.d.mts +6 -0
  36. package/dist/packages/WordprocessingMLPackage.mjs +11 -0
  37. package/dist/parts/wml/commentStyles.d.mts +3 -0
  38. package/dist/parts/wml/commentStyles.mjs +25 -0
  39. package/dist/parts/wml/comments.mjs +4 -22
  40. package/dist/parts/wml/placeholderStyle.d.mts +3 -0
  41. package/dist/parts/wml/placeholderStyle.mjs +13 -0
  42. package/package.json +2 -2
@@ -15,11 +15,11 @@ import { builtInOf, idOfBuiltIn, styleNameOf, styleIdOf } from './styles.mjs';
15
15
  import { cellOf } from './Table.mjs';
16
16
  import { ContentControl, collectRunControls } from './ContentControl.mjs';
17
17
  import { sdt as sdtOf, nextSdtId } from '@docx4j/generated-objects-ts/builders/wml';
18
- import { controlIdScope, sdtKindFor } from '../customxml/insert.mjs';
18
+ import { controlIdScope, sdtKindFor, declareSdtExtensions } 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, pruneParagraphProperties } 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;
@@ -125,11 +125,23 @@ export class Paragraph {
125
125
  * changing it. Needs the package's resolver; see `Body.propertyResolver`.
126
126
  */
127
127
  get effectivePPr() {
128
- return this.parentBody.propertyResolver.getEffectivePPr(this.p.pPr);
128
+ return this.parentBody.propertyResolver.getEffectivePPr(this.p.pPr, this.cellContext);
129
129
  }
130
130
  /** The paragraph mark's effective run properties (docx4j `getEffectiveParagraphMarkRPr`). */
131
131
  get effectiveParagraphMarkRPr() {
132
- return this.parentBody.propertyResolver.getEffectiveParagraphMarkRPr(this.p.pPr);
132
+ return this.parentBody.propertyResolver.getEffectiveParagraphMarkRPr(this.p.pPr, this.cellContext);
133
+ }
134
+ /**
135
+ * Where this paragraph sits in a table, as far as its formatting goes (docx4j
136
+ * `PropertyResolver.cellContextOf`; CR-007): the table style its table resolves to and the
137
+ * conditional formats it is under. Undefined outside a table, and in a text box, note or comment
138
+ * reached from a cell. Every effective read of the paragraph and of its runs passes it, so a run
139
+ * in a header row the table style makes bold reads bold. Each read walks the table's rows; code
140
+ * resolving every paragraph of a large table should walk with a `TableContextTracker` instead.
141
+ * Needs the package's resolver, as the effective reads do.
142
+ */
143
+ get cellContext() {
144
+ return this.parentBody.propertyResolver.cellContextOf(this.p);
133
145
  }
134
146
  /** The `w:pPr` a formatting read works from: the resolved one, or this paragraph's own. */
135
147
  readPPr(options) {
@@ -152,9 +164,29 @@ export class Paragraph {
152
164
  const val = v === 'Left' ? 'left' : v === 'Centered' ? 'center' : v === 'Right' ? 'right' : v === 'Justified' ? 'both' : undefined;
153
165
  if (val === undefined)
154
166
  delete pPr.jc;
167
+ else if (this.inheritedAlignment() === v)
168
+ delete pPr.jc;
155
169
  else
156
170
  pPr.jc = { val };
157
171
  this.settleFormatting();
172
+ pruneParagraphProperties(this.p); // no empty w:pPr left (the editor's finding on 0.3.1)
173
+ }
174
+ /**
175
+ * The alignment the paragraph resolves to with no `w:jc` of its own: its style's, else the
176
+ * defaults'. Word's Ctrl+L on a centred paragraph whose style is left-aligned removes the `w:jc`
177
+ * rather than writing `w:jc="left"`, with tracking on (check 26) and off (the editor's
178
+ * `measure-word2010.docx` and `measure-word15.docx`, 2026-09-30; CR-002 section 38), so a value the
179
+ * paragraph already inherits is not written. Measured for left over a left-aligned style; taken to
180
+ * hold for any value. Undefined without a resolver, where the value is written as asked.
181
+ */
182
+ inheritedAlignment() {
183
+ var _a;
184
+ const resolver = (_a = this.parentBody.package_) === null || _a === void 0 ? void 0 : _a.propertyResolverOrUndefined;
185
+ if (!resolver)
186
+ return undefined;
187
+ const { jc: _jc, pPrChange: _change, PARENT: _parent, ...rest } = this.p.pPr;
188
+ // in a table cell what is inherited includes the table style's (CR-007)
189
+ return alignmentOf(resolver.getEffectivePPr(rest, resolver.cellContextOf(this.p)), true);
158
190
  }
159
191
  /** Indents and spacing in points, as Office JS; the effective values (CR-001 Phase B step 2). */
160
192
  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; }
@@ -204,7 +236,7 @@ export class Paragraph {
204
236
  const holders = () => runsOf(this.p).map((r) => r.value);
205
237
  if ((options === null || options === void 0 ? void 0 : options.direct) === true)
206
238
  return new Font(holders, () => this.fontTracking());
207
- return new Font(holders, () => this.fontTracking(), (rPr) => this.parentBody.propertyResolver.getEffectiveRPr(rPr, this.p.pPr), (rPr) => { var _a; return (_a = runFontSelectorOf(this.parentBody.package_)) === null || _a === void 0 ? void 0 : _a.asciiFontName(rPr); });
239
+ return new Font(holders, () => this.fontTracking(), (rPr) => this.parentBody.propertyResolver.getEffectiveRPr(rPr, this.p.pPr, this.cellContext), (rPr) => { var _a; return (_a = runFontSelectorOf(this.parentBody.package_)) === null || _a === void 0 ? void 0 : _a.asciiFontName(rPr); });
208
240
  }
209
241
  /** w14:paraId, the stable address Word gives paragraphs. */
210
242
  get paraId() {
@@ -408,6 +440,13 @@ export class Paragraph {
408
440
  return;
409
441
  }
410
442
  tracker.markParagraphDeleted(this.p);
443
+ // Word, deleting a paragraph mark ALONE under tracking, gives the next paragraph this one's
444
+ // properties as a w:pPrChange (CR-002 section 38 item 2, the editor's measure-word15.docx), so
445
+ // that the joined line reads in them. This method deletes the whole paragraph, text and mark - the
446
+ // engine's only mark deletion, a Range being within one paragraph - and what Word writes for that
447
+ // is not measured (test/README.md check 33). 0.3.1 carried the properties here too, which turned
448
+ // an agent's new heading, put in after a paragraph it then deleted, into body text once accepted
449
+ // (the editor's finding on 0.3.1); until the check, nothing is carried.
411
450
  }
412
451
  /**
413
452
  * The paragraph's content and its mark marked deleted, the paragraph element itself kept:
@@ -479,8 +518,9 @@ export class Paragraph {
479
518
  if (!same) {
480
519
  const rest = (props) => canonical(Object.fromEntries(Object.entries(props).filter(([key]) => !FORMATTING_MEMBERS.has(key))));
481
520
  const resolver = (_b = this.parentBody.package_) === null || _b === void 0 ? void 0 : _b.propertyResolverOrUndefined;
521
+ const cell = resolver === null || resolver === void 0 ? void 0 : resolver.cellContextOf(this.p);
482
522
  same = resolver !== undefined && rest(current) === rest(before)
483
- && canonical(formattingOf(resolver.getEffectivePPr(current), true)) === canonical(formattingOf(resolver.getEffectivePPr(before), true));
523
+ && canonical(formattingOf(resolver.getEffectivePPr(current, cell), true)) === canonical(formattingOf(resolver.getEffectivePPr(before, cell), true));
484
524
  }
485
525
  if (same)
486
526
  restorePPr(this.p, recorded);
@@ -533,7 +573,7 @@ export class Paragraph {
533
573
  * is removed, a run left empty is removed. Text is inserted into the w:t at `start`,
534
574
  * else into a new run with the formatting of the nearest run.
535
575
  */
536
- splice(start, end, text) {
576
+ splice(start, end, text, prefer = 'before') {
537
577
  var _a, _b, _c, _d, _e, _f;
538
578
  var _g;
539
579
  const length = this.text.length;
@@ -544,7 +584,7 @@ export class Paragraph {
544
584
  // a replacement is the w:del first and the w:ins after it, as Word writes one
545
585
  const deletions = end > start ? this.deleteText(tracker, start, end) : [];
546
586
  if (text.length > 0)
547
- this.insertTracked(tracker, start, text, deletions[deletions.length - 1]);
587
+ this.insertTracked(tracker, start, text, deletions[deletions.length - 1], prefer);
548
588
  return new Range(this, start, start + text.length);
549
589
  }
550
590
  let segs = this.segments();
@@ -602,8 +642,12 @@ export class Paragraph {
602
642
  }
603
643
  const ending = segs.find((s) => s.editable && s.end === start);
604
644
  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;
645
+ // `prefer` says which run takes text put exactly between two: the one ending there (typed text,
646
+ // `Paragraph.insertText`) or the one starting there (`Range.insertText('Before')`, which Office
647
+ // JS puts inside the link the range is on: check 32)
648
+ const [near, far] = prefer === 'after' ? [starting, ending] : [ending, starting];
649
+ 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
650
+ const edge = target ? undefined : near !== null && near !== void 0 ? near : far;
607
651
  if (edge === null || edge === void 0 ? void 0 : edge.revision) {
608
652
  const run = runOf([textItem(text)], copyRPr(edge.run.rPr));
609
653
  this.insertBesideRevision(edge.revision, run, edge === ending ? 'after' : 'before');
@@ -618,11 +662,11 @@ export class Paragraph {
618
662
  // no w:t to extend: a new run next to the segment at `start`, with that run's formatting
619
663
  const before = [...segs].reverse().find((s) => s.end <= start);
620
664
  const after = segs.find((s) => s.start >= start);
621
- const neighbour = before !== null && before !== void 0 ? before : after;
665
+ const neighbour = prefer === 'after' ? after !== null && after !== void 0 ? after : before : before !== null && before !== void 0 ? before : after;
622
666
  const firstRun = (_e = runsOf(this.p)[0]) === null || _e === void 0 ? void 0 : _e.value.rPr;
623
667
  const rPr = (neighbour === null || neighbour === void 0 ? void 0 : neighbour.run.rPr) ? deepCopy(neighbour.run.rPr) : firstRun ? deepCopy(firstRun) : undefined;
624
668
  const run = runOf([textItem(text)], rPr);
625
- if (before)
669
+ if (neighbour === before && before)
626
670
  before.runOwner.splice(before.runIndex + 1, 0, run);
627
671
  else if (after)
628
672
  after.runOwner.splice(after.runIndex, 0, run);
@@ -643,19 +687,33 @@ export class Paragraph {
643
687
  * run there when the offset falls inside one. PARENT is linked on what is inserted.
644
688
  */
645
689
  insertItemsAt(offset, items) {
646
- var _a, _b, _c;
647
- var _d;
690
+ var _a, _b, _c, _d;
691
+ var _e;
648
692
  if (items.length === 0)
649
693
  return;
694
+ const tracker = this.changeTracker;
695
+ if (tracker) {
696
+ // With tracking on, what goes in is an insertion: its runs in w:ins, in a link, a control or a
697
+ // field too, placed as typed text is - beside another's revision, or between its halves, never
698
+ // in it. Before, `insertOoxml` into a paragraph went in untracked (CR-002 section 37).
699
+ wrapInsertedRuns(tracker, items, this.p);
700
+ this.splitAt(offset);
701
+ const segs = this.segments();
702
+ const before = [...segs].reverse().find((s) => s.end <= offset);
703
+ const after = segs.find((s) => s.start >= offset);
704
+ tracker.assertEditable((_a = (before !== null && before !== void 0 ? before : after)) === null || _a === void 0 ? void 0 : _a.revision);
705
+ this.placeInsertion(offset, items, before, after, () => tracker.nextId());
706
+ return;
707
+ }
650
708
  this.splitAt(offset);
651
709
  const segs = this.segments();
652
710
  const after = segs.find((s) => s.start >= offset);
653
711
  const before = [...segs].reverse().find((s) => s.end <= offset);
654
712
  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 = []));
713
+ const list = neighbour ? neighbour.runOwner : ((_b = (_e = this.p).content) !== null && _b !== void 0 ? _b : (_e.content = []));
656
714
  const at = neighbour ? (after ? neighbour.runIndex : neighbour.runIndex + 1) : (offset === 0 ? 0 : list.length);
657
715
  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;
716
+ 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
717
  for (const item of items)
660
718
  linkParents(item, owner);
661
719
  }
@@ -754,6 +812,11 @@ export class Paragraph {
754
812
  const sdt = sdtOf([this.element], { kind: sdtKindFor(kind), id: nextSdtId(controlIdScope(this.parentBody)), form: 'block' });
755
813
  this.container.splice(at, 1, sdt);
756
814
  linkParents(sdt, (_a = this.p.PARENT) !== null && _a !== void 0 ? _a : this.parentBody.container);
815
+ declareSdtExtensions(this.parentBody, sdt);
816
+ // under tracking the control itself is the insertion, the paragraph what it was (check 30)
817
+ const tracker = this.changeTracker;
818
+ if (tracker)
819
+ markHolderInserted(tracker, sdt, this.container, () => { });
757
820
  return new ContentControl(sdt, this.container, this.parentBody);
758
821
  }
759
822
  removeEmptyRuns() {
@@ -787,7 +850,7 @@ export class Paragraph {
787
850
  * Returns the `w:del` elements it made, in document order.
788
851
  */
789
852
  deleteText(tracker, start, end) {
790
- var _a, _b, _c, _d, _e, _f;
853
+ var _a, _b, _c, _d, _e;
791
854
  const targets = this.isolate(start, end);
792
855
  for (const seg of targets)
793
856
  tracker.assertEditable(seg.revision);
@@ -833,7 +896,11 @@ export class Paragraph {
833
896
  const del = tracker.del(runs);
834
897
  owner.splice(at, 0, del);
835
898
  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) });
899
+ const revision = group[0].revision;
900
+ 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) };
901
+ if (revision && revision.items === owner)
902
+ anchor.revision = revision;
903
+ made.unshift(anchor);
837
904
  }
838
905
  return made;
839
906
  }
@@ -846,17 +913,35 @@ export class Paragraph {
846
913
  * then does what it did before.
847
914
  */
848
915
  insertBetweenHalves(at, item, nextId) {
849
- var _a;
850
916
  this.splitAt(at);
851
917
  const after = this.segments().find((s) => s.start >= at && s.revision);
852
918
  const holder = after === null || after === void 0 ? void 0 : after.revision;
853
919
  const index = holder && after ? holder.items.indexOf(after.runOwner[after.runIndex]) : -1;
854
920
  if (!holder || index <= 0)
855
921
  return false;
922
+ return this.splitHolderAround(holder, index, item, nextId);
923
+ }
924
+ /**
925
+ * Moves a revision's items from `index` on into a copy of it - its attributes, a new `w:id` - and
926
+ * puts `item` between the two; with nothing from `index` on, `item` just goes after the revision.
927
+ * False, with nothing changed, where the revision's items cannot be told apart from its attributes.
928
+ * Internal: `Range.insertContentControl` uses it to put a new control between the halves of the
929
+ * insertion it wraps part of (check 30).
930
+ */
931
+ splitHolderAround(holder, index, item, nextId) {
932
+ var _a;
856
933
  const value = holder.value;
857
934
  const itemsKey = Object.keys(value).find((key) => value[key] === holder.items);
858
935
  if (!itemsKey)
859
936
  return false;
937
+ const parent = (_a = holder.value.PARENT) !== null && _a !== void 0 ? _a : this.p;
938
+ const at = holder.owner.indexOf(holder.element) + 1;
939
+ const items = Array.isArray(item) ? item : [item];
940
+ if (index >= holder.items.length) {
941
+ holder.owner.splice(at, 0, ...items);
942
+ linkParents(items, parent);
943
+ return true;
944
+ }
860
945
  const copy = {};
861
946
  for (const [key, v] of Object.entries(value))
862
947
  if (key !== itemsKey)
@@ -864,11 +949,9 @@ export class Paragraph {
864
949
  copy.id = nextId();
865
950
  copy[itemsKey] = holder.items.splice(index);
866
951
  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);
952
+ holder.owner.splice(at, 0, ...items, second);
870
953
  linkParents(copy[itemsKey], copy);
871
- linkParents([item, second], parent);
954
+ linkParents([...items, second], parent);
872
955
  return true;
873
956
  }
874
957
  /**
@@ -884,14 +967,16 @@ export class Paragraph {
884
967
  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
968
  index--;
886
969
  }
887
- holder.owner.splice(index, 0, item);
888
- linkParents(item, (_c = holder.value.PARENT) !== null && _c !== void 0 ? _c : this.p);
970
+ const items = Array.isArray(item) ? item : [item];
971
+ holder.owner.splice(index, 0, ...items);
972
+ linkParents(items, (_c = holder.value.PARENT) !== null && _c !== void 0 ? _c : this.p);
889
973
  }
890
974
  /**
891
975
  * A fresh annotation id for markup this paragraph writes itself: the package's counter when there
892
976
  * is a package that keeps one, since a document has one id space; else one above every id in the
893
977
  * story.
894
978
  */
979
+ /** @internal `Comment` uses it to split an insertion at a comment's end. */
895
980
  nextAnnotationId() {
896
981
  const pkg = this.parentBody.package_;
897
982
  if (pkg && typeof pkg.markupRoots === 'function' && 'annotationIdFloor' in pkg) {
@@ -907,12 +992,18 @@ export class Paragraph {
907
992
  * `w:ins` is extended rather than nested in another one, as Word does. `anchor` is the `w:del`
908
993
  * of a replacement, which the insertion must follow.
909
994
  */
910
- insertTracked(tracker, at, text, anchor) {
911
- var _a, _b, _c, _d, _e;
912
- var _f;
995
+ insertTracked(tracker, at, text, anchor, prefer = 'before') {
996
+ var _a, _b, _c;
913
997
  if (anchor) {
914
998
  const rPr = copyRPr((_a = runsOf(anchor.element.value, { view: 'original' })[0]) === null || _a === void 0 ? void 0 : _a.value.rPr);
915
999
  const ins = tracker.ins([runOf([textItem(text)], rPr)]);
1000
+ // A replacement inside another's revision - an insertion, moved text - leaves its w:del in
1001
+ // that revision; the new text is not put in it too, but the revision split after the deletion
1002
+ // and the text between the halves, as for anything typed in it (check 21; the editor's finding
1003
+ // on 0.3.0, CR-002 section 37).
1004
+ const holder = anchor.revision;
1005
+ if (holder && this.splitHolderAround(holder, holder.items.indexOf(anchor.element) + 1, ins, () => tracker.nextId()))
1006
+ return;
916
1007
  anchor.owner.splice(anchor.owner.indexOf(anchor.element) + 1, 0, ins);
917
1008
  linkParents(ins, anchor.parent);
918
1009
  return;
@@ -921,7 +1012,8 @@ export class Paragraph {
921
1012
  const segs = this.segments();
922
1013
  const before = [...segs].reverse().find((s) => s.end <= at);
923
1014
  const after = segs.find((s) => s.start >= at);
924
- for (const [seg, side] of [[before, 'after'], [after, 'before']]) {
1015
+ const sides = [[before, 'after'], [after, 'before']];
1016
+ for (const [seg, side] of prefer === 'after' ? [...sides].reverse() : sides) {
925
1017
  if (!seg || !tracker.ownInsertion(seg.revision))
926
1018
  continue;
927
1019
  if (seg.editable) {
@@ -935,36 +1027,46 @@ export class Paragraph {
935
1027
  }
936
1028
  return;
937
1029
  }
938
- const neighbour = before !== null && before !== void 0 ? before : after;
1030
+ const neighbour = prefer === 'after' ? after !== null && after !== void 0 ? after : before : before !== null && before !== void 0 ? before : after;
939
1031
  tracker.assertEditable(neighbour === null || neighbour === void 0 ? void 0 : neighbour.revision);
940
1032
  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
1033
  const ins = tracker.ins([runOf([textItem(text)], rPr)]);
1034
+ this.placeInsertion(at, [ins], before, after, () => tracker.nextId(), prefer);
1035
+ }
1036
+ /**
1037
+ * An insertion put at `at`, `before` and `after` being the segments either side of it once the runs
1038
+ * are split there: never inside another's revision.
1039
+ */
1040
+ placeInsertion(at, items, before, after, nextId, prefer = 'before') {
1041
+ var _a, _b;
1042
+ var _c;
942
1043
  // Typed inside another's revision - a w:ins, a w:moveTo - the revision is split and the typist's
943
1044
  // w:ins goes between its halves, as Word writes it (test/README.md check 20 case 01, check 21's
944
1045
  // first run); before, it went to the revision's edge, putting the text in the wrong place.
945
1046
  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())) {
1047
+ && this.insertBetweenHalves(at, items, nextId)) {
947
1048
  return;
948
1049
  }
949
1050
  // at the start of a revision, beside it - and outside a move's range that opens there (case 03)
950
1051
  if (!before && (after === null || after === void 0 ? void 0 : after.revision)) {
951
- this.insertBesideRevision(after.revision, ins, 'before');
1052
+ this.insertBesideRevision(after.revision, items, 'before');
952
1053
  return;
953
1054
  }
1055
+ const neighbour = prefer === 'after' ? after !== null && after !== void 0 ? after : before : before !== null && before !== void 0 ? before : after;
954
1056
  if (neighbour) {
955
1057
  const owner = neighbour.revision ? neighbour.revision.owner : neighbour.runOwner;
956
1058
  const item = neighbour.revision ? neighbour.revision.element : neighbour.runOwner[neighbour.runIndex];
957
1059
  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);
1060
+ owner.splice(owner.indexOf(item) + (neighbour === before ? 1 : 0), 0, ...items);
1061
+ linkParents(items, (_a = parent.PARENT) !== null && _a !== void 0 ? _a : this.p);
960
1062
  }
961
1063
  else {
962
- const content = ((_e = (_f = this.p).content) !== null && _e !== void 0 ? _e : (_f.content = []));
1064
+ const content = ((_b = (_c = this.p).content) !== null && _b !== void 0 ? _b : (_c.content = []));
963
1065
  if (at === 0)
964
- content.unshift(ins);
1066
+ content.unshift(...items);
965
1067
  else
966
- content.push(ins);
967
- linkParents(ins, this.p);
1068
+ content.push(...items);
1069
+ linkParents(items, this.p);
968
1070
  }
969
1071
  }
970
1072
  /**
@@ -6,8 +6,9 @@ import { ContentControl } from './ContentControl.mjs';
6
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
- import { controlIdScope, sdtKindFor } from '../customxml/insert.mjs';
9
+ import { controlIdScope, sdtKindFor, declareSdtExtensions } 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
  /**
@@ -73,7 +74,7 @@ export class Range {
73
74
  };
74
75
  if ((options === null || options === void 0 ? void 0 : options.direct) === true)
75
76
  return new Font(holders, () => this.paragraph.fontTracking());
76
- return new Font(holders, () => this.paragraph.fontTracking(), (rPr) => this.paragraph.parentBody.propertyResolver.getEffectiveRPr(rPr, this.paragraph.p.pPr), (rPr) => { var _a; return (_a = runFontSelectorOf(this.paragraph.parentBody.package_)) === null || _a === void 0 ? void 0 : _a.asciiFontName(rPr); });
77
+ return new Font(holders, () => this.paragraph.fontTracking(), (rPr) => this.paragraph.parentBody.propertyResolver.getEffectiveRPr(rPr, this.paragraph.p.pPr, this.paragraph.cellContext), (rPr) => { var _a; return (_a = runFontSelectorOf(this.paragraph.parentBody.package_)) === null || _a === void 0 ? void 0 : _a.asciiFontName(rPr); });
77
78
  }
78
79
  /** The runs the span covers (a run partly inside counts). */
79
80
  get runs() {
@@ -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,12 +209,13 @@ 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
213
216
  // section), and that refusal must come before any run is split.
214
217
  const sdt = sdtOf([], { kind: sdtKindFor(kind), id: nextSdtId(controlIdScope(body)), form: 'run' });
218
+ declareSdtExtensions(body, sdt);
215
219
  if (this.start === this.end) {
216
220
  paragraph.insertItemsAt(this.start, [sdt]);
217
221
  return new ContentControl(sdt, containerOf(sdt, paragraph), body);
@@ -232,9 +236,49 @@ export class Range {
232
236
  items.push(element);
233
237
  }
234
238
  const at = owner.indexOf(items[0]);
239
+ const tracker = paragraph.changeTracker;
240
+ const revision = segments[0].revision;
241
+ if (tracker && revision) {
242
+ // The runs are in a revision: the control goes between its halves, not in it, and the runs go
243
+ // into the control in a revision of the same kind and attributes with a new id - Word's form for
244
+ // inserted text wrapped in a new control (check 30, inserted-text-wrapped: the w:ins inside the
245
+ // control, the control's own markers outside it).
246
+ const index = revision.items.indexOf(items[0]);
247
+ revision.items.splice(index, items.length);
248
+ const value = revision.value;
249
+ const itemsKey = Object.keys(value).find((key) => value[key] === revision.items);
250
+ const inner = {};
251
+ for (const [key, v] of Object.entries(value))
252
+ if (key !== itemsKey && key !== 'PARENT')
253
+ inner[key] = v;
254
+ inner.id = tracker.nextId();
255
+ inner[itemsKey] = items;
256
+ const wrapped = { name: revision.element.name, value: inner };
257
+ linkParents(items, inner);
258
+ sdt.value.sdtContent.content = [wrapped];
259
+ linkParents(wrapped, sdt.value.sdtContent);
260
+ const holderOwner = revision.owner;
261
+ const parent = (_a = revision.value.PARENT) !== null && _a !== void 0 ? _a : paragraph.p;
262
+ if (revision.items.length === 0) {
263
+ holderOwner.splice(holderOwner.indexOf(revision.element), 1, sdt); // the whole revision wrapped
264
+ linkParents(sdt, parent);
265
+ }
266
+ else if (index === 0) {
267
+ holderOwner.splice(holderOwner.indexOf(revision.element), 0, sdt); // its start wrapped
268
+ linkParents(sdt, parent);
269
+ }
270
+ else if (!paragraph.splitHolderAround(revision, index, sdt, () => tracker.nextId())) {
271
+ throw new Docx4JException('This range is in a revision that cannot be split around a content control');
272
+ }
273
+ markHolderInserted(tracker, sdt, holderOwner, () => { });
274
+ return new ContentControl(sdt, holderOwner, body);
275
+ }
235
276
  sdt.value.sdtContent.content = items;
236
277
  owner.splice(at, items.length, sdt);
237
- linkParents(sdt, (_a = segments[0].run.PARENT) !== null && _a !== void 0 ? _a : paragraph.p);
278
+ linkParents(sdt, (_b = segments[0].run.PARENT) !== null && _b !== void 0 ? _b : paragraph.p);
279
+ // under tracking the control itself is the insertion, the runs in it what they were (check 30)
280
+ if (tracker)
281
+ markHolderInserted(tracker, sdt, owner, () => { });
238
282
  return new ContentControl(sdt, owner, body);
239
283
  }
240
284
  /**
@@ -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;