3  How It Works

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

3.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] --> pandoc[Pandoc]
  pandoc --> lua[Lua filter]
  lua --> nrepl[Jank nREPL server]
  nrepl --> lua
  lua --> out[HTML / PDF]

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

3.2 Port discovery

The filter needs to find a running Jank nREPL server. It tries several sources in order, validating that each candidate port is actually reachable before using it:

  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.

3.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 project root:

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.

3.4 Code evaluation

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

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

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

3.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 or :kind/markdown 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 SVG via the dot command
: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.

3.4.4 4. Hiccup rendering

When a value has :kind/hiccup metadata, the filter calls janqua-hiccup->html — a function bootstrapped in the Jank session 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.

3.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-to-json — a function bootstrapped in the Jank session alongside janqua-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.

3.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 pipes the DOT source through the dot -Tsvg command and embeds the resulting SVG directly. This requires Graphviz to be installed on the build machine.

3.5 Code block attributes

The filter respects several attributes on {.clojure .jank} blocks:

Attribute Effect
output=html Force HTML output (override Kindly)
output=markdown Force Markdown output
output=hidden Suppress both code and output
echo=false Hide the source code, show only results
eval=false Show code without evaluating

Kindly metadata on the result takes precedence over output= when both are present.

3.6 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 the Jank session: janqua-hiccup->html (hiccup-to-HTML conversion) and janqua-to-json (Clojure data-to-JSON conversion for chart kinds)
  3. Begins processing code blocks in document order

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