Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ Cargo.lock.bak
# IDE
.idea/
.vscode/
.claude/
*.iml

# LibreOffice lock files (created when opening a doc for testing)
Expand Down
60 changes: 60 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,66 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [0.3.0] — 2026-06-14

A math-rendering and robustness release. The native math layout engine gained
font-aware metrics, real bold, and properly-centred accents; custom OTF/CFF
fonts now embed correctly in PDF; and a broad rendering sweep fixed several
overflow / silent-drop bugs.

### Added

- **Font-aware math metrics.** The math layout engine now measures glyph
advances against the face that will actually render them (the embedded PDF
font, or bundled DejaVu for SVG/PNG) via a `GlyphMetrics` provider, so
reserved width matches drawn glyphs — equations stay aligned under
`--pdf-font`, not just with the default font.
- **Math bold.** `\mathbf`, `\textbf`, `\boldsymbol`, `\bm`, `\pmb`, `\mathbfit`
now render in real bold weight (PDF/SVG/PNG); previously a no-op, and
`\boldsymbol` leaked literal source.
- **`\dfrac` / `\tfrac` / `\cfrac`** and **`\mid` / `\vert` / `\Vert`** are now
recognised (previously leaked literal source).
- **GFM table column alignment** (`:---`, `:---:`, `---:`) is honoured in PDF,
SVG, PNG, and HTML.
- **Inline `<svg>…</svg>`** blocks are now rasterised and embedded instead of
being silently dropped.

### Fixed

- **OTF/CFF font embedding.** PDF now emits `CIDFontType0` + `FontFile3` for
CFF/OpenType faces (e.g. STIX Two Math) instead of mislabelling them as
TrueType, which strict readers rejected.
- **Math accents** (`\bar`, `\hat`, `\vec`, `\tilde`, `\dot`, `\ddot`) are drawn
as centred geometry rather than zero-advance combining marks, which had
landed at the glyph's right edge (and differently per font — e.g. `\bar{d}`
rendered as "đ").
- **Long titles no longer overflow.** Title slides (all layouts) and section
dividers auto-shrink to fit; wrapped content-slide headings push the body and
underline down instead of overprinting them.
- **Nested blockquotes** (`>`/`>>`/`>>>`) keep every level instead of rendering
only the deepest.
- **Long unbreakable tokens** (URLs, hashes, CamelCase) hard-break to wrap
instead of overflowing the slide edge (PDF/SVG); HTML uses `overflow-wrap`.
- **Tabs in code** expand to 4-column tab stops instead of rendering as
missing-glyph boxes.
- **`{width=N%}` > 100** clamps instead of leaking the literal attribute text.
- **`:::` columns** handle leading/trailing/duplicate dividers (the fence form
`::: … ::: … :::`) instead of dumping all content into the right column.
- **Task-list items** show only their checkbox, not a checkbox *and* a bullet
(PDF/SVG/HTML/PPTX).
- **Display-math pagination** weights generated equation images by their real
aspect ratio, so a one-line equation no longer splits onto a near-empty
continuation slide.

### Known limitations

- Right-to-left text uses `direction: rtl` for right-alignment but does not
perform full Unicode bidi reordering of mixed LTR/RTL runs.
- CJK and colour-emoji glyphs require `--cjk <font>` for PDF (the bundled DejaVu
face does not cover them).
- Table column alignment is not yet applied to the editable Office/ODF outputs
(pptx/odp/odt/docx).

## [0.2.0] — 2026-05-24

A feature-and-fix-heavy follow-up to the initial release. New CLI surface
Expand Down
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 6 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "md2any"
version = "0.2.0"
version = "0.3.0"
edition = "2021"
rust-version = "1.74"
description = "Markdown → PowerPoint, OpenDocument Impress, PDF, Word, Writer, HTML, SVG, and PNG. One markdown source, one small Rust binary."
Expand All @@ -22,6 +22,11 @@ exclude = [
"/examples/*.odp",
"/examples/*.docx",
"/examples/*.odt",
# The manual in binary office formats (~1.2 MB) is redundant with HELP.md
# for a source crate; keep it in the repo, out of the published package.
"/docs/manual",
# Large pre-rendered sample images not referenced by any shipped example.
"/examples/assets/*.png",
]

[[bin]]
Expand Down
32 changes: 29 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@
binary that converts markdown into editable PowerPoint, OpenDocument Impress,
PDF, Word, LibreOffice Writer, standalone HTML, and per-slide SVG/PNG files.

<p align="center">
<img src="docs/img/slides.png" alt="Content, code, and table slides rendered by md2any" width="100%">
</p>

```bash
md2any talk.md # → talk.pptx (PowerPoint)
md2any talk.md -o talk.odp # → talk.odp (LibreOffice Impress)
Expand Down Expand Up @@ -44,6 +48,24 @@ md2any is the only tool in this list that produces every common Office /
OpenDocument format **natively** from a single small binary — no headless
browser, no LaTeX stack, no language runtime.

## Native math — no LaTeX, no KaTeX, no browser

A built-in layout engine renders display math (fractions, radicals, scripts,
matrices, cases, accents, bold) as **selectable text and vector geometry** —
not images, and without a TeX or JavaScript stack. Bring any math font with
`--pdf-font` (here, STIX Two Math); metrics adapt to the face automatically.

<p align="center">
<img src="docs/img/equations.png" alt="Euler's identity, the Lorentz factor, the Dirac equation, and Bayes' theorem" width="100%">
</p>

It scales all the way up to the full **Standard Model Lagrangian** on a single
A4 page ([source](examples/standard-model-lagrangian-a4.md)):

<p align="center">
<img src="docs/img/lagrangian.png" alt="The Standard Model Lagrangian rendered on one A4 page" width="62%">
</p>

## Install

### Prebuilt binaries
Expand Down Expand Up @@ -138,13 +160,13 @@ md2any hello.md --serve # live preview at http://localhost:8421
| **Themes** | Light + dark built-in, full custom palettes via YAML overlay |
| **Layouts** | Clean / studio / frame / bold |
| **Aspect ratios** | 16:9, 4:3, 9:16, A3/A4/A5, Letter, Legal, Tabloid + landscape variants + custom `WxH[unit]` |
| **Markdown** | Lists (9 deep nesting), tables, dark-by-default code blocks (40+ tags incl. mainframe/IBM i, Haskell/BCPL, web/API/config, and `bf`), block quotes, footnotes, links (clickable), strike, inline code |
| **Markdown** | Lists (9 deep nesting), task lists, tables with column alignment (`:---:` / `---:`), dark-by-default code blocks (40+ tags incl. mainframe/IBM i, Haskell/BCPL, web/API/config, and `bf`), block quotes, footnotes, links (clickable), strike, inline code, inline `<svg>` |
| **Layout extras** | Side-by-side columns (`:::`), landscape code two-up (`--code-columns` / `columns=2`), image/text full-page hints, per-slide background (`<!-- bg: -->`), TOC injection, hand-tuned pagination with widow/orphan control |
| **Math** | `$inline$` and `$$display$$` LaTeX subset -> Unicode, including accents/text/math alphabets; `--math source` preserves source; `--math svg` uses a built-in box layout for display fractions, radicals, scripts, matrices/cases/arrays, scalable delimiters, and aligned equations |
| **Math** | `$inline$` and `$$display$$` LaTeX subset -> Unicode, including accents/text/math alphabets; `--math source` preserves source; `--math svg` uses a built-in box layout for display fractions, radicals, scripts, matrices/cases/arrays, scalable delimiters, aligned equations, **bold** (`\mathbf`/`\boldsymbol`), and drawn accents (`\bar`/`\hat`/`\vec`). Font-aware metrics keep equations aligned under `--pdf-font` (incl. OTF/CFF math fonts like STIX) |
| **Diagrams** | Embedded `dot`, `mermaid`, `plantuml` (shells out if installed) |
| **Speaker notes** | `<!-- notes: -->` HTML comments |
| **Transitions** | `fade`, `push`, `wipe`, `cover`, `split` (PPTX/ODP/PDF) |
| **RTL / CJK** | `direction: rtl` for Arabic/Hebrew; CJK via the consumer's font |
| **RTL / CJK** | `direction: rtl` right-aligns Arabic/Hebrew (no full bidi reordering of mixed runs yet); CJK & emoji need `--cjk <font>` for PDF |
| **Workflow** | `--watch` for rebuild-on-save, `--serve` for live preview with hot reload, `--check` for lint mode |
| **Handouts** | `--handout 2|4|6` for N-up A4 printable PDF |
| **Multi-file** | Concat multiple `.md` files; `-` reads stdin |
Expand Down Expand Up @@ -371,6 +393,10 @@ std::fs::write("talk.pdf", bytes)?;
`--font-fallback`
- DejaVu does not include CJK glyphs; for PDF Chinese/Japanese/Korean decks,
pass `--cjk PATH` or include a CJK face in `--font-fallback`
- `direction: rtl` right-aligns Arabic/Hebrew but does not yet perform full
Unicode bidirectional reordering of mixed LTR/RTL runs
- GFM table column alignment (`:---:`, `---:`) is applied in PDF / SVG / PNG /
HTML; the editable Office/ODF outputs (pptx/odp/odt/docx) don't carry it yet
- Speaker notes attach natively in PPTX / ODP, can be emitted as a PDF
companion with `--with-notes`, can be appended to DOCX/ODT via
`--doc-style speaker-notes`, and can be bundled with deck + handout via
Expand Down
Binary file added docs/img/equations.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/img/lagrangian.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/img/slides.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
97 changes: 97 additions & 0 deletions docs/torture-test-findings.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
# md2any torture-test findings

Broad stress test of the renderer (2026-06-14). Six domains — tables/lists/columns,
code/syntax, typography/unicode, images/SVG, themes/layouts/aspects, and multi-format
robustness — each rendered to PDF/PNG (and cross-checked in HTML/Office formats) and
visually inspected.

**Multi-format output is solid:** 72 artifacts across all 8 formats (pptx, odp, pdf,
docx, odt, html, svg, png) — no crashes, no corrupt archives, no dropped content.
All defects below are in the slide layout/render layer.

Status legend: ☐ open · ☑ fixed · ◐ in progress · ⊘ won't-fix / larger effort

---

## HIGH

- ☑ **H1 — Long titles overflow & overprint the footer.** Fixed across **all**
title layouts (Clean/Studio/Frame/Bold): `fit_hero_size` shrinks the hero title
to fit above the subtitle/footer; Bold no longer drops the title off the top of
its accent block (`src/pdf.rs`). Subtitle/author stay visible.

- ☑ **H2 — Wrapped content headings collide with body.** Fixed: `render_content_slide`
measures the wrapped title line count and pushes the underline + body down by the
extra lines (`src/pdf.rs`). Was: fixed-height title band.

- ☑ **H3 — Section-divider long titles clipped off the bottom.** Fixed: section
divider (Clean/Frame) now auto-shrinks via `fit_hero_size` (`src/pdf.rs`).

- ☑ **H4 — Inline `<svg>…</svg>` silently dropped.** Fixed: the parser accumulates
inline SVG across HTML events and routes it through the image pipeline as a
base64 `data:image/svg+xml` block, rasterised like external SVG (`src/parser.rs`).

- ☑ **H5 — Table column alignment ignored** (`:---:`, `---:`). Fixed: added
`ir::ColumnAlign` + `Table.aligns`, captured from the GFM delimiter row; honoured
in **PDF / SVG / PNG / HTML**. (Office formats — pptx/odp/odt/docx — thread the
data but don't yet apply native cell alignment; see L7.)

- ☑ **H6 — Tabs in code → tofu boxes + broken indentation.** Fixed: code lines
expand tabs to 4-column tab stops at parse time, fixing every backend
(`src/parser.rs`).

## Known-limitation class (larger efforts)

- ⊘ **H7 — RTL (Arabic/Hebrew) not reordered.** No bidi; Hebrew reversed, Arabic
left-aligned. Needs a bidi pass — larger effort.

- ⊘ **H8 — CJK + emoji → tofu in PDF.** Only DejaVu embedded; no CJK/emoji fallback
font. HTML preserves them. Needs a fallback font pipeline — larger effort.

## MEDIUM

- ☑ **M1 — Nested blockquotes collapse.** Fixed: the quote accumulator only
resets on the outermost `>`, so nested levels keep the outer paragraphs
(`src/parser.rs`).

- ☑ **M2 — Long unbreakable tokens overflow the right edge.** Fixed: PDF/SVG
wrappers hard-break a token wider than the line into character chunks; HTML
uses `overflow-wrap: anywhere` (`src/pdf.rs`, `src/svg.rs`, `src/html.rs`).

- ☑ **M3 — `{width=N%}` > 100 leaks literal `{width=…}` as body text** (+ spurious
slide). Fixed: `parse_width_pct` now clamps to 1..=100 instead of returning `None`
(`src/parser.rs`).

- ☑ **M4 — `:::` columns: a leading/extra marker dumps everything into the right
column** (empty left). Fixed: `coalesce_columns` now splits on all dividers and
drops empty segments, so the fence form `::: … ::: … :::` works (`src/paginate.rs`).

- ☑ **M5 — Task-list items render a bullet AND a checkbox** (`● ☐ task`). Fixed:
added `ListItem::is_task()`; bullet suppressed in pdf/svg/html/pptx render_list.
ODT/DOCX use native list styles and still show both — needs a no-bullet list
style declaration (follow-up, see L7).

- ☐ **M6 — Pagination over-splits in 16:9** — image+caption and tables split with
slides ~60% empty (a4 fits them). Same family as the math-image weight fix already
landed; the height budget is too conservative for several block types in 16:9.

## LOW / polish

- ☐ L1 — Code: leading/trailing blank lines inside a fence kept verbatim.
- ☐ L2 — Rust `'static` lifetime mis-highlighted as a char/string literal.
- ☐ L3 — `&nbsp;` not decoded (renders literal `nbsp`).
- ☐ L4 — `₿` (U+20BF) tofu; other currency symbols fine.
- ☐ L5 — Misleading "aspect too small" error for ratios like `16:10`/`1:1` (only
`16:9`/`4:3`/`9:16` presets accepted).
- ☐ L6 — TOC lists only H1 sections.

---

## Fix order

Cheap/safe first, then the big UX wins:
M3 (width clamp) → M5 (task-list bullet) → M4 (`:::` leading marker) →
H1/H2/H3 (title & heading auto-fit + body push-down) → H4 (inline SVG) →
H5 (table alignment, needs IR field) → M6 (16:9 pagination budget) →
M1 (nested blockquotes) → M2 (token overflow) → low/polish.
H7/H8 (RTL, CJK/emoji) are larger and tracked separately.
78 changes: 78 additions & 0 deletions examples/famous-equations.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
---
title: A Wall of Famous Equations
subtitle: The greatest hits, set in STIX Two Math
author: md2any
date: auto
theme: light
aspect: 16:9
layout: clean
math: svg
math_block_align: center
math_scale: 1.2
math_max_height: 230
toc: false
---

# A Wall of Famous Equations

A greatest-hits tour — from a patent clerk's β to the geometry of spacetime.

## Euler's Identity

The most beautiful equation in mathematics — five constants, one line.

$$ e^{i\pi} + 1 = 0 $$

## Mass–Energy Equivalence

Einstein's 1905 sequel to *On the Electrodynamics of Moving Bodies*.

$$ E = mc^2 $$

## The Lorentz Factor

Straight out of §6 of that 1905 paper — the β that warps space and time.

$$ \gamma = \frac{1}{\sqrt{1 - \dfrac{v^2}{c^2}}} $$

## The Schrödinger Equation

How a quantum state evolves in time.

$$ i\hbar\frac{\partial}{\partial t}\Psi = \hat{H}\Psi $$

## The Dirac Equation

Relativity meets the quantum — and predicts antimatter.

$$ \left(i\hbar\gamma^\mu\partial_\mu - mc\right)\psi = 0 $$

## Einstein's Field Equations

Matter tells spacetime how to curve; spacetime tells matter how to move.

$$ G_{\mu\nu} + \Lambda g_{\mu\nu} = \frac{8\pi G}{c^4}\,T_{\mu\nu} $$

## The Gaussian Integral

The bell curve's deceptively simple area.

$$ \int_{-\infty}^{\infty} e^{-x^2}\,dx = \sqrt{\pi} $$

## Bayes' Theorem

How evidence updates belief.

$$ P(A \mid B) = \frac{P(B \mid A)\,P(A)}{P(B)} $$

## Boltzmann's Entropy

Carved on his gravestone in Vienna.

$$ S = k_B \ln W $$

## The Basel Problem

Euler summed the reciprocal squares — and π showed up.

$$ \sum_{n=1}^{\infty} \frac{1}{n^2} = \frac{\pi^2}{6} $$
Loading
Loading