Markdown mode
glu can convert Markdown files directly to PDF. Instead of writing Lua scripts, you write Markdown and glu handles the typesetting automatically.
glu document.mdThis produces document.pdf. glu detects the mode by the file extension: .lua for Lua scripts, .md for Markdown.
Pipeline
The Markdown-to-PDF pipeline has several stages:
Each stage is optional — a plain Markdown file without any Lua or frontmatter works fine.
YAML frontmatter
A YAML block at the beginning of the file sets document metadata:
---
title: My Document
author: Jane Doe
css: custom.css
papersize: a4
format: PDF/UA
lang: en
---| Field | Description |
|---|---|
title |
PDF document title |
author |
PDF author field |
css |
Additional CSS file to load |
papersize |
Page size (a4, letter, 20cm 20cm, etc.) |
format |
PDF conformance level: PDF/UA / PDF/UA-1 (tagged PDF, ISO 14289-1) or PDF/UA-2 (PDF 2.0, ISO 14289-2) |
lang |
Document language (e.g. de, en) |
highlight-style |
Syntax highlighting style (default: github) |
All frontmatter fields (including custom ones) are available from Lua as the _frontmatter global table:
-- Access standard fields
local title = _frontmatter.title
-- Access custom fields
local version = _frontmatter.versionThis means you can add arbitrary key-value pairs to the frontmatter and read them from Lua blocks or the companion Lua file.
Companion Lua file
If a Lua file with the same base name exists (e.g., report.lua for report.md), it is loaded automatically before the Markdown is processed. This is the place for function definitions, variable initialization, and callback registration.
report.md ← your document
report.lua ← loaded automatically (if present)Example report.lua:
local frontend = require("glu.frontend")
function currency(amount)
return string.format("%.2f €", amount)
end
total = 42 * 3The functions and variables are available in the Markdown file via Lua blocks and inline expressions.
Lua blocks
Fenced code blocks with {lua} are executed and removed from the output:
```{lua}
result = math.sqrt(144)
```Lua blocks run in the same Lua state as the companion file. They execute in document order, so later blocks can use variables from earlier ones.
Returning text
If a Lua block uses return, the returned string is inserted into the Markdown source at the position of the block:
```{lua}
return '<div class="note">'
```
This paragraph is wrapped in a div.
```{lua}
return '</div>'
```This is useful for injecting HTML tags, dynamic content, or computed Markdown.
Recording and processing text
Two Lua blocks can work together to capture and transform a section of text. startrecording() begins capturing all text between Lua blocks; stoprecording() stops capturing and returns the captured text as a Lua string:
```{lua}
startrecording()
```
This text will be captured and processed.
```{lua}
local content = stoprecording()
return string.upper(content)
```The result: “THIS TEXT WILL BE CAPTURED AND PROCESSED.” — the Markdown between the two blocks was captured, transformed by string.upper(), and re-inserted via return.
Recording works on the raw Markdown text level (before goldmark converts to HTML). This means:
- Markdown formatting (
**bold**,# heading) inside the recorded text is preserved and processed normally after the Lua transformation {= expr =}inline expressions inside recorded text are expanded after the Lua transformation — if the transformation alters the{= ... =}syntax, the expressions will break
| Function | Description |
|---|---|
startrecording() |
Begins capturing text. Subsequent text goes to the buffer instead of the output. |
stoprecording() |
Stops capturing and returns the captured text as a string. |
Display-only blocks
To show Lua code without executing it, use {!lua} instead of {lua}. The block is rendered with syntax highlighting but not executed:
```{!lua}
-- This code is displayed, not executed
items = { "one", "two", "three" }
return #items
```This is useful for documentation or slides where you want to show Lua examples.
Showing fenced code blocks verbatim
To display a ```{lua} block literally (including the backtick fencing), wrap it with more backticks. Use 5 backticks to protect the inner 3-backtick block from execution:
`````
```{lua}
return "This is shown as code, not executed"
```
`````The outer fence must have strictly more backticks than any inner fence. This works for any number of nesting levels — wrap with 6 backticks to show 5 backticks, and so on.
Inline expressions
Use {= expr =} to insert the result of a Lua expression:
The total is {= total =} and the square root of 144 is {= math.sqrt(144) =}.Any Lua expression that returns a value works — variables, function calls, arithmetic:
The price is {= currency(19.99) =}.Markdown features
glu uses goldmark with these extensions:
- Tables — pipe tables with alignment
- Strikethrough —
~~deleted~~ - Autolinks — URLs are automatically linked
- Syntax highlighting — fenced code blocks with a language tag are highlighted using chroma
Standard Markdown features: headings, bold, italic, code, lists, blockquotes, horizontal rules, and links.
Raw HTML is supported inside Markdown. This allows using CSS features like leaders for table-of-contents dot fills via inline <span> elements, or generating barcodes with <barcode> elements.
Syntax highlighting
Fenced code blocks with a language identifier are automatically syntax-highlighted:
```go
func main() {
fmt.Println("Hello")
}
```The highlight style can be set in the frontmatter:
---
highlight-style: monokai
---The default style is github. Any chroma style can be used (e.g. monokai, dracula, solarized-dark, tango).
Internal links
HTML elements with an id attribute create PDF destinations. Use <a href="#..."> to link to them:
<h2 id="introduction">Introduction</h2>
Some text...
Go back to <a href="#introduction">Introduction</a>.The id attribute works on any block-level HTML element. The href must start with # followed by the target id.
Default stylesheet
Markdown mode includes a built-in CSS stylesheet that provides sensible defaults:
- A4 page size with 2cm margins
- Serif font (CrimsonPro) for body text at 10pt
- Scaled heading sizes (h1: 24pt down to h6: 10pt italic)
- Monospace font (CamingoCode) for
codeandpreblocks - Table styling with header borders
- Blockquote indentation and italic style
The defaults can be overridden with a custom CSS file.
Custom CSS
Additional CSS can be loaded in two ways:
Via frontmatter:
---
css: style.css
---Via command line:
glu --css style.css document.mdThe custom CSS is applied after the default stylesheet, so it overrides matching rules.
Tagged PDF (PDF/UA)
Setting format: PDF/UA (or the explicit aliases PDF/UA-1 and PDF/UA-2) in the frontmatter produces a tagged, accessible PDF that conforms to the chosen PDF/UA revision:
---
title: Accessible Report
author: Jane Doe
format: PDF/UA
lang: en
---format value |
Standard | PDF version | XMP pdfuaid:part |
|---|---|---|---|
PDF/UA or PDF/UA-1 |
ISO 14289-1 | 1.7 | 1 |
PDF/UA-2 |
ISO 14289-2 | 2.0 | 2 (plus pdfuaid:rev = 2024) |
When PDF/UA is active, glu automatically:
- Creates a document structure tree with roles derived from HTML elements (P, H1–H6, Table, L/LI, Figure, etc.)
- Tags table cells as TH/TD with proper THead/TBody grouping and scope attributes
- Marks page headers and footers as pagination artifacts
- Generates PDF bookmarks from headings (under UA-2 these point to structure destinations, ISO 14289-2 §8.8)
- Sets the document language from the
langfield
Under PDF/UA-2 the structure tree additionally uses the HTML5 namespace for HTML-equivalent roles (lowercase p, h1–h6, figure, table, …) with a RoleMapNS mapping each one to the corresponding PDF 2.0 Standard Structure Namespace role. Document-level and PDF-specific roles without HTML5 equivalents (Document, Note, LBody, Lbl, L) stay in the PDF 2.0 SSN.
No additional Lua code or CSS is needed — the Markdown structure maps directly to the PDF structure tree.
The lang field sets the document’s default language tag (BCP 47). This is required for PDF/UA conformance and used by screen readers to select the correct pronunciation.
veraPDF is the canonical conformance checker:
verapdf --flavour ua1 report.pdf # for PDF/UA / PDF/UA-1
verapdf --flavour ua2 report.pdf # for PDF/UA-2CSS reference
You can use @page to change the page size and margins:
@page {
size: letter;
margin: 1in;
}
body {
font-size: 11pt;
line-height: 1.5;
}
h1 {
color: #333;
}Page margin boxes and counters
CSS @page rules support margin boxes for headers and footers. Inside margin boxes, use the content property with counter() to display page numbers:
@page {
size: a5;
@bottom-center {
content: "Page " counter(page) " of " counter(pages);
font-size: 9pt;
color: #666;
}
}Available counters:
| Counter | Description |
|---|---|
page |
Current page number (1-based) |
pages |
Total number of pages |
The content property can mix literal strings and counter values freely: content: counter(page) " / " counter(pages);
Images can be placed in margin boxes using url():
@page {
@top-right {
content: url("logo.pdf");
}
}The image is scaled proportionally to fit the margin box height and aligned according to the box position (left, center, or right).
Use @page :first to override margin boxes on the first page:
@page :first {
@top-right {
content: ""; /* no logo on first page */
}
}Available margin boxes: top-left-corner, top-left, top-center, top-right, top-right-corner, bottom-left-corner, bottom-left, bottom-center, bottom-right, bottom-right-corner.
Auxiliary file
Forward references like counter(pages) (the total page count) and the table of contents cannot be resolved on the first typesetting pass — they depend on values that aren’t known until the document is fully laid out. glu solves this with an auxiliary file and an automatic re-run loop:
For a source file document.md (or --output FILE if set), glu writes <stem>-aux.json — a formatted JSON file next to the PDF.
- Pass 1 — glu typesets the document.
counter(pages)shows0because no previous data exists. After the pass, glu writes the actual page count and collected headings to the aux file. - Pass 2 — glu reads the new aux file, starts with a fresh Lua state, and typesets again. The forward references now resolve to real values.
- Convergence — when a pass writes an aux file that is byte-identical to what it read, glu stops.
The loop is bounded by --max-passes (default 3). If the aux file oscillates between two different states across passes — typically a sign of non-deterministic Lua code — glu aborts the loop early with exit code 6 instead of wasting more passes. --max-passes=1 disables auto-rerun entirely.
Each pass runs with a fresh Lua state: companion .lua is re-loaded, {lua} blocks are re-run, lifecycle callbacks fire again. This is deliberate — accumulating state across passes would compound side effects (counters, registered callbacks, file writes).
Use --clean to delete the aux file before pass 1:
glu --clean document.mdCross-references
The two-pass loop is what makes CSS target-counter(attr(href), page)
and target-text(attr(href)) resolve to real page numbers in
Markdown documents. The first pass renders the cross-references as
? and collects {id → {page, text}} for every anchor; the second
pass reads the aux entry back and emits final values. See the
cross-references reference for the CSS
shape, and these two example folders for end-to-end Markdown setups:
- toc-target-counter — table of contents with dotted leaders and resolved page numbers.
- cross-reference-inline — inline
<span id>anchors plustarget-textfor “see Chapter 3” references.
JSON format
The aux file uses _-prefixed keys for system data. User data can use any key without underscore prefix:
{
"_headings": [
{ "level": "h1", "text": "Introduction", "page": 1 },
{ "level": "h2", "text": "Summary", "page": 3 }
],
"_pages": 3,
"my_index": ["Alpha", "Beta"]
}| Key | Type | Description |
|---|---|---|
_pages |
number | Total page count (set by glu after each run) |
_headings |
array | Heading entries with level, text, and page |
| user keys | any | Custom data stored by Lua code |
Accessing aux data from Lua
The aux file is exposed as the _aux global table in Lua. It is readable and writable — any changes are saved back to the JSON file after the run.
-- Read system data from previous run
if _aux._pages then
print("Last run: " .. _aux._pages .. " pages")
end
-- Read headings (also available as _toc for backward compatibility)
if _toc then
for _, h in ipairs(_toc) do
print(h.level .. ": " .. h.text .. " (page " .. h.page .. ")")
end
end
-- Store custom data that persists across runs
_aux.build_count = (_aux.build_count or 0) + 1
_aux.my_keywords = { "typesetting", "PDF", "Lua" }The _toc global is an alias for _aux._headings and remains available for backward compatibility.
System keys (with _ prefix) are updated by glu after each run. You can read and even overwrite them, but glu will set _pages and _headings to the actual values at the end of the run. To store persistent custom data, use keys without underscore prefix.
The lifecycle callbacks document_start, content_ready, and document_end provide precise control over when _aux is read or modified. Modifications made in document_end are included when the aux file is written.
Go templates
The --template flag enables Go template expansion before Markdown processing:
glu --template document.mdThis processes the file through Go’s text/template engine first, allowing template directives like {{.Variable}} and {{range}} loops.
Debug modes
Two debug flags let you inspect intermediate stages of the pipeline:
--markdown — prints the expanded Markdown (after Lua blocks and inline expressions, before goldmark) to stdout:
glu --markdown document.md--html — prints the generated HTML (after goldmark conversion, before PDF rendering) to stdout:
glu --html document.mdBoth flags suppress PDF generation and write to stdout instead. Use them together with shell redirection to save the output:
glu --html document.md > output.htmlComplete example
report.md:
---
title: Monthly Report
author: Jane Doe
---
# Monthly Report
This report covers the period from January to March.
| Month | Revenue | Expenses |
|---------|----------|----------|
| January | {= currency(15420) =} | {= currency(12300) =} |
| February| {= currency(18900) =} | {= currency(14100) =} |
| March | {= currency(21050) =} | {= currency(15800) =} |
**Total revenue:** {= currency(15420 + 18900 + 21050) =}
---
> Report generated with glu.report.lua:
function currency(amount)
return string.format("%.2f €", amount)
endRun:
glu report.md # → report.pdfCommand line reference
See Using glu for the full list of options and subcommands. The flags that matter most for Markdown mode are:
| Flag | Purpose |
|---|---|
--template |
Apply Go text/template expansion before frontmatter extraction. |
--css FILE |
Load an additional CSS file after the default stylesheet. |
--markdown |
Print expanded Markdown to stdout (debug, no PDF). |
--html |
Print generated HTML to stdout (debug, no PDF). |
--clean |
Delete the aux file before the first pass. |
--max-passes N |
Cap the auto-rerun loop (default 3). |