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:
| Phase | YAML key | Runs with | Best for |
|---|---|---|---|
| Parse time | pandoc.filters.parse | FORMAT == "markdown" | Cheap, pure AST rewrites that should affect Emanote’s model |
| Render time | pandoc.filters.render.html | FORMAT == "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:
- title extraction
- tags and links
- Backlinks
- tasks
- table structure
- search text
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, anddebug -
Pandoc APIs such as
pandoc.pipe,pandoc.system,pandoc.mediabag,pandoc.template, andpandoc.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.luaworks when the file exists in your notebook -
lua-filters/list-table.luaandlua-filters/wordcount.luawork 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:
- Declare a filter in frontmatter before creating it on disk.
-
Create the
.luafile. - 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
-
Filter declarations are note-local: Markdown frontmatter or explicit Org
#+PANDOC_FILTERS_PARSE:/#+PANDOC_FILTERS_RENDER_HTML:keywords. Cascadingpandoc.filtersfrom an ancestor `index.yaml` is still tracked under #263. -
Parse-time filters run with
FORMAT == "markdown". Writer-specific HTML filters should usepandoc.filters.render.html, which runs withFORMAT == "html"and receives the note’s effective metadata indoc.meta. -
Filters that need filesystem, process, media, module-loading, or dynamic-code IO belong under
pandoc.filters.render.html, notpandoc.filters.parse.
Bundled filters
Two curated filters ship in Emanote’s default layer under emanote/default/lua-filters/:
-
list-table.luaturns nested bullet lists into HTML tables. It is bundled from the maintained pandoc-ext/list-table extension. -
wordcount.luaappends aN words · M charactersfooter to the document. This Emanote-specific filter is derived from the retiredpandoc/lua-filterswordcount filter; upstream prints to stdout and callsos.exit(0), which would terminate the live server.
Local demo filter
This docs notebook also includes a local custom filter:
-
slides.luaturns a:::slidesdiv into a navigable Markdown presentation at HTML render time, used by A Markdown Presentation about Lua Filters.
Demos
list-table.lua
| row 1, column 1 | row 1, column 2 | row 1, column 3 |
|---|---|---|
| row 2, column 1 | row 2, column 3 | |
| row 3, column 1 | row 3, column 2 | Well! |
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".