diff --git a/src/nextjournal/clerk/render.cljs b/src/nextjournal/clerk/render.cljs index 71ba532f3..8be0baa33 100644 --- a/src/nextjournal/clerk/render.cljs +++ b/src/nextjournal/clerk/render.cljs @@ -172,6 +172,14 @@ (defn render-unreadable-edn [edn] [:span.inspected-value.whitespace-nowrap.cmt-default edn]) +(def !read-string-without-tag-table + (delay (eval 'nextjournal.clerk.viewer/read-string-without-tag-table))) + +(defn render-read+inspect + [x] (try [inspect @!read-string-without-tag-table] + (catch js/Error _e + (render-unreadable-edn x)))) + (defn error-badge [& content] [:div.bg-red-50.rounded-sm.text-xs.text-red-400.px-2.py-1.items-center.sans-serif.inline-flex [:svg.h-4.w-4.text-red-400 {:xmlns "http://www.w3.org/2000/svg" :viewBox "0 0 20 20" :fill "currentColor" :aria-hidden "true"} @@ -333,6 +341,9 @@ (get-in x [:nextjournal/render-opts :id]) (with-meta {:key (str (get-in x [:nextjournal/render-opts :id]) "@" @!eval-counter)}))))) +(defn render-children [xs opts] + (into [:<>] (nextjournal.clerk.render/inspect-children opts) xs)) + (def expand-style ["cursor-pointer" "bg-indigo-50" @@ -435,6 +446,18 @@ [:span.cmt-number.inspected-value (if (js/Number.isNaN num) "NaN" (str num))]) +(defn render-hex-number [num] (render-number (str "0x" (.toString (js/Number. num) 16)))) + +(defn render-map-entry [xs opts] + (into [:<>] (comp (nextjournal.clerk.render/inspect-children opts) (interpose " ")) xs)) + +(defn render-symbol [x] [:span.cmt-keyword.inspected-value (str x)]) +(defn render-keyword [x] [:span.cmt-atom.inspected-value (str x)]) +(defn render-nil [_] [:span.cmt-default.inspected-value "nil"]) +(defn render-boolean [x] [:span.cmt-bool.inspected-value (str x)]) +(defn render-char [c] [:span.cmt-string.inspected-value "\\" c]) +(defn render-var [x] [:span.inspected-value [:span.cmt-meta "#'" (str x)]]) + (defn sort! [!sort i k] (let [{:keys [sort-key sort-order]} @!sort] (reset! !sort {:sort-index i @@ -526,12 +549,30 @@ [:div.rounded.border.border-red-200.border-t-0.overflow-hidden [throwable-view ex opts]])) -(defn render-tagged-value - ([tag value] (render-tagged-value {:space? true} tag value)) +(defn render-tagged-value* + ([tag value] (render-tagged-value* {:space? true} tag value)) ([{:keys [space?]} tag value] [:span.inspected-value.whitespace-nowrap [:span.cmt-meta tag] (when space? nbsp) value])) + +(defn render-tagged-value [{:keys [tag value space?]} opts] + [render-tagged-value + {:space? (:nextjournal/value space?)} + (str "#" (:nextjournal/value tag)) + [nextjournal.clerk.render/inspect-presented value]]) + +(defn render-js-object [v opts] + [render-tagged-value* {:space? true} + "#js" + [nextjournal.clerk.render/render-map v opts]]) + +(defn render-js-array [v opts] + [render-tagged-value* {:space? true} + "#js" + [nextjournal.clerk.render/render-coll v opts]]) + + (defn set-viewers! [scope viewers] #_(js/console.log :set-viewers! {:scope scope :viewers viewers}) (swap! !viewers assoc scope (vec viewers)) @@ -695,12 +736,16 @@ (let [re-eval (fn [{:keys [form]}] (viewer/->viewer-fn form))] (w/postwalk (fn [x] (cond-> x (viewer/viewer-fn? x) re-eval)) doc))) +(defn replace-viewer-fns [{:as doc :keys [name->viewer]}] + (assoc (w/postwalk-replace name->viewer doc) + :name->viewer name->viewer)) + (defn ^:export set-state! [{:as state :keys [doc]}] (when (contains? state :doc) (when (exists? js/window) ;; TODO: can we restore the scroll position when navigating back? (.scrollTo js/window #js {:top 0})) - (reset! !doc doc)) + (reset! !doc (replace-viewer-fns doc))) ;; (when (and error (contains? @!doc :status)) ;; (swap! !doc dissoc :status)) (when (remount? doc) @@ -713,7 +758,7 @@ (defn patch-state! [{:keys [patch]}] (if (remount? patch) - (do (swap! !doc #(re-eval-viewer-fns (apply-patch % patch))) + (do (swap! !doc #(re-eval-viewer-fns (replace-viewer-fns (apply-patch % patch)))) ;; TODO: figure out why it doesn't work without `js/setTimeout` (js/setTimeout #(swap! !eval-counter inc) 10)) (swap! !doc apply-patch patch))) @@ -967,6 +1012,9 @@ [:span {:dangerouslySetInnerHTML {:__html (.renderToString katex tex-string (j/obj :displayMode (not inline?) :throwOnError false))}}] default-loading-view))) +(defn render-katex-inline [tex opts] + (nextjournal.clerk.render/render-katex tex (assoc opts :inline? true))) + (defn render-mathjax [value] (let [mathjax (hooks/use-d3-require "https://run.nextjournalusercontent.com/data/QmQadTUYtF4JjbwhUFzQy9BQiK52ace3KqVHreUqL7ohoZ?filename=es5/tex-svg-full.js&content-type=application/javascript") ref-fn (react/useCallback (fn [el] @@ -1027,10 +1075,44 @@ [render-code code-string (assoc opts :language "clojure")]]]))) +(defn render-example [{:keys [form val]} opts] + [:div.mb-3.last:mb-0 + [:div.bg-slate-100.dark:bg-slate-800.px-4.py-2.border-l-2.border-slate-200.dark:border-slate-700 + (inspect-presented opts form)] + [:div.pt-2.px-4.border-l-2.border-transparent + (inspect-presented opts val)]]) + +(defn render-examples [examples opts] + [:div + [:div.uppercase.tracking-wider.text-xs.font-sans.font-bold.text-slate-500.dark:text-white.mb-2.mt-3 "Examples"] + (into [:div] (inspect-children opts) examples)]) + +(defn render-row [items opts] + (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 + [inspect-presented opts item]])) + items)) + +(defn render-col [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 + [inspect-presented opts item]])) + items)) + +(defn render-empty-fragment [_ _] [:<>]) + (defn url-for [{:as src :keys [blob-id]}] (if (string? src) src (str "/_blob/" blob-id (when-let [opts (seq (dissoc src :blob-id))] (str "?" (opts->query opts)))))) +(defn render-image [blob-or-url] + [:div.flex.flex-col.items-center.not-prose + [:img {:src (url-for blob-or-url)}]]) + (def consume-view-context view-context/consume) diff --git a/src/nextjournal/clerk/viewer.cljc b/src/nextjournal/clerk/viewer.cljc index 7a5018fc6..17b18e32b 100644 --- a/src/nextjournal/clerk/viewer.cljc +++ b/src/nextjournal/clerk/viewer.cljc @@ -660,7 +660,7 @@ (->> (mapv (partial with-viewer (cond-> result-viewer (hidden-viewer-eval-result? cell) - (assoc :render-fn '(fn [_ _] [:<>])))))))) + (assoc :render-fn 'nextjournal.clerk.render/render-empty-fragment))))))) (defn transform-cell [cell] (let [{:keys [code? result?]} (->visibility cell)] @@ -677,7 +677,7 @@ (def cell-viewer {:name `cell-viewer :transform-fn (update-val transform-cell) - :render-fn '(fn [xs opts] (into [:<>] (nextjournal.clerk.render/inspect-children opts) xs))}) + :render-fn 'nextjournal.clerk.render/render-children}) (defn lift-block-images "Lift an image node to top-level when it is the only child of a paragraph." @@ -750,7 +750,7 @@ (def table-missing-viewer {:name `table-missing-viewer :pred #{:nextjournal/missing} - :render-fn '(fn [x] [:<>])}) + :render-fn 'nextjournal.clerk.render/render-empty-fragment}) (def table-markup-viewer {:name `table-markup-viewer @@ -809,7 +809,7 @@ ;; formulas {:name :nextjournal.markdown/formula :transform-fn (comp :text ->value) - :render-fn '(fn [tex] (nextjournal.clerk.render/render-katex tex {:inline? true}))} + :render-fn 'nextjournal.clerk.render/render-katex-inline} {:name :nextjournal.markdown/block-formula :transform-fn (comp :text ->value) :render-fn 'nextjournal.clerk.render/render-katex} @@ -855,7 +855,7 @@ :transform-fn (fn [wrapped-value] (with-viewer `html-viewer [:sup.sidenote-ref (-> wrapped-value ->value :ref inc)]))}]) (def char-viewer - {:name `char-viewer :pred char? :render-fn '(fn [c] [:span.cmt-string.inspected-value "\\" c])}) + {:name `char-viewer :pred char? :render-fn 'nextjournal.clerk.render/char-viewer}) (def string-viewer {:name `string-viewer @@ -874,27 +874,25 @@ (instance? clojure.lang.BigInt %)) pr-str))])}) (def number-hex-viewer - {:name `number-hex-viewer :render-fn '(fn [num] (nextjournal.clerk.render/render-number (str "0x" (.toString (js/Number. num) 16))))}) + {:name `number-hex-viewer :render-fn 'nextjournal.clerk.render/render-hex-number}) (def symbol-viewer - {:name `symbol-viewer :pred symbol? :render-fn '(fn [x] [:span.cmt-keyword.inspected-value (str x)])}) + {:name `symbol-viewer :pred symbol? :render-fn 'nextjournal.clerk.render/render-symbol}) (def keyword-viewer - {:name `keyword-viewer :pred keyword? :render-fn '(fn [x] [:span.cmt-atom.inspected-value (str x)])}) + {:name `keyword-viewer :pred keyword? :render-fn 'nextjournal.clerk.render/render-keyword}) (def nil-viewer - {:name `nil-viewer :pred nil? :render-fn '(fn [_] [:span.cmt-default.inspected-value "nil"])}) + {:name `nil-viewer :pred nil? :render-fn 'nextjournal.clerk.render/render-nil}) (def boolean-viewer - {:name `boolean-viewer :pred boolean? :render-fn '(fn [x] [:span.cmt-bool.inspected-value (str x)])}) + {:name `boolean-viewer :pred boolean? :render-fn 'nextjournal.clerk.render/render-boolean}) (def map-entry-viewer - {:name `map-entry-viewer :pred map-entry? :render-fn '(fn [xs opts] (into [:<>] (comp (nextjournal.clerk.render/inspect-children opts) (interpose " ")) xs)) :page-size 2}) + {:name `map-entry-viewer :pred map-entry? :render-fn 'nextjournal.clerk.render/render-map-entry :page-size 2}) (def read+inspect-viewer - {:name `read+inspect-viewer :render-fn '(fn [x] (try [nextjournal.clerk.render/inspect (nextjournal.clerk.viewer/read-string-without-tag-table x)] - (catch js/Error _e - (nextjournal.clerk.render/render-unreadable-edn x))))}) + {:name `read+inspect-viewer :render-fn 'nextjournal.clerk.render/render-read+inspect}) (def vector-viewer {:name `vector-viewer :pred vector? :render-fn 'nextjournal.clerk.render/render-coll :opening-paren "[" :closing-paren "]" :page-size 20}) @@ -914,7 +912,7 @@ {:name `var-viewer :pred var? :transform-fn (comp #?(:cljs var->symbol :clj symbol) ->value) - :render-fn '(fn [x] [:span.inspected-value [:span.cmt-meta "#'" (str x)]])}) + :render-fn 'nextjournal.clerk.render/render-var}) (defn ->opts [wrapped-value] (select-keys wrapped-value [:nextjournal/budget :nextjournal/css-class :nextjournal/width :nextjournal/render-opts @@ -961,9 +959,7 @@ :nextjournal/width (image-width image)} mark-presented))]) :name `image-viewer - :render-fn '(fn [blob-or-url] [:div.flex.flex-col.items-center.not-prose - [:img {:src #?(:clj (nextjournal.clerk.render/url-for blob-or-url) - :cljs blob-or-url)}]])}) + :render-fn 'nextjournal.clerk.render/render-image}) (def ideref-viewer {:name `ideref-viewer @@ -1038,21 +1034,10 @@ (update-val (fn [v] (if (string? v) v (str/trim (with-out-str (pprint/pprint v)))))))}) (def row-viewer - {:name `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 `row-viewer :render-fn 'nextjournal.clerk.render/render-row}) (def col-viewer - {:name `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 `col-viewer :render-fn 'nextjournal.clerk.render/render-col}) (def table-viewers [(-> string-viewer @@ -1111,14 +1096,9 @@ (def tagged-value-viewer {:name `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])) + :render-fn 'nextjournal.clerk.render/render-tagged-value :transform-fn mark-preserve-keys}) - #?(:cljs (def js-promise-viewer {:name `js-promise-viewer :pred #(instance? js/Promise %) :render-fn 'nextjournal.clerk.render/render-promise})) @@ -1129,9 +1109,7 @@ :pred goog/isObject :page-size 20 :opening-paren "{" :closing-paren "}" - :render-fn '(fn [v opts] (nextjournal.clerk.render/render-tagged-value {:space? true} - "#js" - (nextjournal.clerk.render/render-map v opts))) + :render-fn 'nextjournal.clerk.render/render-js-object :transform-fn (update-val (fn [^js o] (into {} (comp (remove (fn [k] (identical? "function" (goog/typeOf (j/get o k))))) @@ -1148,10 +1126,7 @@ {:name `js-array-viewer :pred js-iterable? :transform-fn (update-val seq) - :render-fn '(fn [v opts] - (nextjournal.clerk.render/render-tagged-value {:space? true} - "#js" - (nextjournal.clerk.render/render-coll v opts))) + :render-fn 'nextjournal.clerk.render/render-js-array :opening-paren "[" :closing-paren "]" :page-size 20})) @@ -1977,12 +1952,7 @@ :transform-fn (fn [wrapped-value] (-> wrapped-value mark-preserve-keys - (assoc :nextjournal/viewer {:render-fn '(fn [{:keys [form val]} opts] - [:div.mb-3.last:mb-0 - [:div.bg-slate-100.dark:bg-slate-800.px-4.py-2.border-l-2.border-slate-200.dark:border-slate-700 - (nextjournal.clerk.render/inspect-presented opts form)] - [:div.pt-2.px-4.border-l-2.border-transparent - (nextjournal.clerk.render/inspect-presented opts val)]])}) + (assoc :nextjournal/viewer {:render-fn 'nextjournal.clerk.render/render-example}) (update-in [:nextjournal/value :val] maybe-wrap-var-from-def (get-in wrapped-value [:nextjournal/value :form])) (update-in [:nextjournal/value :form] code)))}) @@ -1990,8 +1960,4 @@ {:name `examples-viewer :transform-fn (update-val (fn [examples] (mapv (partial with-viewer example-viewer) examples))) - :render-fn '(fn [examples opts] - [:div - [:div.uppercase.tracking-wider.text-xs.font-sans.font-bold.text-slate-500.dark:text-white.mb-2.mt-3 "Examples"] - (into [:div] - (nextjournal.clerk.render/inspect-children opts) examples)])}) + :render-fn 'nextjournal.clerk.render/render-examples})