web-doc 0.6.2 → 0.7.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 (188) hide show
  1. package/THIRD_PARTY_NOTICES.md +26 -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 +47 -2
  14. package/dist/edit/assets.d.ts +23 -0
  15. package/dist/edit/assets.js +75 -0
  16. package/dist/edit/docx/elements.d.ts +6 -0
  17. package/dist/edit/docx/elements.js +90 -0
  18. package/dist/edit/docx/engine.d.ts +51 -0
  19. package/dist/edit/docx/engine.js +473 -0
  20. package/dist/edit/docx/handlers.d.ts +3 -0
  21. package/dist/edit/docx/handlers.js +15 -0
  22. package/dist/edit/docx/ids.d.ts +45 -0
  23. package/dist/edit/docx/ids.js +101 -0
  24. package/dist/edit/docx/model.d.ts +92 -0
  25. package/dist/edit/docx/model.js +339 -0
  26. package/dist/edit/docx/operations.d.ts +37 -0
  27. package/dist/edit/docx/operations.js +3 -0
  28. package/dist/edit/docx/provider.d.ts +13 -0
  29. package/dist/edit/docx/provider.js +34 -0
  30. package/dist/edit/docx/schemas.d.ts +6 -0
  31. package/dist/edit/docx/schemas.js +192 -0
  32. package/dist/edit/docx/session.d.ts +37 -0
  33. package/dist/edit/docx/session.js +426 -0
  34. package/dist/edit/docx/structure-ops.d.ts +6 -0
  35. package/dist/edit/docx/structure-ops.js +494 -0
  36. package/dist/edit/docx/style.d.ts +45 -0
  37. package/dist/edit/docx/style.js +375 -0
  38. package/dist/edit/docx/table-ops.d.ts +4 -0
  39. package/dist/edit/docx/table-ops.js +219 -0
  40. package/dist/edit/docx/text-ops.d.ts +18 -0
  41. package/dist/edit/docx/text-ops.js +458 -0
  42. package/dist/edit/docx/text.d.ts +34 -0
  43. package/dist/edit/docx/text.js +263 -0
  44. package/dist/edit/docx/types.d.ts +191 -0
  45. package/dist/edit/docx/types.js +1 -0
  46. package/dist/edit/docx/write.d.ts +50 -0
  47. package/dist/edit/docx/write.js +352 -0
  48. package/dist/edit/engine.d.ts +117 -0
  49. package/dist/edit/engine.js +1 -0
  50. package/dist/edit/history.d.ts +53 -0
  51. package/dist/edit/history.js +101 -0
  52. package/dist/edit/ooxml/names.d.ts +20 -0
  53. package/dist/edit/ooxml/names.js +61 -0
  54. package/dist/edit/ooxml/opc.d.ts +50 -0
  55. package/dist/edit/ooxml/opc.js +150 -0
  56. package/dist/edit/ooxml/package.d.ts +82 -0
  57. package/dist/edit/ooxml/package.js +233 -0
  58. package/dist/edit/ooxml/patch.d.ts +51 -0
  59. package/dist/edit/ooxml/patch.js +250 -0
  60. package/dist/edit/ooxml/transaction.d.ts +39 -0
  61. package/dist/edit/ooxml/transaction.js +316 -0
  62. package/dist/edit/ooxml/worker.d.ts +8 -0
  63. package/dist/edit/ooxml/worker.js +12 -0
  64. package/dist/edit/ooxml/writer.d.ts +21 -0
  65. package/dist/edit/ooxml/writer.js +187 -0
  66. package/dist/edit/ooxml/xml.d.ts +74 -0
  67. package/dist/edit/ooxml/xml.js +451 -0
  68. package/dist/edit/ooxml/zip.d.ts +54 -0
  69. package/dist/edit/ooxml/zip.js +280 -0
  70. package/dist/edit/operations.d.ts +19 -0
  71. package/dist/edit/operations.js +137 -0
  72. package/dist/edit/pdf/engine/compact.d.ts +6 -0
  73. package/dist/edit/pdf/engine/compact.js +442 -0
  74. package/dist/edit/pdf/engine/document.d.ts +95 -0
  75. package/dist/edit/pdf/engine/document.js +868 -0
  76. package/dist/edit/pdf/engine/elements.d.ts +45 -0
  77. package/dist/edit/pdf/engine/elements.js +313 -0
  78. package/dist/edit/pdf/engine/existing-text.d.ts +4 -0
  79. package/dist/edit/pdf/engine/existing-text.js +424 -0
  80. package/dist/edit/pdf/engine/fonts.d.ts +78 -0
  81. package/dist/edit/pdf/engine/fonts.js +466 -0
  82. package/dist/edit/pdf/engine/geometry.d.ts +36 -0
  83. package/dist/edit/pdf/engine/geometry.js +97 -0
  84. package/dist/edit/pdf/engine/handler.d.ts +21 -0
  85. package/dist/edit/pdf/engine/handler.js +108 -0
  86. package/dist/edit/pdf/engine/images.d.ts +33 -0
  87. package/dist/edit/pdf/engine/images.js +188 -0
  88. package/dist/edit/pdf/engine/layout.d.ts +23 -0
  89. package/dist/edit/pdf/engine/layout.js +177 -0
  90. package/dist/edit/pdf/engine/operations.d.ts +67 -0
  91. package/dist/edit/pdf/engine/operations.js +3 -0
  92. package/dist/edit/pdf/engine/pages.d.ts +6 -0
  93. package/dist/edit/pdf/engine/pages.js +98 -0
  94. package/dist/edit/pdf/engine/pdfium.d.ts +195 -0
  95. package/dist/edit/pdf/engine/pdfium.js +249 -0
  96. package/dist/edit/pdf/engine/shapes.d.ts +4 -0
  97. package/dist/edit/pdf/engine/shapes.js +183 -0
  98. package/dist/edit/pdf/engine/tables.d.ts +44 -0
  99. package/dist/edit/pdf/engine/tables.js +315 -0
  100. package/dist/edit/pdf/engine/text-box.d.ts +71 -0
  101. package/dist/edit/pdf/engine/text-box.js +317 -0
  102. package/dist/edit/pdf/engine/text-layout.d.ts +28 -0
  103. package/dist/edit/pdf/engine/text-layout.js +67 -0
  104. package/dist/edit/pdf/engine/transform.d.ts +5 -0
  105. package/dist/edit/pdf/engine/transform.js +137 -0
  106. package/dist/edit/pdf/provider.d.ts +35 -0
  107. package/dist/edit/pdf/provider.js +92 -0
  108. package/dist/edit/pdf/range-map.d.ts +14 -0
  109. package/dist/edit/pdf/range-map.js +107 -0
  110. package/dist/edit/pdf/schemas.d.ts +11 -0
  111. package/dist/edit/pdf/schemas.js +291 -0
  112. package/dist/edit/pdf/selection.d.ts +5 -0
  113. package/dist/edit/pdf/selection.js +152 -0
  114. package/dist/edit/pdf/session.d.ts +53 -0
  115. package/dist/edit/pdf/session.js +273 -0
  116. package/dist/edit/pdf/types.d.ts +336 -0
  117. package/dist/edit/pdf/types.js +1 -0
  118. package/dist/edit/pptx/elements.d.ts +74 -0
  119. package/dist/edit/pptx/elements.js +301 -0
  120. package/dist/edit/pptx/engine.d.ts +37 -0
  121. package/dist/edit/pptx/engine.js +466 -0
  122. package/dist/edit/pptx/geometry.d.ts +72 -0
  123. package/dist/edit/pptx/geometry.js +193 -0
  124. package/dist/edit/pptx/handler.d.ts +7 -0
  125. package/dist/edit/pptx/handler.js +91 -0
  126. package/dist/edit/pptx/handlers.d.ts +3 -0
  127. package/dist/edit/pptx/handlers.js +21 -0
  128. package/dist/edit/pptx/image-table-ops.d.ts +5 -0
  129. package/dist/edit/pptx/image-table-ops.js +233 -0
  130. package/dist/edit/pptx/model.d.ts +69 -0
  131. package/dist/edit/pptx/model.js +170 -0
  132. package/dist/edit/pptx/operations.d.ts +50 -0
  133. package/dist/edit/pptx/operations.js +11 -0
  134. package/dist/edit/pptx/provider.d.ts +17 -0
  135. package/dist/edit/pptx/provider.js +40 -0
  136. package/dist/edit/pptx/schemas.d.ts +6 -0
  137. package/dist/edit/pptx/schemas.js +220 -0
  138. package/dist/edit/pptx/session.d.ts +44 -0
  139. package/dist/edit/pptx/session.js +114 -0
  140. package/dist/edit/pptx/shape-ops.d.ts +10 -0
  141. package/dist/edit/pptx/shape-ops.js +486 -0
  142. package/dist/edit/pptx/slide-ops.d.ts +6 -0
  143. package/dist/edit/pptx/slide-ops.js +353 -0
  144. package/dist/edit/pptx/style.d.ts +31 -0
  145. package/dist/edit/pptx/style.js +181 -0
  146. package/dist/edit/pptx/text-ops.d.ts +13 -0
  147. package/dist/edit/pptx/text-ops.js +267 -0
  148. package/dist/edit/pptx/text-write.d.ts +67 -0
  149. package/dist/edit/pptx/text-write.js +293 -0
  150. package/dist/edit/pptx/text.d.ts +41 -0
  151. package/dist/edit/pptx/text.js +99 -0
  152. package/dist/edit/pptx/types.d.ts +232 -0
  153. package/dist/edit/pptx/types.js +1 -0
  154. package/dist/edit/schema.d.ts +11 -0
  155. package/dist/edit/schema.js +275 -0
  156. package/dist/edit/session.d.ts +66 -0
  157. package/dist/edit/session.js +701 -0
  158. package/dist/edit/sessions.d.ts +8 -0
  159. package/dist/edit/sessions.js +1 -0
  160. package/dist/edit/types.d.ts +276 -0
  161. package/dist/edit/types.js +1 -0
  162. package/dist/edit/worker-engine.d.ts +31 -0
  163. package/dist/edit/worker-engine.js +91 -0
  164. package/dist/fonts/THIRD_PARTY_NOTICES.md +3 -0
  165. package/dist/fonts/manifest.json +6 -1
  166. package/dist/fonts/noto-sans-latin-cyrillic.ttf +0 -0
  167. package/dist/fonts.d.ts +2 -0
  168. package/dist/fonts.js +4 -0
  169. package/dist/headless.d.ts +4 -0
  170. package/dist/headless.js +4 -0
  171. package/dist/index.d.ts +5 -0
  172. package/dist/index.js +5 -0
  173. package/dist/limits.js +3 -0
  174. package/dist/ooxml-edit-worker.d.ts +1 -0
  175. package/dist/ooxml-edit-worker.js +8 -0
  176. package/dist/pdf-edit-worker.d.ts +1 -0
  177. package/dist/pdf-edit-worker.js +35 -0
  178. package/dist/spreadsheet-viewport.d.ts +5 -0
  179. package/dist/spreadsheet-viewport.js +11 -0
  180. package/dist/ui.js +7 -0
  181. package/dist/viewer.d.ts +6 -0
  182. package/dist/viewer.js +293 -19
  183. package/dist/viewport.d.ts +13 -0
  184. package/dist/viewport.js +126 -1
  185. package/dist/worker-protocol.d.ts +39 -2
  186. package/dist/workers/ooxml-edit-worker.js +8671 -0
  187. package/dist/workers/pdf-edit-worker.js +10844 -0
  188. package/package.json +3 -3
@@ -1,5 +1,8 @@
1
1
  import { abortError, ViewerError } from "../errors.js";
2
- import { fitInlineImagesToPage } from "./docx-images.js";
2
+ import { DocxSession } from "../edit/docx/session.js";
3
+ import { PptxSession } from "../edit/pptx/session.js";
4
+ import { createDocxParagraphIdResolver, } from "./docx-paragraphs.js";
5
+ import { prepareDocxForDisplay } from "./docx-prepass.js";
3
6
  import { enforceContainerLimits } from "../limits.js";
4
7
  const MODERN_FORMATS = [
5
8
  "docx",
@@ -17,12 +20,38 @@ const ROW_HEADER_WIDTH = 50;
17
20
  const COLUMN_HEADER_HEIGHT = 22;
18
21
  export class OfficeDocumentAdapter {
19
22
  id = "office";
23
+ /**
24
+ * PPTX and DOCX editing on the OOXML package layer. The worker and the
25
+ * engine client are imported on the first `edit()`; viewing never loads
26
+ * them.
27
+ */
28
+ edit = {
29
+ formats: ["pptx", "pptm", "ppsx", "docx", "docm"],
30
+ load: async (original, context) => context.format === "docx"
31
+ ? (await import("../edit/docx/provider.js")).loadDocxEditEngine(original, context, this.#options.edit ?? {})
32
+ : (await import("../edit/pptx/provider.js")).loadPptxEditEngine(original, context, this.#options.edit ?? {}),
33
+ createSession: (core, access) => core.format === "docx"
34
+ ? new DocxSession(core, access)
35
+ : new PptxSession(core),
36
+ };
20
37
  formats = [...MODERN_FORMATS, ...LEGACY_FORMATS];
21
38
  #options;
22
39
  constructor(options = {}) {
23
40
  this.#options = options;
24
41
  }
25
42
  async open(input, context) {
43
+ return this.#open(input, context, false);
44
+ }
45
+ /**
46
+ * Edited bytes open as a fresh document (the engine owns no reusable
47
+ * state); a presentation lays its slides out progressively so the slide
48
+ * on screen paints without waiting for the whole deck — Firefox takes
49
+ * seconds for a 500-slide preflight. The viewer closes `previous`.
50
+ */
51
+ reopen(_previous, data, context) {
52
+ return this.#open(data, context, true);
53
+ }
54
+ async #open(input, context, reopening) {
26
55
  throwIfAborted(context.signal);
27
56
  let format = context.format;
28
57
  let data = input;
@@ -57,12 +86,16 @@ export class OfficeDocumentAdapter {
57
86
  useGoogleFonts: false,
58
87
  maxZipEntryBytes: context.limits.maxZipEntryBytes,
59
88
  mode: "main",
89
+ ...(reopening ? { progressiveLayout: true } : {}),
60
90
  };
61
- const buffer = exactArrayBuffer(data);
62
91
  try {
63
92
  const kind = kindFor(format);
64
93
  if (kind === "document") {
65
- const backend = await this.#loadDocx(buffer, engineOptions);
94
+ // What the renderer sees: oversized inline pictures fitted and every
95
+ // paragraph carrying an id; the bytes a session saves are the input.
96
+ const display = await prepareDocxForDisplay(data, context.limits, context.signal);
97
+ throwIfAborted(context.signal);
98
+ const backend = await this.#loadDocx(exactArrayBuffer(display.bytes), engineOptions);
66
99
  throwIfAborted(context.signal, backend);
67
100
  context.reportProgress({
68
101
  phase: "parsing",
@@ -70,8 +103,15 @@ export class OfficeDocumentAdapter {
70
103
  total: 1,
71
104
  ratio: 1,
72
105
  });
73
- return { kind, format: context.format, backend, warnings };
106
+ return {
107
+ kind,
108
+ format: context.format,
109
+ backend,
110
+ warnings,
111
+ paragraphIdOf: createDocxParagraphIdResolver(docxModelOf(backend)),
112
+ };
74
113
  }
114
+ const buffer = exactArrayBuffer(data);
75
115
  if (kind === "presentation") {
76
116
  const backend = await this.#loadPptx(buffer, engineOptions);
77
117
  throwIfAborted(context.signal, backend);
@@ -232,6 +272,7 @@ export class OfficeDocumentAdapter {
232
272
  return runs.map((run) => {
233
273
  const logicalStart = logicalOffset;
234
274
  logicalOffset += run.text.length;
275
+ const paragraphId = run.paragraphId ?? handle.paragraphIdOf(run.source);
235
276
  return {
236
277
  text: run.text,
237
278
  x: run.x,
@@ -240,6 +281,7 @@ export class OfficeDocumentAdapter {
240
281
  height: run.h,
241
282
  font: run.font,
242
283
  fontSize: run.fontSize,
284
+ ...(paragraphId === undefined ? {} : { paragraphId }),
243
285
  ...(run.letterSpacingPx === undefined
244
286
  ? {}
245
287
  : { letterSpacingPx: run.letterSpacingPx }),
@@ -326,11 +368,10 @@ export class OfficeDocumentAdapter {
326
368
  return convertInWorker(data, format, workerUrl, moduleUrl, context.signal, context.limits.maxOperationMs);
327
369
  }
328
370
  async #loadDocx(data, options) {
329
- const backend = this.#options.engines?.docx
330
- ? await this.#options.engines.docx(data, options)
331
- : await (await import("@silurus/ooxml/docx")).DocxDocument.load(data, options);
332
- fitDocxInlineImages(backend);
333
- return backend;
371
+ if (this.#options.engines?.docx)
372
+ return this.#options.engines.docx(data, options);
373
+ const { DocxDocument } = await import("@silurus/ooxml/docx");
374
+ return DocxDocument.load(data, options);
334
375
  }
335
376
  async #loadXlsx(data, options) {
336
377
  if (this.#options.engines?.xlsx)
@@ -341,13 +382,22 @@ export class OfficeDocumentAdapter {
341
382
  async #loadPptx(data, options) {
342
383
  if (this.#options.engines?.pptx)
343
384
  return this.#options.engines.pptx(data, options);
344
- const { PptxPresentation } = await import("@silurus/ooxml-pptx/pptx");
385
+ const { PptxPresentation } = await import("@silurus/ooxml/pptx");
345
386
  return PptxPresentation.load(data, options);
346
387
  }
347
388
  }
348
389
  export function createOfficeAdapter(options = {}) {
349
390
  return new OfficeDocumentAdapter(options);
350
391
  }
392
+ /** The engine's model when it is reachable; a worker-mode getter throws. */
393
+ function docxModelOf(backend) {
394
+ try {
395
+ return backend.document;
396
+ }
397
+ catch {
398
+ return undefined;
399
+ }
400
+ }
351
401
  export function sanitizeOfficeHyperlink(target) {
352
402
  if (!target)
353
403
  return undefined;
@@ -672,22 +722,3 @@ function normalizeOfficeError(error) {
672
722
  function textDirection(text) {
673
723
  return /[\u0590-\u08ff\ufb1d-\ufefc]/u.test(text) ? "rtl" : "ltr";
674
724
  }
675
- /**
676
- * Oversized inline pictures are shrunk to the section's content box before
677
- * the engine paginates (the layout is built lazily on first page access).
678
- * The model is reachable in `main` mode only; a worker-mode engine keeps
679
- * Word's geometry.
680
- */
681
- function fitDocxInlineImages(backend) {
682
- if (backend.mode === "worker")
683
- return;
684
- let model;
685
- try {
686
- model = backend.document;
687
- }
688
- catch {
689
- return;
690
- }
691
- if (model)
692
- fitInlineImagesToPage(model);
693
- }
@@ -1,4 +1,6 @@
1
1
  import type { AdapterOpenContext, DocumentAdapter, DocumentInfo, PageSize, RenderViewport, TextRun } from "../contracts.js";
2
+ import type { EditEngineProvider } from "../edit/engine.js";
3
+ import type { PdfEditProviderOptions } from "../edit/pdf/provider.js";
2
4
  export interface PdfBackend {
3
5
  readonly pageCount: number;
4
6
  pageSize?(pageIndex: number, signal?: AbortSignal): PageSize | Promise<PageSize>;
@@ -13,6 +15,8 @@ export interface PdfAdapterOptions {
13
15
  readonly wasmUrl?: string | URL;
14
16
  readonly iccUrl?: string | URL;
15
17
  readonly open?: (data: Uint8Array, context: AdapterOpenContext) => Promise<PdfBackend>;
18
+ /** Where the PDF edit worker and its WebAssembly live. */
19
+ readonly edit?: PdfEditProviderOptions;
16
20
  }
17
21
  interface PdfHandle {
18
22
  readonly backend: PdfBackend;
@@ -21,6 +25,11 @@ export declare class PdfDocumentAdapter implements DocumentAdapter<PdfHandle> {
21
25
  #private;
22
26
  readonly id = "pdf";
23
27
  readonly formats: readonly ["pdf"];
28
+ /**
29
+ * PDFium-backed editing. The worker, its WebAssembly and the engine client
30
+ * are imported on the first `edit()`; viewing never loads them.
31
+ */
32
+ readonly edit: EditEngineProvider;
24
33
  constructor(options?: PdfAdapterOptions);
25
34
  open(data: Uint8Array, context: AdapterOpenContext): Promise<PdfHandle>;
26
35
  getInfo(handle: PdfHandle): Promise<DocumentInfo>;
@@ -1,4 +1,5 @@
1
1
  import { abortError, ViewerError } from "../errors.js";
2
+ import { PdfSession } from "../edit/pdf/session.js";
2
3
  // Preserve the public zoom=1 contract: one PDF point maps to one renderer CSS
3
4
  // pixel. Per-page geometry still prevents the viewport from coercing pages to
4
5
  // A4 or changing their width while virtualizing.
@@ -7,6 +8,15 @@ export class PdfDocumentAdapter {
7
8
  id = "pdf";
8
9
  formats = ["pdf"];
9
10
  #options;
11
+ /**
12
+ * PDFium-backed editing. The worker, its WebAssembly and the engine client
13
+ * are imported on the first `edit()`; viewing never loads them.
14
+ */
15
+ edit = {
16
+ formats: ["pdf"],
17
+ load: async (original, context) => (await import("../edit/pdf/provider.js")).loadPdfEditEngine(original, context, this.#options.edit ?? {}),
18
+ createSession: (core) => new PdfSession(core),
19
+ };
10
20
  constructor(options = {}) {
11
21
  this.#options = options;
12
22
  }
Binary file
@@ -1,11 +1,22 @@
1
+ import type { EditEngineProvider } from "./edit/engine.js";
2
+ import type { EditSession } from "./edit/sessions.js";
3
+ import type { DocumentChange, LayoutChange, EditOptions, EditStateChange, PageHit, PageRect, ViewportRect } from "./edit/types.js";
1
4
  export declare const supportedFormats: readonly ["docx", "docm", "xlsx", "xlsm", "pptx", "pptm", "ppsx", "doc", "xls", "ppt", "pdf", "csv", "tsv", "png", "jpeg", "gif", "webp", "svg", "bmp", "tiff"];
2
5
  export type DocumentFormat = (typeof supportedFormats)[number];
3
6
  export type ViewerLocale = "en" | "ru";
4
7
  export type FitMode = "none" | "page" | "width";
5
8
  export type ViewerStatus = "idle" | "loading" | "ready" | "error" | "destroyed";
6
9
  export type RenderUnit = "page" | "slide" | "sheet" | "image";
7
- export type ViewerErrorCode = "unsupported-format" | "fidelity-unsupported" | "invalid-file" | "encrypted-document" | "resource-limit" | "network-error" | "aborted" | "font-unavailable" | "render-failed" | "worker-crashed" | "lifecycle-error" | "internal";
8
- export type ViewerWarningCode = "format-hint-mismatch" | "unsupported-feature" | "font-substitution" | "font-unavailable" | "external-resource-blocked" | "fidelity-degraded";
10
+ export type ViewerErrorCode = "unsupported-format" | "fidelity-unsupported" | "invalid-file" | "encrypted-document" | "resource-limit" | "network-error" | "aborted" | "font-unavailable" | "render-failed" | "worker-crashed" | "lifecycle-error" | "edit-unsupported" | "invalid-operation" | "edit-conflict" | "edit-failed"
11
+ /** An OOXML package the editing layer cannot open: ZIP64, encryption, an unknown method, several disks. */
12
+ | "unsupported-package"
13
+ /** An OOXML part that can be read but not patched: not UTF-8, or not byte-stable through decoding. */
14
+ | "unsupported-part"
15
+ /** An XML part the scanner cannot parse. */
16
+ | "malformed-xml"
17
+ /** A patch that overlaps, is stale, is not well-formed, or does not read back as expected. */
18
+ | "invalid-patch" | "internal";
19
+ export type ViewerWarningCode = "format-hint-mismatch" | "unsupported-feature" | "font-substitution" | "font-unavailable" | "external-resource-blocked" | "fidelity-degraded" | "privacy-not-guaranteed";
9
20
  export interface ViewerErrorData {
10
21
  readonly name: "ViewerError";
11
22
  readonly code: ViewerErrorCode;
@@ -34,6 +45,12 @@ export interface ResourceLimits {
34
45
  readonly maxDocumentUnits: number;
35
46
  readonly maxConcurrentRenders: number;
36
47
  readonly maxOperationMs: number;
48
+ /** Operations in one `EditSession.apply()` call. */
49
+ readonly maxEditOperations: number;
50
+ /** Undoable edit batches kept; older ones are folded into the starting point. */
51
+ readonly maxEditHistory: number;
52
+ /** Memory for retained edit checkpoints; fewer are kept when a file is big. */
53
+ readonly maxEditCheckpointBytes: number;
37
54
  }
38
55
  export type BinaryDocumentSource = ArrayBuffer | Uint8Array | Blob;
39
56
  export type DocumentSource = BinaryDocumentSource | URL | string;
@@ -164,6 +181,14 @@ export interface TextRun {
164
181
  readonly hyperlink?: HyperlinkTarget;
165
182
  readonly row?: number;
166
183
  readonly column?: number;
184
+ /**
185
+ * DOCX: the `w:p` of the source XML this run was laid out from, as the
186
+ * file's `w14:paraId` or the deterministic id the viewer assigns to a
187
+ * paragraph without one (eight hex digits). The runs of a paragraph that
188
+ * continues on the next page share it. Absent when the run belongs to no
189
+ * source paragraph.
190
+ */
191
+ readonly paragraphId?: string;
167
192
  }
168
193
  export type HyperlinkTarget = {
169
194
  readonly kind: "external";
@@ -216,6 +241,9 @@ export interface ViewerEventMap {
216
241
  };
217
242
  readonly viewchange: ViewerState;
218
243
  readonly searchchange: SearchResult | null;
244
+ readonly editstatechange: EditStateChange;
245
+ readonly documentchange: DocumentChange;
246
+ readonly layoutchange: LayoutChange;
219
247
  }
220
248
  /**
221
249
  * Tuning for the fuzzy fallback that runs when the exact search finds
@@ -389,6 +417,8 @@ export interface DocumentCapabilities {
389
417
  readonly cellSelection: boolean;
390
418
  readonly search: boolean;
391
419
  readonly thumbnails: boolean;
420
+ /** True when `edit()` is available for this document. */
421
+ readonly editing: boolean;
392
422
  }
393
423
  export type DocumentMetadata = DocumentInfo;
394
424
  export interface AdapterOpenContext {
@@ -410,6 +440,13 @@ export interface DocumentAdapter<THandle = unknown> {
410
440
  getTextMap?(handle: THandle, pageIndex: number, signal?: AbortSignal): Promise<readonly TextRun[]>;
411
441
  close(handle: THandle): void | Promise<void>;
412
442
  destroy?(): void | Promise<void>;
443
+ /** Editing support for some of this adapter's formats. */
444
+ readonly edit?: EditEngineProvider;
445
+ /**
446
+ * Opens edited bytes of a document, reusing what `previous` holds (for
447
+ * example a worker). `previous` stays open; the viewer closes it afterwards.
448
+ */
449
+ reopen?(previous: THandle, data: Uint8Array, context: AdapterOpenContext): Promise<THandle>;
413
450
  }
414
451
  export type ViewerEventListener<K extends keyof ViewerEventMap> = (event: ViewerEventMap[K]) => void;
415
452
  export interface ViewerApi {
@@ -445,6 +482,14 @@ export interface ViewerApi {
445
482
  copySelection(): Promise<string>;
446
483
  getOriginalBytes(): Uint8Array | undefined;
447
484
  downloadOriginal(fileName?: string): Blob;
485
+ /** Starts editing the loaded document, or returns the session already started. */
486
+ edit(options?: EditOptions): Promise<EditSession>;
487
+ /** The active session of the loaded document, if `edit()` was called. */
488
+ getEditSession(): EditSession | undefined;
489
+ /** Client-space rectangle of a page-space rectangle; undefined if the page is not mounted. */
490
+ pageToClient(pageIndex: number, rect: PageRect): ViewportRect | undefined;
491
+ /** Page under a client-space point and the point in page space; undefined outside pages. */
492
+ clientToPage(clientX: number, clientY: number): PageHit | undefined;
448
493
  on<K extends keyof ViewerEventMap>(type: K, listener: ViewerEventListener<K>): () => void;
449
494
  destroy(): Promise<void>;
450
495
  }
@@ -0,0 +1,23 @@
1
+ import type { BinaryData, JsonSchema } from "./types.js";
2
+ export declare function isAssetReference(value: unknown): value is string;
3
+ /** The content-addressed id of `bytes`. */
4
+ export declare function assetIdOf(bytes: Uint8Array): Promise<string>;
5
+ /** Something that answers an asset reference with its bytes. */
6
+ export interface AssetSource {
7
+ get(id: string): Uint8Array | undefined;
8
+ }
9
+ export declare class AssetStore implements AssetSource {
10
+ #private;
11
+ get(id: string): Uint8Array | undefined;
12
+ has(id: string): boolean;
13
+ set(id: string, bytes: Uint8Array): void;
14
+ /** Bytes held, for limits. */
15
+ get byteLength(): number;
16
+ }
17
+ /**
18
+ * Bytes of a `BinaryData` value that passed schema validation: inline bytes,
19
+ * a base64 string, or an asset reference the store knows.
20
+ */
21
+ export declare function resolveBinary(data: BinaryData, assets: AssetSource): Uint8Array;
22
+ /** Names of an operation's top-level properties its schema marks as binary. */
23
+ export declare function binaryFields(schema: JsonSchema | undefined): string[];
@@ -0,0 +1,75 @@
1
+ import { ViewerError } from "../errors.js";
2
+ /*
3
+ * Binary payloads of operations live in a content-addressed store for the
4
+ * session: an operation refers to them as `asset:<sha-256 hex>`, so the
5
+ * history, undo, redo, dry runs and recovery never copy image bytes again.
6
+ */
7
+ const REFERENCE = /^asset:[0-9a-f]{64}$/;
8
+ export function isAssetReference(value) {
9
+ return typeof value === "string" && REFERENCE.test(value);
10
+ }
11
+ /** The content-addressed id of `bytes`. */
12
+ export async function assetIdOf(bytes) {
13
+ const digest = await globalThis.crypto.subtle.digest("SHA-256", bytes.slice().buffer);
14
+ let hex = "";
15
+ for (const byte of new Uint8Array(digest))
16
+ hex += byte.toString(16).padStart(2, "0");
17
+ return `asset:${hex}`;
18
+ }
19
+ export class AssetStore {
20
+ #bytes = new Map();
21
+ #total = 0;
22
+ get(id) {
23
+ return this.#bytes.get(id);
24
+ }
25
+ has(id) {
26
+ return this.#bytes.has(id);
27
+ }
28
+ set(id, bytes) {
29
+ if (this.#bytes.has(id))
30
+ return;
31
+ this.#bytes.set(id, bytes);
32
+ this.#total += bytes.byteLength;
33
+ }
34
+ /** Bytes held, for limits. */
35
+ get byteLength() {
36
+ return this.#total;
37
+ }
38
+ }
39
+ /**
40
+ * Bytes of a `BinaryData` value that passed schema validation: inline bytes,
41
+ * a base64 string, or an asset reference the store knows.
42
+ */
43
+ export function resolveBinary(data, assets) {
44
+ if (data instanceof Uint8Array)
45
+ return data;
46
+ if (isAssetReference(data)) {
47
+ const bytes = assets.get(data);
48
+ if (!bytes)
49
+ throw new ViewerError("invalid-operation", `Unknown asset ${data}`, {
50
+ details: {
51
+ issues: [
52
+ {
53
+ operationIndex: -1,
54
+ path: "",
55
+ code: "unknown-asset",
56
+ message: `Unknown asset ${data}`,
57
+ },
58
+ ],
59
+ },
60
+ });
61
+ return bytes;
62
+ }
63
+ return Uint8Array.from(atob(data), (character) => character.charCodeAt(0));
64
+ }
65
+ /** Names of an operation's top-level properties its schema marks as binary. */
66
+ export function binaryFields(schema) {
67
+ const properties = schema?.properties;
68
+ if (!properties || typeof properties !== "object")
69
+ return [];
70
+ return Object.entries(properties)
71
+ .filter(([, property]) => typeof property === "object" &&
72
+ property !== null &&
73
+ property["x-binary"] === true)
74
+ .map(([name]) => name);
75
+ }
@@ -0,0 +1,6 @@
1
+ import type { PageRect } from "../types.js";
2
+ import { type AnyRecord, type DocxModel } from "./model.js";
3
+ import type { DocxElement } from "./types.js";
4
+ export declare const NO_PAGE = -1;
5
+ export declare const EMPTY_RECT: PageRect;
6
+ export declare function toElement(model: DocxModel, record: AnyRecord): DocxElement;
@@ -0,0 +1,90 @@
1
+ import { tableRows, tableText, } from "./model.js";
2
+ import { IMPLEMENTED_OPERATIONS } from "./schemas.js";
3
+ import { resolveParagraphStyle, resolveTextStyle } from "./style.js";
4
+ import { firstTextItem } from "./text.js";
5
+ /*
6
+ * `DocxElement` values from the model's records, without geometry: the
7
+ * engine never lays out, so `pageIndex` is −1 and `bounds` empty until
8
+ * the session joins the renderer's runs on the main thread.
9
+ */
10
+ export const NO_PAGE = -1;
11
+ export const EMPTY_RECT = { x: 0, y: 0, width: 0, height: 0 };
12
+ /** The operations each kind accepts once its handler ships. */
13
+ const OPERATIONS = {
14
+ paragraph: [
15
+ "replaceText",
16
+ "setTextStyle",
17
+ "setParagraphStyle",
18
+ "insertParagraph",
19
+ "insertTable",
20
+ "insertImage",
21
+ "moveElement",
22
+ "deleteElement",
23
+ ],
24
+ table: [
25
+ "setTableCell",
26
+ "insertParagraph",
27
+ "insertTable",
28
+ "insertImage",
29
+ "moveElement",
30
+ "deleteElement",
31
+ ],
32
+ image: ["deleteElement"],
33
+ other: [],
34
+ };
35
+ /** A read-only paragraph only accepts siblings placed next to it. */
36
+ const INSERTIONS = ["insertParagraph", "insertTable", "insertImage"];
37
+ function operationsOf(kind, readOnly) {
38
+ return OPERATIONS[kind].filter((name) => IMPLEMENTED_OPERATIONS.includes(name) &&
39
+ (!readOnly || INSERTIONS.includes(name)));
40
+ }
41
+ export function toElement(model, record) {
42
+ const base = {
43
+ pageIndex: NO_PAGE,
44
+ bounds: EMPTY_RECT,
45
+ fragments: [],
46
+ story: { kind: "body" },
47
+ };
48
+ if (record.kind === "paragraph")
49
+ return paragraphElement(model, record, base);
50
+ if (record.kind === "table")
51
+ return {
52
+ ...base,
53
+ id: record.elementId,
54
+ kind: "table",
55
+ text: tableText(record),
56
+ table: { rows: tableRows(record) },
57
+ operations: operationsOf("table", false),
58
+ };
59
+ return {
60
+ ...base,
61
+ id: record.elementId,
62
+ kind: record.kind,
63
+ parentId: record.paragraph.elementId,
64
+ ...(record.kind === "image" && record.extent
65
+ ? {
66
+ imageSize: {
67
+ width: record.extent.cx / EMU_PER_PX,
68
+ height: record.extent.cy / EMU_PER_PX,
69
+ },
70
+ }
71
+ : {}),
72
+ operations: operationsOf(record.kind, false),
73
+ };
74
+ }
75
+ /** EMU per CSS pixel at 96 dpi. */
76
+ const EMU_PER_PX = 9525;
77
+ function paragraphElement(model, record, base) {
78
+ const first = firstTextItem(record.text);
79
+ return {
80
+ ...base,
81
+ id: record.elementId,
82
+ kind: "paragraph",
83
+ text: record.text.text,
84
+ ...(record.table ? { parentId: record.table.elementId } : {}),
85
+ textStyle: resolveTextStyle(model.styles, record.pPr, first?.rPr),
86
+ paragraphStyle: resolveParagraphStyle(model.styles, record.pPr),
87
+ ...(record.readOnlyReason ? { readOnlyReason: record.readOnlyReason } : {}),
88
+ operations: operationsOf("paragraph", record.readOnlyReason !== undefined),
89
+ };
90
+ }
@@ -0,0 +1,51 @@
1
+ import type { ResourceLimits } from "../../contracts.js";
2
+ import type { EditEngine, EngineBatch, EngineChange, MaterializedDocument, MaterializeOptions, RestoreTarget } from "../engine.js";
3
+ import { OoxmlPackage } from "../ooxml/package.js";
4
+ import type { EditFindOptions, EditOperation, ElementQuery, OperationIssue, PagePoint, TextTarget } from "../types.js";
5
+ import { DocxModel, type AnyRecord } from "./model.js";
6
+ import type { DocxElement } from "./types.js";
7
+ export declare class DocxEditEngine implements EditEngine {
8
+ #private;
9
+ readonly schemas: import("../types.js").OperationSchemaSet;
10
+ private constructor();
11
+ /** Opens the package and reads the block index, so a broken document fails here. */
12
+ static open(bytes: Uint8Array, limits: ResourceLimits, signal?: AbortSignal): Promise<DocxEditEngine>;
13
+ /** The package behind the engine, for tests and the operations. */
14
+ get package(): OoxmlPackage;
15
+ /** The engine has no pages of its own; the renderer counts them. */
16
+ get pageCount(): number;
17
+ /** The block index at the current revision. */
18
+ model(signal?: AbortSignal): Promise<DocxModel>;
19
+ validate(operations: readonly EditOperation[], signal: AbortSignal): Promise<readonly OperationIssue[]>;
20
+ /** Plain operation arrays, as the unit tests pass them, become the next batch. */
21
+ apply(input: EngineBatch | readonly EditOperation[], signal: AbortSignal): Promise<EngineChange>;
22
+ materialize(purposeOrSignal?: "show" | "save" | AbortSignal, options?: MaterializeOptions, signal?: AbortSignal): Promise<Uint8Array>;
23
+ /**
24
+ * `save` is the package as the session changed it: ids written only on
25
+ * the paragraphs the session rebuilt or created. `show` also stamps every
26
+ * other paragraph with the id the engine knows it by, in a copy, so the
27
+ * display pre-pass and the viewer's runs name the engine's paragraphs
28
+ * after edits that moved paragraphs around. Without changes both are
29
+ * the original bytes.
30
+ */
31
+ materializeDocument(purposeOrSignal?: "show" | "save" | AbortSignal, _options?: MaterializeOptions, signal?: AbortSignal): Promise<MaterializedDocument>;
32
+ restore(input: RestoreTarget | readonly (readonly EditOperation[])[], signal: AbortSignal): Promise<void>;
33
+ putAsset(id: string, data: Uint8Array, _signal: AbortSignal): Promise<void>;
34
+ /**
35
+ * Every element of the body story, in document order, without geometry.
36
+ * `pageIndex` and `intersects` are the session's to apply once the runs
37
+ * are joined; `kinds` filters here.
38
+ */
39
+ getElements(query: ElementQuery, signal: AbortSignal): Promise<readonly DocxElement[]>;
40
+ getElement(id: string, signal: AbortSignal): Promise<DocxElement | undefined>;
41
+ /** The record an element id names, at the current revision. */
42
+ locate(id: string, signal?: AbortSignal): Promise<AnyRecord | undefined>;
43
+ /** Hit-testing needs the renderer's geometry; the session answers it. */
44
+ elementsAt(_pageIndex: number, _point: PagePoint, _signal: AbortSignal): Promise<readonly DocxElement[]>;
45
+ /**
46
+ * Matches in paragraph text, in document order, without rectangles or
47
+ * pages: the session adds those from the renderer's runs.
48
+ */
49
+ findText(query: string, options: EditFindOptions, signal: AbortSignal): Promise<readonly TextTarget[]>;
50
+ dispose(): Promise<void>;
51
+ }