web-doc 0.6.2 → 0.8.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 (201) hide show
  1. package/THIRD_PARTY_NOTICES.md +33 -1
  2. package/dist/adapters/docx-images.d.ts +13 -4
  3. package/dist/adapters/docx-images.js +3 -0
  4. package/dist/adapters/docx-paragraphs.d.ts +65 -0
  5. package/dist/adapters/docx-paragraphs.js +165 -0
  6. package/dist/adapters/docx-prepass.d.ts +17 -0
  7. package/dist/adapters/docx-prepass.js +230 -0
  8. package/dist/adapters/office.d.ts +31 -5
  9. package/dist/adapters/office.js +60 -29
  10. package/dist/adapters/pdf.d.ts +9 -0
  11. package/dist/adapters/pdf.js +10 -0
  12. package/dist/assets/pdfium/pdfium.wasm +0 -0
  13. package/dist/contracts.d.ts +53 -2
  14. package/dist/edit/ai/outline.d.ts +47 -0
  15. package/dist/edit/ai/outline.js +338 -0
  16. package/dist/edit/ai/targets.d.ts +16 -0
  17. package/dist/edit/ai/targets.js +309 -0
  18. package/dist/edit/ai/tools.d.ts +28 -0
  19. package/dist/edit/ai/tools.js +605 -0
  20. package/dist/edit/ai/types.d.ts +175 -0
  21. package/dist/edit/ai/types.js +1 -0
  22. package/dist/edit/assets.d.ts +23 -0
  23. package/dist/edit/assets.js +75 -0
  24. package/dist/edit/docx/elements.d.ts +6 -0
  25. package/dist/edit/docx/elements.js +90 -0
  26. package/dist/edit/docx/engine.d.ts +57 -0
  27. package/dist/edit/docx/engine.js +547 -0
  28. package/dist/edit/docx/handlers.d.ts +3 -0
  29. package/dist/edit/docx/handlers.js +15 -0
  30. package/dist/edit/docx/ids.d.ts +45 -0
  31. package/dist/edit/docx/ids.js +101 -0
  32. package/dist/edit/docx/model.d.ts +94 -0
  33. package/dist/edit/docx/model.js +350 -0
  34. package/dist/edit/docx/operations.d.ts +47 -0
  35. package/dist/edit/docx/operations.js +3 -0
  36. package/dist/edit/docx/provider.d.ts +16 -0
  37. package/dist/edit/docx/provider.js +37 -0
  38. package/dist/edit/docx/schemas.d.ts +6 -0
  39. package/dist/edit/docx/schemas.js +192 -0
  40. package/dist/edit/docx/session.d.ts +48 -0
  41. package/dist/edit/docx/session.js +467 -0
  42. package/dist/edit/docx/structure-ops.d.ts +6 -0
  43. package/dist/edit/docx/structure-ops.js +529 -0
  44. package/dist/edit/docx/style.d.ts +45 -0
  45. package/dist/edit/docx/style.js +375 -0
  46. package/dist/edit/docx/table-ops.d.ts +4 -0
  47. package/dist/edit/docx/table-ops.js +241 -0
  48. package/dist/edit/docx/text-ops.d.ts +40 -0
  49. package/dist/edit/docx/text-ops.js +491 -0
  50. package/dist/edit/docx/text.d.ts +34 -0
  51. package/dist/edit/docx/text.js +284 -0
  52. package/dist/edit/docx/tracked.d.ts +52 -0
  53. package/dist/edit/docx/tracked.js +347 -0
  54. package/dist/edit/docx/types.d.ts +206 -0
  55. package/dist/edit/docx/types.js +1 -0
  56. package/dist/edit/docx/write.d.ts +64 -0
  57. package/dist/edit/docx/write.js +375 -0
  58. package/dist/edit/engine.d.ts +128 -0
  59. package/dist/edit/engine.js +1 -0
  60. package/dist/edit/history.d.ts +77 -0
  61. package/dist/edit/history.js +123 -0
  62. package/dist/edit/ooxml/names.d.ts +20 -0
  63. package/dist/edit/ooxml/names.js +61 -0
  64. package/dist/edit/ooxml/opc.d.ts +50 -0
  65. package/dist/edit/ooxml/opc.js +150 -0
  66. package/dist/edit/ooxml/package.d.ts +82 -0
  67. package/dist/edit/ooxml/package.js +233 -0
  68. package/dist/edit/ooxml/patch.d.ts +51 -0
  69. package/dist/edit/ooxml/patch.js +250 -0
  70. package/dist/edit/ooxml/transaction.d.ts +39 -0
  71. package/dist/edit/ooxml/transaction.js +316 -0
  72. package/dist/edit/ooxml/worker.d.ts +8 -0
  73. package/dist/edit/ooxml/worker.js +12 -0
  74. package/dist/edit/ooxml/writer.d.ts +21 -0
  75. package/dist/edit/ooxml/writer.js +187 -0
  76. package/dist/edit/ooxml/xml.d.ts +74 -0
  77. package/dist/edit/ooxml/xml.js +451 -0
  78. package/dist/edit/ooxml/zip.d.ts +54 -0
  79. package/dist/edit/ooxml/zip.js +280 -0
  80. package/dist/edit/operations.d.ts +19 -0
  81. package/dist/edit/operations.js +137 -0
  82. package/dist/edit/pdf/engine/compact.d.ts +6 -0
  83. package/dist/edit/pdf/engine/compact.js +442 -0
  84. package/dist/edit/pdf/engine/document.d.ts +95 -0
  85. package/dist/edit/pdf/engine/document.js +868 -0
  86. package/dist/edit/pdf/engine/elements.d.ts +45 -0
  87. package/dist/edit/pdf/engine/elements.js +313 -0
  88. package/dist/edit/pdf/engine/existing-text.d.ts +4 -0
  89. package/dist/edit/pdf/engine/existing-text.js +424 -0
  90. package/dist/edit/pdf/engine/fonts.d.ts +78 -0
  91. package/dist/edit/pdf/engine/fonts.js +466 -0
  92. package/dist/edit/pdf/engine/geometry.d.ts +36 -0
  93. package/dist/edit/pdf/engine/geometry.js +97 -0
  94. package/dist/edit/pdf/engine/handler.d.ts +21 -0
  95. package/dist/edit/pdf/engine/handler.js +108 -0
  96. package/dist/edit/pdf/engine/images.d.ts +33 -0
  97. package/dist/edit/pdf/engine/images.js +188 -0
  98. package/dist/edit/pdf/engine/layout.d.ts +23 -0
  99. package/dist/edit/pdf/engine/layout.js +177 -0
  100. package/dist/edit/pdf/engine/operations.d.ts +67 -0
  101. package/dist/edit/pdf/engine/operations.js +3 -0
  102. package/dist/edit/pdf/engine/pages.d.ts +6 -0
  103. package/dist/edit/pdf/engine/pages.js +98 -0
  104. package/dist/edit/pdf/engine/pdfium.d.ts +195 -0
  105. package/dist/edit/pdf/engine/pdfium.js +249 -0
  106. package/dist/edit/pdf/engine/shapes.d.ts +4 -0
  107. package/dist/edit/pdf/engine/shapes.js +183 -0
  108. package/dist/edit/pdf/engine/tables.d.ts +44 -0
  109. package/dist/edit/pdf/engine/tables.js +315 -0
  110. package/dist/edit/pdf/engine/text-box.d.ts +71 -0
  111. package/dist/edit/pdf/engine/text-box.js +317 -0
  112. package/dist/edit/pdf/engine/text-layout.d.ts +28 -0
  113. package/dist/edit/pdf/engine/text-layout.js +67 -0
  114. package/dist/edit/pdf/engine/transform.d.ts +5 -0
  115. package/dist/edit/pdf/engine/transform.js +137 -0
  116. package/dist/edit/pdf/provider.d.ts +35 -0
  117. package/dist/edit/pdf/provider.js +92 -0
  118. package/dist/edit/pdf/range-map.d.ts +16 -0
  119. package/dist/edit/pdf/range-map.js +107 -0
  120. package/dist/edit/pdf/schemas.d.ts +11 -0
  121. package/dist/edit/pdf/schemas.js +291 -0
  122. package/dist/edit/pdf/selection.d.ts +5 -0
  123. package/dist/edit/pdf/selection.js +152 -0
  124. package/dist/edit/pdf/session.d.ts +63 -0
  125. package/dist/edit/pdf/session.js +312 -0
  126. package/dist/edit/pdf/types.d.ts +336 -0
  127. package/dist/edit/pdf/types.js +1 -0
  128. package/dist/edit/pptx/elements.d.ts +74 -0
  129. package/dist/edit/pptx/elements.js +301 -0
  130. package/dist/edit/pptx/engine.d.ts +37 -0
  131. package/dist/edit/pptx/engine.js +466 -0
  132. package/dist/edit/pptx/geometry.d.ts +72 -0
  133. package/dist/edit/pptx/geometry.js +193 -0
  134. package/dist/edit/pptx/handler.d.ts +7 -0
  135. package/dist/edit/pptx/handler.js +99 -0
  136. package/dist/edit/pptx/handlers.d.ts +3 -0
  137. package/dist/edit/pptx/handlers.js +21 -0
  138. package/dist/edit/pptx/image-table-ops.d.ts +5 -0
  139. package/dist/edit/pptx/image-table-ops.js +233 -0
  140. package/dist/edit/pptx/model.d.ts +69 -0
  141. package/dist/edit/pptx/model.js +170 -0
  142. package/dist/edit/pptx/operations.d.ts +50 -0
  143. package/dist/edit/pptx/operations.js +11 -0
  144. package/dist/edit/pptx/provider.d.ts +17 -0
  145. package/dist/edit/pptx/provider.js +40 -0
  146. package/dist/edit/pptx/schemas.d.ts +6 -0
  147. package/dist/edit/pptx/schemas.js +220 -0
  148. package/dist/edit/pptx/session.d.ts +54 -0
  149. package/dist/edit/pptx/session.js +145 -0
  150. package/dist/edit/pptx/shape-ops.d.ts +10 -0
  151. package/dist/edit/pptx/shape-ops.js +486 -0
  152. package/dist/edit/pptx/slide-ops.d.ts +6 -0
  153. package/dist/edit/pptx/slide-ops.js +353 -0
  154. package/dist/edit/pptx/style.d.ts +31 -0
  155. package/dist/edit/pptx/style.js +181 -0
  156. package/dist/edit/pptx/text-ops.d.ts +13 -0
  157. package/dist/edit/pptx/text-ops.js +267 -0
  158. package/dist/edit/pptx/text-write.d.ts +67 -0
  159. package/dist/edit/pptx/text-write.js +293 -0
  160. package/dist/edit/pptx/text.d.ts +41 -0
  161. package/dist/edit/pptx/text.js +99 -0
  162. package/dist/edit/pptx/types.d.ts +232 -0
  163. package/dist/edit/pptx/types.js +1 -0
  164. package/dist/edit/schema.d.ts +11 -0
  165. package/dist/edit/schema.js +275 -0
  166. package/dist/edit/session.d.ts +77 -0
  167. package/dist/edit/session.js +943 -0
  168. package/dist/edit/sessions.d.ts +8 -0
  169. package/dist/edit/sessions.js +1 -0
  170. package/dist/edit/types.d.ts +290 -0
  171. package/dist/edit/types.js +1 -0
  172. package/dist/edit/worker-engine.d.ts +31 -0
  173. package/dist/edit/worker-engine.js +91 -0
  174. package/dist/fonts/THIRD_PARTY_NOTICES.md +3 -0
  175. package/dist/fonts/manifest.json +6 -1
  176. package/dist/fonts/noto-sans-latin-cyrillic.ttf +0 -0
  177. package/dist/fonts.d.ts +2 -0
  178. package/dist/fonts.js +4 -0
  179. package/dist/fuzzy-alignment.d.ts +11 -4
  180. package/dist/fuzzy-alignment.js +3 -9
  181. package/dist/headless.d.ts +5 -1
  182. package/dist/headless.js +8 -1
  183. package/dist/index.d.ts +10 -2
  184. package/dist/index.js +16 -2
  185. package/dist/limits.js +6 -0
  186. package/dist/ooxml-edit-worker.d.ts +1 -0
  187. package/dist/ooxml-edit-worker.js +8 -0
  188. package/dist/pdf-edit-worker.d.ts +1 -0
  189. package/dist/pdf-edit-worker.js +35 -0
  190. package/dist/spreadsheet-viewport.d.ts +5 -0
  191. package/dist/spreadsheet-viewport.js +11 -0
  192. package/dist/ui.js +7 -0
  193. package/dist/viewer.d.ts +6 -0
  194. package/dist/viewer.js +293 -19
  195. package/dist/viewport.d.ts +13 -0
  196. package/dist/viewport.js +126 -1
  197. package/dist/worker-protocol.d.ts +39 -2
  198. package/dist/workers/fuzzy-search-worker.js +1 -1
  199. package/dist/workers/ooxml-edit-worker.js +9212 -0
  200. package/dist/workers/pdf-edit-worker.js +10847 -0
  201. package/package.json +3 -3
@@ -0,0 +1,35 @@
1
+ import { createPdfEditHandler } from "./edit/pdf/engine/handler.js";
2
+ import { Pdfium } from "./edit/pdf/engine/pdfium.js";
3
+ import { attachWorkerEndpoint } from "./worker-endpoint.js";
4
+ /*
5
+ * Module worker that owns a PDFium instance for one edit session. The WASM is
6
+ * fetched from the URL the main thread resolved, so hosting the assets
7
+ * elsewhere (assetBaseUrl) needs no change here.
8
+ */
9
+ async function fetchBytes(url) {
10
+ const response = await fetch(url);
11
+ if (!response.ok)
12
+ throw new Error(`Fetching ${url} failed with HTTP ${response.status}`);
13
+ return new Uint8Array(await response.arrayBuffer());
14
+ }
15
+ attachWorkerEndpoint(self, createPdfEditHandler({
16
+ loadPdfium: async (wasmUrl) => Pdfium.load(await fetchBytes(wasmUrl)),
17
+ fetchBytes,
18
+ decodeImage: async (bytes, mimeType) => {
19
+ const bitmap = await createImageBitmap(new Blob([new Uint8Array(bytes)], { type: mimeType }));
20
+ try {
21
+ const canvas = new OffscreenCanvas(bitmap.width, bitmap.height);
22
+ const context = canvas.getContext("2d");
23
+ context.drawImage(bitmap, 0, 0);
24
+ const { data } = context.getImageData(0, 0, bitmap.width, bitmap.height);
25
+ return {
26
+ width: bitmap.width,
27
+ height: bitmap.height,
28
+ rgba: new Uint8Array(data.buffer),
29
+ };
30
+ }
31
+ finally {
32
+ bitmap.close();
33
+ }
34
+ },
35
+ }));
@@ -22,6 +22,11 @@ export declare class SpreadsheetViewport {
22
22
  #private;
23
23
  constructor(container: HTMLElement, host: ViewportHost);
24
24
  setDocument(info: DocumentInfo | undefined): void;
25
+ /** Sheets are not editable yet, so new content is treated as a new document. */
26
+ replaceDocument(info: DocumentInfo, _changedPages?: readonly number[], _revision?: number): void;
27
+ /** Sheet geometry is cell-addressed; page-space helpers do not apply. */
28
+ pageToClient(): undefined;
29
+ clientToPage(): undefined;
25
30
  update(): void;
26
31
  panBy(deltaX: number, deltaY: number): void;
27
32
  /** Sheet matches are cell-addressed; the sheet viewport scrolls per cell already. */
@@ -203,6 +203,17 @@ export class SpreadsheetViewport {
203
203
  this.#root.scrollTo({ left: 0, top: 0 });
204
204
  this.schedule();
205
205
  }
206
+ /** Sheets are not editable yet, so new content is treated as a new document. */
207
+ replaceDocument(info, _changedPages, _revision) {
208
+ this.setDocument(info);
209
+ }
210
+ /** Sheet geometry is cell-addressed; page-space helpers do not apply. */
211
+ pageToClient() {
212
+ return undefined;
213
+ }
214
+ clientToPage() {
215
+ return undefined;
216
+ }
206
217
  update() {
207
218
  if (!this.#info)
208
219
  return;
package/dist/ui.js CHANGED
@@ -120,6 +120,13 @@ export class BasicViewerUi {
120
120
  if (!this.#thumbnailPanel.hidden)
121
121
  void this.#renderThumbnails();
122
122
  this.#renderSheetTabs();
123
+ }), viewer.on("documentchange", () => {
124
+ // Edited content: counters, tabs and thumbnails follow the new document.
125
+ this.#info = viewer.getDocumentInfo();
126
+ this.#renderSheetTabs();
127
+ this.#syncState(viewer.state);
128
+ if (!this.#thumbnailPanel.hidden)
129
+ void this.#renderThumbnails();
123
130
  }));
124
131
  this.root.addEventListener("keydown", this.#onKeyDown);
125
132
  document.addEventListener("fullscreenchange", this.#onFullscreenChange);
package/dist/viewer.d.ts CHANGED
@@ -1,4 +1,6 @@
1
1
  import type { CellRange, CellSelection, DocumentInfo, DocumentSource, FitMode, HeadlessRenderOptions, OpenDocumentOptions, ResourceLimits, SearchOptions, SearchResult, SpreadsheetViewportRange, TextSelection, TextSelectionRange, ThumbnailRenderOptions, ViewerApi, ViewerEventListener, ViewerEventMap, ViewerFetch, ViewerLogger, ViewerOptions, ViewerState } from "./contracts.js";
2
+ import type { EditOptions, PageHit, PageRect, ViewportRect } from "./edit/types.js";
3
+ import type { EditSession } from "./edit/sessions.js";
2
4
  import type { AdapterRegistry } from "./registry.js";
3
5
  import type { FontManager } from "./fonts.js";
4
6
  import { RenderScheduler } from "./render-scheduler.js";
@@ -47,6 +49,10 @@ export declare class DocumentViewer implements ViewerApi {
47
49
  copySelection(): Promise<string>;
48
50
  getOriginalBytes(): Uint8Array | undefined;
49
51
  downloadOriginal(fileName?: string): Blob;
52
+ edit(options?: EditOptions): Promise<EditSession>;
53
+ getEditSession(): EditSession | undefined;
54
+ pageToClient(pageIndex: number, rect: PageRect): ViewportRect | undefined;
55
+ clientToPage(clientX: number, clientY: number): PageHit | undefined;
50
56
  on<K extends keyof ViewerEventMap>(type: K, listener: ViewerEventListener<K>): () => void;
51
57
  destroy(): Promise<void>;
52
58
  }
package/dist/viewer.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { EditSessionController, } from "./edit/session.js";
1
2
  import { linkedAbortController } from "./abort.js";
2
3
  import { detectFormat } from "./detect.js";
3
4
  import { abortError, normalizeError, ViewerError } from "./errors.js";
@@ -28,6 +29,12 @@ export class DocumentViewer {
28
29
  #info;
29
30
  #original;
30
31
  #originalFileName;
32
+ #originalContentType;
33
+ /** Limits the current document was opened with; reused when it is reopened. */
34
+ #limits;
35
+ #session;
36
+ /** Shared by concurrent `edit()` calls while the engine starts. */
37
+ #sessionStart;
31
38
  #activeLoad;
32
39
  #activeSearch;
33
40
  #selection = null;
@@ -45,6 +52,7 @@ export class DocumentViewer {
45
52
  constructor(options, runtime) {
46
53
  this.#options = options;
47
54
  this.#runtime = runtime;
55
+ this.#limits = runtime.limits;
48
56
  this.#state = freezeState({
49
57
  status: "idle",
50
58
  pageIndex: 0,
@@ -77,6 +85,7 @@ export class DocumentViewer {
77
85
  onTextSelection: (range) => void this.selectText(range),
78
86
  onCellSelection: (selection) => this.#applyInteractiveCellSelection(selection),
79
87
  getSelectionText: () => this.#getCachedSelectionText(),
88
+ onPainted: (pages, revision) => this.#onPainted(pages, revision),
80
89
  }, options);
81
90
  }
82
91
  }
@@ -151,31 +160,21 @@ export class DocumentViewer {
151
160
  await adapter.close(handle);
152
161
  throw abortError();
153
162
  }
154
- if (!Number.isInteger(backendInfo.pageCount) ||
155
- backendInfo.pageCount < 1 ||
156
- backendInfo.pageCount > limits.maxDocumentUnits) {
163
+ let info;
164
+ try {
165
+ info = describeDocument(adapter, backendInfo, detection.format, limits);
166
+ }
167
+ catch (error) {
157
168
  await adapter.close(handle);
158
- throw new ViewerError("resource-limit", "Document unit count exceeds the configured limit", {
159
- details: {
160
- pageCount: backendInfo.pageCount,
161
- limit: limits.maxDocumentUnits,
162
- },
163
- });
169
+ throw error;
164
170
  }
165
- const info = immutableInfo({
166
- ...backendInfo,
167
- capabilities: {
168
- textSelection: Boolean(adapter.getTextMap),
169
- cellSelection: backendInfo.unit === "sheet",
170
- search: Boolean(adapter.getTextMap),
171
- thumbnails: backendInfo.unit !== "sheet",
172
- },
173
- });
174
171
  this.#adapter = adapter;
175
172
  this.#handle = handle;
176
173
  this.#info = info;
177
174
  this.#original = loaded.bytes;
178
175
  this.#originalFileName = fileName;
176
+ this.#originalContentType = contentType;
177
+ this.#limits = limits;
179
178
  this.#update({
180
179
  status: "ready",
181
180
  format: detection.format,
@@ -671,6 +670,45 @@ export class DocumentViewer {
671
670
  }
672
671
  return blob;
673
672
  }
673
+ async edit(options = {}) {
674
+ const { adapter, info } = this.#assertReady();
675
+ if (this.#session?.core.usable)
676
+ return this.#session.session;
677
+ if (this.#sessionStart)
678
+ return this.#sessionStart;
679
+ const format = editableFormat(adapter, info.format);
680
+ if (!format || !adapter.edit)
681
+ throw new ViewerError("edit-unsupported", "Editing is not available for this document", { details: { format: info.format, reason: "no-engine" } });
682
+ const start = this.#startSession(adapter.edit, format, options);
683
+ this.#sessionStart = start;
684
+ try {
685
+ return await start;
686
+ }
687
+ finally {
688
+ if (this.#sessionStart === start)
689
+ this.#sessionStart = undefined;
690
+ }
691
+ }
692
+ getEditSession() {
693
+ this.#assertAlive();
694
+ return this.#session?.session;
695
+ }
696
+ pageToClient(pageIndex, rect) {
697
+ this.#assertAlive();
698
+ if (this.#state.status !== "ready" ||
699
+ !Number.isInteger(pageIndex) ||
700
+ ![rect.x, rect.y, rect.width, rect.height].every(Number.isFinite))
701
+ return undefined;
702
+ return this.#viewport?.pageToClient(pageIndex, rect);
703
+ }
704
+ clientToPage(clientX, clientY) {
705
+ this.#assertAlive();
706
+ if (this.#state.status !== "ready" ||
707
+ !Number.isFinite(clientX) ||
708
+ !Number.isFinite(clientY))
709
+ return undefined;
710
+ return this.#viewport?.clientToPage(clientX, clientY);
711
+ }
674
712
  on(type, listener) {
675
713
  this.#assertAlive();
676
714
  const listeners = this.#listeners.get(type) ?? new Set();
@@ -700,6 +738,194 @@ export class DocumentViewer {
700
738
  this.#listeners.clear();
701
739
  this.#runtime.release(this);
702
740
  }
741
+ async #startSession(provider, format, options) {
742
+ const generation = this.#generation;
743
+ const operation = this.#startOperation(options.signal);
744
+ // A session that broke (failed recovery) makes way for a fresh engine.
745
+ const stale = this.#session;
746
+ this.#session = undefined;
747
+ if (stale)
748
+ await stale.core.end();
749
+ try {
750
+ const engine = await provider.load(this.#original.slice(), {
751
+ format,
752
+ limits: this.#limits,
753
+ fonts: this.#runtime.fonts.registered,
754
+ signal: operation.signal,
755
+ ...(this.#originalFileName ? { fileName: this.#originalFileName } : {}),
756
+ ...(this.#runtime.assetBaseUrl
757
+ ? { assetBaseUrl: this.#runtime.assetBaseUrl }
758
+ : {}),
759
+ });
760
+ if (generation !== this.#generation || operation.signal.aborted) {
761
+ await engine.dispose().catch(() => undefined);
762
+ throw abortError();
763
+ }
764
+ const host = {
765
+ format,
766
+ limits: this.#limits,
767
+ prepareDocument: (bytes, signal) => this.#prepareDocument(bytes, signal),
768
+ commitDocument: (prepared, changedPages) => this.#commitDocument(prepared, changedPages),
769
+ discardDocument: (prepared) => this.#discardDocument(prepared),
770
+ emit: (type, event) => this.#emit(type, event),
771
+ pageOf: (paragraphId) => {
772
+ let first;
773
+ for (const [pageIndex, runs] of this.#textMaps)
774
+ if ((first === undefined || pageIndex < first) &&
775
+ runs.some((run) => run.paragraphId === paragraphId))
776
+ first = pageIndex;
777
+ return first;
778
+ },
779
+ };
780
+ const core = new EditSessionController(engine, host, this.#original, this.#info.pageCount);
781
+ const session = provider.createSession(core, {
782
+ getTextRuns: (pageIndex, signal) => this.#getTextRuns(pageIndex, signal),
783
+ cachedPages: () => [...this.#textMaps.keys()],
784
+ });
785
+ this.#session = { core, session };
786
+ this.#emit("editstatechange", {
787
+ ...core.state,
788
+ active: true,
789
+ format,
790
+ });
791
+ return session;
792
+ }
793
+ catch (error) {
794
+ const normalized = normalizeError(error, "edit-failed");
795
+ throw normalized.code === "edit-failed" && !normalized.details
796
+ ? new ViewerError("edit-failed", normalized.message, {
797
+ cause: error,
798
+ details: { stage: "load" },
799
+ })
800
+ : normalized;
801
+ }
802
+ finally {
803
+ this.#finishOperation(operation);
804
+ }
805
+ }
806
+ /**
807
+ * Shows edited bytes as the current document: opens them through the
808
+ * adapter, swaps the handle, drops every cache derived from the old content
809
+ * and keeps the view where it was. Returns the new page count.
810
+ */
811
+ /**
812
+ * Phase one of showing edited bytes: opens them next to the current
813
+ * document. Everything that can fail or be aborted happens here.
814
+ */
815
+ async #prepareDocument(bytes, signal) {
816
+ const { adapter, handle, info } = this.#assertReady();
817
+ const generation = this.#generation;
818
+ enforceContainerLimits(bytes, info.format, this.#limits);
819
+ const context = {
820
+ format: info.format,
821
+ signal,
822
+ limits: this.#limits,
823
+ reportProgress: (progress) => this.#emit("progress", progress),
824
+ reportWarning: (warning) => this.#emit("warning", warning),
825
+ ...(this.#originalFileName ? { fileName: this.#originalFileName } : {}),
826
+ ...(this.#originalContentType
827
+ ? { contentType: this.#originalContentType }
828
+ : {}),
829
+ ...(this.#runtime.assetBaseUrl
830
+ ? { assetBaseUrl: this.#runtime.assetBaseUrl }
831
+ : {}),
832
+ };
833
+ const next = adapter.reopen
834
+ ? await adapter.reopen(handle, bytes, context)
835
+ : await adapter.open(bytes, context);
836
+ let nextInfo;
837
+ try {
838
+ if (generation !== this.#generation || signal.aborted)
839
+ throw abortError();
840
+ const backendInfo = await adapter.getInfo(next);
841
+ if (generation !== this.#generation || signal.aborted)
842
+ throw abortError();
843
+ nextInfo = describeDocument(adapter, backendInfo, info.format, this.#limits);
844
+ }
845
+ catch (error) {
846
+ await adapter.close(next);
847
+ throw error;
848
+ }
849
+ return {
850
+ pageCount: nextInfo.pageCount,
851
+ handle: next,
852
+ info: nextInfo,
853
+ generation,
854
+ };
855
+ }
856
+ /**
857
+ * Phase two: swaps the prepared document in. Synchronous and never throws
858
+ * for a preparation of the current document, so a session that reached
859
+ * this point always completes; the old handle is closed in the background.
860
+ */
861
+ #commitDocument(prepared, changedPages) {
862
+ if (prepared.generation !== this.#generation || !this.#handle) {
863
+ this.#discardDocument(prepared);
864
+ throw new ViewerError("lifecycle-error", "The document was replaced while an edit was being shown");
865
+ }
866
+ const { adapter, handle: previous } = this.#assertReady();
867
+ this.#handle = prepared.handle;
868
+ this.#info = prepared.info;
869
+ this.#invalidateContentCaches();
870
+ this.#update({
871
+ pageCount: prepared.info.pageCount,
872
+ pageIndex: Math.min(this.#state.pageIndex, prepared.info.pageCount - 1),
873
+ });
874
+ for (const warning of prepared.info.warnings ?? [])
875
+ this.#emit("warning", warning);
876
+ const revision = (this.#session?.core.state.revision ?? 0) + 1;
877
+ if (this.#viewport)
878
+ this.#viewport.replaceDocument(prepared.info, changedPages, revision);
879
+ else
880
+ // Headless: nothing paints, so geometry is "current" as soon as the
881
+ // session has emitted documentchange, which happens before the next
882
+ // macrotask.
883
+ setTimeout(() => this.#onPainted([...changedPages], revision), 0);
884
+ void Promise.resolve(adapter.close(previous)).catch((error) => this.#reportError(error));
885
+ return prepared.info.pageCount;
886
+ }
887
+ #discardDocument(prepared) {
888
+ void Promise.resolve(this.#adapter?.close(prepared.handle)).catch((error) => this.#reportError(error));
889
+ }
890
+ /** Painted pages of a content revision become a `layoutchange` while a session is active. */
891
+ #onPainted(pages, revision) {
892
+ const session = this.#session;
893
+ // The viewport reports only paints of its current content, so a revision
894
+ // the session has not committed yet is the only thing to drop.
895
+ if (!session || revision > session.core.state.revision)
896
+ return;
897
+ this.#emit("layoutchange", {
898
+ sessionId: session.core.sessionId,
899
+ revision,
900
+ pages: Object.freeze([...pages]),
901
+ });
902
+ }
903
+ #reportError(error) {
904
+ const report = globalThis
905
+ .reportError;
906
+ if (report)
907
+ report(error);
908
+ else
909
+ console.error(error);
910
+ }
911
+ /** Forgets everything derived from the document's content, not from its view. */
912
+ #invalidateContentCaches() {
913
+ this.#textMaps.clear();
914
+ this.#textMapBytes = 0;
915
+ // The worker keeps its index; a stale key makes the next search rebuild it.
916
+ this.#fuzzyIndexKey = undefined;
917
+ this.#activeSearch?.abort();
918
+ this.#activeSearch = undefined;
919
+ this.#searchGeneration += 1;
920
+ if (this.#searchResult) {
921
+ this.#searchResult = null;
922
+ this.#emit("searchchange", null);
923
+ }
924
+ if (this.#selection) {
925
+ this.#selection = null;
926
+ this.#emit("selectionchange", null);
927
+ }
928
+ }
703
929
  async #getTextRuns(pageIndex, signal) {
704
930
  const { adapter, handle, info } = this.#assertReady();
705
931
  assertPageIndex(pageIndex, info.pageCount);
@@ -779,6 +1005,11 @@ export class DocumentViewer {
779
1005
  this.#update({ panX: x, panY: y });
780
1006
  }
781
1007
  async #closeDocument() {
1008
+ const session = this.#session;
1009
+ this.#session = undefined;
1010
+ this.#sessionStart = undefined;
1011
+ if (session)
1012
+ await session.core.end();
782
1013
  const adapter = this.#adapter;
783
1014
  const handle = this.#handle;
784
1015
  this.#adapter = undefined;
@@ -786,6 +1017,7 @@ export class DocumentViewer {
786
1017
  this.#info = undefined;
787
1018
  this.#original = undefined;
788
1019
  this.#originalFileName = undefined;
1020
+ this.#originalContentType = undefined;
789
1021
  this.#activeSearch?.abort();
790
1022
  this.#activeSearch = undefined;
791
1023
  this.#searchResult = null;
@@ -856,9 +1088,51 @@ export class DocumentViewer {
856
1088
  }
857
1089
  #emit(type, event) {
858
1090
  for (const listener of this.#listeners.get(type) ?? [])
859
- listener(event);
1091
+ try {
1092
+ listener(event);
1093
+ }
1094
+ catch (error) {
1095
+ // A listener's bug is its own; it never fails the call that emitted.
1096
+ this.#reportError(error);
1097
+ }
860
1098
  }
861
1099
  }
1100
+ /** The edit format of `format` when `adapter` can edit it. */
1101
+ /** The session format of a document format the adapter's provider edits. */
1102
+ function editableFormat(adapter, format) {
1103
+ const formats = adapter.edit?.formats ?? [];
1104
+ return formats.includes(format) ? SESSION_FORMATS[format] : undefined;
1105
+ }
1106
+ const SESSION_FORMATS = {
1107
+ pdf: "pdf",
1108
+ pptx: "pptx",
1109
+ pptm: "pptx",
1110
+ ppsx: "pptx",
1111
+ docx: "docx",
1112
+ docm: "docx",
1113
+ };
1114
+ /** Validates backend info and freezes it with the viewer's capability flags. */
1115
+ function describeDocument(adapter, backendInfo, format, limits) {
1116
+ if (!Number.isInteger(backendInfo.pageCount) ||
1117
+ backendInfo.pageCount < 1 ||
1118
+ backendInfo.pageCount > limits.maxDocumentUnits)
1119
+ throw new ViewerError("resource-limit", "Document unit count exceeds the configured limit", {
1120
+ details: {
1121
+ pageCount: backendInfo.pageCount,
1122
+ limit: limits.maxDocumentUnits,
1123
+ },
1124
+ });
1125
+ return immutableInfo({
1126
+ ...backendInfo,
1127
+ capabilities: {
1128
+ textSelection: Boolean(adapter.getTextMap),
1129
+ cellSelection: backendInfo.unit === "sheet",
1130
+ search: Boolean(adapter.getTextMap),
1131
+ thumbnails: backendInfo.unit !== "sheet",
1132
+ editing: editableFormat(adapter, format) !== undefined,
1133
+ },
1134
+ });
1135
+ }
862
1136
  function clampZoom(value) {
863
1137
  if (!Number.isFinite(value))
864
1138
  return 1;
@@ -1,4 +1,5 @@
1
1
  import type { CellRange, CellSelection, DocumentInfo, HeadlessRenderOptions, SearchMatch, TextRun, TextSelectionRange, ViewerState, SpreadsheetViewportRange } from "./contracts.js";
2
+ import type { PageHit, PageRect, ViewportRect } from "./edit/types.js";
2
3
  export interface ViewportHost {
3
4
  readonly state: ViewerState;
4
5
  renderPage(pageIndex: number, target: HTMLCanvasElement, options: HeadlessRenderOptions): Promise<void>;
@@ -12,9 +13,15 @@ export interface ViewportHost {
12
13
  onTextSelection(range: TextSelectionRange): void;
13
14
  onCellSelection(selection: CellRange | CellSelection | null): void;
14
15
  getSelectionText(): string | undefined;
16
+ /** Pages whose paint for content revision `revision` just finished. */
17
+ onPainted(pages: readonly number[], revision: number): void;
15
18
  }
16
19
  interface ViewportStrategy {
17
20
  setDocument(info: DocumentInfo | undefined): void;
21
+ /** New content of the same document: keeps zoom and scroll, re-renders. */
22
+ replaceDocument(info: DocumentInfo, changedPages?: readonly number[], revision?: number): void;
23
+ pageToClient(pageIndex: number, rect: PageRect): ViewportRect | undefined;
24
+ clientToPage(clientX: number, clientY: number): PageHit | undefined;
18
25
  update(): void;
19
26
  panBy(deltaX: number, deltaY: number): void;
20
27
  goToPage(pageIndex: number): void;
@@ -30,6 +37,9 @@ export declare class AdaptiveViewport implements ViewportStrategy {
30
37
  readonly layout?: "continuous" | "single";
31
38
  });
32
39
  setDocument(info: DocumentInfo | undefined): void;
40
+ replaceDocument(info: DocumentInfo, changedPages?: readonly number[], revision?: number): void;
41
+ pageToClient(pageIndex: number, rect: PageRect): ViewportRect | undefined;
42
+ clientToPage(clientX: number, clientY: number): PageHit | undefined;
33
43
  update(): void;
34
44
  panBy(deltaX: number, deltaY: number): void;
35
45
  goToPage(pageIndex: number): void;
@@ -45,6 +55,9 @@ export declare class ViewerViewport {
45
55
  readonly layout?: "continuous" | "single";
46
56
  });
47
57
  setDocument(info: DocumentInfo | undefined): void;
58
+ replaceDocument(info: DocumentInfo, changedPages?: readonly number[], revision?: number): void;
59
+ pageToClient(pageIndex: number, rect: PageRect): ViewportRect | undefined;
60
+ clientToPage(clientX: number, clientY: number): PageHit | undefined;
48
61
  update(): void;
49
62
  panBy(deltaX: number, deltaY: number): void;
50
63
  goToPage(pageIndex: number): void;