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

chadtmiller.com/pds.js

An atproto PDS in JavaScript, for Cloudflare Workers and Node

0
0
main
134 lines · 4.5 KB Raw History
// The address of a line, so a reader can send somebody to it.
//
// In a diff the fragment names the file, the side, and one line or a span of
// them: `line-src/pds.js-new:12-20`. The path is written whole, and the side
// and the numbers close the fragment, so the reader of a link can see what it
// points at. A file's own page has the path in the address already, so there
// the fragment is the numbers alone: `L12-20`.

/**
 * The id one diff row answers to, so a link naming a line finds it.
 * @param {string} path
 * @param {string} key - the row's key, as `lineKey` writes it
 * @returns {string}
 */
export function lineId(path, key) {
  return `line-${path}-${key}`;
}

/**
 * The fragment naming a selected line or span.
 * @param {{path: string, side: string, start: number, end: number}} selection
 * @returns {string}
 */
export function lineFragment(selection) {
  const at =
    selection.end === selection.start
      ? String(selection.start)
      : `${selection.start}-${selection.end}`;
  return `line-${selection.path}-${selection.side}:${at}`;
}

/** A path may hold a hyphen, so the side and the numbers are read from the end. */
const FRAGMENT = /^line-(.+)-(old|new):(\d+)(?:-(\d+))?$/;

/**
 * Read a selection back out of a fragment.
 * @param {string} hash - the fragment, decoded, without its `#`
 * @returns {{path: string, side: string, start: number, end: number}|null}
 */
export function parseLineFragment(hash) {
  const found = FRAGMENT.exec(hash);
  if (!found) return null;
  const [, path, side, first, last] = found;
  const one = Number(first);
  const other = last === undefined ? one : Number(last);
  return {
    path,
    side,
    start: Math.min(one, other),
    end: Math.max(one, other),
  };
}

/**
 * The id one source line answers to, which is the fragment naming that line
 * alone. The browser scrolls to it without being asked.
 * @param {number} number
 * @returns {string}
 */
export function sourceLineId(number) {
  return `L${number}`;
}

/**
 * The fragment naming a line or a span of them on a file's own page.
 * @param {{start: number, end: number}} span
 * @returns {string}
 */
export function sourceFragment(span) {
  return span.end === span.start
    ? `L${span.start}`
    : `L${span.start}-${span.end}`;
}

/** The second number reads with or without its own L, as a forge writes it. */
const SOURCE = /^L(\d+)(?:-L?(\d+))?$/;

/**
 * Read a span back out of a fragment on a file's own page.
 * @param {string} hash - the fragment, decoded, without its `#`
 * @returns {{start: number, end: number}|null}
 */
export function parseSourceFragment(hash) {
  const found = SOURCE.exec(hash);
  if (!found) return null;
  const one = Number(found[1]);
  const other = found[2] === undefined ? one : Number(found[2]);
  return { start: Math.min(one, other), end: Math.max(one, other) };
}

/**
 * The span a click makes. Shift takes it from the line already selected to
 * the line clicked; anything else selects the one line.
 * @param {{start: number, end: number}|null|undefined} span
 * @param {number} at
 * @param {boolean} [shift]
 * @returns {{start: number, end: number}}
 */
export function extendSpan(span, at, shift) {
  if (shift && span) {
    return { start: Math.min(span.start, at), end: Math.max(span.start, at) };
  }
  return { start: at, end: at };
}

/**
 * The selection a click makes in a diff. A shift-click reaches the line
 * already selected only in the same file and on the same side.
 * @param {{path: string, side: string, start: number, end: number}|null} selection
 * @param {{path: string, side: string, at: number}} target
 * @param {boolean} [shift]
 * @returns {{path: string, side: string, start: number, end: number}}
 */
export function extendSelection(selection, target, shift) {
  const { path, side, at } = target;
  const here =
    selection?.path === path && selection.side === side ? selection : null;
  return { path, side, ...extendSpan(here, at, shift) };
}

/**
 * Whether a diff row is one of the lines a selection names.
 * @param {{path: string, side: string, start: number, end: number}|null|undefined} selection
 * @param {string} path
 * @param {{oldLine: number|null, newLine: number|null}} line
 * @returns {boolean}
 */
export function inSelection(selection, path, line) {
  if (!selection || selection.path !== path) return false;
  const side = line.newLine != null ? 'new' : 'old';
  const at = line.newLine ?? line.oldLine;
  if (at == null || side !== selection.side) return false;
  return at >= selection.start && at <= selection.end;
}