Handlers

Handlers are small build-time transforms that turn a special markdown form into rendered HTML. Two trigger shapes:

  • Inline: `prefix: content`: a plain inline-code span where the content starts with a registered prefix and a colon.
  • Code block: ```lang: a fenced code block whose language tag matches a registered handler.

Vaults currently includes three built-in handlers and lets you add your own under .vaults/handlers/.

Built-in: `dice:`

Click the rolled die for a fresh result.

MarkdownRenders as
`dice: 1d20+5`
`dice: 8d6`
`dice: 1d100`

Unrecognised formulas degrade to a struck-through code span instead of crashing the build:

MarkdownRenders as
`dice: not-a-formula`not-a-formula

Supported syntax: XdY, XdY+Z, XdY-Z. More elaborate dice notation (advantage, exploding, keep-highest) is not currently supported.

Built-in: `fm:`

Inserts a value from this page's frontmatter. The frontmatter on this very page is:

title: Handlers

So `fm: title` renders as: Handlers. Frontmatter values flow through the rest of the markdown pipeline, so you can put inline markup in your frontmatter and it will render. This is handy when the same value appears in a heading and in prose, and you don't want to hand-sync the formatting.

Missing keys render a visible warning marker so typos surface instead of silently emitting "undefined":

MarkdownRenders as
`fm: nope`{{nope}}

Date frontmatter values (YAML auto-parses ISO 8601 to JS Date) format as YYYY-MM-DD. Arrays join with , . Objects emit the warning marker, but you can dot-path into them: `fm: stats.hp` walks nested keys, with any missing segment along the path triggering the warning.

Numeric segments index into arrays, so `fm: foundry.data.results.0.name` pulls the first row's name field. Witchwood encounters uses this to render a RollTable defined entirely in foundry.data as a markdown table in the page body, with no duplicated content between the Foundry doc and the wiki.

For values that should appear inside a <pre><code> (a script body, a long string), there's a fenced-code form keyed on fm. The body is the dot-path. Any text after the lang on the fence is the language hint for the rendered code element:

```fm javascript
foundry.data.command
```

Renders as <pre><code class="language-javascript">…value…</code></pre>. The macro pages (Toggle feast, Toggle lights, Toggle ambient noise) use this to display their command source without duplicating the script between the frontmatter and the body.

Built-in: statblock

A code-block handler keyed on ```statblock, schema-compatible with the Fantasy Statblocks Obsidian plugin. See the dedicated Statblocks page for a full demo.

Pseudodragon

Tiny dragon neutral good

Armor Class 13

Hit Points 7 (2d4 + 2)

Speed 15 ft., fly 60 ft.

STR
6 (-2)
DEX
15 (+2)
CON
13 (+1)
INT
10 (+0)
WIS
12 (+1)
CHA
10 (+0)

Skills Perception +5, Stealth +4

Senses blindsight 10 ft., darkvision 60 ft., passive Perception 15

Languages understands Common and Draconic but can't speak

Challenge 1/4

Keen Senses. The pseudodragon has advantage on Wisdom (Perception) checks that rely on sight, hearing, or smell.

Actions

Bite. Melee Weapon Attack: +4 to hit, reach 5 ft., one target. Hit: piercing damage.

Note the button inside the action description. Handler descriptions support inline handler chaining, so dice expressions in stat damage rolls click through like everywhere else.

Writing a custom handler

Drop a file in .vaults/handlers/ and export a handler (or handlers: []):

// .vaults/handlers/shout.mjs
export const handler = {
  inline: "shout",
  render(content, ctx) {
    return { html: "<strong>" + content.toUpperCase() + "</strong>" };
  },
};

Now `shout: hello` renders as a bold uppercase HELLO anywhere in the vault.

Handler API surface:

  • Inline: { inline: "prefix", render(content, ctx) }
  • Code block: { codeBlock: "lang", render(content, ctx) }
  • Return { html: "..." } to insert raw markup, or { markdown: "..." } to re-process through the rest of the pipeline (wikilinks resolve, embeds inline, dice buttons in your output get picked up).
  • ctx.frontmatter is the rendering page's parsed frontmatter; ctx.pagePath is its vault-relative path; ctx.escape(s) is an HTML-escape helper; ctx.applyInlineHandlers(s) lets your handler invoke other inline handlers (this is how statblock's desc fields support ).

Browser-side assets

Handlers can include JS and CSS to the deploy:

export const handler = {
  codeBlock: "widget",
  assets: {
    scripts: ["./widget.runtime.js"],
    styles: ["./widget.css"],
  },
  render() { return { html: '<div class="widget"></div>' }; },
};

Foundry import opt-in

By default, handler CSS/JS only reaches the wiki. The Foundry VTT module ignores it because running arbitrary scripts from a third-party URL inside a Foundry world is the kind of thing that warrants explicit consent. To make a handler's assets eligible for import into Foundry, add a foundry block:

export const handler = {
  inline: "clicker",
  assets: {
    scripts: ["./clicker.runtime.js"],
    styles: ["./clicker.css"],
    foundry: { scripts: true, styles: true },  // both default false
  },
  render: ...,
};

Two layers of consent gate this:

  1. Handler-side opt-in (above): only handlers that set foundry.scripts / foundry.styles get bundled into the deploy's _handlers.foundry.{js,css}. Everything else stays wiki-only.
  2. GM-side opt-in in the Foundry module's per-vault settings dialog ("Import handler stylesheets" / "Import handler scripts" checkboxes, both default off).

Live demo: this vault includes a `clicker:` inline handler with both opted in. `clicker: try me` renders as (click it!). On the wiki it works because the handler's CSS + JS included at _handlers.{css,js}. In Foundry it works only if the GM checked both import boxes for this vault. Otherwise the journal page shows an unstyled, inert button (since the wiki HTML containing <button class="vaults-clicker"> survives the sync, but the styling/behaviour does not).

Updated