ToC
Generated with Clerk from book.clj@c47477c

๐Ÿ“– Book of Clerk

โš–๏ธ Rationale

Computational notebooks allow arguing from evidence by mixing prose with executable code. For a good overview of problems users encounter in traditional notebooks like Jupyter, see I don't like notebooks and Whatโ€™s Wrong with Computational Notebooks? Pain Points, Needs, and Design Opportunities.

Specifically Clerk wants to address the following problems:

  • Less helpful than my editor
  • Notebook code being hard to reuse
  • Reproduction problems coming from out-of-order execution
  • Problems with archival and putting notebooks in source control

Clerk is a notebook library for Clojure that aims to address these problems by doing less, namely:

  • no editing environment, folks can keep using the editors they know and love
  • no new format: Clerk notebooks are either regular Clojure namespaces (interspersed with markdown comments) or regular markdown files (interspersed with Clojure code fences). This also means Clerk notebooks are meant to be stored in source control.
  • no out-of-order execution: Clerk notebooks always evaluate from top to bottom. Clerk builds a dependency graph of Clojure vars and only recomputes the needed changes to keep the feedback loop fast.
  • no external process: Clerk runs inside your Clojure process, giving Clerk access to all code on the classpath.

๐Ÿš€ Getting Started

๐Ÿคน Clerk Demo

When you're not yet familiar with Clerk, we recommend cloning and playing with the nextjournal/clerk-demo repo.

git clone git@github.com:nextjournal/clerk-demo.git
cd clerk-demo

Then open dev/user.clj from the project in your favorite editor start a REPL into the project, see

๐Ÿ”Œ In an Existing Project

To use Clerk in your project, add the following dependency to your deps.edn:

{:deps {io.github.nextjournal/clerk {:mvn/version "0.9.513"}}}

Require and start Clerk as part of your system start, e.g. in user.clj:

(require '[nextjournal.clerk :as clerk])
;; start Clerk's built-in webserver on the default port 7777, opening the browser when done
(clerk/serve! {:browse? true})
;; either call `clerk/show!` explicitly to show a given notebook.
(clerk/show! "notebooks/rule_30.clj")

You can then access Clerk at http://localhost:7777.

โฑ File Watcher

You can load, evaluate, and present a file with the clerk/show! function, but in most cases it's easier to start a file watcher with something like:

(clerk/serve! {:watch-paths ["notebooks" "src"]})

... which will automatically reload and re-eval any clojure (clj) or markdown (md) files that change, displaying the most recently changed one in your browser.

To make this performant enough to feel good, Clerk caches the computations it performs while evaluating each file. Likewise, to make sure it doesn't send too much data to the browser at once, Clerk paginates data structures within an interactive viewer.

๐Ÿ”ช Editor Integration

A recommended alternative to the file watcher is setting up a hotkey in your editor to save & clerk/show! the active file.

Emacs

In Emacs, add the following to your config:

(defun clerk-show ()
(interactive)
(when-let
((filename
(buffer-file-name)))
(save-buffer)
(cider-interactive-eval
(concat "(nextjournal.clerk/show! \"" filename "\")"))))
(define-key clojure-mode-map (kbd "<M-return>") 'clerk-show)

IntelliJ/Cursive

In IntelliJ/Cursive, you can set up REPL commands via:

  • going to Toolsโ†’REPLโ†’Add New REPL Command, then
  • add the following command: (show! "~file-path");
  • make sure the command is executed in the nextjournal.clerk namespace;
  • lastly assign a shortcut of your choice via Settingsโ†’Keymap

Neovim + Conjure

With neovim + conjure one can use the following vimscript function to save the file and show it with Clerk:

function! ClerkShow()
exe "w"
exe "ConjureEval (nextjournal.clerk/show! \"" . expand("%:p") . "\")"
endfunction
nmap <silent> <localleader>cs :execute ClerkShow()<CR>

๐Ÿ” Viewers

Clerk comes with a number of useful built-in viewers e.g. for Clojure data, html & hiccup, tables, plots &c.

When showing large data structures, Clerk's default viewers will paginate the results.

๐Ÿงฉ Clojure Data

The default set of viewers are able to render Clojure data.

(def clojure-data
{:hello "world ๐Ÿ‘‹"
:tacos (map #(repeat % '๐ŸŒฎ) (range 1 30))
:zeta "The\npurpose\nof\nvisualization\nis\ninsight,\nnot\npictures."})
{:hello "
world ๐Ÿ‘‹"
:tacos ((๐ŸŒฎ) (๐ŸŒฎ ๐ŸŒฎ) (๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ) (๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ) (๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ) (๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ) (๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ) (๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ) (๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ) (๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ) (๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ) (๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ) (๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ) (๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ) (๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ) (๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ) (๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ) (๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ) (๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ 16 more elided) (20 more elided) 9 more elided) :zeta "
Theโ†ฉ๏ธŽpurposeโ†ฉ๏ธŽofโ†ฉ๏ธŽvisualizationโ†ฉ๏ธŽisโ†ฉ๏ธŽinsight,โ†ฉ๏ธŽnotโ†ฉ๏ธŽpictures."}

Viewers can handle lazy infinite sequences, partially loading data by default with the ability to load more data on request.

(range)
(0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 1000000+ more elided)
(def fib (lazy-cat [0 1] (map + fib (rest fib))))
(0 1 1 2 3 5 8 13 21 34 55 89 144 233 377 610 987 1597 2584 4181 73 more elided)

In addition, there's a number of built-in viewers that can be called explicity using functions.

๐ŸŒ Hiccup, HTML & SVG

The html viewer interprets hiccup when passed a vector.

(clerk/html [:div "As Clojurians we " [:em "really"] " enjoy hiccup"])
As Clojurians we really enjoy hiccup

Alternatively you can pass it an HTML string.

(clerk/html "Never <strong>forget</strong>.")
Never forget.

You can style elements, using Tailwind CSS.

(clerk/html [:button.bg-sky-500.hover:bg-sky-700.text-white.rounded-xl.px-2.py-1 "โœจ Tailwind CSS"])

The html viewer is able to display SVG, taking either a hiccup vector or a SVG string.

(clerk/html [:svg {:width 500 :height 100}
[:circle {:cx 25 :cy 50 :r 25 :fill "blue"}]
[:circle {:cx 100 :cy 75 :r 25 :fill "red"}]])

You can also embed other viewers inside of hiccup.

(clerk/html [:div.flex.justify-center.space-x-6
[:p "a table is next to me"]
(clerk/table [[1 2] [3 4]])])

a table is next to me

12
34

๐Ÿ”ข Tables

Clerk provides a built-in data table viewer that supports the three most common tabular data shapes out of the box: a sequence of maps, where each map's keys are column names; a seq of seq, which is just a grid of values with an optional header; a map of seqs, in with keys are column names and rows are the values for that column.

(clerk/table [[1 2]
[3 4]]) ;; seq of seqs
12
34
(clerk/table (clerk/use-headers [["odd numbers" "even numbers"]
[1 2]
[3 4]])) ;; seq of seqs with header
odd numbers
even numbers
12
34
(clerk/table [{"odd numbers" 1 "even numbers" 2}
{"odd numbers" 3 "even numbers" 4}]) ;; seq of maps
odd numbers
even numbers
12
34
(clerk/table {"odd numbers" [1 3]
"even numbers" [2 4]}) ;; map of seqs
odd numbers
even numbers
12
34

Internally the table viewer will normalize all of the above to a map with :rows and an optional :head key, also giving you control over the column order.

(clerk/table {:head ["odd numbers" "even numbers"]
:rows [[1 2] [3 4]]}) ;; map with `:rows` and optional `:head` keys
odd numbers
even numbers
12
34

๐Ÿงฎ TeX

As we've already seen, all comment blocks can contain TeX (we use KaTeX under the covers). In addition, you can call the TeX viewer programmatically. Here, for example, are Maxwell's equations in differential form:

(clerk/tex "
\\begin{alignedat}{2}
\\nabla\\cdot\\vec{E} = \\frac{\\rho}{\\varepsilon_0} & \\qquad \\text{Gauss' Law} \\\\
\\nabla\\cdot\\vec{B} = 0 & \\qquad \\text{Gauss' Law ($\\vec{B}$ Fields)} \\\\
\\nabla\\times\\vec{E} = -\\frac{\\partial \\vec{B}}{\\partial t} & \\qquad \\text{Faraday's Law} \\\\
\\nabla\\times\\vec{B} = \\mu_0\\vec{J}+\\mu_0\\varepsilon_0\\frac{\\partial\\vec{E}}{\\partial t} & \\qquad \\text{Ampere's Law}
\\end{alignedat}
")
Loading...

๐Ÿ“Š Plotly

Clerk also has built-in support for Plotly's low-ceremony plotting. See Plotly's JavaScript docs for more examples and options.

(clerk/plotly {:data [{:z [[1 2 3] [3 2 1]] :type "surface"}]
:layout {:margin {:l 20 :r 0 :b 20 :t 20}}
:config {:displayModeBar false
:displayLogo false}})
Loading...

๐Ÿ—บ Vega Lite

But Clerk also has Vega Lite for those who prefer that grammar.

(clerk/vl {:width 650 :height 400 :data {:url "https://vega.github.io/vega-datasets/data/us-10m.json"
:format {:type "topojson" :feature "counties"}}
:transform [{:lookup "id" :from {:data {:url "https://vega.github.io/vega-datasets/data/unemployment.tsv"}
:key "id" :fields ["rate"]}}]
:projection {:type "albersUsa"} :mark "geoshape" :encoding {:color {:field "rate" :type "quantitative"}}
:embed/opts {:actions false}})
Loading...

You can provide a map of embed options to the vega viewer via the :embed/opts key.

Clerk handles conversion from EDN to JSON for you. The official Vega-Lite examples are in JSON, but a Clojure/EDN version available: Carsten Behring's Vega gallery in EDN.

๐ŸŽผ Code

The code viewer uses clojure-mode for syntax highlighting.

(clerk/code (macroexpand '(when test
expression-1
expression-2)))
(if test (do expression-1 expression-2))
(clerk/code '(ns foo "A great ns" (:require [clojure.string :as str])))
(ns foo "A great ns" (:require [clojure.string :as str]))
(clerk/code "(defn my-fn\n \"This is a Doc String\"\n [args]\n 42)")
(defn my-fn
"This is a Doc String"
[args]
42)

๐Ÿž Images

Clerk now has built-in support for the java.awt.image.BufferedImage class, which is the native image format of the JVM.

When combined with javax.imageio.ImageIO/read, one can easily load images in a variety of formats from a java.io.File, an java.io.InputStream, or any resource that a java.net.URL can address.

For example, we can fetch a photo of De zaaier, Vincent van Gogh's famous painting of a farmer sowing a field from Wiki Commons like this:

(ImageIO/read (URL. "https://upload.wikimedia.org/wikipedia/commons/thumb/3/31/The_Sower.jpg/1510px-The_Sower.jpg"))

We've put some effort into making the default image rendering pleasing. The viewer uses the dimensions and aspect ratio of each image to guess the best way to display it in classic DWIM fashion. For example, an image larger than 900px wide with an aspect ratio larger then two will be displayed full width:

(ImageIO/read (URL. "https://images.unsplash.com/photo-1532879311112-62b7188d28ce?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8"))

On the other hand, smaller images are centered and shown using their intrinsic dimensions:

(ImageIO/read (URL. "https://nextjournal.com/data/QmSJ6eu6kUFeWrqXyYaiWRgJxAVQt2ivaoNWc1dtTEADCf?filename=thermo.png&content-type=image/png"))

๐Ÿ“’ Markdown

The same Markdown support Clerk uses for comment blocks is also available programmatically:

(clerk/md (clojure.string/join "\n" (map #(str "* Item " (inc %)) (range 3))))
  • Item 1
  • Item 2
  • Item 3

For a more advanced example of ingesting markdown files and transforming the content to HTML using Hiccup, see notebooks/markdown.md in the clerk-demo repo.

๐Ÿ”  Grid Layouts

Layouts can be composed via rows and cols

Passing :width, :height or any other style attributes to ::clerk/opts will assign them on the row or col that contains your items. You can use this to size your containers accordingly.

(clerk/row image-1 image-2 image-3)
(clerk/col {::clerk/opts {:width 150}} image-1 image-2 image-3)

Laying out stuff is not limited to images. You can use it to lay out any Clerk viewer. E.g. combine it with HTML viewers to render nice captions:

(defn caption [text]
(clerk/html [:span.text-slate-500.text-xs.text-center.font-sans text]))
#object[nextjournal.clerk.book$caption 0x5157098e "
nextjournal.clerk.book$caption@5157098e"
]
(clerk/row
(clerk/col image-1 (caption "Figure 1: Decorative A"))
(clerk/col image-2 (caption "Figure 2: Decorative B"))
(clerk/col image-3 (caption "Figure 3: Decorative C")))
Figure 1: Decorative A
Figure 2: Decorative B
Figure 3: Decorative C

Alternative notations

By default, row and col operate on & rest so you can pass any number of items to the functions. But the viewers are smart enough to accept any sequential list of items.

(v/row [image-1 image-2 image-3])

๐Ÿฑ Composing Viewers

Viewers compose, so, for example, you can lay out multiple independent Vega charts using Clerkโ€™s grid viewers:

show code
#'nextjournal.clerk.book/stock-chart
(clerk/col
(clerk/row (stock-chart "AAPL")
(stock-chart "AMZN")
(stock-chart "GOOG")
(stock-chart "IBM")
(stock-chart "MSFT"))
combined-stocks-chart)
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...

Viewers can also be embedded in Hiccup. The following example shows how this is used to provide a custom callout for a clerk/image.

(clerk/html
[:div.relative
(clerk/image "https://images.unsplash.com/photo-1608993659399-6508f918dfde?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=2070&q=80")
[:div.absolute
{:class "left-[25%] top-[21%]"}
[:div.border-4.border-emerald-400.rounded-full.shadow
{:class "w-8 h-8"}]
[:div.border-t-4.border-emerald-400.absolute
{:class "w-[80px] rotate-[30deg] left-4 translate-x-[10px] translate-y-[10px]"}]
[:div.border-4.border-emerald-400.absolute.text-white.font-sans.p-3.rounded-md
{:class "bg-black bg-opacity-60 text-[13px] w-[280px] top-[66px]"}
"Cat's paws are adapted to climbing and jumping, walking and running, and have protractible claws for self-defense and hunting."]]])
Cat's paws are adapted to climbing and jumping, walking and running, and have protractible claws for self-defense and hunting.

๐Ÿคน๐Ÿป Applying Viewers

Metadata Notation

In the examples above, we've used convience helper functions like clerk/html or clerk/plotly to wrap values in a viewer. If you call this on the REPL, you'll notice a given value gets wrapped in a map under the :nextjournal/value key with the viewer being in the :nextjournal/viewer key.

You can also select a viewer using Clojure metadata in order to avoid Clerk interfering with the value.

(def my-dataset
[{:temperature 41.0 :date (java.time.LocalDate/parse "2022-08-01")}
{:temperature 39.0 :date (java.time.LocalDate/parse "2022-08-01")}
{:temperature 34.0 :date (java.time.LocalDate/parse "2022-08-01")}
{:temperature 29.0 :date (java.time.LocalDate/parse "2022-08-01")}])
:temperature
:date
41#object[java.time.LocalDate 0x1dc28d86 "
2022-08-01"
]
39#object[java.time.LocalDate 0x10c88312 "
2022-08-01"
]
34#object[java.time.LocalDate 0x47ce7d4d "
2022-08-01"
]
29#object[java.time.LocalDate 0x1b5a3cfc "
2022-08-01"
]

As you can see above, the table viewer is being applied to the value of the my-dataset var, not the var itself. If you want your viewer to access the raw var, you can opt out of this with a truthy :var-from-def? key on the viewer.

(def raw-var :baz)
{:nextjournal.clerk/var-from-def (var nextjournal.clerk.book/raw-var)}

๐Ÿ‘ Writing Viewers

Let's explore how Clerk viewers work and how you create your own to gain better insight into your problem at hand.

v/default-viewers
[{:pred #object[clojure.core$char_QMARK___5425 0x772c170 "
clojure.core$char_QMARK___5425@772c170"
]
:render-fn (fn [c] [:span.cmt-string.inspected-value "
\"
c])}
{:closing-paren "
""
:opening-paren "
""
:page-size 80 :pred #object[clojure.core$string_QMARK___5427 0x4e9658b5 "
clojure.core$string_QMARK___5427@4e9658b5"
]
:render-fn nextjournal.clerk.render/render-quoted-string}
{:pred #object[clojure.core$number_QMARK_ 0x385dae6a "
clojure.core$number_QMARK_@385dae6a"
]
:render-fn nextjournal.clerk.render/render-number :transform-fn #object[nextjournal.clerk.viewer$update_val$fn__14291 0x127d2cac "
nextjournal.clerk.viewer$update_val$fn__14291@127d2cac"
]
}
{:name nextjournal.clerk.viewer/number-hex-viewer :render-fn (fn [num] (nextjournal.clerk.render/render-number (str "
0x"
(.toString (js/Number. num) 16))))}
{:pred #object[clojure.core$symbol_QMARK_ 0x70e0accd "
clojure.core$symbol_QMARK_@70e0accd"
]
:render-fn (fn [x] [:span.cmt-keyword.inspected-value (str x)])}
{:pred #object[clojure.core$keyword_QMARK_ 0x4a595315 "
clojure.core$keyword_QMARK_@4a595315"
]
:render-fn (fn [x] [:span.cmt-atom.inspected-value (str x)])}
{:pred #object[clojure.core$nil_QMARK_ 0x7359781c "
clojure.core$nil_QMARK_@7359781c"
]
:render-fn (fn [_] [:span.cmt-default.inspected-value "
nil"])}
{:pred #object[clojure.core$boolean_QMARK_ 0x7dac3fd8 "
clojure.core$boolean_QMARK_@7dac3fd8"
]
:render-fn (fn [x] [:span.cmt-bool.inspected-value (str x)])}
{:name nextjournal.clerk.viewer/map-entry-viewer :page-size 2 :pred #object[clojure.core$map_entry_QMARK_ 0x7fc0fdf0 "
clojure.core$map_entry_QMARK_@7fc0fdf0"
]
:render-fn (fn [xs opts] (into [:<>] (comp (nextjournal.clerk.render/inspect-children opts) (interpose "
"))
xs))}
{:pred #object[nextjournal.clerk.viewer$var_from_def_QMARK_ 0x7d130fe8 "
nextjournal.clerk.viewer$var_from_def_QMARK_@7d130fe8"
]
:transform-fn #object[nextjournal.clerk.viewer$update_val$fn__14291 0x4679fde2 "
nextjournal.clerk.viewer$update_val$fn__14291@4679fde2"
]
}
{:name nextjournal.clerk.viewer/read+inspect-viewer :render-fn (fn [x] (try [nextjournal.clerk.render/inspect (read-string x)] (catch js/Error _e (nextjournal.clerk.render/render-unreadable-edn x))))} {:closing-paren "
]"
:opening-paren "
["
:page-size 20 :pred #object[clojure.core$vector_QMARK___5431 0x5cf69333 "
clojure.core$vector_QMARK___5431@5cf69333"
]
:render-fn nextjournal.clerk.render/render-coll}
{:closing-paren "
}"
:opening-paren "
#{"
3 more elided}
{5 more elided} {4 more elided} {6 more elided} {3 more elided} {4 more elided} {3 more elided} {2 more elided} 21 more elided]

These are the default viewers that come with Clerk.

(into #{} (map type) v/default-viewers)
#{clojure.lang.PersistentArrayMap}

Each viewer is a simple Clojure map.

(assoc (frequencies (mapcat keys v/default-viewers)) :total (count v/default-viewers))
{:closing-paren 5 :name 24 :opening-paren 5 :page-size 7 :pred 20 :render-fn 33 :total 41 :transform-fn 25 :var-from-def? 1}

We have a total of 41 viewers in the defaults. Let's start with a simple example and explain the different extensions points in the viewer api.

๐ŸŽช Presentation

Clerk's rendering happens in the browser. On the Clojure-side, a given document is presented. Presenting takes a value and transforms it such that Clerk can send it to the browser where it will be rendered.

show code

Let's start with one of the simplest examples. You can see that present takes our value 1 and transforms it into a map, with 1 under a :nextjournal/value key and the number viewer assigned under the :nextjournal/viewer key. We call this map a wrapped-value.

(v/present 1)
{:path [],
:nextjournal/value 1,
:nextjournal/viewer
{:render-fn {:form nextjournal.clerk.render/render-number},
:hash "5dr5kWRtr7mCj9ikbmX2S4HCkE4Xxt"}}

This data structure is is sent over Clerk's websocket to the browser, where it will be displayed using the :render-fn found in the :nextjournal/viewer key.

Now onto something slightly more complex, #{1 2 3}.

(v/present #{1 2 3})
{:path [],
:nextjournal/value
[{:path [0],
:nextjournal/value 1,
:nextjournal/viewer
{:render-fn {:form nextjournal.clerk.render/render-number},
:hash "5dr5kWRtr7mCj9ikbmX2S4HCkE4Xxt"}}
{:path [1],
:nextjournal/value 2,
:nextjournal/viewer
{:render-fn {:form nextjournal.clerk.render/render-number},
:hash "5dr5kWRtr7mCj9ikbmX2S4HCkE4Xxt"}}
{:path [2],
:nextjournal/value 3,
:nextjournal/viewer
{:render-fn {:form nextjournal.clerk.render/render-number},
:hash "5dr5kWRtr7mCj9ikbmX2S4HCkE4Xxt"}}],
:nextjournal/viewer
{:render-fn {:form nextjournal.clerk.render/render-coll},
:opening-paren "#{",
:closing-paren ("}"),
:page-size 20,
:hash "5dsqjWFHxfJMj6muMSLAWx8pdnsF3M"}}

Here, we're giving it a set with 1, 2, 3 in it. In its generalized form, present is a function that does a depth-first traversal of a given tree, starting at the root node. It will select a viewer for this root node, and unless told otherwise, descend further down the tree to present its child nodes.

Compare this with the simple 1 example above! You should recognize the leaf values. Also note that the container is no longer a set, but it has been transformed into a vector. This transformation exists to support pagination of long unordered sequences like maps and sets and so we can efficiently access a value inside this tree using get-in.

You might ask yourself why we don't just send the unmodified value to the browser. For one, we could easily overload the browser with too much data. Secondly we will look at examples of being able to select viewers based on Clojure and Java types, which cannot be serialized and sent to the browser.

โš™๏ธ Transform

When writing your own viewer, the first extention point you should reach for is :tranform-fn.

(v/with-viewer {:transform-fn #(clerk/html [:pre (pr-str %)])}
"Exploring the viewer api")
{:nextjournal/value "Exploring the viewer api", :nextjournal/viewers [{:pred #object[sicmutils.expression$literal_QMARK_ 0x47a11047 "sicmutils.expression$literal_QMARK_@47a11047"], :transform-fn #object[clojure.core$comp$fn__5825 0x3f3c1e4d "clojure.core$comp$fn__5825@3f3c1e4d"], :render-fn (fn [label->val] (reagent.core/with-let [!selected-label (reagent.core/atom (ffirst label->val))] [:<> (into [:div.flex.items-center.font-sans.text-xs.mb-3 [:span.text-slate-500.mr-2 "View-as:"]] (map (fn [label] [:button.px-3.py-1.font-medium.hover:bg-indigo-50.rounded-full.hover:text-indigo-600.transition {:class (if (= (clojure.core/deref !selected-label) label) "bg-indigo-100 text-indigo-600" "text-slate-500"), :on-click (fn* [] (reset! !selected-label label))} label])) (keys label->val)) [nextjournal.clerk.render/inspect-presented (get label->val (clojure.core/deref !selected-label))]]))} {:pred #object[clojure.core$char_QMARK___5425 0x772c170 "clojure.core$char_QMARK___5425@772c170"], :render-fn (fn [c] [:span.cmt-string.inspected-value "\\" c])} {:pred #object[clojure.core$string_QMARK___5427 0x4e9658b5 "clojure.core$string_QMARK___5427@4e9658b5"], :render-fn nextjournal.clerk.render/render-quoted-string, :opening-paren "\"", :closing-paren "\"", :page-size 80} {:pred #object[clojure.core$number_QMARK_ 0x385dae6a "clojure.core$number_QMARK_@385dae6a"], :render-fn nextjournal.clerk.render/render-number, :transform-fn #object[nextjournal.clerk.viewer$update_val$fn__14291 0x127d2cac "nextjournal.clerk.viewer$update_val$fn__14291@127d2cac"]} {:name nextjournal.clerk.viewer/number-hex-viewer, :render-fn (fn [num] (nextjournal.clerk.render/render-number (str "0x" (.toString (js/Number. num) 16))))} {:pred #object[clojure.core$symbol_QMARK_ 0x70e0accd "clojure.core$symbol_QMARK_@70e0accd"], :render-fn (fn [x] [:span.cmt-keyword.inspected-value (str x)])} {:pred #object[clojure.core$keyword_QMARK_ 0x4a595315 "clojure.core$keyword_QMARK_@4a595315"], :render-fn (fn [x] [:span.cmt-atom.inspected-value (str x)])} {:pred #object[clojure.core$nil_QMARK_ 0x7359781c "clojure.core$nil_QMARK_@7359781c"], :render-fn (fn [_] [:span.cmt-default.inspected-value "nil"])} {:pred #object[clojure.core$boolean_QMARK_ 0x7dac3fd8 "clojure.core$boolean_QMARK_@7dac3fd8"], :render-fn (fn [x] [:span.cmt-bool.inspected-value (str x)])} {:pred #object[clojure.core$map_entry_QMARK_ 0x7fc0fdf0 "clojure.core$map_entry_QMARK_@7fc0fdf0"], :name nextjournal.clerk.viewer/map-entry-viewer, :render-fn (fn [xs opts] (into [:<>] (comp (nextjournal.clerk.render/inspect-children opts) (interpose " ")) xs)), :page-size 2} {:pred #object[nextjournal.clerk.viewer$var_from_def_QMARK_ 0x7d130fe8 "nextjournal.clerk.viewer$var_from_def_QMARK_@7d130fe8"], :transform-fn #object[nextjournal.clerk.viewer$update_val$fn__14291 0x4679fde2 "nextjournal.clerk.viewer$update_val$fn__14291@4679fde2"]} {:name nextjournal.clerk.viewer/read+inspect-viewer, :render-fn (fn [x] (try [nextjournal.clerk.render/inspect (read-string x)] (catch js/Error _e (nextjournal.clerk.render/render-unreadable-edn x))))} {:pred #object[clojure.core$vector_QMARK___5431 0x5cf69333 "clojure.core$vector_QMARK___5431@5cf69333"], :render-fn nextjournal.clerk.render/render-coll, :opening-paren "[", :closing-paren "]", :page-size 20} {:pred #object[clojure.core$set_QMARK_ 0x58d8059b "clojure.core$set_QMARK_@58d8059b"], :render-fn nextjournal.clerk.render/render-coll, :opening-paren "#{", :closing-paren "}", :page-size 20} {:pred #object[clojure.core$sequential_QMARK_ 0x38e10ff0 "clojure.core$sequential_QMARK_@38e10ff0"], :render-fn nextjournal.clerk.render/render-coll, :opening-paren "(", :closing-paren ")", :page-size 20} {:pred #object[nextjournal.clerk.viewer$viewer_eval_QMARK_ 0x327f1b25 "nextjournal.clerk.viewer$viewer_eval_QMARK_@327f1b25"], :var-from-def? true, :transform-fn #object[clojure.core$comp$fn__5825 0x17532d9e "clojure.core$comp$fn__5825@17532d9e"], :render-fn (fn [x opts] (if (nextjournal.clerk.render/reagent-atom? x) [nextjournal.clerk.render/render-tagged-value {:space? false} "#object" [nextjournal.clerk.render/inspect [(symbol (pr-str (type x))) (clojure.core/deref x)]]] [nextjournal.clerk.render/inspect x]))} {:pred #object[clojure.core$map_QMARK___5429 0x2317355b "clojure.core$map_QMARK___5429@2317355b"], :name nextjournal.clerk.viewer/map-viewer, :render-fn nextjournal.clerk.render/render-map, :opening-paren "{", :closing-paren "}", :page-size 10} {:pred #object[clojure.core$some_fn$sp1__8670 0x5bc073b4 "clojure.core$some_fn$sp1__8670@5bc073b4"], :transform-fn #object[clojure.core$comp$fn__5825 0x60e15be9 "clojure.core$comp$fn__5825@60e15be9"], :render-fn (fn [x] [:span.inspected-value [:span.cmt-meta "#'" (str x)]])} {:name nextjournal.clerk.viewer/throwable-viewer, :render-fn nextjournal.clerk.render/render-throwable, :pred #object[nextjournal.clerk.viewer$fn__14441 0x3ec95b6b "nextjournal.clerk.viewer$fn__14441@3ec95b6b"], :transform-fn #object[clojure.core$comp$fn__5825 0x68f0f3b5 "clojure.core$comp$fn__5825@68f0f3b5"]} {:pred #object[nextjournal.clerk.viewer$fn__14444 0x7425c9a4 "nextjournal.clerk.viewer$fn__14444@7425c9a4"], :transform-fn #object[nextjournal.clerk.viewer$fn__14447 0x37615690 "nextjournal.clerk.viewer$fn__14447@37615690"], :render-fn (fn [blob-or-url] [:div.flex.flex-col.items-center.not-prose [:img {:src (nextjournal.clerk.render/url-for blob-or-url)}]])} {:pred #object[nextjournal.clerk.viewer$fn__14451 0x6e829796 "nextjournal.clerk.viewer$fn__14451@6e829796"], :transform-fn #object[nextjournal.clerk.viewer$update_val$fn__14291 0x3950a23f "nextjournal.clerk.viewer$update_val$fn__14291@3950a23f"]} {:pred #object[clojure.core$partial$fn__5857 0x36871421 "clojure.core$partial$fn__5857@36871421"], :transform-fn #object[nextjournal.clerk.viewer$fn__14456 0x538498ef "nextjournal.clerk.viewer$fn__14456@538498ef"]} {:pred #object[clojure.core$constantly$fn__5689 0x42e10399 "clojure.core$constantly$fn__5689@42e10399"], :transform-fn #object[nextjournal.clerk.viewer$update_val$fn__14291 0x3c86a471 "nextjournal.clerk.viewer$update_val$fn__14291@3c86a471"]} {:name nextjournal.clerk.viewer/elision-viewer, :render-fn nextjournal.clerk.render/render-elision, :transform-fn #object[nextjournal.clerk.viewer$mark_presented 0x1eea0a7 "nextjournal.clerk.viewer$mark_presented@1eea0a7"]} {:name nextjournal.clerk.viewer/katex-viewer, :render-fn nextjournal.clerk.render/render-katex, :transform-fn #object[nextjournal.clerk.viewer$mark_presented 0x1eea0a7 "nextjournal.clerk.viewer$mark_presented@1eea0a7"]} {:name nextjournal.clerk.viewer/mathjax-viewer, :render-fn nextjournal.clerk.render/render-mathjax, :transform-fn #object[nextjournal.clerk.viewer$mark_presented 0x1eea0a7 "nextjournal.clerk.viewer$mark_presented@1eea0a7"]} {:name nextjournal.clerk.viewer/html-viewer, :render-fn identity, :transform-fn #object[clojure.core$comp$fn__5825 0x3d93abff "clojure.core$comp$fn__5825@3d93abff"]} {:name nextjournal.clerk.viewer/plotly-viewer, :render-fn nextjournal.clerk.render/render-plotly, :transform-fn #object[nextjournal.clerk.viewer$mark_presented 0x1eea0a7 "nextjournal.clerk.viewer$mark_presented@1eea0a7"]} {:name nextjournal.clerk.viewer/vega-lite-viewer, :render-fn nextjournal.clerk.render/render-vega-lite, :transform-fn #object[nextjournal.clerk.viewer$mark_presented 0x1eea0a7 "nextjournal.clerk.viewer$mark_presented@1eea0a7"]} {:name nextjournal.clerk.viewer/markdown-viewer, :transform-fn #object[nextjournal.clerk.viewer$fn__14473 0x4aa71f42 "nextjournal.clerk.viewer$fn__14473@4aa71f42"]} {:name nextjournal.clerk.viewer/code-viewer, :render-fn nextjournal.clerk.render/render-code, :transform-fn #object[clojure.core$comp$fn__5825 0x430b8f4b "clojure.core$comp$fn__5825@430b8f4b"]} {:name nextjournal.clerk.viewer/code-folded-viewer, :render-fn nextjournal.clerk.render/render-folded-code, :transform-fn #object[clojure.core$comp$fn__5825 0x3714e415 "clojure.core$comp$fn__5825@3714e415"]} {:name nextjournal.clerk.viewer/reagent-viewer, :render-fn nextjournal.clerk.render/render-reagent, :transform-fn #object[nextjournal.clerk.viewer$mark_presented 0x1eea0a7 "nextjournal.clerk.viewer$mark_presented@1eea0a7"]} {:name nextjournal.clerk.viewer/row-viewer, :render-fn (fn [items opts] (let [item-count (count items)] (into [:div {:class "md:flex md:flex-row md:gap-4 not-prose", :style opts}] (map (fn [item] [:div.flex.items-center.justify-center.flex-auto (nextjournal.clerk.render/inspect-presented opts item)])) items)))} {:name nextjournal.clerk.viewer/col-viewer, :render-fn (fn [items opts] (into [:div {:class "md:flex md:flex-col md:gap-4 clerk-grid not-prose", :style opts}] (map (fn [item] [:div.flex.items-center.justify-center (nextjournal.clerk.render/inspect-presented opts item)])) items))} {:name nextjournal.clerk.viewer/table-viewer, :transform-fn #object[nextjournal.clerk.viewer$fn__14488 0x17f87d5b "nextjournal.clerk.viewer$fn__14488@17f87d5b"]} {:name nextjournal.clerk.viewer/table-error-viewer, :render-fn nextjournal.clerk.render/render-table-error, :page-size 1} {:name nextjournal.clerk.viewer/code-block-viewer, :transform-fn #object[nextjournal.clerk.viewer$fn__14500 0x65572de9 "nextjournal.clerk.viewer$fn__14500@65572de9"]} {:name nextjournal.clerk.viewer/result-viewer, :render-fn nextjournal.clerk.render/render-result, :transform-fn #object[clojure.core$comp$fn__5825 0x7ab997df "clojure.core$comp$fn__5825@7ab997df"]} {:name nextjournal.clerk.viewer/tagged-value-viewer, :render-fn (fn [{:keys [tag value space?]} opts] (nextjournal.clerk.render/render-tagged-value {:space? (:nextjournal/value space?)} (str "#" (:nextjournal/value tag)) [nextjournal.clerk.render/inspect-presented value])), :transform-fn #object[nextjournal.clerk.viewer$mark_preserve_keys 0x6a1de0d1 "nextjournal.clerk.viewer$mark_preserve_keys@6a1de0d1"]} {:name nextjournal.clerk.viewer/notebook-viewer, :render-fn nextjournal.clerk.render/render-notebook, :transform-fn #object[nextjournal.clerk.viewer$fn__14526 0x3edb4a6c "nextjournal.clerk.viewer$fn__14526@3edb4a6c"]} {:name nextjournal.clerk.viewer/hide-result-viewer, :transform-fn #object[nextjournal.clerk.viewer$fn__14342 0x62bf103b "nextjournal.clerk.viewer$fn__14342@62bf103b"]}], :nextjournal/budget 200, :store!-wrapped-value #object[nextjournal.clerk.viewer$present$fn__14679 0x540e74dc "nextjournal.clerk.viewer$present$fn__14679@540e74dc"], :present-elision-fn #object[clojure.core$partial$fn__5857 0x51101de4 "clojure.core$partial$fn__5857@51101de4"], :path [], :!budget #object[clojure.lang.Atom 0x5979bfac {:status :ready, :val 200}]}

As you can see the argument to the :transform-fn isn't just the string we're passing it, but a wrapped-value. We will look at what this enables in a bit. But let's look at one of the simplest examples first.

A first simple example

(def greet-viewer
{:transform-fn (clerk/update-val #(clerk/html [:strong "Hello, " % " ๐Ÿ‘‹"]))})
{:transform-fn #object[nextjournal.clerk.viewer$update_val$fn__14291 0x9f941a6 "
nextjournal.clerk.viewer$update_val$fn__14291@9f941a6"
]
}

For this simple greet-viewer we're only doing a simple value transformation. For this, clerk/update-val is a small helper function which takes a function f and returns a function to update only the value inside a wrapped-value, a shorthand for #(update % :nextjournal/val f)

(v/with-viewer greet-viewer
"James Clerk Maxwell")
Hello, James Clerk Maxwell ๐Ÿ‘‹

The :transform-fn runs on the JVM, which means you can explore what it does at your REPL by calling v/present on such a value.

(v/present (v/with-viewer greet-viewer
"James Clerk Maxwell"))
{:path [],
:nextjournal/value [:strong "Hello, " "James Clerk Maxwell" " ๐Ÿ‘‹"],
:nextjournal/viewer
{:name nextjournal.clerk.viewer/html-viewer,
:render-fn {:form identity},
:hash "5du5eveS8XMqMDMYH5rpQ1bqryEJdg"}}

Passing modified viewers down the tree

(defn add-child-viewers [viewer viewers]
(update viewer :transform-fn (fn [transform-fn-orig]
(fn [wrapped-value]
(update (transform-fn-orig wrapped-value) :nextjournal/viewers clerk/add-viewers viewers)))))
#object[nextjournal.clerk.book$add_child_viewers 0x6974e510 "
nextjournal.clerk.book$add_child_viewers@6974e510"
]
v/table-viewer
{:name nextjournal.clerk.viewer/table-viewer :transform-fn #object[nextjournal.clerk.viewer$fn__14488 0x17f87d5b "
nextjournal.clerk.viewer$fn__14488@17f87d5b"
]
}
(def custom-table-viewer
(add-child-viewers v/table-viewer
[(assoc v/table-head-viewer :transform-fn (v/update-val (partial map (comp (partial str "Column: ") str/capitalize name))))
(assoc v/table-missing-viewer :render-fn '(fn [x] [:span.red "N/A"]))]))
{:name nextjournal.clerk.viewer/table-viewer :transform-fn #object[nextjournal.clerk.book$add_child_viewers$fn__46731$fn__46732 0x4aa6341c "
nextjournal.clerk.book$add_child_viewers$fn__46731$fn__46732@4aa6341c"
]
}
(clerk/with-viewer custom-table-viewer
{:col/a [1 2 3 4] :col/b [1 2 3] :col/c [1 2 3]})
Column: A
Column: B
Column: C
111
222
333
4N/AN/A
(clerk/with-viewer custom-table-viewer
{:col/a [1 2 3 4] :col/b [1 2 3] :col/c [1 2 3]})
Column: A
Column: B
Column: C
111
222
333
4N/AN/A

๐Ÿข Recursion

But this presentation and hence tranformation of nodes further down the tree isn't always what you want. For example, the plotly or vl viewers want to receive the child value unaltered in order to use it as a spec.

To stop Clerk's presentation from descending into child nodes, use clerk/mark-presented as a :transform-fn. Compare the result below in which [1 2 3] appears unaltered with what you see above.

(v/present (clerk/with-viewer {:transform-fn clerk/mark-presented
:render-fn '(fn [x] [:pre (pr-str x)])}
[1 2 3]))
{:path [],
:nextjournal/value [1 2 3],
:nextjournal/viewer
{:render-fn {:form (fn [x] [:pre (pr-str x)])},
:hash "5drrcyM7H3mSPam5pRxc7darH4gVKZ"}}

Clerk's presentation will also transform maps into sequences in order to paginate large maps. When you're dealing with a map that you know is bounded and would like to preserve its keys, there's clerk/mark-preserve-keys. This will still transform (and paginate) the values of the map, but leave the keys unaltered.

(v/present (clerk/with-viewer {:transform-fn clerk/mark-preserve-keys}
{:hello 42}))
{:path [],
:nextjournal/value
{:hello
{:path [:hello],
:nextjournal/value 42,
:nextjournal/viewer
{:render-fn {:form nextjournal.clerk.render/render-number},
:hash "5dr5kWRtr7mCj9ikbmX2S4HCkE4Xxt"}}},
:nextjournal/viewer
{:name nextjournal.clerk.viewer/map-viewer,
:render-fn {:form nextjournal.clerk.render/render-map},
:opening-paren "{",
:closing-paren ("}"),
:page-size 10,
:hash "5drc3ac5ux7kS4h2vozdxwsrKDqSmn"}}

๐Ÿ”ฌ Render

As we've just seen, you can also do a lot with :transform-fn and using clerk/html on the JVM. When you want to run code in the browser where Clerk's viewers are rendered, reach for :render-fn. As an example, we'll write a multiviewer for a sicmutils literal expression that will compute two alternative representations and let the user switch between them in the browser.

We start with a simple function that takes a such an expression and turns it into a map with two representation, one TeX and the original form.

(defn transform-literal [expr]
{:TeX (-> expr sicm/->TeX clerk/tex)
:original (clerk/code (with-out-str (sicm/print-expression (sicm/freeze expr))))})
#object[nextjournal.clerk.book$transform_literal 0x7579be99 "
nextjournal.clerk.book$transform_literal@7579be99"
]

Our literal-viewer calls this transform-literal function and also calls clerk/mark-preserve-keys. This tells Clerk to leave the keys of the map as-is.

In our :render-fn, which is called in the browser we will recieve this map. Note that this is a quoted form, not a function. Clerk will send this form to the browser for evaluation. There it will create a reagent/atom that holds the selection state. Lastly, v/inspect-presented is a component that takes a wrapped-value that ran through v/present and show it.

(def literal-viewer
{:pred sicmutils.expression/literal?
:transform-fn (comp clerk/mark-preserve-keys
(clerk/update-val transform-literal))
:render-fn '(fn [label->val]
(reagent.core/with-let [!selected-label (reagent.core/atom (ffirst label->val))]
[:<> (into
[:div.flex.items-center.font-sans.text-xs.mb-3
[:span.text-slate-500.mr-2 "View-as:"]]
(map (fn [label]
[:button.px-3.py-1.font-medium.hover:bg-indigo-50.rounded-full.hover:text-indigo-600.transition
{:class (if (= @!selected-label label) "bg-indigo-100 text-indigo-600" "text-slate-500")
:on-click #(reset! !selected-label label)}
label]))
(keys label->val))
[nextjournal.clerk.render/inspect-presented (get label->val @!selected-label)]]))})
{:pred #object[sicmutils.expression$literal_QMARK_ 0x47a11047 "
sicmutils.expression$literal_QMARK_@47a11047"
]
:render-fn (fn [label->val] (reagent.core/with-let [!selected-label (reagent.core/atom (ffirst label->val))] [:<> (into [:div.flex.items-center.font-sans.text-xs.mb-3 [:span.text-slate-500.mr-2 "
View-as:"]]
(map (fn [label] [:button.px-3.py-1.font-medium.hover:bg-indigo-50.rounded-full.hover:text-indigo-600.transition {:class (if (= (clojure.core/deref !selected-label) label) "
bg-indigo-100 text-indigo-600"
"
text-slate-500")
:on-click (fn* [] (reset! !selected-label label))}
label]))
(keys label->val))
[nextjournal.clerk.render/inspect-presented (get label->val (clojure.core/deref !selected-label))]]))
:transform-fn #object[clojure.core$comp$fn__5825 0x3f3c1e4d "
clojure.core$comp$fn__5825@3f3c1e4d"
]
}

Now let's see if this works. Try switching to the original representation!

(sicm/+ (sicm/square (sicm/sin 'x))
(sicm/square (sicm/cos 'x)))
View-as:
Loading...

๐Ÿฅ‡ Selection

Without a viewer specified, Clerk will go through the sequence of viewers and apply the :pred function in the viewer to find a matching one. Use v/viewer-for to select a viewer for a given value.

(def char?-viewer
(v/viewer-for v/default-viewers \A))
{:pred #object[clojure.core$char_QMARK___5425 0x772c170 "
clojure.core$char_QMARK___5425@772c170"
]
:render-fn (fn [c] [:span.cmt-string.inspected-value "
\"
c])}

If we select a specific viewer (here the v/html-viewer using clerk/html) this is the viewer we will get.

(def html-viewer
(v/viewer-for v/default-viewers (clerk/html [:h1 "foo"])))
{:name nextjournal.clerk.viewer/html-viewer :render-fn identity :transform-fn #object[clojure.core$comp$fn__5825 0x3d93abff "
clojure.core$comp$fn__5825@3d93abff"
]
}

Instead of specifying a viewer for every value, we can also modify the viewers per namespace. Here, we add the literal-viewer from above to the whole namespace.

(clerk/add-viewers! [literal-viewer])
[{:pred #object[sicmutils.expression$literal_QMARK_ 0x47a11047 "
sicmutils.expression$literal_QMARK_@47a11047"
]
:render-fn (fn [label->val] (reagent.core/with-let [!selected-label (reagent.core/atom (ffirst label->val))] [:<> (into [:div.flex.items-center.font-sans.text-xs.mb-3 [:span.text-slate-500.mr-2 "
View-as:"]]
(map (fn [label] [:button.px-3.py-1.font-medium.hover:bg-indigo-50.rounded-full.hover:text-indigo-600.transition {:class (if (= (clojure.core/deref !selected-label) label) "
bg-indigo-100 text-indigo-600"
"
text-slate-500")
:on-click (fn* [] (reset! !selected-label label))}
label]))
(keys label->val))
[nextjournal.clerk.render/inspect-presented (get label->val (clojure.core/deref !selected-label))]]))
:transform-fn #object[clojure.core$comp$fn__5825 0x3f3c1e4d "
clojure.core$comp$fn__5825@3f3c1e4d"
]
}
{:pred #object[clojure.core$char_QMARK___5425 0x772c170 "
clojure.core$char_QMARK___5425@772c170"
]
:render-fn (fn [c] [:span.cmt-string.inspected-value "
\"
c])}
{:closing-paren "
""
:opening-paren "
""
:page-size 80 :pred #object[clojure.core$string_QMARK___5427 0x4e9658b5 "
clojure.core$string_QMARK___5427@4e9658b5"
]
:render-fn nextjournal.clerk.render/render-quoted-string}
{:pred #object[clojure.core$number_QMARK_ 0x385dae6a "
clojure.core$number_QMARK_@385dae6a"
]
:render-fn nextjournal.clerk.render/render-number :transform-fn #object[nextjournal.clerk.viewer$update_val$fn__14291 0x127d2cac "
nextjournal.clerk.viewer$update_val$fn__14291@127d2cac"
]
}
{:name nextjournal.clerk.viewer/number-hex-viewer :render-fn (fn [num] (nextjournal.clerk.render/render-number (str "
0x"
(.toString (js/Number. num) 16))))}
{:pred #object[clojure.core$symbol_QMARK_ 0x70e0accd "
clojure.core$symbol_QMARK_@70e0accd"
]
:render-fn (fn [x] [:span.cmt-keyword.inspected-value (str x)])}
{:pred #object[clojure.core$keyword_QMARK_ 0x4a595315 "
clojure.core$keyword_QMARK_@4a595315"
]
:render-fn (fn [x] [:span.cmt-atom.inspected-value (str x)])}
{:pred #object[clojure.core$nil_QMARK_ 0x7359781c "
clojure.core$nil_QMARK_@7359781c"
]
:render-fn (fn [_] [:span.cmt-default.inspected-value "
nil"])}
{:pred #object[clojure.core$boolean_QMARK_ 0x7dac3fd8 "
clojure.core$boolean_QMARK_@7dac3fd8"
]
:render-fn (fn [x] [:span.cmt-bool.inspected-value (str x)])}
{:name nextjournal.clerk.viewer/map-entry-viewer :page-size 2 :pred #object[clojure.core$map_entry_QMARK_ 0x7fc0fdf0 "
clojure.core$map_entry_QMARK_@7fc0fdf0"
]
:render-fn (3 more elided)}
{2 more elided} {2 more elided} {5 more elided} {5 more elided} {5 more elided} {4 more elided} {6 more elided} {3 more elided} {4 more elided} {3 more elided} 22 more elided]

As you can see we now get this viewer automatically, without needing to explicitly select it.

(sicm/+ (sicm/square (sicm/sin 'x))
(sicm/square (sicm/cos 'x)))
View-as:
Loading...

๐Ÿ”“ Elisions

(def string?-viewer
(v/viewer-for v/default-viewers "Denn wir sind wie Baumstรคmme im Schnee."))
{:closing-paren "
""
:opening-paren "
""
:page-size 80 :pred #object[clojure.core$string_QMARK___5427 0x4e9658b5 "
clojure.core$string_QMARK___5427@4e9658b5"
]
:render-fn nextjournal.clerk.render/render-quoted-string}

Notice that for the string? viewer above, there's a :page-size of 80. This is the case for all collection viewers in Clerk and controls how many elements are displayed. So using the default string?-viewer above, we're showing the first 80 characters.

(def long-string
(str/join (into [] cat (repeat 10 "Denn wir sind wie Baumstรคmme im Schnee.\n"))))
"
Denn wir sind wie Baumstรคmme im Schnee.โ†ฉ๏ธŽDenn wir sind wie Baumstรคmme im Schnee.320 more elided"

If we change the viewer and set a different :n in :page-size, we only see 10 characters.

(v/with-viewer (assoc string?-viewer :page-size 10)
long-string)
"
Denn wir s390 more elided"

Or, we can turn off eliding, by dissoc'ing :page-size alltogether.

(v/with-viewer (dissoc string?-viewer :page-size)
long-string)
"
Denn wir sind wie Baumstรคmme im Schnee.โ†ฉ๏ธŽDenn wir sind wie Baumstรคmme im Schnee.โ†ฉ๏ธŽDenn wir sind wie Baumstรคmme im Schnee.โ†ฉ๏ธŽDenn wir sind wie Baumstรคmme im Schnee.โ†ฉ๏ธŽDenn wir sind wie Baumstรคmme im Schnee.โ†ฉ๏ธŽDenn wir sind wie Baumstรคmme im Schnee.โ†ฉ๏ธŽDenn wir sind wie Baumstรคmme im Schnee.โ†ฉ๏ธŽDenn wir sind wie Baumstรคmme im Schnee.โ†ฉ๏ธŽDenn wir sind wie Baumstรคmme im Schnee.โ†ฉ๏ธŽDenn wir sind wie Baumstรคmme im Schnee."

The operations above were changes to a single viewer. But we also have a function update-viewers to update a given viewers by applying a select-fn->update-fn map. Here, the predicate is the keyword :page-size and our update function is called for every viewer with :page-size and is dissoc'ing them.

(def without-pagination
{:page-size #(dissoc % :page-size)})
{:page-size #object[nextjournal.clerk.book$fn__46754 0x3fe17b54 "
nextjournal.clerk.book$fn__46754@3fe17b54"
]
}

Here's the updated-viewers:

(def viewers-without-lazy-loading
(v/update-viewers v/default-viewers without-pagination))
[{:pred #object[clojure.core$char_QMARK___5425 0x772c170 "
clojure.core$char_QMARK___5425@772c170"
]
:render-fn (fn [c] [:span.cmt-string.inspected-value "
\"
c])}
{:closing-paren "
""
:opening-paren "
""
:pred #object[clojure.core$string_QMARK___5427 0x4e9658b5 "
clojure.core$string_QMARK___5427@4e9658b5"
]
:render-fn nextjournal.clerk.render/render-quoted-string}
{:pred #object[clojure.core$number_QMARK_ 0x385dae6a "
clojure.core$number_QMARK_@385dae6a"
]
:render-fn nextjournal.clerk.render/render-number :transform-fn #object[nextjournal.clerk.viewer$update_val$fn__14291 0x127d2cac "
nextjournal.clerk.viewer$update_val$fn__14291@127d2cac"
]
}
{:name nextjournal.clerk.viewer/number-hex-viewer :render-fn (fn [num] (nextjournal.clerk.render/render-number (str "
0x"
(.toString (js/Number. num) 16))))}
{:pred #object[clojure.core$symbol_QMARK_ 0x70e0accd "
clojure.core$symbol_QMARK_@70e0accd"
]
:render-fn (fn [x] [:span.cmt-keyword.inspected-value (str x)])}
{:pred #object[clojure.core$keyword_QMARK_ 0x4a595315 "
clojure.core$keyword_QMARK_@4a595315"
]
:render-fn (fn [x] [:span.cmt-atom.inspected-value (str x)])}
{:pred #object[clojure.core$nil_QMARK_ 0x7359781c "
clojure.core$nil_QMARK_@7359781c"
]
:render-fn (fn [_] [:span.cmt-default.inspected-value "
nil"])}
{:pred #object[clojure.core$boolean_QMARK_ 0x7dac3fd8 "
clojure.core$boolean_QMARK_@7dac3fd8"
]
:render-fn (fn [x] [:span.cmt-bool.inspected-value (str x)])}
{:name nextjournal.clerk.viewer/map-entry-viewer :pred #object[clojure.core$map_entry_QMARK_ 0x7fc0fdf0 "
clojure.core$map_entry_QMARK_@7fc0fdf0"
]
:render-fn (fn [xs opts] (into [:<>] (comp (nextjournal.clerk.render/inspect-children opts) (interpose "
"))
xs))}
{:pred #object[nextjournal.clerk.viewer$var_from_def_QMARK_ 0x7d130fe8 "
nextjournal.clerk.viewer$var_from_def_QMARK_@7d130fe8"
]
:transform-fn #object[nextjournal.clerk.viewer$update_val$fn__14291 0x4679fde2 "
nextjournal.clerk.viewer$update_val$fn__14291@4679fde2"
]
}
{:name nextjournal.clerk.viewer/read+inspect-viewer :render-fn (fn [x] (try [nextjournal.clerk.render/inspect (read-string x)] (catch js/Error _e (nextjournal.clerk.render/render-unreadable-edn x))))} {:closing-paren "
]"
:opening-paren "
["
:pred #object[clojure.core$vector_QMARK___5431 0x5cf69333 "
clojure.core$vector_QMARK___5431@5cf69333"
]
:render-fn nextjournal.clerk.render/render-coll}
{:closing-paren "
}"
:opening-paren "
#{"
:pred #object[clojure.core$set_QMARK_ 0x58d8059b "
clojure.core$set_QMARK_@58d8059b"
]
:render-fn nextjournal.clerk.render/render-coll}
{4 more elided} {4 more elided} {5 more elided} {3 more elided} {4 more elided} {3 more elided} {2 more elided} 21 more elided]

Now let's confirm these modified viewers don't have :page-size on them anymore.

(filter :page-size viewers-without-lazy-loading)
()

And compare it with the defaults:

(filter :page-size v/default-viewers)
({:closing-paren "
""
:opening-paren "
""
:page-size 80 :pred #object[clojure.core$string_QMARK___5427 0x4e9658b5 "
clojure.core$string_QMARK___5427@4e9658b5"
]
:render-fn nextjournal.clerk.render/render-quoted-string}
{:name nextjournal.clerk.viewer/map-entry-viewer :page-size 2 :pred #object[clojure.core$map_entry_QMARK_ 0x7fc0fdf0 "
clojure.core$map_entry_QMARK_@7fc0fdf0"
]
:render-fn (fn [xs opts] (into [:<>] (comp (nextjournal.clerk.render/inspect-children opts) (interpose "
"))
xs))}
{:closing-paren "
]"
:opening-paren "
["
:page-size 20 :pred #object[clojure.core$vector_QMARK___5431 0x5cf69333 "
clojure.core$vector_QMARK___5431@5cf69333"
]
:render-fn nextjournal.clerk.render/render-coll}
{:closing-paren "
}"
:opening-paren "
#{"
:page-size 20 :pred #object[clojure.core$set_QMARK_ 0x58d8059b "
clojure.core$set_QMARK_@58d8059b"
]
:render-fn nextjournal.clerk.render/render-coll}
{:closing-paren "
)"
:opening-paren "
("
:page-size 20 :pred #object[clojure.core$sequential_QMARK_ 0x38e10ff0 "
clojure.core$sequential_QMARK_@38e10ff0"
]
:render-fn nextjournal.clerk.render/render-coll}
{:closing-paren "
}"
:name nextjournal.clerk.viewer/map-viewer :opening-paren "
{"
:page-size 10 :pred #object[clojure.core$map_QMARK___5429 0x2317355b "
clojure.core$map_QMARK___5429@2317355b"
]
:render-fn nextjournal.clerk.render/render-map}
{:name nextjournal.clerk.viewer/table-error-viewer :page-size 1 :render-fn nextjournal.clerk.render/render-table-error})

Now let's display our clojure-data var from above using these modified viewers.

(clerk/with-viewers viewers-without-lazy-loading
clojure-data)
{:hello "
world ๐Ÿ‘‹"
:tacos ((๐ŸŒฎ) (๐ŸŒฎ ๐ŸŒฎ) (๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ) (๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ) (๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ) (๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ) (๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ) (๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ) (๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ) (๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ) (๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ) (๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ) (๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ) (๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ) (๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ) (๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ) (๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ) (๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ) (๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ) (๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ) (๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ) (๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ) (๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ) (๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ) (๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ) (๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ) (๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ) (๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ) (๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ ๐ŸŒฎ)) :zeta "
Theโ†ฉ๏ธŽpurposeโ†ฉ๏ธŽofโ†ฉ๏ธŽvisualizationโ†ฉ๏ธŽisโ†ฉ๏ธŽinsight,โ†ฉ๏ธŽnotโ†ฉ๏ธŽpictures."}

๐Ÿ‘ท Loading Libraries

This is a custom viewer for Mermaid, a markdown-like syntax for creating diagrams from text. Note that this library isn't bundles with Clerk but we use a component based on d3-require to load it at runtime.

(def mermaid-viewer
{:transform-fn clerk/mark-presented
:render-fn '(fn [value]
(when value
[nextjournal.clerk.render/with-d3-require {:package ["mermaid@8.14/dist/mermaid.js"]}
(fn [mermaid]
[:div {:ref (fn [el] (when el
(.render mermaid (str (gensym)) value #(set! (.-innerHTML el) %))))}])]))})
{:render-fn (fn [value] (when value [nextjournal.clerk.render/with-d3-require {:package ["
mermaid@8.14/dist/mermaid.js"]}
(fn [mermaid] [:div {:ref (fn [el] (when el (.render mermaid (str (gensym)) value (fn* [%1] (set! (.-innerHTML el) %1)))))}])]))
:transform-fn #object[nextjournal.clerk$mark_presented 0x65f6a9f8 "
nextjournal.clerk$mark_presented@65f6a9f8"
]
}

We can then use the above viewer using with-viewer.

(clerk/with-viewer mermaid-viewer
"stateDiagram-v2
[*] --> Still
Still --> [*]
Still --> Moving
Moving --> Still
Moving --> Crash
Crash --> [*]")
Loading...

โš™๏ธ Customizations

Clerk allows easy customization of visibility, result width and budget. All settings can be applied document-wide using ns metadata or a top-level settings marker and per form using metadata.

Let's start with a concrete example to understand how this works.

๐Ÿ™ˆ Visibility

By default, Clerk will always show code and results for a notebook.

You can use a map of the following shape to set the visibility of code and results individually:

{:nextjournal.clerk/visibility {:code :hide :result :show}}

The above example will hide the code and show only results.

Valid values are :show, :hide and :fold (only available for :code). Using {:code :fold} will hide the code cell initially but show an indicator to toggle its visibility:

show code
[8 4 6 17 19 2 18 9 23 11 14 24 21 7 3 5 13 12 15 22 5 more elided]

The visibility map can be used in the following ways:

Set document defaults via the ns form

(ns visibility
{:nextjournal.clerk/visibility {:code :fold}})

The above example will hide all code cells by default but show an indicator to toggle their visibility instead. In this case results will always show because thatโ€™s the default.

As metadata to control a single top-level form

^{::clerk/visibility {:code :hide}} (shuffle (range 25))

This will hide the code but only show the result:

[8 4 6 17 19 2 18 9 23 11 14 24 21 7 3 5 13 12 15 22 5 more elided]

Setting visibility as metadata will override the document-wide visibility settings for this one specific form.

As top-level form to change the document defaults

Independently of what defaults are set via your ns form, you can use a top-level map as a marker to override the visibility settings for any forms following it.

Example: Code is hidden by default but you want to show code for all top-level forms after a certain point:

(ns visibility
{:nextjournal.clerk/visibility {:code :hide}})
(+ 39 3) ;; code will be hidden
(range 25) ;; code will be hidden
{:nextjournal.clerk/visibility {:code :show}}
(range 500) ;; code will be visible
(rand-int 42) ;; code will be visible

This comes in quite handy for debugging too!

๐Ÿฝ Table of Contents

If you want a table of contents like the one in this document, set the :nextjournal.clerk/toc option.

(ns doc-with-table-of-contents
{:nextjournal.clerk/toc true})

If you want it to be collapsed initially, use :collapsed as a value.

๐Ÿ”ฎ Result Expansion

If you want to better see the shape of your data without needing to click and expand it first, set the :nextjournal.clerk/auto-expand-results? option.

({:dice [2 4 6 1 5 3] :name "
Karen Meyer"
:role :admin}
{:dice [1 4 6 5 2 3] :name "
Karen Stasฤnyk"
:role :designer}
{:dice [4 6 2 3 1 5] :name "
Vlad Stasฤnyk"
:role :programmer}
{:dice [3 6 4 5 1 2] :name "
Karen Miller"
:role :programmer}
{:dice [3 6 2 1 5 4] :name "
Oscar Ronin"
:role :admin}
{:dice [4 5 6 2 3 1] :name "
Oscar Black"
:role :programmer}
{:dice [4 5 2 3 1 6] :name "
Vlad Meyer"
:role :designer}
{:dice [3 5 1 4 2 6] :name "
Rebecca Black"
:role :programmer}
{:dice [6 1 2 4 3 5] :name "
Conrad Black"
:role :operator}
{:dice [1 5 6 3 2 4] :name "
Rebecca Black"
:role :designer}
{:dice [2 1 5 6 4 3] :name "
Rebecca Stasฤnyk"
:role :admin}
{:dice [6 3 1 5 4 2] :name "
Karen Meyer"
:role :operator}
{:dice [1 4 3 3 more elided] :name "
Oscar Meyer"
:role :operator}
{3 more elided} {3 more elided})

This option might become the default in the future.

๐Ÿ™…๐Ÿผโ€โ™‚๏ธ Viewer Budget

In order to not send too much data to the browser, Clerk uses a per-result budget to limit. You can see this budget in action above. Use the :nextjournal.clerk/budget key to change its default value of 200 or disable it completely using nil.

({:dice [2 4 6 1 5 3] :name "
Karen Meyer"
:role :admin}
{:dice [1 4 6 5 2 3] :name "
Karen Stasฤnyk"
:role :designer}
{:dice [4 6 2 3 1 5] :name "
Vlad Stasฤnyk"
:role :programmer}
{:dice [3 6 4 5 1 2] :name "
Karen Miller"
:role :programmer}
{:dice [3 6 2 1 5 4] :name "
Oscar Ronin"
:role :admin}
{:dice [4 5 6 2 3 1] :name "
Oscar Black"
:role :programmer}
{:dice [4 5 2 3 1 6] :name "
Vlad Meyer"
:role :designer}
{:dice [3 5 1 4 2 6] :name "
Rebecca Black"
:role :programmer}
{:dice [6 1 2 4 3 5] :name "
Conrad Black"
:role :operator}
{:dice [1 5 6 3 2 4] :name "
Rebecca Black"
:role :designer}
{:dice [2 1 5 6 4 3] :name "
Rebecca Stasฤnyk"
:role :admin}
{:dice [6 3 1 5 4 2] :name "
Karen Meyer"
:role :operator}
{:dice [1 4 3 6 5 2] :name "
Oscar Meyer"
:role :operator}
{:dice [6 3 4 1 2 5] :name "
Vlad Ronin"
:role :designer}
{:dice [3 5 6 4 1 2] :name "
Karen Miller"
:role :programmer})

๐Ÿงฑ Static Building

Clerk can make a static HTML build from a collection of notebooks. The entry point for this is the nextjournal.clerk/build! function. You can pass it a set of notebooks via the :paths option (also supporting glob patterns).

When Clerk is building multuple notebooks, it will automatically generate an index page that will be the first to show up when opening the build. You can override this index page via the :index option.

Also notably, there is a :compile-css option which compiles a css file containing only the used CSS classes from the generated markup. (Otherwise, Clerk is using Tailwind's Play CDN script which can the page flicker, initially.)

If set, the :ssr option will use React's server-side-rendering to include the generated markup in the build HTML.

For a full list of options see the docstring in nextjournal.clerk/build!.

Here are some examples:

;; Building a single notebook
(clerk/build! {:paths ["notebooks/rule_30.clj"]})
;; Building all notebooks in `notebook/` with a custom index page.
(clerk/build! {:paths ["notebooks/*"]
:index "notebooks/welcome.clj"})

โšก๏ธ Incremental Computation

๐Ÿ”– Parsing

First, we parse a given Clojure file using rewrite-clj.

(def parsed
(parser/parse-file "book.clj"))
{:blocks [{:loc {:column 1 :end-column 28 :end-line 20 :line 2} :text "
^{:nextjournal.clerk/visibility {:code :hide}}โ†ฉ๏ธŽ(ns nextjournal.clerk.bookโ†ฉ๏ธŽ {:ne778 more elided"
:type :code}
{:loc {:column 1 :end-column 75 :end-line 148 :line 145} :text "
(def clojure-dataโ†ฉ๏ธŽ {:hello "world ๐Ÿ‘‹"โ†ฉ๏ธŽ :tacos (map #(repeat % '๐ŸŒฎ) (range 1 378 more elided"
:type :code}
{:loc {:column 1 :end-column 8 :end-line 152 :line 152} :text "
(range)"
:type :code}
{:loc {:column 1 :end-column 50 :end-line 154 :line 154} :text "
(def fib (lazy-cat [0 1] (map + fib (rest fib))))"
:type :code}
{:loc {:column 1 :end-column 71 :end-line 163 :line 163} :text "
(clerk/html [:div "As Clojurians we " [:em "really"] " enjoy hiccup"])"
:type :code}
{:loc {:column 1 :end-column 46 :end-line 166 :line 166} :text "
(clerk/html "Never <strong>forget</strong>.")"
:type :code}
{:loc {:column 1 :end-column 100 :end-line 169 :line 169} :text "
(clerk/html [:button.bg-sky-500.hover:bg-sky-700.text-white.rounded-xl.px-2.py-119 more elided"
:type :code}
{:loc {:column 1 :end-column 60 :end-line 174 :line 172} :text "
(clerk/html [:svg {:width 500 :height 100}โ†ฉ๏ธŽ [:circle {:cx 25 :cy 5081 more elided"
:type :code}
{:loc {:column 1 :end-column 43 :end-line 180 :line 178} :text "
(clerk/html [:div.flex.justify-center.space-x-6โ†ฉ๏ธŽ [:p "a table is nex52 more elided"
:type :code}
{3 more elided} {3 more elided} {3 more elided} {3 more elided} {3 more elided} {3 more elided} {3 more elided} {3 more elided} {3 more elided} {3 more elided} {3 more elided} 66 more elided]
:file "
book.clj"}

๐Ÿง Analysis

Then, each expression is analysed using tools.analyzer. A dependency graph, the analyzed form and the originating file is recorded.

(def analyzed
(ana/build-graph parsed))
{:->analysis-info {BufferedImage {} Exception {} IllegalArgumentException {} JarFile {} Object {} PngEncoder {} Throwable {} URI {} URL {} ViewerEval {} 1022 more elided} :auto-expand-results? nil :blocks [{:file "
book.clj"
:form (ns nextjournal.clerk.book {:nextjournal.clerk/open-graph {:description "
Clerkโ€™s official documentation."
:image "
https://cdn.nextjournal.com/data/QmbHy6nYRgveyxTvKDJvyy2VF9teeXYkAXXDbgbKZK6YRC?58 more elided"
:title "
The Book of Clerk"
:url "
https://book.clerk.vision"}
:nextjournal.clerk/toc true}
(:require [clojure.string :as str] [next.jdbc :as jdbc] [nextjournal.clerk :as clerk] [nextjournal.clerk.parser :as parser] [nextjournal.clerk.eval :as eval] [nextjournal.clerk.analyzer :as ana] [nextjournal.clerk.viewer :as v] [sicmutils.env :as sicm] [weavejester.dependency :as dep]) (:import (javax.imageio ImageIO) (java.net URL)))
:freezable? true :id nextjournal.clerk.book/anon-expr-5dsSZfM2UfyGCWPTkmyW1BmjLWkHSc :loc {:column 1 :end-column 28 :end-line 20 :line 2} :text "
^{:nextjournal.clerk/visibility {:code :hide}}โ†ฉ๏ธŽ(ns nextjournal.clerk.bookโ†ฉ๏ธŽ {:ne778 more elided"
:text-without-meta "
(ns nextjournal.clerk.bookโ†ฉ๏ธŽ {:nextjournal.clerk/toc trueโ†ฉ๏ธŽ :nextjournal.clerk/731 more elided"
:type :code :visibility {:code :hide :result :hide}}
{:file "
book.clj"
:form (def clojure-data {:hello "
world ๐Ÿ‘‹"
:tacos (map (fn* [%1] (repeat %1 (quote ๐ŸŒฎ))) (range 1 30)) :zeta "
Theโ†ฉ๏ธŽpurposeโ†ฉ๏ธŽofโ†ฉ๏ธŽvisualizationโ†ฉ๏ธŽisโ†ฉ๏ธŽinsight,โ†ฉ๏ธŽnotโ†ฉ๏ธŽpictures."})
:freezable? true :id nextjournal.clerk.book/clojure-data :loc {:column 1 :end-column 75 :end-line 148 :line 145} :text "
(def clojure-dataโ†ฉ๏ธŽ {:hello "world ๐Ÿ‘‹"โ†ฉ๏ธŽ :tacos (map #(repeat % '๐ŸŒฎ) (range 1 378 more elided"
:text-without-meta "
(def clojure-dataโ†ฉ๏ธŽ {:hello "world ๐Ÿ‘‹"โ†ฉ๏ธŽ :tacos (map #(repeat % '๐ŸŒฎ) (range 1 378 more elided"
:type :code :var nextjournal.clerk.book/clojure-data :vars #{1 more elided} 1 more elided}
{9 more elided} {11 more elided} {9 more elided} {9 more elided} {9 more elided} {9 more elided} {9 more elided} {9 more elided} {9 more elided} {9 more elided} {9 more elided} {9 more elided} {9 more elided} {9 more elided} {9 more elided} {9 more elided} {9 more elided} {9 more elided} 66 more elided]
:css-class nil :error-on-missing-vars :on :file "
book.clj"
:graph {2 more elided} :ns #object[clojure.lang.Namespace 0x177abf68 "
nextjournal.clerk.book"
]
:ns? true :open-graph {5 more elided} 1 more elided}

This analysis is done recursively, descending into all dependency symbols.

(ana/find-location 'nextjournal.clerk.analyzer/analyze-file)
"
/root/.gitlibs/libs/io.github.nextjournal/clerk/9fa6b3761fec5544bacdf4117d0ec9b843 more elided"
(ana/find-location `dep/depend)
"
/mnt/mvn-cache/weavejester/dependency/0.2.1/dependency-0.2.1.jar"
(ana/find-location 'io.methvin.watcher.DirectoryChangeEvent)
"
/mnt/mvn-cache/io/methvin/directory-watcher/0.17.3/directory-watcher-0.17.3.jar"
(ana/find-location 'java.util.UUID)
nil
(let [{:keys [graph]} analyzed]
(dep/transitive-dependencies graph 'nextjournal.clerk.book/analyzed))
#{Exception IllegalArgumentException JarFile clojure.lang.IObj clojure.lang.Numbers clojure.lang.PersistentHashMap clojure.lang.RT clojure.lang.Util clojure.lang.Var java.lang.AssertionError java.lang.IllegalArgumentException java.util.jar.JarFile java.util.zip.ZipFile babashka.fs/exists? babashka.fs/file-separator babashka.fs/normalize babashka.fs/read-all-bytes babashka.fs/unixify babashka.fs/windows? clojure.core/*data-readers* 260 more elided}

๐Ÿชฃ Hashing

Then we can use this information to hash each expression.

(def hashes
(:->hash (ana/hash analyzed)))
{BufferedImage "
5dsgvkMmMLJgogxVktTjrTwjJ4afJa"
Exception "
5dsgvkMmMLJgogxVktTjrTwjJ4afJa"
IllegalArgumentException "
5dsgvkMmMLJgogxVktTjrTwjJ4afJa"
JarFile "
5dsgvkMmMLJgogxVktTjrTwjJ4afJa"
Object "
5dsgvkMmMLJgogxVktTjrTwjJ4afJa"
PngEncoder "
5dsgvkMmMLJgogxVktTjrTwjJ4afJa"
Throwable "
5dsgvkMmMLJgogxVktTjrTwjJ4afJa"
URI "
5dsgvkMmMLJgogxVktTjrTwjJ4afJa"
URL "
5dsgvkMmMLJgogxVktTjrTwjJ4afJa"
ViewerEval "
5dsgvkMmMLJgogxVktTjrTwjJ4afJa"
1013 more elided}

๐Ÿ—ƒ Cached Evaluation

Clerk uses the hashes as filenames and only re-evaluates forms that haven't been seen before. The cache is using nippy.

(def rand-fifteen
(do (Thread/sleep 10)
(shuffle (range 15))))
[3 0 12 6 4 13 1 9 14 8 5 10 11 7 2]

We can look up the cache key using the var name in the hashes map.

(when-let [form-hash (get hashes 'nextjournal.clerk.book/rand-fifteen)]
(let [hash (slurp (eval/->cache-file (str "@" form-hash)))]
(eval/thaw-from-cas hash)))
[6 4 5 1 8 7 0 13 9 2 10 14 12 3 11]

As an escape hatch, you can tag a form or var with ::clerk/no-cache to always re-evaluate it. The following form will never be cached.

(shuffle (range 42))
[16 2 36 1 35 10 41 23 24 30 21 4 7 39 34 14 12 20 17 15 22 more elided]

For side effectful functions that should be cached, like a database query, you can add a value like this #inst to control when evaluation should happen.

(def query-results
(let [_run-at #_(java.util.Date.) #inst "2021-05-20T08:28:29.445-00:00"
ds (next.jdbc/get-datasource {:dbtype "sqlite" :dbname "chinook.db"})]
(with-open [conn (next.jdbc/get-connection ds)]
(clerk/table (next.jdbc/execute! conn ["SELECT AlbumId, Bytes, Name, TrackID, UnitPrice FROM tracks"])))))
:tracks/AlbumId
:tracks/Bytes
:tracks/Name
:tracks/TrackId
:tracks/UnitPrice
111170334For Those About To Rock (We Salute You)10.99
25510424Balls to the Wall20.99
33990994Fast As a Shark30.99
34331779Restless and Wild40.99
36290521Princess of the Dawn50.99
16713451Put The Finger On You60.99
17636561Let's Get It Up70.99
16852860Inject The Venom80.99
16599424Snowballed90.99
18611245Evil Walks100.99
16566314C.O.D.110.99
18596840Breaking The Rules120.99
16706347Night Of The Long Knives130.99
18817038Spellbound140.99
410847611Go Down150.99
47032162Dog Eat Dog160.99
412021261Let There Be Rock170.99
48776140Bad Boy Boogie180.99
410617116Problem Child190.99
412066294Overdose200.99
3483 more elided