@erdemtuna/doc-review 0.9.0 → 0.11.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/README.md +58 -222
- package/{src → lib}/SKILL.md +5 -2
- package/lib/anchor-text.js +155 -0
- package/lib/atomic-write.js +54 -0
- package/lib/changes-controller.js +184 -0
- package/lib/chrome-api.js +78 -0
- package/lib/chrome-client.js +1700 -0
- package/lib/chrome-session.js +73 -0
- package/lib/chrome.html +53 -0
- package/lib/cli.js +255 -0
- package/lib/click-target.js +28 -0
- package/lib/comment-anchor.js +23 -0
- package/lib/comment-target.js +164 -0
- package/lib/comments-controller.js +109 -0
- package/lib/contextual-controller.js +33 -0
- package/lib/contracts/feedback.js +1 -0
- package/lib/contracts/frame.js +7 -0
- package/lib/contracts/history.js +1 -0
- package/lib/contracts/index.js +2 -0
- package/lib/contracts/page.js +23 -0
- package/lib/controller-store.js +96 -0
- package/lib/document-execution.js +115 -0
- package/lib/edit-limits.js +24 -0
- package/lib/editing.js +107 -0
- package/lib/execution-client.js +19 -0
- package/lib/feedback-controller.js +113 -0
- package/lib/feedback-panel-controller.js +161 -0
- package/lib/frame-channel.js +42 -0
- package/lib/frame-controller.js +313 -0
- package/lib/frame-host.js +136 -0
- package/lib/frame-policy.js +50 -0
- package/lib/history-client.js +112 -0
- package/lib/history-coordinator.js +217 -0
- package/lib/history-policy.js +43 -0
- package/lib/history-server.js +464 -0
- package/lib/html-transform.js +52 -0
- package/lib/icons.js +325 -0
- package/{src → lib}/markdown.js +25 -26
- package/lib/paths.js +83 -0
- package/lib/poll-transport.js +220 -0
- package/lib/positioning.js +97 -0
- package/lib/recovery-controller.js +123 -0
- package/lib/review-controller.js +93 -0
- package/lib/review-mode.js +66 -0
- package/lib/revision-diff.js +482 -0
- package/lib/revision-schema.js +235 -0
- package/lib/revision-store.js +190 -0
- package/lib/save-controller.js +303 -0
- package/lib/sdk.js +2629 -0
- package/lib/semantic-snapshot.js +268 -0
- package/lib/serialize.js +32 -0
- package/lib/server-entry.js +26 -0
- package/lib/server-lock.js +109 -0
- package/lib/server.js +1501 -0
- package/lib/setup-guidance.js +119 -0
- package/{src → lib}/setup.js +44 -55
- package/lib/state.js +733 -0
- package/lib/toolbar-controller.js +32 -0
- package/lib/ui/THIRD_PARTY_NOTICES.md +1254 -0
- package/lib/ui/chrome.css +4881 -0
- package/lib/ui/chrome.js +52 -0
- package/lib/view-identity.js +112 -0
- package/package.json +50 -14
- package/src/anchor-text.js +0 -160
- package/src/atomic-write.js +0 -53
- package/src/chrome-client.js +0 -2747
- package/src/chrome-session.js +0 -85
- package/src/chrome.css +0 -652
- package/src/chrome.html +0 -196
- package/src/cli.js +0 -271
- package/src/click-target.js +0 -26
- package/src/comment-anchor.js +0 -22
- package/src/comment-target.js +0 -164
- package/src/comparison-view.js +0 -337
- package/src/document-execution.js +0 -103
- package/src/edit-limits.js +0 -23
- package/src/editing.js +0 -99
- package/src/execution-client.js +0 -63
- package/src/frame-channel.js +0 -44
- package/src/frame-policy.js +0 -54
- package/src/history-client.js +0 -104
- package/src/history-coordinator.js +0 -186
- package/src/history-policy.js +0 -43
- package/src/history-server.js +0 -463
- package/src/html-transform.js +0 -62
- package/src/icons.js +0 -251
- package/src/paths.js +0 -91
- package/src/poll-transport.js +0 -222
- package/src/positioning.js +0 -107
- package/src/review-mode.js +0 -59
- package/src/revision-diff.js +0 -440
- package/src/revision-schema.js +0 -219
- package/src/revision-store.js +0 -177
- package/src/sdk.js +0 -2607
- package/src/semantic-snapshot.js +0 -230
- package/src/serialize.js +0 -32
- package/src/server-entry.js +0 -26
- package/src/server-lock.js +0 -101
- package/src/server.js +0 -1466
- package/src/setup-guidance.js +0 -128
- package/src/state.js +0 -732
- package/src/view-identity.js +0 -99
- /package/{src → lib}/document-trust.js +0 -0
package/src/review-mode.js
DELETED
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
export const REVIEW_MODES = Object.freeze(["view", "edit"]);
|
|
2
|
-
|
|
3
|
-
export function normalizeReviewMode(mode) {
|
|
4
|
-
return mode === "edit" ? "edit" : "view";
|
|
5
|
-
}
|
|
6
|
-
|
|
7
|
-
export function savePolicyForPage(page) {
|
|
8
|
-
return page && (page.savePolicy === "feedback-only" || page.kind === "url" || page.markdown || page.feedbackOnly)
|
|
9
|
-
? "feedback-only"
|
|
10
|
-
: "writable";
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export function reviewConfiguration(page, mode) {
|
|
14
|
-
return {
|
|
15
|
-
mode: normalizeReviewMode(mode),
|
|
16
|
-
savePolicy: savePolicyForPage(page),
|
|
17
|
-
};
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export function applyBodyReviewMode(body, mode) {
|
|
21
|
-
const normalized = normalizeReviewMode(mode);
|
|
22
|
-
if (normalized === "edit") {
|
|
23
|
-
if (body.getAttribute("contenteditable") !== "true") body.setAttribute("contenteditable", "true");
|
|
24
|
-
if (body.getAttribute("spellcheck") !== "false") body.setAttribute("spellcheck", "false");
|
|
25
|
-
} else {
|
|
26
|
-
if (body.hasAttribute("contenteditable")) body.removeAttribute("contenteditable");
|
|
27
|
-
if (body.hasAttribute("spellcheck")) body.removeAttribute("spellcheck");
|
|
28
|
-
}
|
|
29
|
-
return normalized;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/** Keep the active mode intact when an application hydrates and rewrites body attributes. */
|
|
33
|
-
export function keepBodyInReviewMode(body, initialMode = "view") {
|
|
34
|
-
let mode = normalizeReviewMode(initialMode);
|
|
35
|
-
let applying = false;
|
|
36
|
-
const enforce = () => {
|
|
37
|
-
if (applying) return;
|
|
38
|
-
applying = true;
|
|
39
|
-
applyBodyReviewMode(body, mode);
|
|
40
|
-
applying = false;
|
|
41
|
-
};
|
|
42
|
-
const Observer = body.ownerDocument.defaultView.MutationObserver;
|
|
43
|
-
const observer = new Observer(enforce);
|
|
44
|
-
observer.observe(body, { attributes: true, attributeFilter: ["contenteditable", "spellcheck"] });
|
|
45
|
-
enforce();
|
|
46
|
-
return {
|
|
47
|
-
get mode() {
|
|
48
|
-
return mode;
|
|
49
|
-
},
|
|
50
|
-
setMode(nextMode) {
|
|
51
|
-
mode = normalizeReviewMode(nextMode);
|
|
52
|
-
enforce();
|
|
53
|
-
return mode;
|
|
54
|
-
},
|
|
55
|
-
disconnect() {
|
|
56
|
-
observer.disconnect();
|
|
57
|
-
},
|
|
58
|
-
};
|
|
59
|
-
}
|
package/src/revision-diff.js
DELETED
|
@@ -1,440 +0,0 @@
|
|
|
1
|
-
import { diffArrays, diffLines, diffWordsWithSpace } from "diff";
|
|
2
|
-
|
|
3
|
-
export const DIFF_VERSION = 2;
|
|
4
|
-
export const DIFF_LIMITS = Object.freeze({
|
|
5
|
-
maxBlocks: 2000,
|
|
6
|
-
maxCharacters: 1000000,
|
|
7
|
-
maxBlockCharacters: 100000,
|
|
8
|
-
maxRuns: 1000,
|
|
9
|
-
maxTokens: 50000,
|
|
10
|
-
maxChanges: 4000,
|
|
11
|
-
maxEditLength: 2000,
|
|
12
|
-
maxRows: 50000,
|
|
13
|
-
maxResponseCharacters: 16000000,
|
|
14
|
-
timeoutMs: 250,
|
|
15
|
-
});
|
|
16
|
-
|
|
17
|
-
class ComparisonLimit extends Error {}
|
|
18
|
-
const emptyCounts = () => ({ added: 0, modified: 0, removed: 0, total: 0 });
|
|
19
|
-
const attributes = new Set([
|
|
20
|
-
"id", "href", "src", "alt", "title", "start", "reversed", "value",
|
|
21
|
-
"rowspan", "colspan", "scope", "headers", "type",
|
|
22
|
-
]);
|
|
23
|
-
const stable = (value) => {
|
|
24
|
-
if (Array.isArray(value)) return `[${value.map(stable).join(",")}]`;
|
|
25
|
-
if (value && typeof value === "object") {
|
|
26
|
-
return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${stable(value[key])}`).join(",")}}`;
|
|
27
|
-
}
|
|
28
|
-
return JSON.stringify(value);
|
|
29
|
-
};
|
|
30
|
-
const content = (block) => stable({
|
|
31
|
-
tag: block.tag, text: block.text, attributes: block.attributes,
|
|
32
|
-
runs: block.runs, path: block.path,
|
|
33
|
-
});
|
|
34
|
-
const hash = (value) => {
|
|
35
|
-
let n = 2166136261;
|
|
36
|
-
for (let i = 0; i < value.length; i++) n = Math.imul(n ^ value.charCodeAt(i), 16777619);
|
|
37
|
-
return (n >>> 0).toString(36);
|
|
38
|
-
};
|
|
39
|
-
|
|
40
|
-
function context(options) {
|
|
41
|
-
const limits = { ...DIFF_LIMITS };
|
|
42
|
-
for (const key of Object.keys(limits)) {
|
|
43
|
-
if (options?.[key] !== undefined) {
|
|
44
|
-
if (!Number.isSafeInteger(options[key]) || options[key] < 1) throw new TypeError(`Invalid ${key}`);
|
|
45
|
-
limits[key] = Math.min(limits[key], options[key]);
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
const deadline = Date.now() + limits.timeoutMs;
|
|
49
|
-
return {
|
|
50
|
-
limits,
|
|
51
|
-
check() {
|
|
52
|
-
if (Date.now() > deadline) throw new ComparisonLimit("time");
|
|
53
|
-
},
|
|
54
|
-
primitives() {
|
|
55
|
-
this.check();
|
|
56
|
-
return { timeout: Math.max(1, deadline - Date.now()), maxEditLength: limits.maxEditLength };
|
|
57
|
-
},
|
|
58
|
-
words(before, after) {
|
|
59
|
-
if ((before.match(/\s+|[^\s]+/g)?.length || 0) +
|
|
60
|
-
(after.match(/\s+|[^\s]+/g)?.length || 0) > limits.maxTokens) {
|
|
61
|
-
throw new ComparisonLimit("tokens");
|
|
62
|
-
}
|
|
63
|
-
const parts = diffWordsWithSpace(before, after, this.primitives());
|
|
64
|
-
if (!parts) throw new ComparisonLimit("word-diff");
|
|
65
|
-
this.check();
|
|
66
|
-
return parts.map(({ value, added, removed }) => ({
|
|
67
|
-
value, ...(added ? { added: true } : {}), ...(removed ? { removed: true } : {}),
|
|
68
|
-
}));
|
|
69
|
-
},
|
|
70
|
-
};
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
function validateSnapshot(snapshot, ctx) {
|
|
74
|
-
if (!snapshot || snapshot.version !== 1 || !Array.isArray(snapshot.blocks) ||
|
|
75
|
-
!Array.isArray(snapshot.limitations) || snapshot.limitations.length > 64 ||
|
|
76
|
-
snapshot.limitations.some((item) => typeof item !== "string" || item.length > 256)) {
|
|
77
|
-
throw new TypeError("Invalid semantic snapshot");
|
|
78
|
-
}
|
|
79
|
-
if (snapshot.blocks.length > ctx.limits.maxBlocks) throw new ComparisonLimit("blocks");
|
|
80
|
-
let characters = 0;
|
|
81
|
-
const ids = new Set();
|
|
82
|
-
for (const block of snapshot.blocks) {
|
|
83
|
-
ctx.check();
|
|
84
|
-
if (!block || typeof block.id !== "string" || !block.id || block.id.length > 4096 ||
|
|
85
|
-
ids.has(block.id) || typeof block.tag !== "string" || !/^[a-z][a-z0-9-]{0,63}$/.test(block.tag) ||
|
|
86
|
-
typeof block.selector !== "string" || block.selector.length > 4096 ||
|
|
87
|
-
typeof block.text !== "string" || !Array.isArray(block.runs) ||
|
|
88
|
-
!Array.isArray(block.path) || block.path.length > 64 ||
|
|
89
|
-
block.path.some((item) => typeof item !== "string" || item.length > 4096) ||
|
|
90
|
-
!block.attributes || typeof block.attributes !== "object" || Array.isArray(block.attributes)) {
|
|
91
|
-
throw new TypeError("Invalid semantic block");
|
|
92
|
-
}
|
|
93
|
-
ids.add(block.id);
|
|
94
|
-
if (block.text.length > ctx.limits.maxBlockCharacters) throw new ComparisonLimit("block-text");
|
|
95
|
-
if (block.runs.length > ctx.limits.maxRuns) throw new ComparisonLimit("runs");
|
|
96
|
-
characters += block.text.length + block.id.length + block.selector.length +
|
|
97
|
-
block.path.reduce((sum, item) => sum + item.length, 0);
|
|
98
|
-
for (const [key, value] of Object.entries(block.attributes)) {
|
|
99
|
-
if (!attributes.has(key) || typeof value !== "string" || value.length > 4096 ||
|
|
100
|
-
(key === "value" && block.tag !== "li") ||
|
|
101
|
-
(key === "type" && block.tag !== "button")) throw new TypeError("Invalid semantic attributes");
|
|
102
|
-
characters += value.length;
|
|
103
|
-
}
|
|
104
|
-
for (const run of block.runs) {
|
|
105
|
-
if (!run || typeof run.text !== "string" || !Array.isArray(run.marks) ||
|
|
106
|
-
run.marks.length > 16 || run.marks.some((mark) => typeof mark !== "string" || mark.length > 64) ||
|
|
107
|
-
(run.href !== undefined && (typeof run.href !== "string" || run.href.length > 4096))) {
|
|
108
|
-
throw new TypeError("Invalid semantic run");
|
|
109
|
-
}
|
|
110
|
-
characters += run.text.length + (run.href?.length || 0) +
|
|
111
|
-
run.marks.reduce((sum, mark) => sum + mark.length, 0);
|
|
112
|
-
}
|
|
113
|
-
if (characters > ctx.limits.maxCharacters * 2) throw new ComparisonLimit("characters");
|
|
114
|
-
if (block.runs.map((run) => run.text).join("") !== block.text) {
|
|
115
|
-
throw new TypeError("Semantic text does not match runs");
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
function result(mode, ctx, limitations) {
|
|
121
|
-
return {
|
|
122
|
-
version: DIFF_VERSION, mode, status: "complete", changes: [], rows: [], hunks: [],
|
|
123
|
-
counts: emptyCounts(), limits: { ...ctx.limits }, limitations: [...new Set(limitations)].sort(),
|
|
124
|
-
};
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
function failed(mode, ctx, error) {
|
|
128
|
-
if (!(error instanceof ComparisonLimit) && !(error instanceof TypeError)) throw error;
|
|
129
|
-
return {
|
|
130
|
-
version: DIFF_VERSION, mode,
|
|
131
|
-
status: error instanceof ComparisonLimit ? "limited" : "unavailable",
|
|
132
|
-
changes: [], rows: [], hunks: [], counts: null, limits: { ...(ctx?.limits || DIFF_LIMITS) },
|
|
133
|
-
limitations: [error instanceof ComparisonLimit ? `comparison-limit:${error.message}` : "invalid-input"],
|
|
134
|
-
};
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
function append(output, ctx, change, beforeIndex, afterIndex) {
|
|
138
|
-
ctx.check();
|
|
139
|
-
if (output.changes.length >= ctx.limits.maxChanges) throw new ComparisonLimit("changes");
|
|
140
|
-
change.id = `change-${beforeIndex ?? "none"}-${afterIndex ?? "none"}-${hash(stable(change))}`;
|
|
141
|
-
output.changes.push(change);
|
|
142
|
-
output.counts[change.kind]++;
|
|
143
|
-
output.counts.total++;
|
|
144
|
-
return change;
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
function appendRow(output, ctx, row) {
|
|
148
|
-
ctx.check();
|
|
149
|
-
if (output.rows.length >= ctx.limits.maxRows) throw new ComparisonLimit("rows");
|
|
150
|
-
output.rows.push({
|
|
151
|
-
id: `row-${row.beforeIndex ?? "none"}-${row.afterIndex ?? "none"}`,
|
|
152
|
-
...row,
|
|
153
|
-
});
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
function finish(output, ctx) {
|
|
157
|
-
const order = new Map();
|
|
158
|
-
for (const [index, row] of output.rows.entries()) {
|
|
159
|
-
if (row.changeId && !order.has(row.changeId)) order.set(row.changeId, index);
|
|
160
|
-
const kind = row.kind === "unchanged" ? "context" : "changes";
|
|
161
|
-
const previous = output.hunks.at(-1);
|
|
162
|
-
if (previous?.kind === kind) previous.count++;
|
|
163
|
-
else output.hunks.push({ id: `hunk-${row.id}`, kind, start: index, count: 1 });
|
|
164
|
-
}
|
|
165
|
-
output.changes.sort((a, b) => order.get(a.id) - order.get(b.id));
|
|
166
|
-
if (JSON.stringify(output).length > ctx.limits.maxResponseCharacters) {
|
|
167
|
-
throw new ComparisonLimit("response");
|
|
168
|
-
}
|
|
169
|
-
ctx.check();
|
|
170
|
-
return output;
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
function groups(blocks, key) {
|
|
174
|
-
const map = new Map();
|
|
175
|
-
blocks.forEach((block, index) => {
|
|
176
|
-
const value = key(block);
|
|
177
|
-
if (!value) return;
|
|
178
|
-
if (!map.has(value)) map.set(value, []);
|
|
179
|
-
map.get(value).push(index);
|
|
180
|
-
});
|
|
181
|
-
return map;
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
/**
|
|
185
|
-
* Compare equivalent semantic representations. Local block IDs are never
|
|
186
|
-
* matching evidence. Repeated, moved and contextual matches cannot navigate.
|
|
187
|
-
* Optional third-argument budgets can only lower the documented hard ceilings.
|
|
188
|
-
*
|
|
189
|
-
* Version 2 adds monotonic rows covering each endpoint exactly once. Row
|
|
190
|
-
* indices are zero-based; null denotes a gap. changeId refers to changes,
|
|
191
|
-
* while moveId links two positional rows to a single counted relocation.
|
|
192
|
-
* Context/changes hunks partition all rows with zero-based start and count.
|
|
193
|
-
* Neither context collapsing nor row grouping can change the actual counts.
|
|
194
|
-
*/
|
|
195
|
-
export function compareSemanticSnapshots(before, after, options = {}) {
|
|
196
|
-
let ctx;
|
|
197
|
-
try {
|
|
198
|
-
ctx = context(options);
|
|
199
|
-
validateSnapshot(before, ctx);
|
|
200
|
-
validateSnapshot(after, ctx);
|
|
201
|
-
const output = result("semantic", ctx, [...before.limitations, ...after.limitations]);
|
|
202
|
-
const oldBlocks = before.blocks;
|
|
203
|
-
const newBlocks = after.blocks;
|
|
204
|
-
const oldKeys = oldBlocks.map(content);
|
|
205
|
-
const newKeys = newBlocks.map(content);
|
|
206
|
-
const oldContent = groups(oldBlocks, (block) => block.text);
|
|
207
|
-
const newContent = groups(newBlocks, (block) => block.text);
|
|
208
|
-
const oldIds = groups(oldBlocks, (block) => block.attributes.id);
|
|
209
|
-
const newIds = groups(newBlocks, (block) => block.attributes.id);
|
|
210
|
-
const oldSignatures = groups(oldBlocks, content);
|
|
211
|
-
const newSignatures = groups(newBlocks, content);
|
|
212
|
-
const pairs = new Map();
|
|
213
|
-
const used = new Set();
|
|
214
|
-
const pair = (i, j, confidence, evidence) => {
|
|
215
|
-
if (pairs.has(i) || used.has(j)) return;
|
|
216
|
-
pairs.set(i, {
|
|
217
|
-
i, j, confidence, evidence,
|
|
218
|
-
moved: stable(oldBlocks[i].path) !== stable(newBlocks[j].path),
|
|
219
|
-
});
|
|
220
|
-
used.add(j);
|
|
221
|
-
};
|
|
222
|
-
for (const [id, indices] of oldIds) {
|
|
223
|
-
const candidates = newIds.get(id);
|
|
224
|
-
if (indices.length === 1 && candidates?.length === 1) {
|
|
225
|
-
pair(indices[0], candidates[0], "exact", "authored-id");
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
for (const [text, indices] of oldContent) {
|
|
229
|
-
const candidates = newContent.get(text);
|
|
230
|
-
if (indices.length === 1 && candidates?.length === 1) {
|
|
231
|
-
const oldId = oldBlocks[indices[0]].attributes.id;
|
|
232
|
-
const newId = newBlocks[candidates[0]].attributes.id;
|
|
233
|
-
if (!oldId || !newId || oldId === newId) pair(indices[0], candidates[0], "exact", "unique-text");
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
const oldRest = oldKeys.flatMap((key, i) => pairs.has(i) ? [] : [{ key, i }]);
|
|
237
|
-
const newRest = newKeys.flatMap((key, j) => used.has(j) ? [] : [{ key, j }]);
|
|
238
|
-
const sequence = diffArrays(oldRest, newRest, {
|
|
239
|
-
...ctx.primitives(), comparator: (a, b) => a.key === b.key,
|
|
240
|
-
});
|
|
241
|
-
if (!sequence) throw new ComparisonLimit("sequence-diff");
|
|
242
|
-
let oldAt = 0;
|
|
243
|
-
let newAt = 0;
|
|
244
|
-
for (const part of sequence) {
|
|
245
|
-
if (!part.added && !part.removed) {
|
|
246
|
-
for (let k = 0; k < part.count; k++) {
|
|
247
|
-
const { i, key } = oldRest[oldAt + k];
|
|
248
|
-
const { j } = newRest[newAt + k];
|
|
249
|
-
const repeated = oldSignatures.get(key)?.length > 1 || newSignatures.get(key)?.length > 1;
|
|
250
|
-
pair(i, j, repeated ? "ambiguous" : "context", repeated ? "repeated-content" : "sequence");
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
if (!part.added) oldAt += part.count;
|
|
254
|
-
if (!part.removed) newAt += part.count;
|
|
255
|
-
}
|
|
256
|
-
// Crossing matches identify relocation without treating inserted siblings as moves.
|
|
257
|
-
const ordered = [...pairs.values()].sort((a, b) => a.i - b.i);
|
|
258
|
-
let maximum = -1;
|
|
259
|
-
for (const item of ordered) {
|
|
260
|
-
if (item.j < maximum) item.moved = true;
|
|
261
|
-
maximum = Math.max(maximum, item.j);
|
|
262
|
-
}
|
|
263
|
-
let minimum = Infinity;
|
|
264
|
-
for (let i = ordered.length - 1; i >= 0; i--) {
|
|
265
|
-
if (ordered[i].j > minimum) ordered[i].moved = true;
|
|
266
|
-
minimum = Math.min(minimum, ordered[i].j);
|
|
267
|
-
}
|
|
268
|
-
const anchors = [
|
|
269
|
-
{ i: -1, j: -1 },
|
|
270
|
-
...ordered.filter((item) => !item.moved),
|
|
271
|
-
{ i: oldBlocks.length, j: newBlocks.length },
|
|
272
|
-
];
|
|
273
|
-
for (let a = 1; a < anchors.length; a++) {
|
|
274
|
-
ctx.check();
|
|
275
|
-
const left = anchors[a - 1];
|
|
276
|
-
const right = anchors[a];
|
|
277
|
-
const oldGap = [];
|
|
278
|
-
const newGap = [];
|
|
279
|
-
for (let i = left.i + 1; i < right.i; i++) if (!pairs.has(i)) oldGap.push(i);
|
|
280
|
-
for (let j = left.j + 1; j < right.j; j++) if (!used.has(j)) newGap.push(j);
|
|
281
|
-
if (oldGap.length !== newGap.length) continue;
|
|
282
|
-
for (let k = 0; k < oldGap.length; k++) {
|
|
283
|
-
const i = oldGap[k];
|
|
284
|
-
const j = newGap[k];
|
|
285
|
-
const oldBlock = oldBlocks[i];
|
|
286
|
-
const newBlock = newBlocks[j];
|
|
287
|
-
const oldId = oldBlock.attributes.id;
|
|
288
|
-
const newId = newBlock.attributes.id;
|
|
289
|
-
if (oldBlock.tag !== newBlock.tag || (oldId && newId && oldId !== newId) ||
|
|
290
|
-
oldContent.get(oldBlock.text)?.length > 1 ||
|
|
291
|
-
newContent.get(newBlock.text)?.length > 1) continue;
|
|
292
|
-
pair(i, j, "context", "neighbor-context");
|
|
293
|
-
}
|
|
294
|
-
}
|
|
295
|
-
const makeChange = (i, j, matched) => {
|
|
296
|
-
const oldBlock = i === null ? null : oldBlocks[i];
|
|
297
|
-
const newBlock = j === null ? null : newBlocks[j];
|
|
298
|
-
const kind = !oldBlock ? "added" : !newBlock ? "removed" : "modified";
|
|
299
|
-
const fields = [];
|
|
300
|
-
if (!oldBlock || !newBlock) fields.push("block");
|
|
301
|
-
else {
|
|
302
|
-
if (oldBlock.text !== newBlock.text) fields.push("text");
|
|
303
|
-
if (oldBlock.tag !== newBlock.tag || stable(oldBlock.path) !== stable(newBlock.path)) fields.push("structure");
|
|
304
|
-
if (stable(oldBlock.attributes) !== stable(newBlock.attributes)) fields.push("attributes");
|
|
305
|
-
if (stable(oldBlock.runs) !== stable(newBlock.runs)) fields.push("runs");
|
|
306
|
-
if (matched.moved) fields.push("order");
|
|
307
|
-
}
|
|
308
|
-
const confidence = matched?.moved ? "ambiguous" : (matched?.confidence || "context");
|
|
309
|
-
return append(output, ctx, {
|
|
310
|
-
kind, before: oldBlock?.text || "", after: newBlock?.text || "",
|
|
311
|
-
beforeBlock: oldBlock, afterBlock: newBlock, confidence,
|
|
312
|
-
evidence: matched?.evidence || "unmatched",
|
|
313
|
-
navigation: confidence === "exact" && newBlock?.selector
|
|
314
|
-
? { selector: newBlock.selector, blockId: newBlock.id } : null,
|
|
315
|
-
fields, segments: ctx.words(oldBlock?.text || "", newBlock?.text || ""),
|
|
316
|
-
}, i, j);
|
|
317
|
-
};
|
|
318
|
-
const oldChanges = new Map();
|
|
319
|
-
const newChanges = new Map();
|
|
320
|
-
for (let i = 0; i < oldBlocks.length; i++) {
|
|
321
|
-
const matched = pairs.get(i);
|
|
322
|
-
let change;
|
|
323
|
-
if (!matched) change = makeChange(i, null, null);
|
|
324
|
-
else if (oldKeys[i] !== newKeys[matched.j] || matched.moved) change = makeChange(i, matched.j, matched);
|
|
325
|
-
if (change) {
|
|
326
|
-
oldChanges.set(i, change);
|
|
327
|
-
if (matched) newChanges.set(matched.j, change);
|
|
328
|
-
}
|
|
329
|
-
}
|
|
330
|
-
for (let j = 0; j < newBlocks.length; j++) {
|
|
331
|
-
if (!used.has(j)) newChanges.set(j, makeChange(null, j, null));
|
|
332
|
-
}
|
|
333
|
-
const add = (i, j) => {
|
|
334
|
-
const match = i === null ? null : pairs.get(i);
|
|
335
|
-
const change = oldChanges.get(i) || newChanges.get(j);
|
|
336
|
-
appendRow(output, ctx, {
|
|
337
|
-
kind: i === null ? "added" : j === null ? "removed" : change ? "modified" : "unchanged",
|
|
338
|
-
beforeIndex: i, afterIndex: j,
|
|
339
|
-
beforeBlock: i === null ? null : oldBlocks[i],
|
|
340
|
-
afterBlock: j === null ? null : newBlocks[j],
|
|
341
|
-
changeId: change?.id || null,
|
|
342
|
-
confidence: change?.confidence || match?.confidence || "context",
|
|
343
|
-
evidence: change?.evidence || match?.evidence || "unmatched",
|
|
344
|
-
segments: change?.segments || [],
|
|
345
|
-
...(change?.fields.includes("order") ? { moveId: change.id } : {}),
|
|
346
|
-
});
|
|
347
|
-
};
|
|
348
|
-
// Only monotonic matches share a row. Relocations occupy both original
|
|
349
|
-
// positions, linked to the same counted change, never crossing columns.
|
|
350
|
-
let i = 0;
|
|
351
|
-
let j = 0;
|
|
352
|
-
for (const match of [...pairs.values()].filter((item) => !item.moved).sort((a, b) => a.i - b.i)) {
|
|
353
|
-
while (i < match.i) add(i++, null);
|
|
354
|
-
while (j < match.j) add(null, j++);
|
|
355
|
-
add(i++, j++);
|
|
356
|
-
}
|
|
357
|
-
while (i < oldBlocks.length) add(i++, null);
|
|
358
|
-
while (j < newBlocks.length) add(null, j++);
|
|
359
|
-
return finish(output, ctx);
|
|
360
|
-
} catch (error) {
|
|
361
|
-
return failed("semantic", ctx, error);
|
|
362
|
-
}
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
/** Raw-source hunks are inert text. They are not DOM navigation targets. */
|
|
366
|
-
export function compareSources(before, after, options = {}) {
|
|
367
|
-
let ctx;
|
|
368
|
-
try {
|
|
369
|
-
ctx = context(options);
|
|
370
|
-
if (typeof before !== "string" || typeof after !== "string") throw new TypeError("Sources must be strings");
|
|
371
|
-
if (before.length > ctx.limits.maxCharacters || after.length > ctx.limits.maxCharacters) {
|
|
372
|
-
throw new ComparisonLimit("characters");
|
|
373
|
-
}
|
|
374
|
-
if ((before.match(/\n/g)?.length || 0) + (after.match(/\n/g)?.length || 0) > ctx.limits.maxTokens) {
|
|
375
|
-
throw new ComparisonLimit("lines");
|
|
376
|
-
}
|
|
377
|
-
const output = result("source", ctx, ["source-is-not-rendered-dom"]);
|
|
378
|
-
output.endOfFile = {
|
|
379
|
-
before: { empty: !before.length, newline: before.endsWith("\n") },
|
|
380
|
-
after: { empty: !after.length, newline: after.endsWith("\n") },
|
|
381
|
-
};
|
|
382
|
-
const lines = (text) => text.match(/[^\n]*\n|[^\n]+$/g) || [];
|
|
383
|
-
const addLine = (oldText, newText, i, j, change = null) => {
|
|
384
|
-
appendRow(output, ctx, {
|
|
385
|
-
kind: oldText === null ? "added" : newText === null ? "removed" : change ? "modified" : "unchanged",
|
|
386
|
-
beforeIndex: i === null ? null : i - 1, afterIndex: j === null ? null : j - 1,
|
|
387
|
-
beforeBlock: oldText === null ? null : { text: oldText, startLine: i, endLine: i },
|
|
388
|
-
afterBlock: newText === null ? null : { text: newText, startLine: j, endLine: j },
|
|
389
|
-
changeId: change?.id || null, confidence: "exact", evidence: "source-lines",
|
|
390
|
-
segments: change ? ctx.words(oldText || "", newText || "") : [],
|
|
391
|
-
});
|
|
392
|
-
};
|
|
393
|
-
const parts = diffLines(before, after, ctx.primitives());
|
|
394
|
-
if (!parts) throw new ComparisonLimit("line-diff");
|
|
395
|
-
let oldLine = 1;
|
|
396
|
-
let newLine = 1;
|
|
397
|
-
for (let p = 0; p < parts.length; p++) {
|
|
398
|
-
const part = parts[p];
|
|
399
|
-
if (!part.added && !part.removed) {
|
|
400
|
-
for (const [offset, text] of lines(part.value).entries()) {
|
|
401
|
-
addLine(text, text, oldLine + offset, newLine + offset);
|
|
402
|
-
}
|
|
403
|
-
oldLine += part.count;
|
|
404
|
-
newLine += part.count;
|
|
405
|
-
continue;
|
|
406
|
-
}
|
|
407
|
-
let oldValue = "";
|
|
408
|
-
let newValue = "";
|
|
409
|
-
let oldCount = 0;
|
|
410
|
-
let newCount = 0;
|
|
411
|
-
do {
|
|
412
|
-
const current = parts[p];
|
|
413
|
-
if (current.removed) { oldValue += current.value; oldCount += current.count; }
|
|
414
|
-
if (current.added) { newValue += current.value; newCount += current.count; }
|
|
415
|
-
p++;
|
|
416
|
-
} while (p < parts.length && (parts[p].added || parts[p].removed));
|
|
417
|
-
p--;
|
|
418
|
-
const kind = oldCount && newCount ? "modified" : oldCount ? "removed" : "added";
|
|
419
|
-
const change = append(output, ctx, {
|
|
420
|
-
kind, before: oldValue, after: newValue,
|
|
421
|
-
beforeBlock: oldCount ? { startLine: oldLine, endLine: oldLine + oldCount - 1, text: oldValue } : null,
|
|
422
|
-
afterBlock: newCount ? { startLine: newLine, endLine: newLine + newCount - 1, text: newValue } : null,
|
|
423
|
-
confidence: "exact", evidence: "source-lines", navigation: null,
|
|
424
|
-
fields: ["source"], segments: ctx.words(oldValue, newValue),
|
|
425
|
-
}, oldLine, newLine);
|
|
426
|
-
const oldLines = lines(oldValue);
|
|
427
|
-
const newLines = lines(newValue);
|
|
428
|
-
for (let offset = 0; offset < Math.max(oldLines.length, newLines.length); offset++) {
|
|
429
|
-
addLine(oldLines[offset] ?? null, newLines[offset] ?? null,
|
|
430
|
-
offset < oldLines.length ? oldLine + offset : null,
|
|
431
|
-
offset < newLines.length ? newLine + offset : null, change);
|
|
432
|
-
}
|
|
433
|
-
oldLine += oldCount;
|
|
434
|
-
newLine += newCount;
|
|
435
|
-
}
|
|
436
|
-
return finish(output, ctx);
|
|
437
|
-
} catch (error) {
|
|
438
|
-
return failed("source", ctx, error);
|
|
439
|
-
}
|
|
440
|
-
}
|