Pandoc Lua Filters

A Pandoc Lua filter rewrites the parsed Pandoc document before Emanote turns it into an HTML page.

Filters are note-local. Enable them in Markdown frontmatter or, for Org notes, with #+PANDOC_FILTERS_PARSE: / #+PANDOC_FILTERS_RENDER_HTML:.

Choose a phase

Emanote supports two filter phases:

PhaseYAML keyRuns withBest for
Parse timepandoc.filters.parseFORMAT == "markdown"Cheap, pure AST rewrites that should affect Emanote’s model
Render timepandoc.filters.render.htmlFORMAT == "html"HTML-specific output and IO work

Parse-time filters

Parse-time filters run immediately after Markdown parsing:

pandoc:
  filters:
    parse:
      - lua-filters/list-table.lua

Use parse-time filters when the rewritten document should affect Emanote’s semantic model:

Parse-time filters should stay cheap. They run when Emanote parses notes, so expensive work here slows model updates even when Emanote only needs metadata, links, search text, or other non-HTML data.

Parse-time filters cannot use IO. Emanote rejects direct references before running the filter and also runs parse filters with IO-capable APIs disabled. That includes:

  • Lua APIs such as io, os, print, require, load, dofile, and debug
  • Pandoc APIs such as pandoc.pipe, pandoc.system, pandoc.mediabag, pandoc.template, and pandoc.zip
  • nested Pandoc filter runners such as pandoc.utils.run_lua_filter

Render-time filters

Render-time filters run when Emanote renders a note to HTML:

pandoc:
  filters:
    render:
      html:
        - filters/slides.lua

Use render-time filters for:

  • raw HTML, CSS, or JavaScript
  • writer-specific filters that branch on FORMAT == "html"
  • diagrams and other generated assets
  • calls to external tools
  • filesystem, process, cache, media, or module-loading work

Render-time filters keep parsing fast because their IO work is paid only when Emanote is producing HTML.

Filter paths

Filter paths are resolved against your notebook layers first, then against Emanote’s default layer.

That means:

  • filters/custom.lua works when the file exists in your notebook
  • lua-filters/list-table.lua and lua-filters/wordcount.lua work without copying anything into your notes
  • multiple filters run in declaration order

This page chains the bundled list-table.lua and wordcount.lua, and you can see the wordcount footer at the bottom.

Org notes

Org notes use Org keywords. Add one keyword line per filter:

#+PANDOC_FILTERS_PARSE: lua-filters/list-table.lua
#+PANDOC_FILTERS_PARSE: lua-filters/wordcount.lua
#+PANDOC_FILTERS_RENDER_HTML: filters/slides.lua

#+PANDOC_FILTERS_PARSE: is parse-time. #+PANDOC_FILTERS_RENDER_HTML: is render-time HTML.

Hot reload

Edits to .lua files hot-reload. The live server re-parses every note that references a changed filter, with no touch of the note required.

Hot reload also covers missing-at-parse-time filter references:

  1. Declare a filter in frontmatter before creating it on disk.
  2. Create the .lua file.
  3. Every dependent note re-parses when the file lands.

.lua files are recognised as filters for hot-reload and remain linkable as source files. See Embedding for a source-file embed example.

Limitations

Remaining limitations
  • Filter declarations are note-local: Markdown frontmatter or explicit Org #+PANDOC_FILTERS_PARSE: / #+PANDOC_FILTERS_RENDER_HTML: keywords. Cascading pandoc.filters from an ancestor `index.yaml` is still tracked under #263.
  • Parse-time filters run with FORMAT == "markdown". Writer-specific HTML filters should use pandoc.filters.render.html, which runs with FORMAT == "html" and receives the note’s effective metadata in doc.meta.
  • Filters that need filesystem, process, media, module-loading, or dynamic-code IO belong under pandoc.filters.render.html, not pandoc.filters.parse.

Bundled filters

Two curated filters ship in Emanote’s default layer under emanote/default/lua-filters/:

Local demo filter

This docs notebook also includes a local custom filter:

Demos

list-table.lua

row 1, column 1row 1, column 2row 1, column 3
row 2, column 1row 2, column 3
row 3, column 1row 3, column 2Well!

wordcount.lua

The footer at the bottom of this page is emitted by parse-time wordcount.lua — every save recomputes it.

slides.lua

See A Markdown Presentation about Lua Filters for a full Markdown presentation about Lua filters, rendered by this notebook’s local filters/slides.lua with FORMAT == "html".