Attachments and a companion-Lua pattern for ZUGFeRD invoices

Attachments and a companion-Lua pattern for ZUGFeRD invoices

June 2, 2026

glu’s Markdown mode now ships a generic attachments: frontmatter key for embedding arbitrary files, and a worked example showing how to build a ZUGFeRD/Factur-X compliant electronic invoice from a Markdown document using only the existing companion-Lua mechanism — without domain-specific code in glu’s core.

The attachments: frontmatter key

Embed supplementary files alongside the rendered document:

---
attachments:
  - file: spec.pdf
    name: Technical specification.pdf
    description: source for the rendered tables
  - file: data.csv
    mimetype: text/csv
---

Paths are resolved relative to the Markdown source file. Each attached file is written with /AFRelationship /Alternative, so PDF viewers and pdfdetach -list pick it up immediately.

Field Required Default
file yes
name no basename of file
description no empty
mimetype no application/octet-stream

See the Attachments section of the Markdown mode reference for details.

Companion-Lua pattern for compliance formats

Domain-specific formats like ZUGFeRD / Factur-X, XRechnung, PEPPOL or BSI TR-RESISCAN all share the same shape: a base PDF flavor (typically PDF/A-3) plus an embedded structured-data file plus a custom XMP extension schema plus an output intent. glu does not ship flavor-specific frontmatter for any of these. The pattern is:

  • Set the base format via the generic format: frontmatter key (e.g. format: PDF/A-3b)
  • Do the format-specific plumbing — XML attachment with the right name, XMP extension schema, output-intent ICC profile, data extraction for inline-expression authoring — in the auto-loaded companion Lua (<stem>.lua next to the Markdown file)

This keeps glu’s core focused on typesetting, lets each compliance format evolve at its own pace, and gives the author full control over attachment order, metadata, and validation.

The new example boxesandglue-examples/glu/markdown/zugferd-invoice/ shows the pattern end-to-end for ZUGFeRD/Factur-X. The companion Lua:

local frontend = require("glu.frontend")
local cxpath   = require("xml.cxpath")

-- 1. parse the CII XML once, expose fields as a flat `zugferd` global
--    so the Markdown body can use {= zugferd.id =} etc.
local doc = cxpath.open(script_dir .. "/invoice.xml")
doc:set_namespace("rsm", "urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100")
-- ... XPath mappings to seller/buyer/lines/totals ...
zugferd = { id = ..., currency = ..., lines = {...}, total = ..., ... }

-- 2. on first page_init, attach the XML, register the XMP extension
--    schema, load the output-intent ICC profile.
local initialized = false
frontend.add_callback("page_init", "zugferd-compliance", function(d, _, _)
    if initialized then return end
    initialized = true
    d:load_colorprofile(script_dir .. "/AdobeRGB1998.icc")
    d:attach_file{ filename=..., name="factur-x.xml", mimetype="text/xml" }
    d:add_xmp_extension{ schema="ZUGFeRD PDFA Extension Schema", ... }
end)

The Markdown body uses inline expressions for the data:

# Rechnung Nr. {= zugferd.id =}

An: {= zugferd.buyer.name =}, {= zugferd.buyer.line1 =}

**Gesamtbetrag: {= zugferd.total =} {= zugferd.currency =}**

All zugferd.* fields are kept as strings — byte-faithful to the XML to avoid float rounding drift (XML 9.9000 → float 9.8999… → reformatted 9,89 would be a compliance break), and to leave locale formatting in the author’s hands.

Verifying ZUGFeRD conformance

pdfdetach -list rechnung.pdf
# 1: factur-x.xml

exiftool -XMP-pdfaid:Part -XMP-zf:ConformanceLevel \
         -XMP-zf:DocumentFileName rechnung.pdf
# Part                : 3
# Conformance Level   : EN 16931
# Document File Name  : factur-x.xml

Side effect: <br> matches browser behaviour again

While building the ZUGFeRD example we hit an htmlbag bug where source like

<div>foo<br>
  bar</div>

rendered bar with a leading-space indent — the newline after the forced line break was surviving into inline layout as inter-word glue. Browsers, by spec, swallow that whitespace at the line break. htmlbag now does the same: the rendered output of every boxesandglue-examples/glu/ entry is byte-identical to before (none of the 36 examples relied on the previous behaviour), but new authoring with stacked <br> lines is no longer a typographic trap.

Example

The new example folder ships a working ZUGFeRD invoice setup: boxesandglue-examples/glu/markdown/zugferd-invoice.