glu
Callbacks

Callbacks

glu provides a callback system that covers the full document lifecycle — from initialization through page-level and element-level events to final cleanup. Callbacks are Lua functions that run at specific points during PDF generation.

Registering a callback

Use frontend.add_callback() to register a named callback for an event:

local frontend = require("glu.frontend")

-- Lifecycle: runs before the aux file is loaded
frontend.add_callback("document_start", "my_init", function()
    print("Starting document processing")
end)

-- Page-level: runs when a new page is created (for backgrounds)
frontend.add_callback("page_init", "page_bg", function(doc, page, pagenum, pageinfo)
    -- draw a background, watermark, etc.
end)

-- Page-level: runs before each page is shipped out (for overlays)
frontend.add_callback("pre_shipout", "page_frame", function(doc, page, pagenum, pageinfo)
    -- draw a frame, add a page number, etc.
end)

-- Element-level: runs after each block element is built
frontend.add_callback("post_element", "heading_style", function(element, doc)
    -- inspect or decorate the element
end)

-- Lifecycle: runs after the PDF has been written
frontend.add_callback("document_end", "my_cleanup", function()
    print("PDF finished")
end)

Each callback has a name (here "page_frame") that identifies it. Names must be unique per event — registering a callback with the same name replaces the previous one.

Events

Lifecycle events

Event When it fires Arguments
document_start After companion Lua, before aux file is loaded (none)
content_ready After aux file is loaded, before content processing (none)
document_end After the PDF file has been written (none)

Processing events

Event When it fires Arguments
page_init After a new page is created doc, page, pagenum, pageinfo
pre_shipout Before a page is written to the PDF doc, page, pagenum, pageinfo
post_element After a block element’s VList is built element, doc

document_start

Fires once at the very beginning, after the companion Lua file has been loaded but before the auxiliary file is read. This is the earliest point where user code can run. The _aux table is not yet available.

Use this for initialization tasks that must happen before any data from previous runs is loaded.

frontend.add_callback("document_start", "init", function()
    print("Starting at " .. os.date())
end)

content_ready

Fires once after the auxiliary file has been loaded and the _aux / _toc globals are set. This is the last callback before content processing begins (Lua blocks, Markdown conversion, or HTML parsing).

Use this to inspect or modify _aux data before it influences the document, or to set up state that depends on cross-run data like the page count or table of contents.

frontend.add_callback("content_ready", "setup", function()
    if _aux._pages and _aux._pages > 0 then
        print("Previous run had " .. _aux._pages .. " pages")
    end
end)

document_end

Fires once after the PDF file has been written and closed. This is the last callback in the lifecycle.

Use this for cleanup, logging, or writing custom data to _aux that should persist to the next run.

frontend.add_callback("document_end", "done", function()
    _aux.last_build = os.date()
    print("PDF written successfully")
end)

Any modifications to _aux made in this callback are included when the auxiliary file is saved.

page_init

Fires once for every page, immediately after it is created. The page is empty at this point — content will be placed on it afterwards. This is the ideal place for backgrounds, watermarks, or any decoration that should appear behind the page content (PDF rendering order: first drawn = furthest back).

Arguments

The arguments are the same as for pre_shipout:

Argument Type Description
doc Document The current document
page Page The newly created page
pagenum number The page number (1-based)
pageinfo PageInfo Page layout and CSS information (see PageInfo below)

pre_shipout

Fires once for every page, just before it is written to the PDF file. The page content has already been placed. This is the ideal place for overlays like page numbers, frames, or headers that should appear on top of the content.

Arguments

Argument Type Description
doc Document The current document (same as from frontend.new())
page Page The page about to be shipped out
pagenum number The page number (1-based)
pageinfo PageInfo Page layout and CSS information (see below)

The page object provides page.width and page.height as dimensions. Use page.width.pt to get the numeric value in points (for calculations or SVG generation).

Place content on the page with:

page:output_at(x, y, content)

PageInfo object

The pageinfo parameter provides access to the current page dimensions and CSS page areas. All dimension values are ScaledPoints (use .pt for the numeric value in points).

Property Type Description
margin_top ScaledPoint Top margin from the CSS @page rule
margin_bottom ScaledPoint Bottom margin
margin_left ScaledPoint Left margin
margin_right ScaledPoint Right margin
content_width ScaledPoint Available width for content (page width minus margins, borders, padding)
content_height ScaledPoint Available height for content
page_area_left ScaledPoint X position where the content area starts
page_area_top ScaledPoint Y position where the content area starts
page_areas table or nil CSS page margin boxes (see below)

The page_areas table contains entries from the CSS @page rule for margin boxes like @top-center or @bottom-right. Each entry is a table of CSS properties:

local areas = pageinfo.page_areas
if areas and areas["@top-center"] then
    local content = areas["@top-center"].content
    -- use the content value ...
end

post_element

Fires once for every block-level HTML element (<h1>, <p>, <ul>, <table>, etc.) after its vertical list (VList) has been built.

This callback runs during the VList building phase — before any pages are created. It is useful for:

  • Collecting data — e.g. headings for a table of contents
  • Adding visual decorations — e.g. SVG backgrounds or underlines behind specific elements
  • Inspecting elements — e.g. logging element sizes for debugging

Arguments

Argument Type Description
element ElementInfo Information about the processed block element
doc Document The current document (for creating SVG nodes etc.)

ElementInfo object

Properties (read-only):

Property Type Description
tag_name string The HTML tag name (e.g. "h1", "p", "ul")
text_content string The concatenated text content of the element
width ScaledPoint Width of the element’s VList
height ScaledPoint Height of the element’s VList
depth ScaledPoint Depth of the element’s VList

Methods:

element:set_background(node)

Places an SVGNode or VList behind the element content. The background is drawn first in the PDF content stream, so the element text appears on top.

element:set_background(svgnode)   -- SVGNode from doc:create_svg_node()
element:set_background(vlist)     -- VList from doc:format_paragraph()

Timing

post_element fires during CreateVlist(), which runs before OutputPages(). This means:

  1. All element callbacks complete before the first page is created. Data collected in post_element (like a heading list) is fully available when pre_shipout fires.
  2. Modifications via set_background() are included in the page layout. The background nodes become part of the element’s VList before pages are broken.
Lua companion file loaded
    ↓
document_start          ← before aux file is read
    ↓
Read aux file → _aux/_toc globals set
    ↓
content_ready           ← aux loaded, before processing
    ↓
CreateVlist() ← post_element fires here for each block element
    ↓
OutputPages()
    ├─ new page    ← page_init fires here
    ├─ place content
    ├─ shipout     ← pre_shipout fires here
    └─ repeat for each page
    ↓
PDF written
    ↓
document_end            ← after PDF is closed
    ↓
Write aux file (includes _aux modifications)

Callback ordering

By default, callbacks run in the order they were registered. The optional fourth argument to add_callback controls positioning:

-- Append (default)
frontend.add_callback("pre_shipout", "background", fn)
frontend.add_callback("pre_shipout", "background", fn, "back")

-- Prepend
frontend.add_callback("pre_shipout", "watermark", fn, "front")

-- Relative positioning
frontend.add_callback("pre_shipout", "border", fn, { after = "background" })
frontend.add_callback("pre_shipout", "header", fn, { before = "footer" })
Position Description
"back" (default) Append to the end of the callback list
"front" Insert at the beginning
{after = "name"} Insert after the named callback
{before = "name"} Insert before the named callback

If the referenced callback name does not exist, before/after falls back to appending.

Removing and listing callbacks

Remove a callback by name:

frontend.remove_callback("pre_shipout", "page_frame")

List all registered callback names for an event (in execution order):

local names = frontend.list_callbacks("pre_shipout")
for _, name in ipairs(names) do
    print(name)
end

Where to register callbacks

In Markdown mode, callbacks are typically registered in the companion Lua file. For example, if your document is report.md, put callback registrations in report.lua:

report.md      ← your document
report.lua     ← callback registrations (loaded automatically)

In Lua script mode, register callbacks before creating pages.

Example: page background (page_init)

This example draws a light gray background in the content area using page_init. Because it fires before any content is placed, the background appears behind the text:

local frontend = require("glu.frontend")

frontend.add_callback("page_init", "page_bg", function(doc, page, pagenum, pageinfo)
    local w = page.width.pt
    local h = page.height.pt
    local mt = pageinfo.margin_top.pt
    local mb = pageinfo.margin_bottom.pt
    local ml = pageinfo.margin_left.pt
    local mr = pageinfo.margin_right.pt

    local svg = string.format(
        '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 %g %g">' ..
        '<rect x="%g" y="%g" width="%g" height="%g" fill="#f5f5f5"/>' ..
        '</svg>',
        w, h, ml, mt, w - ml - mr, h - mt - mb
    )

    local svgdoc = frontend.parse_svg_string(svg)
    local svgnode = doc:create_svg_node(svgdoc, page.width, page.height)
    page:output_at(0, page.height, svgnode)
end)

Example: page frame (pre_shipout)

This example draws a decorative blue frame at the exact page margins using pre_shipout. Because it fires after content is placed, the frame appears on top:

local frontend = require("glu.frontend")

frontend.add_callback("pre_shipout", "page_frame", function(doc, page, pagenum, pageinfo)
    local w = page.width.pt
    local h = page.height.pt
    local mt = pageinfo.margin_top.pt
    local mb = pageinfo.margin_bottom.pt
    local ml = pageinfo.margin_left.pt
    local mr = pageinfo.margin_right.pt

    local svg = string.format(
        '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 %g %g">' ..
        '<rect x="%g" y="%g" width="%g" height="%g" ' ..
        'fill="none" stroke="#336699" stroke-width="1.5" rx="4" ry="4"/>' ..
        '</svg>',
        w, h, ml, mt, w - ml - mr, h - mt - mb
    )

    local svgdoc = frontend.parse_svg_string(svg)
    local svgnode = doc:create_svg_node(svgdoc, page.width, page.height)
    page:output_at(0, page.height, svgnode)
end)

Both callbacks use pageinfo to read the actual CSS margins, so decorations adapt automatically when the page layout changes.

Example: page numbers

local frontend = require("glu.frontend")

frontend.add_callback("pre_shipout", "page_number", function(doc, page, pagenum, pageinfo)
    local ff = doc:load_font_family("serif")
    local txt = frontend.text({
        font_family = ff,
        font_size = "9pt",
        color = "#666666",
    })
    txt:append(tostring(pagenum))

    local vl = doc:format_paragraph(txt, "50pt", { alignment = "center" })
    local x = (page.width - frontend.sp("50pt")) / 2
    page:output_at(x, frontend.sp("1.5cm"), vl)
end)

Example: collecting headings

This example collects all <h1> and <h2> headings during document processing. The toc table is fully populated before any pre_shipout callback runs, so it can be used for rendering a table of contents.

local frontend = require("glu.frontend")

local toc = {}

frontend.add_callback("post_element", "toc_collect", function(element)
    if element.tag_name == "h1" or element.tag_name == "h2" then
        toc[#toc + 1] = {
            level = element.tag_name,
            text = element.text_content,
        }
    end
end)

Example: SVG background behind headings

This example places a light blue background with a bottom line behind every <h1> heading. It uses doc:create_svg_node() to render the SVG and element:set_background() to insert it behind the heading text.

local frontend = require("glu.frontend")

frontend.add_callback("post_element", "h1_background", function(element, doc)
    if element.tag_name ~= "h1" then return end

    local w = element.width.pt
    local h = element.height.pt

    local svg = string.format(
        '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 %g %g">' ..
        '<rect x="0" y="0" width="%g" height="%g" fill="#dbe9f6" rx="4" ry="4"/>' ..
        '<line x1="0" y1="%g" x2="%g" y2="%g" stroke="#336699" stroke-width="2"/>' ..
        '</svg>',
        w, h, w, h, h, w, h
    )

    local svgdoc = frontend.parse_svg_string(svg)
    local svgnode = doc:create_svg_node(svgdoc, element.width, element.height)
    element:set_background(svgnode)
end)

The SVG viewBox matches the element dimensions (in points), so the background covers the full heading area. The set_background() call inserts the rendered SVG behind the heading text using a negative-kern overlay technique.

A complete working example combining all callbacks is available in glu/playground/example.lua.