| paths |
|
|---|
Guidance for developing Lua filters in Quarto's filter system.
-- ✅ Correct - import from main.lua
import("./quarto-pre/shortcodes.lua")
-- ❌ Wrong - require is for modules only
require("./quarto-pre/shortcodes")-- ✅ Correct - modules from modules/
local patterns = require("modules/patterns")
local md = require("modules/md")
-- ❌ Wrong - using import for modules
import("./modules/patterns.lua")Always use _quarto.ast.walk() to properly handle custom nodes:
-- ✅ Correct - handles custom nodes
doc = _quarto.ast.walk(doc, {
Callout = function(callout)
-- Process callout
end
})
-- ❌ Wrong - misses custom nodes
pandoc.walk_block(div, filter)-- Check if custom node of specific type
if is_custom_node(node, "Callout") then
-- Handle callout custom node
end
-- Check if regular Pandoc node (NOT custom node)
if is_regular_node(node, "Div") then
-- Handle regular Div
end
-- Check custom node by presence of is_custom_node flag
if node.is_custom_node then
-- It's some custom node type
endUse the proxy pattern for slot modification:
-- ✅ Correct - proxy pattern
local new_callout = callout:clone()
new_callout.content = modified_content
return new_callout
-- ❌ Wrong - direct assignment may not work
callout.content = modified_content
return calloutUse _quarto.format for format checks:
-- HTML output (includes HTML-based formats)
if _quarto.format.isHtmlOutput() then ... end
-- LaTeX/PDF output
if _quarto.format.isLatexOutput() then ... end
-- Typst output
if _quarto.format.isTypstOutput() then ... end
-- Word/DOCX output
if _quarto.format.isDocxOutput() then ... end
-- Reveal.js slides
if _quarto.format.isRevealJsOutput() then ... end
-- Dashboard format
if _quarto.format.isDashboardOutput() then ... end-- Read metadata option with default
local show_icon = option("callout-icon", true)
-- Read execution parameter
local engine = param("execution-engine")
-- Read option with nil fallback
local custom = option("my-option")
if custom ~= nil then
-- Option was set
endUse logging functions from common/log.lua:
-- Debug info (verbose)
info("Processing element: " .. el.t)
-- Warnings (appear as INFO on TypeScript side)
warn("Deprecated feature used")
-- Errors
error("Invalid configuration")
-- Conditional debug output
if quarto.log.debug then
quarto.utils.dump(node)
endUse pandoc.pipe() instead of io.popen() for calling external programs:
-- ✅ Correct - pandoc.pipe passes args as array, no shell interpretation
local ok, result = pcall(pandoc.pipe, command, {"arg1", "arg2"}, "")
if not ok then
quarto.log.error("Command failed: " .. tostring(result))
end
-- ❌ Wrong - io.popen uses shell, breaks on paths with spaces
local handle = io.popen(command .. " arg1 arg2", "r")io.popen() passes a string to the shell, which breaks when paths contain spaces (e.g., C:\Program Files\...). pandoc.pipe() calls the executable directly with arguments as an array — no shell, no quoting issues.
Reference: quarto-pre/shiny.lua, quarto-post/pdf-images.lua
function my_filter()
return {
Div = function(div)
-- Return nil to continue (no change)
if not should_process(div) then
return nil
end
-- Return new element to replace
return pandoc.Div(modified_content)
-- Return empty list to remove element
-- return {}
end
}
end-- String matching
if string.match(text, "pattern") then ... end
-- String substitution
local result = string.gsub(text, "old", "new")
-- Check class presence
if div.classes:includes("callout") then ... end
-- Check attribute
local value = div.attributes["data-foo"]-- Create elements
local div = pandoc.Div(content, pandoc.Attr(id, classes, attributes))
local span = pandoc.Span(inlines)
local para = pandoc.Para(inlines)
-- Raw output
local raw = pandoc.RawBlock("html", "<div>...</div>")
local raw = pandoc.RawInline("latex", "\\textbf{}")
-- Stringify content
local text = pandoc.utils.stringify(inlines)-- Pretty-print any object
quarto.utils.dump(node)
-- Type checking
print("Type: " .. type(obj))
print("Pandoc type: " .. (obj.t or "none"))
-- Trace execution
warn("Reached checkpoint: " .. checkpoint_name)When adding filters to main.lua:
-- Each filter gets a name and filter function
{ name = "pre-my-feature", filter = my_feature() }
-- Filter order matters - check dependencies
-- Filters in same stage run in order definedConsult src/resources/lua-types/ for available methods, properties, and function signatures:
lua-types/pandoc/- Pandoc Lua API (blocks, inlines, List, utils, etc.)lua-types/quarto/- Quarto Lua API (format detection, custom nodes, etc.)
These type definition files document the complete API surface.
- Use
import()for filters -require()for modules only - Use
_quarto.ast.walk()- Notpandoc.walk_*for custom nodes - Check node types carefully -
is_custom_node()vsis_regular_node() - Use proxy pattern - For modifying custom node slots
- Use
_quarto.format- For format detection - Return
nilto continue - Return value replaces element warn()= INFO level - On TypeScript side