@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
package/README.md CHANGED
@@ -103,6 +103,25 @@ p?.insertParagraph('Inserted after the fourth block', 'After');
103
103
  (await pkg.paragraphAt({ contains: 'Chapter 2' }))?.delete();
104
104
  ```
105
105
 
106
+ `search` and `replaceText` take Office JS's `matchCase`, `matchWholeWord` and `matchWildcards`
107
+ (Word's `?`, `*`, `[a-z]`, `<`, `>`, `@` and `(...)` groups, with `\1` in the replacement), and
108
+ `matchRegExp` for an ECMAScript regular expression, with `$1` in the replacement. A match never
109
+ crosses a paragraph.
110
+
111
+ ```ts
112
+ body.replaceText('(\\d+) pages', '$1 pp.', { matchRegExp: true });
113
+ body.replaceText('(quick) (fox)', '\\2 \\1', { matchWildcards: true });
114
+ ```
115
+
116
+ `range.hyperlink = 'https://example.com/'` (or `'#bookmark'` for a place in the document) makes the
117
+ range a link, styled as Word styles one: its runs take the `Hyperlink` character style, over any
118
+ character style they had, and lose their direct colour; `''` removes the links the range touches,
119
+ style and all. A document that lacks the `Hyperlink` definition gets it when it is saved.
120
+
121
+ A style the document lacks is spliced in from docx4j's defaults, with the styles it is based on and
122
+ linked to: `await pkg.styles.ensure(['FootnoteText', 'FootnoteReference'])` returns the ids it
123
+ added, and a document that already has them keeps its styles part byte for byte.
124
+
106
125
  Docx4j's names are there as aliases (`addParagraphOfText`, `addStyledParagraphOfText`,
107
126
  `addObject`, `getContent`), and the tree stays reachable: `paragraph.p` is the `P`,
108
127
  `body.content` the live array.
@@ -173,6 +192,12 @@ list.setLevelStartingNumber(0, 5);
173
192
  const again = p4.restartList(); // the same list, numbering again from its start
174
193
  ```
175
194
 
195
+ Without a paragraph, `pkg.numbering.newList()` makes a definition and returns its `w:numId`, and
196
+ `pkg.numbering.restart(numId)` one that numbers again from the start. Both take the identifiers to
197
+ use when the next free one is the wrong answer - two sessions allocating in one co-edited
198
+ document - checked unused, a clash throwing rather than renumbering:
199
+ `await pkg.numbering.newList({ numId: 4242, abstractNumId: 77 })`.
200
+
176
201
  A level writer changes the `w:abstractNum`, so it copies that definition first when another
177
202
  `w:num` shares it — the change stays local to this list. `startNewList()` is asynchronous here
178
203
  (Office JS's is not): it copies its definition from docx4j's default `numbering.xml` and adds
@@ -208,6 +233,21 @@ body.acceptAll(); // or rejectAll(), or accept()/reject() one at a time
208
233
  `getTrackedChanges()` is also on `Paragraph` and `Range`. Text and searches read the accepted
209
234
  view throughout, so an agent sees the document as it will read once the changes are taken.
210
235
 
236
+ To record one caller's edits without touching the document's own setting - an agent working in a
237
+ document whose `w:trackRevisions` belongs to its owner - use `withTracking`. Nothing is written
238
+ to the settings part, and the author, date and mode are put back when `fn` returns or throws:
239
+
240
+ ```ts
241
+ await pkg.withTracking({ author: { name: 'Agent' } }, () => {
242
+ body.replaceText('colour', 'color');
243
+ });
244
+ ```
245
+
246
+ Calls nest. While two overlap, the later one's author and mode apply to every edit, since nothing
247
+ can tell which asynchronous call an edit came from: await one before starting another when they
248
+ must be recorded as different authors. A revision is dated to the minute, as Word dates them, and
249
+ its id is above every annotation id in the document, including those in parts nobody has read.
250
+
211
251
  ### Comments
212
252
 
213
253
  `getComments()` on the body, a paragraph or a range, and `insertComment` on a range or a
@@ -230,6 +270,15 @@ for (const c of await body.getComments()) { // document order, replies nested
230
270
  }
231
271
  ```
232
272
 
273
+ `insertComment` and `reply` take the author, initials, date and `w:id` when they are not the
274
+ package's - a comment made by another person in a co-edited document - and paragraphs as well as
275
+ a string. A chosen id is checked unused, and a clash throws:
276
+
277
+ ```ts
278
+ await hit.insertComment('Checked', { author: { name: 'Grace Hopper' }, date: new Date('2026-09-27T10:15:00Z'), id: 42 });
279
+ await hit.insertComment([p1, p2]); // w:p elements, for rich content
280
+ ```
281
+
233
282
  ### XML in
234
283
 
235
284
  A whole `word/document.xml` becomes the main document part's contents with `setXml`, and a
@@ -253,6 +302,33 @@ import * as el from '@docx4j/generated-objects-ts/el/org_docx4j_wml';
253
302
  body.addObject(el.p({ content: [el.r({ content: [el.t({ value: 'Hello World' })] })] }));
254
303
  ```
255
304
 
305
+ A part that cannot be unmarshalled throws `PartUnmarshalException`, whose message names the part
306
+ and, where the parser supplies a position (xmldom in Node; not a browser), the element or attribute
307
+ and its line: `/word/styles.xml: w:sz/@w:val (line 7): Argument [NaN] must be an integer, but it is not a number.`
308
+ `partName`, `location`, `line` and `column` are fields of their own.
309
+
310
+ ### XPath over the document
311
+
312
+ `pkg.selectObjects(xpath)` evaluates an XPath over a part - the main document part unless you pass
313
+ another - and answers as docx4j's `getJAXBNodesViaXPath` does: for each hit, the node, the tree
314
+ object behind it and its path. `body.select(xpath)` gives the content-API view instead, where the
315
+ element has one (`Paragraph`, `Table`, `TableRow`, `TableCell`, `ContentControl`, a `Range` for a
316
+ run):
317
+
318
+ ```ts
319
+ const hits = await pkg.selectObjects("//w:p[w:pPr/w:pStyle/@w:val='Heading1']");
320
+ hits[0].object; // the P itself, not a copy: change it and the document changes
321
+ hits[0].path; // '/w:document/w:body[1]/w:p[3]'
322
+
323
+ for (const heading of await body.select("//w:p[w:pPr/w:pStyle/@w:val='Heading1']")) {
324
+ if (heading instanceof Paragraph) heading.styleBuiltIn = 'Heading2';
325
+ }
326
+ ```
327
+
328
+ The prefixes are the objects package's (`w`, `r`, `wp`, `a`, ...), and `namespaces` adds others.
329
+ Each call evaluates a fresh snapshot of the part, so what it selects is never stale and changing the
330
+ snapshot's DOM changes nothing. The answers are held to docx4j's own by the XPath goldens (CR-006).
331
+
256
332
  ### Tables, pictures and content controls
257
333
 
258
334
  `Table`, `TableRow` and `TableCell` are Office JS shapes too; a cell is a `Body` of its own, so
@@ -350,6 +426,7 @@ document part (Word drops a custom XML part the main part does not relate to):
350
426
 
351
427
  ```ts
352
428
  const part = pkg.customXmlParts.add('<greeting xmlns="http://example.com/g"><to>World</to></greeting>');
429
+ // or with the itemID and part name to use, checked unused: { itemID: '{4B0E8F1A-...}', partName: '/customXml/item7.xml' }
353
430
  control.xmlMapping.setMapping('/ns0:greeting[1]/ns0:to[1]', "xmlns:ns0='http://example.com/g'", part);
354
431
  control.xmlMapping.setMappingByNode(part.selectSingleNode('/ns0:greeting/ns0:to', "xmlns:ns0='http://example.com/g'"));
355
432
  ```
@@ -475,6 +552,10 @@ await Word.run(pkg, async (context) => {
475
552
  await writeFile('edited.docx', await pkg.save());
476
553
  ```
477
554
 
555
+ The callback's `context.document` is typed `ShimmedDocument`, so add-in code that uses
556
+ `paragraphs.items`, `load('items/text')`, `getFirst()` or `getCount().value` type-checks here as it
557
+ does against Office JS.
558
+
478
559
  `Word.supported` is the set of `Class.member` strings this package implements
479
560
  (`Word.supported.has('Body.insertParagraph')`), generated from the compile-time subset, so a
480
561
  tool can check a script before running it. The selection an add-in edits is passed in:
@@ -5,10 +5,16 @@ import type { OpcPackage } from '../../packages/OpcPackage.mjs';
5
5
  import { type Element, type TextViewOptions } from './tree.mjs';
6
6
  import { Paragraph } from './Paragraph.mjs';
7
7
  import { Range } from './Range.mjs';
8
- import { Table } from './Table.mjs';
8
+ import { Table, type TableRow, type TableCell } from './Table.mjs';
9
9
  import { ContentControl, type ContentControlType } from './ContentControl.mjs';
10
10
  import { InlinePicture, type InlinePictureOptions } from './InlinePicture.mjs';
11
- import type { SearchOptions } from './search.mjs';
11
+ import { type SearchOptions } from './search.mjs';
12
+ import type { Bound } from './binder.mjs';
13
+ /**
14
+ * What `Body.select` returns for one hit: the content-API view of the selected element where there
15
+ * is one, and the `Bound` itself otherwise (CR-006 section 2).
16
+ */
17
+ export type SelectResult = Paragraph | Table | TableRow | TableCell | ContentControl | Range | Bound;
12
18
  import type { Comment } from './Comment.mjs';
13
19
  import { type ChangeTracker } from './tracking.mjs';
14
20
  import { type TrackedChange } from './TrackedChange.mjs';
@@ -75,6 +81,34 @@ export declare class Body {
75
81
  get content(): Element[];
76
82
  /** Every paragraph in document order, descending into tables and content controls. */
77
83
  get paragraphs(): Paragraph[];
84
+ /**
85
+ * The content-API views an XPath selects (CR-006): a `ContentControl`, `Table`, `TableRow`,
86
+ * `TableCell` or `Paragraph` where the selected element has one, at any depth - an inline content
87
+ * control is a `ContentControl`, a nested table a `Table`; a `Range` over the whole paragraph for a
88
+ * `w:r` or anything in one (`w:t`, `w:tab`), wherever the run sits (a hyperlink, an insertion, an
89
+ * inline control); and the `Bound` itself for anything else - an attribute, a text node, a `w:pPr`,
90
+ * a `w:hyperlink`, a wildcard child. An attribute or a text node is always the `Bound`, never its
91
+ * element's view, so that what matched is not lost. A caller that wants the tree objects rather
92
+ * than views uses `pkg.selectObjects`.
93
+ *
94
+ * The expression is evaluated over a snapshot of the part this body belongs to, so it sees the
95
+ * whole part - `//w:p` from a header's body selects that header's paragraphs. Every call marshals
96
+ * the part afresh (CR-006 section 2).
97
+ */
98
+ select(xpath: string, options?: {
99
+ namespaces?: Record<string, string>;
100
+ }): Promise<SelectResult[]>;
101
+ /**
102
+ * Every view this body can make, keyed by the tree object behind it, so that a selected object
103
+ * becomes the view a caller can act on. Built per select: the views are cheap wrappers and nothing
104
+ * is cached but the `Body` itself (CR-002's "views, not a model"). The first view put for an object
105
+ * is the one it keeps, so the most specific kinds go first: a content control before the
106
+ * paragraph that holds an inline one, a table at any depth before the paragraphs in its cells, and
107
+ * a paragraph before the runs in it. A `w:r` and what is in it map to a `Range` over the paragraph,
108
+ * which is the nearest thing the content API has - a run is not addressable on its own in Office
109
+ * JS. A run in a text box is not one of the paragraph's, and stays a `Bound`.
110
+ */
111
+ private viewsByObject;
78
112
  /** The tables directly in this body, as Office JS reports them (nested tables are a cell's). */
79
113
  get tables(): Table[];
80
114
  /** Every content control in this body, in document order, nested ones included. */
@@ -1,7 +1,7 @@
1
1
  import { Docx4JException, PropertyResolverNotCreatedException } from '../../opc/exceptions.mjs';
2
2
  import { typeNameOf, childrenOf, linkParents, BLOCK_LEVEL_TYPES, textOf, textOfView, rowsOf, cellsOf } from './tree.mjs';
3
3
  import { r as textRun, br as breakItem, tbl as tableOf } from '@docx4j/generated-objects-ts/builders/wml';
4
- import { runOf, paragraphOf } from './tree.mjs';
4
+ import { runOf, paragraphOf, runsOf } from './tree.mjs';
5
5
  import { Paragraph } from './Paragraph.mjs';
6
6
  import { Table } from './Table.mjs';
7
7
  import { ContentControl, collectControls } from './ContentControl.mjs';
@@ -9,6 +9,7 @@ import { sdt as sdtOf, nextSdtId } from '@docx4j/generated-objects-ts/builders/w
9
9
  import { controlIdScope, sdtKindFor } from '../customxml/insert.mjs';
10
10
  import { InlinePicture, addImage, writableWidthEmu } from './InlinePicture.mjs';
11
11
  import { contentOf } from './ooxml.mjs';
12
+ import { expandReplacement } from './search.mjs';
12
13
  import { commentApi } from './comments.mjs';
13
14
  import { trackerOf, trackInsertedParagraph, trackInsertedTable } from './tracking.mjs';
14
15
  import { trackedChangesOfRow } from './TrackedChange.mjs';
@@ -80,6 +81,78 @@ export class Body {
80
81
  visit(this.content);
81
82
  return out;
82
83
  }
84
+ /**
85
+ * The content-API views an XPath selects (CR-006): a `ContentControl`, `Table`, `TableRow`,
86
+ * `TableCell` or `Paragraph` where the selected element has one, at any depth - an inline content
87
+ * control is a `ContentControl`, a nested table a `Table`; a `Range` over the whole paragraph for a
88
+ * `w:r` or anything in one (`w:t`, `w:tab`), wherever the run sits (a hyperlink, an insertion, an
89
+ * inline control); and the `Bound` itself for anything else - an attribute, a text node, a `w:pPr`,
90
+ * a `w:hyperlink`, a wildcard child. An attribute or a text node is always the `Bound`, never its
91
+ * element's view, so that what matched is not lost. A caller that wants the tree objects rather
92
+ * than views uses `pkg.selectObjects`.
93
+ *
94
+ * The expression is evaluated over a snapshot of the part this body belongs to, so it sees the
95
+ * whole part - `//w:p` from a header's body selects that header's paragraphs. Every call marshals
96
+ * the part afresh (CR-006 section 2).
97
+ */
98
+ async select(xpath, options = {}) {
99
+ const pkg = this.package_;
100
+ if ((pkg === null || pkg === void 0 ? void 0 : pkg.selectObjects) === undefined)
101
+ throw new Docx4JException('select needs the package that owns this body');
102
+ const bounds = await pkg.selectObjects(xpath, { part: this.part, namespaces: options.namespaces });
103
+ const views = this.viewsByObject();
104
+ return bounds.map((bound) => { var _a; return (_a = (bound.node.nodeType === 1 ? views.get(bound.object) : undefined)) !== null && _a !== void 0 ? _a : bound; });
105
+ }
106
+ /**
107
+ * Every view this body can make, keyed by the tree object behind it, so that a selected object
108
+ * becomes the view a caller can act on. Built per select: the views are cheap wrappers and nothing
109
+ * is cached but the `Body` itself (CR-002's "views, not a model"). The first view put for an object
110
+ * is the one it keeps, so the most specific kinds go first: a content control before the
111
+ * paragraph that holds an inline one, a table at any depth before the paragraphs in its cells, and
112
+ * a paragraph before the runs in it. A `w:r` and what is in it map to a `Range` over the paragraph,
113
+ * which is the nearest thing the content API has - a run is not addressable on its own in Office
114
+ * JS. A run in a text box is not one of the paragraph's, and stays a `Bound`.
115
+ */
116
+ viewsByObject() {
117
+ var _a;
118
+ const out = new Map();
119
+ const put = (value, view) => {
120
+ if (typeof value === 'object' && value !== null && !out.has(value))
121
+ out.set(value, view);
122
+ };
123
+ const controls = this.contentControls;
124
+ for (const control of controls)
125
+ put(control.element.value, control);
126
+ const putTables = (tables) => {
127
+ for (const table of tables) {
128
+ put(table.element.value, table);
129
+ for (const row of table.rows) {
130
+ put(row.element.value, row);
131
+ for (const cell of row.cells) {
132
+ put(cell.element.value, cell);
133
+ putTables(cell.tables);
134
+ }
135
+ }
136
+ }
137
+ };
138
+ putTables(this.tables);
139
+ for (const control of controls)
140
+ if (control.form === 'Block')
141
+ putTables(control.tables);
142
+ for (const paragraph of this.paragraphs) {
143
+ put(paragraph.element.value, paragraph);
144
+ const range = paragraph.getRange();
145
+ // both views, so that a run in a w:del is found as well as one in a w:ins
146
+ for (const view of ['accepted', 'original']) {
147
+ for (const run of runsOf(paragraph.element.value, { view })) {
148
+ put(run.value, range);
149
+ for (const item of (_a = run.value.content) !== null && _a !== void 0 ? _a : [])
150
+ put(item.value, range);
151
+ }
152
+ }
153
+ }
154
+ return out;
155
+ }
83
156
  /** The tables directly in this body, as Office JS reports them (nested tables are a cell's). */
84
157
  get tables() {
85
158
  return this.content.filter((el) => typeNameOf(el) === 'org_docx4j_wml.Tbl').map((element) => new Table(element, this.content, this));
@@ -282,9 +355,11 @@ export class Body {
282
355
  * the package's `changeTrackingMode` is on.
283
356
  */
284
357
  replaceText(find, replace, options) {
285
- const matches = this.search(find, options);
286
- for (let i = matches.length - 1; i >= 0; i--)
287
- matches[i].insertText(replace, 'Replace');
358
+ const matches = this.paragraphs.flatMap((p) => p.searchMatches(find, options));
359
+ for (let i = matches.length - 1; i >= 0; i--) {
360
+ const { range, match } = matches[i];
361
+ range.insertText(expandReplacement(replace, match, options), 'Replace');
362
+ }
288
363
  return matches.length;
289
364
  }
290
365
  /**
@@ -2,7 +2,7 @@ import type * as wml from '@docx4j/generated-objects-ts/modules/org_docx4j_wml';
2
2
  import { Body } from './Body.mjs';
3
3
  import { Paragraph } from './Paragraph.mjs';
4
4
  import { Range } from './Range.mjs';
5
- import { type CommentParts } from './comments.mjs';
5
+ import { type CommentContent, type CommentOptions, type CommentParts } from './comments.mjs';
6
6
  /**
7
7
  * A subset of Office JS `Word.Comment` over a `w:comment` and the markers that anchor it. A view:
8
8
  * the tree is the state, and a view may be created and discarded freely. Replies are `Comment`s
@@ -51,8 +51,11 @@ export declare class Comment {
51
51
  get replies(): Comment[];
52
52
  /** The comment this one replies to (extension). */
53
53
  get parent(): Comment | undefined;
54
- /** A reply in the same thread, anchored to the same range, as Word writes them. */
55
- reply(text: string): Promise<Comment>;
54
+ /**
55
+ * A reply in the same thread, anchored to the same range, as Word writes them. `options` are
56
+ * `insertComment`'s (CR-002 section 28.3).
57
+ */
58
+ reply(content: CommentContent, options?: CommentOptions): Promise<Comment>;
56
59
  /** Removes the comment and its replies: the markers, the reference runs and every side-part entry. */
57
60
  delete(): Promise<void>;
58
61
  /**
@@ -68,8 +71,15 @@ export declare class Comment {
68
71
  /** Used by `commentsOf` while threading. */
69
72
  addReply(reply: Comment): void;
70
73
  }
71
- /** `Range.insertComment` (CR-002 section 3.8): the markers, the comment and the side-part entries. */
72
- export declare function insertComment(range: Range, text: string): Promise<Comment>;
74
+ /**
75
+ * `Range.insertComment` (CR-002 section 3.8): the markers, the comment and the side-part entries.
76
+ *
77
+ * `options` (CR-002 section 28.3) name the author, the initials, the date and the `w:id`: in a
78
+ * co-editing room the author is the peer who made the comment and the date is when they made it,
79
+ * neither of which is a property of whoever is saving. A given `id` is checked unused and a clash
80
+ * throws.
81
+ */
82
+ export declare function insertComment(range: Range, content: CommentContent, options?: CommentOptions): Promise<Comment>;
73
83
  /**
74
84
  * `getComments()` on a body, a paragraph or a range: the comments anchored there in document
75
85
  * order, replies nested under their parent (a reply is in the array only when its parent is not).
@@ -105,12 +105,16 @@ export class Comment {
105
105
  get parent() {
106
106
  return this.parentComment;
107
107
  }
108
- /** A reply in the same thread, anchored to the same range, as Word writes them. */
109
- async reply(text) {
108
+ /**
109
+ * A reply in the same thread, anchored to the same range, as Word writes them. `options` are
110
+ * `insertComment`'s (CR-002 section 28.3).
111
+ */
112
+ async reply(content, options) {
113
+ const paragraphs = paragraphsOfContent(content);
110
114
  const parts = await this.parts.ensureAll();
111
- const id = nextCommentId(parts.comments, this.body.container);
115
+ const id = commentId(parts, this.body, options);
112
116
  placeAfter(this.body, this.id, id);
113
- const reply = addComment(parts, this.body, id, text, this);
117
+ const reply = addComment(parts, this.body, id, paragraphs, this, options);
114
118
  this.replyList.push(reply);
115
119
  reply.parentComment = this;
116
120
  return reply;
@@ -291,6 +295,10 @@ function placeAfter(body, parentId, id) {
291
295
  const start = markers.find((m) => m.kind === 'start');
292
296
  const end = [...markers].reverse().find((m) => m.kind === 'end');
293
297
  const reference = [...markers].reverse().find((m) => m.kind === 'reference');
298
+ if (!((reference === null || reference === void 0 ? void 0 : reference.run) && reference.runOwner) && !end) {
299
+ // refused before anything is written, so that no marker is left naming a comment never made
300
+ throw new Docx4JException(`Comment ${parentId} has no markers in this body; cannot anchor a reply`);
301
+ }
294
302
  if (start)
295
303
  insertAt(start.owner, start.owner.indexOf(start.item) + 1, [el.commentRangeStart(wmlFactory.createCommentRangeStart({ id }))], parentOf(start.item));
296
304
  if (end)
@@ -301,37 +309,37 @@ function placeAfter(body, parentId, id) {
301
309
  else if (end) {
302
310
  insertAt(end.owner, end.owner.indexOf(end.item) + 1, [referenceRun(id)], parentOf(end.item));
303
311
  }
304
- else {
305
- throw new Docx4JException(`Comment ${parentId} has no markers in this body; cannot anchor a reply`);
306
- }
307
312
  }
308
- /** The `w:comment` and the side-part entries of a new comment; the markers are already written. */
309
- function addComment(parts, body, id, text, parent) {
310
- var _a, _b, _c, _d;
311
- var _e, _f;
312
- const author = parts.author;
313
- const paragraphs = text.split('\n').map((line, i) => commentParagraph(line, i === 0));
313
+ /**
314
+ * The `w:comment` and the side-part entries of a new comment; the markers are already written. The
315
+ * paragraphs come checked, by `paragraphsOfContent` before the markers were placed.
316
+ */
317
+ function addComment(parts, body, id, paragraphs, parent, options) {
318
+ var _a, _b, _c, _d, _e, _f, _g;
319
+ var _h, _j;
320
+ const author = (_a = options === null || options === void 0 ? void 0 : options.author) !== null && _a !== void 0 ? _a : parts.author;
314
321
  const paraId = randomHexId((candidate) => paraIdTaken(parts, body.container, candidate));
315
322
  paragraphs[0].value.paraId = paraId;
316
323
  const comment = wmlFactory.createCommentsComment({
317
- id, author: author.name, initials: initialsOf(author), date: dateToCalendar(new Date()),
324
+ id, author: author.name, initials: (_b = options === null || options === void 0 ? void 0 : options.initials) !== null && _b !== void 0 ? _b : initialsOf(author),
325
+ date: dateToCalendar((_c = options === null || options === void 0 ? void 0 : options.date) !== null && _c !== void 0 ? _c : new Date()),
318
326
  content: paragraphs,
319
327
  });
320
- const comments = ((_a = (_e = parts.comments).comment) !== null && _a !== void 0 ? _a : (_e.comment = []));
328
+ const comments = ((_d = (_h = parts.comments).comment) !== null && _d !== void 0 ? _d : (_h.comment = []));
321
329
  comments.push(comment);
322
330
  linkParents(comment, parts.comments);
323
331
  const extended = parts.ensureCommentsEx();
324
332
  const entry = w15Factory.createCTCommentEx({ paraId, done: '0' });
325
333
  if (parent)
326
334
  entry.paraIdParent = parent.ensureParaId();
327
- ((_b = extended.commentEx) !== null && _b !== void 0 ? _b : (extended.commentEx = [])).push(entry);
335
+ ((_e = extended.commentEx) !== null && _e !== void 0 ? _e : (extended.commentEx = [])).push(entry);
328
336
  linkParents(entry, extended);
329
337
  if (parts.commentsIds) {
330
338
  const durableId = randomHexId((candidate) => { var _a; return ((_a = parts.commentsIds.commentId) !== null && _a !== void 0 ? _a : []).some((e) => e.durableId === candidate); });
331
339
  const commentId = w16cidFactory.createCTCommentId({ paraId, durableId });
332
- ((_c = (_f = parts.commentsIds).commentId) !== null && _c !== void 0 ? _c : (_f.commentId = [])).push(commentId);
340
+ ((_f = (_j = parts.commentsIds).commentId) !== null && _f !== void 0 ? _f : (_j.commentId = [])).push(commentId);
333
341
  linkParents(commentId, parts.commentsIds);
334
- (_d = parts.extensible) === null || _d === void 0 ? void 0 : _d.set(durableId, new Date());
342
+ (_g = parts.extensible) === null || _g === void 0 ? void 0 : _g.set(durableId, new Date());
335
343
  }
336
344
  addPerson(parts, author.name, author.email);
337
345
  return new Comment(comment, parts, body);
@@ -354,13 +362,53 @@ function addPerson(parts, name, email) {
354
362
  list.push(person);
355
363
  linkParents(person, people);
356
364
  }
357
- /** `Range.insertComment` (CR-002 section 3.8): the markers, the comment and the side-part entries. */
358
- export async function insertComment(range, text) {
365
+ /**
366
+ * `Range.insertComment` (CR-002 section 3.8): the markers, the comment and the side-part entries.
367
+ *
368
+ * `options` (CR-002 section 28.3) name the author, the initials, the date and the `w:id`: in a
369
+ * co-editing room the author is the peer who made the comment and the date is when they made it,
370
+ * neither of which is a property of whoever is saving. A given `id` is checked unused and a clash
371
+ * throws.
372
+ */
373
+ export async function insertComment(range, content, options) {
374
+ // Everything that can refuse the call runs before anything is written: the content, then the id
375
+ // once the parts can say which are taken. A refusal after the markers were placed left them naming
376
+ // a comment that does not exist (CR-002 section 31).
377
+ const paragraphs = paragraphsOfContent(content);
359
378
  const body = range.paragraph.parentBody;
360
379
  const parts = await commentPartsAccess().create(body);
361
- const id = nextCommentId(parts.comments, body.container);
380
+ const id = commentId(parts, body, options);
362
381
  placeAround(range, id);
363
- return addComment(parts, body, id, text, undefined);
382
+ return addComment(parts, body, id, paragraphs, undefined, options);
383
+ }
384
+ /**
385
+ * The `w:id` for a new comment: the caller's when given, checked unused across the comments part and
386
+ * the markers in the story, else the next free one. A clash throws rather than renumbering, as a
387
+ * chosen list `numId` does: the caller is asserting something about a room's state (section 28.3).
388
+ */
389
+ function commentId(parts, body, options) {
390
+ var _a;
391
+ if ((options === null || options === void 0 ? void 0 : options.id) === undefined)
392
+ return nextCommentId(parts.comments, body.container);
393
+ const id = options.id;
394
+ if (!Number.isInteger(id) || id < 0)
395
+ throw new Docx4JException(`A comment id must be a non-negative integer, not ${id}`);
396
+ if (((_a = parts.comments.comment) !== null && _a !== void 0 ? _a : []).some((c) => c.id === id)) {
397
+ throw new Docx4JException(`The comment id ${id} is already in use in this document's comments part`);
398
+ }
399
+ if (markersOf(body.container).some((m) => m.id === id)) {
400
+ throw new Docx4JException(`The comment id ${id} is already named by a marker in this story`);
401
+ }
402
+ return id;
403
+ }
404
+ /** The paragraphs of a comment's content: a string splits on newlines, elements are taken as they are. */
405
+ function paragraphsOfContent(content) {
406
+ if (typeof content === 'string')
407
+ return content.split('\n').map((line, i) => commentParagraph(line, i === 0));
408
+ const paragraphs = [...content];
409
+ if (paragraphs.length === 0)
410
+ throw new Docx4JException('A comment needs at least one paragraph');
411
+ return paragraphs;
364
412
  }
365
413
  /**
366
414
  * `getComments()` on a body, a paragraph or a range: the comments anchored there in document
@@ -307,7 +307,7 @@ export declare class NumberingFacade {
307
307
  */
308
308
  newList(options?: {
309
309
  bullet?: boolean;
310
- }): Promise<string>;
310
+ } & ChosenListIds): Promise<string>;
311
311
  /**
312
312
  * A `w:num` that numbers as `numId` does but starts again, and its own `w:numId`:
313
313
  * `List.restart()` without needing a `Body`. The counters are shared by abstract definition,
@@ -317,5 +317,16 @@ export declare class NumberingFacade {
317
317
  * Synchronous, so the numbering part must have been read: `await pkg.getPropertyResolver()`,
318
318
  * `newList()` or any list read does it.
319
319
  */
320
- restart(numId: string | number): string;
320
+ restart(numId: string | number, options?: {
321
+ newNumId?: string | number;
322
+ }): string;
323
+ }
324
+ /**
325
+ * The identifiers a caller may choose for a new list definition, when the next integer is the wrong
326
+ * answer: in a co-editing room two sessions allocate at once, so the editor picks random ids and
327
+ * needs them honoured rather than renumbered (CR-002 section 24).
328
+ */
329
+ export interface ChosenListIds {
330
+ numId?: string | number | undefined;
331
+ abstractNumId?: string | number | undefined;
321
332
  }
@@ -669,23 +669,29 @@ export async function startNewList(paragraph, options) {
669
669
  * `w:numId`. The paragraph half of `startNewList` is separate because it is separable: nothing
670
670
  * about building a list definition needs one.
671
671
  */
672
- async function addListDefinition(part, bullet) {
672
+ async function addListDefinition(part, bullet, ids) {
673
673
  var _a, _b, _c;
674
674
  const numbering = await part.getContents();
675
675
  const defaults = await defaultNumbering();
676
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);
@@ -697,10 +703,10 @@ async function addListDefinition(part, bullet) {
697
703
  * list (CR-001 section 15.2), and it is what {@link List.restart} and
698
704
  * {@link NumberingFacade.restart} both do.
699
705
  */
700
- function addRestartedNum(numbering, abstractNumId, start) {
706
+ function addRestartedNum(numbering, abstractNumId, start, newNumId) {
701
707
  var _a;
702
708
  const created = f.createNumberingNum({
703
- numId: nextNumId(numbering),
709
+ numId: newNumId === undefined ? nextNumId(numbering) : requireFreeId(numbering, 'numId', newNumId),
704
710
  abstractNumId: f.createNumberingNumAbstractNumId({ val: abstractNumId }),
705
711
  lvlOverride: [f.createNumberingNumLvlOverride({
706
712
  ilvl: 0,
@@ -776,9 +782,14 @@ export class NumberingFacade {
776
782
  * caller writes `w:numPr` itself.
777
783
  */
778
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);
779
790
  const main = this.pkg.getMainDocumentPart();
780
791
  const part = await ensureNumberingPartOf(this.pkg, main);
781
- const numId = await addListDefinition(part, (options === null || options === void 0 ? void 0 : options.bullet) === true);
792
+ const numId = await addListDefinition(part, (options === null || options === void 0 ? void 0 : options.bullet) === true, options);
782
793
  this.pkg.refreshPropertyResolver();
783
794
  return String(numId);
784
795
  }
@@ -791,7 +802,7 @@ export class NumberingFacade {
791
802
  * Synchronous, so the numbering part must have been read: `await pkg.getPropertyResolver()`,
792
803
  * `newList()` or any list read does it.
793
804
  */
794
- restart(numId) {
805
+ restart(numId, options) {
795
806
  var _a, _b;
796
807
  const part = this.pkg.getMainDocumentPart().numberingDefinitionsPart;
797
808
  if (part === undefined)
@@ -799,7 +810,7 @@ export class NumberingFacade {
799
810
  const definition = part.definitions.list(numId);
800
811
  if (definition === undefined)
801
812
  throw new ItemNotFound(`No w:num for numId ${numId}`);
802
- 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);
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);
803
814
  this.pkg.refreshPropertyResolver();
804
815
  return String(created);
805
816
  }
@@ -840,6 +851,34 @@ function nextNumId(numbering) {
840
851
  var _a;
841
852
  return ((_a = numbering.num) !== null && _a !== void 0 ? _a : []).reduce((max, num) => Math.max(max, Number(num.numId)), 0) + 1;
842
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
+ }
843
882
  /** A free `w:abstractNumId`. */
844
883
  function nextAbstractNumId(numbering) {
845
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