er markerbart (tittel + ingress + brødtekst), ikke bare .body.
var article = document.querySelector("article");
var toolbar = document.getElementById("annoToolbar");
var popover = document.getElementById("annoPopover");
var sidebar = document.getElementById("annoSidebar");
var listEl = document.getElementById("annoList");
var annotations = load();
var pendingRange = null; // {text} for ny annotering
var editingId = null; // id når en eksisterende annotering redigeres
function load() {
try {
var raw = localStorage.getItem(STORE_KEY);
return raw ? JSON.parse(raw) : [];
} catch (e) { return []; }
}
function save() {
try { localStorage.setItem(STORE_KEY, JSON.stringify(annotations)); } catch (e) {}
}
// --- Markering -> verktøylinje -------------------------------------------
document.addEventListener("mouseup", function (e) {
if (toolbar.contains(e.target) || popover.contains(e.target)) return;
setTimeout(handleSelection, 0);
});
// Hent kontekst rundt markeringen (ordene foran/bak i samme avsnitt),
// slik at korte markeringer (ett ord) kan plasseres entydig i eksporten.
function getContext(range, selText) {
var BLOCK = /^(P|LI|H1|H2|H3|H4|BLOCKQUOTE|TD)$/;
var block = range.commonAncestorContainer;
if (block.nodeType === 3) block = block.parentElement;
while (block && block !== article && !BLOCK.test(block.tagName)) block = block.parentElement;
if (!block || block === article) return "";
try {
var beforeR = document.createRange();
beforeR.selectNodeContents(block);
beforeR.setEnd(range.startContainer, range.startOffset);
var afterR = document.createRange();
afterR.selectNodeContents(block);
afterR.setStart(range.endContainer, range.endOffset);
var before = beforeR.toString().replace(/\s+/g, " ");
var after = afterR.toString().replace(/\s+/g, " ");
var W = 55;
if (before.length > W) before = "…" + before.slice(-W);
if (after.length > W) after = after.slice(0, W) + "…";
return (before + "〈" + selText + "〉" + after).trim();
} catch (e) { return ""; }
}
function handleSelection() {
var sel = window.getSelection();
if (!sel || sel.isCollapsed) { hideToolbar(); return; }
var text = sel.toString().replace(/\s+/g, " ").trim();
if (text.length < 2) { hideToolbar(); return; }
var range = sel.getRangeAt(0);
if (!article.contains(range.commonAncestorContainer)) { hideToolbar(); return; }
var rect = range.getBoundingClientRect();
pendingRange = { text: text, context: getContext(range, text) };
showToolbar(rect);
}
function showToolbar(rect) {
toolbar.classList.add("show");
var tw = toolbar.offsetWidth;
var x = window.scrollX + rect.left + rect.width / 2 - tw / 2;
var y = window.scrollY + rect.top - toolbar.offsetHeight - 8;
x = Math.max(8, x);
if (y < window.scrollY + 4) y = window.scrollY + rect.bottom + 8;
toolbar.style.left = x + "px";
toolbar.style.top = y + "px";
popover.classList.remove("show");
}
function hideToolbar() { toolbar.classList.remove("show"); }
// Bygg verktøylinje-knapper
Object.keys(INTENTS).forEach(function (key) {
var b = document.createElement("button");
b.innerHTML = '' + INTENTS[key].label;
b.addEventListener("mousedown", function (ev) { ev.preventDefault(); });
b.addEventListener("click", function () { openPopover(key); });
toolbar.appendChild(b);
});
// Intent-velger i popoveren (brukes både ved ny og ved redigering)
var pick = popover.querySelector(".intent-pick");
Object.keys(INTENTS).forEach(function (key) {
var b = document.createElement("button");
b.type = "button";
b.dataset.intent = key;
b.innerHTML = '' + INTENTS[key].label;
b.addEventListener("mousedown", function (ev) { ev.preventDefault(); });
b.addEventListener("click", function () {
popover.dataset.intent = key;
updateIntentPick(key);
});
pick.appendChild(b);
});
function updateIntentPick(sel) {
Array.prototype.forEach.call(pick.children, function (b) {
b.classList.toggle("active", b.dataset.intent === sel);
});
}
function positionPopover(rect) {
popover.style.left = Math.max(8, window.scrollX + rect.left) + "px";
popover.style.top = (window.scrollY + rect.top) + "px";
}
function openPopover(intentKey) {
if (!pendingRange) return;
editingId = null;
popover.querySelector(".ph").textContent =
INTENTS[intentKey].label + " — «" + truncate(pendingRange.text, 60) + "»";
var ta = popover.querySelector("textarea");
ta.value = "";
popover.dataset.intent = intentKey;
updateIntentPick(intentKey);
popover.querySelector(".del-edit").style.display = "none";
popover.classList.add("show");
positionPopover(toolbar.getBoundingClientRect());
toolbar.classList.remove("show");
setTimeout(function () { ta.focus(); }, 10);
}
// Åpne popover for å REDIGERE en eksisterende annotering
function openEdit(id, rect) {
var a = annotations.filter(function (x) { return x.id === id; })[0];
if (!a) return;
editingId = id;
pendingRange = null;
popover.querySelector(".ph").textContent = "Rediger — «" + truncate(a.text, 60) + "»";
var ta = popover.querySelector("textarea");
ta.value = a.comment || "";
popover.dataset.intent = a.intent;
updateIntentPick(a.intent);
popover.querySelector(".del-edit").style.display = "";
popover.classList.add("show");
positionPopover(rect);
toolbar.classList.remove("show");
window.getSelection().removeAllRanges();
setTimeout(function () { ta.focus(); }, 10);
}
// Klikk på en markering i artikkelen -> rediger den
article.addEventListener("click", function (e) {
var span = e.target.closest ? e.target.closest("span.anno") : null;
if (!span || !span.dataset.id) return;
e.preventDefault();
openEdit(span.dataset.id, span.getBoundingClientRect());
});
popover.querySelector(".cancel").addEventListener("click", function () {
popover.classList.remove("show"); pendingRange = null; editingId = null;
});
popover.querySelector(".del-edit").addEventListener("click", function () {
if (!editingId) return;
annotations = annotations.filter(function (x) { return x.id !== editingId; });
editingId = null;
save();
popover.classList.remove("show");
render();
});
popover.querySelector(".primary").addEventListener("click", function () {
var intent = popover.dataset.intent;
var cmt = popover.querySelector("textarea").value.trim();
if (editingId) {
annotations.forEach(function (a) {
if (a.id === editingId) { a.intent = intent; a.comment = cmt; }
});
editingId = null;
save();
popover.classList.remove("show");
render();
return;
}
if (!pendingRange) return;
annotations.push({
id: Date.now() + "-" + Math.floor(Math.random() * 1e6),
intent: intent,
text: pendingRange.text,
context: pendingRange.context || "",
comment: cmt
});
save();
popover.classList.remove("show");
pendingRange = null;
window.getSelection().removeAllRanges();
render();
});
// --- Rendering: marker tekst i artikkelen og bygg sidepanel --------------
function render() {
clearMarks();
annotations.forEach(function (a, idx) { markInArticle(a, idx + 1); });
renderList();
}
function clearMarks() {
var marks = article.querySelectorAll("span.anno");
marks.forEach(function (m) {
var parent = m.parentNode;
while (m.firstChild) {
if (m.firstChild.classList && m.firstChild.classList.contains("anno-num")) {
m.removeChild(m.firstChild);
} else {
parent.insertBefore(m.firstChild, m);
}
}
parent.removeChild(m);
parent.normalize();
});
}
// Finn første tekst-treff i artikkelen og pakk det inn. Teller per-tekst
// forekomster slik at like sitater markeres i rekkefølge.
var occCounters = {};
function markInArticle(a, num) {
var needle = a.text;
if (!occCounters[needle]) occCounters[needle] = 0;
var skip = occCounters[needle];
occCounters[needle]++;
var walker = document.createTreeWalker(article, NodeFilter.SHOW_TEXT, null);
var node, found = 0;
while ((node = walker.nextNode())) {
var nv = node.nodeValue;
var pos = nv.indexOf(needle);
while (pos !== -1) {
if (found === skip) {
wrapTextNode(node, pos, needle.length, a, num);
return;
}
found++;
pos = nv.indexOf(needle, pos + 1);
}
}
}
function wrapTextNode(node, start, len, a, num) {
var range = document.createRange();
range.setStart(node, start);
range.setEnd(node, start + len);
var span = document.createElement("span");
span.className = "anno anno-" + INTENTS[a.intent].cls;
span.dataset.id = a.id;
span.title = INTENTS[a.intent].label + (a.comment ? ": " + a.comment : "");
try {
range.surroundContents(span);
var sup = document.createElement("sup");
sup.className = "anno-num";
sup.textContent = num;
span.appendChild(sup);
} catch (e) { /* range spente over elementgrense — hopp over markering */ }
}
function renderList() {
occCounters = {}; // nullstill for neste render
listEl.innerHTML = "";
if (!annotations.length) {
var em = document.createElement("div");
em.className = "empty";
em.textContent = "Ingen annoteringer ennå. Marker tekst i artikkelen for å begynne.";
listEl.appendChild(em);
return;
}
annotations.forEach(function (a, idx) {
var item = document.createElement("div");
item.className = "anno-item";
var cls = INTENTS[a.intent].cls;
item.innerHTML =
'' +
'' + (idx + 1) + '' +
'' + INTENTS[a.intent].label + '' +
'' +
'' +
'
' +
'«' + escHtml(truncate(a.text, 90)) + '»
' +
(a.comment ? '' + escHtml(a.comment) + '
' : '');
item.querySelector(".edit").addEventListener("click", function () {
openEdit(a.id, item.getBoundingClientRect());
});
item.querySelector(".del").addEventListener("click", function () {
annotations = annotations.filter(function (x) { return x.id !== a.id; });
save(); render();
});
listEl.appendChild(item);
});
}
// --- Eksport: kompakt annoteringsliste (kun annoteringer, ikke brødtekst) -
function buildAnnotatedMarkdown() {
var header = "# Annoteringer — " + SOURCE_FILE + " · «" + TITLE + "»";
if (!annotations.length) {
return header + "\n\n(Ingen annoteringer.)\n";
}
function occurrences(s) {
if (!s) return 0;
var hay = article.textContent.replace(/\s+/g, " ");
var n = 0, i = 0;
while ((i = hay.indexOf(s, i)) !== -1) { n++; i += s.length; }
return n;
}
var blocks = annotations.map(function (a, idx) {
var lines = [(idx + 1) + ". [" + INTENTS[a.intent].upper + "] «" + a.text + "»"];
// Ta med kontekst kun når markeringen er kort eller forekommer flere ganger
// (ellers holder vi eksporten kompakt).
if (a.context && (a.text.length < 30 || occurrences(a.text) > 1)) {
lines.push(" ↳ i: «" + a.context + "»");
}
lines.push(" → " + (a.comment || ""));
return lines.join("\n");
});
return header + "\n\n" + blocks.join("\n\n") + "\n";
}
function showExport() {
var overlay = document.getElementById("exportOverlay");
var ta = document.getElementById("exportText");
ta.value = buildAnnotatedMarkdown();
overlay.classList.add("show");
ta.focus(); ta.select();
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(ta.value).catch(function () {});
}
}
// --- Helpers --------------------------------------------------------------
function truncate(s, n) { return s.length > n ? s.slice(0, n - 1) + "…" : s; }
function escHtml(s) {
return s.replace(/&/g, "&").replace(//g, ">");
}
// --- Bind topp-/panel-knapper --------------------------------------------
document.getElementById("btnExport").addEventListener("click", showExport);
document.getElementById("btnExportTop").addEventListener("click", showExport);
document.getElementById("btnClear").addEventListener("click", function () {
if (!annotations.length) return;
if (confirm("Tøm alle annoteringer for denne artikkelen?")) {
annotations = []; save(); render();
}
});
document.getElementById("exportClose").addEventListener("click", function () {
document.getElementById("exportOverlay").classList.remove("show");
});
document.getElementById("exportCopy").addEventListener("click", function () {
var ta = document.getElementById("exportText");
ta.select();
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(ta.value);
} else { document.execCommand("copy"); }
});
document.getElementById("sidebarToggle").addEventListener("click", function () {
sidebar.classList.toggle("mobile-show");
});
// Skjul verktøylinje ved klikk utenfor
document.addEventListener("mousedown", function (e) {
if (!toolbar.contains(e.target) && !popover.contains(e.target)) {
if (!window.getSelection().toString().trim()) hideToolbar();
}
});
render();
})();
`;
// ---------------------------------------------------------------------------
// HTML-shell
// ---------------------------------------------------------------------------
function buildPage(meta, body, articleKey, sourceFile) {
const bodyHtml = markdownToHtml(body);
const title = meta.title || "Kronikk";
const metaLine = [meta.serie, meta.lesetid].filter(Boolean).join(" · ");
return `
${esc(title)}
${meta.kicker ? `${esc(meta.kicker)}
` : ""}
${inline(title)}
${meta.subtitle ? `${inline(meta.subtitle)}
` : ""}
${meta.byline ? `
${esc(meta.byline)}
` : ""}
${metaLine ? `
${esc(metaLine)}
` : ""}
${bodyHtml}
Annoteringer — kopier og lim tilbake
`;
}
// ---------------------------------------------------------------------------
// Main
// ---------------------------------------------------------------------------
function main() {
const args = process.argv.slice(2);
if (!args.length) {
console.error("Bruk: node build-html.mjs [flere.md ...]");
process.exit(1);
}
// Output følger serien (kjøres fra serie-mappa), ikke scriptet i tools/.
const outDir = path.join(process.cwd(), "review");
if (!fs.existsSync(outDir)) fs.mkdirSync(outDir, { recursive: true });
for (const arg of args) {
const inPath = path.isAbsolute(arg) ? arg : path.join(process.cwd(), arg);
if (!fs.existsSync(inPath)) {
console.error(`Fant ikke: ${inPath}`);
continue;
}
const raw = fs.readFileSync(inPath, "utf8");
const { meta, body } = parseFrontMatter(raw);
const sourceFile = path.basename(inPath);
const base = sourceFile.replace(/\.md$/i, "");
const html = buildPage(meta, body, base, sourceFile);
const outPath = path.join(outDir, base + ".html");
fs.writeFileSync(outPath, html, "utf8");
console.log(`Skrev ${outPath} (${(html.length / 1024).toFixed(1)} KB)`);
}
}
main();