Something went wrong. Try again.
Monorepo for Aesthetic.Computer aesthetic.computer
Something went wrong. Try again.
JavaScript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512#!/usr/bin/env node// papermill.mjs — Build script for translated papers// Usage:// node papermill.mjs build Build all translated PDFs// node papermill.mjs build da Build only Danish translations// node papermill.mjs build es Build only Spanish translations// node papermill.mjs build zh Build only Chinese translations// node papermill.mjs build --format cards Build cards-format PDFs// node papermill.mjs deploy Copy compiled PDFs to site directory// node papermill.mjs sync-index Extract titles from .tex files → translations.json// node papermill.mjs status Show which translations exist
import { execSync } from "child_process";import { readdirSync, existsSync, copyFileSync, readFileSync, writeFileSync, mkdirSync,} from "fs";import { join, basename } from "path";
const PAPERS_DIR = new URL(".", import.meta.url).pathname;const SITE_DIR = join( PAPERS_DIR, "../system/public/papers.aesthetic.computer",);const LANGS = ["da", "es", "zh", "ja", "ru"];const LANG_NAMES = { da: "Danish", es: "Spanish", zh: "Chinese", ja: "Japanese", ru: "Russian" };
// Map paper dir name to output PDF base name (matching existing site naming)const PAPER_MAP = { "arxiv-ac": { base: "ac", siteName: "aesthetic-computer-26-arxiv", paperId: "ac", }, "arxiv-api": { base: "api", siteName: "piece-api-26-arxiv", paperId: "api", }, "arxiv-archaeology": { base: "archaeology", siteName: "repo-archaeology-26-arxiv", paperId: "archaeology", }, "arxiv-dead-ends": { base: "dead-ends", siteName: "dead-ends-26-arxiv", paperId: "dead-ends", }, "arxiv-diversity": { base: "diversity", siteName: "citation-diversity-audit-26", paperId: "diversity", }, "arxiv-folk-songs": { base: "folk-songs", siteName: "folk-songs-26-arxiv", paperId: "folk-songs", }, "arxiv-goodiepal": { base: "goodiepal", siteName: "radical-computer-art-26-arxiv", paperId: "goodiepal", }, "arxiv-kidlisp": { base: "kidlisp", siteName: "kidlisp-26-arxiv", paperId: "kidlisp", }, "arxiv-kidlisp-reference": { base: "kidlisp-reference", siteName: "kidlisp-reference-26-arxiv", paperId: "kidlisp-ref", }, "arxiv-network-audit": { base: "network-audit", siteName: "network-audit-26-arxiv", paperId: "network-audit", }, "arxiv-notepat": { base: "notepat", siteName: "notepat-26-arxiv", paperId: "notepat", }, "arxiv-os": { base: "os", siteName: "ac-native-os-26-arxiv", paperId: "os", }, "arxiv-pieces": { base: "pieces", siteName: "pieces-not-programs-26-arxiv", paperId: "pieces", }, "arxiv-sustainability": { base: "sustainability", siteName: "who-pays-for-creative-tools-26-arxiv", paperId: "who-pays", }, "arxiv-whistlegraph": { base: "whistlegraph", siteName: "whistlegraph-26-arxiv", paperId: "whistlegraph", }, "arxiv-complex": { base: "complex", siteName: "sucking-on-the-complex-26-arxiv", paperId: "complex", }, "arxiv-plork": { base: "plork", siteName: "plorking-the-planet-26-arxiv", paperId: "plork", }, "arxiv-calarts": { base: "calarts", siteName: "calarts-callouts-papers-26-arxiv", paperId: "calarts", }, "arxiv-futures": { base: "futures", siteName: "five-years-from-now-26-arxiv", paperId: "futures", }, "arxiv-identity": { base: "identity", siteName: "handle-identity-atproto-26-arxiv", paperId: "identity", }, "arxiv-open-schools": { base: "open-schools", siteName: "open-schools-26-arxiv", paperId: "open-schools", }, "arxiv-kidlisp-cards": { base: "kidlisp-cards", siteName: "kidlisp-cards-26-arxiv", paperId: "kidlisp-cards", }, "arxiv-score-analysis": { base: "score-analysis", siteName: "reading-the-score-26-arxiv", paperId: "score-analysis", }, "arxiv-latency": { base: "latency", siteName: "where-the-microseconds-go-26-arxiv", paperId: "latency", }, "arxiv-penrose": { base: "penrose", siteName: "diagrams-from-data-26-arxiv", paperId: "penrose", }, "arxiv-keymaps": { base: "keymaps", siteName: "keymaps-as-social-software-26-arxiv", paperId: "keymaps", }, "arxiv-calarts-news": { base: "calarts-news", siteName: "whats-new-calarts-26-arxiv", paperId: "calarts-news", },};
// --- File discovery ---
function findTranslatedFiles(format) { const results = []; for (const [dir, info] of Object.entries(PAPER_MAP)) { const paperDir = join(PAPERS_DIR, dir); if (!existsSync(paperDir)) continue; for (const lang of LANGS) { const suffix = format ? `-${format}` : ""; const texBase = format ? `${info.base}-${format}-${lang}` : `${info.base}-${lang}`; const texFile = join(paperDir, `${texBase}.tex`); const pdfFile = join(paperDir, `${texBase}.pdf`); const siteBase = format ? `${info.siteName}-${format}-${lang}` : `${info.siteName}-${lang}`; results.push({ dir, lang, format: format || "layout", base: info.base, siteName: info.siteName, texFile, pdfFile, texExists: existsSync(texFile), pdfExists: existsSync(pdfFile), sitePdf: join(SITE_DIR, `${siteBase}.pdf`), }); } } return results;}
function findCardsFiles() { const results = []; for (const [dir, info] of Object.entries(PAPER_MAP)) { const paperDir = join(PAPERS_DIR, dir); if (!existsSync(paperDir)) continue; const texFile = join(paperDir, `${info.base}-cards.tex`); const pdfFile = join(paperDir, `${info.base}-cards.pdf`); results.push({ dir, lang: "en", format: "cards", base: info.base, siteName: info.siteName, texFile, pdfFile, texExists: existsSync(texFile), pdfExists: existsSync(pdfFile), sitePdf: join(SITE_DIR, `${info.siteName}-cards.pdf`), }); } return results;}
function buildPaper(entry) { if (!entry.texExists) { console.log( ` SKIP ${entry.dir}/${basename(entry.texFile)} (not found)`, ); return false; } const paperDir = join(PAPERS_DIR, entry.dir); const texName = basename(entry.texFile, ".tex"); console.log(` BUILD ${entry.dir}/${texName}.tex ...`); try { // Run xelatex + bibtex + xelatex + xelatex (full 3-pass build for citations) execSync( `cd "${paperDir}" && xelatex -interaction=nonstopmode "${texName}.tex" && bibtex "${texName}" 2>/dev/null; xelatex -interaction=nonstopmode "${texName}.tex" && xelatex -interaction=nonstopmode "${texName}.tex"`, { stdio: "pipe", timeout: 180000 }, ); console.log(` OK ${texName}.pdf`); return true; } catch (e) { console.error(` FAIL ${texName}.tex — ${e.message?.slice(0, 200)}`); try { const log = execSync( `tail -30 "${join(paperDir, texName + ".log")}"`, { encoding: "utf8" }, ); console.error(` LOG:\n${log}`); } catch (_) {} return false; }}
function deployPaper(entry) { if (!entry.pdfExists) return false; mkdirSync(SITE_DIR, { recursive: true }); copyFileSync(entry.pdfFile, entry.sitePdf); console.log(` DEPLOY ${basename(entry.sitePdf)}`); return true;}
// --- Title extraction from .tex files ---
function extractTitleFromTex(texPath) { if (!existsSync(texPath)) return null; const content = readFileSync(texPath, "utf8"); const lines = content.split("\n").slice(0, 250); // Title can be after preamble
let titleParts = []; let subtitle = null;
// Title pattern: any bold font + large fontsize + \color{*dark*} — can span multiple lines // Matches \acbold, \kidlispbold, \wgbold, etc. // The bold-font prefix (\acbold, \kidlispbold, …) is optional: CJK/Cyrillic // editions drop it (those display faces are Latin-only) and set the title in // \fontsize…\selectfont\bfseries\color{…dark} instead, so accept both forms. const titleRe = /\\(?:[a-z]+bold\\)?fontsize\{[^}]+\}\{[^}]+\}\\selectfont(?:\\bfseries)?\\color\{[a-z]*dark\}\s*(.+?)\s*\}\\par/; // Subtitle pattern: any light font + smaller fontsize + \color{*pink/brand*} const subtitleRe = /\\[a-z]+(?:light|font)\\fontsize\{[^}]+\}\{[^}]+\}\\selectfont\\color\{[a-z]+\}\s*(.+?)\s*\}\\par/;
for (const line of lines) { const boldMatch = line.match(titleRe); if (boldMatch) { titleParts.push(boldMatch[1].replace(/\\par$/, "").trim()); }
const lightMatch = line.match(subtitleRe); if (lightMatch && !subtitle) { subtitle = lightMatch[1].replace(/\\par$/, "").trim(); } }
// Join multi-line titles (e.g., "Playable" + "Folk Songs") let title = titleParts.length > 0 ? titleParts.join(" ") : null;
// Clean up LaTeX commands from extracted text if (title) title = cleanLatex(title); if (subtitle) subtitle = cleanLatex(subtitle);
return { title, subtitle };}
function cleanLatex(text) { return text .replace(/\\acrandname\{\}/g, "Aesthetic Computer") .replace(/\\ac\{\}/g, "Aesthetic Computer") .replace(/\\acos\{\}/g, "AC Native OS") .replace(/\\np\{\}/g, "notepat") .replace(/\\acdot/g, ".") .replace(/\{\\color\{[^}]+\}([^}]*)\}/g, "$1") // {\color{acpink}text} → text .replace(/\\color\{[^}]+\}/g, "") // bare \color{...} .replace(/\\textsc\{([^}]+)\}/g, "$1") .replace(/\\textbf\{([^}]+)\}/g, "$1") .replace(/\\textit\{([^}]+)\}/g, "$1") .replace(/\\texttt\{([^}]+)\}/g, "$1") .replace(/\\emph\{([^}]+)\}/g, "$1") .replace(/\\url\{([^}]+)\}/g, "$1") .replace(/\\href\{[^}]+\}\{([^}]+)\}/g, "$1") // Accented letters (Spanish) — longer patterns first .replace(/\\'\{\\i\}/g, "í") // \'{\i} → í .replace(/\\'\{a\}/g, "á") .replace(/\\'\{e\}/g, "é") .replace(/\\'\{i\}/g, "í") .replace(/\\'\{o\}/g, "ó") .replace(/\\'\{u\}/g, "ú") .replace(/\\'a/g, "á") .replace(/\\'e/g, "é") .replace(/\\'i/g, "í") .replace(/\\'o/g, "ó") .replace(/\\'u/g, "ú") .replace(/\\~\{n\}/g, "ñ") .replace(/\\~n/g, "ñ") // Danish letters. LaTeX rule: a letter-command like `\ae` gobbles one // following space, so `\ae re` → `ære`. Forms handled: {\x}, \x{}, \x\<sp> // (explicit space — keeps space), \x followed by space+letter (gobble), // \x followed by other non-letter (keep). .replace(/\{\\aa\}/g, "å") .replace(/\\aa\{\}/g, "å") .replace(/\\aa\\ /g, "å ") .replace(/\\aa /g, "å") .replace(/\\aa(?=[^a-zA-Z])/g, "å") .replace(/\{\\AA\}/g, "Å") .replace(/\{\\o\}/g, "ø") .replace(/\\o\{\}/g, "ø") .replace(/\\o\\ /g, "ø ") .replace(/\\o (?=[a-zA-Z])/g, "ø") .replace(/\\o(?=[^a-zA-Z])/g, "ø") .replace(/\{\\O\}/g, "Ø") .replace(/\{\\ae\}/g, "æ") .replace(/\\ae\{\}/g, "æ") .replace(/\\ae\\ /g, "æ ") .replace(/\\ae /g, "æ") .replace(/\\ae(?=[^a-zA-Z])/g, "æ") .replace(/\{\\AE\}/g, "Æ") .replace(/\\\\/g, "") .replace(/\\,/g, "") .replace(/\\&/g, "&") .replace(/---/g, "\u2014") .replace(/--/g, "\u2013") .replace(/``/g, "\u201c") .replace(/''/g, "\u201d") .replace(/~/g, " ") .replace(/\s+/g, " ") .trim();}
function syncIndex() { console.log("\nSync-index: extracting titles from .tex files...\n"); const translations = {};
for (const [dir, info] of Object.entries(PAPER_MAP)) { const paperDir = join(PAPERS_DIR, dir); if (!existsSync(paperDir)) continue;
const paperId = info.paperId; const entry = {};
// Extract English title + subtitle (base .tex file) const enTex = join(paperDir, `${info.base}.tex`); const enData = extractTitleFromTex(enTex); if (enData) { entry.en = {}; if (enData.title) entry.en.title = enData.title; if (enData.subtitle) entry.en.subtitle = enData.subtitle; }
// Extract translated titles for (const lang of LANGS) { const texPath = join(paperDir, `${info.base}-${lang}.tex`); const data = extractTitleFromTex(texPath); if (data && data.title) { entry[lang] = {}; entry[lang].title = data.title; if (data.subtitle) entry[lang].subtitle = data.subtitle; } }
if (Object.keys(entry).length > 0) { translations[paperId] = entry; } }
const outPath = join(SITE_DIR, "translations.json"); mkdirSync(SITE_DIR, { recursive: true }); writeFileSync(outPath, JSON.stringify(translations, null, 2), "utf8"); console.log(` WROTE ${outPath}`); console.log( ` ${Object.keys(translations).length} papers, ${LANGS.length} languages\n`, ); return translations;}
// --- CLI ---const args = process.argv.slice(2);const cmd = args[0];const langFilter = args.find((a) => LANGS.includes(a));const formatFlag = args.includes("--format") ? args[args.indexOf("--format") + 1] : null;
if (cmd === "status" || !cmd) { const files = findTranslatedFiles(); console.log("\nPapermill Translation Status\n"); console.log( "Paper".padEnd(30) + LANGS.map((l) => LANG_NAMES[l].padEnd(12)).join(""), ); console.log("-".repeat(30 + LANGS.length * 12)); let currentDir = ""; for (const f of files) { if (f.dir !== currentDir) { currentDir = f.dir; process.stdout.write(f.dir.padEnd(30)); } const status = f.pdfExists ? "PDF" : f.texExists ? "tex" : "---"; process.stdout.write(status.padEnd(12)); if (LANGS.indexOf(f.lang) === LANGS.length - 1) process.stdout.write("\n"); } // Cards status const cardsFiles = findCardsFiles(); const hasCards = cardsFiles.some((f) => f.texExists); if (hasCards) { console.log("Cards Format\n"); console.log("Paper".padEnd(30) + "Status"); console.log("-".repeat(42)); for (const f of cardsFiles) { const status = f.pdfExists ? "PDF" : f.texExists ? "tex" : "---"; console.log(f.dir.padEnd(30) + status); } console.log(); }} else if (cmd === "build") { let files; if (formatFlag === "cards") { // Build English cards files = findCardsFiles(); } else { files = findTranslatedFiles(formatFlag).filter( (f) => !langFilter || f.lang === langFilter, ); } const toBuild = files.filter((f) => f.texExists); const label = formatFlag ? ` (${formatFlag} format)` : ""; console.log( `\nBuilding ${toBuild.length} paper${toBuild.length !== 1 ? "s" : ""}${label}...\n`, ); let ok = 0, fail = 0; for (const entry of toBuild) { if (buildPaper(entry)) ok++; else fail++; } console.log(`\nDone: ${ok} built, ${fail} failed.\n`);} else if (cmd === "deploy") { const files = findTranslatedFiles(); const toDeploy = files.filter((f) => f.pdfExists); console.log( `\nDeploying ${toDeploy.length} translated PDF${toDeploy.length !== 1 ? "s" : ""}...\n`, ); for (const entry of toDeploy) { deployPaper(entry); } // Deploy English cards PDFs const cardsFiles = findCardsFiles(); const cardsToDeploy = cardsFiles.filter((f) => f.pdfExists); if (cardsToDeploy.length > 0) { console.log( `\nDeploying ${cardsToDeploy.length} cards PDF${cardsToDeploy.length !== 1 ? "s" : ""}...\n`, ); for (const entry of cardsToDeploy) { deployPaper(entry); } } // Auto sync-index on deploy syncIndex(); console.log("\nDone.\n");} else if (cmd === "sync-index") { syncIndex();} else { console.log( "Usage: node papermill.mjs [build [da|es|zh] [--format cards] | deploy | sync-index | status]", );}