@docx4j/core-ts 0.1.4 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +76 -0
- package/dist/model/content/Body.d.mts +36 -2
- package/dist/model/content/Body.mjs +79 -4
- package/dist/model/content/Comment.d.mts +15 -5
- package/dist/model/content/Comment.mjs +70 -22
- package/dist/model/content/List.d.mts +80 -1
- package/dist/model/content/List.mjs +140 -22
- package/dist/model/content/Paragraph.d.mts +11 -1
- package/dist/model/content/Paragraph.mjs +18 -7
- package/dist/model/content/Range.d.mts +29 -2
- package/dist/model/content/Range.mjs +137 -7
- package/dist/model/content/binder.d.mts +44 -0
- package/dist/model/content/binder.mjs +95 -0
- package/dist/model/content/comments.d.mts +19 -1
- package/dist/model/content/index.d.mts +6 -4
- package/dist/model/content/index.mjs +4 -2
- package/dist/model/content/search.d.mts +27 -2
- package/dist/model/content/search.mjs +59 -3
- package/dist/model/content/stylesFacade.d.mts +37 -0
- package/dist/model/content/stylesFacade.mjs +114 -0
- package/dist/model/content/tracking.d.mts +57 -2
- package/dist/model/content/tracking.mjs +47 -15
- package/dist/model/customxml/CustomXmlPartCollection.d.mts +7 -0
- package/dist/model/customxml/CustomXmlPartCollection.mjs +40 -6
- package/dist/model/customxml/xpath.d.mts +18 -3
- package/dist/model/customxml/xpath.mjs +18 -3
- package/dist/office-js/index.d.mts +3 -1
- package/dist/office-js/run.d.mts +12 -2
- package/dist/office-js/run.mjs +8 -1
- package/dist/office-js/shimmed.d.mts +46 -0
- package/dist/office-js/shimmed.mjs +1 -0
- package/dist/office-js/supported.generated.mjs +1 -0
- package/dist/opc/exceptions.d.mts +25 -0
- package/dist/opc/exceptions.mjs +20 -0
- package/dist/opc/index.d.mts +1 -1
- package/dist/opc/index.mjs +1 -1
- package/dist/opc/mce/understood.mjs +37 -8
- package/dist/packages/WordprocessingMLPackage.d.mts +101 -2
- package/dist/packages/WordprocessingMLPackage.mjs +155 -1
- package/dist/parts/XmlPart.d.mts +44 -0
- package/dist/parts/XmlPart.mjs +86 -3
- package/dist/parts/sml/index.d.mts +9 -8
- package/dist/parts/sml/index.mjs +23 -19
- package/dist/parts/wml/index.d.mts +10 -2
- package/dist/parts/wml/index.mjs +11 -3
- package/dist/parts/wml/spliceableStyles.d.mts +1 -0
- package/dist/parts/wml/spliceableStyles.mjs +122 -0
- package/package.json +2 -2
|
@@ -398,22 +398,10 @@ export class List {
|
|
|
398
398
|
* Extension name (Office JS has no restart verb); `Paragraph.restartList()` is the sugar.
|
|
399
399
|
*/
|
|
400
400
|
restart() {
|
|
401
|
-
var _a, _b
|
|
402
|
-
const
|
|
403
|
-
const num = this.element;
|
|
404
|
-
const start = (_b = (_a = this.level(0)) === null || _a === void 0 ? void 0 : _a.start) !== null && _b !== void 0 ? _b : 1;
|
|
405
|
-
const created = f.createNumberingNum({
|
|
406
|
-
numId: nextNumId(numbering),
|
|
407
|
-
abstractNumId: f.createNumberingNumAbstractNumId({ val: num.abstractNumId.val }),
|
|
408
|
-
lvlOverride: [f.createNumberingNumLvlOverride({
|
|
409
|
-
ilvl: 0,
|
|
410
|
-
startOverride: f.createNumberingNumLvlOverrideStartOverride({ val: start }),
|
|
411
|
-
})],
|
|
412
|
-
});
|
|
413
|
-
((_c = numbering.num) !== null && _c !== void 0 ? _c : (numbering.num = [])).push(created);
|
|
414
|
-
linkParents(created, numbering);
|
|
401
|
+
var _a, _b;
|
|
402
|
+
const numId = addRestartedNum(this.live(), this.element.abstractNumId.val, (_b = (_a = this.level(0)) === null || _a === void 0 ? void 0 : _a.start) !== null && _b !== void 0 ? _b : 1);
|
|
415
403
|
this.written();
|
|
416
|
-
return new List(String(
|
|
404
|
+
return new List(String(numId), this.body);
|
|
417
405
|
}
|
|
418
406
|
/**
|
|
419
407
|
* Gives this list a `w:abstractNum` of its own where another `w:num` shares the one it names
|
|
@@ -668,29 +656,66 @@ export function detachFromList(paragraph) {
|
|
|
668
656
|
* be unmarshalled, and a document with no numbering part gets one here (CR-002 section 17).
|
|
669
657
|
*/
|
|
670
658
|
export async function startNewList(paragraph, options) {
|
|
671
|
-
var _a, _b
|
|
659
|
+
var _a, _b;
|
|
672
660
|
const body = paragraph.parentBody;
|
|
673
661
|
const part = await ensureNumberingPart(body);
|
|
662
|
+
const numId = await addListDefinition(part, (options === null || options === void 0 ? void 0 : options.bullet) === true);
|
|
663
|
+
(_a = packageOf(body)) === null || _a === void 0 ? void 0 : _a.refreshPropertyResolver();
|
|
664
|
+
return attachToList(paragraph, numId, (_b = options === null || options === void 0 ? void 0 : options.level) !== null && _b !== void 0 ? _b : 0);
|
|
665
|
+
}
|
|
666
|
+
/**
|
|
667
|
+
* A `w:abstractNum` copied from docx4j's default definitions (its `numbering.xml` resource:
|
|
668
|
+
* `w:abstractNum` 1 is Word's decimal set and 0 its bullet set) and a `w:num` naming it; the new
|
|
669
|
+
* `w:numId`. The paragraph half of `startNewList` is separate because it is separable: nothing
|
|
670
|
+
* about building a list definition needs one.
|
|
671
|
+
*/
|
|
672
|
+
async function addListDefinition(part, bullet, ids) {
|
|
673
|
+
var _a, _b, _c;
|
|
674
674
|
const numbering = await part.getContents();
|
|
675
675
|
const defaults = await defaultNumbering();
|
|
676
|
-
const template = ((_a = defaults.abstractNum) !== null && _a !== void 0 ? _a : [])[
|
|
676
|
+
const template = ((_a = defaults.abstractNum) !== null && _a !== void 0 ? _a : [])[bullet ? 0 : 1];
|
|
677
677
|
if (template === undefined)
|
|
678
678
|
throw new Docx4JException('The default numbering definitions are missing an abstractNum');
|
|
679
|
+
// both identifiers are settled before anything is written, so a clash leaves the part as it was
|
|
680
|
+
// (CR-002 section 31: an orphan w:abstractNum was saved for every refused numId)
|
|
681
|
+
const abstractNumId = (ids === null || ids === void 0 ? void 0 : ids.abstractNumId) === undefined
|
|
682
|
+
? nextAbstractNumId(numbering)
|
|
683
|
+
: requireFreeId(numbering, 'abstractNumId', ids.abstractNumId);
|
|
684
|
+
const numId = (ids === null || ids === void 0 ? void 0 : ids.numId) === undefined ? nextNumId(numbering) : requireFreeId(numbering, 'numId', ids.numId);
|
|
679
685
|
const abstract = deepCopy(template);
|
|
680
|
-
abstract.abstractNumId =
|
|
686
|
+
abstract.abstractNumId = abstractNumId;
|
|
681
687
|
abstract.nsid = f.createCTLongHexNumber({ val: hexId() });
|
|
682
688
|
delete abstract.styleLink;
|
|
683
689
|
delete abstract.numStyleLink;
|
|
684
690
|
((_b = numbering.abstractNum) !== null && _b !== void 0 ? _b : (numbering.abstractNum = [])).push(abstract);
|
|
685
691
|
linkParents(abstract, numbering);
|
|
686
692
|
const num = f.createNumberingNum({
|
|
687
|
-
numId
|
|
688
|
-
abstractNumId: f.createNumberingNumAbstractNumId({ val:
|
|
693
|
+
numId,
|
|
694
|
+
abstractNumId: f.createNumberingNumAbstractNumId({ val: abstractNumId }),
|
|
689
695
|
});
|
|
690
696
|
((_c = numbering.num) !== null && _c !== void 0 ? _c : (numbering.num = [])).push(num);
|
|
691
697
|
linkParents(num, numbering);
|
|
692
|
-
|
|
693
|
-
|
|
698
|
+
return num.numId;
|
|
699
|
+
}
|
|
700
|
+
/**
|
|
701
|
+
* A `w:num` naming the same `w:abstractNum` as another, with a `w:startOverride` at level 0; the
|
|
702
|
+
* new `w:numId`. The counters are shared by abstract definition, so this is how Word restarts a
|
|
703
|
+
* list (CR-001 section 15.2), and it is what {@link List.restart} and
|
|
704
|
+
* {@link NumberingFacade.restart} both do.
|
|
705
|
+
*/
|
|
706
|
+
function addRestartedNum(numbering, abstractNumId, start, newNumId) {
|
|
707
|
+
var _a;
|
|
708
|
+
const created = f.createNumberingNum({
|
|
709
|
+
numId: newNumId === undefined ? nextNumId(numbering) : requireFreeId(numbering, 'numId', newNumId),
|
|
710
|
+
abstractNumId: f.createNumberingNumAbstractNumId({ val: abstractNumId }),
|
|
711
|
+
lvlOverride: [f.createNumberingNumLvlOverride({
|
|
712
|
+
ilvl: 0,
|
|
713
|
+
startOverride: f.createNumberingNumLvlOverrideStartOverride({ val: start }),
|
|
714
|
+
})],
|
|
715
|
+
});
|
|
716
|
+
((_a = numbering.num) !== null && _a !== void 0 ? _a : (numbering.num = [])).push(created);
|
|
717
|
+
linkParents(created, numbering);
|
|
718
|
+
return created.numId;
|
|
694
719
|
}
|
|
695
720
|
/**
|
|
696
721
|
* The numbering part, created with docx4j's default numbering where the document has none
|
|
@@ -703,6 +728,14 @@ async function ensureNumberingPart(body) {
|
|
|
703
728
|
if (pkg === undefined || main === undefined) {
|
|
704
729
|
throw new Docx4JException('This body has no package, so a list cannot be added to it');
|
|
705
730
|
}
|
|
731
|
+
return ensureNumberingPartOf(pkg, main);
|
|
732
|
+
}
|
|
733
|
+
/**
|
|
734
|
+
* The same, reached from the package rather than from a body, which is what
|
|
735
|
+
* {@link NumberingFacade} uses: creating a list definition needs no paragraph and no body, and
|
|
736
|
+
* making one go through either costs a caller that holds neither (CR-002 section 19).
|
|
737
|
+
*/
|
|
738
|
+
async function ensureNumberingPartOf(pkg, main) {
|
|
706
739
|
const existing = main.numberingDefinitionsPart;
|
|
707
740
|
if (existing !== undefined) {
|
|
708
741
|
// the resolver read the part privately; unmarshalling it gives a second tree, so the
|
|
@@ -725,6 +758,63 @@ async function ensureNumberingPart(body) {
|
|
|
725
758
|
await pkg.refresh();
|
|
726
759
|
return created;
|
|
727
760
|
}
|
|
761
|
+
/**
|
|
762
|
+
* The document's list definitions, as verbs that need no paragraph (CR-002 section 19, at the
|
|
763
|
+
* editor's request): `pkg.numbering`.
|
|
764
|
+
*
|
|
765
|
+
* `Paragraph.startNewList()` and `List.restart()` are Office JS's shapes and stay as they are,
|
|
766
|
+
* but both build a definition first and touch a paragraph only at the end. An editor that keeps
|
|
767
|
+
* its own document model has the numbering part and a `w:numId` to write, and nothing else - and
|
|
768
|
+
* going through a paragraph would mean projecting a tree, running the verb and projecting back,
|
|
769
|
+
* a whole-body round trip for what is otherwise one attribute. So the definition half is here.
|
|
770
|
+
*/
|
|
771
|
+
export class NumberingFacade {
|
|
772
|
+
constructor(pkg) {
|
|
773
|
+
this.pkg = pkg;
|
|
774
|
+
}
|
|
775
|
+
/**
|
|
776
|
+
* A new list definition, and its `w:numId` as a string for a `w:numPr` to name. The numbering
|
|
777
|
+
* part is created with docx4j's default definitions where the document has none, and an
|
|
778
|
+
* existing one is unmarshalled so that what is written to it is saved.
|
|
779
|
+
*
|
|
780
|
+
* Asynchronous for the same reasons `startNewList` is: the default definitions are XML to be
|
|
781
|
+
* unmarshalled, and the part may have to be created. Nothing is attached to anything; the
|
|
782
|
+
* caller writes `w:numPr` itself.
|
|
783
|
+
*/
|
|
784
|
+
async newList(options) {
|
|
785
|
+
// the form is checked before a numbering part can be created for a call that is then refused
|
|
786
|
+
if ((options === null || options === void 0 ? void 0 : options.numId) !== undefined)
|
|
787
|
+
requireIdForm('numId', options.numId);
|
|
788
|
+
if ((options === null || options === void 0 ? void 0 : options.abstractNumId) !== undefined)
|
|
789
|
+
requireIdForm('abstractNumId', options.abstractNumId);
|
|
790
|
+
const main = this.pkg.getMainDocumentPart();
|
|
791
|
+
const part = await ensureNumberingPartOf(this.pkg, main);
|
|
792
|
+
const numId = await addListDefinition(part, (options === null || options === void 0 ? void 0 : options.bullet) === true, options);
|
|
793
|
+
this.pkg.refreshPropertyResolver();
|
|
794
|
+
return String(numId);
|
|
795
|
+
}
|
|
796
|
+
/**
|
|
797
|
+
* A `w:num` that numbers as `numId` does but starts again, and its own `w:numId`:
|
|
798
|
+
* `List.restart()` without needing a `Body`. The counters are shared by abstract definition,
|
|
799
|
+
* so restarting means a new `w:num` with a `w:startOverride`, spent on its first use in a
|
|
800
|
+
* story (CR-001 section 15.2).
|
|
801
|
+
*
|
|
802
|
+
* Synchronous, so the numbering part must have been read: `await pkg.getPropertyResolver()`,
|
|
803
|
+
* `newList()` or any list read does it.
|
|
804
|
+
*/
|
|
805
|
+
restart(numId, options) {
|
|
806
|
+
var _a, _b;
|
|
807
|
+
const part = this.pkg.getMainDocumentPart().numberingDefinitionsPart;
|
|
808
|
+
if (part === undefined)
|
|
809
|
+
throw new Docx4JException('This document has no numbering part, so there is no list to restart');
|
|
810
|
+
const definition = part.definitions.list(numId);
|
|
811
|
+
if (definition === undefined)
|
|
812
|
+
throw new ItemNotFound(`No w:num for numId ${numId}`);
|
|
813
|
+
const created = addRestartedNum(part.makeLive(), definition.num.abstractNumId.val, (_b = (_a = definition.level('0')) === null || _a === void 0 ? void 0 : _a.start) !== null && _b !== void 0 ? _b : 1, options === null || options === void 0 ? void 0 : options.newNumId);
|
|
814
|
+
this.pkg.refreshPropertyResolver();
|
|
815
|
+
return String(created);
|
|
816
|
+
}
|
|
817
|
+
}
|
|
728
818
|
// ----------------------------------------------------------------------------- the mechanics
|
|
729
819
|
const TWIPS_PER_POINT = 20;
|
|
730
820
|
/** Office JS `Word.ListNumbering` as `w:numFmt`. */
|
|
@@ -761,6 +851,34 @@ function nextNumId(numbering) {
|
|
|
761
851
|
var _a;
|
|
762
852
|
return ((_a = numbering.num) !== null && _a !== void 0 ? _a : []).reduce((max, num) => Math.max(max, Number(num.numId)), 0) + 1;
|
|
763
853
|
}
|
|
854
|
+
/**
|
|
855
|
+
* A chosen identifier in range: a `w:numId` from 1, since 0 is what a `w:numPr` says to mean "not
|
|
856
|
+
* numbered" (`detachFromList` writes it, and Word and the Emulator read it so), and a
|
|
857
|
+
* `w:abstractNumId` from 0 (CR-002 section 31).
|
|
858
|
+
*/
|
|
859
|
+
function requireIdForm(kind, chosen) {
|
|
860
|
+
const id = Number(chosen);
|
|
861
|
+
const least = kind === 'numId' ? 1 : 0;
|
|
862
|
+
if (!Number.isInteger(id) || id < least) {
|
|
863
|
+
throw new Docx4JException(`A ${kind} must be an integer from ${least}, not ${String(chosen)}`);
|
|
864
|
+
}
|
|
865
|
+
return id;
|
|
866
|
+
}
|
|
867
|
+
/**
|
|
868
|
+
* A chosen identifier, checked unused, as `addExternalRelationship`'s proposed id is. A clash throws
|
|
869
|
+
* rather than renumbering: a caller who names an id is asserting something about a room's state, and
|
|
870
|
+
* quietly moving it would break the peer that named it (CR-002 section 24).
|
|
871
|
+
*/
|
|
872
|
+
function requireFreeId(numbering, kind, chosen) {
|
|
873
|
+
var _a, _b;
|
|
874
|
+
const id = requireIdForm(kind, chosen);
|
|
875
|
+
const taken = kind === 'numId'
|
|
876
|
+
? ((_a = numbering.num) !== null && _a !== void 0 ? _a : []).some((num) => Number(num.numId) === id)
|
|
877
|
+
: ((_b = numbering.abstractNum) !== null && _b !== void 0 ? _b : []).some((a) => Number(a.abstractNumId) === id);
|
|
878
|
+
if (taken)
|
|
879
|
+
throw new Docx4JException(`The ${kind} ${id} is already in use in this document's numbering part`);
|
|
880
|
+
return id;
|
|
881
|
+
}
|
|
764
882
|
/** A free `w:abstractNumId`. */
|
|
765
883
|
function nextAbstractNumId(numbering) {
|
|
766
884
|
var _a;
|
|
@@ -7,6 +7,7 @@ import type { Body, BlockElement } from './Body.mjs';
|
|
|
7
7
|
import { type TableCell } from './Table.mjs';
|
|
8
8
|
import { ContentControl, type ContentControlType } from './ContentControl.mjs';
|
|
9
9
|
import { InlinePicture, type InlinePictureOptions } from './InlinePicture.mjs';
|
|
10
|
+
import { type CommentContent, type CommentOptions } from './comments.mjs';
|
|
10
11
|
import type { Comment } from './Comment.mjs';
|
|
11
12
|
import { type ChangeTracker } from './tracking.mjs';
|
|
12
13
|
import { TrackedChange } from './TrackedChange.mjs';
|
|
@@ -160,6 +161,15 @@ export declare class Paragraph {
|
|
|
160
161
|
insertOoxml(ooxml: string, location: 'Before' | 'After' | 'Start' | 'End' | 'Replace'): Promise<(Paragraph | BlockElement)[]>;
|
|
161
162
|
/** Matches within this paragraph. */
|
|
162
163
|
search(text: string, options?: SearchOptions): Range[];
|
|
164
|
+
/**
|
|
165
|
+
* `search`, keeping each match's capture groups, which is what a replacement's `$1` or Word's `\1`
|
|
166
|
+
* expands from (CR-002 section 26). Not an Office JS member; `replaceText` is its only caller here
|
|
167
|
+
* and `Body.replaceText` the other.
|
|
168
|
+
*/
|
|
169
|
+
searchMatches(text: string, options?: SearchOptions): {
|
|
170
|
+
range: Range;
|
|
171
|
+
match: RegExpExecArray;
|
|
172
|
+
}[];
|
|
163
173
|
/**
|
|
164
174
|
* Replaces every match of `find` with `replace`, last match first so that the offsets of the
|
|
165
175
|
* ones still to do stay valid (CR-002 section 3.7). Returns the number replaced; tracked when
|
|
@@ -261,7 +271,7 @@ export declare class Paragraph {
|
|
|
261
271
|
/** The comments anchored in this paragraph, replies nested under their parent. */
|
|
262
272
|
getComments(): Promise<Comment[]>;
|
|
263
273
|
/** Comments the whole paragraph (sugar over `getRange().insertComment`). */
|
|
264
|
-
insertComment(
|
|
274
|
+
insertComment(content: CommentContent, options?: CommentOptions): Promise<Comment>;
|
|
265
275
|
/**
|
|
266
276
|
* Wraps this paragraph in a new block-level content control (Office JS
|
|
267
277
|
* `Paragraph.insertContentControl`), typed for the kind when one is given. The `w:id` is free in
|
|
@@ -10,7 +10,7 @@ function withRPr(run, rPr) {
|
|
|
10
10
|
import { Font } from './Font.mjs';
|
|
11
11
|
import { runFontSelectorOf } from '../fonts/lookup.mjs';
|
|
12
12
|
import { Range } from './Range.mjs';
|
|
13
|
-
import { searchPattern,
|
|
13
|
+
import { searchPattern, matchesOf, expandReplacement } from './search.mjs';
|
|
14
14
|
import { builtInOf, idOfBuiltIn, styleNameOf, styleIdOf } from './styles.mjs';
|
|
15
15
|
import { cellOf } from './Table.mjs';
|
|
16
16
|
import { ContentControl, collectRunControls } from './ContentControl.mjs';
|
|
@@ -336,7 +336,16 @@ export class Paragraph {
|
|
|
336
336
|
}
|
|
337
337
|
/** Matches within this paragraph. */
|
|
338
338
|
search(text, options) {
|
|
339
|
-
return
|
|
339
|
+
return this.searchMatches(text, options).map((m) => m.range);
|
|
340
|
+
}
|
|
341
|
+
/**
|
|
342
|
+
* `search`, keeping each match's capture groups, which is what a replacement's `$1` or Word's `\1`
|
|
343
|
+
* expands from (CR-002 section 26). Not an Office JS member; `replaceText` is its only caller here
|
|
344
|
+
* and `Body.replaceText` the other.
|
|
345
|
+
*/
|
|
346
|
+
searchMatches(text, options) {
|
|
347
|
+
return matchesOf(this.text, searchPattern(text, options))
|
|
348
|
+
.map((match) => ({ range: new Range(this, match.index, match.index + match[0].length), match }));
|
|
340
349
|
}
|
|
341
350
|
/**
|
|
342
351
|
* Replaces every match of `find` with `replace`, last match first so that the offsets of the
|
|
@@ -344,9 +353,11 @@ export class Paragraph {
|
|
|
344
353
|
* the package's `changeTrackingMode` is on.
|
|
345
354
|
*/
|
|
346
355
|
replaceText(find, replace, options) {
|
|
347
|
-
const matches = this.
|
|
348
|
-
for (let i = matches.length - 1; i >= 0; i--)
|
|
349
|
-
matches[i]
|
|
356
|
+
const matches = this.searchMatches(find, options);
|
|
357
|
+
for (let i = matches.length - 1; i >= 0; i--) {
|
|
358
|
+
const { range, match } = matches[i];
|
|
359
|
+
range.insertText(expandReplacement(replace, match, options), 'Replace');
|
|
360
|
+
}
|
|
350
361
|
return matches.length;
|
|
351
362
|
}
|
|
352
363
|
/** The tracked changes in this paragraph, in document order (CR-002 phase F). */
|
|
@@ -672,8 +683,8 @@ export class Paragraph {
|
|
|
672
683
|
return commentApi().commentsOf(this);
|
|
673
684
|
}
|
|
674
685
|
/** Comments the whole paragraph (sugar over `getRange().insertComment`). */
|
|
675
|
-
async insertComment(
|
|
676
|
-
return commentApi().insertComment(this.getRange(),
|
|
686
|
+
async insertComment(content, options) {
|
|
687
|
+
return commentApi().insertComment(this.getRange(), content, options);
|
|
677
688
|
}
|
|
678
689
|
// --- content controls (CR-002 phase E) ---
|
|
679
690
|
/**
|
|
@@ -5,6 +5,7 @@ import type { BlockElement } from './Body.mjs';
|
|
|
5
5
|
import { type Element, type TextViewOptions } from './tree.mjs';
|
|
6
6
|
import { ContentControl, type ContentControlType } from './ContentControl.mjs';
|
|
7
7
|
import { type SearchOptions } from './search.mjs';
|
|
8
|
+
import { type CommentContent, type CommentOptions } from './comments.mjs';
|
|
8
9
|
import type { Comment } from './Comment.mjs';
|
|
9
10
|
import type { TrackedChange } from './TrackedChange.mjs';
|
|
10
11
|
/**
|
|
@@ -54,7 +55,11 @@ export declare class Range {
|
|
|
54
55
|
/** Removes the span's text. */
|
|
55
56
|
delete(): void;
|
|
56
57
|
search(text: string, options?: SearchOptions): Range[];
|
|
57
|
-
/**
|
|
58
|
+
/**
|
|
59
|
+
* Replaces every match in this span, last first so the offsets stay valid; returns the count
|
|
60
|
+
* (CR-002 section 3.7). The replacement's `$1` or Word's `\1` expands as it does on a paragraph
|
|
61
|
+
* (section 26; this one was missed there, section 31).
|
|
62
|
+
*/
|
|
58
63
|
replaceText(find: string, replace: string, options?: SearchOptions): number;
|
|
59
64
|
/** The tracked changes this span covers, in document order (CR-002 phase F). */
|
|
60
65
|
getTrackedChanges(): TrackedChange[];
|
|
@@ -68,11 +73,33 @@ export declare class Range {
|
|
|
68
73
|
* boundaries, as `font` does), the comment itself and the side-part entries, creating any of
|
|
69
74
|
* the comment parts the document lacks. Asynchronous because it unmarshals those parts.
|
|
70
75
|
*/
|
|
71
|
-
insertComment(
|
|
76
|
+
insertComment(content: CommentContent, options?: CommentOptions): Promise<Comment>;
|
|
72
77
|
/**
|
|
73
78
|
* Wraps the runs this span covers in a run-level content control (Office JS
|
|
74
79
|
* `Range.insertContentControl`), splitting the runs at the boundaries as `font` does. An empty
|
|
75
80
|
* span gets an empty control at its position. Returns the control.
|
|
76
81
|
*/
|
|
77
82
|
insertContentControl(kind?: ContentControlType): ContentControl;
|
|
83
|
+
/**
|
|
84
|
+
* Office JS `Range.hyperlink`: the address of the first hyperlink in this range, or a new
|
|
85
|
+
* hyperlink over it. `address#location` separates the address from an optional location within
|
|
86
|
+
* it, so `"#heading"` is a link inside this document (a `w:anchor`) and
|
|
87
|
+
* `"https://example.com"` an external one (a relationship). Reading gives `""` where the range
|
|
88
|
+
* is in no hyperlink.
|
|
89
|
+
*
|
|
90
|
+
* Setting removes the hyperlinks the range already carries and wraps its runs in one
|
|
91
|
+
* `w:hyperlink`, splitting runs at the boundaries as `font` and `insertContentControl` do;
|
|
92
|
+
* setting `""` removes them and wraps nothing. An external address becomes a relationship of
|
|
93
|
+
* this range's part, which is why the part must exist (CR-002 section 20).
|
|
94
|
+
*/
|
|
95
|
+
get hyperlink(): string;
|
|
96
|
+
set hyperlink(value: string);
|
|
97
|
+
/** The `w:hyperlink` this range's first run sits in, if any. */
|
|
98
|
+
private hyperlinkHolder;
|
|
99
|
+
/** Unwraps every `w:hyperlink` the range touches, keeping its runs where they are. */
|
|
100
|
+
private removeHyperlinks;
|
|
101
|
+
/** The target of a relationship of this range's part, for the getter. */
|
|
102
|
+
private relationshipTarget;
|
|
103
|
+
/** An external relationship of this range's part for the address, and its id. */
|
|
104
|
+
private addHyperlinkRelationship;
|
|
78
105
|
}
|
|
@@ -4,9 +4,11 @@ import { typeNameOf, linkParents, runItemsOf } from './tree.mjs';
|
|
|
4
4
|
import { Docx4JException } from '../../opc/exceptions.mjs';
|
|
5
5
|
import { ContentControl } from './ContentControl.mjs';
|
|
6
6
|
import { sdt as sdtOf, nextSdtId } from '@docx4j/generated-objects-ts/builders/wml';
|
|
7
|
+
import { hyperlink as hyperlinkOf } from '@docx4j/generated-objects-ts/el/org_docx4j_wml';
|
|
8
|
+
import { Namespaces } from '../../parts/Namespaces.mjs';
|
|
7
9
|
import { controlIdScope, sdtKindFor } from '../customxml/insert.mjs';
|
|
8
10
|
import { contentOf } from './ooxml.mjs';
|
|
9
|
-
import { searchPattern, findAll } from './search.mjs';
|
|
11
|
+
import { searchPattern, findAll, matchesOf, expandReplacement } from './search.mjs';
|
|
10
12
|
import { commentApi } from './comments.mjs';
|
|
11
13
|
/**
|
|
12
14
|
* A subset of Office JS `Word.Range`: a span of text within one paragraph, [start, end) in the
|
|
@@ -149,11 +151,19 @@ export class Range {
|
|
|
149
151
|
const base = this.start;
|
|
150
152
|
return findAll(this.text, searchPattern(text, options)).map(([s, e]) => new Range(this.paragraph, base + s, base + e));
|
|
151
153
|
}
|
|
152
|
-
/**
|
|
154
|
+
/**
|
|
155
|
+
* Replaces every match in this span, last first so the offsets stay valid; returns the count
|
|
156
|
+
* (CR-002 section 3.7). The replacement's `$1` or Word's `\1` expands as it does on a paragraph
|
|
157
|
+
* (section 26; this one was missed there, section 31).
|
|
158
|
+
*/
|
|
153
159
|
replaceText(find, replace, options) {
|
|
154
|
-
const
|
|
155
|
-
|
|
156
|
-
|
|
160
|
+
const base = this.start;
|
|
161
|
+
const matches = matchesOf(this.text, searchPattern(find, options));
|
|
162
|
+
for (let i = matches.length - 1; i >= 0; i--) {
|
|
163
|
+
const match = matches[i];
|
|
164
|
+
new Range(this.paragraph, base + match.index, base + match.index + match[0].length)
|
|
165
|
+
.insertText(expandReplacement(replace, match, options), 'Replace');
|
|
166
|
+
}
|
|
157
167
|
return matches.length;
|
|
158
168
|
}
|
|
159
169
|
/** The tracked changes this span covers, in document order (CR-002 phase F). */
|
|
@@ -186,8 +196,8 @@ export class Range {
|
|
|
186
196
|
* boundaries, as `font` does), the comment itself and the side-part entries, creating any of
|
|
187
197
|
* the comment parts the document lacks. Asynchronous because it unmarshals those parts.
|
|
188
198
|
*/
|
|
189
|
-
async insertComment(
|
|
190
|
-
return commentApi().insertComment(this,
|
|
199
|
+
async insertComment(content, options) {
|
|
200
|
+
return commentApi().insertComment(this, content, options);
|
|
191
201
|
}
|
|
192
202
|
// --- content controls (CR-002 phase E) ---
|
|
193
203
|
/**
|
|
@@ -227,6 +237,126 @@ export class Range {
|
|
|
227
237
|
linkParents(sdt, (_a = segments[0].run.PARENT) !== null && _a !== void 0 ? _a : paragraph.p);
|
|
228
238
|
return new ContentControl(sdt, owner, body);
|
|
229
239
|
}
|
|
240
|
+
/**
|
|
241
|
+
* Office JS `Range.hyperlink`: the address of the first hyperlink in this range, or a new
|
|
242
|
+
* hyperlink over it. `address#location` separates the address from an optional location within
|
|
243
|
+
* it, so `"#heading"` is a link inside this document (a `w:anchor`) and
|
|
244
|
+
* `"https://example.com"` an external one (a relationship). Reading gives `""` where the range
|
|
245
|
+
* is in no hyperlink.
|
|
246
|
+
*
|
|
247
|
+
* Setting removes the hyperlinks the range already carries and wraps its runs in one
|
|
248
|
+
* `w:hyperlink`, splitting runs at the boundaries as `font` and `insertContentControl` do;
|
|
249
|
+
* setting `""` removes them and wraps nothing. An external address becomes a relationship of
|
|
250
|
+
* this range's part, which is why the part must exist (CR-002 section 20).
|
|
251
|
+
*/
|
|
252
|
+
get hyperlink() {
|
|
253
|
+
var _a;
|
|
254
|
+
const holder = this.hyperlinkHolder();
|
|
255
|
+
if (holder === undefined)
|
|
256
|
+
return '';
|
|
257
|
+
const anchor = (_a = holder.value.anchor) !== null && _a !== void 0 ? _a : '';
|
|
258
|
+
const address = holder.value.id === undefined ? '' : this.relationshipTarget(holder.value.id);
|
|
259
|
+
return anchor === '' ? address : `${address}#${anchor}`;
|
|
260
|
+
}
|
|
261
|
+
set hyperlink(value) {
|
|
262
|
+
var _a;
|
|
263
|
+
const paragraph = this.paragraph;
|
|
264
|
+
this.removeHyperlinks();
|
|
265
|
+
if (value === '')
|
|
266
|
+
return;
|
|
267
|
+
const hash = value.indexOf('#');
|
|
268
|
+
const address = hash === -1 ? value : value.slice(0, hash);
|
|
269
|
+
const location = hash === -1 ? '' : value.slice(hash + 1);
|
|
270
|
+
// w:history="1" is what Word writes on every hyperlink it makes, and what docx4j writes when
|
|
271
|
+
// it builds one (MainDocumentPart.addHyperlink): "add the target to the viewed hyperlinks"
|
|
272
|
+
// (ECMA-376 17.16.22). Readers do not depend on it, but a document holding one holder from
|
|
273
|
+
// each source should not differ over it. Found by the docx4j-ts-editor session, ED-003
|
|
274
|
+
// section 11.8.
|
|
275
|
+
const holder = hyperlinkOf({ content: [], history: true });
|
|
276
|
+
if (location !== '')
|
|
277
|
+
holder.value.anchor = location;
|
|
278
|
+
if (address !== '')
|
|
279
|
+
holder.value.id = this.addHyperlinkRelationship(address);
|
|
280
|
+
if (this.start === this.end) {
|
|
281
|
+
paragraph.insertItemsAt(this.start, [holder]);
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
paragraph.splitAt(this.start);
|
|
285
|
+
paragraph.splitAt(this.end);
|
|
286
|
+
const segments = paragraph.segments().filter((seg) => seg.start >= this.start && seg.end <= this.end);
|
|
287
|
+
if (segments.length === 0)
|
|
288
|
+
throw new Docx4JException('This range covers no run');
|
|
289
|
+
const owner = segments[0].runOwner;
|
|
290
|
+
if (segments.some((seg) => seg.runOwner !== owner)) {
|
|
291
|
+
throw new Docx4JException('This range spans more than one run holder (a hyperlink or a tracked change); wrap a narrower span');
|
|
292
|
+
}
|
|
293
|
+
const items = [];
|
|
294
|
+
for (const segment of segments) {
|
|
295
|
+
const element = owner[segment.runIndex];
|
|
296
|
+
if (!items.includes(element))
|
|
297
|
+
items.push(element);
|
|
298
|
+
}
|
|
299
|
+
const at = owner.indexOf(items[0]);
|
|
300
|
+
holder.value.content = items;
|
|
301
|
+
owner.splice(at, items.length, holder);
|
|
302
|
+
linkParents(holder, (_a = segments[0].run.PARENT) !== null && _a !== void 0 ? _a : paragraph.p);
|
|
303
|
+
}
|
|
304
|
+
/** The `w:hyperlink` this range's first run sits in, if any. */
|
|
305
|
+
hyperlinkHolder() {
|
|
306
|
+
for (const segment of this.paragraph.segments()) {
|
|
307
|
+
if (segment.end <= this.start && this.start !== this.end)
|
|
308
|
+
continue;
|
|
309
|
+
if (segment.start >= this.end && this.start !== this.end)
|
|
310
|
+
break;
|
|
311
|
+
const parent = segment.run.PARENT;
|
|
312
|
+
const found = hyperlinkHolders(this.paragraph).find((h) => h.value === parent);
|
|
313
|
+
if (found)
|
|
314
|
+
return found;
|
|
315
|
+
}
|
|
316
|
+
return undefined;
|
|
317
|
+
}
|
|
318
|
+
/** Unwraps every `w:hyperlink` the range touches, keeping its runs where they are. */
|
|
319
|
+
removeHyperlinks() {
|
|
320
|
+
var _a, _b, _c;
|
|
321
|
+
for (const holder of hyperlinkHolders(this.paragraph)) {
|
|
322
|
+
const owner = containerOf(holder, this.paragraph);
|
|
323
|
+
const at = owner.indexOf(holder);
|
|
324
|
+
if (at === -1)
|
|
325
|
+
continue;
|
|
326
|
+
owner.splice(at, 1, ...((_a = holder.value.content) !== null && _a !== void 0 ? _a : []));
|
|
327
|
+
for (const item of ((_b = holder.value.content) !== null && _b !== void 0 ? _b : [])) {
|
|
328
|
+
linkParents(item, (_c = holder.PARENT) !== null && _c !== void 0 ? _c : this.paragraph.p);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
/** The target of a relationship of this range's part, for the getter. */
|
|
333
|
+
relationshipTarget(id) {
|
|
334
|
+
var _a, _b, _c;
|
|
335
|
+
const part = this.paragraph.parentBody.part;
|
|
336
|
+
return (_c = (_b = (_a = part === null || part === void 0 ? void 0 : part.relationshipsPart) === null || _a === void 0 ? void 0 : _a.getRelationshipById(id)) === null || _b === void 0 ? void 0 : _b.target) !== null && _c !== void 0 ? _c : '';
|
|
337
|
+
}
|
|
338
|
+
/** An external relationship of this range's part for the address, and its id. */
|
|
339
|
+
addHyperlinkRelationship(address) {
|
|
340
|
+
const part = this.paragraph.parentBody.part;
|
|
341
|
+
if (!part)
|
|
342
|
+
throw new Docx4JException('This range has no part, so a hyperlink cannot be related to it');
|
|
343
|
+
return part.getRelationshipsPart(true).addExternalRelationship(Namespaces.HYPERLINK, address).id;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
/** Every `w:hyperlink` of a paragraph, outermost first (they do not nest in practice). */
|
|
347
|
+
function hyperlinkHolders(paragraph) {
|
|
348
|
+
const out = [];
|
|
349
|
+
const visit = (items) => {
|
|
350
|
+
for (const item of items !== null && items !== void 0 ? items : []) {
|
|
351
|
+
if (typeNameOf(item) === 'org_docx4j_wml.P.Hyperlink')
|
|
352
|
+
out.push(item);
|
|
353
|
+
const value = item.value;
|
|
354
|
+
if (typeof value === 'object' && value !== null)
|
|
355
|
+
visit(runItemsOf(value));
|
|
356
|
+
}
|
|
357
|
+
};
|
|
358
|
+
visit(runItemsOf(paragraph.p));
|
|
359
|
+
return out;
|
|
230
360
|
}
|
|
231
361
|
/** The run-level array holding an element, after `insertItemsAt` put it somewhere in the paragraph. */
|
|
232
362
|
function containerOf(element, paragraph) {
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { type Jsonix } from '@docx4j/generated-objects-ts';
|
|
2
|
+
/** What an XPath selected, and the tree object behind it (CR-006 section 2). */
|
|
3
|
+
export interface Bound {
|
|
4
|
+
/**
|
|
5
|
+
* The DOM node the expression selected. It is in a snapshot marshalled for this call, so changing
|
|
6
|
+
* it changes nothing: edits go through the content API or `setContents`, never here.
|
|
7
|
+
*/
|
|
8
|
+
node: Node;
|
|
9
|
+
/**
|
|
10
|
+
* The tree object: for an element, what a walk of the typed tree would hand you at that path,
|
|
11
|
+
* never a `{ name, value }` wrapper; for an attribute or a text node, the object of the element
|
|
12
|
+
* that owns it. Undefined for an element the model does not build - a wildcard child is its own
|
|
13
|
+
* object, so this is undefined only above the root or for a node the marshal did not report.
|
|
14
|
+
*/
|
|
15
|
+
object: unknown;
|
|
16
|
+
/** Set when an attribute was selected: its qualified name on `object`'s element. */
|
|
17
|
+
attribute?: string;
|
|
18
|
+
/** The node's path from the part's root, `/w:document/w:body/w:p[3]/w:r[2]`. */
|
|
19
|
+
path: string;
|
|
20
|
+
}
|
|
21
|
+
/** A marshalled snapshot and the element-to-object map recorded while it was written. */
|
|
22
|
+
export interface BoundSnapshot {
|
|
23
|
+
root: Element;
|
|
24
|
+
/** The object each element was marshalled from; a wildcard child maps to the original tree node. */
|
|
25
|
+
objects: WeakMap<Element, unknown>;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Marshals a root element and records what each DOM element came from. One marshal per call: the
|
|
29
|
+
* tree has no change log a cached snapshot could be checked against, and a stale answer is worse
|
|
30
|
+
* than a slow one (docx4j's `refreshXmlFirst`, always true here). The editor measured 349 to 411 ms
|
|
31
|
+
* for a 255-page main document part, against 975 ms for its first unmarshal, and runs a selection
|
|
32
|
+
* on Enter - so no cache is needed, which is what CR-006 section 2 was uncertain about.
|
|
33
|
+
*/
|
|
34
|
+
export declare function bindSnapshot<T>(element: Jsonix.TypedNamedValue<T>): Promise<BoundSnapshot>;
|
|
35
|
+
/** Turns selected nodes into `Bound`s against a snapshot. */
|
|
36
|
+
export declare function boundsOf(nodes: readonly Node[], snapshot: BoundSnapshot): Bound[];
|
|
37
|
+
/**
|
|
38
|
+
* The node's path from the document element, in the prefixed form the part itself uses, with a
|
|
39
|
+
* position on every step: `/w:document/w:body/w:p[3]/w:r[2]`, and `/@w:val` for an attribute. The
|
|
40
|
+
* position counts siblings of the same qualified name, as XPath does, and is written even when
|
|
41
|
+
* there is only one - a path that is stable whether or not a sibling is added later is worth more
|
|
42
|
+
* than a short one, and docx4j's harness records the same form.
|
|
43
|
+
*/
|
|
44
|
+
export declare function pathOf(node: Node): string;
|