@erdemtuna/doc-review 0.9.0 → 0.10.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 +52 -227
- package/lib/anchor-text.js +155 -0
- package/lib/atomic-write.js +54 -0
- package/lib/chrome-api.js +78 -0
- package/lib/chrome-client.js +2255 -0
- package/lib/chrome-session.js +76 -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/comparison-view.js +380 -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/document-execution.js +115 -0
- package/lib/edit-limits.js +24 -0
- package/lib/editing.js +107 -0
- package/lib/execution-client.js +69 -0
- package/lib/feedback-controller.js +106 -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 +210 -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 +249 -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/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 +300 -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/view-identity.js +112 -0
- package/package.json +24 -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/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}/SKILL.md +0 -0
- /package/{src → lib}/chrome.css +0 -0
- /package/{src → lib}/chrome.html +0 -0
- /package/{src → lib}/document-trust.js +0 -0
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
const GAP = 12;
|
|
2
|
+
export function visibleViewport(win = window) {
|
|
3
|
+
const viewport = win.visualViewport;
|
|
4
|
+
return viewport
|
|
5
|
+
? { left: viewport.offsetLeft, top: viewport.offsetTop, width: viewport.width, height: viewport.height }
|
|
6
|
+
: { left: 0, top: 0, width: win.innerWidth, height: win.innerHeight };
|
|
7
|
+
}
|
|
8
|
+
export function lastVisibleRect(rects) {
|
|
9
|
+
if (!Array.isArray(rects))
|
|
10
|
+
return null;
|
|
11
|
+
for (let index = rects.length - 1; index >= 0; index -= 1) {
|
|
12
|
+
const rect = rects[index];
|
|
13
|
+
if (rect && rect.width >= 0 && rect.height >= 0)
|
|
14
|
+
return rect;
|
|
15
|
+
}
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
const clamp = (value, min, max) => Math.max(min, Math.min(max, value));
|
|
19
|
+
function frameRectToChrome(rect, frameRect) {
|
|
20
|
+
return {
|
|
21
|
+
left: frameRect.left + rect.left,
|
|
22
|
+
right: frameRect.left + rect.right,
|
|
23
|
+
top: frameRect.top + rect.top,
|
|
24
|
+
bottom: frameRect.top + rect.bottom,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
export function placeContextualSurface(geometry, options) {
|
|
28
|
+
const { frameRect, viewport, surfaceWidth = 340, surfaceHeight = 220, narrow = false, toolbarHeight = 48, } = options;
|
|
29
|
+
const bottomSheet = () => ({
|
|
30
|
+
kind: "sheet",
|
|
31
|
+
left: viewport.left,
|
|
32
|
+
top: viewport.top + viewport.height - Math.min(surfaceHeight, viewport.height),
|
|
33
|
+
width: viewport.width,
|
|
34
|
+
});
|
|
35
|
+
if (!geometry || geometry.relation === "unavailable" || !geometry.clip)
|
|
36
|
+
return { kind: "hidden" };
|
|
37
|
+
if (narrow)
|
|
38
|
+
return bottomSheet();
|
|
39
|
+
const clip = frameRectToChrome(geometry.clip, frameRect);
|
|
40
|
+
const minLeft = viewport.left + GAP;
|
|
41
|
+
const maxLeft = viewport.left + viewport.width - surfaceWidth - GAP;
|
|
42
|
+
const viewportMinTop = viewport.top + toolbarHeight + GAP;
|
|
43
|
+
const minTop = Math.max(viewportMinTop, clip.top + GAP);
|
|
44
|
+
const maxTop = viewport.top + viewport.height - surfaceHeight - GAP;
|
|
45
|
+
const clipBottomTop = clip.bottom - surfaceHeight - GAP;
|
|
46
|
+
const horizontal = Number.isFinite(geometry.horizontal)
|
|
47
|
+
? frameRect.left + geometry.horizontal
|
|
48
|
+
: clip.left;
|
|
49
|
+
const edgeLeft = clamp(horizontal + GAP, minLeft, maxLeft);
|
|
50
|
+
if (geometry.relation === "above") {
|
|
51
|
+
return { kind: "edge-top", left: edgeLeft, top: minTop, width: surfaceWidth };
|
|
52
|
+
}
|
|
53
|
+
if (geometry.relation === "below") {
|
|
54
|
+
return {
|
|
55
|
+
kind: "edge-bottom",
|
|
56
|
+
left: edgeLeft,
|
|
57
|
+
top: Math.max(viewportMinTop, Math.min(maxTop, clipBottomTop)),
|
|
58
|
+
width: surfaceWidth,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
const target = lastVisibleRect(geometry.rects);
|
|
62
|
+
if (!target)
|
|
63
|
+
return { kind: "hidden" };
|
|
64
|
+
const rect = frameRectToChrome(target, frameRect);
|
|
65
|
+
const attachedMaxTop = Math.min(maxTop, clipBottomTop);
|
|
66
|
+
const clampTop = (top) => clamp(top, minTop, Math.max(minTop, attachedMaxTop));
|
|
67
|
+
const candidates = [
|
|
68
|
+
{ left: rect.right + GAP, top: clampTop(rect.bottom - surfaceHeight) },
|
|
69
|
+
{ left: rect.left - surfaceWidth - GAP, top: clampTop(rect.bottom - surfaceHeight) },
|
|
70
|
+
{ left: clamp(rect.left, minLeft, maxLeft), top: rect.bottom + GAP },
|
|
71
|
+
{ left: clamp(rect.left, minLeft, maxLeft), top: rect.top - surfaceHeight - GAP },
|
|
72
|
+
];
|
|
73
|
+
const fit = candidates.find((candidate) => candidate.left >= minLeft &&
|
|
74
|
+
candidate.left <= maxLeft &&
|
|
75
|
+
candidate.top >= minTop &&
|
|
76
|
+
candidate.top <= attachedMaxTop &&
|
|
77
|
+
candidate.left + surfaceWidth <= Math.min(viewport.left + viewport.width - GAP, clip.right) &&
|
|
78
|
+
candidate.top + surfaceHeight <= clip.bottom);
|
|
79
|
+
const fallback = fit || {
|
|
80
|
+
left: clamp(rect.right + GAP, minLeft, maxLeft),
|
|
81
|
+
top: clampTop(rect.top),
|
|
82
|
+
};
|
|
83
|
+
return { kind: "attached", ...fallback, width: surfaceWidth };
|
|
84
|
+
}
|
|
85
|
+
export function alignedCardPosition(rects, { frameRect, viewport, width = 300, height = 180 }) {
|
|
86
|
+
const target = lastVisibleRect(rects);
|
|
87
|
+
if (!target)
|
|
88
|
+
return null;
|
|
89
|
+
const top = frameRect.top + target.top;
|
|
90
|
+
const right = frameRect.left + target.right;
|
|
91
|
+
if (top < frameRect.top || top > frameRect.bottom || right < frameRect.left || right > frameRect.right)
|
|
92
|
+
return null;
|
|
93
|
+
return {
|
|
94
|
+
left: Math.max(viewport.left + 12, Math.min(viewport.left + viewport.width - width - 12, right + 12)),
|
|
95
|
+
top: Math.max(viewport.top + 12, Math.min(viewport.top + viewport.height - height - 12, top)),
|
|
96
|
+
};
|
|
97
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { decodePage, record } from "./chrome-api.js";
|
|
2
|
+
export function createReviewController(options) {
|
|
3
|
+
let source = null;
|
|
4
|
+
let revision = 0;
|
|
5
|
+
let navigationRevision = 0;
|
|
6
|
+
let disposed = false;
|
|
7
|
+
const cleanup = new Set();
|
|
8
|
+
const invalidate = () => { revision++; };
|
|
9
|
+
const report = (error) => { if (!disposed)
|
|
10
|
+
options.failed(error); };
|
|
11
|
+
async function refresh() {
|
|
12
|
+
const started = ++revision;
|
|
13
|
+
const key = options.key();
|
|
14
|
+
const generation = options.generation();
|
|
15
|
+
options.beforeRefresh();
|
|
16
|
+
const page = await options.request(`/api/page/${key}?session=${encodeURIComponent(options.sessionId)}`);
|
|
17
|
+
if (disposed || started !== revision || key !== options.key() || generation !== options.generation())
|
|
18
|
+
return;
|
|
19
|
+
options.refreshPage(decodePage(page), true);
|
|
20
|
+
}
|
|
21
|
+
function connect() {
|
|
22
|
+
if (disposed || source)
|
|
23
|
+
return;
|
|
24
|
+
source = (options.eventSource || ((url) => new EventSource(url)))(`/events/${options.sessionId}`);
|
|
25
|
+
source.addEventListener("ended", () => { if (!disposed)
|
|
26
|
+
options.ended(); });
|
|
27
|
+
source.addEventListener("reload", () => {
|
|
28
|
+
if (disposed)
|
|
29
|
+
return;
|
|
30
|
+
invalidate();
|
|
31
|
+
void options.reload().catch(report);
|
|
32
|
+
});
|
|
33
|
+
source.addEventListener("history", () => { if (!disposed)
|
|
34
|
+
void options.history().catch(report); });
|
|
35
|
+
source.addEventListener("agent", (event) => {
|
|
36
|
+
if (disposed)
|
|
37
|
+
return;
|
|
38
|
+
try {
|
|
39
|
+
const value = record(JSON.parse(event.data));
|
|
40
|
+
if (typeof value.state !== "string")
|
|
41
|
+
throw new Error("Invalid agent state event.");
|
|
42
|
+
options.agent(value.state);
|
|
43
|
+
}
|
|
44
|
+
catch (error) {
|
|
45
|
+
report(error);
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
source.addEventListener("refresh", () => { if (!disposed)
|
|
49
|
+
void refresh().catch(report); });
|
|
50
|
+
// EventSource owns reconnect/backoff; reconnect errors are not failed mutations.
|
|
51
|
+
source.onerror = () => { };
|
|
52
|
+
}
|
|
53
|
+
async function navigate(body) {
|
|
54
|
+
if (disposed)
|
|
55
|
+
throw new Error("Review ended");
|
|
56
|
+
if (options.hasDrafts())
|
|
57
|
+
throw new Error("Save or cancel open comment drafts before changing pages");
|
|
58
|
+
const started = ++navigationRevision;
|
|
59
|
+
invalidate();
|
|
60
|
+
const key = options.key();
|
|
61
|
+
const generation = options.generation();
|
|
62
|
+
await options.save();
|
|
63
|
+
if (disposed || started !== navigationRevision || key !== options.key() || generation !== options.generation()) {
|
|
64
|
+
throw new Error("The page changed before navigating. Retry on the current page.");
|
|
65
|
+
}
|
|
66
|
+
if ("key" in body)
|
|
67
|
+
options.suspend();
|
|
68
|
+
const result = record(await options.request(`/api/session/${options.sessionId}/${"key" in body ? "goto" : "navigate"}`, { method: "POST", body: JSON.stringify(body) }));
|
|
69
|
+
// A source reload does not undo a navigation already accepted by the server.
|
|
70
|
+
if (disposed || started !== navigationRevision)
|
|
71
|
+
return;
|
|
72
|
+
const next = "key" in body ? body.key : result.key;
|
|
73
|
+
if (typeof next !== "string")
|
|
74
|
+
throw new Error("The server returned an invalid navigation target.");
|
|
75
|
+
await options.load(next);
|
|
76
|
+
}
|
|
77
|
+
return {
|
|
78
|
+
connect, refresh, navigate, invalidate,
|
|
79
|
+
own(stop) { cleanup.add(stop); return stop; },
|
|
80
|
+
dispose() {
|
|
81
|
+
if (disposed)
|
|
82
|
+
return;
|
|
83
|
+
disposed = true;
|
|
84
|
+
revision++;
|
|
85
|
+
navigationRevision++;
|
|
86
|
+
source?.close();
|
|
87
|
+
source = null;
|
|
88
|
+
for (const stop of cleanup)
|
|
89
|
+
stop();
|
|
90
|
+
cleanup.clear();
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
export const REVIEW_MODES = Object.freeze(["view", "edit"]);
|
|
2
|
+
export function normalizeReviewMode(mode) {
|
|
3
|
+
return mode === "edit" ? "edit" : "view";
|
|
4
|
+
}
|
|
5
|
+
export function savePolicyForPage(page) {
|
|
6
|
+
return page && ((typeof page === "object" || typeof page === "function") &&
|
|
7
|
+
(("savePolicy" in page && page.savePolicy === "feedback-only") ||
|
|
8
|
+
("kind" in page && page.kind === "url") ||
|
|
9
|
+
("markdown" in page && page.markdown) || ("feedbackOnly" in page && page.feedbackOnly)))
|
|
10
|
+
? "feedback-only"
|
|
11
|
+
: "writable";
|
|
12
|
+
}
|
|
13
|
+
export function reviewConfiguration(page, mode) {
|
|
14
|
+
return {
|
|
15
|
+
mode: normalizeReviewMode(mode),
|
|
16
|
+
savePolicy: savePolicyForPage(page),
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
export function applyBodyReviewMode(body, mode) {
|
|
20
|
+
const normalized = normalizeReviewMode(mode);
|
|
21
|
+
if (normalized === "edit") {
|
|
22
|
+
if (body.getAttribute("contenteditable") !== "true")
|
|
23
|
+
body.setAttribute("contenteditable", "true");
|
|
24
|
+
if (body.getAttribute("spellcheck") !== "false")
|
|
25
|
+
body.setAttribute("spellcheck", "false");
|
|
26
|
+
}
|
|
27
|
+
else {
|
|
28
|
+
if (body.hasAttribute("contenteditable"))
|
|
29
|
+
body.removeAttribute("contenteditable");
|
|
30
|
+
if (body.hasAttribute("spellcheck"))
|
|
31
|
+
body.removeAttribute("spellcheck");
|
|
32
|
+
}
|
|
33
|
+
return normalized;
|
|
34
|
+
}
|
|
35
|
+
/** Keep the active mode intact when an application hydrates and rewrites body attributes. */
|
|
36
|
+
export function keepBodyInReviewMode(body, initialMode = "view") {
|
|
37
|
+
let mode = normalizeReviewMode(initialMode);
|
|
38
|
+
let applying = false;
|
|
39
|
+
const enforce = () => {
|
|
40
|
+
if (applying)
|
|
41
|
+
return;
|
|
42
|
+
applying = true;
|
|
43
|
+
applyBodyReviewMode(body, mode);
|
|
44
|
+
applying = false;
|
|
45
|
+
};
|
|
46
|
+
const view = body.ownerDocument.defaultView;
|
|
47
|
+
if (!view)
|
|
48
|
+
throw new TypeError("The review body must belong to a window.");
|
|
49
|
+
const Observer = view.MutationObserver;
|
|
50
|
+
const observer = new Observer(enforce);
|
|
51
|
+
observer.observe(body, { attributes: true, attributeFilter: ["contenteditable", "spellcheck"] });
|
|
52
|
+
enforce();
|
|
53
|
+
return {
|
|
54
|
+
get mode() {
|
|
55
|
+
return mode;
|
|
56
|
+
},
|
|
57
|
+
setMode(nextMode) {
|
|
58
|
+
mode = normalizeReviewMode(nextMode);
|
|
59
|
+
enforce();
|
|
60
|
+
return mode;
|
|
61
|
+
},
|
|
62
|
+
disconnect() {
|
|
63
|
+
observer.disconnect();
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
}
|