Something went wrong. Try again.
Monorepo for Aesthetic.Computer aesthetic.computer
Something went wrong. Try again.
JavaScript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426#!/usr/bin/env node// cards-convert.mjs — Convert two-column papers to cards format// Usage: node cards-convert.mjs arxiv-notepat/notepat.tex// node cards-convert.mjs all Convert all papers in PAPER_MAP//// Creates {base}-cards.tex from {base}.tex by:// 1. Replacing the preamble with ac-paper-cards setup// 2. Reformatting the title block to cards-style centered layout// 3. Keeping all body content intact (sections auto-break via cards.sty)
import { readFileSync, writeFileSync, existsSync } from "fs";import { join, basename, dirname } from "path";import { execSync } from "child_process";
const PAPERS_DIR = new URL(".", import.meta.url).pathname;
const LANG_LABELS = { da: "Dansk", es: "Español", zh: "中文", ja: "日本語" };
const PAPER_MAP = { "arxiv-ac": { base: "ac", title: "\\acrandname{} '26", siteName: "aesthetic-computer-26-arxiv" }, "arxiv-api": { base: "api", title: "From \\texttt{setup()} to \\texttt{boot()}", siteName: "piece-api-26-arxiv" }, "arxiv-archaeology": { base: "archaeology", title: "Repository Archaeology", siteName: "repo-archaeology-26-arxiv" }, "arxiv-dead-ends": { base: "dead-ends", title: "Vestigial Features", siteName: "dead-ends-26-arxiv" }, "arxiv-diversity": { base: "diversity", title: "Citation Diversity Audit", siteName: "citation-diversity-audit-26" }, "arxiv-folk-songs": { base: "folk-songs", title: "Playable Folk Songs", siteName: "folk-songs-26-arxiv" }, "arxiv-goodiepal": { base: "goodiepal", title: "Radical Computer Art", siteName: "radical-computer-art-26-arxiv" }, "arxiv-kidlisp": { base: "kidlisp", title: "Kid{\\color{acpurple}Lisp} '26", siteName: "kidlisp-26-arxiv" }, "arxiv-kidlisp-reference": { base: "kidlisp-reference", title: "KidLisp Language Reference", siteName: "kidlisp-reference-26-arxiv" }, "arxiv-network-audit": { base: "network-audit", title: "Network Audit", siteName: "network-audit-26-arxiv" }, "arxiv-notepat": { base: "notepat", title: "notepat{\\color{acpurple}.}{\\color{acpink}com}", siteName: "notepat-26-arxiv" }, "arxiv-os": { base: "os", title: "AC Native OS '26", siteName: "ac-native-os-26-arxiv" }, "arxiv-pieces": { base: "pieces", title: "Pieces Not Programs", siteName: "pieces-not-programs-26-arxiv" }, "arxiv-plork": { base: "plork", title: "PLOrk'ing the Planet", siteName: "plorking-the-planet-26-arxiv" }, "arxiv-sustainability": { base: "sustainability", title: "Who Pays for Creative Tools?", siteName: "who-pays-for-creative-tools-26-arxiv" }, "arxiv-whistlegraph": { base: "whistlegraph", title: "Whistlegraph", siteName: "whistlegraph-26-arxiv" }, "arxiv-complex": { base: "complex", title: "Sucking on the Complex", siteName: "sucking-on-the-complex-26-arxiv" }, "arxiv-kidlisp-cards": { base: "kidlisp-cards", title: "Kid{\\color{acpurple}Lisp} Cards", siteName: "kidlisp-cards-26-arxiv" }, "arxiv-score-analysis": { base: "score-analysis", title: "Reading the Score", siteName: "reading-the-score-26-arxiv" }, "arxiv-calarts": { base: "calarts", title: "CalArts, Callouts, and Papers", siteName: "calarts-callouts-papers-26-arxiv" }, "arxiv-open-schools": { base: "open-schools", title: "Get Closed Source Out of Schools", siteName: "open-schools-26-arxiv" }, "arxiv-futures": { base: "futures", title: "Five Years from Now", siteName: "five-years-from-now-26-arxiv" }, "arxiv-identity": { base: "identity", title: "Handle Identity on the AT Protocol", siteName: "handle-identity-atproto-26-arxiv" }, "arxiv-ucla-arts": { base: "ucla-arts", title: "Two Departments, One Building", siteName: "ucla-arts-funding-26-arxiv" }, "arxiv-holden": { base: "holden", title: "The Potter and the Prompt", siteName: "potter-and-prompt-26-arxiv" }, "arxiv-url-tradition": { base: "url-tradition", title: "The URL Tradition", siteName: "url-tradition-26-arxiv" }, "arxiv-latency": { base: "latency", title: "Where the Microseconds Go", siteName: "where-the-microseconds-go-26-arxiv" }, "arxiv-keymaps": { base: "keymaps", title: "Keymaps as Social Software", siteName: "keymaps-social-software-26-arxiv" }, "arxiv-fraserin": { base: "fraserin", title: "A Fraserin' Art + Tech", siteName: "fraserin-essay-26-arxiv" }, "arxiv-comp-strats": { base: "comp-strats", title: "Comp Strats", siteName: "comp-strats-26-arxiv" }, // --- Dossiers --- "arxiv-rhizome": { base: "rhizome", title: "Rhizome.org — A Dossier", siteName: "rhizome-dossier-26-arxiv" }, "arxiv-sfpc": { base: "sfpc", title: "School for Poetic Computation — A Dossier", siteName: "sfpc-dossier-26-arxiv" }, "arxiv-eyebeam": { base: "eyebeam", title: "Eyebeam — A Dossier", siteName: "eyebeam-dossier-26-arxiv" }, "arxiv-recurse": { base: "recurse", title: "Recurse Center — A Dossier", siteName: "recurse-dossier-26-arxiv" }, "arxiv-internet-archive": { base: "internet-archive", title: "Internet Archive — A Dossier", siteName: "internet-archive-dossier-26-arxiv" }, "arxiv-mellon": { base: "mellon", title: "Mellon Foundation — A Dossier", siteName: "mellon-dossier-26-arxiv" }, "arxiv-pioneer-works": { base: "pioneer-works", title: "Pioneer Works — A Dossier", siteName: "pioneer-works-dossier-26-arxiv" }, "arxiv-new-inc": { base: "new-inc", title: "NEW INC — A Dossier", siteName: "new-inc-dossier-26-arxiv" }, "arxiv-studio-museum": { base: "studio-museum", title: "Studio Museum in Harlem — A Dossier", siteName: "studio-museum-dossier-26-arxiv" }, "arxiv-hathitrust": { base: "hathitrust", title: "HathiTrust — A Dossier", siteName: "hathitrust-dossier-26-arxiv" }, "arxiv-the-kitchen": { base: "the-kitchen", title: "The Kitchen — A Dossier", siteName: "the-kitchen-dossier-26-arxiv" }, "arxiv-machine-project": { base: "machine-project", title: "Machine Project — A Dossier", siteName: "machine-project-dossier-26-arxiv" }, "arxiv-heavy-manners-library": { base: "heavy-manners-library", title: "Heavy Manners Library — A Dossier", siteName: "heavy-manners-library-dossier-26-arxiv" }, "arxiv-creative-time": { base: "creative-time", title: "Creative Time — A Dossier", siteName: "creative-time-dossier-26-arxiv" }, "arxiv-creative-capital": { base: "creative-capital", title: "Creative Capital — A Dossier", siteName: "creative-capital-dossier-26-arxiv" }, "arxiv-microvision": { base: "microvision", title: "MicroVision — A Dossier", siteName: "microvision-dossier-26-arxiv" }, "arxiv-calarts-news": { base: "calarts-news", title: "What's New CalArts!? — A Dossier", siteName: "whats-new-calarts-26-arxiv" },};
function getAvailableTranslations(dir, info) { return Object.fromEntries( Object.entries(LANG_LABELS).filter(([code]) => existsSync(join(PAPERS_DIR, dir, `${info.base}-${code}.tex`)) ) );}
// Fit source tables to the narrow card without elongating them.//// The old approach converted tabularx -> tabular and rewrote every X// column to a fixed p{0.28\linewidth}. On a 4x6 card that forces every// X cell to wrap into 3-5 lines, producing absurdly tall tables with a// wasted-whitespace left column (the "stretched / too elongated" bug).//// Instead, KEEP tabularx (its X column auto-distributes the remaining// width, which is exactly what we want) but retarget the width to// \linewidth -- the source targets \columnwidth, the narrow two-column// arxiv measure, which is wrong on a single-column card -- and wrap the// whole thing in adjustbox so an over-wide (e.g. all-numeric, no-X)// table shrinks to the card instead of overflowing.function convertTabularxToTabular(body) { return body .replace( /\\begin\{tabularx\}\{[^}]*\}\{/g, "\\begin{adjustbox}{max width=\\linewidth,center}%\n\\begin{tabularx}{\\linewidth}{", ) .replace( /\\end\{tabularx\}/g, "\\end{tabularx}%\n\\end{adjustbox}", );}
function extractFromTex(content) { // Extract pdftitle const pdftitleMatch = content.match(/pdftitle\s*=\s*\{([^}]+)\}/); const pdftitle = pdftitleMatch ? pdftitleMatch[1] : "Untitled";
// Extract subtitle from \aclight\fontsize line const subtitleMatch = content.match( /\\aclight\\fontsize\{[^}]+\}\{[^}]+\}\\selectfont\\color\{acpink\}\s*(.+?)\s*\}\\par/ ); const subtitle = subtitleMatch ? subtitleMatch[1].trim() : null;
// Extract graphicspath const gpMatch = content.match(/\\graphicspath\{\{([^}]+)\}\}/); const graphicspath = gpMatch ? gpMatch[1] : "figures/";
// Check for listings const hasListings = content.includes("\\usepackage{listings}") || content.includes("\\begin{lstlisting}");
// Check for CJK const hasCJK = content.includes("\\usepackage{xeCJK}"); const cjkFontMatch = content.match(/\\setCJKmainfont\{([^}]+)\}/); const cjkFont = cjkFontMatch ? cjkFontMatch[1] : "Droid Sans Fallback";
// Check for KidLisp-specific fonts const hasKidlispFonts = content.includes("kidlispbold") || content.includes("kidlispfont");
// Find body start const bodyStart = content.indexOf("\\begin{document}"); const bodyEnd = content.indexOf("\\end{document}");
if (bodyStart === -1 || bodyEnd === -1) return null;
// Extract body content after \begin{document} let body = content.substring(bodyStart + "\\begin{document}".length, bodyEnd).trim();
// Remove the existing title block (everything before the first \section) const firstSection = body.search(/\\section\{/); let titleContent = ""; let mainBody = body;
if (firstSection > 0) { titleContent = body.substring(0, firstSection).trim(); mainBody = body.substring(firstSection).trim(); }
// Extract abstract from title content — only use \begin{abstract}...\end{abstract} let abstract = ""; const abstractMatch = titleContent.match( /\\begin\{abstract\}([\s\S]*?)\\end\{abstract\}/ ); if (abstractMatch) { abstract = abstractMatch[1].trim(); }
// Extract extra \newcommand definitions from the preamble (before \begin{document}) const preamble = content.substring(0, bodyStart); const extraCommands = []; // Match \newcommand{\acos} and similar custom commands (not \ac, \np, \acrandname which are in the sty) const cmdRe = /^(\\newcommand\{\\(?!ac\b|np\b|acdot\b|acrandletter\b|acrandname\b)[a-zA-Z]+\}.*)$/gm; let cmdMatch; while ((cmdMatch = cmdRe.exec(preamble)) !== null) { extraCommands.push(cmdMatch[1]); }
// Extract extra \definecolor lines not already in ac-paper-cards.sty // (sty defines: acpink, acpurple, acdark, acgray, aclight-bg, accard, draftcolor) const styColors = new Set(["acpink", "acpurple", "acdark", "acgray", "aclight-bg", "accard", "draftcolor"]); const colorRe = /^(\\definecolor\{([^}]+)\}.*)$/gm; let colorMatch; while ((colorMatch = colorRe.exec(preamble)) !== null) { if (!styColors.has(colorMatch[2])) { extraCommands.push(colorMatch[1]); } }
// Extract extra graphicspath entries (some papers reference other paper's figures) // Use greedy match to handle nested braces like {{figures/}{../../papers/arxiv-ac/figures/}} const multiGpMatch = content.match(/\\graphicspath\{(.+)\}/); const fullGraphicspath = multiGpMatch ? multiGpMatch[1] : `{${graphicspath}}`;
// Extract lstdefinestyle and lstset blocks (brace-balanced) const lstStyles = []; const lstBlockRe = /\\(?:lstdefinestyle|lstset|lstdefinelanguage)\b/g; let lstMatch; while ((lstMatch = lstBlockRe.exec(preamble)) !== null) { // Find the opening { of the definition body and balance braces let pos = lstMatch.index + lstMatch[0].length; // For lstdefinestyle/lstdefinelanguage, skip the {name} part first if (lstMatch[0] !== "\\lstset") { const nameStart = preamble.indexOf("{", pos); if (nameStart === -1) continue; const nameEnd = preamble.indexOf("}", nameStart); if (nameEnd === -1) continue; pos = nameEnd + 1; } const bodyStart = preamble.indexOf("{", pos); if (bodyStart === -1) continue; let depth = 1; let i = bodyStart + 1; while (i < preamble.length && depth > 0) { if (preamble[i] === "{") depth++; else if (preamble[i] === "}") depth--; i++; } if (depth === 0) { lstStyles.push(preamble.substring(lstMatch.index, i)); } }
return { pdftitle, subtitle, graphicspath, fullGraphicspath, hasListings, hasCJK, cjkFont, hasKidlispFonts, extraCommands, lstStyles, abstract, mainBody, titleContent, };}
function generateCardsTeX(dir, info, parsed) { const extraPackages = []; if (parsed.hasListings) extraPackages.push("\\usepackage{listings}");
// Front cover illustration: if figures/cover.png exists, it leads the // title card (front-and-center, no pals logo). Otherwise fall back to // the pals wordmark. Covers are always .png (gpt-image platter output). const hasCover = existsSync(join(PAPERS_DIR, dir, "figures", "cover.png")); const titleLead = hasCover ? `\\href{https://papers.aesthetic.computer}{\\includegraphics[width=0.72\\textwidth]{figures/cover}}\\par\\vspace{0.25em}` : `\\href{https://papers.aesthetic.computer}{\\includegraphics[height=11em]{pals}}\\par\\vspace{0.1em}`;
const cjkBlock = parsed.hasCJK ? `\\usepackage{xeCJK}\n\\setCJKmainfont{${parsed.cjkFont}}` : "";
const kidlispFonts = parsed.hasKidlispFonts ? `\\newfontfamily\\kidlispbold{ywft-processing-bold}[\n Path=../../system/public/type/webfonts/,\n Extension=.ttf\n]\n\\newfontfamily\\kidlispfont{ywft-processing-light}[\n Path=../../system/public/type/webfonts/,\n Extension=.ttf\n]` : "";
const title = info.title || parsed.pdftitle; const subtitle = parsed.subtitle || "";
// Git hash for revision stamp let gitHash = "unknown"; try { gitHash = execSync("git rev-parse --short HEAD", { encoding: "utf8" }).trim(); } catch (_) {}
// Translation links for title card const cjkLangs = new Set(["zh", "ja", "ko"]); const translations = getAvailableTranslations(dir, info); const translationLinks = Object.keys(translations).length > 0 ? Object.entries(translations) .map(([code, label]) => { const displayLabel = cjkLangs.has(code) ? `{\\accjk ${label}}` : label; return `\\href{https://papers.aesthetic.computer/${info.siteName}-${code}.pdf}{${displayLabel}}`; }) .join(" · ") : "";
// Extra custom commands from the base .tex preamble const extraCmds = parsed.extraCommands.length > 0 ? "\n% Extra commands from base paper\n" + parsed.extraCommands.join("\n") : "";
// Custom listing styles from the base .tex preamble const lstStyleBlock = parsed.lstStyles.length > 0 ? "\n" + parsed.lstStyles.join("\n") : "";
const abstractCard = parsed.abstract ? `% ============================================================% ABSTRACT CARD% ============================================================\\clearpage\\begin{accentcard}\\cardtitle{Abstract}
${parsed.abstract}\\end{accentcard}
` : "";
return `% !TEX program = xelatex% Cards format — auto-generated from ${info.base}.tex by cards-convert.mjs\\documentclass[11pt]{article}
\\usepackage{fontspec}\\usepackage{unicode-math}% Latin Modern by OTF filename (not font name): xelatex on macOS (Core% Text, no fontconfig) cannot resolve "Latin Modern Roman" by name, which% silently produced ~4KB broken card PDFs. kpathsea finds the OTFs in% texmf-dist on both macOS and the oven's Linux xelatex.\\setmainfont{lmroman10-regular.otf}[ BoldFont=lmroman10-bold.otf, ItalicFont=lmroman10-italic.otf, BoldItalicFont=lmroman10-bolditalic.otf,]\\setsansfont{lmsans10-regular.otf}[ BoldFont=lmsans10-bold.otf, ItalicFont=lmsans10-oblique.otf, BoldItalicFont=lmsans10-boldoblique.otf,]\\setmonofont{lmmono10-regular.otf}[Scale=0.88, BoldFont=lmmonolt10-bold.otf, ItalicFont=lmmono10-italic.otf,]${cjkBlock ? "\n" + cjkBlock : ""}
\\usepackage{graphicx}\\graphicspath{${parsed.fullGraphicspath}}\\usepackage{booktabs}\\usepackage{tabularx}\\usepackage{adjustbox}\\usepackage{ragged2e}\\usepackage{microtype}\\usepackage{natbib}${extraPackages.join("\n")}${kidlispFonts ? "\n" + kidlispFonts : ""}${lstStyleBlock}
\\makeatletter\\def\\input@path{{../}}\\makeatother\\usepackage{ac-paper-cards}${extraCmds}
\\hypersetup{ pdftitle={${parsed.pdftitle}},}
\\renewcommand{\\acpdfbase}{${info.siteName}}\\begin{document}
% ============================================================% TITLE CARD% ============================================================\\thispagestyle{empty}\\vspace*{0.05em}\\begin{center}${titleLead}{\\acbold\\fontsize{18pt}{22pt}\\selectfont\\color{acdark} ${title}}\\par\\vspace{0.1em}${subtitle ? `{\\fontsize{9pt}{11pt}\\selectfont\\color{acpink} ${subtitle}}\\par\n\\vspace{0.2em}` : "\\vspace{0.2em}"}{\\scriptsize\\color{acgray}\\href{https://prompt.ac/@jeffrey}{\\color{cyan!70!blue}\\textbf{@jeffrey}}\\,\\textperiodcentered\\,Aesthetic{\\acdot}Computer\\,\\textperiodcentered\\,ORCID~\\href{https://orcid.org/0009-0007-4460-4913}{0009-0007-4460-4913}}\\par\\vspace{0.2em}\\rule{0.5\\textwidth}{0.5pt}\\par\\vspace{0.12em}\\ifacdraft\\colorbox{yellow!60}{\\small\\color{red!80!black}\\textbf{\\textit{working draft --- not for citation}}}\\par\\vspace{0.1em}\\fi${translationLinks ? `{\\small\\sffamily ${translationLinks}}\\par\\vspace{0.15em}` : ""}{\\scriptsize\\color{acgray} March 2026 \\,\\textperiodcentered\\, \\href{https://github.com/whistlegraph/aesthetic-computer/commit/${gitHash}}{${gitHash}}}\\par\\end{center}
% ============================================================% INDEX CARD% ============================================================\\cardindex
${abstractCard}% ============================================================% BODY% ============================================================${convertTabularxToTabular(parsed.mainBody)}
\\end{document}`;}
function convertPaper(dirName) { const info = PAPER_MAP[dirName]; if (!info) { console.error(` Unknown paper: ${dirName}`); return false; }
const texPath = join(PAPERS_DIR, dirName, `${info.base}.tex`); const outPath = join(PAPERS_DIR, dirName, `${info.base}-cards.tex`);
if (!existsSync(texPath)) { console.error(` NOT FOUND: ${texPath}`); return false; }
const content = readFileSync(texPath, "utf8"); const parsed = extractFromTex(content);
if (!parsed) { console.error(` PARSE FAIL: ${texPath}`); return false; }
const cardsTeX = generateCardsTeX(dirName, info, parsed); writeFileSync(outPath, cardsTeX, "utf8"); console.log(` WROTE ${dirName}/${info.base}-cards.tex`); return true;}
// --- CLI ---const target = process.argv[2];
if (!target) { console.log("Usage: node cards-convert.mjs <dir-name|all>"); console.log(" node cards-convert.mjs arxiv-notepat"); console.log(" node cards-convert.mjs all"); process.exit(0);}
if (target === "all") { console.log("\nConverting all papers to cards format...\n"); let ok = 0; for (const dir of Object.keys(PAPER_MAP)) { if (convertPaper(dir)) ok++; } console.log(`\nDone: ${ok}/${Object.keys(PAPER_MAP).length} converted.\n`);} else { convertPaper(target);}