T48a Docs · Frame: guides render at build in a Vite plugin, so a bad one fails the build; focus rings whole in the rail and list; plain heading words, unique ids, captions only when named, code under example= refused

This commit is contained in:
ZacharyZhang-NY committed 2026-10-01 12:01:42 -04:00
1 parent 128cc5992f
commit 0b300f1f94
9 files changed
+71 -26

No files matched your search

+29 -16
View File
@@ -1,7 +1,9 @@
import { readFileSync } from "node:fs";
import { fileURLToPath } from "node:url";
import { Marked, type Tokens } from "marked";
const sources = import.meta.glob<string>("./*.md", { query: "?raw", import: "default", eager: true });
const examples = import.meta.glob<string>("../../../examples/docs/*.rs", { query: "?raw", import: "default", eager: true });
// Runs in Vite's config at build time, never in the browser.
const at = (relative: string) => fileURLToPath(new URL(relative, import.meta.url));
/** The guides in reading order, by file name. */
const ORDER = ["introduction"];
@@ -23,14 +25,20 @@ const slugify = (text: string) =>
.replace(/[^a-z0-9]+/g, "-")
.replace(/^-|-$/g, "");
/** A heading's words: no backticks, links as their text. */
const plain = (markdown: string) => markdown.replace(/`/g, "").replace(/\[([^\]]*)\]\([^)]*\)/g, "$1");
/** A heading's words: no code, emphasis or link marks. */
const plain = (markdown: string) =>
markdown
.replace(/`/g, "")
.replace(/\[([^\]]*)\]\([^)]*\)/g, "$1")
.replace(/(\*\*|\*)([^*]+)\1/g, "$2");
function read(file: string): Guide {
const source = sources[`./${file}.md`];
if (source === undefined) throw new Error(`docs: no ${file}.md`);
function read(file: string, files: string[]): Guide {
const path = at(`./${file}.md`);
files.push(path);
const source = readFileSync(path, "utf8");
let title = "";
const toc: Guide["toc"] = [];
const ids = new Set<string>();
const marked = new Marked({
renderer: {
heading({ tokens, depth, text }: Tokens.Heading) {
@@ -42,10 +50,9 @@ function read(file: string): Guide {
}
const html = this.parser.parseInline(tokens);
const id = slugify(words);
if (depth === 2) {
if (toc.some((h) => h.id === id)) throw new Error(`docs: ${file}.md repeats heading ${id}`);
toc.push({ id, text: words });
}
if (ids.has(id)) throw new Error(`docs: ${file}.md repeats heading ${id}`);
ids.add(id);
if (depth === 2) toc.push({ id, text: words });
return `<h${depth} id="${id}">${html}</h${depth}>`;
},
code({ text, lang }: Tokens.Code) {
@@ -54,12 +61,14 @@ function read(file: string): Guide {
let code = text;
let caption = language;
if (example) {
const body = examples[`../../../examples/docs/${example}.rs`];
if (body === undefined) throw new Error(`docs: ${file}.md names a missing example ${example}`);
code = body.trimEnd();
if (text.trim()) throw new Error(`docs: ${file}.md puts code under example=${example}`);
const path = at(`../../../examples/docs/${example}.rs`);
files.push(path);
code = readFileSync(path, "utf8").trimEnd();
caption = `examples/docs/${example}.rs · cargo run --example docs_${example}`;
}
return `<figure class="code"><figcaption>${escape(caption)}</figcaption><button class="copy" type="button">Copy</button><pre><code>${escape(code)}</code></pre></figure>`;
const head = caption ? `<figcaption>${escape(caption)}</figcaption>` : "";
return `<figure class="code">${head}<button class="copy" type="button">Copy</button><pre><code>${escape(code)}</code></pre></figure>`;
},
},
});
@@ -68,4 +77,8 @@ function read(file: string): Guide {
return { slug: file === "introduction" ? "" : file, title, html, toc };
}
export const guides: Guide[] = ORDER.map(read);
/** Every guide, and every file read to make them. */
export function render(): { guides: Guide[]; files: string[] } {
const files: string[] = [];
return { guides: ORDER.map((file) => read(file, files)), files };
}
+3
View File
@@ -0,0 +1,3 @@
declare module "virtual:guides" {
export const guides: import("./guides").Guide[];
}
+2 -1
View File
@@ -1,6 +1,7 @@
import { Link, useNavigate, useParams } from "@tanstack/react-router";
import { useEffect, type MouseEvent } from "react";
import { guides, type Guide } from "../docs/guides";
import type { Guide } from "../docs/guides";
import { guides } from "virtual:guides";
import { NotFound } from "./NotFound";
const to = (guide: Guide) => (guide.slug ? `/docs/${guide.slug}/` : "/docs/");
+3 -2
View File
@@ -165,7 +165,7 @@ img.shot { display: block; max-width: 100%; height: auto; }
.pager .next { margin-left: auto; text-align: right; }
.docs { display: grid; grid-template-columns: 200px minmax(0, 720px) 200px; justify-content: center; gap: 64px; padding: 0 64px; }
.docs-rail, .doc-toc { position: sticky; top: 64px; height: calc(100vh - 64px); overflow-y: auto; padding: 96px 0 64px; }
.docs-rail, .doc-toc { position: sticky; top: 64px; height: calc(100vh - 64px); overflow-y: auto; padding: 96px 8px 64px; margin: 0 -8px; }
.docs-rail ol, .doc-toc ol { list-style: none; margin: 0; padding: 0; }
.docs-rail li { padding: 5px 0; font-size: 15px; }
.doc-toc li { padding: 4px 0; font-size: 13px; line-height: 20px; }
@@ -191,6 +191,7 @@ img.shot { display: block; max-width: 100%; height: auto; }
.code { position: relative; margin: 28px 0; background: var(--code); border-radius: 12px; }
.code figcaption { padding: 14px 88px 0 20px; font: 400 12px/18px var(--mono); color: var(--faint); overflow-wrap: anywhere; }
.code pre { margin: 0; padding: 14px 20px 20px; overflow-x: auto; }
.code button + pre { padding-top: 20px; }
.code code { display: block; font: 400 13.5px/22px var(--mono); background: none; padding: 0; color: var(--fg); }
.code .copy { position: absolute; top: 10px; right: 12px; padding: 4px 8px; font-size: 12px; color: var(--muted); transition: color 160ms ease-out; }
.code .copy:hover { color: var(--fg); }
@@ -208,7 +209,7 @@ img.shot { display: block; max-width: 100%; height: auto; }
@media (max-width: 1023px) {
.docs { grid-template-columns: minmax(0, 1fr); gap: 24px; }
.docs-rail { position: static; height: auto; padding: 96px 0 0; }
.docs-rail { position: static; height: auto; overflow: visible; padding: 96px 0 0; margin: 0; }
.docs-rail ol { display: flex; flex-wrap: wrap; gap: 4px 20px; }
.doc { padding-top: 48px; }
.reader { grid-template-columns: minmax(0, 1fr); }