A robust Markdown to Typst converter in Python with support for multiple Markdown parsers.
- Multiple parser backends: Choose from markdown-it-py, mistune, or marko at runtime
- GFM support: Tables, strikethrough, footnotes, and other GitHub Flavored Markdown extensions
- Math support:
$...$and$$...$$rendered via mitex (currency-safe:$5 to $10stays literal) - Mermaid diagrams:
```mermaidcode blocks rendered via mmdr, or fully faithful (bold labels and all) via mermaid-cli with--mermaid cli - Auto-imports: Required Typst packages are automatically imported based on content
- Direct PDF output:
md2pdfcommand converts Markdown to PDF in one step (requirestypstCLI) - Document classes:
article,report, andbookpresets (like LaTeX), selectable via front matter, CLI, or config - Title blocks: Automatic title page/block from front matter (title, author, date, version, publisher)
[TOC]support: Table of contents via[TOC]marker- Diagram blocks:
```diagramkeeps ASCII-art diagrams from breaking across pages - Configurable: User config, project config, YAML front matter, and CLI options with clear cascade
- Extensible: Plugin support for parser-specific extensions
- Well-tested: Comprehensive test suite with TCK validation against CommonMark
# Using pip
pip install md2typst
# Using uv
uv add md2typst# Convert a file (writes input.typ by default)
md2typst input.md
# Explicit output path
md2typst input.md -o output.typ
# Output to stdout
md2typst input.md -o -
# Convert from stdin (outputs to stdout)
echo "# Hello **World**" | md2typst
# Use a specific parser
md2typst --parser mistune input.md
# Convert multiple files at once
md2typst *.md
md2pdf *.md
# Convert directly to PDF (requires typst CLI)
md2pdf input.md
md2pdf input.md -o custom-output.pdf
# Use a document class
md2pdf --class report input.md
# List available parsers
md2typst --list-parsersfrom md2typst import convert
# Simple conversion
typst = convert("# Hello **World**")
print(typst)
# Output: = Hello *World*
# With specific parser
typst = convert("~~deleted~~", parser="mistune")
print(typst)
# Output: #strike[deleted]
# With configuration
from md2typst import convert_with_config
from md2typst.config import Config
config = Config(parser="marko", plugins=["gfm"])
typst = convert_with_config("| A | B |\n|---|---|\n| 1 | 2 |", config)| Parser | CLI Name | Description |
|---|---|---|
| markdown-it-py | markdown-it |
Default. CommonMark compliant, extensible |
| mistune | mistune |
Fast, pure Python |
| marko | marko |
CommonMark compliant, extensible |
All parsers have GFM extensions (tables, strikethrough, footnotes) enabled by default.
| Markdown | Typst |
|---|---|
# Heading |
= Heading |
## Heading 2 |
== Heading 2 |
*italic* |
_italic_ |
**bold** |
*bold* |
~~strike~~ |
#strike[strike] |
`code` |
`code` |
[text](url) |
#link("url")[text] |
 |
#image("url", alt: "alt") |
> quote |
#block(...)[quote] |
$E=mc^2$ |
#mi("E=mc^2") |
$$...\int...$$ |
#mitex(\...`)` |
--- |
#line(length: 100%) |
| GFM tables | #table(...) |
text[^1] / [^1]: note |
text#footnote[note] |
```mermaid |
#mermaid("...") |
```diagram |
#block(breakable: false)[...] |
[TOC] |
#outline(indent: auto, depth: 4) |
Configuration is loaded from multiple sources (highest priority first):
- CLI arguments (
--parser,--plugin, ...) - Front matter in the document
- Explicit config file (
--config path/to/config.toml) md2typst.tomlin the current or parent directories[tool.md2typst]section inpyproject.toml- User config at
~/.config/md2typst/config.toml(platform-specific viaplatformdirs) - Built-in defaults
The [style] section customizes Typst's default output. Set your preferred font, language, and page layout once in your user config — applied to every document you convert.
~/.config/md2typst/config.toml:
[style]
# Font as single string or a fallback list
font = ["Libertinus Serif", "New Computer Modern", "Times New Roman"]
font_size = "11pt"
language = "en"
paper = "a4"
margin = "2.5cm"
# Raw Typst code emitted before the body (after the structured fields above)
preamble = """
#set par(justify: true, first-line-indent: 1em)
#show heading.where(level: 1): it => { it; v(0.5em) }
"""
# Raw Typst code emitted after the body, e.g. a bibliography (see Typst Templates)
# postamble = '#bibliography("refs.bib")'This generates at the top of every converted document:
#set text(font: ("Libertinus Serif", "New Computer Modern", "Times New Roman"), size: 11pt, lang: "en")
#set page(paper: "a4", margin: 2.5cm)
#set par(justify: true, first-line-indent: 1em)
#show heading.where(level: 1): it => { it; v(0.5em) }md2typst.toml (project-level, overrides user config):
parser = "mistune"
plugins = ["strikethrough", "table"]
[parser_options]
html = true
[style]
language = "fr"pyproject.toml:
[tool.md2typst]
parser = "markdown-it"
plugins = ["gfm"]Markdown files can include YAML front matter. Any field is exposed as a #let doc-<key> variable in Typst, except reserved keys with special handling:
preamble— raw Typst code emitted before the body, concatenated with configstyle.preamblepostamble— raw Typst code emitted after the body (e.g.#bibliography(...)), concatenated with configstyle.postamblestylesheet/stylesheets— additional Typst modules to importfont,font_size,language,paper,margin— override the corresponding[style]fields at document level
---
title: My Document
author: Jane Doe
language: fr # overrides config style.language
font: EB Garamond # overrides config style.font
stylesheet: my-style
preamble: |
#set par(justify: true)
#show heading.where(level: 1): it => { it; v(0.5em) }
---
# Hello WorldThis generates:
#let doc-title = "My Document"
#let doc-author = "Jane Doe"
#import "my-style.typ": *
#set text(font: "EB Garamond", lang: "fr")
#set par(justify: true)
#show heading.where(level: 1): it => { it; v(0.5em) }
= Hello WorldThe output ordering is: variables, stylesheet imports, package imports, #set directives from [style], preamble, content, then postamble.
Typst templates (like faithful-acmart, or any package that exposes a #show: template.with(...) rule) work directly through preamble and postamble: the preamble imports the template and applies the show rule, your Markdown becomes the body, and the postamble emits trailing content such as the bibliography.
---
preamble: |
#import "@preview/faithful-acmart:0.1.0": *
#show: acmart.with(
format: "acmsmall",
title: "A Faithful Paper",
authors: (
(
name: "Ada Lovelace",
email: "ada@example.org",
affiliation: (institution: "Analytical Engine Co.", city: "London", country: "UK"),
),
),
abstract: [We convert Markdown to Typst, faithfully.],
)
postamble: |
#bibliography("refs.bib")
---
# Introduction
The body is plain **Markdown**, rendered through the ACM template.The template's arguments (authors, affiliations, …) are written as literal Typst inside preamble, since each template has its own parameter shape. Do not also set the reserved title/author front-matter keys — those drive md2typst's own built-in title block, which you don't want when a template owns the title.
Reusable across documents. Put the template setup in a document class so each paper only supplies its own metadata:
# md2typst.toml
default_class = "acmart"
[classes.acmart]
preamble = """
#import "@preview/faithful-acmart:0.1.0": *
#show: acmart.with(title: doc-paper-title, abstract: [#doc-abstract])
"""
postamble = '#bibliography("refs.bib")'---
paper-title: A Faithful Paper
abstract: We convert Markdown to Typst, faithfully.
---
# IntroductionEach front-matter key becomes a #let doc-<key> = ... variable (hyphenated) that the class preamble references — here doc-paper-title and doc-abstract. Use non-reserved key names (e.g. paper-title, not title): the reserved keys title, subtitle, author, authors, date, version, publisher also trigger md2typst's own built-in title block, which you don't want when the template renders the title. Front matter → Typst conversion covers scalars and lists of scalars; deeply nested template arguments (e.g. ACM's per-author affiliation dicts) are best written inline in preamble as shown above.
Citations: Pandoc-style
@key/[@key]citations are not yet parsed —@is escaped as literal text. For now, put#cite(<key>)calls inpreamble/postambleor hand-written raw sections, and the#bibliography(...)inpostamble.
Dollar-sign math syntax is enabled by default with the markdown-it parser. The mitex package import is added automatically when math is detected.
Inline: $E = mc^2$
Display:
$$
\int_0^\infty e^{-x^2} dx = \frac{\sqrt{\pi}}{2}
$$Currency safety: markdown-it is configured with allow_space=False and allow_digits=False, so text like $5 to $10 per month or $5 to $X is treated as literal currency. Compact math expressions ($E=mc^2$, $x_1$) still work; space-padded forms ($ x = 1 $) are not recognized as math — write them without spaces around the delimiters.
The mistune parser does not enable math by default (its hardcoded regex would mis-parse currency); opt in with --plugin math if you need it. marko has no built-in math support.
Fenced code blocks with language mermaid are converted to native Typst diagrams using the mmdr package. The import is added automatically.
```mermaid
graph LR
A[Start] --> B{Decision}
B -->|Yes| C[OK]
B -->|No| D[End]
```Two backends are available via --mermaid (or mermaid_backend in config):
| Backend | How it renders | Notes |
|---|---|---|
mmdr (default) |
#mermaid(...) at Typst-compile time via the mmdr package |
No extra tooling. Its Rust renderer has no htmlLabels support, so inline formatting tags (<b>, <i>, …) are stripped from labels; <br/> line breaks are kept. |
cli |
Pre-renders each diagram to a PDF via the official mermaid-cli (mmdc) and embeds it with #image(...) |
Fully faithful to the Mermaid spec, including bold labels. Requires npm install -g @mermaid-js/mermaid-cli plus a headless Chrome. PDFs are written next to the output. |
md2typst diagram.md --mermaid cli # faithful PDF rendering via mermaid-cliPDF (not SVG) is used because Typst's SVG renderer cannot draw the <foreignObject> Mermaid emits for HTML labels (typst#1421).
Use ```diagram for ASCII-art or box-drawing diagrams that must not break across pages:
```diagram
┌──────────┐ ┌──────────┐
│ Client │──────▶│ Server │
└──────────┘ └──────────┘
```The content is wrapped in #block(breakable: false) in the Typst output.
md2typst supports named document classes inspired by LaTeX (article, report, book). Each class provides a complete set of Typst styling rules.
Select a class via front matter, CLI, or config:
---
class: report
title: My Report
---md2pdf --class book input.md# In md2typst.toml or ~/.config/md2typst/config.toml
default_class = "article"| Class | Title | Sections | Page breaks | TOC |
|---|---|---|---|---|
| article | Inline on first page | Numbered, no breaks | None | Inline |
| report | Centered with rule | Numbered (skip h1), breaks before ## |
Before sections | Own page |
| book | Full title page | "Chapter N" for h1, numbered | Chapters on odd pages | Odd page |
Classes are defined in [classes.<name>] config sections. The class preamble replaces the base [style] preamble; scalar fields (font, paper, etc.) are inherited unless overridden.
When title metadata is present in front matter, a title block is automatically generated. Each document class formats it differently.
---
title: My Document
subtitle: A Technical Overview
author: Jane Doe
date: April 2026
version: "1.0"
publisher: Acme Corp
---Supported fields: title, subtitle, author, authors, date, version, publisher. Classes define a doc-make-title() function in their preamble to customize the title formatting.
Place [TOC] on its own line to generate a table of contents:
[TOC]
## Introduction
...md2typst --help
Options:
-o, --output FILE Output file (default: <input>.typ, or stdout for stdin)
-p, --parser NAME Parser to use (markdown-it, mistune, marko)
--class NAME Document class (article, report, book)
--mermaid [mmdr|cli] Mermaid backend (default: mmdr; cli uses mermaid-cli)
--plugin NAME Load parser plugin (can be repeated)
--stylesheet NAME Import Typst stylesheet (can be repeated)
--config FILE Path to configuration file
--list-parsers List available parsers
--show-config Show effective configuration
--debug Show debug info (config sources, generated Typst)git clone https://github.com/user/md2typst.git
cd md2typst
uv sync# Run all tests (benchmarks skipped by default)
uv run pytest
# Run by category
uv run pytest -m unit # Unit tests (fast)
uv run pytest -m integration # Integration tests
uv run pytest -m e2e # End-to-end tests
uv run pytest -m benchmark # Benchmark teststests/
├── a_unit/ # Unit tests (AST, generator)
├── b_integration/ # Integration tests (parsers, config, TCK)
├── c_e2e/ # End-to-end tests
├── d_benchmark/ # Performance benchmarks
└── fixtures/ # Test fixtures (CommonMark, GFM)
The samples/ directory contains example documents for each class with a local config and Makefile:
cd samples
make all # Build article.pdf, report.pdf, book.pdf
make clean # Remove generated files# Type checking
uv run mypy src/
# Linting
uv run ruff check src/
# Formatting
uv run ruff format src/Markdown Input → Parser → AST → Generator → Typst Output
The converter uses a parser-agnostic AST (Abstract Syntax Tree) that decouples parsing from code generation. This allows:
- Swapping parsers without changing the generator
- Consistent output regardless of parser choice
- Easy extension with new parsers
MIT
Contributions are welcome! Please:
- Fork the repository
- Create a feature branch
- Add tests for new functionality
- Ensure all tests pass
- Submit a pull request