clj-format has a single extension point: the dynamic var
clj-format.core/*dsl-preprocessor*. Before compilation, every vector-shaped
format argument is passed through this function. The default value is
identity, so by default nothing happens — the DSL flows straight into the
compiler.
An extension namespace rebinds this var (typically at load time) to a walker
that expands custom directives into forms the compiler already understands.
This document explains how the hook works, how to build one, and how the
bundled clj-format.figlet extension uses it.
(def ^:dynamic *dsl-preprocessor* identity)
fmt argument to clj-format, when it is a
vector). Strings and bare keywords bypass the preprocessor entirely, so you
only need to handle vector forms.clj-format call. Your walker is
responsible for recursing into nested directives where relevant.Because *dsl-preprocessor* is a dynamic var, callers can also rebind it
per-call with binding:
(binding [clj-format.core/*dsl-preprocessor* my-preprocessor]
(clj-format.core/clj-format nil [:my-directive ...]))
Extensions installed at the root binding apply globally; binding is the
right escape hatch for tests, for composition, or for scoping an extension to
a single call site.
A minimal extension has three parts:
Here is a skeleton that adds a [:shout "hello"] directive expanding to
upper-cased text:
(ns my.app.shout
(:require [clojure.string :as str]
[clj-format.core :as core]))
(defn- shout-form? [x]
(and (vector? x) (= :shout (first x))))
(defn- expand-shout [[_ text]]
(str/upper-case text))
(defn expand [dsl]
(cond
(shout-form? dsl) (expand-shout dsl)
(vector? dsl) (mapv expand dsl)
(seq? dsl) (mapv expand dsl)
:else dsl))
(alter-var-root #'core/*dsl-preprocessor* (constantly expand))
Requiring my.app.shout once installs the preprocessor. From then on:
(clj-format.core/clj-format nil [:shout "hello"]) ;; => "HELLO"
(clj-format.core/clj-format nil ["[" [:shout "hi"] "]"]) ;; => "[HI]"
Notes on the walker:
[:str {:width 10}]), or even
another form that will itself be handled by the compiler.Because *dsl-preprocessor* is a single var, the last extension to call
alter-var-root wins at install time. If you need two extensions to coexist,
compose them explicitly:
(require 'my.app.shout 'other.app.glow) ;; each installs its own expander
(alter-var-root #'clj-format.core/*dsl-preprocessor*
(constantly (comp my.app.shout/expand other.app.glow/expand)))
comp here runs the walkers in sequence on the same DSL tree. The order
matters if the extensions rewrite overlapping forms; if they do not, either
order is fine.
For per-call composition inside a test or library, binding is cleaner than
re-installing at the root:
(binding [clj-format.core/*dsl-preprocessor*
(comp my.app.shout/expand other.app.glow/expand)]
(clj-format.core/clj-format nil dsl))
clj-format.figletThe bundled clj-format.figlet namespace is the reference implementation of
the extension pattern. It adds a [:figlet opts? & body-strings] directive
that expands to a FIGlet ASCII-art banner.
Source: src/clj_format/figlet.clj.
Highlights:
[:figlet "Hello"] ;; default font
[:figlet {:font "small"} "Hello"] ;; named font
[:figlet {:font "slant"} "Line 1" "Line 2"]
clj-format.figlet/expand walks vectors and seqs the same
way the skeleton above does, so figlet forms nested inside :each, :if,
or [:table … :format …] are all rewritten.(alter-var-root #'core/*dsl-preprocessor* (constantly expand))
once at the bottom of the file. Projects that never require
clj-format.figlet leave *dsl-preprocessor* at its identity default and
pay nothing.
clj-figlet is declared with :scope "provided"
in clj-format's project.clj, so it is not pulled transitively. Consumer
projects that want the :figlet directive add it explicitly:
[com.github.danlentz/clj-figlet "0.1.4"] ; Leiningen
com.github.danlentz/clj-figlet {:mvn/version "0.1.4"} ; deps.edn / bb.edn
Then (require 'clj-format.figlet) once at startup to install the
preprocessor. Because the namespace is lazy-loaded, projects that never
require it pay nothing at runtime.
[:figlet …] form
must therefore be literal strings, not runtime values. For runtime-derived
banners, call clj-figlet.core/render yourself and pass the resulting
string as a normal clj-format argument — the same way you would pass any
other precomputed string.The figlet extension also composes with tables: because :format on a
[:col …] accepts any Clojure function, a row-local computed column can call
clj-figlet.core/render directly, and :overflow :wrap lays the resulting
multi-line banner into the cell. See the "Anything multi-line goes in a cell"
section of the README for a worked recipe.
A few things to keep in mind when designing your own extension:
clj-format.compiler/compile-format. If you emit something the compiler
does not understand, the error surfaces as a compile-phase
ExceptionInfo — exactly the same path as a malformed hand-written DSL.*dsl-preprocessor* is defined in
clj-format.core (.cljc), so extensions can be ClojureScript-compatible
if they avoid JVM-only APIs. clj-format.figlet is JVM-only because
clj-figlet is; a pure-Clojure extension can live in .cljc and target
both platforms.binding, and composition work uniformly. If you find yourself
wanting multiple independent extensions, compose them with comp as shown
above.Can you improve this documentation?Edit on GitHub
cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |