// CLI: parse ESC/POS .bin print jobs logged by the proxy. // // This is a *parser*: it decomposes the raw byte stream into the ESC/POS // command grammar — each control sequence recognized by its opcode with its // parsed parameters, and text as raw byte spans (decoded via the selected code // page). Nothing is interpreted into higher-level meaning (no "this is bold", // no line/style inference). Every byte is preserved and the stream can be // rebuilt via `serialize()` (verified by default). // // Usage: // node parse-log.mjs [options] // // Options: // --all treat each argument as a directory and parse all *.bin in it // --hex also print the raw bytes of every token // --text print only the decoded text spans (convenience view) // --image render raster bit images as ASCII art (downsampled) // --max-width n ASCII image width (default 80) // --no-verify skip the losslessness round-trip check import fs from "node:fs"; import path from "node:path"; import { parse, serialize, renderText, renderImage, renderTokens, summarize } from "./parser.js"; function parseArgs(argv) { const opts = { files: [], mode: "tokens", hex: false, image: false, maxWidth: 80, all: false, verify: true }; for (let i = 0; i < argv.length; i++) { const a = argv[i]; if (a === "--text") opts.mode = "text"; else if (a === "--hex") opts.hex = true; else if (a === "--image") opts.image = true; else if (a === "--all") opts.all = true; else if (a === "--no-verify") opts.verify = false; else if (a === "--max-width") opts.maxWidth = Number(argv[++i]) || 80; else if (a.startsWith("--")) console.error(`[warn] unknown option ${a}`); else opts.files.push(a); } return opts; } function expandTargets(targets, all) { const out = []; for (const t of targets) { const stat = fs.statSync(t); if (stat.isDirectory()) { out.push( ...fs .readdirSync(t) .filter((f) => f.endsWith(".bin")) .map((f) => path.join(t, f)) .sort() ); } else out.push(t); } return out; } function main() { const opts = parseArgs(process.argv.slice(2)); if (opts.files.length === 0) { console.error( "usage: node parse-log.mjs [--hex|--text] [--image] [--all] [--no-verify]" ); process.exit(1); } const files = expandTargets(opts.files, opts.all); let hadError = false; let allOk = true; for (const f of files) { let buf; try { buf = fs.readFileSync(f); } catch (err) { console.error(`[error] cannot read ${f}: ${err.message}`); hadError = true; continue; } const events = parse(buf); const s = summarize(events); const name = path.basename(f); let ok = true; if (opts.verify) { const out = serialize(events); ok = out.length === buf.length && out.equals(buf); if (!ok) { allOk = false; console.error(`[FAIL] ${name}: round-trip mismatch (${out.length} vs ${buf.length} bytes)`); } } const status = opts.verify ? (ok ? "OK" : "LOSSY") : "unverified"; if (opts.mode === "text") { console.log(`\n===== ${name} (${buf.length} bytes) =====\n`); console.log(renderText(events)); continue; } console.log(`\n===== ${name} (${buf.length} bytes) — round-trip: ${status} =====\n`); console.log(renderTokens(events, { hex: opts.hex })); if (opts.image) { for (const e of events) { if (e.type === "image") { console.log(`\n-- raster image ${e.width}x${e.height} --`); console.log(renderImage(e, opts.maxWidth)); } } } console.log( `\n[summary] ${s.commands} commands, ${s.images} image(s), ${s.cuts} cut(s), ` + `${s.text} text runs (${s.textBytes} bytes)` ); } if (opts.verify && !allOk) { console.error("\n[error] at least one file was not losslessly reconstructed"); process.exit(1); } process.exit(hadError ? 1 : 0); } main();