~ knotless Search/
↑↓ move↵ open⇧↵ all results

chadtmiller.com/pds.js

An atproto PDS in JavaScript, for Cloudflare Workers and Node

0
0
main
98 lines · 3.7 KB Raw History
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>
  );
}