Skip to content

Repository files navigation

resumark

CI

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 → role gibberish — 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 order

No other typesetting engine treats extraction order as a testable contract.

Install

$ cargo install --locked resumark

Or 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.

Quickstart

$ 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 type

The 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.

The Markdown dialect

# 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 | aside shown 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.

The contract, precisely

  1. 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.
  2. 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.
  3. 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.
  4. Verified, not hoped. --verify asserts both properties against real pdftotext output; 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.

Using it with an AI agent

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).

Why not Typst or LaTeX?

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.

Library

The engine underneath (parselayoutpdfgenverify) 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.

License

MIT. The embedded Montserrat fonts are licensed under the SIL Open Font License 1.1.

About

ATS-safe resume typesetting from Markdown, in Rust — extraction order as a verified contract

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages