@betteroffice/docx 0.4.3 → 0.5.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/LICENSE +1 -1
- package/README.md +108 -598
- package/dist/chunk-25WGDXBF.mjs +1 -0
- package/dist/chunk-2CCPNW44.mjs +1 -0
- package/dist/chunk-32A4I3T4.mjs +1 -0
- package/dist/chunk-4LO3PSLZ.mjs +2 -0
- package/dist/chunk-67MGRMP3.mjs +1 -0
- package/dist/chunk-6AUR4AA2.mjs +1 -0
- package/dist/chunk-6WT6HKAY.mjs +1 -0
- package/dist/chunk-77ZWGQZN.mjs +1 -0
- package/dist/chunk-7C3Y7LIH.mjs +1 -0
- package/dist/chunk-7WIUNGQW.mjs +1 -0
- package/dist/chunk-7WWFGMMF.mjs +1 -0
- package/dist/chunk-AK2NG7PT.mjs +1 -0
- package/dist/chunk-BR5BFOHF.mjs +1 -0
- package/dist/chunk-CDNWHQH2.mjs +1 -0
- package/dist/chunk-CRTTEHZN.mjs +1 -0
- package/dist/chunk-ETDMQ5GE.mjs +1 -0
- package/dist/{wasm-KSZTTO3B.mjs → chunk-GBOHQ3CJ.mjs} +1 -1
- package/dist/chunk-H7K4SJKK.mjs +1 -0
- package/dist/chunk-ICJH7QUY.mjs +1 -0
- package/dist/chunk-IHETKRDA.mjs +1 -0
- package/dist/chunk-ISBZLVIF.mjs +1 -0
- package/dist/chunk-JSYH6HNV.mjs +2 -0
- package/dist/chunk-KICHSQFN.mjs +1 -0
- package/dist/chunk-KXIGXLFA.mjs +5 -0
- package/dist/chunk-KXLTK7AT.mjs +57 -0
- package/dist/chunk-L3JRNJ34.mjs +1 -0
- package/dist/chunk-LKDHP22R.mjs +1 -0
- package/dist/chunk-LPOEM5WH.mjs +1 -0
- package/dist/chunk-M44BABH2.mjs +1 -0
- package/dist/chunk-MAYPJU23.mjs +1 -0
- package/dist/chunk-N5RE2S3U.mjs +2 -0
- package/dist/chunk-NLYHFVF5.mjs +1 -0
- package/dist/chunk-ONYCW7VV.mjs +4 -0
- package/dist/chunk-QGZ2AB3M.mjs +1 -0
- package/dist/chunk-QWHU7YIW.mjs +2 -0
- package/dist/{chunk-VH7P4734.mjs → chunk-QX2CDKPG.mjs} +1 -1
- package/dist/chunk-RGDE6YQB.mjs +1 -0
- package/dist/chunk-TE3AU2JK.mjs +1 -0
- package/dist/chunk-UDVEZ722.mjs +1 -0
- package/dist/chunk-UQSVFDRV.mjs +2 -0
- package/dist/chunk-VHOOSS4D.mjs +1 -0
- package/dist/chunk-WIDM4EMT.mjs +1 -0
- package/dist/chunk-YITJ4QAO.mjs +1 -0
- package/dist/collaboration/index.d.mts +111 -11
- package/dist/collaboration/index.mjs +1 -1
- package/dist/{content-DSb-6a9T.d.mts → content-DVT06V_6.d.mts} +24 -3
- package/dist/core.d.mts +9 -9
- package/dist/core.mjs +1 -1
- package/dist/displayListQueries-6XjS7B5e.d.mts +1473 -0
- package/dist/docx/index.d.mts +8 -7
- package/dist/docx/index.mjs +1 -2
- package/dist/docxWorker.mjs +1 -0
- package/dist/editor/index.d.mts +14 -12
- package/dist/editor/index.mjs +1 -1
- package/dist/{fontLoader-BNSs0E8K.d.mts → fontLoader-CBkmh4ps.d.mts} +1 -1
- package/dist/{formatting-gqLFeJ9X.d.mts → formatting-p5MneNuy.d.mts} +32 -1
- package/dist/generated/edit/docx_edit_bg.wasm +0 -0
- package/dist/generated/layout/docx_layout_bg.wasm +0 -0
- package/dist/generated/opc/ooxml_opc_bg.wasm +0 -0
- package/dist/generated/parse/docx_parse_bg.wasm +0 -0
- package/dist/index-C-KKULi5.d.mts +5596 -0
- package/dist/layout/index.d.mts +4 -4
- package/dist/layout/index.mjs +1 -1
- package/dist/layout/pagination/index.d.mts +4 -4
- package/dist/layout/render/index.d.mts +37 -53
- package/dist/layout/render/index.mjs +3 -1
- package/dist/{pagedExport-BmSlEhoP.d.mts → pagedExport-DDgA5O7E.d.mts} +92 -9
- package/dist/plugin-api/RenderedDomContext.d.mts +5 -7
- package/dist/plugin-api/index.d.mts +3 -3
- package/dist/plugin-api/resolveItemPositions.d.mts +3 -3
- package/dist/{rezip-Bxiq4Z_R.d.mts → rezip-CfwAYdV4.d.mts} +7 -1
- package/dist/styles/index.d.mts +3 -3
- package/dist/styles/index.mjs +1 -1
- package/dist/{styles-AryxLAhx.d.mts → styles-DZZz2oRy.d.mts} +2 -2
- package/dist/types/content.d.mts +2 -2
- package/dist/types/document.d.mts +6 -6
- package/dist/{types-Cz7S77Ut.d.mts → types-bvPg3KsF.d.mts} +16 -3
- package/dist/utils/comments.d.mts +2 -2
- package/dist/utils/findReplace.mjs +1 -2
- package/dist/utils/index.d.mts +5 -5
- package/dist/utils/index.mjs +1 -1
- package/dist/utils/listState.d.mts +2 -5
- package/dist/utils/stylePreview.d.mts +2 -2
- package/dist/utils/stylePreview.mjs +1 -1
- package/dist/wasm-edit.d.mts +233 -48
- package/dist/wasm-edit.mjs +1 -1
- package/dist/wasm-layout.d.mts +7 -1
- package/dist/wasm-layout.mjs +1 -1
- package/dist/wasm-opc.mjs +1 -1
- package/dist/wasm-parse.d.mts +7 -1
- package/dist/wasm-parse.mjs +1 -1
- package/dist/worker.d.mts +60 -0
- package/dist/worker.mjs +1 -0
- package/dist/yrs/docxWorker.d.mts +20 -0
- package/dist/yrs/docxWorker.mjs +3 -0
- package/dist/yrs/index.d.mts +12 -7
- package/dist/yrs/index.mjs +1 -1
- package/package.json +14 -4
- package/dist/chunk-3RPZ44TT.mjs +0 -1
- package/dist/chunk-47Q2FMFF.mjs +0 -1
- package/dist/chunk-5Z22FORG.mjs +0 -1
- package/dist/chunk-7ZV6P6NZ.mjs +0 -57
- package/dist/chunk-AXFMDV66.mjs +0 -1
- package/dist/chunk-EBYC3CX7.mjs +0 -1
- package/dist/chunk-FK7X7557.mjs +0 -1
- package/dist/chunk-GKWXOZZH.mjs +0 -1
- package/dist/chunk-LIOWZSE7.mjs +0 -1
- package/dist/chunk-ML6E23W2.mjs +0 -1
- package/dist/chunk-MLEMGJP7.mjs +0 -1
- package/dist/chunk-QBMNRD5V.mjs +0 -2
- package/dist/chunk-SHVHVJUB.mjs +0 -1
- package/dist/compareDocx-URPCPAQT.mjs +0 -1
- package/dist/computeLayout-ZTO6TBWX.mjs +0 -1
- package/dist/displayListQueries-BVBDVXoP.d.mts +0 -306
- package/dist/frameDelta-CLeO7K5z.d.mts +0 -807
- package/dist/index-DzZVJC8-.d.mts +0 -2467
- package/dist/parseWasm-BESKFFA3.mjs +0 -1
- package/dist/rustDisplayList-B7uPoItK.d.mts +0 -315
- package/dist/wasm-JPDPN2B2.mjs +0 -1
- package/dist/wasm-QI7MWC5D.mjs +0 -1
- package/dist/yrs/residentEngineWorker.d.mts +0 -2
- package/dist/yrs/residentEngineWorker.mjs +0 -1
package/README.md
CHANGED
|
@@ -1,654 +1,164 @@
|
|
|
1
1
|
# @betteroffice/docx
|
|
2
2
|
|
|
3
|
-
Framework-free
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
measures every line and pages are replayed onto canvas.
|
|
3
|
+
Framework-free DOCX parsing, serialization, CRDT editing, layout, and canvas rendering backed by
|
|
4
|
+
Rust/WebAssembly. Use it for document processing or a custom editor adapter.
|
|
5
|
+
|
|
6
|
+
## Install
|
|
8
7
|
|
|
9
8
|
```bash
|
|
10
9
|
bun add @betteroffice/docx
|
|
11
10
|
```
|
|
12
11
|
|
|
13
|
-
|
|
14
|
-
[`@betteroffice/docx-react`](https://www.npmjs.com/package/@betteroffice/docx-react).
|
|
15
|
-
Reach for this package directly for headless parsing/serialization or when
|
|
16
|
-
building a custom adapter.
|
|
12
|
+
`@betteroffice/fonts` is an optional peer dependency for font assets.
|
|
17
13
|
|
|
18
|
-
##
|
|
14
|
+
## Usage
|
|
19
15
|
|
|
20
|
-
|
|
16
|
+
Parse a DOCX and save it while preserving untouched package parts:
|
|
21
17
|
|
|
22
18
|
```ts
|
|
23
19
|
import { readFile, writeFile } from 'node:fs/promises';
|
|
24
|
-
import { parseDocx, repackDocx } from '@betteroffice/docx
|
|
20
|
+
import { parseDocx, repackDocx } from '@betteroffice/docx';
|
|
25
21
|
|
|
26
|
-
const document = await parseDocx(await readFile('
|
|
22
|
+
const document = await parseDocx(await readFile('input.docx'));
|
|
27
23
|
// document.package.document.content: the body's paragraphs and tables
|
|
28
24
|
// document.package also holds styles, numbering, theme, media, headers and footers
|
|
29
|
-
|
|
30
25
|
const bytes = await repackDocx(document);
|
|
31
|
-
await writeFile('
|
|
26
|
+
await writeFile('output.docx', new Uint8Array(bytes));
|
|
32
27
|
```
|
|
33
28
|
|
|
34
|
-
|
|
35
|
-
preserved; use `createDocx` for documents built from scratch.
|
|
29
|
+
## Entry points
|
|
36
30
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
31
|
+
| entry point | description |
|
|
32
|
+
| --- | --- |
|
|
33
|
+
| `parseDocx` | Read an `ArrayBuffer`, `Uint8Array`, `Blob`, or `File` into a document model. |
|
|
34
|
+
| `repackDocx`, `repackDocxWithWarnings` | Save a parsed model against its original package; the latter also returns save warnings. |
|
|
35
|
+
| `createDocx` | Write a new document model without an original package. |
|
|
36
|
+
| `createYrsSession` (from `@betteroffice/docx/yrs`) | Asynchronously create a CRDT editing session. |
|
|
37
|
+
| `saveYrsDocx` (from `@betteroffice/docx/yrs`) | Save a session opened from DOCX bytes, returning bytes and persisted paragraph anchors. |
|
|
41
38
|
|
|
42
|
-
|
|
39
|
+
For a React editor and viewer, use
|
|
40
|
+
[`@betteroffice/docx-react`](https://www.npmjs.com/package/@betteroffice/docx-react).
|
|
43
41
|
|
|
44
|
-
|
|
42
|
+
## Runtime and assets
|
|
45
43
|
|
|
46
|
-
|
|
47
|
-
|
|
44
|
+
The package ships container, parser, layout, and editing WebAssembly assets in `dist/generated/`.
|
|
45
|
+
Async entry points load the assets they need; no manual initialization is required for parsing or
|
|
46
|
+
creating a session. Preserve the packaged asset URLs when bundling for a browser.
|
|
48
47
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
provider.connect();
|
|
53
|
-
```
|
|
48
|
+
Core sessions expose synchronous editing and read methods after asynchronous creation. The React
|
|
49
|
+
editor runs its engine in a Web Worker by default and exposes promise-returning document reads,
|
|
50
|
+
edits, and exports. Release a direct session with `session.destroy()` when finished.
|
|
54
51
|
|
|
55
|
-
|
|
56
|
-
`createWorkerCollaborationReplica`, or the value published by the React
|
|
57
|
-
editor's `collaboration.onReplica` callback. The provider handles Yjs sync-v1 and
|
|
58
|
-
awareness; room routing, authentication, and reconnection remain transport
|
|
59
|
-
concerns. Pass a persisted Yrs update as `collaboration.initialUpdate`
|
|
60
|
-
when a React editor joins an existing room so it hydrates the shared history
|
|
61
|
-
instead of independently importing the same DOCX.
|
|
52
|
+
## Undo grouping
|
|
62
53
|
|
|
63
|
-
|
|
54
|
+
A story is one text flow: the body, a header or footer, a footnote, or a comment.
|
|
55
|
+
`session.setUndoCaptureMode(mode)` selects how local transactions group into undo steps;
|
|
56
|
+
`session.undoCaptureMode()` reads the mode. Call `session.beginUndoCapture()` after import or seeding.
|
|
64
57
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
`
|
|
68
|
-
|
|
58
|
+
| Mode | Grouping |
|
|
59
|
+
| --- | --- |
|
|
60
|
+
| `auto` (default) | Nearby edits coalesce; switching stories closes the group. |
|
|
61
|
+
| `manual` | Edits coalesce until an explicit boundary, including across stories. |
|
|
69
62
|
|
|
70
|
-
|
|
71
|
-
|
|
63
|
+
`session.addUndoBoundary()` closes the current group. Changing modes also closes it while keeping
|
|
64
|
+
history; setting the same mode does not. In manual mode, close groups before unrelated edits.
|
|
65
|
+
Remote transactions stay outside local undo history; edit batches supply atomic execution.
|
|
72
66
|
|
|
73
|
-
|
|
67
|
+
## Selection state
|
|
74
68
|
|
|
75
|
-
`session.
|
|
76
|
-
|
|
69
|
+
`session.selectionContext(range)` aggregates formatting across a paragraph-addressed selection.
|
|
70
|
+
Toggle formatting is `true`, `false`, or `'mixed'`; value formatting is `null` when absent or
|
|
71
|
+
nonuniform. Use this state to drive custom formatting controls.
|
|
77
72
|
|
|
78
|
-
|
|
79
|
-
| --- | --- |
|
|
80
|
-
| `auto` (default) | Edits within 500 ms coalesce; switching stories closes the group. |
|
|
81
|
-
| `manual` | Edits coalesce across pauses and stories until an explicit boundary. |
|
|
73
|
+
## Host proposals
|
|
82
74
|
|
|
83
|
-
`session.
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
75
|
+
`session.proposeChanges({ expectVersion, proposals })` applies a version-checked round of tracked
|
|
76
|
+
changes grouped by host proposal IDs, outside undo history. Each proposal names its `id`, target
|
|
77
|
+
`paragraph`, an `op` (`replaceText` or `insertText`) and `suggest: { author, date }` for attribution.
|
|
78
|
+
A refusal leaves the round unapplied.
|
|
87
79
|
|
|
88
|
-
|
|
89
|
-
session.
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
session.addUndoBoundary();
|
|
93
|
-
session.deleteRange(markerRange);
|
|
94
|
-
session.addUndoBoundary();
|
|
95
|
-
session.setUndoCaptureMode('auto');
|
|
96
|
-
```
|
|
80
|
+
`session.setProposalStates({ expectVersion, expectPreviewVersion, changes })` changes how proposals
|
|
81
|
+
preview without changing the document or undo history. `session.withdrawProposals({ expectVersion,
|
|
82
|
+
ids })` settles accepted proposals as plain text and removes rejected or undecided ones, so later
|
|
83
|
+
rounds search the text shown by the preview.
|
|
97
84
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
edit batches provide atomic execution.
|
|
102
|
-
The host must close manual groups so later unrelated edits do not join them.
|
|
85
|
+
Read the registry with `session.getProposals()` or observe it with `session.onProposalChange()`.
|
|
86
|
+
Opening another document clears the registry. Use `proposalRevisionPreview(snapshot)` from
|
|
87
|
+
`@betteroffice/docx/yrs` as the layout render environment's `revisionPreview` to display decisions.
|
|
103
88
|
|
|
104
|
-
|
|
89
|
+
## Comment ranges
|
|
105
90
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
`generation`, as a deterministic shared seed needs. Source IDs are kept as authored, and every
|
|
110
|
-
paragraph authored in the session gets a fresh, valid ID that avoids every ID the
|
|
111
|
-
package already uses.
|
|
91
|
+
`session.setCommentRanges(commentId, ranges)` replaces an existing comment's sticky anchors while
|
|
92
|
+
preserving its metadata. Ranges may span paragraphs or address separate stories; each must be
|
|
93
|
+
nonempty, ordered, and within existing paragraphs. Invalid input throws before changing the comment.
|
|
112
94
|
|
|
113
|
-
`saveYrsDocx(session)`
|
|
114
|
-
|
|
95
|
+
Reanchoring joins local undo capture. `saveYrsDocx(session)` writes the current ranges; a host that
|
|
96
|
+
removes all commented text must choose a surviving range or remove the comment.
|
|
115
97
|
|
|
116
|
-
|
|
117
|
-
import { createYrsSession, saveYrsDocx } from '@betteroffice/docx/yrs';
|
|
98
|
+
## Edit batches
|
|
118
99
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
100
|
+
Read targets with their session version, then apply all steps atomically against that version.
|
|
101
|
+
See [version-checked DOCX batches](https://docs.betteroffice.dev/docs/javascript#edit-docx-in-version-checked-batches)
|
|
102
|
+
for operations, offsets, limits, and typed refusals.
|
|
122
103
|
|
|
123
|
-
|
|
124
|
-
reopened.openDocx(saved.bytes, true);
|
|
125
|
-
reopened.resolveParagraphAnchor(anchor); // { status: 'found', anchor: { kind: 'session', … } }
|
|
126
|
-
```
|
|
104
|
+
## Paragraph anchors
|
|
127
105
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
assigns IDs to source paragraphs without one across every story part, comments,
|
|
133
|
-
note separators and retained XML included, and repairs duplicates: source IDs and
|
|
134
|
-
saved IDs keep theirs over copies. It refuses, changing nothing, rather than guess at
|
|
135
|
-
an ambiguous comment reference. The change is replicated, stays out of undo history,
|
|
136
|
-
and later saves keep it; a save whose stories are otherwise unchanged patches the IDs
|
|
137
|
-
into the source bytes. `session.paragraphIdentities()` lists each paragraph's session,
|
|
138
|
-
persisted and exact-source anchors. A persisted anchor is scoped to the document the
|
|
139
|
-
host chose, and repeated source IDs resolve as `ambiguous`.
|
|
140
|
-
|
|
141
|
-
### Selection state
|
|
142
|
-
|
|
143
|
-
`session.selectionContext(range)` aggregates the range for toolbars and
|
|
144
|
-
assistive technology. Toggle marks, including `superscript` and `subscript`,
|
|
145
|
-
are `true`, `false` or `'mixed'`; value marks such as `fontFamily`, `color` and
|
|
146
|
-
`highlight` are `null` when absent or not uniform. `highlight` reports Word's
|
|
147
|
-
highlight name (`'yellow'`) or an unmapped hex value. `toggleMark` accepts
|
|
148
|
-
`{ type: 'superscript' }` and `{ type: 'subscript' }`; adding one clears the other.
|
|
149
|
-
|
|
150
|
-
### Version-checked edit batches
|
|
151
|
-
|
|
152
|
-
Read what you will target together with the session version, then apply a batch
|
|
153
|
-
against that version. Every step resolves against the state that was read; the
|
|
154
|
-
batch commits as one transaction or returns a typed refusal with nothing changed.
|
|
106
|
+
Session paragraph keys identify paragraphs within a collaborative session; persisted anchors identify
|
|
107
|
+
saved paragraphs by package part and OOXML paragraph ID. See
|
|
108
|
+
[DOCX paragraph anchors](https://docs.betteroffice.dev/docs/javascript#docx-paragraph-anchors)
|
|
109
|
+
for saving, reopening, resolving, and assigning IDs.
|
|
155
110
|
|
|
156
|
-
|
|
157
|
-
const read = session.readParagraphs({ story: 'body', view: 'accepted' });
|
|
158
|
-
if (!read.ok) throw new Error(read.failure.message);
|
|
159
|
-
const clause = read.paragraphs.find((paragraph) => paragraph.text.startsWith('Term'))!;
|
|
160
|
-
|
|
161
|
-
const result = session.applyEdits({
|
|
162
|
-
expectVersion: read.version,
|
|
163
|
-
steps: [
|
|
164
|
-
{
|
|
165
|
-
op: 'replaceText',
|
|
166
|
-
target: { kind: 'search', text: '30 days', view: 'accepted',
|
|
167
|
-
within: { kind: 'paragraph', story: 'body', paraId: clause.paraId } },
|
|
168
|
-
text: '45 days',
|
|
169
|
-
expect: { text: '30 days' },
|
|
170
|
-
},
|
|
171
|
-
{
|
|
172
|
-
op: 'insertParagraphs',
|
|
173
|
-
target: { story: 'body', paraId: clause.paraId },
|
|
174
|
-
at: 'end',
|
|
175
|
-
paragraphs: [{ text: 'Renewal is automatic.' }],
|
|
176
|
-
},
|
|
177
|
-
],
|
|
178
|
-
});
|
|
179
|
-
if (!result.ok) console.warn(result.failure.code, result.failure.message);
|
|
180
|
-
```
|
|
111
|
+
## Structured export
|
|
181
112
|
|
|
182
|
-
`
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
version.
|
|
113
|
+
For `exportDocxStructured` and `exportDocxMarkdown`, see
|
|
114
|
+
[structured content](https://docs.betteroffice.dev/docs/javascript#export-docx-as-structured-content)
|
|
115
|
+
for anchored JSON or Markdown, revision views, story selection, and live-session exports.
|
|
186
116
|
|
|
187
|
-
|
|
188
|
-
(
|
|
189
|
-
|
|
190
|
-
`accepted` view includes pending insertions and hides pending deletions;
|
|
191
|
-
`original` does the reverse. `findText` is exact, case-sensitive and
|
|
192
|
-
paragraph-local, and a `search` target must match exactly once in its scope.
|
|
117
|
+
For `exportDocxStructuredWithPages`, see
|
|
118
|
+
[page fragments](https://docs.betteroffice.dev/docs/javascript#page-fragments) for font requirements,
|
|
119
|
+
layout maps, and page geometry.
|
|
193
120
|
|
|
194
|
-
|
|
195
|
-
| --- | --- |
|
|
196
|
-
| `insertText` | Inserts at the start or end of a target, formatted like typing there. |
|
|
197
|
-
| `replaceText` | Replaces a target's text inside one paragraph, keeping the paragraph. |
|
|
198
|
-
| `deleteText` | Deletes a target's text inside one paragraph. |
|
|
199
|
-
| `insertParagraphs` | Inserts complete paragraphs before or after an anchor, in the given or the anchor's style. |
|
|
200
|
-
| `deleteParagraphs` | Deletes a contiguous span of complete paragraphs without merging properties into a neighbour. |
|
|
201
|
-
| `setParagraphStyle` | Applies a document paragraph style with its paragraph and run formatting. |
|
|
202
|
-
|
|
203
|
-
Refusals carry a `code` (`stale-version`, `missing-target`, `ambiguous-target`,
|
|
204
|
-
`content-mismatch`, `overlapping-steps`, `locked-target`,
|
|
205
|
-
`tracked-revision-conflict`, `unsupported`, `invalid-step`, `limit-exceeded`),
|
|
206
|
-
the failing `stepIndex`, and the target. Editor hosts add `read-only` for an
|
|
207
|
-
editor that does not accept edits. Malformed requests throw.
|
|
208
|
-
`validateEdits(request)` runs the same checks and previews each step without
|
|
209
|
-
changing anything or reserving ids.
|
|
210
|
-
|
|
211
|
-
An applied batch is exactly one undo step in both capture modes; pass
|
|
212
|
-
`history: 'none'` to keep it out of undo history while preserving existing undo
|
|
213
|
-
and redo entries. `source: 'agent'` records provenance only. Add
|
|
214
|
-
`suggest: { author, date }` to a text step to record it as a tracked change.
|
|
215
|
-
|
|
216
|
-
Paragraph ids are session keys; inserted paragraphs get Word paragraph IDs as
|
|
217
|
-
typed ones do, so `saveYrsDocx` returns persisted anchors for them.
|
|
218
|
-
|
|
219
|
-
### Host proposals
|
|
220
|
-
|
|
221
|
-
`session.proposeChanges()` records a round of reviewable tracked changes under
|
|
222
|
-
the host's own proposal ids. Each proposal names its paragraph by any paragraph
|
|
223
|
-
anchor, such as a persisted Word paragraph ID, and replaces, deletes
|
|
224
|
-
(`replaceWith: ''`) or inserts text:
|
|
121
|
+
Deprecated: `collectHeadings` from `@betteroffice/docx/utils`; use `session.headings(story)` instead.
|
|
225
122
|
|
|
226
|
-
|
|
227
|
-
const result = session.proposeChanges({
|
|
228
|
-
expectVersion: session.version(),
|
|
229
|
-
proposals: [
|
|
230
|
-
{
|
|
231
|
-
id: 'p-17',
|
|
232
|
-
paragraph: { kind: 'persisted', story: { partUri: '/word/document.xml', kind: 'body' }, paraId: '1A2B3C4D' },
|
|
233
|
-
suggest: { author: 'Reviewer', date: new Date().toISOString() },
|
|
234
|
-
op: 'replaceText',
|
|
235
|
-
search: '30 days',
|
|
236
|
-
replaceWith: '45 days',
|
|
237
|
-
occurrence: 'all',
|
|
238
|
-
},
|
|
239
|
-
],
|
|
240
|
-
});
|
|
241
|
-
```
|
|
123
|
+
## Content controls
|
|
242
124
|
|
|
243
|
-
`
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
empty (hyperlinks, fields, content controls and pending insertions count as
|
|
247
|
-
text), and refuses with `missing-target` anywhere else. Text typed into a
|
|
248
|
-
paragraph without runs takes the paragraph mark's run formatting, as in Word.
|
|
249
|
-
The round applies as one batch
|
|
250
|
-
outside undo history, or nothing changes and a typed refusal names the
|
|
251
|
-
`proposalId`: an ambiguous paragraph ID refuses with `ambiguous-target`, and
|
|
252
|
-
adjoining or overlapping proposals with `overlapping-steps`. Retrying an id with
|
|
253
|
-
the same edit is a no-op; the same id with another edit refuses with
|
|
254
|
-
`proposal-id-conflict`. Each record lists the revision ids its proposal created.
|
|
255
|
-
|
|
256
|
-
`session.setProposalStates()` marks proposals `accepted`, `rejected` or back to
|
|
257
|
-
`proposed`. Decisions keep the tracked revisions and change only how they render:
|
|
258
|
-
`proposalRevisionPreview(snapshot)` is the `revisionPreview` render environment
|
|
259
|
-
entry that shows accepted proposals as plain text and hides rejected ones. They
|
|
260
|
-
leave the document, its version and undo history untouched, and each change
|
|
261
|
-
increments `previewVersion`, which `expectPreviewVersion` checks
|
|
262
|
-
(`stale-preview`). `getProposals()` and `onProposalChange()` read and observe
|
|
263
|
-
the registry; opening another document forgets it.
|
|
264
|
-
|
|
265
|
-
`session.withdrawProposals({ expectVersion, ids })` ends proposals the host is
|
|
266
|
-
done with, settling each as its decision previews it: accepted ones apply as
|
|
267
|
-
plain text and rejected or undecided ones are removed, in one change outside
|
|
268
|
-
undo history. The document then reads as the preview did, so the next round's
|
|
269
|
-
searches match the text the reader saw. Ids that name no proposal are ignored.
|
|
270
|
-
|
|
271
|
-
### Structured export
|
|
272
|
-
|
|
273
|
-
Export read-only structured JSON or Markdown with block and inline anchors and
|
|
274
|
-
diagnostics.
|
|
125
|
+
`listDocxContentControls` and `findDocxContentControls` inspect controls in DOCX bytes.
|
|
126
|
+
See [content controls](https://docs.betteroffice.dev/docs/javascript#fill-docx-content-controls)
|
|
127
|
+
for session discovery, version-checked fills, and supported control types.
|
|
275
128
|
|
|
276
|
-
|
|
277
|
-
import { exportDocxMarkdown, exportDocxStructured } from '@betteroffice/docx';
|
|
278
|
-
|
|
279
|
-
const content = await exportDocxStructured(bytes, {
|
|
280
|
-
revisionView: 'accepted',
|
|
281
|
-
stories: ['body', 'headers', 'footers'],
|
|
282
|
-
});
|
|
283
|
-
for (const block of content.stories[0].blocks) {
|
|
284
|
-
if (block.kind === 'heading') console.log(block.heading.outlineLevel, block.anchor);
|
|
285
|
-
}
|
|
286
|
-
const { markdown, anchors } = await exportDocxMarkdown(bytes, { revisionView: 'markup' });
|
|
287
|
-
```
|
|
129
|
+
## Compare documents
|
|
288
130
|
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
committed, flushed or published. Bytes exports and `renderDocxMarkdown(content)`
|
|
293
|
-
return snapshot content and throw `DocxExportError` for unusable options. A
|
|
294
|
-
refusal's `failure` is `{ code, target, message }`, with `target: null` when it
|
|
295
|
-
concerns no anchor; `unsupported` means the session holds no document content.
|
|
296
|
-
`session.headings(story)` lists a story's headings classified the same way; the
|
|
297
|
-
legacy `collectHeadings` tree walker from `@betteroffice/docx/utils` is deprecated
|
|
298
|
-
in its favour.
|
|
299
|
-
|
|
300
|
-
Content is `schemaVersion: 1`: ordered stories (`body` by default; `headers`,
|
|
301
|
-
`footers`, `footnotes`, `endnotes` and `comments` only when selected), each a list
|
|
302
|
-
of paragraphs, headings (outline level and whether it came from direct formatting,
|
|
303
|
-
the style chain, document defaults or a `HeadingN` style id), list items (numbering
|
|
304
|
-
format and the rendered marker, counted as Word counts them: numbering instances of
|
|
305
|
-
one abstract definition continue each other unless one overrides a start and so
|
|
306
|
-
begins its own list, levels begin at `w:start` and restart as `w:lvlRestart` says),
|
|
307
|
-
tables on the source grid with spans, skipped grid columns and vertical-merge
|
|
308
|
-
continuations, content controls, section breaks, and placeholders naming the
|
|
309
|
-
source element of other content. Inlines cover text, tabs, line,
|
|
310
|
-
page and column breaks where the source has them, note and comment references,
|
|
311
|
-
fields with their cached result in result order, hyperlinks and nested fields
|
|
312
|
-
included, all anchored to the field (a numeric field's result
|
|
313
|
-
blocks stay inside the field), images (alt text and the relationship of the part
|
|
314
|
-
that owns them) and inline controls. A header or footer part
|
|
315
|
-
referenced through several relationships is one story with every section that
|
|
316
|
-
uses it while the copies hold the same content, and one story per copy with a
|
|
317
|
-
diagnostic once they differ. `revisionView` is required: `accepted` and
|
|
318
|
-
`original` project pending revisions like `readParagraphs`, and `markup` keeps
|
|
319
|
-
both with `revisions` attribution on each inline, moves as `moveFrom` and
|
|
320
|
-
`moveTo`. Other revision history exports as an anchored `unsupported`
|
|
321
|
-
placeholder with an `unsupported-revision` diagnostic.
|
|
322
|
-
|
|
323
|
-
Anchors use the batch offsets and session keys: a paragraph anchor's `paraId` is the
|
|
324
|
-
batch target key, and a range is paragraph-local UTF-16 in the view it
|
|
325
|
-
names, one U+FFFC per atom, the same shape the edit batches take; ranges from a
|
|
326
|
-
session export address the version it returned. Content without a session
|
|
327
|
-
location (comment bodies, omitted raw XML) carries a `sourcePart` anchor: the
|
|
328
|
-
part, its SHA-256, and element-child ordinals into its XML. Content with no
|
|
329
|
-
location of its own carries `{ kind: 'unlocated', story, reason }`: the reason is
|
|
330
|
-
`duplicate-paragraph-id` (a paragraph whose Word paragraph ID repeats an earlier one
|
|
331
|
-
opens with a session key of its own, and edits and merges repair a key two paragraphs
|
|
332
|
-
share, so only shared state written outside the engine has it), `story-too-large`,
|
|
333
|
-
`missing-story` or
|
|
334
|
-
`provenance-unavailable`. Comment metadata reads
|
|
335
|
-
the session's comment store once a field has been written there, and the source
|
|
336
|
-
until then. Ids are deterministic export-tree paths, and a bytes export is
|
|
337
|
-
identical across runs. Exports admit one top-level block at a time, charge text before copying it, and stop at
|
|
338
|
-
`maxBlocks` (10,000 by default, every nested block counted) or `maxBytes`
|
|
339
|
-
(8,388,608 by default, on the compact JSON) at a whole top-level block, set
|
|
340
|
-
`truncated`, and end with a `truncated` diagnostic; the prefix already admitted is
|
|
341
|
-
kept, and a larger `maxBytes` never keeps fewer blocks. Of a story longer than
|
|
342
|
-
four units per remaining byte beyond one million, only the leading paragraphs
|
|
343
|
-
within that bound are read: those that fit are kept, then the export stops and says
|
|
344
|
-
so. A comment body, field payload, shape or table cell past the bound is not read
|
|
345
|
-
at all. Markdown keeps story separators, headings, lists, tables (pipe tables, or
|
|
346
|
-
entity-escaped HTML, nested where needed, for merged or multi-block cells), attributed insertions and deletions, and a
|
|
347
|
-
`<!-- docx-export:N -->` marker per block, nested ones included, mapped to its
|
|
348
|
-
anchor in `anchors`. Only http, https, mailto and internal-anchor targets are linked,
|
|
349
|
-
judged after entity and percent decoding, and `&` in a target is written `&`; document text, titles and alt text never keep a
|
|
350
|
-
line break or unescaped HTML.
|
|
351
|
-
|
|
352
|
-
### Page fragments
|
|
353
|
-
|
|
354
|
-
A paged export attaches a page map to the structured content: the physical page,
|
|
355
|
-
section and displayed page label showing each block and inline, and the body,
|
|
356
|
-
header, footer or note occurrence it sits in.
|
|
131
|
+
`compareDocx` compares two DOCX files and returns tracked text changes or diagnostics.
|
|
132
|
+
See [DOCX comparison](https://docs.betteroffice.dev/docs/javascript#compare-docx-files-into-tracked-changes)
|
|
133
|
+
for supported inputs, attribution, and refusal behavior.
|
|
357
134
|
|
|
358
|
-
|
|
359
|
-
import { exportDocxStructuredWithPages, renderDocxMarkdownWithPages } from '@betteroffice/docx';
|
|
360
|
-
|
|
361
|
-
const paged = await exportDocxStructuredWithPages(
|
|
362
|
-
bytes,
|
|
363
|
-
{ revisionView: 'markup', stories: ['body', 'headers', 'footnotes'], includeGeometry: true },
|
|
364
|
-
{ fonts: [{ key: 'sans', data: fontBase64 }], defaultChain: ['sans'] }
|
|
365
|
-
);
|
|
366
|
-
paged.layout.pages; // pageIndex, displayedLabel, sectionIndex, parityFiller, size
|
|
367
|
-
paged.layout.fragments; // pageIndex, occurrenceId, nodeId, slice, continuation flags, geometry
|
|
368
|
-
const { markdown } = await renderDocxMarkdownWithPages(paged, { pageMarkers: true });
|
|
369
|
-
```
|
|
135
|
+
## Collaboration
|
|
370
136
|
|
|
371
|
-
`
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
its lowercase family, else `defaultChain`; nothing is looked up on the system and
|
|
375
|
-
no editor's fonts change), lays out every section, header, footer and note, and
|
|
376
|
-
exports from that one state. Its options are plain JSON: `measurementDefaults`
|
|
377
|
-
(`fontFamily`, `fontSize`), `renderEnvironment` (`showHiddenText`,
|
|
378
|
-
`defaultTabStopTwips`) and `compatibility`, all fingerprinted in the map's
|
|
379
|
-
provenance. A requirement left with no font, including the `measurementDefaults`
|
|
380
|
-
family text naming none falls back to, or any text measured with stand-in metrics
|
|
381
|
-
is refused as `layout-unavailable`, as is a live layout whose font store was
|
|
382
|
-
cleared or added to since.
|
|
383
|
-
The snapshot map is deterministic for the same bytes, fonts and options and
|
|
384
|
-
carries no session token. `session.exportStructuredWithPages(options)` reads the
|
|
385
|
-
region layout a session retains and lays nothing out, flushes nothing and loads no
|
|
386
|
-
font; the layout must have lowered the current version from the session's own
|
|
387
|
-
stories, with section, settings and note metadata that describe it and a
|
|
388
|
-
registered font for every requirement. Its map carries `documentVersion` and a
|
|
389
|
-
`layoutVersion` to pass back as `expectLayoutVersion`; its references describe the
|
|
390
|
-
authoritative layout of that version, which a later edit may supersede before it is
|
|
391
|
-
painted.
|
|
392
|
-
|
|
393
|
-
`pageIndex` counts physical pages from zero, blank parity pages included, while
|
|
394
|
-
`displayedNumber`/`displayedLabel` follow the section's PAGE numbering (restarts,
|
|
395
|
-
continuation and Roman or letter formats). A header or footer part stays one exported story with an occurrence
|
|
396
|
-
on every page showing it; a note has an occurrence where its note area is, which
|
|
397
|
-
may differ from its reference's page. Text slices are ranges in the export's own
|
|
398
|
-
offsets, split where the node, paragraph, view or page changes; atoms (fields,
|
|
399
|
-
controls, images, note marks, breaks) are sliced whole, and one split across
|
|
400
|
-
pages carries `partial` and an `anchor-only` diagnostic. Table
|
|
401
|
-
fragments list their row window with `continuedFromPrevious`, `continuedOnNext`
|
|
402
|
-
and `repeatedHeader`, and each cell paragraph has fragments of its own for the lines
|
|
403
|
-
its cell shows (a line the cell cuts through is listed with a `clipped-content`
|
|
404
|
-
diagnostic). Pages are
|
|
405
|
-
laid out with revision markup, so while a laid-out story holds pending revisions a
|
|
406
|
-
paged export reads the `markup` view, and `accepted` or `original` return
|
|
407
|
-
`unsupported-revision-layout`. The other refusal codes are `stale-document`,
|
|
408
|
-
`stale-layout` (stale section, settings or note metadata, fonts, options or
|
|
409
|
-
`expectLayoutVersion`), `layout-unavailable`, `layout-not-converged` and
|
|
410
|
-
`unsupported`.
|
|
411
|
-
Geometry is off by default; rectangles are unzoomed CSS pixels (96 per inch) from
|
|
412
|
-
the physical page's top-left corner. `maxFragments` (100,000 by default) and
|
|
413
|
-
`maxLayoutBytes` bound the map separately and mark it `truncated`; mapping stops a
|
|
414
|
-
page past the limit, so a truncated map leaves out the diagnostics of later pages
|
|
415
|
-
and of paragraphs no page shows. With
|
|
416
|
-
`pageMarkers`, Markdown follows each block marker with
|
|
417
|
-
`<!-- docx-pages: 0=i 1=ii -->` (labels percent-encoded) and writes no page break
|
|
418
|
-
into the text; a map from other content is refused. The map is `schemaVersion: 1`,
|
|
419
|
-
versioned apart from the content: additive fields keep the version, and a change
|
|
420
|
-
existing readers would misread bumps it.
|
|
421
|
-
|
|
422
|
-
### Content controls
|
|
423
|
-
|
|
424
|
-
List a document's content controls and fill its plain- and rich-text controls in
|
|
425
|
-
one version-checked batch.
|
|
137
|
+
`CollaborationProvider` from `@betteroffice/docx/collaboration` connects a replica to a binary
|
|
138
|
+
transport. See the [collaboration guide](https://docs.betteroffice.dev/docs/collaboration)
|
|
139
|
+
for shared state, transport wiring, checkpoints, and deployment.
|
|
426
140
|
|
|
427
|
-
|
|
428
|
-
const read = session.listContentControls();
|
|
429
|
-
if (!read.ok) throw new Error(read.failure.message);
|
|
430
|
-
const name = read.content.controls.find((control) => control.tag === 'customer.name')!;
|
|
431
|
-
|
|
432
|
-
const result = session.applyEdits({
|
|
433
|
-
expectVersion: read.version,
|
|
434
|
-
steps: [
|
|
435
|
-
{ op: 'setContentControlText', target: { kind: 'id', controlId: name.controlId }, text: 'Ada Lovelace' },
|
|
436
|
-
{ op: 'setContentControlText', target: { kind: 'tag', tag: 'customer.address' }, text: '12 Example Street\nLondon' },
|
|
437
|
-
],
|
|
438
|
-
});
|
|
439
|
-
```
|
|
141
|
+
## Editor plugins
|
|
440
142
|
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
export's control metadata (`controlId`, `ooxmlId`, `controlType`, `tag`, `alias`,
|
|
444
|
-
`lock`, `showingPlaceholder`, `dataBound`) plus `placement`, `anchor`,
|
|
445
|
-
`parentControlId`, `value` (its text with tabs as `\t` and line and paragraph
|
|
446
|
-
breaks as LF, or `unavailable` with a reason), `multiLine` and `effectiveLock`,
|
|
447
|
-
which folds in the locks of the controls containing it. `stories` narrows the read
|
|
448
|
-
to some categories; `maxControls` (10,000) and `maxBytes` (8,388,608) refuse with
|
|
449
|
-
`limit-exceeded` rather than returning part of the list, and `complete: false`
|
|
450
|
-
includes coverage diagnostics. Tags, aliases and
|
|
451
|
-
`ooxmlId`s match exactly and case-sensitively, and `findContentControls` returns
|
|
452
|
-
every match. `listDocxContentControls(bytes)` and
|
|
453
|
-
`findDocxContentControls(bytes, query)` read bytes without a session and throw
|
|
454
|
-
`DocxContentControlsError` for refused options. Concurrent fills from
|
|
455
|
-
collaborators resolve last-writer-wins for an inline control and merge like
|
|
456
|
-
concurrent typing for a block control.
|
|
457
|
-
|
|
458
|
-
A step's `target` is `{ kind: 'id', controlId }`, `{ kind: 'tag', tag }` or
|
|
459
|
-
`{ kind: 'ooxmlId', ooxmlId }`. `controlId` is a locator scoped to the
|
|
460
|
-
version or snapshot it was read at; list again after a version change. Tags are the template author's names for
|
|
461
|
-
controls. `ooxmlId` is the control's authored `w:id`, the identity that survives
|
|
462
|
-
save and reopen. A tag or `ooxmlId` write searches every story regardless of read
|
|
463
|
-
filters and needs exactly one control in the document to carry it.
|
|
464
|
-
|
|
465
|
-
`setContentControlText` replaces the content of a plain- or rich-text control
|
|
466
|
-
with plain text and clears its placeholder flag everywhere it is recorded,
|
|
467
|
-
leaving tags, aliases, ids, bindings and every other property as captured. CRLF
|
|
468
|
-
becomes LF; LF is a line break, except in a rich-text block control, where it
|
|
469
|
-
starts a paragraph: surviving paragraphs keep their ids and properties and new
|
|
470
|
-
ones take the first paragraph's style defaults. A plain-text control accepts LF
|
|
471
|
-
only when its `w:text` sets `w:multiLine` and keeps its lines in one paragraph.
|
|
472
|
-
The text takes the formatting of the control's first text run, or the control's
|
|
473
|
-
own run properties while it shows its placeholder. Equal text is a no-op unless
|
|
474
|
-
the placeholder still shows or a plain-text control holds several paragraphs,
|
|
475
|
-
which the fill joins into one. Refusals
|
|
476
|
-
carry the batch code and a `reason`: `missing-control`, `missing-tag` or
|
|
477
|
-
`missing-ooxml-id`, `ambiguous-control-id`, `ambiguous-tag` or
|
|
478
|
-
`ambiguous-ooxml-id`, `content-locked`, `bound-control`,
|
|
479
|
-
`unsupported-control-type`, `unsupported-children` (content other than text,
|
|
480
|
-
tabs and line breaks, such as bookmarks, fields or tables), `nested-controls`,
|
|
481
|
-
`unknown-lock`, `unsupported-suggestion`, `provenance-unavailable` (the session
|
|
482
|
-
was not opened from DOCX bytes), `unsupported-story` (a control kept only in
|
|
483
|
-
source XML, such as a comment body), `multiline-not-allowed` and `invalid-text`.
|
|
484
|
-
Checkbox, dropdown and date controls keep `setContentControlValue`; a
|
|
485
|
-
string passed to it fills a text control through the same step, and
|
|
486
|
-
`clearContentControlValue` never erases a text control's text.
|
|
487
|
-
|
|
488
|
-
### Compare documents into tracked changes
|
|
489
|
-
|
|
490
|
-
Compare an original and a revised DOCX and get back the original with the
|
|
491
|
-
body text differences as tracked insertions and deletions, attributed to the
|
|
492
|
-
author and date you pass:
|
|
143
|
+
For React host extensions, use the
|
|
144
|
+
[DOCX plugin API](https://docs.betteroffice.dev/docs/javascript#host-docx-editor-plugins).
|
|
493
145
|
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
const result = await compareDocx(original, revised, {
|
|
498
|
-
author: 'Contract review',
|
|
499
|
-
date: '2024-05-06T09:30:00+02:00',
|
|
500
|
-
granularity: 'word',
|
|
501
|
-
unsupported: 'fail',
|
|
502
|
-
});
|
|
503
|
-
if (result.ok) {
|
|
504
|
-
for (const change of result.changes) console.log(change.id, change.kind, change.revised.text);
|
|
505
|
-
} else {
|
|
506
|
-
console.warn(result.diagnostics.map((diagnostic) => diagnostic.code));
|
|
507
|
-
}
|
|
508
|
-
```
|
|
146
|
+
Deprecated: `EditorPluginCore`, `PluginPanelProps`, `PanelConfig`, and `SidebarItemContext` from
|
|
147
|
+
`@betteroffice/docx/plugin-api`; use `defineDocxPlugin` and `plugins` from `@betteroffice/docx-react`.
|
|
509
148
|
|
|
510
|
-
|
|
511
|
-
packages are inspected in full before anything is authored, and any blocking
|
|
512
|
-
difference refuses the comparison with `{ ok: false, diagnostics }`; no partial
|
|
513
|
-
redline is ever returned. `unsupported: 'report'` keeps inspecting and returns
|
|
514
|
-
every diagnostic, bounded by `limits.maxDiagnostics`. Blocking codes are
|
|
515
|
-
`invalid-options`, `invalid-docx`, `existing-revisions` (anywhere, headers and
|
|
516
|
-
property revisions included), `paragraph-insertion`, `paragraph-deletion`,
|
|
517
|
-
`paragraph-move`, `ambiguous-alignment`, `table-change`, `structure-change`,
|
|
518
|
-
`field-change`, `object-change`, `content-control-change`, `formatting-change`,
|
|
519
|
-
`unsupported-content` and `unsupported-formatting` (a changed paragraph holding
|
|
520
|
-
content, or run properties, that edits cannot carry, such as fields,
|
|
521
|
-
hyperlinks, bookmarks, objects, page breaks, patterned shading or `w:lang`),
|
|
522
|
-
`out-of-scope-change` (headers, footers, notes, comments), `opaque-part-change`,
|
|
523
|
-
`provenance-unavailable`, `limit-exceeded`, `diagnostics-truncated`,
|
|
524
|
-
`batch-refused`, `serialization-failed` and `roundtrip-mismatch`.
|
|
525
|
-
`metadata-difference` (informational: modified dates, revision-session ids and
|
|
526
|
-
document statistics, kept from the original) and `ambiguous-identity` (a
|
|
527
|
-
warning: repeated paragraph ids, so changes are addressed by position) accompany
|
|
528
|
-
a result. Every part whose content type or content is XML is scanned for
|
|
529
|
-
revisions, and wrappers such as custom XML elements, body-level markers and
|
|
530
|
-
every part must match exactly; an XML-typed part that cannot be read refuses the
|
|
531
|
-
comparison.
|
|
532
|
-
|
|
533
|
-
Paragraphs are aligned conservatively: text unique to one paragraph on each side
|
|
534
|
-
anchors the alignment, crossing anchors are moves, one paragraph against one
|
|
535
|
-
between anchors is a replacement, and several need shared words and exactly one
|
|
536
|
-
best correspondence. Clearing a paragraph's text is a deletion; the paragraph
|
|
537
|
-
mark stays. Differences are reported by Unicode word, keeping punctuation and
|
|
538
|
-
whitespace tokens, or with `granularity: 'char'` by grapheme cluster. Each
|
|
539
|
-
change is one tracked replacement: deleted text keeps its formatting and
|
|
540
|
-
inserted text takes the revised formatting. Changes are ordered by original
|
|
541
|
-
position, `id` is `change-0`, `change-1` and so on, and their spans address the
|
|
542
|
-
inputs by part, SHA-256 and element-child path with paragraph-local UTF-16
|
|
543
|
-
offsets. `date` must be RFC 3339 with an explicit offset and is recorded in UTC;
|
|
544
|
-
the clock is never read. `author` must not be blank.
|
|
545
|
-
|
|
546
|
-
Only the changed paragraphs' content is rewritten; their start tags and
|
|
547
|
-
properties, every other paragraph and every other part keep the original's
|
|
548
|
-
bytes, and identical inputs return the original bytes. Rewritten runs carry the
|
|
549
|
-
session's formatting vocabulary, so run properties a style supplies are written
|
|
550
|
-
as direct formatting, and complex-script fonts, sizes, bold and italic are
|
|
551
|
-
written only as the source states them. The result is reopened and checked
|
|
552
|
-
against both inputs before it is returned, with each run's effective formatting
|
|
553
|
-
read from the XML, style toggles such as bold combined across every level of a
|
|
554
|
-
style's `basedOn` chain; a difference refuses with `roundtrip-mismatch`.
|
|
555
|
-
Defaults, which are also the ceilings: 32 MiB per input, 128 MiB inflated,
|
|
556
|
-
10,000 paragraphs per input and 1,048,576 UTF-16 units of text in both inputs,
|
|
557
|
-
counted across every story while the parts are read, 250,000 alignment cells,
|
|
558
|
-
4,000,000 diff cells, 128 changes, 256 diagnostics, 64 MiB of staged state,
|
|
559
|
-
8 MiB of result JSON without the output package (at least 1 KiB) and a 64 MiB
|
|
560
|
-
output, the no-op included.
|
|
561
|
-
Review in BetterOffice is tested.
|
|
562
|
-
|
|
563
|
-
### Editor plugin contract (deprecated)
|
|
564
|
-
|
|
565
|
-
`@betteroffice/docx/plugin-api` keeps its geometry and sidebar building blocks:
|
|
566
|
-
`RenderedDomContext`, `PositionCoordinates`, `SidebarItem`,
|
|
567
|
-
`createRenderedDomContext`, `createCanvasHostProjector` and
|
|
568
|
-
`resolveItemPositions`. The snapshot-based `EditorPluginCore`,
|
|
569
|
-
`PluginPanelProps`, `PanelConfig` and `SidebarItemContext` are deprecated: they
|
|
570
|
-
pass serialized `Document` snapshots and have no host that manages their
|
|
571
|
-
lifecycle. Host plugins for the React editor use `defineDocxPlugin` and the
|
|
572
|
-
`plugins` prop of
|
|
573
|
-
[`@betteroffice/docx-react`](https://www.npmjs.com/package/@betteroffice/docx-react),
|
|
574
|
-
which provide versioned reads, granted commands and edit batches, lifecycle
|
|
575
|
-
events and cleanup.
|
|
576
|
-
|
|
577
|
-
### Reanchor an existing comment
|
|
578
|
-
|
|
579
|
-
`session.setCommentRanges(commentId, ranges)` replaces only the sticky anchors of
|
|
580
|
-
an existing comment. The id, author, date, body, reply relationship and resolution
|
|
581
|
-
state remain intact. Ranges use the same paragraph locations as `addComment`;
|
|
582
|
-
one range may span paragraphs, and separate ranges may address different stories.
|
|
583
|
-
|
|
584
|
-
Every range must be non-empty, ordered, and within existing paragraphs. An empty
|
|
585
|
-
list, unknown comment/story/paragraph, or invalid offset throws before any content
|
|
586
|
-
changes. The host supplies the surviving text's new range.
|
|
587
|
-
|
|
588
|
-
If replacement removes all commented text, the host must explicitly choose a new
|
|
589
|
-
non-empty range or handle the comment's removal. Rejected reanchoring leaves the
|
|
590
|
-
comment unchanged and does not roll back a text replacement already performed.
|
|
591
|
-
|
|
592
|
-
`saveYrsDocx()` writes the current ranges under the comment's own ID, with its
|
|
593
|
-
author, date, body, replies and resolution.
|
|
594
|
-
|
|
595
|
-
Reanchoring joins the current local undo capture, so an immediate replacement and
|
|
596
|
-
reanchor can undo together. Comment edits participate in the session's history;
|
|
597
|
-
undoing comment changes conservatively invalidates all stories for saved anchors.
|
|
598
|
-
|
|
599
|
-
The undo manager retains comment item boundaries with their undo/redo entries so
|
|
600
|
-
anchors inside replaced text survive repeated history traversal with Yrs 0.27.
|
|
601
|
-
Discarding history releases its bookkeeping; undo/redo traverses a local snapshot
|
|
602
|
-
when those boundaries need restoration.
|
|
603
|
-
|
|
604
|
-
### External drop positions
|
|
605
|
-
|
|
606
|
-
`RenderedDomContext.getPositionAtPoint(clientX, clientY)` queries the same canvas
|
|
607
|
-
hit testing used for the caret. Pass viewport coordinates, such as a drop event's
|
|
608
|
-
`clientX` and `clientY`. Page scroll and CSS zoom are resolved from the live page
|
|
609
|
-
geometry. The query never changes selection or focus.
|
|
610
|
-
|
|
611
|
-
The result contains `position`, `pageIndex`, and `region`. Header/footer hits also
|
|
612
|
-
carry `rId`; footnote/endnote hits carry `noteId`. Positions are local to that
|
|
613
|
-
region's display stream in the layout the context renders, and shift with every
|
|
614
|
-
edit.
|
|
615
|
-
|
|
616
|
-
In React, `editorRef.getPositionAtPoint(clientX, clientY)` runs the same query and
|
|
617
|
-
adds `version` and `target`: the collapsed accepted-view range at the point, keyed
|
|
618
|
-
by session paragraph keys, in the body, header, footer, note or table cell story
|
|
619
|
-
the point is in. Use it as a text step's target with `expectVersion: version`:
|
|
149
|
+
## Fonts for custom adapters
|
|
620
150
|
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
}
|
|
629
|
-
```
|
|
151
|
+
See [font setup](https://docs.betteroffice.dev/docs/packages#fonts) for optional measurement assets.
|
|
152
|
+
For browser font loading, `createFontLoadScope()` from `@betteroffice/docx/utils` isolates each
|
|
153
|
+
editor's load notifications. `registerDocumentFaces(faces, scope)` registers embedded faces and
|
|
154
|
+
returns their CSS family aliases when documents use different bytes under the same family name.
|
|
155
|
+
Release the scope with `scope.dispose()` when its editor is removed.
|
|
156
|
+
|
|
157
|
+
## Documentation
|
|
630
158
|
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
`YrsLoc` (offsets that count text a pending deletion hides) without the version
|
|
638
|
-
check; passing a number to it retains body mapping.
|
|
639
|
-
|
|
640
|
-
Text runs and their editable content boxes are accepted. Page margins, images,
|
|
641
|
-
page gaps, points outside pages, and queries without ready canvas geometry return
|
|
642
|
-
`null`.
|
|
643
|
-
|
|
644
|
-
### Font loads per editor
|
|
645
|
-
|
|
646
|
-
Several editors can share one page. `createFontLoadScope()` gives each its own
|
|
647
|
-
font loads: `onFontsLoaded` and `onFontError` on a scope hear the loads made
|
|
648
|
-
through it and module-level ones, never another scope's. `registerDocumentFaces(faces, scope)`
|
|
649
|
-
registers a document's embedded faces under their own family names, or under
|
|
650
|
-
an alias when another open document embeds different faces under the same name,
|
|
651
|
-
and resolves to the family each was registered under. Draw with that map as
|
|
652
|
-
`DrawPageOptions.fontFamilies`. The scope holds the faces until its next
|
|
653
|
-
document or `dispose()`. `loadEmbeddedFontFamilies` does the same from a
|
|
654
|
-
package's font table.
|
|
159
|
+
- [JavaScript guide](https://docs.betteroffice.dev/docs/javascript)
|
|
160
|
+
- [Collaboration guide](https://docs.betteroffice.dev/docs/collaboration)
|
|
161
|
+
- [Package guide](https://docs.betteroffice.dev/docs/packages)
|
|
162
|
+
- [React editor on npm](https://www.npmjs.com/package/@betteroffice/docx-react)
|
|
163
|
+
- [Locale strings on npm](https://www.npmjs.com/package/@betteroffice/docx-i18n)
|
|
164
|
+
- [Repository](https://github.com/openooxml/betteroffice)
|