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, enabled by default - Mermaid diagrams:
```mermaidcode blocks rendered via mmdr - 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 appended after structured fields
preamble = """
#set par(justify: true, first-line-indent: 1em)
#show heading.where(level: 1): it => { it; v(0.5em) }
"""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, concatenated with configstyle.preamblestylesheet/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, then content.
Dollar-sign math syntax is enabled by default (markdown-it and mistune parsers). 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}
$$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]
```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)
--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