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,547 @@
1
+ import { ViewerError } from "../../errors.js";
2
+ import { AssetStore } from "../assets.js";
3
+ import { OoxmlPackage } from "../ooxml/package.js";
4
+ import { patches } from "../ooxml/patch.js";
5
+ import { invalidOperationError, parseReference } from "../operations.js";
6
+ import { NO_PAGE, toElement } from "./elements.js";
7
+ import { docxHandlers } from "./handlers.js";
8
+ import { freshParagraphId, paragraphsOf } from "./ids.js";
9
+ import { DocxModel } from "./model.js";
10
+ import { issueCollector, } from "./operations.js";
11
+ import { docxOperationSchemas } from "./schemas.js";
12
+ import { revisionsOf } from "./tracked.js";
13
+ import { attributeProblem, namespacePatches } from "./write.js";
14
+ /*
15
+ * The DOCX edit engine: the package layer under a block index of the body
16
+ * story. It runs inside the OOXML edit worker in the browser and directly
17
+ * in Node tests. It never lays out: elements carry no geometry, and the
18
+ * session joins the renderer's runs on the main thread.
19
+ *
20
+ * Paragraph ids: a paragraph with a `w14:paraId` keeps it; one without is
21
+ * numbered from its position when the document opens, and the engine then
22
+ * tracks those ids by document order (`#unauthored`), writing a
23
+ * `w14:paraId` on every paragraph it rebuilds or creates. The shown copy
24
+ * carries an id on every paragraph, so the viewer's runs name the same
25
+ * paragraphs; the saved file carries ids only where the session wrote.
26
+ */
27
+ /**
28
+ * The part a shown copy carries with the ids the engine owns, so a copy
29
+ * restored as a base knows which `w14:paraId` values to leave out of a
30
+ * saved file. An XML part: its content type is the one every package
31
+ * declares for the extension, so `[Content_Types].xml` stays untouched.
32
+ */
33
+ const UNAUTHORED_PART = "/webdoc/unauthored.xml";
34
+ const UNAUTHORED_NS = "urn:web-doc:docx-edit";
35
+ function unauthoredPartXml(ids) {
36
+ const items = [...ids].map((id) => `<p id="${id}"/>`).join("");
37
+ return new TextEncoder().encode(`<?xml version="1.0" encoding="UTF-8" standalone="yes"?><unauthored xmlns="${UNAUTHORED_NS}">${items}</unauthored>`);
38
+ }
39
+ function unauthoredIdsOf(bytes) {
40
+ const text = new TextDecoder().decode(bytes);
41
+ return [...text.matchAll(/<p id="([0-9A-F]{8})"\/>/g)].map((m) => m[1]);
42
+ }
43
+ /** The tracked-change record of a batch, when it writes revisions. */
44
+ function trackedOf(mode) {
45
+ if (mode.changeMode !== "tracked")
46
+ return undefined;
47
+ return {
48
+ author: mode.author ?? "",
49
+ ...(mode.timestamp === undefined ? {} : { date: mode.timestamp }),
50
+ };
51
+ }
52
+ /** ISO 8601 as `xsd:dateTime` takes it; what `w:date` carries. */
53
+ const DATE_TIME = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})?$/;
54
+ /**
55
+ * What a tracked batch must carry before any revision is written: an author
56
+ * the file can hold (the core requires one too; the engine used on its own
57
+ * does the same) and a date the file can hold.
58
+ */
59
+ function trackedModeIssues(tracked) {
60
+ if (!tracked)
61
+ return [];
62
+ const issues = [];
63
+ const author = attributeProblem(tracked.author);
64
+ if (tracked.author.trim().length === 0)
65
+ issues.push({
66
+ operationIndex: -1,
67
+ path: "/author",
68
+ code: "required",
69
+ message: "Tracked changes name their author; pass ApplyOptions.author",
70
+ });
71
+ else if (author)
72
+ issues.push({
73
+ operationIndex: -1,
74
+ path: "/author",
75
+ code: "invalid-value",
76
+ message: `The author holds ${author}`,
77
+ });
78
+ if (tracked.date !== undefined) {
79
+ const date = attributeProblem(tracked.date);
80
+ if (date ||
81
+ !DATE_TIME.test(tracked.date) ||
82
+ Number.isNaN(Date.parse(tracked.date)))
83
+ issues.push({
84
+ operationIndex: -1,
85
+ path: "/timestamp",
86
+ code: "invalid-value",
87
+ message: date
88
+ ? `The timestamp holds ${date}`
89
+ : "The timestamp must be an ISO 8601 date-time",
90
+ });
91
+ }
92
+ return issues;
93
+ }
94
+ export class DocxEditEngine {
95
+ schemas = docxOperationSchemas;
96
+ #original;
97
+ #limits;
98
+ #assets = new AssetStore();
99
+ #pkg;
100
+ #model;
101
+ /** The styles and theme, read once: no operation changes them. */
102
+ #styles;
103
+ /** Ids of the paragraphs without `w14:paraId`, in document order; undefined until read from the bytes. */
104
+ #unauthored;
105
+ #disposed = false;
106
+ #stateId = 0;
107
+ constructor(original, pkg, limits) {
108
+ this.#original = original;
109
+ this.#pkg = pkg;
110
+ this.#limits = limits;
111
+ }
112
+ /** Opens the package and reads the block index, so a broken document fails here. */
113
+ static async open(bytes, limits, signal) {
114
+ const pkg = await OoxmlPackage.open(bytes, {
115
+ limits,
116
+ ...(signal ? { signal } : {}),
117
+ });
118
+ const engine = new DocxEditEngine(bytes, pkg, limits);
119
+ await engine.model(signal);
120
+ return engine;
121
+ }
122
+ /** The package behind the engine, for tests and the operations. */
123
+ get package() {
124
+ return this.#pkg;
125
+ }
126
+ /** The engine has no pages of its own; the renderer counts them. */
127
+ get pageCount() {
128
+ return 0;
129
+ }
130
+ /** The block index at the current revision. */
131
+ model(signal) {
132
+ this.#assertAlive();
133
+ const revision = this.#pkg.revision;
134
+ if (!this.#model)
135
+ return this.#startModel(signal);
136
+ return this.#model.then((current) => current.revision === revision ? current : this.#startModel(signal));
137
+ }
138
+ #startModel(signal) {
139
+ const pending = DocxModel.load(this.#pkg, signal, this.#unauthored, this.#styles).then((model) => {
140
+ this.#unauthored ??= [...model.unauthoredIds];
141
+ this.#styles ??= model.styles;
142
+ return model;
143
+ });
144
+ this.#model = pending;
145
+ pending.catch(() => {
146
+ if (this.#model === pending)
147
+ this.#model = undefined;
148
+ });
149
+ return pending;
150
+ }
151
+ async validate(operations, signal, mode = {}) {
152
+ const issues = [];
153
+ const tracked = trackedOf(mode);
154
+ const modeIssues = trackedModeIssues(tracked);
155
+ if (modeIssues.length > 0)
156
+ return modeIssues;
157
+ const base = await this.#context(0, signal, 0, new Set(), tracked);
158
+ for (const [index, operation] of operations.entries()) {
159
+ const context = { ...base, operationIndex: index };
160
+ const handler = docxHandlers.get(operation.op);
161
+ if (!handler) {
162
+ issues.push({
163
+ operationIndex: index,
164
+ path: "/op",
165
+ code: "unknown-operation",
166
+ message: `Unknown operation ${operation.op}`,
167
+ });
168
+ continue;
169
+ }
170
+ // A same-batch reference names an element that does not exist yet:
171
+ // its target is checked when the batch is applied, the rest now.
172
+ const referenced = referenceFields(operation);
173
+ const forward = referenced.find((entry) => entry.reference >= index);
174
+ if (forward) {
175
+ issues.push({
176
+ operationIndex: index,
177
+ path: `/${forward.field}`,
178
+ code: "unknown-target",
179
+ message: `"${forward.value}" must refer to an earlier operation`,
180
+ });
181
+ continue;
182
+ }
183
+ const collect = issueCollector(index, issues);
184
+ const skipped = referenced.map((entry) => `/${entry.field}`);
185
+ await handler.validate(operation, context, referenced.length === 0
186
+ ? collect
187
+ : (path, code, message) => {
188
+ if (!skipped.some((prefix) => path === prefix || path.startsWith(`${prefix}/`)) &&
189
+ !path.startsWith("/range"))
190
+ collect(path, code, message);
191
+ });
192
+ }
193
+ return issues;
194
+ }
195
+ /** Plain operation arrays, as the unit tests pass them, become the next batch. */
196
+ async apply(input, signal) {
197
+ const batch = Array.isArray(input)
198
+ ? { stateId: this.#nextStateId(), operations: input }
199
+ : input;
200
+ this.#stateId = Math.max(this.#stateId, batch.stateId);
201
+ const tracked = trackedOf(batch);
202
+ const modeIssues = trackedModeIssues(tracked);
203
+ if (modeIssues.length > 0)
204
+ throw invalidOperationError(modeIssues);
205
+ const snapshot = this.#pkg.snapshot();
206
+ const unauthored = this.#unauthored ? [...this.#unauthored] : undefined;
207
+ const createdIds = [];
208
+ const removedIds = [];
209
+ const warnings = [];
210
+ const createdByOperation = [];
211
+ const issued = new Set();
212
+ const remappedIds = {};
213
+ let reflowFrom;
214
+ try {
215
+ for (const [index, raw] of batch.operations.entries()) {
216
+ throwIfAborted(signal);
217
+ const operation = resolveReferences(raw, createdByOperation, index);
218
+ const handler = docxHandlers.get(operation.op);
219
+ if (!handler)
220
+ throw new ViewerError("invalid-operation", `Unknown operation ${operation.op}`);
221
+ const context = await this.#context(batch.stateId, signal, index, issued, tracked);
222
+ const issues = [];
223
+ await handler.validate(operation, context, issueCollector(index, issues));
224
+ if (issues.length > 0)
225
+ throw invalidOperationError(issues);
226
+ const result = await handler.apply(operation, context);
227
+ this.#model = undefined;
228
+ const gone = new Set([
229
+ ...(result.stamped ?? []),
230
+ ...(result.removedParagraphIds ?? []),
231
+ ]);
232
+ if (gone.size > 0 && this.#unauthored)
233
+ this.#unauthored = this.#unauthored.filter((id) => !gone.has(id));
234
+ createdByOperation.push([...result.createdIds]);
235
+ createdIds.push(...result.createdIds);
236
+ removedIds.push(...(result.removedIds ?? []));
237
+ warnings.push(...result.warnings);
238
+ reflowFrom ??= result.reflowFrom;
239
+ // A table renamed twice in one batch maps its first id to its last.
240
+ for (const [from, to] of Object.entries(result.remappedIds ?? {})) {
241
+ const origin = Object.entries(remappedIds).find(([, value]) => value === from)?.[0] ?? from;
242
+ remappedIds[origin] = to;
243
+ }
244
+ }
245
+ // The index after the batch; an inconsistency surfaces here and
246
+ // rolls the batch back like any other failure.
247
+ await this.model(signal);
248
+ }
249
+ catch (error) {
250
+ // Everything the batch did, ids included, is undone.
251
+ this.#pkg.restore(snapshot);
252
+ this.#unauthored = unauthored;
253
+ this.#model = undefined;
254
+ throw error;
255
+ }
256
+ this.#pkg.release(snapshot);
257
+ return {
258
+ createdIds,
259
+ removedIds,
260
+ // A flow document reflows from the first changed paragraph; the host
261
+ // turns it into pages.
262
+ changedPages: [],
263
+ ...(reflowFrom === undefined ? {} : { reflowFrom }),
264
+ ...(Object.keys(remappedIds).length > 0 ? { remappedIds } : {}),
265
+ warnings,
266
+ };
267
+ }
268
+ #nextStateId() {
269
+ this.#stateId += 1;
270
+ return this.#stateId;
271
+ }
272
+ async #context(stateId, signal, operationIndex = 0, issued = new Set(), tracked) {
273
+ const model = await this.model(signal);
274
+ const taken = new Set([...model.takenIds, ...issued]);
275
+ let count = 0;
276
+ let revisions = 0;
277
+ return {
278
+ pkg: this.#pkg,
279
+ model,
280
+ limits: this.#limits,
281
+ assets: this.#assets,
282
+ stateId,
283
+ operationIndex,
284
+ freshParagraphId: () => {
285
+ const id = freshParagraphId(stateId, operationIndex, count, taken);
286
+ count += 1;
287
+ issued.add(id);
288
+ taken.add(id);
289
+ return id;
290
+ },
291
+ ...(tracked ? { tracked } : {}),
292
+ nextRevisionId: () => {
293
+ revisions += 1;
294
+ return model.maxRevisionId + revisions;
295
+ },
296
+ };
297
+ }
298
+ /** The revisions of a paragraph; none for another element or an unknown id. */
299
+ async revisions(id, signal) {
300
+ const model = await this.model(signal);
301
+ const record = model.byId.get(id);
302
+ if (!record || record.kind !== "paragraph")
303
+ return [];
304
+ return revisionsOf(model.document, record.node);
305
+ }
306
+ async materialize(purposeOrSignal = "show", options = {}, signal = new AbortController().signal) {
307
+ return (await this.materializeDocument(purposeOrSignal, options, signal))
308
+ .bytes;
309
+ }
310
+ /**
311
+ * `save` is the package as the session changed it: ids written only on
312
+ * the paragraphs the session rebuilt or created. `show` also stamps every
313
+ * other paragraph with the id the engine knows it by, in a copy, so the
314
+ * display pre-pass and the viewer's runs name the engine's paragraphs
315
+ * after edits that moved paragraphs around. Without changes both are
316
+ * the original bytes.
317
+ */
318
+ async materializeDocument(purposeOrSignal = "show", _options = {}, signal = new AbortController().signal) {
319
+ this.#assertAlive();
320
+ const purpose = purposeOrSignal instanceof AbortSignal ? "show" : purposeOrSignal;
321
+ const own = purposeOrSignal instanceof AbortSignal ? purposeOrSignal : signal;
322
+ const bytes = await this.#pkg.save({}, own);
323
+ const fromShownBase = this.#pkg.has(UNAUTHORED_PART);
324
+ if (purpose === "save")
325
+ return {
326
+ bytes: fromShownBase ? await this.#unstamped(bytes, own) : bytes,
327
+ warnings: [],
328
+ };
329
+ if (this.#pkg.changedParts.length === 0 && !fromShownBase)
330
+ return { bytes, warnings: [] };
331
+ return { bytes: await this.#stamped(bytes, own), warnings: [] };
332
+ }
333
+ /**
334
+ * A copy of `bytes` with a `w14:paraId` on every paragraph of the main
335
+ * part and the engine's own ids listed in a part of their own.
336
+ */
337
+ async #stamped(bytes, signal) {
338
+ const model = await this.model(signal);
339
+ const copy = await OoxmlPackage.open(bytes, {
340
+ limits: this.#limits,
341
+ signal,
342
+ });
343
+ const part = await copy.xml(model.mainPart, signal);
344
+ const owned = this.#unauthored ?? [];
345
+ // Ids already written (by an edit, or by the shown base this state
346
+ // came from) stay; the rest go on the unmarked paragraphs in order.
347
+ const attributed = new Set();
348
+ for (const paragraph of paragraphsOf(part)) {
349
+ const id = part.attribute(paragraph, "w14:paraId");
350
+ if (id)
351
+ attributed.add(id.toUpperCase());
352
+ }
353
+ const queue = owned.filter((id) => !attributed.has(id));
354
+ const items = [];
355
+ for (const paragraph of paragraphsOf(part)) {
356
+ if (part.attribute(paragraph, "w14:paraId"))
357
+ continue;
358
+ const id = queue.shift();
359
+ if (id === undefined)
360
+ throw new ViewerError("internal", "The document has more unmarked paragraphs than the session knows");
361
+ items.push(patches.setAttribute(part, paragraph, "w14:paraId", id));
362
+ }
363
+ const list = owned.length > 0 ? unauthoredPartXml(owned) : undefined;
364
+ if (items.length === 0) {
365
+ // Already the shown form of this state: nothing to write.
366
+ const current = copy.has(UNAUTHORED_PART)
367
+ ? await copy.part(UNAUTHORED_PART, signal)
368
+ : undefined;
369
+ if (current === undefined
370
+ ? list === undefined
371
+ : list !== undefined && sameBytes(current, list))
372
+ return bytes;
373
+ }
374
+ const transaction = copy.transaction();
375
+ if (items.length > 0)
376
+ transaction.patch(part, [...namespacePatches(part), ...items]);
377
+ if (list)
378
+ transaction.setPart(UNAUTHORED_PART, list, "application/xml");
379
+ else if (copy.has(UNAUTHORED_PART))
380
+ transaction.removePart(UNAUTHORED_PART);
381
+ await transaction.commit(signal);
382
+ return copy.save({}, signal);
383
+ }
384
+ /**
385
+ * A copy of `bytes` without the ids a shown base brought: the engine's
386
+ * own `w14:paraId` values go, the ones an edit wrote stay, and the list
387
+ * part goes with them.
388
+ */
389
+ async #unstamped(bytes, signal) {
390
+ const model = await this.model(signal);
391
+ const copy = await OoxmlPackage.open(bytes, {
392
+ limits: this.#limits,
393
+ signal,
394
+ });
395
+ const part = await copy.xml(model.mainPart, signal);
396
+ const owned = model.unauthoredSet;
397
+ const items = [];
398
+ for (const paragraph of paragraphsOf(part)) {
399
+ const id = part.attribute(paragraph, "w14:paraId")?.toUpperCase();
400
+ if (id && owned.has(id))
401
+ items.push(patches.removeAttribute(part, paragraph, "w14:paraId"));
402
+ }
403
+ const transaction = copy.transaction();
404
+ if (items.length > 0)
405
+ transaction.patch(part, items);
406
+ if (copy.has(UNAUTHORED_PART))
407
+ transaction.removePart(UNAUTHORED_PART);
408
+ await transaction.commit(signal);
409
+ return copy.save({}, signal);
410
+ }
411
+ async restore(input, signal) {
412
+ const target = Array.isArray(input)
413
+ ? {
414
+ batches: input.map((operations, index) => ({ stateId: index + 1, operations })),
415
+ }
416
+ : input;
417
+ this.#assertAlive();
418
+ this.#pkg = await OoxmlPackage.open(target.base ?? this.#original, {
419
+ limits: this.#limits,
420
+ signal,
421
+ });
422
+ // The original has the ids its positions give; a base is a shown copy
423
+ // with every paragraph marked and the engine's own ids listed.
424
+ this.#unauthored = undefined;
425
+ if (this.#pkg.has(UNAUTHORED_PART))
426
+ this.#unauthored = unauthoredIdsOf(await this.#pkg.part(UNAUTHORED_PART, signal));
427
+ this.#model = undefined;
428
+ await this.model(signal);
429
+ for (const batch of target.batches)
430
+ await this.apply(batch, signal);
431
+ }
432
+ async putAsset(id, data, _signal) {
433
+ this.#assets.set(id, data);
434
+ }
435
+ /**
436
+ * Every element of the body story, in document order, without geometry.
437
+ * `pageIndex` and `intersects` are the session's to apply once the runs
438
+ * are joined; `kinds` filters here.
439
+ */
440
+ async getElements(query, signal) {
441
+ const model = await this.model(signal);
442
+ const out = [];
443
+ for (const record of model.records) {
444
+ if (query.kinds && !query.kinds.includes(record.kind))
445
+ continue;
446
+ out.push(toElement(model, record));
447
+ }
448
+ return out;
449
+ }
450
+ async getElement(id, signal) {
451
+ const record = await this.locate(id, signal);
452
+ return record ? toElement(await this.model(signal), record) : undefined;
453
+ }
454
+ /** The record an element id names, at the current revision. */
455
+ async locate(id, signal) {
456
+ return (await this.model(signal)).byId.get(id);
457
+ }
458
+ /** Hit-testing needs the renderer's geometry; the session answers it. */
459
+ async elementsAt(_pageIndex, _point, _signal) {
460
+ return [];
461
+ }
462
+ /**
463
+ * Matches in paragraph text, in document order, without rectangles or
464
+ * pages: the session adds those from the renderer's runs.
465
+ */
466
+ async findText(query, options, signal) {
467
+ if (query.length === 0)
468
+ return [];
469
+ const model = await this.model(signal);
470
+ // A case-insensitive regular expression keeps offsets on the original
471
+ // text, where a lower-cased copy can change length.
472
+ const needle = new RegExp(query.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), options.caseSensitive ? "g" : "gi");
473
+ const limit = options.maxResults ?? Number.POSITIVE_INFINITY;
474
+ const targets = [];
475
+ for (const record of model.records) {
476
+ if (record.kind !== "paragraph")
477
+ continue;
478
+ const text = record.text.text;
479
+ if (!text)
480
+ continue;
481
+ needle.lastIndex = 0;
482
+ while (targets.length < limit) {
483
+ const match = needle.exec(text);
484
+ if (!match)
485
+ break;
486
+ const at = match.index;
487
+ const end = at + match[0].length;
488
+ if (match[0].length === 0)
489
+ needle.lastIndex += 1;
490
+ targets.push({
491
+ pageIndex: NO_PAGE,
492
+ text: text.slice(at, end),
493
+ rects: [],
494
+ elementIds: [record.elementId],
495
+ ranges: [
496
+ {
497
+ start: { elementId: record.elementId, offset: at },
498
+ end: { elementId: record.elementId, offset: end },
499
+ },
500
+ ],
501
+ });
502
+ }
503
+ if (targets.length >= limit)
504
+ break;
505
+ }
506
+ return targets;
507
+ }
508
+ async dispose() {
509
+ this.#disposed = true;
510
+ this.#model = undefined;
511
+ }
512
+ #assertAlive() {
513
+ if (this.#disposed)
514
+ throw new ViewerError("lifecycle-error", "The DOCX engine was disposed");
515
+ }
516
+ }
517
+ const REFERENCE_FIELDS = ["target", "before", "after"];
518
+ function referenceFields(operation) {
519
+ const out = [];
520
+ for (const field of REFERENCE_FIELDS) {
521
+ const value = operation[field];
522
+ if (typeof value !== "string")
523
+ continue;
524
+ const reference = parseReference(value);
525
+ if (reference !== undefined)
526
+ out.push({ field, value, reference });
527
+ }
528
+ return out;
529
+ }
530
+ /** Replaces `"$<n>"` references with the first id operation `n` created. */
531
+ function resolveReferences(operation, created, index) {
532
+ let resolved = operation;
533
+ for (const { field, value, reference } of referenceFields(operation)) {
534
+ const id = reference < index ? created[reference]?.[0] : undefined;
535
+ if (!id)
536
+ throw new ViewerError("invalid-operation", `Operation ${index} refers to "${value}", which created nothing`, { details: { operationIndex: index, target: value } });
537
+ resolved = { ...resolved, [field]: id };
538
+ }
539
+ return resolved;
540
+ }
541
+ function sameBytes(a, b) {
542
+ return a.length === b.length && a.every((byte, index) => byte === b[index]);
543
+ }
544
+ function throwIfAborted(signal) {
545
+ if (signal?.aborted)
546
+ throw new ViewerError("aborted", "The operation was aborted");
547
+ }
@@ -0,0 +1,3 @@
1
+ import type { DocxOperationHandler } from "./operations.js";
2
+ /** Handlers by operation name; `IMPLEMENTED_OPERATIONS` in schemas.ts lists the same names. */
3
+ export declare const docxHandlers: ReadonlyMap<string, DocxOperationHandler>;
@@ -0,0 +1,15 @@
1
+ import { deleteElementHandler, insertImageHandler, insertParagraphHandler, moveElementHandler, } from "./structure-ops.js";
2
+ import { insertTableHandler, setTableCellHandler } from "./table-ops.js";
3
+ import { replaceTextHandler, setParagraphStyleHandler, setTextStyleHandler, } from "./text-ops.js";
4
+ /** Handlers by operation name; `IMPLEMENTED_OPERATIONS` in schemas.ts lists the same names. */
5
+ export const docxHandlers = new Map([
6
+ ["replaceText", replaceTextHandler],
7
+ ["setTextStyle", setTextStyleHandler],
8
+ ["setParagraphStyle", setParagraphStyleHandler],
9
+ ["insertParagraph", insertParagraphHandler],
10
+ ["deleteElement", deleteElementHandler],
11
+ ["moveElement", moveElementHandler],
12
+ ["insertImage", insertImageHandler],
13
+ ["insertTable", insertTableHandler],
14
+ ["setTableCell", setTableCellHandler],
15
+ ]);
@@ -0,0 +1,45 @@
1
+ import type { XmlElement, XmlPart } from "../ooxml/xml.js";
2
+ export declare const W_NS = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
3
+ export declare const W14_NS = "http://schemas.microsoft.com/office/word/2010/wordml";
4
+ export declare const OFFICE_RELATIONSHIPS = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/";
5
+ /** Story parts whose paragraphs share the id sequence, in the order they are numbered. */
6
+ export declare const STORY_RELATIONSHIP_TYPES: readonly ["http://schemas.openxmlformats.org/officeDocument/2006/relationships/header", "http://schemas.openxmlformats.org/officeDocument/2006/relationships/footer", "http://schemas.openxmlformats.org/officeDocument/2006/relationships/footnotes", "http://schemas.openxmlformats.org/officeDocument/2006/relationships/endnotes"];
7
+ /** Prefix of the hidden bookmarks that carry paragraph ids. */
8
+ export declare const PARAGRAPH_BOOKMARK_PREFIX = "_wd";
9
+ /** First bookmark id the pre-pass uses, above what Word writes. */
10
+ export declare const BOOKMARK_ID_BASE = 7000000;
11
+ /** Ids and bookmark numbers shared by every story part of one document. */
12
+ export interface DocxIdState {
13
+ readonly taken: Set<string>;
14
+ generated: number;
15
+ bookmarkId: number;
16
+ }
17
+ export declare function newIdState(): DocxIdState;
18
+ /**
19
+ * The paragraph id generated for the `index`-th paragraph without one, in
20
+ * document order, skipping ids the document already uses.
21
+ */
22
+ export declare function generatedParagraphId(index: number, taken: ReadonlySet<string>): string;
23
+ /** Every `w:p` of a part in document order, nested ones included. */
24
+ export declare function paragraphsOf(part: XmlPart): readonly XmlElement[];
25
+ /** Collects the ids and bookmark numbers a part already uses. */
26
+ export declare function collectIds(part: XmlPart, state: DocxIdState): void;
27
+ export interface ParagraphId {
28
+ readonly paragraph: XmlElement;
29
+ /** Eight upper-case hex digits. */
30
+ readonly id: string;
31
+ /** True when the file carries the id as `w14:paraId`. */
32
+ readonly authored: boolean;
33
+ }
34
+ /**
35
+ * The id of every paragraph of a part, numbering the unmarked ones from
36
+ * where `state` stands. Call it for the parts in numbering order after
37
+ * `collectIds` has seen all of them.
38
+ */
39
+ export declare function assignParagraphIds(part: XmlPart, state: DocxIdState): readonly ParagraphId[];
40
+ /**
41
+ * A fresh id for the `index`-th paragraph an operation creates: derived
42
+ * from the batch's state id and the operation's position, so a replay of
43
+ * the same batches issues the same ids, and skipping any id in use.
44
+ */
45
+ export declare function freshParagraphId(stateId: number, operationIndex: number, index: number, taken: ReadonlySet<string>): string;