{"_id":"@arena.ai/faster-latex","_rev":"3-4df8d5740512c6656b2a087a4ea83a4c","name":"@arena.ai/faster-latex","dist-tags":{"placeholder":"0.0.0","latest":"0.3.1"},"versions":{"0.0.0":{"name":"@arena.ai/faster-latex","version":"0.0.0","license":"Apache-2.0","_id":"@arena.ai/faster-latex@0.0.0","maintainers":[{"name":"matthova","email":"matthova87@gmail.com"}],"homepage":"https://github.com/Arena-OSS/faster-latex#readme","bugs":{"url":"https://github.com/Arena-OSS/faster-latex/issues"},"dist":{"shasum":"a3d536e885a9b9e1899df2b8cedc8e01d8f905dc","tarball":"https://registry.npmjs.org/@arena.ai/faster-latex/-/faster-latex-0.0.0.tgz","fileCount":4,"integrity":"sha512-UbijcqTFXPdZsOjQBOExfGVvhvlLqx42u5K9bjFdzccu4jD7nE17WZCV+iBl6ovNPWv9Ifr7HkVZiKlXuDWBWw==","signatures":[{"sig":"MEUCICKRWcVm1cdmdum8PFzidKrdvge7QXeQ18wdqRmFj6FaAiEAxRB6f8a2NNyPqDjnfC3ySTm4NjE9IPf9ugEVCrUq1zs=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":1153},"main":"index.js","type":"module","types":"index.d.ts","_npmUser":{"name":"matthova","email":"matthova87@gmail.com"},"repository":{"url":"git+https://github.com/Arena-OSS/faster-latex.git","type":"git"},"_npmVersion":"10.9.8","description":"Reserved placeholder for the faster-latex SDK. Not yet released — see the `faster-latex` package.","directories":{},"_nodeVersion":"22.22.3","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"_npmOperationalInternal":{"tmp":"tmp/faster-latex_0.0.0_1787637195938_0.3214121769085718","host":"s3://npm-registry-packages-npm-production"}},"0.3.0":{"name":"@arena.ai/faster-latex","version":"0.3.0","keywords":["latex","html","mathml","wasm","webassembly","parser","math","renderer"],"author":{"name":"Matt Hova"},"license":"Apache-2.0","_id":"@arena.ai/faster-latex@0.3.0","maintainers":[{"name":"matthova","email":"matthova87@gmail.com"}],"homepage":"https://github.com/matthova/faster-latex#readme","bugs":{"url":"https://github.com/matthova/faster-latex/issues"},"dist":{"shasum":"a76d19eefff34519f5497f94059be75fc2cd8ed4","tarball":"https://registry.npmjs.org/@arena.ai/faster-latex/-/faster-latex-0.3.0.tgz","fileCount":32,"integrity":"sha512-fYIgY7cH1ZXJOLGZuaeekR0g52EyqPO2pjfGWVheyubbRXvhtEUJh40ZOE4E7zevAhtJoRmikfAu+QvMpkPH/g==","signatures":[{"sig":"MEQCIEMJA6yJ1hPbKKidP5zMLN+1/3toI7pt92JdtbMWSRomAiBl1zpuvCa5LIdVh1yh2vzjMS/IHSQTWjKzbdVxnyv8dQ==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":5205063},"main":"./bundler/faster_latex.js","type":"module","types":"./bundler/faster_latex.d.ts","module":"./bundler/faster_latex.js","exports":{".":{"types":"./bundler/faster_latex.d.ts","default":"./bundler/faster_latex.js"},"./web":{"types":"./web/faster_latex.d.ts","default":"./web/faster_latex.js"},"./node":{"types":"./node/faster_latex.d.ts","default":"./node/faster_latex.js"},"./worker":{"types":"./worker/index.d.ts","default":"./worker/index.js"},"./web-pdf":{"types":"./web-pdf/faster_latex.d.ts","default":"./web-pdf/faster_latex.js"},"./package.json":"./package.json","./web-graphics":{"types":"./web-graphics/faster_latex.d.ts","default":"./web-graphics/faster_latex.js"},"./worker/worker.js":"./worker/worker.js","./packages/manifest.json":"./packages/manifest.json"},"gitHead":"5f69162199849558db370ec20277e062c44eb06e","_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:118ade06-e470-466d-86f5-03015bd67df4"}},"repository":{"url":"git+https://github.com/matthova/faster-latex.git","type":"git","directory":"sdk/faster-latex"},"_npmVersion":"12.0.2","description":"Render full LaTeX documents to HTML (with MathML math) in the browser or on the server (SSR) — pure Rust compiled to WebAssembly.","directories":{},"sideEffects":["./bundler/faster_latex.js","./web/faster_latex.js"],"_nodeVersion":"22.23.2","publishConfig":{"access":"public","provenance":false},"_hasShrinkwrap":false,"_npmOperationalInternal":{"tmp":"tmp/faster-latex_0.3.0_1787669629816_0.3378791092270139","host":"s3://npm-registry-packages-npm-production"}},"0.3.1":{"name":"@arena.ai/faster-latex","version":"0.3.1","description":"Render full LaTeX documents to HTML (with MathML math) in the browser or on the server (SSR) — pure Rust compiled to WebAssembly.","type":"module","license":"Apache-2.0","author":{"name":"Matt Hova"},"repository":{"type":"git","url":"git+https://github.com/matthova/faster-latex.git","directory":"sdk/faster-latex"},"homepage":"https://github.com/matthova/faster-latex#readme","bugs":{"url":"https://github.com/matthova/faster-latex/issues"},"keywords":["latex","html","mathml","wasm","webassembly","parser","math","renderer"],"main":"./bundler/faster_latex.js","module":"./bundler/faster_latex.js","types":"./bundler/faster_latex.d.ts","exports":{".":{"types":"./bundler/faster_latex.d.ts","default":"./bundler/faster_latex.js"},"./web":{"types":"./web/faster_latex.d.ts","default":"./web/faster_latex.js"},"./web-graphics":{"types":"./web-graphics/faster_latex.d.ts","default":"./web-graphics/faster_latex.js"},"./web-pdf":{"types":"./web-pdf/faster_latex.d.ts","default":"./web-pdf/faster_latex.js"},"./node":{"types":"./node/faster_latex.d.ts","default":"./node/faster_latex.js"},"./worker":{"types":"./worker/index.d.ts","default":"./worker/index.js"},"./worker/worker.js":"./worker/worker.js","./packages/manifest.json":"./packages/manifest.json","./package.json":"./package.json"},"sideEffects":["./bundler/faster_latex.js","./web/faster_latex.js"],"publishConfig":{"access":"public","provenance":false},"_id":"@arena.ai/faster-latex@0.3.1","_integrity":"sha512-TzDde5wZscDjUYaKuzHiEbHC3Rb8U29hvy6LBo8CxTYI7z5RYAXfO+BX60ks4ySj+WqAlkTj6dExpUQ80vAceg==","_resolved":"/tmp/fl-release-jYDMGu/arena.ai-faster-latex-0.3.1.tgz","_from":"file:/tmp/fl-release-jYDMGu/arena.ai-faster-latex-0.3.1.tgz","_nodeVersion":"22.23.2","_npmVersion":"12.0.2","dist":{"integrity":"sha512-TzDde5wZscDjUYaKuzHiEbHC3Rb8U29hvy6LBo8CxTYI7z5RYAXfO+BX60ks4ySj+WqAlkTj6dExpUQ80vAceg==","shasum":"245d5d5cd40e6e2b08d220d979db1239d152723a","tarball":"https://registry.npmjs.org/@arena.ai/faster-latex/-/faster-latex-0.3.1.tgz","fileCount":32,"unpackedSize":5220155,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIQC6UFK65zXLF6nlilBAx+ij14GV2edo/wKfUVNyPiaoTwIgM07Gm42/miVj31CnGUWH0cOKOXHFj3qBP6qbqK5wN0Y="}]},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:118ade06-e470-466d-86f5-03015bd67df4"}},"directories":{},"maintainers":[{"name":"matthova","email":"matthova87@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/faster-latex_0.3.1_1787862900110_0.8669193836187785"},"_hasShrinkwrap":false}},"time":{"created":"2026-08-25T05:53:15.789Z","modified":"2026-08-27T20:35:00.575Z","0.0.0":"2026-08-25T05:53:16.091Z","0.3.0":"2026-08-25T14:53:49.987Z","0.3.1":"2026-08-27T20:35:00.287Z"},"bugs":{"url":"https://github.com/matthova/faster-latex/issues"},"author":{"name":"Matt Hova"},"license":"Apache-2.0","homepage":"https://github.com/matthova/faster-latex#readme","keywords":["latex","html","mathml","wasm","webassembly","parser","math","renderer"],"repository":{"type":"git","url":"git+https://github.com/matthova/faster-latex.git","directory":"sdk/faster-latex"},"description":"Render full LaTeX documents to HTML (with MathML math) in the browser or on the server (SSR) — pure Rust compiled to WebAssembly.","maintainers":[{"name":"matthova","email":"matthova87@gmail.com"}],"readme":"# faster-latex\n\nRender **full LaTeX documents to HTML** — sections, text, environments, lists,\ntables, cross-references, and math — in **pure Rust**, compiled to **WebAssembly**\nfor the browser. Math is delegated to\n[`pulldown-latex`](https://crates.io/crates/pulldown-latex), which emits **MathML\nCore** (rendered natively by every modern browser).\n\n- **Infallible by contract.** `render_to_html` never panics on any input; it\n  returns HTML plus a list of non-fatal warnings.\n- **Runtime macros.** `\\newcommand`/`\\newenvironment` work in text *and* inside\n  math — one thing pulldown-latex or LaTeX.js alone don't give you.\n- **Real documents, not just snippets.** Front matter (`\\title`/`\\author` after\n  `\\begin{document}`), natbib citations, a `.bib` bibliography, `\\chapter`/`\\part`,\n  `\\input`/`\\include`, `\\DeclareMathOperator` and physics/siunitx math, and\n  counter registers all render — the constructs ordinary papers actually use.\n- **Small and dependency-light.** The core crate's only runtime dependency is\n  `pulldown-latex` (→ `bumpalo`). The core WASM bundle is CI-gated at 640 KiB.\n- **PDF without a browser.** `fl doc.tex --pdf` runs a TeX-style layout engine\n  (Knuth–Plass line breaking, OpenType-`MATH` math) and writes the PDF itself —\n  no Chrome, no Node, no network. It also compiles to wasm, so the same engine\n  exports a PDF *in* the browser from a lazily loaded bundle. See\n  [`docs/pdf-output.md`](docs/pdf-output.md).\n- **Apache-2.0 licensed, with no copyleft anywhere in the stack.**\n\n```rust\nuse faster_latex::{render_to_html, Options};\n\nlet out = render_to_html(\n    r\"\\section{Hi} Euler: $e^{i\\pi}+1=0$.\",\n    &Options::default(),\n);\nassert!(out.html.contains(\"<h2\"));\nassert!(out.html.contains(\"<math\"));\nfor w in &out.warnings {\n    eprintln!(\"{} (line {}): {}\", w.kind.as_str(), w.line, w.message);\n}\n```\n\n## Pipeline\n\n```\nsource ─▶ lex ─▶ expand macros ─▶ parse ─▶ number (pass 1) ─▶ emit HTML (pass 2)\n                                                │\n                                    math snippets ─▶ pulldown-latex ─▶ MathML\n```\n\nMath and verbatim are captured as **opaque raw slices in the lexer**, before the\nmacro expander runs, so their contents can never be mangled. Macro expansion is\ndriven by an explicit work stack (no recursion) with a **fuel budget** and\n**depth limit**, so macro bombs warn instead of hanging. Every user macro is also\nrecorded in `\\def` form and re-fed to pulldown-latex per math snippet, which is\nhow a `\\newcommand` defined once works in both text and `$…$`.\n\n## Live editing\n\n`render` is the API for rendering a document once. For an editor, use `Session`,\nwhich re-renders and hands back only the top-level blocks that changed:\n\n```js\nconst session = new Session();\nconst tpl = document.createElement(\"template\");\n\nfunction onInput(src) {\n  const u = session.update(src, opts);\n  if (u.full) {\n    preview.innerHTML = u.patch.html;\n    return;\n  }\n  const article = preview.firstElementChild;\n  const anchor = article.children[u.patch.from + u.patch.removed] ?? null;\n  for (let i = 0; i < u.patch.removed; i++) article.children[u.patch.from].remove();\n  if (u.patch.html) {\n    tpl.innerHTML = u.patch.html;\n    article.insertBefore(tpl.content, anchor);\n  }\n}\n```\n\nThe engine is not what makes a keystroke slow on a large document — the browser\nis, relaying out a DOM that `innerHTML` just discarded and rebuilt for an edit\nthat changes a median of **one byte** of the output. On Euclid's *Elements*\n(614 KiB of LaTeX, 1.4 MB of HTML) a keystroke costs **344 ms** assigning\n`innerHTML` and **61 ms** applying the splice; on Keynes' *A Treatise on\nProbability* (1.28 MB), 347 ms → 85 ms. The wasm bridge itself is 1.7 ms of that,\nwhich is why \"send less over the bridge\" is not the fix.\n\nThe measurements, the correctness oracle, and what is still slow are in\n[`docs/incremental-rendering.md`](docs/incremental-rendering.md); the harness is\n[`bench/incremental/`](bench/incremental/).\n\n## Supported subset (≈ LaTeX.js scope + runtime macros)\n\n| Area | Supported |\n|------|-----------|\n| Structure | `\\documentclass`, preamble, `\\title`/`\\author`/`\\date`/`\\maketitle` (in preamble **or** body), `\\tableofcontents`, `\\appendix`, `\\today` |\n| Front matter | `\\subtitle`, `\\and`, `\\thanks`, `\\affiliation`/`\\institution`/`\\institute`/`\\address`, `\\email`, `\\keywords`/`IEEEkeywords`, `\\subjclass`, `\\ccsdesc` (+ `CCSXML` dropped), `\\IEEEauthorblockN`/`A`, `\\inst` |\n| Sectioning | `\\part`/`\\chapter` (book/report) … `\\subparagraph` (+ `*`), automatic numbering (chapter-prefixed, appendix letters, Roman parts), `\\label`/`\\ref`/`\\eqref`/`\\pageref`/`\\nameref` |\n| Multi-file | `\\input`/`\\include` spliced from a caller-supplied file map; `\\frontmatter`/`\\mainmatter`/`\\backmatter` |\n| Text | `\\textbf` `\\textit` `\\emph` `\\texttt` `\\textsc` `\\underline`; declaration forms (`\\bfseries`, …); font sizes `\\tiny`…`\\Huge`; `\\\\`, paragraphs, comments |\n| Typography | `~`, `--`/`---`, `` `` ``/`''` quotes, accents → Unicode, many symbol commands (`\\LaTeX`, `\\S`, `\\ss`, `\\ae`, …), `\\verb` |\n| Lists | `itemize`, `enumerate`, `description` (nested), `\\item[…]` |\n| Blocks | `quote`, `quotation`, `verse`, `center`, `flushleft`, `flushright`, `abstract`, `verbatim` |\n| Floats | `figure`/`table` with `\\caption` + numbering, `\\includegraphics` (URL-sanitized; relative widths honored in paper mode), TikZ/PGF & pgfplots → SVG (`graphics` feature) |\n| Tables | `tabular` with `l`/`c`/`r` columns and `\\hline` |\n| Citations | natbib `\\cite`/`\\citep`/`\\citet`/`\\citealt`/`\\citealp`/`\\citeauthor`/`\\citeyear`/`\\citenum`/`\\nocite` (+ post-notes, author-year labels) |\n| Bibliography | `thebibliography`/`\\bibitem`, **or** `\\bibliography{refs}` resolved from a `.bib` file (cited entries, in order) |\n| Counters | `\\newcounter`/`\\setcounter`/`\\stepcounter`/`\\addtocounter`/`\\value`, `\\arabic`/`\\alph`/`\\Alph`/`\\roman`/`\\Roman`/`\\fnsymbol`; `\\newif`/`\\iffoo`…`\\else`…`\\fi` |\n| Math | `$…$`, `\\(…\\)`, `\\[…\\]`, `$$…$$`, `equation`(*), `align`(*) with per-line numbering + `\\notag`, `gather`, `multline`, `displaymath`; `\\DeclareMathOperator`(*), `\\DeclarePairedDelimiter`, `\\ensuremath`, physics & siunitx shims |\n| Macros | `\\newcommand`/`\\renewcommand`/`\\providecommand` (9 args + optional default), `\\newenvironment` |\n\n### Deliberately punted (warn, never crash)\n\nReal package loading (`\\usepackage` is ignored with a warning); a general TeX\nexpander (`\\def` bodies, `\\ifnum`/`\\ifx`/`\\csname`/`\\expandafter`, catcodes —\n`\\newif` conditionals *are* honoured); `\\newcommand` with an optional argument\nused **inside math** (warns and falls back to raw rather than mis-render);\ncustom numbering formats (`\\renewcommand{\\thesection}{…}`, `\\numberwithin`) are\ninert. Unknown commands render as a visible `<span class=\"fl-unknown\">` fallback\n(off by default); unknown environments still render their contents. Anything\nunsupported produces a `Warning`, not a failure — and never leaks its argument\ninto the prose (enforced by the `corpus_argument_leaks` oracle over complete\ndocuments).\n\n## Security\n\nRendered HTML is safe to insert into a page:\n\n- All text is HTML-escaped; math text (`\\text{…}`) is escaped by pulldown-latex.\n- `\\includegraphics` URLs are allowlisted to relative paths and `http`/`https`;\n  `javascript:`, `data:`, `vbscript:`, etc. are rejected with a warning and the\n  image is omitted (the URL is never echoed into the output).\n- Locked in by the `952-xss-attempts.tex` fixture and a panic-smoke test over\n  mutated inputs.\n\n## Rust API\n\n```rust\npub struct Options {\n    pub base_url: Option<String>,\n    pub predefined_macros: Vec<String>,       // raw \\newcommand… lines\n    pub files: BTreeMap<String, String>,      // \\input/\\include/\\bibliography sources\n    pub full_document: bool,                  // full <html> shell vs. bare <article>\n    pub paper: bool,                          // LaTeX-faithful paged \"paper\" mode (needs `paper` feature)\n    pub max_expansions: u32,                  // macro-bomb guard (default 100_000)\n    pub max_nodes: u32,                       // node-count guard (default 500_000)\n    pub debug_unknown: bool,                  // render unknown constructs as red badges\n    pub today: Option<String>,                // what \\today expands to (engine reads no clock)\n}\npub struct RenderOutput {\n    pub html: String,\n    pub warnings: Vec<Warning>,\n    pub effective_source: String,             // input after macro-prepend + include splicing\n}\npub fn render_to_html(source: &str, options: &Options) -> RenderOutput; // infallible\npub const ARTICLE_CSS: &str;  // shipped web stylesheet, version-locked to the crate\n#[cfg(feature = \"paper\")]\npub const PAPER_CSS: &str;    // LaTeX-faithful paged stylesheet (paper mode)\npub const VERSION: &str;\n\n#[cfg(feature = \"pdf\")]        // browser-free PDF output — see docs/pdf-output.md\npub mod pdf {\n    pub struct PdfOptions { pub fonts: FontSet, pub images: ImageSet,\n                            pub producer: Option<String>, pub title: Option<String>,\n                            pub compress: bool }\n    pub struct PdfOutput { pub bytes: Vec<u8>, pub pages: usize, pub warnings: Vec<Warning> }\n    pub fn render_to_pdf(source: &str, options: &Options, pdf: &PdfOptions) -> PdfOutput; // infallible\n}\n```\n\nOptional `serde` feature derives `Serialize` on `Warning`/`WarningKind`.\n\n### Paper mode (`paper` feature)\n\nThe default output is a responsive *web article*. The optional `paper` feature\n(implied by `cli`) adds a **LaTeX-faithful paged mode** — document-aware sheet\nsize/orientation, class- and style-aware text blocks and base type metrics,\nLatin Modern typography, `fancyhdr` running heads/feet as `@page` margin boxes,\nreal page breaks, a full title page, dot-leader TOC/LoF/LoT, a two-column index,\nand hanging-indent bibliography. Printed by headless Chrome it produces a\ndocument that reads as the same paper as a `pdflatex` build. It is gated behind\na feature so its CSS never enters the core wasm bundle, whose 640 KiB budget is\nenforced in CI.\n\n```sh\ncargo fl document.tex --paper -o document.html   # then print to PDF from Chrome\n```\n\nThe `docs/pdf-parity-plan.md` scoreboard and the `scripts/torture-{pdf,diff}.mjs`\nharness measure paper-mode output against a committed reference PDF (see\n`npm run torture:diff`; needs Chrome + poppler).\n\n### PDF output without a browser (`pdf` feature)\n\nThe optional `pdf` feature (also implied by `cli`) writes a **PDF directly**,\nwith no browser, no Node and no network:\n\n```sh\ncargo fl document.tex --pdf            # writes document.pdf\n```\n\nIt runs its own TeX-style layout engine over the numbered AST: Knuth–Plass\nparagraph breaking, page composition with floats and footnotes, and math laid\nout from the **OpenType `MATH` table** of Latin Modern Math (fraction and script\nshifts, stretchy delimiters built from the font's variant and assembly recipes,\nbig operators with limits). Cross-references resolve through a second pass, the\nway LaTeX's `.aux` file does. Output is searchable, hyperlinked, and byte-for-byte\nreproducible.\n\nFonts are supplied by the caller — the library never reads the filesystem — and\nthe CLI finds the SHA-pinned Latin Modern family that `npm run assets` caches.\nWith no fonts at all it falls back to the **PDF base-14** metrics that are\ncompiled in, so `render_to_pdf` is infallible and needs no assets.\n\n```rust\nuse faster_latex::{Options, pdf::{PdfOptions, render_to_pdf}};\nlet out = render_to_pdf(source, &Options::default(), &PdfOptions::default());\nassert!(out.bytes.starts_with(b\"%PDF-\"));\n```\n\nThree dependencies, all optional: `pdf-writer` (PDF objects), `ttf-parser`\n(OpenType metrics), and `hypher` (compact Knuth--Liang English hyphenation).\nNone reaches the **core** wasm bundle, which is\nbyte-for-byte unchanged by all of this.\n\nThe same engine runs in the browser, as a third lazily loaded bundle\n(`@arena.ai/faster-latex/web-pdf`, +109 KiB brotli over core) exporting `render_pdf`. It is\nfetched on the *click* that asks for a PDF, so it never touches the render path —\nthat is what the demo's **Export PDF** button does. Exporting the torture test\nfrom the browser and running `fl --pdf` on it produce byte-identical files.\n\nThis is newer and less proven than the Chrome path: on the torture test it\nscores **word F1 0.945, 13/15 pages, and PAA 0.612** (Chrome: 0.986, 15/15,\nand 0.99). English Knuth--Liang hyphenation is implemented; the largest\nremaining page-count gap is TeX's float-page composition. See\n[`docs/pdf-output.md`](docs/pdf-output.md) for the scoreboard\n(`npm run torture:diff:rust`), the design, and the ranked list of gaps.\n\nBeyond that one torture document, faster-latex is scored page-for-page against\n**all three de-facto LaTeX engines** — pdfLaTeX, XeLaTeX and LuaLaTeX — on the\nten demo documents, with pinned, reproducible goldens and a CI regression\nratchet. Each page also reports its distance from the three engines' own\npairwise spatial-consensus floor. The workflow, metrics, and gates are in\n[`docs/engine-parity-testing.md`](docs/engine-parity-testing.md) (`npm run\nexamples:reference` to build the goldens, `npm run examples:parity` to score).\nThe browser-free exporter has its own breadth scorer over those same 30 PDFs:\n`npm run examples:parity:direct` renders every fixture with `fl --pdf` and\nreports the worst pdfLaTeX/XeLaTeX/LuaLaTeX result.\nA head-to-head **four-renderer shootout** pits faster-latex against the three\nTeX engines on speed and pixel-perfection. The three TeX engines remain the\nparity oracle; faster-latex is timed and compared through its own browser-free\nPDF backend (`fl --pdf`), so both axes are measured PDF-to-PDF with no browser\nin the loop. See [`docs/engine-shootout.md`](docs/engine-shootout.md)\n(`npm run examples:shootout`; needs Docker and poppler).\nA separate **browser shootout** compares the shipped wasm bundle against\n[LaTeX.js](https://latex.js.org/), the closest pure-JS competitor, on speed,\ncompatibility, scaling and footprint — the two renderers you would actually\nchoose between for LaTeX in a page. See\n[`docs/latexjs-shootout.md`](docs/latexjs-shootout.md).\nThe ordered implementation plan for closing the remaining visual gap is\n[`docs/pixel-parity-roadmap.md`](docs/pixel-parity-roadmap.md).\n\nSeparately from all of that, `fl --trip` runs **Knuth's TRIP test** — the\ncertification torture test for TeX82 itself — against a TeX82 engine core that\nlives beside the renderer and shares nothing with it. **It passes** — all four\nartifacts byte-identical after the documented normalizations — and `npm run\ntrip` re-scores it against ground truth generated on the same machine by a\nknown-passing TeX, failing on any regression. The suite certifies its\nown tolerance set on every reference build: if a real conforming engine does not\nreconcile with Knuth's canonical files through the same normalizer that scores\nfaster-latex, it refuses to produce a reference. See\n[`docs/trip-test.md`](docs/trip-test.md).\n\nTo review a single commit's visual effect on the torture test, `npm run\ntorture:triptych` renders the test to PDF at two git refs and writes per-page\n`reference | before | after` contact sheets to `target/pdf/triptych/`, plus a\nmetrics delta and a per-page \"changed pixels\" count:\n\n```sh\nnpm run torture:triptych -- --before HEAD~1 --after HEAD   # the commit you just made\nnpm run torture:triptych -- --before main --after working  # uncommitted work vs main\n```\n\nEach ref is built in a throwaway git worktree, so the working tree is untouched.\n\n## The SDK family\n\nThe engine is published as a small family of packages developed together in a\npnpm workspace under [`sdk/`](sdk):\n\n| Package | What it is |\n|---|---|\n| [`faster-latex`](sdk/faster-latex) | The engine: wasm builds + the worker wrapper. |\n| [`@arena.ai/faster-latex-dom`](sdk/dom) | Framework-neutral DOM controller — WASM loading, worker lifecycle, incremental block patching, deduplicated styles. |\n| [`@arena.ai/faster-latex-react`](sdk/react) | Thin React integration (`<FasterLatex>`, `useFasterLatex`), no duplicate WASM. |\n\nA runnable Vite + React demo of `@arena.ai/faster-latex-react` is in\n[`examples/react-demo`](examples/react-demo) — `npm run demo` (missing generated\nengine and SDK artifacts are built automatically on first run).\n\nReleases are cut with [Changesets](.changeset/README.md); the engine version is\nmirrored into Cargo by `pnpm version-packages`. The ranked plan for future web\nand native SDKs is in [`docs/platform-support.md`](docs/platform-support.md), and\nthe backend-neutral render-tree protocol the native SDKs will decode is designed\nin [`docs/render-tree-protocol.md`](docs/render-tree-protocol.md).\n\n## WebAssembly / JavaScript\n\nPublished to npm as [**`@arena.ai/faster-latex`**](https://www.npmjs.com/package/@arena.ai/faster-latex).\nThe package ships **two builds** behind one name — a `bundler` build (the default\nimport, for Vite/webpack/Rollup) and a `web` build (`@arena.ai/faster-latex/web`, a plain\nES module that fetches its own `.wasm`, for a CDN or `<script type=module>`).\nTypeScript types are included for both.\n\nFor a live editor or a React app, prefer `@arena.ai/faster-latex-dom` / `@arena.ai/faster-latex-react` over\ncalling `render`/`Session` directly — they own the worker lifecycle, incremental\npatching, and style installation for you.\n\n### With a bundler (Vite, webpack, Rollup, Next.js…)\n\n```sh\nnpm install @arena.ai/faster-latex\n```\n\n```js\n// The bundler build auto-initializes the wasm on import — no init() call.\nimport { render, article_css, version } from \"@arena.ai/faster-latex\";\n\nconst { html, warnings } = render(source, { fullDocument: false });\ndocument.getElementById(\"out\").innerHTML = html;\n\n// Inject the shipped stylesheet once:\nconst style = document.createElement(\"style\");\nstyle.textContent = article_css();\ndocument.head.appendChild(style);\n```\n\n### From a CDN / no build step (browser, Deno)\n\nUse the `web` subpath; call the default-exported `init()` once before rendering.\nThe `.wasm` is resolved relative to the module URL, so no path config is needed:\n\n```html\n<script type=\"module\">\n  import init, { render } from \"https://cdn.jsdelivr.net/npm/@arena.ai/faster-latex/web/faster_latex.js\";\n  await init();\n  document.getElementById(\"out\").innerHTML = render(\"$e^{i\\\\pi}+1=0$\").html;\n</script>\n```\n\n(unpkg works too: `https://unpkg.com/@arena.ai/faster-latex/web/faster_latex.js`. Pin a\nversion with `@arena.ai/faster-latex@0.2.0` for production.)\n\n`render(source, options?)` returns a plain object `{ html, warnings }` (no\n`.free()`-able classes). The bundle uses the default allocator and\n`console_error_panic_hook` (default-on `panic-hook` feature).\n\n### Building the package locally\n\n```sh\npnpm install            # once, to set up the workspace\npnpm build:npm          # -> sdk/faster-latex/ (bundler/ + web/ + worker/ …)\nnpm pack sdk/faster-latex   # inspect the tarball\n```\n\nThe package version lives in the tracked manifest `sdk/faster-latex/package.json`\n(the single source of truth). `pnpm version-packages` applies pending\n[Changesets](.changeset/README.md) and mirrors the engine version into\n`Cargo.toml`; CI publishes the whole SDK family (`@arena.ai/faster-latex`,\n`@arena.ai/faster-latex-dom`, `@arena.ai/faster-latex-react`) idempotently via Changesets — see\n[`.github/workflows/release.yml`](.github/workflows/release.yml).\n\n### Stylesheet requirement\n\nMathML renders without any CSS. Injecting `ARTICLE_CSS` gets you correct math\nspacing and declares the Latin Modern Math webfont from an immutable URL, so\nstretchy delimiters have the same variants as TeX. To also load the Latin Modern\nRoman faces used by `\\text{…}` inside math, link the upstream stylesheet:\n\n```html\n<link rel=\"stylesheet\"\n      href=\"https://cdn.jsdelivr.net/gh/carloskiki/pulldown-latex@c8b7d9865bb2852c19e4f3d9f0f8bda3909f2648/styles.min.css\">\n```\n\nThe math webfont itself is about 390 KB and is downloaded only when a page uses\nit. The URL of the complete upstream stylesheet is exposed as\n`MATH_FONT_CSS_URL`, and `full_document` output embeds `ARTICLE_CSS` and links it\nfor you.\n\n> Pinned to a commit, not a tag: upstream publishes `pulldown-latex` 0.8.0 to\n> crates.io but its highest git tag is `v0.7.1`, so the `@0.8.0` ref previously\n> used here never existed and silently 404'd.\n\n## Demo\n\nA framework-free live editor lives in [`demo/`](demo/):\n\n```sh\nwasm-pack build crates/faster-latex-wasm --target web --out-name faster_latex\ncp -r crates/faster-latex-wasm/pkg demo/pkg\ncd demo && python3 -m http.server 8000   # then open http://localhost:8000\n```\n\n## Development\n\n```sh\ncargo test --workspace\ncargo clippy --workspace --all-targets -- -D warnings\ncargo fmt --check\nnode scripts/check-wasm-size.mjs --build   # build the npm package and assert the wasm size budgets\nwasm-pack test --headless --chrome crates/faster-latex-wasm\n```\n\nThe wasm **size budgets** (core + graphics) are enforced in CI and defined once,\nwith their rationale, in `scripts/check-wasm-size.mjs` — run it with `--build` to\nreproduce the CI check locally. Raise a budget only deliberately, with a comment\nsaying why.\n\nCI enforces `cargo fmt --all -- --check` and the size budgets above. To catch\nboth locally on every commit — auto-formatting staged Rust and, when a change\ncan affect the bundle, asserting the wasm size budget — enable the tracked\npre-commit hook once per clone:\n\n```sh\ngit config core.hooksPath .githooks\n```\n\nIt runs `cargo fmt --all` and re-stages the result, then (only when `crates/**`\nor `Cargo.toml`/`Cargo.lock` are staged) builds the wasm and checks the size\nbudget. Skip the whole hook for a single commit with `git commit --no-verify`,\nor skip just the slow size build with `SKIP_WASM_SIZE=1 git commit`.\n\nHTML snapshots (via [`insta`](https://insta.rs)) live in\n`crates/faster-latex/tests/corpus/` — update them with `cargo insta review`.\n\n## Benchmarks\n\nThe name claims speed, so the repo measures it — against every major LaTeX\nparser and renderer, with a correctness gate attached to every number.\n\n```sh\nnpm run bench:install   # pinned third-party engines (exact versions, committed lockfile)\nnpm run bench           # writes bench/results/<stamp>.json and bench/RESULTS.md\n```\n\nResults: [`bench/RESULTS.md`](bench/RESULTS.md). Methodology, prior art and the\nbenchmark's own known limits: [`docs/benchmark-plan.md`](docs/benchmark-plan.md).\nWhere the engine spends its time, what optimisations worked and which\nplausible-looking ones measured as noise:\n[`docs/performance.md`](docs/performance.md).\n\nTwo things make it worth reading rather than skipping:\n\n- **Engines only race within their group.** Document→HTML, document→AST,\n  math→MathML and document→PDF are four different jobs. A math-only renderer is\n  not \"faster than\" a document engine, and the tables never pretend otherwise.\n- **Every speed number carries a fidelity score** — the share of the committed\n  pdfLaTeX ground truth's words that survive into the engine's output. The\n  fastest possible LaTeX renderer is `() => \"\"`; a benchmark that cannot tell\n  the difference is measuring nothing.\n\nThere is also a headless-Chrome suite, because LaTeX.js builds a DOM and has no\nstring-output path: measuring it under Node measures jsdom. In the browser both\nengines get a fair run — and that is the environment `faster-latex` actually\nships into.\n\n## Why a new parser?\n\nAs of mid-2026 there is no maintained, permissively licensed, pure-Rust\nfull-document LaTeX parser. The strong parsers (texlab, RusTeX) are GPL; Tectonic\nis a C/XeTeX core with no WASM build; `mitex`'s text mode is unfinished. So the\ndocument parser/emitter here is written from scratch (~3–5 kLoC) and only the\nmath layer is reused, from the MIT-licensed `pulldown-latex`. See\n[`crates/faster-latex/tests/corpus/ATTRIBUTION.md`](crates/faster-latex/tests/corpus/ATTRIBUTION.md)\nfor the MIT reference projects (LaTeX.js, unified-latex) used to shape the scope.\n\n## License\n\nLicensed under the Apache License, Version 2.0 ([LICENSE](LICENSE)).\n\n**No GPL/AGPL/copyleft code is used anywhere in the dependency graph** — the\nentire runtime stack is `faster-latex` → `pulldown-latex` → `bumpalo`, all\npermissively licensed.\n\nUnless you explicitly state otherwise, any contribution intentionally submitted\nfor inclusion in the work by you, as defined in the Apache-2.0 license, shall be\nlicensed as above, without any additional terms or conditions.\n","readmeFilename":"README.md"}