Monorepo for Aesthetic.Computer aesthetic.computer
4
fork

Configure Feed

Select the types of activity you want to include in your feed.

at main 461 lines 14 kB view raw
1#!/usr/bin/env node 2// papermill.mjs — Build script for translated papers 3// Usage: 4// node papermill.mjs build Build all translated PDFs 5// node papermill.mjs build da Build only Danish translations 6// node papermill.mjs build es Build only Spanish translations 7// node papermill.mjs build zh Build only Chinese translations 8// node papermill.mjs build --format cards Build cards-format PDFs 9// node papermill.mjs deploy Copy compiled PDFs to site directory 10// node papermill.mjs sync-index Extract titles from .tex files → translations.json 11// node papermill.mjs status Show which translations exist 12 13import { execSync } from "child_process"; 14import { 15 readdirSync, 16 existsSync, 17 copyFileSync, 18 readFileSync, 19 writeFileSync, 20 mkdirSync, 21} from "fs"; 22import { join, basename } from "path"; 23 24const PAPERS_DIR = new URL(".", import.meta.url).pathname; 25const SITE_DIR = join( 26 PAPERS_DIR, 27 "../system/public/papers.aesthetic.computer", 28); 29const LANGS = ["da", "es", "zh", "ja"]; 30const LANG_NAMES = { da: "Danish", es: "Spanish", zh: "Chinese", ja: "Japanese" }; 31 32// Map paper dir name to output PDF base name (matching existing site naming) 33const PAPER_MAP = { 34 "arxiv-ac": { 35 base: "ac", 36 siteName: "aesthetic-computer-26-arxiv", 37 paperId: "ac", 38 }, 39 "arxiv-api": { 40 base: "api", 41 siteName: "piece-api-26-arxiv", 42 paperId: "api", 43 }, 44 "arxiv-archaeology": { 45 base: "archaeology", 46 siteName: "repo-archaeology-26-arxiv", 47 paperId: "archaeology", 48 }, 49 "arxiv-dead-ends": { 50 base: "dead-ends", 51 siteName: "dead-ends-26-arxiv", 52 paperId: "dead-ends", 53 }, 54 "arxiv-diversity": { 55 base: "diversity", 56 siteName: "citation-diversity-audit-26", 57 paperId: "diversity", 58 }, 59 "arxiv-folk-songs": { 60 base: "folk-songs", 61 siteName: "folk-songs-26-arxiv", 62 paperId: "folk-songs", 63 }, 64 "arxiv-goodiepal": { 65 base: "goodiepal", 66 siteName: "radical-computer-art-26-arxiv", 67 paperId: "goodiepal", 68 }, 69 "arxiv-kidlisp": { 70 base: "kidlisp", 71 siteName: "kidlisp-26-arxiv", 72 paperId: "kidlisp", 73 }, 74 "arxiv-kidlisp-reference": { 75 base: "kidlisp-reference", 76 siteName: "kidlisp-reference-26-arxiv", 77 paperId: "kidlisp-ref", 78 }, 79 "arxiv-network-audit": { 80 base: "network-audit", 81 siteName: "network-audit-26-arxiv", 82 paperId: "network-audit", 83 }, 84 "arxiv-notepat": { 85 base: "notepat", 86 siteName: "notepat-26-arxiv", 87 paperId: "notepat", 88 }, 89 "arxiv-os": { 90 base: "os", 91 siteName: "ac-native-os-26-arxiv", 92 paperId: "os", 93 }, 94 "arxiv-pieces": { 95 base: "pieces", 96 siteName: "pieces-not-programs-26-arxiv", 97 paperId: "pieces", 98 }, 99 "arxiv-sustainability": { 100 base: "sustainability", 101 siteName: "who-pays-for-creative-tools-26-arxiv", 102 paperId: "who-pays", 103 }, 104 "arxiv-whistlegraph": { 105 base: "whistlegraph", 106 siteName: "whistlegraph-26-arxiv", 107 paperId: "whistlegraph", 108 }, 109 "arxiv-complex": { 110 base: "complex", 111 siteName: "sucking-on-the-complex-26-arxiv", 112 paperId: "complex", 113 }, 114 "arxiv-plork": { 115 base: "plork", 116 siteName: "plorking-the-planet-26-arxiv", 117 paperId: "plork", 118 }, 119 "arxiv-calarts": { 120 base: "calarts", 121 siteName: "calarts-callouts-papers-26-arxiv", 122 paperId: "calarts", 123 }, 124 "arxiv-futures": { 125 base: "futures", 126 siteName: "five-years-from-now-26-arxiv", 127 paperId: "futures", 128 }, 129 "arxiv-identity": { 130 base: "identity", 131 siteName: "handle-identity-atproto-26-arxiv", 132 paperId: "identity", 133 }, 134 "arxiv-open-schools": { 135 base: "open-schools", 136 siteName: "open-schools-26-arxiv", 137 paperId: "open-schools", 138 }, 139 "arxiv-kidlisp-cards": { 140 base: "kidlisp-cards", 141 siteName: "kidlisp-cards-26-arxiv", 142 paperId: "kidlisp-cards", 143 }, 144 "arxiv-score-analysis": { 145 base: "score-analysis", 146 siteName: "reading-the-score-26-arxiv", 147 paperId: "score-analysis", 148 }, 149 "arxiv-latency": { 150 base: "latency", 151 siteName: "where-the-microseconds-go-26-arxiv", 152 paperId: "latency", 153 }, 154 "arxiv-penrose": { 155 base: "penrose", 156 siteName: "diagrams-from-data-26-arxiv", 157 paperId: "penrose", 158 }, 159}; 160 161// --- File discovery --- 162 163function findTranslatedFiles(format) { 164 const results = []; 165 for (const [dir, info] of Object.entries(PAPER_MAP)) { 166 const paperDir = join(PAPERS_DIR, dir); 167 if (!existsSync(paperDir)) continue; 168 for (const lang of LANGS) { 169 const suffix = format ? `-${format}` : ""; 170 const texBase = format 171 ? `${info.base}-${format}-${lang}` 172 : `${info.base}-${lang}`; 173 const texFile = join(paperDir, `${texBase}.tex`); 174 const pdfFile = join(paperDir, `${texBase}.pdf`); 175 const siteBase = format 176 ? `${info.siteName}-${format}-${lang}` 177 : `${info.siteName}-${lang}`; 178 results.push({ 179 dir, 180 lang, 181 format: format || "layout", 182 base: info.base, 183 siteName: info.siteName, 184 texFile, 185 pdfFile, 186 texExists: existsSync(texFile), 187 pdfExists: existsSync(pdfFile), 188 sitePdf: join(SITE_DIR, `${siteBase}.pdf`), 189 }); 190 } 191 } 192 return results; 193} 194 195function findCardsFiles() { 196 const results = []; 197 for (const [dir, info] of Object.entries(PAPER_MAP)) { 198 const paperDir = join(PAPERS_DIR, dir); 199 if (!existsSync(paperDir)) continue; 200 const texFile = join(paperDir, `${info.base}-cards.tex`); 201 const pdfFile = join(paperDir, `${info.base}-cards.pdf`); 202 results.push({ 203 dir, 204 lang: "en", 205 format: "cards", 206 base: info.base, 207 siteName: info.siteName, 208 texFile, 209 pdfFile, 210 texExists: existsSync(texFile), 211 pdfExists: existsSync(pdfFile), 212 sitePdf: join(SITE_DIR, `${info.siteName}-cards.pdf`), 213 }); 214 } 215 return results; 216} 217 218function buildPaper(entry) { 219 if (!entry.texExists) { 220 console.log( 221 ` SKIP ${entry.dir}/${basename(entry.texFile)} (not found)`, 222 ); 223 return false; 224 } 225 const paperDir = join(PAPERS_DIR, entry.dir); 226 const texName = basename(entry.texFile, ".tex"); 227 console.log(` BUILD ${entry.dir}/${texName}.tex ...`); 228 try { 229 // Run xelatex + bibtex + xelatex + xelatex (full 3-pass build for citations) 230 execSync( 231 `cd "${paperDir}" && xelatex -interaction=nonstopmode "${texName}.tex" && bibtex "${texName}" 2>/dev/null; xelatex -interaction=nonstopmode "${texName}.tex" && xelatex -interaction=nonstopmode "${texName}.tex"`, 232 { stdio: "pipe", timeout: 180000 }, 233 ); 234 console.log(` OK ${texName}.pdf`); 235 return true; 236 } catch (e) { 237 console.error(` FAIL ${texName}.tex — ${e.message?.slice(0, 200)}`); 238 try { 239 const log = execSync( 240 `tail -30 "${join(paperDir, texName + ".log")}"`, 241 { encoding: "utf8" }, 242 ); 243 console.error(` LOG:\n${log}`); 244 } catch (_) {} 245 return false; 246 } 247} 248 249function deployPaper(entry) { 250 if (!entry.pdfExists) return false; 251 mkdirSync(SITE_DIR, { recursive: true }); 252 copyFileSync(entry.pdfFile, entry.sitePdf); 253 console.log(` DEPLOY ${basename(entry.sitePdf)}`); 254 return true; 255} 256 257// --- Title extraction from .tex files --- 258 259function extractTitleFromTex(texPath) { 260 if (!existsSync(texPath)) return null; 261 const content = readFileSync(texPath, "utf8"); 262 const lines = content.split("\n").slice(0, 250); // Title can be after preamble 263 264 let titleParts = []; 265 let subtitle = null; 266 267 // Title pattern: any bold font + large fontsize + \color{*dark*} — can span multiple lines 268 // Matches \acbold, \kidlispbold, \wgbold, etc. 269 const titleRe = 270 /\\[a-z]+bold\\fontsize\{[^}]+\}\{[^}]+\}\\selectfont\\color\{[a-z]+\}\s*(.+?)\s*\}\\par/; 271 // Subtitle pattern: any light font + smaller fontsize + \color{*pink/brand*} 272 const subtitleRe = 273 /\\[a-z]+(?:light|font)\\fontsize\{[^}]+\}\{[^}]+\}\\selectfont\\color\{[a-z]+\}\s*(.+?)\s*\}\\par/; 274 275 for (const line of lines) { 276 const boldMatch = line.match(titleRe); 277 if (boldMatch) { 278 titleParts.push(boldMatch[1].replace(/\\par$/, "").trim()); 279 } 280 281 const lightMatch = line.match(subtitleRe); 282 if (lightMatch && !subtitle) { 283 subtitle = lightMatch[1].replace(/\\par$/, "").trim(); 284 } 285 } 286 287 // Join multi-line titles (e.g., "Playable" + "Folk Songs") 288 let title = titleParts.length > 0 ? titleParts.join(" ") : null; 289 290 // Clean up LaTeX commands from extracted text 291 if (title) title = cleanLatex(title); 292 if (subtitle) subtitle = cleanLatex(subtitle); 293 294 return { title, subtitle }; 295} 296 297function cleanLatex(text) { 298 return text 299 .replace(/\\ac\{\}/g, "Aesthetic Computer") 300 .replace(/\\acos\{\}/g, "AC Native OS") 301 .replace(/\\np\{\}/g, "notepat") 302 .replace(/\\acdot/g, ".") 303 .replace(/\{\\color\{[^}]+\}([^}]*)\}/g, "$1") // {\color{acpink}text} → text 304 .replace(/\\color\{[^}]+\}/g, "") // bare \color{...} 305 .replace(/\\textsc\{([^}]+)\}/g, "$1") 306 .replace(/\\textbf\{([^}]+)\}/g, "$1") 307 .replace(/\\textit\{([^}]+)\}/g, "$1") 308 .replace(/\\texttt\{([^}]+)\}/g, "$1") 309 .replace(/\\emph\{([^}]+)\}/g, "$1") 310 .replace(/\\url\{([^}]+)\}/g, "$1") 311 .replace(/\\href\{[^}]+\}\{([^}]+)\}/g, "$1") 312 .replace(/\\\\/g, "") 313 .replace(/\\,/g, "") 314 .replace(/\\&/g, "&") 315 .replace(/---/g, "\u2014") 316 .replace(/--/g, "\u2013") 317 .replace(/``/g, "\u201c") 318 .replace(/''/g, "\u201d") 319 .replace(/~/g, " ") 320 .replace(/\s+/g, " ") 321 .trim(); 322} 323 324function syncIndex() { 325 console.log("\nSync-index: extracting titles from .tex files...\n"); 326 const translations = {}; 327 328 for (const [dir, info] of Object.entries(PAPER_MAP)) { 329 const paperDir = join(PAPERS_DIR, dir); 330 if (!existsSync(paperDir)) continue; 331 332 const paperId = info.paperId; 333 const entry = {}; 334 335 // Extract English title + subtitle (base .tex file) 336 const enTex = join(paperDir, `${info.base}.tex`); 337 const enData = extractTitleFromTex(enTex); 338 if (enData) { 339 entry.en = {}; 340 if (enData.title) entry.en.title = enData.title; 341 if (enData.subtitle) entry.en.subtitle = enData.subtitle; 342 } 343 344 // Extract translated titles 345 for (const lang of LANGS) { 346 const texPath = join(paperDir, `${info.base}-${lang}.tex`); 347 const data = extractTitleFromTex(texPath); 348 if (data && data.title) { 349 entry[lang] = {}; 350 entry[lang].title = data.title; 351 if (data.subtitle) entry[lang].subtitle = data.subtitle; 352 } 353 } 354 355 if (Object.keys(entry).length > 0) { 356 translations[paperId] = entry; 357 } 358 } 359 360 const outPath = join(SITE_DIR, "translations.json"); 361 mkdirSync(SITE_DIR, { recursive: true }); 362 writeFileSync(outPath, JSON.stringify(translations, null, 2), "utf8"); 363 console.log(` WROTE ${outPath}`); 364 console.log( 365 ` ${Object.keys(translations).length} papers, ${LANGS.length} languages\n`, 366 ); 367 return translations; 368} 369 370// --- CLI --- 371const args = process.argv.slice(2); 372const cmd = args[0]; 373const langFilter = args.find((a) => LANGS.includes(a)); 374const formatFlag = args.includes("--format") 375 ? args[args.indexOf("--format") + 1] 376 : null; 377 378if (cmd === "status" || !cmd) { 379 const files = findTranslatedFiles(); 380 console.log("\nPapermill Translation Status\n"); 381 console.log( 382 "Paper".padEnd(30) + 383 LANGS.map((l) => LANG_NAMES[l].padEnd(12)).join(""), 384 ); 385 console.log("-".repeat(30 + LANGS.length * 12)); 386 let currentDir = ""; 387 for (const f of files) { 388 if (f.dir !== currentDir) { 389 currentDir = f.dir; 390 process.stdout.write(f.dir.padEnd(30)); 391 } 392 const status = f.pdfExists ? "PDF" : f.texExists ? "tex" : "---"; 393 process.stdout.write(status.padEnd(12)); 394 if (LANGS.indexOf(f.lang) === LANGS.length - 1) 395 process.stdout.write("\n"); 396 } 397 // Cards status 398 const cardsFiles = findCardsFiles(); 399 const hasCards = cardsFiles.some((f) => f.texExists); 400 if (hasCards) { 401 console.log("Cards Format\n"); 402 console.log("Paper".padEnd(30) + "Status"); 403 console.log("-".repeat(42)); 404 for (const f of cardsFiles) { 405 const status = f.pdfExists ? "PDF" : f.texExists ? "tex" : "---"; 406 console.log(f.dir.padEnd(30) + status); 407 } 408 console.log(); 409 } 410} else if (cmd === "build") { 411 let files; 412 if (formatFlag === "cards") { 413 // Build English cards 414 files = findCardsFiles(); 415 } else { 416 files = findTranslatedFiles(formatFlag).filter( 417 (f) => !langFilter || f.lang === langFilter, 418 ); 419 } 420 const toBuild = files.filter((f) => f.texExists); 421 const label = formatFlag ? ` (${formatFlag} format)` : ""; 422 console.log( 423 `\nBuilding ${toBuild.length} paper${toBuild.length !== 1 ? "s" : ""}${label}...\n`, 424 ); 425 let ok = 0, 426 fail = 0; 427 for (const entry of toBuild) { 428 if (buildPaper(entry)) ok++; 429 else fail++; 430 } 431 console.log(`\nDone: ${ok} built, ${fail} failed.\n`); 432} else if (cmd === "deploy") { 433 const files = findTranslatedFiles(); 434 const toDeploy = files.filter((f) => f.pdfExists); 435 console.log( 436 `\nDeploying ${toDeploy.length} translated PDF${toDeploy.length !== 1 ? "s" : ""}...\n`, 437 ); 438 for (const entry of toDeploy) { 439 deployPaper(entry); 440 } 441 // Deploy English cards PDFs 442 const cardsFiles = findCardsFiles(); 443 const cardsToDeploy = cardsFiles.filter((f) => f.pdfExists); 444 if (cardsToDeploy.length > 0) { 445 console.log( 446 `\nDeploying ${cardsToDeploy.length} cards PDF${cardsToDeploy.length !== 1 ? "s" : ""}...\n`, 447 ); 448 for (const entry of cardsToDeploy) { 449 deployPaper(entry); 450 } 451 } 452 // Auto sync-index on deploy 453 syncIndex(); 454 console.log("\nDone.\n"); 455} else if (cmd === "sync-index") { 456 syncIndex(); 457} else { 458 console.log( 459 "Usage: node papermill.mjs [build [da|es|zh] [--format cards] | deploy | sync-index | status]", 460 ); 461}