SPB Git

Repository Public Research preview

latex.mjs 7.7 KB 98 lines javascript

1#!/usr/bin/env node2/**3 * Command-line front end of the LaTeX QA engine (no dependencies).4 * Works inside LATEX_MCP_ROOT (default: the current directory). Prints JSON.5 * Exit codes: 0 = done and the check passed, 1 = the check failed, 2 = usage or tool error.6 */7import { Latex, explainError } from "./lib/engine.mjs";89const USAGE = `Usage: node latex.mjs <command> [file.tex] [options]1011  status                     installed tools, engines, root12  qa <file>                  whole procedure: clean build, log, overflow, references, bibliography; one verdict13  compile <file>             build to PDF        --engine pdflatex|xelatex|lualatex  --clean  --keep-going  --driver builtin14  log <file>                 parsed log of the last build15  overflow <file>            overfull boxes; overflowing Beamer frames   --hbox <pt>  --vbox <pt>16  beamer <file>              frame-by-frame report of a deck17  refs <file>                labels, references, citations (static, no build needed)18  bib <file>                 bibliography backend and its log19  count <file>               word count20  lint <file>                chktex, curated rules   --all21  numbering <file>           numbers of labels, captions, citations from the .aux22  docx <file>                LaTeX → Word with resolved numbers   --out <path.docx>  --overwrite  --csl <file>23  text <file>                text of the PDF   --from <page>  --to <page>24  explain "<error message>"  cause and fix of an error25  clean <file> --yes         remove the build directory made by this tool2627Common options: --build-dir <dir>  --timeout <seconds>28Shell escape is always off. Paths must stay inside LATEX_MCP_ROOT (default: current directory).`;2930const argv = process.argv.slice(2);31const cmd = argv.shift();32const opts = {}, pos = [];33const FLAGS = new Set(["clean", "keep-going", "all", "overwrite", "yes", "help"]);34for (let i = 0; i < argv.length; i++) {35  const a = argv[i];36  if (a.startsWith("--")) { const k = a.slice(2); if (FLAGS.has(k)) opts[k] = true; else opts[k] = argv[++i]; }37  else pos.push(a);38}39const out = (data, code = 0) => { process.stdout.write(JSON.stringify(data, null, 2) + "\n"); process.exitCode = code; };40const num = (v) => (v == null ? undefined : Number(v));4142if (!cmd || cmd === "help" || cmd === "--help" || opts.help) { process.stdout.write(USAGE + "\n"); process.exitCode = cmd ? 0 : 2; }43else {44  try {45    if (cmd === "explain") {46      if (!pos[0]) throw Object.assign(new Error("explain needs the error message"), { code: "usage" });47      out({ message: pos.join(" "), ...explainError(pos.join(" ")) });48    } else {49      const L = new Latex(process.env);50      const base = { file: pos[0], build_dir: opts["build-dir"] };51      const needFile = () => { if (!pos[0]) throw Object.assign(new Error(`${cmd} needs a .tex file`), { code: "usage" }); };52      const compileArgs = { ...base, engine: opts.engine, clean: !!opts.clean, halt_on_error: !opts["keep-going"], driver: opts.driver, timeout_s: num(opts.timeout) };53      switch (cmd) {54        case "status": out(await L.toolchainStatus()); break;55        case "compile": { needFile(); const r = await L.compile(compileArgs); out(r, r.success ? 0 : 1); break; }56        case "log": { needFile(); const r = L.parseLogFile(base); out(r, r.summary.errors ? 1 : 0); break; }57        case "overflow": { needFile(); const r = L.checkOverflow({ ...base, hbox_threshold_pt: num(opts.hbox), vbox_threshold_pt: num(opts.vbox) }); out(r, r.passed && !(r.beamer?.overflowing_frames.length) ? 0 : 1); break; }58        case "beamer": { needFile(); const r = L.beamerReport({ ...base, hbox_threshold_pt: num(opts.hbox), vbox_threshold_pt: num(opts.vbox) }); out(r, r.overflowing_frames.length ? 1 : 0); break; }59        case "refs": { needFile(); const r = L.auditReferences(base); out(r, r.passed ? 0 : 1); break; }60        case "bib": { needFile(); const r = L.bibAudit(base); out(r, r.passed || r.expected_backend == null ? 0 : 1); break; }61        case "count": needFile(); out(await L.wordCount(base)); break;62        case "lint": needFile(); out(await L.lint({ ...base, all: !!opts.all })); break;63        case "numbering": needFile(); out(L.extractNumbering(base)); break;64        case "docx": { needFile(); const r = await L.toDocx({ ...base, output: opts.out, overwrite: !!opts.overwrite, csl: opts.csl }); out(r, r.unresolved_labels.length ? 1 : 0); break; }65        case "text": needFile(); out(await L.pdfText({ ...base, first_page: num(opts.from), last_page: num(opts.to) })); break;66        case "clean": needFile(); out(L.cleanBuild({ ...base, confirm: !!opts.yes })); break;67        case "qa": {68          needFile();69          const build = await L.compile({ ...compileArgs, clean: true });70          const checks = [{ check: "clean build from scratch", passed: build.success, detail: build.success ? `${build.pages} pages` : build.errors.map((e) => `${e.file ?? "?"}:${e.line ?? "?"} ${e.message}`).slice(0, 5) }];71          let overflow = null, refs = null, bib = null;72          if (build.log) {73            const s = build.summary;74            checks.push({ check: "no undefined reference or citation in the log", passed: s.undefined_references + s.undefined_citations === 0, detail: [...build.undefined_references.map((r) => `ref ${r.label} (${r.file}:${r.line})`), ...build.undefined_citations.map((r) => `cite ${r.key} (${r.file}:${r.line})`)] });75            checks.push({ check: "no multiply defined label", passed: s.duplicate_labels === 0, detail: build.duplicate_labels.map((d) => d.label) });76            checks.push({ check: "no rerun pending", passed: !s.rerun_needed, detail: [] });77            overflow = L.checkOverflow({ ...base, hbox_threshold_pt: num(opts.hbox), vbox_threshold_pt: num(opts.vbox) });78            checks.push({ check: `no overfull box above ${overflow.thresholds_pt.hbox} pt`, passed: overflow.passed, detail: [...overflow.overfull_hbox, ...overflow.overfull_vbox].slice(0, 10).map((b) => `${b.box} ${b.amount_pt} pt at ${b.file ?? "?"}:${b.line_start ?? "?"}`) });79            if (overflow.beamer) checks.push({ check: "no Beamer frame overflows", passed: overflow.beamer.overflowing_frames.length === 0, detail: overflow.beamer.overflowing_frames.map((f) => `frame ${f.number} "${f.title ?? "untitled"}" (${f.file}:${f.line}) +${f.overflow_pt} pt`) });80          }81          refs = L.auditReferences(base);82          checks.push({ check: "every float is referenced", passed: refs.unreferenced_floats.length === 0, detail: refs.unreferenced_floats.map((f) => `${f.label} (${f.file}:${f.line})`) });83          checks.push({ check: "no duplicate label in the sources", passed: refs.duplicate_labels.length === 0, detail: refs.duplicate_labels.map((d) => `${d.label}: ${d.locations.map((l) => `${l.file}:${l.line}`).join(", ")}`) });84          bib = L.bibAudit(base);85          if (bib.expected_backend) checks.push({ check: `bibliography (${bib.expected_backend}) ran without missing entry`, passed: bib.passed, detail: [...(bib.missing_entries ?? []), ...(bib.backend_mismatch ? [bib.backend_mismatch] : []), ...(bib.note ? [bib.note] : [])] });86          const passed = checks.every((c) => c.passed);87          out({ file: pos[0], passed, checks, pdf: build.pdf, pages: build.pages, engine: build.engine, shell_commands_blocked: build.shell_commands_blocked, advisory: { unused_labels: refs.unused_labels.length, unused_bib_entries: refs.unused_bib_entries.map((e) => e.key), incomplete_bib_entries: bib.incomplete_entries, underfull_boxes: overflow?.underfull_count ?? null } }, passed ? 0 : 1);88          break;89        }90        default: process.stderr.write(`Unknown command: ${cmd}\n\n${USAGE}\n`); process.exitCode = 2;91      }92    }93  } catch (e) {94    process.stdout.write(JSON.stringify({ error: { code: e.code ?? "error", message: String(e.message ?? e) } }, null, 2) + "\n");95    process.exitCode = 2;96  }97}98
Files main
Menu

Theme