chadtmiller.com/knotless
git that lives in your Atmosphere account
1
10
The pages, the harness and the docs site are formatted by oxfmt
.claude/hooks/format.mjsnew+34−0
| @@ -0,0 +1,34 @@ | ||
// Claude Code's PostToolUse hook: formats each file an agent writes as | ||
// `just fmt` would, so what is committed passes the format checks. Rust | ||
// by rustfmt; the pages', the harness's and the docs site's code by oxfmt, | ||
// which skips what .oxfmtrc.json ignores. Anything else, or a file a | ||
// formatter cannot parse yet, is left as written: the checks still catch | ||
// what is pushed. | ||
import { execFileSync } from 'node:child_process'; | ||
import { existsSync, readFileSync } from 'node:fs'; | ||
import { relative, resolve } from 'node:path'; | ||
| ||
const root = resolve(import.meta.dirname, '../..'); | ||
const input = JSON.parse(readFileSync(0, 'utf8')); | ||
const file = input.tool_input?.file_path; | ||
if (!file || !existsSync(file)) process.exit(0); | ||
const path = relative(root, resolve(file)); | ||
| ||
const run = (cmd, args) => { | ||
try { | ||
execFileSync(cmd, args, { cwd: root, stdio: 'ignore' }); | ||
} catch { | ||
// Left as written. | ||
} | ||
}; | ||
| ||
if (path.endsWith('.rs')) { | ||
run('rustup', ['run', 'stable', 'rustfmt', '--edition', '2024', path]); | ||
} else if ( | ||
/^(web|harness|docs-site)\//.test(path) && | ||
!path.includes('node_modules/') && | ||
/\.(svelte|ts|js|mjs|cjs|css|json)$/.test(path) | ||
) { | ||
const oxfmt = resolve(root, 'web/node_modules/.bin/oxfmt'); | ||
if (existsSync(oxfmt)) run(oxfmt, ['--no-error-on-unmatched-pattern', path]); | ||
} | ||
.claude/settings.jsonnew+15−0
| @@ -0,0 +1,15 @@ | ||
{ | ||
"hooks": { | ||
"PostToolUse": [ | ||
{ | ||
"matcher": "Edit|MultiEdit|Write", | ||
"hooks": [ | ||
{ | ||
"type": "command", | ||
"command": "node \"$CLAUDE_PROJECT_DIR/.claude/hooks/format.mjs\"" | ||
} | ||
] | ||
} | ||
] | ||
} | ||
} | ||
.knotless/workflows/web.yml+11−8
| @@ -1,23 +1,26 @@ | ||
# The web pages' checks, as `just lint` runs them: svelte-check, which | ||
# fails on any warning, that each component uses only its own layer | ||
# and those below (harness/layers.mjs), and that every theme's text meets | ||
# WCAG AA contrast on what it is drawn on (harness/contrast.mjs). The | ||
# packages are kept between runs and installed again only when the | ||
# The web pages' checks, as `just lint` runs them: that the pages, the | ||
# harness and the docs site are formatted as `just fmt` leaves them (oxfmt), | ||
# svelte-check, which fails on any warning, that each component uses only | ||
# its own layer and those below (harness/layers.mjs), and that every theme's | ||
# text meets WCAG AA contrast on what it is drawn on (harness/contrast.mjs). | ||
# The packages are kept between runs and installed again only when the | ||
# lockfile changes. | ||
image: node:24.22.1 | ||
# Every push to main, and pulls into it or a feature/ branch, at their | ||
# heads merged with their base, that change the pages, the layers' check | ||
# or this file. A pull changing none passes at once, noted as skipped. | ||
# heads merged with their base, that change the pages, the harness, the | ||
# docs site, the format's settings or this file. A pull changing none passes at once, noted as skipped. | ||
on: | ||
push: {refs: [refs/heads/main]} | ||
pull: | ||
base: [refs/heads/main, refs/heads/feature/*] | ||
paths: [web/**, lexicons/**, harness/layers.mjs, harness/contrast.mjs, .knotless/workflows/web.yml, .knotless/uses.lock] | ||
paths: [web/**, lexicons/**, harness/**, docs-site/**, .oxfmtrc.json, .knotless/workflows/web.yml, .knotless/uses.lock] | ||
cache: [web/node_modules] | ||
steps: | ||
- uses: chadtmiller.com/steps/npm-ci@1 | ||
name: packages | ||
with: {dir: web} | ||
- name: format | ||
run: web/node_modules/.bin/oxfmt --check web harness docs-site | ||
- name: layers | ||
run: node harness/layers.mjs | ||
- name: contrast | ||
docs/development.md+9−1
| @@ -34,7 +34,8 @@ just setup # npm install in harness/ and web/, llvm-tools for coverage | ||
just test # every test | ||
just test-spaces # against the alpha: KNOTLESS_DEV_ENV names its dev-env | ||
just cov # both runs side by side, failing under 100% lines or functions | ||
just lint # rustfmt, clippy and svelte-check | ||
just fmt # rustfmt and oxfmt | ||
just lint # rustfmt, oxfmt, clippy and svelte-check | ||
just lex # lint the lexicons, and check they only grow from main's | ||
just bench 50 # the performance table, at 50 ms RTT | ||
just bench-cards # what a social card costs to draw, with the PNGs | ||
| @@ -77,6 +78,13 @@ knotless-serve. | ||
the root gets `data-ready` once mounted. Screenshots wait for both. | ||
- Components are layered as atoms, molecules, organisms and templates; a | ||
component uses its own layer and those below, and `just lint` checks it. | ||
- The pages, the harness and the docs site are formatted by | ||
[oxfmt](https://oxc.rs/docs/guide/usage/formatter), set in `.oxfmtrc.json`: | ||
tabs in `web/`, and two spaces and no semicolons in the other two. Markdown, | ||
YAML, the recorded fixtures and the lexicons are left as they are. `just fmt` | ||
formats them, the `web` check fails on what it would change, and | ||
`.claude/settings.json` has Claude Code format each file it writes. oxfmt comes | ||
from `web/node_modules`, and may need a second run to settle. | ||
| ||
The queries are listed in the [API reference](reference/api.md#web-data). | ||
| ||
docs-site/src/content.config.ts+5−5
| @@ -1,8 +1,8 @@ | ||
import { defineCollection } from 'astro:content'; | ||
import { docsLoader } from '@astrojs/starlight/loaders'; | ||
import { docsSchema } from '@astrojs/starlight/schema'; | ||
import { defineCollection } from 'astro:content' | ||
import { docsLoader } from '@astrojs/starlight/loaders' | ||
import { docsSchema } from '@astrojs/starlight/schema' | ||
| ||
// The pages content.mjs copies from docs/. | ||
export const collections = { | ||
docs: defineCollection({ loader: docsLoader(), schema: docsSchema() }) | ||
}; | ||
docs: defineCollection({ loader: docsLoader(), schema: docsSchema() }), | ||
} | ||
docs-site/src/theme.css+129−124
| @@ -8,213 +8,218 @@ | ||
| ||
:root, | ||
:root[data-theme='light'] { | ||
--sl-color-white: var(--fg); | ||
--sl-color-gray-1: var(--fg); | ||
--sl-color-gray-2: var(--fg); | ||
--sl-color-gray-3: var(--mute); | ||
--sl-color-gray-4: var(--faint); | ||
--sl-color-gray-5: var(--line); | ||
--sl-color-gray-6: var(--soft); | ||
--sl-color-gray-7: var(--panel); | ||
--sl-color-black: var(--bg); | ||
--sl-color-accent-low: var(--accent-soft); | ||
--sl-color-accent: var(--primary); | ||
--sl-color-accent-high: var(--accent); | ||
--sl-color-bg-nav: var(--bg); | ||
--sl-color-bg-sidebar: var(--bg); | ||
--sl-color-hairline: var(--line); | ||
--sl-color-hairline-light: var(--line); | ||
--sl-color-hairline-shade: var(--line); | ||
--sl-font: var(--font-sans); | ||
--sl-font-mono: var(--font-mono); | ||
--sl-line-height: var(--leading-prose); | ||
/* Type on the pages' prose scale, not Starlight's larger one. */ | ||
--sl-text-body: var(--text-md); | ||
--sl-text-h1: var(--text-3xl); | ||
--sl-text-h2: var(--text-xl); | ||
--sl-text-h3: var(--text-lg); | ||
--sl-text-h4: var(--text-md); | ||
--sl-text-h5: var(--text-md); | ||
--sl-text-h6: var(--text-md); | ||
--sl-color-white: var(--fg); | ||
--sl-color-gray-1: var(--fg); | ||
--sl-color-gray-2: var(--fg); | ||
--sl-color-gray-3: var(--mute); | ||
--sl-color-gray-4: var(--faint); | ||
--sl-color-gray-5: var(--line); | ||
--sl-color-gray-6: var(--soft); | ||
--sl-color-gray-7: var(--panel); | ||
--sl-color-black: var(--bg); | ||
--sl-color-accent-low: var(--accent-soft); | ||
--sl-color-accent: var(--primary); | ||
--sl-color-accent-high: var(--accent); | ||
--sl-color-bg-nav: var(--bg); | ||
--sl-color-bg-sidebar: var(--bg); | ||
--sl-color-hairline: var(--line); | ||
--sl-color-hairline-light: var(--line); | ||
--sl-color-hairline-shade: var(--line); | ||
--sl-font: var(--font-sans); | ||
--sl-font-mono: var(--font-mono); | ||
--sl-line-height: var(--leading-prose); | ||
/* Type on the pages' prose scale, not Starlight's larger one. */ | ||
--sl-text-body: var(--text-md); | ||
--sl-text-h1: var(--text-3xl); | ||
--sl-text-h2: var(--text-xl); | ||
--sl-text-h3: var(--text-lg); | ||
--sl-text-h4: var(--text-md); | ||
--sl-text-h5: var(--text-md); | ||
--sl-text-h6: var(--text-md); | ||
} | ||
| ||
h1, h2, h3, h4, h5, h6 { | ||
font-weight: var(--weight-semibold); | ||
letter-spacing: var(--tracking-tight); | ||
h1, | ||
h2, | ||
h3, | ||
h4, | ||
h5, | ||
h6 { | ||
font-weight: var(--weight-semibold); | ||
letter-spacing: var(--tracking-tight); | ||
} | ||
| ||
/* A section's heading is ruled off, as in the pages' prose. */ | ||
.sl-markdown-content .level-h2 { | ||
padding-bottom: var(--space-1-5); | ||
border-bottom: var(--border); | ||
padding-bottom: var(--space-1-5); | ||
border-bottom: var(--border); | ||
} | ||
| ||
/* The header: the pages' brand, in their mono. */ | ||
.brand { | ||
display: flex; | ||
align-items: baseline; | ||
gap: var(--space-2); | ||
font: var(--weight-semibold) var(--text-base) var(--font-mono); | ||
color: var(--fg); | ||
text-decoration: none; | ||
white-space: nowrap; | ||
display: flex; | ||
align-items: baseline; | ||
gap: var(--space-2); | ||
font: var(--weight-semibold) var(--text-base) var(--font-mono); | ||
color: var(--fg); | ||
text-decoration: none; | ||
white-space: nowrap; | ||
} | ||
.brand.section { | ||
font-weight: var(--weight-normal); | ||
color: var(--mute); | ||
font-weight: var(--weight-normal); | ||
color: var(--mute); | ||
} | ||
| ||
/* Links underlined quietly, as the pages' prose has them. */ | ||
.sl-markdown-content a:not(:where(.not-content *)) { | ||
color: var(--fg); | ||
text-decoration-color: var(--link-line); | ||
text-underline-offset: var(--underline-offset); | ||
color: var(--fg); | ||
text-decoration-color: var(--link-line); | ||
text-underline-offset: var(--underline-offset); | ||
} | ||
.sl-markdown-content a:not(:where(.not-content *)):hover { | ||
color: var(--fg); | ||
text-decoration-color: currentColor; | ||
color: var(--fg); | ||
text-decoration-color: currentColor; | ||
} | ||
| ||
/* Sidebar: the page one is on marked as the pages mark the tab one is on. */ | ||
.sidebar-content a[aria-current='page'] { | ||
background: var(--accent-soft); | ||
color: var(--fg); | ||
font-weight: var(--weight-semibold); | ||
background: var(--accent-soft); | ||
color: var(--fg); | ||
font-weight: var(--weight-semibold); | ||
} | ||
| ||
/* Diagrams take the page's colours, as the repository pages' do. */ | ||
.sl-markdown-content .diagram { | ||
line-height: normal; | ||
overflow-x: auto; | ||
--pele-bg: var(--bg); | ||
--pele-fg: var(--fg); | ||
--pele-muted: var(--mute); | ||
--pele-line: var(--mute); | ||
--pele-surface: var(--soft); | ||
--pele-surface-alt: var(--panel); | ||
--pele-border: var(--faint); | ||
--pele-accent: var(--primary); | ||
--pele-critical: var(--rem); | ||
--pele-error: var(--rem); | ||
--pele-font: var(--font-sans); | ||
--pele-font-mono: var(--font-mono); | ||
line-height: normal; | ||
overflow-x: auto; | ||
--pele-bg: var(--bg); | ||
--pele-fg: var(--fg); | ||
--pele-muted: var(--mute); | ||
--pele-line: var(--mute); | ||
--pele-surface: var(--soft); | ||
--pele-surface-alt: var(--panel); | ||
--pele-border: var(--faint); | ||
--pele-accent: var(--primary); | ||
--pele-critical: var(--rem); | ||
--pele-error: var(--rem); | ||
--pele-font: var(--font-sans); | ||
--pele-font-mono: var(--font-mono); | ||
} | ||
| ||
/* Reference: a table of names and what each does, drawn as a list of | ||
them (reference.mjs), each name on a line of its own. */ | ||
.sl-markdown-content .ref-list { | ||
margin-top: var(--space-4); | ||
border-top: var(--border); | ||
margin-top: var(--space-4); | ||
border-top: var(--border); | ||
} | ||
.sl-markdown-content .ref-list dt { | ||
margin-top: 0; | ||
padding-top: var(--space-4); | ||
font-weight: var(--weight-normal); | ||
margin-top: 0; | ||
padding-top: var(--space-4); | ||
font-weight: var(--weight-normal); | ||
} | ||
.sl-markdown-content .ref-list dt code { | ||
background: none; | ||
padding: 0; | ||
font-size: var(--text-md); | ||
font-weight: var(--weight-semibold); | ||
/* A signature breaks between its words, never inside a flag. */ | ||
overflow-wrap: normal; | ||
word-break: normal; | ||
background: none; | ||
padding: 0; | ||
font-size: var(--text-md); | ||
font-weight: var(--weight-semibold); | ||
/* A signature breaks between its words, never inside a flag. */ | ||
overflow-wrap: normal; | ||
word-break: normal; | ||
} | ||
.sl-markdown-content .ref-list dd { | ||
margin: var(--space-1) 0 0; | ||
padding-bottom: var(--space-4); | ||
border-bottom: var(--border); | ||
margin: var(--space-1) 0 0; | ||
padding-bottom: var(--space-4); | ||
border-bottom: var(--border); | ||
} | ||
.sl-markdown-content .ref-list dd > * + * { | ||
margin-top: var(--space-2); | ||
margin-top: var(--space-2); | ||
} | ||
.sl-markdown-content .ref-meta { | ||
font-size: var(--text-sm); | ||
color: var(--mute); | ||
font-size: var(--text-sm); | ||
color: var(--mute); | ||
} | ||
.ref-label { | ||
font-size: var(--text-sm); | ||
font-weight: var(--weight-medium); | ||
color: var(--faint); | ||
margin-right: var(--space-1); | ||
font-size: var(--text-sm); | ||
font-weight: var(--weight-medium); | ||
color: var(--faint); | ||
margin-right: var(--space-1); | ||
} | ||
| ||
/* An HTTP method, as a badge before its route. */ | ||
.method { | ||
display: inline-block; | ||
padding: 0 var(--space-1-5); | ||
border-radius: var(--radius-sm); | ||
font: var(--weight-semibold) var(--text-2xs) / 1.6 var(--font-mono); | ||
letter-spacing: 0.02em; | ||
vertical-align: 0.15em; | ||
background: var(--accent-soft); | ||
color: var(--accent); | ||
display: inline-block; | ||
padding: 0 var(--space-1-5); | ||
border-radius: var(--radius-sm); | ||
font: var(--weight-semibold) var(--text-2xs) / 1.6 var(--font-mono); | ||
letter-spacing: 0.02em; | ||
vertical-align: 0.15em; | ||
background: var(--accent-soft); | ||
color: var(--accent); | ||
} | ||
.method-post { | ||
background: var(--soft); | ||
color: var(--green); | ||
background: var(--soft); | ||
color: var(--green); | ||
} | ||
.method-ws { | ||
color: var(--purple); | ||
color: var(--purple); | ||
} | ||
| ||
/* A method's lexicon, folded away under it. */ | ||
.sl-markdown-content .lexicon { | ||
margin-top: var(--space-2); | ||
border: var(--border); | ||
border-radius: var(--radius-md); | ||
font-size: var(--text-sm); | ||
margin-top: var(--space-2); | ||
border: var(--border); | ||
border-radius: var(--radius-md); | ||
font-size: var(--text-sm); | ||
} | ||
.sl-markdown-content .lexicon summary { | ||
padding: var(--space-1-5) var(--space-3); | ||
color: var(--mute); | ||
cursor: pointer; | ||
padding: var(--space-1-5) var(--space-3); | ||
color: var(--mute); | ||
cursor: pointer; | ||
} | ||
.sl-markdown-content .lexicon[open] summary { | ||
border-bottom: var(--border); | ||
border-bottom: var(--border); | ||
} | ||
.sl-markdown-content .lexicon > :not(summary) { | ||
margin: var(--space-3); | ||
margin: var(--space-3); | ||
} | ||
.sl-markdown-content .lex-title { | ||
font-weight: var(--weight-semibold); | ||
color: var(--fg); | ||
margin-bottom: var(--space-1) !important; | ||
font-weight: var(--weight-semibold); | ||
color: var(--fg); | ||
margin-bottom: var(--space-1) !important; | ||
} | ||
.sl-markdown-content .lex-fields { | ||
display: table; | ||
table-layout: fixed; | ||
width: calc(100% - 2 * var(--space-3)); | ||
font-size: var(--text-sm); | ||
display: table; | ||
table-layout: fixed; | ||
width: calc(100% - 2 * var(--space-3)); | ||
font-size: var(--text-sm); | ||
} | ||
.sl-markdown-content .lex-fields td { | ||
vertical-align: top; | ||
vertical-align: top; | ||
} | ||
.lex-type { | ||
display: block; | ||
margin-top: var(--space-0-5); | ||
color: var(--mute); | ||
font-size: var(--text-xs); | ||
display: block; | ||
margin-top: var(--space-0-5); | ||
color: var(--mute); | ||
font-size: var(--text-xs); | ||
} | ||
.sl-markdown-content .lex-fields td:first-child { | ||
width: 40%; | ||
width: 40%; | ||
} | ||
.sl-markdown-content .ref-list dt .word { | ||
white-space: nowrap; | ||
white-space: nowrap; | ||
} | ||
.sl-markdown-content .lex-source { | ||
color: var(--mute); | ||
color: var(--mute); | ||
} | ||
| ||
/* The header's link to the repository, quiet as the pages' header links | ||
are, and no divider after it: Starlight's sits before the theme | ||
picker, which the docs leave out. */ | ||
.social-icons::after { | ||
display: none; | ||
display: none; | ||
} | ||
.social-icons a { | ||
color: var(--mute); | ||
color: var(--mute); | ||
} | ||
.social-icons a:hover { | ||
color: var(--fg); | ||
color: var(--fg); | ||
} | ||
docs-site/astro.config.mjs+12−2
| @@ -13,14 +13,24 @@ const page = (label, slug) => ({ label, slug }) | ||
export default defineConfig({ | ||
site: 'https://knotless.dev', | ||
base, | ||
markdown: { processor: unified({ remarkPlugins: [[remarkLinks, { base }], remarkMermaid, remarkReference] }) }, | ||
markdown: { | ||
processor: unified({ | ||
remarkPlugins: [[remarkLinks, { base }], remarkMermaid, remarkReference], | ||
}), | ||
}, | ||
integrations: [ | ||
starlight({ | ||
title: 'knotless docs', | ||
description: 'Git repositories stored in atproto accounts: guides, reference and design.', | ||
customCss: ['./src/theme.css'], | ||
components: { SiteTitle: './src/SiteTitle.astro', ThemeSelect: './src/ThemeSelect.astro' }, | ||
social: [{ icon: 'seti:git', label: 'The repository', href: 'https://knotless.dev/chadtmiller.com/knotless' }], | ||
social: [ | ||
{ | ||
icon: 'seti:git', | ||
label: 'The repository', | ||
href: 'https://knotless.dev/chadtmiller.com/knotless', | ||
}, | ||
], | ||
lastUpdated: false, | ||
// Code as the pages show it: a plain block, GitHub's colours. | ||
expressiveCode: { | ||
docs-site/package.json+15−15
| @@ -1,17 +1,17 @@ | ||
{ | ||
"name": "knotless-docs", | ||
"private": true, | ||
"type": "module", | ||
"scripts": { | ||
"content": "node content.mjs", | ||
"dev": "node content.mjs && astro dev", | ||
"build": "node content.mjs && astro build --force", | ||
"preview": "astro preview" | ||
}, | ||
"dependencies": { | ||
"@astrojs/markdown-remark": "7.3.5", | ||
"@astrojs/starlight": "0.42.7", | ||
"astro": "7.4.3", | ||
"pele": "1.8.0" | ||
} | ||
"name": "knotless-docs", | ||
"private": true, | ||
"type": "module", | ||
"scripts": { | ||
"content": "node content.mjs", | ||
"dev": "node content.mjs && astro dev", | ||
"build": "node content.mjs && astro build --force", | ||
"preview": "astro preview" | ||
}, | ||
"dependencies": { | ||
"@astrojs/markdown-remark": "7.3.5", | ||
"@astrojs/starlight": "0.42.7", | ||
"astro": "7.4.3", | ||
"pele": "1.8.0" | ||
} | ||
} | ||
docs-site/plugins.mjs+6−2
| @@ -23,7 +23,10 @@ function resolve(page, url, base) { | ||
const target = posix.normalize(posix.join('docs', posix.dirname(page), path)) | ||
const hash = anchor ? `#${anchor}` : '' | ||
if (target.startsWith('docs/') && target.endsWith('.md') && target !== 'docs/design.md') { | ||
const slug = target.slice('docs/'.length).replace(/\.md$/, '').replace(/(^|\/)README$/, '') | ||
const slug = target | ||
.slice('docs/'.length) | ||
.replace(/\.md$/, '') | ||
.replace(/(^|\/)README$/, '') | ||
return `${base}/${slug ? `${slug.toLowerCase()}/` : ''}${hash}` | ||
} | ||
if (target === 'docs/design.md') return `${base}/design/${hash}` | ||
| @@ -37,7 +40,8 @@ export function remarkLinks({ base }) { | ||
return (tree, file) => { | ||
const page = relative(content, file.path).replace(/(^|\/)index\.md$/, '$1README.md') | ||
walk(tree, (node) => { | ||
if (node.type === 'link' || node.type === 'definition') node.url = resolve(page, node.url, base) | ||
if (node.type === 'link' || node.type === 'definition') | ||
node.url = resolve(page, node.url, base) | ||
}) | ||
} | ||
} | ||
docs-site/reference.mjs+86−18
| @@ -26,9 +26,16 @@ const lexicons = new Map( | ||
const METHOD = { query: 'GET', procedure: 'POST', subscription: 'WS' } | ||
| ||
const text = (node) => | ||
node.type === 'text' || node.type === 'inlineCode' ? node.value : (node.children ?? []).map(text).join('') | ||
node.type === 'text' || node.type === 'inlineCode' | ||
? node.value | ||
: (node.children ?? []).map(text).join('') | ||
| ||
const escape = (s) => String(s).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"') | ||
const escape = (s) => | ||
String(s) | ||
.replace(/&/g, '&') | ||
.replace(/</g, '<') | ||
.replace(/>/g, '>') | ||
.replace(/"/g, '"') | ||
| ||
/** `GET /refs`, `POST dev.knotless.x`, `dev.knotless.x?q=`: its method, and the NSID it names, if any. */ | ||
function route(code) { | ||
| @@ -52,7 +59,8 @@ function type(field) { | ||
break | ||
case 'integer': | ||
bits.push('integer') | ||
if (field.minimum !== undefined && field.maximum !== undefined) bits.push(`${field.minimum} to ${field.maximum}`) | ||
if (field.minimum !== undefined && field.maximum !== undefined) | ||
bits.push(`${field.minimum} to ${field.maximum}`) | ||
break | ||
case 'array': | ||
bits.push(`array of ${type(field.items)}`) | ||
| @@ -62,7 +70,9 @@ function type(field) { | ||
bits.push(field.ref.replace(/^#/, '').replace(/^dev\.knotless\./, '')) | ||
break | ||
case 'union': | ||
bits.push(`one of ${field.refs.map((r) => r.replace(/^#/, '').replace(/^dev\.knotless\./, '')).join(', ')}`) | ||
bits.push( | ||
`one of ${field.refs.map((r) => r.replace(/^#/, '').replace(/^dev\.knotless\./, '')).join(', ')}`, | ||
) | ||
break | ||
default: | ||
bits.push(field.type) | ||
| @@ -99,7 +109,9 @@ function lexicon(nsid) { | ||
fields('Parameters', main.parameters) + | ||
fields('Input', main.input?.schema) + | ||
fields('Output', main.output?.schema) + | ||
(main.errors?.length ? `<p class="lex-title">Errors</p><p>${main.errors.map((e) => `<code>${escape(e.name)}</code>`).join(', ')}</p>` : '') | ||
(main.errors?.length | ||
? `<p class="lex-title">Errors</p><p>${main.errors.map((e) => `<code>${escape(e.name)}</code>`).join(', ')}</p>` | ||
: '') | ||
const source = `${REPO}/blob/main/lexicons/dev/knotless/${nsid.split('.').pop()}.json` | ||
return ( | ||
`<details class="lexicon"><summary>Lexicon</summary>` + | ||
| @@ -110,12 +122,20 @@ function lexicon(nsid) { | ||
} | ||
| ||
/** A heading's anchor as GitHub, and harness/docs.mjs, make it. */ | ||
const slug = (s) => s.trim().toLowerCase().replace(/[^\p{L}\p{N}_\- ]/gu, '').replaceAll(' ', '-') | ||
const slug = (s) => | ||
s | ||
.trim() | ||
.toLowerCase() | ||
.replace(/[^\p{L}\p{N}_\- ]/gu, '') | ||
.replaceAll(' ', '-') | ||
| ||
const badge = (method) => | ||
method && { | ||
type: 'methodBadge', | ||
data: { hName: 'span', hProperties: { className: ['method', `method-${method.toLowerCase()}`] } }, | ||
data: { | ||
hName: 'span', | ||
hProperties: { className: ['method', `method-${method.toLowerCase()}`] }, | ||
}, | ||
children: [{ type: 'text', value: method }], | ||
} | ||
const gap = { type: 'text', value: ' ' } | ||
| @@ -123,7 +143,10 @@ const gap = { type: 'text', value: ' ' } | ||
/** A command or route as code that breaks only between its words: a browser would break inside `--flag` after its hyphens. */ | ||
const signature = (code) => ({ | ||
type: 'html', | ||
value: `<code>${code.split(/(\s+)/).map((w) => (/\s/.test(w) ? w : `<span class="word">${escape(w)}</span>`)).join('')}</code>`, | ||
value: `<code>${code | ||
.split(/(\s+)/) | ||
.map((w) => (/\s/.test(w) ? w : `<span class="word">${escape(w)}</span>`)) | ||
.join('')}</code>`, | ||
}) | ||
| ||
/** A cell's code, if the cell starts with some. */ | ||
| @@ -148,19 +171,49 @@ function asList(table) { | ||
const { method, nsid } = route(lead(first)) | ||
const [code, ...others] = first.children | ||
const shown = [signature(method ? code.value.replace(/^[A-Z]+\s+/, '') : code.value), ...others] | ||
const term = { type: 'refTerm', data: { hName: 'dt' }, children: method ? [badge(method), gap, ...shown] : shown } | ||
const term = { | ||
type: 'refTerm', | ||
data: { hName: 'dt' }, | ||
children: method ? [badge(method), gap, ...shown] : shown, | ||
} | ||
const middle = rest.slice(0, -1).flatMap((cell, i) => | ||
text(cell).trim() | ||
? [{ type: 'refMeta', data: { hName: 'p', hProperties: { className: ['ref-meta'] } }, children: [{ type: 'html', value: `<span class="ref-label">${escape(heads[i + 1])}</span> ` }, ...cell.children] }] | ||
? [ | ||
{ | ||
type: 'refMeta', | ||
data: { hName: 'p', hProperties: { className: ['ref-meta'] } }, | ||
children: [ | ||
{ | ||
type: 'html', | ||
value: `<span class="ref-label">${escape(heads[i + 1])}</span> `, | ||
}, | ||
...cell.children, | ||
], | ||
}, | ||
] | ||
: [], | ||
) | ||
// The last column is the description; labelled too when others are, so each says what it is. | ||
const label = rest.length > 1 && heads.at(-1) ? [{ type: 'html', value: `<span class="ref-label">${escape(heads.at(-1))}</span> ` }] : [] | ||
const description = { type: 'paragraph', children: [...label, ...(rest.at(-1)?.children ?? [])] } | ||
const detail = { type: 'refDetail', data: { hName: 'dd' }, children: [...middle, description, ...(nsid ? [{ type: 'html', value: lexicon(nsid) }] : [])] } | ||
const label = | ||
rest.length > 1 && heads.at(-1) | ||
? [{ type: 'html', value: `<span class="ref-label">${escape(heads.at(-1))}</span> ` }] | ||
: [] | ||
const description = { | ||
type: 'paragraph', | ||
children: [...label, ...(rest.at(-1)?.children ?? [])], | ||
} | ||
const detail = { | ||
type: 'refDetail', | ||
data: { hName: 'dd' }, | ||
children: [...middle, description, ...(nsid ? [{ type: 'html', value: lexicon(nsid) }] : [])], | ||
} | ||
return [term, detail] | ||
}) | ||
return { type: 'refList', data: { hName: 'dl', hProperties: { className: ['ref-list'] } }, children: items } | ||
return { | ||
type: 'refList', | ||
data: { hName: 'dl', hProperties: { className: ['ref-list'] } }, | ||
children: items, | ||
} | ||
} | ||
| ||
export function remarkReference() { | ||
| @@ -168,18 +221,33 @@ export function remarkReference() { | ||
const visit = (parent) => { | ||
parent.children?.forEach((node, i) => { | ||
if (node.type === 'table' && definitional(node)) parent.children[i] = asList(node) | ||
else if (node.type === 'heading' && node.children.length === 1 && node.children[0].type === 'inlineCode') { | ||
else if ( | ||
node.type === 'heading' && | ||
node.children.length === 1 && | ||
node.children[0].type === 'inlineCode' | ||
) { | ||
// `### \`GET /healthz\``, `### \`dev.knotless.listRepos\``: the method a badge before it. The | ||
// anchor is fixed first, from the heading as written, so links to it still land. | ||
const code = node.children[0] | ||
const { method, nsid } = route(code.value) | ||
if (!method) return | ||
node.data = { ...node.data, hProperties: { ...node.data?.hProperties, id: slug(code.value) } } | ||
node.children = [badge(method), gap, { ...code, value: code.value.replace(/^[A-Z]+\s+/, '') }] | ||
node.data = { | ||
...node.data, | ||
hProperties: { ...node.data?.hProperties, id: slug(code.value) }, | ||
} | ||
node.children = [ | ||
badge(method), | ||
gap, | ||
{ ...code, value: code.value.replace(/^[A-Z]+\s+/, '') }, | ||
] | ||
if (nsid) { | ||
// At the end of its section, after what is said of it. | ||
let end = i + 1 | ||
while (end < parent.children.length && !(parent.children[end].type === 'heading' && parent.children[end].depth <= node.depth)) end++ | ||
while ( | ||
end < parent.children.length && | ||
!(parent.children[end].type === 'heading' && parent.children[end].depth <= node.depth) | ||
) | ||
end++ | ||
parent.children.splice(end, 0, { type: 'html', value: lexicon(nsid) }) | ||
} | ||
} else visit(node) | ||
harness/e2e/flows.spec.mjs+349−96
445 lines changed: not loaded until asked for.
harness/e2e/pages.mjs+17−5
harness/e2e/private.spec.mjs+87−24
harness/e2e/setup.mjs+24−3
harness/shots/compare.mjs+41−13
harness/shots/fixtures.mjs+176−36
harness/shots/look.mjs+69−21
harness/shots/outputs.mjs+15−3
harness/shots/playwright.config.mjs+18−3
harness/shots/setup.mjs+15−4
harness/shots/views.spec.mjs+60−7
harness/shots/world.mjs+604−128
732 lines changed: not loaded until asked for.
harness/answers.mjs+18−5
harness/authorize.mjs+6−3
harness/changed.mjs+32−9
harness/contrast.mjs+9−4
harness/cov.mjs+196−59
harness/docs.mjs+12−4
harness/files-bench.mjs+29−8
harness/layers.mjs+6−2
harness/lexicons.mjs+72−29
harness/package.json+1−1
harness/pds.mjs+39−24
harness/plans.mjs+7−2
harness/quarantine.json+20−5
harness/queue-stats.mjs+107−48
harness/scenarios.mjs+39−23
harness/star-fixture.mjs+17−3
web/src/lib/atoms/ArchivedTag.svelte+3−1
web/src/lib/atoms/Avatar.svelte+19−2
web/src/lib/atoms/Bars.svelte+4−1
web/src/lib/atoms/HalfTag.svelte+3−1
web/src/lib/atoms/Label.svelte+3−1
web/src/lib/atoms/Meta.svelte+17−2
web/src/lib/atoms/More.svelte+5−1
web/src/lib/atoms/Pin.svelte+19−6
web/src/lib/atoms/PrivateTag.svelte+2−1
web/src/lib/atoms/ReactionPill.svelte+21−2
web/src/lib/atoms/Said.svelte+5−1
web/src/lib/atoms/Sha.svelte+3−1
web/src/lib/atoms/Spark.svelte+6−2
web/src/lib/molecules/ActorMenu.svelte+15−3
web/src/lib/molecules/AddMenu.svelte+23−4
web/src/lib/molecules/AssigneesFact.svelte+40−5
web/src/lib/molecules/BranchMenu.svelte+56−8
web/src/lib/molecules/CheckMark.svelte+14−2
web/src/lib/molecules/CloneMenu.svelte+9−3
web/src/lib/molecules/Command.svelte+4−1
web/src/lib/molecules/CommitAuthor.svelte+16−2
web/src/lib/molecules/CommitList.svelte+7−1
web/src/lib/molecules/Committed.svelte+20−3
web/src/lib/molecules/Crowd.svelte+16−2
web/src/lib/molecules/Crumbs.svelte+7−1
web/src/lib/molecules/Delegate.svelte+14−2
web/src/lib/molecules/Dialog.svelte+31−3
web/src/lib/molecules/FeedItem.svelte+117−19
web/src/lib/molecules/FileViews.svelte+7−1
web/src/lib/molecules/FilterMenu.svelte+16−2
web/src/lib/molecules/FollowButton.svelte+18−3
web/src/lib/molecules/ForkButton.svelte+34−5
web/src/lib/molecules/HandleInput.svelte+44−5
web/src/lib/molecules/ItemRow.svelte+21−3
web/src/lib/molecules/LabelPicker.svelte+29−3
web/src/lib/molecules/Labels.svelte+3−1
web/src/lib/molecules/LangEdge.svelte+17−3
web/src/lib/molecules/LineQuote.svelte+5−1
web/src/lib/molecules/LinkRows.svelte+27−5
web/src/lib/molecules/MarkdownField.svelte+60−11
web/src/lib/molecules/MentionList.svelte+25−2
web/src/lib/molecules/MinutesNote.svelte+3−1
web/src/lib/molecules/NameCheck.svelte+15−3
web/src/lib/molecules/NotificationItem.svelte+92−13
web/src/lib/molecules/PullTabs.svelte+27−4
web/src/lib/molecules/ReactionBar.svelte+40−6
web/src/lib/molecules/RefCounts.svelte+16−3
web/src/lib/molecules/ReleaseName.svelte+12−3
web/src/lib/molecules/RepoLinks.svelte+3−1
web/src/lib/molecules/RepoMenu.svelte+3−1
web/src/lib/molecules/RepoPeople.svelte+14−2
web/src/lib/molecules/RepoSite.svelte+14−3
web/src/lib/molecules/RepoSwitch.svelte+22−4
web/src/lib/molecules/RepoTopics.svelte+3−1
web/src/lib/molecules/RerunMenu.svelte+29−5
web/src/lib/molecules/ReviewersFact.svelte+88−12
web/src/lib/molecules/RunImages.svelte+6−1
web/src/lib/molecules/SiteLink.svelte+3−1
web/src/lib/molecules/StarButton.svelte+16−3
web/src/lib/molecules/SubscribeButton.svelte+26−5
web/src/lib/molecules/TabLink.svelte+10−3
web/src/lib/molecules/ThemeCard.svelte+22−5
web/src/lib/molecules/Verdict.svelte+13−2
web/src/lib/molecules/WatchMenu.svelte+22−3
web/src/lib/organisms/ActivityRollup.svelte+30−7
web/src/lib/organisms/ActivityStrip.svelte+12−3
web/src/lib/organisms/ActorList.svelte+6−1
web/src/lib/organisms/AgentChat.svelte+24−6
web/src/lib/organisms/AskedFor.svelte+6−1
web/src/lib/organisms/BlameView.svelte+35−6
web/src/lib/organisms/BlockList.svelte+43−9
web/src/lib/organisms/CheckList.svelte+31−5
web/src/lib/organisms/CodeResults.svelte+32−5
web/src/lib/organisms/CodeView.svelte+22−3
web/src/lib/organisms/Comment.svelte+30−3
web/src/lib/organisms/CommitEvent.svelte+8−1
web/src/lib/organisms/Composer.svelte+13−3
web/src/lib/organisms/DangerZone.svelte+88−14
web/src/lib/organisms/DeleteData.svelte+57−13
web/src/lib/organisms/Diff.svelte+5−1
web/src/lib/organisms/DiffFile.svelte+62−9
web/src/lib/organisms/EjectedList.svelte+7−1
web/src/lib/organisms/EmailSettings.svelte+33−7
web/src/lib/organisms/Feed.svelte+11−3
web/src/lib/organisms/FileFinder.svelte+68−11
web/src/lib/organisms/FileList.svelte+63−10
web/src/lib/organisms/FileTree.svelte+21−3
web/src/lib/organisms/GrantNote.svelte+6−2
web/src/lib/organisms/ItemFacts.svelte+14−2
web/src/lib/organisms/ItemList.svelte+52−8
web/src/lib/organisms/LandedList.svelte+8−2
web/src/lib/organisms/LineComposer.svelte+38−5
web/src/lib/organisms/LineThread.svelte+29−5
web/src/lib/organisms/LoginDialog.svelte+50−7
web/src/lib/organisms/MemberList.svelte+25−3
web/src/lib/organisms/MergeBox.svelte+336−105
441 lines changed: not loaded until asked for.
web/src/lib/organisms/MergeChecks.svelte+60−9
web/src/lib/organisms/NewRepoForm.svelte+58−7
web/src/lib/organisms/NotificationList.svelte+10−2
web/src/lib/organisms/NotificationSettings.svelte+51−10
web/src/lib/organisms/OpenFacts.svelte+22−4
web/src/lib/organisms/OpenForm.svelte+54−5
web/src/lib/organisms/OrgDangerZone.svelte+35−15
web/src/lib/organisms/OrgMembers.svelte+229−37
web/src/lib/organisms/OwnerCards.svelte+10−1
web/src/lib/organisms/ProfileCard.svelte+38−7
web/src/lib/organisms/ProfileForm.svelte+63−14
web/src/lib/organisms/ProfileRepos.svelte+61−14
web/src/lib/organisms/PullDiff.svelte+34−4
web/src/lib/organisms/PullStack.svelte+16−5
web/src/lib/organisms/QueueLine.svelte+38−7
web/src/lib/organisms/RefList.svelte+30−3
web/src/lib/organisms/ReleaseCard.svelte+17−4
web/src/lib/organisms/ReleaseList.svelte+30−4
web/src/lib/organisms/RepoCards.svelte+15−2
web/src/lib/organisms/RepoFacts.svelte+15−3
web/src/lib/organisms/RepoHeader.svelte+83−12
web/src/lib/organisms/RepoList.svelte+28−3
web/src/lib/organisms/ReportDialog.svelte+6−1
web/src/lib/organisms/ReportList.svelte+6−3
web/src/lib/organisms/Reviewers.svelte+88−13
web/src/lib/organisms/ReviewMenu.svelte+34−4
web/src/lib/organisms/RunList.svelte+39−9
web/src/lib/organisms/RunnerList.svelte+41−7
web/src/lib/organisms/RunnerMinutes.svelte+13−2
web/src/lib/organisms/RunWorkflow.svelte+49−11
web/src/lib/organisms/SearchPalette.svelte+96−17
web/src/lib/organisms/SecretList.svelte+118−16
web/src/lib/organisms/SettingsAccess.svelte+167−21
web/src/lib/organisms/SettingsBranches.svelte+265−33
web/src/lib/organisms/SettingsGeneral.svelte+82−10
web/src/lib/organisms/SettingsLabels.svelte+84−13
web/src/lib/organisms/SettingsStorage.svelte+14−4
web/src/lib/organisms/SiteHeader.svelte+44−11
web/src/lib/organisms/StackScope.svelte+11−3
web/src/lib/organisms/StartOrgDialog.svelte+30−8
web/src/lib/organisms/StepList.svelte+10−2
web/src/lib/organisms/StepSheet.svelte+38−10
web/src/lib/organisms/TakedownForm.svelte+45−8
web/src/lib/organisms/ThemeBrowser.svelte+42−6
web/src/lib/organisms/ThemeEditor.svelte+121−25
web/src/lib/organisms/ThemeSettings.svelte+220−59
web/src/lib/organisms/Toolbar.svelte+27−2
web/src/lib/organisms/TransferOffer.svelte+9−3
web/src/lib/organisms/UsageDays.svelte+27−4
web/src/lib/organisms/WriteSettings.svelte+4−1
web/src/lib/templates/Listing.svelte+44−4
web/src/lib/templates/ProfilePage.svelte+46−6
web/src/lib/templates/RepoPage.svelte+43−5
web/src/lib/templates/RepoSettings.svelte+23−5
web/src/lib/templates/SettingsPage.svelte+40−7
web/src/lib/activity.ts+35−8
web/src/lib/badges.ts+62−15
web/src/lib/contrast.json+25−5
web/src/lib/contrast.ts+18−6
web/src/lib/data.ts+48−11
web/src/lib/diffs.ts+7−2
web/src/lib/feed.ts+23−4
web/src/lib/format.ts+75−17
web/src/lib/highlight.ts+14−3
web/src/lib/icons.ts+149−3
web/src/lib/live.ts+3−2
web/src/lib/notifications.svelte.ts+39−10
web/src/lib/pages.svelte.ts+10−2
web/src/lib/preferences.ts+10−4
web/src/lib/releases.ts+18−4
web/src/lib/sealed.ts+18−5
web/src/lib/steps.ts+8−4
web/src/lib/tasks.ts+20−2
web/src/lib/theme.ts+19−7
web/src/lib/typeahead.ts+3−1
web/src/routes/[authority]/[repo]/[kind=item]/[did]/[rkey]/files/+page.svelte+123−21
web/src/routes/[authority]/[repo]/[kind=item]/[did]/[rkey]/files/+page.ts+5−1
web/src/routes/[authority]/[repo]/[kind=item]/[did]/[rkey]/+page.svelte+391−64
455 lines changed: not loaded until asked for.
web/src/routes/[authority]/[repo]/[kind=item]/[did]/[rkey]/+page.ts+4−1
web/src/routes/[authority]/[repo]/blame/[...rest]/+page.svelte+30−5
web/src/routes/[authority]/[repo]/blame/[...rest]/+page.ts+5−1
web/src/routes/[authority]/[repo]/blob/[...rest]/+page.svelte+21−4
web/src/routes/[authority]/[repo]/blob/[...rest]/+page.ts+5−1
web/src/routes/[authority]/[repo]/branches/+page.svelte+8−1
web/src/routes/[authority]/[repo]/branches/+page.ts+5−1
web/src/routes/[authority]/[repo]/check/[did]/[rkey]/+page.svelte+71−12
web/src/routes/[authority]/[repo]/check/[did]/[rkey]/+page.ts+3−1
web/src/routes/[authority]/[repo]/checks/+page.svelte+38−6
web/src/routes/[authority]/[repo]/checks/+page.ts+5−1
web/src/routes/[authority]/[repo]/commit/[rev]/+page.svelte+11−2
web/src/routes/[authority]/[repo]/commit/[rev]/+page.ts+3−1
web/src/routes/[authority]/[repo]/commits/[...rest]/+page.svelte+21−3
web/src/routes/[authority]/[repo]/commits/[...rest]/+page.ts+9−1
web/src/routes/[authority]/[repo]/issues/new/+page.svelte+14−2
web/src/routes/[authority]/[repo]/issues/new/+page.ts+5−1
web/src/routes/[authority]/[repo]/issues/+page.svelte+8−1
web/src/routes/[authority]/[repo]/issues/+page.ts+5−1
web/src/routes/[authority]/[repo]/pulls/new/+page.svelte+84−16
web/src/routes/[authority]/[repo]/pulls/new/+page.ts+20−3
web/src/routes/[authority]/[repo]/pulls/+page.svelte+9−1
web/src/routes/[authority]/[repo]/pulls/+page.ts+5−1
web/src/routes/[authority]/[repo]/queue/+page.svelte+6−1
web/src/routes/[authority]/[repo]/releases/+page.svelte+7−1
web/src/routes/[authority]/[repo]/releases/+page.ts+5−1
web/src/routes/[authority]/[repo]/settings/access/+page.svelte+9−1
web/src/routes/[authority]/[repo]/settings/access/+page.ts+4−1
web/src/routes/[authority]/[repo]/settings/actions/+page.svelte+11−2
web/src/routes/[authority]/[repo]/settings/actions/+page.ts+17−3
web/src/routes/[authority]/[repo]/settings/branches/+page.svelte+8−1
web/src/routes/[authority]/[repo]/settings/branches/+page.ts+4−1
web/src/routes/[authority]/[repo]/settings/labels/+page.ts+4−1
web/src/routes/[authority]/[repo]/settings/+page.svelte+16−2
web/src/routes/[authority]/[repo]/settings/+page.ts+7−2
web/src/routes/[authority]/[repo]/tags/+page.svelte+8−1
web/src/routes/[authority]/[repo]/tags/+page.ts+4−1
web/src/routes/[authority]/[repo]/tree/[...rest]/+page.svelte+8−1
web/src/routes/[authority]/[repo]/tree/[...rest]/+page.ts+6−1
web/src/routes/[authority]/[repo]/+page.svelte+13−2
web/src/routes/[authority]/[repo]/+page.ts+6−1
web/src/routes/[authority]/+page.svelte+56−11
web/src/routes/[authority]/+page.ts+40−6
web/src/routes/admin/+page.svelte+9−2
web/src/routes/admin/+page.ts+8−1
web/src/routes/guidelines/+page.svelte+79−17
web/src/routes/new/+page.svelte+12−3
web/src/routes/notifications/+page.svelte+19−4
web/src/routes/notifications/+page.ts+6−1
web/src/routes/privacy/+page.svelte+131−23
web/src/routes/search/+page.svelte+93−22
web/src/routes/search/+page.ts+34−7
web/src/routes/settings/appearance/+page.svelte+9−1
web/src/routes/settings/appearance/+page.ts+23−35
web/src/routes/settings/notifications/+page.ts+6−1
web/src/routes/settings/organization/+page.svelte+3−1
web/src/routes/settings/+layout.svelte+22−5
web/src/routes/settings/+layout.ts+6−1
web/src/routes/start/+page.svelte+28−7
web/src/routes/stats/+page.svelte+47−10
web/src/routes/steps/[authority]/[repo]/[step]/+page.svelte+17−4
web/src/routes/steps/+page.svelte+28−6
web/src/routes/terms/+page.svelte+126−24
web/src/routes/+error.svelte+5−1
web/src/routes/+layout.ts+8−2
web/src/routes/+page.svelte+25−5
web/src/routes/+page.ts+4−1
web/src/app.css+7616−1525
A large diff, not shown.
web/src/env.ts+2−1
web/src/hooks.server.ts+19−3
web/src/tokens.css+9−3
web/package-lock.json+414−0
414 lines changed: not loaded until asked for.