MARKTerm is a suite of tools to render Markdown anywhere, built on top of Markd:
marktermrenders Markdown to the terminal, with themes, images, and HTML-style links. Inspired by Glow.markpdfrenders Markdown to small, fast, self-contained PDF files.markmarkrenders Markdown to Markdown, for normalizing or filtering documents.
It can also be used as a Crystal library.
- It will syntax highlight code blocks
- It will try to handle light and dark terminal themes. Since it uses the terminal's colors, it should match things like vs code themes in the vs code terminal, etc.
- In general it tries to look good and not gaudy
- It will do the right thing if output is not a tty
- Optional hyphenation of long words when wrapping
(
--hyphenate, English or Spanish patterns) - Can be used as a library or as a program
Done recently (markpdf):
- ✅ Built-in stylesheets (default, book, dark, sepia) with
--style,--print-styleand repeatable--css - ✅ Pageless single-page output (
--pageless) - ✅ PDF outline bookmarks from headings
- ✅ Two-pass table of contents (
--toc) with linked, numbered entries - ✅ Task-list checkboxes (☑/☐)
- ✅ Wide tables scale to fit and split across pages at row boundaries
- ✅ Collapse-style table borders and keep-with-next pagination
- ✅ Superscript and subscript via inline HTML
- ✅ Math: styled Unicode pass (always) + optional libtexprintf text art for display math (GPL build flag)
- ✅ Emoji and CJK rendering via font fallback (CID font embedding through a libharu patch)
- ✅ Richer header/footer templating:
left|center|rightsections via|, and a base-14 font fallback so headers and footers work even without embedded fonts
Done (markterm):
- ✅ Configurable themes
- ✅ Implement HTML-style links as supported in kitty/alacritty
- ✅ Don't break paragraphs on soft breaks
- ✅ Implement images as supported in kitty (requires timg, kinda buggy)
- ✅ Images in all terminals (requires catimg, kinda useless)
- ✅ Implement HTML block support
- ✅ Better textual image display when images are not supported
- ✅ Maybe only support timg with options
- ✅ Support being used in a pipeline
- ✅ Task lists, GFM alerts, wrapped tables
- ✅ Footnotes (rendered as endnotes: notes collect at the end of the document)
- ✅ Wrap styled table cells at word boundaries when tables are squeezed
- ✅ Internal piping to $PAGER for tall documents (
--no-pageropts out) - ✅ CLI switches for images and links (
--images,--no-images,--no-links) - ✅ Color capability detection: NO_COLOR and colorless terminals get plain text with ATX heading hashes
The forks and patches this suite carries are meant to shrink over time. These are the changes we want merged upstream; open pull requests are linked:
- markd: footnotes —
PR #78, open since
March 2025, awaiting review;
shard.ymlpins the PR branch - litehtml: clip-based subtree pruning in the draw walk — PR #485, in review; makes paginated drawing linear instead of quadratic
- litehtml:
get_row_boxes, the table-row box API markpdf needs to split tables at row boundaries — local-only commit on top of the #485 branch; goes upstream once it lands - libharu: cmap format 12 support and an alternate-CID encoder API, which together enable non-BMP glyphs (emoji) — currently a local patch applied at build time (ext/libharu-cid-fixes.patch); no PR yet
Either get a static binary from the releases page or build from source:
- Install crystal
- Checkout the repo
- run
shards build
This is the help:
Markterm - A tool to render markdown to the terminal
Usage:
markterm <file> [--theme <theme>][--code-theme <code-theme>][--links][--color][--width <width>][--hyphenate][--language <language>][--images|--no-images][--no-links][--no-pager]
markterm -h | --help
markterm --version
Options:
-h --help Show this screen.
-t <theme>, --theme <theme> Theme to use for coloring output
--code-theme <code-theme> Theme to use for coloring code blocks
--version Show version.
-l, --links Force html-like links
--no-links Never emit html-like links
-c, --color Force color output even when piping
-w <width>, --width <width> Maximum line width for text wrapping (0 to disable, auto-detects if not specified)
--hyphenate Break long words at syllable boundaries when wrapping
--language <language> Hyphenation language: en or es [default: en]
--images Force images where the terminal can show them
--no-images Never draw images; show placeholders instead
--no-pager Never pipe output to $PAGER
--config <path> Read options from this YAML file instead of
~/.config/markterm/config.yml
If you use "-" as the file argument, markterm will read from stdin.
Options can also be set in ~/.config/markterm/config.yml (keys are the
long option names, e.g. "theme: monokai") or through MARKTERM_*
environment variables (e.g. MARKTERM_WIDTH). Command line options win
over environment variables, which win over the config file. Run with
--print-config to dump the effective configuration as YAML.
There is a similar markmark binary that will render markdown to markdown.
The markterm, markmark and markpdf CLIs share a configuration
mechanism (via docopt-config).
Every option can be set in four places, in decreasing precedence:
- the command line (e.g.
--width 60) - environment variables prefixed with the tool's name (e.g.
MARKTERM_WIDTH=60,MARKPDF_STYLE=book) - a YAML config file:
~/.config/markterm/config.yml,~/.config/markmark/config.ymlor~/.config/markpdf/config.yml(honoring$XDG_CONFIG_HOME) - the built-in defaults shown in the help
Config file keys are the long option names, with underscores or dashes, and values are typed:
# ~/.config/markterm/config.yml
theme: monokai
width: 80 # numbers work for numeric options
hyphenate: true# ~/.config/markpdf/config.yml
page-size: letter
margin: "15,25" # CSS-style: top/bottom, left/right
css:
- tweaks.css # repeatable options accept lists
font:
- LiberationSerif.ttf
- LiberationSans.ttfEnvironment variables follow the same rules: booleans accept
true/yes/1 (and false/no/0 to turn an option off), and
repeatable options accept comma-separated lists
(MARKPDF_FONT=a.ttf,b.ttf).
Every tool also accepts a --print-config flag, which prints the
effective configuration — command line, environment and config file
merged — as YAML that can be saved and used as a config file as is:
$ markpdf --print-config --style book
---
page_size: a4
margin: "20"
style: book
language: enmarkterm and markmark still require their file argument, so pass
one (or - for stdin) when printing their configuration, e.g.
markterm --print-config - < document.md.
Pass --config <path> to read the configuration from a specific file
instead of the default one; the file must exist. The flag itself can
only come from the command line.
A missing config file is not an error; without one the tools behave exactly as they always have. A config file that exists but cannot be parsed produces a warning on stderr and is otherwise ignored.
The markpdf binary renders markdown to PDF. It converts the markdown to
HTML with markd, lays it out with litehtml,
and writes the PDF through libharu,
via the C++ shim in ext/. Styling is CSS: markpdf ships a roster of
built-in stylesheets (pick one with --style), and you can add your own
rules on top with --css.
For print-on-demand, --kdp mode makes the output Amazon-KDP-compliant
(embedded fonts, gutter by page count, recto chapter starts, even page
count) — see KDP.md for the full print-production guide.
Markpdf - A tool to render markdown to PDF
Usage:
markpdf [<file>] [--font <font>...][--css <css>...][options]
markpdf --list-styles
markpdf -h | --help
markpdf --version
Options:
-h --help Show this screen.
-t <theme>, --theme <theme> Theme to use for coloring output
--code-theme <code-theme> Theme to use for coloring code blocks
--version Show version.
-o <output>, --output <output> Write the PDF to a file (defaults to standard output)
--page-size <size> Page size: a0..a6, b0..b6, letter, legal, or
custom WxH with an optional unit per side
(6x9 or 6x9in = 152.4x228.6mm, 100x200mm)
[default: a4]
--margin <margins> Page margins in mm, CSS-style: 1 value (all sides),
2 (top/bottom, left/right), 4 (top, right, bottom,
left) or 5 (... plus gutter) [default: 20]
--mirror-headers Mirror running headers and footers on verso
(even) pages — pairs with a gutter margin
--kdp KDP print mode: embed every font, drop the
outline, scrub metadata, size the gutter
from the page count (unless --margin sets
one), start chapters on recto pages and
pad odd page counts to even
--style <style> Built-in stylesheet setting layout and typography
(themes set colors instead): see --list-styles
[default: default]
--list-styles List the built-in stylesheets and exit
--print-style Print the built-in stylesheet named by --style to
standard output and exit
--css <css> Extra CSS file layered on top of the style; last
declaration wins (may be repeated)
--font <font> TTF font file to embed (can be repeated). Fonts are
matched by their internal family name; system fonts
are used automatically when available.
--emoji-font <font> TTF font used for emoji and symbols the main fonts
lack (auto-detected from system fonts by default)
--header <header> Page header text; "%p" is the page number, "%t"
the total page count. Split it with "|" into
left|center|right sections
--footer <footer> Page footer text; supports the same placeholders
and sections
--pageless Single-page output: one page as tall as the document,
no headers/footers — good for on-screen reading,
wrong for printing. Very long documents scale
down to fit the PDF page-size limit.
--hyphenate Insert soft hyphens at hyphenation points, so
fully justified paragraphs can break long words
with a hyphen at the line end
--language <language> Hyphenation language for --hyphenate: en or es
[default: en]
--no-remote-images Skip http(s) image sources instead of fetching
them; remote fetching can also be turned off
programmatically with Markd::Pdf
--toc Prepend a table of contents with page numbers;
every entry links to its section. The layout
runs repeatedly until the numbers stop moving
(a TOC's own length shifts the pages it points to)
--toc-depth <depth> Deepest heading level the TOC lists, or a
range N-M listing only levels N through M
(2-6 skips a level-1 document title);
levels run 1 to 6 [default: 1]
--toc-title <title> Title above the table of contents [default: Contents]
--config <path> Read options from this YAML file instead of
~/.config/markpdf/config.yml
--print-config Print the effective configuration as YAML
(command line, environment and config file
merged) and exit
If you use "-" as the file argument, markpdf will read from stdin.
Complete HTML documents (and .html files) are rendered directly,
skipping the markdown conversion.
Images are resolved relative to the input file's directory.
Options can also be set in ~/.config/markpdf/config.yml (keys are the
long option names, e.g. "page-size: letter"; list-valued keys work for
repeatable options, e.g. "font: [font1.ttf, font2.ttf]") or through
MARKPDF_* environment variables (e.g. MARKPDF_STYLE). Command line
options win over environment variables, which win over the config file.
The look layers like this: --style picks a whole layout/typography
stylesheet, --theme recolors it, and --css overrides anything on top.
Force a new page anywhere with an empty marker div:
...end of chapter one
<div style="page-break-before: always"></div>
# Chapter twoTo break on a pattern — say, every chapter — put the property on a selector in your own CSS instead:
h2 { page-break-before: always; }break-before: page, page-break-after: always and break-after: page
work the same way. Named pages work too: page-break-before: right
(recto, and left/verso) opens the element on a right-hand page,
inserting a blank page when it would otherwise land on a wrong-parity
one — the classic convention that chapters start on odd pages. Blank
filler pages carry no content and no header or footer.
--toc prepends a linked table of contents with page numbers. Because
the TOC's own length shifts every page after it, the numbers are found
by fixed point: markpdf renders, reads back where each heading landed,
rebuilds the TOC, and re-renders until what the TOC says matches where
things are — two or three passes for a typical book. Entries link to
their sections in the PDF.
markpdf book.md --toc --toc-depth 2 --toc-title "Inhalt" --kdp -o book.pdf--toc-depth caps how deep the listing goes (1 = chapters only, up to
6), and also takes a range: --toc-depth 2-6 lists everything except
a level-1 document title — the usual shape of a markdown book whose H1
is the title and whose H2s are the chapters (a narrower N-M lists
only those levels, so 2-2 is chapter titles alone);
--toc-title sets the heading above it. TOC pages are ordinary
pages: they count toward the page count, the kdp recto rule and the
even-page pad, and the TOC's numbers account for all of it. With
--pageless the entries lose their page numbers (there are no pages),
and --toc on a document without headings warns and renders without
the TOC.
Built-in stylesheets set layout and typography; -t themes set colors.
The rendered stylesheet is layered style → theme → --css, each
later layer winning on equal specificity. --css may be repeated.
| style | look |
|---|---|
| default | clean sans-serif print style |
| book | serif, justified, indented — long prose / e-readers |
| dark | dark page, light text — screen reading |
| sepia | warm paper tones, serif — e-reader default look |
See them, print one out, tweak it, and feed it back:
$ markpdf --list-styles
default clean sans-serif print style (current)
book serif, justified, indented — for long prose / e-readers
dark dark page, light text — for screen reading
sepia warm paper tones, serif — e-reader default look
$ markpdf --print-style --style book > my-book.css
$ $EDITOR my-book.css
$ markpdf book.md --style book --css my-book.css -o book.pdfThe dark style automatically uses a dark syntax-highlighting theme for
code blocks unless you pass --code-theme or -t explicitly.
For on-screen reading, --pageless skips pagination entirely: the
output is a single page as tall as the document (the --page-size
still sets its width, --margin the outer whitespace). Headers,
footers and page numbers do not apply in this mode. Documents longer
than the PDF page-dimension limit (14,400 pt ≈ 20 printed pages) are
scaled down uniformly — zoom in your viewer; text stays vector-crisp.
Markdown math ($E = mc^2$ inline, $$…$$ display) is rendered as
styled Unicode: italic serif with real sub/superscripts and LaTeX
commands mapped to symbols (∫ ∑ ∞ π ± ≤ …). For display math you can
get true text-art rendering (integral signs with limits, fraction
bars) by enabling the optional GPL-3 libtexprintf
library — it lives in ext/libtexprintf as a git submodule:
git submodule update --init ext/libtexprintf
make -C ext WITH_TEXMATH=1
WITH_TEXMATH=1 shards buildNote the license trade-off: libtexprintf is GPL-3, and statically linking it makes the resulting markpdf binary effectively GPL-3. The default build does not use it, keeps your existing license, and renders math with the Unicode styling pass.
Text uses embedded TrueType fonts with full Unicode support: the shim
matches the CSS font-family names against the fonts you pass with
--font and against installed system fonts (/usr/share/fonts,
~/.fonts, ...), falling back to the PDF base-14 fonts for Latin text
when nothing matches.
Complete HTML documents are detected automatically and rendered directly — no markdown conversion — so markpdf doubles as a small HTML→PDF converter for the HTML subset litehtml supports.
Links pointing at http(s):// or mailto: URIs become clickable PDF
link annotations, and internal anchors (including footnote references
and their back-links) jump to their targets. Note that the footnote
syntax produces endnotes: definitions are collected in a section at
the end of the document, not at the bottom of the referencing page.
Fenced code blocks get tartrazine syntax highlighting (the docopt
lexer included).
Example with a dark base16 theme, page numbers and a header:
markpdf notes.md -o notes.pdf -t "0x96f" --header "notes" --footer "%p / %t"Building markpdf from source requires libharu (pacman -S libharu,
apt install libharu-dev, ...) and a C++ toolchain: run make -C ext
once to build the shim, then shards build. See
BUILDING.md for all build modes — including the GPL-3
math build and the license-clean alternative — plus tests and static
release binaries.
bin/markpdf-web (built alongside the other binaries) serves a small
Kemal site that doubles as markpdf's playground
and landing page: visitors edit markdown, tweak the styling knobs
(style, themes, page size, margins, headers, pageless, custom CSS),
and see the PDF update live.
bin/markpdf-web # listens on :3000
PORT=8080 bin/markpdf-web # or wherever you likeDocuments live in the visitor's browser (localStorage) and in
shareable #d=... URLs. Submitted markdown is uploaded to the server,
rendered in memory, and streamed back with the response — no PDF file
is ever written (only images that need converting pass through a
private temp directory, deleted right after the render). Renders are
serialized
through a mutex because the PDF shim is not known to be thread-safe,
so it is meant for demo traffic, not for heavy use.
Limits (all env-tunable) keep the demo from being set on fire:
| Variable | Default | Meaning |
|---|---|---|
MARKPDF_WEB_MAX_MARKDOWN_KB |
512 | Largest accepted markdown body |
MARKPDF_WEB_MAX_RENDER_SECONDS |
30 | Render timeout, answers 503 |
MARKPDF_WEB_MAX_QUEUE |
8 | Waiting renders before answers turn 429 |
MARKPDF_WEB_MAX_RENDERS_PER_MINUTE |
30 | Renders per minute per client IP |
MARKPDF_WEB_FETCH_IMAGES |
off | Set to 1 to fetch remote images |
MARKPDF_MAX_IMAGE_MB |
8 | Largest remote image body fetched |
Remote image fetching is off on the demo — a markdown file must not
make the server talk to the network. Re-enable it with
MARKPDF_WEB_FETCH_IMAGES=1; when it is on, images are only fetched
from public http(s) hosts — loopback, private ranges and link-local
addresses (cloud metadata included) are refused, redirects included —
and anything bigger than the image cap is dropped. The CLI fetches by
default and can skip remote images with --no-remote-images.
Local/relative image sources resolve against an empty directory, so
server files are never reachable as "images". Temp PDFs are deleted
the moment their bytes are read, and a sweeper removes crash orphans
older than an hour every ten minutes.
Or run it from Docker (multi-stage build on alpine:edge, runtime ships DejaVu/Noto fonts and runs as an unprivileged user):
docker build -f Dockerfile.web -t markpdf-web .
docker run -p 3000:3000 markpdf-web-
Add the dependency to your
shard.yml:dependencies: markterm: github: ralsina/markterm
In your code, use it like this:
puts Markd.to_term(source)
puts Markd.to_md(source)The PDF side is a library too: Markd::Pdf::Renderer owns every
option as instance state, so instances are independent and reusable —
no global style accumulates between renders.
require "markterm/pdf"
renderer = Markd::Pdf::Renderer.new(style: "book",
header: "notes", footer: "%p / %t")
renderer.add_css(File.read("my-book.css"))
pages = renderer.render(source, "book.pdf")
# or one-shot, no instance to keep:
Markd::Pdf.render(source, "out.pdf", style: "dark")The only process-wide state is the font cache
(Markd::Pdf.register_font, Markd::Pdf.emoji_font=): parsed fonts
are cached for the life of the process because font metadata parsing
is expensive, and duplicate registrations are ignored.
Renderers are not thread-safe: litehtml and libharu make no thread-safety claims either. If several threads need to render, serialize the renders or use one process per worker.
- Fork it (https://github.com/ralsina/markterm/fork)
- Create your feature branch (
git checkout -b my-new-feature) - Commit your changes (
git commit -am 'Add some feature') - Push to the branch (
git push origin my-new-feature) - Create a new Pull Request
- Roberto Alsina - creator and maintainer

