@docx4j/core-ts 0.1.5 → 0.2.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.
Files changed (49) hide show
  1. package/README.md +81 -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 +13 -2
  7. package/dist/model/content/List.mjs +48 -9
  8. package/dist/model/content/Paragraph.d.mts +11 -1
  9. package/dist/model/content/Paragraph.mjs +37 -21
  10. package/dist/model/content/Range.d.mts +36 -5
  11. package/dist/model/content/Range.mjs +97 -26
  12. package/dist/model/content/binder.d.mts +44 -0
  13. package/dist/model/content/binder.mjs +128 -0
  14. package/dist/model/content/comments.d.mts +19 -1
  15. package/dist/model/content/index.d.mts +5 -3
  16. package/dist/model/content/index.mjs +3 -1
  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/opc/exceptions.d.mts +25 -0
  33. package/dist/opc/exceptions.mjs +20 -0
  34. package/dist/opc/index.d.mts +1 -1
  35. package/dist/opc/index.mjs +1 -1
  36. package/dist/opc/mce/understood.mjs +37 -8
  37. package/dist/packages/WordprocessingMLPackage.d.mts +105 -3
  38. package/dist/packages/WordprocessingMLPackage.mjs +165 -2
  39. package/dist/parts/XmlPart.d.mts +44 -0
  40. package/dist/parts/XmlPart.mjs +86 -3
  41. package/dist/parts/sml/index.d.mts +9 -8
  42. package/dist/parts/sml/index.mjs +23 -19
  43. package/dist/parts/wml/index.d.mts +10 -2
  44. package/dist/parts/wml/index.mjs +11 -3
  45. package/dist/parts/wml/spliceableStyles.d.mts +1 -0
  46. package/dist/parts/wml/spliceableStyles.mjs +122 -0
  47. package/dist/xml/dom.d.mts +10 -1
  48. package/dist/xml/dom.mjs +46 -2
  49. package/package.json +2 -2
@@ -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). */
@@ -516,11 +527,8 @@ export class Paragraph {
516
527
  const value = seg.text.substring(0, from) + middle + seg.text.substring(to);
517
528
  if (value === '')
518
529
  toRemove.push(seg);
519
- else {
520
- t.value = value;
521
- if (/^\s|\s$|\s\s/.test(value))
522
- t.space = 'preserve';
523
- }
530
+ else
531
+ setText(t, value);
524
532
  }
525
533
  else
526
534
  toRemove.push(seg);
@@ -543,9 +551,7 @@ export class Paragraph {
543
551
  if (target) {
544
552
  const t = target.item.value;
545
553
  const at = start - target.start;
546
- t.value = target.text.substring(0, at) + text + target.text.substring(at);
547
- if (/^\s|\s$|\s\s/.test(t.value))
548
- t.space = 'preserve';
554
+ setText(t, target.text.substring(0, at) + text + target.text.substring(at));
549
555
  }
550
556
  else {
551
557
  // no w:t to extend: a new run next to the segment at `start`, with that run's formatting
@@ -602,9 +608,7 @@ export class Paragraph {
602
608
  const t = seg.item.value;
603
609
  const head = seg.text.substring(0, at);
604
610
  const tail = seg.text.substring(at);
605
- t.value = head;
606
- if (/^\s|\s$|\s\s/.test(head))
607
- t.space = 'preserve';
611
+ setText(t, head);
608
612
  const rest = seg.owner.splice(seg.index + 1);
609
613
  const second = runOf([textItem(tail), ...rest], seg.run.rPr ? deepCopy(seg.run.rPr) : undefined);
610
614
  seg.runOwner.splice(seg.runIndex + 1, 0, second);
@@ -672,8 +676,8 @@ export class Paragraph {
672
676
  return commentApi().commentsOf(this);
673
677
  }
674
678
  /** Comments the whole paragraph (sugar over `getRange().insertComment`). */
675
- async insertComment(text) {
676
- return commentApi().insertComment(this.getRange(), text);
679
+ async insertComment(content, options) {
680
+ return commentApi().insertComment(this.getRange(), content, options);
677
681
  }
678
682
  // --- content controls (CR-002 phase E) ---
679
683
  /**
@@ -796,9 +800,7 @@ export class Paragraph {
796
800
  continue;
797
801
  if (seg.editable) {
798
802
  const t = seg.item.value;
799
- t.value = side === 'after' ? seg.text + text : text + seg.text;
800
- if (/^\s|\s$|\s\s/.test(t.value))
801
- t.space = 'preserve';
803
+ setText(t, side === 'after' ? seg.text + text : text + seg.text);
802
804
  }
803
805
  else {
804
806
  const run = runOf([textItem(text)], copyRPr(seg.run.rPr));
@@ -916,3 +918,17 @@ function outlineLevelOf(pPr) {
916
918
  const level = (_a = pPr === null || pPr === void 0 ? void 0 : pPr.outlineLvl) === null || _a === void 0 ? void 0 : _a.val;
917
919
  return level === undefined ? 10 : level + 1;
918
920
  }
921
+ /**
922
+ * Sets a `w:t`'s text with `xml:space="preserve"` exactly when the text needs it - whitespace at
923
+ * either end, or two whitespace characters together - which is how the builders' `t()` and Word
924
+ * write it. Setting the attribute and never taking it off left a text shortened from "Font styles "
925
+ * to "Font X" still preserving, the same text but not the same bytes as a `w:t` made afresh
926
+ * (CR-002 section 32 item 2).
927
+ */
928
+ function setText(t, value) {
929
+ t.value = value;
930
+ if (/^\s|\s$|\s\s/.test(value))
931
+ t.space = 'preserve';
932
+ else
933
+ delete t.space;
934
+ }
@@ -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,7 +73,7 @@ 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
@@ -85,14 +90,40 @@ export declare class Range {
85
90
  * Setting removes the hyperlinks the range already carries and wraps its runs in one
86
91
  * `w:hyperlink`, splitting runs at the boundaries as `font` and `insertContentControl` do;
87
92
  * setting `""` removes them and wraps nothing. An external address becomes a relationship of
88
- * this range's part, which is why the part must exist (CR-002 section 20).
93
+ * this range's part, which is why the part must exist (CR-002 section 20). Only the hyperlinks
94
+ * the range touches are removed; before 0.2.1 every one in the paragraph was (section 25).
95
+ *
96
+ * The runs are styled as Word styles them, measured through Office JS (`test/README.md` check
97
+ * 15, CR-002 section 25): each run a link wraps takes the `Hyperlink` character style, over any
98
+ * character style it had, and loses its direct colour, keeping the rest of its direct formatting;
99
+ * a link removed takes the style off its runs again. The `Hyperlink` definition is added to the
100
+ * styles part when the document lacks it, at the latest when the package is saved - a setter
101
+ * cannot await it - so `await pkg.styles.ensure('Hyperlink')` first where an effective read
102
+ * before the save must see it. Under change tracking the restyle is a formatting revision, as a
103
+ * `font` write is.
89
104
  */
90
105
  get hyperlink(): string;
91
106
  set hyperlink(value: string);
92
- /** The `w:hyperlink` this range's first run sits in, if any. */
107
+ /** The first `w:hyperlink` this range touches, if any. */
93
108
  private hyperlinkHolder;
94
- /** Unwraps every `w:hyperlink` the range touches, keeping its runs where they are. */
109
+ /**
110
+ * Unwraps every `w:hyperlink` the range touches, keeping its runs where they are and taking the
111
+ * `Hyperlink` style off them. Until 0.2.1 this unwrapped every hyperlink of the paragraph, so
112
+ * linking one word unlinked the others (CR-002 section 25).
113
+ */
95
114
  private removeHyperlinks;
115
+ /**
116
+ * Whether this range touches a hyperlink's text: overlaps it, or for an empty range lies in it
117
+ * (from its first character up to, not including, its end). A hyperlink holding no text touches
118
+ * nothing.
119
+ */
120
+ private touches;
121
+ /**
122
+ * The `Hyperlink` character style on every run of a hyperlink, in place of any character style
123
+ * and with the direct colour removed, or off again when the link goes: what Word does (CR-002
124
+ * section 25). A run whose properties are then empty loses its `w:rPr`, as in Word's output.
125
+ */
126
+ private restyle;
96
127
  /** The target of a relationship of this range's part, for the getter. */
97
128
  private relationshipTarget;
98
129
  /** An external relationship of this range's part for the address, and its id. */
@@ -1,14 +1,14 @@
1
1
  import { Font } from './Font.mjs';
2
2
  import { runFontSelectorOf } from '../fonts/lookup.mjs';
3
- import { typeNameOf, linkParents, runItemsOf } from './tree.mjs';
3
+ import { typeNameOf, linkParents, runItemsOf, runsOf } from './tree.mjs';
4
4
  import { Docx4JException } from '../../opc/exceptions.mjs';
5
5
  import { ContentControl } from './ContentControl.mjs';
6
- import { sdt as sdtOf, nextSdtId } from '@docx4j/generated-objects-ts/builders/wml';
6
+ import { sdt as sdtOf, nextSdtId, applyRunOptions } from '@docx4j/generated-objects-ts/builders/wml';
7
7
  import { hyperlink as hyperlinkOf } from '@docx4j/generated-objects-ts/el/org_docx4j_wml';
8
8
  import { Namespaces } from '../../parts/Namespaces.mjs';
9
9
  import { controlIdScope, sdtKindFor } from '../customxml/insert.mjs';
10
10
  import { contentOf } from './ooxml.mjs';
11
- import { searchPattern, findAll } from './search.mjs';
11
+ import { searchPattern, findAll, matchesOf, expandReplacement } from './search.mjs';
12
12
  import { commentApi } from './comments.mjs';
13
13
  /**
14
14
  * A subset of Office JS `Word.Range`: a span of text within one paragraph, [start, end) in the
@@ -151,11 +151,19 @@ export class Range {
151
151
  const base = this.start;
152
152
  return findAll(this.text, searchPattern(text, options)).map(([s, e]) => new Range(this.paragraph, base + s, base + e));
153
153
  }
154
- /** 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
+ */
155
159
  replaceText(find, replace, options) {
156
- const matches = this.search(find, options);
157
- for (let i = matches.length - 1; i >= 0; i--)
158
- 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
+ }
159
167
  return matches.length;
160
168
  }
161
169
  /** The tracked changes this span covers, in document order (CR-002 phase F). */
@@ -188,8 +196,8 @@ export class Range {
188
196
  * boundaries, as `font` does), the comment itself and the side-part entries, creating any of
189
197
  * the comment parts the document lacks. Asynchronous because it unmarshals those parts.
190
198
  */
191
- async insertComment(text) {
192
- return commentApi().insertComment(this, text);
199
+ async insertComment(content, options) {
200
+ return commentApi().insertComment(this, content, options);
193
201
  }
194
202
  // --- content controls (CR-002 phase E) ---
195
203
  /**
@@ -239,7 +247,17 @@ export class Range {
239
247
  * Setting removes the hyperlinks the range already carries and wraps its runs in one
240
248
  * `w:hyperlink`, splitting runs at the boundaries as `font` and `insertContentControl` do;
241
249
  * setting `""` removes them and wraps nothing. An external address becomes a relationship of
242
- * this range's part, which is why the part must exist (CR-002 section 20).
250
+ * this range's part, which is why the part must exist (CR-002 section 20). Only the hyperlinks
251
+ * the range touches are removed; before 0.2.1 every one in the paragraph was (section 25).
252
+ *
253
+ * The runs are styled as Word styles them, measured through Office JS (`test/README.md` check
254
+ * 15, CR-002 section 25): each run a link wraps takes the `Hyperlink` character style, over any
255
+ * character style it had, and loses its direct colour, keeping the rest of its direct formatting;
256
+ * a link removed takes the style off its runs again. The `Hyperlink` definition is added to the
257
+ * styles part when the document lacks it, at the latest when the package is saved - a setter
258
+ * cannot await it - so `await pkg.styles.ensure('Hyperlink')` first where an effective read
259
+ * before the save must see it. Under change tracking the restyle is a formatting revision, as a
260
+ * `font` write is.
243
261
  */
244
262
  get hyperlink() {
245
263
  var _a;
@@ -251,7 +269,7 @@ export class Range {
251
269
  return anchor === '' ? address : `${address}#${anchor}`;
252
270
  }
253
271
  set hyperlink(value) {
254
- var _a;
272
+ var _a, _b, _c;
255
273
  const paragraph = this.paragraph;
256
274
  this.removeHyperlinks();
257
275
  if (value === '')
@@ -259,7 +277,12 @@ export class Range {
259
277
  const hash = value.indexOf('#');
260
278
  const address = hash === -1 ? value : value.slice(0, hash);
261
279
  const location = hash === -1 ? '' : value.slice(hash + 1);
262
- const holder = hyperlinkOf({ content: [] });
280
+ // w:history="1" is what Word writes on every hyperlink it makes, and what docx4j writes when
281
+ // it builds one (MainDocumentPart.addHyperlink): "add the target to the viewed hyperlinks"
282
+ // (ECMA-376 17.16.22). Readers do not depend on it, but a document holding one holder from
283
+ // each source should not differ over it. Found by the docx4j-ts-editor session, ED-003
284
+ // section 11.8.
285
+ const holder = hyperlinkOf({ content: [], history: true });
263
286
  if (location !== '')
264
287
  holder.value.anchor = location;
265
288
  if (address !== '')
@@ -287,35 +310,73 @@ export class Range {
287
310
  holder.value.content = items;
288
311
  owner.splice(at, items.length, holder);
289
312
  linkParents(holder, (_a = segments[0].run.PARENT) !== null && _a !== void 0 ? _a : paragraph.p);
313
+ this.restyle(holder.value, true);
314
+ (_c = (_b = paragraph.parentBody.package_) === null || _b === void 0 ? void 0 : _b.requireStyles) === null || _c === void 0 ? void 0 : _c.call(_b, [HYPERLINK_STYLE]);
290
315
  }
291
- /** The `w:hyperlink` this range's first run sits in, if any. */
316
+ /** The first `w:hyperlink` this range touches, if any. */
292
317
  hyperlinkHolder() {
293
- for (const segment of this.paragraph.segments()) {
294
- if (segment.end <= this.start && this.start !== this.end)
295
- continue;
296
- if (segment.start >= this.end && this.start !== this.end)
297
- break;
298
- const parent = segment.run.PARENT;
299
- const found = hyperlinkHolders(this.paragraph).find((h) => h.value === parent);
300
- if (found)
301
- return found;
302
- }
303
- return undefined;
318
+ return hyperlinkHolders(this.paragraph).find((holder) => this.touches(holder));
304
319
  }
305
- /** Unwraps every `w:hyperlink` the range touches, keeping its runs where they are. */
320
+ /**
321
+ * Unwraps every `w:hyperlink` the range touches, keeping its runs where they are and taking the
322
+ * `Hyperlink` style off them. Until 0.2.1 this unwrapped every hyperlink of the paragraph, so
323
+ * linking one word unlinked the others (CR-002 section 25).
324
+ */
306
325
  removeHyperlinks() {
307
326
  var _a, _b, _c;
308
- for (const holder of hyperlinkHolders(this.paragraph)) {
327
+ for (const holder of hyperlinkHolders(this.paragraph).filter((h) => this.touches(h))) {
309
328
  const owner = containerOf(holder, this.paragraph);
310
329
  const at = owner.indexOf(holder);
311
330
  if (at === -1)
312
331
  continue;
332
+ this.restyle(holder.value, false);
313
333
  owner.splice(at, 1, ...((_a = holder.value.content) !== null && _a !== void 0 ? _a : []));
314
334
  for (const item of ((_b = holder.value.content) !== null && _b !== void 0 ? _b : [])) {
315
335
  linkParents(item, (_c = holder.PARENT) !== null && _c !== void 0 ? _c : this.paragraph.p);
316
336
  }
317
337
  }
318
338
  }
339
+ /**
340
+ * Whether this range touches a hyperlink's text: overlaps it, or for an empty range lies in it
341
+ * (from its first character up to, not including, its end). A hyperlink holding no text touches
342
+ * nothing.
343
+ */
344
+ touches(holder) {
345
+ let start;
346
+ let end = 0;
347
+ for (const segment of this.paragraph.segments()) {
348
+ if (!within(segment.run, holder.value, this.paragraph.p))
349
+ continue;
350
+ start !== null && start !== void 0 ? start : (start = segment.start);
351
+ end = segment.end;
352
+ }
353
+ if (start === undefined)
354
+ return false;
355
+ return this.start === this.end ? start <= this.start && this.start < end : start < this.end && end > this.start;
356
+ }
357
+ /**
358
+ * The `Hyperlink` character style on every run of a hyperlink, in place of any character style
359
+ * and with the direct colour removed, or off again when the link goes: what Word does (CR-002
360
+ * section 25). A run whose properties are then empty loses its `w:rPr`, as in Word's output.
361
+ */
362
+ restyle(holder, linked) {
363
+ var _a, _b, _c;
364
+ const tracking = this.paragraph.fontTracking();
365
+ const runs = new Set([...runsOf(holder), ...runsOf(holder, { view: 'original' })].map((r) => r.value));
366
+ for (const run of runs) {
367
+ if (!linked && ((_b = (_a = run.rPr) === null || _a === void 0 ? void 0 : _a.rStyle) === null || _b === void 0 ? void 0 : _b.val) !== HYPERLINK_STYLE)
368
+ continue;
369
+ (_c = run.rPr) !== null && _c !== void 0 ? _c : (run.rPr = { TYPE_NAME: 'org_docx4j_wml.RPr' });
370
+ // the properties as they are now become w:rPrChange, unless this run is our own insertion
371
+ if (tracking && !tracking.ownInsertions.has(run))
372
+ tracking.tracker.recordRPrChange(run.rPr);
373
+ applyRunOptions(run.rPr, linked ? { style: HYPERLINK_STYLE, color: '' } : { style: '' });
374
+ if (Object.keys(run.rPr).every((key) => key === 'TYPE_NAME'))
375
+ delete run.rPr;
376
+ else
377
+ linkParents(run.rPr, run);
378
+ }
379
+ }
319
380
  /** The target of a relationship of this range's part, for the getter. */
320
381
  relationshipTarget(id) {
321
382
  var _a, _b, _c;
@@ -330,6 +391,16 @@ export class Range {
330
391
  return part.getRelationshipsPart(true).addExternalRelationship(Namespaces.HYPERLINK, address).id;
331
392
  }
332
393
  }
394
+ /** The character style Word gives the runs of a hyperlink it makes. */
395
+ const HYPERLINK_STYLE = 'Hyperlink';
396
+ /** Whether `object` is `ancestor` or below it, following `PARENT` no further than `stop`. */
397
+ function within(object, ancestor, stop) {
398
+ for (let o = object; o !== undefined && o !== stop; o = o.PARENT) {
399
+ if (o === ancestor)
400
+ return true;
401
+ }
402
+ return false;
403
+ }
333
404
  /** Every `w:hyperlink` of a paragraph, outermost first (they do not nest in practice). */
334
405
  function hyperlinkHolders(paragraph) {
335
406
  const out = [];
@@ -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, with one path cache for them all. */
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;
@@ -0,0 +1,128 @@
1
+ // CR-006: XPath over the WordprocessingML tree, through a read-only binder recorded while marshalling.
2
+ //
3
+ // docx4j answers `getJAXBNodesViaXPath` with a JAXB `Binder<Node>`, which keeps the link between a
4
+ // DOM node and the object it came from in both directions. This runtime has no binder, so CR-002
5
+ // section 3.6 left XPath over the typed tree out; the demand appeared (the editor's Developer
6
+ // perspective selects by XPath) and this is the cheap half - DOM node to object, which is what a
7
+ // selection needs. Nothing is written back through the DOM (CR-006 section 7).
8
+ //
9
+ // The link is recorded by `@docx4j/jsonix` 3.4.0's `onElement` callback, reached through the objects
10
+ // facade's `marshalNode(value, { onElement })` (objects CR-007): the runtime calls it once per
11
+ // class-typed value with the element it was written into, and once per wildcard DOM child with the
12
+ // **imported copy** of that child - `writeNode` does `importNode`, so the element in the snapshot is
13
+ // not the node in the tree and a map keyed on it would miss exactly the content a caller most wants
14
+ // (jsonix-CR-006 section 2, finding 3).
15
+ import { marshalNode } from '@docx4j/generated-objects-ts';
16
+ /**
17
+ * Marshals a root element and records what each DOM element came from. One marshal per call: the
18
+ * tree has no change log a cached snapshot could be checked against, and a stale answer is worse
19
+ * than a slow one (docx4j's `refreshXmlFirst`, always true here). The editor measured 349 to 411 ms
20
+ * for a 255-page main document part, against 975 ms for its first unmarshal, and runs a selection
21
+ * on Enter - so no cache is needed, which is what CR-006 section 2 was uncertain about.
22
+ */
23
+ export async function bindSnapshot(element) {
24
+ const objects = new WeakMap();
25
+ const root = await marshalNode(element, {
26
+ onElement: (node, value) => {
27
+ if (isElement(node))
28
+ objects.set(node, value);
29
+ },
30
+ });
31
+ return { root, objects };
32
+ }
33
+ /** Turns selected nodes into `Bound`s against a snapshot, with one path cache for them all. */
34
+ export function boundsOf(nodes, snapshot) {
35
+ const paths = new Paths();
36
+ return nodes.map((node) => {
37
+ const owner = owningElement(node);
38
+ const bound = {
39
+ node,
40
+ object: owner === undefined ? undefined : snapshot.objects.get(owner),
41
+ path: paths.of(node),
42
+ };
43
+ if (node.nodeType === 2)
44
+ bound.attribute = node.name;
45
+ return bound;
46
+ });
47
+ }
48
+ /**
49
+ * The element an XPath result belongs to: itself for an element, its owner for an attribute, its
50
+ * parent for a text, comment or processing-instruction node. A document node has none.
51
+ */
52
+ function owningElement(node) {
53
+ if (node.nodeType === 1)
54
+ return node;
55
+ if (node.nodeType === 2) {
56
+ const owner = node.ownerElement;
57
+ return owner === null ? undefined : owner;
58
+ }
59
+ const parent = node.parentNode;
60
+ return parent !== null && parent.nodeType === 1 ? parent : undefined;
61
+ }
62
+ /**
63
+ * The node's path from the document element, in the prefixed form the part itself uses, with a
64
+ * position on every step: `/w:document/w:body/w:p[3]/w:r[2]`, and `/@w:val` for an attribute. The
65
+ * position counts siblings of the same qualified name, as XPath does, and is written even when
66
+ * there is only one - a path that is stable whether or not a sibling is added later is worth more
67
+ * than a short one, and docx4j's harness records the same form.
68
+ */
69
+ export function pathOf(node) {
70
+ return new Paths().of(node);
71
+ }
72
+ /**
73
+ * Paths for the nodes of one snapshot. Each parent's children are counted once, on first need,
74
+ * and each element's path is kept for its descendants, so a hit list costs one pass over the
75
+ * parents it touches. Counting a node's preceding siblings afresh for every node and every
76
+ * ancestor was quadratic in a parent's children: the 6,972 hits of `//w:p[w:pPr/w:pStyle]` over a
77
+ * 7,088-paragraph body took 21 s in Chromium, where the marshal took 0.5 s (CR-006 section 10).
78
+ */
79
+ class Paths {
80
+ constructor() {
81
+ /** For a parent, the position of each element child among its siblings of the same name. */
82
+ this.positions = new Map();
83
+ this.elementPaths = new Map();
84
+ }
85
+ of(node) {
86
+ if (node.nodeType === 2) {
87
+ const owner = node.ownerElement;
88
+ return `${owner === null ? '' : this.of(owner)}/@${node.name}`;
89
+ }
90
+ if (node.nodeType === 3 || node.nodeType === 4) {
91
+ const parent = node.parentNode;
92
+ return `${parent === null ? '' : this.of(parent)}/text()`;
93
+ }
94
+ if (node.nodeType !== 1)
95
+ return '';
96
+ const known = this.elementPaths.get(node);
97
+ if (known !== undefined)
98
+ return known;
99
+ const element = node;
100
+ const parent = element.parentNode;
101
+ const path = parent === null || parent.nodeType !== 1
102
+ ? `/${element.nodeName}`
103
+ : `${this.of(parent)}/${element.nodeName}[${this.position(parent, element)}]`;
104
+ this.elementPaths.set(node, path);
105
+ return path;
106
+ }
107
+ position(parent, element) {
108
+ var _a, _b;
109
+ let positions = this.positions.get(parent);
110
+ if (positions === undefined) {
111
+ positions = new Map();
112
+ const counts = new Map();
113
+ for (let sibling = parent.firstChild; sibling; sibling = sibling.nextSibling) {
114
+ if (sibling.nodeType !== 1)
115
+ continue;
116
+ const name = sibling.nodeName;
117
+ const n = ((_a = counts.get(name)) !== null && _a !== void 0 ? _a : 0) + 1;
118
+ counts.set(name, n);
119
+ positions.set(sibling, n);
120
+ }
121
+ this.positions.set(parent, positions);
122
+ }
123
+ return (_b = positions.get(element)) !== null && _b !== void 0 ? _b : 0;
124
+ }
125
+ }
126
+ function isElement(node) {
127
+ return typeof node === 'object' && node !== null && node.nodeType === 1;
128
+ }
@@ -16,6 +16,24 @@ export declare const COMMENT_REFERENCE_STYLE = "CommentReference";
16
16
  * Who a comment (and, with phase F, a tracked change) is by. `WordprocessingMLPackage.author`;
17
17
  * there is no signed-in user here, so the package carries the identity.
18
18
  */
19
+ /**
20
+ * What a comment is made of: the text, or the paragraphs of a comment that was edited rich
21
+ * (CR-002 section 28.3). A string splits on newlines, as it did before this option existed.
22
+ */
23
+ export type CommentContent = string | Element<wml.P>[];
24
+ /**
25
+ * Who made a comment and when, and its `w:id`, for a caller that knows better than the package does:
26
+ * in a co-editing room the author is the peer who made it and the date is when they made it
27
+ * (CR-002 section 28.3). Anything omitted falls back to what the package would have used.
28
+ */
29
+ export interface CommentOptions {
30
+ author?: Author | undefined;
31
+ /** Overrides the initials derived from the author's name. */
32
+ initials?: string | undefined;
33
+ date?: Date | undefined;
34
+ /** Checked unused across the comments part and the story's markers; a clash throws. */
35
+ id?: number | undefined;
36
+ }
19
37
  export interface Author {
20
38
  name: string;
21
39
  /** Word's `w:initials`; derived from the name when not given. */
@@ -69,7 +87,7 @@ export declare function commentPartsAccess(): CommentPartsAccess;
69
87
  /** How `Body`, `Paragraph` and `Range` reach the `Comment` views; registered by `Comment.mts`. */
70
88
  export interface CommentApi {
71
89
  commentsOf(scope: Body | Paragraph | Range): Promise<Comment[]>;
72
- insertComment(range: Range, text: string): Promise<Comment>;
90
+ insertComment(range: Range, content: CommentContent, options?: CommentOptions): Promise<Comment>;
73
91
  }
74
92
  export declare function setCommentApi(implementation: CommentApi): void;
75
93
  export declare function commentApi(): CommentApi;
@@ -4,14 +4,16 @@ export { Range } from './Range.mjs';
4
4
  export { BUILT_IN_STYLES, builtInOf, idOfBuiltIn, displayNameOf, styleNameOf, styleIdOf, type BuiltInStyle } from './styles.mjs';
5
5
  export { Comment } from './Comment.mjs';
6
6
  export { List, ListItem, ListItemNotFoundError, NumberingFacade, listsOf, listLabelsOf, numberingEmulatorOf, levelOf, type ListLevelType, type ListNumbering, type ListBullet, type StartListOptions, type ListLabel, } from './List.mjs';
7
- export { type Author, type CommentParts, type CommentPartsAccess, type CommentsExtensible, type CommentMarker, COMMENT_TEXT_STYLE, COMMENT_REFERENCE_STYLE, markersOf, markersOfParagraph, commentIdsOf } from './comments.mjs';
7
+ export { type Author, type CommentContent, type CommentOptions, type CommentParts, type CommentPartsAccess, type CommentsExtensible, type CommentMarker, COMMENT_TEXT_STYLE, COMMENT_REFERENCE_STYLE, markersOf, markersOfParagraph, commentIdsOf } from './comments.mjs';
8
8
  export { Font, type UnderlineType, type RPrHolder, type FontTracking } from './Font.mjs';
9
9
  export { Table, TableRow, TableCell, cellOf } from './Table.mjs';
10
10
  export { InlinePicture, imageInfoOf, naturalSizeEmu, addImage, writableWidthEmu, type ImageFormat, type ImageInfo, type InlinePictureOptions, type NewPicture } from './InlinePicture.mjs';
11
11
  export { ContentControl, collectControls, collectRunControls, W14_NS, W15_NS, type ContentControlType, type ContentControlForm, type ContentControlAppearance } from './ContentControl.mjs';
12
12
  export { contentOf, isFlatOpc, rewriteRelationshipIds, type OoxmlOptions } from './ooxml.mjs';
13
- export { type SearchOptions, searchPattern } from './search.mjs';
13
+ export { type SearchOptions, searchPattern, findAll, matchesOf, expandReplacement } from './search.mjs';
14
14
  export { segmentsOf, runsOf, runItemsOf, childrenOf, rowsOf, cellsOf, BLOCK_LEVEL_TYPES, SDT_TYPES, runOf, paragraphOf, textOfView, revisionKindOf, type TextSegment, type Located, type TextView, type TextViewOptions, type RevisionKind, type RevisionHolder } from './tree.mjs';
15
15
  export { TrackedChange, trackedChangesOfParagraph, trackedChangesOfRow, joinWithNext, type TrackedChangeType, type TrackedChangeTarget } from './TrackedChange.mjs';
16
- export { ChangeTracker, trackerOf, calendarOf, dateOf, copyRPr, markDeleted, markInserted, toDeletedText, toRestoredText, restoreRPr, restorePPr, trackInsertedParagraph, trackInsertedTable, wrapNewRuns, paraRPrOf, rowPrOf, pruneParagraphProperties, type ChangeTrackingMode, type TrackingHost, } from './tracking.mjs';
16
+ export { ChangeTracker, trackerOf, calendarOf, dateOf, copyRPr, markDeleted, markInserted, toDeletedText, toRestoredText, restoreRPr, restorePPr, trackInsertedParagraph, trackInsertedTable, wrapNewRuns, paraRPrOf, rowPrOf, pruneParagraphProperties, type ChangeTrackingMode, type TrackingHost, type TrackingScope, } from './tracking.mjs';
17
17
  export { wml, wmlOne, p, r, t, br, tab, tbl, tr, tc, sdt, sdtPr, nextSdtId, sdtProperty, sdtKindOf, inlinePicture, rPrToElements, rPrFromElements, textOf, walk, walkAll, find, linkParents, applyRunOptions, readRunOptions, UNDERLINE_TO_WML, isElement, typeNameOf, W_NS, type Element, type Wrapper, type WmlOptions, type Raw, type Interpolated, type RunOptions, type RunFormatting, type ParagraphOptions, type TableOptions, type CellOptions, type SdtKind, type SdtForm, type SdtOptions } from '@docx4j/generated-objects-ts/builders/wml';
18
+ export { StylesFacade, type StylePackageLike } from './stylesFacade.mjs';
19
+ export { bindSnapshot, boundsOf, pathOf, type Bound, type BoundSnapshot } from './binder.mjs';
@@ -12,10 +12,12 @@ export { Table, TableRow, TableCell, cellOf } from './Table.mjs';
12
12
  export { InlinePicture, imageInfoOf, naturalSizeEmu, addImage, writableWidthEmu } from './InlinePicture.mjs';
13
13
  export { ContentControl, collectControls, collectRunControls, W14_NS, W15_NS } from './ContentControl.mjs';
14
14
  export { contentOf, isFlatOpc, rewriteRelationshipIds } from './ooxml.mjs';
15
- export { searchPattern } from './search.mjs';
15
+ export { searchPattern, findAll, matchesOf, expandReplacement } from './search.mjs';
16
16
  export { segmentsOf, runsOf, runItemsOf, childrenOf, rowsOf, cellsOf, BLOCK_LEVEL_TYPES, SDT_TYPES, runOf, paragraphOf, textOfView, revisionKindOf } from './tree.mjs';
17
17
  // change tracking (CR-002 phase F)
18
18
  export { TrackedChange, trackedChangesOfParagraph, trackedChangesOfRow, joinWithNext } from './TrackedChange.mjs';
19
19
  export { ChangeTracker, trackerOf, calendarOf, dateOf, copyRPr, markDeleted, markInserted, toDeletedText, toRestoredText, restoreRPr, restorePPr, trackInsertedParagraph, trackInsertedTable, wrapNewRuns, paraRPrOf, rowPrOf, pruneParagraphProperties, } from './tracking.mjs';
20
20
  // The objects package's builders, re-exported so that one import serves the content API and the tree it works on.
21
21
  export { wml, wmlOne, p, r, t, br, tab, tbl, tr, tc, sdt, sdtPr, nextSdtId, sdtProperty, sdtKindOf, inlinePicture, rPrToElements, rPrFromElements, textOf, walk, walkAll, find, linkParents, applyRunOptions, readRunOptions, UNDERLINE_TO_WML, isElement, typeNameOf, W_NS } from '@docx4j/generated-objects-ts/builders/wml';
22
+ export { StylesFacade } from './stylesFacade.mjs';
23
+ export { bindSnapshot, boundsOf, pathOf } from './binder.mjs';