Document converter in Rust: Markdown, LaTeX, Typst, DOCX, HTML. MIT-licensed, WASM-first, runs in the browser.
See the codeConvert LaTeX, Markdown, Typst, Word, HTML and MyST in the browser, in Node or from the command line. MIT licensed, written in Rust and compiled to WebAssembly.
Try it in the playground: a research paper is already loaded, and nothing leaves your browser.

docmux converts between markup formats with a Reader → AST → Writer pipeline. Each format is a separate Rust crate, so a WASM build only carries what it uses. It covers a part of what Pandoc does, under the MIT license, and it is meant to be embedded in web applications.
Unit tests pass long before real documents do, so docmux is also checked against the LaTeX sources of real arXiv papers, 397 so far and counting, written between 1998 and 2025. Some are well known, from machine learning (Transformer, GPT-3, ResNet) to mathematics (Perelman's Ricci flow, Viazovska's sphere packing), physics (the CMS Higgs observation) and astronomy (Planck 2018); most are a random sample from across arXiv. The Typst output of all 397 compiles with no errors and no leftover LaTeX commands, and their Word output carries 178,132 equations as native Word math, read back independently with pandoc. The harness lives in scripts/corpus/; the sources are downloaded, not committed.
| Format | Reader | Writer |
|---|---|---|
| Markdown (CommonMark + GFM) | ✅ | ✅ |
| HTML5 | ✅ | ✅ |
| LaTeX | ✅ | ✅ |
| Typst | ✅ | ✅ |
| MyST Markdown | ✅ | ✅ |
| DOCX | ✅ | ✅ |
| Plaintext | ❌ | ✅ |
# Install from source
cargo install --path crates/docmux-cli
# Convert Markdown to HTML
docmux input.md -o output.html
# Standalone HTML with math support
docmux paper.md -o paper.html --standalone --math=katex
# LaTeX to Typst
docmux paper.tex -t typst -o paper.typ
# DOCX to Markdown
docmux report.docx -o report.md
# Dump AST as JSON
docmux input.md -t json
npm install @docmux/wasm
import { convert, convertStandalone } from "@docmux/wasm";
const result = await convert("# Hello\n\nWorld!", "markdown", "html");
if (result.error) {
console.error(result.error);
} else {
console.log(result.output); // <h1 id="hello">1 Hello</h1>\n<p>World!</p>\n
}
For Vite or vanilla JS without bundler plugins, use the web target:
import init, { convert } from "@docmux/wasm/web";
await init();
const html = convert("# Hello", "markdown", "html");
\input / \include)For LaTeX papers that split their body across multiple files, pass a files
map alongside the main source:
import init, { convertWithFiles } from '@docmux/wasm/web';
await init();
const files = new Map([
['intro.tex', new TextEncoder().encode(introSource)],
['body.tex', new TextEncoder().encode(bodySource)],
]);
const md = convertWithFiles(
mainTex,
'latex',
'markdown',
files,
new Map(), // resources (images), empty if none
false, // standalone
);
\input{intro} resolves against intro.tex (the .tex extension is added
automatically if the bare key is missing). The CLI does the same thing
automatically when you point it at a single .tex file on disk:
docmux paper/main.tex --to markdown
See the @docmux/wasm README for full API documentation.
use docmux_reader_markdown::MarkdownReader;
use docmux_writer_html::HtmlWriter;
use docmux_core::{Reader, Writer, WriteOptions};
let reader = MarkdownReader::new();
let writer = HtmlWriter::new();
let doc = reader.read("# Hello\n\nWorld!")?;
let html = writer.write(&doc, &WriteOptions::default())?;
\newcommand, \def, \DeclareMathOperator) are expanded, layout commands keep their text, and \input files are followed.bbl that arXiv ships: numeric, alphabetic or author-year, each linked to its reference$variable$ syntax with conditionals and loops--standalone, --toc, --number-sections, --template, --bibliography, --csl, --math, --css, --wrap, and moreInput → Reader → [Document AST] → Transforms → Writer → Output
The AST is format-agnostic: N readers × M writers give N×M conversions without N×M converters. Each reader, writer, and transform is a separate crate under crates/.
Contributions are welcome! Here's how to get started:
# Rust workspace
cargo check --workspace
cargo test --workspace
cargo clippy --workspace --all-targets -- -D warnings
cargo fmt --all -- --check
# WASM build
cargo build --target wasm32-unknown-unknown -p docmux-wasm
# npm package
cd npm && pnpm install && pnpm run build && node test.mjs
# Playground
cd playground && pnpm install && pnpm run dev
unwrap() in library code: use ? and proper error typesany in TypeScript: use interfaces, generics, discriminated unionscargo fmt and cargo clippy before committingcargo test --workspace && cargo clippy --workspace --all-targets -- -D warningsMIT, see LICENSE.
Document converter in Rust: Markdown, LaTeX, Typst, DOCX, HTML. MIT-licensed, WASM-first, runs in the browser.
See the codeConvert LaTeX, Markdown, Typst, Word, HTML and MyST in the browser, in Node or from the command line. MIT licensed, written in Rust and compiled to WebAssembly.
Try it in the playground: a research paper is already loaded, and nothing leaves your browser.

docmux converts between markup formats with a Reader → AST → Writer pipeline. Each format is a separate Rust crate, so a WASM build only carries what it uses. It covers a part of what Pandoc does, under the MIT license, and it is meant to be embedded in web applications.
Unit tests pass long before real documents do, so docmux is also checked against the LaTeX sources of real arXiv papers, 397 so far and counting, written between 1998 and 2025. Some are well known, from machine learning (Transformer, GPT-3, ResNet) to mathematics (Perelman's Ricci flow, Viazovska's sphere packing), physics (the CMS Higgs observation) and astronomy (Planck 2018); most are a random sample from across arXiv. The Typst output of all 397 compiles with no errors and no leftover LaTeX commands, and their Word output carries 178,132 equations as native Word math, read back independently with pandoc. The harness lives in scripts/corpus/; the sources are downloaded, not committed.
| Format | Reader | Writer |
|---|---|---|
| Markdown (CommonMark + GFM) | ✅ | ✅ |
| HTML5 | ✅ | ✅ |
| LaTeX | ✅ | ✅ |
| Typst | ✅ | ✅ |
| MyST Markdown | ✅ | ✅ |
| DOCX | ✅ | ✅ |
| Plaintext | ❌ | ✅ |
# Install from source
cargo install --path crates/docmux-cli
# Convert Markdown to HTML
docmux input.md -o output.html
# Standalone HTML with math support
docmux paper.md -o paper.html --standalone --math=katex
# LaTeX to Typst
docmux paper.tex -t typst -o paper.typ
# DOCX to Markdown
docmux report.docx -o report.md
# Dump AST as JSON
docmux input.md -t json
npm install @docmux/wasm
import { convert, convertStandalone } from "@docmux/wasm";
const result = await convert("# Hello\n\nWorld!", "markdown", "html");
if (result.error) {
console.error(result.error);
} else {
console.log(result.output); // <h1 id="hello">1 Hello</h1>\n<p>World!</p>\n
}
For Vite or vanilla JS without bundler plugins, use the web target:
import init, { convert } from "@docmux/wasm/web";
await init();
const html = convert("# Hello", "markdown", "html");
\input / \include)For LaTeX papers that split their body across multiple files, pass a files
map alongside the main source:
import init, { convertWithFiles } from '@docmux/wasm/web';
await init();
const files = new Map([
['intro.tex', new TextEncoder().encode(introSource)],
['body.tex', new TextEncoder().encode(bodySource)],
]);
const md = convertWithFiles(
mainTex,
'latex',
'markdown',
files,
new Map(), // resources (images), empty if none
false, // standalone
);
\input{intro} resolves against intro.tex (the .tex extension is added
automatically if the bare key is missing). The CLI does the same thing
automatically when you point it at a single .tex file on disk:
docmux paper/main.tex --to markdown
See the @docmux/wasm README for full API documentation.
use docmux_reader_markdown::MarkdownReader;
use docmux_writer_html::HtmlWriter;
use docmux_core::{Reader, Writer, WriteOptions};
let reader = MarkdownReader::new();
let writer = HtmlWriter::new();
let doc = reader.read("# Hello\n\nWorld!")?;
let html = writer.write(&doc, &WriteOptions::default())?;
\newcommand, \def, \DeclareMathOperator) are expanded, layout commands keep their text, and \input files are followed.bbl that arXiv ships: numeric, alphabetic or author-year, each linked to its reference$variable$ syntax with conditionals and loops--standalone, --toc, --number-sections, --template, --bibliography, --csl, --math, --css, --wrap, and moreInput → Reader → [Document AST] → Transforms → Writer → Output
The AST is format-agnostic: N readers × M writers give N×M conversions without N×M converters. Each reader, writer, and transform is a separate crate under crates/.
Contributions are welcome! Here's how to get started:
# Rust workspace
cargo check --workspace
cargo test --workspace
cargo clippy --workspace --all-targets -- -D warnings
cargo fmt --all -- --check
# WASM build
cargo build --target wasm32-unknown-unknown -p docmux-wasm
# npm package
cd npm && pnpm install && pnpm run build && node test.mjs
# Playground
cd playground && pnpm install && pnpm run dev
unwrap() in library code: use ? and proper error typesany in TypeScript: use interfaces, generics, discriminated unionscargo fmt and cargo clippy before committingcargo test --workspace && cargo clippy --workspace --all-targets -- -D warningsMIT, see LICENSE.