page-harness 0.1.3 → 0.1.4
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/LICENSE +421 -21
- package/README.md +219 -77
- package/README.zh-CN.md +219 -77
- package/dist/cli.js +426 -207
- package/dist/cli.js.map +1 -1
- package/dist/client/page/live-overlay.js +2 -2
- package/dist/client/page/overlay.js +17 -17
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -2239,7 +2239,97 @@ function techOf(item) {
|
|
|
2239
2239
|
function style(item) {
|
|
2240
2240
|
return item.patch?.style ?? {};
|
|
2241
2241
|
}
|
|
2242
|
-
function
|
|
2242
|
+
function imageCopyEn(item) {
|
|
2243
|
+
const s = style(item);
|
|
2244
|
+
const blob = Object.values(s).join(" ");
|
|
2245
|
+
const white = /#fff\b|#ffffff|white|rgb\(\s*255\s*,\s*255\s*,\s*255/i.test(blob);
|
|
2246
|
+
const border = !!(s.border || s["border-width"] || s["border-style"] || s["border-color"]);
|
|
2247
|
+
const rot = /rotate\(\s*(-?[\d.]+)\s*deg/i.exec(s.transform || blob);
|
|
2248
|
+
const deg = rot ? Number(rot[1]) : 0;
|
|
2249
|
+
const shadow = !!s["box-shadow"];
|
|
2250
|
+
const bits = [];
|
|
2251
|
+
if (border) bits.push(white ? "a white border" : "a border");
|
|
2252
|
+
if (rot && deg < -0.2) bits.push("a slight tilt left");
|
|
2253
|
+
else if (rot && deg > 0.2) bits.push("a slight tilt right");
|
|
2254
|
+
if (shadow) bits.push("a shadow");
|
|
2255
|
+
const polaroid = white && border && !!rot && shadow;
|
|
2256
|
+
const say = bits.length ? `This photo now has ${bits.join(", ")}${polaroid ? ", like a polaroid on a desk" : ""}.` : "This photo looks different. Before and after are below.";
|
|
2257
|
+
let title = "Photo appearance changed";
|
|
2258
|
+
if (polaroid) title = "Photo tilted like a polaroid";
|
|
2259
|
+
else if (white && border && rot) title = "Photo has a white border and a tilt";
|
|
2260
|
+
else if (bits[0]) title = `Photo has ${bits[0]}`;
|
|
2261
|
+
return { title, say };
|
|
2262
|
+
}
|
|
2263
|
+
function textCopyEn(item) {
|
|
2264
|
+
const before = item.anchor?.text?.trim();
|
|
2265
|
+
const after = item.patch?.text?.trim();
|
|
2266
|
+
const s = style(item);
|
|
2267
|
+
const italic = s["font-style"] === "italic";
|
|
2268
|
+
const bangGone = !!before?.endsWith("!") && !!after && !after.endsWith("!");
|
|
2269
|
+
const what = isHeading(item) ? "The heading" : "This text";
|
|
2270
|
+
const parts = [];
|
|
2271
|
+
if (before && after && before !== after) parts.push(`changed from \u201C${clip2(before, 80)}\u201D to \u201C${clip2(after, 80)}\u201D`);
|
|
2272
|
+
else if (after) parts.push(`changed to \u201C${clip2(after, 80)}\u201D`);
|
|
2273
|
+
if (italic) parts.push(parts.length ? "and is now italic" : "is now italic");
|
|
2274
|
+
if (bangGone) parts.push("and the exclamation mark is gone");
|
|
2275
|
+
if (!parts.length) return genericCopyEn(item);
|
|
2276
|
+
const title = italic && bangGone ? `${what} is italic, without the exclamation mark` : italic ? `${what} is italic` : bangGone ? `${what} lost its exclamation mark` : what === "The heading" ? "Heading text changed" : "Text changed";
|
|
2277
|
+
return { title, say: `${what} ${parts.join(", ")}.` };
|
|
2278
|
+
}
|
|
2279
|
+
function rampCopyEn(members) {
|
|
2280
|
+
const books = bookish(members);
|
|
2281
|
+
const noun = books ? "bibliography" : "list";
|
|
2282
|
+
const rows = members.map((i) => {
|
|
2283
|
+
const n = Number(/nth-(?:child|of-type)\((\d+)\)/i.exec(i.selector)?.[1] ?? NaN);
|
|
2284
|
+
const s = style(i);
|
|
2285
|
+
return { n, px: px(s["font-size"]), border: !!(s["border-bottom"] || s["border-bottom-width"]), bold: Number(s["font-weight"]) >= 600 || /bold/i.test(s["font-weight"] || "") };
|
|
2286
|
+
}).filter((r) => !Number.isNaN(r.n) && !Number.isNaN(r.px)).sort((a, b) => a.n - b.n);
|
|
2287
|
+
let decreasing = rows.length ? 1 : 0;
|
|
2288
|
+
for (let i = 1; i < rows.length; i++) {
|
|
2289
|
+
if (rows[i].px < rows[i - 1].px - 0.5) decreasing = i + 1;
|
|
2290
|
+
else break;
|
|
2291
|
+
}
|
|
2292
|
+
const lined = rows.find((r) => r.border);
|
|
2293
|
+
const cut = lined ? rows.indexOf(lined) + 1 : decreasing;
|
|
2294
|
+
const restBold = rows.slice(cut).some((r) => r.bold);
|
|
2295
|
+
const ul = members.find((m) => listRootSelector(m.selector) === norm(m.selector));
|
|
2296
|
+
const bullets = /none/i.test(style(ul ?? members[0])["list-style"] || style(ul ?? members[0])["list-style-type"] || "");
|
|
2297
|
+
const bits = [`The ${noun} ${bullets ? "is no longer a same-size bulleted list" : "uses a new type scale"}`];
|
|
2298
|
+
if (cut >= 2) bits.push(`the first ${cut} items step down in size`);
|
|
2299
|
+
if (lined) bits.push(`item ${lined.n} has a line under it`);
|
|
2300
|
+
if (restBold) bits.push("the later items are the same bold size");
|
|
2301
|
+
const title = cut >= 2 ? `${noun[0].toUpperCase()}${noun.slice(1)} steps down in size` : `${noun[0].toUpperCase()}${noun.slice(1)} appearance changed`;
|
|
2302
|
+
return { title, say: `${bits.join(", ")}.` };
|
|
2303
|
+
}
|
|
2304
|
+
function genericCopyEn(item) {
|
|
2305
|
+
const s = style(item);
|
|
2306
|
+
const bits = [];
|
|
2307
|
+
if (s["font-style"] === "italic") bits.push("is italic");
|
|
2308
|
+
if (Number(s["font-weight"]) >= 600 || /bold/i.test(s["font-weight"] || "")) bits.push("is bold");
|
|
2309
|
+
if (s["font-size"]) bits.push(`size is ${s["font-size"]}`);
|
|
2310
|
+
if (s["font-family"]) bits.push("uses a different font");
|
|
2311
|
+
if (s.color) bits.push("text color changed");
|
|
2312
|
+
if (s["background-color"] || s.background) bits.push("background changed");
|
|
2313
|
+
if (s["border-radius"]) bits.push("corners changed");
|
|
2314
|
+
if (s.border || s["border-color"]) bits.push("border changed");
|
|
2315
|
+
if (s["text-decoration"] || s["text-decoration-line"]) bits.push("text decoration changed");
|
|
2316
|
+
if (s.opacity) bits.push("opacity changed");
|
|
2317
|
+
const where = targetWordsEn(item);
|
|
2318
|
+
if (!bits.length) return { title: "Appearance changed", say: `${where} looks different. Before and after are below.` };
|
|
2319
|
+
return { title: "Appearance changed", say: `${where} ${bits.join(", ")}.` };
|
|
2320
|
+
}
|
|
2321
|
+
function targetWordsEn(item) {
|
|
2322
|
+
const text = item.anchor?.text?.trim();
|
|
2323
|
+
if (text) return `\u201C${clip2(text, 42)}\u201D`;
|
|
2324
|
+
const tag = (item.anchor?.tag || "").toLowerCase();
|
|
2325
|
+
if (tag === "img" || /(?:^|[\s>+~])img(?![a-z-])/i.test(item.selector)) return "this photo";
|
|
2326
|
+
if (/^h[1-6]$/.test(tag) || /(?:^|[\s>+~])h[1-6](?![a-z-])/i.test(item.selector)) return "this heading";
|
|
2327
|
+
const label = item.label?.replace(/^[a-z0-9-]+\s*/i, "").replace(/^"|"$/g, "").trim();
|
|
2328
|
+
if (label) return `\u201C${clip2(label, 42)}\u201D`;
|
|
2329
|
+
return "this part of the page";
|
|
2330
|
+
}
|
|
2331
|
+
function targetWords(item, lang = "zh-CN") {
|
|
2332
|
+
if (lang === "en") return targetWordsEn(item);
|
|
2243
2333
|
const text = item.anchor?.text?.trim();
|
|
2244
2334
|
if (text) return `\u300C${clip2(text, 42)}\u300D\u8FD9\u4E00\u5757`;
|
|
2245
2335
|
const tag = (item.anchor?.tag || "").toLowerCase();
|
|
@@ -2259,7 +2349,8 @@ function bookish(items) {
|
|
|
2259
2349
|
const blob = items.map((i) => `${i.anchor?.text ?? ""} ${i.label ?? ""} ${i.note ?? ""}`).join(" ");
|
|
2260
2350
|
return /书目|书单|bibliography|\bC\+\+|Tour of|Programming:/i.test(blob);
|
|
2261
2351
|
}
|
|
2262
|
-
function imageCopy(item) {
|
|
2352
|
+
function imageCopy(item, lang = "zh-CN") {
|
|
2353
|
+
if (lang === "en") return imageCopyEn(item);
|
|
2263
2354
|
const s = style(item);
|
|
2264
2355
|
const blob = Object.values(s).join(" ");
|
|
2265
2356
|
const white = /#fff\b|#ffffff|white|rgb\(\s*255\s*,\s*255\s*,\s*255/i.test(blob);
|
|
@@ -2280,7 +2371,8 @@ function imageCopy(item) {
|
|
|
2280
2371
|
else if (bits[0]) title = `\u7167\u7247${bits[0]}`;
|
|
2281
2372
|
return { title, say };
|
|
2282
2373
|
}
|
|
2283
|
-
function textCopy(item) {
|
|
2374
|
+
function textCopy(item, lang = "zh-CN") {
|
|
2375
|
+
if (lang === "en") return textCopyEn(item);
|
|
2284
2376
|
const before = item.anchor?.text?.trim();
|
|
2285
2377
|
const after = item.patch?.text?.trim();
|
|
2286
2378
|
const s = style(item);
|
|
@@ -2303,7 +2395,8 @@ function px(value) {
|
|
|
2303
2395
|
const n = parseFloat(value);
|
|
2304
2396
|
return Number.isFinite(n) ? n : NaN;
|
|
2305
2397
|
}
|
|
2306
|
-
function rampCopy(members) {
|
|
2398
|
+
function rampCopy(members, lang = "zh-CN") {
|
|
2399
|
+
if (lang === "en") return rampCopyEn(members);
|
|
2307
2400
|
const books = bookish(members);
|
|
2308
2401
|
const noun = books ? "\u4E66\u76EE" : "\u5217\u8868";
|
|
2309
2402
|
const rows = members.map((i) => {
|
|
@@ -2328,7 +2421,8 @@ function rampCopy(members) {
|
|
|
2328
2421
|
const title = cut >= 2 ? `${noun}\u6539\u6210\u7531\u5927\u5230\u5C0F\u7684\u5B57\u53F7\u9636\u68AF` : `${noun}\u7684\u5916\u89C2\u6539\u4E86`;
|
|
2329
2422
|
return { title, say: `${bits.join("\uFF0C")}\u3002` };
|
|
2330
2423
|
}
|
|
2331
|
-
function genericCopy(item) {
|
|
2424
|
+
function genericCopy(item, lang = "zh-CN") {
|
|
2425
|
+
if (lang === "en") return genericCopyEn(item);
|
|
2332
2426
|
const s = style(item);
|
|
2333
2427
|
const bits = [];
|
|
2334
2428
|
if (s["font-style"] === "italic") bits.push("\u6539\u6210\u4E86\u659C\u4F53");
|
|
@@ -2341,31 +2435,31 @@ function genericCopy(item) {
|
|
|
2341
2435
|
if (s.border || s["border-color"]) bits.push("\u8FB9\u6846\u6539\u4E86");
|
|
2342
2436
|
if (s["text-decoration"] || s["text-decoration-line"]) bits.push("\u6587\u5B57\u88C5\u9970\u6539\u4E86");
|
|
2343
2437
|
if (s.opacity) bits.push("\u900F\u660E\u5EA6\u6539\u4E86");
|
|
2344
|
-
const where = targetWords(item);
|
|
2438
|
+
const where = targetWords(item, "zh-CN");
|
|
2345
2439
|
if (!bits.length) return { title: "\u8FD9\u4E00\u5757\u7684\u5916\u89C2\u6539\u4E86", say: `${where}\u7684\u5916\u89C2\u6539\u4E86\uFF0C\u5DEE\u522B\u770B\u4E0B\u9762\u7684\u6539\u524D\u548C\u6539\u540E\u3002` };
|
|
2346
2440
|
return { title: "\u8FD9\u4E00\u5757\u7684\u5916\u89C2\u6539\u4E86", say: `${where}${bits.join("\uFF0C")}\u3002` };
|
|
2347
2441
|
}
|
|
2348
|
-
function noteCopy(item) {
|
|
2442
|
+
function noteCopy(item, lang = "zh-CN") {
|
|
2349
2443
|
const note = item.note.trim();
|
|
2350
|
-
const line = note.split(/[。.!!?\n]/)[0]?.trim() || "\u6587\u5B57\u610F\u89C1";
|
|
2444
|
+
const line = note.split(/[。.!!?\n]/)[0]?.trim() || (lang === "en" ? "Note" : "\u6587\u5B57\u610F\u89C1");
|
|
2351
2445
|
return {
|
|
2352
2446
|
title: clip2(line, 36),
|
|
2353
2447
|
say: note,
|
|
2354
|
-
where: `\u5173\u4E8E${targetWords(item)}`
|
|
2448
|
+
where: lang === "en" ? `About ${targetWords(item, "en")}` : `\u5173\u4E8E${targetWords(item, "zh-CN")}`
|
|
2355
2449
|
};
|
|
2356
2450
|
}
|
|
2357
|
-
function copyFor(members, ramp) {
|
|
2358
|
-
if (ramp) return rampCopy(members);
|
|
2451
|
+
function copyFor(members, ramp, lang = "zh-CN") {
|
|
2452
|
+
if (ramp) return rampCopy(members, lang);
|
|
2359
2453
|
const item = members[0];
|
|
2360
|
-
if (!item || item.kind === "note") return noteCopy(item);
|
|
2361
|
-
if (item.patch?.text !== void 0) return textCopy(item);
|
|
2362
|
-
if (isImage(item)) return imageCopy(item);
|
|
2363
|
-
return genericCopy(item);
|
|
2454
|
+
if (!item || item.kind === "note") return noteCopy(item, lang);
|
|
2455
|
+
if (item.patch?.text !== void 0) return textCopy(item, lang);
|
|
2456
|
+
if (isImage(item)) return imageCopy(item, lang);
|
|
2457
|
+
return genericCopy(item, lang);
|
|
2364
2458
|
}
|
|
2365
|
-
function entryFrom(members, extras, ramp) {
|
|
2459
|
+
function entryFrom(members, extras, ramp, lang = "zh-CN") {
|
|
2366
2460
|
const root = ramp ? listRootSelector(members[0].selector) : null;
|
|
2367
2461
|
const primary = ramp ? members.find((m) => norm(m.selector) === root) ?? [...members].sort((a, b) => a.selector.length - b.selector.length || a.n - b.n)[0] : members[0];
|
|
2368
|
-
const copy = copyFor(members, ramp);
|
|
2462
|
+
const copy = copyFor(members, ramp, lang);
|
|
2369
2463
|
const ordered = [...members].sort((a, b) => a.n - b.n);
|
|
2370
2464
|
const lines = [...ordered, ...extras].map(techOf);
|
|
2371
2465
|
return {
|
|
@@ -2394,7 +2488,7 @@ function rampGroups(patches) {
|
|
|
2394
2488
|
}
|
|
2395
2489
|
return [...buckets.values()].filter((g) => g.length >= 2);
|
|
2396
2490
|
}
|
|
2397
|
-
function readerEntries(items) {
|
|
2491
|
+
function readerEntries(items, lang = "zh-CN") {
|
|
2398
2492
|
const paths = [...new Set(items.map((i) => i.path))];
|
|
2399
2493
|
const out = [];
|
|
2400
2494
|
for (const path18 of paths) {
|
|
@@ -2408,9 +2502,9 @@ function readerEntries(items) {
|
|
|
2408
2502
|
const extras = rest.filter((i) => folded.has(i.id));
|
|
2409
2503
|
const singles = rest.filter((i) => !folded.has(i.id));
|
|
2410
2504
|
const built = [
|
|
2411
|
-
...notes.map((i) => entryFrom([i], [], false)),
|
|
2412
|
-
...ramps.map((g) => entryFrom(g, extras.filter((e) => g.some((m) => isAncestorSelector(e.selector, m.selector))), true)),
|
|
2413
|
-
...singles.map((i) => entryFrom([i], extras.filter((e) => isAncestorSelector(e.selector, i.selector)), false))
|
|
2505
|
+
...notes.map((i) => entryFrom([i], [], false, lang)),
|
|
2506
|
+
...ramps.map((g) => entryFrom(g, extras.filter((e) => g.some((m) => isAncestorSelector(e.selector, m.selector))), true, lang)),
|
|
2507
|
+
...singles.map((i) => entryFrom([i], extras.filter((e) => isAncestorSelector(e.selector, i.selector)), false, lang))
|
|
2414
2508
|
];
|
|
2415
2509
|
out.push(...built);
|
|
2416
2510
|
}
|
|
@@ -2431,9 +2525,13 @@ function mdImage(label, src) {
|
|
|
2431
2525
|
if (!src) return [];
|
|
2432
2526
|
return [`**${label}**`, "", ``, ""];
|
|
2433
2527
|
}
|
|
2434
|
-
function reviewMarkdown(target, items, approvals) {
|
|
2435
|
-
const entries = readerEntries(items);
|
|
2528
|
+
function reviewMarkdown(target, items, approvals, lang = "zh-CN") {
|
|
2529
|
+
const entries = readerEntries(items, lang);
|
|
2530
|
+
const before = lang === "en" ? "Before" : "\u6539\u524D";
|
|
2531
|
+
const after = lang === "en" ? "After" : "\u6539\u540E";
|
|
2532
|
+
const details = lang === "en" ? "Selector and style" : "\u9009\u62E9\u5668\u548C\u6837\u5F0F";
|
|
2436
2533
|
const open = entries.filter((e) => e.status === "open").length;
|
|
2534
|
+
const who = (name) => name === "\u533F\u540D" || name === "Anonymous" ? lang === "en" ? "Anonymous" : "\u533F\u540D" : name;
|
|
2437
2535
|
const lines = [`# Page review`, "", `Site: ${target}`, `Items: ${entries.length} (${open} open)`, ""];
|
|
2438
2536
|
const paths = [.../* @__PURE__ */ new Set([...entries.map((e) => e.path), ...approvals.map((a) => a.path)])];
|
|
2439
2537
|
for (const p of paths) {
|
|
@@ -2447,15 +2545,15 @@ function reviewMarkdown(target, items, approvals) {
|
|
|
2447
2545
|
for (const note of authorNotes(e)) lines.push(note, "");
|
|
2448
2546
|
const look = e.primary.look;
|
|
2449
2547
|
if (e.kind === "patch") {
|
|
2450
|
-
lines.push(...mdImage(
|
|
2548
|
+
lines.push(...mdImage(before, look?.beforePage ?? look?.beforeImage), ...mdImage(after, look?.afterPage ?? look?.afterImage));
|
|
2451
2549
|
if (!look?.beforeImage && !look?.afterImage && !look?.beforePage && !look?.afterPage) {
|
|
2452
2550
|
const text = e.lines.find((l) => l.text);
|
|
2453
|
-
if (text?.text) lines.push(
|
|
2551
|
+
if (text?.text) lines.push(lang === "en" ? `${before} \u2192 ${after}: ${text.text}` : `${before} \u2192 ${after}\uFF1A${text.text}`, "");
|
|
2454
2552
|
}
|
|
2455
2553
|
}
|
|
2456
|
-
lines.push(`By ${e.authorName}`, "");
|
|
2554
|
+
lines.push(`By ${who(e.authorName)}`, "");
|
|
2457
2555
|
if (e.lines.length) {
|
|
2458
|
-
lines.push("<details>",
|
|
2556
|
+
lines.push("<details>", `<summary>${details}</summary>`, "");
|
|
2459
2557
|
for (const line of e.lines) {
|
|
2460
2558
|
lines.push(`- \`${line.selector}\``);
|
|
2461
2559
|
if (line.css) lines.push(` - CSS: \`${line.css}\``);
|
|
@@ -2465,7 +2563,7 @@ function reviewMarkdown(target, items, approvals) {
|
|
|
2465
2563
|
lines.push("", "</details>", "");
|
|
2466
2564
|
}
|
|
2467
2565
|
const replies = e.members.flatMap((m) => m.replies);
|
|
2468
|
-
for (const r of replies) lines.push(`- ${r.author.name}: ${r.text.replace(/\n/g, " ")}`);
|
|
2566
|
+
for (const r of replies) lines.push(`- ${who(r.author.name)}: ${r.text.replace(/\n/g, " ")}`);
|
|
2469
2567
|
if (replies.length) lines.push("");
|
|
2470
2568
|
}
|
|
2471
2569
|
}
|
|
@@ -2997,10 +3095,76 @@ function packageVersion() {
|
|
|
2997
3095
|
}
|
|
2998
3096
|
return "0.0.0";
|
|
2999
3097
|
}
|
|
3098
|
+
var BM = "\u2726 Page-Harness";
|
|
3099
|
+
var CMD = "page-harness <url> --no-agent";
|
|
3100
|
+
var HOME_COPY = {
|
|
3101
|
+
en: {
|
|
3102
|
+
title: "Drag to your bookmarks bar",
|
|
3103
|
+
sub: "Click it on any page to start marking.",
|
|
3104
|
+
mine: "My notes ({n})",
|
|
3105
|
+
back: "\u2190 Home",
|
|
3106
|
+
bar: "Bookmarks",
|
|
3107
|
+
docs: "Docs",
|
|
3108
|
+
gh: "GitHub",
|
|
3109
|
+
trouble: "Having trouble?",
|
|
3110
|
+
helpTitle: "Bookmark did nothing?",
|
|
3111
|
+
help: [
|
|
3112
|
+
[
|
|
3113
|
+
{ k: "t", v: "HTTPS pages can block requests to 127.0.0.1 on this machine. Use " },
|
|
3114
|
+
{ k: "c", v: CMD },
|
|
3115
|
+
{ k: "t", v: " and the proxy will carry them." }
|
|
3116
|
+
],
|
|
3117
|
+
[{ k: "t", v: "Some sites set a CSP, which stops the page from reaching this machine." }],
|
|
3118
|
+
[{ k: "t", v: "The bookmark only works for this run. Drag it again after a restart." }]
|
|
3119
|
+
],
|
|
3120
|
+
md: "Export Markdown",
|
|
3121
|
+
json: "Export JSON",
|
|
3122
|
+
copy: "Copy prompt",
|
|
3123
|
+
copied: "Copied",
|
|
3124
|
+
empty: "No notes for this site yet.",
|
|
3125
|
+
none: "No notes yet.",
|
|
3126
|
+
ungrouped: "(ungrouped)"
|
|
3127
|
+
},
|
|
3128
|
+
"zh-CN": {
|
|
3129
|
+
title: "\u62D6\u5230\u4E66\u7B7E\u680F\uFF0C\u5C31\u80FD\u7528",
|
|
3130
|
+
sub: "\u5728\u4EFB\u4F55\u7F51\u9875\u4E0A\u70B9\u4E00\u4E0B\uFF0C\u5C31\u80FD\u76F4\u63A5\u6807\u8BB0\u3002",
|
|
3131
|
+
mine: "\u6211\u7684\u6807\u8BB0 ({n})",
|
|
3132
|
+
back: "\u2190 \u9996\u9875",
|
|
3133
|
+
bar: "\u4E66\u7B7E\u680F",
|
|
3134
|
+
docs: "\u6587\u6863",
|
|
3135
|
+
gh: "GitHub",
|
|
3136
|
+
trouble: "\u9047\u5230\u95EE\u9898\uFF1F",
|
|
3137
|
+
helpTitle: "\u70B9\u4E86\u4E66\u7B7E\u6CA1\u53CD\u5E94\uFF1F",
|
|
3138
|
+
help: [
|
|
3139
|
+
[
|
|
3140
|
+
{ k: "t", v: "HTTPS \u7F51\u9875\u53EF\u80FD\u4F1A\u62E6\u4E0B\u53D1\u5F80\u672C\u673A 127.0.0.1 \u7684\u8BF7\u6C42\uFF0C\u6362\u7528 " },
|
|
3141
|
+
{ k: "c", v: CMD },
|
|
3142
|
+
{ k: "t", v: " \u8D70\u4EE3\u7406\u5C31\u884C\u3002" }
|
|
3143
|
+
],
|
|
3144
|
+
[{ k: "t", v: "\u6709\u7684\u7F51\u7AD9\u8BBE\u4E86 CSP\uFF0C\u4F1A\u7981\u6B62\u9875\u9762\u8FDE\u672C\u673A\u3002" }],
|
|
3145
|
+
[{ k: "t", v: "\u4E66\u7B7E\u53EA\u5728\u8FD9\u6B21\u542F\u52A8\u671F\u95F4\u6709\u6548\uFF0C\u91CD\u65B0\u542F\u52A8\u540E\u8981\u518D\u62D6\u4E00\u6B21\u3002" }]
|
|
3146
|
+
],
|
|
3147
|
+
md: "\u5BFC\u51FA Markdown",
|
|
3148
|
+
json: "\u5BFC\u51FA JSON",
|
|
3149
|
+
copy: "\u590D\u5236 Prompt",
|
|
3150
|
+
copied: "\u5DF2\u590D\u5236",
|
|
3151
|
+
empty: "\u8FD9\u4E2A\u7F51\u7AD9\u8FD8\u6CA1\u6709\u6807\u8BB0\u3002",
|
|
3152
|
+
none: "\u8FD8\u6CA1\u6709\u6807\u8BB0\u3002",
|
|
3153
|
+
ungrouped: "\uFF08\u672A\u5206\u7EC4\uFF09"
|
|
3154
|
+
}
|
|
3155
|
+
};
|
|
3156
|
+
function esc(s) {
|
|
3157
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
3158
|
+
}
|
|
3159
|
+
function helpInner(lines) {
|
|
3160
|
+
return lines.map((line) => `<p>\xB7 ${line.map((bit) => bit.k === "c" ? `<code>${esc(bit.v)}</code>` : esc(bit.v)).join("")}</p>`).join("");
|
|
3161
|
+
}
|
|
3000
3162
|
function homeHtml(boot) {
|
|
3001
3163
|
const json2 = JSON.stringify(boot).replace(/</g, "\\u003c");
|
|
3164
|
+
const t = HOME_COPY[boot.lang ?? "en"];
|
|
3165
|
+
const htmlLang = boot.lang ?? "en";
|
|
3002
3166
|
return `<!doctype html>
|
|
3003
|
-
<html lang="
|
|
3167
|
+
<html lang="${htmlLang}">
|
|
3004
3168
|
<head>
|
|
3005
3169
|
<meta charset="utf-8">
|
|
3006
3170
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
@@ -3008,35 +3172,42 @@ function homeHtml(boot) {
|
|
|
3008
3172
|
<link rel="stylesheet" href="/__ph/home.css">
|
|
3009
3173
|
</head>
|
|
3010
3174
|
<body>
|
|
3011
|
-
<div class="
|
|
3012
|
-
<
|
|
3013
|
-
|
|
3014
|
-
|
|
3015
|
-
|
|
3016
|
-
|
|
3017
|
-
|
|
3018
|
-
|
|
3019
|
-
|
|
3020
|
-
|
|
3021
|
-
|
|
3022
|
-
|
|
3175
|
+
<div class="top">
|
|
3176
|
+
<div class="brand"><i></i><span>page-harness</span></div>
|
|
3177
|
+
<a class="navl" id="tonotes" href="#notes">${esc(t.mine.replace("{n}", "0"))}</a>
|
|
3178
|
+
</div>
|
|
3179
|
+
<div class="view on" id="home">
|
|
3180
|
+
<div class="hero">
|
|
3181
|
+
<div class="bar" id="bar"><span id="bar-label">${esc(t.bar)}</span><span class="chip" id="chip-docs">${esc(t.docs)}</span><span class="chip" id="chip-gh">${esc(t.gh)}</span></div>
|
|
3182
|
+
<div class="copy">
|
|
3183
|
+
<h1 id="hero-title">${esc(t.title)}</h1>
|
|
3184
|
+
<p class="sub" id="hero-sub">${esc(t.sub)}</p>
|
|
3185
|
+
</div>
|
|
3186
|
+
<a class="bm" id="bm" href="#" draggable="true">${BM}</a>
|
|
3187
|
+
</div>
|
|
3188
|
+
<button type="button" class="help" id="help">${esc(t.trouble)}</button>
|
|
3189
|
+
</div>
|
|
3190
|
+
<div class="view" id="notes">
|
|
3191
|
+
<div class="notes">
|
|
3192
|
+
<div class="side" id="doms"></div>
|
|
3193
|
+
<section class="main">
|
|
3194
|
+
<div class="mh">
|
|
3195
|
+
<h2 id="host-title"></h2>
|
|
3196
|
+
<span class="actions">
|
|
3197
|
+
<button type="button" class="sb" id="exmd" data-testid="export-md">${esc(t.md)}</button>
|
|
3198
|
+
<button type="button" class="sb" id="exjson" data-testid="export-json">${esc(t.json)}</button>
|
|
3199
|
+
<button type="button" class="sb p" id="cp" data-testid="copy-prompt">${esc(t.copy)}</button>
|
|
3200
|
+
</span>
|
|
3023
3201
|
</div>
|
|
3024
|
-
<
|
|
3202
|
+
<div id="list"></div>
|
|
3025
3203
|
</section>
|
|
3026
|
-
|
|
3027
|
-
|
|
3028
|
-
|
|
3029
|
-
<
|
|
3030
|
-
<
|
|
3031
|
-
|
|
3032
|
-
|
|
3033
|
-
<button type="button" class="btn sm" id="exmd" data-testid="export-md">Export Markdown</button>
|
|
3034
|
-
<button type="button" class="btn sm" id="exjson" data-testid="export-json">Export JSON</button>
|
|
3035
|
-
<button type="button" class="btn sm copy" id="cp" data-testid="copy-prompt">Copy prompt</button>
|
|
3036
|
-
</span>
|
|
3037
|
-
</header>
|
|
3038
|
-
<div id="list"></div>
|
|
3039
|
-
</section>
|
|
3204
|
+
</div>
|
|
3205
|
+
</div>
|
|
3206
|
+
<div class="modal" id="modal">
|
|
3207
|
+
<div class="mc" id="modal-card" role="dialog" aria-modal="true" aria-labelledby="help-title">
|
|
3208
|
+
<b id="help-title">${esc(t.helpTitle)}</b>
|
|
3209
|
+
<div id="help-body">${helpInner(t.help)}</div>
|
|
3210
|
+
</div>
|
|
3040
3211
|
</div>
|
|
3041
3212
|
<script type="application/json" id="ph-boot">${json2}</script>
|
|
3042
3213
|
<script src="/__ph/home.js"></script>
|
|
@@ -3055,145 +3226,158 @@ ${HOME_FONTS_CSS}
|
|
|
3055
3226
|
* { box-sizing: border-box; }
|
|
3056
3227
|
html, body { height: 100%; margin: 0; }
|
|
3057
3228
|
body { background: var(--base); color: var(--fg); font: var(--fs)/1.5 var(--font); -webkit-font-smoothing: antialiased; }
|
|
3058
|
-
button
|
|
3059
|
-
button { cursor: pointer; }
|
|
3229
|
+
button { font: inherit; color: inherit; cursor: pointer; }
|
|
3060
3230
|
button:disabled { cursor: not-allowed; opacity: .45; }
|
|
3061
|
-
button:focus-visible,
|
|
3062
|
-
|
|
3063
|
-
.
|
|
3064
|
-
aside { background: var(--bg2); border-right: 1px solid var(--line); display: flex; flex-direction: column; min-height: 0; }
|
|
3065
|
-
.brand { display: flex; align-items: center; gap: 8px; padding: 14px 14px 8px; font-weight: 700; font-size: var(--fs-lg); }
|
|
3231
|
+
button:focus-visible, a:focus-visible { outline: none; box-shadow: var(--focus); }
|
|
3232
|
+
.top { position: fixed; z-index: 2; top: 0; left: 0; right: 0; display: flex; justify-content: space-between; align-items: center; padding: 18px 28px; }
|
|
3233
|
+
.brand { display: flex; align-items: center; gap: 8px; font-weight: 600; }
|
|
3066
3234
|
.brand i { width: 10px; height: 10px; border-radius: 3px; background: var(--accent); transform: rotate(45deg); flex: none; }
|
|
3067
|
-
.
|
|
3068
|
-
.
|
|
3069
|
-
.
|
|
3070
|
-
.
|
|
3071
|
-
.
|
|
3072
|
-
.
|
|
3073
|
-
.
|
|
3235
|
+
.navl { color: var(--fg2); text-decoration: none; font-size: 14px; padding: 6px 12px; border-radius: 8px; }
|
|
3236
|
+
.navl:hover { background: var(--bg2); color: var(--fg); }
|
|
3237
|
+
.navl.back { background: var(--bg); border: 1px solid var(--line); color: var(--fg2); box-shadow: var(--shadow-sm); }
|
|
3238
|
+
.view { display: none; }
|
|
3239
|
+
.view.on { display: block; }
|
|
3240
|
+
.hero { min-height: 100vh; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 22px; padding: 80px 24px; text-align: center; background: radial-gradient(600px 300px at 50% 42%, var(--accent-soft), transparent 70%); }
|
|
3241
|
+
.bar { width: min(520px, 100%); height: 40px; border-radius: 10px; background: var(--bg); border: 1px dashed color-mix(in srgb, var(--accent) 28%, var(--line-strong)); display: flex; align-items: center; padding: 0 12px; gap: 8px; color: var(--fg3); font-size: 13px; }
|
|
3242
|
+
.bar.hot { border-color: var(--accent); background: var(--accent-soft); color: var(--accent); }
|
|
3243
|
+
.chip { background: var(--bg); border: 1px solid var(--line); border-radius: 7px; padding: 4px 10px; color: var(--fg); font-size: 13px; line-height: 1.2; }
|
|
3244
|
+
.copy { display: flex; flex-direction: column; align-items: center; gap: 8px; }
|
|
3245
|
+
.hero h1 { margin: 0; font-size: 30px; letter-spacing: -0.02em; font-weight: 700; line-height: 1.25; }
|
|
3246
|
+
.sub { margin: 0; color: var(--fg2); font-size: 15px; }
|
|
3247
|
+
.bm { display: inline-flex; align-items: center; gap: 8px; padding: 14px 26px; border-radius: 12px; background: var(--accent); color: var(--accent-fg); font-weight: 600; font-size: 16px; text-decoration: none; box-shadow: 0 10px 30px color-mix(in srgb, var(--accent) 25%, transparent), inset 0 1px 0 rgba(255, 255, 255, 0.25); cursor: grab; transition: transform .15s var(--ease); }
|
|
3248
|
+
.bm:hover { transform: translateY(-2px); }
|
|
3074
3249
|
.bm:active { cursor: grabbing; }
|
|
3075
|
-
.
|
|
3076
|
-
|
|
3077
|
-
.
|
|
3078
|
-
.
|
|
3079
|
-
|
|
3080
|
-
.
|
|
3081
|
-
.
|
|
3082
|
-
.
|
|
3083
|
-
.
|
|
3084
|
-
.
|
|
3085
|
-
|
|
3086
|
-
|
|
3087
|
-
.
|
|
3088
|
-
.
|
|
3089
|
-
.
|
|
3090
|
-
.
|
|
3091
|
-
.
|
|
3092
|
-
.
|
|
3093
|
-
.
|
|
3094
|
-
|
|
3095
|
-
.
|
|
3096
|
-
.
|
|
3097
|
-
.
|
|
3098
|
-
.
|
|
3099
|
-
|
|
3100
|
-
.note
|
|
3101
|
-
.
|
|
3102
|
-
.
|
|
3103
|
-
.
|
|
3104
|
-
.note.patch .num { background: var(--ok); color: #fff; border-radius: 4px; }
|
|
3105
|
-
.say { word-break: break-word; }
|
|
3106
|
-
.sel { margin-top: 2px; font: var(--fs-xs)/1.4 var(--mono); color: var(--fg3); word-break: break-word; }
|
|
3250
|
+
.help { position: fixed; bottom: 22px; left: 50%; transform: translateX(-50%); border: 0; background: none; font-size: 13px; color: var(--fg3); padding: 4px 8px; }
|
|
3251
|
+
.help:hover { color: var(--fg2); }
|
|
3252
|
+
.modal { position: fixed; z-index: 20; inset: 0; background: var(--mask); display: none; align-items: center; justify-content: center; padding: 24px; }
|
|
3253
|
+
.modal.on { display: flex; }
|
|
3254
|
+
.mc { background: var(--bg); color: var(--fg2); border-radius: 14px; padding: 22px 24px; width: min(440px, 100%); box-shadow: 0 20px 60px rgba(0, 0, 0, 0.13); font-size: 14px; line-height: 1.6; }
|
|
3255
|
+
.mc b { display: block; color: var(--fg); margin: 0 0 8px; font-size: 15px; }
|
|
3256
|
+
.mc p { margin: 0 0 6px; }
|
|
3257
|
+
.mc code { font-family: var(--mono); font-size: 12px; background: var(--code-bg); padding: 1px 5px; border-radius: 4px; }
|
|
3258
|
+
.notes { display: grid; grid-template-columns: 240px 1fr; height: 100vh; padding-top: 64px; }
|
|
3259
|
+
.side { border-right: 1px solid var(--line); padding: 12px; overflow: auto; }
|
|
3260
|
+
.site { width: 100%; display: flex; justify-content: space-between; align-items: center; gap: 8px; padding: 9px 12px; border: 0; border-radius: 8px; background: transparent; text-align: left; font-size: 14px; }
|
|
3261
|
+
.site:hover { background: var(--bg2); }
|
|
3262
|
+
.site.on { background: var(--accent-soft); color: var(--accent-hover); }
|
|
3263
|
+
.site .n { flex: none; color: var(--fg3); font-size: 12px; }
|
|
3264
|
+
.site.on .n { color: var(--accent); }
|
|
3265
|
+
.main { display: flex; flex-direction: column; min-width: 0; min-height: 0; padding: 8px 28px 24px; }
|
|
3266
|
+
.mh { display: flex; justify-content: space-between; align-items: center; gap: 12px; padding: 12px 0 4px; }
|
|
3267
|
+
.mh h2 { margin: 0; min-width: 0; font-size: 18px; font-weight: 700; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
3268
|
+
.actions { display: flex; gap: 8px; flex-wrap: wrap; justify-content: flex-end; flex: none; }
|
|
3269
|
+
.sb { padding: 7px 12px; border-radius: 8px; border: 1px solid var(--line); background: var(--bg); font-size: 13px; white-space: nowrap; }
|
|
3270
|
+
.sb:hover:not(:disabled) { background: var(--bg2); }
|
|
3271
|
+
.sb.p { background: var(--accent); color: var(--accent-fg); border-color: var(--accent); font-weight: 600; }
|
|
3272
|
+
.sb.p:hover:not(:disabled) { background: var(--accent-hover); }
|
|
3273
|
+
.sb.p.on, .sb.p.on:hover:not(:disabled) { background: var(--ok); border-color: var(--ok); color: #fff; }
|
|
3274
|
+
#list { flex: 1; overflow: auto; min-height: 0; }
|
|
3275
|
+
.note { display: flex; gap: 12px; align-items: flex-start; padding: 14px 0; border-bottom: 1px solid var(--line2); }
|
|
3276
|
+
.pin { flex: none; min-width: 24px; height: 24px; padding: 0 6px; box-sizing: border-box; border-radius: 12px; background: var(--accent); color: var(--accent-fg); font-size: 12px; font-weight: 600; display: flex; align-items: center; justify-content: center; }
|
|
3277
|
+
.say { font-size: 14px; word-break: break-word; }
|
|
3278
|
+
.q { margin-top: 3px; color: var(--fg3); font-size: 12px; line-height: 1.4; word-break: break-word; }
|
|
3107
3279
|
.empty { color: var(--fg2); font-size: var(--fs-sm); padding: 12px 6px; }
|
|
3108
3280
|
@media (max-width: 800px) {
|
|
3109
|
-
.
|
|
3110
|
-
|
|
3111
|
-
|
|
3281
|
+
.top { padding: 14px 16px; }
|
|
3282
|
+
.hero { padding: 88px 16px 80px; }
|
|
3283
|
+
.hero h1 { font-size: 26px; }
|
|
3284
|
+
.notes { grid-template-columns: 1fr; height: auto; min-height: 100vh; }
|
|
3285
|
+
.side { max-height: 40vh; border-right: 0; border-bottom: 1px solid var(--line); }
|
|
3286
|
+
.main { padding: 8px 16px 24px; }
|
|
3287
|
+
.mh { align-items: flex-start; flex-wrap: wrap; }
|
|
3288
|
+
}
|
|
3289
|
+
@media (prefers-reduced-motion: reduce) {
|
|
3290
|
+
* { transition: none !important; }
|
|
3291
|
+
.bm:hover { transform: none; }
|
|
3112
3292
|
}
|
|
3113
|
-
@media (prefers-reduced-motion: reduce) { * { transition: none !important; } }
|
|
3114
3293
|
`;
|
|
3115
3294
|
var HOME_JS = `
|
|
3116
3295
|
const boot = JSON.parse(document.getElementById('ph-boot').textContent)
|
|
3117
|
-
const
|
|
3118
|
-
const
|
|
3119
|
-
|
|
3120
|
-
|
|
3121
|
-
|
|
3122
|
-
src: '\u6765\u6E90',
|
|
3123
|
-
local: '\u672C\u673A ' + location.host,
|
|
3124
|
-
cdn: 'jsDelivr CDN',
|
|
3125
|
-
by: '\u6309\u7F51\u7AD9',
|
|
3126
|
-
pick: '\u9009\u62E9\u4E00\u4E2A\u7F51\u7AD9',
|
|
3127
|
-
notes: '\u6761',
|
|
3128
|
-
empty: '\u8FD9\u4E2A\u7F51\u7AD9\u8FD8\u6CA1\u6709\u610F\u89C1\u3002',
|
|
3129
|
-
none: '\u8FD8\u6CA1\u6709\u610F\u89C1\u3002\u5728\u7F51\u9875\u4E0A\u70B9\u4E66\u7B7E\uFF0C\u53D6\u70B9\u540E\u5199\u4E00\u6761\u3002',
|
|
3130
|
-
md: '\u5BFC\u51FA Markdown',
|
|
3131
|
-
json: '\u5BFC\u51FA JSON',
|
|
3132
|
-
copy: '\u590D\u5236 Prompt',
|
|
3133
|
-
copied: '\u5DF2\u590D\u5236',
|
|
3134
|
-
bm: 'PH \u8BC4\u5BA1',
|
|
3135
|
-
limit: '\u672C\u673A\u811A\u672C\u6765\u81EA ' + location.origin + '\uFF0C\u9002\u5408 http \u9875\u9762\u3002https \u9875\u9762\u7ECF\u5E38\u4F1A\u62E6\u622A http://127.0.0.1 \u7684\u811A\u672C\uFF0C\u8FD9\u65F6\u6539\u7528 jsDelivr\uFF08\u811A\u672C\u8D70 https\uFF09\u3002\u610F\u89C1\u4ECD\u8981\u5199\u56DE\u672C\u673A\u7684 http \u63A7\u5236\u7AEF\u53E3\uFF1AChrome \u901A\u5E38\u5141\u8BB8\u8BBF\u95EE 127.0.0.1\uFF0C\u5E76\u53EF\u80FD\u5F39\u51FA\u672C\u5730\u7F51\u7EDC\u6743\u9650\uFF08\u670D\u52A1\u4F1A\u56DE\u5E94 Access-Control-Allow-Private-Network\uFF09\uFF1BFirefox \u548C Safari \u591A\u534A\u4F1A\u5F53\u6210\u6DF7\u5408\u5185\u5BB9\u62E6\u6389\u3002\u88AB\u62E6\u4F4F\u65F6\uFF0C\u7528 page-harness <\u7F51\u5740> --no-agent \u8D70\u4EE3\u7406\u3002\u4E66\u7B7E\u91CC\u6709\u672C\u6B21\u8FD0\u884C\u7684\u4EE4\u724C\uFF0C\u53EA\u5728\u4F60\u70B9\u8FC7\u4E66\u7B7E\u7684\u9875\u9762\u91CC\u6709\u6548\u3002\u9875\u9762\u7684 CSP \u5982\u679C\u7981\u6B62\u8FDE\u63A5 127.0.0.1\uFF0C\u4FDD\u5B58\u4E5F\u4F1A\u5931\u8D25\u3002',
|
|
3136
|
-
ungrouped: '\uFF08\u672A\u5206\u7EC4\uFF09',
|
|
3137
|
-
} : {
|
|
3138
|
-
tag: 'Review only \xB7 no agent',
|
|
3139
|
-
step: 'Drag the button to the bookmarks bar',
|
|
3140
|
-
lead: 'Click it on any site. The dock is Pick and Notes only.',
|
|
3141
|
-
src: 'Source',
|
|
3142
|
-
local: 'This server ' + location.host,
|
|
3143
|
-
cdn: 'jsDelivr CDN',
|
|
3144
|
-
by: 'By site',
|
|
3145
|
-
pick: 'Pick a site',
|
|
3146
|
-
notes: 'notes',
|
|
3147
|
-
empty: 'No notes for this site yet.',
|
|
3148
|
-
none: 'No notes yet. Open a page, click the bookmark, pick an element and comment.',
|
|
3149
|
-
md: 'Export Markdown',
|
|
3150
|
-
json: 'Export JSON',
|
|
3151
|
-
copy: 'Copy prompt',
|
|
3152
|
-
copied: 'Copied',
|
|
3153
|
-
bm: 'PH Review',
|
|
3154
|
-
limit: 'The local script is served from ' + location.origin + ' and works on http pages. HTTPS pages often block a script from http://127.0.0.1 (mixed content); switch the source to jsDelivr so the script itself loads over HTTPS. Saving a note still calls this http control port. Chrome usually allows 127.0.0.1 and may ask for local-network permission (the server answers Access-Control-Allow-Private-Network). Firefox and Safari often block it as mixed content. When that happens, run page-harness <url> --no-agent and use the proxy. The bookmarklet carries this run\\'s token and only works on pages where you click it. A host page CSP that forbids connections to 127.0.0.1 will also block saving.',
|
|
3155
|
-
ungrouped: '(ungrouped)',
|
|
3156
|
-
}
|
|
3157
|
-
document.documentElement.lang = zh ? 'zh-CN' : 'en'
|
|
3296
|
+
const copy = ${JSON.stringify(HOME_COPY)}
|
|
3297
|
+
const lang = boot.lang === 'zh-CN' || boot.lang === 'en' ? boot.lang : ((navigator.language || '').toLowerCase().startsWith('zh') ? 'zh-CN' : 'en')
|
|
3298
|
+
const t = copy[lang]
|
|
3299
|
+
const BM = '\u2726 Page-Harness'
|
|
3300
|
+
document.documentElement.lang = lang
|
|
3158
3301
|
document.title = 'page-harness'
|
|
3159
|
-
|
|
3160
|
-
|
|
3161
|
-
|
|
3162
|
-
document.getElementById('
|
|
3163
|
-
|
|
3164
|
-
|
|
3165
|
-
|
|
3166
|
-
|
|
3167
|
-
|
|
3168
|
-
|
|
3169
|
-
|
|
3170
|
-
|
|
3302
|
+
const bm = document.getElementById('bm')
|
|
3303
|
+
const bar = document.getElementById('bar')
|
|
3304
|
+
const home = document.getElementById('home')
|
|
3305
|
+
const notes = document.getElementById('notes')
|
|
3306
|
+
const tonotes = document.getElementById('tonotes')
|
|
3307
|
+
const doms = document.getElementById('doms')
|
|
3308
|
+
const list = document.getElementById('list')
|
|
3309
|
+
const hostTitle = document.getElementById('host-title')
|
|
3310
|
+
const exmd = document.getElementById('exmd')
|
|
3311
|
+
const exjson = document.getElementById('exjson')
|
|
3312
|
+
const cp = document.getElementById('cp')
|
|
3313
|
+
const modal = document.getElementById('modal')
|
|
3314
|
+
const helpTitle = document.getElementById('help-title')
|
|
3315
|
+
const helpBody = document.getElementById('help-body')
|
|
3171
3316
|
|
|
3172
|
-
function
|
|
3173
|
-
|
|
3174
|
-
|
|
3317
|
+
function fillHelp() {
|
|
3318
|
+
helpTitle.textContent = t.helpTitle
|
|
3319
|
+
helpBody.replaceChildren()
|
|
3320
|
+
for (const line of t.help) {
|
|
3321
|
+
const p = document.createElement('p')
|
|
3322
|
+
p.append('\xB7 ')
|
|
3323
|
+
for (const bit of line) {
|
|
3324
|
+
if (bit.k === 'c') {
|
|
3325
|
+
const code = document.createElement('code')
|
|
3326
|
+
code.textContent = bit.v
|
|
3327
|
+
p.append(code)
|
|
3328
|
+
} else p.append(bit.v)
|
|
3329
|
+
}
|
|
3330
|
+
helpBody.append(p)
|
|
3331
|
+
}
|
|
3332
|
+
}
|
|
3333
|
+
function applyCopy() {
|
|
3334
|
+
document.getElementById('hero-title').textContent = t.title
|
|
3335
|
+
document.getElementById('hero-sub').textContent = t.sub
|
|
3336
|
+
document.getElementById('bar-label').textContent = t.bar
|
|
3337
|
+
document.getElementById('chip-docs').textContent = t.docs
|
|
3338
|
+
document.getElementById('chip-gh').textContent = t.gh
|
|
3339
|
+
document.getElementById('help').textContent = t.trouble
|
|
3340
|
+
bm.textContent = BM
|
|
3341
|
+
exmd.textContent = t.md
|
|
3342
|
+
exjson.textContent = t.json
|
|
3343
|
+
cp.textContent = t.copy
|
|
3344
|
+
fillHelp()
|
|
3345
|
+
}
|
|
3346
|
+
function bookmarklet() {
|
|
3347
|
+
const cfgObj = { controlOrigin: location.origin, bookmarklet: true, noAgent: true, token: boot.token }
|
|
3348
|
+
if (boot.lang) cfgObj.lang = boot.lang
|
|
3349
|
+
const cfg = JSON.stringify(cfgObj).replace(/</g, '\\\\u003c')
|
|
3350
|
+
const srcUrl = JSON.stringify(location.origin + '/__ph/overlay.js').replace(/</g, '\\\\u003c')
|
|
3175
3351
|
return 'javascript:(function(){if(window.__phOverlay)return;window.__PH_CONFIG=' + cfg + ';var s=document.createElement(\\'script\\');s.src=' + srcUrl + ';document.documentElement.appendChild(s);})();'
|
|
3176
3352
|
}
|
|
3177
|
-
|
|
3178
|
-
|
|
3179
|
-
return location.origin + '/__ph/overlay.js'
|
|
3180
|
-
}
|
|
3181
|
-
function paintBm() { bm.href = bookmarklet(scriptUrl()) }
|
|
3182
|
-
src.addEventListener('change', paintBm)
|
|
3183
|
-
paintBm()
|
|
3353
|
+
bm.href = bookmarklet()
|
|
3354
|
+
applyCopy()
|
|
3184
3355
|
|
|
3185
3356
|
let sites = []
|
|
3186
3357
|
let cur = ''
|
|
3187
|
-
|
|
3188
|
-
|
|
3358
|
+
let total = 0
|
|
3359
|
+
let ready = false
|
|
3189
3360
|
function label(host) { return host || t.ungrouped }
|
|
3361
|
+
function notesOpen() { return location.hash === '#notes' }
|
|
3362
|
+
function applyView() {
|
|
3363
|
+
const on = notesOpen()
|
|
3364
|
+
home.classList.toggle('on', !on)
|
|
3365
|
+
notes.classList.toggle('on', on)
|
|
3366
|
+
tonotes.textContent = on ? t.back : t.mine.replace('{n}', String(total))
|
|
3367
|
+
tonotes.href = on ? '#' : '#notes'
|
|
3368
|
+
tonotes.classList.toggle('back', on)
|
|
3369
|
+
}
|
|
3370
|
+
function setExport(on) { exmd.disabled = exjson.disabled = cp.disabled = !on }
|
|
3190
3371
|
async function loadSites() {
|
|
3191
3372
|
const r = await fetch('/__ph/api/review/sites', { headers: { Accept: 'application/json' } })
|
|
3192
3373
|
const data = await r.json()
|
|
3193
3374
|
sites = data.sites || []
|
|
3194
|
-
|
|
3375
|
+
total = sites.reduce((n, s) => n + (Number(s.total) || 0), 0)
|
|
3376
|
+
if (!sites.some((s) => s.host === cur)) cur = sites[0] ? sites[0].host : ''
|
|
3377
|
+
ready = true
|
|
3378
|
+
applyView()
|
|
3195
3379
|
renderSites()
|
|
3196
|
-
await renderNotes()
|
|
3380
|
+
if (notesOpen()) await renderNotes()
|
|
3197
3381
|
}
|
|
3198
3382
|
function renderSites() {
|
|
3199
3383
|
doms.replaceChildren()
|
|
@@ -3205,56 +3389,69 @@ function renderSites() {
|
|
|
3205
3389
|
return
|
|
3206
3390
|
}
|
|
3207
3391
|
for (const s of sites) {
|
|
3208
|
-
const e = document.createElement('
|
|
3209
|
-
e.
|
|
3392
|
+
const e = document.createElement('button')
|
|
3393
|
+
e.type = 'button'
|
|
3394
|
+
e.className = 'site' + (s.host === cur ? ' on' : '')
|
|
3210
3395
|
e.dataset.testid = 'domain'
|
|
3211
3396
|
e.dataset.host = s.host
|
|
3212
3397
|
const name = document.createElement('span')
|
|
3213
3398
|
name.textContent = label(s.host)
|
|
3214
3399
|
const n = document.createElement('span')
|
|
3215
3400
|
n.className = 'n'
|
|
3216
|
-
n.textContent = s.total
|
|
3401
|
+
n.textContent = String(s.total)
|
|
3217
3402
|
e.append(name, n)
|
|
3218
|
-
e.addEventListener('click', () => { cur = s.host;
|
|
3403
|
+
e.addEventListener('click', () => { cur = s.host; renderSites(); void renderNotes() })
|
|
3219
3404
|
doms.append(e)
|
|
3220
3405
|
}
|
|
3221
3406
|
}
|
|
3222
3407
|
async function renderNotes() {
|
|
3223
|
-
|
|
3224
|
-
|
|
3225
|
-
|
|
3226
|
-
|
|
3408
|
+
const has = sites.some((s) => s.host === cur)
|
|
3409
|
+
hostTitle.textContent = has ? label(cur) : ''
|
|
3410
|
+
setExport(has)
|
|
3411
|
+
list.replaceChildren()
|
|
3412
|
+
if (!has) {
|
|
3413
|
+
const e = document.createElement('div')
|
|
3414
|
+
e.className = 'empty'
|
|
3415
|
+
e.textContent = t.none
|
|
3416
|
+
list.append(e)
|
|
3417
|
+
return
|
|
3418
|
+
}
|
|
3227
3419
|
const r = await fetch('/__ph/api/review/host?host=' + encodeURIComponent(cur), { headers: { Accept: 'application/json' } })
|
|
3228
3420
|
const data = await r.json()
|
|
3229
3421
|
list.replaceChildren()
|
|
3230
3422
|
const items = data.items || []
|
|
3231
|
-
if (!items.length) {
|
|
3423
|
+
if (!items.length) {
|
|
3424
|
+
const e = document.createElement('div')
|
|
3425
|
+
e.className = 'empty'
|
|
3426
|
+
e.textContent = t.empty
|
|
3427
|
+
list.append(e)
|
|
3428
|
+
return
|
|
3429
|
+
}
|
|
3232
3430
|
for (const item of items) {
|
|
3233
3431
|
const e = document.createElement('div')
|
|
3234
|
-
|
|
3235
|
-
e.className = 'note ' + kind + (item.status === 'resolved' ? ' done' : '')
|
|
3432
|
+
e.className = 'note'
|
|
3236
3433
|
e.dataset.testid = 'note'
|
|
3237
|
-
const
|
|
3238
|
-
|
|
3239
|
-
|
|
3434
|
+
const pin = document.createElement('span')
|
|
3435
|
+
pin.className = 'pin'
|
|
3436
|
+
pin.textContent = String(item.n || '')
|
|
3240
3437
|
const col = document.createElement('div')
|
|
3241
3438
|
const body = document.createElement('div')
|
|
3242
3439
|
body.className = 'say'
|
|
3243
|
-
body.textContent = item.note ||
|
|
3440
|
+
body.textContent = item.note || ''
|
|
3244
3441
|
const meta = document.createElement('div')
|
|
3245
|
-
meta.className = '
|
|
3246
|
-
const
|
|
3247
|
-
meta.textContent =
|
|
3442
|
+
meta.className = 'q'
|
|
3443
|
+
const quote = item.text ? '\u201C' + item.text + '\u201D' : (item.selector || '')
|
|
3444
|
+
meta.textContent = [quote, item.path].filter(Boolean).join(' \xB7 ')
|
|
3445
|
+
if (item.selector) meta.title = item.selector
|
|
3248
3446
|
col.append(body, meta)
|
|
3249
|
-
e.append(
|
|
3447
|
+
e.append(pin, col)
|
|
3250
3448
|
list.append(e)
|
|
3251
3449
|
}
|
|
3252
|
-
void on
|
|
3253
3450
|
}
|
|
3254
3451
|
function download(kind) {
|
|
3255
|
-
if (!
|
|
3452
|
+
if (!sites.some((s) => s.host === cur)) return
|
|
3256
3453
|
const a = document.createElement('a')
|
|
3257
|
-
a.href = '/__ph/api/review/export.' + kind + '?host=' + encodeURIComponent(cur)
|
|
3454
|
+
a.href = '/__ph/api/review/export.' + kind + '?host=' + encodeURIComponent(cur) + '&lang=' + encodeURIComponent(lang)
|
|
3258
3455
|
a.download = ''
|
|
3259
3456
|
a.click()
|
|
3260
3457
|
}
|
|
@@ -3278,14 +3475,21 @@ async function writeClipboard(text) {
|
|
|
3278
3475
|
}
|
|
3279
3476
|
cp.addEventListener('click', async () => {
|
|
3280
3477
|
if (cp.disabled) return
|
|
3281
|
-
const
|
|
3282
|
-
const r = await fetch('/__ph/api/review/prompt?host=' + encodeURIComponent(cur) + '&lang=' + lang, { headers: { Accept: 'text/plain' } })
|
|
3478
|
+
const r = await fetch('/__ph/api/review/prompt?host=' + encodeURIComponent(cur) + '&lang=' + encodeURIComponent(lang), { headers: { Accept: 'text/plain' } })
|
|
3283
3479
|
const text = await r.text()
|
|
3284
3480
|
if (!(await writeClipboard(text))) return
|
|
3285
3481
|
cp.textContent = t.copied
|
|
3286
3482
|
cp.classList.add('on')
|
|
3287
3483
|
setTimeout(() => { cp.textContent = t.copy; cp.classList.remove('on') }, 1500)
|
|
3288
3484
|
})
|
|
3485
|
+
bm.addEventListener('dragstart', () => bar.classList.add('hot'))
|
|
3486
|
+
bm.addEventListener('dragend', () => bar.classList.remove('hot'))
|
|
3487
|
+
document.getElementById('help').addEventListener('click', () => modal.classList.add('on'))
|
|
3488
|
+
modal.addEventListener('click', () => modal.classList.remove('on'))
|
|
3489
|
+
document.getElementById('modal-card').addEventListener('click', (e) => e.stopPropagation())
|
|
3490
|
+
document.addEventListener('keydown', (e) => { if (e.key === 'Escape') modal.classList.remove('on') })
|
|
3491
|
+
window.addEventListener('hashchange', () => { applyView(); if (notesOpen() && ready) void renderNotes() })
|
|
3492
|
+
applyView()
|
|
3289
3493
|
void loadSites()
|
|
3290
3494
|
`;
|
|
3291
3495
|
|
|
@@ -4704,7 +4908,7 @@ async function startServers(o) {
|
|
|
4704
4908
|
if (!existsSync9(p)) return send(res, 503, `/* page-harness: ${file} missing, run npm run build */`, "text/javascript");
|
|
4705
4909
|
let body = readFileSync9(p, "utf8");
|
|
4706
4910
|
if (file === "overlay.js") {
|
|
4707
|
-
body = `window.__PH_CONFIG=${JSON.stringify({ controlOrigin: controlOrigin(req), appPort: o.appPort, target: o.target.host, relay: proxy.site, ...o.noAgent ? { noAgent: true } : {} })};
|
|
4911
|
+
body = `window.__PH_CONFIG=${JSON.stringify({ controlOrigin: controlOrigin(req), appPort: o.appPort, target: o.target.host, relay: proxy.site, ...o.noAgent ? { noAgent: true } : {}, ...o.lang ? { lang: o.lang } : {} })};
|
|
4708
4912
|
` + body;
|
|
4709
4913
|
}
|
|
4710
4914
|
return send(res, 200, body, "text/javascript; charset=utf-8", { "Cross-Origin-Resource-Policy": "same-origin" });
|
|
@@ -4765,7 +4969,7 @@ async function startServers(o) {
|
|
|
4765
4969
|
}
|
|
4766
4970
|
if (o.bookmarklet && (p === "/" || p === "/__ph" || p === "/__ph/" || p === "/__ph/home") && req.method === "GET") {
|
|
4767
4971
|
o.auth.issuePageSession(req, res);
|
|
4768
|
-
return send(res, 200, homeHtml({ token: o.bookmarkletToken ?? "", version: packageVersion() }), "text/html; charset=utf-8", {
|
|
4972
|
+
return send(res, 200, homeHtml({ token: o.bookmarkletToken ?? "", version: packageVersion(), lang: o.lang }), "text/html; charset=utf-8", {
|
|
4769
4973
|
"Content-Security-Policy": "default-src 'none'; script-src 'self'; style-src 'self'; font-src 'self'; connect-src 'self'; base-uri 'none'; form-action 'none'; frame-ancestors 'none'",
|
|
4770
4974
|
"Referrer-Policy": "no-referrer"
|
|
4771
4975
|
});
|
|
@@ -5010,8 +5214,9 @@ async function serveReviewExtras(o, req, res, url) {
|
|
|
5010
5214
|
const slice = o.gateway.reviewSlice(host);
|
|
5011
5215
|
if (p === "/__ph/api/review/host") return json(res, { host, items: slice.items.map(slimItem) }), true;
|
|
5012
5216
|
const fileHost = (host || "ungrouped").replace(/[^\w.-]+/g, "_");
|
|
5217
|
+
const lang = url.searchParams.get("lang") === "en" ? "en" : "zh-CN";
|
|
5013
5218
|
if (p === "/__ph/api/review/export.md") {
|
|
5014
|
-
const md = reviewMarkdown(siteUrl(host), slice.items, slice.approvals);
|
|
5219
|
+
const md = reviewMarkdown(siteUrl(host), slice.items, slice.approvals, lang);
|
|
5015
5220
|
return send(res, 200, md, "text/markdown; charset=utf-8", { "Content-Disposition": `attachment; filename="review-${fileHost}.md"` }), true;
|
|
5016
5221
|
}
|
|
5017
5222
|
if (p === "/__ph/api/review/export.json") {
|
|
@@ -5019,7 +5224,6 @@ async function serveReviewExtras(o, req, res, url) {
|
|
|
5019
5224
|
"Content-Disposition": `attachment; filename="review-${fileHost}.json"`
|
|
5020
5225
|
}), true;
|
|
5021
5226
|
}
|
|
5022
|
-
const lang = url.searchParams.get("lang") === "en" ? "en" : "zh-CN";
|
|
5023
5227
|
const page = siteUrl(host);
|
|
5024
5228
|
const text = reviewPrompt(slice.items, {
|
|
5025
5229
|
site: page,
|
|
@@ -5337,18 +5541,18 @@ function turnText(turn) {
|
|
|
5337
5541
|
|
|
5338
5542
|
// src/server/prompt.ts
|
|
5339
5543
|
import path14 from "path";
|
|
5340
|
-
var
|
|
5544
|
+
var esc2 = (s) => s.replace(/</g, "<");
|
|
5341
5545
|
function buildPrompt(msg, opts) {
|
|
5342
5546
|
const parts = [];
|
|
5343
5547
|
const images = [];
|
|
5344
|
-
if (msg.pageUrl) parts.push(`<page url="${
|
|
5548
|
+
if (msg.pageUrl) parts.push(`<page url="${esc2(msg.pageUrl)}" />`);
|
|
5345
5549
|
if (msg.picks.length) {
|
|
5346
5550
|
const lines = msg.picks.map((p, i) => {
|
|
5347
5551
|
const html = truncate(p.outerHTML.replace(/\s+/g, " "), 1200);
|
|
5348
5552
|
return `${i + 1}. <${p.tagName.toLowerCase()}> selector: ${p.selector}${p.text ? `
|
|
5349
5553
|
text: ${JSON.stringify(truncate(p.text, 200))}` : ""}${p.url && p.url !== msg.pageUrl ? `
|
|
5350
5554
|
page: ${p.url}` : ""}
|
|
5351
|
-
html: ${
|
|
5555
|
+
html: ${esc2(html)}`;
|
|
5352
5556
|
});
|
|
5353
5557
|
parts.push(`<picked-elements>
|
|
5354
5558
|
${lines.join("\n")}
|
|
@@ -6426,7 +6630,8 @@ async function startHarness(o) {
|
|
|
6426
6630
|
keepHost: o.keepHost,
|
|
6427
6631
|
noAgent,
|
|
6428
6632
|
bookmarklet,
|
|
6429
|
-
bookmarkletToken
|
|
6633
|
+
bookmarkletToken,
|
|
6634
|
+
lang: o.lang
|
|
6430
6635
|
});
|
|
6431
6636
|
let closed = false;
|
|
6432
6637
|
return {
|
|
@@ -6463,6 +6668,15 @@ function lanAddress() {
|
|
|
6463
6668
|
return void 0;
|
|
6464
6669
|
}
|
|
6465
6670
|
|
|
6671
|
+
// src/shared/lang.ts
|
|
6672
|
+
function parseLang(raw) {
|
|
6673
|
+
if (raw === void 0 || raw.trim() === "") return void 0;
|
|
6674
|
+
const s = raw.trim().toLowerCase();
|
|
6675
|
+
if (s === "en" || s.startsWith("en-")) return "en";
|
|
6676
|
+
if (s === "zh" || s.startsWith("zh-")) return "zh-CN";
|
|
6677
|
+
throw new Error("--lang must be zh or en");
|
|
6678
|
+
}
|
|
6679
|
+
|
|
6466
6680
|
// src/cli/index.ts
|
|
6467
6681
|
var HELP = `page-harness \u2014 Pick, Chat and Artifacts on top of any running frontend, driven by your local coding agent.
|
|
6468
6682
|
Without a URL it starts review-only: a home page with a bookmarklet, and notes grouped by site. No agent.
|
|
@@ -6495,6 +6709,7 @@ Options
|
|
|
6495
6709
|
--no-agent Review only. Do not detect or start an agent CLI. The dock is Pick and Notes
|
|
6496
6710
|
(comment, edit, reply, resolve, approve). No Chat, Artifacts, Live or page control.
|
|
6497
6711
|
Implied when no frontend URL is given.
|
|
6712
|
+
--lang <zh|en> Home page and dock language. Omit it and each browser keeps its own.
|
|
6498
6713
|
--share Listen on the network and print an invite link: teammates join as reviewers
|
|
6499
6714
|
(comment, reply, resolve, approve, edit text/style in review mode; never the agent)
|
|
6500
6715
|
--verbose / --quiet
|
|
@@ -6579,6 +6794,7 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
6579
6794
|
review: { type: "boolean" },
|
|
6580
6795
|
source: { type: "boolean" },
|
|
6581
6796
|
"no-agent": { type: "boolean" },
|
|
6797
|
+
lang: { type: "string" },
|
|
6582
6798
|
share: { type: "boolean" },
|
|
6583
6799
|
verbose: { type: "boolean" },
|
|
6584
6800
|
quiet: { type: "boolean" },
|
|
@@ -6596,6 +6812,7 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
6596
6812
|
}
|
|
6597
6813
|
if (values.verbose) log.setVerbose(true);
|
|
6598
6814
|
if (values.quiet) log.setQuiet(true);
|
|
6815
|
+
const lang = parseLang(values.lang);
|
|
6599
6816
|
const rawTarget = positionals[0];
|
|
6600
6817
|
if (!rawTarget) {
|
|
6601
6818
|
if (values.source) throw new Error("--source needs a frontend URL");
|
|
@@ -6612,7 +6829,8 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
6612
6829
|
controlPort: controlPort2,
|
|
6613
6830
|
allowHosts: values["allow-host"],
|
|
6614
6831
|
publicScheme: schemeOf(values["public-scheme"]),
|
|
6615
|
-
noAgent: true
|
|
6832
|
+
noAgent: true,
|
|
6833
|
+
lang
|
|
6616
6834
|
});
|
|
6617
6835
|
log.info(`v${version()}`);
|
|
6618
6836
|
process.stderr.write(
|
|
@@ -6657,7 +6875,8 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
6657
6875
|
liveGuardHook: values["live-guard-hook"],
|
|
6658
6876
|
mode: values.review ? "review" : values.source ? "source" : void 0,
|
|
6659
6877
|
share: values.share,
|
|
6660
|
-
noAgent
|
|
6878
|
+
noAgent,
|
|
6879
|
+
lang
|
|
6661
6880
|
});
|
|
6662
6881
|
const agentLine = h.agents.map((a) => a.available ? `${a.id === h.defaultAgent ? log.bold(a.id) : a.id}${a.version ? ` ${log.dim(a.version)}` : ""}` : log.dim(`${a.id} (not found)`)).join(" ");
|
|
6663
6882
|
log.info(`v${version()}`);
|