@betteroffice/docx 0.4.2 → 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.
Files changed (124) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +108 -598
  3. package/dist/chunk-25WGDXBF.mjs +1 -0
  4. package/dist/chunk-2CCPNW44.mjs +1 -0
  5. package/dist/chunk-32A4I3T4.mjs +1 -0
  6. package/dist/chunk-4LO3PSLZ.mjs +2 -0
  7. package/dist/chunk-67MGRMP3.mjs +1 -0
  8. package/dist/chunk-6AUR4AA2.mjs +1 -0
  9. package/dist/chunk-6WT6HKAY.mjs +1 -0
  10. package/dist/chunk-77ZWGQZN.mjs +1 -0
  11. package/dist/chunk-7C3Y7LIH.mjs +1 -0
  12. package/dist/chunk-7WIUNGQW.mjs +1 -0
  13. package/dist/chunk-7WWFGMMF.mjs +1 -0
  14. package/dist/chunk-AK2NG7PT.mjs +1 -0
  15. package/dist/chunk-BR5BFOHF.mjs +1 -0
  16. package/dist/chunk-CDNWHQH2.mjs +1 -0
  17. package/dist/chunk-CRTTEHZN.mjs +1 -0
  18. package/dist/chunk-ETDMQ5GE.mjs +1 -0
  19. package/dist/{wasm-QXYYV7LU.mjs → chunk-GBOHQ3CJ.mjs} +1 -1
  20. package/dist/chunk-H7K4SJKK.mjs +1 -0
  21. package/dist/chunk-ICJH7QUY.mjs +1 -0
  22. package/dist/chunk-IHETKRDA.mjs +1 -0
  23. package/dist/chunk-ISBZLVIF.mjs +1 -0
  24. package/dist/chunk-JSYH6HNV.mjs +2 -0
  25. package/dist/chunk-KICHSQFN.mjs +1 -0
  26. package/dist/chunk-KXIGXLFA.mjs +5 -0
  27. package/dist/chunk-KXLTK7AT.mjs +57 -0
  28. package/dist/chunk-L3JRNJ34.mjs +1 -0
  29. package/dist/chunk-LKDHP22R.mjs +1 -0
  30. package/dist/chunk-LPOEM5WH.mjs +1 -0
  31. package/dist/chunk-M44BABH2.mjs +1 -0
  32. package/dist/chunk-MAYPJU23.mjs +1 -0
  33. package/dist/chunk-N5RE2S3U.mjs +2 -0
  34. package/dist/chunk-NLYHFVF5.mjs +1 -0
  35. package/dist/chunk-ONYCW7VV.mjs +4 -0
  36. package/dist/chunk-QGZ2AB3M.mjs +1 -0
  37. package/dist/chunk-QWHU7YIW.mjs +2 -0
  38. package/dist/{chunk-VH7P4734.mjs → chunk-QX2CDKPG.mjs} +1 -1
  39. package/dist/chunk-RGDE6YQB.mjs +1 -0
  40. package/dist/chunk-TE3AU2JK.mjs +1 -0
  41. package/dist/chunk-UDVEZ722.mjs +1 -0
  42. package/dist/chunk-UQSVFDRV.mjs +2 -0
  43. package/dist/chunk-VHOOSS4D.mjs +1 -0
  44. package/dist/chunk-WIDM4EMT.mjs +1 -0
  45. package/dist/chunk-YITJ4QAO.mjs +1 -0
  46. package/dist/collaboration/index.d.mts +111 -11
  47. package/dist/collaboration/index.mjs +1 -1
  48. package/dist/{content-DSb-6a9T.d.mts → content-DVT06V_6.d.mts} +24 -3
  49. package/dist/core.d.mts +9 -9
  50. package/dist/core.mjs +1 -1
  51. package/dist/displayListQueries-6XjS7B5e.d.mts +1473 -0
  52. package/dist/docx/index.d.mts +8 -7
  53. package/dist/docx/index.mjs +1 -2
  54. package/dist/docxWorker.mjs +1 -0
  55. package/dist/editor/index.d.mts +14 -12
  56. package/dist/editor/index.mjs +1 -1
  57. package/dist/{fontLoader-BNSs0E8K.d.mts → fontLoader-CBkmh4ps.d.mts} +1 -1
  58. package/dist/{formatting-gqLFeJ9X.d.mts → formatting-p5MneNuy.d.mts} +32 -1
  59. package/dist/generated/edit/docx_edit_bg.wasm +0 -0
  60. package/dist/generated/layout/docx_layout_bg.wasm +0 -0
  61. package/dist/generated/opc/ooxml_opc_bg.wasm +0 -0
  62. package/dist/generated/parse/docx_parse_bg.wasm +0 -0
  63. package/dist/index-C-KKULi5.d.mts +5596 -0
  64. package/dist/layout/index.d.mts +4 -4
  65. package/dist/layout/index.mjs +1 -1
  66. package/dist/layout/pagination/index.d.mts +4 -4
  67. package/dist/layout/render/index.d.mts +37 -53
  68. package/dist/layout/render/index.mjs +3 -1
  69. package/dist/{pagedExport-BmSlEhoP.d.mts → pagedExport-DDgA5O7E.d.mts} +92 -9
  70. package/dist/plugin-api/RenderedDomContext.d.mts +5 -7
  71. package/dist/plugin-api/index.d.mts +3 -3
  72. package/dist/plugin-api/resolveItemPositions.d.mts +3 -3
  73. package/dist/{rezip-Bxiq4Z_R.d.mts → rezip-CfwAYdV4.d.mts} +7 -1
  74. package/dist/styles/index.d.mts +3 -3
  75. package/dist/styles/index.mjs +1 -1
  76. package/dist/{styles-AryxLAhx.d.mts → styles-DZZz2oRy.d.mts} +2 -2
  77. package/dist/types/content.d.mts +2 -2
  78. package/dist/types/document.d.mts +6 -6
  79. package/dist/{types-Cz7S77Ut.d.mts → types-bvPg3KsF.d.mts} +16 -3
  80. package/dist/utils/comments.d.mts +2 -2
  81. package/dist/utils/findReplace.mjs +1 -2
  82. package/dist/utils/index.d.mts +5 -5
  83. package/dist/utils/index.mjs +1 -1
  84. package/dist/utils/listState.d.mts +2 -5
  85. package/dist/utils/stylePreview.d.mts +2 -2
  86. package/dist/utils/stylePreview.mjs +1 -1
  87. package/dist/wasm-edit.d.mts +233 -48
  88. package/dist/wasm-edit.mjs +1 -1
  89. package/dist/wasm-layout.d.mts +7 -1
  90. package/dist/wasm-layout.mjs +1 -1
  91. package/dist/wasm-opc.mjs +1 -1
  92. package/dist/wasm-parse.d.mts +7 -1
  93. package/dist/wasm-parse.mjs +1 -1
  94. package/dist/worker.d.mts +60 -0
  95. package/dist/worker.mjs +1 -0
  96. package/dist/yrs/docxWorker.d.mts +20 -0
  97. package/dist/yrs/docxWorker.mjs +3 -0
  98. package/dist/yrs/index.d.mts +12 -7
  99. package/dist/yrs/index.mjs +1 -1
  100. package/package.json +14 -4
  101. package/dist/chunk-47Q2FMFF.mjs +0 -1
  102. package/dist/chunk-4PRCO6EC.mjs +0 -1
  103. package/dist/chunk-5Z22FORG.mjs +0 -1
  104. package/dist/chunk-7SUVUKWT.mjs +0 -1
  105. package/dist/chunk-7ZV6P6NZ.mjs +0 -57
  106. package/dist/chunk-AXFMDV66.mjs +0 -1
  107. package/dist/chunk-EBYC3CX7.mjs +0 -1
  108. package/dist/chunk-GKWXOZZH.mjs +0 -1
  109. package/dist/chunk-LIOWZSE7.mjs +0 -1
  110. package/dist/chunk-ML6E23W2.mjs +0 -1
  111. package/dist/chunk-MLEMGJP7.mjs +0 -1
  112. package/dist/chunk-QBMNRD5V.mjs +0 -2
  113. package/dist/chunk-RNNTXPXC.mjs +0 -1
  114. package/dist/compareDocx-CWOYEBCE.mjs +0 -1
  115. package/dist/computeLayout-ZTO6TBWX.mjs +0 -1
  116. package/dist/displayListQueries-BVBDVXoP.d.mts +0 -306
  117. package/dist/frameDelta-CLeO7K5z.d.mts +0 -807
  118. package/dist/index-DzZVJC8-.d.mts +0 -2467
  119. package/dist/parseWasm-BESKFFA3.mjs +0 -1
  120. package/dist/rustDisplayList-B7uPoItK.d.mts +0 -315
  121. package/dist/wasm-GFT55YT6.mjs +0 -1
  122. package/dist/wasm-QI7MWC5D.mjs +0 -1
  123. package/dist/yrs/residentEngineWorker.d.mts +0 -2
  124. package/dist/yrs/residentEngineWorker.mjs +0 -1
package/README.md CHANGED
@@ -1,654 +1,164 @@
1
1
  # @betteroffice/docx
2
2
 
3
- Framework-free core for the BetterOffice DOCX editor — the Rust engine (OOXML
4
- parse/serialize, CRDT editing core, text shaping, pagination) compiled to
5
- WebAssembly, plus the display-list, canvas-render, geometry, and accessibility
6
- helpers the adapters build on. Layout never touches the DOM: the engine
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
- Most apps want the turnkey React component in
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
- ## Parse and save
14
+ ## Usage
19
15
 
20
- A full round trip: open a `.docx`, inspect the typed model, write bytes back.
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/docx';
20
+ import { parseDocx, repackDocx } from '@betteroffice/docx';
25
21
 
26
- const document = await parseDocx(await readFile('contract.docx'));
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('contract-out.docx', Buffer.from(bytes));
26
+ await writeFile('output.docx', new Uint8Array(bytes));
32
27
  ```
33
28
 
34
- `repackDocx` round-trips against the original buffer so untouched parts are
35
- preserved; use `createDocx` for documents built from scratch.
29
+ ## Entry points
36
30
 
37
- The engine ships as four wasm assets (container, parser, layout, editing core)
38
- in `dist/generated/`. Browsers fetch them lazily behind the async entry points
39
- (`parseDocx`, save, the layout engine, `createYrsSession`); Node and Bun read
40
- them from disk synchronously on first use. No manual init call is required.
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
- ## Collaboration
39
+ For a React editor and viewer, use
40
+ [`@betteroffice/docx-react`](https://www.npmjs.com/package/@betteroffice/docx-react).
43
41
 
44
- Connect the editor's Yrs replica to any reliable binary transport:
42
+ ## Runtime and assets
45
43
 
46
- ```ts
47
- import { CollaborationProvider } from '@betteroffice/docx/collaboration';
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
- const provider = new CollaborationProvider(replica, createTransport(), {
50
- user: { name: "Ada" }, // identity for this peer's remote caret
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
- `replica` can be a direct `YrsSession`, the worker-aware adapter returned by
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
- ## Development
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
- The generated `.wasm` binaries are intentionally not committed. From the
66
- repository root, install `wasm-pack` 0.15.0 and `binaryen`, then run
67
- `bun run build:docx-wasm`.
68
- Package builds, demo startup, and CI run this step automatically.
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
- [JavaScript guide](https://docs.betteroffice.dev/docs/javascript) ·
71
- [Changelog](https://github.com/openooxml/betteroffice/blob/main/packages/docx/CHANGELOG.md) · Apache-2.0.
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
- ### Undo capture modes and boundaries
67
+ ## Selection state
74
68
 
75
- `session.setUndoCaptureMode(mode)` selects how tracked local transactions are
76
- combined into undo steps. `session.undoCaptureMode()` returns the current mode.
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
- | Mode | Grouping |
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.addUndoBoundary()` closes the current group in any mode. Changing modes
84
- also closes the group; setting the same mode again does not. Both operations
85
- preserve undo/redo history and are safe before capture starts. Repeated boundaries
86
- create no empty undo steps. Remote transactions remain outside local undo history.
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
- ```ts
89
- session.setUndoCaptureMode('manual');
90
- session.insertText(firstLocation, 'Prefixo ');
91
- session.insertText(secondLocation, 'Suffix');
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
- The two insertions undo together; marker deletion is a separate step. Boundaries
99
- are also available in automatic mode when a host action needs to be isolated from
100
- surrounding typing. Undo/redo close capture as usual. Manual mode groups history;
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
- ### Paragraph identities
89
+ ## Comment ranges
105
90
 
106
- A session addresses paragraphs by session keys (`YrsLoc.paraId`); Word stores its
107
- own paragraph ID (`w14:paraId`) in the file. A session anchor resolves on every
108
- replica of one collaborative session, and each seeding open (`openDocx`, `seedFromDocx`, `documentToYrs`) starts a new one unless given a fixed
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)` saves a session opened from DOCX bytes and returns each saved
114
- paragraph's persisted anchor, qualified by its package part:
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
- ```ts
117
- import { createYrsSession, saveYrsDocx } from '@betteroffice/docx/yrs';
98
+ ## Edit batches
118
99
 
119
- const { secondParaId } = session.splitParagraph({ story: 'body', paraId, offset: 12 });
120
- const saved = await saveYrsDocx(session);
121
- const anchor = saved.paragraphs.find((p) => p.session.paraId === secondParaId)!.persisted;
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
- const reopened = await createYrsSession();
124
- reopened.openDocx(saved.bytes, true);
125
- reopened.resolveParagraphAnchor(anchor); // { status: 'found', anchor: { kind: 'session', … } }
126
- ```
104
+ ## Paragraph anchors
127
105
 
128
- `saved.conflicts` lists saved paragraphs whose ID the live session reassigned while the
129
- save ran, such as by a duplicate repair after a remote update; their anchors find them in
130
- the saved bytes only. The React editor's Save writes and records these IDs. `saveYrsDocx`
131
- also returns anchors and preserves unchanged source parts. `session.persistParagraphIds()`
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
- ```ts
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
- `version()` changes with every committed change, local or remote (including
183
- undo and redo), and when the session reopens its document. Tokens are scoped to
184
- one session; after a lost response, read again rather than retrying with a new
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
- Offsets are UTF-16 positions into a paragraph's projected text. Each inline atom
188
- (hard break, image, content control, note reference, field, other embed) is one
189
- U+FFFC listed in `atoms`, tabs stay `\t`, and paragraph marks are excluded. The
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
- | Step | Effect |
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
- ```ts
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
- `occurrence` is `'first'` (the default), `'all'` or a one-based number, counting
244
- non-overlapping matches of the accepted text. `search: ''` fills an empty
245
- paragraph: it matches once, at the start of a paragraph whose accepted text is
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
- ```ts
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
- On a live session, `session.exportStructured(options)` and
290
- `session.exportMarkdown(options)` return `{ ok: true, version, content }` from one
291
- read of the committed state: anchors resolve against that version, and nothing is
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 `&amp;`; 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
- ```ts
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
- `exportDocxStructuredWithPages` opens the bytes in a private session, registers
372
- exactly the fonts it is given, as base64 data, in a measurement font store of its
373
- own (a requirement `family|bold|italic` takes the chain named by its key, else by
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
- ```ts
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
- Controls come in document order (body, headers, footers, footnotes, endnotes,
442
- comments; a control before the controls inside it), each with the structured
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
- ```ts
495
- import { compareDocx } from '@betteroffice/docx';
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
- `compareDocx` compares the text of body paragraphs whose structure is unchanged. Both
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
- ```ts
622
- const hit = editorRef.current!.getPositionAtPoint(event.clientX, event.clientY);
623
- if (hit) {
624
- await editorRef.current!.applyEdits({
625
- expectVersion: hit.version,
626
- steps: [{ op: 'insertText', target: hit.target, at: 'start', text: '{{customer.name}}' }],
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
- It returns `null` while typed or composed input is pending and until the painted
632
- pages show the current version (retry after `flushPendingInput()` or on the next
633
- frame), and a batch applied after further typing refuses with `stale-version`, so a
634
- drop never lands at a shifted position. Plugins get the same query as
635
- `context.geometry.getPositionAtPoint`, with the layout's `layoutId`. The lower-level
636
- `editorRef.getEditorRef()?.displayPositionToYrsLoc(hit)` maps a hit to its live
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)