javascript:(async () => { const rawUrl = location.origin + location.pathname + location.search; const canonicalUrl = cleanUrl( document.querySelector('link[rel="canonical"]')?.href || rawUrl ); const bytes = new TextEncoder().encode(canonicalUrl); const digest = await crypto.subtle.digest("SHA-256", bytes); const hash = [...new Uint8Array(digest)] .map(b => b.toString(16).padStart(2, "0")) .join(""); const params = new URLSearchParams({ rawUrl, canonicalUrl }); const PW_BASE_URL = "https://cx22.localhost/cygnus_x1/labs/KSN-01/"; const url = `${PW_BASE_URL}api/${hash}.json?${params}`; const response = await fetch(url); if (!response.ok) { if (response.status === 404) return; throw new Error(`PW: ${response.status}`); } const annotations = await response.json(); for (const { selector, href } of annotations) { const range = resolveTextQuoteSelector(selector); if (range) { applyLink(range, href); } } function resolveTextQuoteSelector(selector) { if (selector?.type !== "TextQuoteSelector" || !selector.exact) { return null; } const nodes = []; const walker = document.createTreeWalker( document.body, NodeFilter.SHOW_TEXT, { acceptNode(node) { const parent = node.parentElement; if (!parent) return NodeFilter.FILTER_REJECT; if (["SCRIPT", "STYLE", "NOSCRIPT", "TEXTAREA"].includes(parent.tagName)) { return NodeFilter.FILTER_REJECT; } return NodeFilter.FILTER_ACCEPT; } } ); while (walker.nextNode()) { nodes.push(walker.currentNode); } const text = nodes.map(node => node.nodeValue).join(""); const { exact, prefix = "", suffix = "" } = selector; let offset = 0; while ((offset = text.indexOf(exact, offset)) !== -1) { const before = text.slice( Math.max(0, offset - prefix.length), offset ); const after = text.slice( offset + exact.length, offset + exact.length + suffix.length ); const prefixMatches = !prefix || before.endsWith(prefix); const suffixMatches = !suffix || after.startsWith(suffix); if (prefixMatches && suffixMatches) { const start = offset; const end = offset + exact.length; let position = 0; let startNode, startOffset; let endNode, endOffset; for (const node of nodes) { const next = position + node.nodeValue.length; if (!startNode && start >= position && start <= next) { startNode = node; startOffset = start - position; } if (end >= position && end <= next) { endNode = node; endOffset = end - position; break; } position = next; } if (startNode && endNode) { const range = document.createRange(); range.setStart(startNode, startOffset); range.setEnd(endNode, endOffset); return range; } } offset++; } return null; } function applyLink(range, href) { // Don't create nested links. if ( range.startContainer.parentElement?.closest("a") || range.endContainer.parentElement?.closest("a") || range.cloneContents().querySelector?.("a") ) { return; } const a = document.createElement("a"); a.href = href; a.target = "_blank"; a.dataset.pw = "link"; a.appendChild(range.extractContents()); range.insertNode(a); } function cleanUrl(url) { const u = new URL(url); // Fragments are not part of page identity. u.hash = ""; // Remove common tracking parameters. for (const key of [...u.searchParams.keys()]) { if ( key.startsWith("utm_") || ["fbclid", "gclid", "dclid", "msclkid"].includes(key) ) { u.searchParams.delete(key); } } // Deterministic ordering. u.searchParams.sort(); return u.toString(); } })();