Something went wrong. Try again.
A reference for terminal control sequences whose examples run live in libghostty-vt compiled to WebAssembly.
Something went wrong. Try again.
7.5 kB · 183 lines
JavaScript
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184// SPDX-FileCopyrightText: © 2026 Jeffrey C. Ollie <jeff@ocjtech.us>// SPDX-License-Identifier: MIT
// Writes a plain Markdown copy of every page, and an llms.txt indexing// them (https://llmstxt.org/), from the same SuperMD sources Zine builds.//// node tools/markdown.mjs website out-dir//// Each page's copy goes beside it as index.html.md, which is where llms.txt// readers look for a page whose URL ends in a slash. SuperMD is CommonMark// plus a little, and this site uses only two parts of the little: the// `vt-case` block directive, which becomes a plain input block and expected// screen, and root-relative links, which become absolute links to the// other pages' Markdown copies.
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";import { join } from "node:path";import { alsoSequences, readPages, siteBase, siteOutline } from "./pages.mjs";
const [websiteDir, outDir] = process.argv.slice(2);if (!websiteDir || !outDir) { console.error("usage: node tools/markdown.mjs website-dir out-dir"); process.exit(2);}
const base = siteBase(websiteDir);const pages = readPages(websiteDir);const outline = siteOutline(pages);const mdUrl = (page) => `${page.url}index.html.md`;
// The same links the sequence layout builds; see the comment there.function references(c) { const refs = []; if (c.ecma48) { refs.push( `- **ECMA-48:** [§${c.ecma48} ${c.ecma48_name}, p. ${c.ecma48_page}](https://archive.org/details/ecma-48-5th-edition-june-1991/page/n${c.ecma48_page + 13}/mode/1up)`, ); } if (c.dec070) { refs.push( `- **DEC STD 070:** [§${c.dec070} ${c.dec070_name}, p. ${c.dec070_page}](https://archive.org/details/bitsavers_decstandar0VideoSystemsReferenceManualDec91_74264381/page/n${c.dec070_pdf - 1}/mode/1up)`, ); } if (c.vt220) refs.push(`- **VT220:** [${c.vt220_name}](https://vt100.net/docs/vt220-rm/${c.vt220})`); if (c.vt340) refs.push(`- **VT330/VT340:** [${c.vt340_name}](https://vt100.net/docs/vt3xx-gp/${c.vt340})`); if (c.vt510) refs.push(`- **VT510:** [${c.vt510_name}](https://vt100.net/docs/vt510-rm/${c.vt510})`); if (c.spec) refs.push(`- **Specification:** [${c.spec_name}](${c.spec})`); if (c.xterm) refs.push(`- **xterm:** [${c.xterm}](https://invisible-island.net/xterm/ctlseqs/ctlseqs.html)`); return refs;}
function convertBody(body) { const lines = body.split("\n"); const out = []; let fence = false; for (let i = 0; i < lines.length; i++) { const line = lines[i]; const directive = /^>\s*\[\]\(\$block\.attrs\(([^)]*)\)\)\s*$/.exec(line); if (directive && !fence) { const attrs = Array.from(directive[1].matchAll(/"([^"]*)"/g), (m) => m[1]); const inner = []; while (i + 1 < lines.length && lines[i + 1].startsWith(">")) inner.push(lines[++i].replace(/^> ?/, "")); out.push(...convertBlock(attrs, inner)); continue; } if (/^```/.test(line)) fence = !fence; // SuperMD headings start at `#` and render one level down, under the // page title; here the title is the `#`, so they move down a level. if (!fence && /^#{1,5} /.test(line)) { out.push(`#${line}`); continue; } out.push(fence ? line : absoluteLinks(line)); } return out.join("\n");}
function convertBlock(attrs, inner) { // A diagram's source is Mermaid; say so on its fence. if (attrs.includes("diagram")) { const at = inner.findIndex((line) => /^```/.test(line)); return at < 0 ? inner : [...inner.slice(0, at), "```mermaid", ...inner.slice(at + 1)]; } if (!attrs.includes("vt-case")) return inner; const blocks = []; let current = null; for (const line of inner) { if (/^```/.test(line)) { if (current === null) current = []; else { blocks.push(current); current = null; } } else if (current !== null) current.push(line); } const [input = [], expected = []] = blocks; const out = []; if (attrs.includes("known-difference")) { out.push("*A known difference: libghostty-vt does not pass this case.*", ""); } if (attrs.includes("checksum-report")) { out.push("*Run with checksum reports allowed, as xterm is once GetChecksum is allowed.*", ""); } if (attrs.includes("no-column-resize")) { out.push("*Run with DECCOLM kept from resizing, as xterm is once ColumnMode is disallowed.*", ""); } out.push("Input, one step per line:", "", "```text", ...input, "```", "", "Expected screen:", "", "```text", ...expected, "```"); return out;}
// `[text](/csi/cub/)` → the Markdown copy of that page.function absoluteLinks(line) { return line.replace(/\]\(\/([^)\s]*)\)/g, (_, path) => `](${base}${path}${path.endsWith("/") || path === "" ? "index.html.md" : ""})`);}
function listItem(page) { const c = page.fields.custom; const label = c?.mnemonic ? `${c.mnemonic}: ${c.name}` : page.fields.title; const desc = page.fields.description ? `: ${page.fields.description}` : ""; return `- [${label}](${mdUrl(page)})${desc}`;}
function render(page) { const { fields } = page; const c = fields.custom ?? {}; const out = [`# ${fields.title}`, ""]; if (fields.description) out.push(`> ${fields.description}`, ""); if (c.sequence) { out.push(`- **Sequence:** \`${c.sequence}\``); if (c.defaults) out.push(`- **Defaults:** ${c.defaults}`); // Other sequences the page covers. for (const { mnemonic, name, syntax } of alsoSequences(readFileSync(page.file, "utf8"))) { out.push(`- **${mnemonic}**, ${name}: \`${syntax}\``); } out.push(...references(c), ""); } out.push(convertBody(page.body).trim(), "");
// Index and section pages list their pages in the layout, not the source. if (page.path === "") { for (const { page: s, children } of outline.sections) { out.push(`## ${s.fields.title}`, "", ...children.map(listItem), ""); } out.push("## Guides", "", ...outline.guides.map(listItem), ""); } else { const section = outline.sections.find((s) => s.page === page); if (section) out.push(...section.children.map(listItem), ""); }
out.push( "---", "", `This is the Markdown version of <${page.url}>. On that page every validation case runs live in libghostty-vt, the terminal emulation core of Ghostty, compiled to WebAssembly.`, "", ); if (/\$block\.attrs\([^)]*"vt-case"/.test(page.body)) { out.push(`The validation cases are written in the notation described in <${base}notation/index.html.md>.`, ""); } return out.join("\n");}
for (const page of pages) { const dir = join(outDir, page.path); mkdirSync(dir, { recursive: true }); writeFileSync(join(dir, "index.html.md"), render(page));}
const index = pages.find((p) => p.path === "");const llms = [ `# control-codes`, "", `> ${index.fields.description}`, "", "Each sequence page gives the sequence's syntax, what it does, where ECMA-48, DEC STD 070, the VT220 manual and the VT510 manual define it, and validation cases: input and the screen it should leave behind. The Notation guide explains how cases are written. Every link below is the Markdown version of a page.", "",];for (const { page: s, children } of outline.sections) { llms.push(`## ${s.fields.title}`, "", ...children.map(listItem), "");}llms.push("## Guides", "", listItem(index), ...outline.guides.map(listItem), "");writeFileSync(join(outDir, "llms.txt"), llms.join("\n"));
console.log(`wrote ${pages.length} Markdown pages and llms.txt`);