{
"$type": "site.standard.document",
"bskyPostRef": {
"cid": "bafyreih3kksyt4rwldqtp7rg7xa5zpgnmkkin643ikzt37oiftusgkfcpq",
"uri": "at://did:plc:t4aigbwuwix7x3q42qzjc6mn/app.bsky.feed.post/3med6fwyjy7s2"
},
"coverImage": {
"$type": "blob",
"ref": {
"$link": "bafkreiarvja3nxyesd2ra34odtnibzd4b4qbhg4btcjt2gtkl3pgke3mla"
},
"mimeType": "image/jpeg",
"size": 22024
},
"path": "/link/535/17272052/pandoc-style-filters-for-apex",
"publishedAt": "2026-02-07T15:36:00.000Z",
"site": "https://brett.trpstra.net",
"tags": [
"Apex",
"Pandoc JSON AST",
"ApexMarkdown/apex-filters",
"github.com/ApexMarkdown/apex-filters",
"See the docs",
"ApexMarkdown/apex-filter-uppercase",
"ApexMarkdown/apex-filter-unwrap",
"GitHub",
"Pandoc filters",
"Apex wiki: Filters",
"on Mastodon",
"Bluesky",
"Twitter",
"Click here if you'd like to help out.",
"Mastodon",
"everywhere else"
],
"textContent": "In my quest to make Apex as complete as possible before I integrate it into Marked, I’ve added Pandoc-compatible filters, which can be written in Lua for native execution, or in any language using a Pandoc JSON pipeline.\n\nThe filter system runs your code on the document _after_ parsing and _before_ rendering. The pipeline is: Markdown → AST → **Pandoc-style JSON** → your filters → JSON → HTML. That means you can transform the document in any language that speaks JSON — Ruby, Python, Lua, Node, whatever — using the same Pandoc JSON AST that Pandoc uses. If you’ve written a Pandoc filter, the proecess is the same: read one JSON document from stdin, write one JSON document to stdout.\n\n### Running filters from the CLI\n\nThree main options:\n\n * **`--filter NAME`** — Run a single filter. Apex looks for an executable named `NAME` in your user filters directory (`~/.config/apex/filters` or `$XDG_CONFIG_HOME/apex/filters`). So `apex --filter title input.md` runs `~/.config/apex/filters/title`. These can exist inside of subdirectories.\n\n * **`--filters`** — Run _all_ executables in that directory, in sorted order. Handy if you keep a fixed pipeline (e.g. `01-title`, `10-delink`) and just want one flag.\n\n * **`--lua-filter FILE`** — Run a Lua script as a filter. Apex calls `lua FILE`; the script reads Pandoc JSON from stdin and writes JSON to stdout. You need a JSON library (e.g. **dkjson** : `luarocks install dkjson`). No Pandoc Lua runtime required.\n\n\n\n\nFilters run in sequence. If any filter exits non-zero or outputs invalid JSON, Apex aborts unless you pass `--no-strict-filters` (then it skips the bad filter and continues).\n\n### The central filter list: install and list\n\nThere’s a small index of filters at ApexMarkdown/apex-filters. It’s a single JSON file listing filter id, title, description, author, repo, etc. This will grow as I and others create new filters.\n\n * **`apex --list-filters`** — Prints “Installed Filters” (what’s in your `~/.config/apex/filters` dir) and “Available Filters” from that index, with titles and descriptions.\n\n * **`apex --install-filter ID`** — Installs a filter by id from the index (e.g. `apex --install-filter unwrap`). It clones the repo into your filters directory. You can also install by Git URL or GitHub shorthand (user/repo).\n\n * **`apex --uninstall-filter ID`** — Removes that filter (with a confirmation prompt).\n\n\n\n\nTo add your own filter to the list, open a pull request on github.com/ApexMarkdown/apex-filters. Once it’s merged, everyone can `--list-filters` and `--install-filter your-id`. See the docs for more info on contributing.\n\n### Example filters in the wild\n\nTwo good references:\n\n * **ApexMarkdown/apex-filter-uppercase** — Lua filter that uppercases every `Str` inline. Shows how to walk the AST with dkjson and mutate in place.\n\n * **ApexMarkdown/apex-filter-unwrap** — Lua filter that unwraps elements starting with `< ` (e.g. custom block markers). More involved AST walking.\n\n\n\n\nInstall them with `apex --install-filter uppercase` and `apex --install-filter unwrap`, then use `--filter uppercase` or `--filter unwrap` in your pipeline.\n\n### Short Ruby example\n\nA minimal filter that reads Pandoc JSON, does one thing (e.g. prepend an H1 from metadata), and writes JSON back. Ruby’s stdlib `json` is enough.\n\n\n #!/usr/bin/env ruby require \"json\" doc = JSON.parse($stdin.read) blocks = doc[\"blocks\"] || [] meta = doc[\"meta\"] || {} # Get title from meta if present (simplified) title = meta.dig(\"title\", \"c\") || \"Untitled\" title = title.is_a?(String) ? title : title.to_s header = { \"t\" => \"Header\", \"c\" => [1, [\"\", [], []], [{ \"t\" => \"Str\", \"c\" => title }]] } doc[\"blocks\"] = [header] + blocks puts JSON.dump(doc)\n\nSave as `~/.config/apex/filters/title`, `chmod +x`, then `apex --filter title doc.md > out.html`.\n\n### Short Lua example\n\nSame idea in Lua: read JSON, tweak `doc.blocks` (or `doc.meta`), write JSON. Requires `dkjson`.\n\n\n local json = require(\"dkjson\") local input = io.read(\"*a\") local doc = json.decode(input) if not doc then io.stderr:write(\"Invalid JSON\\n\") os.exit(1) end -- Optional: transform doc.blocks or doc.meta -- doc.blocks = ... io.write(json.encode(doc))\n\nRun it with `apex --lua-filter myfilter.lua input.md > output.html`.\n\n### Is This Feature Complete?\n\nNo, but close. I can’t get to 100% Pandoc compatibility at this point without some restructuring of the AST generated by cmark-gfm. I’m not sure 100% parity is the goal at this point. So some existing Pandoc Lua filters require some updates to work with Apex. Additionally, this feature is literally what I came up with in two days and has a lot of testing ahead. If you’re willing to help, especially if you have Pandoc filters you’d like to port, please keep me posted on GitHub.\n\n### Am I Trying to Replace Pandoc?\n\nNo, really I’m not. I’m not trying to replicate Pandoc’s amazing export abilities, or many of its extensions.\n\nHowever, Pandoc’s relatively vast compatibility with various flavors of Markdown is similar to what I want to do in Apex, so supporting many of its extensions makes sense, and supporting filters means users who’ve written their own pipelines can easily switch to using Apex as a primary renderer. That’s going to be important if I want Marked to have Pandoc capabilities without using Pandoc as an external dependency.\n\n### Learn more\n\n * Pandoc filters — The JSON AST format and filter contract Apex follows.\n * Apex wiki: Filters — Full protocol, block/inline types, Lua details, and more examples.\n\n\n\nLike or share this post on Mastodon, Bluesky, or Twitter.\n\n* * *\n\nBrettTerpstra.com is supported by readers like you. Click here if you'd like to help out.\n\nFind Brett on Mastodon, Bluesky, GitHub, and everywhere else.",
"title": "Pandoc-style filters for Apex",
"updatedAt": "2026-02-07T15:36:00.000Z"
}