revise 3.1.7 → 4.0.1
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.
- package/CHANGELOG.md +1677 -184
- package/FONT-LICENSE-HAN-EXT-B.txt +91 -0
- package/FONT-LICENSE-KATEX.txt +95 -0
- package/FONT-LICENSE-LIBERATION.txt +102 -0
- package/FONT-LICENSE-NOTO-MULTILINGUAL.txt +93 -0
- package/FONT-LICENSE-NOTO-SANS-SC.txt +93 -0
- package/FONT-LICENSE-NOTO.txt +94 -0
- package/FONT-LICENSE-SOURCE-CODE-PRO.txt +94 -0
- package/FONT-LICENSE-SOURCE-SERIF-4.txt +93 -0
- package/LICENSE +41 -21
- package/README.md +29 -121
- package/THIRD-PARTY-NOTICES.md +2751 -0
- package/assets/historyMaterializeWorker-ZFUHJDWr.js +619 -0
- package/chunks/AgentPageRenderer-CHHktDQ1.js +1 -0
- package/chunks/AgentPageRenderer-CN-N65JC.js +1 -0
- package/chunks/AttachmentDocumentImporter-DnHC9t7I.js +1 -0
- package/chunks/AttachmentDocumentImporter-Dqg7IPtK.js +1 -0
- package/chunks/ColorParserFull-BKH72fK8.js +446 -0
- package/chunks/DocumentScrubber-Ch_bOtHW.js +1 -0
- package/chunks/DocumentScrubber-DRzACkWJ.js +1 -0
- package/chunks/DocxConverter-BGPkGmyg.js +13 -0
- package/chunks/DocxConverter-DeurS5EF.js +13 -0
- package/chunks/DocxGraft-D71OgoMh.js +1 -0
- package/chunks/DocxGraft-QLkGEcwQ.js +1 -0
- package/chunks/FileImporter-BIT0ZBDY.js +1 -0
- package/chunks/FileImporter-Cgwoawuk.js +1 -0
- package/chunks/OdtConverter-2pm0SH5c.js +6 -0
- package/chunks/OdtConverter-CSUQTfVp.js +6 -0
- package/chunks/OfficeImportShared-C_mYnG5V.js +1 -0
- package/chunks/OfficeImportShared-O-d47J7O.js +1 -0
- package/chunks/PdfConverter-BYg_pr5J.js +21 -0
- package/chunks/PdfConverter-Z2HlY84F.js +21 -0
- package/chunks/PdfImportService-C2gwK1Sq.js +2 -0
- package/chunks/PdfImportService-CxtUtxM6.js +2 -0
- package/chunks/RtfConverter-BrtojpDV.js +15 -0
- package/chunks/RtfConverter-DlztsAoK.js +15 -0
- package/chunks/XmlConverter-3XWISlqr.js +2 -0
- package/chunks/XmlConverter-B1Kcohr0.js +2 -0
- package/chunks/__vite-browser-external-l0sNRNKZ.js +0 -0
- package/chunks/_revise-sdk-lazy-harfbuzz-X8gnYTZL.js +1 -0
- package/chunks/ar-vDYSGTpE.js +13 -0
- package/chunks/browser-module-BVdVmrPb.js +3 -0
- package/chunks/browser-module-CWlRHeO2.js +3 -0
- package/chunks/bundle.min-DRpfgV69.js +782 -0
- package/chunks/de-CWX4cvtt.js +13 -0
- package/chunks/es-DoAhvF6_.js +13 -0
- package/chunks/fil-DdAeD8Br.js +13 -0
- package/chunks/fr-CazsUjNQ.js +13 -0
- package/chunks/html2canvas.esm-CntiHiYW.js +997 -0
- package/chunks/index-BWdRWxoH.js +1807 -0
- package/chunks/index-D9_PwgOX.js +402 -0
- package/chunks/index-DcIXw_KB.js +402 -0
- package/chunks/ja-alh_QHTB.js +13 -0
- package/chunks/jspdf.es.min-BuJNZ-oY.js +1882 -0
- package/chunks/katex-DWj8TvwO.js +1462 -0
- package/chunks/pdf-BLWDCiFF.js +1828 -0
- package/chunks/persistInlineDocumentImages-DQj2iaqg.js +1 -0
- package/chunks/pseudoLocale-Bld5ZUFr.js +1 -0
- package/chunks/pseudoLocale-CLm3_c4X.js +1 -0
- package/chunks/tree-sitter-D6zWWUMP.js +4 -0
- package/chunks/wasmInline.generated-CBYFkdSM.js +1 -0
- package/chunks/web-tree-sitter-BXGZGnNj.js +2 -0
- package/chunks/zh-20eXwzFZ.js +13 -0
- package/cli-shim.js +34 -0
- package/full.js +1 -0
- package/index.d.ts +995 -13
- package/index.js +1 -31
- package/legacy/server.d.ts +19 -19
- package/legacy/server.js +39 -23
- package/package.json +55 -45
- package/serverToolContract.d.ts +420 -0
- package/serverToolInputs.generated.d.ts +269 -0
- package/style.css +1 -0
- package/version.d.ts +4 -0
- package/version.js +2 -0
- package/agent.d.ts +0 -139
- package/chunks/chunk-SMTOJL2X.js +0 -2436
- package/chunks/chunk-U5X47PZ6.js +0 -135
- package/chunks/chunk-U6YKQBKL.js +0 -15
- package/chunks/package-5SHL5FOJ.js +0 -14
- package/chunks/render-QD5VWKSV.js +0 -109
- package/cli-output.schema.json +0 -2159
- package/cli.js +0 -4152
- package/document.d.ts +0 -439
- package/docx.d.ts +0 -73
- package/errors.d.ts +0 -14
- package/legacy/editor.d.ts +0 -19
- package/legacy/editor.js +0 -16
- package/legacy/style.css +0 -2
- package/package.d.ts +0 -23
- package/render.d.ts +0 -27
package/chunks/chunk-SMTOJL2X.js
DELETED
|
@@ -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-]+)="([^"]*"[^"]*)"/g,
|
|
1109
|
-
(whole, name, value) => {
|
|
1110
|
-
if (value.includes("'") || value.includes("'")) return whole;
|
|
1111
|
-
changed = true;
|
|
1112
|
-
return ` ${name}='${value.replace(/"/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, "&").replace(/"/g, """).replace(/</g, "<");
|
|
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
|
-
};
|