@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.
Files changed (48) hide show
  1. package/README.md +76 -0
  2. package/dist/model/content/Body.d.mts +36 -2
  3. package/dist/model/content/Body.mjs +79 -4
  4. package/dist/model/content/Comment.d.mts +15 -5
  5. package/dist/model/content/Comment.mjs +70 -22
  6. package/dist/model/content/List.d.mts +80 -1
  7. package/dist/model/content/List.mjs +140 -22
  8. package/dist/model/content/Paragraph.d.mts +11 -1
  9. package/dist/model/content/Paragraph.mjs +18 -7
  10. package/dist/model/content/Range.d.mts +29 -2
  11. package/dist/model/content/Range.mjs +137 -7
  12. package/dist/model/content/binder.d.mts +44 -0
  13. package/dist/model/content/binder.mjs +95 -0
  14. package/dist/model/content/comments.d.mts +19 -1
  15. package/dist/model/content/index.d.mts +6 -4
  16. package/dist/model/content/index.mjs +4 -2
  17. package/dist/model/content/search.d.mts +27 -2
  18. package/dist/model/content/search.mjs +59 -3
  19. package/dist/model/content/stylesFacade.d.mts +37 -0
  20. package/dist/model/content/stylesFacade.mjs +114 -0
  21. package/dist/model/content/tracking.d.mts +57 -2
  22. package/dist/model/content/tracking.mjs +47 -15
  23. package/dist/model/customxml/CustomXmlPartCollection.d.mts +7 -0
  24. package/dist/model/customxml/CustomXmlPartCollection.mjs +40 -6
  25. package/dist/model/customxml/xpath.d.mts +18 -3
  26. package/dist/model/customxml/xpath.mjs +18 -3
  27. package/dist/office-js/index.d.mts +3 -1
  28. package/dist/office-js/run.d.mts +12 -2
  29. package/dist/office-js/run.mjs +8 -1
  30. package/dist/office-js/shimmed.d.mts +46 -0
  31. package/dist/office-js/shimmed.mjs +1 -0
  32. package/dist/office-js/supported.generated.mjs +1 -0
  33. package/dist/opc/exceptions.d.mts +25 -0
  34. package/dist/opc/exceptions.mjs +20 -0
  35. package/dist/opc/index.d.mts +1 -1
  36. package/dist/opc/index.mjs +1 -1
  37. package/dist/opc/mce/understood.mjs +37 -8
  38. package/dist/packages/WordprocessingMLPackage.d.mts +101 -2
  39. package/dist/packages/WordprocessingMLPackage.mjs +155 -1
  40. package/dist/parts/XmlPart.d.mts +44 -0
  41. package/dist/parts/XmlPart.mjs +86 -3
  42. package/dist/parts/sml/index.d.mts +9 -8
  43. package/dist/parts/sml/index.mjs +23 -19
  44. package/dist/parts/wml/index.d.mts +10 -2
  45. package/dist/parts/wml/index.mjs +11 -3
  46. package/dist/parts/wml/spliceableStyles.d.mts +1 -0
  47. package/dist/parts/wml/spliceableStyles.mjs +122 -0
  48. 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, _c;
402
- const numbering = this.live();
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(created.numId), this.body);
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, _c, _d, _e;
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 : [])[(options === null || options === void 0 ? void 0 : options.bullet) === true ? 0 : 1];
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 = nextAbstractNumId(numbering);
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: nextNumId(numbering),
688
- abstractNumId: f.createNumberingNumAbstractNumId({ val: abstract.abstractNumId }),
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
- (_d = packageOf(body)) === null || _d === void 0 ? void 0 : _d.refreshPropertyResolver();
693
- return attachToList(paragraph, num.numId, (_e = options === null || options === void 0 ? void 0 : options.level) !== null && _e !== void 0 ? _e : 0);
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(text: string): Promise<Comment>;
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, findAll } from './search.mjs';
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 findAll(this.text, searchPattern(text, options)).map(([s, e]) => new Range(this, s, e));
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.search(find, options);
348
- for (let i = matches.length - 1; i >= 0; i--)
349
- matches[i].insertText(replace, 'Replace');
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(text) {
676
- return commentApi().insertComment(this.getRange(), text);
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
- /** Replaces every match in this span, last first so the offsets stay valid; returns the count (CR-002 section 3.7). */
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(text: string): Promise<Comment>;
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
- /** Replaces every match in this span, last first so the offsets stay valid; returns the count (CR-002 section 3.7). */
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 matches = this.search(find, options);
155
- for (let i = matches.length - 1; i >= 0; i--)
156
- matches[i].insertText(replace, 'Replace');
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(text) {
190
- return commentApi().insertComment(this, text);
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;