import { highlightedLines } from "./highlight.ts"; import { detectLanguage, languageNamed } from "./languages.ts"; // as bare jsx text, a newline would collapse const NEWLINE = "\n"; const waiting = new WeakMap void>(); let visibility: IntersectionObserver | undefined; export type CodeOptions = { /** Picks the grammar by file name. */ readonly path?: string; /** A grammar or an alias such as `ts`; wins over `path`. */ readonly language?: string; /** What each line's gutter shows: line numbers by default, `false` for no gutter. */ readonly gutter?: ((index: number) => string) | false; }; const grammarOf = ({ path, language }: Pick) => language ? languageNamed(language) ?? language : path ? detectLanguage(path) : null; /** `source`'s lines highlighted, the grammar picked as `code` picks it; `null` without one. */ export async function highlight( source: string, options: Pick, ): Promise { const grammar = grammarOf(options); return grammar === null ? null : await highlightedLines(grammar, source); } /** * Source as sorcery shows it: each line shares a grid row with its gutter, so wrapped lines don't displace the * gutter below. Plain at first, then highlighted once the worker answers. */ export function code( source: string, { path, language, gutter = (i) => String(i + 1) }: CodeOptions = {}, ): HTMLElement { const lines = source.split("\n"); if (lines.length > 1 && lines.at(-1) === "") lines.pop(); // the newline belongs inside the line's own text: a grid item, firefox's copy adds a break after each const pre = (
      {lines.map((line, i) => (
        
          {gutter ? {gutter(i)} : undefined}
          {line}{i < lines.length - 1 ? NEWLINE : ""}
        
      ))}
    
) as HTMLPreElement; const grammar = grammarOf({ path, language }); if (grammar !== null) { if (!visibility) { const observer = visibility = new IntersectionObserver((entries) => { for (const { target, isIntersecting } of entries) { if (!isIntersecting || !target.isConnected) continue; const highlight = waiting.get(target); observer.unobserve(target); waiting.delete(target); highlight?.(); } }); // code is built detached; observe only while mounted so discarded views aren't retained new MutationObserver((records) => { for (const record of records) { for (const node of [...record.addedNodes, ...record.removedNodes]) { if (!(node instanceof Element)) continue; const blocks = [node, ...node.querySelectorAll("pre.code")]; for (const block of blocks) { if (!waiting.has(block)) continue; if (block.isConnected) observer.observe(block); else observer.unobserve(block); } } } }).observe(document, { childList: true, subtree: true }); } waiting.set(pre, () => { highlightedLines(grammar, lines.join("\n")) .then((highlighted) => { if (highlighted === null) return; const targets = pre.querySelectorAll(".code-text"); for (const [i, line] of highlighted.entries()) { targets[i]?.replaceChildren(...line, ...(i < highlighted.length - 1 ? [NEWLINE] : [])); } }) .catch((error) => console.warn("arborium:", error)); }); } return pre; }