Extending Quarto workshop @ posit::conf(2025)

Filters manipulate the AST between the parsing and the writing phase. First, understand the AST.
BlocksBlocks contain other BlocksBlocks contain InlinesStr and SpaceE.g. A Strong filter function




→


E.g. A single Inline. Node is replaced.


E.g. An array of Inline, a.k.a an Inlines. Spliced in.

Node is removed

nilNode is unchanged

Block elementsBlock elements contain other Block elementsBlock elements contain Inline elementsInline elements contain other Inline elementsA filter function is called on every instance of a particular type of node.
The input is the node itself, the output replaces the node.
Take a look at the AST diagram on the next slide.
What are some other types of Block nodes?
What are some other types of Inline nodes?
If we wrote a filter for Para, how many times would it be called?
If we wrote a filter for Str, which of the following would be affected?
Filter in the titleIntroduction in the headingLua in the link textlua-filters in the link URLquarto in the code block06:00
Exercise: 03-filters/your-turns/1-explore-ast
Other Block nodes: Header, BulletList, CodeBlock, Meta is special.
Other Inline nodes: Link, Image, Code
A filter function for Para would be called four times.
Affected by a Str filter function?
Filter in the title YesIntroduction in the heading. YesLua in the link text. Yeslua-filters in the link URL. Noquarto in the code block. NoRemove # fmt: skip comments from code cells. Discussion
Number all callouts. Discussion
Put the contents of an SVG image in a raw HTML block rather than using <img>. Discussion
Display the language on every code cell. Discussion
Collect all code chunks and display in a code appendix. Discussion
Filters are written in the programming language Lua.
A filter is a Lua file that contains one or more filter functions.
A filter function is a function whose name is a type of node.
Strong nodesA filter function that returns nil, leaves the node unchanged.
Example: 03-filters/examples/1-writing-filters
quarto.log.output(): Positron/VS Code look in Terminal, RStudio look in Background Jobs.
Strong filter function is called twice.el is an Strong object, an example of an Inline.el contains a content field which is an Inlines.pandoc.Emph() creates a Emph node another example of an Inline node.el.content gets the content field from the el object.Inline elementsWrite a filter, replace-emph.lua, that turns all italic text to underlined text.
Add the Strong filter function from replace-strong.lua to replace-emph.lua. What happens?
Other challenges:
Write a filter that removes all bold and italic formatting, leaving just the text.
Write a filter that converts all double quotes to single quotes.
10:00
Exercise: 03-filters/your-turns/2-write-a-filter
A filter on an Inline must return either:
nil, node is unchanged, e.g. no-change.luaInline which replaces the original, e.g. replace-strong.luaInline (known as an Inlines) which replaces the original, spliced into its siblings.Inlines with three elementsI really really like bold and really like italics, and really really really can’t decide which to use.
Example: 03-filters/examples/2-return-types
I like bold and really like italics, and really can’t decide which to use.
Example: 03-filters/examples/2-return-types
InlinesThis won’t work because el.content is an Inlines object:
See a useful pattern in the next section.
Example: 03-filters/examples/3-target-text
shoutClasses are in el.classes (also el.identifier and el.attributes).
includes() is a method on Pandoc lists.
Example: 03-filters/examples/3-target-text
Complete says.lua, a filter that:
Span elements with class says, andChallenge: Instead of Simon, let the user specify the name as an attribute, e.g. [Write a filter]{.says name="Charlotte"}
10:00
Exercise: 03-filters/your-turns/3-simon-says
says.lua
Example: 03-filters/examples/4-filters-in-practice/1-target-content-in-div
walk to apply filter functions to childrenExample: 03-filters/examples/4-filters-in-practice/2-walk-children-nodes
Example: 03-filters/examples/4-filters-in-practice/3-format-specific-output
Meta to examine metadataExample: 03-filters/examples/4-filters-in-practice/4-meta-filter
Filter functions in the same filter are run in a specific order: Inline elements, Inlines(), Block elements, Blocks(), Meta(), Pandoc().
Specify a different order by returning an array of filter sets.
Example: 03-filters/examples/4-filters-in-practice/5-filter-sets
Quarto’s internal filters are grouped and run in sequence: ast, quarto, render.
By default, custom filters are run pre-quarto.
You might need to run a filter later, e.g. after quarto has processed cross-references.
quarto create extension filter creates boilerplate. Drop your .lua files in.
Users must opt-in to extension under filters:
Users specify format: shouty-html, and get filter applied automatically.
Lua functions that insert their output into the AST.
Can take arguments: args, kwargs, meta, raw_args, context
https://quarto.org/docs/extensions/lua.html#learning-lua
I also quite liked: https://ebens.me/posts/lua-for-programmers-part-1/
AST diagrams are WIP
The AST diagrams you’ve seen are produced using Pandoc’s version of markdown.
Quarto specific features won’t appear in the AST diagrams as you might expect. E.g. cross-references, executable code blocks (ones with {), shortcodes, callouts, etc..
Use quarto.log.output() to examine the AST as it is when your filter is run.
This will improve!