@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.
- package/dist/model/content/Body.mjs +38 -18
- package/dist/model/content/Comment.d.mts +6 -0
- package/dist/model/content/Comment.mjs +76 -22
- package/dist/model/content/ContentControl.d.mts +6 -0
- package/dist/model/content/ContentControl.mjs +51 -5
- package/dist/model/content/Paragraph.d.mts +37 -2
- package/dist/model/content/Paragraph.mjs +148 -33
- package/dist/model/content/Range.mjs +46 -3
- package/dist/model/content/Table.mjs +6 -8
- package/dist/model/content/TrackedChange.d.mts +32 -4
- package/dist/model/content/TrackedChange.mjs +271 -34
- package/dist/model/content/index.d.mts +1 -1
- package/dist/model/content/index.mjs +1 -1
- package/dist/model/content/stylesFacade.mjs +5 -2
- package/dist/model/content/tracking.d.mts +51 -5
- package/dist/model/content/tracking.mjs +151 -33
- package/dist/packages/WordprocessingMLPackage.d.mts +6 -0
- package/dist/packages/WordprocessingMLPackage.mjs +11 -0
- package/dist/parts/wml/commentStyles.d.mts +3 -0
- package/dist/parts/wml/commentStyles.mjs +25 -0
- package/dist/parts/wml/comments.mjs +4 -22
- package/package.json +1 -1
|
@@ -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
|
-
|
|
606
|
-
|
|
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
|
|
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 : ((
|
|
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 = (
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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([
|
|
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
|
-
|
|
888
|
-
|
|
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
|
|
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
|
-
|
|
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,
|
|
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,
|
|
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,
|
|
959
|
-
linkParents(
|
|
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 = ((
|
|
1077
|
+
const content = ((_b = (_c = this.p).content) !== null && _b !== void 0 ? _b : (_c.content = []));
|
|
963
1078
|
if (at === 0)
|
|
964
|
-
content.unshift(
|
|
1079
|
+
content.unshift(...items);
|
|
965
1080
|
else
|
|
966
|
-
content.push(
|
|
967
|
-
linkParents(
|
|
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, (
|
|
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 {
|
|
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
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
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;
|