Skip to content

Latest commit

 

History

History
262 lines (188 loc) · 5.76 KB

File metadata and controls

262 lines (188 loc) · 5.76 KB
paths
src/resources/filters/**/*.lua

Lua Filter Development Conventions

Guidance for developing Lua filters in Quarto's filter system.

Module Loading

Use import() for Filter Files

-- ✅ Correct - import from main.lua
import("./quarto-pre/shortcodes.lua")

-- ❌ Wrong - require is for modules only
require("./quarto-pre/shortcodes")

Use require() for Modules

-- ✅ Correct - modules from modules/
local patterns = require("modules/patterns")
local md = require("modules/md")

-- ❌ Wrong - using import for modules
import("./modules/patterns.lua")

Custom Node Patterns

Walking Custom Nodes

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)

Checking Node Types

-- 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
end

Slot Assignment

Use 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 callout

Format Detection

Use _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

Options and Parameters

-- 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
end

Logging

Use 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)
end

External Command Execution

Use 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

Filter Return Values

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

Common Utilities

String Operations

-- 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"]

Pandoc Helpers

-- 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)

Debugging

-- 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)

Filter Chain Integration

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 defined

API Reference

Consult 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.

Key Conventions Summary

  1. Use import() for filters - require() for modules only
  2. Use _quarto.ast.walk() - Not pandoc.walk_* for custom nodes
  3. Check node types carefully - is_custom_node() vs is_regular_node()
  4. Use proxy pattern - For modifying custom node slots
  5. Use _quarto.format - For format detection
  6. Return nil to continue - Return value replaces element
  7. warn() = INFO level - On TypeScript side