@docx4j/core-ts 0.1.4 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +76 -0
- package/dist/model/content/Body.d.mts +36 -2
- package/dist/model/content/Body.mjs +79 -4
- package/dist/model/content/Comment.d.mts +15 -5
- package/dist/model/content/Comment.mjs +70 -22
- package/dist/model/content/List.d.mts +80 -1
- package/dist/model/content/List.mjs +140 -22
- package/dist/model/content/Paragraph.d.mts +11 -1
- package/dist/model/content/Paragraph.mjs +18 -7
- package/dist/model/content/Range.d.mts +29 -2
- package/dist/model/content/Range.mjs +137 -7
- package/dist/model/content/binder.d.mts +44 -0
- package/dist/model/content/binder.mjs +95 -0
- package/dist/model/content/comments.d.mts +19 -1
- package/dist/model/content/index.d.mts +6 -4
- package/dist/model/content/index.mjs +4 -2
- package/dist/model/content/search.d.mts +27 -2
- package/dist/model/content/search.mjs +59 -3
- package/dist/model/content/stylesFacade.d.mts +37 -0
- package/dist/model/content/stylesFacade.mjs +114 -0
- package/dist/model/content/tracking.d.mts +57 -2
- package/dist/model/content/tracking.mjs +47 -15
- package/dist/model/customxml/CustomXmlPartCollection.d.mts +7 -0
- package/dist/model/customxml/CustomXmlPartCollection.mjs +40 -6
- package/dist/model/customxml/xpath.d.mts +18 -3
- package/dist/model/customxml/xpath.mjs +18 -3
- package/dist/office-js/index.d.mts +3 -1
- package/dist/office-js/run.d.mts +12 -2
- package/dist/office-js/run.mjs +8 -1
- package/dist/office-js/shimmed.d.mts +46 -0
- package/dist/office-js/shimmed.mjs +1 -0
- package/dist/office-js/supported.generated.mjs +1 -0
- package/dist/opc/exceptions.d.mts +25 -0
- package/dist/opc/exceptions.mjs +20 -0
- package/dist/opc/index.d.mts +1 -1
- package/dist/opc/index.mjs +1 -1
- package/dist/opc/mce/understood.mjs +37 -8
- package/dist/packages/WordprocessingMLPackage.d.mts +101 -2
- package/dist/packages/WordprocessingMLPackage.mjs +155 -1
- package/dist/parts/XmlPart.d.mts +44 -0
- package/dist/parts/XmlPart.mjs +86 -3
- package/dist/parts/sml/index.d.mts +9 -8
- package/dist/parts/sml/index.mjs +23 -19
- package/dist/parts/wml/index.d.mts +10 -2
- package/dist/parts/wml/index.mjs +11 -3
- package/dist/parts/wml/spliceableStyles.d.mts +1 -0
- package/dist/parts/wml/spliceableStyles.mjs +122 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -103,6 +103,20 @@ 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
|
+
A style the document lacks is spliced in from docx4j's defaults, with the styles it is based on and
|
|
117
|
+
linked to: `await pkg.styles.ensure(['FootnoteText', 'FootnoteReference'])` returns the ids it
|
|
118
|
+
added, and a document that already has them keeps its styles part byte for byte.
|
|
119
|
+
|
|
106
120
|
Docx4j's names are there as aliases (`addParagraphOfText`, `addStyledParagraphOfText`,
|
|
107
121
|
`addObject`, `getContent`), and the tree stays reachable: `paragraph.p` is the `P`,
|
|
108
122
|
`body.content` the live array.
|
|
@@ -173,6 +187,12 @@ list.setLevelStartingNumber(0, 5);
|
|
|
173
187
|
const again = p4.restartList(); // the same list, numbering again from its start
|
|
174
188
|
```
|
|
175
189
|
|
|
190
|
+
Without a paragraph, `pkg.numbering.newList()` makes a definition and returns its `w:numId`, and
|
|
191
|
+
`pkg.numbering.restart(numId)` one that numbers again from the start. Both take the identifiers to
|
|
192
|
+
use when the next free one is the wrong answer - two sessions allocating in one co-edited
|
|
193
|
+
document - checked unused, a clash throwing rather than renumbering:
|
|
194
|
+
`await pkg.numbering.newList({ numId: 4242, abstractNumId: 77 })`.
|
|
195
|
+
|
|
176
196
|
A level writer changes the `w:abstractNum`, so it copies that definition first when another
|
|
177
197
|
`w:num` shares it — the change stays local to this list. `startNewList()` is asynchronous here
|
|
178
198
|
(Office JS's is not): it copies its definition from docx4j's default `numbering.xml` and adds
|
|
@@ -208,6 +228,21 @@ body.acceptAll(); // or rejectAll(), or accept()/reject() one at a time
|
|
|
208
228
|
`getTrackedChanges()` is also on `Paragraph` and `Range`. Text and searches read the accepted
|
|
209
229
|
view throughout, so an agent sees the document as it will read once the changes are taken.
|
|
210
230
|
|
|
231
|
+
To record one caller's edits without touching the document's own setting - an agent working in a
|
|
232
|
+
document whose `w:trackRevisions` belongs to its owner - use `withTracking`. Nothing is written
|
|
233
|
+
to the settings part, and the author, date and mode are put back when `fn` returns or throws:
|
|
234
|
+
|
|
235
|
+
```ts
|
|
236
|
+
await pkg.withTracking({ author: { name: 'Agent' } }, () => {
|
|
237
|
+
body.replaceText('colour', 'color');
|
|
238
|
+
});
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
Calls nest. While two overlap, the later one's author and mode apply to every edit, since nothing
|
|
242
|
+
can tell which asynchronous call an edit came from: await one before starting another when they
|
|
243
|
+
must be recorded as different authors. A revision is dated to the minute, as Word dates them, and
|
|
244
|
+
its id is above every annotation id in the document, including those in parts nobody has read.
|
|
245
|
+
|
|
211
246
|
### Comments
|
|
212
247
|
|
|
213
248
|
`getComments()` on the body, a paragraph or a range, and `insertComment` on a range or a
|
|
@@ -230,6 +265,15 @@ for (const c of await body.getComments()) { // document order, replies nested
|
|
|
230
265
|
}
|
|
231
266
|
```
|
|
232
267
|
|
|
268
|
+
`insertComment` and `reply` take the author, initials, date and `w:id` when they are not the
|
|
269
|
+
package's - a comment made by another person in a co-edited document - and paragraphs as well as
|
|
270
|
+
a string. A chosen id is checked unused, and a clash throws:
|
|
271
|
+
|
|
272
|
+
```ts
|
|
273
|
+
await hit.insertComment('Checked', { author: { name: 'Grace Hopper' }, date: new Date('2026-09-27T10:15:00Z'), id: 42 });
|
|
274
|
+
await hit.insertComment([p1, p2]); // w:p elements, for rich content
|
|
275
|
+
```
|
|
276
|
+
|
|
233
277
|
### XML in
|
|
234
278
|
|
|
235
279
|
A whole `word/document.xml` becomes the main document part's contents with `setXml`, and a
|
|
@@ -253,6 +297,33 @@ import * as el from '@docx4j/generated-objects-ts/el/org_docx4j_wml';
|
|
|
253
297
|
body.addObject(el.p({ content: [el.r({ content: [el.t({ value: 'Hello World' })] })] }));
|
|
254
298
|
```
|
|
255
299
|
|
|
300
|
+
A part that cannot be unmarshalled throws `PartUnmarshalException`, whose message names the part
|
|
301
|
+
and, where the parser supplies a position (xmldom in Node; not a browser), the element or attribute
|
|
302
|
+
and its line: `/word/styles.xml: w:sz/@w:val (line 7): Argument [NaN] must be an integer, but it is not a number.`
|
|
303
|
+
`partName`, `location`, `line` and `column` are fields of their own.
|
|
304
|
+
|
|
305
|
+
### XPath over the document
|
|
306
|
+
|
|
307
|
+
`pkg.selectObjects(xpath)` evaluates an XPath over a part - the main document part unless you pass
|
|
308
|
+
another - and answers as docx4j's `getJAXBNodesViaXPath` does: for each hit, the node, the tree
|
|
309
|
+
object behind it and its path. `body.select(xpath)` gives the content-API view instead, where the
|
|
310
|
+
element has one (`Paragraph`, `Table`, `TableRow`, `TableCell`, `ContentControl`, a `Range` for a
|
|
311
|
+
run):
|
|
312
|
+
|
|
313
|
+
```ts
|
|
314
|
+
const hits = await pkg.selectObjects("//w:p[w:pPr/w:pStyle/@w:val='Heading1']");
|
|
315
|
+
hits[0].object; // the P itself, not a copy: change it and the document changes
|
|
316
|
+
hits[0].path; // '/w:document/w:body[1]/w:p[3]'
|
|
317
|
+
|
|
318
|
+
for (const heading of await body.select("//w:p[w:pPr/w:pStyle/@w:val='Heading1']")) {
|
|
319
|
+
if (heading instanceof Paragraph) heading.styleBuiltIn = 'Heading2';
|
|
320
|
+
}
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
The prefixes are the objects package's (`w`, `r`, `wp`, `a`, ...), and `namespaces` adds others.
|
|
324
|
+
Each call evaluates a fresh snapshot of the part, so what it selects is never stale and changing the
|
|
325
|
+
snapshot's DOM changes nothing. The answers are held to docx4j's own by the XPath goldens (CR-006).
|
|
326
|
+
|
|
256
327
|
### Tables, pictures and content controls
|
|
257
328
|
|
|
258
329
|
`Table`, `TableRow` and `TableCell` are Office JS shapes too; a cell is a `Body` of its own, so
|
|
@@ -350,6 +421,7 @@ document part (Word drops a custom XML part the main part does not relate to):
|
|
|
350
421
|
|
|
351
422
|
```ts
|
|
352
423
|
const part = pkg.customXmlParts.add('<greeting xmlns="http://example.com/g"><to>World</to></greeting>');
|
|
424
|
+
// or with the itemID and part name to use, checked unused: { itemID: '{4B0E8F1A-...}', partName: '/customXml/item7.xml' }
|
|
353
425
|
control.xmlMapping.setMapping('/ns0:greeting[1]/ns0:to[1]', "xmlns:ns0='http://example.com/g'", part);
|
|
354
426
|
control.xmlMapping.setMappingByNode(part.selectSingleNode('/ns0:greeting/ns0:to', "xmlns:ns0='http://example.com/g'"));
|
|
355
427
|
```
|
|
@@ -475,6 +547,10 @@ await Word.run(pkg, async (context) => {
|
|
|
475
547
|
await writeFile('edited.docx', await pkg.save());
|
|
476
548
|
```
|
|
477
549
|
|
|
550
|
+
The callback's `context.document` is typed `ShimmedDocument`, so add-in code that uses
|
|
551
|
+
`paragraphs.items`, `load('items/text')`, `getFirst()` or `getCount().value` type-checks here as it
|
|
552
|
+
does against Office JS.
|
|
553
|
+
|
|
478
554
|
`Word.supported` is the set of `Class.member` strings this package implements
|
|
479
555
|
(`Word.supported.has('Body.insertParagraph')`), generated from the compile-time subset, so a
|
|
480
556
|
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
|
|
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.
|
|
286
|
-
for (let i = matches.length - 1; i >= 0; i--)
|
|
287
|
-
matches[i]
|
|
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
|
-
/**
|
|
55
|
-
|
|
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
|
-
/**
|
|
72
|
-
|
|
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
|
-
/**
|
|
109
|
-
|
|
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 =
|
|
115
|
+
const id = commentId(parts, this.body, options);
|
|
112
116
|
placeAfter(this.body, this.id, id);
|
|
113
|
-
const reply = addComment(parts, this.body, id,
|
|
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
|
-
/**
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
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:
|
|
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 = ((
|
|
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
|
-
((
|
|
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
|
-
((
|
|
340
|
+
((_f = (_j = parts.commentsIds).commentId) !== null && _f !== void 0 ? _f : (_j.commentId = [])).push(commentId);
|
|
333
341
|
linkParents(commentId, parts.commentsIds);
|
|
334
|
-
(
|
|
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
|
-
/**
|
|
358
|
-
|
|
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 =
|
|
380
|
+
const id = commentId(parts, body, options);
|
|
362
381
|
placeAround(range, id);
|
|
363
|
-
return addComment(parts, body, id,
|
|
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
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type * as wml from '@docx4j/generated-objects-ts/modules/org_docx4j_wml';
|
|
2
|
-
import type { ListDefinition, LevelDefinition } from '../listnumbering/definitions.mjs';
|
|
2
|
+
import type { NumberingDefinitions, ListDefinition, LevelDefinition } from '../listnumbering/definitions.mjs';
|
|
3
3
|
import type { Emulator } from '../listnumbering/Emulator.mjs';
|
|
4
4
|
import { Font } from './Font.mjs';
|
|
5
5
|
import type { Body } from './Body.mjs';
|
|
@@ -31,6 +31,38 @@ export interface ListLabel {
|
|
|
31
31
|
/** Whether the level's `w:numFmt` is `bullet`. */
|
|
32
32
|
isBullet: boolean;
|
|
33
33
|
}
|
|
34
|
+
/**
|
|
35
|
+
* The content API must not import `WordprocessingMLPackage` or `parts/wml` at runtime (that
|
|
36
|
+
* would be a cycle, since `parts/wml` builds the `Body` views), so the numbering part is
|
|
37
|
+
* reached by shape, as `Body.propertyResolver` and `fonts/lookup` are.
|
|
38
|
+
*/
|
|
39
|
+
interface NumberingPartLike {
|
|
40
|
+
readonly isUnmarshalled: boolean;
|
|
41
|
+
readonly contents: wml.Numbering;
|
|
42
|
+
readonly definitions: NumberingDefinitions;
|
|
43
|
+
getEmulator(reset?: boolean): Emulator;
|
|
44
|
+
makeLive(): wml.Numbering;
|
|
45
|
+
getContents(): Promise<wml.Numbering>;
|
|
46
|
+
}
|
|
47
|
+
/** As much of an `XmlPart` as the story walk reads. */
|
|
48
|
+
interface StoryPartLike {
|
|
49
|
+
readonly contentType: string;
|
|
50
|
+
readonly isUnmarshalled: boolean;
|
|
51
|
+
readonly contents: unknown;
|
|
52
|
+
}
|
|
53
|
+
/** As much of a `MainDocumentPart` as the list views use. */
|
|
54
|
+
interface MainPartLike {
|
|
55
|
+
numberingDefinitionsPart?: NumberingPartLike;
|
|
56
|
+
readonly headerParts: StoryPartLike[];
|
|
57
|
+
readonly footerParts: StoryPartLike[];
|
|
58
|
+
addTargetPart(part: unknown): unknown;
|
|
59
|
+
}
|
|
60
|
+
/** As much of a `WordprocessingMLPackage` as the list views use. */
|
|
61
|
+
interface ListPackageLike {
|
|
62
|
+
getMainDocumentPart(): MainPartLike;
|
|
63
|
+
refreshPropertyResolver(): void;
|
|
64
|
+
refresh(): Promise<unknown>;
|
|
65
|
+
}
|
|
34
66
|
/**
|
|
35
67
|
* The package's numbering emulator, or undefined where the document has no lists (or nothing
|
|
36
68
|
* has read the numbering part: `await pkg.getBody()` builds the resolver, which reads it).
|
|
@@ -251,3 +283,50 @@ export declare function detachFromList(paragraph: Paragraph): void;
|
|
|
251
283
|
* be unmarshalled, and a document with no numbering part gets one here (CR-002 section 17).
|
|
252
284
|
*/
|
|
253
285
|
export declare function startNewList(paragraph: Paragraph, options?: StartListOptions): Promise<List>;
|
|
286
|
+
/**
|
|
287
|
+
* The document's list definitions, as verbs that need no paragraph (CR-002 section 19, at the
|
|
288
|
+
* editor's request): `pkg.numbering`.
|
|
289
|
+
*
|
|
290
|
+
* `Paragraph.startNewList()` and `List.restart()` are Office JS's shapes and stay as they are,
|
|
291
|
+
* but both build a definition first and touch a paragraph only at the end. An editor that keeps
|
|
292
|
+
* its own document model has the numbering part and a `w:numId` to write, and nothing else - and
|
|
293
|
+
* going through a paragraph would mean projecting a tree, running the verb and projecting back,
|
|
294
|
+
* a whole-body round trip for what is otherwise one attribute. So the definition half is here.
|
|
295
|
+
*/
|
|
296
|
+
export declare class NumberingFacade {
|
|
297
|
+
private readonly pkg;
|
|
298
|
+
constructor(pkg: ListPackageLike);
|
|
299
|
+
/**
|
|
300
|
+
* A new list definition, and its `w:numId` as a string for a `w:numPr` to name. The numbering
|
|
301
|
+
* part is created with docx4j's default definitions where the document has none, and an
|
|
302
|
+
* existing one is unmarshalled so that what is written to it is saved.
|
|
303
|
+
*
|
|
304
|
+
* Asynchronous for the same reasons `startNewList` is: the default definitions are XML to be
|
|
305
|
+
* unmarshalled, and the part may have to be created. Nothing is attached to anything; the
|
|
306
|
+
* caller writes `w:numPr` itself.
|
|
307
|
+
*/
|
|
308
|
+
newList(options?: {
|
|
309
|
+
bullet?: boolean;
|
|
310
|
+
} & ChosenListIds): Promise<string>;
|
|
311
|
+
/**
|
|
312
|
+
* A `w:num` that numbers as `numId` does but starts again, and its own `w:numId`:
|
|
313
|
+
* `List.restart()` without needing a `Body`. The counters are shared by abstract definition,
|
|
314
|
+
* so restarting means a new `w:num` with a `w:startOverride`, spent on its first use in a
|
|
315
|
+
* story (CR-001 section 15.2).
|
|
316
|
+
*
|
|
317
|
+
* Synchronous, so the numbering part must have been read: `await pkg.getPropertyResolver()`,
|
|
318
|
+
* `newList()` or any list read does it.
|
|
319
|
+
*/
|
|
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;
|
|
332
|
+
}
|