chadtmiller.com/pds.js
An atproto PDS in JavaScript, for Cloudflare Workers and Node
0
0
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.
<div className="overflow-x-auto py-4 font-mono text-[12.5px] leading-6">
<div className="w-max min-w-full">
{lines.map((html, index) => {
const number = index + 1;
const marked = span && number >= span.start && number <= span.end;
return (
<div
// A line is named by its number, and two lines of a file never
// share one.
key={number}
id={sourceLineId(number)}
className={cn(
'flex w-full whitespace-pre',
marked && 'bg-key/20',
)}
>
<a
href={`#${sourceFragment({ start: number, end: number })}`}
style={gutter}
aria-label={`Link to line ${number}; shift-click to end a span`}
className={cn(
'shrink-0 cursor-pointer select-none border-border border-r pr-3 pl-5 text-right text-faint tabular-nums hover:text-foreground',
marked && 'text-key',
)}
onClick={(event) => {
event.preventDefault();
setHash(
sourceFragment(extendSpan(span, number, event.shiftKey)),
);
}}
>
{number}
</a>
<span
className="pr-5 pl-4"
// biome-ignore lint/security/noDangerouslySetInnerHtml: the highlighter escapes its input
dangerouslySetInnerHTML={{ __html: html }}
/>
</div>
);
})}
</div>
</div>
);
}