docxodus 12.0.1 → 12.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/README.md +9 -4
  2. package/dist/editor.bundle.js +197 -25
  3. package/dist/editor.d.ts +42 -3
  4. package/dist/editor.d.ts.map +1 -1
  5. package/dist/editor.js +204 -16
  6. package/dist/editor.js.map +1 -1
  7. package/dist/embed.bundle.js +635 -50
  8. package/dist/embed.d.ts +30 -1
  9. package/dist/embed.d.ts.map +1 -1
  10. package/dist/embed.iife.js +635 -50
  11. package/dist/embed.js +124 -13
  12. package/dist/embed.js.map +1 -1
  13. package/dist/export-assets.json +33 -33
  14. package/dist/history.d.ts +265 -0
  15. package/dist/history.d.ts.map +1 -0
  16. package/dist/history.js +307 -0
  17. package/dist/history.js.map +1 -0
  18. package/dist/index.d.ts +7 -0
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js +20 -2
  21. package/dist/index.js.map +1 -1
  22. package/dist/ribbon-chrome.d.ts +2 -2
  23. package/dist/ribbon-chrome.d.ts.map +1 -1
  24. package/dist/ribbon-chrome.js +6 -4
  25. package/dist/ribbon-chrome.js.map +1 -1
  26. package/dist/ribbon.js +39 -8
  27. package/dist/ribbon.js.map +1 -1
  28. package/dist/session.js +1 -1
  29. package/dist/session.js.map +1 -1
  30. package/dist/types.d.ts +2 -0
  31. package/dist/types.d.ts.map +1 -1
  32. package/dist/types.js.map +1 -1
  33. package/dist/wasm/_framework/DocumentFormat.OpenXml.Framework.wasm +0 -0
  34. package/dist/wasm/_framework/DocumentFormat.OpenXml.Framework.wasm.br +0 -0
  35. package/dist/wasm/_framework/DocumentFormat.OpenXml.wasm +0 -0
  36. package/dist/wasm/_framework/DocumentFormat.OpenXml.wasm.br +0 -0
  37. package/dist/wasm/_framework/Docxodus.wasm +0 -0
  38. package/dist/wasm/_framework/Docxodus.wasm.br +0 -0
  39. package/dist/wasm/_framework/DocxodusWasm.wasm +0 -0
  40. package/dist/wasm/_framework/DocxodusWasm.wasm.br +0 -0
  41. package/dist/wasm/_framework/System.Collections.Concurrent.wasm +0 -0
  42. package/dist/wasm/_framework/System.Collections.Concurrent.wasm.br +0 -0
  43. package/dist/wasm/_framework/System.Collections.wasm +0 -0
  44. package/dist/wasm/_framework/System.Collections.wasm.br +0 -0
  45. package/dist/wasm/_framework/System.IO.Compression.wasm +0 -0
  46. package/dist/wasm/_framework/System.IO.Compression.wasm.br +0 -0
  47. package/dist/wasm/_framework/System.IO.Packaging.wasm +0 -0
  48. package/dist/wasm/_framework/System.IO.Packaging.wasm.br +0 -0
  49. package/dist/wasm/_framework/System.Linq.Expressions.wasm +0 -0
  50. package/dist/wasm/_framework/System.Linq.Expressions.wasm.br +0 -0
  51. package/dist/wasm/_framework/System.Memory.wasm +0 -0
  52. package/dist/wasm/_framework/System.Memory.wasm.br +0 -0
  53. package/dist/wasm/_framework/System.Net.Http.wasm +0 -0
  54. package/dist/wasm/_framework/System.Net.Http.wasm.br +0 -0
  55. package/dist/wasm/_framework/System.Private.CoreLib.wasm +0 -0
  56. package/dist/wasm/_framework/System.Private.CoreLib.wasm.br +0 -0
  57. package/dist/wasm/_framework/System.Private.Xml.Linq.wasm +0 -0
  58. package/dist/wasm/_framework/System.Private.Xml.Linq.wasm.br +0 -0
  59. package/dist/wasm/_framework/System.Private.Xml.wasm +0 -0
  60. package/dist/wasm/_framework/System.Private.Xml.wasm.br +0 -0
  61. package/dist/wasm/_framework/System.Runtime.InteropServices.JavaScript.wasm +0 -0
  62. package/dist/wasm/_framework/System.Runtime.InteropServices.JavaScript.wasm.br +0 -0
  63. package/dist/wasm/_framework/System.Runtime.wasm +0 -0
  64. package/dist/wasm/_framework/System.Runtime.wasm.br +0 -0
  65. package/dist/wasm/_framework/System.Security.Cryptography.wasm +0 -0
  66. package/dist/wasm/_framework/System.Security.Cryptography.wasm.br +0 -0
  67. package/dist/wasm/_framework/System.Text.Json.wasm +0 -0
  68. package/dist/wasm/_framework/System.Text.Json.wasm.br +0 -0
  69. package/dist/wasm/_framework/System.Text.RegularExpressions.wasm +0 -0
  70. package/dist/wasm/_framework/System.Text.RegularExpressions.wasm.br +0 -0
  71. package/dist/wasm/_framework/dotnet.boot.js +21 -21
  72. package/dist/wasm/_framework/dotnet.boot.js.br +0 -0
  73. package/dist/wasm/_framework/dotnet.native.wasm +0 -0
  74. package/dist/wasm/_framework/dotnet.native.wasm.br +0 -0
  75. package/package.json +4 -3
@@ -141,8 +141,14 @@ var Docxodus = (() => {
141
141
  DocxDiffFormatComparison: () => DocxDiffFormatComparison,
142
142
  DocxDiffRevisionGranularity: () => DocxDiffRevisionGranularity,
143
143
  DocxEditor: () => DocxEditor,
144
+ DocxHistoryArchive: () => DocxHistoryArchive,
145
+ DocxHistoryClient: () => DocxHistoryClient,
146
+ DocxHistoryDocument: () => DocxHistoryDocument,
147
+ DocxHistoryError: () => DocxHistoryError,
148
+ DocxHistoryReader: () => DocxHistoryReader,
144
149
  DocxSession: () => DocxSession,
145
150
  EmptyParagraphMode: () => EmptyParagraphMode,
151
+ MAX_HISTORY_ARCHIVE_BYTES: () => MAX_HISTORY_ARCHIVE_BYTES,
146
152
  PaginationEngine: () => PaginationEngine,
147
153
  PaginationMode: () => PaginationMode,
148
154
  PlaceholderKinds: () => PlaceholderKinds,
@@ -166,6 +172,8 @@ var Docxodus = (() => {
166
172
  createBlankDocx: () => createBlankDocx,
167
173
  createEditor: () => createEditor,
168
174
  createExternalAnnotationSet: () => createExternalAnnotationSet,
175
+ createHistoryViewer: () => createHistoryViewer,
176
+ createMemoryHistoryStorage: () => createMemoryHistoryStorage,
169
177
  createRibbonEditor: () => createRibbonEditor,
170
178
  createUnavailablePageMap: () => createUnavailablePageMap,
171
179
  createViewer: () => createViewer,
@@ -200,6 +208,7 @@ var Docxodus = (() => {
200
208
  getWasmExports: () => getWasmExports,
201
209
  hasAnnotations: () => hasAnnotations,
202
210
  initialize: () => initialize,
211
+ installHistoryStorageImports: () => installHistoryStorageImports,
203
212
  isDeletion: () => isDeletion,
204
213
  isFormatChange: () => isFormatChange,
205
214
  isInitialized: () => isInitialized,
@@ -207,6 +216,8 @@ var Docxodus = (() => {
207
216
  isMove: () => isMove,
208
217
  mountRibbon: () => mountRibbon,
209
218
  navigateToPageCitation: () => navigateToPageCitation,
219
+ openDocxHistory: () => openDocxHistory,
220
+ openDocxHistoryArchive: () => openDocxHistoryArchive,
210
221
  openDocxSession: () => openDocxSession2,
211
222
  paginateHtml: () => paginateHtml,
212
223
  parseSectionDimensions: () => parseSectionDimensions,
@@ -2161,6 +2172,288 @@ var Docxodus = (() => {
2161
2172
  return new DocxSession(handle, bridge);
2162
2173
  }
2163
2174
 
2175
+ // src/history.ts
2176
+ var MAX_HISTORY_ARCHIVE_BYTES = 64 * 1024 * 1024;
2177
+ var DocxHistoryError = class extends Error {
2178
+ constructor(code, message) {
2179
+ super(message);
2180
+ this.code = code;
2181
+ this.name = "DocxHistoryError";
2182
+ }
2183
+ };
2184
+ var adapters = /* @__PURE__ */ new Map();
2185
+ var nextAdapter = 0;
2186
+ function adapter(id) {
2187
+ const store = adapters.get(id);
2188
+ if (!store) throw new DocxHistoryError("Closed", "History storage adapter is closed.");
2189
+ return store;
2190
+ }
2191
+ function toBase64(bytes) {
2192
+ let binary = "";
2193
+ for (let offset = 0; offset < bytes.length; offset += 32768)
2194
+ binary += String.fromCharCode(...bytes.subarray(offset, offset + 32768));
2195
+ return btoa(binary);
2196
+ }
2197
+ function fromBase64(encoded) {
2198
+ const binary = atob(encoded);
2199
+ return Uint8Array.from(binary, (c) => c.charCodeAt(0));
2200
+ }
2201
+ function installHistoryStorageImports(setModuleImports) {
2202
+ setModuleImports("docxodus.history", {
2203
+ readBlob: async (id, json) => {
2204
+ const reference = JSON.parse(json);
2205
+ const bytes = await adapter(id).readBlob(reference);
2206
+ if (bytes === null) return "";
2207
+ if (bytes.length !== reference.length) throw new DocxHistoryError("PayloadMismatch", "Stored blob length differs from its reference.");
2208
+ return bytes.length === 0 ? "=" : toBase64(bytes);
2209
+ },
2210
+ putBlob: async (id, json, bytes) => adapter(id).putBlob(JSON.parse(json), bytes.slice()),
2211
+ readHead: async (id, documentId) => JSON.stringify(await adapter(id).readHead(documentId)),
2212
+ advanceHead: async (id, documentId, expected, state) => JSON.stringify(await adapter(id).advanceHead(documentId, JSON.parse(expected), JSON.parse(state))),
2213
+ initializeHead: async (id, documentId, head) => {
2214
+ const storage = adapter(id);
2215
+ if (!storage.initializeHead) throw new DocxHistoryError("InitializationUnsupported", "Storage cannot import an exact head.");
2216
+ return JSON.stringify(await storage.initializeHead(documentId, JSON.parse(head)));
2217
+ }
2218
+ });
2219
+ }
2220
+ var DocxHistoryClient = class {
2221
+ constructor(bridge, storage) {
2222
+ this.bridge = bridge;
2223
+ this.closed = false;
2224
+ if (nextAdapter >= 2147483647) throw new Error("History adapter identifiers exhausted.");
2225
+ this.adapterId = ++nextAdapter;
2226
+ adapters.set(this.adapterId, storage);
2227
+ try {
2228
+ this.handle = storage.initializeHead && bridge.OpenWithInitialization ? bridge.OpenWithInitialization(this.adapterId) : bridge.Open(this.adapterId);
2229
+ } catch (error) {
2230
+ adapters.delete(this.adapterId);
2231
+ throw error;
2232
+ }
2233
+ }
2234
+ close() {
2235
+ if (this.closed) return;
2236
+ this.bridge.Close(this.handle);
2237
+ adapters.delete(this.adapterId);
2238
+ this.closed = true;
2239
+ }
2240
+ /** Bind an identity without reading, creating a version, or taking ownership of this client. */
2241
+ document(documentId) {
2242
+ return new DocxHistoryDocument(documentId, (operation, fields, bytes) => this.call(operation, documentId, fields, bytes));
2243
+ }
2244
+ async exportHistoryArchive(documentId) {
2245
+ return fromBase64((await this.call("exportArchive", documentId)).bytes);
2246
+ }
2247
+ async importHistoryArchive(bytes) {
2248
+ checkArchiveSize(bytes);
2249
+ return (await this.call("importArchive", "", {}, bytes)).import;
2250
+ }
2251
+ async read(documentId) {
2252
+ return (await this.call("read", documentId)).view;
2253
+ }
2254
+ /** Host-triggered validated metadata tail; first join starts at the latest checkpoint. */
2255
+ async readChangesSince(documentId, after, maxEntriesToScan = 1e4) {
2256
+ return (await this.call("updates", documentId, { expectedHead: after, maxEntriesToScan })).update;
2257
+ }
2258
+ async readOperationsSince(documentId, after, maxEntriesToScan = 1e4) {
2259
+ return (await this.call("operations", documentId, { expectedHead: after, maxEntriesToScan })).operationUpdate;
2260
+ }
2261
+ async getOperation(documentId, operationId) {
2262
+ return (await this.call("getOperation", documentId, { operationId })).operation;
2263
+ }
2264
+ async exportOperationProposal(documentId, operationId) {
2265
+ return fromBase64((await this.call("exportOperationProposal", documentId, { operationId })).bytes);
2266
+ }
2267
+ async compareVersions(documentId, beforeVersionId, afterVersionId) {
2268
+ return fromBase64((await this.call("compare", documentId, { beforeVersionId, afterVersionId })).bytes);
2269
+ }
2270
+ async createVersion(documentId, expectedHead, bytes, metadata, requestId) {
2271
+ return (await this.call("create", documentId, { expectedHead, metadata, requestId }, bytes)).view;
2272
+ }
2273
+ async listVersions(documentId, cursor = null, limit = 25) {
2274
+ return (await this.call("list", documentId, { versionId: cursor, limit })).page;
2275
+ }
2276
+ async getVersion(documentId, versionId) {
2277
+ return (await this.call("get", documentId, { versionId })).version;
2278
+ }
2279
+ async exportVersion(documentId, versionId) {
2280
+ return fromBase64((await this.call("export", documentId, { versionId })).bytes);
2281
+ }
2282
+ async materialize(documentId, sequence, maxEntriesToScan = 1e4) {
2283
+ return fromBase64((await this.call("materialize", documentId, { sequence, maxEntriesToScan })).bytes);
2284
+ }
2285
+ async replay(documentId, sequence, maxEntriesToScan = 1e4) {
2286
+ return fromBase64((await this.call("replay", documentId, { sequence, maxEntriesToScan })).bytes);
2287
+ }
2288
+ async resolveSequenceAtTime(documentId, cutoff, maxEntriesToScan = 1e4) {
2289
+ return (await this.call("resolveTime", documentId, { cutoff, maxEntriesToScan })).sequence;
2290
+ }
2291
+ async restoreVersion(documentId, expectedHead, versionId, metadata, requestId) {
2292
+ return (await this.call("restore", documentId, { expectedHead, versionId, metadata, requestId })).view;
2293
+ }
2294
+ async call(operation, documentId, fields = {}, bytes = new Uint8Array()) {
2295
+ if (this.closed) throw new DocxHistoryError("Closed", "History client is closed.");
2296
+ return parseResult(await this.bridge.Invoke(
2297
+ this.handle,
2298
+ JSON.stringify({ schemaVersion: 1, operation, documentId, ...fields }),
2299
+ bytes
2300
+ ));
2301
+ }
2302
+ };
2303
+ var DocxHistoryReader = class {
2304
+ /** @internal Obtain through client.document() or openDocxHistoryArchive(). */
2305
+ constructor(documentId, call) {
2306
+ this.call = call;
2307
+ this.#documentId = documentId;
2308
+ }
2309
+ #documentId;
2310
+ get documentId() {
2311
+ return this.#documentId;
2312
+ }
2313
+ async read() {
2314
+ return (await this.call("read")).view;
2315
+ }
2316
+ async listVersions(cursor = null, limit = 25) {
2317
+ return (await this.call("list", { versionId: cursor, limit })).page;
2318
+ }
2319
+ async getVersion(versionId) {
2320
+ return (await this.call("get", { versionId })).version;
2321
+ }
2322
+ /** Omit versionId for latest; pass an ID to keep a UI operation pinned to a captured version. */
2323
+ async exportDocx(versionId = null) {
2324
+ return fromBase64((await this.call("exportDocx", { versionId })).bytes);
2325
+ }
2326
+ async exportHistoryArchive() {
2327
+ return fromBase64((await this.call("exportArchive")).bytes);
2328
+ }
2329
+ async materialize(sequence, maxEntriesToScan = 1e4) {
2330
+ return fromBase64((await this.call("materialize", { sequence, maxEntriesToScan })).bytes);
2331
+ }
2332
+ async replay(sequence, maxEntriesToScan = 1e4) {
2333
+ return fromBase64((await this.call("replay", { sequence, maxEntriesToScan })).bytes);
2334
+ }
2335
+ async resolveSequenceAtTime(cutoff, maxEntriesToScan = 1e4) {
2336
+ return (await this.call("resolveTime", { cutoff, maxEntriesToScan })).sequence;
2337
+ }
2338
+ async readChangesSince(after, maxEntriesToScan = 1e4) {
2339
+ return (await this.call("updates", { expectedHead: after, maxEntriesToScan })).update;
2340
+ }
2341
+ async readOperationsSince(after, maxEntriesToScan = 1e4) {
2342
+ return (await this.call("operations", { expectedHead: after, maxEntriesToScan })).operationUpdate;
2343
+ }
2344
+ async getOperation(operationId) {
2345
+ return (await this.call("getOperation", { operationId })).operation;
2346
+ }
2347
+ async exportOperationProposal(operationId) {
2348
+ return fromBase64((await this.call("exportOperationProposal", { operationId })).bytes);
2349
+ }
2350
+ /** Redlined DOCX through DocxCompare's existing accepted-input revision policy. */
2351
+ async compareVersions(beforeVersionId, afterVersionId) {
2352
+ return fromBase64((await this.call("compare", { beforeVersionId, afterVersionId })).bytes);
2353
+ }
2354
+ };
2355
+ var DocxHistoryDocument = class extends DocxHistoryReader {
2356
+ async createVersion(expectedHead, bytes, metadata, requestId) {
2357
+ requireRequestId(requestId);
2358
+ return (await this.call("create", { expectedHead, metadata, requestId }, bytes)).view;
2359
+ }
2360
+ async restoreVersion(expectedHead, versionId, metadata, requestId) {
2361
+ requireRequestId(requestId);
2362
+ return (await this.call("restore", { expectedHead, versionId, metadata, requestId })).view;
2363
+ }
2364
+ };
2365
+ var DocxHistoryArchive = class _DocxHistoryArchive extends DocxHistoryReader {
2366
+ constructor(bridge, handle, info) {
2367
+ super(info.documentId, async (operation, fields = {}, bytes = new Uint8Array()) => {
2368
+ if (this.closed) throw new DocxHistoryError("Closed", "History archive is closed.");
2369
+ return parseResult(await bridge.Invoke(
2370
+ handle,
2371
+ JSON.stringify({ schemaVersion: 1, operation, documentId: this.documentId, ...fields }),
2372
+ bytes
2373
+ ));
2374
+ });
2375
+ this.bridge = bridge;
2376
+ this.handle = handle;
2377
+ this.info = info;
2378
+ this.closed = false;
2379
+ }
2380
+ /** For custom loaders; normal callers use openDocxHistoryArchive(). */
2381
+ static async open(bridge, bytes) {
2382
+ checkArchiveSize(bytes);
2383
+ if (!bridge.OpenArchive) throw new Error("This WASM build does not include portable history.");
2384
+ const result = parseResult(await bridge.OpenArchive(bytes));
2385
+ return new _DocxHistoryArchive(bridge, result.handle, result.archive);
2386
+ }
2387
+ close() {
2388
+ if (this.closed) return;
2389
+ this.bridge.Close(this.handle);
2390
+ this.closed = true;
2391
+ }
2392
+ };
2393
+ function parseResult(json) {
2394
+ const result = JSON.parse(json);
2395
+ if (!result.success) throw new DocxHistoryError(result.errorCode, result.message);
2396
+ return result;
2397
+ }
2398
+ function checkArchiveSize(bytes) {
2399
+ if (bytes.length > MAX_HISTORY_ARCHIVE_BYTES) throw new DocxHistoryError("ResourceLimit", "History archive exceeds 64 MiB.");
2400
+ }
2401
+ function requireRequestId(requestId) {
2402
+ if (typeof requestId !== "string" || !requestId.trim()) throw new DocxHistoryError("InvalidRequest", "A durable requestId is required.");
2403
+ }
2404
+ function createMemoryHistoryStorage(maxBlobBytes = 256 * 1024 * 1024) {
2405
+ if (!Number.isSafeInteger(maxBlobBytes) || maxBlobBytes <= 0) throw new RangeError("Invalid blob byte limit.");
2406
+ const blobs = /* @__PURE__ */ new Map();
2407
+ const heads = /* @__PURE__ */ new Map();
2408
+ const copy = (value) => JSON.parse(JSON.stringify(value));
2409
+ const key = (reference) => {
2410
+ if (reference.digest.algorithm !== "SHA-256" || !/^[a-f0-9]{64}$/.test(reference.digest.value) || !Number.isSafeInteger(reference.length) || reference.length < 0 || reference.length > maxBlobBytes)
2411
+ throw new DocxHistoryError("InvalidRequest", "Invalid blob reference or byte limit.");
2412
+ return `${reference.digest.value}:${reference.length}`;
2413
+ };
2414
+ const equalReference = (a, b) => a.length === b.length && a.digest.algorithm === b.digest.algorithm && a.digest.value === b.digest.value;
2415
+ const equalHead = (a, b) => a === null || b === null ? a === b : a.revision === b.revision && equalReference(a.state, b.state);
2416
+ return {
2417
+ async readBlob(reference) {
2418
+ return blobs.get(key(reference))?.slice() ?? null;
2419
+ },
2420
+ async putBlob(reference, bytes) {
2421
+ const blobKey = key(reference);
2422
+ const captured = bytes.slice();
2423
+ if (captured.length !== reference.length) throw new DocxHistoryError("PayloadMismatch", "Blob length mismatch.");
2424
+ const digest = Array.from(
2425
+ new Uint8Array(await crypto.subtle.digest("SHA-256", captured)),
2426
+ (n) => n.toString(16).padStart(2, "0")
2427
+ ).join("");
2428
+ if (digest !== blobKey.split(":")[0]) throw new DocxHistoryError("PayloadMismatch", "Blob digest mismatch.");
2429
+ if (!blobs.has(blobKey)) blobs.set(blobKey, captured);
2430
+ },
2431
+ async readHead(documentId) {
2432
+ return copy(heads.get(documentId) ?? null);
2433
+ },
2434
+ async advanceHead(documentId, expected, state) {
2435
+ key(state);
2436
+ const current = heads.get(documentId) ?? null;
2437
+ if (!equalHead(current, expected)) return null;
2438
+ const revision = BigInt(current?.revision ?? "0") + 1n;
2439
+ if (revision > 9223372036854775807n) throw new RangeError("History revision exhausted.");
2440
+ const head = { revision: String(revision), state: copy(state) };
2441
+ heads.set(documentId, head);
2442
+ return copy(head);
2443
+ },
2444
+ async initializeHead(documentId, head) {
2445
+ key(head.state);
2446
+ if (typeof head.revision !== "string" || !/^[1-9][0-9]*$/.test(head.revision) || BigInt(head.revision) > 9223372036854775807n)
2447
+ throw new DocxHistoryError("InvalidRequest", "Imported head revision must be a positive Int64 string.");
2448
+ const existing = heads.get(documentId);
2449
+ if (existing) return { initialized: false, head: copy(existing) };
2450
+ const captured = copy(head);
2451
+ heads.set(documentId, captured);
2452
+ return { initialized: true, head: copy(captured) };
2453
+ }
2454
+ };
2455
+ }
2456
+
2164
2457
  // src/page-number-format.ts
2165
2458
  var ROMAN_ONES = ["", "i", "ii", "iii", "iv", "v", "vi", "vii", "viii", "ix"];
2166
2459
  var ROMAN_TENS = ["", "x", "xx", "xxx", "xl", "l", "lx", "lxx", "lxxx", "xc"];
@@ -5610,11 +5903,11 @@ var Docxodus = (() => {
5610
5903
  if (!this.partUriFor(which, "default")) this.seedStory(which, "default");
5611
5904
  if (!this.partUriFor(which, kind)) this.seedStory(which, kind);
5612
5905
  }
5613
- const res = parseResult(this.bridge.EnsureHeaderFooterVisible(this.handle, this.bodyAnchorId, kind));
5906
+ const res = parseResult2(this.bridge.EnsureHeaderFooterVisible(this.handle, this.bodyAnchorId, kind));
5614
5907
  if (!res.success) return false;
5615
5908
  } else {
5616
5909
  if (!this.bridge.SetHeaderFooterKindEnabled) return false;
5617
- const res = parseResult(this.bridge.SetHeaderFooterKindEnabled(this.handle, this.bodyAnchorId, kind, false));
5910
+ const res = parseResult2(this.bridge.SetHeaderFooterKindEnabled(this.handle, this.bodyAnchorId, kind, false));
5618
5911
  if (!res.success) return false;
5619
5912
  for (const which of ["header", "footer"]) {
5620
5913
  if (this.kinds[which] === kind) this.kinds[which] = "default";
@@ -5639,7 +5932,7 @@ var Docxodus = (() => {
5639
5932
  * Deliberately a PLAIN field (no `\*` switch), exactly as Word inserts one, so it follows the
5640
5933
  * section's page-number format — see {@link setPageNumbering}. */
5641
5934
  insertPageNumber(which, anchorId, field) {
5642
- const res = parseResult(this.bridge.InsertPageNumberField(this.handle, anchorId, field, ""));
5935
+ const res = parseResult2(this.bridge.InsertPageNumberField(this.handle, anchorId, field, ""));
5643
5936
  if (!res.success) return false;
5644
5937
  this.refresh(which);
5645
5938
  return true;
@@ -5662,7 +5955,7 @@ var Docxodus = (() => {
5662
5955
  /** Set this section's page numbering (`w:pgNumType`) — Word's *Format Page Numbers…*. */
5663
5956
  setPageNumbering(op) {
5664
5957
  if (!this.bodyAnchorId) return false;
5665
- const res = parseResult(this.bridge.SetPageNumbering(this.handle, this.bodyAnchorId, JSON.stringify(op)));
5958
+ const res = parseResult2(this.bridge.SetPageNumbering(this.handle, this.bodyAnchorId, JSON.stringify(op)));
5666
5959
  if (!res.success) return false;
5667
5960
  this.reloadSection();
5668
5961
  return true;
@@ -5674,7 +5967,7 @@ var Docxodus = (() => {
5674
5967
  /** Remove this section's page-numbering start/format. */
5675
5968
  clearPageNumbering() {
5676
5969
  if (!this.bodyAnchorId) return false;
5677
- const res = parseResult(this.bridge.ClearPageNumbering(this.handle, this.bodyAnchorId));
5970
+ const res = parseResult2(this.bridge.ClearPageNumbering(this.handle, this.bodyAnchorId));
5678
5971
  if (!res.success) return false;
5679
5972
  this.reloadSection();
5680
5973
  return true;
@@ -5985,7 +6278,7 @@ var Docxodus = (() => {
5985
6278
  seedStory(which, kind) {
5986
6279
  if (!this.bodyAnchorId) return;
5987
6280
  const set = which === "header" ? this.bridge.SetHeaderText : this.bridge.SetFooterText;
5988
- const res = parseResult(set(this.handle, this.bodyAnchorId, kind, ""));
6281
+ const res = parseResult2(set(this.handle, this.bodyAnchorId, kind, ""));
5989
6282
  if (!res.success) return;
5990
6283
  this.callbacks.refreshAnchorMap();
5991
6284
  this.sectionInfo = this.readSectionInfo(this.bodyAnchorId);
@@ -6141,7 +6434,7 @@ var Docxodus = (() => {
6141
6434
  const range = d.caretRangeFromPoint?.(x, y);
6142
6435
  return range ? { node: range.startContainer, offset: range.startOffset } : null;
6143
6436
  }
6144
- function parseResult(json) {
6437
+ function parseResult2(json) {
6145
6438
  try {
6146
6439
  return JSON.parse(json);
6147
6440
  } catch {
@@ -8374,7 +8667,7 @@ span.comment-highlight.docx-comment-active { background: hsl(var(--docx-comment-
8374
8667
  };
8375
8668
  }
8376
8669
  var honeyPotFix = makeHoneyPotFix();
8377
- var adapter = makeAdapter({
8670
+ var adapter2 = makeAdapter({
8378
8671
  typeKey: "element",
8379
8672
  defaultDropEffect: "move",
8380
8673
  mount: function mount(api2) {
@@ -8463,8 +8756,8 @@ span.comment-highlight.docx-comment-active { background: hsl(var(--docx-comment-
8463
8756
  },
8464
8757
  onPostDispatch: honeyPotFix.getOnPostDispatch()
8465
8758
  });
8466
- var dropTargetForElements = adapter.dropTarget;
8467
- var monitorForElements = adapter.monitor;
8759
+ var dropTargetForElements = adapter2.dropTarget;
8760
+ var monitorForElements = adapter2.monitor;
8468
8761
  function draggable(args) {
8469
8762
  if (true) {
8470
8763
  if (args.dragHandle && !args.element.contains(args.dragHandle)) {
@@ -8488,7 +8781,7 @@ span.comment-highlight.docx-comment-active { background: hsl(var(--docx-comment-
8488
8781
  // this is because you *must* have a draggable element to start a drag
8489
8782
  // but you _might_ not have any drop targets immediately
8490
8783
  // (You might create drop targets async)
8491
- adapter.registerUsage(),
8784
+ adapter2.registerUsage(),
8492
8785
  addToRegistry(args),
8493
8786
  addAttribute(args.element, {
8494
8787
  attribute: "draggable",
@@ -9571,6 +9864,12 @@ span.comment-highlight.docx-comment-active { background: hsl(var(--docx-comment-
9571
9864
  const text = (unit.textContent ?? "").trim().replace(/\s+/g, " ");
9572
9865
  return text.length > 48 ? `${text.slice(0, 48)}\u2026` : text;
9573
9866
  }
9867
+ var BLOCK_HANDLE_WIDTH = 26;
9868
+ var BLOCK_HANDLE_HEIGHT = 28;
9869
+ var BLOCK_HANDLE_GAP = 6;
9870
+ function clampToRange(value, low, high) {
9871
+ return Math.max(low, Math.min(value, high));
9872
+ }
9574
9873
  function ensureBlockDragStyles(doc) {
9575
9874
  if (blockDragStyledDocuments.has(doc)) return;
9576
9875
  blockDragStyledDocuments.add(doc);
@@ -9929,22 +10228,55 @@ span.comment-highlight.docx-comment-active { background: hsl(var(--docx-comment-
9929
10228
  const span = trimmedSpan(block, { start: Math.min(start2, end), length: Math.abs(end - start2) });
9930
10229
  return span.length > 0 ? span : null;
9931
10230
  }
9932
- function selectRange(el, start2, length) {
9933
- const sel = typeof window !== "undefined" ? window.getSelection() : null;
9934
- if (!sel || !el.isConnected) return;
9935
- el.focus();
9936
- const range = document.createRange();
10231
+ function contentRangeIn(el, start2, length) {
10232
+ if (!el.isConnected) return null;
10233
+ const doc = el.ownerDocument;
10234
+ const range = doc.createRange();
9937
10235
  const from = contentPositionIn(el, start2);
9938
10236
  const to = contentPositionIn(el, start2 + length);
9939
10237
  try {
9940
10238
  range.setStart(from.node, from.offset);
9941
10239
  range.setEnd(to.node, to.offset);
9942
10240
  } catch {
9943
- return;
10241
+ return null;
9944
10242
  }
10243
+ return range;
10244
+ }
10245
+ function selectRange(el, start2, length) {
10246
+ const sel = typeof window !== "undefined" ? window.getSelection() : null;
10247
+ if (!sel || !el.isConnected) return;
10248
+ el.focus();
10249
+ const range = contentRangeIn(el, start2, length);
10250
+ if (!range) return;
9945
10251
  sel.removeAllRanges();
9946
10252
  sel.addRange(range);
9947
10253
  }
10254
+ var FIND_HIGHLIGHT = "docxodus-find";
10255
+ var FIND_HIGHLIGHT_ACTIVE = "docxodus-find-active";
10256
+ var FIND_PAINT_LIMIT = 500;
10257
+ var findHighlightStyledDocuments = /* @__PURE__ */ new WeakSet();
10258
+ var findPaintOwner = null;
10259
+ function ensureFindHighlightStyles(doc) {
10260
+ if (findHighlightStyledDocuments.has(doc)) return;
10261
+ findHighlightStyledDocuments.add(doc);
10262
+ const style = doc.createElement("style");
10263
+ style.dataset.docxodusFindHighlight = "true";
10264
+ style.textContent = `
10265
+ ::highlight(${FIND_HIGHLIGHT}) { background-color: rgba(15, 118, 110, .16); }
10266
+ ::highlight(${FIND_HIGHLIGHT_ACTIVE}) { background-color: rgba(15, 118, 110, .38); }
10267
+ `;
10268
+ (doc.head ?? doc.documentElement).appendChild(style);
10269
+ }
10270
+ function highlightRegistry() {
10271
+ if (typeof CSS === "undefined") return null;
10272
+ const registry = CSS.highlights;
10273
+ const ctor = globalThis.Highlight;
10274
+ return registry && typeof ctor === "function" ? registry : null;
10275
+ }
10276
+ function makeHighlight(ranges) {
10277
+ const ctor = globalThis.Highlight;
10278
+ return new ctor(...ranges);
10279
+ }
9948
10280
  function inBorderWrapper(el) {
9949
10281
  const style = el.parentElement?.getAttribute("style") ?? "";
9950
10282
  return /border-(top|bottom|left|right):\s*(?!none)[^;]+/i.test(style);
@@ -10033,6 +10365,10 @@ span.comment-highlight.docx-comment-active { background: hsl(var(--docx-comment-
10033
10365
  this.blockMoveMenu = null;
10034
10366
  this.blockMoveLive = null;
10035
10367
  this.blockDragSource = null;
10368
+ /** Whether hover or focus currently wants a handle for `blockDragSource`.
10369
+ * Tracked apart from the handle's `display`, which the viewport clip also drives: a handle
10370
+ * withdrawn because its block scrolled out of view has to come back when it scrolls in. */
10371
+ this.blockHandleWanted = false;
10036
10372
  this.blockDragCleanup = [];
10037
10373
  /** Block boxes measured at drag start — see `BlockDropZone`. Empty when no drag is in flight. */
10038
10374
  this.dropZones = [];
@@ -10311,6 +10647,7 @@ span.comment-highlight.docx-comment-active { background: hsl(var(--docx-comment-
10311
10647
  document.removeEventListener("mouseup", this.onMouseUp, true);
10312
10648
  }
10313
10649
  this.clearDragSelection();
10650
+ this.clearFindMatches();
10314
10651
  this.teardownBlockDrag();
10315
10652
  this.gutter?.dispose();
10316
10653
  this.gutter = null;
@@ -10429,11 +10766,47 @@ span.comment-highlight.docx-comment-active { background: hsl(var(--docx-comment-
10429
10766
  }
10430
10767
  return null;
10431
10768
  }
10769
+ /**
10770
+ * The rectangle the floating handle may occupy: the window, narrowed by every ancestor that
10771
+ * clips its overflow.
10772
+ *
10773
+ * The handle is `position: fixed`, so it is placed in viewport coordinates and no ancestor's
10774
+ * overflow clips it for us. But the editor is routinely mounted inside a bounded scroller —
10775
+ * the ribbon puts its surface in one, inside a clipped card — and a block scrolled out of that
10776
+ * scroller has to take its handle with it rather than leave it parked over the host's chrome.
10777
+ *
10778
+ * Gated on the computed `overflow` rather than on whether an element scrolls right now, so the
10779
+ * answer does not depend on how much content happens to be loaded.
10780
+ */
10781
+ blockHandleClipRect() {
10782
+ const view = this.container.ownerDocument.defaultView;
10783
+ const clip = {
10784
+ top: 0,
10785
+ left: 0,
10786
+ right: view?.innerWidth ?? Number.MAX_SAFE_INTEGER,
10787
+ bottom: view?.innerHeight ?? Number.MAX_SAFE_INTEGER
10788
+ };
10789
+ for (let el = this.editRoot; el; el = el.parentElement) {
10790
+ const style = view?.getComputedStyle(el);
10791
+ if (!style || style.overflowX === "visible" && style.overflowY === "visible") continue;
10792
+ const box = el.getBoundingClientRect();
10793
+ if (style.overflowY !== "visible") {
10794
+ clip.top = Math.max(clip.top, box.top);
10795
+ clip.bottom = Math.min(clip.bottom, box.bottom);
10796
+ }
10797
+ if (style.overflowX !== "visible") {
10798
+ clip.left = Math.max(clip.left, box.left);
10799
+ clip.right = Math.min(clip.right, box.right);
10800
+ }
10801
+ }
10802
+ return clip;
10803
+ }
10432
10804
  showBlockHandle(unit) {
10433
10805
  const handle = this.blockDragHandle;
10434
10806
  if (!handle || !this.isMovableBlockUnit(unit)) return;
10435
10807
  const changed = unit !== this.blockDragSource;
10436
10808
  this.blockDragSource = unit;
10809
+ this.blockHandleWanted = true;
10437
10810
  const known = this.blockMoveTargetsFor(unit, { cachedOnly: true });
10438
10811
  if (known?.size === 0) {
10439
10812
  handle.style.display = "none";
@@ -10441,19 +10814,31 @@ span.comment-highlight.docx-comment-active { background: hsl(var(--docx-comment-
10441
10814
  }
10442
10815
  if (changed && known === void 0) this.prefetchBlockMoveTargets(unit);
10443
10816
  const rect = unit.getBoundingClientRect();
10817
+ const clip = this.blockHandleClipRect();
10818
+ if (rect.bottom <= clip.top || rect.top >= clip.bottom || rect.right <= clip.left || rect.left >= clip.right) {
10819
+ if (!this.blockDragging && this.blockMoveMenu?.style.display !== "block")
10820
+ handle.style.display = "none";
10821
+ return;
10822
+ }
10444
10823
  handle.style.display = "flex";
10445
- handle.style.left = `${Math.max(4, rect.left - 32)}px`;
10446
- handle.style.top = `${Math.max(4, rect.top + (unit.tagName === "TABLE" ? 6 : Math.max(0, (rect.height - 28) / 2)))}px`;
10824
+ const top2 = rect.top + (unit.tagName === "TABLE" ? 6 : Math.max(0, (rect.height - BLOCK_HANDLE_HEIGHT) / 2));
10825
+ handle.style.left = `${clampToRange(
10826
+ rect.left - (BLOCK_HANDLE_WIDTH + BLOCK_HANDLE_GAP),
10827
+ clip.left,
10828
+ clip.right - BLOCK_HANDLE_WIDTH
10829
+ )}px`;
10830
+ handle.style.top = `${clampToRange(top2, clip.top, clip.bottom - BLOCK_HANDLE_HEIGHT)}px`;
10447
10831
  const preview = blockPreviewText(unit);
10448
10832
  handle.setAttribute("aria-label", preview ? `Move block: ${preview}` : "Move block");
10449
10833
  }
10450
10834
  hideBlockHandle() {
10451
10835
  if (this.blockDragging || this.blockMoveMenu?.style.display === "block") return;
10836
+ this.blockHandleWanted = false;
10452
10837
  if (this.blockDragHandle) this.blockDragHandle.style.display = "none";
10453
10838
  }
10454
10839
  positionBlockHandle() {
10455
10840
  const source = this.currentBlockDragSource();
10456
- if (source && this.blockDragHandle?.style.display !== "none") this.showBlockHandle(source);
10841
+ if (source && this.blockHandleWanted) this.showBlockHandle(source);
10457
10842
  }
10458
10843
  /** Draw the drop line on `zone`'s requested edge, or take it away when there is no target. */
10459
10844
  paintDropIndicator(data) {
@@ -10891,6 +11276,7 @@ span.comment-highlight.docx-comment-active { background: hsl(var(--docx-comment-
10891
11276
  this.blockMoveMenu = null;
10892
11277
  this.blockMoveLive = null;
10893
11278
  this.blockDragSource = null;
11279
+ this.blockHandleWanted = false;
10894
11280
  this.blockDragging = false;
10895
11281
  this.blockDragPointerDown = false;
10896
11282
  }
@@ -12660,15 +13046,65 @@ span.comment-highlight.docx-comment-active { background: hsl(var(--docx-comment-
12660
13046
  }
12661
13047
  return out;
12662
13048
  }
12663
- /** Select a match and scroll it into view. */
13049
+ /**
13050
+ * Select a match, focus its block and scroll it into view — the caret lands on the hit, so
13051
+ * typing continues in the document. A find box that is still being typed into wants
13052
+ * `showFindMatches` instead; this is the commit step (closing the bar, jumping in to edit).
13053
+ */
12664
13054
  selectMatch(match) {
12665
13055
  if (!match.block.isConnected) return;
13056
+ this.clearFindMatches();
12666
13057
  this.activeBlock = match.block;
12667
13058
  selectRange(match.block, match.start, match.length);
12668
13059
  match.block.scrollIntoView({ block: "center", behavior: "smooth" });
12669
13060
  }
12670
- /** Replace one match's text (formatting of the surrounding run is inherited). */
12671
- replaceMatch(match, replacement) {
13061
+ /**
13062
+ * Paint `matches` and scroll the one at `activeIndex` into view WITHOUT moving focus or the
13063
+ * caret. This is what a find field calls on every keystroke: the document shows where the hits
13064
+ * are and rides to the current one, while the keyboard stays in the search box.
13065
+ *
13066
+ * Falls back to the document selection where the CSS Custom Highlight API is missing — still
13067
+ * without focusing the block, so the next character typed goes to the search field either way.
13068
+ */
13069
+ showFindMatches(matches, activeIndex) {
13070
+ const active = matches[activeIndex];
13071
+ this.clearFindMatches();
13072
+ if (!active || !active.block.isConnected) return;
13073
+ this.activeBlock = active.block;
13074
+ const activeRange = contentRangeIn(active.block, active.start, active.length);
13075
+ const registry = highlightRegistry();
13076
+ if (registry && activeRange) {
13077
+ ensureFindHighlightStyles(this.container.ownerDocument);
13078
+ const rest = [];
13079
+ for (let i = 0; i < matches.length && rest.length < FIND_PAINT_LIMIT; i++) {
13080
+ if (i === activeIndex) continue;
13081
+ const range = contentRangeIn(matches[i].block, matches[i].start, matches[i].length);
13082
+ if (range) rest.push(range);
13083
+ }
13084
+ if (rest.length > 0) registry.set(FIND_HIGHLIGHT, makeHighlight(rest));
13085
+ registry.set(FIND_HIGHLIGHT_ACTIVE, makeHighlight([activeRange]));
13086
+ findPaintOwner = this;
13087
+ } else if (activeRange) {
13088
+ const sel = this.container.ownerDocument.defaultView?.getSelection();
13089
+ sel?.removeAllRanges();
13090
+ sel?.addRange(activeRange);
13091
+ }
13092
+ active.block.scrollIntoView({ block: "center", behavior: "smooth" });
13093
+ }
13094
+ /** Drop the find painting (closing the find bar, or committing a match to the caret). */
13095
+ clearFindMatches() {
13096
+ if (findPaintOwner !== this) return;
13097
+ const registry = highlightRegistry();
13098
+ registry?.delete(FIND_HIGHLIGHT);
13099
+ registry?.delete(FIND_HIGHLIGHT_ACTIVE);
13100
+ findPaintOwner = null;
13101
+ }
13102
+ /**
13103
+ * Replace one match's text (formatting of the surrounding run is inherited). `focus: false`
13104
+ * leaves the keyboard where it is — a Replace button pressed from the find bar must not drag
13105
+ * the caret into the document mid-search.
13106
+ */
13107
+ replaceMatch(match, replacement, options = {}) {
12672
13108
  const block = match.block;
12673
13109
  if (this.closed || !block.isConnected) return false;
12674
13110
  const unid = block.getAttribute("data-anchor");
@@ -12689,7 +13125,7 @@ span.comment-highlight.docx-comment-active { background: hsl(var(--docx-comment-
12689
13125
  );
12690
13126
  if (!res.success) return false;
12691
13127
  const fresh = this.swapBlock(block, unid, res.modified?.[0]);
12692
- if (fresh) selectRange(fresh, span.start, replacement.length);
13128
+ if (fresh && options.focus !== false) selectRange(fresh, span.start, replacement.length);
12693
13129
  return true;
12694
13130
  }
12695
13131
  /** Replace every occurrence; returns how many were replaced. */
@@ -12708,7 +13144,11 @@ span.comment-highlight.docx-comment-active { background: hsl(var(--docx-comment-
12708
13144
  for (const m of list.slice().sort((a, b) => b.start - a.start)) {
12709
13145
  if (!current.isConnected) break;
12710
13146
  const before = current;
12711
- if (this.replaceMatch({ block: current, start: m.start, length: m.length }, replacement)) {
13147
+ if (this.replaceMatch(
13148
+ { block: current, start: m.start, length: m.length },
13149
+ replacement,
13150
+ { focus: options.focus }
13151
+ )) {
12712
13152
  count++;
12713
13153
  current = this.activeBlock && this.activeBlock !== before ? this.activeBlock : current;
12714
13154
  }
@@ -13390,7 +13830,7 @@ span.comment-highlight.docx-comment-active { background: hsl(var(--docx-comment-
13390
13830
  }
13391
13831
 
13392
13832
  // src/ribbon-chrome.ts
13393
- var RIBBON_STYLE_VERSION = "10";
13833
+ var RIBBON_STYLE_VERSION = "11";
13394
13834
  var RIBBON_STYLE_ATTR = "data-docxodus-ribbon-styles";
13395
13835
  var COMMENT_GUTTER_WIDTH = 264;
13396
13836
  var RIBBON_CSS = `
@@ -13823,9 +14263,11 @@ span.comment-highlight.docx-comment-active { background: hsl(var(--docx-comment-
13823
14263
  background: linear-gradient(to top, var(--dxr-desk), transparent);
13824
14264
  }
13825
14265
  .dxr[data-chrome] .dxr-surface { position: relative; margin: 26px auto; padding: 0 16px 96px; }
13826
- /* The comment gutter sits to the right of the sheet; the surface reserves its width so the
13827
- page shifts left rather than the bubbles covering it \u2014 Word's markup area. */
13828
- .dxr[data-chrome="full"] .dxr-surface[data-comments="on"] { padding-right: calc(var(--dxr-gutter) + 8px); }
14266
+ /* Reserve the markup area only while a visible comment or draft needs it. The gutter owns
14267
+ its empty/hidden state, so ordinary documents stay centered without disabling comments. */
14268
+ .dxr[data-chrome="full"] .dxr-surface[data-comments="on"]:has(> .docx-comment-gutter:not([data-empty]):not([hidden])) {
14269
+ padding-right: calc(var(--dxr-gutter) + 8px);
14270
+ }
13829
14271
  .dxr-surface [contenteditable="true"]:focus {
13830
14272
  outline: 2px solid var(--dxr-accent);
13831
14273
  outline-offset: 2px;
@@ -15976,11 +16418,18 @@ span.comment-highlight.docx-comment-active { background: hsl(var(--docx-comment-
15976
16418
  if (!this.live || !text.value) return;
15977
16419
  let count = 0;
15978
16420
  this.run("replace all", () => {
15979
- count = this.live.replaceAll(text.value, replace.value, { matchCase: matchCase.checked });
16421
+ count = this.live.replaceAll(text.value, replace.value, {
16422
+ matchCase: matchCase.checked,
16423
+ focus: false
16424
+ });
15980
16425
  });
15981
16426
  this.setStatus(`Replaced ${count} occurrence${count === 1 ? "" : "s"}`);
15982
16427
  this.refreshFind(true);
15983
16428
  });
16429
+ for (const id of ["findprev", "findnext", "replaceone", "replaceall"]) {
16430
+ const button = this.control(id);
16431
+ if (button) this.keepSelection(button);
16432
+ }
15984
16433
  bar.hidden = true;
15985
16434
  }
15986
16435
  openFindBar(withReplace) {
@@ -15991,28 +16440,44 @@ span.comment-highlight.docx-comment-active { background: hsl(var(--docx-comment-
15991
16440
  const text = this.require("findtext");
15992
16441
  text.focus();
15993
16442
  text.select();
15994
- this.refreshFind(false);
16443
+ this.refreshFind(true);
15995
16444
  }
16445
+ /**
16446
+ * Close the bar and hand the current hit over to the caret: the document is where the user is
16447
+ * going next, and Word leaves the insertion point on the match it stopped at. Until this runs
16448
+ * the match is only PAINTED (see `showFindMatches`), so the search field keeps the keyboard for
16449
+ * as long as the bar is open.
16450
+ */
15996
16451
  closeFindBar() {
15997
16452
  const bar = this.control("findbar");
15998
16453
  if (bar) bar.hidden = true;
16454
+ const match = this.findMatches[this.findIndex];
16455
+ if (match) this.live?.selectMatch(match);
16456
+ else this.live?.clearFindMatches();
15999
16457
  this.findMatches = [];
16000
16458
  this.findIndex = -1;
16001
16459
  }
16002
- refreshFind(select) {
16460
+ /**
16461
+ * Re-scan for the current query. `show` paints the hits and rides to the first one — it never
16462
+ * focuses the document, so typing the second character of a query that already matched goes on
16463
+ * refining the search instead of being typed into the document.
16464
+ */
16465
+ refreshFind(show) {
16003
16466
  const text = this.require("findtext");
16004
16467
  const matchCase = this.require("findcase").checked;
16005
16468
  this.findMatches = this.live && text.value ? this.live.find(text.value, { matchCase }) : [];
16006
16469
  this.findIndex = this.findMatches.length > 0 ? 0 : -1;
16007
16470
  this.updateFindCount();
16008
- if (select && this.findIndex >= 0) this.live?.selectMatch(this.findMatches[this.findIndex]);
16471
+ if (!show) return;
16472
+ if (this.findIndex >= 0) this.live?.showFindMatches(this.findMatches, this.findIndex);
16473
+ else this.live?.clearFindMatches();
16009
16474
  }
16010
16475
  stepFind(direction) {
16011
16476
  if (!this.live) return;
16012
16477
  if (this.findMatches.length === 0) this.refreshFind(false);
16013
16478
  if (this.findMatches.length === 0) return;
16014
16479
  this.findIndex = (this.findIndex + direction + this.findMatches.length) % this.findMatches.length;
16015
- this.live.selectMatch(this.findMatches[this.findIndex]);
16480
+ this.live.showFindMatches(this.findMatches, this.findIndex);
16016
16481
  this.updateFindCount();
16017
16482
  }
16018
16483
  replaceCurrent() {
@@ -16021,12 +16486,12 @@ span.comment-highlight.docx-comment-active { background: hsl(var(--docx-comment-
16021
16486
  const match = this.findMatches[this.findIndex];
16022
16487
  if (!match) return;
16023
16488
  const replacement = this.require("replacetext").value;
16024
- this.run("replace", () => this.live.replaceMatch(match, replacement));
16489
+ this.run("replace", () => this.live.replaceMatch(match, replacement, { focus: false }));
16025
16490
  const keep = this.findIndex;
16026
16491
  this.refreshFind(false);
16027
16492
  if (this.findMatches.length > 0) {
16028
16493
  this.findIndex = Math.min(keep, this.findMatches.length - 1);
16029
- this.live.selectMatch(this.findMatches[this.findIndex]);
16494
+ this.live.showFindMatches(this.findMatches, this.findIndex);
16030
16495
  this.updateFindCount();
16031
16496
  }
16032
16497
  }
@@ -16458,6 +16923,16 @@ span.comment-highlight.docx-comment-active { background: hsl(var(--docx-comment-
16458
16923
 
16459
16924
  // src/index.ts
16460
16925
  var import_meta = {};
16926
+ function openDocxHistory(storage) {
16927
+ const bridge = ensureInitialized().HistoryBridge;
16928
+ if (!bridge) throw new Error("This WASM build does not include history bindings.");
16929
+ return new DocxHistoryClient(bridge, storage);
16930
+ }
16931
+ function openDocxHistoryArchive(bytes) {
16932
+ const bridge = ensureInitialized().HistoryBridge;
16933
+ if (!bridge) throw new Error("This WASM build does not include history bindings.");
16934
+ return DocxHistoryArchive.open(bridge, bytes);
16935
+ }
16461
16936
  function openDocxSession2(bytes, settings) {
16462
16937
  const wasm = ensureInitialized();
16463
16938
  return openDocxSession(bytes, wasm, settings);
@@ -16512,14 +16987,16 @@ span.comment-highlight.docx-comment-active { background: hsl(var(--docx-comment-
16512
16987
  /* @vite-ignore */
16513
16988
  dotnetPath
16514
16989
  );
16515
- const { getAssemblyExports, getConfig } = await dotnet.withDiagnosticTracing(false).create();
16990
+ const { getAssemblyExports, getConfig, setModuleImports } = await dotnet.withDiagnosticTracing(false).create();
16991
+ installHistoryStorageImports(setModuleImports);
16516
16992
  const config = getConfig();
16517
16993
  const exports = await getAssemblyExports(config.mainAssemblyName);
16518
16994
  wasmExports = {
16519
16995
  DocumentConverter: exports.DocxodusWasm.DocumentConverter,
16520
16996
  DocumentComparer: exports.DocxodusWasm.DocumentComparer,
16521
16997
  DocxDiffBridge: exports.DocxodusWasm.DocxDiffBridge,
16522
- DocxSessionBridge: exports.DocxodusWasm.DocxSessionBridge
16998
+ DocxSessionBridge: exports.DocxodusWasm.DocxSessionBridge,
16999
+ HistoryBridge: exports.DocxodusWasm.HistoryBridge
16523
17000
  };
16524
17001
  return true;
16525
17002
  } catch {
@@ -17894,6 +18371,20 @@ ${parsed.documentElement.outerHTML}`;
17894
18371
  }
17895
18372
  throw new Error("Docxodus embed: unsupported document source");
17896
18373
  }
18374
+ function scopedDocumentFragment(mount2, html) {
18375
+ const parsed = new DOMParser().parseFromString(html, "text/html");
18376
+ const fragment = document.createDocumentFragment();
18377
+ parsed.querySelectorAll("style").forEach((sourceStyle) => {
18378
+ const style = document.createElement("style");
18379
+ style.textContent = sourceStyle.textContent;
18380
+ scopeStyleElement(style, mount2.selector);
18381
+ fragment.appendChild(style);
18382
+ });
18383
+ const body = document.createElement("template");
18384
+ body.innerHTML = parsed.body.innerHTML;
18385
+ fragment.appendChild(body.content);
18386
+ return fragment;
18387
+ }
17897
18388
  async function createViewer(container, source, options = {}) {
17898
18389
  const el = resolveContainer2(container);
17899
18390
  const { wasmBasePath: wasmBasePath2, ...conversion } = options;
@@ -17906,18 +18397,7 @@ ${parsed.documentElement.outerHTML}`;
17906
18397
  renderFootnotesAndEndnotes: true,
17907
18398
  ...opts
17908
18399
  });
17909
- const parsed = new DOMParser().parseFromString(lastHtml, "text/html");
17910
- const fragment = document.createDocumentFragment();
17911
- parsed.querySelectorAll("style").forEach((sourceStyle) => {
17912
- const style = document.createElement("style");
17913
- style.textContent = sourceStyle.textContent;
17914
- scopeStyleElement(style, mount2.selector);
17915
- fragment.appendChild(style);
17916
- });
17917
- const body = document.createElement("template");
17918
- body.innerHTML = parsed.body.innerHTML;
17919
- fragment.appendChild(body.content);
17920
- mount2.root.replaceChildren(fragment);
18400
+ mount2.root.replaceChildren(scopedDocumentFragment(mount2, lastHtml));
17921
18401
  };
17922
18402
  await render(source, conversion);
17923
18403
  return {
@@ -17931,6 +18411,111 @@ ${parsed.documentElement.outerHTML}`;
17931
18411
  }
17932
18412
  };
17933
18413
  }
18414
+ async function createHistoryViewer(container, history, documentId, options = {}) {
18415
+ const el = resolveContainer2(container);
18416
+ const { wasmBasePath: wasmBasePath2, maxEntriesToScan = 1e4, ...conversion } = options;
18417
+ if (!Number.isSafeInteger(maxEntriesToScan) || maxEntriesToScan <= 0 || maxEntriesToScan > 2147483647)
18418
+ throw new RangeError("Invalid history scan budget.");
18419
+ await ensureWasm(wasmBasePath2);
18420
+ const mount2 = createScopedMount(el);
18421
+ let accepted = null;
18422
+ let acceptedBytes = null;
18423
+ let displayedBytes = null;
18424
+ let shownSequence = "0";
18425
+ let following = true;
18426
+ let closed = false;
18427
+ let html = "";
18428
+ let queue = Promise.resolve();
18429
+ const clone = (value) => JSON.parse(JSON.stringify(value));
18430
+ const assertOpen = () => {
18431
+ if (closed) throw new DocxHistoryError("Closed", "History viewer is destroyed.");
18432
+ };
18433
+ const enqueue = (action) => {
18434
+ const result = queue.then(() => {
18435
+ assertOpen();
18436
+ return action();
18437
+ });
18438
+ queue = result.then(() => void 0, () => void 0);
18439
+ return result;
18440
+ };
18441
+ const sameHead = (a, b) => a.revision === b.revision && a.state.length === b.state.length && a.state.digest.algorithm === b.state.digest.algorithm && a.state.digest.value === b.state.digest.value;
18442
+ const paint = async (bytes, sequence) => {
18443
+ const nextHtml = await convertDocxToHtml(bytes, { renderFootnotesAndEndnotes: true, ...conversion });
18444
+ assertOpen();
18445
+ const fragment = scopedDocumentFragment(mount2, nextHtml);
18446
+ mount2.root.replaceChildren(fragment);
18447
+ html = nextHtml;
18448
+ displayedBytes = bytes;
18449
+ shownSequence = sequence;
18450
+ };
18451
+ const refresh = async (resume = false) => {
18452
+ const update = await history.readChangesSince(documentId, accepted?.view.head ?? null, maxEntriesToScan);
18453
+ assertOpen();
18454
+ const duplicate = accepted !== null && sameHead(accepted.view.head, update.view.head);
18455
+ const bytes = duplicate ? acceptedBytes : await history.exportVersion(documentId, update.view.version.id);
18456
+ assertOpen();
18457
+ const wantLive = following || resume;
18458
+ const changedContent = accepted === null || accepted.view.state.snapshot.contentDigest.value !== update.view.state.snapshot.contentDigest.value || accepted.view.state.epoch !== update.view.state.epoch;
18459
+ if (wantLive && (changedContent || !following)) await paint(bytes, update.view.state.sequence);
18460
+ else if (wantLive) {
18461
+ displayedBytes = bytes;
18462
+ shownSequence = update.view.state.sequence;
18463
+ }
18464
+ assertOpen();
18465
+ accepted = update;
18466
+ acceptedBytes = bytes;
18467
+ if (wantLive) following = true;
18468
+ return clone(update);
18469
+ };
18470
+ const showSequence = async (sequence) => {
18471
+ const bytes = await history.materialize(documentId, sequence, maxEntriesToScan);
18472
+ assertOpen();
18473
+ await paint(bytes, sequence);
18474
+ following = false;
18475
+ };
18476
+ const viewer = {
18477
+ element: el,
18478
+ get html() {
18479
+ return html;
18480
+ },
18481
+ get head() {
18482
+ assertOpen();
18483
+ return clone(accepted.view.head);
18484
+ },
18485
+ get sequence() {
18486
+ return shownSequence;
18487
+ },
18488
+ get following() {
18489
+ return following;
18490
+ },
18491
+ refresh: () => enqueue(() => refresh()),
18492
+ showSequence: (sequence) => enqueue(() => showSequence(sequence)),
18493
+ showTime: (cutoff) => enqueue(async () => {
18494
+ const sequence = await history.resolveSequenceAtTime(documentId, cutoff, maxEntriesToScan);
18495
+ assertOpen();
18496
+ await showSequence(sequence);
18497
+ }),
18498
+ resume: () => enqueue(() => refresh(true)),
18499
+ exportDisplayed: () => {
18500
+ assertOpen();
18501
+ return displayedBytes.slice();
18502
+ },
18503
+ destroy: () => {
18504
+ if (closed) return;
18505
+ closed = true;
18506
+ acceptedBytes = null;
18507
+ displayedBytes = null;
18508
+ mount2.root.remove();
18509
+ }
18510
+ };
18511
+ try {
18512
+ await viewer.refresh();
18513
+ return viewer;
18514
+ } catch (error) {
18515
+ viewer.destroy();
18516
+ throw error;
18517
+ }
18518
+ }
17934
18519
  async function createEditor(container, source, options = {}) {
17935
18520
  const el = resolveContainer2(container);
17936
18521
  const { wasmBasePath: wasmBasePath2, ...editorOptions } = options;