/* Applies security attributes to external anchor elements on the page. When the external-link-qr entitlement is active, also injects a QR trigger span into each external link. Do not handle internal navigation or routing. Do not modify non-HTTP links (mailto:, tel:, etc.). Do not generate QR codes — that is the responsibility of external-link-qr.ts. RFC-0011 Phase 2: Extracted from layout.astro inline script into src/scripts canonical pattern. RFC-0932: accept ExternalLinkBehaviorOptions, inject QR trigger span when entitled. RFC-1097: step 6 — compass.migrate codemod run Mechanical v1 to v2 header migration across the workspace: 942 files rewritten — CHANGE_SUMMARY windows collapsed into history, forbidden v1 blocks stripped, KEY_DECISIONS seeded from @ai-invariant comments (5 files) or TODO placeholders (103 files), blocks reordered to canonical order. RFC-1097: sweep — werkstatt-engine clean Sweep batch 4: 73 Compass headers on headerless engine files (certification, component-runtime, isolation, evolution, testing), real KEY_DECISIONS on 75 files (kernel, cache, dht, swim, gitmesh, runtime), ~80 purpose expansions (CONTRACT-02/PURPOSE-02), non-goals on 13 CONTRACT-03 files, CS-07 history literal fix repo-wide (253 files). Policy: .template.ts/.template.astro excludedPaths. werkstatt-engine now 0 diagnostics. */ export interface ExternalLinkBehaviorOptions { /** Whether the external-link-qr entitlement is resolved. Default false (fail-closed). */ externalLinkQrEntitled?: boolean; } // @ai-invariant: This function must only modify external HTTP/HTTPS links. Never touch mailto:, tel:, or internal links. export function applyExternalLinkBehavior(options?: ExternalLinkBehaviorOptions): void { const anchors = document.querySelectorAll("a[href]"); const qrEntitled = options?.externalLinkQrEntitled === true; for (const anchor of anchors) { const href = anchor.getAttribute("href"); if (!href) { continue; } let resolvedUrl; try { resolvedUrl = new URL(href, window.location.href); } catch { continue; } const isHttpLink = resolvedUrl.protocol === "http:" || resolvedUrl.protocol === "https:"; const isExternalLink = isHttpLink && resolvedUrl.origin !== window.location.origin; if (!isExternalLink) { continue; } anchor.setAttribute("data-external-link", "1"); anchor.setAttribute("target", "_blank"); anchor.setAttribute("rel", "noopener noreferrer"); if (qrEntitled && anchor.getAttribute("data-external-link-qr") !== "off") { // Skip QR trigger for anchors wrapping media elements — a text arrow // appended after an image/SVG/canvas looks visually wrong. const hasMediaChild = anchor.querySelector("img, svg, canvas, picture"); if (hasMediaChild) { continue; } if (!anchor.querySelector("[data-qr-trigger]")) { const trigger = document.createElement("span"); trigger.className = "external-link-qr-trigger"; trigger.setAttribute("role", "button"); trigger.setAttribute("tabindex", "0"); trigger.setAttribute("aria-label", "QR-Code anzeigen"); trigger.setAttribute("data-qr-trigger", ""); trigger.textContent = "\u2197"; anchor.appendChild(trigger); } } } }