Files
CursorInitGeneral/.cursor/skills/html-doc/assets/annotate.js

233 lines
11 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* ============================================================
html-doc · 批注模块annotate.js
让读者在文档里逐字段/逐区块填备注,自动暂存到 localStorage
一键导出「把备注嵌进 JSON 的 HTML 副本」发回,对方打开即可看到全部批注。
用法(生成单文件文档时,把本文件全部内联进 <script>
1) 表格模式:给 <table> 加 data-annotate="唯一名",会为每行追加一列「备注」。
- 可选:给某个 <tr> 加 data-note-id="稳定key" 覆盖自动 key防行序漂移
2) 区块模式:给任意元素加 data-note-id="稳定key",其后会插入一个备注框。
3) 命名空间:给 <body> 加 data-note-ns="文档唯一标识",隔离不同文档的暂存。
纯前端、零依赖、幂等(重复打开导出的副本不会重复注入)。
============================================================ */
(function () {
"use strict";
var NS = (document.body && document.body.dataset.noteNs) || document.title || "html-doc";
var STORAGE_PREFIX = "htmldoc-note::" + NS + "::";
var NOTE_COL_LABEL = "备注 / 批注";
// 1. 读取上一份文件嵌入的备注(对方发回来的文件里带着)
var embedded = {};
var embEl = document.getElementById("embedded-notes");
if (embEl) {
try { embedded = JSON.parse(embEl.textContent || "{}"); } catch (e) { embedded = {}; }
}
// 2. 注入自带样式
var style = document.createElement("style");
style.id = "hd-note-style";
style.textContent = [
".hd-note-toolbar{position:sticky;top:0;z-index:30;display:flex;align-items:center;gap:10px;flex-wrap:wrap;",
"background:var(--panel,#171a21);border:1px solid var(--border,#2a2f3a);border-radius:10px;padding:12px 16px;",
"margin:0 0 24px;box-shadow:0 6px 18px rgba(0,0,0,.35)}",
".hd-note-toolbar .nt-title{font-weight:600;font-size:14px}",
".hd-note-toolbar .nt-count{color:var(--accent-2,#7ee0a2);font-size:12.5px}",
".hd-note-toolbar .nt-hint{color:var(--text-dim,#9aa1ad);font-size:12.5px;flex-basis:100%;margin-top:2px}",
".hd-note-btn{background:var(--panel-2,#1e222b);color:var(--text,#d7dbe2);border:1px solid var(--border,#2a2f3a);",
"border-radius:7px;padding:6px 12px;font-size:13px;cursor:pointer;font-family:inherit}",
".hd-note-btn:hover{border-color:var(--accent,#5aa9ff);color:var(--accent,#5aa9ff)}",
".hd-note-btn.primary{background:rgba(90,169,255,.14);border-color:rgba(90,169,255,.5);color:var(--accent,#5aa9ff)}",
".hd-note-btn.danger:hover{border-color:var(--danger,#ff6b6b);color:var(--danger,#ff6b6b)}",
"th.hd-note-col,td.hd-note-cell{min-width:200px}",
"textarea.hd-note{width:100%;background:var(--code-bg,#11141a);color:var(--text,#d7dbe2);",
"border:1px solid var(--border,#2a2f3a);border-radius:6px;padding:6px 8px;font-family:inherit;font-size:13px;",
"line-height:1.5;resize:vertical;min-height:38px}",
"textarea.hd-note:focus{outline:none;border-color:var(--accent,#5aa9ff)}",
"textarea.hd-note.filled{border-color:rgba(126,224,162,.5);background:rgba(126,224,162,.06)}",
".hd-note-block{margin:8px 0 18px}",
".hd-note-block .lbl{font-size:12px;color:var(--text-dim,#9aa1ad);margin-bottom:4px}"
].join("");
document.head.appendChild(style);
var entries = []; // {key, label, group, ta}
function restore(key) {
if (Object.prototype.hasOwnProperty.call(embedded, key)) return embedded[key]; // 收到的文件优先显示对方批注
var ls = localStorage.getItem(STORAGE_PREFIX + key);
return ls !== null ? ls : "";
}
function makeTextarea(key, label, group) {
var ta = document.createElement("textarea");
ta.className = "hd-note";
ta.rows = 2;
ta.placeholder = "在此填写备注…";
ta.dataset.key = key;
var val = restore(key);
ta.value = val;
if (val.trim()) ta.classList.add("filled");
ta.addEventListener("input", function () {
localStorage.setItem(STORAGE_PREFIX + key, ta.value);
ta.classList.toggle("filled", ta.value.trim() !== "");
updateCount();
});
entries.push({ key: key, label: label, group: group, ta: ta });
return ta;
}
// 3a. 表格模式table[data-annotate]
var annTables = document.querySelectorAll("table[data-annotate]");
Array.prototype.forEach.call(annTables, function (table, ti) {
var tableKey = table.getAttribute("data-annotate") || ("table" + ti);
var section = table.closest("section");
var h2 = section ? section.querySelector("h2.section, h2, h3") : null;
var group = h2 ? h2.textContent.trim() : tableKey;
var headRow = table.querySelector("thead tr");
if (headRow) {
var th = document.createElement("th");
th.className = "hd-note-col";
th.textContent = NOTE_COL_LABEL;
headRow.appendChild(th);
}
var rows = table.querySelectorAll("tbody tr");
Array.prototype.forEach.call(rows, function (tr, idx) {
var firstCell = tr.querySelector("td");
var label = firstCell ? firstCell.textContent.trim() : ("行" + idx);
var key = tr.getAttribute("data-note-id") || (tableKey + "::" + idx + "::" + label);
var td = document.createElement("td");
td.className = "hd-note-cell";
td.appendChild(makeTextarea(key, label, group));
tr.appendChild(td);
});
});
// 3b. 区块模式:任意 [data-note-id](表格行已在上面处理,这里排除)
var blocks = document.querySelectorAll("[data-note-id]");
Array.prototype.forEach.call(blocks, function (el) {
if (el.tagName === "TR" || el.closest("table[data-annotate]")) return;
var key = el.getAttribute("data-note-id");
var section = el.closest("section");
var h2 = section ? section.querySelector("h2.section, h2, h3") : null;
var group = h2 ? h2.textContent.trim() : "区块批注";
var label = (el.textContent || "").trim().slice(0, 40) || key;
var wrap = document.createElement("div");
wrap.className = "hd-note-block";
var lbl = document.createElement("div");
lbl.className = "lbl";
lbl.textContent = NOTE_COL_LABEL;
wrap.appendChild(lbl);
wrap.appendChild(makeTextarea(key, label, group));
if (el.nextSibling) el.parentNode.insertBefore(wrap, el.nextSibling);
else el.parentNode.appendChild(wrap);
});
if (!entries.length) return; // 没有任何批注目标就不加工具栏
// 4. 工具栏
var bar = document.createElement("div");
bar.className = "hd-note-toolbar";
bar.innerHTML =
'<span class="nt-title">批注模式</span>' +
'<span class="nt-count" id="hdNoteCount">已填 0 条</span>' +
'<button class="hd-note-btn primary" id="hdBtnExport">导出带备注的副本 (.html)</button>' +
'<button class="hd-note-btn" id="hdBtnMd">复制备注为 Markdown</button>' +
'<button class="hd-note-btn danger" id="hdBtnClear">清空本地备注</button>' +
'<span class="nt-hint">填写会自动暂存到本机浏览器;填完点「导出带备注的副本」下载一份 HTML 发回即可,对方打开就能看到所有批注。</span>';
var main = document.querySelector("main") || document.body;
var head = main.querySelector("header.page-head");
if (head && head.nextSibling) main.insertBefore(bar, head.nextSibling);
else main.insertBefore(bar, main.firstChild);
function collect() {
var out = {};
entries.forEach(function (e) { if (e.ta.value.trim() !== "") out[e.key] = e.ta.value; });
return out;
}
function updateCount() {
var n = entries.filter(function (e) { return e.ta.value.trim() !== ""; }).length;
var el = document.getElementById("hdNoteCount");
if (el) el.textContent = "已填 " + n + " 条";
}
updateCount();
function pad(n) { return (n < 10 ? "0" : "") + n; }
function stamp() {
var d = new Date();
return "" + d.getFullYear() + pad(d.getMonth() + 1) + pad(d.getDate()) + "-" + pad(d.getHours()) + pad(d.getMinutes());
}
function flash(btn, msg) {
if (!btn) return;
var old = btn.textContent;
btn.textContent = msg;
setTimeout(function () { btn.textContent = old; }, 1500);
}
// 5. 导出:克隆文档 → 剔除注入的 UI → 仅嵌入备注 JSON再次打开由本脚本幂等重建
function exportHtml() {
var data = collect();
var clone = document.documentElement.cloneNode(true);
clone.querySelectorAll(".hd-note-col,.hd-note-cell,.hd-note-block,.hd-note-toolbar,#hd-note-style,#embedded-notes")
.forEach(function (el) { el.parentNode.removeChild(el); });
var s = document.createElement("script");
s.type = "application/json";
s.id = "embedded-notes";
s.textContent = JSON.stringify(data, null, 2);
clone.querySelector("body").appendChild(s);
var html = "<!DOCTYPE html>\n" + clone.outerHTML;
var blob = new Blob([html], { type: "text/html;charset=utf-8" });
var a = document.createElement("a");
a.href = URL.createObjectURL(blob);
a.download = "notes-" + NS.replace(/[^a-zA-Z0-9_-]+/g, "_") + "-" + stamp() + ".html";
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(a.href);
}
// 6. 复制为 Markdown按 group 分组)
function toMarkdown() {
var byGroup = {}, order = [];
entries.forEach(function (e) {
if (e.ta.value.trim() === "") return;
if (!byGroup[e.group]) { byGroup[e.group] = []; order.push(e.group); }
byGroup[e.group].push("- **" + e.label + "**: " + e.ta.value.replace(/\n+/g, " "));
});
if (!order.length) return "(暂无备注)";
var lines = ["# " + NS + " · 批注", ""];
order.forEach(function (g) { lines.push("## " + g); lines.push.apply(lines, byGroup[g]); lines.push(""); });
return lines.join("\n");
}
function copyMd() {
var md = toMarkdown();
var done = function () { flash(document.getElementById("hdBtnMd"), "已复制 Markdown"); };
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(md).then(done, function () { fallbackCopy(md, done); });
} else { fallbackCopy(md, done); }
}
function fallbackCopy(text, cb) {
var t = document.createElement("textarea");
t.value = text; t.style.position = "fixed"; t.style.opacity = "0";
document.body.appendChild(t); t.select();
try { document.execCommand("copy"); } catch (e) {}
document.body.removeChild(t); if (cb) cb();
}
function clearAll() {
if (!window.confirm("确定清空本机暂存的所有备注?(不影响已导出的文件)")) return;
entries.forEach(function (e) {
localStorage.removeItem(STORAGE_PREFIX + e.key);
e.ta.value = ""; e.ta.classList.remove("filled");
});
updateCount();
}
var be = document.getElementById("hdBtnExport");
var bm = document.getElementById("hdBtnMd");
var bc = document.getElementById("hdBtnClear");
if (be) be.addEventListener("click", exportHtml);
if (bm) bm.addEventListener("click", copyMd);
if (bc) bc.addEventListener("click", clearAll);
})();