import { useEffect, useMemo } from 'react'; import { highlightToLines, languageFor } from '#/lib/highlight.js'; import { extendSpan, parseSourceFragment, sourceFragment, sourceLineId, } from '#/lib/line-links.js'; import { useHash } from '#/lib/navigation.jsx'; import { cn } from '#/lib/utils.js'; /** * A file's text, a line to a row, each row addressed by its number. * * A click on a number selects that line and writes it into the address; a * shift-click takes the span from the line already selected to this one. Every * number is a real link, so the browser's own menu copies it. * * highlight.js escapes the source it is given, so nothing in the file can * reach the page as markup. */ export function SourceLines({ text, path }) { const [hash, setHash] = useHash(); const span = parseSourceFragment(hash); const lines = useMemo( () => highlightToLines(text.replace(/\n$/, ''), languageFor(path)), [text, path], ); // ch is the advance of a figure in the face the gutter is set in, so the // column holds exactly its digits plus its own padding. const digits = Math.max(2, String(lines.length).length); const gutter = { width: `calc(${digits}ch + 2rem)` }; // A link from elsewhere names a line, and the file arrives after the first // paint, so the browser's own fragment scroll finds nothing. This one runs // once the rows are on the page. The page keys this component by the file, // so opening another one runs it again. It reads the address from the window // rather than from the hook above, so a click on a number moves nothing // under the reader. useEffect(() => { const named = parseSourceFragment( decodeURIComponent(window.location.hash.slice(1)), ); if (!named) return; document .getElementById(sourceLineId(named.start)) ?.scrollIntoView({ block: 'center' }); }, []); return ( // One box as wide as the longest line, with every row filling it. Sized to // each row instead, a short row's wash would stop where its text does and // leave the rest of the scroll unpainted.
{lines.map((html, index) => { const number = index + 1; const marked = span && number >= span.start && number <= span.end; return ( ); })}
); }