Skip to content

Commit cb079b1

Browse files
borkdudemk
andauthored
Experimental support for cherry for compiling viewer fns (#446)
This is a first step towards supporting compiling Clerk viewer functions using https://github.com/squint-cljs/cherry. This is is currently experimental so subject to change. Fixes #441. Co-authored-by: Martin Kavalar <[email protected]>
1 parent 6114fae commit cb079b1

File tree

11 files changed

+309
-34
lines changed

11 files changed

+309
-34
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,3 +21,4 @@ clerk.iml
2121
.clj-kondo/marick
2222
.clj-kondo/nextjournal/clerk
2323
.secrets.env
24+
report.html

bb.edn

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,11 @@
3131
build:static-app {:doc "Builds a static app with default notebooks"
3232
:task (apply clojure "-X:demo:nextjournal/clerk" *command-line-args*)}
3333

34+
build:static-app-test-release
35+
{:doc "Builds static app which uses output of build:js.
36+
Run with additional --paths notebooks/cherry.clj to test single notebook."
37+
:task (apply clojure "-M:demo:nextjournal/clerk:test-release-js" *command-line-args*)}
38+
3439
-check {:depends [lint test:clj]}
3540

3641
check {:doc "Check to run before pushing"

deps.edn

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626

2727
:aliases {:nextjournal/clerk {:extra-deps {org.clojure/clojure {:mvn/version "1.11.1"} ;; for `:as-alias` support in static build
2828
org.slf4j/slf4j-nop {:mvn/version "1.7.36"}
29-
org.babashka/cli {:mvn/version "0.5.40"}}
29+
org.babashka/cli {:mvn/version "0.6.50"}}
3030
:extra-paths ["notebooks"]
3131
:exec-fn nextjournal.clerk/build!
3232
:exec-args {:paths-fn nextjournal.clerk.builder/clerk-docs}
@@ -75,4 +75,9 @@
7575
io.github.clojure/tools.build {:git/tag "v0.6.1" :git/sha "515b334"}
7676
io.github.slipset/deps-deploy {:git/sha "b4359c5d67ca002d9ed0c4b41b710d7e5a82e3bf"}}
7777
:extra-paths ["bb" "src" "resources"] ;; for loading lookup-url in build
78-
:ns-default build}}}
78+
:ns-default build}
79+
80+
:test-release-js {:jvm-opts ["-Dclerk.resource_manifest={\"/js/viewer.js\" \"/viewer.js\"}"]
81+
:exec-args {:out-path "build"}
82+
:main-opts ["-m" "babashka.cli.exec"]}
83+
}}

notebooks/cherry.clj

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
;; # Compile viewer functions using cherry
2+
(ns notebooks.cherry
3+
#_{:nextjournal.clerk/visibility {:code :hide}
4+
:nextjournal.clerk/auto-expand-results? true}
5+
(:require [nextjournal.clerk :as clerk]
6+
[nextjournal.clerk.viewer :as viewer]))
7+
8+
(comment
9+
(clerk/clear-cache!))
10+
11+
(clerk/with-viewer
12+
{:render-fn
13+
'(fn [value]
14+
[:pre (time (do (dotimes [_ 100000]
15+
(js/Math.sin 100))
16+
(pr-str (interleave (cycle [1]) (frequencies [1 2 3 1 2 3])))))])}
17+
(+ 1 2 3 5))
18+
19+
;; Better performance:
20+
21+
(clerk/with-viewer
22+
{:render-fn
23+
'(fn [value]
24+
[:pre
25+
(time (do (dotimes [_ 100000]
26+
(js/Math.sin 100))
27+
(pr-str (interleave (cycle [1]) (frequencies [1 2 3 1 2 3])))))])
28+
:evaluator :cherry}
29+
(+ 1 2 3 5))
30+
31+
;; Let's use a render function in the :render-fn next
32+
33+
(clerk/with-viewer
34+
{:render-fn
35+
'(fn [value]
36+
[nextjournal.clerk.render/render-code "(+ 1 2 3)"])
37+
:evaluator :cherry}
38+
(+ 1 2 3 5))
39+
40+
;; Recursive ...
41+
42+
(clerk/with-viewer
43+
{:render-fn
44+
'(fn [value]
45+
[nextjournal.clerk.render/inspect {:a (range 30)}])
46+
:evaluator :cherry}
47+
nil)
48+
49+
;; cherry vega viewer!
50+
51+
(def cherry-vega-viewer (assoc viewer/vega-lite-viewer :evaluator :cherry))
52+
53+
(clerk/with-viewer
54+
cherry-vega-viewer
55+
{:width 700 :height 400 :data {:url "https://vega.github.io/vega-datasets/data/us-10m.json"
56+
:format {:type "topojson" :feature "counties"}}
57+
:transform [{:lookup "id" :from {:data {:url "https://vega.github.io/vega-datasets/data/unemployment.tsv"}
58+
:key "id" :fields ["rate"]}}]
59+
:projection {:type "albersUsa"} :mark "geoshape" :encoding {:color {:field "rate" :type "quantitative"}}})
60+
61+
;; ## Input text and compile on the fly with cherry
62+
63+
(clerk/with-viewer
64+
{:evaluator :cherry
65+
:render-fn
66+
'(fn [value]
67+
(let [default-value "(defn foo [x] (+ x 10))
68+
(foo 10)"
69+
!input (reagent.core/atom default-value)
70+
!compiled (reagent.core/atom (nextjournal.clerk.cherry-env/cherry-compile-string @!input))
71+
click-handler (fn []
72+
(reset! !compiled (nextjournal.clerk.cherry-env/cherry-compile-string @!input)))]
73+
(fn [value]
74+
[:div
75+
[:div.flex
76+
[:div.viewer-code.flex-auto.w-80.mb-2 [nextjournal.clerk.render.code/editor !input]]
77+
[:button.flex-none.bg-slate-100.mb-2.pl-2.pr-2
78+
{:on-click click-handler}
79+
"Compile!"]]
80+
[:div.bg-slate-50
81+
[nextjournal.clerk.render/render-code @!compiled]]
82+
[nextjournal.clerk.render/inspect
83+
(try (js/eval @!compiled)
84+
(catch :default e e))]])))}
85+
nil)
86+
87+
;; ## Functions defined with `defn` are part of the global context
88+
89+
;; (for now) and can be called in successive expressions
90+
91+
(clerk/eval-cljs-str {:evaluator :cherry}
92+
"(defn foo [x] x)")
93+
94+
(clerk/eval-cljs-str {:evaluator :cherry}
95+
"(foo 1)")
96+
97+
;; ## Async/await works cherry
98+
99+
;; Here we dynamically import a module, await its value and then pull out the
100+
;; default function, which we expose as a global function. Because s-expressions
101+
;; serialized to the client currently don't preserve metadata in clerk, and
102+
;; async functions need `^:async`, we use a plain string.
103+
104+
105+
^::clerk/no-cache
106+
(clerk/eval-cljs
107+
{:evaluator :cherry}
108+
'(defn emoji-picker
109+
{:async true}
110+
[]
111+
(js/await (js/import "https://cdn.skypack.dev/emoji-picker-element"))
112+
(nextjournal.clerk.viewer/html [:div
113+
[:p "My cool emoji picker:"]
114+
[:emoji-picker]])))
115+
116+
;; In the next block we call it:
117+
118+
^::clerk/no-cache
119+
(clerk/with-viewer
120+
{:evaluator :cherry
121+
:render-fn '(fn [_]
122+
[nextjournal.clerk.render/render-promise
123+
(emoji-picker)])}
124+
nil)
125+
126+
;; ## Macros
127+
128+
^::clerk/no-cache
129+
(clerk/eval-cljs
130+
{:evaluator :cherry}
131+
'(defn clicks []
132+
(reagent.core/with-let [!s (reagent.core/atom 0)]
133+
[:button {:on-click (fn []
134+
(swap! !s inc))}
135+
"Clicks: " @!s])))
136+
137+
^::clerk/no-cache
138+
(clerk/with-viewer
139+
{:evaluator :cherry
140+
:render-fn '(fn [_]
141+
[clicks])
142+
}
143+
nil)
144+

render/deps.edn

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,11 @@
22
:deps {applied-science/js-interop {:mvn/version "0.3.3"}
33
binaryage/devtools {:mvn/version "1.0.3"}
44
cider/cider-nrepl {:mvn/version "0.28.3"}
5-
org.babashka/sci {:git/sha "4ab93c2530f174491a03083b0f15b76f8a176178" :git/url "https://github.com/babashka/sci"}
5+
org.babashka/sci {:mvn/version "0.7.39"}
66
reagent/reagent {:mvn/version "1.2.0"}
77
io.github.babashka/sci.configs {:git/sha "0702ea5a21ad92e6d7cca6d36de84271083ea68f"}
88
io.github.nextjournal/clojure-mode {:git/sha "ac038ebf6e5da09dd2b8a31609e9ff4a65e36852"}
99
metosin/reitit-frontend {:mvn/version "0.5.15"}
10-
thheller/shadow-cljs {:mvn/version "2.22.7"}}}
10+
thheller/shadow-cljs {:mvn/version "2.22.7"}
11+
io.github.squint-cljs/cherry {;; :local/root "/Users/borkdude/dev/cherry"
12+
:git/sha "ac27030016e5ae8f5280a62eaf81dfe0dc83924b"}}}

shadow-cljs.edn

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,6 @@
1010
:modules {:viewer {:entries [nextjournal.clerk.sci-env
1111
nextjournal.clerk.static-app
1212
nextjournal.clerk.trim-image]}}
13-
:js-options {:output-feature-set :es8}}}}
13+
:js-options {:output-feature-set :es8}
14+
:build-hooks [(shadow.cljs.build-report/hook
15+
{:output-to "report.html"})]}}}

src/nextjournal/clerk.clj

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -349,8 +349,9 @@
349349

350350
(defn eval-cljs-str
351351
"Evaluates the given ClojureScript `code-string` in the browser."
352-
[code-string]
353-
(v/eval-cljs-str code-string))
352+
([code-string] (eval-cljs-str code-string nil))
353+
([opts code-string]
354+
(v/eval-cljs-str opts code-string)))
354355

355356
(defn eval-cljs
356357
"Evaluates the given ClojureScript forms in the browser."

src/nextjournal/clerk/builder.clj

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,8 @@
5858
"viewers/plotly"
5959
"viewers/table"
6060
"viewers/tex"
61-
"viewers/vega"]))
61+
"viewers/vega"
62+
"cherry"]))
6263

6364

6465
(defn strip-index [path]
@@ -361,3 +362,7 @@
361362
:index "notebooks/rule_30.clj"
362363
:paths ["notebooks/hello.clj"
363364
"notebooks/markdown.md"]})
365+
#_(build-static-app! {;; test against cljs release `bb build:js`
366+
:resource->url {"/js/viewer.js" "/viewer.js"}
367+
:paths ["notebooks/cherry.clj"]
368+
:out-path "build"})

src/nextjournal/clerk/cherry_env.cljs

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
(ns nextjournal.clerk.cherry-env
2+
(:refer-clojure :exclude [time])
3+
(:require [applied-science.js-interop :as j]
4+
[cherry.embed :as cherry]
5+
[cljs.math]
6+
[cljs.reader]
7+
[clojure.string :as str]
8+
[goog.object]
9+
[nextjournal.clerk.parser]
10+
[nextjournal.clerk.render :as render]
11+
[nextjournal.clerk.render.code]
12+
[nextjournal.clerk.render.hooks]
13+
[nextjournal.clerk.render.navbar]
14+
[nextjournal.clerk.trim-image]
15+
[nextjournal.clerk.viewer :as viewer]
16+
[nextjournal.clojure-mode.commands]
17+
[nextjournal.clojure-mode.extensions.eval-region]
18+
[nextjournal.clojure-mode.keymap]
19+
[reagent.core :as reagent]
20+
[reagent.ratom :as ratom]
21+
[sci.configs.reagent.reagent :as sci.configs.reagent]))
22+
23+
(cherry/preserve-ns 'cljs.core)
24+
(cherry/preserve-ns 'nextjournal.clerk.render)
25+
(cherry/preserve-ns 'nextjournal.clerk.render.code)
26+
(cherry/preserve-ns 'nextjournal.clerk.render.hooks)
27+
(cherry/preserve-ns 'nextjournal.clerk.render.navbar)
28+
29+
(j/assoc-in! js/globalThis [:reagent :core :atom] reagent/atom)
30+
31+
(def reagent-ratom-namespace
32+
#js {:with-let-values ratom/with-let-values
33+
:reactive? ratom/reactive?
34+
:-ratom-context sci.configs.reagent/-ratom-context
35+
:atom reagent.ratom/atom
36+
:make-reaction reagent.ratom/make-reaction
37+
:make-track reagent.ratom/make-track
38+
:track! reagent.ratom/track!})
39+
40+
(defn munge-ns-obj [m]
41+
(.forEach (js/Object.keys m)
42+
(fn [k i]
43+
(unchecked-set m (munge k) (unchecked-get m k))
44+
(js-delete m k)))
45+
m)
46+
47+
(j/update-in! js/globalThis [:reagent :ratom] j/extend! (munge-ns-obj reagent-ratom-namespace))
48+
(j/assoc! js/globalThis :global_eval (fn [x]
49+
(js/eval.apply js/globalThis #js [x])))
50+
51+
(def cherry-macros {'reagent.core {'with-let sci.configs.reagent/with-let}})
52+
53+
(declare eval-form)
54+
55+
(defn ->viewer-fn-with-error [form]
56+
(try (binding [*eval* eval-form]
57+
(viewer/->viewer-fn form))
58+
(catch js/Error e
59+
(viewer/map->ViewerFn
60+
{:form form
61+
:f (fn [_]
62+
[render/error-view (ex-info (str "error in render-fn: " (.-message e)) {:render-fn form} e)])}))))
63+
64+
(defn ->viewer-eval-with-error [form]
65+
(try (eval-form form)
66+
(catch js/Error e
67+
(js/console.error "error in viewer-eval" e)
68+
(ex-info (str "error in viewer-eval: " (.-message e)) {:form form} e))))
69+
70+
(defn ^:export cherry-compile-string [s]
71+
(cherry/compile-string
72+
s
73+
{:macros cherry-macros}))
74+
75+
(defn ^:export eval-form [f]
76+
(js/global-eval (cherry/compile-string
77+
(str f)
78+
{:macros cherry-macros})))

0 commit comments

Comments
 (0)