The Pattern Behind Every Lua Filter We Write

Guten Tag! šŸ‘‹ 

Many greetings from Munich, Germany. Last email I showed you how BIRD Investment’s fact sheet keeps its branding consistent across HTML and PDF.

Today, I want to show you a different part. This one needs a Lua filter: getting a table to sit off to one side with the surrounding paragraph text wrapping around it. You know, the way you’d see in a printed magazine layout:

In the .qmd source, that’s marked with a simple div:

::: {.wrap-table}
(table and paragraph content here)
:::

On the HTML side, that’s handled with plain CSS:

  • float the table’s wrapper right,

  • give it a fixed width,

  • let the paragraph text flow around it.

Nothing too unusual that a code agent can’t set up for you.

The filter that makes the Typst output work

On the Typst side, there’s no built-in ā€œwrap-tableā€ concept. So this is where the filter comes in. By this point of our email chain, you may recognize the pattern:

  • The filter checks whether a div is marked .wrap-table

  • The first block inside this div is treated as the thing that needs to be wrapped around. Everything else is the flowing body text.

  • Then each block is handed off to Pandoc’s own writer to get real Typst syntax back as a string

  • And then we glue both strings into a call to a Typst function, wrap-table(), that already knows how to lay a table and flowing text out around each other.

In code, the glue part of this equation looks roughly like this:

Here, obstacle_typst and body_typst are the table and the paragraph text, already converted to Typst syntax. The filter’s only real job is finding them and sticking them into that wrap-table() function. All the actual ā€œhow does a table and flowing text share a pageā€ logic lives inside plain Typst code as part of the wrap-table() function.

I won’t pretend this is entirely without gotchas:

  • Quarto wraps R or Python chunk output in its own div wrappers before your filter ever sees it, and

  • numbered tables go through their own internal scaffolding that a naive filter would just skip over.

Both of those need to be handled explicitly which is more than annoying. But as soon as you’ve seen this in action in a real example, you can deal with it (or instruct a coding agent to do that.)

But the cool thing is: It’s only this transition from Quarto to Typst that’s troublesome. Inside Typst, you have access to the vast sea of extensions. Like the meander package that makes the ā€œflowing text around an obstacleā€ part really easy:

So there you have it. Even for the seemingly complicated layout stuff, the same three step process applies.

Lua filters are only the glue (the annoying scaffolding if you will) that you have to apply so that the Quarto content is translated to Typst. This is where the real magic happens. And that’s exactly the type of thing you’ll learn in my upcoming workshop.

Best,
Albert

PS: If you haven’t grabbed a seat yet, registration for September 23 or September 25 is still open here:

Whenever you’re ready, there are three I can help you:

  1. Automate Your Data Reports: This course helps data analysts eliminate manual copy-paste reporting by automating PDF reports end-to-end, saving hours every cycle and preventing costly mistakes. (Using the lovely Typst language šŸ˜)

  2. Generate Insights in Minutes, not Hours: This comprehensive course teaches you to handle data faster, smarter, and more efficiently.

  3. Bespoke Data Science Solutions: I’ve helped clients build their own data science solutions. Whether building custom web apps, PDF reports, AI automations or teaching workshops, I’ve got you covered. You can reach out to me via this form (or simply hit reply to this email)

Reply

or to participate.