chadtmiller.com/knotless

Git hosted in your atproto account, on any stock PDS, fast enough for agents.

git clone knotless://chadtmiller.com/knotless

needs the knotless git helper. set it up

main
89 lines · 2.8 KB Raw History
// Highlighting, painted with the CSS Custom Highlight API rather than
// spans, so the text stays one node per line and the page stays small.

/** The text in `el`, as one node, or nothing for an empty line. */
function textOf(el: Element): Text | null {
	el.normalize();
	const walker = document.createTreeWalker(el, NodeFilter.SHOW_TEXT);
	return walker.nextNode() as Text | null;
}

/** The highlight named `name`, made if there is none yet. */
function named(name: string): Highlight {
	let h = CSS.highlights.get(name);
	if (!h) CSS.highlights.set(name, (h = new Highlight()));
	return h;
}

/**
 * Paints the ranges `ranges` encodes over the text of the `code`
 * elements in `el`, taken as if joined by newlines. `ranges` is
 * `name:start,length,start,length…;…`, each start counted from the
 * end of the range before it.
 */
function paint(el: Element, ranges: string): [Highlight, Range][] {
	const nodes: [number, Text][] = [];
	let at = 0;
	for (const code of el.querySelectorAll('code')) {
		const text = textOf(code);
		if (text) nodes.push([at, text]);
		at += (text ? text.length : 0) + 1;
	}
	const painted: [Highlight, Range][] = [];
	for (const group of ranges.split(';')) {
		if (!group) continue;
		const [name, list] = group.split(':');
		const h = named(name);
		const x = list.split(',');
		let o = 0;
		let j = 0;
		for (let i = 0; i < x.length; i += 2) {
			o += +x[i];
			const end = o + +x[i + 1];
			while (j < nodes.length && nodes[j][0] + nodes[j][1].length <= o) j++;
			for (let k = j; k < nodes.length && nodes[k][0] < end; k++) {
				const [a, t] = nodes[k];
				const r = new Range();
				r.setStart(t, Math.max(o, a) - a);
				r.setEnd(t, Math.min(end, a + t.length) - a);
				h.add(r);
				painted.push([h, r]);
			}
			o = end;
		}
	}
	return painted;
}

/** An action painting `ranges` over the code in its element, again as what it shows changes. */
export function highlight(el: Element, shown: { ranges: string | null; text: unknown }) {
	if (!globalThis.CSS?.highlights) return {};
	let painted = paint(el, shown.ranges ?? '');
	const clear = () => painted.forEach(([h, r]) => h.delete(r));
	return {
		update(next: typeof shown) {
			clear();
			painted = paint(el, next.ranges ?? '');
		},
		destroy: clear
	};
}

/** Marks line `line` of the text in `el`, or nothing. */
export function target(el: Element | undefined, line: number | null) {
	if (!globalThis.CSS?.highlights) return;
	CSS.highlights.delete('target');
	const text = el && line ? textOf(el) : null;
	if (!text || !line) return;
	const data = text.data;
	let a = 0;
	for (let i = 1; i < line; i++) {
		a = data.indexOf('\n', a) + 1;
		if (!a) return;
	}
	const e = data.indexOf('\n', a);
	const r = new Range();
	r.setStart(text, a);
	r.setEnd(text, e < 0 ? data.length : e + 1);
	CSS.highlights.set('target', new Highlight(r));
}