Skip to content

Latest commit

 

History

271 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

MARKTerm

CI

MARKTerm is a suite of tools to render Markdown anywhere, built on top of Markd:

  • markterm renders Markdown to the terminal, with themes, images, and HTML-style links. Inspired by Glow.
  • markpdf renders Markdown to small, fast, self-contained PDF files.
  • markmark renders Markdown to Markdown, for normalizing or filtering documents.

It can also be used as a Crystal library.

Features

  • 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

markterm on a light terminal markterm on a dark terminal

TODO

Done recently (markpdf):

  • ✅ Built-in stylesheets (default, book, dark, sepia) with --style, --print-style and 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|right sections 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-pager opts 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

Upstreaming

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.yml pins 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

Usage as a program

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.

Configuration

The markterm, markmark and markpdf CLIs share a configuration mechanism (via docopt-config). Every option can be set in four places, in decreasing precedence:

  1. the command line (e.g. --width 60)
  2. environment variables prefixed with the tool's name (e.g. MARKTERM_WIDTH=60, MARKPDF_STYLE=book)
  3. a YAML config file: ~/.config/markterm/config.yml, ~/.config/markmark/config.yml or ~/.config/markpdf/config.yml (honoring $XDG_CONFIG_HOME)
  4. 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.ttf

Environment 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: en

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

markpdf

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.

Manual page breaks

Force a new page anywhere with an empty marker div:

...end of chapter one

<div style="page-break-before: always"></div>

# Chapter two

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

Table of contents

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

Styles

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

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

Math

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 build

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

markpdf-web

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 like

Documents 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

Usage as a library

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

Contributing

  1. Fork it (https://github.com/ralsina/markterm/fork)
  2. Create your feature branch (git checkout -b my-new-feature)
  3. Commit your changes (git commit -am 'Add some feature')
  4. Push to the branch (git push origin my-new-feature)
  5. Create a new Pull Request

Contributors

About

No description, website, or topics provided.

Resources

Stars

37 stars

Watchers

2 watching

Forks

Releases

Packages

Contributors

Languages