ATS-safe resume typesetting from Markdown, in Rust — with extraction order as a contract.
Write your resume in a small Markdown dialect; get a clean, tightly typeset one-page PDF. What makes resumark different is the part you can't see:
Visually identical PDFs can carry scrambled text layers. A two-column layout that looks right can extract as
dates → company → rolegibberish — and applicant tracking systems parse the text layer, not the pixels. Most resume tools hope their output extracts sensibly. resumark guarantees the text layer reads in the document's declared order, and ships the proof.
Every text run is emitted into the PDF content stream in declared reading
order, baselines are arranged so position-based extractors recover the same
sequence, and --verify round-trips the output through pdftotext in both
modes and asserts it:
$ resumark resume.md --verify
wrote resume.pdf (1 page, scale 1.00)
verify: stream order (pdftotext -raw) OK — 34/34 strings in declared order
verify: reading order (pdftotext) OK — 32/32 strings in declared orderNo other typesetting engine treats extraction order as a testable contract.
$ cargo install --locked resumarkOr from source: git clone https://github.com/ResonanceCache/resumark && cargo install --path resumark.
The five Montserrat weights are embedded in the binary (SIL OFL 1.1), so a
single resumark binary is all you need. --verify additionally wants
pdftotext from poppler on your PATH
(brew install poppler / apt install poppler-utils); rendering works
without it.
$ resumark resume.md # → resume.pdf, auto-fit to one page
$ resumark resume.md --verify # + prove the extraction order
$ resumark resume.md --json # machine-readable report
$ resumark resume.md --a4 # A4 instead of US Letter
$ resumark resume.md --pages 2 # allow two pages before shrinking typeThe type scale auto-shrinks in 2% steps (down to 78%) until the content fits the page budget. If it still doesn't fit, resumark says so — cut a bullet.
# Your Name
> email@example.com | 555-0100 | City, ST
> [github.com/you](https://github.com/you)
## Experience
### Role Title
**Company** *Optional Extra* | Dates | Location
- bullet — supports **bold**, *italic*, [links](https://example.com), and bare URLs
- another bullet
## Skills {skills}
- **Languages:** Rust, Python
- **Systems:** Kafka, Postgres
## Education {education}
### School | Years
**Degree** | Detail#name,>contact lines (right-aligned in the header)##section label; a{skills}tag renders label/value rows (only{skills}changes rendering —{education}is a conventional label)###entry title, with an optional| asideshown beneath it- The
**Company**line takes|-separated dates/location after it - Any plain line inside a section becomes a small note paragraph
See examples/resume.md for a complete resume.
- Declared order. Layout assigns every text run an explicit emission index in reading order — independent of where it lands visually. For an entry row that's role → company → extra → dates; when an aside sits under the role (education years), the block reads column-wise, exactly as an eye or a parser scans it.
- Stream order = declared order. The PDF content stream is written in
that sequence, so stream-order extractors (
pdftotext -raw, and the parsers many ATS products build on) see the declared order verbatim. - Positional order is asserted structurally. Baselines are deliberately offset — the role sits a hair above the company line — so geometry-based extractors read the role first. Positional layout analysis is heuristic, though (poppler can scan a run of stacked entry rows column-wise), so the reading-mode check asserts what geometry can actually promise: sections in order, roles in order, every field after its own role, bullets in sequence — everything a parser needs to reconstruct the resume correctly.
- Verified, not hoped.
--verifyasserts both properties against realpdftotextoutput; the same harness runs as an integration test in CI. Fonts are embedded as CID-keyed subsets with exact ToUnicode CMaps, so what extracts is character-for-character what you wrote.
resumark is agent-friendly by design: --json emits everything an agent
needs to iterate (pages, fit scale, warnings, per-mode verification results
with the first out-of-order string). The repo ships a Claude Code skill in
.claude/skills/resume that teaches the
workflow: draft the Markdown from a person's background, render, verify, and
fix what the report flags. Clone the repo and the skill is available in
Claude Code sessions there; or copy the skill directory into your own
project's .claude/skills/ (or ~/.claude/skills/ to have it available in
every session).
Typst and LaTeX are magnificent general typesetters — and neither makes any promise about what an ATS parser sees. Their text layers usually extract reasonably; nothing checks it, and multi-column templates routinely scramble. resumark is not a general typesetter: it does one document class, does it tightly, and treats the invisible layer as the product.
The engine underneath (parse → layout → pdfgen → verify) is an
ordinary Rust library and the CLI is a thin wrapper over
resumark::render_bytes. The API is not stabilized yet; if you want the
extraction-order-contract layer as a standalone crate for other document
types, open an issue.
MIT. The embedded Montserrat fonts are licensed under the SIL Open Font License 1.1.