docxodus 12.1.0 → 12.3.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 (87) hide show
  1. package/README.md +14 -4
  2. package/dist/editor.bundle.js +137 -23
  3. package/dist/editor.d.ts +25 -3
  4. package/dist/editor.d.ts.map +1 -1
  5. package/dist/editor.js +132 -13
  6. package/dist/editor.js.map +1 -1
  7. package/dist/embed.bundle.js +1163 -48
  8. package/dist/embed.d.ts +30 -1
  9. package/dist/embed.d.ts.map +1 -1
  10. package/dist/embed.iife.js +1163 -48
  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-checkpoints.d.ts +48 -0
  15. package/dist/history-checkpoints.d.ts.map +1 -0
  16. package/dist/history-checkpoints.js +100 -0
  17. package/dist/history-checkpoints.js.map +1 -0
  18. package/dist/history-controls.d.ts +30 -0
  19. package/dist/history-controls.d.ts.map +1 -0
  20. package/dist/history-controls.js +333 -0
  21. package/dist/history-controls.js.map +1 -0
  22. package/dist/history-indexeddb.d.ts +12 -0
  23. package/dist/history-indexeddb.d.ts.map +1 -0
  24. package/dist/history-indexeddb.js +154 -0
  25. package/dist/history-indexeddb.js.map +1 -0
  26. package/dist/history.d.ts +265 -0
  27. package/dist/history.d.ts.map +1 -0
  28. package/dist/history.js +307 -0
  29. package/dist/history.js.map +1 -0
  30. package/dist/index.d.ts +10 -0
  31. package/dist/index.d.ts.map +1 -1
  32. package/dist/index.js +23 -2
  33. package/dist/index.js.map +1 -1
  34. package/dist/ribbon-chrome.d.ts +2 -2
  35. package/dist/ribbon-chrome.d.ts.map +1 -1
  36. package/dist/ribbon-chrome.js +6 -4
  37. package/dist/ribbon-chrome.js.map +1 -1
  38. package/dist/ribbon.js +42 -9
  39. package/dist/ribbon.js.map +1 -1
  40. package/dist/session.js +1 -1
  41. package/dist/session.js.map +1 -1
  42. package/dist/types.d.ts +2 -0
  43. package/dist/types.d.ts.map +1 -1
  44. package/dist/types.js.map +1 -1
  45. package/dist/wasm/_framework/DocumentFormat.OpenXml.Framework.wasm +0 -0
  46. package/dist/wasm/_framework/DocumentFormat.OpenXml.Framework.wasm.br +0 -0
  47. package/dist/wasm/_framework/DocumentFormat.OpenXml.wasm +0 -0
  48. package/dist/wasm/_framework/DocumentFormat.OpenXml.wasm.br +0 -0
  49. package/dist/wasm/_framework/Docxodus.wasm +0 -0
  50. package/dist/wasm/_framework/Docxodus.wasm.br +0 -0
  51. package/dist/wasm/_framework/DocxodusWasm.wasm +0 -0
  52. package/dist/wasm/_framework/DocxodusWasm.wasm.br +0 -0
  53. package/dist/wasm/_framework/System.Collections.Concurrent.wasm +0 -0
  54. package/dist/wasm/_framework/System.Collections.Concurrent.wasm.br +0 -0
  55. package/dist/wasm/_framework/System.Collections.wasm +0 -0
  56. package/dist/wasm/_framework/System.Collections.wasm.br +0 -0
  57. package/dist/wasm/_framework/System.IO.Compression.wasm +0 -0
  58. package/dist/wasm/_framework/System.IO.Compression.wasm.br +0 -0
  59. package/dist/wasm/_framework/System.IO.Packaging.wasm +0 -0
  60. package/dist/wasm/_framework/System.IO.Packaging.wasm.br +0 -0
  61. package/dist/wasm/_framework/System.Linq.Expressions.wasm +0 -0
  62. package/dist/wasm/_framework/System.Linq.Expressions.wasm.br +0 -0
  63. package/dist/wasm/_framework/System.Memory.wasm +0 -0
  64. package/dist/wasm/_framework/System.Memory.wasm.br +0 -0
  65. package/dist/wasm/_framework/System.Net.Http.wasm +0 -0
  66. package/dist/wasm/_framework/System.Net.Http.wasm.br +0 -0
  67. package/dist/wasm/_framework/System.Private.CoreLib.wasm +0 -0
  68. package/dist/wasm/_framework/System.Private.CoreLib.wasm.br +0 -0
  69. package/dist/wasm/_framework/System.Private.Xml.Linq.wasm +0 -0
  70. package/dist/wasm/_framework/System.Private.Xml.Linq.wasm.br +0 -0
  71. package/dist/wasm/_framework/System.Private.Xml.wasm +0 -0
  72. package/dist/wasm/_framework/System.Private.Xml.wasm.br +0 -0
  73. package/dist/wasm/_framework/System.Runtime.InteropServices.JavaScript.wasm +0 -0
  74. package/dist/wasm/_framework/System.Runtime.InteropServices.JavaScript.wasm.br +0 -0
  75. package/dist/wasm/_framework/System.Runtime.wasm +0 -0
  76. package/dist/wasm/_framework/System.Runtime.wasm.br +0 -0
  77. package/dist/wasm/_framework/System.Security.Cryptography.wasm +0 -0
  78. package/dist/wasm/_framework/System.Security.Cryptography.wasm.br +0 -0
  79. package/dist/wasm/_framework/System.Text.Json.wasm +0 -0
  80. package/dist/wasm/_framework/System.Text.Json.wasm.br +0 -0
  81. package/dist/wasm/_framework/System.Text.RegularExpressions.wasm +0 -0
  82. package/dist/wasm/_framework/System.Text.RegularExpressions.wasm.br +0 -0
  83. package/dist/wasm/_framework/dotnet.boot.js +21 -21
  84. package/dist/wasm/_framework/dotnet.boot.js.br +0 -0
  85. package/dist/wasm/_framework/dotnet.native.wasm +0 -0
  86. package/dist/wasm/_framework/dotnet.native.wasm.br +0 -0
  87. package/package.json +6 -3
@@ -141,8 +141,15 @@ 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
+ HistoryCheckpoints: () => HistoryCheckpoints,
152
+ MAX_HISTORY_ARCHIVE_BYTES: () => MAX_HISTORY_ARCHIVE_BYTES,
146
153
  PaginationEngine: () => PaginationEngine,
147
154
  PaginationMode: () => PaginationMode,
148
155
  PlaceholderKinds: () => PlaceholderKinds,
@@ -166,6 +173,8 @@ var Docxodus = (() => {
166
173
  createBlankDocx: () => createBlankDocx,
167
174
  createEditor: () => createEditor,
168
175
  createExternalAnnotationSet: () => createExternalAnnotationSet,
176
+ createHistoryViewer: () => createHistoryViewer,
177
+ createMemoryHistoryStorage: () => createMemoryHistoryStorage,
169
178
  createRibbonEditor: () => createRibbonEditor,
170
179
  createUnavailablePageMap: () => createUnavailablePageMap,
171
180
  createViewer: () => createViewer,
@@ -199,15 +208,21 @@ var Docxodus = (() => {
199
208
  getVersion: () => getVersion,
200
209
  getWasmExports: () => getWasmExports,
201
210
  hasAnnotations: () => hasAnnotations,
211
+ historyControlError: () => historyControlError,
202
212
  initialize: () => initialize,
213
+ installHistoryStorageImports: () => installHistoryStorageImports,
203
214
  isDeletion: () => isDeletion,
204
215
  isFormatChange: () => isFormatChange,
205
216
  isInitialized: () => isInitialized,
206
217
  isInsertion: () => isInsertion,
207
218
  isMove: () => isMove,
219
+ mountHistoryControls: () => mountHistoryControls,
208
220
  mountRibbon: () => mountRibbon,
209
221
  navigateToPageCitation: () => navigateToPageCitation,
222
+ openDocxHistory: () => openDocxHistory,
223
+ openDocxHistoryArchive: () => openDocxHistoryArchive,
210
224
  openDocxSession: () => openDocxSession2,
225
+ openIndexedDbHistoryStore: () => openIndexedDbHistoryStore,
211
226
  paginateHtml: () => paginateHtml,
212
227
  parseSectionDimensions: () => parseSectionDimensions,
213
228
  projectAnnotationsOntoHtml: () => projectAnnotationsOntoHtml,
@@ -2161,6 +2176,872 @@ var Docxodus = (() => {
2161
2176
  return new DocxSession(handle, bridge);
2162
2177
  }
2163
2178
 
2179
+ // src/history.ts
2180
+ var MAX_HISTORY_ARCHIVE_BYTES = 64 * 1024 * 1024;
2181
+ var DocxHistoryError = class extends Error {
2182
+ constructor(code, message) {
2183
+ super(message);
2184
+ this.code = code;
2185
+ this.name = "DocxHistoryError";
2186
+ }
2187
+ };
2188
+ var adapters = /* @__PURE__ */ new Map();
2189
+ var nextAdapter = 0;
2190
+ function adapter(id) {
2191
+ const store = adapters.get(id);
2192
+ if (!store) throw new DocxHistoryError("Closed", "History storage adapter is closed.");
2193
+ return store;
2194
+ }
2195
+ function toBase64(bytes) {
2196
+ let binary = "";
2197
+ for (let offset = 0; offset < bytes.length; offset += 32768)
2198
+ binary += String.fromCharCode(...bytes.subarray(offset, offset + 32768));
2199
+ return btoa(binary);
2200
+ }
2201
+ function fromBase64(encoded) {
2202
+ const binary = atob(encoded);
2203
+ return Uint8Array.from(binary, (c) => c.charCodeAt(0));
2204
+ }
2205
+ function installHistoryStorageImports(setModuleImports) {
2206
+ setModuleImports("docxodus.history", {
2207
+ readBlob: async (id, json) => {
2208
+ const reference = JSON.parse(json);
2209
+ const bytes = await adapter(id).readBlob(reference);
2210
+ if (bytes === null) return "";
2211
+ if (bytes.length !== reference.length) throw new DocxHistoryError("PayloadMismatch", "Stored blob length differs from its reference.");
2212
+ return bytes.length === 0 ? "=" : toBase64(bytes);
2213
+ },
2214
+ putBlob: async (id, json, bytes) => adapter(id).putBlob(JSON.parse(json), bytes.slice()),
2215
+ readHead: async (id, documentId) => JSON.stringify(await adapter(id).readHead(documentId)),
2216
+ advanceHead: async (id, documentId, expected, state) => JSON.stringify(await adapter(id).advanceHead(documentId, JSON.parse(expected), JSON.parse(state))),
2217
+ initializeHead: async (id, documentId, head) => {
2218
+ const storage = adapter(id);
2219
+ if (!storage.initializeHead) throw new DocxHistoryError("InitializationUnsupported", "Storage cannot import an exact head.");
2220
+ return JSON.stringify(await storage.initializeHead(documentId, JSON.parse(head)));
2221
+ }
2222
+ });
2223
+ }
2224
+ var DocxHistoryClient = class {
2225
+ constructor(bridge, storage) {
2226
+ this.bridge = bridge;
2227
+ this.closed = false;
2228
+ if (nextAdapter >= 2147483647) throw new Error("History adapter identifiers exhausted.");
2229
+ this.adapterId = ++nextAdapter;
2230
+ adapters.set(this.adapterId, storage);
2231
+ try {
2232
+ this.handle = storage.initializeHead && bridge.OpenWithInitialization ? bridge.OpenWithInitialization(this.adapterId) : bridge.Open(this.adapterId);
2233
+ } catch (error) {
2234
+ adapters.delete(this.adapterId);
2235
+ throw error;
2236
+ }
2237
+ }
2238
+ close() {
2239
+ if (this.closed) return;
2240
+ this.bridge.Close(this.handle);
2241
+ adapters.delete(this.adapterId);
2242
+ this.closed = true;
2243
+ }
2244
+ /** Bind an identity without reading, creating a version, or taking ownership of this client. */
2245
+ document(documentId) {
2246
+ return new DocxHistoryDocument(documentId, (operation, fields, bytes) => this.call(operation, documentId, fields, bytes));
2247
+ }
2248
+ async exportHistoryArchive(documentId) {
2249
+ return fromBase64((await this.call("exportArchive", documentId)).bytes);
2250
+ }
2251
+ async importHistoryArchive(bytes) {
2252
+ checkArchiveSize(bytes);
2253
+ return (await this.call("importArchive", "", {}, bytes)).import;
2254
+ }
2255
+ async read(documentId) {
2256
+ return (await this.call("read", documentId)).view;
2257
+ }
2258
+ /** Host-triggered validated metadata tail; first join starts at the latest checkpoint. */
2259
+ async readChangesSince(documentId, after, maxEntriesToScan = 1e4) {
2260
+ return (await this.call("updates", documentId, { expectedHead: after, maxEntriesToScan })).update;
2261
+ }
2262
+ async readOperationsSince(documentId, after, maxEntriesToScan = 1e4) {
2263
+ return (await this.call("operations", documentId, { expectedHead: after, maxEntriesToScan })).operationUpdate;
2264
+ }
2265
+ async getOperation(documentId, operationId) {
2266
+ return (await this.call("getOperation", documentId, { operationId })).operation;
2267
+ }
2268
+ async exportOperationProposal(documentId, operationId) {
2269
+ return fromBase64((await this.call("exportOperationProposal", documentId, { operationId })).bytes);
2270
+ }
2271
+ async compareVersions(documentId, beforeVersionId, afterVersionId) {
2272
+ return fromBase64((await this.call("compare", documentId, { beforeVersionId, afterVersionId })).bytes);
2273
+ }
2274
+ async createVersion(documentId, expectedHead, bytes, metadata, requestId) {
2275
+ return (await this.call("create", documentId, { expectedHead, metadata, requestId }, bytes)).view;
2276
+ }
2277
+ async listVersions(documentId, cursor = null, limit = 25) {
2278
+ return (await this.call("list", documentId, { versionId: cursor, limit })).page;
2279
+ }
2280
+ async getVersion(documentId, versionId) {
2281
+ return (await this.call("get", documentId, { versionId })).version;
2282
+ }
2283
+ async exportVersion(documentId, versionId) {
2284
+ return fromBase64((await this.call("export", documentId, { versionId })).bytes);
2285
+ }
2286
+ async materialize(documentId, sequence, maxEntriesToScan = 1e4) {
2287
+ return fromBase64((await this.call("materialize", documentId, { sequence, maxEntriesToScan })).bytes);
2288
+ }
2289
+ async replay(documentId, sequence, maxEntriesToScan = 1e4) {
2290
+ return fromBase64((await this.call("replay", documentId, { sequence, maxEntriesToScan })).bytes);
2291
+ }
2292
+ async resolveSequenceAtTime(documentId, cutoff, maxEntriesToScan = 1e4) {
2293
+ return (await this.call("resolveTime", documentId, { cutoff, maxEntriesToScan })).sequence;
2294
+ }
2295
+ async restoreVersion(documentId, expectedHead, versionId, metadata, requestId) {
2296
+ return (await this.call("restore", documentId, { expectedHead, versionId, metadata, requestId })).view;
2297
+ }
2298
+ async call(operation, documentId, fields = {}, bytes = new Uint8Array()) {
2299
+ if (this.closed) throw new DocxHistoryError("Closed", "History client is closed.");
2300
+ return parseResult(await this.bridge.Invoke(
2301
+ this.handle,
2302
+ JSON.stringify({ schemaVersion: 1, operation, documentId, ...fields }),
2303
+ bytes
2304
+ ));
2305
+ }
2306
+ };
2307
+ var DocxHistoryReader = class {
2308
+ /** @internal Obtain through client.document() or openDocxHistoryArchive(). */
2309
+ constructor(documentId, call) {
2310
+ this.call = call;
2311
+ this.#documentId = documentId;
2312
+ }
2313
+ #documentId;
2314
+ get documentId() {
2315
+ return this.#documentId;
2316
+ }
2317
+ async read() {
2318
+ return (await this.call("read")).view;
2319
+ }
2320
+ async listVersions(cursor = null, limit = 25) {
2321
+ return (await this.call("list", { versionId: cursor, limit })).page;
2322
+ }
2323
+ async getVersion(versionId) {
2324
+ return (await this.call("get", { versionId })).version;
2325
+ }
2326
+ /** Omit versionId for latest; pass an ID to keep a UI operation pinned to a captured version. */
2327
+ async exportDocx(versionId = null) {
2328
+ return fromBase64((await this.call("exportDocx", { versionId })).bytes);
2329
+ }
2330
+ async exportHistoryArchive() {
2331
+ return fromBase64((await this.call("exportArchive")).bytes);
2332
+ }
2333
+ async materialize(sequence, maxEntriesToScan = 1e4) {
2334
+ return fromBase64((await this.call("materialize", { sequence, maxEntriesToScan })).bytes);
2335
+ }
2336
+ async replay(sequence, maxEntriesToScan = 1e4) {
2337
+ return fromBase64((await this.call("replay", { sequence, maxEntriesToScan })).bytes);
2338
+ }
2339
+ async resolveSequenceAtTime(cutoff, maxEntriesToScan = 1e4) {
2340
+ return (await this.call("resolveTime", { cutoff, maxEntriesToScan })).sequence;
2341
+ }
2342
+ async readChangesSince(after, maxEntriesToScan = 1e4) {
2343
+ return (await this.call("updates", { expectedHead: after, maxEntriesToScan })).update;
2344
+ }
2345
+ async readOperationsSince(after, maxEntriesToScan = 1e4) {
2346
+ return (await this.call("operations", { expectedHead: after, maxEntriesToScan })).operationUpdate;
2347
+ }
2348
+ async getOperation(operationId) {
2349
+ return (await this.call("getOperation", { operationId })).operation;
2350
+ }
2351
+ async exportOperationProposal(operationId) {
2352
+ return fromBase64((await this.call("exportOperationProposal", { operationId })).bytes);
2353
+ }
2354
+ /** Redlined DOCX through DocxCompare's existing accepted-input revision policy. */
2355
+ async compareVersions(beforeVersionId, afterVersionId) {
2356
+ return fromBase64((await this.call("compare", { beforeVersionId, afterVersionId })).bytes);
2357
+ }
2358
+ };
2359
+ var DocxHistoryDocument = class extends DocxHistoryReader {
2360
+ async createVersion(expectedHead, bytes, metadata, requestId) {
2361
+ requireRequestId(requestId);
2362
+ return (await this.call("create", { expectedHead, metadata, requestId }, bytes)).view;
2363
+ }
2364
+ async restoreVersion(expectedHead, versionId, metadata, requestId) {
2365
+ requireRequestId(requestId);
2366
+ return (await this.call("restore", { expectedHead, versionId, metadata, requestId })).view;
2367
+ }
2368
+ };
2369
+ var DocxHistoryArchive = class _DocxHistoryArchive extends DocxHistoryReader {
2370
+ constructor(bridge, handle, info) {
2371
+ super(info.documentId, async (operation, fields = {}, bytes = new Uint8Array()) => {
2372
+ if (this.closed) throw new DocxHistoryError("Closed", "History archive is closed.");
2373
+ return parseResult(await bridge.Invoke(
2374
+ handle,
2375
+ JSON.stringify({ schemaVersion: 1, operation, documentId: this.documentId, ...fields }),
2376
+ bytes
2377
+ ));
2378
+ });
2379
+ this.bridge = bridge;
2380
+ this.handle = handle;
2381
+ this.info = info;
2382
+ this.closed = false;
2383
+ }
2384
+ /** For custom loaders; normal callers use openDocxHistoryArchive(). */
2385
+ static async open(bridge, bytes) {
2386
+ checkArchiveSize(bytes);
2387
+ if (!bridge.OpenArchive) throw new Error("This WASM build does not include portable history.");
2388
+ const result = parseResult(await bridge.OpenArchive(bytes));
2389
+ return new _DocxHistoryArchive(bridge, result.handle, result.archive);
2390
+ }
2391
+ close() {
2392
+ if (this.closed) return;
2393
+ this.bridge.Close(this.handle);
2394
+ this.closed = true;
2395
+ }
2396
+ };
2397
+ function parseResult(json) {
2398
+ const result = JSON.parse(json);
2399
+ if (!result.success) throw new DocxHistoryError(result.errorCode, result.message);
2400
+ return result;
2401
+ }
2402
+ function checkArchiveSize(bytes) {
2403
+ if (bytes.length > MAX_HISTORY_ARCHIVE_BYTES) throw new DocxHistoryError("ResourceLimit", "History archive exceeds 64 MiB.");
2404
+ }
2405
+ function requireRequestId(requestId) {
2406
+ if (typeof requestId !== "string" || !requestId.trim()) throw new DocxHistoryError("InvalidRequest", "A durable requestId is required.");
2407
+ }
2408
+ function createMemoryHistoryStorage(maxBlobBytes = 256 * 1024 * 1024) {
2409
+ if (!Number.isSafeInteger(maxBlobBytes) || maxBlobBytes <= 0) throw new RangeError("Invalid blob byte limit.");
2410
+ const blobs = /* @__PURE__ */ new Map();
2411
+ const heads = /* @__PURE__ */ new Map();
2412
+ const copy = (value) => JSON.parse(JSON.stringify(value));
2413
+ const key = (reference) => {
2414
+ if (reference.digest.algorithm !== "SHA-256" || !/^[a-f0-9]{64}$/.test(reference.digest.value) || !Number.isSafeInteger(reference.length) || reference.length < 0 || reference.length > maxBlobBytes)
2415
+ throw new DocxHistoryError("InvalidRequest", "Invalid blob reference or byte limit.");
2416
+ return `${reference.digest.value}:${reference.length}`;
2417
+ };
2418
+ const equalReference = (a, b) => a.length === b.length && a.digest.algorithm === b.digest.algorithm && a.digest.value === b.digest.value;
2419
+ const equalHead2 = (a, b) => a === null || b === null ? a === b : a.revision === b.revision && equalReference(a.state, b.state);
2420
+ return {
2421
+ async readBlob(reference) {
2422
+ return blobs.get(key(reference))?.slice() ?? null;
2423
+ },
2424
+ async putBlob(reference, bytes) {
2425
+ const blobKey = key(reference);
2426
+ const captured = bytes.slice();
2427
+ if (captured.length !== reference.length) throw new DocxHistoryError("PayloadMismatch", "Blob length mismatch.");
2428
+ const digest = Array.from(
2429
+ new Uint8Array(await crypto.subtle.digest("SHA-256", captured)),
2430
+ (n) => n.toString(16).padStart(2, "0")
2431
+ ).join("");
2432
+ if (digest !== blobKey.split(":")[0]) throw new DocxHistoryError("PayloadMismatch", "Blob digest mismatch.");
2433
+ if (!blobs.has(blobKey)) blobs.set(blobKey, captured);
2434
+ },
2435
+ async readHead(documentId) {
2436
+ return copy(heads.get(documentId) ?? null);
2437
+ },
2438
+ async advanceHead(documentId, expected, state) {
2439
+ key(state);
2440
+ const current = heads.get(documentId) ?? null;
2441
+ if (!equalHead2(current, expected)) return null;
2442
+ const revision = BigInt(current?.revision ?? "0") + 1n;
2443
+ if (revision > 9223372036854775807n) throw new RangeError("History revision exhausted.");
2444
+ const head = { revision: String(revision), state: copy(state) };
2445
+ heads.set(documentId, head);
2446
+ return copy(head);
2447
+ },
2448
+ async initializeHead(documentId, head) {
2449
+ key(head.state);
2450
+ if (typeof head.revision !== "string" || !/^[1-9][0-9]*$/.test(head.revision) || BigInt(head.revision) > 9223372036854775807n)
2451
+ throw new DocxHistoryError("InvalidRequest", "Imported head revision must be a positive Int64 string.");
2452
+ const existing = heads.get(documentId);
2453
+ if (existing) return { initialized: false, head: copy(existing) };
2454
+ const captured = copy(head);
2455
+ heads.set(documentId, captured);
2456
+ return { initialized: true, head: copy(captured) };
2457
+ }
2458
+ };
2459
+ }
2460
+
2461
+ // src/history-checkpoints.ts
2462
+ var HistoryCheckpoints = class _HistoryCheckpoints {
2463
+ constructor(document2, journal) {
2464
+ this.document = document2;
2465
+ this.journal = journal;
2466
+ this.current = null;
2467
+ this.request = null;
2468
+ this.running = false;
2469
+ this.stale = false;
2470
+ }
2471
+ /** Pass an already captured view when opening its exact version in an editor (for example, after import). */
2472
+ static async open(document2, journal, view) {
2473
+ const controls = new _HistoryCheckpoints(document2, journal);
2474
+ controls.request = structuredClone(await journal.read());
2475
+ if (controls.request && controls.request.documentId !== document2.documentId)
2476
+ throw new DocxHistoryError("InvalidRequest", "The pending checkpoint belongs to another document.");
2477
+ controls.current = view === void 0 ? await document2.read() : structuredClone(view);
2478
+ if (controls.current && controls.current.state.documentId !== document2.documentId)
2479
+ throw new DocxHistoryError("InvalidRequest", "The captured view belongs to another document.");
2480
+ return controls;
2481
+ }
2482
+ get view() {
2483
+ return structuredClone(this.current);
2484
+ }
2485
+ get hasPending() {
2486
+ return this.request !== null;
2487
+ }
2488
+ get needsRefresh() {
2489
+ return this.stale;
2490
+ }
2491
+ async refresh() {
2492
+ return this.exclusive(async () => {
2493
+ this.current = await this.document.read();
2494
+ this.stale = false;
2495
+ return this.view;
2496
+ });
2497
+ }
2498
+ async save(bytes, metadata) {
2499
+ return this.start({
2500
+ kind: "save",
2501
+ head: this.current?.head ?? null,
2502
+ bytes,
2503
+ id: crypto.randomUUID(),
2504
+ documentId: this.document.documentId,
2505
+ metadata
2506
+ });
2507
+ }
2508
+ async restore(target, metadata) {
2509
+ if (!this.current) throw new DocxHistoryError("NotFound", "Save a checkpoint before restoring a version.");
2510
+ return this.start({
2511
+ kind: "restore",
2512
+ head: this.current.head,
2513
+ target,
2514
+ id: crypto.randomUUID(),
2515
+ documentId: this.document.documentId,
2516
+ metadata
2517
+ });
2518
+ }
2519
+ async retry() {
2520
+ return this.exclusive(async () => {
2521
+ if (!this.request) throw new DocxHistoryError("InvalidRequest", "There is no pending checkpoint.");
2522
+ return this.publish();
2523
+ });
2524
+ }
2525
+ async start(request) {
2526
+ return this.exclusive(async () => {
2527
+ if (this.request) throw new DocxHistoryError("PendingRequest", "Retry the pending checkpoint first.");
2528
+ if (this.stale) throw new DocxHistoryError("StaleHead", "Refresh history before saving again.");
2529
+ this.request = structuredClone(request);
2530
+ return this.publish();
2531
+ });
2532
+ }
2533
+ async publish() {
2534
+ const intended = this.request;
2535
+ const request = await this.journal.put(structuredClone(intended));
2536
+ this.request = structuredClone(request);
2537
+ if (request.id !== intended.id)
2538
+ throw new DocxHistoryError("PendingRequest", "Another tab has a pending checkpoint. Retry it before saving your draft.");
2539
+ let view;
2540
+ try {
2541
+ view = request.kind === "save" ? await this.document.createVersion(request.head, request.bytes, request.metadata, request.id) : await this.document.restoreVersion(request.head, request.target, request.metadata, request.id);
2542
+ } catch (error) {
2543
+ if (error instanceof DocxHistoryError && error.code === "StaleHead") {
2544
+ this.stale = true;
2545
+ await this.journal.remove(request.id);
2546
+ this.request = null;
2547
+ }
2548
+ throw error;
2549
+ }
2550
+ this.current = view;
2551
+ await this.journal.remove(request.id);
2552
+ this.request = null;
2553
+ return this.view;
2554
+ }
2555
+ async exclusive(action) {
2556
+ if (this.running) throw new DocxHistoryError("Busy", "A history command is still running.");
2557
+ this.running = true;
2558
+ try {
2559
+ return await action();
2560
+ } finally {
2561
+ this.running = false;
2562
+ }
2563
+ }
2564
+ };
2565
+
2566
+ // src/history-indexeddb.ts
2567
+ async function openIndexedDbHistoryStore(name) {
2568
+ const db = await new Promise((resolve, reject) => {
2569
+ let blocked = false;
2570
+ const request = indexedDB.open(name, 1);
2571
+ request.onupgradeneeded = () => {
2572
+ for (const store of ["blobs", "heads", "requests"]) request.result.createObjectStore(store);
2573
+ };
2574
+ request.onsuccess = () => {
2575
+ if (blocked) request.result.close();
2576
+ else resolve(request.result);
2577
+ };
2578
+ request.onerror = () => reject(request.error);
2579
+ request.onblocked = () => {
2580
+ blocked = true;
2581
+ reject(new Error("Close other tabs using this history store, then try again."));
2582
+ };
2583
+ });
2584
+ db.onversionchange = () => db.close();
2585
+ function transaction(name2, mode, action) {
2586
+ return new Promise((resolve, reject) => {
2587
+ const tx = db.transaction(name2, mode);
2588
+ let value;
2589
+ tx.oncomplete = () => resolve(value);
2590
+ tx.onabort = () => reject(tx.error ?? new Error("History storage transaction was aborted."));
2591
+ try {
2592
+ action(tx.objectStore(name2), (result) => {
2593
+ value = result;
2594
+ });
2595
+ } catch (error) {
2596
+ tx.abort();
2597
+ reject(error);
2598
+ }
2599
+ });
2600
+ }
2601
+ const storage = {
2602
+ readBlob(reference) {
2603
+ const key = referenceKey(reference);
2604
+ return transaction("blobs", "readonly", (store, result) => {
2605
+ store.get(key).onsuccess = (event) => result(event.target.result ?? null);
2606
+ });
2607
+ },
2608
+ async putBlob(reference, bytes) {
2609
+ const key = referenceKey(reference);
2610
+ const captured = bytes.slice();
2611
+ if (captured.length !== reference.length) throw new DocxHistoryError("PayloadMismatch", "Stored document length does not match its reference.");
2612
+ const hash = Array.from(
2613
+ new Uint8Array(await crypto.subtle.digest("SHA-256", captured)),
2614
+ (byte) => byte.toString(16).padStart(2, "0")
2615
+ ).join("");
2616
+ if (hash !== key.split(":")[0])
2617
+ throw new DocxHistoryError("PayloadMismatch", "Stored document bytes do not match their reference.");
2618
+ await transaction("blobs", "readwrite", (store, result) => {
2619
+ store.put(captured, key);
2620
+ result();
2621
+ });
2622
+ },
2623
+ readHead(documentId) {
2624
+ return transaction("heads", "readonly", (store, result) => {
2625
+ store.get(documentId).onsuccess = (event) => result(event.target.result ?? null);
2626
+ });
2627
+ },
2628
+ advanceHead(documentId, expected, state) {
2629
+ referenceKey(state);
2630
+ const captured = structuredClone({ expected, state });
2631
+ if (expected) validateHead(expected);
2632
+ const revision = BigInt(expected?.revision ?? "0") + 1n;
2633
+ if (revision > 9223372036854775807n) throw new RangeError("History revision exhausted.");
2634
+ return transaction("heads", "readwrite", (store, result) => {
2635
+ store.get(documentId).onsuccess = (event) => {
2636
+ const current = event.target.result ?? null;
2637
+ if (!equalHead(current, captured.expected)) {
2638
+ result(null);
2639
+ return;
2640
+ }
2641
+ const head = { revision: String(revision), state: captured.state };
2642
+ store.put(head, documentId);
2643
+ result(head);
2644
+ };
2645
+ });
2646
+ },
2647
+ initializeHead(documentId, head) {
2648
+ validateHead(head);
2649
+ const captured = structuredClone(head);
2650
+ return transaction("heads", "readwrite", (store, result) => {
2651
+ store.get(documentId).onsuccess = (event) => {
2652
+ const existing = event.target.result;
2653
+ if (existing) {
2654
+ result({ initialized: false, head: existing });
2655
+ return;
2656
+ }
2657
+ store.put(captured, documentId);
2658
+ result({ initialized: true, head: captured });
2659
+ };
2660
+ });
2661
+ }
2662
+ };
2663
+ return {
2664
+ storage,
2665
+ journal(documentId) {
2666
+ return {
2667
+ read: () => transaction("requests", "readonly", (store, result) => {
2668
+ store.get(documentId).onsuccess = (event) => result(event.target.result ?? null);
2669
+ }),
2670
+ put(request) {
2671
+ if (request.documentId !== documentId) throw new Error("Checkpoint document identity does not match.");
2672
+ const captured = structuredClone(request);
2673
+ return transaction("requests", "readwrite", (store, result) => {
2674
+ store.get(documentId).onsuccess = (event) => {
2675
+ const existing = event.target.result;
2676
+ if (existing) {
2677
+ result(existing);
2678
+ return;
2679
+ }
2680
+ store.put(captured, documentId);
2681
+ result(captured);
2682
+ };
2683
+ });
2684
+ },
2685
+ remove(requestId) {
2686
+ return transaction("requests", "readwrite", (store, result) => {
2687
+ store.get(documentId).onsuccess = (event) => {
2688
+ if (event.target.result?.id === requestId) store.delete(documentId);
2689
+ result();
2690
+ };
2691
+ });
2692
+ }
2693
+ };
2694
+ },
2695
+ close: () => db.close()
2696
+ };
2697
+ }
2698
+ function referenceKey(reference) {
2699
+ if (reference.digest.algorithm !== "SHA-256" || !/^[a-f0-9]{64}$/.test(reference.digest.value) || !Number.isSafeInteger(reference.length) || reference.length < 0)
2700
+ throw new DocxHistoryError("InvalidRequest", "Invalid history blob reference.");
2701
+ return `${reference.digest.value}:${reference.length}`;
2702
+ }
2703
+ function validateHead(head) {
2704
+ referenceKey(head.state);
2705
+ if (typeof head.revision !== "string" || !/^[1-9][0-9]*$/.test(head.revision) || BigInt(head.revision) > 9223372036854775807n)
2706
+ throw new DocxHistoryError("InvalidRequest", "Invalid history revision.");
2707
+ }
2708
+ function equalHead(a, b) {
2709
+ return a === null || b === null ? 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;
2710
+ }
2711
+
2712
+ // src/history-controls.ts
2713
+ function mountHistoryControls(container, options) {
2714
+ const pageSize = options.pageSize ?? 25;
2715
+ if (!Number.isInteger(pageSize) || pageSize < 1 || pageSize > 100) throw new RangeError("History page size must be 1\u2013100.");
2716
+ if (options.checkpoints && options.checkpoints.document !== options.reader)
2717
+ throw new Error("History controls and checkpoints must use the same document.");
2718
+ return new HistoryPanel(container, options, pageSize);
2719
+ }
2720
+ var HistoryPanel = class {
2721
+ constructor(container, options, pageSize) {
2722
+ this.options = options;
2723
+ this.pageSize = pageSize;
2724
+ this.actions = /* @__PURE__ */ new Map();
2725
+ this.events = new AbortController();
2726
+ this.records = [];
2727
+ this.next = null;
2728
+ this.view = null;
2729
+ this.active = null;
2730
+ this.destroyed = false;
2731
+ const doc = container.ownerDocument;
2732
+ this.element = doc.createElement("section");
2733
+ this.element.className = "dx-history";
2734
+ this.element.setAttribute("aria-label", "Document history");
2735
+ const style = doc.createElement("style");
2736
+ style.textContent = HISTORY_CSS;
2737
+ const title = doc.createElement("h2");
2738
+ title.textContent = "Document history";
2739
+ this.status = doc.createElement("p");
2740
+ this.status.setAttribute("role", "status");
2741
+ this.status.setAttribute("aria-live", "polite");
2742
+ this.fieldset = doc.createElement("fieldset");
2743
+ const legend = doc.createElement("legend");
2744
+ legend.textContent = "Versions and checkpoints";
2745
+ this.fieldset.append(legend);
2746
+ this.element.append(style, title, this.status, this.fieldset);
2747
+ container.append(this.element);
2748
+ const note = doc.createElement("p");
2749
+ note.textContent = options.checkpoints ? "Browse saved versions without changing your draft." : "Read-only history. Preview or download any saved version.";
2750
+ this.fieldset.append(note);
2751
+ this.button("refresh", "Refresh history", () => this.load(true));
2752
+ this.button("latest", "Open latest", async () => this.preview(await options.reader.exportDocx(), "Latest saved version"));
2753
+ this.versions = this.select("Version");
2754
+ this.versions.size = 6;
2755
+ this.detail = doc.createElement("p");
2756
+ this.fieldset.append(this.detail);
2757
+ this.versions.addEventListener("change", () => this.update(), { signal: this.events.signal });
2758
+ this.button("preview", "Preview selected", async () => this.preview(await options.reader.exportDocx(this.selected().id), versionTitle(this.selected())));
2759
+ this.button("download", "Download selected", async () => this.download(await options.reader.exportDocx(this.selected().id), `version-${this.selected().record.sequence}.docx`));
2760
+ this.before = this.select("Compare from");
2761
+ this.before.addEventListener("change", () => this.update(), { signal: this.events.signal });
2762
+ const compareNote = doc.createElement("p");
2763
+ compareNote.textContent = "Compare from this version to the selected version above.";
2764
+ this.fieldset.append(compareNote);
2765
+ this.button("compare", "Compare versions", async () => {
2766
+ const before = this.records[Number(this.before.value)];
2767
+ await this.preview(await options.reader.compareVersions(before.id, this.selected().id), "Comparison with tracked changes");
2768
+ });
2769
+ this.button("more", "Load older versions", async () => {
2770
+ if (this.next) await this.page(this.next, false);
2771
+ });
2772
+ this.author = this.input("Your name", "text", options.author ?? "You");
2773
+ this.label = this.input("Checkpoint name (optional)", "text");
2774
+ this.author.parentElement.hidden = this.label.parentElement.hidden = !options.checkpoints;
2775
+ this.button("save", "Save checkpoint", async () => {
2776
+ const view = await options.checkpoints.save(await options.capture(), this.metadata());
2777
+ await options.onCheckpoint?.(view, "save");
2778
+ await this.load(false);
2779
+ this.label.value = "";
2780
+ this.status.textContent = "Checkpoint saved. Your draft remains open.";
2781
+ });
2782
+ this.button("restore", "Restore selected", async () => {
2783
+ const version = this.selected();
2784
+ if (!doc.defaultView?.confirm(`Restore ${versionTitle(version)} as a new checkpoint?
2785
+
2786
+ Your current draft and all later versions will be kept.`)) {
2787
+ this.status.textContent = "Restore canceled. Your draft and history are unchanged.";
2788
+ return;
2789
+ }
2790
+ const view = await options.checkpoints.restore(version.id, this.metadata());
2791
+ await options.onCheckpoint?.(view, "restore");
2792
+ await this.load(false);
2793
+ this.status.textContent = "Restored as a new checkpoint. Your draft and later versions are kept.";
2794
+ });
2795
+ const restoreNote = doc.createElement("p");
2796
+ restoreNote.hidden = !options.checkpoints;
2797
+ restoreNote.textContent = "Restore creates a new checkpoint. Open latest to preview it; your draft stays open.";
2798
+ this.fieldset.append(restoreNote);
2799
+ this.button("retry", "Retry checkpoint", async () => {
2800
+ const view = await options.checkpoints.retry();
2801
+ await options.onCheckpoint?.(view, "retry");
2802
+ await this.load(false);
2803
+ this.status.textContent = "Checkpoint recovered. Your draft remains open. Refresh history to check for newer versions.";
2804
+ });
2805
+ const sharing = this.disclosure("Download with history");
2806
+ const sharingNote = doc.createElement("p");
2807
+ sharingNote.textContent = "Includes retained drafts and collaboration proposals. Share this file only when you want to include that history. A DOCX download keeps existing Word comments and revisions, without external history.";
2808
+ sharing.append(sharingNote);
2809
+ this.button("archive", "Download .docxhistory", async () => this.download(await options.reader.exportHistoryArchive(), "docxhistory"), sharing);
2810
+ const time = this.disclosure("Find a version by time");
2811
+ const cutoff = this.input("Saved at or before (local time)", "datetime-local", "", time);
2812
+ this.button("time", "Preview at time", async () => {
2813
+ if (!cutoff.value) throw new DocxHistoryError("InvalidRequest", "Choose a date and time first.");
2814
+ const sequence = await options.reader.resolveSequenceAtTime(new Date(cutoff.value).toISOString());
2815
+ await this.preview(await options.reader.materialize(sequence), "Version at selected time");
2816
+ }, time);
2817
+ const activity = this.disclosure("Recorded collaboration");
2818
+ const decisions = doc.createElement("ol");
2819
+ let showOlderActivity = () => {
2820
+ };
2821
+ this.button("activity", "Load activity", async () => {
2822
+ const { operations } = await options.reader.readOperationsSince(null);
2823
+ const resolved = new Set(operations.filter((op) => op.record.status === "accepted").map((op) => op.input.request.resolves?.digest.value));
2824
+ let shown = 0;
2825
+ showOlderActivity = () => {
2826
+ const page = operations.slice(Math.max(0, operations.length - shown - this.pageSize), operations.length - shown).reverse();
2827
+ decisions.append(...page.map((op) => this.activityItem(op, resolved.has(op.id.digest.value))));
2828
+ shown += page.length;
2829
+ this.actions.get("activity-more").hidden = shown >= operations.length;
2830
+ };
2831
+ decisions.replaceChildren();
2832
+ showOlderActivity();
2833
+ this.status.textContent = operations.length ? "Recorded activity loaded." : "No recorded collaboration.";
2834
+ }, activity);
2835
+ activity.append(decisions);
2836
+ this.button("activity-more", "Load older activity", async () => showOlderActivity(), activity);
2837
+ this.actions.get("activity-more").hidden = true;
2838
+ this.ready = this.run("Loading history", () => this.load(false));
2839
+ }
2840
+ refresh() {
2841
+ return this.run("Loading history", () => this.load(true));
2842
+ }
2843
+ async destroy() {
2844
+ this.destroyed = true;
2845
+ this.events.abort();
2846
+ this.element.remove();
2847
+ await this.active?.catch(() => {
2848
+ });
2849
+ }
2850
+ async load(refresh) {
2851
+ this.view = this.options.checkpoints ? refresh ? await this.options.checkpoints.refresh() : this.options.checkpoints.view : await this.options.reader.read();
2852
+ if (this.view) await this.page(this.view.version.id, true);
2853
+ else {
2854
+ this.records = [];
2855
+ this.next = null;
2856
+ this.versions.replaceChildren();
2857
+ this.before.replaceChildren();
2858
+ }
2859
+ this.status.textContent = this.options.checkpoints?.hasPending ? "A checkpoint needs recovery. Retry it before saving more changes." : this.view ? "History loaded. Select a version to preview, download or restore." : "No checkpoints yet. Save your first checkpoint when you are ready.";
2860
+ }
2861
+ async page(cursor, reset) {
2862
+ const page = await this.options.reader.listVersions(cursor, this.pageSize);
2863
+ if (reset) {
2864
+ this.records = [];
2865
+ this.versions.replaceChildren();
2866
+ this.before.replaceChildren();
2867
+ }
2868
+ const start2 = this.records.length;
2869
+ this.records.push(...page.versions);
2870
+ this.next = page.next;
2871
+ for (let index = start2; index < this.records.length; index++) {
2872
+ for (const select of [this.versions, this.before]) {
2873
+ const option = select.ownerDocument.createElement("option");
2874
+ option.value = String(index);
2875
+ option.textContent = option.title = versionTitle(this.records[index]);
2876
+ select.append(option);
2877
+ }
2878
+ }
2879
+ if (reset) {
2880
+ this.versions.value = "0";
2881
+ this.before.value = this.records.length > 1 ? "1" : "0";
2882
+ }
2883
+ }
2884
+ update() {
2885
+ const hasVersion = this.records.length > 0;
2886
+ const pending = this.options.checkpoints?.hasPending ?? false;
2887
+ const stale = this.options.checkpoints?.needsRefresh ?? false;
2888
+ for (const key of ["latest", "preview", "download", "archive", "time", "activity"]) this.actions.get(key).disabled = !hasVersion;
2889
+ this.actions.get("more").hidden = !this.next;
2890
+ this.actions.get("compare").disabled = !hasVersion || this.before.value === this.versions.value;
2891
+ this.actions.get("save").hidden = !this.options.checkpoints || !this.options.capture;
2892
+ this.actions.get("save").disabled = pending || stale;
2893
+ this.actions.get("restore").hidden = !this.options.checkpoints;
2894
+ this.actions.get("restore").disabled = !hasVersion || pending || stale;
2895
+ this.actions.get("retry").hidden = !pending;
2896
+ this.detail.textContent = hasVersion ? [
2897
+ versionTitle(this.selected()),
2898
+ this.selected().record.metadata.message,
2899
+ this.selected().record.restoredFrom ? "Restored from an earlier version." : ""
2900
+ ].filter(Boolean).join(" \u2014 ") : "";
2901
+ }
2902
+ selected() {
2903
+ return this.records[Number(this.versions.value)];
2904
+ }
2905
+ activityItem(op, resolved) {
2906
+ const doc = this.element.ownerDocument;
2907
+ const item = doc.createElement("li");
2908
+ const state = op.record.status === "accepted" ? "Accepted" : resolved ? "Conflict resolved" : "Conflict needs review";
2909
+ const label = doc.createElement("p");
2910
+ label.textContent = `${op.input.request.metadata.author} \xB7 ${state} \xB7 ${formatTime(op.input.request.metadata.createdAt)}`;
2911
+ item.append(label);
2912
+ const description = doc.createElement("p");
2913
+ this.commandButton("View decision", async () => {
2914
+ const decision = await this.options.reader.getOperation(op.id);
2915
+ const { kind, metadata } = decision.input.request;
2916
+ description.textContent = [
2917
+ { text: "Text edit", package: "Document edit", discard: "Discarded proposal" }[kind],
2918
+ metadata.label,
2919
+ metadata.message,
2920
+ decision.record.conflict
2921
+ ].filter(Boolean).join(" \u2014 ");
2922
+ }, item);
2923
+ item.append(description);
2924
+ this.commandButton("Download proposal", async () => {
2925
+ await this.download(await this.options.reader.exportOperationProposal(op.id), `proposal-${op.record.revision}.docx`);
2926
+ }, item);
2927
+ return item;
2928
+ }
2929
+ metadata() {
2930
+ return { author: this.author.value.trim() || "You", createdAt: (/* @__PURE__ */ new Date()).toISOString(), label: this.label.value.trim() || void 0 };
2931
+ }
2932
+ async preview(bytes, title) {
2933
+ if (!this.destroyed) await this.options.preview(bytes, title);
2934
+ }
2935
+ async download(bytes, suffix) {
2936
+ if (this.destroyed) return;
2937
+ const stem = (this.options.documentName ?? "document").replace(/\.(docx|docxhistory)$/i, "");
2938
+ const name = suffix === "docxhistory" ? `${stem}.docxhistory` : `${stem}-${suffix}`;
2939
+ if (this.options.download) {
2940
+ await this.options.download(bytes, name);
2941
+ return;
2942
+ }
2943
+ const url = URL.createObjectURL(new Blob([bytes.slice()], { type: suffix === "docxhistory" ? "application/octet-stream" : "application/vnd.openxmlformats-officedocument.wordprocessingml.document" }));
2944
+ const link = this.element.ownerDocument.createElement("a");
2945
+ link.href = url;
2946
+ link.download = name;
2947
+ link.click();
2948
+ setTimeout(() => URL.revokeObjectURL(url), 1e3);
2949
+ }
2950
+ async run(label, action) {
2951
+ if (this.destroyed) throw new DocxHistoryError("Closed", "History controls are closed.");
2952
+ if (this.active) throw new DocxHistoryError("Busy", "A history command is still running.");
2953
+ this.fieldset.disabled = true;
2954
+ this.element.setAttribute("aria-busy", "true");
2955
+ this.status.textContent = `${label}\u2026`;
2956
+ const work = Promise.resolve().then(action);
2957
+ this.active = work;
2958
+ try {
2959
+ await work;
2960
+ if (this.status.textContent === `${label}\u2026`) this.status.textContent = "Ready.";
2961
+ } catch (error) {
2962
+ if (!this.destroyed) this.status.textContent = historyControlError(error, this.options.checkpoints?.hasPending);
2963
+ throw error;
2964
+ } finally {
2965
+ this.active = null;
2966
+ if (!this.destroyed) {
2967
+ this.fieldset.disabled = false;
2968
+ this.element.setAttribute("aria-busy", "false");
2969
+ this.update();
2970
+ }
2971
+ }
2972
+ }
2973
+ button(key, title, action, parent = this.fieldset) {
2974
+ this.actions.set(key, this.commandButton(title, action, parent));
2975
+ }
2976
+ commandButton(title, action, parent) {
2977
+ const button = parent.ownerDocument.createElement("button");
2978
+ button.type = "button";
2979
+ button.textContent = title;
2980
+ button.addEventListener("click", () => {
2981
+ void this.run(title, action).catch(() => {
2982
+ });
2983
+ }, { signal: this.events.signal });
2984
+ parent.append(button);
2985
+ return button;
2986
+ }
2987
+ select(title) {
2988
+ const label = this.fieldset.ownerDocument.createElement("label");
2989
+ label.textContent = title;
2990
+ const select = label.ownerDocument.createElement("select");
2991
+ select.setAttribute("aria-label", title);
2992
+ label.append(select);
2993
+ this.fieldset.append(label);
2994
+ return select;
2995
+ }
2996
+ input(title, type, value = "", parent = this.fieldset) {
2997
+ const label = parent.ownerDocument.createElement("label");
2998
+ label.textContent = title;
2999
+ const input = label.ownerDocument.createElement("input");
3000
+ input.type = type;
3001
+ input.value = value;
3002
+ label.append(input);
3003
+ parent.append(label);
3004
+ return input;
3005
+ }
3006
+ disclosure(title) {
3007
+ const details = this.fieldset.ownerDocument.createElement("details");
3008
+ const summary = details.ownerDocument.createElement("summary");
3009
+ summary.textContent = title;
3010
+ details.append(summary);
3011
+ this.fieldset.append(details);
3012
+ return details;
3013
+ }
3014
+ };
3015
+ function historyControlError(error, pending = false) {
3016
+ const code = error instanceof DocxHistoryError ? error.code : "";
3017
+ if (code === "StaleHead") return "A newer checkpoint exists. Your draft is safe. Refresh history, review the newer version, then save again.";
3018
+ if (code === "ImportConflict") return "A different local history already exists. Open this file read-only to explore it.";
3019
+ if (code === "InitializationUnsupported") return "This storage cannot import history. Open read-only or choose storage that supports importing.";
3020
+ if (pending) return "The checkpoint could not be confirmed. Your draft is safe. Retry checkpoint to recover the original request.";
3021
+ if (code === "ResourceLimit") return "This history file exceeds browser processing limits, which can apply even below 64 MiB. Your document is unchanged.";
3022
+ if (code === "UnsupportedVersion") return "This history file uses an unsupported version. Open it with a newer app. Your document is unchanged.";
3023
+ if (code === "InvalidManifest") return "This history file is damaged or incomplete. Choose another copy. Your document is unchanged.";
3024
+ return `History could not be loaded. Your document is unchanged. ${error instanceof Error ? error.message : "Please try again."}`;
3025
+ }
3026
+ function versionTitle(version) {
3027
+ const { metadata, sequence } = version.record;
3028
+ return `${metadata.label || `Version ${BigInt(sequence) + 1n}`} \xB7 ${metadata.author} \xB7 ${formatTime(metadata.createdAt)}`;
3029
+ }
3030
+ function formatTime(value) {
3031
+ const time = new Date(value);
3032
+ return Number.isNaN(time.getTime()) ? value : time.toLocaleString();
3033
+ }
3034
+ var HISTORY_CSS = `
3035
+ .dx-history{font:14px/1.5 system-ui,sans-serif;color:#203047;background:#fff;border:1px solid #cbd5e1;border-radius:12px;padding:18px;min-width:0}
3036
+ .dx-history *{box-sizing:border-box}.dx-history h2{font-size:20px;margin:0 0 8px}.dx-history p{margin:8px 0;overflow-wrap:anywhere}
3037
+ .dx-history fieldset{border:0;padding:0;margin:0;min-width:0}.dx-history legend{position:absolute;width:1px;height:1px;overflow:hidden;clip-path:inset(50%)}
3038
+ .dx-history label{display:block;font-weight:600;margin:12px 0 6px}.dx-history input,.dx-history select{display:block;width:100%;max-width:100%;margin-top:4px;font:inherit;color:inherit;border:1px solid #94a3b8;border-radius:6px;padding:8px;background:#fff}
3039
+ .dx-history button{min-height:40px;margin:4px 6px 4px 0;padding:7px 12px;border:1px solid #94a3b8;border-radius:6px;background:#f8fafc;color:inherit;font:inherit;cursor:pointer}
3040
+ .dx-history button:disabled{opacity:.5;cursor:wait}.dx-history :focus-visible{outline:3px solid #2563eb;outline-offset:2px}.dx-history [hidden]{display:none}
3041
+ .dx-history details{margin-top:16px;border-top:1px solid #e2e8f0;padding-top:12px}.dx-history summary{cursor:pointer;font-weight:600;padding:4px 0}
3042
+ .dx-history [role=status]{min-height:42px}.dx-history ol{padding-left:22px}.dx-history option{padding:6px}
3043
+ `;
3044
+
2164
3045
  // src/page-number-format.ts
2165
3046
  var ROMAN_ONES = ["", "i", "ii", "iii", "iv", "v", "vi", "vii", "viii", "ix"];
2166
3047
  var ROMAN_TENS = ["", "x", "xx", "xxx", "xl", "l", "lx", "lxx", "lxxx", "xc"];
@@ -5610,11 +6491,11 @@ var Docxodus = (() => {
5610
6491
  if (!this.partUriFor(which, "default")) this.seedStory(which, "default");
5611
6492
  if (!this.partUriFor(which, kind)) this.seedStory(which, kind);
5612
6493
  }
5613
- const res = parseResult(this.bridge.EnsureHeaderFooterVisible(this.handle, this.bodyAnchorId, kind));
6494
+ const res = parseResult2(this.bridge.EnsureHeaderFooterVisible(this.handle, this.bodyAnchorId, kind));
5614
6495
  if (!res.success) return false;
5615
6496
  } else {
5616
6497
  if (!this.bridge.SetHeaderFooterKindEnabled) return false;
5617
- const res = parseResult(this.bridge.SetHeaderFooterKindEnabled(this.handle, this.bodyAnchorId, kind, false));
6498
+ const res = parseResult2(this.bridge.SetHeaderFooterKindEnabled(this.handle, this.bodyAnchorId, kind, false));
5618
6499
  if (!res.success) return false;
5619
6500
  for (const which of ["header", "footer"]) {
5620
6501
  if (this.kinds[which] === kind) this.kinds[which] = "default";
@@ -5639,7 +6520,7 @@ var Docxodus = (() => {
5639
6520
  * Deliberately a PLAIN field (no `\*` switch), exactly as Word inserts one, so it follows the
5640
6521
  * section's page-number format — see {@link setPageNumbering}. */
5641
6522
  insertPageNumber(which, anchorId, field) {
5642
- const res = parseResult(this.bridge.InsertPageNumberField(this.handle, anchorId, field, ""));
6523
+ const res = parseResult2(this.bridge.InsertPageNumberField(this.handle, anchorId, field, ""));
5643
6524
  if (!res.success) return false;
5644
6525
  this.refresh(which);
5645
6526
  return true;
@@ -5662,7 +6543,7 @@ var Docxodus = (() => {
5662
6543
  /** Set this section's page numbering (`w:pgNumType`) — Word's *Format Page Numbers…*. */
5663
6544
  setPageNumbering(op) {
5664
6545
  if (!this.bodyAnchorId) return false;
5665
- const res = parseResult(this.bridge.SetPageNumbering(this.handle, this.bodyAnchorId, JSON.stringify(op)));
6546
+ const res = parseResult2(this.bridge.SetPageNumbering(this.handle, this.bodyAnchorId, JSON.stringify(op)));
5666
6547
  if (!res.success) return false;
5667
6548
  this.reloadSection();
5668
6549
  return true;
@@ -5674,7 +6555,7 @@ var Docxodus = (() => {
5674
6555
  /** Remove this section's page-numbering start/format. */
5675
6556
  clearPageNumbering() {
5676
6557
  if (!this.bodyAnchorId) return false;
5677
- const res = parseResult(this.bridge.ClearPageNumbering(this.handle, this.bodyAnchorId));
6558
+ const res = parseResult2(this.bridge.ClearPageNumbering(this.handle, this.bodyAnchorId));
5678
6559
  if (!res.success) return false;
5679
6560
  this.reloadSection();
5680
6561
  return true;
@@ -5985,7 +6866,7 @@ var Docxodus = (() => {
5985
6866
  seedStory(which, kind) {
5986
6867
  if (!this.bodyAnchorId) return;
5987
6868
  const set = which === "header" ? this.bridge.SetHeaderText : this.bridge.SetFooterText;
5988
- const res = parseResult(set(this.handle, this.bodyAnchorId, kind, ""));
6869
+ const res = parseResult2(set(this.handle, this.bodyAnchorId, kind, ""));
5989
6870
  if (!res.success) return;
5990
6871
  this.callbacks.refreshAnchorMap();
5991
6872
  this.sectionInfo = this.readSectionInfo(this.bodyAnchorId);
@@ -6141,7 +7022,7 @@ var Docxodus = (() => {
6141
7022
  const range = d.caretRangeFromPoint?.(x, y);
6142
7023
  return range ? { node: range.startContainer, offset: range.startOffset } : null;
6143
7024
  }
6144
- function parseResult(json) {
7025
+ function parseResult2(json) {
6145
7026
  try {
6146
7027
  return JSON.parse(json);
6147
7028
  } catch {
@@ -8374,7 +9255,7 @@ span.comment-highlight.docx-comment-active { background: hsl(var(--docx-comment-
8374
9255
  };
8375
9256
  }
8376
9257
  var honeyPotFix = makeHoneyPotFix();
8377
- var adapter = makeAdapter({
9258
+ var adapter2 = makeAdapter({
8378
9259
  typeKey: "element",
8379
9260
  defaultDropEffect: "move",
8380
9261
  mount: function mount(api2) {
@@ -8463,8 +9344,8 @@ span.comment-highlight.docx-comment-active { background: hsl(var(--docx-comment-
8463
9344
  },
8464
9345
  onPostDispatch: honeyPotFix.getOnPostDispatch()
8465
9346
  });
8466
- var dropTargetForElements = adapter.dropTarget;
8467
- var monitorForElements = adapter.monitor;
9347
+ var dropTargetForElements = adapter2.dropTarget;
9348
+ var monitorForElements = adapter2.monitor;
8468
9349
  function draggable(args) {
8469
9350
  if (true) {
8470
9351
  if (args.dragHandle && !args.element.contains(args.dragHandle)) {
@@ -8488,7 +9369,7 @@ span.comment-highlight.docx-comment-active { background: hsl(var(--docx-comment-
8488
9369
  // this is because you *must* have a draggable element to start a drag
8489
9370
  // but you _might_ not have any drop targets immediately
8490
9371
  // (You might create drop targets async)
8491
- adapter.registerUsage(),
9372
+ adapter2.registerUsage(),
8492
9373
  addToRegistry(args),
8493
9374
  addAttribute(args.element, {
8494
9375
  attribute: "draggable",
@@ -9935,22 +10816,55 @@ span.comment-highlight.docx-comment-active { background: hsl(var(--docx-comment-
9935
10816
  const span = trimmedSpan(block, { start: Math.min(start2, end), length: Math.abs(end - start2) });
9936
10817
  return span.length > 0 ? span : null;
9937
10818
  }
9938
- function selectRange(el, start2, length) {
9939
- const sel = typeof window !== "undefined" ? window.getSelection() : null;
9940
- if (!sel || !el.isConnected) return;
9941
- el.focus();
9942
- const range = document.createRange();
10819
+ function contentRangeIn(el, start2, length) {
10820
+ if (!el.isConnected) return null;
10821
+ const doc = el.ownerDocument;
10822
+ const range = doc.createRange();
9943
10823
  const from = contentPositionIn(el, start2);
9944
10824
  const to = contentPositionIn(el, start2 + length);
9945
10825
  try {
9946
10826
  range.setStart(from.node, from.offset);
9947
10827
  range.setEnd(to.node, to.offset);
9948
10828
  } catch {
9949
- return;
10829
+ return null;
9950
10830
  }
10831
+ return range;
10832
+ }
10833
+ function selectRange(el, start2, length) {
10834
+ const sel = typeof window !== "undefined" ? window.getSelection() : null;
10835
+ if (!sel || !el.isConnected) return;
10836
+ el.focus();
10837
+ const range = contentRangeIn(el, start2, length);
10838
+ if (!range) return;
9951
10839
  sel.removeAllRanges();
9952
10840
  sel.addRange(range);
9953
10841
  }
10842
+ var FIND_HIGHLIGHT = "docxodus-find";
10843
+ var FIND_HIGHLIGHT_ACTIVE = "docxodus-find-active";
10844
+ var FIND_PAINT_LIMIT = 500;
10845
+ var findHighlightStyledDocuments = /* @__PURE__ */ new WeakSet();
10846
+ var findPaintOwner = null;
10847
+ function ensureFindHighlightStyles(doc) {
10848
+ if (findHighlightStyledDocuments.has(doc)) return;
10849
+ findHighlightStyledDocuments.add(doc);
10850
+ const style = doc.createElement("style");
10851
+ style.dataset.docxodusFindHighlight = "true";
10852
+ style.textContent = `
10853
+ ::highlight(${FIND_HIGHLIGHT}) { background-color: rgba(15, 118, 110, .16); }
10854
+ ::highlight(${FIND_HIGHLIGHT_ACTIVE}) { background-color: rgba(15, 118, 110, .38); }
10855
+ `;
10856
+ (doc.head ?? doc.documentElement).appendChild(style);
10857
+ }
10858
+ function highlightRegistry() {
10859
+ if (typeof CSS === "undefined") return null;
10860
+ const registry = CSS.highlights;
10861
+ const ctor = globalThis.Highlight;
10862
+ return registry && typeof ctor === "function" ? registry : null;
10863
+ }
10864
+ function makeHighlight(ranges) {
10865
+ const ctor = globalThis.Highlight;
10866
+ return new ctor(...ranges);
10867
+ }
9954
10868
  function inBorderWrapper(el) {
9955
10869
  const style = el.parentElement?.getAttribute("style") ?? "";
9956
10870
  return /border-(top|bottom|left|right):\s*(?!none)[^;]+/i.test(style);
@@ -10321,6 +11235,7 @@ span.comment-highlight.docx-comment-active { background: hsl(var(--docx-comment-
10321
11235
  document.removeEventListener("mouseup", this.onMouseUp, true);
10322
11236
  }
10323
11237
  this.clearDragSelection();
11238
+ this.clearFindMatches();
10324
11239
  this.teardownBlockDrag();
10325
11240
  this.gutter?.dispose();
10326
11241
  this.gutter = null;
@@ -12719,15 +13634,65 @@ span.comment-highlight.docx-comment-active { background: hsl(var(--docx-comment-
12719
13634
  }
12720
13635
  return out;
12721
13636
  }
12722
- /** Select a match and scroll it into view. */
13637
+ /**
13638
+ * Select a match, focus its block and scroll it into view — the caret lands on the hit, so
13639
+ * typing continues in the document. A find box that is still being typed into wants
13640
+ * `showFindMatches` instead; this is the commit step (closing the bar, jumping in to edit).
13641
+ */
12723
13642
  selectMatch(match) {
12724
13643
  if (!match.block.isConnected) return;
13644
+ this.clearFindMatches();
12725
13645
  this.activeBlock = match.block;
12726
13646
  selectRange(match.block, match.start, match.length);
12727
13647
  match.block.scrollIntoView({ block: "center", behavior: "smooth" });
12728
13648
  }
12729
- /** Replace one match's text (formatting of the surrounding run is inherited). */
12730
- replaceMatch(match, replacement) {
13649
+ /**
13650
+ * Paint `matches` and scroll the one at `activeIndex` into view WITHOUT moving focus or the
13651
+ * caret. This is what a find field calls on every keystroke: the document shows where the hits
13652
+ * are and rides to the current one, while the keyboard stays in the search box.
13653
+ *
13654
+ * Falls back to the document selection where the CSS Custom Highlight API is missing — still
13655
+ * without focusing the block, so the next character typed goes to the search field either way.
13656
+ */
13657
+ showFindMatches(matches, activeIndex) {
13658
+ const active = matches[activeIndex];
13659
+ this.clearFindMatches();
13660
+ if (!active || !active.block.isConnected) return;
13661
+ this.activeBlock = active.block;
13662
+ const activeRange = contentRangeIn(active.block, active.start, active.length);
13663
+ const registry = highlightRegistry();
13664
+ if (registry && activeRange) {
13665
+ ensureFindHighlightStyles(this.container.ownerDocument);
13666
+ const rest = [];
13667
+ for (let i = 0; i < matches.length && rest.length < FIND_PAINT_LIMIT; i++) {
13668
+ if (i === activeIndex) continue;
13669
+ const range = contentRangeIn(matches[i].block, matches[i].start, matches[i].length);
13670
+ if (range) rest.push(range);
13671
+ }
13672
+ if (rest.length > 0) registry.set(FIND_HIGHLIGHT, makeHighlight(rest));
13673
+ registry.set(FIND_HIGHLIGHT_ACTIVE, makeHighlight([activeRange]));
13674
+ findPaintOwner = this;
13675
+ } else if (activeRange) {
13676
+ const sel = this.container.ownerDocument.defaultView?.getSelection();
13677
+ sel?.removeAllRanges();
13678
+ sel?.addRange(activeRange);
13679
+ }
13680
+ active.block.scrollIntoView({ block: "center", behavior: "smooth" });
13681
+ }
13682
+ /** Drop the find painting (closing the find bar, or committing a match to the caret). */
13683
+ clearFindMatches() {
13684
+ if (findPaintOwner !== this) return;
13685
+ const registry = highlightRegistry();
13686
+ registry?.delete(FIND_HIGHLIGHT);
13687
+ registry?.delete(FIND_HIGHLIGHT_ACTIVE);
13688
+ findPaintOwner = null;
13689
+ }
13690
+ /**
13691
+ * Replace one match's text (formatting of the surrounding run is inherited). `focus: false`
13692
+ * leaves the keyboard where it is — a Replace button pressed from the find bar must not drag
13693
+ * the caret into the document mid-search.
13694
+ */
13695
+ replaceMatch(match, replacement, options = {}) {
12731
13696
  const block = match.block;
12732
13697
  if (this.closed || !block.isConnected) return false;
12733
13698
  const unid = block.getAttribute("data-anchor");
@@ -12748,7 +13713,7 @@ span.comment-highlight.docx-comment-active { background: hsl(var(--docx-comment-
12748
13713
  );
12749
13714
  if (!res.success) return false;
12750
13715
  const fresh = this.swapBlock(block, unid, res.modified?.[0]);
12751
- if (fresh) selectRange(fresh, span.start, replacement.length);
13716
+ if (fresh && options.focus !== false) selectRange(fresh, span.start, replacement.length);
12752
13717
  return true;
12753
13718
  }
12754
13719
  /** Replace every occurrence; returns how many were replaced. */
@@ -12767,7 +13732,11 @@ span.comment-highlight.docx-comment-active { background: hsl(var(--docx-comment-
12767
13732
  for (const m of list.slice().sort((a, b) => b.start - a.start)) {
12768
13733
  if (!current.isConnected) break;
12769
13734
  const before = current;
12770
- if (this.replaceMatch({ block: current, start: m.start, length: m.length }, replacement)) {
13735
+ if (this.replaceMatch(
13736
+ { block: current, start: m.start, length: m.length },
13737
+ replacement,
13738
+ { focus: options.focus }
13739
+ )) {
12771
13740
  count++;
12772
13741
  current = this.activeBlock && this.activeBlock !== before ? this.activeBlock : current;
12773
13742
  }
@@ -13449,7 +14418,7 @@ span.comment-highlight.docx-comment-active { background: hsl(var(--docx-comment-
13449
14418
  }
13450
14419
 
13451
14420
  // src/ribbon-chrome.ts
13452
- var RIBBON_STYLE_VERSION = "10";
14421
+ var RIBBON_STYLE_VERSION = "11";
13453
14422
  var RIBBON_STYLE_ATTR = "data-docxodus-ribbon-styles";
13454
14423
  var COMMENT_GUTTER_WIDTH = 264;
13455
14424
  var RIBBON_CSS = `
@@ -13882,9 +14851,11 @@ span.comment-highlight.docx-comment-active { background: hsl(var(--docx-comment-
13882
14851
  background: linear-gradient(to top, var(--dxr-desk), transparent);
13883
14852
  }
13884
14853
  .dxr[data-chrome] .dxr-surface { position: relative; margin: 26px auto; padding: 0 16px 96px; }
13885
- /* The comment gutter sits to the right of the sheet; the surface reserves its width so the
13886
- page shifts left rather than the bubbles covering it \u2014 Word's markup area. */
13887
- .dxr[data-chrome="full"] .dxr-surface[data-comments="on"] { padding-right: calc(var(--dxr-gutter) + 8px); }
14854
+ /* Reserve the markup area only while a visible comment or draft needs it. The gutter owns
14855
+ its empty/hidden state, so ordinary documents stay centered without disabling comments. */
14856
+ .dxr[data-chrome="full"] .dxr-surface[data-comments="on"]:has(> .docx-comment-gutter:not([data-empty]):not([hidden])) {
14857
+ padding-right: calc(var(--dxr-gutter) + 8px);
14858
+ }
13888
14859
  .dxr-surface [contenteditable="true"]:focus {
13889
14860
  outline: 2px solid var(--dxr-accent);
13890
14861
  outline-offset: 2px;
@@ -15400,7 +16371,8 @@ span.comment-highlight.docx-comment-active { background: hsl(var(--docx-comment-
15400
16371
  comments: this.options.comments,
15401
16372
  commentAuthor: this.author
15402
16373
  });
15403
- this.require("save").disabled = false;
16374
+ const saveButton = this.control("save");
16375
+ if (saveButton) saveButton.disabled = false;
15404
16376
  this.require("ribbon").setAttribute("aria-disabled", "false");
15405
16377
  this.setState("ready");
15406
16378
  this.setStatus(`Rendered in ${Math.round(performance.now() - started)} ms`);
@@ -16035,11 +17007,18 @@ span.comment-highlight.docx-comment-active { background: hsl(var(--docx-comment-
16035
17007
  if (!this.live || !text.value) return;
16036
17008
  let count = 0;
16037
17009
  this.run("replace all", () => {
16038
- count = this.live.replaceAll(text.value, replace.value, { matchCase: matchCase.checked });
17010
+ count = this.live.replaceAll(text.value, replace.value, {
17011
+ matchCase: matchCase.checked,
17012
+ focus: false
17013
+ });
16039
17014
  });
16040
17015
  this.setStatus(`Replaced ${count} occurrence${count === 1 ? "" : "s"}`);
16041
17016
  this.refreshFind(true);
16042
17017
  });
17018
+ for (const id of ["findprev", "findnext", "replaceone", "replaceall"]) {
17019
+ const button = this.control(id);
17020
+ if (button) this.keepSelection(button);
17021
+ }
16043
17022
  bar.hidden = true;
16044
17023
  }
16045
17024
  openFindBar(withReplace) {
@@ -16050,28 +17029,44 @@ span.comment-highlight.docx-comment-active { background: hsl(var(--docx-comment-
16050
17029
  const text = this.require("findtext");
16051
17030
  text.focus();
16052
17031
  text.select();
16053
- this.refreshFind(false);
17032
+ this.refreshFind(true);
16054
17033
  }
17034
+ /**
17035
+ * Close the bar and hand the current hit over to the caret: the document is where the user is
17036
+ * going next, and Word leaves the insertion point on the match it stopped at. Until this runs
17037
+ * the match is only PAINTED (see `showFindMatches`), so the search field keeps the keyboard for
17038
+ * as long as the bar is open.
17039
+ */
16055
17040
  closeFindBar() {
16056
17041
  const bar = this.control("findbar");
16057
17042
  if (bar) bar.hidden = true;
17043
+ const match = this.findMatches[this.findIndex];
17044
+ if (match) this.live?.selectMatch(match);
17045
+ else this.live?.clearFindMatches();
16058
17046
  this.findMatches = [];
16059
17047
  this.findIndex = -1;
16060
17048
  }
16061
- refreshFind(select) {
17049
+ /**
17050
+ * Re-scan for the current query. `show` paints the hits and rides to the first one — it never
17051
+ * focuses the document, so typing the second character of a query that already matched goes on
17052
+ * refining the search instead of being typed into the document.
17053
+ */
17054
+ refreshFind(show) {
16062
17055
  const text = this.require("findtext");
16063
17056
  const matchCase = this.require("findcase").checked;
16064
17057
  this.findMatches = this.live && text.value ? this.live.find(text.value, { matchCase }) : [];
16065
17058
  this.findIndex = this.findMatches.length > 0 ? 0 : -1;
16066
17059
  this.updateFindCount();
16067
- if (select && this.findIndex >= 0) this.live?.selectMatch(this.findMatches[this.findIndex]);
17060
+ if (!show) return;
17061
+ if (this.findIndex >= 0) this.live?.showFindMatches(this.findMatches, this.findIndex);
17062
+ else this.live?.clearFindMatches();
16068
17063
  }
16069
17064
  stepFind(direction) {
16070
17065
  if (!this.live) return;
16071
17066
  if (this.findMatches.length === 0) this.refreshFind(false);
16072
17067
  if (this.findMatches.length === 0) return;
16073
17068
  this.findIndex = (this.findIndex + direction + this.findMatches.length) % this.findMatches.length;
16074
- this.live.selectMatch(this.findMatches[this.findIndex]);
17069
+ this.live.showFindMatches(this.findMatches, this.findIndex);
16075
17070
  this.updateFindCount();
16076
17071
  }
16077
17072
  replaceCurrent() {
@@ -16080,12 +17075,12 @@ span.comment-highlight.docx-comment-active { background: hsl(var(--docx-comment-
16080
17075
  const match = this.findMatches[this.findIndex];
16081
17076
  if (!match) return;
16082
17077
  const replacement = this.require("replacetext").value;
16083
- this.run("replace", () => this.live.replaceMatch(match, replacement));
17078
+ this.run("replace", () => this.live.replaceMatch(match, replacement, { focus: false }));
16084
17079
  const keep = this.findIndex;
16085
17080
  this.refreshFind(false);
16086
17081
  if (this.findMatches.length > 0) {
16087
17082
  this.findIndex = Math.min(keep, this.findMatches.length - 1);
16088
- this.live.selectMatch(this.findMatches[this.findIndex]);
17083
+ this.live.showFindMatches(this.findMatches, this.findIndex);
16089
17084
  this.updateFindCount();
16090
17085
  }
16091
17086
  }
@@ -16517,6 +17512,16 @@ span.comment-highlight.docx-comment-active { background: hsl(var(--docx-comment-
16517
17512
 
16518
17513
  // src/index.ts
16519
17514
  var import_meta = {};
17515
+ function openDocxHistory(storage) {
17516
+ const bridge = ensureInitialized().HistoryBridge;
17517
+ if (!bridge) throw new Error("This WASM build does not include history bindings.");
17518
+ return new DocxHistoryClient(bridge, storage);
17519
+ }
17520
+ function openDocxHistoryArchive(bytes) {
17521
+ const bridge = ensureInitialized().HistoryBridge;
17522
+ if (!bridge) throw new Error("This WASM build does not include history bindings.");
17523
+ return DocxHistoryArchive.open(bridge, bytes);
17524
+ }
16520
17525
  function openDocxSession2(bytes, settings) {
16521
17526
  const wasm = ensureInitialized();
16522
17527
  return openDocxSession(bytes, wasm, settings);
@@ -16571,14 +17576,16 @@ span.comment-highlight.docx-comment-active { background: hsl(var(--docx-comment-
16571
17576
  /* @vite-ignore */
16572
17577
  dotnetPath
16573
17578
  );
16574
- const { getAssemblyExports, getConfig } = await dotnet.withDiagnosticTracing(false).create();
17579
+ const { getAssemblyExports, getConfig, setModuleImports } = await dotnet.withDiagnosticTracing(false).create();
17580
+ installHistoryStorageImports(setModuleImports);
16575
17581
  const config = getConfig();
16576
17582
  const exports = await getAssemblyExports(config.mainAssemblyName);
16577
17583
  wasmExports = {
16578
17584
  DocumentConverter: exports.DocxodusWasm.DocumentConverter,
16579
17585
  DocumentComparer: exports.DocxodusWasm.DocumentComparer,
16580
17586
  DocxDiffBridge: exports.DocxodusWasm.DocxDiffBridge,
16581
- DocxSessionBridge: exports.DocxodusWasm.DocxSessionBridge
17587
+ DocxSessionBridge: exports.DocxodusWasm.DocxSessionBridge,
17588
+ HistoryBridge: exports.DocxodusWasm.HistoryBridge
16582
17589
  };
16583
17590
  return true;
16584
17591
  } catch {
@@ -17953,6 +18960,20 @@ ${parsed.documentElement.outerHTML}`;
17953
18960
  }
17954
18961
  throw new Error("Docxodus embed: unsupported document source");
17955
18962
  }
18963
+ function scopedDocumentFragment(mount2, html) {
18964
+ const parsed = new DOMParser().parseFromString(html, "text/html");
18965
+ const fragment = document.createDocumentFragment();
18966
+ parsed.querySelectorAll("style").forEach((sourceStyle) => {
18967
+ const style = document.createElement("style");
18968
+ style.textContent = sourceStyle.textContent;
18969
+ scopeStyleElement(style, mount2.selector);
18970
+ fragment.appendChild(style);
18971
+ });
18972
+ const body = document.createElement("template");
18973
+ body.innerHTML = parsed.body.innerHTML;
18974
+ fragment.appendChild(body.content);
18975
+ return fragment;
18976
+ }
17956
18977
  async function createViewer(container, source, options = {}) {
17957
18978
  const el = resolveContainer2(container);
17958
18979
  const { wasmBasePath: wasmBasePath2, ...conversion } = options;
@@ -17965,18 +18986,7 @@ ${parsed.documentElement.outerHTML}`;
17965
18986
  renderFootnotesAndEndnotes: true,
17966
18987
  ...opts
17967
18988
  });
17968
- const parsed = new DOMParser().parseFromString(lastHtml, "text/html");
17969
- const fragment = document.createDocumentFragment();
17970
- parsed.querySelectorAll("style").forEach((sourceStyle) => {
17971
- const style = document.createElement("style");
17972
- style.textContent = sourceStyle.textContent;
17973
- scopeStyleElement(style, mount2.selector);
17974
- fragment.appendChild(style);
17975
- });
17976
- const body = document.createElement("template");
17977
- body.innerHTML = parsed.body.innerHTML;
17978
- fragment.appendChild(body.content);
17979
- mount2.root.replaceChildren(fragment);
18989
+ mount2.root.replaceChildren(scopedDocumentFragment(mount2, lastHtml));
17980
18990
  };
17981
18991
  await render(source, conversion);
17982
18992
  return {
@@ -17990,6 +19000,111 @@ ${parsed.documentElement.outerHTML}`;
17990
19000
  }
17991
19001
  };
17992
19002
  }
19003
+ async function createHistoryViewer(container, history, documentId, options = {}) {
19004
+ const el = resolveContainer2(container);
19005
+ const { wasmBasePath: wasmBasePath2, maxEntriesToScan = 1e4, ...conversion } = options;
19006
+ if (!Number.isSafeInteger(maxEntriesToScan) || maxEntriesToScan <= 0 || maxEntriesToScan > 2147483647)
19007
+ throw new RangeError("Invalid history scan budget.");
19008
+ await ensureWasm(wasmBasePath2);
19009
+ const mount2 = createScopedMount(el);
19010
+ let accepted = null;
19011
+ let acceptedBytes = null;
19012
+ let displayedBytes = null;
19013
+ let shownSequence = "0";
19014
+ let following = true;
19015
+ let closed = false;
19016
+ let html = "";
19017
+ let queue = Promise.resolve();
19018
+ const clone = (value) => JSON.parse(JSON.stringify(value));
19019
+ const assertOpen = () => {
19020
+ if (closed) throw new DocxHistoryError("Closed", "History viewer is destroyed.");
19021
+ };
19022
+ const enqueue = (action) => {
19023
+ const result = queue.then(() => {
19024
+ assertOpen();
19025
+ return action();
19026
+ });
19027
+ queue = result.then(() => void 0, () => void 0);
19028
+ return result;
19029
+ };
19030
+ 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;
19031
+ const paint = async (bytes, sequence) => {
19032
+ const nextHtml = await convertDocxToHtml(bytes, { renderFootnotesAndEndnotes: true, ...conversion });
19033
+ assertOpen();
19034
+ const fragment = scopedDocumentFragment(mount2, nextHtml);
19035
+ mount2.root.replaceChildren(fragment);
19036
+ html = nextHtml;
19037
+ displayedBytes = bytes;
19038
+ shownSequence = sequence;
19039
+ };
19040
+ const refresh = async (resume = false) => {
19041
+ const update = await history.readChangesSince(documentId, accepted?.view.head ?? null, maxEntriesToScan);
19042
+ assertOpen();
19043
+ const duplicate = accepted !== null && sameHead(accepted.view.head, update.view.head);
19044
+ const bytes = duplicate ? acceptedBytes : await history.exportVersion(documentId, update.view.version.id);
19045
+ assertOpen();
19046
+ const wantLive = following || resume;
19047
+ const changedContent = accepted === null || accepted.view.state.snapshot.contentDigest.value !== update.view.state.snapshot.contentDigest.value || accepted.view.state.epoch !== update.view.state.epoch;
19048
+ if (wantLive && (changedContent || !following)) await paint(bytes, update.view.state.sequence);
19049
+ else if (wantLive) {
19050
+ displayedBytes = bytes;
19051
+ shownSequence = update.view.state.sequence;
19052
+ }
19053
+ assertOpen();
19054
+ accepted = update;
19055
+ acceptedBytes = bytes;
19056
+ if (wantLive) following = true;
19057
+ return clone(update);
19058
+ };
19059
+ const showSequence = async (sequence) => {
19060
+ const bytes = await history.materialize(documentId, sequence, maxEntriesToScan);
19061
+ assertOpen();
19062
+ await paint(bytes, sequence);
19063
+ following = false;
19064
+ };
19065
+ const viewer = {
19066
+ element: el,
19067
+ get html() {
19068
+ return html;
19069
+ },
19070
+ get head() {
19071
+ assertOpen();
19072
+ return clone(accepted.view.head);
19073
+ },
19074
+ get sequence() {
19075
+ return shownSequence;
19076
+ },
19077
+ get following() {
19078
+ return following;
19079
+ },
19080
+ refresh: () => enqueue(() => refresh()),
19081
+ showSequence: (sequence) => enqueue(() => showSequence(sequence)),
19082
+ showTime: (cutoff) => enqueue(async () => {
19083
+ const sequence = await history.resolveSequenceAtTime(documentId, cutoff, maxEntriesToScan);
19084
+ assertOpen();
19085
+ await showSequence(sequence);
19086
+ }),
19087
+ resume: () => enqueue(() => refresh(true)),
19088
+ exportDisplayed: () => {
19089
+ assertOpen();
19090
+ return displayedBytes.slice();
19091
+ },
19092
+ destroy: () => {
19093
+ if (closed) return;
19094
+ closed = true;
19095
+ acceptedBytes = null;
19096
+ displayedBytes = null;
19097
+ mount2.root.remove();
19098
+ }
19099
+ };
19100
+ try {
19101
+ await viewer.refresh();
19102
+ return viewer;
19103
+ } catch (error) {
19104
+ viewer.destroy();
19105
+ throw error;
19106
+ }
19107
+ }
17993
19108
  async function createEditor(container, source, options = {}) {
17994
19109
  const el = resolveContainer2(container);
17995
19110
  const { wasmBasePath: wasmBasePath2, ...editorOptions } = options;