@erdemtuna/doc-review 0.7.0 → 0.8.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 +12 -5
- package/package.json +3 -2
- package/src/SKILL.md +27 -5
- package/src/anchor-text.js +45 -0
- package/src/chrome-client.js +1272 -252
- package/src/chrome-session.js +70 -0
- package/src/chrome.css +278 -329
- package/src/chrome.html +81 -35
- package/src/cli.js +13 -5
- package/src/comment-anchor.js +22 -0
- package/src/comment-target.js +77 -0
- package/src/icons.js +251 -0
- package/src/paths.js +5 -1
- package/src/positioning.js +107 -0
- package/src/review-mode.js +59 -0
- package/src/sdk.js +792 -69
- package/src/serialize.js +0 -14
- package/src/server.js +15 -4
- package/src/state.js +20 -1
package/src/serialize.js
CHANGED
|
@@ -9,20 +9,6 @@
|
|
|
9
9
|
export const UI_ATTR = "data-eh-ui";
|
|
10
10
|
export const MARK_ATTR = "data-eh-mark";
|
|
11
11
|
|
|
12
|
-
/** Keep a hydrated app editable even if its framework removes the attribute. */
|
|
13
|
-
export function keepBodyEditable(body) {
|
|
14
|
-
const enforce = () => {
|
|
15
|
-
if (body.getAttribute("contenteditable") !== "true") {
|
|
16
|
-
body.setAttribute("contenteditable", "true");
|
|
17
|
-
}
|
|
18
|
-
};
|
|
19
|
-
enforce();
|
|
20
|
-
const Observer = body.ownerDocument.defaultView.MutationObserver;
|
|
21
|
-
const observer = new Observer(enforce);
|
|
22
|
-
observer.observe(body, { attributes: true, attributeFilter: ["contenteditable"] });
|
|
23
|
-
return observer;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
12
|
/** Serialize a document to the HTML that should live on disk. */
|
|
27
13
|
export function serializeDocument(doc) {
|
|
28
14
|
const clone = doc.documentElement.cloneNode(true);
|
package/src/server.js
CHANGED
|
@@ -4,6 +4,7 @@ import path from "node:path";
|
|
|
4
4
|
import crypto from "node:crypto";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
6
|
import { atomicWrite, Store, resolveAsset } from "./state.js";
|
|
7
|
+
import { normalizeCommentAnchor } from "./comment-anchor.js";
|
|
7
8
|
import { injectSdk, stripSdk } from "./html-transform.js";
|
|
8
9
|
import { isMarkdown, renderMarkdownPage } from "./markdown.js";
|
|
9
10
|
import { canonicalTarget, ensureStateDir, localUrl, SERVER_PROTOCOL, serverPath, stateDir, targetKey } from "./paths.js";
|
|
@@ -274,7 +275,7 @@ export function createServer({ store: suppliedStore, storeOptions, owner = null,
|
|
|
274
275
|
id: c.id,
|
|
275
276
|
kind: c.kind,
|
|
276
277
|
quote: c.quote,
|
|
277
|
-
anchor: c.anchor,
|
|
278
|
+
anchor: c.anchor == null ? c.anchor : normalizeCommentAnchor(c.kind, c.anchor),
|
|
278
279
|
feedback: c.feedback,
|
|
279
280
|
...(c.correction ? { correction: true, correction_of: c.correctionOf } : {}),
|
|
280
281
|
})),
|
|
@@ -562,6 +563,10 @@ export function createServer({ store: suppliedStore, storeOptions, owner = null,
|
|
|
562
563
|
if (route === "/chrome.css") return serveFile(res, path.join(here, "chrome.css"));
|
|
563
564
|
if (route === "/chrome.js") return serveFile(res, path.join(here, "chrome-client.js"));
|
|
564
565
|
if (route === "/chrome-session.js") return serveFile(res, path.join(here, "chrome-session.js"));
|
|
566
|
+
if (route === "/icons.js") return serveFile(res, path.join(here, "icons.js"), opaqueModuleCors(req));
|
|
567
|
+
if (route === "/positioning.js") return serveFile(res, path.join(here, "positioning.js"), opaqueModuleCors(req));
|
|
568
|
+
if (route === "/review-mode.js") return serveFile(res, path.join(here, "review-mode.js"), opaqueModuleCors(req));
|
|
569
|
+
if (route === "/comment-target.js") return serveFile(res, path.join(here, "comment-target.js"), opaqueModuleCors(req));
|
|
565
570
|
if (route === "/sdk.js") return serveFile(res, path.join(here, "sdk.js"), opaqueModuleCors(req));
|
|
566
571
|
if (route === "/editing.js") return serveFile(res, path.join(here, "editing.js"), opaqueModuleCors(req));
|
|
567
572
|
if (route === "/anchor-text.js") return serveFile(res, path.join(here, "anchor-text.js"), opaqueModuleCors(req));
|
|
@@ -829,15 +834,21 @@ export function createServer({ store: suppliedStore, storeOptions, owner = null,
|
|
|
829
834
|
|
|
830
835
|
if (action === "comment" && req.method === "POST") {
|
|
831
836
|
const body = await readBody(req);
|
|
837
|
+
const kind = body.kind === "element" ? "element" : "selection";
|
|
838
|
+
const feedback = String(body.feedback || "").trim();
|
|
832
839
|
const comment = {
|
|
833
840
|
id: uid("c"),
|
|
834
|
-
kind
|
|
841
|
+
kind,
|
|
835
842
|
quote: String(body.quote || ""),
|
|
836
|
-
anchor:
|
|
837
|
-
|
|
843
|
+
anchor: normalizeCommentAnchor(
|
|
844
|
+
kind,
|
|
845
|
+
body.anchor || (kind === "selection" ? { quote: String(body.quote || "") } : null)
|
|
846
|
+
),
|
|
847
|
+
feedback,
|
|
838
848
|
createdAt: Date.now(),
|
|
839
849
|
};
|
|
840
850
|
if (!comment.feedback) return json(res, 400, { error: "empty feedback" });
|
|
851
|
+
if (!comment.anchor) return json(res, 400, { error: "invalid comment anchor" });
|
|
841
852
|
store.addComment(key, comment);
|
|
842
853
|
return json(res, 200, { comment, page: pageState(key) });
|
|
843
854
|
}
|
package/src/state.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import crypto from "node:crypto";
|
|
2
2
|
import fs from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
|
+
import { normalizeCommentAnchor } from "./comment-anchor.js";
|
|
4
5
|
import { canonicalTarget, ensureStateDir, pageKey, realFile, statePath, targetKey } from "./paths.js";
|
|
5
6
|
|
|
6
7
|
/** Anything untouched this long is review debris, not work in progress. */
|
|
@@ -46,6 +47,17 @@ function normalizeState(parsed, makeBatchId) {
|
|
|
46
47
|
receipts: parsed.receipts && typeof parsed.receipts === "object" ? parsed.receipts : {},
|
|
47
48
|
};
|
|
48
49
|
let changed = !parsed.batches || !parsed.receipts;
|
|
50
|
+
const normalizeAnchor = (comment) => {
|
|
51
|
+
if (!comment || comment.anchor == null) return;
|
|
52
|
+
const normalized = normalizeCommentAnchor(comment.kind === "element" ? "element" : "selection", comment.anchor);
|
|
53
|
+
if (JSON.stringify(normalized) !== JSON.stringify(comment.anchor)) {
|
|
54
|
+
comment.anchor = normalized;
|
|
55
|
+
changed = true;
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
for (const page of Object.values(data.pages)) {
|
|
59
|
+
for (const comment of page?.comments || []) normalizeAnchor(comment);
|
|
60
|
+
}
|
|
49
61
|
for (const record of Object.values(data.batches)) {
|
|
50
62
|
if (!record || typeof record !== "object" || !record.batch || !Array.isArray(record.cleanup)) {
|
|
51
63
|
throw new Error("Invalid doc-review state: malformed feedback batch.");
|
|
@@ -73,6 +85,9 @@ function normalizeState(parsed, makeBatchId) {
|
|
|
73
85
|
if (!DELIVERY_STATES.has(record.delivery_state)) {
|
|
74
86
|
throw new Error(`Invalid doc-review state: unknown delivery state ${record.delivery_state}.`);
|
|
75
87
|
}
|
|
88
|
+
for (const page of record.batch.pages || []) {
|
|
89
|
+
for (const comment of page.comments || []) normalizeAnchor(comment);
|
|
90
|
+
}
|
|
76
91
|
}
|
|
77
92
|
return { data, changed };
|
|
78
93
|
}
|
|
@@ -234,8 +249,12 @@ export class Store {
|
|
|
234
249
|
}
|
|
235
250
|
|
|
236
251
|
addComment(key, comment) {
|
|
252
|
+
const anchor = comment.anchor == null
|
|
253
|
+
? comment.anchor
|
|
254
|
+
: normalizeCommentAnchor(comment.kind === "element" ? "element" : "selection", comment.anchor);
|
|
255
|
+
if (comment.anchor != null && !anchor) throw new Error("Invalid comment anchor.");
|
|
237
256
|
return this.update(key, (page) => {
|
|
238
|
-
page.comments.push(comment);
|
|
257
|
+
page.comments.push({ ...comment, ...(comment.anchor !== undefined ? { anchor } : {}) });
|
|
239
258
|
});
|
|
240
259
|
}
|
|
241
260
|
|