Extending Quarto workshop @ posit::conf(2025)
Filters manipulate the AST between the parsing and the writing phase. First, understand the AST.
Blocks
Blocks
contain other Blocks
Blocks
contain Inlines
Str
and Space
E.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
nil
Node 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.lua
Inline
which replaces the original, e.g. replace-strong.lua
Inline
(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
Inlines
This 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
shout
Classes 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!