npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@arena.ai/faster-latex

v0.3.1

Published

Render full LaTeX documents to HTML (with MathML math) in the browser or on the server (SSR) — pure Rust compiled to WebAssembly.

Readme

faster-latex

Render full LaTeX documents to HTML — sections, text, environments, lists, tables, cross-references, and math — in pure Rust, compiled to WebAssembly for the browser. Math is delegated to pulldown-latex, which emits MathML Core (rendered natively by every modern browser).

  • Infallible by contract. render_to_html never panics on any input; it returns HTML plus a list of non-fatal warnings.
  • Runtime macros. \newcommand/\newenvironment work in text and inside math — one thing pulldown-latex or LaTeX.js alone don't give you.
  • Real documents, not just snippets. Front matter (\title/\author after \begin{document}), natbib citations, a .bib bibliography, \chapter/\part, \input/\include, \DeclareMathOperator and physics/siunitx math, and counter registers all render — the constructs ordinary papers actually use.
  • Small and dependency-light. The core crate's only runtime dependency is pulldown-latex (→ bumpalo). The core WASM bundle is CI-gated at 640 KiB.
  • PDF without a browser. fl doc.tex --pdf runs a TeX-style layout engine (Knuth–Plass line breaking, OpenType-MATH math) and writes the PDF itself — no Chrome, no Node, no network. It also compiles to wasm, so the same engine exports a PDF in the browser from a lazily loaded bundle. See docs/pdf-output.md.
  • Apache-2.0 licensed, with no copyleft anywhere in the stack.
use faster_latex::{render_to_html, Options};

let out = render_to_html(
    r"\section{Hi} Euler: $e^{i\pi}+1=0$.",
    &Options::default(),
);
assert!(out.html.contains("<h2"));
assert!(out.html.contains("<math"));
for w in &out.warnings {
    eprintln!("{} (line {}): {}", w.kind.as_str(), w.line, w.message);
}

Pipeline

source ─▶ lex ─▶ expand macros ─▶ parse ─▶ number (pass 1) ─▶ emit HTML (pass 2)
                                                │
                                    math snippets ─▶ pulldown-latex ─▶ MathML

Math and verbatim are captured as opaque raw slices in the lexer, before the macro expander runs, so their contents can never be mangled. Macro expansion is driven by an explicit work stack (no recursion) with a fuel budget and depth limit, so macro bombs warn instead of hanging. Every user macro is also recorded in \def form and re-fed to pulldown-latex per math snippet, which is how a \newcommand defined once works in both text and $…$.

Live editing

render is the API for rendering a document once. For an editor, use Session, which re-renders and hands back only the top-level blocks that changed:

const session = new Session();
const tpl = document.createElement("template");

function onInput(src) {
  const u = session.update(src, opts);
  if (u.full) {
    preview.innerHTML = u.patch.html;
    return;
  }
  const article = preview.firstElementChild;
  const anchor = article.children[u.patch.from + u.patch.removed] ?? null;
  for (let i = 0; i < u.patch.removed; i++) article.children[u.patch.from].remove();
  if (u.patch.html) {
    tpl.innerHTML = u.patch.html;
    article.insertBefore(tpl.content, anchor);
  }
}

The engine is not what makes a keystroke slow on a large document — the browser is, relaying out a DOM that innerHTML just discarded and rebuilt for an edit that changes a median of one byte of the output. On Euclid's Elements (614 KiB of LaTeX, 1.4 MB of HTML) a keystroke costs 344 ms assigning innerHTML and 61 ms applying the splice; on Keynes' A Treatise on Probability (1.28 MB), 347 ms → 85 ms. The wasm bridge itself is 1.7 ms of that, which is why "send less over the bridge" is not the fix.

The measurements, the correctness oracle, and what is still slow are in docs/incremental-rendering.md; the harness is bench/incremental/.

Supported subset (≈ LaTeX.js scope + runtime macros)

| Area | Supported | |------|-----------| | Structure | \documentclass, preamble, \title/\author/\date/\maketitle (in preamble or body), \tableofcontents, \appendix, \today | | Front matter | \subtitle, \and, \thanks, \affiliation/\institution/\institute/\address, \email, \keywords/IEEEkeywords, \subjclass, \ccsdesc (+ CCSXML dropped), \IEEEauthorblockN/A, \inst | | Sectioning | \part/\chapter (book/report) … \subparagraph (+ *), automatic numbering (chapter-prefixed, appendix letters, Roman parts), \label/\ref/\eqref/\pageref/\nameref | | Multi-file | \input/\include spliced from a caller-supplied file map; \frontmatter/\mainmatter/\backmatter | | Text | \textbf \textit \emph \texttt \textsc \underline; declaration forms (\bfseries, …); font sizes \tiny…\Huge; \\, paragraphs, comments | | Typography | ~, --/---, ``/'' quotes, accents → Unicode, many symbol commands (\LaTeX, \S, \ss, \ae, …), \verb | | Lists | itemize, enumerate, description (nested), \item[…] | | Blocks | quote, quotation, verse, center, flushleft, flushright, abstract, verbatim | | Floats | figure/table with \caption + numbering, \includegraphics (URL-sanitized; relative widths honored in paper mode), TikZ/PGF & pgfplots → SVG (graphics feature) | | Tables | tabular with l/c/r columns and \hline | | Citations | natbib \cite/\citep/\citet/\citealt/\citealp/\citeauthor/\citeyear/\citenum/\nocite (+ post-notes, author-year labels) | | Bibliography | thebibliography/\bibitem, or \bibliography{refs} resolved from a .bib file (cited entries, in order) | | Counters | \newcounter/\setcounter/\stepcounter/\addtocounter/\value, \arabic/\alph/\Alph/\roman/\Roman/\fnsymbol; \newif/\iffoo…\else…\fi | | Math | $…$, \(…\), \[…\], $$…$$, equation(), align() with per-line numbering + \notag, gather, multline, displaymath; \DeclareMathOperator(*), \DeclarePairedDelimiter, \ensuremath, physics & siunitx shims | | Macros | \newcommand/\renewcommand/\providecommand (9 args + optional default), \newenvironment |

Deliberately punted (warn, never crash)

Real package loading (\usepackage is ignored with a warning); a general TeX expander (\def bodies, \ifnum/\ifx/\csname/\expandafter, catcodes — \newif conditionals are honoured); \newcommand with an optional argument used inside math (warns and falls back to raw rather than mis-render); custom numbering formats (\renewcommand{\thesection}{…}, \numberwithin) are inert. Unknown commands render as a visible <span class="fl-unknown"> fallback (off by default); unknown environments still render their contents. Anything unsupported produces a Warning, not a failure — and never leaks its argument into the prose (enforced by the corpus_argument_leaks oracle over complete documents).

Security

Rendered HTML is safe to insert into a page:

  • All text is HTML-escaped; math text (\text{…}) is escaped by pulldown-latex.
  • \includegraphics URLs are allowlisted to relative paths and http/https; javascript:, data:, vbscript:, etc. are rejected with a warning and the image is omitted (the URL is never echoed into the output).
  • Locked in by the 952-xss-attempts.tex fixture and a panic-smoke test over mutated inputs.

Rust API

pub struct Options {
    pub base_url: Option<String>,
    pub predefined_macros: Vec<String>,       // raw \newcommand… lines
    pub files: BTreeMap<String, String>,      // \input/\include/\bibliography sources
    pub full_document: bool,                  // full <html> shell vs. bare <article>
    pub paper: bool,                          // LaTeX-faithful paged "paper" mode (needs `paper` feature)
    pub max_expansions: u32,                  // macro-bomb guard (default 100_000)
    pub max_nodes: u32,                       // node-count guard (default 500_000)
    pub debug_unknown: bool,                  // render unknown constructs as red badges
    pub today: Option<String>,                // what \today expands to (engine reads no clock)
}
pub struct RenderOutput {
    pub html: String,
    pub warnings: Vec<Warning>,
    pub effective_source: String,             // input after macro-prepend + include splicing
}
pub fn render_to_html(source: &str, options: &Options) -> RenderOutput; // infallible
pub const ARTICLE_CSS: &str;  // shipped web stylesheet, version-locked to the crate
#[cfg(feature = "paper")]
pub const PAPER_CSS: &str;    // LaTeX-faithful paged stylesheet (paper mode)
pub const VERSION: &str;

#[cfg(feature = "pdf")]        // browser-free PDF output — see docs/pdf-output.md
pub mod pdf {
    pub struct PdfOptions { pub fonts: FontSet, pub images: ImageSet,
                            pub producer: Option<String>, pub title: Option<String>,
                            pub compress: bool }
    pub struct PdfOutput { pub bytes: Vec<u8>, pub pages: usize, pub warnings: Vec<Warning> }
    pub fn render_to_pdf(source: &str, options: &Options, pdf: &PdfOptions) -> PdfOutput; // infallible
}

Optional serde feature derives Serialize on Warning/WarningKind.

Paper mode (paper feature)

The default output is a responsive web article. The optional paper feature (implied by cli) adds a LaTeX-faithful paged mode — document-aware sheet size/orientation, class- and style-aware text blocks and base type metrics, Latin Modern typography, fancyhdr running heads/feet as @page margin boxes, real page breaks, a full title page, dot-leader TOC/LoF/LoT, a two-column index, and hanging-indent bibliography. Printed by headless Chrome it produces a document that reads as the same paper as a pdflatex build. It is gated behind a feature so its CSS never enters the core wasm bundle, whose 640 KiB budget is enforced in CI.

cargo fl document.tex --paper -o document.html   # then print to PDF from Chrome

The docs/pdf-parity-plan.md scoreboard and the scripts/torture-{pdf,diff}.mjs harness measure paper-mode output against a committed reference PDF (see npm run torture:diff; needs Chrome + poppler).

PDF output without a browser (pdf feature)

The optional pdf feature (also implied by cli) writes a PDF directly, with no browser, no Node and no network:

cargo fl document.tex --pdf            # writes document.pdf

It runs its own TeX-style layout engine over the numbered AST: Knuth–Plass paragraph breaking, page composition with floats and footnotes, and math laid out from the OpenType MATH table of Latin Modern Math (fraction and script shifts, stretchy delimiters built from the font's variant and assembly recipes, big operators with limits). Cross-references resolve through a second pass, the way LaTeX's .aux file does. Output is searchable, hyperlinked, and byte-for-byte reproducible.

Fonts are supplied by the caller — the library never reads the filesystem — and the CLI finds the SHA-pinned Latin Modern family that npm run assets caches. With no fonts at all it falls back to the PDF base-14 metrics that are compiled in, so render_to_pdf is infallible and needs no assets.

use faster_latex::{Options, pdf::{PdfOptions, render_to_pdf}};
let out = render_to_pdf(source, &Options::default(), &PdfOptions::default());
assert!(out.bytes.starts_with(b"%PDF-"));

Three dependencies, all optional: pdf-writer (PDF objects), ttf-parser (OpenType metrics), and hypher (compact Knuth--Liang English hyphenation). None reaches the core wasm bundle, which is byte-for-byte unchanged by all of this.

The same engine runs in the browser, as a third lazily loaded bundle (@arena.ai/faster-latex/web-pdf, +109 KiB brotli over core) exporting render_pdf. It is fetched on the click that asks for a PDF, so it never touches the render path — that is what the demo's Export PDF button does. Exporting the torture test from the browser and running fl --pdf on it produce byte-identical files.

This is newer and less proven than the Chrome path: on the torture test it scores word F1 0.945, 13/15 pages, and PAA 0.612 (Chrome: 0.986, 15/15, and 0.99). English Knuth--Liang hyphenation is implemented; the largest remaining page-count gap is TeX's float-page composition. See docs/pdf-output.md for the scoreboard (npm run torture:diff:rust), the design, and the ranked list of gaps.

Beyond that one torture document, faster-latex is scored page-for-page against all three de-facto LaTeX engines — pdfLaTeX, XeLaTeX and LuaLaTeX — on the ten demo documents, with pinned, reproducible goldens and a CI regression ratchet. Each page also reports its distance from the three engines' own pairwise spatial-consensus floor. The workflow, metrics, and gates are in docs/engine-parity-testing.md (npm run examples:reference to build the goldens, npm run examples:parity to score). The browser-free exporter has its own breadth scorer over those same 30 PDFs: npm run examples:parity:direct renders every fixture with fl --pdf and reports the worst pdfLaTeX/XeLaTeX/LuaLaTeX result. A head-to-head four-renderer shootout pits faster-latex against the three TeX engines on speed and pixel-perfection. The three TeX engines remain the parity oracle; faster-latex is timed and compared through its own browser-free PDF backend (fl --pdf), so both axes are measured PDF-to-PDF with no browser in the loop. See docs/engine-shootout.md (npm run examples:shootout; needs Docker and poppler). A separate browser shootout compares the shipped wasm bundle against LaTeX.js, the closest pure-JS competitor, on speed, compatibility, scaling and footprint — the two renderers you would actually choose between for LaTeX in a page. See docs/latexjs-shootout.md. The ordered implementation plan for closing the remaining visual gap is docs/pixel-parity-roadmap.md.

Separately from all of that, fl --trip runs Knuth's TRIP test — the certification torture test for TeX82 itself — against a TeX82 engine core that lives beside the renderer and shares nothing with it. It passes — all four artifacts byte-identical after the documented normalizations — and npm run trip re-scores it against ground truth generated on the same machine by a known-passing TeX, failing on any regression. The suite certifies its own tolerance set on every reference build: if a real conforming engine does not reconcile with Knuth's canonical files through the same normalizer that scores faster-latex, it refuses to produce a reference. See docs/trip-test.md.

To review a single commit's visual effect on the torture test, npm run torture:triptych renders the test to PDF at two git refs and writes per-page reference | before | after contact sheets to target/pdf/triptych/, plus a metrics delta and a per-page "changed pixels" count:

npm run torture:triptych -- --before HEAD~1 --after HEAD   # the commit you just made
npm run torture:triptych -- --before main --after working  # uncommitted work vs main

Each ref is built in a throwaway git worktree, so the working tree is untouched.

The SDK family

The engine is published as a small family of packages developed together in a pnpm workspace under sdk/:

| Package | What it is | |---|---| | faster-latex | The engine: wasm builds + the worker wrapper. | | @arena.ai/faster-latex-dom | Framework-neutral DOM controller — WASM loading, worker lifecycle, incremental block patching, deduplicated styles. | | @arena.ai/faster-latex-react | Thin React integration (<FasterLatex>, useFasterLatex), no duplicate WASM. |

A runnable Vite + React demo of @arena.ai/faster-latex-react is in examples/react-demo — npm run demo (missing generated engine and SDK artifacts are built automatically on first run).

Releases are cut with Changesets; the engine version is mirrored into Cargo by pnpm version-packages. The ranked plan for future web and native SDKs is in docs/platform-support.md, and the backend-neutral render-tree protocol the native SDKs will decode is designed in docs/render-tree-protocol.md.

WebAssembly / JavaScript

Published to npm as @arena.ai/faster-latex. The package ships two builds behind one name — a bundler build (the default import, for Vite/webpack/Rollup) and a web build (@arena.ai/faster-latex/web, a plain ES module that fetches its own .wasm, for a CDN or <script type=module>). TypeScript types are included for both.

For a live editor or a React app, prefer @arena.ai/faster-latex-dom / @arena.ai/faster-latex-react over calling render/Session directly — they own the worker lifecycle, incremental patching, and style installation for you.

With a bundler (Vite, webpack, Rollup, Next.js…)

npm install @arena.ai/faster-latex
// The bundler build auto-initializes the wasm on import — no init() call.
import { render, article_css, version } from "@arena.ai/faster-latex";

const { html, warnings } = render(source, { fullDocument: false });
document.getElementById("out").innerHTML = html;

// Inject the shipped stylesheet once:
const style = document.createElement("style");
style.textContent = article_css();
document.head.appendChild(style);

From a CDN / no build step (browser, Deno)

Use the web subpath; call the default-exported init() once before rendering. The .wasm is resolved relative to the module URL, so no path config is needed:

<script type="module">
  import init, { render } from "https://cdn.jsdelivr.net/npm/@arena.ai/faster-latex/web/faster_latex.js";
  await init();
  document.getElementById("out").innerHTML = render("$e^{i\\pi}+1=0$").html;
</script>

(unpkg works too: https://unpkg.com/@arena.ai/faster-latex/web/faster_latex.js. Pin a version with @arena.ai/[email protected] for production.)

render(source, options?) returns a plain object { html, warnings } (no .free()-able classes). The bundle uses the default allocator and console_error_panic_hook (default-on panic-hook feature).

Building the package locally

pnpm install            # once, to set up the workspace
pnpm build:npm          # -> sdk/faster-latex/ (bundler/ + web/ + worker/ …)
npm pack sdk/faster-latex   # inspect the tarball

The package version lives in the tracked manifest sdk/faster-latex/package.json (the single source of truth). pnpm version-packages applies pending Changesets and mirrors the engine version into Cargo.toml; CI publishes the whole SDK family (@arena.ai/faster-latex, @arena.ai/faster-latex-dom, @arena.ai/faster-latex-react) idempotently via Changesets — see .github/workflows/release.yml.

Stylesheet requirement

MathML renders without any CSS. Injecting ARTICLE_CSS gets you correct math spacing and declares the Latin Modern Math webfont from an immutable URL, so stretchy delimiters have the same variants as TeX. To also load the Latin Modern Roman faces used by \text{…} inside math, link the upstream stylesheet:

<link rel="stylesheet"
      href="https://cdn.jsdelivr.net/gh/carloskiki/pulldown-latex@c8b7d9865bb2852c19e4f3d9f0f8bda3909f2648/styles.min.css">

The math webfont itself is about 390 KB and is downloaded only when a page uses it. The URL of the complete upstream stylesheet is exposed as MATH_FONT_CSS_URL, and full_document output embeds ARTICLE_CSS and links it for you.

Pinned to a commit, not a tag: upstream publishes pulldown-latex 0.8.0 to crates.io but its highest git tag is v0.7.1, so the @0.8.0 ref previously used here never existed and silently 404'd.

Demo

A framework-free live editor lives in demo/:

wasm-pack build crates/faster-latex-wasm --target web --out-name faster_latex
cp -r crates/faster-latex-wasm/pkg demo/pkg
cd demo && python3 -m http.server 8000   # then open http://localhost:8000

Development

cargo test --workspace
cargo clippy --workspace --all-targets -- -D warnings
cargo fmt --check
node scripts/check-wasm-size.mjs --build   # build the npm package and assert the wasm size budgets
wasm-pack test --headless --chrome crates/faster-latex-wasm

The wasm size budgets (core + graphics) are enforced in CI and defined once, with their rationale, in scripts/check-wasm-size.mjs — run it with --build to reproduce the CI check locally. Raise a budget only deliberately, with a comment saying why.

CI enforces cargo fmt --all -- --check and the size budgets above. To catch both locally on every commit — auto-formatting staged Rust and, when a change can affect the bundle, asserting the wasm size budget — enable the tracked pre-commit hook once per clone:

git config core.hooksPath .githooks

It runs cargo fmt --all and re-stages the result, then (only when crates/** or Cargo.toml/Cargo.lock are staged) builds the wasm and checks the size budget. Skip the whole hook for a single commit with git commit --no-verify, or skip just the slow size build with SKIP_WASM_SIZE=1 git commit.

HTML snapshots (via insta) live in crates/faster-latex/tests/corpus/ — update them with cargo insta review.

Benchmarks

The name claims speed, so the repo measures it — against every major LaTeX parser and renderer, with a correctness gate attached to every number.

npm run bench:install   # pinned third-party engines (exact versions, committed lockfile)
npm run bench           # writes bench/results/<stamp>.json and bench/RESULTS.md

Results: bench/RESULTS.md. Methodology, prior art and the benchmark's own known limits: docs/benchmark-plan.md. Where the engine spends its time, what optimisations worked and which plausible-looking ones measured as noise: docs/performance.md.

Two things make it worth reading rather than skipping:

  • Engines only race within their group. Document→HTML, document→AST, math→MathML and document→PDF are four different jobs. A math-only renderer is not "faster than" a document engine, and the tables never pretend otherwise.
  • Every speed number carries a fidelity score — the share of the committed pdfLaTeX ground truth's words that survive into the engine's output. The fastest possible LaTeX renderer is () => ""; a benchmark that cannot tell the difference is measuring nothing.

There is also a headless-Chrome suite, because LaTeX.js builds a DOM and has no string-output path: measuring it under Node measures jsdom. In the browser both engines get a fair run — and that is the environment faster-latex actually ships into.

Why a new parser?

As of mid-2026 there is no maintained, permissively licensed, pure-Rust full-document LaTeX parser. The strong parsers (texlab, RusTeX) are GPL; Tectonic is a C/XeTeX core with no WASM build; mitex's text mode is unfinished. So the document parser/emitter here is written from scratch (~3–5 kLoC) and only the math layer is reused, from the MIT-licensed pulldown-latex. See crates/faster-latex/tests/corpus/ATTRIBUTION.md for the MIT reference projects (LaTeX.js, unified-latex) used to shape the scope.

License

Licensed under the Apache License, Version 2.0 (LICENSE).

No GPL/AGPL/copyleft code is used anywhere in the dependency graph — the entire runtime stack is faster-latex → pulldown-latex → bumpalo, all permissively licensed.

Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be licensed as above, without any additional terms or conditions.