faster-latex
v0.1.0
Published
Render full LaTeX documents to HTML (with MathML math) in the browser — pure Rust compiled to WebAssembly.
Maintainers
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_htmlnever panics on any input; it returns HTML plus a list of non-fatal warnings. - Runtime macros.
\newcommand/\newenvironmentwork in text and inside math — one thing pulldown-latex or LaTeX.js alone don't give you. - Small and dependency-light. The core crate's only runtime dependency is
pulldown-latex(→bumpalo). The WASM bundle is ~360 KB. - Dual-licensed MIT OR Apache-2.0, 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 ─▶ MathMLMath 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 $…$.
Supported subset (≈ LaTeX.js scope + runtime macros)
| Area | Supported |
|------|-----------|
| Structure | \documentclass, preamble, \title/\author/\date/\maketitle, \tableofcontents, \appendix |
| Sectioning | \section…\subparagraph (+ *), automatic numbering, \label/\ref/\eqref/\pageref/\nameref |
| 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) |
| Tables | tabular with l/c/r columns and \hline |
| Bibliography | thebibliography/\bibitem/\cite |
| Math | $…$, \(…\), \[…\], $$…$$, equation(), align(), gather, multline, displaymath |
| Macros | \newcommand/\renewcommand/\providecommand (9 args + optional default), \newenvironment |
Deliberately punted (warn, never crash)
Real package loading (\usepackage is ignored with a warning); \def, catcodes,
conditionals, and other TeX primitives; TikZ / PGF; float placement and
pagination; BibTeX processing; per-line equation numbering in align (rendered
unnumbered with a warning). Unknown commands render as a visible
<span class="fl-unknown"> fallback; unknown environments still render their
contents. Anything unsupported produces a Warning, not a failure.
Security
Rendered HTML is safe to insert into a page:
- All text is HTML-escaped; math text (
\text{…}) is escaped by pulldown-latex. \includegraphicsURLs are allowlisted to relative paths andhttp/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.texfixture 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 full_document: bool, // full <html> shell vs. bare <article>
pub max_expansions: u32, // macro-bomb guard (default 100_000)
pub max_nodes: u32, // node-count guard (default 500_000)
}
pub struct RenderOutput { pub html: String, pub warnings: Vec<Warning> }
pub fn render_to_html(source: &str, options: &Options) -> RenderOutput; // infallible
pub const ARTICLE_CSS: &str; // shipped stylesheet, version-locked to the crate
pub const VERSION: &str;Optional serde feature derives Serialize on Warning/WarningKind.
WebAssembly / JavaScript
Published to npm as faster-latex.
The package ships two builds behind one name — a bundler build (the default
import, for Vite/webpack/Rollup) and a web build (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.
With a bundler (Vite, webpack, Rollup, Next.js…)
npm install faster-latex// The bundler build auto-initializes the wasm on import — no init() call.
import { render, article_css, version } from "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/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/faster-latex/web/faster_latex.js. Pin a
version with [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
npm run build:npm # -> dist/ (bundler/ + web/ + package.json)
npm run pack:npm # build + npm pack, to inspect the tarball
npm run publish:npm # build + npm publish ./dist --access publicThe package version is taken from [workspace.package].version in Cargo.toml
(one source of truth). CI publishes automatically when a vX.Y.Z tag matching
that version is pushed — see .github/workflows/npm-publish.yml.
Stylesheet requirement
MathML renders without any CSS, but for correct spacing and fonts you must load
the pulldown-latex math stylesheet and inject ARTICLE_CSS:
<link rel="stylesheet"
href="https://cdn.jsdelivr.net/gh/carloskiki/[email protected]/styles.min.css">The full_document output embeds ARTICLE_CSS and links this stylesheet for you.
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:8000Development
cargo test --workspace
cargo clippy --workspace --all-targets -- -D warnings
cargo fmt --check
wasm-pack build crates/faster-latex-wasm --target web # pkg/*.wasm should be < 500 KB
wasm-pack test --headless --chrome crates/faster-latex-wasmHTML snapshots (via insta) live in
crates/faster-latex/tests/corpus/ — update them with cargo insta review.
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 either of
- Apache License, Version 2.0 (LICENSE-APACHE)
- MIT license (LICENSE-MIT)
at your option. No GPL/AGPL/copyleft code is used anywhere in the dependency
graph — the entire runtime stack is faster-latex → pulldown-latex →
bumpalo, all MIT/Apache.
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 dual-licensed as above, without any additional terms or conditions.
