Something went wrong. Try again.
Monorepo for Aesthetic.Computer aesthetic.computer
Something went wrong. Try again.
JavaScript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144#!/usr/bin/env node// fix-dossier-tables.mjs — make dossier tables stop overflowing the column.//// The bug: many dossiers use `\begin{tabularx}{\columnwidth}{lr}` (or// similar) with NO `X` column. tabularx needs at least one X column to// distribute width; with none it ignores the width target and lays the// table out at natural width, which overflows the narrow two-column// measure and gets clipped at the page edge (the "page 4 tables cut// off" report). Longer strings in translations make it worse.//// Fix: every tabularx whose colspec has no `X` is converted to a plain// `tabular` wrapped in `\resizebox{\columnwidth}{!}{...}` so it can// never exceed the column. tabularx blocks that DO have an X column// already wrap correctly and are left untouched. Idempotent.//// Usage:// node papers/bin/fix-dossier-tables.mjs (all dossiers, en)// node papers/bin/fix-dossier-tables.mjs arxiv-recurse// node papers/bin/fix-dossier-tables.mjs --dry
import { readFileSync, writeFileSync, existsSync, readdirSync } from "node:fs";import { resolve, dirname, join } from "node:path";import { fileURLToPath } from "node:url";
const HERE = dirname(fileURLToPath(import.meta.url));const PAPERS_DIR = resolve(HERE, "..");
const DOSSIERS = [ "arxiv-rhizome", "arxiv-sfpc", "arxiv-eyebeam", "arxiv-recurse", "arxiv-internet-archive", "arxiv-mellon", "arxiv-pioneer-works", "arxiv-new-inc", "arxiv-studio-museum", "arxiv-hathitrust", "arxiv-the-kitchen", "arxiv-machine-project", "arxiv-heavy-manners-library", "arxiv-microvision", "arxiv-calarts-news",];
const argv = process.argv.slice(2);const dry = argv.includes("--dry");const only = argv.filter((a) => !a.startsWith("--"));
const MARK = "% [fix-dossier-tables]";
// Read a brace-balanced {...} group starting at `i` (s[i] must be '{').// Returns { body, end } where end is the index just past the closing '}'.function readGroup(s, i) { if (s[i] !== "{") return null; let depth = 0; for (let j = i; j < s.length; j++) { if (s[j] === "{") depth++; else if (s[j] === "}") { depth--; if (depth === 0) return { body: s.slice(i + 1, j), end: j + 1 }; } } return null;}
function fixFile(path) { let s = readFileSync(path, "utf8"); const orig = s; let out = ""; let i = 0; let fixed = 0; let skipped = 0;
while (i < s.length) { const at = s.indexOf("\\begin{tabularx}", i); if (at === -1) { out += s.slice(i); break; } // width group, then colspec group const wStart = at + "\\begin{tabularx}".length; const w = readGroup(s, wStart); if (!w) { out += s.slice(i, wStart); i = wStart; continue; } const c = readGroup(s, w.end); if (!c) { out += s.slice(i, w.end); i = w.end; continue; }
const colspec = c.body; const hasX = /X/.test(colspec); const endTok = "\\end{tabularx}"; const endAt = s.indexOf(endTok, c.end); if (endAt === -1) { out += s.slice(i, c.end); i = c.end; continue; }
const inner = s.slice(c.end, endAt); // table body between header and \end const alreadyWrapped = s.slice(Math.max(0, at - 120), at).includes(MARK);
if (hasX || alreadyWrapped) { // Leave well-formed (X-column) or already-fixed tables alone. out += s.slice(i, endAt + endTok.length); i = endAt + endTok.length; skipped++; continue; }
// Convert: tabularx (no X) → tabular wrapped in a width clamp. out += s.slice(i, at); out += `${MARK}\n\\resizebox{\\columnwidth}{!}{%\n` + `\\begin{tabular}{${colspec}}` + inner + `\\end{tabular}%\n}`; i = endAt + endTok.length; fixed++; }
// Ensure \resizebox is available (graphicx). Almost always already // loaded for \includegraphics, but inject defensively if absent. if (fixed > 0 && !/\\usepackage(\[[^\]]*\])?\{graphicx\}/.test(out) && !/\\usepackage(\[[^\]]*\])?\{graphics\}/.test(out)) { out = out.replace( /(\\documentclass[^\n]*\n)/, `$1\\usepackage{graphicx} ${MARK}\n`, ); }
const changed = out !== orig; if (changed && !dry) writeFileSync(path, out, "utf8"); return { changed, fixed, skipped };}
const targets = (only.length ? only : DOSSIERS).filter((d) => existsSync(join(PAPERS_DIR, d)),);
let totalFixed = 0;for (const dir of targets) { const full = join(PAPERS_DIR, dir); for (const f of readdirSync(full)) { // English + any translation, but NOT the generated -cards.tex // (cards-convert already converts tabularx→tabular for cards). if (!f.endsWith(".tex") || f.endsWith("-cards.tex")) continue; const r = fixFile(join(full, f)); if (r.fixed || r.changed) { totalFixed += r.fixed; console.log( ` ${r.changed ? (dry ? "would fix" : "fixed") : "ok"} ${dir}/${f} — ` + `${r.fixed} wrapped, ${r.skipped} left (X-col/already)`, ); } }}console.log(`\n${dry ? "[dry] " : ""}${totalFixed} table(s) wrapped across ${targets.length} dossier(s).`);