chadtmiller.com/pds.js
An atproto PDS in JavaScript, for Cloudflare Workers and Node
0
0
// 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;
}