Clerk enables a rich, local-first notebook experience using standard Clojure namespaces and Markdown files with Clojure code fences. You bring your own editor and workflow, your own interactive computing habits, and Clerk enhances all of that with literate programming and rich visualizations.
Inside clj files, comment blocks are interpreted as prose written in an extended dialect of Markdown. Clerk supports inline TeX, so we can insert the Euler–Lagrange equation quite easily:
Loading...
When Clerk interprets an md file, the relationship between code blocks and prose is reversed. Instead of the file being code by default with prose in comment blocks, it will be treated as Markdown by default with Clojure code in code fences. Clerk's code fences have a twist, though: they evaluate their contents.
There are loads of other goodies to share, most of which we'll see a bit farther down the page.
Operation
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 clj or 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.
Pagination
As an example, the infinite sequence returned by (range) will be loaded a little bit at a time as you click on the results. (Note the little underscore under the first paren, it lets you switch this sequence to a vertical rather than horizontal view).
(range)
(0123456789101112131415161718191000000+ more elided)
Opaque objects are printed as they would be in the Clojure REPL, like so:
(defnotebooks
(clojure.java.io/file"notebooks"))
#object[java.io.File0x2f08014b"
notebooks"
]
You can leave a form at the top-level like this to examine the result of evaluating it, though you'd probably use your live programming environment to do this most of the time.
(into #{} (map str) (file-seq notebooks))
#{"
notebooks"
"
notebooks/controls.clj"
"
notebooks/data_science.clj"
"
notebooks/dictionary.clj"
"
notebooks/elements.clj"
"
notebooks/git.clj"
"
notebooks/images.clj"
"
notebooks/index.md"
"
notebooks/introduction.clj"
"
notebooks/logo.clj"
"
notebooks/markdown.md"
"
notebooks/perceptron.clj"
"
notebooks/rule_30.clj"
"
notebooks/semantic.clj"
"
notebooks/sicmutils.clj"
"
notebooks/slideshow.md"
"
notebooks/src"
"
notebooks/src/demo"
"
notebooks/src/demo/lib.cljc"
"
notebooks/static_site_generation.md"
1 more elided}
Sometimes you don't want Clerk to cache a form. You can turn off caching for a form by placing a special piece of metadata before it, like this:
(shuffle (range100))
[61832258580674376701063227843755446480 more elided]
Another useful technique is to put an instant marking the last time a form was run. This way you can update this result at any time by updating the instant.
:tacos((🌮)(🌮🌮)(🌮🌮🌮)(🌮🌮🌮🌮)(🌮🌮🌮🌮🌮)(🌮🌮🌮🌮🌮🌮)(🌮🌮🌮🌮🌮🌮🌮)(🌮🌮🌮🌮🌮🌮🌮🌮)(🌮🌮🌮🌮🌮🌮🌮🌮🌮)(🌮🌮🌮🌮🌮🌮🌮🌮🌮🌮)(🌮🌮🌮🌮🌮🌮🌮🌮🌮🌮🌮)(🌮🌮🌮🌮🌮🌮🌮🌮🌮🌮🌮🌮)(🌮🌮🌮🌮🌮🌮🌮🌮🌮🌮🌮🌮🌮)(🌮🌮🌮🌮🌮🌮🌮🌮🌮🌮🌮🌮🌮🌮)(🌮🌮🌮🌮🌮🌮🌮🌮🌮🌮🌮🌮🌮🌮🌮)(🌮🌮🌮🌮🌮🌮🌮🌮🌮🌮🌮🌮🌮🌮🌮🌮)(🌮🌮🌮🌮🌮🌮🌮🌮🌮🌮🌮🌮🌮🌮🌮🌮🌮)(🌮🌮🌮🌮🌮🌮🌮🌮🌮🌮🌮🌮🌮🌮🌮🌮🌮🌮)(🌮🌮🌮16 more elided)(20 more elided)79 more elided)}
👁 Clerk Viewer API
In addition to these basic viewers for Clojure data structures, Clerk comes with a set of built-in viewers for many kinds of things, and a moldable viewer API that can be extended while you work.
🧩 Built-in Viewers
🔢 Data 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.
There's a code viewer uses that clojure-mode for syntax highlighting.
(clerk/code (macroexpand '(when test
expression-1
expression-2)))
(if test (do expression-1 expression-2))
🧮 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:
The html viewer interprets hiccup when passed a vector. (This can be quite useful for building arbitrary layouts in your notebooks.)
(clerk/html [:table
[:tr [:td"◤"] [:td"◥"]]
[:tr [:td"◉"] [:td"◉"]]
[:tr [:td"◣"] [:td"◢"]]])
◤
◥
◉
◉
◣
◢
Alternatively you can also just pass an HTML string, perhaps generated by your code:
(clerk/html"“A brilliant solution to the wrong problem can be worse than no solution at all. Solve the correct problem.”<br/>—<em>Donald Norman</em>")
“A brilliant solution to the wrong problem can be worse than no solution at all. Solve the correct problem.” —Donald Norman
🚀 Extensibility
In addition to these defaults, you can also attach a custom viewer to any form. Here we make our own little viewer to greet James Clerk Maxwell:
(clerk/with-viewer '(fn [name] [:div"Greetings to " [:strong name] "!"])
"James Clerk Maxwell")
Greetings to James Clerk Maxwell!
But we can do more interesting things, like using a predicate function to match numbers and turn them into headings, or converting string into paragraphs.
Keep in mind when writing your own :render-fn that it will run entirely in the browser, and so will not have access to your local bindings on the JVM side. If you need to your viewer to pre-process what it sends to the browser, you can specify a :transform-fn that will be called before the data is sent over the wire.
🏞 Customizing Data Fetching
Sometimes you might want to create a custom viewer that overrides Clerk's automatic paging behavior. In this example, we use a custom transform-fn that specifies a content-type to tell Clerk to serve arbitrary byte arrays as PNG images.
Notice that the image is conveyed out-of-band using the url-for function to get a URL from which to fetch the blob.
xs))}{3 more elided}{2 more elided}{6 more elided}{6 more elided}{6 more elided}{5 more elided}{4 more elided}{4 more elided}{6 more elided}23 more elided]
This is just a taste of what's possible using Clerk. Take a look in the notebooks directory to see a collection of worked examples in different domains.