import { resolve, basename, relative } from "path"; import * as fs from "fs"; import { execSync } from "child_process"; const root = resolve(import.meta.dirname, ".."); const dry = process.argv.includes("--dry"); const bytes2utf8 = new TextDecoder(); /** * Base64 to UTF-8 * @param encoded Base64 encoded string * @returns UTF-8 string */ export const base64Decode = (encoded) => bytes2utf8.decode(Uint8Array.from(atob(encoded), (m) => m.charCodeAt(0))); const yarn = (cmd, stdio = "inherit") => { const script = `yarn run ${cmd}`; if (dry) { return script; } return execSync(script, { stdio }); }; const typlite = (input, output) => { const assets_flag = dry ? "" : `--assets-path ${relative(root, resolve(output, "../assets/images/", basename(input.slice(0, -4))))}`; // return stdout const res = yarn(`--silent typlite ${assets_flag} --root ${root} ${input} -`, "pipe"); return res.toString(); }; const convert = async (inp, out, opts) => { const input = resolve(root, inp); const output = resolve(root, out); const { before } = opts || {}; const res = typlite(input, output).trim(); if (dry) { console.log(res); return; } const outputContent = `\n${before || ""}${res}\n`; await fs.promises.writeFile(output, outputContent); }; // todo: generate me using typlite. const maintainerMd = async () => { const maintainers = JSON.parse(yarn(`--silent maintainers --input=action=maintainers`, "pipe")); const features = JSON.parse(yarn(`--silent maintainers --input=action=features`, "pipe")); const output = []; output.push("\n"); output.push("# Tinymist Maintainers\n\n"); output.push( "Tinymist [ˈtaɪni mɪst] is an integrated language service for [Typst](https://typst.app/) [taɪpst].", ); output.push( "\nThis page is generated from [./MAINTAINERS.typ](./MAINTAINERS.typ) and renders information of [maintainers](#maintainers) and [features.](#features)\n", ); output.push("## Maintainers\n"); const italicIt = (it) => `*${it}*`; const featureLink = (it) => { const feature = features.find((f) => f.name === it); if (feature) { return `[${it}](#${it.replace(/\s+/g, "-").toLowerCase()})`; } return it; }; const fsPath = (it) => { if (!fs.existsSync(it)) { throw new Error(`Path ${it} does not exist!`); } return `[\`${it}\`](./${it})`; }; for (const maintainer of maintainers) { output.push(`- [**${maintainer["name"]}**](https://github.com/${maintainer["github-name"]})`); output.push(` - Email: ${maintainer.email}`); if (maintainer.maintains.length > 0) { const rendered = maintainer.maintains.map(featureLink).map(italicIt); if (rendered.length > 1) { const last = rendered.pop(); output.push(` - Maintains: ${rendered.join(", ")}, and ${last}`); } else { output.push(` - Maintains: ${rendered.join(", ")}`); } } output.push(""); } output.push("## Features\n"); for (const feature of features) { output.push(`### ${feature.name}`); output.push(`${feature.description}`); output.push(`- Scope: ${feature.scope.map(fsPath).join(", ")}`); } const outPath = resolve(root, "MAINTAINERS.md"); const outputContent = output.join("\n"); if (dry) { console.log(content); return; } await fs.promises.writeFile(outPath, outputContent); }; const isCheck = process.argv.includes("--check"); const tasks = [ { input: "docs/tinymist/introduction.typ", output: "README.md", title: "Tinymist", }, { input: "docs/tinymist/release-instruction.typ", output: "docs/release-instruction.md", title: "Release Instructions", }, { input: "docs/tinymist/crates/typlite.typ", output: "crates/typlite/README.md", title: "Typlite", }, { input: "docs/tinymist/frontend/emacs.typ", output: "editors/emacs/README.md", title: "Tinymist Emacs Support for Typst", }, { input: "docs/tinymist/frontend/helix.typ", output: "editors/helix/README.md", title: "Tinymist Helix Support for Typst", }, { input: "docs/tinymist/frontend/neovim.typ", output: "editors/neovim/README.md", title: "Tinymist Neovim Support for Typst", }, { input: "docs/tinymist/frontend/sublime-text.typ", output: "editors/sublime-text/README.md", title: "Tinymist Sublime Support for Typst", }, { input: "docs/tinymist/frontend/vscode.typ", output: "editors/vscode/README.md", title: "Tinymist Typst VS Code Extension", }, { input: "docs/tinymist/frontend/zed.typ", output: "editors/zed/README.md", title: "Tinymist Zed Support for Typst", }, ]; const main = async () => { await Promise.all([ ...tasks.map((task) => convert(task.input, task.output, { before: `# ${task.title}\n\n` })), maintainerMd(), ]); if (isCheck) { // any dirty git files? await Promise.all( tasks.map(async (task) => { const gitStatus = execSync(`git status --porcelain ${task.output}`, { encoding: "utf-8", }).trim(); if (gitStatus) { throw new Error( `The file ${task.output} is not up to date. Please run \`node scripts/link-docs.mjs\` to update it.`, ); } }), ); } }; main();