revise 3.0.1 → 3.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +57 -0
- package/README.md +56 -71
- package/chunks/{chunk-ZLQVPMBN.js → chunk-SCNG3PKR.js} +582 -61
- package/cli-output.schema.json +114 -6
- package/cli.js +305 -85
- package/document.d.ts +98 -18
- package/index.d.ts +2 -2
- package/index.js +1 -1
- package/package.json +3 -3
|
@@ -897,12 +897,38 @@ var ReviseDocument = class _ReviseDocument {
|
|
|
897
897
|
pattern.source,
|
|
898
898
|
pattern.flags.includes("g") ? pattern.flags : `${pattern.flags}g`
|
|
899
899
|
);
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
900
|
+
const view = options.view ?? "tracked";
|
|
901
|
+
return textBlocks(ydocToDocument2(this.currentYDoc)).map((block) => {
|
|
902
|
+
const matches = matchesIn(block.segments, matcher, view);
|
|
903
|
+
return {
|
|
904
|
+
id: block.id,
|
|
905
|
+
blockIndex: block.topIndex,
|
|
906
|
+
count: matches.length,
|
|
907
|
+
text: segmentsText(block.segments, view),
|
|
908
|
+
matches
|
|
909
|
+
};
|
|
910
|
+
}).filter((hit) => hit.count > 0);
|
|
911
|
+
}
|
|
912
|
+
/**
|
|
913
|
+
* HTML the read tools returned (`read_blocks_from_index`,
|
|
914
|
+
* `read_specific_blocks`), in a view. The tools read the accepted view;
|
|
915
|
+
* "tracked" marks pending changes as `<ins>`/`<del>` with
|
|
916
|
+
* `data-change-id` and `data-author`, and shows whole blocks a change
|
|
917
|
+
* deletes where they were; "original" shows the text before every change.
|
|
918
|
+
* Block ids are unchanged. The editing tools read `<ins>` as its text and
|
|
919
|
+
* ignore `<del>`, so tracked HTML pasted back edits the accepted text.
|
|
920
|
+
*/
|
|
921
|
+
viewHtml(html, view) {
|
|
922
|
+
if (view === "accepted") return Promise.resolve(html);
|
|
923
|
+
return this.serial(async () => {
|
|
924
|
+
await installDomShims();
|
|
925
|
+
const { AgentHtmlConverter } = await engineModules2.agentHtml();
|
|
926
|
+
const raw = ydocToDocument2(this.currentYDoc);
|
|
927
|
+
const authors = new Map(
|
|
928
|
+
this.currentSession.listSuggestions().map((r) => [r.id, r.agentName ?? r.authorType ?? "unknown"])
|
|
929
|
+
);
|
|
930
|
+
return renderView(html, raw, view, authors, AgentHtmlConverter);
|
|
931
|
+
});
|
|
906
932
|
}
|
|
907
933
|
/** What the document holds, in one glance: page setup, header and footer
|
|
908
934
|
* text with live fields, title, headings, lists, tables, review state. */
|
|
@@ -912,23 +938,60 @@ var ReviseDocument = class _ReviseDocument {
|
|
|
912
938
|
);
|
|
913
939
|
}
|
|
914
940
|
// ── editing ──────────────────────────────────────────────────────────────
|
|
915
|
-
/**
|
|
916
|
-
*
|
|
917
|
-
*
|
|
918
|
-
|
|
941
|
+
/**
|
|
942
|
+
* Replace every occurrence everywhere: the body, tables, lists, page
|
|
943
|
+
* headers and footers, and footnotes and endnotes. Tracked changes in
|
|
944
|
+
* suggesting mode. Curly and straight quotes and non-breaking spaces match
|
|
945
|
+
* each other unless `exact`, and the replacement keeps the document's
|
|
946
|
+
* characters (see matchDocumentCharacters). Replacing text with itself
|
|
947
|
+
* changes nothing and is not a failure.
|
|
948
|
+
*/
|
|
949
|
+
replaceAll(find, replace, options = {}) {
|
|
919
950
|
return this.serial(async () => {
|
|
920
951
|
if (!find) {
|
|
921
952
|
throw new ReviseError("Nothing to find: give some text.", "usage");
|
|
922
953
|
}
|
|
923
|
-
const
|
|
924
|
-
|
|
954
|
+
const only = options.blocks ?? options.ids;
|
|
955
|
+
const all = replaceTargets(ydocToDocument2(this.currentYDoc)).filter(
|
|
956
|
+
(block) => !only || only.includes(block.id)
|
|
925
957
|
);
|
|
958
|
+
const exactMatch = textMatcher(find, {
|
|
959
|
+
exact: options.exact,
|
|
960
|
+
ignoreCase: options.ignoreCase
|
|
961
|
+
});
|
|
962
|
+
const loosest = textMatcher(find, { ignoreCase: true });
|
|
963
|
+
const caseOnly = textMatcher(find, {
|
|
964
|
+
exact: options.exact,
|
|
965
|
+
ignoreCase: true
|
|
966
|
+
});
|
|
967
|
+
const targets = [];
|
|
968
|
+
const missed = /* @__PURE__ */ new Map();
|
|
969
|
+
for (const block of all) {
|
|
970
|
+
const matches = exactMatch(block.text);
|
|
971
|
+
if (matches.length) targets.push({ ...block, matches });
|
|
972
|
+
const left = [...matches];
|
|
973
|
+
for (const text of loosest(block.text)) {
|
|
974
|
+
const at = left.indexOf(text);
|
|
975
|
+
if (at >= 0) {
|
|
976
|
+
left.splice(at, 1);
|
|
977
|
+
continue;
|
|
978
|
+
}
|
|
979
|
+
const reason = caseOnly(text).length ? "case" : "characters";
|
|
980
|
+
const entry = missed.get(text) ?? { count: 0, reason };
|
|
981
|
+
entry.count += 1;
|
|
982
|
+
missed.set(text, entry);
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
const nearMisses = [...missed].map(([text, m]) => ({ text, ...m }));
|
|
986
|
+
const regions = {};
|
|
926
987
|
if (find === replace) {
|
|
927
988
|
return {
|
|
928
989
|
replaced: 0,
|
|
929
990
|
blocks: targets.map((t) => t.id),
|
|
991
|
+
regions,
|
|
930
992
|
changeIds: [],
|
|
931
|
-
failures: []
|
|
993
|
+
failures: [],
|
|
994
|
+
nearMisses
|
|
932
995
|
};
|
|
933
996
|
}
|
|
934
997
|
const failures = [];
|
|
@@ -939,11 +1002,16 @@ var ReviseDocument = class _ReviseDocument {
|
|
|
939
1002
|
block_ids: [target.id],
|
|
940
1003
|
context_notes: "N/A"
|
|
941
1004
|
});
|
|
1005
|
+
const distinct = [...new Set(target.matches)];
|
|
942
1006
|
const result = await this.currentSession.tools.executeDynamic(
|
|
943
1007
|
"replace",
|
|
944
1008
|
{
|
|
945
1009
|
id: target.id,
|
|
946
|
-
replacements:
|
|
1010
|
+
replacements: distinct.map((text) => ({
|
|
1011
|
+
find: text,
|
|
1012
|
+
replace: matchDocumentCharacters(find, text, replace),
|
|
1013
|
+
occurrence: "all"
|
|
1014
|
+
})),
|
|
947
1015
|
context_notes: "N/A"
|
|
948
1016
|
}
|
|
949
1017
|
);
|
|
@@ -956,26 +1024,52 @@ var ReviseDocument = class _ReviseDocument {
|
|
|
956
1024
|
});
|
|
957
1025
|
}
|
|
958
1026
|
} else {
|
|
959
|
-
replaced += target.
|
|
1027
|
+
replaced += target.matches.length;
|
|
1028
|
+
regions[target.region] = (regions[target.region] ?? 0) + target.matches.length;
|
|
960
1029
|
changeIds.push(...result.value.suggestionIds ?? []);
|
|
961
1030
|
}
|
|
962
1031
|
}
|
|
963
1032
|
return {
|
|
964
1033
|
replaced,
|
|
965
1034
|
blocks: targets.map((t) => t.id),
|
|
1035
|
+
regions,
|
|
966
1036
|
changeIds,
|
|
967
|
-
failures
|
|
1037
|
+
failures,
|
|
1038
|
+
nearMisses
|
|
968
1039
|
};
|
|
969
1040
|
});
|
|
970
1041
|
}
|
|
1042
|
+
/** @deprecated The 3.0 name of `replaceAll`. */
|
|
1043
|
+
replaceText(find, replace, options = {}) {
|
|
1044
|
+
return this.replaceAll(find, replace, options);
|
|
1045
|
+
}
|
|
1046
|
+
/** The document as it was before every pending change, exported from
|
|
1047
|
+
* a copy: this document keeps its changes. */
|
|
1048
|
+
async exportOriginal(options) {
|
|
1049
|
+
const copy = await _ReviseDocument.restore(await this.snapshot(), {
|
|
1050
|
+
author: this.settings.author
|
|
1051
|
+
});
|
|
1052
|
+
try {
|
|
1053
|
+
await copy.reject({ all: true });
|
|
1054
|
+
const result = await copy.export({
|
|
1055
|
+
...options,
|
|
1056
|
+
view: void 0,
|
|
1057
|
+
mode: "final"
|
|
1058
|
+
});
|
|
1059
|
+
return { ...result, view: "original" };
|
|
1060
|
+
} finally {
|
|
1061
|
+
copy.dispose();
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
971
1064
|
// ── review ───────────────────────────────────────────────────────────────
|
|
972
1065
|
/** Pending tracked changes with where they are and what they do. */
|
|
973
1066
|
changes(filter = {}) {
|
|
974
1067
|
return this.serial(() => this.filteredChanges(filter));
|
|
975
1068
|
}
|
|
976
1069
|
async filteredChanges(filter = {}) {
|
|
977
|
-
return (
|
|
978
|
-
(
|
|
1070
|
+
return applyChangeFilter(
|
|
1071
|
+
await describeChanges(this.currentYDoc, this.currentSession),
|
|
1072
|
+
filter
|
|
979
1073
|
);
|
|
980
1074
|
}
|
|
981
1075
|
/** The tracked changes a selection would settle, without settling them. */
|
|
@@ -993,22 +1087,21 @@ var ReviseDocument = class _ReviseDocument {
|
|
|
993
1087
|
return this.serial(() => this.settle(selection, false));
|
|
994
1088
|
}
|
|
995
1089
|
async selectedIds(selection, known) {
|
|
996
|
-
const { section, exceptSection, all } = selection;
|
|
1090
|
+
const { section, exceptSection, blocks, all } = selection;
|
|
997
1091
|
const ids = selection.ids ?? [];
|
|
998
|
-
const
|
|
999
|
-
|
|
1000
|
-
Boolean(all),
|
|
1001
|
-
Boolean(section || exceptSection)
|
|
1002
|
-
];
|
|
1092
|
+
const filtered = Boolean(section || exceptSection || blocks?.length);
|
|
1093
|
+
const kinds = [ids.length > 0, Boolean(all), filtered];
|
|
1003
1094
|
if (kinds.filter(Boolean).length !== 1) {
|
|
1004
1095
|
throw new ReviseError(
|
|
1005
|
-
"Select tracked changes one way: ids, all, or
|
|
1096
|
+
"Select tracked changes one way: ids, all, or filters (section, exceptSection, blocks).",
|
|
1006
1097
|
"invalid_selection"
|
|
1007
1098
|
);
|
|
1008
1099
|
}
|
|
1009
|
-
if (
|
|
1010
|
-
|
|
1011
|
-
|
|
1100
|
+
if (filtered) {
|
|
1101
|
+
return applyChangeFilter(
|
|
1102
|
+
known ?? await describeChanges(this.currentYDoc, this.currentSession),
|
|
1103
|
+
{ section, exceptSection, blocks }
|
|
1104
|
+
).map((c) => c.id);
|
|
1012
1105
|
}
|
|
1013
1106
|
return all ? "all" : ids;
|
|
1014
1107
|
}
|
|
@@ -1048,9 +1141,15 @@ var ReviseDocument = class _ReviseDocument {
|
|
|
1048
1141
|
/** Write the document in a format. Reports what the output actually
|
|
1049
1142
|
* contains: its pending changes, and for .docx how much of the original
|
|
1050
1143
|
* file was carried over byte for byte. */
|
|
1051
|
-
export(options) {
|
|
1144
|
+
async export(options) {
|
|
1145
|
+
const view = viewOfMode(options);
|
|
1146
|
+
if (view === "original") return this.exportOriginal(options);
|
|
1052
1147
|
return this.serial(async () => {
|
|
1053
|
-
options = {
|
|
1148
|
+
options = {
|
|
1149
|
+
...options,
|
|
1150
|
+
format: exportFormat(options.format),
|
|
1151
|
+
mode: view === "accepted" ? "final" : options.mode
|
|
1152
|
+
};
|
|
1054
1153
|
const mode = options.format === "docx" ? options.mode ?? "review" : "final";
|
|
1055
1154
|
const pending = this.currentSession.listSuggestions().length;
|
|
1056
1155
|
const { bytes, outcome } = await exportYDoc(
|
|
@@ -1079,6 +1178,7 @@ var ReviseDocument = class _ReviseDocument {
|
|
|
1079
1178
|
bytes,
|
|
1080
1179
|
format: options.format,
|
|
1081
1180
|
mode,
|
|
1181
|
+
view: mode === "final" ? "accepted" : "tracked",
|
|
1082
1182
|
pendingTrackedChanges: mode === "final" ? 0 : pending,
|
|
1083
1183
|
fidelity: outcome,
|
|
1084
1184
|
warnings
|
|
@@ -1184,6 +1284,16 @@ async function compare(originalSource, revisedSource, options = {}) {
|
|
|
1184
1284
|
}
|
|
1185
1285
|
async function convert(source, format, options = {}) {
|
|
1186
1286
|
format = exportFormat(format);
|
|
1287
|
+
const view = viewOfMode(options);
|
|
1288
|
+
if (view === "original") {
|
|
1289
|
+
const doc = await ReviseDocument.open(source);
|
|
1290
|
+
try {
|
|
1291
|
+
return (await doc.export({ format, view })).bytes;
|
|
1292
|
+
} finally {
|
|
1293
|
+
doc.dispose();
|
|
1294
|
+
}
|
|
1295
|
+
}
|
|
1296
|
+
if (view === "accepted") options = { mode: "final" };
|
|
1187
1297
|
const { bytes, filename } = await readSource(source);
|
|
1188
1298
|
assertReadable(filename);
|
|
1189
1299
|
const ydoc = await importYDoc(bytes, filename);
|
|
@@ -1193,6 +1303,22 @@ async function convert(source, format, options = {}) {
|
|
|
1193
1303
|
title: stem(filename)
|
|
1194
1304
|
});
|
|
1195
1305
|
}
|
|
1306
|
+
function viewOfMode(options) {
|
|
1307
|
+
const { mode, view } = options;
|
|
1308
|
+
if (view !== void 0 && !["tracked", "accepted", "original"].includes(view)) {
|
|
1309
|
+
throw new ReviseError(
|
|
1310
|
+
`Unknown view "${view}". Use tracked, accepted, or original.`,
|
|
1311
|
+
"usage"
|
|
1312
|
+
);
|
|
1313
|
+
}
|
|
1314
|
+
if (view && mode && mode === "final" !== (view !== "tracked")) {
|
|
1315
|
+
throw new ReviseError(
|
|
1316
|
+
`mode "${mode}" and view "${view}" disagree; give one of them.`,
|
|
1317
|
+
"usage"
|
|
1318
|
+
);
|
|
1319
|
+
}
|
|
1320
|
+
return view;
|
|
1321
|
+
}
|
|
1196
1322
|
var EXPORT_FORMATS = ["docx", "pdf", "markdown", "html", "txt"];
|
|
1197
1323
|
function exportFormat(format) {
|
|
1198
1324
|
const name = String(format).toLowerCase();
|
|
@@ -1207,16 +1333,28 @@ function exportFormat(format) {
|
|
|
1207
1333
|
}
|
|
1208
1334
|
function textBlocks(doc) {
|
|
1209
1335
|
const out = [];
|
|
1210
|
-
const
|
|
1211
|
-
const
|
|
1212
|
-
|
|
1213
|
-
|
|
1336
|
+
const segmentsOf = (node) => {
|
|
1337
|
+
const segments = [];
|
|
1338
|
+
for (const child of node.children ?? []) {
|
|
1339
|
+
const c = child;
|
|
1340
|
+
if (c.type !== "text" || !c.text) continue;
|
|
1341
|
+
const change = c.marks?.deletion ? "deleted" : c.marks?.insertion ? "inserted" : null;
|
|
1342
|
+
const changeId = change ? c.marks?.suggestionId ?? null : null;
|
|
1343
|
+
const last = segments[segments.length - 1];
|
|
1344
|
+
if (last && last.change === change && last.changeId === changeId) {
|
|
1345
|
+
last.text += c.text;
|
|
1346
|
+
} else {
|
|
1347
|
+
segments.push({ text: c.text, change, changeId });
|
|
1348
|
+
}
|
|
1349
|
+
}
|
|
1350
|
+
return segments;
|
|
1351
|
+
};
|
|
1214
1352
|
doc.children.forEach((block, topIndex) => {
|
|
1215
1353
|
const visit = (node) => {
|
|
1216
1354
|
const n = node;
|
|
1217
1355
|
if (!n || typeof n !== "object") return;
|
|
1218
1356
|
if ((n.type === "paragraph" || n.type === "list-item") && n.id) {
|
|
1219
|
-
out.push({ id: n.id,
|
|
1357
|
+
out.push({ id: n.id, segments: segmentsOf(n), topIndex });
|
|
1220
1358
|
return;
|
|
1221
1359
|
}
|
|
1222
1360
|
n.children?.forEach(visit);
|
|
@@ -1225,36 +1363,308 @@ function textBlocks(doc) {
|
|
|
1225
1363
|
});
|
|
1226
1364
|
return out;
|
|
1227
1365
|
}
|
|
1366
|
+
function replaceTargets(doc) {
|
|
1367
|
+
const out = [];
|
|
1368
|
+
const textOf = (node) => (node.children ?? []).map((child) => {
|
|
1369
|
+
const c = child;
|
|
1370
|
+
return c.type === "text" && !c.marks?.deletion ? c.text ?? "" : "";
|
|
1371
|
+
}).join("");
|
|
1372
|
+
const visit = (node, region) => {
|
|
1373
|
+
const n = node;
|
|
1374
|
+
if (!n || typeof n !== "object") return;
|
|
1375
|
+
if ((n.type === "paragraph" || n.type === "list-item") && n.id) {
|
|
1376
|
+
out.push({ id: n.id, text: textOf(n), region });
|
|
1377
|
+
return;
|
|
1378
|
+
}
|
|
1379
|
+
n.children?.forEach((child) => visit(child, region));
|
|
1380
|
+
};
|
|
1381
|
+
doc.children.forEach((block) => visit(block, "body"));
|
|
1382
|
+
for (const block of doc.chrome ?? []) {
|
|
1383
|
+
block.children.forEach((column) => visit(column, block.side));
|
|
1384
|
+
}
|
|
1385
|
+
for (const note of doc.notes ?? []) {
|
|
1386
|
+
const region = note.noteKind === "endnote" ? "endnote" : "footnote";
|
|
1387
|
+
note.children.forEach((child) => visit(child, region));
|
|
1388
|
+
}
|
|
1389
|
+
return out;
|
|
1390
|
+
}
|
|
1391
|
+
var LOOSE_CHARACTERS = {
|
|
1392
|
+
"\u2018": "'",
|
|
1393
|
+
// ‘
|
|
1394
|
+
"\u2019": "'",
|
|
1395
|
+
// ’
|
|
1396
|
+
"\u201A": "'",
|
|
1397
|
+
// ‚
|
|
1398
|
+
"\u201B": "'",
|
|
1399
|
+
// ‛
|
|
1400
|
+
"\u2032": "'",
|
|
1401
|
+
// ′
|
|
1402
|
+
"\u201C": '"',
|
|
1403
|
+
// “
|
|
1404
|
+
"\u201D": '"',
|
|
1405
|
+
// ”
|
|
1406
|
+
"\u201E": '"',
|
|
1407
|
+
// „
|
|
1408
|
+
"\u201F": '"',
|
|
1409
|
+
// ‟
|
|
1410
|
+
"\u2033": '"',
|
|
1411
|
+
// ″
|
|
1412
|
+
"\xA0": " ",
|
|
1413
|
+
// no-break space
|
|
1414
|
+
"\u202F": " ",
|
|
1415
|
+
// narrow no-break space
|
|
1416
|
+
"\u2007": " ",
|
|
1417
|
+
// figure space
|
|
1418
|
+
"\u2009": " ",
|
|
1419
|
+
// thin space
|
|
1420
|
+
"\u2011": "-",
|
|
1421
|
+
// non-breaking hyphen
|
|
1422
|
+
"\u2010": "-"
|
|
1423
|
+
// hyphen
|
|
1424
|
+
};
|
|
1425
|
+
function fold(text, options) {
|
|
1426
|
+
return Array.from(text, (char) => {
|
|
1427
|
+
let c = options.exact ? char : LOOSE_CHARACTERS[char] ?? char;
|
|
1428
|
+
if (options.ignoreCase) {
|
|
1429
|
+
const lower = c.toLowerCase();
|
|
1430
|
+
if (lower.length === c.length) c = lower;
|
|
1431
|
+
}
|
|
1432
|
+
return c;
|
|
1433
|
+
});
|
|
1434
|
+
}
|
|
1435
|
+
function textMatcher(find, options) {
|
|
1436
|
+
const needle = fold(find, options).join("");
|
|
1437
|
+
return (text) => {
|
|
1438
|
+
const chars = Array.from(text);
|
|
1439
|
+
const hay = fold(text, options).join("");
|
|
1440
|
+
const matches = [];
|
|
1441
|
+
const starts = [];
|
|
1442
|
+
let offset = 0;
|
|
1443
|
+
for (const c of fold(text, options)) {
|
|
1444
|
+
starts.push(offset);
|
|
1445
|
+
offset += c.length;
|
|
1446
|
+
}
|
|
1447
|
+
let from = 0;
|
|
1448
|
+
while (needle) {
|
|
1449
|
+
const at = hay.indexOf(needle, from);
|
|
1450
|
+
if (at < 0) break;
|
|
1451
|
+
const first = starts.indexOf(at);
|
|
1452
|
+
let last = first;
|
|
1453
|
+
while (last < starts.length && starts[last] < at + needle.length) last++;
|
|
1454
|
+
if (first >= 0) matches.push(chars.slice(first, last).join(""));
|
|
1455
|
+
from = at + Math.max(needle.length, 1);
|
|
1456
|
+
}
|
|
1457
|
+
return matches;
|
|
1458
|
+
};
|
|
1459
|
+
}
|
|
1460
|
+
function matchDocumentCharacters(find, matched, replace) {
|
|
1461
|
+
const findChars = Array.from(find);
|
|
1462
|
+
const matchedChars = Array.from(matched);
|
|
1463
|
+
const out = Array.from(replace);
|
|
1464
|
+
for (const plain of ["'", '"', " ", "-"]) {
|
|
1465
|
+
const documentChars = findChars.map((c, i) => c === plain ? matchedChars[i] : void 0).filter((c) => c !== void 0);
|
|
1466
|
+
if (documentChars.every((c) => c === plain)) continue;
|
|
1467
|
+
const slots = out.flatMap((c, i) => c === plain ? [i] : []);
|
|
1468
|
+
if (slots.length !== documentChars.length) continue;
|
|
1469
|
+
slots.forEach((slot, n) => out[slot] = documentChars[n]);
|
|
1470
|
+
}
|
|
1471
|
+
return out.join("");
|
|
1472
|
+
}
|
|
1473
|
+
var shows = (segment, view) => view === "tracked" || segment.change === null || segment.change === "inserted" === (view === "accepted");
|
|
1474
|
+
function segmentsText(segments, view) {
|
|
1475
|
+
return segments.filter((segment) => shows(segment, view)).map(
|
|
1476
|
+
(segment) => view !== "tracked" || segment.change === null ? segment.text : segment.change === "deleted" ? `[-${segment.text}-]` : `{+${segment.text}+}`
|
|
1477
|
+
).join("");
|
|
1478
|
+
}
|
|
1479
|
+
function matchesIn(segments, matcher, view) {
|
|
1480
|
+
const found = [];
|
|
1481
|
+
const scan = (side, keep) => {
|
|
1482
|
+
const places = [];
|
|
1483
|
+
const owners = [];
|
|
1484
|
+
let text = "";
|
|
1485
|
+
let at = 0;
|
|
1486
|
+
for (const segment of segments) {
|
|
1487
|
+
if (shows(segment, side)) {
|
|
1488
|
+
for (let i = 0; i < segment.text.length; i++) {
|
|
1489
|
+
places.push(at + i);
|
|
1490
|
+
owners.push(segment);
|
|
1491
|
+
}
|
|
1492
|
+
text += segment.text;
|
|
1493
|
+
}
|
|
1494
|
+
at += segment.text.length;
|
|
1495
|
+
}
|
|
1496
|
+
for (const match of text.matchAll(matcher)) {
|
|
1497
|
+
if (!match[0]) continue;
|
|
1498
|
+
const start = match.index ?? 0;
|
|
1499
|
+
const touched = owners.slice(start, start + match[0].length).find((owner) => owner.change !== null);
|
|
1500
|
+
const change = touched?.change ?? null;
|
|
1501
|
+
if (!keep(change)) continue;
|
|
1502
|
+
found.push({
|
|
1503
|
+
text: match[0],
|
|
1504
|
+
change,
|
|
1505
|
+
changeId: touched?.changeId ?? null,
|
|
1506
|
+
at: places[start]
|
|
1507
|
+
});
|
|
1508
|
+
}
|
|
1509
|
+
};
|
|
1510
|
+
if (view === "tracked") {
|
|
1511
|
+
scan("accepted", (change) => change !== "deleted");
|
|
1512
|
+
scan("original", (change) => change === "deleted");
|
|
1513
|
+
} else {
|
|
1514
|
+
scan(view, () => true);
|
|
1515
|
+
}
|
|
1516
|
+
return found.sort(
|
|
1517
|
+
(a, b) => a.at - b.at || Number(b.change === "deleted") - Number(a.change === "deleted")
|
|
1518
|
+
).map(({ text, change, changeId }) => ({ text, change, changeId }));
|
|
1519
|
+
}
|
|
1520
|
+
var OPEN = "\uE000";
|
|
1521
|
+
var DELETED = "\uE001";
|
|
1522
|
+
var INSERTED = "\uE002";
|
|
1523
|
+
var CLOSE = "\uE003";
|
|
1524
|
+
var CHANGE_BASE = 57600;
|
|
1525
|
+
var SENTINEL_RUN = /([])([-])([\s\S]*?)/g;
|
|
1526
|
+
function renderView(html, raw, view, authors, AgentHtmlConverter) {
|
|
1527
|
+
const ids = topLevelIds(html);
|
|
1528
|
+
const blocks = [...raw.children, ...raw.notes ?? []];
|
|
1529
|
+
const position = new Map(blocks.map((block, i) => [block.id, i]));
|
|
1530
|
+
const known = ids.map((id) => position.get(id)).filter((at) => at !== void 0);
|
|
1531
|
+
if (!known.length) return html;
|
|
1532
|
+
const deleted = (block) => block !== void 0 && wholeBlockDeletion(block) !== null;
|
|
1533
|
+
let first = Math.min(...known);
|
|
1534
|
+
let last = Math.max(...known);
|
|
1535
|
+
if (blocks.slice(0, first).every((block) => deleted(block))) first = 0;
|
|
1536
|
+
while (last + 1 < blocks.length && deleted(blocks[last + 1])) last += 1;
|
|
1537
|
+
const changes = [];
|
|
1538
|
+
const indexOf = (id) => {
|
|
1539
|
+
let i = changes.indexOf(id);
|
|
1540
|
+
if (i < 0) i = changes.push(id) - 1;
|
|
1541
|
+
return String.fromCharCode(CHANGE_BASE + Math.min(i, 63743 - CHANGE_BASE));
|
|
1542
|
+
};
|
|
1543
|
+
const marked = (node, deletedBy) => {
|
|
1544
|
+
if (!node || typeof node !== "object") return node;
|
|
1545
|
+
const n = node;
|
|
1546
|
+
if (n.type === "text" && n.text) {
|
|
1547
|
+
const deletion = n.marks?.deletion || deletedBy && !n.marks?.insertion;
|
|
1548
|
+
if (!deletion && !n.marks?.insertion) return node;
|
|
1549
|
+
const id = n.marks?.suggestionId ?? deletedBy ?? "";
|
|
1550
|
+
const kind = deletion ? DELETED : INSERTED;
|
|
1551
|
+
return { ...n, text: `${OPEN}${kind}${indexOf(id)}${n.text}${CLOSE}` };
|
|
1552
|
+
}
|
|
1553
|
+
const structural = node;
|
|
1554
|
+
const by = structural.structuralSuggestionKind === "delete" ? structural.structuralSuggestionId : deletedBy;
|
|
1555
|
+
return Array.isArray(n.children) ? { ...n, children: n.children.map((child) => marked(child, by)) } : node;
|
|
1556
|
+
};
|
|
1557
|
+
const range = blocks.slice(first, last + 1).map((block) => marked(block));
|
|
1558
|
+
const doc = { ...raw, children: range, notes: void 0 };
|
|
1559
|
+
const rendered = AgentHtmlConverter.extractSpecificBlocks(
|
|
1560
|
+
doc,
|
|
1561
|
+
range.map((block) => block.id),
|
|
1562
|
+
void 0,
|
|
1563
|
+
{ scanDocumentFontFamily: false }
|
|
1564
|
+
).foundBlocks.map((block, i) => {
|
|
1565
|
+
const whole = view === "tracked" ? wholeBlockDeletion(range[i]) : null;
|
|
1566
|
+
return whole && !block.html.includes(OPEN) ? block.html.replace(
|
|
1567
|
+
/^<([a-z][\w-]*)/,
|
|
1568
|
+
`<$1 data-change="deleted" data-change-id="${escapeAttr(whole)}" data-author="${escapeAttr(authors.get(whole) ?? "unknown")}"`
|
|
1569
|
+
) : block.html;
|
|
1570
|
+
});
|
|
1571
|
+
const header = /^\s*Showing \d+ block\(s\):\s*/.exec(html) ? `Showing ${rendered.length} block(s):
|
|
1572
|
+
|
|
1573
|
+
` : "";
|
|
1574
|
+
return (header + rendered.join("\n")).replace(
|
|
1575
|
+
SENTINEL_RUN,
|
|
1576
|
+
(_, kind, index, text) => {
|
|
1577
|
+
const deletion = kind === DELETED;
|
|
1578
|
+
if (view === "original") return deletion ? text : "";
|
|
1579
|
+
const id = changes[index.charCodeAt(0) - CHANGE_BASE] ?? "";
|
|
1580
|
+
const tag = deletion ? "del" : "ins";
|
|
1581
|
+
return `<${tag} data-change-id="${escapeAttr(id)}" data-author="${escapeAttr(authors.get(id) ?? "unknown")}">${text}</${tag}>`;
|
|
1582
|
+
}
|
|
1583
|
+
);
|
|
1584
|
+
}
|
|
1585
|
+
function wholeBlockDeletion(block) {
|
|
1586
|
+
const b = block;
|
|
1587
|
+
if (b.structuralSuggestionKind === "delete" && b.structuralSuggestionId) {
|
|
1588
|
+
return b.structuralSuggestionId;
|
|
1589
|
+
}
|
|
1590
|
+
let id = null;
|
|
1591
|
+
let all = true;
|
|
1592
|
+
let any = false;
|
|
1593
|
+
const visit = (node) => {
|
|
1594
|
+
if (!all || !node || typeof node !== "object") return;
|
|
1595
|
+
const n = node;
|
|
1596
|
+
if (n.type === "text") {
|
|
1597
|
+
if (!n.text) return;
|
|
1598
|
+
any = true;
|
|
1599
|
+
if (!n.marks?.deletion) all = false;
|
|
1600
|
+
else id ??= n.marks.suggestionId ?? null;
|
|
1601
|
+
return;
|
|
1602
|
+
}
|
|
1603
|
+
n.children?.forEach(visit);
|
|
1604
|
+
};
|
|
1605
|
+
visit(block);
|
|
1606
|
+
return any && all ? id : null;
|
|
1607
|
+
}
|
|
1608
|
+
function topLevelIds(html) {
|
|
1609
|
+
const body = new DOMParser().parseFromString(
|
|
1610
|
+
`<!doctype html><body>${html}</body>`,
|
|
1611
|
+
"text/html"
|
|
1612
|
+
).body;
|
|
1613
|
+
return Array.from(body.children).map((el) => el.getAttribute("id")).filter((id) => Boolean(id));
|
|
1614
|
+
}
|
|
1615
|
+
var escapeAttr = (value) => value.replace(/&/g, "&").replace(/"/g, """).replace(/</g, "<");
|
|
1616
|
+
var placesByEntries = /* @__PURE__ */ new WeakMap();
|
|
1228
1617
|
async function describeChanges(ydoc, session) {
|
|
1229
1618
|
const records = session.listSuggestions();
|
|
1230
1619
|
const doc = ydocToDocument2(ydoc);
|
|
1231
|
-
const
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1620
|
+
const { documentHeadingSections } = await engineModules2.outline();
|
|
1621
|
+
const span = /* @__PURE__ */ new Map();
|
|
1622
|
+
let next = 0;
|
|
1623
|
+
const number = (node) => {
|
|
1624
|
+
if (!node || typeof node !== "object") return;
|
|
1625
|
+
const n = node;
|
|
1626
|
+
const isBlock = typeof n.id === "string" && n.type !== "text";
|
|
1627
|
+
const start = next;
|
|
1628
|
+
if (isBlock) next += 1;
|
|
1629
|
+
n.children?.forEach(number);
|
|
1630
|
+
if (isBlock) span.set(n.id, { start, end: next - 1 });
|
|
1631
|
+
};
|
|
1632
|
+
doc.children.forEach(number);
|
|
1633
|
+
const bodyEnd = next;
|
|
1634
|
+
(doc.chrome ?? []).forEach(number);
|
|
1635
|
+
(doc.notes ?? []).forEach(number);
|
|
1636
|
+
const headings = documentHeadingSections(doc, { maxDepth: 3 }).flatMap((h) => {
|
|
1637
|
+
const start = span.get(h.itemId ?? h.blockId)?.start;
|
|
1638
|
+
return start === void 0 ? [] : [{ title: h.title, level: h.level, start }];
|
|
1639
|
+
}).sort((a, b) => a.start - b.start);
|
|
1640
|
+
const sections = headings.map((h, i) => ({
|
|
1641
|
+
...h,
|
|
1642
|
+
end: (headings.slice(i + 1).find((later) => later.level <= h.level)?.start ?? bodyEnd) - 1
|
|
1643
|
+
}));
|
|
1644
|
+
const sectionAt = (at) => {
|
|
1241
1645
|
let title;
|
|
1242
|
-
for (const
|
|
1243
|
-
if (start
|
|
1244
|
-
title =
|
|
1646
|
+
for (const section of sections) {
|
|
1647
|
+
if (section.start > at) break;
|
|
1648
|
+
title = section.title;
|
|
1245
1649
|
}
|
|
1246
1650
|
return title;
|
|
1247
1651
|
};
|
|
1652
|
+
const positionOf = new Map(
|
|
1653
|
+
doc.children.map((block, index) => [block.id, index])
|
|
1654
|
+
);
|
|
1248
1655
|
const found = /* @__PURE__ */ new Map();
|
|
1249
|
-
|
|
1250
|
-
const visit = (node) => {
|
|
1656
|
+
const collect = (block, blockIndex, label) => {
|
|
1657
|
+
const visit = (node, path) => {
|
|
1251
1658
|
if (!node || typeof node !== "object") return;
|
|
1252
1659
|
const n = node;
|
|
1660
|
+
const here = typeof n.id === "string" && n.type !== "text" ? [...path, n.id] : path;
|
|
1253
1661
|
const id = n.marks?.suggestionId ?? n.suggestionId ?? n.structuralSuggestionId;
|
|
1254
1662
|
if (id) {
|
|
1255
1663
|
const entry = found.get(id) ?? {
|
|
1256
1664
|
blockIndex,
|
|
1257
1665
|
blockId: block.id ?? "",
|
|
1666
|
+
label,
|
|
1667
|
+
path: here,
|
|
1258
1668
|
deleted: "",
|
|
1259
1669
|
inserted: ""
|
|
1260
1670
|
};
|
|
@@ -1264,32 +1674,142 @@ async function describeChanges(ydoc, session) {
|
|
|
1264
1674
|
}
|
|
1265
1675
|
found.set(id, entry);
|
|
1266
1676
|
}
|
|
1267
|
-
n.children?.forEach(visit);
|
|
1677
|
+
n.children?.forEach((child) => visit(child, here));
|
|
1268
1678
|
};
|
|
1269
|
-
visit(block);
|
|
1270
|
-
}
|
|
1271
|
-
|
|
1679
|
+
visit(block, []);
|
|
1680
|
+
};
|
|
1681
|
+
doc.children.forEach(
|
|
1682
|
+
(block, blockIndex) => collect(block, blockIndex, void 0)
|
|
1683
|
+
);
|
|
1684
|
+
for (const block of doc.chrome ?? []) {
|
|
1685
|
+
collect(block, void 0, block.side === "header" ? "Header" : "Footer");
|
|
1686
|
+
}
|
|
1687
|
+
const noteCount = { footnote: 0, endnote: 0 };
|
|
1688
|
+
for (const note of doc.notes ?? []) {
|
|
1689
|
+
const kind = note.noteKind === "endnote" ? "endnote" : "footnote";
|
|
1690
|
+
noteCount[kind] += 1;
|
|
1691
|
+
collect(
|
|
1692
|
+
note,
|
|
1693
|
+
void 0,
|
|
1694
|
+
`${kind === "endnote" ? "Endnote" : "Footnote"} ${noteCount[kind]}`
|
|
1695
|
+
);
|
|
1696
|
+
}
|
|
1697
|
+
const changes = /* @__PURE__ */ new WeakMap();
|
|
1698
|
+
const entries = records.map((record) => {
|
|
1272
1699
|
const at = found.get(record.id);
|
|
1273
1700
|
const blockIndex = at?.blockIndex ?? (record.blockId ? positionOf.get(record.blockId) : void 0) ?? (record.tableId ? positionOf.get(record.tableId) : void 0);
|
|
1274
|
-
|
|
1701
|
+
const path = at?.path ?? [record.tableId, record.blockId].filter(
|
|
1702
|
+
(id) => Boolean(id && span.has(id))
|
|
1703
|
+
);
|
|
1704
|
+
const place = path.length ? span.get(path[path.length - 1]) : void 0;
|
|
1705
|
+
const entry = {
|
|
1275
1706
|
id: record.id,
|
|
1276
1707
|
type: record.type ?? "text",
|
|
1277
1708
|
author: record.agentName ?? record.authorType ?? "unknown",
|
|
1278
1709
|
...at?.blockId || record.blockId ? { blockId: at?.blockId || record.blockId } : {},
|
|
1279
|
-
...blockIndex !== void 0 ? { blockIndex
|
|
1710
|
+
...blockIndex !== void 0 ? { blockIndex } : {},
|
|
1711
|
+
...at?.label ? { section: at.label } : place && place.start < bodyEnd ? { section: sectionAt(place.start) } : {},
|
|
1280
1712
|
deleted: at?.deleted ?? "",
|
|
1281
1713
|
inserted: at?.inserted ?? ""
|
|
1282
1714
|
};
|
|
1715
|
+
if (place) changes.set(entry, { at: place.start, path });
|
|
1716
|
+
return entry;
|
|
1283
1717
|
});
|
|
1718
|
+
placesByEntries.set(entries, { span, sections, changes });
|
|
1719
|
+
return entries;
|
|
1284
1720
|
}
|
|
1285
|
-
function
|
|
1286
|
-
|
|
1287
|
-
const title = entry.section.trim().toLowerCase();
|
|
1721
|
+
function headingMatches(title, query) {
|
|
1722
|
+
const heading = title.trim().toLowerCase();
|
|
1288
1723
|
const q = query.trim().toLowerCase().replace(/\.$/, "");
|
|
1289
1724
|
if (/^[\d.]+$/.test(q)) {
|
|
1290
|
-
return new RegExp(`^${q.replace(/\./g, "\\.")}(?:[.\\s)]|$)`).test(
|
|
1725
|
+
return new RegExp(`^${q.replace(/\./g, "\\.")}(?:[.\\s)]|$)`).test(heading);
|
|
1291
1726
|
}
|
|
1292
|
-
return
|
|
1727
|
+
return heading.includes(q);
|
|
1728
|
+
}
|
|
1729
|
+
function parseBlockQuery(query) {
|
|
1730
|
+
return query.split(",").map((part) => part.trim()).filter(Boolean).map((part) => {
|
|
1731
|
+
const [from, to, ...rest] = part.split(/\.\.\.?|…/).map((id) => id.trim());
|
|
1732
|
+
if (to === void 0) return from;
|
|
1733
|
+
if (!from || !to || rest.length) {
|
|
1734
|
+
throw new ReviseError(
|
|
1735
|
+
`"${part}" is not a block range; write first..last.`,
|
|
1736
|
+
"invalid_selection"
|
|
1737
|
+
);
|
|
1738
|
+
}
|
|
1739
|
+
return { from, to };
|
|
1740
|
+
});
|
|
1741
|
+
}
|
|
1742
|
+
function applyChangeFilter(entries, filter) {
|
|
1743
|
+
const { section, exceptSection, blocks } = filter;
|
|
1744
|
+
if (!section && !exceptSection && !blocks?.length) return entries;
|
|
1745
|
+
const places = placesByEntries.get(entries);
|
|
1746
|
+
if (!places) return entries;
|
|
1747
|
+
const sectionRanges = (query) => {
|
|
1748
|
+
const q = query.trim().toLowerCase();
|
|
1749
|
+
const exact = places.sections.filter(
|
|
1750
|
+
(s) => s.title.toLowerCase() === q || s.title.toLowerCase().replace(/^[\d.]+[.)]?\s+/, "") === q
|
|
1751
|
+
);
|
|
1752
|
+
const candidates = exact.length ? exact : places.sections.filter((s) => headingMatches(s.title, query));
|
|
1753
|
+
const matched = candidates.filter(
|
|
1754
|
+
(s) => !candidates.some(
|
|
1755
|
+
(outer) => outer !== s && outer.start < s.start && s.end <= outer.end
|
|
1756
|
+
)
|
|
1757
|
+
);
|
|
1758
|
+
if (matched.length > 1) {
|
|
1759
|
+
throw new ReviseError(
|
|
1760
|
+
`"${query}" matches ${matched.length} headings: ${matched.map((s) => `"${s.title}"`).join(", ")}. Name one by its number or full title.`,
|
|
1761
|
+
"section_ambiguous"
|
|
1762
|
+
);
|
|
1763
|
+
}
|
|
1764
|
+
if (!matched.length) {
|
|
1765
|
+
throw new ReviseError(
|
|
1766
|
+
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.`,
|
|
1767
|
+
"section_not_found"
|
|
1768
|
+
);
|
|
1769
|
+
}
|
|
1770
|
+
return matched;
|
|
1771
|
+
};
|
|
1772
|
+
const spanOf = (id) => {
|
|
1773
|
+
const found = places.span.get(id);
|
|
1774
|
+
if (!found) {
|
|
1775
|
+
throw new ReviseError(`No block with id "${id}".`, "block_not_found");
|
|
1776
|
+
}
|
|
1777
|
+
return found;
|
|
1778
|
+
};
|
|
1779
|
+
const inside = sectionRanges;
|
|
1780
|
+
const keepSections = section ? inside(section) : void 0;
|
|
1781
|
+
const dropSections = exceptSection ? inside(exceptSection) : void 0;
|
|
1782
|
+
const ids = /* @__PURE__ */ new Set();
|
|
1783
|
+
const ranges = [];
|
|
1784
|
+
for (const item of blocks ?? []) {
|
|
1785
|
+
if (typeof item === "string") {
|
|
1786
|
+
spanOf(item);
|
|
1787
|
+
ids.add(item);
|
|
1788
|
+
continue;
|
|
1789
|
+
}
|
|
1790
|
+
const from = spanOf(item.from);
|
|
1791
|
+
const to = spanOf(item.to);
|
|
1792
|
+
if (from.start > to.start) {
|
|
1793
|
+
throw new ReviseError(
|
|
1794
|
+
`Block "${item.from}" comes after "${item.to}"; write the range first..last.`,
|
|
1795
|
+
"invalid_selection"
|
|
1796
|
+
);
|
|
1797
|
+
}
|
|
1798
|
+
ranges.push({ start: from.start, end: to.end });
|
|
1799
|
+
}
|
|
1800
|
+
const within = (at, r) => at >= r.start && at <= r.end;
|
|
1801
|
+
return entries.filter((entry) => {
|
|
1802
|
+
const place = places.changes.get(entry);
|
|
1803
|
+
if (!place) return false;
|
|
1804
|
+
if (keepSections && !keepSections.some((r) => within(place.at, r))) {
|
|
1805
|
+
return false;
|
|
1806
|
+
}
|
|
1807
|
+
if (dropSections?.some((r) => within(place.at, r))) return false;
|
|
1808
|
+
if (blocks?.length && !place.path.some((id) => ids.has(id)) && !ranges.some((r) => within(place.at, r))) {
|
|
1809
|
+
return false;
|
|
1810
|
+
}
|
|
1811
|
+
return true;
|
|
1812
|
+
});
|
|
1293
1813
|
}
|
|
1294
1814
|
async function markdownOf(ydoc) {
|
|
1295
1815
|
const bytes = await exportDocument(ydoc, {
|
|
@@ -1645,6 +2165,7 @@ export {
|
|
|
1645
2165
|
seedYDoc,
|
|
1646
2166
|
compare,
|
|
1647
2167
|
convert,
|
|
2168
|
+
parseBlockQuery,
|
|
1648
2169
|
formatToolResult,
|
|
1649
2170
|
agentTools
|
|
1650
2171
|
};
|