Attachments and a companion-Lua pattern for ZUGFeRD invoices
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>.luanext 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.xmlSide 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.