// 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)); }