chadtmiller.com/knotless
Git hosted in your atproto account, on any stock PDS, fast enough for agents.
git clone knotless://chadtmiller.com/knotlessneeds the knotless git helper. set it up
Draw Mermaid diagrams in Markdown with Pele
A ```mermaid block in a README, a Markdown file or a pull or issue is drawn as a diagram by Pele (https://pele.run), in the browser. The server still sends the block as code, so a page without scripts shows its source. Pele, and each kind of diagram, load only on a page that has one; the diagram takes the site's colors and fonts, light or dark.
Showing 5 changed files with 66 additions and 3 deletions
web/package.json+1−0
| @@ -15,6 +15,7 @@ | ||
"@sveltejs/kit": "^3.0.0", | ||
"@sveltejs/vite-plugin-svelte": "^7.2.0", | ||
"@types/node": "^24.19.2", | ||
"pele": "^1.8.0", | ||
"svelte": "^5.57.1", | ||
"svelte-check": "^4.6.0", | ||
"typescript": "^6.0.3", | ||
web/src/lib/mermaid.tsnew+59−0
| @@ -0,0 +1,59 @@ | ||
// Mermaid diagrams in Markdown, drawn in the browser by Pele | ||
// (https://pele.run). The server sends a ```mermaid block as code, so a | ||
// page without scripts still shows its source; this swaps each block for | ||
// its diagram, loading Pele, and each kind of diagram, only once a page | ||
// has one. | ||
| ||
import type { Mounted, MountOptions } from 'pele/lazy'; | ||
| ||
const BLOCKS = 'pre > code.language-mermaid'; | ||
| ||
/** Anyone with a repository writes diagrams, so their links are not vouched for. */ | ||
const OPTIONS: MountOptions = { linkRel: 'noopener nofollow ugc' }; | ||
| ||
/** An action drawing the Mermaid blocks in its element, again as the HTML it shows changes. */ | ||
export function diagrams(el: HTMLElement, _html: string) { | ||
let mounted: Mounted[] = []; | ||
let run = 0; | ||
const clear = () => { | ||
run++; | ||
mounted.forEach((m) => m.destroy()); | ||
mounted = []; | ||
}; | ||
async function draw() { | ||
const id = ++run; | ||
const blocks = [...el.querySelectorAll<HTMLElement>(BLOCKS)]; | ||
if (!blocks.length) return; | ||
let pele: typeof import('pele/lazy'); | ||
try { | ||
pele = await import('pele/lazy'); | ||
} catch { | ||
// Not loaded: the source stays shown. | ||
return; | ||
} | ||
for (const code of blocks) { | ||
const pre = code.parentElement; | ||
if (id !== run || !pre?.isConnected) return; | ||
const box = document.createElement('div'); | ||
box.className = 'diagram'; | ||
pre.replaceWith(box); | ||
try { | ||
const m = await pele.mountAsync(box, code.textContent ?? '', OPTIONS); | ||
if (id === run) mounted.push(m); | ||
else m.destroy(); | ||
} catch { | ||
// Its kind of diagram failed to load: show the source again. | ||
box.replaceWith(pre); | ||
} | ||
} | ||
} | ||
// After the HTML it is given is in place. | ||
queueMicrotask(draw); | ||
return { | ||
update() { | ||
clear(); | ||
queueMicrotask(draw); | ||
}, | ||
destroy: clear | ||
}; | ||
} | ||
web/src/lib/organisms/Comment.svelte+2−1
| @@ -1,6 +1,7 @@ | ||
<script lang="ts"> | ||
import type { Snippet } from 'svelte'; | ||
import { date } from '#lib/format.ts'; | ||
import { diagrams } from '#lib/mermaid.ts'; | ||
import Avatar from '#lib/atoms/Avatar.svelte'; | ||
import Person from '#lib/molecules/Person.svelte'; | ||
/** Something said on a pull or an issue: who said it, when, and what. */ | ||
| @@ -15,5 +16,5 @@ | ||
<span class="mute">{said} on {date(createdAt)}</span> | ||
{#if end}<span class="end">{@render end()}</span>{/if} | ||
</div> | ||
<div class="prose">{@html html}</div> | ||
<div class="prose" use:diagrams={html}>{@html html}</div> | ||
</div> | ||
web/src/lib/organisms/Readme.svelte+2−1
| @@ -1,13 +1,14 @@ | ||
<script lang="ts"> | ||
import type { Readme } from '#lib/api/Readme.ts'; | ||
import Icon from '#lib/atoms/Icon.svelte'; | ||
import { diagrams } from '#lib/mermaid.ts'; | ||
let { readme }: { readme: Readme } = $props(); | ||
</script> | ||
| ||
<article class="box"> | ||
<div class="boxhead"><Icon name="file" /><b>{readme.name}</b></div> | ||
{#if readme.html !== null} | ||
<div class="prose">{@html readme.html}</div> | ||
<div class="prose" use:diagrams={readme.html}>{@html readme.html}</div> | ||
{:else} | ||
<div class="prose"><pre>{readme.text}</pre></div> | ||
{/if} | ||
web/src/routes/[authority]/[repo]/blob/[...rest]/+page.svelte+2−1
| @@ -1,6 +1,7 @@ | ||
<script lang="ts"> | ||
import { page } from '$app/state'; | ||
import { repoHref, size, urlPath } from '#lib/format.ts'; | ||
import { diagrams } from '#lib/mermaid.ts'; | ||
import Icon from '#lib/atoms/Icon.svelte'; | ||
import CodeView from '#lib/organisms/CodeView.svelte'; | ||
import Toolbar from '#lib/organisms/Toolbar.svelte'; | ||
| @@ -36,7 +37,7 @@ | ||
{:else if b.shown === 'large'} | ||
<div class="note">Too large to show here: view it raw.</div> | ||
{:else if b.html !== null && !plain} | ||
<div class="prose">{@html b.html}</div> | ||
<div class="prose" use:diagrams={b.html}>{@html b.html}</div> | ||
{:else} | ||
<CodeView text={b.text ?? ''} lines={b.lines} ranges={b.ranges} /> | ||
{/if} | ||