docxodus 9.9.0 → 10.0.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 (111) hide show
  1. package/README.md +178 -0
  2. package/dist/canonical.d.ts +7 -0
  3. package/dist/canonical.d.ts.map +1 -0
  4. package/dist/canonical.js +56 -0
  5. package/dist/canonical.js.map +1 -0
  6. package/dist/docxodus.worker.d.ts +2 -1
  7. package/dist/docxodus.worker.d.ts.map +1 -1
  8. package/dist/docxodus.worker.js +244 -3
  9. package/dist/docxodus.worker.js.map +1 -1
  10. package/dist/editor.bundle.js +1914 -419
  11. package/dist/editor.d.ts +22 -2
  12. package/dist/editor.d.ts.map +1 -1
  13. package/dist/editor.js +94 -9
  14. package/dist/editor.js.map +1 -1
  15. package/dist/embed.bundle.js +2803 -469
  16. package/dist/embed.iife.js +2801 -467
  17. package/dist/export-assets.json +283 -0
  18. package/dist/export-browser.bundle.js +7990 -0
  19. package/dist/export-browser.d.ts +266 -0
  20. package/dist/export-browser.d.ts.map +1 -0
  21. package/dist/export-browser.js +2915 -0
  22. package/dist/export-browser.js.map +1 -0
  23. package/dist/export-resource-limits-v1.json +57 -0
  24. package/dist/font-contract.d.ts +150 -0
  25. package/dist/font-contract.d.ts.map +1 -0
  26. package/dist/font-contract.js +59 -0
  27. package/dist/font-contract.js.map +1 -0
  28. package/dist/font-runtime.d.ts +33 -0
  29. package/dist/font-runtime.d.ts.map +1 -0
  30. package/dist/font-runtime.js +1100 -0
  31. package/dist/font-runtime.js.map +1 -0
  32. package/dist/index.d.ts +62 -5
  33. package/dist/index.d.ts.map +1 -1
  34. package/dist/index.js +130 -16
  35. package/dist/index.js.map +1 -1
  36. package/dist/page-geometry.d.ts +8 -1
  37. package/dist/page-geometry.d.ts.map +1 -1
  38. package/dist/page-geometry.js +37 -11
  39. package/dist/page-geometry.js.map +1 -1
  40. package/dist/pagination.bundle.js +1827 -410
  41. package/dist/pagination.d.ts +249 -15
  42. package/dist/pagination.d.ts.map +1 -1
  43. package/dist/pagination.js +1942 -453
  44. package/dist/pagination.js.map +1 -1
  45. package/dist/react.d.ts +11 -4
  46. package/dist/react.d.ts.map +1 -1
  47. package/dist/react.js +38 -5
  48. package/dist/react.js.map +1 -1
  49. package/dist/render-report-v2.schema.json +1581 -0
  50. package/dist/ribbon-chrome.d.ts +1 -1
  51. package/dist/ribbon-chrome.d.ts.map +1 -1
  52. package/dist/ribbon-chrome.js +45 -0
  53. package/dist/ribbon-chrome.js.map +1 -1
  54. package/dist/ribbon.js +70 -0
  55. package/dist/ribbon.js.map +1 -1
  56. package/dist/session.bundle.js +695 -29
  57. package/dist/session.d.ts +134 -19
  58. package/dist/session.d.ts.map +1 -1
  59. package/dist/session.js +562 -25
  60. package/dist/session.js.map +1 -1
  61. package/dist/types.d.ts +1207 -16
  62. package/dist/types.d.ts.map +1 -1
  63. package/dist/types.js.map +1 -1
  64. package/dist/wasm/_framework/DocumentFormat.OpenXml.Framework.wasm +0 -0
  65. package/dist/wasm/_framework/DocumentFormat.OpenXml.Framework.wasm.br +0 -0
  66. package/dist/wasm/_framework/DocumentFormat.OpenXml.wasm +0 -0
  67. package/dist/wasm/_framework/DocumentFormat.OpenXml.wasm.br +0 -0
  68. package/dist/wasm/_framework/Docxodus.wasm +0 -0
  69. package/dist/wasm/_framework/Docxodus.wasm.br +0 -0
  70. package/dist/wasm/_framework/DocxodusWasm.wasm +0 -0
  71. package/dist/wasm/_framework/DocxodusWasm.wasm.br +0 -0
  72. package/dist/wasm/_framework/System.Collections.Concurrent.wasm +0 -0
  73. package/dist/wasm/_framework/System.Collections.Concurrent.wasm.br +0 -0
  74. package/dist/wasm/_framework/System.Collections.Immutable.wasm +0 -0
  75. package/dist/wasm/_framework/System.Collections.Immutable.wasm.br +0 -0
  76. package/dist/wasm/_framework/System.Collections.wasm +0 -0
  77. package/dist/wasm/_framework/System.Collections.wasm.br +0 -0
  78. package/dist/wasm/_framework/System.IO.Compression.wasm +0 -0
  79. package/dist/wasm/_framework/System.IO.Compression.wasm.br +0 -0
  80. package/dist/wasm/_framework/System.IO.Packaging.wasm +0 -0
  81. package/dist/wasm/_framework/System.IO.Packaging.wasm.br +0 -0
  82. package/dist/wasm/_framework/System.Linq.wasm +0 -0
  83. package/dist/wasm/_framework/System.Linq.wasm.br +0 -0
  84. package/dist/wasm/_framework/System.Private.CoreLib.wasm +0 -0
  85. package/dist/wasm/_framework/System.Private.CoreLib.wasm.br +0 -0
  86. package/dist/wasm/_framework/System.Private.Uri.wasm +0 -0
  87. package/dist/wasm/_framework/System.Private.Uri.wasm.br +0 -0
  88. package/dist/wasm/_framework/System.Private.Xml.Linq.wasm +0 -0
  89. package/dist/wasm/_framework/System.Private.Xml.Linq.wasm.br +0 -0
  90. package/dist/wasm/_framework/System.Private.Xml.wasm +0 -0
  91. package/dist/wasm/_framework/System.Private.Xml.wasm.br +0 -0
  92. package/dist/wasm/_framework/System.Runtime.InteropServices.JavaScript.wasm +0 -0
  93. package/dist/wasm/_framework/System.Runtime.InteropServices.JavaScript.wasm.br +0 -0
  94. package/dist/wasm/_framework/System.Runtime.wasm +0 -0
  95. package/dist/wasm/_framework/System.Runtime.wasm.br +0 -0
  96. package/dist/wasm/_framework/System.Security.Cryptography.wasm +0 -0
  97. package/dist/wasm/_framework/System.Security.Cryptography.wasm.br +0 -0
  98. package/dist/wasm/_framework/System.Text.Json.wasm +0 -0
  99. package/dist/wasm/_framework/System.Text.Json.wasm.br +0 -0
  100. package/dist/wasm/_framework/System.Text.RegularExpressions.wasm +0 -0
  101. package/dist/wasm/_framework/System.Text.RegularExpressions.wasm.br +0 -0
  102. package/dist/wasm/_framework/dotnet.boot.js +21 -21
  103. package/dist/wasm/_framework/dotnet.boot.js.br +0 -0
  104. package/dist/wasm/_framework/dotnet.native.wasm +0 -0
  105. package/dist/wasm/_framework/dotnet.native.wasm.br +0 -0
  106. package/dist/worker-proxy.bundle.js +196 -29
  107. package/dist/worker-proxy.d.ts +36 -3
  108. package/dist/worker-proxy.d.ts.map +1 -1
  109. package/dist/worker-proxy.js +186 -33
  110. package/dist/worker-proxy.js.map +1 -1
  111. package/package.json +22 -6
package/dist/session.js CHANGED
@@ -1,6 +1,46 @@
1
1
  // Copyright (c) Microsoft. All rights reserved.
2
2
  // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
- import { ContextBoundary, DiffFormat, PlaceholderKinds, ProjectionDepth } from "./types.js";
3
+ import { ContextBoundary, DiffFormat, PlaceholderKinds, ProjectionDepth, ProjectionScopes } from "./types.js";
4
+ function mutationBatchChangeSet(before, after, key) {
5
+ const beforeGroups = new Map();
6
+ const afterGroups = new Map();
7
+ const group = (items, target) => {
8
+ items.forEach((item, index) => {
9
+ const identity = key(item);
10
+ const indices = target.get(identity) ?? [];
11
+ indices.push(index);
12
+ target.set(identity, indices);
13
+ });
14
+ };
15
+ group(before, beforeGroups);
16
+ group(after, afterGroups);
17
+ const beforeMatched = before.map(() => false);
18
+ const afterMatched = after.map(() => false);
19
+ const modified = after.map(() => false);
20
+ for (const [identity, afterIndices] of afterGroups) {
21
+ const beforeIndices = beforeGroups.get(identity) ?? [];
22
+ for (const afterIndex of afterIndices) {
23
+ const beforeIndex = beforeIndices.find(index => !beforeMatched[index] && JSON.stringify(before[index]) === JSON.stringify(after[afterIndex]));
24
+ if (beforeIndex === undefined)
25
+ continue;
26
+ beforeMatched[beforeIndex] = true;
27
+ afterMatched[afterIndex] = true;
28
+ }
29
+ const remainingBefore = beforeIndices.filter(index => !beforeMatched[index]);
30
+ const remainingAfter = afterIndices.filter(index => !afterMatched[index]);
31
+ const modifiedCount = Math.min(remainingBefore.length, remainingAfter.length);
32
+ for (let index = 0; index < modifiedCount; index++) {
33
+ beforeMatched[remainingBefore[index]] = true;
34
+ afterMatched[remainingAfter[index]] = true;
35
+ modified[remainingAfter[index]] = true;
36
+ }
37
+ }
38
+ return {
39
+ added: after.filter((_, index) => !afterMatched[index]),
40
+ removed: before.filter((_, index) => !beforeMatched[index]),
41
+ modified: after.filter((_, index) => modified[index]),
42
+ };
43
+ }
4
44
  /**
5
45
  * Stateful in-memory DOCX editing session keyed by markdown-projection anchor ids.
6
46
  * Mirror of the .NET `DocxSession` surface. See
@@ -26,6 +66,312 @@ export class DocxSession {
26
66
  project() {
27
67
  return JSON.parse(this.wasm.Project(this.handle));
28
68
  }
69
+ /** Monotonic document version (0 at open; +1 per committed mutation/undo/redo). */
70
+ getVersion() {
71
+ return JSON.parse(this.wasm.GetVersion(this.handle)).version;
72
+ }
73
+ /**
74
+ * Verification manifest for the current logical checkpoint. Unsaved edits are included and
75
+ * the read does not mutate package bytes, caches, history, or the document version.
76
+ */
77
+ getPackageManifest() {
78
+ return JSON.parse(this.wasm.GetPackageManifest(this.handle));
79
+ }
80
+ /** Register a browser-materialized PageMap without changing the document version. */
81
+ registerPageMap(pageMap, expectedRendererFingerprint) {
82
+ return JSON.parse(this.wasm.RegisterPageMap(this.handle, JSON.stringify(pageMap), expectedRendererFingerprint ?? ""));
83
+ }
84
+ getPageMapStatus(request) {
85
+ return JSON.parse(this.wasm.GetPageMapStatus(this.handle, request ? JSON.stringify(request) : ""));
86
+ }
87
+ getPageCitation(anchorId, request) {
88
+ return JSON.parse(this.wasm.GetPageCitation(this.handle, anchorId, JSON.stringify(request)));
89
+ }
90
+ /** Evaluate optimistic guards without mutating or advancing the version. */
91
+ checkPreconditions(preconditions) {
92
+ return JSON.parse(this.wasm.CheckPreconditions(this.handle, JSON.stringify(preconditions)));
93
+ }
94
+ /**
95
+ * Guard any synchronous mutation. WASM calls are synchronous and single-threaded, so the
96
+ * check and callback form one uninterrupted client-side operation. Prefer a method's native
97
+ * `preconditions` option where it has one (notably replaceTextRange's match-count guard).
98
+ */
99
+ runWithPreconditions(preconditions, mutation) {
100
+ const checked = this.checkPreconditions(preconditions);
101
+ return checked.success ? mutation() : checked;
102
+ }
103
+ /**
104
+ * Execute synchronous mutations atomically by default. Atomic success is one undo/version
105
+ * unit; any failed or thrown step restores the exact package and history checkpoint.
106
+ */
107
+ executeBatch(steps, mode = "atomic") {
108
+ if (mode !== "atomic" && mode !== "best_effort") {
109
+ throw new RangeError(`unknown mutation batch mode: ${String(mode)}`);
110
+ }
111
+ const baseVersion = this.getVersion();
112
+ const observationWarnings = [];
113
+ const inspect = (label, read, fallback) => {
114
+ try {
115
+ return read();
116
+ }
117
+ catch (error) {
118
+ observationWarnings.push(`${label} unavailable: ${error instanceof Error ? error.message : String(error)}`);
119
+ return fallback;
120
+ }
121
+ };
122
+ const beforeRevisions = inspect("Revision delta inspection", () => this.listRevisions(), []);
123
+ const beforeComments = inspect("Comment delta inspection", () => this.listComments(), []);
124
+ const beforeAnnotations = inspect("Annotation delta inspection", () => this.listAnnotations(), []);
125
+ const complete = (result) => {
126
+ try {
127
+ const revisionChanges = mutationBatchChangeSet(beforeRevisions, inspect("Revision delta inspection", () => this.listRevisions(), beforeRevisions), revision => revision.id);
128
+ const commentChanges = mutationBatchChangeSet(beforeComments, inspect("Comment delta inspection", () => this.listComments(), beforeComments), comment => comment.anchorId);
129
+ const annotationChanges = mutationBatchChangeSet(beforeAnnotations, inspect("Annotation delta inspection", () => this.listAnnotations(), beforeAnnotations), annotation => annotation.id ?? "");
130
+ const resultVersion = inspect("Result version inspection", () => this.getVersion(), baseVersion);
131
+ const warnings = [...observationWarnings];
132
+ if ([...revisionChanges.added, ...revisionChanges.modified]
133
+ .some(revision => revision.date !== undefined && revision.date !== null)) {
134
+ warnings.push("Tracked-revision date attributes may use the execution clock; compare revision ids, authors, types, text, and anchors across separate executions.");
135
+ }
136
+ if ([...commentChanges.added, ...commentChanges.modified]
137
+ .some(comment => comment.date !== undefined && comment.date !== null)) {
138
+ warnings.push("Comment date attributes may be generated from the execution clock; supply dates explicitly when byte-identical replay is required.");
139
+ }
140
+ // Same predicate as the .NET receipt (`annotation.Created.HasValue`): the warning is
141
+ // about an execution CLOCK, so an annotation added with no created timestamp is
142
+ // deterministic and must not raise it on one surface and not the other.
143
+ if (annotationChanges.added
144
+ .some(annotation => annotation.created !== undefined && annotation.created !== null)) {
145
+ warnings.push("Auto-generated annotation ids or creation timestamps are execution metadata; supply id and created explicitly when byte-identical replay is required.");
146
+ }
147
+ if (result.steps.some(step => step.results.some(edit => edit.created.length > 0))) {
148
+ warnings.push("Created anchors and related OOXML ids may be generated independently on replay; preview/apply equivalence is semantic and packageHash or anchor ids may differ.");
149
+ }
150
+ if (mode === "best_effort" && !result.success) {
151
+ warnings.push("Best-effort execution retains every successful step despite later failures.");
152
+ }
153
+ // null, never "": an absent hash must not compare equal to another absent hash, or a
154
+ // naive `preview.packageHash === applied.packageHash` replay assertion passes vacuously.
155
+ let packageHash = null;
156
+ if (!this.wasm.GetPackageContentHash) {
157
+ warnings.push("This WASM bundle predates package equivalence hashes; packageHash is unavailable.");
158
+ }
159
+ else {
160
+ try {
161
+ packageHash = this.wasm.GetPackageContentHash(this.handle);
162
+ }
163
+ catch (error) {
164
+ warnings.push(`Package equivalence hash unavailable: ${error instanceof Error ? error.message : String(error)}`);
165
+ }
166
+ }
167
+ return {
168
+ ...result,
169
+ preview: false,
170
+ baseVersion,
171
+ resultVersion,
172
+ packageHash,
173
+ revisionChanges,
174
+ commentChanges,
175
+ annotationChanges,
176
+ warnings,
177
+ html: null,
178
+ };
179
+ }
180
+ catch (error) {
181
+ return {
182
+ ...result,
183
+ preview: false,
184
+ baseVersion,
185
+ resultVersion: inspect("Result version inspection", () => this.getVersion(), baseVersion),
186
+ packageHash: null,
187
+ revisionChanges: { added: [], removed: [], modified: [] },
188
+ commentChanges: { added: [], removed: [], modified: [] },
189
+ annotationChanges: { added: [], removed: [], modified: [] },
190
+ warnings: [
191
+ ...observationWarnings,
192
+ `Batch receipt enrichment unavailable: ${error instanceof Error ? error.message : String(error)}`,
193
+ ],
194
+ html: null,
195
+ };
196
+ }
197
+ };
198
+ const internalFailure = (value) => ({
199
+ success: false,
200
+ error: { code: "internal_error", message: value instanceof Error ? value.message : String(value) },
201
+ created: [], removed: [], modified: [],
202
+ });
203
+ const run = (step) => {
204
+ try {
205
+ const value = step.mutation();
206
+ const results = Array.isArray(value) ? value : [value];
207
+ if (results.some(result => result === null || typeof result !== "object" || typeof result.success !== "boolean")) {
208
+ return [internalFailure("batch mutation returned invalid edit results")];
209
+ }
210
+ // An empty list is a successful no-op step, matching the core contract.
211
+ return results;
212
+ }
213
+ catch (error) {
214
+ return [internalFailure(error)];
215
+ }
216
+ };
217
+ const failureOf = (step, rolledBack) => ({
218
+ index: step.index,
219
+ tool: step.tool,
220
+ action: step.action,
221
+ error: step.results.find(result => !result.success)?.error
222
+ ?? { code: "internal_error", message: "batch step failed without an error" },
223
+ rolledBack,
224
+ });
225
+ const preflightOne = (step) => {
226
+ try {
227
+ return step.preflight?.();
228
+ }
229
+ catch (error) {
230
+ return internalFailure(error).error;
231
+ }
232
+ };
233
+ if (mode === "atomic") {
234
+ const preflight = steps.map(preflightOne);
235
+ const failedPreflight = preflight.findIndex(error => error !== undefined);
236
+ if (failedPreflight >= 0) {
237
+ const source = steps[failedPreflight];
238
+ const failed = {
239
+ index: failedPreflight, tool: source.tool, action: source.action,
240
+ success: false, rolledBack: true,
241
+ results: [{ success: false, error: preflight[failedPreflight], created: [], removed: [], modified: [] }],
242
+ };
243
+ return complete({ mode, status: "failed", success: false, rolledBack: true,
244
+ steps: [failed], failure: failureOf(failed, true) });
245
+ }
246
+ const transaction = this.wasm.BeginTransaction(this.handle);
247
+ const completed = [];
248
+ try {
249
+ for (let index = 0; index < steps.length; index++) {
250
+ const source = steps[index];
251
+ const results = run(source);
252
+ const step = {
253
+ index, tool: source.tool, action: source.action,
254
+ success: results.every(result => result.success), rolledBack: false, results,
255
+ };
256
+ completed.push(step);
257
+ if (!step.success) {
258
+ this.wasm.RollbackTransaction(transaction);
259
+ const rolledBack = completed.map(value => ({ ...value, rolledBack: true }));
260
+ const failed = rolledBack[rolledBack.length - 1];
261
+ return complete({ mode, status: "failed", success: false, rolledBack: true,
262
+ steps: rolledBack, failure: failureOf(failed, true) });
263
+ }
264
+ }
265
+ this.wasm.CommitTransaction(transaction);
266
+ return complete({ mode, status: "ok", success: true, rolledBack: false, steps: completed });
267
+ }
268
+ catch (error) {
269
+ try {
270
+ this.wasm.RollbackTransaction(transaction);
271
+ }
272
+ catch { /* preserve the original */ }
273
+ throw error;
274
+ }
275
+ }
276
+ // Preserve sequential best-effort semantics: a later preflight can observe state created by
277
+ // an earlier successful step, so run it immediately before that step rather than up front.
278
+ const completed = steps.map((source, index) => {
279
+ const preflight = preflightOne(source);
280
+ const results = preflight
281
+ ? [{ success: false, error: preflight, created: [], removed: [], modified: [] }]
282
+ : run(source);
283
+ return {
284
+ index, tool: source.tool, action: source.action,
285
+ success: results.every(result => result.success), rolledBack: false, results,
286
+ };
287
+ });
288
+ const failed = completed.find(step => !step.success);
289
+ return complete({
290
+ mode,
291
+ status: failed ? (completed.some(step => step.success) ? "partial" : "failed") : "ok",
292
+ success: failed === undefined,
293
+ rolledBack: false,
294
+ steps: completed,
295
+ failure: failed ? failureOf(failed, false) : undefined,
296
+ });
297
+ }
298
+ /**
299
+ * Execute the same callback batch algorithm against a complete isolated package clone.
300
+ * Callbacks receive the shadow session explicitly; mutate that argument. The live session's
301
+ * package, caches, version, configuration, and undo/redo history are never execution targets.
302
+ */
303
+ previewBatch(steps, mode = "atomic", options) {
304
+ if (mode !== "atomic" && mode !== "best_effort") {
305
+ throw new RangeError(`unknown mutation batch mode: ${String(mode)}`);
306
+ }
307
+ const htmlMode = options?.html ?? "none";
308
+ if (htmlMode !== "none" && htmlMode !== "scoped" && htmlMode !== "full") {
309
+ throw new RangeError(`unknown preview HTML mode: ${String(htmlMode)}`);
310
+ }
311
+ if (!this.wasm.OpenPreviewSession) {
312
+ throw new Error("This WASM bundle does not support isolated mutation previews.");
313
+ }
314
+ const shadow = new DocxSession(this.wasm.OpenPreviewSession(this.handle), this.wasm);
315
+ try {
316
+ const result = shadow.executeBatch(steps.map(step => ({
317
+ tool: step.tool,
318
+ action: step.action,
319
+ mutation: () => step.mutation(shadow),
320
+ preflight: step.preflight ? () => step.preflight(shadow) : undefined,
321
+ })), mode);
322
+ const warnings = [...result.warnings];
323
+ let html = null;
324
+ // A rendered document always starts with '<'; a leading '{' is the bridge's error object.
325
+ const unwrapRendered = (rendered) => {
326
+ if (rendered.trimStart().startsWith("{")) {
327
+ const envelope = JSON.parse(rendered);
328
+ if (envelope.error) {
329
+ warnings.push(`Preview HTML could not be generated: ${envelope.error}`);
330
+ return null;
331
+ }
332
+ }
333
+ return rendered;
334
+ };
335
+ // Preview HTML MUST come from the façade's preview profile (DocxSessionOps.RenderPreview*),
336
+ // not the editor's authoring profile: the editor render hides comments, annotations and
337
+ // headers/footers, so routing a preview through it would show this surface a materially
338
+ // different document than the stdio/Python/MCP surfaces show for the identical batch.
339
+ const legacyProfileWarning = "This WASM bundle predates the shared preview HTML profile; preview HTML omits comments, " +
340
+ "annotations and headers/footers and may differ from other surfaces.";
341
+ try {
342
+ if (htmlMode === "scoped") {
343
+ if (!options?.htmlAnchorId) {
344
+ warnings.push("Scoped HTML was requested without htmlAnchorId; no HTML was generated.");
345
+ }
346
+ else if (this.wasm.RenderPreviewBlockHtml) {
347
+ html = unwrapRendered(this.wasm.RenderPreviewBlockHtml(shadow.handle, options.htmlAnchorId));
348
+ }
349
+ else {
350
+ warnings.push(legacyProfileWarning);
351
+ html = shadow.renderBlock(options.htmlAnchorId);
352
+ }
353
+ }
354
+ else if (htmlMode === "full") {
355
+ if (this.wasm.RenderPreviewHtml) {
356
+ html = unwrapRendered(this.wasm.RenderPreviewHtml(shadow.handle));
357
+ }
358
+ else {
359
+ warnings.push(legacyProfileWarning);
360
+ html = unwrapRendered(this.wasm.RenderHtmlForReview
361
+ ? this.wasm.RenderHtmlForReview(shadow.handle, "docx-", false, false, 1, true)
362
+ : this.wasm.RenderHtml(shadow.handle, "docx-", false, false, 1));
363
+ }
364
+ }
365
+ }
366
+ catch (error) {
367
+ warnings.push(`Preview HTML could not be generated: ${error instanceof Error ? error.message : String(error)}`);
368
+ }
369
+ return { ...result, preview: true, warnings, html };
370
+ }
371
+ finally {
372
+ shadow.close();
373
+ }
374
+ }
29
375
  /**
30
376
  * Project a slice of the document keyed off an anchor — useful for showing
31
377
  * one section to an LLM at a time without paying the cost of projecting the
@@ -41,8 +387,10 @@ export class DocxSession {
41
387
  *
42
388
  * @see docs/architecture/docx_mutation_api.md
43
389
  */
44
- projectAnchor(anchorId, depth = ProjectionDepth.SubtreeAndFollowingSiblings) {
45
- return JSON.parse(this.wasm.ProjectAnchor(this.handle, anchorId, depth));
390
+ projectAnchor(anchorId, depth = ProjectionDepth.SubtreeAndFollowingSiblings, citation) {
391
+ return JSON.parse(citation
392
+ ? this.wasm.ProjectAnchorWithCitations(this.handle, anchorId, depth, JSON.stringify(citation))
393
+ : this.wasm.ProjectAnchor(this.handle, anchorId, depth));
46
394
  }
47
395
  /**
48
396
  * Render a single block to faithful HTML from the live session — the editor's
@@ -61,11 +409,17 @@ export class DocxSession {
61
409
  return html;
62
410
  }
63
411
  // ─── Tier A: text CRUD ───────────────────────────────────────────────
64
- replaceText(anchorId, markdown) {
65
- return JSON.parse(this.wasm.ReplaceText(this.handle, anchorId, markdown));
66
- }
67
- deleteBlock(anchorId) {
68
- return JSON.parse(this.wasm.DeleteBlock(this.handle, anchorId));
412
+ replaceText(anchorId, markdown, preconditions) {
413
+ const apply = () => JSON.parse(this.wasm.ReplaceText(this.handle, anchorId, markdown));
414
+ return preconditions
415
+ ? this.runWithPreconditions({ ...preconditions, anchorId: preconditions.anchorId ?? anchorId }, apply)
416
+ : apply();
417
+ }
418
+ deleteBlock(anchorId, preconditions) {
419
+ const apply = () => JSON.parse(this.wasm.DeleteBlock(this.handle, anchorId));
420
+ return preconditions
421
+ ? this.runWithPreconditions({ ...preconditions, anchorId: preconditions.anchorId ?? anchorId }, apply)
422
+ : apply();
69
423
  }
70
424
  /** Reorder one top-level paragraph/heading/list/table block relative to another. */
71
425
  moveBlock(sourceAnchorId, targetAnchorId, position) {
@@ -116,17 +470,29 @@ export class DocxSession {
116
470
  }
117
471
  /**
118
472
  * Insert a `rows`×`cols` table before/after the block. `options` controls borders, row-major
119
- * cell markdown, and cell alignment. The returned `EditResult.created` lists the cell-paragraph
473
+ * cell markdown, and cell alignment. The returned `EditResult.created` lists canonical `tc`
120
474
  * anchors (row-major), so each cell can then be addressed to fill/format.
121
475
  */
122
476
  insertTable(anchorId, position, rows, cols, options) {
123
477
  const optionsJson = options ? JSON.stringify(options) : "";
124
478
  return JSON.parse(this.wasm.InsertTable(this.handle, anchorId, position, rows, cols, optionsJson));
125
479
  }
480
+ /** Resolve a canonical `tbl` anchor to explicit table/row/column/cell identities. */
481
+ getTableMetadata(tableAnchorId) {
482
+ return JSON.parse(this.wasm.GetTableMetadata(this.handle, tableAnchorId));
483
+ }
484
+ /** Resolve a canonical `tc` anchor to its zero-based table-grid coordinate and spans. */
485
+ resolveTableCellAnchor(cellAnchorId) {
486
+ return JSON.parse(this.wasm.ResolveTableCellAnchor(this.handle, cellAnchorId));
487
+ }
488
+ /** Resolve a zero-based table-grid coordinate to the physical `tc` covering it. */
489
+ resolveTableCellCoordinate(tableAnchorId, rowIndex, columnIndex) {
490
+ return JSON.parse(this.wasm.ResolveTableCellCoordinate(this.handle, tableAnchorId, rowIndex, columnIndex));
491
+ }
126
492
  /**
127
- * Table row/column editing, addressed by a cell-paragraph anchor (e.g. one returned from
128
- * {@link insertTable}'s `created`). Insert clones the reference row/column's widths and starts
129
- * empty (`created` lists the new cell-paragraph anchors); delete of the last row/column removes
493
+ * Table row/column editing, addressed by the canonical `tc` anchor returned from
494
+ * {@link insertTable}'s `created` or table metadata. Insert clones the reference row/column's
495
+ * widths and starts empty (`created` lists new `tc` anchors); delete of the last row/column removes
130
496
  * the whole table. All four are grid-aware: inserting across a merge extends it, deleting
131
497
  * through one narrows it, and deleting a vertical merge's lead row promotes the next row to
132
498
  * carry it — the grid is never left ragged.
@@ -164,7 +530,7 @@ export class DocxSession {
164
530
  return JSON.parse(this.wasm.UnmergeCells(this.handle, cellAnchorId));
165
531
  }
166
532
  /**
167
- * Table styling, addressed by a cell-paragraph anchor — the post-insert counterpart of
533
+ * Table styling, addressed by a canonical `tc` anchor — the post-insert counterpart of
168
534
  * {@link insertTable}'s options (issue #315 Stage A). `setColumnWidths` retunes `w:tblGrid` +
169
535
  * every row's cell width (one positive twip value per column) and pins the table to fixed
170
536
  * layout, exactly as inserting with explicit `columnWidths` would.
@@ -196,6 +562,10 @@ export class DocxSession {
196
562
  setRepeatHeaderRow(cellAnchorId, repeat) {
197
563
  return JSON.parse(this.wasm.SetRepeatHeaderRow(this.handle, cellAnchorId, repeat));
198
564
  }
565
+ /** Apply row layout options to the row containing the canonical cell anchor. */
566
+ setTableRowOptions(cellAnchorId, options) {
567
+ return JSON.parse(this.wasm.SetTableRowOptions(this.handle, cellAnchorId, options.repeatHeader ?? null, options.allowBreakAcrossPages ?? null, options.heightTwips ?? null, options.heightRule ?? "atLeast"));
568
+ }
199
569
  // ─── Headers / footers / page numbers ────────────────────────────────
200
570
  /**
201
571
  * Set the running header story for the section that owns `anchorId` (any body block in that
@@ -284,6 +654,17 @@ export class DocxSession {
284
654
  insertFootnote(anchorId, characterOffset, markdown) {
285
655
  return JSON.parse(this.wasm.InsertFootnote(this.handle, anchorId, characterOffset, markdown));
286
656
  }
657
+ /**
658
+ * Insert a Word-faithful internal cross-reference — a `REF` field targeting an existing
659
+ * bookmark — at a character offset (issue #545). The field carries a cached result run
660
+ * (the bookmarked text, or the target's auto-number under `referenceNumber`), so
661
+ * renderers that do not recompute fields show a faithful snapshot and Word updates it
662
+ * like a hand-authored cross-reference. A missing or incoherent bookmark fails with
663
+ * `missing_bookmark_target`.
664
+ */
665
+ insertCrossReference(anchorId, characterOffset, bookmarkName, options) {
666
+ return JSON.parse(this.wasm.InsertCrossReference(this.handle, anchorId, characterOffset, bookmarkName, options ? JSON.stringify(options) : ""));
667
+ }
287
668
  /** Create an endnote — see {@link insertFootnote}; writes the endnotes part and a
288
669
  * `w:endnoteReference`, and the created definition anchor has kind `en`. */
289
670
  insertEndnote(anchorId, characterOffset, markdown) {
@@ -343,6 +724,87 @@ export class DocxSession {
343
724
  listComments() {
344
725
  return JSON.parse(this.wasm.ListComments(this.handle));
345
726
  }
727
+ listHyperlinks(scopes = ProjectionScopes.All) {
728
+ return JSON.parse(this.wasm.ListHyperlinks(this.handle, scopes));
729
+ }
730
+ addHyperlink(anchorId, span, kind, target) {
731
+ return JSON.parse(this.wasm.AddHyperlink(this.handle, anchorId, span.start, span.length, kind, target));
732
+ }
733
+ updateHyperlink(hyperlinkId, kind, target) {
734
+ return JSON.parse(this.wasm.UpdateHyperlink(this.handle, hyperlinkId, kind, target));
735
+ }
736
+ removeHyperlink(hyperlinkId) {
737
+ return JSON.parse(this.wasm.RemoveHyperlink(this.handle, hyperlinkId));
738
+ }
739
+ /** Versioned operational facts for native image inspection/mutation in this runtime. */
740
+ getImageCapabilities() {
741
+ return JSON.parse(this.wasm.GetImageCapabilities());
742
+ }
743
+ listImages(scopes = ProjectionScopes.All) {
744
+ return JSON.parse(this.wasm.ListImages(this.handle, scopes));
745
+ }
746
+ insertImage(anchorId, characterOffset, bytes, options = {}) {
747
+ return JSON.parse(this.wasm.InsertImage(this.handle, anchorId, characterOffset, imageBytesToBase64(bytes), JSON.stringify(options)));
748
+ }
749
+ replaceImage(imageId, bytes) {
750
+ return JSON.parse(this.wasm.ReplaceImage(this.handle, imageId, imageBytesToBase64(bytes)));
751
+ }
752
+ setImageDimensions(imageId, dimensions) {
753
+ return JSON.parse(this.wasm.SetImageDimensions(this.handle, imageId, JSON.stringify(dimensions)));
754
+ }
755
+ setImageMetadata(imageId, altText, title) {
756
+ return JSON.parse(this.wasm.SetImageMetadata(this.handle, imageId, altText, title));
757
+ }
758
+ setImageFloatingLayout(imageId, layout) {
759
+ return JSON.parse(this.wasm.SetImageFloatingLayout(this.handle, imageId, JSON.stringify(layout)));
760
+ }
761
+ removeImage(imageId) {
762
+ return JSON.parse(this.wasm.RemoveImage(this.handle, imageId));
763
+ }
764
+ /** Native Word structured-document tags, in outer-before-inner story order. */
765
+ listContentControls(scopes = ProjectionScopes.All) {
766
+ return JSON.parse(this.wasm.ListContentControls(this.handle, scopes));
767
+ }
768
+ fillContentControlText(anchorId, text, options = {}) {
769
+ return JSON.parse(this.wasm.FillContentControlText(this.handle, anchorId, text, JSON.stringify(options)));
770
+ }
771
+ fillContentControlRichText(anchorId, markdown, options = {}) {
772
+ return JSON.parse(this.wasm.FillContentControlRichText(this.handle, anchorId, markdown, JSON.stringify(options)));
773
+ }
774
+ setContentControlChecked(anchorId, isChecked, options = {}) {
775
+ return JSON.parse(this.wasm.SetContentControlChecked(this.handle, anchorId, isChecked, JSON.stringify(options)));
776
+ }
777
+ setContentControlDate(anchorId, value, displayText, options = {}) {
778
+ const timestamp = value instanceof Date ? value.toISOString() : value;
779
+ return JSON.parse(this.wasm.SetContentControlDate(this.handle, anchorId, timestamp, displayText ?? null, JSON.stringify(options)));
780
+ }
781
+ selectContentControlItem(anchorId, value, options = {}) {
782
+ return JSON.parse(this.wasm.SelectContentControlItem(this.handle, anchorId, value, JSON.stringify(options)));
783
+ }
784
+ fillContentControlPicture(anchorId, bytes, options = {}) {
785
+ return JSON.parse(this.wasm.FillContentControlPicture(this.handle, anchorId, imageBytesToBase64(bytes), JSON.stringify(options)));
786
+ }
787
+ addRepeatingSectionItem(sectionAnchorId, afterItemAnchorId, options = {}) {
788
+ return JSON.parse(this.wasm.AddRepeatingSectionItem(this.handle, sectionAnchorId, afterItemAnchorId ?? "", JSON.stringify(options)));
789
+ }
790
+ removeRepeatingSectionItem(itemAnchorId) {
791
+ return JSON.parse(this.wasm.RemoveRepeatingSectionItem(this.handle, itemAnchorId));
792
+ }
793
+ listBookmarks(scopes = ProjectionScopes.All) {
794
+ return JSON.parse(this.wasm.ListBookmarks(this.handle, scopes));
795
+ }
796
+ addBookmark(name, range) {
797
+ return JSON.parse(this.wasm.AddBookmark(this.handle, name, range.startAnchorId, range.startOffset, range.endAnchorId, range.endOffset));
798
+ }
799
+ renameBookmark(name, newName) {
800
+ return JSON.parse(this.wasm.RenameBookmark(this.handle, name, newName));
801
+ }
802
+ moveBookmark(name, range) {
803
+ return JSON.parse(this.wasm.MoveBookmark(this.handle, name, range.startAnchorId, range.startOffset, range.endAnchorId, range.endOffset));
804
+ }
805
+ removeBookmark(name) {
806
+ return JSON.parse(this.wasm.RemoveBookmark(this.handle, name));
807
+ }
346
808
  // ─── Tracked revisions (issue #318) ──────────────────────────────────
347
809
  /** Markup-native tracked-revision listing, in document order across body, headers,
348
810
  * footers, footnotes, and endnotes. Ids are stable while the underlying markup
@@ -363,6 +825,22 @@ export class DocxSession {
363
825
  rejectRevision(revisionId) {
364
826
  return JSON.parse(this.wasm.RejectRevision(this.handle, revisionId));
365
827
  }
828
+ /**
829
+ * Accept every live revision as one undoable session mutation.
830
+ *
831
+ * Fails closed: an unsupported, malformed, or ambiguous registry entry aborts the whole
832
+ * operation (`revisionUnsupported`/`revisionMalformed`/`revisionAmbiguous`) and nothing is
833
+ * mutated. There is no force mode — call {@link listRevisions} and read each entry's
834
+ * `diagnostic` to see what blocks it.
835
+ */
836
+ acceptAllRevisions() {
837
+ return JSON.parse(this.wasm.AcceptAllRevisions(this.handle));
838
+ }
839
+ /** Reject every live revision as one undoable session mutation. Fails closed exactly like
840
+ * {@link acceptAllRevisions}. */
841
+ rejectAllRevisions() {
842
+ return JSON.parse(this.wasm.RejectAllRevisions(this.handle));
843
+ }
366
844
  // ─── Tier C: formatting ──────────────────────────────────────────────
367
845
  applyFormat(anchorId, span, op) {
368
846
  const spanJson = span ? JSON.stringify(span) : "";
@@ -462,6 +940,10 @@ export class DocxSession {
462
940
  * replaces it with `replace`, preserving the surrounding run formatting that
463
941
  * the match didn't touch. Returns one `EditResult` per attempted match.
464
942
  *
943
+ * A `find` that matches nothing fails with a single `text_not_found` result
944
+ * naming the anchor and the needle — never an empty array. Pass
945
+ * `expectedMatchCount: 0` to assert absence as a successful no-op instead.
946
+ *
465
947
  * Run-formatting contract: the replacement text inherits the formatting of
466
948
  * the FIRST run the match spanned. Middle/trailing runs keep their `w:rPr`
467
949
  * but lose the slice of text the match consumed.
@@ -635,8 +1117,11 @@ export class DocxSession {
635
1117
  *
636
1118
  * @see docs/architecture/docx_mutation_api.md#findplaceholders
637
1119
  */
638
- findPlaceholders(kinds = PlaceholderKinds.All, scope = 1, contextChars = 80, boundary = ContextBoundary.Char) {
639
- return JSON.parse(this.wasm.FindPlaceholders(this.handle, kinds, scope, contextChars, boundary));
1120
+ findPlaceholders(kinds = PlaceholderKinds.All, scope = 1, contextChars = 80, boundary = ContextBoundary.Char, citation) {
1121
+ const json = citation
1122
+ ? this.wasm.FindPlaceholdersWithCitations(this.handle, kinds, scope, contextChars, boundary, JSON.stringify(citation))
1123
+ : this.wasm.FindPlaceholders(this.handle, kinds, scope, contextChars, boundary);
1124
+ return JSON.parse(json);
640
1125
  }
641
1126
  /**
642
1127
  * Returns a snapshot of edit-state introspection signals — placeholder counts,
@@ -659,6 +1144,21 @@ export class DocxSession {
659
1144
  }
660
1145
  return raw;
661
1146
  }
1147
+ /**
1148
+ * Return stable semantic changes from the package opened for this session to
1149
+ * its current state. Requires `captureInitialProjection` (enabled by default).
1150
+ */
1151
+ getSemanticChanges() {
1152
+ return JSON.parse(this.wasm.GetSemanticChanges(this.handle));
1153
+ }
1154
+ /**
1155
+ * Run the default deliverable gate on this session's normal clean-save checkpoint.
1156
+ * With initial projection capture enabled (the default), exact opening bytes are the
1157
+ * baseline used for dispositions and semantic/package deltas.
1158
+ */
1159
+ verifyDeliverable() {
1160
+ return JSON.parse(this.wasm.VerifyDeliverable(this.handle));
1161
+ }
662
1162
  // ─── Annotation-based anchor discovery (#132) ────────────────────────
663
1163
  /**
664
1164
  * Resolves an annotation's range to the block-level markdown anchors covering
@@ -674,8 +1174,11 @@ export class DocxSession {
674
1174
  *
675
1175
  * @see docs/architecture/docx_mutation_api.md#findbyannotation
676
1176
  */
677
- findByAnnotation(annotationId) {
678
- return JSON.parse(this.wasm.FindByAnnotation(this.handle, annotationId));
1177
+ findByAnnotation(annotationId, citation) {
1178
+ const json = citation
1179
+ ? this.wasm.FindByAnnotationWithCitations(this.handle, annotationId, JSON.stringify(citation))
1180
+ : this.wasm.FindByAnnotation(this.handle, annotationId);
1181
+ return JSON.parse(json);
679
1182
  }
680
1183
  /**
681
1184
  * Finds every annotation whose `labelId` matches and resolves each of their
@@ -684,8 +1187,11 @@ export class DocxSession {
684
1187
  * annotations on different paragraphs become three entries). Annotations
685
1188
  * whose bookmark resolves to no anchors are omitted from the result.
686
1189
  */
687
- findByLabel(labelId) {
688
- return JSON.parse(this.wasm.FindByLabel(this.handle, labelId));
1190
+ findByLabel(labelId, citation) {
1191
+ const json = citation
1192
+ ? this.wasm.FindByLabelWithCitations(this.handle, labelId, JSON.stringify(citation))
1193
+ : this.wasm.FindByLabel(this.handle, labelId);
1194
+ return JSON.parse(json);
689
1195
  }
690
1196
  /**
691
1197
  * Resolves any bookmark in the main document part (Docxodus-managed or
@@ -693,8 +1199,11 @@ export class DocxSession {
693
1199
  * order. Empty when the bookmark name is unknown. Use this for raw bookmark
694
1200
  * names that didn't come from the annotation system.
695
1201
  */
696
- findByBookmark(bookmarkName) {
697
- return JSON.parse(this.wasm.FindByBookmark(this.handle, bookmarkName));
1202
+ findByBookmark(bookmarkName, citation) {
1203
+ const json = citation
1204
+ ? this.wasm.FindByBookmarkWithCitations(this.handle, bookmarkName, JSON.stringify(citation))
1205
+ : this.wasm.FindByBookmark(this.handle, bookmarkName);
1206
+ return JSON.parse(json);
698
1207
  }
699
1208
  // ─── Text/kind-based anchor discovery (#171) ─────────────────────────
700
1209
  /**
@@ -733,13 +1242,22 @@ export class DocxSession {
733
1242
  return JSON.parse(this.wasm.FindByRegex(this.handle, pattern, regexOptions, options ? JSON.stringify(options) : ""));
734
1243
  }
735
1244
  /**
736
- * Return every anchor of the given `kind` (`"p"`, `"h"`, `"li"`, `"tbl"`,
737
- * `"row"`, `"cell"`, …), in document order. Reads the projection's anchor
1245
+ * Return every anchor of the given `kind` — one of `"p"`, `"h"`, `"li"`,
1246
+ * `"tbl"`, `"tr"`, `"tc"`, `"col"`, `"sdt"`, `"sec"`, `"fn"`, `"en"`,
1247
+ * `"cmt"`, `"unk"` — in document order. Matching is an exact string comparison;
1248
+ * in particular the row and cell tokens are `"tr"` and `"tc"`, never
1249
+ * `"row"`/`"cell"`. Unknown strings are not parsed or rejected: they simply
1250
+ * return an empty array. `"img"` and `"drw"` likewise return empty because the
1251
+ * projection never assigns those reserved kinds; address images through the
1252
+ * image surface instead. Reads the projection's anchor
738
1253
  * index directly — no text scan. Pass `scope` (e.g. `"body"`) to restrict to
739
1254
  * a single part; omit it to span all scopes.
740
1255
  */
741
- findByKind(kind, scope) {
742
- return JSON.parse(this.wasm.FindByKind(this.handle, kind, scope ?? ""));
1256
+ findByKind(kind, scope, citation) {
1257
+ const json = citation
1258
+ ? this.wasm.FindByKindWithCitations(this.handle, kind, scope ?? "", JSON.stringify(citation))
1259
+ : this.wasm.FindByKind(this.handle, kind, scope ?? "");
1260
+ return JSON.parse(json);
743
1261
  }
744
1262
  /**
745
1263
  * Look up a single anchor's preview info — `{ id, kind, scope, textPreview }`.
@@ -793,6 +1311,18 @@ export class DocxSession {
793
1311
  const raw = this.wasm.GetSectionInfo(this.handle, anchorId);
794
1312
  return JSON.parse(raw);
795
1313
  }
1314
+ /** Enumerate the document's explicit style catalog with resolved high-signal properties. */
1315
+ listStyles() {
1316
+ return JSON.parse(this.wasm.ListStyles(this.handle));
1317
+ }
1318
+ /** Inspect direct and effective paragraph/run formatting for one paragraph anchor. */
1319
+ getFormatting(anchorId) {
1320
+ return JSON.parse(this.wasm.GetFormatting(this.handle, anchorId));
1321
+ }
1322
+ /** Enumerate text-bearing runs as mutation-compatible anchor/span pairs. */
1323
+ listInlineSpans(anchorId) {
1324
+ return JSON.parse(this.wasm.ListInlineSpans(this.handle, anchorId));
1325
+ }
796
1326
  /**
797
1327
  * Enumerates every annotation persisted in the document. Lets an agent prime
798
1328
  * itself with "here are the labeled regions you can target" before committing
@@ -851,6 +1381,13 @@ export class DocxSession {
851
1381
  this.close();
852
1382
  }
853
1383
  }
1384
+ function imageBytesToBase64(bytes) {
1385
+ let binary = "";
1386
+ for (let offset = 0; offset < bytes.length; offset += 0x8000) {
1387
+ binary += String.fromCharCode(...bytes.subarray(offset, offset + 0x8000));
1388
+ }
1389
+ return globalThis.btoa(binary);
1390
+ }
854
1391
  /**
855
1392
  * Opens a new {@link DocxSession} over the supplied DOCX bytes.
856
1393
  * The returned session holds its document in WASM memory until you call