crystal-asciidoctor-pdf
= crystal-asciidoctor-pdf
An AsciiDoc-to-PDF converter for Crystal, built on crystal-asciidoctor for parsing and pdf for PDF generation.
NOTE: A French version of this README is available — see link:README.fr.adoc[README.fr.adoc].
== Versions
[cols="2,2,2"] |=== | | Version | Date
| Ruby gem asciidoctor-pdf (port source) | 2.3.24 | 2025-11-15
| Ruby gem asciidoctor (parser, via crystal-asciidoctor) | 2.0.26 | 2025-10-24
| Crystal shard crystal-asciidoctor-pdf | 2.3.24 | 2026-04-10 |===
NOTE: This shard is a port of the Ruby gem asciidoctor-pdf 2.3.24 and relies on crystal-asciidoctor, which itself is ported from asciidoctor 2.0.26. Check versions on RubyGems : https://rubygems.org/gems/asciidoctor-pdf/versions[asciidoctor-pdf] | https://rubygems.org/gems/asciidoctor/versions[asciidoctor].
== Features
=== AsciiDoc conversion
- Paragraphs, sections (levels 1–6), lists (ordered, unordered, description)
- Tables with headers, footers and alternating row colours
- Code blocks with syntax highlighting (Crystal, Ruby, Python, JavaScript, TypeScript, Go, Java, C, C++, Bash, SQL)
- Admonitions (NOTE, TIP, WARNING, CAUTION, IMPORTANT)
- Quotes, verses, sidebars, examples
- Images (JPEG, PNG with transparency)
- Title page, configurable headers and footers
- Table of contents (TOC)
- Multi-column alphabetical index
- Footnotes
=== PDF features
- Bookmarks : document sections automatically produce a bookmark tree in the PDF navigation pane
- Clickable links : URLs in the document produce clickable PDF annotations
- Metadata : title, author and producer recorded in the PDF properties
- Exact text measurement : uses Type1/TrueType font metrics (no approximation)
=== Custom fonts
The converter supports TrueType/OpenType fonts through the theme system. If font paths are set in the theme, they are automatically loaded, embedded and subset into the PDF.
[source,yaml]
Sample theme with TTF fonts
base_font_path: "fonts/OpenSans-Regular.ttf" base_font_bold_path: "fonts/OpenSans-Bold.ttf" base_font_italic_path: "fonts/OpenSans-Italic.ttf" mono_font_path: "fonts/JetBrainsMono-Regular.ttf"
Without TTF fonts, the PDF standard fonts (Helvetica, Courier) are used.
=== Themes
PDF rendering is fully customisable through a YAML file (~60 properties) : font sizes and colours, margins, admonition colours, table styling, headers/footers, etc.
[source,crystal]
theme = AsciidoctorPDF::ThemeLoader.load("my-theme.yml") converter = AsciidoctorPDF::Converter.new("pdf", theme)
=== Title page — behaviour and options
By default the shard renders a dedicated title page (title, subtitle, author, date) without header or footer, followed by the body on the next page.
Three knobs control this behaviour :
-
:title-page:(document attribute) — forces a dedicated title page even if the theme disables it. Standard AsciiDoc. -
:title-page: false(document attribute) — disables the title page. The doctitle is then rendered as a large heading (28 pt) at the top of the first content page, preceded by the:title-logo-image:logo if defined, followed by an "author · date" caption in grey, a separator, and the preamble. Functional equivalent of the standard:!title-page:(which is not detectable on the crystal-asciidoctor side — the negation leaves no trace readable viaattr?/attr). -
theme.title_page_enabled: false(YAML theme) — default with no title page ; document attributes can still reactivate it for a specific document.
==== Title page logo — :title-logo-image:
Standard AsciiDoc / Ruby asciidoctor-pdf 2.3 :
[source,asciidoc]
:title-logo-image: image::path/to/logo.svg[align=center, pdfwidth=200]
Accepted formats (identical to upstream) :
image::PATH[OPTS](canonical form) —OPTSis comma-separated :align=left|center|right,pdfwidth=<points>,width=<points>(alias),alt=...(positional descriptor).- Bare
PATH(withoutimage::or[]) — defaults : centred, 200 pt wide.
Both SVG and bitmap (PNG/JPEG) are supported. For SVG, the shard parses the viewBox to preserve the aspect ratio.
CAUTION: do not insert an image::PATH[] macro above the = Doctitle line. That demotes the doctitle to a level-0 section ("part" in AsciiDoc terminology) — the title then appears typeset at heading size rather than cover size, and the :title-*: attributes no longer apply correctly. Always use :title-logo-image: for cover / header logos.
=== Recipes — document templates
==== Quiz / short sheet with a compact title page (logo + title + outline)
A single title page that combines logo, title, integrated outline, author, date. Ideal for quizzes and short sheets (1–2 content pages).
[source,asciidoc]
= Quiz : Remote work charter :author: Philippe Nénert :revdate: April 29, 2026 :toc: :title-logo-image: image::assets/logo.svg[align=center, pdfwidth=150] :x-title-page-toc:
Quiz preamble.
== Question 1 — On which values ?
- A. …
==== Short document without a title page — header banner
No dedicated title page. The doctitle, logo and author/date caption form a header banner on page 1, followed directly by the content.
[source,asciidoc]
= Quiz : Remote work charter :author: Philippe Nénert :revdate: April 29, 2026 :title-page: false :title-logo-image: image::assets/logo.svg[align=center, pdfwidth=150]
== Identity
Section content…
==== Long document with title page + separate TOC (default)
Default behaviour. Page 1 is the title page (centred-bottom title, author, date), page 2 is the table of contents, content starts on page 3.
[source,asciidoc]
= ISO 27001 procedures manual :author: ALOLI sas :revdate: April 29, 2026 :toc:
== Introduction
…
==== Keep a group of elements pinned at pagination
See <<unbreakable, [%unbreakable]>>.
=== Keep a group of elements together — [%unbreakable]
To prevent a paragraph from being orphaned from its table (or any group from being split across a page break), wrap the elements in an open block (--) and add the [%unbreakable] option :
[source,asciidoc]
[%unbreakable]
[options="header"] |=== | # | Equipment | Employee | Employer | 1 | Laptop computer | | ✓ | 2 | Internet connection | ✓ | |===
Status : correct answer.
Behaviour : before rendering the block, the shard estimates its total height (paragraphs + tables + lists + listings + admonitions). If the remaining space on the current page is insufficient but the block fits on a fresh page, we jump to the next page before starting. If the block exceeds one page in height, we accept normal pagination ("if possible" from the standard).
NOTE : the estimate is deliberately pessimistic. Better to skip too early than to leave an orphan.
=== Built-in themes — call by name
The shard ships ready-to-use themes embedded into the binary at compile time (no runtime file dependency). Selectable through three channels that behave identically :
[cols="1,3"] |=== | Channel | Syntax
| CLI | crystal-asciidoctor-pdf --theme fr my-doc.adoc
| AsciiDoc source | :pdf-theme: fr in the document header
| Crystal API | theme = AsciidoctorPDF::ThemeLoader.builtin("fr") |===
The --theme argument accepts either an embedded name or a YAML file path (via ThemeLoader.resolve).
==== Available themes
[cols="1,3"] |=== | Name (aliases) | Description
| fr (francais, french) | French : translated admonition labels (NOTE / ASTUCE / AVERTISSEMENT / ATTENTION / IMPORTANT), TOC title "Table des matières", Index title "Index". 5 score levels [.x-score-*] enabled. |===
==== Precedence order
. CLI argument --theme (when present) . Document attribute :pdf-theme: (when present) . Built-in default theme (English, standard defaults)
=== Score blocks — [.x-score-<level>] (extension)
For quiz scoring grids, evaluation sheets and rankings, the shard provides five predefined qualitative levels with configurable colours and labels. Activated via the AsciiDoc role [.x-score-<level>] on an example (====) or open (--) block :
[source,asciidoc]
[.x-score-excellent]
12/12 — Charter fully assimilated.
[.x-score-tres-bien]
10-11/12 — A few details to review.
[.x-score-bien]
7-9/12 — A re-read of some sections is recommended.
[.x-score-insuffisant]
4-6/12 — A full re-read of the charter is required.
[.x-score-a-revoir]
0-3/12 — Please re-read the charter carefully.
Rendering : coloured vertical band on the left + bold label + indented text. Same mechanic as standard admonitions, with a palette dedicated to score feedback (dark green → light green → yellow → orange → red).
==== Customisation
Colours and labels editable through the YAML theme :
[source,yaml]
x_score_excellent_color: "2e7d32" x_score_excellent_label: "EXCELLENT" x_score_tres_bien_color: "66bb6a" x_score_tres_bien_label: "TRÈS BIEN" x_score_bien_color: "fbc02d" x_score_bien_label: "BIEN" x_score_insuffisant_color: "ff9800" x_score_insuffisant_label: "INSUFFISANT" x_score_a_revoir_color: "d32f2f" x_score_a_revoir_label: "À REVOIR"
NOTE : non-standard extension (x- prefix). No equivalent in AsciiDoc / Ruby asciidoctor-pdf — see <
=== Admonition translation
Admonition labels (NOTE, TIP, WARNING, CAUTION, IMPORTANT) are in English by default, like Ruby asciidoctor-pdf. Two mechanisms can translate them, layered as a cascade (first match wins) :
==== 1. Per-document via the :<name>-caption: attributes
The AsciiDoc standard. Put in the document header :
[source,asciidoc]
:note-caption: NOTE :tip-caption: ASTUCE :warning-caption: AVERTISSEMENT :caution-caption: ATTENTION :important-caption: IMPORTANT
Ideal for translating a specific document. To translate all quizzes in a project without repeating each time, factor these lines into an included file :
[source,asciidoc]
include::../config/admonitions-fr.adoc[]
==== 2. For a whole theme via the YAML
When you want a theme-side default (and every doc loading that theme benefits without saying anything) :
[source,yaml]
admonition_note_label: NOTE admonition_tip_label: ASTUCE admonition_warning_label: AVERTISSEMENT admonition_caution_label: ATTENTION admonition_important_label: IMPORTANT
==== Resolution cascade
. Document attribute :<name>-caption: (if it differs from the default Note / Tip / Warning / Caution / Important injected by the crystal-asciidoctor parser — otherwise it is ignored as "not overridden"). . Theme property admonition_<name>_label. . Ultimate fallback : name.upcase.
[[convention-x]] ==== x- convention for non-standard extensions
Any AsciiDoc attribute prefixed x- (and any theme property prefixed x_ in YAML) is a proprietary extension to crystal-asciidoctor-pdf, with no native equivalent in standard AsciiDoc nor in Ruby asciidoctor-pdf. Use knowingly if portability of the source to other converters matters to you.
Convention inherited from X-* in HTTP/MIME — explicit, short, universally understood as "non-standard extension".
==== :x-title-page-toc: — title page with integrated outline
Activated per-document :
[source,asciidoc]
= My document :author: Author :revdate: 2026-04-29 :toc: :x-title-page-toc:
Preamble…
== Section 1
Or activated for an entire theme via theme.x_title_page_with_toc: true.
Effect : the title page no longer has the title centred at the bottom, but the title at the top, followed by a separator line, the outline (clickable TOC fed by :toc:), then the author and date at the bottom. The content starts on the next page.
Typical use cases : quizzes, job descriptions, short notes meant to fit on 1–2 pages with a compact mini-cover.
== Installation
. Add this dependency to your shard.yml file : + [source,yaml]
dependencies: crystal-asciidoctor-pdf: github: aloli-crystal/crystal-asciidoctor-pdf
. Run shards install
== Usage
[source,crystal]
require "crystal-asciidoctor-pdf"
input = <<-ADOC = My Document Author
== Introduction
A paragraph with bold and italic.
== Code
[source,crystal] ---- puts "Hello !" ---- ADOC
doc = Asciidoctor.load(input, options: {"safe" => "safe", "outfile" => "document.pdf"}) AsciidoctorPDF::Converter.new.convert(doc)
== Command line
This shard also ships a crystal-asciidoctor-pdf executable.
=== Build the binary
[source,bash]
git clone https://github.com/aloli-crystal/crystal-asciidoctor-pdf.git cd crystal-asciidoctor-pdf shards build --release
The binary is produced at bin/crystal-asciidoctor-pdf. Rather than copying it to a system directory, create a symlink in ~/bin/ (which must be on your PATH) :
[source,bash]
ln -s "$PWD/bin/crystal-asciidoctor-pdf" ~/bin/
That way every shards build --release automatically refreshes the target binary — no reinstall needed.
=== Usage
[source,bash]
crystal-asciidoctor-pdf my_document.adoc
By default the command produces a my_document.adoc.pdf file.
Available options :
[cols="1,3"] |=== | Option | Description
| -o FILE, --out-file FILE | Output PDF file (default : <file>.adoc.pdf)
| -T FILE, --theme FILE | Custom YAML theme file
| -a ATTR, --attribute ATTR | AsciiDoc attribute as name=value
| -N, --no-user-config | Ignore the XDG user configuration (see User configuration)
| --sample | Generate the AsciiDoc reference document (reference.adoc + reference.adoc.pdf)
| -v, --version | Print the version
| -h, --help | Print help |===
=== User configuration
The CLI automatically reads a personal configuration file to fix recurring preferences (default theme, systematic AsciiDoc attributes, author identity) without repeating them in every document.
==== Location (XDG Base Directory)
[source]
${XDG_CONFIG_HOME:-~/.config}/crystal-asciidoctor-pdf/ ├── config.yml ← main preferences └── themes/ ← user themes (optional) └── aloli.yml
The shard follows the https://specifications.freedesktop.org/basedir-spec/[XDG Base Directory Specification]. If $XDG_CONFIG_HOME is set, it is used ; otherwise the default ~/.config/ applies.
==== config.yml format
[source,yaml]
Default theme. Three forms accepted :
- embedded name ("fr", "english", ...)
- filename in ./themes/ ("aloli" → ./themes/aloli.yml)
- absolute path to a .yml
theme: fr
AsciiDoc attributes injected by default into every document
(equivalent to :name: value added at the top)
attributes: x-title-page-toc: "true" toc: macro toclevels: 3 sectnums: "true" pdf-page-size: A4 source-highlighter: rouge
Default author metadata (filled in unless already provided by
the document)
author: Philippe Nénert email: philippe@aloli.fr organization: ALOLI sas
==== Precedence cascade
Preferences are applied in this order, strongest to weakest :
. CLI flags (-T, -a, ...) . Attributes declared in the .adoc document (:pdf-theme:, :title-page-toc:, ...) . User configuration (config.yml) . Defaults compiled into the shard
In other words : the document can always override the user config, and the user config is always overridden by the CLI.
==== Disabling
To reproduce "vanilla" behaviour (CI, debug, tests) :
[source,bash]
crystal-asciidoctor-pdf --no-user-config my_document.adoc
=== Reference document
The --sample option produces an exhaustive AsciiDoc document and its PDF covering every AsciiDoc element (formatting, lists, tables, code, admonitions, formulas, diagrams, etc.) :
[source,bash]
crystal-asciidoctor-pdf --sample
→ reference.adoc + reference.adoc.pdf
This document serves as a conformance test to validate PDF rendering.
== Interactive PDF forms — [x-form] extension
Since v2.3.24.58 you can describe an interactive PDF form (fillable fields, checkboxes, radio buttons, dropdowns, multi-select listboxes) directly in your AsciiDoc source. The PDF renderer produces AcroForm widgets compliant with ISO 32000-1 §12.7 that are fillable in any PDF reader (Acrobat, Preview, Foxit, Firefox, Chrome).
Since v2.3.24.59 the signature field type is also supported (emits a /Sig AcroForm slot ready to receive a PAdES signature when https://github.com/aloli-crystal/pdf-signature[pdf-signature] lands).
=== Minimal example
[source,adoc]
[x-form, id=registration, action=mailto:contact@example.com] ---- fields:
- id: name type: text label: Full name required: true
- id: email type: email label: Email address required: true
- id: consent type: checkbox label: I consent to the processing of my data (GDPR) required: true submit: label: Send ----
=== Full grammar
See doc/x-form-spec.adoc for the reference spec : 11 field types, section-based layout, multi-column layout, options as a simple list or a hash {code: label}, atomic validation (regex, min/max), submit/reset buttons, etc.
=== Typical use cases
- ISO 27001 audits — self-fill conformance questionnaires with one section per Annex A control.
- Quiméo quizzes distributed as PDFs for offline use (instead of a web app).
- Registration forms or GDPR consent declarations signable locally.
A worked example is provided in examples/x-form-audit-iso27001.adoc (regenerate the PDF with crystal-asciidoctor-pdf examples/x-form-audit-iso27001.adoc).
=== x- convention
The x- prefix (like x-title-page-toc, x-score-*) marks a non-standard extension that works only with crystal-asciidoctor-pdf — not with the Ruby gem asciidoctor-pdf nor with other AsciiDoc converters.
== Development
To contribute, clone the three repositories at the same level :
[source,bash]
git clone https://github.com/aloli-crystal/crystal-asciidoctor.git git clone https://github.com/aloli-crystal/pdf.git git clone https://github.com/aloli-crystal/crystal-asciidoctor-pdf.git cd crystal-asciidoctor-pdf shards install crystal spec
Local dependencies (path: ../crystal-asciidoctor and path: ../pdf) are used for development.
== Contributing
. Fork the project ( https://github.com/aloli-crystal/crystal-asciidoctor-pdf/fork ) . Create your feature branch (git checkout -b my-new-feature) . Commit your changes (git commit -am 'Add a new feature') . Push the branch (git push origin my-new-feature) . Create a new Pull Request
== Contributors
- https://github.com/papilip[papilip] — creator and maintainer
crystal-asciidoctor-pdf
- 3
- 0
- 0
- 2
- 7
- 7 days ago
- March 13, 2026
MIT License
Mon, 08 Jun 2026 14:08:24 GMT