From ddbbfc436447e19ecbe75dd11e59224868cc9b64 Mon Sep 17 00:00:00 2001 From: Martin Kavalar Date: Wed, 12 Jan 2022 17:56:52 +0100 Subject: [PATCH 01/10] wip --- notebooks/rule_30.clj | 8 +- notebooks/viewer_api.clj | 18 ++-- src/nextjournal/clerk.clj | 24 +----- src/nextjournal/clerk/sci_viewer.cljs | 36 ++++---- src/nextjournal/clerk/viewer.cljc | 114 +++++++++++--------------- 5 files changed, 87 insertions(+), 113 deletions(-) diff --git a/notebooks/rule_30.clj b/notebooks/rule_30.clj index c906a5e4c..bc59cd17a 100644 --- a/notebooks/rule_30.clj +++ b/notebooks/rule_30.clj @@ -4,10 +4,10 @@ (:require [nextjournal.clerk :as clerk])) (clerk/set-viewers! - [{:pred number? :render-fn #(v/html [:div.inline-block {:style {:width 16 :height 16} - :class (if (pos? %) "bg-black" "bg-white border-solid border-2 border-black")}])} - {:pred list? :render-fn #(v/html (into [:div.flex.flex-col] (v/inspect-children %2) %1))} - {:pred #(and (vector? %) (not (map-entry? %))) :render-fn #(v/html (into [:div.flex.inline-flex] (v/inspect-children %2) %1))}]) + [{:pred number? :render-fn '#(v/html [:div.inline-block {:style {:width 16 :height 16} + :class (if (pos? %) "bg-black" "bg-white border-solid border-2 border-black")}])} + {:pred list? :render-fn '#(v/html (into [:div.flex.flex-col] (v/inspect-children %2) %1))} + {:pred #(and (vector? %) (not (map-entry? %))) :render-fn '#(v/html (into [:div.flex.inline-flex] (v/inspect-children %2) %1))}]) 0 diff --git a/notebooks/viewer_api.clj b/notebooks/viewer_api.clj index 5b299fd57..a27ceb910 100644 --- a/notebooks/viewer_api.clj +++ b/notebooks/viewer_api.clj @@ -49,15 +49,15 @@ expression-2))) ;; ## 🚀 Extensibility -(clerk/with-viewer #(v/html [:div "Greetings to " [:strong %] "!"]) +(clerk/with-viewer '#(v/html [:div "Greetings to " [:strong %] "!"]) "James Maxwell Clerk") -(clerk/with-viewers [{:pred number? :render-fn #(v/html [:div.inline-block [(keyword (str "h" %)) (str "Heading " %)]])}] +(clerk/with-viewers [{:pred number? :render-fn '#(v/html [:div.inline-block [(keyword (str "h" %)) (str "Heading " %)]])}] [1 2 3 4 5]) ^::clerk/no-cache -(clerk/with-viewers [{:pred number? :render-fn #(v/html [:div.inline-block {:style {:width 16 :height 16} - :class (if (pos? %) "bg-black" "bg-white border-solid border-2 border-black")}])}] +(clerk/with-viewers [{:pred number? :render-fn '#(v/html [:div.inline-block {:style {:width 16 :height 16} + :class (if (pos? %) "bg-black" "bg-white border-solid border-2 border-black")}])}] (take 10 (repeatedly #(rand-int 2)))) (clerk/with-viewers @@ -67,11 +67,11 @@ (str "(?i)" "(#(?:[0-9a-f]{2}){2,4}|(#[0-9a-f]{3})|" "(rgb|hsl)a?\\((-?\\d+%?[,\\s]+){2,3}\\s*[\\d\\.]+%?\\))")) %)) - :render-fn #(v/html [:div.inline-block.rounded-sm.shadow - {:style {:width 16 - :height 16 - :border "1px solid rgba(0,0,0,.2)" - :background-color %}}])}] + :render-fn '#(v/html [:div.inline-block.rounded-sm.shadow + {:style {:width 16 + :height 16 + :border "1px solid rgba(0,0,0,.2)" + :background-color %}}])}] ["#571845" "rgb(144,12,62)" "rgba(199,0,57,1.0)" diff --git a/src/nextjournal/clerk.clj b/src/nextjournal/clerk.clj index 9cf3657c0..ff9a1ae40 100644 --- a/src/nextjournal/clerk.clj +++ b/src/nextjournal/clerk.clj @@ -240,27 +240,9 @@ (def use-headers #'v/use-headers) (def hide-result #'v/hide-result) (defn doc-url [path] (v/->SCIEval (list 'v/doc-url path))) - -(defmacro with-viewer - [viewer x] - (let [viewer# (v/->Form viewer)] - `(v/with-viewer* ~viewer# ~x))) - -#_(macroexpand '(with-viewer #(v/html [:div %]) 1)) - -(defmacro with-viewers - [viewers x] - (let [viewers# (v/process-fns viewers)] - `(v/with-viewers* ~viewers# ~x))) - -#_(macroexpand '(with-viewers [{:pred number? :render-fn #(v/html [:div %])}] 1)) - - -(defmacro set-viewers! - ([viewers] (v/set-viewers!* *ns* viewers)) - ([scope viewers] (v/set-viewers!* scope viewers))) - -#_(set-viewers! []) +(def with-viewer #'v/with-viewer) +(def with-viewers #'v/with-viewers) +(def set-viewers! #'v/set-viewers!) (defn file->viewer "Evaluates the given `file` and returns it's viewer representation." diff --git a/src/nextjournal/clerk/sci_viewer.cljs b/src/nextjournal/clerk/sci_viewer.cljs index 1d8ec310f..f3d574e67 100644 --- a/src/nextjournal/clerk/sci_viewer.cljs +++ b/src/nextjournal/clerk/sci_viewer.cljs @@ -5,7 +5,7 @@ [edamame.core :as edamame] [goog.object] [goog.string :as gstring] - [nextjournal.clerk.viewer :as viewer :refer [code html md plotly tex vl with-viewer* with-viewers*] :rename {with-viewer* with-viewer with-viewers* with-viewers}] + [nextjournal.clerk.viewer :as viewer :refer [code html md plotly tex vl with-viewer with-viewers]] [nextjournal.devcards :as dc] [nextjournal.markdown.transform :as md.transform] [nextjournal.viewer.code :as code] @@ -391,7 +391,8 @@ [:span.inspected-value.whitespace-nowrap [:span.syntax-tag tag] value]) -(defn normalize-viewer [x] +(defn normalize-viewer + [x] (if-let [viewer (-> x meta :nextjournal/viewer)] (with-viewer viewer x) x)) @@ -413,19 +414,25 @@ (declare default-viewers) +(defn maybe-eval [render-fn] + (js/console.log :maybe-eval render-fn :eval? (or (symbol? render-fn) (list? render-fn))) + (cond-> render-fn + (or (symbol? render-fn) (list? render-fn)) + *eval*)) + (defn render-with-viewer [{:as opts :keys [viewers]} viewer value] - #_(js/console.log :render-with-viewer {:value value :viewer viewer #_#_ :opts opts}) + (js/console.log :render-with-viewer {:value value :viewer viewer #_#_ :opts opts}) (cond (or (fn? viewer) (viewer/fn+form? viewer)) (viewer value opts) (and (map? viewer) (:render-fn viewer)) - (render-with-viewer opts (:render-fn viewer) value) + (render-with-viewer opts (maybe-eval (:render-fn viewer)) value) (keyword? viewer) (if-let [{:keys [fetch-opts render-fn]} (viewer/find-named-viewer viewers viewer)] (if-not render-fn (html (error-badge "no render function for viewer named " (str viewer))) - (render-fn value (assoc opts :fetch-opts fetch-opts))) + ((maybe-eval render-fn) value (assoc opts :fetch-opts fetch-opts))) (html (error-badge "cannot find viewer named " (str viewer)))) :else @@ -434,12 +441,15 @@ (defn inspect ([x] (r/with-let [!expanded-at (r/atom {})] - [inspect {:!expanded-at !expanded-at} x])) - ([{:as opts :keys [viewers]} x] + [inspect {:!expanded-at !expanded-at :recursion 0} x])) + ([{:as opts :keys [viewers recursion]} x] (let [value (viewer/value x) {:as opts :keys [viewers]} (assoc opts :viewers (vec (concat (viewer/viewers x) viewers))) - all-viewers (viewer/get-viewers (:scope @!doc) viewers)] - (or (when (react/isValidElement value) value) + all-viewers (viewer/get-viewers (:scope @!doc) viewers) + opts (update opts :recursion inc)] + (js/console.log :inspect value) + (or (when (< 20 recursion) [:span "reached recursion limit"]) + (when (react/isValidElement value) value) ;; TODO find option to disable client-side viewer selection (when-let [viewer (or (viewer/viewer x) (viewer/viewer (viewer/wrapped-with-viewer value all-viewers)))] @@ -491,7 +501,7 @@ id (get id)))) (defn error-viewer [e] - (viewer/with-viewer* :code (pr-str e))) + (viewer/with-viewer :code (pr-str e))) (dc/defcard inspect-values @@ -623,6 +633,7 @@ [inspect @!error]])]) (defn ^:export set-state [{:as state :keys [doc error]}] + (js/console.log :set-state state) (doseq [cell (viewer/value doc) :when (viewer/registration? cell) :let [form (viewer/value cell)]] @@ -874,6 +885,7 @@ black")}]))} (html (katex/to-html-string tex-string))) (defn html-viewer [markup] + (js/console.log :html-viewer markup) (if (string? markup) (html [:div {:dangerouslySetInnerHTML {:__html markup}}]) (r/as-element markup))) @@ -969,7 +981,3 @@ black")}]))} (sci/eval-form @!sci-ctx f)) (set! *eval* eval-form) - -(swap! viewer/!viewers (fn [viewers] - (-> (into {} (map (juxt key (comp viewer/process-fns val))) viewers) - (update :root concat js-viewers)))) diff --git a/src/nextjournal/clerk/viewer.cljc b/src/nextjournal/clerk/viewer.cljc index e04ace507..ee5f60983 100644 --- a/src/nextjournal/clerk/viewer.cljc +++ b/src/nextjournal/clerk/viewer.cljc @@ -153,7 +153,7 @@ (def elide-string-length 100) -(declare with-viewer*) +(declare with-viewer) (defn fetch-all [_ x] x) @@ -219,42 +219,16 @@ {:pred string? :render-fn (quote v/string-viewer) :fetch-opts {:n 100}} {:pred number? :render-fn '(fn [x] (v/html [:span.tabular-nums (if (js/Number.isNaN x) "NaN" (str x))]))}]) -(defn process-fns [viewers] - (into [] - (map (fn [{:as viewer :keys [pred fetch-fn render-fn transform-fn]}] - ;; TODO: simplify with own type for things that should not be transmitted to the - ;; browser (`pred`, `fetch-fn` & `transform-fn`) - ;; also remove `symbol?` checks and let viewers use `(quote my-sym)` instead of `'my-sym` - (cond-> viewer - (and pred (or (symbol? pred) (not (ifn? pred)))) - (update :pred #?(:cljs *eval* :clj #(->Fn+Form '(constantly false) (eval %)))) - - (and transform-fn (or (symbol? transform-fn) (not (ifn? transform-fn)))) - (update :transform-fn #?(:cljs *eval* :clj #(->Fn+Form '(constantly false) (eval %)))) - - (and fetch-fn (not (ifn? fetch-fn))) - (update :fetch-fn #?(:cljs *eval* :clj #(->Fn+Form '(constantly false) (eval %)))) - - #?@(:clj [(and render-fn (not (instance? Form render-fn))) - (update :render-fn ->Form)] - :cljs [(and render-fn (not (instance? Fn+Form render-fn))) - (update :render-fn form->fn+form)])))) - viewers)) - -(defn process-viewers [viewers] - #?(:clj (process-fns viewers) - :cljs viewers)) - -(defn processed-default-viewers [] - {:root (process-viewers default-viewers) - :table (process-viewers default-table-cell-viewers)}) +(def all-viewers + {:root default-viewers + :table default-table-cell-viewers}) (defonce ^{:doc "atom containing a map of `:root` and per-namespace viewers."} !viewers - (#?(:clj atom :cljs ratom/atom) (processed-default-viewers))) + (#?(:clj atom :cljs ratom/atom) all-viewers)) -#_(reset! !viewers (processed-default-viewers)) +#_(reset! !viewers all-viewers) ;; heavily inspired by code from Thomas Heller in shadow-cljs, see ;; https://github.com/thheller/shadow-cljs/blob/1708acb21bcdae244b50293d17633ce35a78a467/src/main/shadow/remote/runtime/obj_support.cljc#L118-L144 @@ -278,7 +252,7 @@ (set? xs) (sort resilient-compare xs) :else xs)) -(declare with-viewer*) +(declare with-viewer) (defn find-named-viewer [viewers viewer-name] (first (filter (comp #{viewer-name} :name) viewers))) @@ -311,8 +285,8 @@ #_(wrapped-with-viewer (clojure.java.io/file "notebooks")) #_(wrapped-with-viewer (md "# Hello")) #_(wrapped-with-viewer (html [:h1 "hi"])) -#_(wrapped-with-viewer (with-viewer* :elision {:remaining 10 :count 30 :offset 19})) -#_(wrapped-with-viewer (with-viewer* (->Form '(fn [name] (html [:<> "Hello " name]))) "James")) +#_(wrapped-with-viewer (with-viewer :elision {:remaining 10 :count 30 :offset 19})) +#_(wrapped-with-viewer (with-viewer (->Form '(fn [name] (html [:<> "Hello " name]))) "James")) (defn get-viewers "Returns all the viewers that apply in precendence of: optional local `viewers`, viewers set per `ns`, as well on the `:root`." @@ -346,7 +320,16 @@ #_(sequence (drop+take-xf {}) (range 9)) -(declare with-viewer* assign-closing-parens) +(declare with-viewer assign-closing-parens) + +(defn process-viewer [viewer] + (if-not (map? viewer) + viewer + (cond-> (dissoc viewer :pred :transform-fn :fetch-fn) + (:render-fn viewer) + (update :render-fn ->Form)))) + +#_(process-viewer {:render-fn '(v/html [:h1]) :fetch-fn fetch-all}) (defn describe "Returns a subset of a given `value`." @@ -354,7 +337,7 @@ (describe xs {})) ([xs opts] (assign-closing-parens - (describe xs (merge {:!budget (atom (:budget opts 200)) :path [] :viewers (process-fns (get-viewers *ns* (viewers xs)))} opts) []))) + (describe xs (merge {:!budget (atom (:budget opts 200)) :path [] :viewers (get-viewers *ns* (viewers xs))} opts) []))) ([xs opts current-path] (let [{:as opts :keys [!budget viewers path offset]} (merge {:offset 0} opts) wrapped-value (try (wrapped-with-viewer xs viewers) ;; TODO: respect `viewers` on `xs` @@ -371,7 +354,7 @@ (swap! !budget #(max (dec %) 0))) (merge {:path path} (dissoc wrapped-value [:nextjournal/value :nextjournal/viewer]) - (with-viewer* (cond-> viewer (map? viewer) (dissoc viewer :pred :transform-fn)) + (with-viewer (process-viewer viewer) (cond descend? (let [idx (first (drop (count current-path) path))] (describe (cond (or (map? xs) (set? xs)) (nth (seq (ensure-sorted xs)) idx) @@ -389,7 +372,7 @@ new-offset (min (+ offset (:n fetch-opts)) total) remaining (- total new-offset)] (cond-> [(subs xs offset new-offset)] - (pos? remaining) (conj (with-viewer* :elision {:path path :count total :offset new-offset :remaining remaining})) + (pos? remaining) (conj (with-viewer :elision {:path path :count total :offset new-offset :remaining remaining})) true wrap-value true (assoc :replace-path (conj path offset)))) xs)) @@ -413,7 +396,7 @@ (cond-> children (or (not count) (< (inc offset) count)) - (conj (with-viewer* :elision + (conj (with-viewer :elision (cond-> (assoc count-opts :offset (inc offset) :path path) count (assoc :remaining (- count (inc offset)))))))) @@ -431,11 +414,11 @@ (describe (map vector (range))) (describe (subs (slurp "/usr/share/dict/words") 0 1000)) (describe (plotly {:data [{:z [[1 2 3] [3 2 1]] :type "surface"}]})) - (describe (with-viewer* :html [:h1 "hi"])) - (describe (with-viewer* :html [:ul (for [x (range 3)] [:li x])])) + (describe (with-viewer :html [:h1 "hi"])) + (describe (with-viewer :html [:ul (for [x (range 3)] [:li x])])) (describe (range)) (describe {1 [2]}) - (describe (with-viewer* (->Form '(fn [name] (html [:<> "Hello " name]))) "James"))) + (describe (with-viewer (->Form '(fn [name] (html [:<> "Hello " name]))) "James"))) (defn desc->values "Takes a `description` and returns its value. Inverse of `describe`. Mostly useful for debugging." @@ -450,7 +433,7 @@ (map desc->values)))))) #_(desc->values (describe [1 [2 {:a :b} 2] 3 (range 100)])) -#_(desc->values (describe (with-viewer* :table (normalize-table-data (repeat 60 ["Adelie" "Biscoe" 50 30 200 5000 :female]))))) +#_(desc->values (describe (with-viewer :table (normalize-table-data (repeat 60 ["Adelie" "Biscoe" 50 30 200 5000 :female]))))) (defn path-to-value [path] (conj (interleave path (repeat :nextjournal/value)) :nextjournal/value)) @@ -503,16 +486,17 @@ #_(datafy-scope *ns*) #_(datafy-scope #'datafy-scope) -(declare with-viewer*) +(declare with-viewer) #?(:clj - (defn set-viewers!* [scope viewers] - (assert (or (#{:root} scope) - (instance? clojure.lang.Namespace scope) - (var? scope))) - (let [viewers (process-fns viewers)] - (swap! !viewers assoc scope viewers) - (with-viewer* :eval! `'(v/set-viewers! ~(datafy-scope scope) ~viewers))))) + (defn set-viewers! + ([viewers] (set-viewers! *ns* viewers)) + ([scope viewers] + (assert (or (#{:root} scope) + (instance? clojure.lang.Namespace scope) + (var? scope))) + (swap! !viewers assoc scope viewers) + (with-viewer :eval! `'(v/set-viewers! ~(datafy-scope scope) ~viewers))))) (defn registration? [x] @@ -524,7 +508,7 @@ ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; public api -(defn with-viewer* +(defn with-viewer "Wraps given " [viewer x] (-> x @@ -533,25 +517,25 @@ #_(with-viewer- :latex "x^2") -(defn with-viewers* +(defn with-viewers "Binds viewers to types, eg {:boolean view-fn}" [viewers x] (-> x wrap-value (assoc :nextjournal/viewers viewers))) -#_(->> "x^2" (with-viewer* :latex) (with-viewers* [{:name :latex :render-fn :mathjax}])) +#_(->> "x^2" (with-viewer :latex) (with-viewers [{:name :latex :render-fn :mathjax}])) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; public convience api -(def html (partial with-viewer* :html)) -(def md (partial with-viewer* :markdown)) -(def plotly (partial with-viewer* :plotly)) -(def vl (partial with-viewer* :vega-lite)) -(def tex (partial with-viewer* :latex)) -(def hide-result (partial with-viewer* :hide-result)) -(def notebook (partial with-viewer* :clerk/notebook)) +(def html (partial with-viewer :html)) +(def md (partial with-viewer :markdown)) +(def plotly (partial with-viewer :plotly)) +(def vl (partial with-viewer :vega-lite)) +(def tex (partial with-viewer :latex)) +(def hide-result (partial with-viewer :hide-result)) +(def notebook (partial with-viewer :clerk/notebook)) (defn table "Displays `xs` in a table. @@ -566,8 +550,8 @@ (table {:nextjournal/width :wide} xs)) ([opts xs] (-> (if-let [normalized (normalize-table-data xs)] - (with-viewer* :table normalized) - (with-viewer* :table-error [xs])) + (with-viewer :table normalized) + (with-viewer :table-error [xs])) (merge opts) (update :nextjournal/viewers concat (:table @!viewers))))) @@ -576,6 +560,6 @@ #_(describe (table (set (range 10)))) (defn code [x] - (with-viewer* :code (if (string? x) x (with-out-str (pprint/pprint x))))) + (with-viewer :code (if (string? x) x (with-out-str (pprint/pprint x))))) #_(code '(+ 1 2)) From 29a078cabf321f68816244cbaa08b524d81a1600 Mon Sep 17 00:00:00 2001 From: Martin Kavalar Date: Thu, 13 Jan 2022 08:47:15 +0100 Subject: [PATCH 02/10] Fix all devcards --- src/nextjournal/clerk/viewer.cljc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/nextjournal/clerk/viewer.cljc b/src/nextjournal/clerk/viewer.cljc index ee5f60983..695997dbc 100644 --- a/src/nextjournal/clerk/viewer.cljc +++ b/src/nextjournal/clerk/viewer.cljc @@ -327,7 +327,7 @@ viewer (cond-> (dissoc viewer :pred :transform-fn :fetch-fn) (:render-fn viewer) - (update :render-fn ->Form)))) + (update :render-fn #?(:clj ->Form :cljs *eval*))))) #_(process-viewer {:render-fn '(v/html [:h1]) :fetch-fn fetch-all}) From bfc874d1329483d58855bd257537ff97ff5317e2 Mon Sep 17 00:00:00 2001 From: Martin Kavalar Date: Thu, 13 Jan 2022 09:24:36 +0100 Subject: [PATCH 03/10] Better names --- src/nextjournal/clerk/sci_viewer.cljs | 30 +++++++-------- src/nextjournal/clerk/viewer.cljc | 54 +++++++++++++-------------- 2 files changed, 40 insertions(+), 44 deletions(-) diff --git a/src/nextjournal/clerk/sci_viewer.cljs b/src/nextjournal/clerk/sci_viewer.cljs index f3d574e67..20f812779 100644 --- a/src/nextjournal/clerk/sci_viewer.cljs +++ b/src/nextjournal/clerk/sci_viewer.cljs @@ -95,8 +95,8 @@ :read-cond :allow :readers {'file (partial with-viewer :file) 'object (partial with-viewer :object) - 'function+ viewer/form->fn+form - 'sci-eval viewer/sci-eval} + 'viewer-fn viewer/->viewer-fn + 'sci-eval *eval*} :features #{:clj}})) (defn ^:export read-string [s] @@ -414,15 +414,17 @@ (declare default-viewers) + (defn maybe-eval [render-fn] - (js/console.log :maybe-eval render-fn :eval? (or (symbol? render-fn) (list? render-fn))) - (cond-> render-fn - (or (symbol? render-fn) (list? render-fn)) - *eval*)) + (if (or (symbol? render-fn) (list? render-fn)) + (do + (js/console.log :needs-eval render-fn) + (*eval* render-fn)) + render-fn)) (defn render-with-viewer [{:as opts :keys [viewers]} viewer value] - (js/console.log :render-with-viewer {:value value :viewer viewer #_#_ :opts opts}) - (cond (or (fn? viewer) (viewer/fn+form? viewer)) + #_(js/console.log :render-with-viewer {:value value :viewer viewer #_#_ :opts opts}) + (cond (or (fn? viewer) (viewer/viewer-fn? viewer)) (viewer value opts) (and (map? viewer) (:render-fn viewer)) @@ -447,7 +449,6 @@ {:as opts :keys [viewers]} (assoc opts :viewers (vec (concat (viewer/viewers x) viewers))) all-viewers (viewer/get-viewers (:scope @!doc) viewers) opts (update opts :recursion inc)] - (js/console.log :inspect value) (or (when (< 20 recursion) [:span "reached recursion limit"]) (when (react/isValidElement value) value) ;; TODO find option to disable client-side viewer selection @@ -644,7 +645,7 @@ (dc/defcard eval-viewer "Viewers that are lists are evaluated using sci." - [inspect (with-viewer (viewer/form->fn+form '(fn [x] (v/html [:h3 "Ohai, " x "! 👋"]))) "Hans")]) + [inspect (with-viewer (viewer/->viewer-fn '(fn [x] (v/html [:h3 "Ohai, " x "! 👋"]))) "Hans")]) (dc/defcard notebook @@ -673,11 +674,11 @@ [inspect {:path [] :viewers [{:pred number? - :render-fn (viewer/form->fn+form '#(v/html [:div.inline-block {:style {:width 16 :height 16} - :class (if (pos? %) "bg-black" "bg-white border-solid border-2 border- + :render-fn (viewer/->viewer-fn '#(v/html [:div.inline-block {:style {:width 16 :height 16} + :class (if (pos? %) "bg-black" "bg-white border-solid border-2 border- black")}]))} - {:pred vector? :render-fn (viewer/form->fn+form '#(v/html (into [:div.flex.inline-flex] (v/inspect-children %2) %1)))} - {:pred list? :render-fn (viewer/form->fn+form '#(v/html (into [:div.flex.flex-col] (v/inspect-children %2) %1)))}]} + {:pred vector? :render-fn (viewer/->viewer-fn '#(v/html (into [:div.flex.inline-flex] (v/inspect-children %2) %1)))} + {:pred list? :render-fn (viewer/->viewer-fn '#(v/html (into [:div.flex.flex-col] (v/inspect-children %2) %1)))}]} '([0 1 0] [1 0 1])]) (dc/defcard clj-long @@ -885,7 +886,6 @@ black")}]))} (html (katex/to-html-string tex-string))) (defn html-viewer [markup] - (js/console.log :html-viewer markup) (if (string? markup) (html [:div {:dangerouslySetInnerHTML {:__html markup}}]) (r/as-element markup))) diff --git a/src/nextjournal/clerk/viewer.cljc b/src/nextjournal/clerk/viewer.cljc index 695997dbc..b6161fd36 100644 --- a/src/nextjournal/clerk/viewer.cljc +++ b/src/nextjournal/clerk/viewer.cljc @@ -5,44 +5,35 @@ #?@(:clj [[clojure.repl :refer [demunge]] [nextjournal.clerk.config :as config]] :cljs [[reagent.ratom :as ratom]])) - #?(:clj (:import [clojure.lang IFn] - [java.lang Throwable]))) + #?(:clj (:import [java.lang Throwable]))) -(defrecord Form [form]) (defrecord SCIEval [form]) -(defrecord Fn+Form [form fn] - IFn - (#?(:clj invoke :cljs -invoke) [this x] - ((:fn this) x)) - (#?(:clj invoke :cljs -invoke) [this x y] - ((:fn this) x y))) +(defrecord ViewerFn [form #?(:cljs f)] + #?@(:cljs [IFn + (-invoke [this x] ((:f this) x)) + (-invoke [this x y] ((:f this) x y))])) -(defn fn+form? [x] - (instance? Fn+Form x)) +(defn viewer-fn? [x] + (instance? ViewerFn x)) -(defn form->fn+form - ([form] - (map->Fn+Form {:form form :fn (#?(:clj eval :cljs *eval*) form)}))) +(defn ->viewer-fn [form] + (map->ViewerFn {:form form :f #?(:clj nil :cljs (*eval* form))})) #?(:cljs (defn sci-eval [form] (*eval* form))) #?(:clj - (defmethod print-method Fn+Form [v ^java.io.Writer w] - (.write w (str "#function+ " (pr-str `~(:form v)))))) - -#?(:clj - (defmethod print-method Form [v ^java.io.Writer w] - (.write w (str "#function+ " (pr-str `~(:form v)))))) + (defmethod print-method ViewerFn [v ^java.io.Writer w] + (.write w (str "#viewer-fn " (pr-str `~(:form v)))))) #?(:clj (defmethod print-method SCIEval [v ^java.io.Writer w] (.write w (str "#sci-eval " (pr-str `~(:form v)))))) -#_(binding [*data-readers* {'function+ form->fn+form}] - (read-string (pr-str (form->fn+form '(fn [x] x))))) -#_(binding [*data-readers* {'function+ form->fn+form}] - (read-string (pr-str (form->fn+form 'number?)))) +#_(binding [*data-readers* {'viewer-fn ->viewer-fn}] + (read-string (pr-str (->viewer-fn '(fn [x] x))))) +#_(binding [*data-readers* {'viewer-fn ->viewer-fn}] + (read-string (pr-str (->viewer-fn 'number?)))) (comment (def num? (form->fn+form 'number?)) @@ -220,7 +211,7 @@ {:pred number? :render-fn '(fn [x] (v/html [:span.tabular-nums (if (js/Number.isNaN x) "NaN" (str x))]))}]) (def all-viewers - {:root default-viewers + {:root default-viewers :table default-table-cell-viewers}) (defonce @@ -265,7 +256,7 @@ (if-let [{:as named-viewer :keys [transform-fn]} (find-named-viewer viewers selected-viewer)] (wrap-value (cond-> x transform-fn transform-fn) named-viewer) (throw (ex-info (str "cannot find viewer named " selected-viewer) {:selected-viewer selected-viewer :x (value x) :viewers viewers}))) - (instance? Form selected-viewer) + (viewer-fn? selected-viewer) (wrap-value x selected-viewer)) (let [val (value x)] (loop [v viewers] @@ -322,12 +313,17 @@ (declare with-viewer assign-closing-parens) +(defn process-render-fn [{:as viewer :keys [render-fn]}] + (cond-> viewer + (and render-fn (not (viewer-fn? render-fn))) + (update :render-fn ->viewer-fn))) + (defn process-viewer [viewer] (if-not (map? viewer) viewer - (cond-> (dissoc viewer :pred :transform-fn :fetch-fn) - (:render-fn viewer) - (update :render-fn #?(:clj ->Form :cljs *eval*))))) + (-> viewer + (dissoc :pred :transform-fn :fetch-fn) + process-render-fn))) #_(process-viewer {:render-fn '(v/html [:h1]) :fetch-fn fetch-all}) From 77ba1c6f7b5570999dd1aa656a9b5ceb7c80ce9a Mon Sep 17 00:00:00 2001 From: Martin Kavalar Date: Thu, 13 Jan 2022 10:38:38 +0100 Subject: [PATCH 04/10] Preprocess viewer fns & drop maybe-eval --- src/nextjournal/clerk/sci_viewer.cljs | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/src/nextjournal/clerk/sci_viewer.cljs b/src/nextjournal/clerk/sci_viewer.cljs index 20f812779..3adb01f15 100644 --- a/src/nextjournal/clerk/sci_viewer.cljs +++ b/src/nextjournal/clerk/sci_viewer.cljs @@ -414,27 +414,19 @@ (declare default-viewers) - -(defn maybe-eval [render-fn] - (if (or (symbol? render-fn) (list? render-fn)) - (do - (js/console.log :needs-eval render-fn) - (*eval* render-fn)) - render-fn)) - (defn render-with-viewer [{:as opts :keys [viewers]} viewer value] #_(js/console.log :render-with-viewer {:value value :viewer viewer #_#_ :opts opts}) (cond (or (fn? viewer) (viewer/viewer-fn? viewer)) (viewer value opts) (and (map? viewer) (:render-fn viewer)) - (render-with-viewer opts (maybe-eval (:render-fn viewer)) value) + (render-with-viewer opts (:render-fn viewer) value) (keyword? viewer) (if-let [{:keys [fetch-opts render-fn]} (viewer/find-named-viewer viewers viewer)] (if-not render-fn (html (error-badge "no render function for viewer named " (str viewer))) - ((maybe-eval render-fn) value (assoc opts :fetch-opts fetch-opts))) + (render-fn value (assoc opts :fetch-opts fetch-opts))) (html (error-badge "cannot find viewer named " (str viewer)))) :else @@ -634,7 +626,6 @@ [inspect @!error]])]) (defn ^:export set-state [{:as state :keys [doc error]}] - (js/console.log :set-state state) (doseq [cell (viewer/value doc) :when (viewer/registration? cell) :let [form (viewer/value cell)]] @@ -981,3 +972,7 @@ black")}]))} (sci/eval-form @!sci-ctx f)) (set! *eval* eval-form) + +(swap! viewer/!viewers (fn [viewers] + (-> (into {} (map (juxt key (comp #(into [] (map viewer/process-render-fn) %) val))) viewers) + (update :root concat js-viewers)))) From 17aa68f6745bf844262d5aed45261770d9c567f6 Mon Sep 17 00:00:00 2001 From: Martin Kavalar Date: Thu, 13 Jan 2022 10:51:33 +0100 Subject: [PATCH 05/10] Better ViewerEval name --- src/nextjournal/clerk.clj | 2 +- src/nextjournal/clerk/sci_viewer.cljs | 5 ++--- src/nextjournal/clerk/viewer.cljc | 15 ++++++++++----- 3 files changed, 13 insertions(+), 9 deletions(-) diff --git a/src/nextjournal/clerk.clj b/src/nextjournal/clerk.clj index ff9a1ae40..abe685a5e 100644 --- a/src/nextjournal/clerk.clj +++ b/src/nextjournal/clerk.clj @@ -239,7 +239,7 @@ (def table #'v/table) (def use-headers #'v/use-headers) (def hide-result #'v/hide-result) -(defn doc-url [path] (v/->SCIEval (list 'v/doc-url path))) +(def doc-url #'v/doc-url) (def with-viewer #'v/with-viewer) (def with-viewers #'v/with-viewers) (def set-viewers! #'v/set-viewers!) diff --git a/src/nextjournal/clerk/sci_viewer.cljs b/src/nextjournal/clerk/sci_viewer.cljs index 3adb01f15..e28c6fd5c 100644 --- a/src/nextjournal/clerk/sci_viewer.cljs +++ b/src/nextjournal/clerk/sci_viewer.cljs @@ -96,7 +96,7 @@ :readers {'file (partial with-viewer :file) 'object (partial with-viewer :object) 'viewer-fn viewer/->viewer-fn - 'sci-eval *eval*} + 'viewer-eval *eval*} :features #{:clj}})) (defn ^:export read-string [s] @@ -391,8 +391,7 @@ [:span.inspected-value.whitespace-nowrap [:span.syntax-tag tag] value]) -(defn normalize-viewer - [x] +(defn normalize-viewer [x] (if-let [viewer (-> x meta :nextjournal/viewer)] (with-viewer viewer x) x)) diff --git a/src/nextjournal/clerk/viewer.cljc b/src/nextjournal/clerk/viewer.cljc index b6161fd36..d26cc6abd 100644 --- a/src/nextjournal/clerk/viewer.cljc +++ b/src/nextjournal/clerk/viewer.cljc @@ -4,10 +4,11 @@ [clojure.datafy :as datafy] #?@(:clj [[clojure.repl :refer [demunge]] [nextjournal.clerk.config :as config]] - :cljs [[reagent.ratom :as ratom]])) + :cljs [[reagent.ratom :as ratom]]) + [nextjournal.clerk.viewer :as v]) #?(:clj (:import [java.lang Throwable]))) -(defrecord SCIEval [form]) +(defrecord ViewerEval [form]) (defrecord ViewerFn [form #?(:cljs f)] #?@(:cljs [IFn (-invoke [this x] ((:f this) x)) @@ -20,15 +21,16 @@ (defn ->viewer-fn [form] (map->ViewerFn {:form form :f #?(:clj nil :cljs (*eval* form))})) -#?(:cljs (defn sci-eval [form] (*eval* form))) +(defn ->viewer-eval [form] + (map->ViewerEval {:form form})) #?(:clj (defmethod print-method ViewerFn [v ^java.io.Writer w] (.write w (str "#viewer-fn " (pr-str `~(:form v)))))) #?(:clj - (defmethod print-method SCIEval [v ^java.io.Writer w] - (.write w (str "#sci-eval " (pr-str `~(:form v)))))) + (defmethod print-method ViewerEval [v ^java.io.Writer w] + (.write w (str "#viewer-eval " (pr-str `~(:form v)))))) #_(binding [*data-readers* {'viewer-fn ->viewer-fn}] (read-string (pr-str (->viewer-fn '(fn [x] x))))) @@ -532,6 +534,9 @@ (def tex (partial with-viewer :latex)) (def hide-result (partial with-viewer :hide-result)) (def notebook (partial with-viewer :clerk/notebook)) +(defn doc-url [path] + (->viewer-eval (list 'v/doc-url path))) + (defn table "Displays `xs` in a table. From 85d04b422fb19632095d16b07c2a0b8514cd0049 Mon Sep 17 00:00:00 2001 From: Martin Kavalar Date: Thu, 13 Jan 2022 11:18:07 +0100 Subject: [PATCH 06/10] Fix image notebook --- notebooks/viewers/image.clj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/notebooks/viewers/image.clj b/notebooks/viewers/image.clj index 26131fd28..7aa9b964b 100644 --- a/notebooks/viewers/image.clj +++ b/notebooks/viewers/image.clj @@ -10,7 +10,7 @@ (clerk/set-viewers! [{:pred bytes? :fetch-fn (fn [_ bytes] {:nextjournal/content-type "image/png" :nextjournal/value bytes}) - :render-fn (fn [blob] (v/html [:img {:src (v/url-for blob)}]))}]) + :render-fn '(fn [blob] (v/html [:img {:src (v/url-for blob)}]))}]) (.. (HttpClient/newHttpClient) (send (.build (HttpRequest/newBuilder (URI. "https://upload.wikimedia.org/wikipedia/commons/5/57/James_Clerk_Maxwell.png"))) From c084fb02b3a1413e7dc16f6142e228e6b9c35640 Mon Sep 17 00:00:00 2001 From: Martin Kavalar Date: Thu, 13 Jan 2022 11:21:12 +0100 Subject: [PATCH 07/10] Nix require --- src/nextjournal/clerk/viewer.cljc | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/nextjournal/clerk/viewer.cljc b/src/nextjournal/clerk/viewer.cljc index d26cc6abd..aff91fdca 100644 --- a/src/nextjournal/clerk/viewer.cljc +++ b/src/nextjournal/clerk/viewer.cljc @@ -4,8 +4,7 @@ [clojure.datafy :as datafy] #?@(:clj [[clojure.repl :refer [demunge]] [nextjournal.clerk.config :as config]] - :cljs [[reagent.ratom :as ratom]]) - [nextjournal.clerk.viewer :as v]) + :cljs [[reagent.ratom :as ratom]])) #?(:clj (:import [java.lang Throwable]))) (defrecord ViewerEval [form]) From a4aa4c74ea23aecde15b2d2f0f8fd44ea2a5db77 Mon Sep 17 00:00:00 2001 From: Martin Kavalar Date: Thu, 13 Jan 2022 15:41:52 +0100 Subject: [PATCH 08/10] Fix with-viewer --- notebooks/dice.clj | 12 ++++++------ notebooks/viewer_api.clj | 2 +- src/nextjournal/clerk/viewer.cljc | 7 +++++-- 3 files changed, 12 insertions(+), 9 deletions(-) diff --git a/notebooks/dice.clj b/notebooks/dice.clj index 233d9ecd7..a16a36196 100644 --- a/notebooks/dice.clj +++ b/notebooks/dice.clj @@ -11,12 +11,12 @@ ;; Here, we define a viewer using hiccup that will the dice as well as a button. Note that this button has an `:on-click` event handler that uses `v/clerk-eval` to tell Clerk to evaluate the argument, in this cases `(roll!)` when clicked. ^::clerk/no-cache -(clerk/with-viewer (fn [side] - (v/html [:div.text-center - (when side - [:div.mt-2 {:style {:font-size "6em"}} side]) - [:button.bg-blue-500.hover:bg-blue-700.text-white.font-bold.py-2.px-4.rounded - {:on-click (fn [e] (v/clerk-eval '(roll!)))} "Roll 🎲!"]])) +(clerk/with-viewer '(fn [side] + (v/html [:div.text-center + (when side + [:div.mt-2 {:style {:font-size "6em"}} side]) + [:button.bg-blue-500.hover:bg-blue-700.text-white.font-bold.py-2.px-4.rounded + {:on-click (fn [e] (v/clerk-eval '(roll!)))} "Roll 🎲!"]])) @dice) ;; Our roll! function `resets!` our `dice` with a random side and prints and says the result. Finally it updates the notebook. diff --git a/notebooks/viewer_api.clj b/notebooks/viewer_api.clj index a27ceb910..d9ba8fe89 100644 --- a/notebooks/viewer_api.clj +++ b/notebooks/viewer_api.clj @@ -50,7 +50,7 @@ ;; ## 🚀 Extensibility (clerk/with-viewer '#(v/html [:div "Greetings to " [:strong %] "!"]) - "James Maxwell Clerk") + "James Clerk Maxwell") (clerk/with-viewers [{:pred number? :render-fn '#(v/html [:div.inline-block [(keyword (str "h" %)) (str "Heading " %)]])}] [1 2 3 4 5]) diff --git a/src/nextjournal/clerk/viewer.cljc b/src/nextjournal/clerk/viewer.cljc index aff91fdca..2bc9b9f97 100644 --- a/src/nextjournal/clerk/viewer.cljc +++ b/src/nextjournal/clerk/viewer.cljc @@ -510,9 +510,12 @@ [viewer x] (-> x wrap-value - (assoc :nextjournal/viewer viewer))) + (assoc :nextjournal/viewer (cond-> viewer + (or (list? viewer) (symbol? viewer)) + ->viewer-fn)))) -#_(with-viewer- :latex "x^2") +#_(with-viewer :latex "x^2") +#_(with-viewer '#(v/html [:h3 "Hello " % "!"]) "x^2") (defn with-viewers "Binds viewers to types, eg {:boolean view-fn}" From 9448cb637223dae840bae2a544000eda736e96a9 Mon Sep 17 00:00:00 2001 From: Martin Kavalar Date: Thu, 13 Jan 2022 15:54:42 +0100 Subject: [PATCH 09/10] Drop debug recursion limit --- src/nextjournal/clerk/sci_viewer.cljs | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/nextjournal/clerk/sci_viewer.cljs b/src/nextjournal/clerk/sci_viewer.cljs index e28c6fd5c..91fee7da3 100644 --- a/src/nextjournal/clerk/sci_viewer.cljs +++ b/src/nextjournal/clerk/sci_viewer.cljs @@ -434,14 +434,12 @@ (defn inspect ([x] (r/with-let [!expanded-at (r/atom {})] - [inspect {:!expanded-at !expanded-at :recursion 0} x])) - ([{:as opts :keys [viewers recursion]} x] + [inspect {:!expanded-at !expanded-at} x])) + ([{:as opts :keys [viewers]} x] (let [value (viewer/value x) {:as opts :keys [viewers]} (assoc opts :viewers (vec (concat (viewer/viewers x) viewers))) - all-viewers (viewer/get-viewers (:scope @!doc) viewers) - opts (update opts :recursion inc)] - (or (when (< 20 recursion) [:span "reached recursion limit"]) - (when (react/isValidElement value) value) + all-viewers (viewer/get-viewers (:scope @!doc) viewers)] + (or (when (react/isValidElement value) value) ;; TODO find option to disable client-side viewer selection (when-let [viewer (or (viewer/viewer x) (viewer/viewer (viewer/wrapped-with-viewer value all-viewers)))] From ac9dee5f814b62c3f9bc3df17883c9f849e18895 Mon Sep 17 00:00:00 2001 From: Martin Kavalar Date: Thu, 13 Jan 2022 19:39:26 +0100 Subject: [PATCH 10/10] Remove obsolete forward declarations --- src/nextjournal/clerk/viewer.cljc | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/nextjournal/clerk/viewer.cljc b/src/nextjournal/clerk/viewer.cljc index 2bc9b9f97..c8ee21085 100644 --- a/src/nextjournal/clerk/viewer.cljc +++ b/src/nextjournal/clerk/viewer.cljc @@ -145,15 +145,11 @@ (def elide-string-length 100) -(declare with-viewer) - (defn fetch-all [_ x] x) (defn- var-from-def? [x] (get x :nextjournal.clerk/var-from-def)) -(declare !viewers) - ;; keep viewer selection stricly in Clojure (def default-viewers ;; maybe make this a sorted-map