What the Heck Are Lua Filters?

Remember how I bamboozled Excel users with my cool PDF automation?

If you want to pull that off too, check out my freshly released "Report Automation With Typst" course.

Servus! 👋 

Many Greetings from Munich, Germany. Today we are learning something that has been bamboozling me for years. Whenever I wanted to use Quarto together with Typst, things became confusing pretty fast.

Not because the Typst/Quarto integration is particularly bad. But more because not all the things I know from Quarto or HTML output are directly translatable to Typst. Instead you have to pass your content through Lua filters to code your translation yourself.

That’s my main reason why I’m using Typst separately when all I need is PDF output. For the same reason, my PDF Automation course is designed for this scenario. This means doing data work in R or Python and then integrating the crunched numbers via Typst directly.

But in my freelance work I often end up building branded Quarto templates. That’s because my clients need both HTML and PDF output using their company branding. And as I come back to revisit the Lua basics so often, I figured that you might like to share in that experience. So today, let’s look at that.

A simple template

In the beginning there was light a Quarto file. Let’s have it look like this:

Notice that I’ve already added a filters attribute in the YAML header. It currently links to a new text file that I’ve called filter.lua. That’s where today’s juicy things will happen. But for now it’s empty.

And if you render this bad boi .. (the Quarto file I mean .. not the filter .. You can’t render the filter .. That’s ridiculous) .. Then you’ll get a beautiful HTML output like this:

But when the world I needed them most (i.e. in the Typst output), the columns vanished.

Thankfully, less than 100 years passed, until I discovered that that’s exactly what Lua filters are supposed to fix.

The idea of Lua filters

Cool. We’ve got our setup. Let’s talk Lua filters. These are not a Quarto invention.

You see, behind the scenes, Quarto sends your content through a tool called Pandoc. And in order to understand what’s going on we have to more appropriately talk about the whole chain of steps that Quarto takes. Here’s how it goes.

It all starts with a Quarto file. This one can contain text and code chunks.

As a first step, Quarto translates everything into a plain Markdown format. This produces an .md file and it needs an “engine” that executes your code and then merges the outputs into elements Markdown can cope with. Typically, that engine is knitr or jupyter.

Then comes Pandoc. This tool produces a so-called Abstract Syntax Tree (AST) out of the Markdown content. Now your content is broken down into “leaves” and “nodes”. For example, consider this simple Markdown file:

Pandoc, would represent it like so:

And now it begins. Now all Lua filters are being applied on the elements of that AST. This means mostly the Lua filters that Quarto ships to implement its Quarto features. But as you well know, we can throw our own filters on top of that.

Once all filters were applied, a “Pandoc writer” is called for the target format you want. For our example, this means the Pandoc writer for Typst is called and that produces a .typ file.

Finally, Typst itself is called to give us our sweet, sweet PDF.

A simple Lua filter

What a ride! That’s a lot of steps. And it also explains why Lua filters bamboozle me a lot. After all, they have barely anything to do with Markdown syntax I’m familiar with. Dealing with Lua filters means dealing with Abstract Syntax Trees.

So let’s create a simple Lua filter. One that just inspects something. We don’t want to get too crazy too fast.

To create a filter you just define a Lua function named after the AST node you want to work on. Remember, how my earlier AST sketch showed a “Header” node? That’s actually one we could use. Let’s go with this filter here:

This is a function called “Header” (which hence targets header elements). It’s standard to name the function argument el as in “elements”. Here, all we do is to call Quarto’s log function to throw the header elements into the render logs.

These can then be seen in the logs that Quarto produces. What I usually do in Positron when I create Lua filters is to arrange viewer, terminal and Lua file on a grid so that I can see all of that together:

And from the log output, I can then usually better understand the structure of the element I’m dealing with. Here’s the actual output for our headline.

As you can see, the header element has a bunch of fields like level, attr or content. And by changing those, we can modify the output that lands in our output.

But that’s a story for next time. For now, I’ll let you digest all the information I’ve thrown at you. Hope you enjoyed this week’s newsletter. As always, feel free to hit the reply button to share your thoughts.

Happy Coding and see you next week,
Albert

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.