revise 3.1.7 → 4.0.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 (91) hide show
  1. package/CHANGELOG.md +1671 -184
  2. package/FONT-LICENSE-HAN-EXT-B.txt +91 -0
  3. package/FONT-LICENSE-KATEX.txt +95 -0
  4. package/FONT-LICENSE-LIBERATION.txt +102 -0
  5. package/FONT-LICENSE-NOTO-MULTILINGUAL.txt +93 -0
  6. package/FONT-LICENSE-NOTO-SANS-SC.txt +93 -0
  7. package/FONT-LICENSE-NOTO.txt +94 -0
  8. package/FONT-LICENSE-SOURCE-CODE-PRO.txt +94 -0
  9. package/FONT-LICENSE-SOURCE-SERIF-4.txt +93 -0
  10. package/LICENSE +41 -21
  11. package/README.md +32 -119
  12. package/THIRD-PARTY-NOTICES.md +2751 -0
  13. package/assets/historyMaterializeWorker-ZFUHJDWr.js +619 -0
  14. package/chunks/AgentPageRenderer-5mZ6fxqO.js +1 -0
  15. package/chunks/AgentPageRenderer-C3jIBoeM.js +1 -0
  16. package/chunks/AttachmentDocumentImporter-CXkM4VoT.js +1 -0
  17. package/chunks/AttachmentDocumentImporter-Kox5By7a.js +1 -0
  18. package/chunks/ColorParserFull-BKH72fK8.js +446 -0
  19. package/chunks/DocumentScrubber-BxmlzNNz.js +1 -0
  20. package/chunks/DocumentScrubber-wmjRnX7C.js +1 -0
  21. package/chunks/DocxConverter-BH3E7nfQ.js +13 -0
  22. package/chunks/DocxConverter-DEHGCdib.js +13 -0
  23. package/chunks/DocxGraft-BMvTAnGi.js +1 -0
  24. package/chunks/DocxGraft-V2YXiUmK.js +1 -0
  25. package/chunks/FileImporter-CtX8ZPjV.js +1 -0
  26. package/chunks/FileImporter-Da-7eRh3.js +1 -0
  27. package/chunks/OdtConverter-CMkSeU8e.js +6 -0
  28. package/chunks/OdtConverter-DV94rkDL.js +6 -0
  29. package/chunks/OfficeImportShared-CA3dia_A.js +1 -0
  30. package/chunks/OfficeImportShared-hxIa69By.js +1 -0
  31. package/chunks/PdfConverter-B-uRwLux.js +21 -0
  32. package/chunks/PdfConverter-B8J_5yim.js +21 -0
  33. package/chunks/PdfImportService-BjQ4rl-Z.js +2 -0
  34. package/chunks/PdfImportService-RtFCP6hh.js +2 -0
  35. package/chunks/RtfConverter-CFDnUApT.js +15 -0
  36. package/chunks/RtfConverter-CpJGtC5U.js +15 -0
  37. package/chunks/XmlConverter-CQCrgTf7.js +2 -0
  38. package/chunks/XmlConverter-CePTE21k.js +2 -0
  39. package/chunks/__vite-browser-external-l0sNRNKZ.js +0 -0
  40. package/chunks/_revise-sdk-lazy-harfbuzz-X8gnYTZL.js +1 -0
  41. package/chunks/ar-vDYSGTpE.js +13 -0
  42. package/chunks/browser-module-B-uafDG6.js +3 -0
  43. package/chunks/browser-module-jwkhffzv.js +3 -0
  44. package/chunks/bundle.min-DRpfgV69.js +782 -0
  45. package/chunks/de-CWX4cvtt.js +13 -0
  46. package/chunks/es-DoAhvF6_.js +13 -0
  47. package/chunks/fil-DdAeD8Br.js +13 -0
  48. package/chunks/fr-CazsUjNQ.js +13 -0
  49. package/chunks/html2canvas.esm-CntiHiYW.js +997 -0
  50. package/chunks/index-BWdRWxoH.js +1807 -0
  51. package/chunks/index-C7VhCKXe.js +402 -0
  52. package/chunks/index-p6UytRm8.js +402 -0
  53. package/chunks/ja-alh_QHTB.js +13 -0
  54. package/chunks/jspdf.es.min-BuJNZ-oY.js +1882 -0
  55. package/chunks/katex-DWj8TvwO.js +1462 -0
  56. package/chunks/pdf-BLWDCiFF.js +1828 -0
  57. package/chunks/persistInlineDocumentImages-DQj2iaqg.js +1 -0
  58. package/chunks/pseudoLocale-Bkzfsfiw.js +1 -0
  59. package/chunks/pseudoLocale-D-AfwRwa.js +1 -0
  60. package/chunks/tree-sitter-D6zWWUMP.js +4 -0
  61. package/chunks/wasmInline.generated-CBYFkdSM.js +1 -0
  62. package/chunks/web-tree-sitter-BXGZGnNj.js +2 -0
  63. package/chunks/zh-20eXwzFZ.js +13 -0
  64. package/cli-shim.js +34 -0
  65. package/full.js +1 -0
  66. package/index.d.ts +995 -13
  67. package/index.js +1 -31
  68. package/legacy/server.d.ts +19 -19
  69. package/legacy/server.js +39 -23
  70. package/package.json +55 -45
  71. package/serverToolContract.d.ts +420 -0
  72. package/serverToolInputs.generated.d.ts +269 -0
  73. package/style.css +1 -0
  74. package/version.d.ts +4 -0
  75. package/version.js +2 -0
  76. package/agent.d.ts +0 -139
  77. package/chunks/chunk-SMTOJL2X.js +0 -2436
  78. package/chunks/chunk-U5X47PZ6.js +0 -135
  79. package/chunks/chunk-U6YKQBKL.js +0 -15
  80. package/chunks/package-5SHL5FOJ.js +0 -14
  81. package/chunks/render-QD5VWKSV.js +0 -109
  82. package/cli-output.schema.json +0 -2159
  83. package/cli.js +0 -4152
  84. package/document.d.ts +0 -439
  85. package/docx.d.ts +0 -73
  86. package/errors.d.ts +0 -14
  87. package/legacy/editor.d.ts +0 -19
  88. package/legacy/editor.js +0 -16
  89. package/legacy/style.css +0 -2
  90. package/package.d.ts +0 -23
  91. package/render.d.ts +0 -27
@@ -1,2436 +0,0 @@
1
- // revise: MIT License. https://revise.io/developer/docs
2
- import {
3
- ReviseError
4
- } from "./chunk-U6YKQBKL.js";
5
-
6
- // src/embed-sdk/lib/document.ts
7
- import { engineModules as engineModules2 } from "@reviseio/engine";
8
- import * as Y from "yjs";
9
- import {
10
- createReviseYDoc as createReviseYDoc2,
11
- createServerDocumentSession,
12
- decodeYDoc,
13
- encodeYDoc,
14
- exportDocument,
15
- fileToYDoc,
16
- installDomShims,
17
- parseDocument as parseDocument2,
18
- ydocSourceFidelity as ydocSourceFidelity2,
19
- ydocToDocument as ydocToDocument2
20
- } from "@reviseio/engine";
21
- import { seedDocRootWithClaim as seedDocRootWithClaim2, seedYDocFromFile } from "@reviseio/engine";
22
-
23
- // src/embed-sdk/lib/docx.ts
24
- import { engineModules } from "@reviseio/engine";
25
- import {
26
- createReviseYDoc,
27
- documentToDocx,
28
- parseDocument,
29
- ydocSourceFidelity,
30
- ydocToDocument
31
- } from "@reviseio/engine";
32
- import { createHash } from "node:crypto";
33
- import { seedDocRootWithClaim } from "@reviseio/engine";
34
- async function exportSessionDocx(ydoc, pkg, mode = "review") {
35
- let doc = ydocToDocument(ydoc);
36
- const grafting = Boolean(pkg && doc.docxSource);
37
- const regenerated = grafting ? new Set((await ydocSourceFidelity(ydoc)).regeneratedBlockIds) : null;
38
- if (mode === "final") {
39
- const { scrubDocument } = await engineModules.scrubber();
40
- doc = scrubDocument(doc, {
41
- removeComments: true,
42
- resolveSuggestions: "accept"
43
- }).doc;
44
- }
45
- doc = stampParaIds(
46
- doc,
47
- regenerated,
48
- grafting ? graftKeptParts(doc, await engineModules.docxGraft()) : void 0
49
- );
50
- if (!grafting || !pkg || !doc.docxSource) {
51
- return {
52
- bytes: await documentToDocx(doc),
53
- outcome: {
54
- method: "regenerated",
55
- exact: false,
56
- regeneratedBlocks: doc.children.length,
57
- fallbackCode: doc.docxSource ? "no-original-package" : "not-from-docx"
58
- }
59
- };
60
- }
61
- const { hashBytes } = await engineModules.docxGraft();
62
- doc = {
63
- ...doc,
64
- docxSource: { ...doc.docxSource, sourceHash: await hashBytes(pkg) }
65
- };
66
- let reported = {
67
- method: "regenerated",
68
- exact: false,
69
- regeneratedBlocks: doc.children.length,
70
- fallbackCode: "no-outcome"
71
- };
72
- const bytes = await documentToDocx(doc, {
73
- originalDocx: pkg,
74
- onGraftOutcome: (outcome) => {
75
- reported = outcome.mode === "regenerated" ? {
76
- method: "regenerated",
77
- exact: false,
78
- regeneratedBlocks: doc.children.length,
79
- fallbackCode: outcome.fallbackCode
80
- } : {
81
- method: "graft",
82
- exact: outcome.mode === "exact",
83
- regeneratedBlocks: outcome.regeneratedBlockCount ?? 0
84
- };
85
- }
86
- });
87
- return { bytes, outcome: reported };
88
- }
89
- var PARA_ID_KEY = "w14:paraId";
90
- function paraIdOf(node) {
91
- if (!node.oxmlAttrs) return void 0;
92
- try {
93
- const attrs = JSON.parse(node.oxmlAttrs);
94
- const value = attrs[PARA_ID_KEY];
95
- return typeof value === "string" ? value.toUpperCase() : void 0;
96
- } catch {
97
- return void 0;
98
- }
99
- }
100
- function derivedParaId(blockId, salt = 0) {
101
- const value = createHash("sha1").update(salt ? `${blockId}:${salt}` : blockId).digest().readUInt32BE(0) & 2147483647;
102
- return (value || 1).toString(16).toUpperCase().padStart(8, "0");
103
- }
104
- function childrenOf(node) {
105
- return (Array.isArray(node.children) ? node.children : []).filter(
106
- (child) => child !== null && typeof child === "object" && "type" in child
107
- );
108
- }
109
- function contentSignature(node) {
110
- const n = node;
111
- if (!n || typeof n !== "object") return "";
112
- if (n.type === "text") return JSON.stringify(n.text ?? "");
113
- const children = Array.isArray(n.children) ? n.children : [];
114
- return `${n.type ?? "?"}(${children.map(contentSignature).join(",")})`;
115
- }
116
- function walkIds(node, visit) {
117
- visit(node);
118
- for (const child of childrenOf(node)) walkIds(child, visit);
119
- }
120
- function graftKeptParts(doc, graft) {
121
- const kept = { whole: /* @__PURE__ */ new Set(), head: /* @__PURE__ */ new Set() };
122
- const source = doc.docxSource;
123
- if (!source?.parts) return kept;
124
- const version = source.signatureVersion === graft.CURRENT_GRAFT_SIGNATURE_VERSION ? graft.CURRENT_GRAFT_SIGNATURE_VERSION : 1;
125
- for (const block of doc.children) {
126
- const captured = block.id ? source.parts[block.id] : void 0;
127
- if (!captured || block.type !== "table" && block.type !== "list") {
128
- continue;
129
- }
130
- for (const part of block.children) {
131
- const entry = captured.parts.find((p) => p.id === part.id);
132
- if (!entry || !part.id) continue;
133
- if (entry.signature === graft.partSignature(part, version)) {
134
- kept.whole.add(part.id);
135
- continue;
136
- }
137
- if (block.type === "table" && entry.cells && entry.head === graft.partHeadSignature(part, version)) {
138
- kept.head.add(part.id);
139
- part.children?.forEach((cell, c) => {
140
- const cellEntry = entry.cells[c];
141
- if (cell.id && cellEntry?.id === cell.id && cellEntry.signature === graft.partSignature(cell, version)) {
142
- kept.whole.add(cell.id);
143
- }
144
- });
145
- }
146
- }
147
- }
148
- return kept;
149
- }
150
- function stampParaIds(doc, regenerated, kept = { whole: /* @__PURE__ */ new Set(), head: /* @__PURE__ */ new Set() }) {
151
- const used = /* @__PURE__ */ new Set();
152
- for (const block of doc.children) {
153
- walkIds(block, (node) => {
154
- const id = paraIdOf(node);
155
- if (id) used.add(id);
156
- });
157
- }
158
- const stamp = (node) => {
159
- if (node.id && kept.whole.has(node.id)) return node;
160
- const copy = { ...node };
161
- if ((copy.type === "paragraph" || copy.type === "table-row") && copy.id && !kept.head.has(copy.id) && !paraIdOf(copy)) {
162
- let salt = 0;
163
- let candidate = derivedParaId(copy.id);
164
- while (used.has(candidate)) candidate = derivedParaId(copy.id, ++salt);
165
- used.add(candidate);
166
- let attrs = {};
167
- try {
168
- attrs = copy.oxmlAttrs ? JSON.parse(copy.oxmlAttrs) : {};
169
- } catch {
170
- attrs = {};
171
- }
172
- copy.oxmlAttrs = JSON.stringify({ ...attrs, [PARA_ID_KEY]: candidate });
173
- }
174
- if (Array.isArray(node.children)) {
175
- copy.children = node.children.map(
176
- (child) => child && typeof child === "object" && "type" in child ? stamp(child) : child
177
- );
178
- }
179
- return copy;
180
- };
181
- return {
182
- ...doc,
183
- children: doc.children.map(
184
- (block) => !regenerated || block.id && regenerated.has(block.id) ? stamp(block) : block
185
- )
186
- };
187
- }
188
- function lcsIndexPairs(a, b) {
189
- const n = a.length;
190
- const m = b.length;
191
- if (n === 0 || m === 0) return [];
192
- if (n * m > 4e6) {
193
- const pairs2 = [];
194
- let i2 = 0;
195
- while (i2 < n && i2 < m && a[i2] === b[i2]) pairs2.push([i2, i2++]);
196
- let k = 1;
197
- while (k <= n - i2 && k <= m - i2 && a[n - k] === b[m - k]) {
198
- pairs2.push([n - k, m - k]);
199
- k += 1;
200
- }
201
- return pairs2.sort((x, y) => x[0] - y[0]);
202
- }
203
- const table = new Uint32Array((n + 1) * (m + 1));
204
- for (let i2 = n - 1; i2 >= 0; i2 -= 1) {
205
- for (let j2 = m - 1; j2 >= 0; j2 -= 1) {
206
- table[i2 * (m + 1) + j2] = a[i2] === b[j2] ? table[(i2 + 1) * (m + 1) + j2 + 1] + 1 : Math.max(table[(i2 + 1) * (m + 1) + j2], table[i2 * (m + 1) + j2 + 1]);
207
- }
208
- }
209
- const pairs = [];
210
- let i = 0;
211
- let j = 0;
212
- while (i < n && j < m) {
213
- if (a[i] === b[j]) {
214
- pairs.push([i, j]);
215
- i += 1;
216
- j += 1;
217
- } else if (table[(i + 1) * (m + 1) + j] >= table[i * (m + 1) + j + 1]) {
218
- i += 1;
219
- } else {
220
- j += 1;
221
- }
222
- }
223
- return pairs;
224
- }
225
- function matchIds(previous, next) {
226
- const renamed = /* @__PURE__ */ new Map();
227
- const changed = /* @__PURE__ */ new Set();
228
- const oldIds = /* @__PURE__ */ new Set();
229
- for (const block of previous.children) {
230
- walkIds(block, (node) => node.id && oldIds.add(node.id));
231
- }
232
- const byParaId = /* @__PURE__ */ new Map();
233
- for (const block of previous.children) {
234
- walkIds(block, (node) => {
235
- const explicit = paraIdOf(node);
236
- if (explicit && !byParaId.has(explicit)) byParaId.set(explicit, node);
237
- });
238
- }
239
- for (const block of previous.children) {
240
- walkIds(block, (node) => {
241
- if (!node.id) return;
242
- const derived = derivedParaId(node.id);
243
- if (!byParaId.has(derived)) byParaId.set(derived, node);
244
- });
245
- }
246
- const oldParent = /* @__PURE__ */ new Map();
247
- const indexParents = (node) => {
248
- for (const child of childrenOf(node)) {
249
- oldParent.set(child, node);
250
- indexParents(child);
251
- }
252
- };
253
- previous.children.forEach((block) => indexParents(block));
254
- const seenParaIds = /* @__PURE__ */ new Set();
255
- const duplicated = /* @__PURE__ */ new Set();
256
- for (const block of next.children) {
257
- walkIds(block, (node) => {
258
- const pid = paraIdOf(node);
259
- if (!pid) return;
260
- if (seenParaIds.has(pid)) duplicated.add(pid);
261
- seenParaIds.add(pid);
262
- });
263
- }
264
- const claimed = /* @__PURE__ */ new Set();
265
- let fresh = 0;
266
- const carrySubtree = (from, to) => {
267
- if (from.id && to.id) {
268
- renamed.set(to.id, from.id);
269
- to.id = from.id;
270
- }
271
- claimed.add(from);
272
- const a = childrenOf(from);
273
- const b = childrenOf(to);
274
- b.forEach((child, i) => {
275
- if (a[i] && a[i].type === child.type) carrySubtree(a[i], child);
276
- });
277
- };
278
- const matchLists = (olds, news) => {
279
- const pairs = /* @__PURE__ */ new Map();
280
- for (const node of news) {
281
- const pid = paraIdOf(node);
282
- if (pid && duplicated.has(pid)) continue;
283
- const old = pid ? byParaId.get(pid) : void 0;
284
- if (old && !claimed.has(old) && old.type === node.type) {
285
- pairs.set(node, old);
286
- claimed.add(old);
287
- }
288
- }
289
- const oldSet = new Set(olds);
290
- for (const node of news) {
291
- if (pairs.has(node) || paraIdOf(node)) continue;
292
- let found;
293
- walkIds(node, (inner) => {
294
- if (found || inner === node) return;
295
- const pid = paraIdOf(inner);
296
- let at = pid && !duplicated.has(pid) ? byParaId.get(pid) : void 0;
297
- while (at && !oldSet.has(at)) at = oldParent.get(at);
298
- if (at && !claimed.has(at) && at.type === node.type) found = at;
299
- });
300
- if (found) {
301
- pairs.set(node, found);
302
- claimed.add(found);
303
- }
304
- }
305
- const restOld = olds.filter((node) => !claimed.has(node));
306
- const restNew = news.filter((node) => !pairs.has(node));
307
- const lcs = lcsIndexPairs(
308
- restOld.map(contentSignature),
309
- restNew.map(contentSignature)
310
- );
311
- for (const [i, j] of lcs) {
312
- if (restOld[i].type === restNew[j].type) {
313
- pairs.set(restNew[j], restOld[i]);
314
- claimed.add(restOld[i]);
315
- }
316
- }
317
- for (const node of news) {
318
- const old = pairs.get(node);
319
- if (!old) {
320
- walkIds(node, (n) => {
321
- if (n.id && n.type !== "text") fresh += 1;
322
- if (n.id && oldIds.has(n.id)) n.id = void 0;
323
- });
324
- continue;
325
- }
326
- if (contentSignature(old) === contentSignature(node)) {
327
- carrySubtree(old, node);
328
- } else {
329
- if (old.id && node.id) {
330
- renamed.set(node.id, old.id);
331
- node.id = old.id;
332
- changed.add(old.id);
333
- }
334
- matchLists(childrenOf(old), childrenOf(node));
335
- }
336
- }
337
- };
338
- matchLists(previous.children, next.children);
339
- for (const block of next.children) {
340
- walkIds(block, (node) => {
341
- if (node.type !== "text" && node.id === void 0 && "id" in node) {
342
- node.id = freshId(oldIds);
343
- }
344
- });
345
- }
346
- const source = next.docxSource;
347
- if (source) {
348
- source.spans = source.spans.map((span) => ({
349
- ...span,
350
- blockIds: span.blockIds.map((id) => renamed.get(id) ?? id)
351
- }));
352
- }
353
- const nextIds = /* @__PURE__ */ new Set();
354
- for (const block of next.children) {
355
- walkIds(block, (node) => node.id && nextIds.add(node.id));
356
- }
357
- return {
358
- kept: renamed.size,
359
- fresh,
360
- changedIds: [...changed],
361
- removedIds: [...oldIds].filter((id) => !nextIds.has(id))
362
- };
363
- }
364
- function freshId(avoid) {
365
- const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
366
- for (; ; ) {
367
- let id = "";
368
- for (let i = 0; i < 6; i += 1) {
369
- id += alphabet[Math.floor(Math.random() * alphabet.length)];
370
- }
371
- if (!avoid.has(id)) return id;
372
- }
373
- }
374
- async function relearn(ydoc, bytes, filename) {
375
- let next;
376
- try {
377
- next = await parseDocument(bytes, filename);
378
- } catch (error) {
379
- throw new ReviseError(
380
- `${filename} does not open: ${error.message}. Nothing was changed.`,
381
- "import_failed"
382
- );
383
- }
384
- const previous = ydocToDocument(ydoc);
385
- const ids = matchIds(previous, next);
386
- const { refreshDocxSourceSignatures, storeDocxCarrier } = await engineModules.docxGraft();
387
- refreshDocxSourceSignatures(next);
388
- const synced = createReviseYDoc();
389
- seedDocRootWithClaim(synced, next);
390
- if (next.docxSource?.sourceHash) {
391
- await storeDocxCarrier(synced, bytes, {
392
- hash: next.docxSource.sourceHash
393
- });
394
- }
395
- const styleMap = (doc) => new Map(
396
- (doc.stylesheet?.paragraphStyles ?? []).map((style) => [
397
- style.styleId,
398
- JSON.stringify(style)
399
- ])
400
- );
401
- const stylesBefore = styleMap(previous);
402
- const changedStyles = [...styleMap(next)].filter(([id, json]) => stylesBefore.get(id) !== json).map(([id]) => id);
403
- return {
404
- ydoc: synced,
405
- pkg: bytes,
406
- idsKept: ids.kept,
407
- idsNew: ids.fresh,
408
- changedIds: ids.changedIds,
409
- removedIds: ids.removedIds,
410
- changedBlocks: ids.changedIds.length,
411
- changedStyles
412
- };
413
- }
414
-
415
- // src/embed-sdk/lib/document.ts
416
- var READABLE = /\.(docx|md|markdown|html?|odt|rtf|txt)$/i;
417
- var PLAIN_FORMATS = /\.(md|markdown|html?|txt)$/i;
418
- var EXTENSION_FORMATS = {
419
- ".docx": "docx",
420
- ".pdf": "pdf",
421
- ".md": "markdown",
422
- ".markdown": "markdown",
423
- ".html": "html",
424
- ".htm": "html",
425
- ".txt": "txt"
426
- };
427
- function formatForPath(file) {
428
- const match = /\.[^./\\]+$/.exec(file);
429
- return match ? EXTENSION_FORMATS[match[0].toLowerCase()] : void 0;
430
- }
431
- async function readSource(source) {
432
- if (typeof source !== "string") return source;
433
- const fs = await import("node:fs");
434
- if (!fs.existsSync(source)) {
435
- throw new ReviseError(`No such file: ${source}`, "not_found");
436
- }
437
- try {
438
- return { bytes: new Uint8Array(fs.readFileSync(source)), filename: source };
439
- } catch (error) {
440
- throw new ReviseError(
441
- `Could not read ${baseName(source)}: ${error.message}`,
442
- "read_failed",
443
- { cause: error }
444
- );
445
- }
446
- }
447
- async function importYDoc(bytes, filename) {
448
- try {
449
- return await fileToYDoc(bytes, filename);
450
- } catch (error) {
451
- throw new ReviseError(
452
- `${baseName(filename)} does not open: ${error.message}`,
453
- "import_failed",
454
- { cause: error }
455
- );
456
- }
457
- }
458
- function assertReadable(filename) {
459
- if (!READABLE.test(filename)) {
460
- throw new ReviseError(
461
- `Cannot read ${baseName(filename)}. Supported: .docx, .md, .html, .odt, .rtf, .txt.`,
462
- "unsupported_format"
463
- );
464
- }
465
- }
466
- function baseName(file) {
467
- return file.split(/[/\\]/).pop() ?? file;
468
- }
469
- function stem(file) {
470
- return baseName(file).replace(/\.[^.]+$/, "");
471
- }
472
- async function exportYDoc(ydoc, pkg, { format, mode = "review", title = "Document" }) {
473
- if (format === "pdf")
474
- return { bytes: await exportPdf(ydoc, title), outcome: null };
475
- if (format === "docx") return exportSessionDocx(ydoc, pkg, mode);
476
- return {
477
- bytes: await exportDocument(ydoc, { format, mode, title }),
478
- outcome: null
479
- };
480
- }
481
- async function exportPdf(ydoc, title) {
482
- const [{ serializeDocumentFormat }, { DocumentMutator }] = await Promise.all([
483
- engineModules2.documentFormats(),
484
- engineModules2.mutator()
485
- ]);
486
- const doc = DocumentMutator.materializeAcceptedSuggestions(
487
- ydocToDocument2(ydoc)
488
- );
489
- const output = await serializeDocumentFormat(doc, "pdf", { title });
490
- return typeof output === "string" ? new TextEncoder().encode(output) : new Uint8Array(await output.arrayBuffer());
491
- }
492
- var SNAPSHOT_MAGIC = "RVSNAP";
493
- var SNAPSHOT_SCHEMA = 1;
494
- function encodeSnapshot2(header, state, pkg) {
495
- const json = new TextEncoder().encode(
496
- JSON.stringify({
497
- ...header,
498
- schema: SNAPSHOT_SCHEMA,
499
- stateBytes: state.length,
500
- packageBytes: pkg?.length ?? 0
501
- })
502
- );
503
- const magic = new TextEncoder().encode(SNAPSHOT_MAGIC);
504
- const out = new Uint8Array(
505
- magic.length + 4 + json.length + state.length + (pkg?.length ?? 0)
506
- );
507
- out.set(magic, 0);
508
- new DataView(out.buffer).setUint32(magic.length, json.length);
509
- out.set(json, magic.length + 4);
510
- out.set(state, magic.length + 4 + json.length);
511
- if (pkg) out.set(pkg, magic.length + 4 + json.length + state.length);
512
- return out;
513
- }
514
- function decodeSnapshot(bytes) {
515
- const magic = new TextDecoder().decode(
516
- bytes.subarray(0, SNAPSHOT_MAGIC.length)
517
- );
518
- if (magic !== SNAPSHOT_MAGIC) {
519
- return { header: null, state: bytes, pkg: null };
520
- }
521
- try {
522
- const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
523
- const jsonLength = view.getUint32(SNAPSHOT_MAGIC.length);
524
- const start = SNAPSHOT_MAGIC.length + 4;
525
- const header = JSON.parse(
526
- new TextDecoder().decode(bytes.subarray(start, start + jsonLength))
527
- );
528
- const valid = Number.isInteger(header.schema) && header.schema >= 1 && typeof header.documentId === "string" && (header.mode === "suggesting" || header.mode === "editing") && typeof header.title === "string" && (header.author === void 0 || typeof header.author === "string") && Number.isInteger(header.stateBytes) && header.stateBytes > 0 && Number.isInteger(header.packageBytes) && header.packageBytes >= 0 && start + jsonLength + header.stateBytes + header.packageBytes === bytes.length;
529
- if (!valid && !(header.schema > SNAPSHOT_SCHEMA)) throw new Error("header");
530
- if (header.schema > SNAPSHOT_SCHEMA) {
531
- throw new ReviseError(
532
- `This snapshot uses schema ${header.schema}; this version of revise reads up to ${SNAPSHOT_SCHEMA}. Upgrade revise.`,
533
- "snapshot_too_new"
534
- );
535
- }
536
- const stateStart = start + jsonLength;
537
- const state = bytes.subarray(stateStart, stateStart + header.stateBytes);
538
- const pkg = header.packageBytes ? bytes.subarray(
539
- stateStart + header.stateBytes,
540
- stateStart + header.stateBytes + header.packageBytes
541
- ) : null;
542
- if (state.length !== header.stateBytes || pkg && pkg.length !== header.packageBytes) {
543
- throw new Error("truncated");
544
- }
545
- return { header, state, pkg };
546
- } catch (error) {
547
- if (error instanceof ReviseError) throw error;
548
- throw new ReviseError(
549
- "This snapshot is damaged or incomplete.",
550
- "snapshot_corrupt",
551
- {
552
- cause: error
553
- }
554
- );
555
- }
556
- }
557
- var documentCounter = 0;
558
- var ReviseDocument = class _ReviseDocument {
559
- constructor(currentYDoc, currentSession, settings, originalPackage, ownsYDoc) {
560
- this.currentYDoc = currentYDoc;
561
- this.currentSession = currentSession;
562
- this.settings = settings;
563
- this.originalPackage = originalPackage;
564
- this.ownsYDoc = ownsYDoc;
565
- this.disposed = false;
566
- this.queue = Promise.resolve();
567
- // ── tools ────────────────────────────────────────────────────────────────
568
- this.tools = {
569
- /** Every editing tool's name, description, and JSON input schema. */
570
- definitions: () => {
571
- this.assertOpen();
572
- return this.currentSession.tools.getDefinitions();
573
- },
574
- /** Run a known tool with typed input. Expected failures come back as
575
- * `{ ok: false, error }`; nothing throws for a bad edit. */
576
- call: (name, ...args) => this.serial(
577
- async () => this.afterTool(
578
- name,
579
- await this.currentSession.tools.execute(name, ...args)
580
- )
581
- ),
582
- /** Run a tool by name with untyped input (agent tool calls). Unknown
583
- * tools and bad input come back as `{ ok: false, error }` too. */
584
- run: (name, input = {}, options = {}) => this.serial(async () => {
585
- const definition = this.currentSession.tools.getDefinitions().find((tool) => tool.name === name);
586
- if (!definition) {
587
- return {
588
- ok: false,
589
- error: {
590
- code: "unknown_tool",
591
- message: `Unknown tool "${name}".`
592
- }
593
- };
594
- }
595
- const required = definition.inputSchema.required ?? [];
596
- const filled = required.includes("context_notes") && input.context_notes === void 0 ? { ...input, context_notes: "N/A" } : input;
597
- return this.afterTool(
598
- name,
599
- await this.currentSession.tools.executeDynamic(
600
- name,
601
- filled,
602
- options.direct ? { directMode: true } : void 0
603
- )
604
- );
605
- })
606
- };
607
- }
608
- /** Open a file (path) or bytes. Word files keep their original package,
609
- * so untouched content exports byte for byte. */
610
- static async open(source, options = {}) {
611
- const { bytes, filename } = await readSource(source);
612
- assertReadable(filename);
613
- const ydoc = await importYDoc(bytes, filename);
614
- return _ReviseDocument.build(ydoc, {
615
- mode: PLAIN_FORMATS.test(filename) ? "editing" : "suggesting",
616
- ...options,
617
- title: stem(filename),
618
- package: /\.docx$/i.test(filename) ? bytes : null,
619
- owned: true
620
- });
621
- }
622
- /** Start a document from the HTML the tools read and write (headings,
623
- * `data-style-id="Title"`, lists, tables), or from Markdown. */
624
- static async create(content = {}, options = {}) {
625
- let ydoc;
626
- if (content.html !== void 0) {
627
- await installDomShims();
628
- const { AgentHtmlConverter } = await engineModules2.agentHtml();
629
- const body = /<body[^>]*>([\s\S]*)<\/body>/i.exec(content.html)?.[1] ?? content.html;
630
- ydoc = createReviseYDoc2();
631
- seedDocRootWithClaim2(ydoc, AgentHtmlConverter.fromAgentHtml(body));
632
- } else {
633
- const markdown = content.markdown ?? (content.title ? `# ${content.title}
634
- ` : "\n");
635
- ydoc = await importYDoc(new TextEncoder().encode(markdown), "new.md");
636
- }
637
- return _ReviseDocument.build(ydoc, {
638
- mode: "editing",
639
- ...options,
640
- title: content.title ?? "Document",
641
- package: null,
642
- owned: true
643
- });
644
- }
645
- /** Restore a document from `snapshot()` (or a bare Y.js update). Options
646
- * override what the snapshot recorded. */
647
- static async restore(snapshot2, options = {}) {
648
- const { header, state, pkg } = decodeSnapshot(snapshot2);
649
- let ydoc;
650
- try {
651
- ydoc = decodeYDoc(state);
652
- } catch (error) {
653
- throw new ReviseError("This snapshot is damaged.", "snapshot_corrupt", {
654
- cause: error
655
- });
656
- }
657
- return _ReviseDocument.build(ydoc, {
658
- ...header ? {
659
- documentId: header.documentId,
660
- mode: header.mode,
661
- title: header.title,
662
- ...header.author ? { author: header.author } : {}
663
- } : {},
664
- ...options,
665
- package: pkg,
666
- owned: true
667
- });
668
- }
669
- /** Wrap an existing Revise Y.Doc, e.g. one shared with an editor. The
670
- * Y.Doc stays yours: `dispose()` leaves it intact, and `reload()` is not
671
- * available (it would have to replace it). */
672
- static async fromYDoc(ydoc, options = {}) {
673
- let pkg = options.package;
674
- if (pkg === void 0) {
675
- const { loadDocxCarrier } = await engineModules2.docxGraft();
676
- pkg = loadDocxCarrier(ydoc)?.bytes ?? null;
677
- }
678
- return _ReviseDocument.build(ydoc, {
679
- ...options,
680
- package: pkg,
681
- owned: false
682
- });
683
- }
684
- static async build(ydoc, options) {
685
- const { package: pkg, owned, ...rest } = options;
686
- const settings = {
687
- ...rest,
688
- documentId: rest.documentId ?? `revise-${Date.now()}-${++documentCounter}`,
689
- mode: rest.mode ?? "suggesting",
690
- title: rest.title ?? "Document"
691
- };
692
- return new _ReviseDocument(
693
- ydoc,
694
- await _ReviseDocument.startSession(ydoc, settings),
695
- settings,
696
- pkg ?? null,
697
- owned
698
- );
699
- }
700
- static async startSession(ydoc, settings) {
701
- (await engineModules2.serverSession()).requireDocxRoundTrip(ydoc);
702
- return createServerDocumentSession(ydoc, {
703
- documentId: settings.documentId,
704
- mode: settings.mode,
705
- ...settings.author ? { agentName: settings.author } : {},
706
- ...settings.images ? { images: settings.images } : {}
707
- });
708
- }
709
- assertOpen() {
710
- if (this.disposed) {
711
- throw new ReviseError("This document was disposed.", "disposed");
712
- }
713
- }
714
- /** Run `work` after every operation already queued on this document. */
715
- serial(work) {
716
- if (this.disposed) {
717
- return Promise.reject(
718
- new ReviseError("This document was disposed.", "disposed")
719
- );
720
- }
721
- const run = this.queue.then(() => {
722
- this.assertOpen();
723
- return work();
724
- });
725
- this.queue = run.catch(() => void 0);
726
- return run;
727
- }
728
- /** The live document model. Shared with `@reviseio/editor`. */
729
- get ydoc() {
730
- return this.currentYDoc;
731
- }
732
- /** The low-level tool session behind `tools`: an escape hatch for what
733
- * the methods here don't cover. */
734
- get session() {
735
- return this.currentSession;
736
- }
737
- get mode() {
738
- return this.currentSession.getMode();
739
- }
740
- /** Switch between tracked changes ("suggesting") and direct edits. */
741
- setMode(mode) {
742
- return this.serial(() => {
743
- if (mode === "suggesting") this.currentSession.setSuggestingMode();
744
- else this.currentSession.setEditingMode();
745
- this.settings.mode = mode;
746
- });
747
- }
748
- get author() {
749
- return this.settings.author;
750
- }
751
- get documentId() {
752
- return this.settings.documentId;
753
- }
754
- /** The original Word package that export grafts onto, if any. */
755
- get package() {
756
- return this.originalPackage;
757
- }
758
- /** The complete document (versioned; includes the original Word package)
759
- * to store or hand to `@reviseio/editor`, as of every operation queued
760
- * before this call. Restore with `restore()`. */
761
- snapshot() {
762
- return this.serial(
763
- () => encodeSnapshot2(
764
- {
765
- documentId: this.settings.documentId,
766
- mode: this.mode,
767
- ...this.settings.author ? { author: this.settings.author } : {},
768
- title: this.settings.title
769
- },
770
- encodeYDoc(this.currentYDoc),
771
- this.originalPackage
772
- )
773
- );
774
- }
775
- /** An opaque value that changes whenever the document does (including
776
- * edits that only delete, which a state vector misses). */
777
- version() {
778
- this.assertOpen();
779
- return Y.encodeSnapshot(Y.snapshot(this.currentYDoc));
780
- }
781
- /**
782
- * Re-read the document from new bytes (it changed elsewhere). Block ids
783
- * carry over only where identity is provable (Word's paragraph ids, or
784
- * identical content); `changedIds` lists kept ids whose content changed,
785
- * so earlier reads of them are stale. Replaces the Y.Doc, so it is not
786
- * available on a document made with `fromYDoc`.
787
- */
788
- reload(bytes, filename) {
789
- return this.serial(async () => {
790
- if (!this.ownsYDoc) {
791
- throw new ReviseError(
792
- "reload() would replace a Y.Doc you own (fromYDoc). Open a new document instead.",
793
- "borrowed_ydoc"
794
- );
795
- }
796
- const fresh = await relearn(this.currentYDoc, bytes, filename);
797
- const session = await _ReviseDocument.startSession(
798
- fresh.ydoc,
799
- this.settings
800
- );
801
- this.currentSession.dispose();
802
- this.currentYDoc.destroy();
803
- this.currentYDoc = fresh.ydoc;
804
- this.currentSession = session;
805
- if (/\.docx$/i.test(filename)) this.originalPackage = bytes;
806
- return {
807
- idsKept: fresh.idsKept,
808
- idsNew: fresh.idsNew,
809
- changedIds: fresh.changedIds,
810
- removedIds: fresh.removedIds
811
- };
812
- });
813
- }
814
- /** Start the tool session over: forget what the agent has read and
815
- * searched (e.g. after the host compacts its transcript). */
816
- resetToolContext() {
817
- return this.serial(async () => {
818
- const session = await _ReviseDocument.startSession(
819
- this.currentYDoc,
820
- this.settings
821
- );
822
- this.currentSession.dispose();
823
- this.currentSession = session;
824
- });
825
- }
826
- /** Release the tool session. The document can't be used afterwards; a
827
- * Y.Doc it created is destroyed, one passed to fromYDoc is left alone. */
828
- dispose() {
829
- if (this.disposed) return;
830
- this.disposed = true;
831
- this.currentSession.dispose();
832
- if (this.ownsYDoc) this.currentYDoc.destroy();
833
- }
834
- /** The document's title for exports: its saved title property (dc:title
835
- * in a .docx, or set_title's), else the name it was opened under. */
836
- title() {
837
- const core = this.currentYDoc.getMap("metadata").get("coreProperties");
838
- return typeof core?.title === "string" && core.title.trim() ? core.title : this.settings.title;
839
- }
840
- /** set_title renames the document in the editor. In a file, the title is
841
- * the document property (docProps/core.xml in a .docx, the PDF title), so
842
- * it is saved there too; without this, the tool reported success and the
843
- * saved file kept its old title. */
844
- afterTool(name, result) {
845
- if (name !== "set_title" || !result.ok) return result;
846
- const metadata = this.currentYDoc.getMap("metadata");
847
- const title = metadata.get("title");
848
- if (typeof title !== "string") return result;
849
- const core = metadata.get("coreProperties");
850
- this.currentYDoc.transact(() => {
851
- metadata.set("coreProperties", {
852
- ...core && typeof core === "object" ? core : {},
853
- title
854
- });
855
- });
856
- this.settings.title = title;
857
- return result;
858
- }
859
- // ── reading ──────────────────────────────────────────────────────────────
860
- /** Sections with their block ranges and word counts. */
861
- outline(options = {}) {
862
- return this.serial(async () => {
863
- if (options.limit === void 0) {
864
- return this.allSections(options.offset ?? 0);
865
- }
866
- const measured = await this.currentSession.tools.call(
867
- "measure_document",
868
- {
869
- include_sections: true,
870
- section_limit: options.limit,
871
- section_offset: options.offset ?? 0
872
- }
873
- );
874
- return measured.data;
875
- });
876
- }
877
- /** measure_document with every section from `offset` on, page by page. */
878
- async allSections(offset = 0) {
879
- let data;
880
- const sections = [];
881
- let next = offset;
882
- for (let page = 0; next !== null && page < 50; page++) {
883
- const measured = await this.currentSession.tools.call("measure_document", {
884
- include_sections: true,
885
- section_limit: 100,
886
- section_offset: next
887
- });
888
- data ??= measured.data;
889
- sections.push(...measured.data.sections ?? []);
890
- const after = measured.data.hasMoreSections ? measured.data.nextSectionOffset ?? null : null;
891
- next = after !== null && after > next ? after : null;
892
- }
893
- return {
894
- ...data,
895
- sections,
896
- hasMoreSections: false,
897
- nextSectionOffset: void 0
898
- };
899
- }
900
- /**
901
- * The top-level blocks a selection names, in document order: block ids
902
- * and `first..last` ranges (a nested id stands for the block holding it),
903
- * a section (its heading's range, subsections included; "4,7,9" for
904
- * several), or `count`
905
- * blocks from `index`. Naming a block or section that doesn't exist is an
906
- * error.
907
- */
908
- selectBlocks(selection) {
909
- return this.serial(async () => {
910
- const top = ydocToDocument2(this.currentYDoc).children;
911
- const position = /* @__PURE__ */ new Map();
912
- top.forEach((block, at2) => {
913
- const visit = (node) => {
914
- if (!node || typeof node !== "object") return;
915
- const n = node;
916
- if (typeof n.id === "string" && n.type !== "text") {
917
- position.set(n.id, at2);
918
- }
919
- n.children?.forEach(visit);
920
- };
921
- visit(block);
922
- });
923
- const at = (id) => {
924
- const found = position.get(id);
925
- if (found === void 0) {
926
- throw new ReviseError(`No block with id "${id}".`, "block_not_found");
927
- }
928
- return found;
929
- };
930
- const picked = /* @__PURE__ */ new Set();
931
- const range = (from, to) => {
932
- for (let i = from; i <= to; i++) picked.add(i);
933
- };
934
- for (const item of selection.blocks ? parseBlockQuery(selection.blocks) : []) {
935
- if (typeof item === "string") {
936
- picked.add(at(item));
937
- continue;
938
- }
939
- const from = at(item.from);
940
- const to = at(item.to);
941
- if (from > to) {
942
- throw new ReviseError(
943
- `Block "${item.from}" comes after "${item.to}"; write the range first..last.`,
944
- "invalid_selection"
945
- );
946
- }
947
- range(from, to);
948
- }
949
- if (selection.section !== void 0) {
950
- const measured = { data: await this.allSections() };
951
- const sections = (measured.data.sections ?? []).map((s) => ({
952
- title: s.title,
953
- start: at(s.startBlockId),
954
- end: at(s.endBlockId)
955
- }));
956
- const queries = /^[\d.\s]+(,[\d.\s]+)+$/.test(selection.section) ? selection.section.split(",").map((part) => part.trim()) : [selection.section];
957
- for (const query of queries) {
958
- const q = query.trim().toLowerCase();
959
- const exact = sections.filter(
960
- (s) => s.title.toLowerCase() === q || s.title.toLowerCase().replace(/^[\d.]+[.)]?\s+/, "") === q
961
- );
962
- const candidates = exact.length ? exact : sections.filter((s) => headingMatches(s.title, query));
963
- const matched = candidates.filter(
964
- (s) => !candidates.some(
965
- (outer) => outer !== s && outer.start <= s.start && s.end <= outer.end && (outer.start < s.start || s.end < outer.end)
966
- )
967
- );
968
- if (matched.length > 1) {
969
- throw new ReviseError(
970
- `"${query}" matches ${matched.length} headings: ${matched.map((s) => `"${s.title}"`).join(", ")}. Name one by its number or full title.`,
971
- "section_ambiguous"
972
- );
973
- }
974
- if (!matched.length) {
975
- throw new ReviseError(
976
- sections.length ? `No heading matches "${query}". Headings: ${sections.slice(0, 12).map((s) => `"${s.title}"`).join(", ")}${sections.length > 12 ? ", \u2026" : ""}.` : `No heading matches "${query}": the document has no headings. Select blocks by id instead.`,
977
- "section_not_found"
978
- );
979
- }
980
- range(matched[0].start, matched[0].end);
981
- }
982
- }
983
- if (selection.count !== void 0) {
984
- const from = selection.index ?? 0;
985
- range(from, Math.min(top.length, from + selection.count) - 1);
986
- }
987
- return [...picked].sort((a, b) => a - b).map((i) => top[i].id);
988
- });
989
- }
990
- /**
991
- * Paragraphs that look like section headings but aren't styled as
992
- * headings: short, entirely bold, and numbered like the document's real
993
- * headings ("12. Overtime" between headings 11 and 13) with a number no
994
- * heading has. Word documents often have one typed by hand.
995
- */
996
- unstyledHeadings() {
997
- return this.serial(async () => {
998
- const top = ydocToDocument2(this.currentYDoc).children;
999
- const textOf = (block) => (block.children ?? []).map((node) => node.type === "text" ? node.text ?? "" : "").join("").trim();
1000
- const numberOf = (title) => {
1001
- const m = /^(\d+(?:\.\d+)*)([.)]?)\s+\S/.exec(title);
1002
- return m ? { number: m[1], mark: m[2] } : void 0;
1003
- };
1004
- const headings = top.filter((block) => block.heading !== void 0);
1005
- const used = /* @__PURE__ */ new Map();
1006
- for (const heading of headings) {
1007
- const n = numberOf(textOf(heading));
1008
- if (!n) continue;
1009
- const depth = n.number.split(".").length;
1010
- const entry = used.get(depth) ?? { numbers: /* @__PURE__ */ new Set(), mark: n.mark };
1011
- entry.numbers.add(n.number);
1012
- used.set(depth, entry);
1013
- }
1014
- const found = [];
1015
- top.forEach((block, blockIndex) => {
1016
- if (block.type !== "paragraph" || block.heading !== void 0) return;
1017
- const text = textOf(block);
1018
- const n = numberOf(text);
1019
- if (!n || text.length > 80) return;
1020
- const depth = n.number.split(".").length;
1021
- const siblings = used.get(depth);
1022
- if (!siblings || siblings.numbers.size < 2 || siblings.numbers.has(n.number) || siblings.mark !== n.mark) {
1023
- return;
1024
- }
1025
- const runs = (block.children ?? []).filter(
1026
- (node) => node.type === "text" && (node.text ?? "").trim()
1027
- );
1028
- if (!runs.length || !runs.every((node) => node.marks?.bold)) return;
1029
- found.push({ id: block.id, blockIndex, text, level: depth });
1030
- });
1031
- return found;
1032
- });
1033
- }
1034
- /**
1035
- * read/search HTML for a reader to look at, leaving out what goes without
1036
- * saying: in each paragraph or heading, spacing, line height, keep
1037
- * settings, and text size and color equal to what its paragraph style
1038
- * (data-style-id, else the default "Normal") already sets; in table
1039
- * cells, the default 1px border and 8px padding; and grid columns in
1040
- * tables with no merged cells. Every value left out is one the block
1041
- * gets anyway when it's written back without it. Starts with a comment
1042
- * saying so.
1043
- */
1044
- compactHtml(html) {
1045
- this.assertOpen();
1046
- const raw = ydocToDocument2(this.currentYDoc);
1047
- const styles = raw.stylesheet?.paragraphStyles ?? [];
1048
- const byId = new Map(styles.map((style) => [style.styleId, style]));
1049
- const normal = styles.find((style) => style.default);
1050
- const near = (value, want, unit = "") => {
1051
- if (want === void 0 || !value.endsWith(unit)) return false;
1052
- const n = Number(value.slice(0, value.length - unit.length));
1053
- return Number.isFinite(n) && Math.abs(n - want) < 0.01;
1054
- };
1055
- const sameColor = (value, want) => want !== void 0 && value.toLowerCase() === want.toLowerCase();
1056
- const restyle = (tag, drop) => tag.replace(/ style="([^"]*)"/, (_, style) => {
1057
- const left = style.split(";").map((d) => d.trim()).filter(Boolean).filter((d) => {
1058
- const at = d.indexOf(":");
1059
- return at < 0 || !drop(d.slice(0, at).trim(), d.slice(at + 1).trim());
1060
- });
1061
- return left.length ? ` style="${left.join("; ")}"` : "";
1062
- });
1063
- let changed = false;
1064
- let out = html.replace(/<(p|h[1-6])\b[^>]*>[\s\S]*?<\/\1>/g, (block) => {
1065
- const open = /^<[^>]*>/.exec(block)[0];
1066
- const id = / data-style-id="([^"]*)"/.exec(open)?.[1];
1067
- const style = id === void 0 ? normal : byId.get(id);
1068
- if (!style) return block;
1069
- const attrs = style.paragraphAttrs ?? {};
1070
- const marks = style.runMarks ?? {};
1071
- const text = (prop, value) => prop === "font-size" && near(value, marks.fontSize, "px") || prop === "color" && sameColor(value, marks.color);
1072
- const styled = id !== void 0 || /^<h[1-6]/.test(open);
1073
- const lean = restyle(
1074
- open,
1075
- (prop, value) => styled && (prop === "margin-top" && near(value, attrs.spaceBefore, "px") || prop === "margin-bottom" && near(value, attrs.spaceAfter, "px") || prop === "line-height" && near(value, attrs.lineSpacing) || prop === "break-after" && value === "avoid" && !!attrs.keepNext || prop === "break-inside" && value === "avoid" && !!attrs.keepLines) || text(prop, value)
1076
- );
1077
- const body = block.slice(open.length).replace(/<span\b[^>]*>/g, (span) => restyle(span, text));
1078
- const result = lean + body;
1079
- if (result !== block) changed = true;
1080
- return result;
1081
- });
1082
- out = out.replace(/<t[dh]\b[^>]*>/g, (td) => {
1083
- const lean = restyle(
1084
- td,
1085
- (prop, value) => prop === "border" && value === "1px solid #ccc" || prop === "padding" && value === "8px"
1086
- );
1087
- if (lean !== td) changed = true;
1088
- return lean;
1089
- });
1090
- out = out.replace(
1091
- /<table\b[^>]*>(?:(?!<table\b)[\s\S])*?<\/table>/g,
1092
- (table) => {
1093
- if (/ (colspan|rowspan)="/.test(table)) return table;
1094
- const lean = table.replace(/ data-grid-column="\d+"/g, "");
1095
- if (lean !== table) changed = true;
1096
- return lean;
1097
- }
1098
- );
1099
- out = out.replace(/<(table|t[dh])\b[^>]* data-borders=[^>]*>/g, (tag) => {
1100
- const lean = restyle(
1101
- tag,
1102
- (prop) => /^border(-(top|right|bottom|left))?$/.test(prop)
1103
- );
1104
- if (lean !== tag) changed = true;
1105
- return lean;
1106
- });
1107
- out = out.replace(
1108
- / ([\w-]+)="([^"]*&quot;[^"]*)"/g,
1109
- (whole, name, value) => {
1110
- if (value.includes("'") || value.includes("&#039;")) return whole;
1111
- changed = true;
1112
- return ` ${name}='${value.replace(/&quot;/g, '"')}'`;
1113
- }
1114
- );
1115
- if (!changed) return out;
1116
- const size = normal?.runMarks?.fontSize;
1117
- const note = `<!-- Defaults left out: what a block's style sets${size !== void 0 ? `, ${size}px text` : ""}, 1px cell borders, 8px cell padding. -->
1118
- `;
1119
- return html.length - out.length > note.length ? note + out : html;
1120
- }
1121
- /** Blocks, words, pending changes, and how faithfully it exports to Word. */
1122
- status() {
1123
- return this.serial(async () => {
1124
- const measured = await this.currentSession.tools.call(
1125
- "measure_document",
1126
- {}
1127
- );
1128
- return {
1129
- blocks: measured.data.totalBlocks,
1130
- words: measured.data.wordCount,
1131
- pendingSuggestions: this.currentSession.listSuggestions().length,
1132
- fidelity: this.originalPackage ? await ydocSourceFidelity2(this.currentYDoc) : null
1133
- };
1134
- });
1135
- }
1136
- /** Text-bearing blocks matching a pattern (paragraphs, headings, list
1137
- * items, table-cell paragraphs), with their ids. */
1138
- grep(pattern, options = {}) {
1139
- this.assertOpen();
1140
- const matcher = typeof pattern === "string" ? new RegExp(
1141
- pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"),
1142
- options.ignoreCase ? "gi" : "g"
1143
- ) : new RegExp(
1144
- pattern.source,
1145
- pattern.flags.includes("g") ? pattern.flags : `${pattern.flags}g`
1146
- );
1147
- const view = options.view ?? "tracked";
1148
- return textBlocks(ydocToDocument2(this.currentYDoc)).map((block) => {
1149
- const matches = matchesIn(block.segments, matcher, view);
1150
- return {
1151
- id: block.id,
1152
- blockIndex: block.topIndex,
1153
- count: matches.length,
1154
- text: segmentsText(block.segments, view),
1155
- matches
1156
- };
1157
- }).filter((hit) => hit.count > 0);
1158
- }
1159
- /**
1160
- * HTML the read tools returned (`read_blocks_from_index`,
1161
- * `read_specific_blocks`), in a view. The tools read the accepted view;
1162
- * "tracked" marks pending changes as `<ins>`/`<del>` with
1163
- * `data-change-id` and `data-author`, and shows whole blocks a change
1164
- * deletes where they were; "original" shows the text before every change.
1165
- * Block ids are unchanged. The editing tools read `<ins>` as its text and
1166
- * ignore `<del>`, so tracked HTML pasted back edits the accepted text.
1167
- */
1168
- viewHtml(html, view) {
1169
- if (view === "accepted") return Promise.resolve(html);
1170
- return this.serial(async () => {
1171
- await installDomShims();
1172
- const { AgentHtmlConverter } = await engineModules2.agentHtml();
1173
- const raw = ydocToDocument2(this.currentYDoc);
1174
- const authors = new Map(
1175
- this.currentSession.listSuggestions().map((r) => [r.id, r.agentName ?? r.authorType ?? "unknown"])
1176
- );
1177
- return renderView(html, raw, view, authors, AgentHtmlConverter);
1178
- });
1179
- }
1180
- /** What the document holds, in one glance: page setup, header and footer
1181
- * text with live fields, title, headings, lists, tables, review state. */
1182
- summary() {
1183
- return this.serial(
1184
- () => documentSummary(this.currentYDoc, this.currentSession)
1185
- );
1186
- }
1187
- // ── editing ──────────────────────────────────────────────────────────────
1188
- /**
1189
- * Replace every occurrence everywhere: the body, tables, lists, page
1190
- * headers and footers, and footnotes and endnotes. Tracked changes in
1191
- * suggesting mode. Curly and straight quotes and non-breaking spaces match
1192
- * each other unless `exact`, and the replacement keeps the document's
1193
- * characters (see matchDocumentCharacters). Replacing text with itself
1194
- * changes nothing and is not a failure.
1195
- */
1196
- replaceAll(find, replace, options = {}) {
1197
- return this.serial(async () => {
1198
- if (!find) {
1199
- throw new ReviseError("Nothing to find: give some text.", "usage");
1200
- }
1201
- const only = options.blocks ?? options.ids;
1202
- const all = replaceTargets(ydocToDocument2(this.currentYDoc)).filter(
1203
- (block) => !only || only.includes(block.id)
1204
- );
1205
- const exactMatch = textMatcher(find, {
1206
- exact: options.exact,
1207
- ignoreCase: options.ignoreCase
1208
- });
1209
- const loosest = textMatcher(find, { ignoreCase: true });
1210
- const caseOnly = textMatcher(find, {
1211
- exact: options.exact,
1212
- ignoreCase: true
1213
- });
1214
- const targets = [];
1215
- const missed = /* @__PURE__ */ new Map();
1216
- for (const block of all) {
1217
- const matches = exactMatch(block.text);
1218
- if (matches.length) targets.push({ ...block, matches });
1219
- const left = [...matches];
1220
- for (const text of loosest(block.text)) {
1221
- const at = left.indexOf(text);
1222
- if (at >= 0) {
1223
- left.splice(at, 1);
1224
- continue;
1225
- }
1226
- const reason = caseOnly(text).length ? "case" : "characters";
1227
- const entry = missed.get(text) ?? { count: 0, reason };
1228
- entry.count += 1;
1229
- missed.set(text, entry);
1230
- }
1231
- }
1232
- const nearMisses = [...missed].map(([text, m]) => ({ text, ...m }));
1233
- const regions = {};
1234
- if (find === replace) {
1235
- return {
1236
- replaced: 0,
1237
- blocks: targets.map((t) => t.id),
1238
- regions,
1239
- changeIds: [],
1240
- failures: [],
1241
- nearMisses
1242
- };
1243
- }
1244
- const failures = [];
1245
- const changeIds = [];
1246
- let replaced = 0;
1247
- for (const target of targets) {
1248
- await this.currentSession.tools.executeDynamic("read_specific_blocks", {
1249
- block_ids: [target.id],
1250
- context_notes: "N/A"
1251
- });
1252
- const distinct = [...new Set(target.matches)];
1253
- const result = await this.currentSession.tools.executeDynamic(
1254
- "replace",
1255
- {
1256
- id: target.id,
1257
- replacements: distinct.map((text) => ({
1258
- find: text,
1259
- replace: matchDocumentCharacters(find, text, replace),
1260
- occurrence: "all"
1261
- })),
1262
- context_notes: "N/A"
1263
- }
1264
- );
1265
- if ("error" in result) {
1266
- if (!/^No changes detected/i.test(result.error.message)) {
1267
- failures.push({
1268
- blockId: target.id,
1269
- code: result.error.code,
1270
- message: result.error.message
1271
- });
1272
- }
1273
- } else {
1274
- replaced += target.matches.length;
1275
- regions[target.region] = (regions[target.region] ?? 0) + target.matches.length;
1276
- changeIds.push(...result.value.suggestionIds ?? []);
1277
- }
1278
- }
1279
- return {
1280
- replaced,
1281
- blocks: targets.map((t) => t.id),
1282
- regions,
1283
- changeIds,
1284
- failures,
1285
- nearMisses
1286
- };
1287
- });
1288
- }
1289
- /** @deprecated The 3.0 name of `replaceAll`. */
1290
- replaceText(find, replace, options = {}) {
1291
- return this.replaceAll(find, replace, options);
1292
- }
1293
- /** The document as it was before every pending change, exported from
1294
- * a copy: this document keeps its changes. */
1295
- async exportOriginal(options) {
1296
- const copy = await _ReviseDocument.restore(await this.snapshot(), {
1297
- author: this.settings.author
1298
- });
1299
- try {
1300
- await copy.reject({ all: true });
1301
- const result = await copy.export({
1302
- ...options,
1303
- view: void 0,
1304
- mode: "final"
1305
- });
1306
- return { ...result, view: "original" };
1307
- } finally {
1308
- copy.dispose();
1309
- }
1310
- }
1311
- // ── review ───────────────────────────────────────────────────────────────
1312
- /** Pending tracked changes with where they are and what they do. */
1313
- changes(filter = {}) {
1314
- return this.serial(() => this.filteredChanges(filter));
1315
- }
1316
- async filteredChanges(filter = {}) {
1317
- return applyChangeFilter(
1318
- await describeChanges(this.currentYDoc, this.currentSession),
1319
- filter
1320
- );
1321
- }
1322
- /** The tracked changes a selection would settle, without settling them. */
1323
- preview(selection) {
1324
- return this.serial(async () => {
1325
- const changes = await this.filteredChanges();
1326
- const ids = await this.selectedIds(selection, changes);
1327
- return ids === "all" ? changes : changes.filter((c) => ids.includes(c.id));
1328
- });
1329
- }
1330
- accept(selection) {
1331
- return this.serial(() => this.settle(selection, true));
1332
- }
1333
- reject(selection) {
1334
- return this.serial(() => this.settle(selection, false));
1335
- }
1336
- async selectedIds(selection, known) {
1337
- const { section, exceptSection, blocks, all } = selection;
1338
- const ids = selection.ids ?? [];
1339
- const filtered = Boolean(section || exceptSection || blocks?.length);
1340
- const kinds = [ids.length > 0, Boolean(all), filtered];
1341
- if (kinds.filter(Boolean).length !== 1) {
1342
- throw new ReviseError(
1343
- "Select tracked changes one way: ids, all, or filters (section, exceptSection, blocks).",
1344
- "invalid_selection"
1345
- );
1346
- }
1347
- if (filtered) {
1348
- return applyChangeFilter(
1349
- known ?? await describeChanges(this.currentYDoc, this.currentSession),
1350
- { section, exceptSection, blocks }
1351
- ).map((c) => c.id);
1352
- }
1353
- return all ? "all" : ids;
1354
- }
1355
- async settle(selection, accept) {
1356
- const ids = await this.selectedIds(selection);
1357
- const session = this.currentSession;
1358
- if (ids === "all") {
1359
- return accept ? session.acceptAllSuggestions() : session.rejectAllSuggestions();
1360
- }
1361
- if (!ids.length) return { resolved: [], missing: [], unresolved: [] };
1362
- return accept ? session.acceptSuggestions(ids) : session.rejectSuggestions(ids);
1363
- }
1364
- /**
1365
- * What the pending tracked changes do to the text (before → after), or
1366
- * what changed since `baseline` (a snapshot).
1367
- */
1368
- diff(baseline) {
1369
- return this.serial(async () => {
1370
- let before;
1371
- if (baseline) {
1372
- before = decodeYDoc(decodeSnapshot(baseline).state);
1373
- } else {
1374
- before = decodeYDoc(encodeYDoc(this.currentYDoc));
1375
- const copy = await createServerDocumentSession(before, {
1376
- documentId: `${this.settings.documentId}-diff`
1377
- });
1378
- copy.rejectAllSuggestions();
1379
- copy.dispose();
1380
- }
1381
- return textDiff(
1382
- await markdownOf(before),
1383
- await markdownOf(this.currentYDoc)
1384
- );
1385
- });
1386
- }
1387
- // ── output ───────────────────────────────────────────────────────────────
1388
- /** Write the document in a format. Reports what the output actually
1389
- * contains: its pending changes, and for .docx how much of the original
1390
- * file was carried over byte for byte. */
1391
- async export(options) {
1392
- const view = viewOfMode(options);
1393
- if (view === "original") return this.exportOriginal(options);
1394
- return this.serial(async () => {
1395
- options = {
1396
- ...options,
1397
- format: exportFormat(options.format),
1398
- mode: view === "accepted" ? "final" : options.mode
1399
- };
1400
- const mode = options.format === "docx" ? options.mode ?? "review" : "final";
1401
- const pending = this.currentSession.listSuggestions().length;
1402
- const { bytes, outcome } = await exportYDoc(
1403
- this.currentYDoc,
1404
- this.originalPackage,
1405
- { title: this.title(), ...options, mode }
1406
- );
1407
- const warnings = [];
1408
- if (options.format !== "docx" && (options.mode ?? "review") === "review" && pending) {
1409
- warnings.push(
1410
- `A .${options.format === "markdown" ? "md" : options.format} file can't hold tracked changes: it shows the ${pending} pending change${pending === 1 ? "" : "s"} accepted.`
1411
- );
1412
- }
1413
- if (outcome?.method === "regenerated") {
1414
- if (outcome.fallbackCode === "no-original-package") {
1415
- warnings.push(
1416
- "The original Word file isn't available (e.g. restored from a bare Y.js state), so the whole .docx was written fresh."
1417
- );
1418
- } else if (outcome.fallbackCode !== "not-from-docx") {
1419
- warnings.push(
1420
- `Couldn't carry untouched content over from the original Word file (${outcome.fallbackCode}); the whole .docx was written fresh.`
1421
- );
1422
- }
1423
- }
1424
- return {
1425
- bytes,
1426
- format: options.format,
1427
- mode,
1428
- view: mode === "final" ? "accepted" : "tracked",
1429
- pendingTrackedChanges: mode === "final" ? 0 : pending,
1430
- fidelity: outcome,
1431
- warnings
1432
- };
1433
- });
1434
- }
1435
- /** Pages as images, laid out exactly as the editor lays them out. */
1436
- renderPages(options = {}) {
1437
- return this.serial(async () => {
1438
- try {
1439
- const { renderPages } = await import("./render-QD5VWKSV.js");
1440
- return await renderPages(
1441
- this.currentYDoc,
1442
- this.settings.documentId,
1443
- options
1444
- );
1445
- } catch (error) {
1446
- if (error instanceof ReviseError) throw error;
1447
- throw new ReviseError(
1448
- `Rendering failed: ${error.message}`,
1449
- "render_failed",
1450
- { cause: error }
1451
- );
1452
- }
1453
- });
1454
- }
1455
- };
1456
- async function seedYDoc(ydoc, source) {
1457
- const { bytes, filename } = await readSource(source);
1458
- assertReadable(filename);
1459
- return seedYDocFromFile(ydoc, bytes, filename);
1460
- }
1461
- async function compare(originalSource, revisedSource, options = {}) {
1462
- const [originalFile, revisedFile] = await Promise.all([
1463
- readSource(originalSource),
1464
- readSource(revisedSource)
1465
- ]);
1466
- assertReadable(originalFile.filename);
1467
- assertReadable(revisedFile.filename);
1468
- const [original, revised] = await Promise.all([
1469
- parseDocument2(originalFile.bytes, originalFile.filename),
1470
- parseDocument2(revisedFile.bytes, revisedFile.filename)
1471
- ]);
1472
- const author = options.author ?? baseName(revisedFile.filename);
1473
- const { compareDocuments } = await engineModules2.compare();
1474
- const comparison = compareDocuments(original, revised, {
1475
- authorName: author
1476
- });
1477
- if (!comparison) {
1478
- throw new ReviseError(
1479
- "These documents are too large to compare.",
1480
- "too_large"
1481
- );
1482
- }
1483
- const ydoc = createReviseYDoc2();
1484
- seedDocRootWithClaim2(ydoc, comparison.doc);
1485
- const isDocx = /\.docx$/i.test(revisedFile.filename);
1486
- if (comparison.doc.docxSource?.sourceHash && isDocx) {
1487
- const { storeDocxCarrier } = await engineModules2.docxGraft();
1488
- await storeDocxCarrier(ydoc, revisedFile.bytes, {
1489
- hash: comparison.doc.docxSource.sourceHash
1490
- });
1491
- }
1492
- const document = await ReviseDocument.restore(
1493
- encodeSnapshot2(
1494
- {
1495
- documentId: options.documentId ?? `revise-compare-${Date.now()}`,
1496
- mode: options.mode ?? "suggesting",
1497
- author,
1498
- title: stem(revisedFile.filename)
1499
- },
1500
- encodeYDoc(ydoc),
1501
- isDocx ? revisedFile.bytes : null
1502
- ),
1503
- options.images ? { images: options.images } : {}
1504
- );
1505
- const plainOf = async (y) => new TextDecoder().decode(await exportDocument(y, { format: "txt", mode: "final" })).replace(/\s+/g, " ").trim();
1506
- const redline = isDocx ? await fileToYDoc(
1507
- (await exportSessionDocx(document.ydoc, document.package)).bytes,
1508
- "redline.docx"
1509
- ) : decodeYDoc(encodeYDoc(document.ydoc));
1510
- const rejected = decodeYDoc(encodeYDoc(redline));
1511
- const rejecter = await createServerDocumentSession(rejected, {
1512
- documentId: `${document.documentId}-verify`
1513
- });
1514
- rejecter.rejectAllSuggestions();
1515
- rejecter.dispose();
1516
- const [acceptedText, rejectedText, revisedText, originalText] = await Promise.all([
1517
- plainOf(redline),
1518
- plainOf(rejected),
1519
- plainOf(await fileToYDoc(revisedFile.bytes, revisedFile.filename)),
1520
- plainOf(await fileToYDoc(originalFile.bytes, originalFile.filename))
1521
- ]);
1522
- return {
1523
- document,
1524
- stats: comparison.stats,
1525
- untrackedRegions: comparison.untrackedRegions,
1526
- verified: {
1527
- acceptAllGivesRevised: acceptedText === revisedText,
1528
- rejectAllGivesOriginal: rejectedText === originalText
1529
- }
1530
- };
1531
- }
1532
- async function convert(source, format, options = {}) {
1533
- format = exportFormat(format);
1534
- const view = viewOfMode(options);
1535
- if (view === "original") {
1536
- const doc = await ReviseDocument.open(source);
1537
- try {
1538
- return (await doc.export({ format, view })).bytes;
1539
- } finally {
1540
- doc.dispose();
1541
- }
1542
- }
1543
- if (view === "accepted") options = { mode: "final" };
1544
- const { bytes, filename } = await readSource(source);
1545
- assertReadable(filename);
1546
- const ydoc = await importYDoc(bytes, filename);
1547
- return format === "pdf" ? exportPdf(ydoc, stem(filename)) : exportDocument(ydoc, {
1548
- format,
1549
- mode: options.mode ?? "review",
1550
- title: stem(filename)
1551
- });
1552
- }
1553
- function viewOfMode(options) {
1554
- const { mode, view } = options;
1555
- if (view !== void 0 && !["tracked", "accepted", "original"].includes(view)) {
1556
- throw new ReviseError(
1557
- `Unknown view "${view}". Use tracked, accepted, or original.`,
1558
- "usage"
1559
- );
1560
- }
1561
- if (view && mode && mode === "final" !== (view !== "tracked")) {
1562
- throw new ReviseError(
1563
- `mode "${mode}" and view "${view}" disagree; give one of them.`,
1564
- "usage"
1565
- );
1566
- }
1567
- return view;
1568
- }
1569
- var EXPORT_FORMATS = ["docx", "pdf", "markdown", "html", "txt"];
1570
- function exportFormat(format) {
1571
- const name = String(format).toLowerCase();
1572
- if (name === "md") return "markdown";
1573
- if (!EXPORT_FORMATS.includes(name)) {
1574
- throw new ReviseError(
1575
- `Unknown export format "${format}". Use one of: ${EXPORT_FORMATS.join(", ")}.`,
1576
- "unsupported_format"
1577
- );
1578
- }
1579
- return name;
1580
- }
1581
- function textBlocks(doc) {
1582
- const out = [];
1583
- const segmentsOf = (node) => {
1584
- const segments = [];
1585
- for (const child of node.children ?? []) {
1586
- const c = child;
1587
- if (c.type !== "text" || !c.text) continue;
1588
- const change = c.marks?.deletion ? "deleted" : c.marks?.insertion ? "inserted" : null;
1589
- const changeId = change ? c.marks?.suggestionId ?? null : null;
1590
- const last = segments[segments.length - 1];
1591
- if (last && last.change === change && last.changeId === changeId) {
1592
- last.text += c.text;
1593
- } else {
1594
- segments.push({ text: c.text, change, changeId });
1595
- }
1596
- }
1597
- return segments;
1598
- };
1599
- doc.children.forEach((block, topIndex) => {
1600
- const visit = (node) => {
1601
- const n = node;
1602
- if (!n || typeof n !== "object") return;
1603
- if ((n.type === "paragraph" || n.type === "list-item") && n.id) {
1604
- out.push({ id: n.id, segments: segmentsOf(n), topIndex });
1605
- return;
1606
- }
1607
- n.children?.forEach(visit);
1608
- };
1609
- visit(block);
1610
- });
1611
- return out;
1612
- }
1613
- function replaceTargets(doc) {
1614
- const out = [];
1615
- const textOf = (node) => (node.children ?? []).map((child) => {
1616
- const c = child;
1617
- return c.type === "text" && !c.marks?.deletion ? c.text ?? "" : "";
1618
- }).join("");
1619
- const visit = (node, region) => {
1620
- const n = node;
1621
- if (!n || typeof n !== "object") return;
1622
- if ((n.type === "paragraph" || n.type === "list-item") && n.id) {
1623
- out.push({ id: n.id, text: textOf(n), region });
1624
- return;
1625
- }
1626
- n.children?.forEach((child) => visit(child, region));
1627
- };
1628
- doc.children.forEach((block) => visit(block, "body"));
1629
- for (const block of doc.chrome ?? []) {
1630
- block.children.forEach((column) => visit(column, block.side));
1631
- }
1632
- for (const note of doc.notes ?? []) {
1633
- const region = note.noteKind === "endnote" ? "endnote" : "footnote";
1634
- note.children.forEach((child) => visit(child, region));
1635
- }
1636
- return out;
1637
- }
1638
- var LOOSE_CHARACTERS = {
1639
- "\u2018": "'",
1640
- // ‘
1641
- "\u2019": "'",
1642
- // ’
1643
- "\u201A": "'",
1644
- // ‚
1645
- "\u201B": "'",
1646
- // ‛
1647
- "\u2032": "'",
1648
- // ′
1649
- "\u201C": '"',
1650
- // “
1651
- "\u201D": '"',
1652
- // ”
1653
- "\u201E": '"',
1654
- // „
1655
- "\u201F": '"',
1656
- // ‟
1657
- "\u2033": '"',
1658
- // ″
1659
- "\xA0": " ",
1660
- // no-break space
1661
- "\u202F": " ",
1662
- // narrow no-break space
1663
- "\u2007": " ",
1664
- // figure space
1665
- "\u2009": " ",
1666
- // thin space
1667
- "\u2011": "-",
1668
- // non-breaking hyphen
1669
- "\u2010": "-"
1670
- // hyphen
1671
- };
1672
- function fold(text, options) {
1673
- return Array.from(text, (char) => {
1674
- let c = options.exact ? char : LOOSE_CHARACTERS[char] ?? char;
1675
- if (options.ignoreCase) {
1676
- const lower = c.toLowerCase();
1677
- if (lower.length === c.length) c = lower;
1678
- }
1679
- return c;
1680
- });
1681
- }
1682
- function textMatcher(find, options) {
1683
- const needle = fold(find, options).join("");
1684
- return (text) => {
1685
- const chars = Array.from(text);
1686
- const hay = fold(text, options).join("");
1687
- const matches = [];
1688
- const starts = [];
1689
- let offset = 0;
1690
- for (const c of fold(text, options)) {
1691
- starts.push(offset);
1692
- offset += c.length;
1693
- }
1694
- let from = 0;
1695
- while (needle) {
1696
- const at = hay.indexOf(needle, from);
1697
- if (at < 0) break;
1698
- const first = starts.indexOf(at);
1699
- let last = first;
1700
- while (last < starts.length && starts[last] < at + needle.length) last++;
1701
- if (first >= 0) matches.push(chars.slice(first, last).join(""));
1702
- from = at + Math.max(needle.length, 1);
1703
- }
1704
- return matches;
1705
- };
1706
- }
1707
- function matchDocumentCharacters(find, matched, replace) {
1708
- const findChars = Array.from(find);
1709
- const matchedChars = Array.from(matched);
1710
- const out = Array.from(replace);
1711
- for (const plain of ["'", '"', " ", "-"]) {
1712
- const documentChars = findChars.map((c, i) => c === plain ? matchedChars[i] : void 0).filter((c) => c !== void 0);
1713
- if (documentChars.every((c) => c === plain)) continue;
1714
- const slots = out.flatMap((c, i) => c === plain ? [i] : []);
1715
- if (slots.length !== documentChars.length) continue;
1716
- slots.forEach((slot, n) => out[slot] = documentChars[n]);
1717
- }
1718
- return out.join("");
1719
- }
1720
- var shows = (segment, view) => view === "tracked" || segment.change === null || segment.change === "inserted" === (view === "accepted");
1721
- function segmentsText(segments, view) {
1722
- return segments.filter((segment) => shows(segment, view)).map(
1723
- (segment) => view !== "tracked" || segment.change === null ? segment.text : segment.change === "deleted" ? `[-${segment.text}-]` : `{+${segment.text}+}`
1724
- ).join("");
1725
- }
1726
- function matchesIn(segments, matcher, view) {
1727
- const found = [];
1728
- const scan = (side, keep) => {
1729
- const places = [];
1730
- const owners = [];
1731
- let text = "";
1732
- let at = 0;
1733
- for (const segment of segments) {
1734
- if (shows(segment, side)) {
1735
- for (let i = 0; i < segment.text.length; i++) {
1736
- places.push(at + i);
1737
- owners.push(segment);
1738
- }
1739
- text += segment.text;
1740
- }
1741
- at += segment.text.length;
1742
- }
1743
- for (const match of text.matchAll(matcher)) {
1744
- if (!match[0]) continue;
1745
- const start = match.index ?? 0;
1746
- const touched = owners.slice(start, start + match[0].length).find((owner) => owner.change !== null);
1747
- const change = touched?.change ?? null;
1748
- if (!keep(change)) continue;
1749
- found.push({
1750
- text: match[0],
1751
- change,
1752
- changeId: touched?.changeId ?? null,
1753
- at: places[start]
1754
- });
1755
- }
1756
- };
1757
- if (view === "tracked") {
1758
- scan("accepted", (change) => change !== "deleted");
1759
- scan("original", (change) => change === "deleted");
1760
- } else {
1761
- scan(view, () => true);
1762
- }
1763
- return found.sort(
1764
- (a, b) => a.at - b.at || Number(b.change === "deleted") - Number(a.change === "deleted")
1765
- ).map(({ text, change, changeId }) => ({ text, change, changeId }));
1766
- }
1767
- var OPEN = "\uE000";
1768
- var DELETED = "\uE001";
1769
- var INSERTED = "\uE002";
1770
- var CLOSE = "\uE003";
1771
- var CHANGE_BASE = 57600;
1772
- var SENTINEL_RUN = /([])([-])([\s\S]*?)/g;
1773
- function renderView(html, raw, view, authors, AgentHtmlConverter) {
1774
- const ids = topLevelIds(html);
1775
- const blocks = [...raw.children, ...raw.notes ?? []];
1776
- const position = new Map(blocks.map((block, i) => [block.id, i]));
1777
- const known = ids.map((id) => position.get(id)).filter((at) => at !== void 0);
1778
- if (!known.length) return html;
1779
- const deleted = (block) => block !== void 0 && wholeBlockDeletion(block) !== null;
1780
- const shown = /* @__PURE__ */ new Set();
1781
- for (const at of known) {
1782
- shown.add(at);
1783
- for (let next = at + 1; next < blocks.length && deleted(blocks[next]); next++) {
1784
- shown.add(next);
1785
- }
1786
- }
1787
- const first = Math.min(...known);
1788
- if (blocks.slice(0, first).every((block) => deleted(block))) {
1789
- for (let at = 0; at < first; at++) shown.add(at);
1790
- }
1791
- const order = [...shown].sort((a, b) => a - b);
1792
- const changes = [];
1793
- const indexOf = (id) => {
1794
- let i = changes.indexOf(id);
1795
- if (i < 0) i = changes.push(id) - 1;
1796
- return String.fromCharCode(CHANGE_BASE + Math.min(i, 63743 - CHANGE_BASE));
1797
- };
1798
- const marked = (node, deletedBy) => {
1799
- if (!node || typeof node !== "object") return node;
1800
- const n = node;
1801
- if (n.type === "text" && n.text) {
1802
- const deletion = n.marks?.deletion || deletedBy && !n.marks?.insertion;
1803
- if (!deletion && !n.marks?.insertion) return node;
1804
- const id = n.marks?.suggestionId ?? deletedBy ?? "";
1805
- const kind = deletion ? DELETED : INSERTED;
1806
- return { ...n, text: `${OPEN}${kind}${indexOf(id)}${n.text}${CLOSE}` };
1807
- }
1808
- const structural = node;
1809
- const by = structural.structuralSuggestionKind === "delete" ? structural.structuralSuggestionId : deletedBy;
1810
- return Array.isArray(n.children) ? { ...n, children: n.children.map((child) => marked(child, by)) } : node;
1811
- };
1812
- const children = raw.children.length;
1813
- const markedBlocks = blocks.map((block) => marked(block));
1814
- const doc = {
1815
- ...raw,
1816
- children: markedBlocks.slice(0, children),
1817
- notes: raw.notes ? markedBlocks.slice(children) : raw.notes
1818
- };
1819
- const found = AgentHtmlConverter.extractSpecificBlocks(
1820
- doc,
1821
- order.map((at) => blocks[at].id)
1822
- ).foundBlocks;
1823
- const lines = [];
1824
- found.forEach((block, i) => {
1825
- const gap = i > 0 ? block.blockIndex - found[i - 1].blockIndex - 1 : 0;
1826
- if (gap > 0)
1827
- lines.push(`(${gap} ${gap === 1 ? "block" : "blocks"} omitted...)`);
1828
- const whole = view === "tracked" ? wholeBlockDeletion(blocks[block.blockIndex]) : null;
1829
- lines.push(
1830
- whole && !block.html.includes(OPEN) ? block.html.replace(
1831
- /^<([a-z][\w-]*)/,
1832
- `<$1 data-change="deleted" data-change-id="${escapeAttr(whole)}" data-author="${escapeAttr(authors.get(whole) ?? "unknown")}"`
1833
- ) : block.html
1834
- );
1835
- });
1836
- const header = /^\s*Showing \d+ block\(s\):\s*/.exec(html) ? `Showing ${found.length} block(s):
1837
-
1838
- ` : "";
1839
- return (header + lines.join("\n")).replace(
1840
- SENTINEL_RUN,
1841
- (_, kind, index, text) => {
1842
- const deletion = kind === DELETED;
1843
- if (view === "original") return deletion ? text : "";
1844
- const id = changes[index.charCodeAt(0) - CHANGE_BASE] ?? "";
1845
- const tag = deletion ? "del" : "ins";
1846
- return `<${tag} data-change-id="${escapeAttr(id)}" data-author="${escapeAttr(authors.get(id) ?? "unknown")}">${text}</${tag}>`;
1847
- }
1848
- );
1849
- }
1850
- function wholeBlockDeletion(block) {
1851
- const b = block;
1852
- if (b.structuralSuggestionKind === "delete" && b.structuralSuggestionId) {
1853
- return b.structuralSuggestionId;
1854
- }
1855
- let id = null;
1856
- let all = true;
1857
- let any = false;
1858
- const visit = (node) => {
1859
- if (!all || !node || typeof node !== "object") return;
1860
- const n = node;
1861
- if (n.type === "text") {
1862
- if (!n.text) return;
1863
- any = true;
1864
- if (!n.marks?.deletion) all = false;
1865
- else id ??= n.marks.suggestionId ?? null;
1866
- return;
1867
- }
1868
- n.children?.forEach(visit);
1869
- };
1870
- visit(block);
1871
- return any && all ? id : null;
1872
- }
1873
- function topLevelIds(html) {
1874
- const body = new DOMParser().parseFromString(
1875
- `<!doctype html><body>${html}</body>`,
1876
- "text/html"
1877
- ).body;
1878
- return Array.from(body.children).map((el) => el.getAttribute("id")).filter((id) => Boolean(id));
1879
- }
1880
- var escapeAttr = (value) => value.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;");
1881
- var placesByEntries = /* @__PURE__ */ new WeakMap();
1882
- async function describeChanges(ydoc, session) {
1883
- const records = session.listSuggestions();
1884
- const doc = ydocToDocument2(ydoc);
1885
- const { documentHeadingSections } = await engineModules2.outline();
1886
- const span = /* @__PURE__ */ new Map();
1887
- let next = 0;
1888
- const number = (node) => {
1889
- if (!node || typeof node !== "object") return;
1890
- const n = node;
1891
- const isBlock = typeof n.id === "string" && n.type !== "text";
1892
- const start = next;
1893
- if (isBlock) next += 1;
1894
- n.children?.forEach(number);
1895
- if (isBlock) span.set(n.id, { start, end: next - 1 });
1896
- };
1897
- doc.children.forEach(number);
1898
- const bodyEnd = next;
1899
- (doc.chrome ?? []).forEach(number);
1900
- (doc.notes ?? []).forEach(number);
1901
- const headings = documentHeadingSections(doc, { maxDepth: 3 }).flatMap((h) => {
1902
- const start = span.get(h.itemId ?? h.blockId)?.start;
1903
- return start === void 0 ? [] : [{ title: h.title, level: h.level, start }];
1904
- }).sort((a, b) => a.start - b.start);
1905
- const sections = headings.map((h, i) => ({
1906
- ...h,
1907
- end: (headings.slice(i + 1).find((later) => later.level <= h.level)?.start ?? bodyEnd) - 1
1908
- }));
1909
- const sectionAt = (at) => {
1910
- let title;
1911
- for (const section of sections) {
1912
- if (section.start > at) break;
1913
- title = section.title;
1914
- }
1915
- return title;
1916
- };
1917
- const positionOf = new Map(
1918
- doc.children.map((block, index) => [block.id, index])
1919
- );
1920
- const found = /* @__PURE__ */ new Map();
1921
- const collect = (block, blockIndex, label) => {
1922
- const visit = (node, path) => {
1923
- if (!node || typeof node !== "object") return;
1924
- const n = node;
1925
- const here = typeof n.id === "string" && n.type !== "text" ? [...path, n.id] : path;
1926
- const id = n.marks?.suggestionId ?? n.suggestionId ?? n.structuralSuggestionId;
1927
- if (id) {
1928
- const entry = found.get(id) ?? {
1929
- blockIndex,
1930
- blockId: block.id ?? "",
1931
- label,
1932
- path: here,
1933
- deleted: "",
1934
- inserted: ""
1935
- };
1936
- if (n.type === "text") {
1937
- if (n.marks?.deletion) entry.deleted += n.text ?? "";
1938
- else if (n.marks?.insertion) entry.inserted += n.text ?? "";
1939
- }
1940
- found.set(id, entry);
1941
- }
1942
- n.children?.forEach((child) => visit(child, here));
1943
- };
1944
- visit(block, []);
1945
- };
1946
- doc.children.forEach(
1947
- (block, blockIndex) => collect(block, blockIndex, void 0)
1948
- );
1949
- for (const block of doc.chrome ?? []) {
1950
- collect(block, void 0, block.side === "header" ? "Header" : "Footer");
1951
- }
1952
- const noteCount = { footnote: 0, endnote: 0 };
1953
- for (const note of doc.notes ?? []) {
1954
- const kind = note.noteKind === "endnote" ? "endnote" : "footnote";
1955
- noteCount[kind] += 1;
1956
- collect(
1957
- note,
1958
- void 0,
1959
- `${kind === "endnote" ? "Endnote" : "Footnote"} ${noteCount[kind]}`
1960
- );
1961
- }
1962
- const changes = /* @__PURE__ */ new WeakMap();
1963
- const entries = records.map((record) => {
1964
- const at = found.get(record.id);
1965
- const blockIndex = at?.blockIndex ?? (record.blockId ? positionOf.get(record.blockId) : void 0) ?? (record.tableId ? positionOf.get(record.tableId) : void 0);
1966
- const path = at?.path ?? [record.tableId, record.blockId].filter(
1967
- (id) => Boolean(id && span.has(id))
1968
- );
1969
- const place = path.length ? span.get(path[path.length - 1]) : void 0;
1970
- const entry = {
1971
- id: record.id,
1972
- type: record.type ?? "text",
1973
- author: record.agentName ?? record.authorType ?? "unknown",
1974
- ...at?.blockId || record.blockId ? { blockId: at?.blockId || record.blockId } : {},
1975
- ...blockIndex !== void 0 ? { blockIndex } : {},
1976
- ...at?.label ? { section: at.label } : place && place.start < bodyEnd ? { section: sectionAt(place.start) } : {},
1977
- deleted: at?.deleted ?? "",
1978
- inserted: at?.inserted ?? ""
1979
- };
1980
- if (place) changes.set(entry, { at: place.start, path });
1981
- return entry;
1982
- });
1983
- placesByEntries.set(entries, { span, sections, changes });
1984
- return entries;
1985
- }
1986
- function headingMatches(title, query) {
1987
- const heading = title.trim().toLowerCase();
1988
- const q = query.trim().toLowerCase().replace(/\.$/, "");
1989
- if (/^[\d.]+$/.test(q)) {
1990
- return new RegExp(`^${q.replace(/\./g, "\\.")}(?:[.\\s)]|$)`).test(heading);
1991
- }
1992
- return heading.includes(q);
1993
- }
1994
- function parseBlockQuery(query) {
1995
- return query.split(",").map((part) => part.trim()).filter(Boolean).map((part) => {
1996
- const [from, to, ...rest] = part.split(/\.\.\.?|…/).map((id) => id.trim());
1997
- if (to === void 0) return from;
1998
- if (!from || !to || rest.length) {
1999
- throw new ReviseError(
2000
- `"${part}" is not a block range; write first..last.`,
2001
- "invalid_selection"
2002
- );
2003
- }
2004
- return { from, to };
2005
- });
2006
- }
2007
- function applyChangeFilter(entries, filter) {
2008
- const { section, exceptSection, blocks } = filter;
2009
- if (!section && !exceptSection && !blocks?.length) return entries;
2010
- const places = placesByEntries.get(entries);
2011
- if (!places) return entries;
2012
- const sectionRanges = (query) => {
2013
- const q = query.trim().toLowerCase();
2014
- const exact = places.sections.filter(
2015
- (s) => s.title.toLowerCase() === q || s.title.toLowerCase().replace(/^[\d.]+[.)]?\s+/, "") === q
2016
- );
2017
- const candidates = exact.length ? exact : places.sections.filter((s) => headingMatches(s.title, query));
2018
- const matched = candidates.filter(
2019
- (s) => !candidates.some(
2020
- (outer) => outer !== s && outer.start < s.start && s.end <= outer.end
2021
- )
2022
- );
2023
- if (matched.length > 1) {
2024
- throw new ReviseError(
2025
- `"${query}" matches ${matched.length} headings: ${matched.map((s) => `"${s.title}"`).join(", ")}. Name one by its number or full title.`,
2026
- "section_ambiguous"
2027
- );
2028
- }
2029
- if (!matched.length) {
2030
- throw new ReviseError(
2031
- places.sections.length ? `No heading matches "${query}". Headings: ${places.sections.slice(0, 12).map((s) => `"${s.title}"`).join(", ")}${places.sections.length > 12 ? ", \u2026" : ""}.` : `No heading matches "${query}": the document has no headings. Select by blocks or change ids instead.`,
2032
- "section_not_found"
2033
- );
2034
- }
2035
- return matched;
2036
- };
2037
- const spanOf = (id) => {
2038
- const found = places.span.get(id);
2039
- if (!found) {
2040
- throw new ReviseError(`No block with id "${id}".`, "block_not_found");
2041
- }
2042
- return found;
2043
- };
2044
- const inside = sectionRanges;
2045
- const keepSections = section ? inside(section) : void 0;
2046
- const dropSections = exceptSection ? inside(exceptSection) : void 0;
2047
- const ids = /* @__PURE__ */ new Set();
2048
- const ranges = [];
2049
- for (const item of blocks ?? []) {
2050
- if (typeof item === "string") {
2051
- spanOf(item);
2052
- ids.add(item);
2053
- continue;
2054
- }
2055
- const from = spanOf(item.from);
2056
- const to = spanOf(item.to);
2057
- if (from.start > to.start) {
2058
- throw new ReviseError(
2059
- `Block "${item.from}" comes after "${item.to}"; write the range first..last.`,
2060
- "invalid_selection"
2061
- );
2062
- }
2063
- ranges.push({ start: from.start, end: to.end });
2064
- }
2065
- const within = (at, r) => at >= r.start && at <= r.end;
2066
- return entries.filter((entry) => {
2067
- const place = places.changes.get(entry);
2068
- if (!place) return false;
2069
- if (keepSections && !keepSections.some((r) => within(place.at, r))) {
2070
- return false;
2071
- }
2072
- if (dropSections?.some((r) => within(place.at, r))) return false;
2073
- if (blocks?.length && !place.path.some((id) => ids.has(id)) && !ranges.some((r) => within(place.at, r))) {
2074
- return false;
2075
- }
2076
- return true;
2077
- });
2078
- }
2079
- async function markdownOf(ydoc) {
2080
- const bytes = await exportDocument(ydoc, {
2081
- format: "markdown",
2082
- mode: "final"
2083
- });
2084
- return new TextDecoder().decode(bytes);
2085
- }
2086
- function lcsPairs(a, b) {
2087
- let head = 0;
2088
- while (head < a.length && head < b.length && a[head] === b[head]) head += 1;
2089
- let tail = 0;
2090
- while (tail < a.length - head && tail < b.length - head && a[a.length - 1 - tail] === b[b.length - 1 - tail]) {
2091
- tail += 1;
2092
- }
2093
- const pairs = [];
2094
- for (let k = 0; k < head; k += 1) pairs.push([k, k]);
2095
- const midA = a.slice(head, a.length - tail);
2096
- const midB = b.slice(head, b.length - tail);
2097
- if (midA.length * midB.length <= 4e6) {
2098
- for (const [i, j] of lcsTable(midA, midB)) pairs.push([i + head, j + head]);
2099
- }
2100
- for (let k = tail; k > 0; k -= 1) {
2101
- pairs.push([a.length - k, b.length - k]);
2102
- }
2103
- return pairs;
2104
- }
2105
- function lcsTable(a, b) {
2106
- const n = a.length;
2107
- const m = b.length;
2108
- const table = new Uint32Array((n + 1) * (m + 1));
2109
- for (let i2 = n - 1; i2 >= 0; i2 -= 1) {
2110
- for (let j2 = m - 1; j2 >= 0; j2 -= 1) {
2111
- table[i2 * (m + 1) + j2] = a[i2] === b[j2] ? table[(i2 + 1) * (m + 1) + j2 + 1] + 1 : Math.max(table[(i2 + 1) * (m + 1) + j2], table[i2 * (m + 1) + j2 + 1]);
2112
- }
2113
- }
2114
- const pairs = [];
2115
- let i = 0;
2116
- let j = 0;
2117
- while (i < n && j < m) {
2118
- if (a[i] === b[j]) {
2119
- pairs.push([i, j]);
2120
- i += 1;
2121
- j += 1;
2122
- } else if (table[(i + 1) * (m + 1) + j] >= table[i * (m + 1) + j + 1]) {
2123
- i += 1;
2124
- } else {
2125
- j += 1;
2126
- }
2127
- }
2128
- return pairs;
2129
- }
2130
- function wordDiff(before, after) {
2131
- const a = before.split(/(\s+)/);
2132
- const b = after.split(/(\s+)/);
2133
- if (a.length * b.length > 4e6) return `[-${before}-]{+${after}+}`;
2134
- const pairs = lcsPairs(a, b);
2135
- let i = 0;
2136
- let j = 0;
2137
- let output = "";
2138
- const flush = (ai, bj) => {
2139
- const removed = a.slice(i, ai).join("");
2140
- const added = b.slice(j, bj).join("");
2141
- if (removed) output += `[-${removed}-]`;
2142
- if (added) output += `{+${added}+}`;
2143
- };
2144
- for (const [ai, bj] of pairs) {
2145
- flush(ai, bj);
2146
- output += a[ai];
2147
- i = ai + 1;
2148
- j = bj + 1;
2149
- }
2150
- flush(a.length, b.length);
2151
- return output;
2152
- }
2153
- function textDiff(before, after) {
2154
- const split = (s) => s.split(/\n+/).filter((line) => line.trim());
2155
- const a = split(before);
2156
- const b = split(after);
2157
- const pairs = lcsPairs(a, b);
2158
- const hunks = [];
2159
- let changed = 0;
2160
- let i = 0;
2161
- let j = 0;
2162
- const flush = (ai, bj) => {
2163
- const removed = a.slice(i, ai);
2164
- const added = b.slice(j, bj);
2165
- const paired = Math.min(removed.length, added.length);
2166
- for (let k = 0; k < paired; k += 1) {
2167
- hunks.push(`~ ${wordDiff(removed[k], added[k])}`);
2168
- changed += 1;
2169
- }
2170
- for (const line of removed.slice(paired)) {
2171
- hunks.push(`- ${line}`);
2172
- changed += 1;
2173
- }
2174
- for (const line of added.slice(paired)) {
2175
- hunks.push(`+ ${line}`);
2176
- changed += 1;
2177
- }
2178
- };
2179
- for (const [ai, bj] of pairs) {
2180
- flush(ai, bj);
2181
- i = ai + 1;
2182
- j = bj + 1;
2183
- }
2184
- flush(a.length, b.length);
2185
- return { text: hunks.join("\n"), changed };
2186
- }
2187
- async function documentSummary(ydoc, session) {
2188
- const doc = ydocToDocument2(ydoc);
2189
- const { resolvePageLayoutSettings, getPageDimensionsPx } = await engineModules2.pageLayout();
2190
- const layout = resolvePageLayoutSettings(doc.pageLayout);
2191
- const { pageWidth, pageHeight } = getPageDimensionsPx(layout);
2192
- const inches = (px) => Number((px / 96).toFixed(2));
2193
- const margins = [
2194
- layout.marginTopPx,
2195
- layout.marginRightPx,
2196
- layout.marginBottomPx,
2197
- layout.marginLeftPx
2198
- ].map(inches);
2199
- const measured = await session.tools.call("measure_document", {});
2200
- const textWithFields = (node) => {
2201
- const n = node;
2202
- if (n?.type === "text") {
2203
- const field = n.marks?.field?.toLowerCase();
2204
- if (field) {
2205
- return field === "numpages" || field === "pages" ? "{PAGES}" : `{${field.toUpperCase()}}`;
2206
- }
2207
- return n.text ?? "";
2208
- }
2209
- return (n?.children ?? []).map(textWithFields).join(n?.type === "table-row" ? " | " : "");
2210
- };
2211
- const chrome = (doc.chrome ?? []).filter((block) => block.type === "header-footer").map((block) => {
2212
- const b = block;
2213
- return {
2214
- side: b.side,
2215
- slot: b.slot ?? "default",
2216
- text: textWithFields(block).replace(/\s+/g, " ").trim()
2217
- };
2218
- }).filter((c) => c.text);
2219
- let title;
2220
- const headings = {};
2221
- const lists = [];
2222
- const tables = [];
2223
- let images = 0;
2224
- for (const block of doc.children) {
2225
- if (block.type === "paragraph") {
2226
- if (block.styleId === "Title" && title === void 0) {
2227
- title = textWithFields(block);
2228
- }
2229
- if (block.heading) {
2230
- headings[`H${block.heading}`] = (headings[`H${block.heading}`] ?? 0) + 1;
2231
- }
2232
- } else if (block.type === "list") {
2233
- lists.push(block.children.length);
2234
- } else if (block.type === "table") {
2235
- tables.push(
2236
- `${block.children.length}\xD7${block.children[0]?.children.length ?? 0}`
2237
- );
2238
- } else if (block.type === "image") {
2239
- images += 1;
2240
- }
2241
- }
2242
- const pending = session.listSuggestions().length;
2243
- const data = {
2244
- pages: measured.data.pageCount,
2245
- words: measured.data.wordCount,
2246
- pageSize: layout.pageSize,
2247
- orientation: layout.orientation,
2248
- pageInches: [inches(pageWidth), inches(pageHeight)],
2249
- marginsInches: margins,
2250
- title,
2251
- headings,
2252
- lists,
2253
- tables,
2254
- images,
2255
- headersFooters: chrome,
2256
- pendingTrackedChanges: pending
2257
- };
2258
- const marginText = margins.every((m) => m === margins[0]) ? `${margins[0]} in` : `${margins.join("/")} in (top/right/bottom/left)`;
2259
- const parts = [
2260
- title ? `title "${title}"` : "",
2261
- Object.keys(headings).length ? `headings ${Object.entries(headings).map(([level, n]) => `${n}\xD7${level}`).join(", ")}` : "",
2262
- lists.length ? `${lists.length} list${lists.length > 1 ? "s" : ""} (${lists.join(", ")} items)` : "",
2263
- tables.length ? `${tables.length} table${tables.length > 1 ? "s" : ""} (${tables.join(", ")})` : "",
2264
- images ? `${images} image${images > 1 ? "s" : ""}` : ""
2265
- ].filter(Boolean);
2266
- const text = [
2267
- `${data.pages ?? "?"} page${data.pages === 1 ? "" : "s"}, ${measured.data.wordCount} words; ${layout.pageSize} ${layout.orientation} (${inches(pageWidth)} \xD7 ${inches(pageHeight)} in), margins ${marginText}.`,
2268
- ...parts.length ? [`Contents: ${parts.join("; ")}.`] : [],
2269
- ...chrome.map(
2270
- (c) => `${c.side[0].toUpperCase()}${c.side.slice(1)}${c.slot === "default" ? "" : ` (${c.slot})`}: ${c.text}`
2271
- ),
2272
- ...pending ? [`Pending tracked changes: ${pending}.`] : []
2273
- ].join("\n");
2274
- return { text, data };
2275
- }
2276
-
2277
- // src/embed-sdk/lib/agent.ts
2278
- function formatToolResult(result, maxChars = 1e5) {
2279
- if ("error" in result) {
2280
- return {
2281
- text: `error ${result.error.code}: ${result.error.message}`,
2282
- isError: true
2283
- };
2284
- }
2285
- const value = result.value;
2286
- const lines = [];
2287
- if (value.message) lines.push(value.message);
2288
- if (value.suggestionIds?.length) {
2289
- lines.push(`Tracked changes: ${value.suggestionIds.join(", ")}`);
2290
- }
2291
- if (value.data !== null && value.data !== void 0) {
2292
- const data = JSON.stringify(value.data);
2293
- if (data !== "{}" && data !== "[]") {
2294
- lines.push(
2295
- `data: ${data.length <= 4e3 ? data : compactData(value.data)}`
2296
- );
2297
- }
2298
- }
2299
- if (value.context?.html) lines.push("", value.context.html);
2300
- let text = lines.join("\n") || "Done.";
2301
- if (text.length > maxChars) {
2302
- text = `${text.slice(0, maxChars)}
2303
- \u2026 (result cut at ${maxChars} characters; read fewer blocks at a time, e.g. a later index or specific ids)`;
2304
- }
2305
- return { text, isError: false };
2306
- }
2307
- function compactData(data) {
2308
- const summarize = (value, depth) => {
2309
- if (Array.isArray(value)) {
2310
- return value.length <= 5 && depth < 2 ? value.map((item) => summarize(item, depth + 1)) : `[${value.length} items]`;
2311
- }
2312
- if (value && typeof value === "object") {
2313
- if (depth >= 2) return "{\u2026}";
2314
- return Object.fromEntries(
2315
- Object.entries(value).map(([key, inner]) => [
2316
- key,
2317
- summarize(inner, depth + 1)
2318
- ])
2319
- );
2320
- }
2321
- return typeof value === "string" && value.length > 200 ? `${value.slice(0, 200)}\u2026` : value;
2322
- };
2323
- return `${JSON.stringify(summarize(data, 0))} (summarized; the full data is larger)`;
2324
- }
2325
- function agentTools(doc, options) {
2326
- const definitions = doc.tools.definitions().filter((d) => !options.include || options.include.includes(d.name)).filter((d) => !options.exclude?.includes(d.name));
2327
- const allowed = new Set(definitions.map((d) => d.name));
2328
- const tools = definitions.map((d) => {
2329
- switch (options.provider) {
2330
- case "anthropic":
2331
- return {
2332
- name: d.name,
2333
- description: d.description,
2334
- input_schema: { ...d.inputSchema, type: "object" }
2335
- };
2336
- case "openai":
2337
- return {
2338
- type: "function",
2339
- function: {
2340
- name: d.name,
2341
- description: d.description,
2342
- parameters: d.inputSchema
2343
- }
2344
- };
2345
- default:
2346
- return {
2347
- type: "function",
2348
- name: d.name,
2349
- description: d.description,
2350
- parameters: d.inputSchema,
2351
- strict: false
2352
- };
2353
- }
2354
- });
2355
- const run = async (name, rawInput) => {
2356
- if (!allowed.has(name)) {
2357
- return {
2358
- text: `error unknown_tool: There is no tool named "${name}".`,
2359
- isError: true
2360
- };
2361
- }
2362
- let input = rawInput;
2363
- if (typeof rawInput === "string") {
2364
- try {
2365
- input = rawInput.trim() ? JSON.parse(rawInput) : {};
2366
- } catch (error) {
2367
- return {
2368
- text: `error invalid_json: The arguments are not valid JSON (${error.message}).`,
2369
- isError: true
2370
- };
2371
- }
2372
- }
2373
- if (!input || typeof input !== "object" || Array.isArray(input)) {
2374
- return {
2375
- text: "error invalid_arguments: Tool arguments must be a JSON object.",
2376
- isError: true
2377
- };
2378
- }
2379
- return formatToolResult(
2380
- await doc.tools.run(name, input),
2381
- options.maxResultChars
2382
- );
2383
- };
2384
- const handle = async (call) => {
2385
- switch (options.provider) {
2386
- case "anthropic": {
2387
- const c = call;
2388
- const { text, isError } = await run(c.name, c.input);
2389
- return {
2390
- type: "tool_result",
2391
- tool_use_id: c.id,
2392
- content: text,
2393
- ...isError ? { is_error: true } : {}
2394
- };
2395
- }
2396
- case "openai": {
2397
- const c = call;
2398
- const { text } = await run(c.function.name, c.function.arguments);
2399
- return {
2400
- role: "tool",
2401
- tool_call_id: c.id,
2402
- content: text
2403
- };
2404
- }
2405
- default: {
2406
- const c = call;
2407
- const { text } = await run(c.name, c.arguments);
2408
- return {
2409
- type: "function_call_output",
2410
- call_id: c.call_id,
2411
- output: text
2412
- };
2413
- }
2414
- }
2415
- };
2416
- return {
2417
- tools,
2418
- handle,
2419
- version: () => doc.version(),
2420
- reset: () => doc.resetToolContext()
2421
- };
2422
- }
2423
-
2424
- export {
2425
- relearn,
2426
- READABLE,
2427
- formatForPath,
2428
- exportYDoc,
2429
- ReviseDocument,
2430
- seedYDoc,
2431
- compare,
2432
- convert,
2433
- parseBlockQuery,
2434
- formatToolResult,
2435
- agentTools
2436
- };