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.

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

Running filters from the CLI

Three main options:

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

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

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

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

The central filter list: install and list

There’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.

  • apex --list-filters — Prints “Installed Filters” (what’s in your ~/.config/apex/filters dir) and “Available Filters” from that index, with titles and descriptions.

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

  • apex --uninstall-filter ID — Removes that filter (with a confirmation prompt).

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

Example filters in the wild

Two good references:

Install them with apex --install-filter uppercase and apex --install-filter unwrap, then use --filter uppercase or --filter unwrap in your pipeline.

Short Ruby example

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

#!/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)

Save as ~/.config/apex/filters/title, chmod +x, then apex --filter title doc.md > out.html.

Short Lua example

Same idea in Lua: read JSON, tweak doc.blocks (or doc.meta), write JSON. Requires dkjson.

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

Run it with apex --lua-filter myfilter.lua input.md > output.html.

Is This Feature Complete?

No, 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.

Am I Trying to Replace Pandoc?

No, really I’m not. I’m not trying to replicate Pandoc’s amazing export abilities, or many of its extensions.

However, 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.

Learn more

  • Pandoc filters — The JSON AST format and filter contract Apex follows.
  • Apex wiki: Filters — Full protocol, block/inline types, Lua details, and more examples.