glu
Styling with CSS

Styling with CSS

Markdown and HTML documents are styled with the same CSS. Every property and its limits are listed in the CSS reference, and the Typography chapter explains the ones for type. This page covers how stylesheets get into a document and what belongs to the page as a whole.

Default stylesheet

Both modes start from the same built-in stylesheet:

  • 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 code and pre blocks
  • Table styling with header borders
  • Blockquote indentation and italic style

Your own CSS layers on top of it and overrides matching rules.

Add your own CSS

Source Markdown HTML Relative paths resolve against
css: in the frontmatter yes no the directory of the document
--css flag yes yes the working directory
<style>, style="" raw HTML yes
<link rel="stylesheet"> raw HTML yes the directory of the document

In Markdown:

---
css: style.css
---

In HTML, the stylesheets belong to the document as usual, and the flag works the same in both:

glu --css style.css document.md
glu --css style.css page.html

Rules of the same specificity follow the usual order, and the last one wins:

  1. The default stylesheet
  2. The css: file from the frontmatter
  3. The stylesheets of the document itself, from <style> and <link>
  4. The --css file

So the flag overrides what comes with the document, @page rules included. That makes it the way to adapt a finished document without touching it, for example for print:

glu --css print.css page.html

Page size and margins

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;
}

In Markdown, the papersize field of the frontmatter is a shorthand for the page size.

Page margin boxes and counters

CSS @page rules support margin boxes for headers and footers (see also the headers and footers how-to). 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, because 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 document.html (or --output FILE if set), glu writes <stem>-aux.json, a formatted JSON file next to the PDF.

  1. Pass 1: glu typesets the document. counter(pages) shows 0 because no previous data exists. After the pass, glu writes the actual page count, the collected headings and the anchors to the aux file.
  2. Pass 2: glu runs another pass only when something the first pass read has changed: the page count if the document uses counter(pages), or an anchor that target-counter() or target-text() looked up. When user Lua ran (the companion file, a {lua} block or an inline expression), any change of the aux data counts, since that code can read anything in _aux. The pass reads the new aux file, starts with a fresh Lua state, and typesets again. The forward references now resolve to real values.
  3. Convergence: when nothing a pass read has changed, glu stops. A document without forward references is typeset once.

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: the companion .lua file 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.md

Cross-references

The two-pass loop is what makes CSS target-counter(attr(href), page), target-counters(attr(href), chapter, ".") and target-text(attr(href)) resolve to real page numbers, section numbers and titles. The first pass renders the cross-references as ? and collects {id → {page, text, counters}} 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 setups in Markdown:

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
_anchors object One entry per id, read by target-counter() and target-text()
user keys any Custom data stored by Lua code

Lua code reads and writes this data through the _aux table.

Leaders

Leaders create repeating dot patterns that fill available space, commonly used in table-of-contents entries. Use the CSS content: leader('.') property (from the CSS Generated Content for Paged Media specification) on an empty inline element:

<style>
    .toc-entry { margin-top: 2pt; margin-bottom: 2pt; }
</style>

<p class="toc-entry">
    Chapter 1<span style="content: leader('.')"></span>5
</p>
<p class="toc-entry">
    Chapter 2<span style="content: leader('.')"></span>12
</p>

This produces:

Chapter 1 . . . . . . . . . . . . 5
Chapter 2 . . . . . . . . . . . . 12

The dots are vertically aligned across lines (global grid alignment). The leader pattern can be any string: leader('.') for dots, leader('_') for underscores, and so on.

The <span> must be empty (no child content). The content property is parsed from the inline style attribute.

In Markdown, write the <span> as raw HTML.