chadtmiller.com/knotless
git that lives in your Atmosphere account
1
10
A line's number marks the whole line in place, and shift marks a range
web/src/lib/organisms/CodeView.svelte+26−10
| @@ -1,16 +1,32 @@ | ||
<script lang="ts"> | ||
import { page } from '$app/state'; | ||
import { highlight, target } from '#lib/highlight.ts'; | ||
import type { Snippet } from 'svelte'; | ||
/** A text file, with a gutter of line numbers to link to, and before it `gutter`, a column as tall as the text, when given. */ | ||
import { goto } from '$app/navigation'; | ||
import { highlight } from '#lib/highlight.ts'; | ||
import { onMount, type Snippet } from 'svelte'; | ||
/** A text file, with a gutter of line numbers to link to, and before it `gutter`, a column as tall as the text, when given. A line's number marks it, shift and a second marks the lines between. */ | ||
let { text, lines, ranges, gutter }: { text: string; lines: number; ranges?: string | null; gutter?: Snippet } = $props(); | ||
let code: HTMLElement | undefined = $state(); | ||
$effect(() => { | ||
void text; | ||
const m = /^#L(\d+)$/.exec(page.url.hash); | ||
target(code, m ? +m[1] : null); | ||
return () => target(undefined, null); | ||
/** The lines `#L5` or `#L5-L9` marks, first to last, or null. */ | ||
const marked = $derived.by(() => { | ||
const m = /^#L(\d+)(?:-L(\d+))?$/.exec(page.url.hash); | ||
if (!m) return null; | ||
const a = +m[1]; | ||
const b = m[2] ? +m[2] : a; | ||
const first = Math.max(1, Math.min(a, b)); | ||
const last = Math.min(lines, Math.max(a, b)); | ||
return first <= last ? { first, last } : null; | ||
}); | ||
let ln: HTMLElement | undefined = $state(); | ||
onMount(() => { | ||
if (marked) ln?.querySelector(`#L${marked.first}`)?.scrollIntoView({ block: 'center' }); | ||
}); | ||
function mark(e: MouseEvent) { | ||
const a = (e.target as Element).closest('a'); | ||
const n = a ? +a.id.slice(1) : 0; | ||
if (!n || e.metaKey || e.ctrlKey || e.altKey) return; | ||
e.preventDefault(); | ||
const hash = e.shiftKey && marked ? (n === marked.first ? `#L${n}` : `#L${marked.first}-L${n}`) : `#L${n}`; | ||
goto(hash, { replace: true, reset: false }); | ||
} | ||
</script> | ||
| ||
<div class="code" class:gutted={gutter}>{#if gutter}{@render gutter()}{/if}<pre class="ln" aria-hidden="true">{#each { length: lines }, i (i)}<a id="L{i + 1}" href="#L{i + 1}">{i + 1}</a>{'\n'}{/each}</pre><pre class="src" use:highlight={{ ranges, text }}><code bind:this={code}>{text}</code></pre></div> | ||
<div class="code" class:gutted={gutter}>{#if marked}<div class="marked" style:--first={marked.first} style:--count={marked.last - marked.first + 1}></div>{/if}{#if gutter}{@render gutter()}{/if}<!-- svelte-ignore a11y_click_events_have_key_events, a11y_no_noninteractive_element_interactions --><pre class="ln" aria-hidden="true" bind:this={ln} onclick={mark}>{#each { length: lines }, i (i)}<a id="L{i + 1}" href="#L{i + 1}">{i + 1}</a>{'\n'}{/each}</pre><pre class="src" use:highlight={{ ranges, text }}><code>{text}</code></pre></div> | ||
web/src/lib/highlight.ts+0−17
| @@ -152,20 +152,3 @@ export function fences(el: Element, _html: string) { | ||
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 StaticRange({ startContainer: text, startOffset: a, endContainer: text, endOffset: e < 0 ? data.length : e + 1 }); | ||
CSS.highlights.set('target', new Highlight(r)); | ||
} | ||
web/src/app.css+5−3
| @@ -562,11 +562,13 @@ a.row:hover{background:var(--hover)} | ||
.release-files li{font-size:var(--text-sm)} | ||
.release-files .file-name{flex:1;min-width:0;overflow-wrap:anywhere} | ||
.release-files .digest{font:var(--text-xs) var(--font-mono);color:var(--faint)} | ||
.code{display:grid;grid-template-columns:auto minmax(0,1fr)} | ||
.code{position:relative;display:grid;grid-template-columns:auto minmax(0,1fr)} | ||
/* The lines a link marks: a band across the whole row, behind the numbers and the text. */ | ||
.code .marked{position:absolute;left:0;right:0;top:calc(var(--space-3) + (var(--first) - 1) * var(--leading-code));height:calc(var(--count) * var(--leading-code));background:var(--target);pointer-events:none} | ||
.code>:not(.marked){position:relative} | ||
.code pre{margin:0;padding:var(--space-3) 0;line-height:var(--leading-code);tab-size:4} | ||
.code .ln{text-align:right;color:var(--faint);user-select:none;padding:var(--space-3) var(--space-3) var(--space-3) var(--pad-x);min-width:3.5rem} | ||
.code .ln a{color:inherit} | ||
.code .ln a:target{color:var(--fg)} | ||
.code .src{overflow-x:auto;padding:var(--space-3) var(--pad-x)} | ||
/* Blame: a column beside the code, each run of lines as tall as they are, with its commit along its first line. */ | ||
.code.gutted{grid-template-columns:var(--blame-width) auto minmax(0,1fr)} | ||
| @@ -1620,7 +1622,7 @@ textarea.field{resize:vertical;font-family:var(--font-mono);font-size:var(--text | ||
/* ── Highlighted code ─────────────────────────────────────────────── */ | ||
| ||
::highlight(comment){color:var(--comment)}::highlight(string){color:var(--string)}::highlight(keyword){color:var(--keyword)}::highlight(number){color:var(--number)}::highlight(function){color:var(--function)}::highlight(type){color:var(--type)}::highlight(tag){color:var(--tag)}::highlight(attr){color:var(--attr)} | ||
::highlight(xins){background-color:var(--ins-x);color:var(--fg)}::highlight(xdel){background-color:var(--del-x);color:var(--fg)}::highlight(target){background-color:var(--target)} | ||
::highlight(xins){background-color:var(--ins-x);color:var(--fg)}::highlight(xdel){background-color:var(--del-x);color:var(--fg)} | ||
| ||
/* ── Admin ────────────────────────────────────────────────────────── */ | ||
| ||