mdcast
Markdown → DOCX · ODT · PDF · PDF-presentation · PPTX · reveal.js HTML, in one async Rust library and a thin CLI on top of it.
The pitch:
- One markdown source, six outputs. Write once, render to whatever the audience reads.
- Per-page layout system. Tag a page
hero,image-full,callout,thanks— and have it honoured across every output format. - Pluggable everything. Templates, images, reveal.js distribution — all
fetched through one async
AssetProvidertrait. Your app feeds bytes from a DB, S3, an in-memory map, whatever. - Single self-contained HTML. Reveal.js dist is bundled; with
--embed-resources(default) the deck is one file with zero external URLs.
mdcast does not try to replace pandoc. Pandoc handles DOCX/PPTX/revealjs because no Rust crate matches its OOXML fidelity. Typst handles PDF because the LaTeX toolchain is slow and heavy. The value mdcast adds is the branding-and-layout layer that sits on top of both.
Quick start
Install the engines you want to use:
# both optional, depending on which targets you'll render
Build and render:
You'll get a single self-contained slides.html you can open in any browser.
A minimal markdown example
*F13 — for board discussion*
- --

Closing remarks and next steps.
What you get with no extra config:
| Page | Class | Why |
|---|---|---|
| 1 | hero |
Explicit <page class="hero"> wrapper |
| 2 | content |
Default (no rule matched) |
| 3 | image-full |
Page body is just one image → shape rule |
| 4 | callout |
Body is just a blockquote → shape rule |
| 5 | thanks |
Last page, no explicit class → positional rule |
Run mdcast explain slides.md to print this table for any file.
Page boundaries and classes
Two surface syntaxes, both accepted:
- HTML-style:
<page class="hero">…</page> - Pandoc fenced div:
::: {.hero}…:::
Outside an explicit wrapper, --- thematic breaks split pages. The
auto-classifier then fills in a class:
- Explicit class (from a wrapper) — always wins.
- Content shape —
single_h1_only→section-divider,single_image_only→image-full,single_blockquote_only→callout. - Positional — first page →
hero, last page →thanks. - Default —
content.
All rules live in brand.toml:
[]
= "hero"
= "thanks"
= "content"
[[]]
= "single_h1_only"
= "section-divider"
[[]]
= "single_image_only"
= "image-full"
Built-in classes
| Class | Where it shows up |
|---|---|
hero |
Title / cover |
content |
Body pages — paragraphs, lists, the usual |
thanks |
Closing |
image-full |
Full-bleed image |
section-divider |
Single-heading section break |
callout |
Pull-quote / emphasised single block |
A class name resolves to a different template per target. The same
<page class="hero"> produces:
- a centred large-type cover in PDF (via
typst/layouts/pdf/hero.typ) - a dark-background title slide in PDF-presentation (via
typst/layouts/pdf-presentation/hero.typ) - a
<section class="hero">in reveal.js (styled by the theme CSS) - a
Heroparagraph-style in DOCX/ODT (from the reference doc)
Missing template for some class? The renderer logs a warning and falls back
to content. Authors are never blocked.
Library usage
use Path;
use Arc;
use Registry;
use ;
use ;
use Bytes;
use Result;
async
# async
Anything the provider returns None for falls through to the next layer.
EmbeddedAssets is always at the bottom and ships:
- Built-in Typst layouts (
hero,content,thanks,image-full,section-divider,callout) forpdfandpdf-presentation. - Minimal reveal.js 4.6.1 distribution (with stripped font imports — falls back to system sans-serif).
- Pandoc reference-doc placeholders (a real branded set is still TBD).
Cargo features
[]
= ["pandoc", "typst"]
= [] # DOCX, ODT, PPTX, html-reveal
= [] # PDF, PDF-presentation
Build with only what you need:
Targets
| Target | Engine | Notes |
|---|---|---|
docx |
pandoc | Class = paragraph-style name in reference.docx |
odt |
pandoc | Class = paragraph-style name in reference.odt |
pptx |
pandoc | Class = slide-layout name in reference.pptx |
html-reveal |
pandoc | Single self-contained file; reveal.js dist bundled & inlined |
pdf |
typst | Per-class typst template under typst/layouts/pdf/ |
pdf-presentation |
typst | Per-class typst template under typst/layouts/pdf-presentation/ |
CLI
mdcast render INPUT.md --target <T> --out OUTPUT [--assets DIR] [--brand brand.toml]
mdcast explain INPUT.md [--brand brand.toml]
Targets: docx, odt, pdf, pdf-presentation, pptx, html-reveal.
What's deferred
These are not bugs — they're chosen scope cuts. Each lands as an additive
change at a seam that already exists (see PROJECT_PLAN.md §10).
- Real branded
reference.{docx,odt,pptx}assets (.keepplaceholders for now; pandoc default styling applies). - Markdown → Typst body rendering (current templates use
#raw(body, lang: "markdown")— bodies render as source until the md→typst step lands). - Mermaid → SVG pre-processing (a Rust renderer the team already owns will plug in as a pre-step).
- Brand projection (one
brand.tomlcolour change → propagated to all outputs). - Caching (content-hashed diagram + output cache).
License
MIT OR Apache-2.0