diff --git a/CLAUDE.md b/CLAUDE.md index e9a0782..5fd294c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -7,10 +7,15 @@ Static site generator for Vortex RFC proposals built with Bun. ``` index.ts - Main build script and dev server styles.css - Site styling (light/dark themes) -proposals/ - RFC markdown files (format: NNNN-slug.md) +proposed/ - RFC markdown files in proposed state +accepted/ - RFC markdown files in accepted state +completed/ - RFC markdown files in completed state dist/ - Build output (gitignored) ``` +RFC filenames follow the format `NNNN-slug.md` (e.g., `0001-galp-patches.md`). +Numbering is global across all states - no duplicates allowed. + ## Commands ```sh @@ -21,19 +26,30 @@ bun run clean # Remove dist/ ## How the Build Works -1. Scans `proposals/*.md` for RFC files +1. Scans `proposed/`, `accepted/`, `completed/` for RFC files 2. Parses RFC number from filename (e.g., `0002-foo.md` → RFC 0002) -3. Extracts title from first `# ` heading -4. Converts markdown to HTML using `Bun.markdown.html()` -5. Generates `dist/index.html` (table of contents) -6. Generates `dist/rfc/{number}.html` for each RFC +3. Determines state from containing folder +4. Extracts title from first `# ` heading +5. Converts markdown to HTML using `Bun.markdown.html()` +6. Generates `dist/index.html` (table of contents with filter UI) +7. Generates `dist/rfc/{number}.html` for each RFC ## Dev Server - Uses `Bun.serve()` to serve static files from `dist/` -- Watches `proposals/` and `styles.css` for changes +- Watches `proposed/`, `accepted/`, `completed/`, and `styles.css` for changes - SSE endpoint at `/__reload` for live reload +## RFC States + +RFCs progress through three states by moving files between folders: + +- **proposed**: New RFCs under discussion +- **accepted**: Approved RFCs ready for implementation +- **completed**: Fully implemented RFCs + +The index page shows a state pill for each RFC and supports filtering by state. + ## Styling - CSS custom properties for theming (`--bg`, `--fg`, `--link`, etc.) diff --git a/accepted/.keep b/accepted/.keep new file mode 100644 index 0000000..e69de29 diff --git a/proposals/0000-template.md b/accepted/0000-template.md similarity index 93% rename from proposals/0000-template.md rename to accepted/0000-template.md index 5e9c030..9eda702 100644 --- a/proposals/0000-template.md +++ b/accepted/0000-template.md @@ -1,5 +1,4 @@ - Start Date: (today's date, YYYY-MM-DD) -- RFC PR: [vortex-data/rfcs#0000](https://github.com/vortex-data/rfcs/pull/0000) - Tracking Issue: [vortex-data/vortex#0000](https://github.com/vortex-data/vortex/issues/0000) ## Summary @@ -23,7 +22,14 @@ Describe the proposed design in enough detail that someone familiar with Vortex - Why is this the best approach in the space of possible designs? - Which crates are affected and how the dependency graph changes, if at all. -Use code examples and diagrams where they might help. +Use code examples and diagrams where they might help, like this: + +```rust +pub fn main() { + let x = f32::to_bits(100.0f32); + dbg!(x); +} +``` ## Compatibility diff --git a/bun.lock b/bun.lock index f87bb78..0820453 100644 --- a/bun.lock +++ b/bun.lock @@ -1,8 +1,12 @@ { "lockfileVersion": 1, + "configVersion": 0, "workspaces": { "": { "name": "vortexrfc", + "dependencies": { + "shiki": "^4.0.0", + }, "devDependencies": { "@types/bun": "latest", "prettier": "^3.8.1", @@ -13,16 +17,106 @@ }, }, "packages": { + "@shikijs/core": ["@shikijs/core@4.0.0", "", { "dependencies": { "@shikijs/primitive": "4.0.0", "@shikijs/types": "4.0.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-tvV94Dwyz4qFZ8R0MUaFx5Yptgy8yrloa4dwynEJDGjKz+8vqO8Q6FmPZL9W1gSzFHOUMOGQzIHK62aGourFxA=="], + + "@shikijs/engine-javascript": ["@shikijs/engine-javascript@4.0.0", "", { "dependencies": { "@shikijs/types": "4.0.0", "@shikijs/vscode-textmate": "^10.0.2", "oniguruma-to-es": "^4.3.4" } }, "sha512-+PEyTS+JTz2lLy2C1Dwwx6hzoehIzqxQYh5MEjv9V4JtSabx+bIkRHfQT+6DnBmPAplGH0exBknWeiJSXC7w1w=="], + + "@shikijs/engine-oniguruma": ["@shikijs/engine-oniguruma@4.0.0", "", { "dependencies": { "@shikijs/types": "4.0.0", "@shikijs/vscode-textmate": "^10.0.2" } }, "sha512-KXmq4b6Xw16+4+rz5M4NZMoe/tzs5kTOMSJz8+LCyxSrwmxwTBAM/ab85iSO2Gw79E47HkW4B9HPHUXhrNOivw=="], + + "@shikijs/langs": ["@shikijs/langs@4.0.0", "", { "dependencies": { "@shikijs/types": "4.0.0" } }, "sha512-dSAT6fBcnOcYZQMWZO8+OmzUKKm+OO0As/qZ3TXLiSy0JsCTEYz1TaX7TDupnYLz7dr0oF2DOTEgPocx1D3aFw=="], + + "@shikijs/primitive": ["@shikijs/primitive@4.0.0", "", { "dependencies": { "@shikijs/types": "4.0.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-6K2zD7JTgsyFc2vM1rqy8eRGC8D5Hius3qzVONjq2lHMrqfTSn1HcGeJZiFPYSV9m3DQuBHncBbA5xe0hKSOkQ=="], + + "@shikijs/themes": ["@shikijs/themes@4.0.0", "", { "dependencies": { "@shikijs/types": "4.0.0" } }, "sha512-xe42kvxOXan5ouXxULez6qwDNUJkoP6kicfg0wKuJBkeIaHLxZBZa2gEGYutL1q27DQZ5+XoR6caVX+E/aNR5A=="], + + "@shikijs/types": ["@shikijs/types@4.0.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-LCnfBTtQKNtJyc1qMShZr2dJt1uxNI6pI0/YTc2DSNET91aUvnMGHUHsucVCC5AJVcv5XyBqk2NgYRwd20EjbA=="], + + "@shikijs/vscode-textmate": ["@shikijs/vscode-textmate@10.0.2", "", {}, "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg=="], + "@types/bun": ["@types/bun@1.3.9", "", { "dependencies": { "bun-types": "1.3.9" } }, "sha512-KQ571yULOdWJiMH+RIWIOZ7B2RXQGpL1YQrBtLIV3FqDcCu6FsbFUBwhdKUlCKUpS3PJDsHlJ1QKlpxoVR+xtw=="], + "@types/hast": ["@types/hast@3.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ=="], + + "@types/mdast": ["@types/mdast@4.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA=="], + "@types/node": ["@types/node@25.3.0", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-4K3bqJpXpqfg2XKGK9bpDTc6xO/xoUP/RBWS7AtRMug6zZFaRekiLzjVtAoZMquxoAbzBvy5nxQ7veS5eYzf8A=="], + "@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="], + + "@ungap/structured-clone": ["@ungap/structured-clone@1.3.0", "", {}, "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g=="], + "bun-types": ["bun-types@1.3.9", "", { "dependencies": { "@types/node": "*" } }, "sha512-+UBWWOakIP4Tswh0Bt0QD0alpTY8cb5hvgiYeWCMet9YukHbzuruIEeXC2D7nMJPB12kbh8C7XJykSexEqGKJg=="], + "ccount": ["ccount@2.0.1", "", {}, "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg=="], + + "character-entities-html4": ["character-entities-html4@2.1.0", "", {}, "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA=="], + + "character-entities-legacy": ["character-entities-legacy@3.0.0", "", {}, "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ=="], + + "comma-separated-tokens": ["comma-separated-tokens@2.0.3", "", {}, "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg=="], + + "dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="], + + "devlop": ["devlop@1.1.0", "", { "dependencies": { "dequal": "^2.0.0" } }, "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA=="], + + "hast-util-to-html": ["hast-util-to-html@9.0.5", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "ccount": "^2.0.0", "comma-separated-tokens": "^2.0.0", "hast-util-whitespace": "^3.0.0", "html-void-elements": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "stringify-entities": "^4.0.0", "zwitch": "^2.0.4" } }, "sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw=="], + + "hast-util-whitespace": ["hast-util-whitespace@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw=="], + + "html-void-elements": ["html-void-elements@3.0.0", "", {}, "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg=="], + + "mdast-util-to-hast": ["mdast-util-to-hast@13.2.1", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "@ungap/structured-clone": "^1.0.0", "devlop": "^1.0.0", "micromark-util-sanitize-uri": "^2.0.0", "trim-lines": "^3.0.0", "unist-util-position": "^5.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" } }, "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA=="], + + "micromark-util-character": ["micromark-util-character@2.1.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q=="], + + "micromark-util-encode": ["micromark-util-encode@2.0.1", "", {}, "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw=="], + + "micromark-util-sanitize-uri": ["micromark-util-sanitize-uri@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-encode": "^2.0.0", "micromark-util-symbol": "^2.0.0" } }, "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ=="], + + "micromark-util-symbol": ["micromark-util-symbol@2.0.1", "", {}, "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q=="], + + "micromark-util-types": ["micromark-util-types@2.0.2", "", {}, "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA=="], + + "oniguruma-parser": ["oniguruma-parser@0.12.1", "", {}, "sha512-8Unqkvk1RYc6yq2WBYRj4hdnsAxVze8i7iPfQr8e4uSP3tRv0rpZcbGUDvxfQQcdwHt/e9PrMvGCsa8OqG9X3w=="], + + "oniguruma-to-es": ["oniguruma-to-es@4.3.4", "", { "dependencies": { "oniguruma-parser": "^0.12.1", "regex": "^6.0.1", "regex-recursion": "^6.0.2" } }, "sha512-3VhUGN3w2eYxnTzHn+ikMI+fp/96KoRSVK9/kMTcFqj1NRDh2IhQCKvYxDnWePKRXY/AqH+Fuiyb7VHSzBjHfA=="], + "prettier": ["prettier@3.8.1", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg=="], + "property-information": ["property-information@7.1.0", "", {}, "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ=="], + + "regex": ["regex@6.1.0", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg=="], + + "regex-recursion": ["regex-recursion@6.0.2", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg=="], + + "regex-utilities": ["regex-utilities@2.3.0", "", {}, "sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng=="], + + "shiki": ["shiki@4.0.0", "", { "dependencies": { "@shikijs/core": "4.0.0", "@shikijs/engine-javascript": "4.0.0", "@shikijs/engine-oniguruma": "4.0.0", "@shikijs/langs": "4.0.0", "@shikijs/themes": "4.0.0", "@shikijs/types": "4.0.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-rjKoiw30ZaFsM0xnPPwxco/Jftz/XXqZkcQZBTX4LGheDw8gCDEH87jdgaKDEG3FZO2bFOK27+sR/sDHhbBXfg=="], + + "space-separated-tokens": ["space-separated-tokens@2.0.2", "", {}, "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q=="], + + "stringify-entities": ["stringify-entities@4.0.4", "", { "dependencies": { "character-entities-html4": "^2.0.0", "character-entities-legacy": "^3.0.0" } }, "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg=="], + + "trim-lines": ["trim-lines@3.0.1", "", {}, "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg=="], + "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], "undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="], + + "unist-util-is": ["unist-util-is@6.0.1", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g=="], + + "unist-util-position": ["unist-util-position@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA=="], + + "unist-util-stringify-position": ["unist-util-stringify-position@4.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ=="], + + "unist-util-visit": ["unist-util-visit@5.1.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg=="], + + "unist-util-visit-parents": ["unist-util-visit-parents@6.0.2", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0" } }, "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ=="], + + "vfile": ["vfile@6.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile-message": "^4.0.0" } }, "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q=="], + + "vfile-message": ["vfile-message@4.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw=="], + + "zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="], } } diff --git a/completed/.keep b/completed/.keep new file mode 100644 index 0000000..e69de29 diff --git a/index.ts b/index.ts index 9d6b2a8..291dc87 100644 --- a/index.ts +++ b/index.ts @@ -1,5 +1,6 @@ import { $, type Server } from "bun"; import { watch } from "fs"; +import { createHighlighter, type Highlighter } from "shiki"; const isDev = process.argv.includes("--dev"); const PORT = 3000; @@ -21,12 +22,17 @@ interface RFCGitInfo { author: GitHubAuthor | null; } +type RFCState = "proposed" | "accepted" | "completed"; + +const RFC_STATES: RFCState[] = ["proposed", "accepted", "completed"]; + interface RFC { number: string; title: string; filename: string; html: string; git: RFCGitInfo; + state: RFCState; } const THEME_SCRIPT = ` @@ -65,6 +71,33 @@ function updateToggleIcon() { document.addEventListener('DOMContentLoaded', updateToggleIcon); `; +const FILTER_SCRIPT = ` +function filterRFCs(state) { + const items = document.querySelectorAll('.rfc-list li'); + const buttons = document.querySelectorAll('.filter-btn'); + + buttons.forEach(btn => { + btn.classList.toggle('active', btn.dataset.state === state); + }); + + items.forEach(item => { + if (state === 'all' || item.dataset.state === state) { + item.style.display = ''; + } else { + item.style.display = 'none'; + } + }); + + // Save filter preference + localStorage.setItem('rfc-filter', state); +} + +document.addEventListener('DOMContentLoaded', function() { + const saved = localStorage.getItem('rfc-filter') || 'all'; + filterRFCs(saved); +}); +`; + const LIVE_RELOAD_SCRIPT = ` (function() { const evtSource = new EventSource('/__reload'); @@ -127,6 +160,10 @@ function escapeHTML(str: string): string { .replace(/"/g, """); } +function stateLabel(state: RFCState): string { + return state.charAt(0).toUpperCase() + state.slice(1); +} + function indexPage( rfcs: RFC[], repoUrl: string | null, @@ -152,9 +189,10 @@ function indexPage( } return ` -
  • +
  • RFC ${rfc.number} + ${stateLabel(rfc.state)} ${escapeHTML(rfc.title)} ${dateStr} ${authorHTML} @@ -162,12 +200,22 @@ function indexPage( }) .join("\n"); + const filterButtons = ` +
    + + + + +
    `; + const content = `

    Request for Comments

    Technical proposals for the Vortex file format.

    +${filterButtons} `; + + `; return baseHTML("Vortex RFCs", content, "styles.css", liveReload, repoUrl); } @@ -185,12 +233,13 @@ function rfcPage( repoUrl: string | null, liveReload: boolean = false, ): string { - let gitHeader = ""; + let gitHeader = ` +
    +
    + ${stateLabel(rfc.state)} +
    `; if (rfc.git.accepted || rfc.git.author) { - gitHeader = ` -
    `; - // Author section if (rfc.git.author) { gitHeader += ` @@ -227,10 +276,10 @@ function rfcPage( ${formatDate(rfc.git.lastUpdated.date)}${commitLink}
    `; } + } - gitHeader += ` + gitHeader += `
    `; - } const content = ` ← Back to index${gitHeader} @@ -341,28 +390,34 @@ interface ValidationError { async function validateProposals(): Promise { const errors: ValidationError[] = []; const glob = new Bun.Glob("*"); - const seenNumbers = new Map(); - - for await (const filename of glob.scan("./proposals")) { - // Check filename format: NNNN-slug.md - if (!filename.match(/^\d{4}-[a-zA-Z0-9_-]+\.md$/)) { - errors.push({ - filename, - message: `Invalid filename format. Expected: NNNN-name.md (e.g., 0007-my-proposal.md)`, - }); - continue; - } + const seenNumbers = new Map(); // number -> "folder/filename" - // Check for duplicate RFC numbers - const number = filename.slice(0, 4); - const existing = seenNumbers.get(number); - if (existing) { - errors.push({ - filename, - message: `Duplicate RFC number ${number} (also used by ${existing})`, - }); - } else { - seenNumbers.set(number, filename); + for (const state of RFC_STATES) { + const folder = `./${state}`; + + for await (const filename of glob.scan(folder)) { + const fullPath = `${state}/${filename}`; + + // Check filename format: NNNN-slug.md + if (!filename.match(/^\d{4}-[a-zA-Z0-9_-]+\.md$/)) { + errors.push({ + filename: fullPath, + message: `Invalid filename format. Expected: NNNN-name.md (e.g., 0007-my-proposal.md)`, + }); + continue; + } + + // Check for duplicate RFC numbers across all folders + const number = filename.slice(0, 4); + const existing = seenNumbers.get(number); + if (existing) { + errors.push({ + filename: fullPath, + message: `Duplicate RFC number ${number} (also used by ${existing})`, + }); + } else { + seenNumbers.set(number, fullPath); + } } } @@ -380,6 +435,64 @@ function parseTitle(markdown: string, filename: string): string { return filename.replace(/^\d+-/, "").replace(/\.md$/, "").replace(/-/g, " "); } +// Syntax highlighting with Shiki +let highlighter: Highlighter | null = null; + +async function getHighlighter(): Promise { + if (!highlighter) { + highlighter = await createHighlighter({ + themes: ["github-light", "github-dark"], + langs: ["rust", "python", "markdown"], + }); + } + return highlighter; +} + +async function highlightCodeBlocks(html: string): Promise { + const hl = await getHighlighter(); + + // Match
    ...
    blocks + const codeBlockRegex = + /
    ([\s\S]*?)<\/code><\/pre>/g;
    +
    +  const matches = [...html.matchAll(codeBlockRegex)];
    +  let result = html;
    +
    +  for (const match of matches) {
    +    const [fullMatch, lang, code] = match;
    +    if (!lang || !code) continue;
    +
    +    // Decode HTML entities back to raw code
    +    const rawCode = code
    +      .replace(/</g, "<")
    +      .replace(/>/g, ">")
    +      .replace(/&/g, "&")
    +      .replace(/"/g, '"')
    +      .replace(/'/g, "'");
    +
    +    // Check if this language is supported
    +    const loadedLangs = hl.getLoadedLanguages();
    +    if (!loadedLangs.includes(lang)) {
    +      // Skip unsupported languages, leave original markup
    +      continue;
    +    }
    +
    +    // Generate highlighted HTML with both themes using CSS variables
    +    const highlighted = hl.codeToHtml(rawCode.trim(), {
    +      lang,
    +      themes: {
    +        light: "github-light",
    +        dark: "github-dark",
    +      },
    +      defaultColor: false,
    +    });
    +
    +    result = result.replace(fullMatch, highlighted);
    +  }
    +
    +  return result;
    +}
    +
     async function build(liveReload: boolean = false): Promise {
       console.log("Building Vortex RFC site...\n");
     
    @@ -402,22 +515,29 @@ async function build(liveReload: boolean = false): Promise {
       const glob = new Bun.Glob("*.md");
       const rfcs: RFC[] = [];
     
    -  // Parse all RFC markdown files
    -  for await (const filename of glob.scan("./proposals")) {
    -    console.log(`Processing ${filename}...`);
    +  // Parse all RFC markdown files from each state folder
    +  for (const state of RFC_STATES) {
    +    const folder = `./${state}`;
    +
    +    for await (const filename of glob.scan(folder)) {
    +      console.log(`Processing ${state}/${filename}...`);
     
    -    const path = `./proposals/${filename}`;
    -    const content = await Bun.file(path).text();
    -    const html = Bun.markdown.html(content, { autolinks: true });
    -    const number = parseRFCNumber(filename);
    -    const title = parseTitle(content, filename);
    -    const git = await getGitHistory(path, repoPath);
    +      const path = `${folder}/${filename}`;
    +      const content = await Bun.file(path).text();
    +      const rawHtml = Bun.markdown.html(content, { autolinks: true });
    +      const html = await highlightCodeBlocks(rawHtml);
    +      const number = parseRFCNumber(filename);
    +      const title = parseTitle(content, filename);
    +      const git = await getGitHistory(path, repoPath);
     
    -    rfcs.push({ number, title, filename, html, git });
    +      rfcs.push({ number, title, filename, html, git, state });
    +    }
       }
     
       if (rfcs.length === 0) {
    -    console.log("No RFC files found in ./proposals/");
    +    console.log(
    +      "No RFC files found in ./proposed/, ./accepted/, or ./completed/",
    +    );
         return 0;
       }
     
    @@ -468,7 +588,9 @@ async function startDevServer() {
       // Initial build with live reload enabled
       await build(true);
       console.log(`\nStarting dev server at http://localhost:${PORT}`);
    -  console.log("Watching for changes in ./proposals/ and ./styles.css\n");
    +  console.log(
    +    "Watching for changes in ./proposed/, ./accepted/, ./completed/, and ./styles.css\n",
    +  );
     
       // Debounce rebuilds
       let rebuildTimeout: Timer | null = null;
    @@ -481,12 +603,14 @@ async function startDevServer() {
         }, 100);
       };
     
    -  // Watch proposals directory
    -  watch("./proposals", { recursive: true }, (_event, filename) => {
    -    if (filename?.endsWith(".md")) {
    -      scheduleRebuild();
    -    }
    -  });
    +  // Watch all state directories
    +  for (const state of RFC_STATES) {
    +    watch(`./${state}`, { recursive: true }, (_event, filename) => {
    +      if (filename?.endsWith(".md")) {
    +        scheduleRebuild();
    +      }
    +    });
    +  }
     
       // Watch styles.css
       watch("./styles.css", () => {
    diff --git a/package.json b/package.json
    index b539045..3cf61b3 100644
    --- a/package.json
    +++ b/package.json
    @@ -14,5 +14,8 @@
       },
       "peerDependencies": {
         "typescript": "^5"
    +  },
    +  "dependencies": {
    +    "shiki": "^4.0.0"
       }
     }
    diff --git a/proposed/.keep b/proposed/.keep
    new file mode 100644
    index 0000000..e69de29
    diff --git a/styles.css b/styles.css
    index 4f65a45..60c5f0b 100644
    --- a/styles.css
    +++ b/styles.css
    @@ -493,3 +493,126 @@ footer {
       color: var(--fg-muted);
       font-size: 0.875rem;
     }
    +
    +/* State filter bar */
    +.filter-bar {
    +  display: flex;
    +  gap: 0.5rem;
    +  margin-bottom: 1.5rem;
    +  flex-wrap: wrap;
    +}
    +
    +.filter-btn {
    +  font-family: inherit;
    +  font-size: 0.875rem;
    +  padding: 0.375rem 0.75rem;
    +  border: 1px solid var(--border);
    +  border-radius: 4px;
    +  background: var(--bg);
    +  color: var(--fg-muted);
    +  cursor: pointer;
    +  transition:
    +    background 0.15s ease,
    +    color 0.15s ease,
    +    border-color 0.15s ease;
    +}
    +
    +.filter-btn:hover {
    +  background: var(--bg-alt);
    +  color: var(--fg);
    +}
    +
    +.filter-btn.active {
    +  background: var(--fg);
    +  color: var(--bg);
    +  border-color: var(--fg);
    +}
    +
    +/* State pills */
    +.rfc-state-pill {
    +  display: inline-block;
    +  font-size: 0.75rem;
    +  font-weight: 500;
    +  padding: 0.125rem 0.5rem;
    +  border-radius: 9999px;
    +  text-transform: capitalize;
    +  flex-shrink: 0;
    +}
    +
    +.state-proposed {
    +  background: #fef3c7;
    +  color: #92400e;
    +}
    +
    +.state-accepted {
    +  background: #dbeafe;
    +  color: #1e40af;
    +}
    +
    +.state-completed {
    +  background: #d1fae5;
    +  color: #065f46;
    +}
    +
    +:root[data-theme="dark"] .state-proposed {
    +  background: #78350f;
    +  color: #fef3c7;
    +}
    +
    +:root[data-theme="dark"] .state-accepted {
    +  background: #1e3a8a;
    +  color: #dbeafe;
    +}
    +
    +:root[data-theme="dark"] .state-completed {
    +  background: #064e3b;
    +  color: #d1fae5;
    +}
    +
    +/* Shiki syntax highlighting - dual theme support */
    +pre.shiki {
    +  font-family:
    +    "IBM Plex Mono", "SF Mono", "Menlo", "Monaco", "Consolas", monospace;
    +  border: 1px solid var(--border);
    +  border-radius: 4px;
    +  padding: 1rem;
    +  overflow-x: auto;
    +  margin-bottom: 1rem;
    +  font-size: 0.875rem;
    +  line-height: 1.5;
    +}
    +
    +pre.shiki code {
    +  background: none;
    +  padding: 0;
    +  border-radius: 0;
    +}
    +
    +/* Light theme (default) */
    +:root pre.shiki,
    +:root pre.shiki span {
    +  color: var(--shiki-light);
    +  background-color: var(--shiki-light-bg);
    +}
    +
    +:root[data-theme="light"] pre.shiki,
    +:root[data-theme="light"] pre.shiki span {
    +  color: var(--shiki-light);
    +  background-color: var(--shiki-light-bg);
    +}
    +
    +/* Dark theme */
    +:root[data-theme="dark"] pre.shiki,
    +:root[data-theme="dark"] pre.shiki span {
    +  color: var(--shiki-dark);
    +  background-color: var(--shiki-dark-bg);
    +}
    +
    +/* System preference dark mode */
    +@media (prefers-color-scheme: dark) {
    +  :root:not([data-theme="light"]) pre.shiki,
    +  :root:not([data-theme="light"]) pre.shiki span {
    +    color: var(--shiki-dark);
    +    background-color: var(--shiki-dark-bg);
    +  }
    +}