crystal-asciidoctor-pdf

Crystal shard: AsciiDoc to PDF converter, equivalent of the asciidoctor-pdf Ruby gem

= 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 via attr? / 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) — OPTS is comma-separated : align=left|center|right, pdfwidth=<points>, width=<points> (alias), alt=... (positional descriptor).
  • Bare PATH (without image:: 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

Repository

crystal-asciidoctor-pdf

Owner
Statistic
  • 3
  • 0
  • 0
  • 2
  • 7
  • 7 days ago
  • March 13, 2026
License

MIT License

Links
Synced at

Mon, 08 Jun 2026 14:08:24 GMT

Languages