5  How It Works

This chapter explains the internals of Janqua — how your {.clojure .jank} code blocks turn into rendered output.

5.1 Architecture overview

Janqua is a Pandoc Lua filter. When Quarto renders a .qmd file, it calls Pandoc under the hood. Pandoc reads the document, builds an abstract syntax tree (AST), and then runs any filters specified in the frontmatter. The Janqua filter intercepts code blocks marked with the .jank class (typically written as {.clojure .jank} for editor syntax highlighting), evaluates them via a Jank nREPL server, and replaces them with the appropriate output.

flowchart LR
  qmd[.qmd file] --> quarto[Quarto]
  quarto --> pandoc[Pandoc]
  pandoc -- AST --> lua[Lua filter]
  lua --> nrepl[Jank nREPL server]
  nrepl --> lua
  lua -- AST --> pandoc
  pandoc --> out[HTML]

The filter has two phases, executed in order:

  1. Meta — reads document metadata and resolves the Jank nREPL port
  2. CodeBlock — processes each {.clojure .jank} code block

5.2 Port discovery

The filter needs to find a running Jank nREPL server. It tries several sources in order, probing each candidate port with a real nREPL evaluation before using it — a plain TCP-connect check would falsely accept a stale port that the OS had recycled to an unrelated service:

  1. Frontmatter — an explicit jank.port value in the document metadata
  2. PID + port files.jank-pid and .jank-nrepl-port written by the lifecycle script
  3. Environment variableJANK_PORT
  4. Process scan — searches for a jank process listening on a TCP port (via lsof or ss). Note: if multiple projects run Jank simultaneously, this step could find the wrong one — steps 1–3 are project-specific and preferred.
  5. Auto-start — if nothing is found, the filter launches jank repl via the lifecycle script and waits for it to be ready

This chain means zero configuration for the common case: just run quarto render and the filter handles the rest.

Auto-start fires loud: the filter prints a framed stderr block listing the new process’s PID, port, log path, and the exact stop command, so the side effect is never silent. Authors who prefer manual control can disable auto-start by setting JANK_AUTO_START=0 or by adding jank: { auto-start: false } to the document frontmatter; the filter then prints a “no nREPL found” notice and skips evaluation rather than spawning a process.

5.3 Process management

Jank’s REPL exits when its standard input closes. To keep it alive across renders, the lifecycle script (jank-lifecycle.sh) pipes tail -f /dev/null into the REPL’s stdin — an infinite stream of nothing that keeps the process running.

The lifecycle script manages three files in the rendering directory (your project root if you have a _quarto.yml, otherwise the directory containing the document being rendered — the filter asks Quarto where to anchor):

File Purpose
.jank-pid PID of the running Jank process
.jank-nrepl-port The nREPL port Jank is listening on
.jank-repl.log Jank’s stdout/stderr output

A background monitor watches the Jank process and cleans up these files if it crashes. It verifies the PID in the file still matches before deleting, to avoid removing a newer instance’s files if Jank was restarted quickly.

The stop command verifies the PID still belongs to a Jank process before sending a kill signal — checking both the process name and command line arguments. This prevents accidentally killing an unrelated process that reused the PID. If no PID file exists, stop refuses to guess and asks the user to kill manually.

5.4 Code evaluation

Each {.clojure .jank} code block goes through these steps:

5.4.1 1. Kindly wrapping

The user’s code is wrapped in a function call that captures the result and its metadata:

;; User writes:
^:kind/hiccup [:div "hello"]

;; Filter sends to Jank:
((fn [v]
   ((fn [m]
      ((fn [kind]
         {:janqua/kind kind
          :janqua/value (pr-str v)})
       (when m (or (:kindly/kind m) ...))))
    (meta v)))
 (do ^:kind/hiccup [:div "hello"]))

The wrapper uses nested fn calls rather than let bindings — this works around a Jank bug where reader-attached metadata (like ^:kind/hiccup) is lost through let bindings.

5.4.2 2. nREPL communication

The wrapped code is sent to Jank via clj-nrepl-eval, a Babashka-based nREPL client. The filter parses the response to extract:

  • stdout — anything printed during evaluation
  • value — the return value (serialized via pr-str)

Session state persists between evaluations — defs and requires carry forward, so earlier code blocks can define functions used by later ones.

5.4.3 3. Kind detection

The filter examines the :janqua/kind field from the wrapper response:

Kind Output
:kind/hiccup Hiccup data is converted to HTML via a helper function defined in the Jank session
:kind/html String is inserted as raw HTML
:kind/md String is parsed as Markdown and inserted
:kind/hidden Output is suppressed (code still evaluates)
:kind/mermaid String is rendered as a Mermaid diagram (client-side JS)
:kind/graphviz String is rendered as a Graphviz diagram (client-side JS via viz-js)
:kind/tex String is rendered as a LaTeX formula
:kind/code String is displayed as syntax-highlighted Clojure code
:kind/vega-lite Map is rendered as a Vega-Lite chart
:kind/plotly Map is rendered as a Plotly chart
:kind/echarts Map is rendered as an ECharts chart
:kind/cytoscape Map is rendered as a Cytoscape graph
:kind/highcharts Map is rendered as a Highcharts chart
(none) Value is shown as a syntax-highlighted code block

For string-valued kinds (kind/html, kind/md, kind/mermaid, kind/graphviz, kind/tex, kind/code), strings can’t hold metadata directly (they’re primitives in Jank). The convention is to wrap them in a vector: ^:kind/html ["<b>bold</b>"] — the filter auto-unwraps the first element.

5.4.4 4. Hiccup rendering

When a value has :kind/hiccup metadata, the filter calls janqua.runtime/hiccup->html — a function bootstrapped in a dedicated janqua.runtime namespace on first use. This converts Clojure-style hiccup data structures into HTML strings:

[:div {:class "box"} [:span "hi"]]
;; becomes:
"<div class=\"box\"><span>hi</span></div>"

This runs inside Jank (not in Lua), so it has access to the full Clojure data model.

5.4.5 5. JS chart rendering

For chart kinds (kind/plotly, kind/vega-lite, kind/echarts, kind/cytoscape, kind/highcharts), the filter:

  1. Converts the Clojure value to JSON by calling janqua.runtime/to-json — a function bootstrapped in the Jank session alongside janqua.runtime/hiccup->html
  2. Emits a <div> with a unique ID and a <script> tag that loads the chart library from CDN and renders the spec

Each chart kind loads its own library (e.g., Plotly.js, Vega-Lite + Vega Embed, ECharts) from a CDN. Multiple charts on the same page get unique div IDs to avoid collisions. The CDN URLs are pinned to exact versions and tagged with Subresource Integrity (SRI) hashes, so the browser refuses to execute a script if its bytes don’t match — protecting viewers against a CDN compromise or a malicious patch pushed under a loose version tag.

5.4.6 6. Diagram rendering

Mermaid and Graphviz diagrams can’t use Quarto’s native {mermaid} / {dot} blocks — those are processed as executable cells in an earlier pipeline stage, before Lua filters run.

  • Mermaid: The filter loads the Mermaid JS library from CDN and calls mermaid.render() client-side, similar to the JS chart pattern.
  • Graphviz: The filter loads viz-js (a WebAssembly build of Graphviz) from CDN and calls viz.renderSVGElement() client-side. No local dot install needed.

5.5 Error handling

When something goes wrong inside the filter — a chart’s value can’t be encoded to JSON, hiccup-to-HTML conversion fails, the Kindly wrapper response can’t be parsed — Janqua surfaces the failure as a visible error block in the rendered document, mirroring how user-code exceptions are shown. Each block contains the failure reason and the original Clojure value so the cause is debuggable from the rendered page alone. The same message is also written to stderr so build logs flag the failure even when nobody opens the HTML.

5.6 Code block attributes

See Authoring Jank documents § Code block attributes for the user-facing reference.

5.7 Bootstrap sequence

On the first evaluation in a document, the filter:

  1. Resolves the nREPL port (auto-starting Jank if needed)
  2. Sends bootstrap definitions to a dedicated janqua.runtime namespace in the Jank session: janqua.runtime/hiccup->html (hiccup-to-HTML conversion) and janqua.runtime/to-json (Clojure data-to-JSON conversion for chart kinds). The bootstrap uses create-ns + intern rather than (ns ...) (defn ...), so the user’s current namespace is never switched — subsequent code blocks evaluate in a clean session.
  3. Begins processing code blocks in document order

Subsequent renders reuse the same Jank process, so startup cost is only paid once.