Skip to content

Commit c4f5fe1

Browse files
zampinomk
andauthored
Improve Tap Inspector (#447)
This ports the tap inspector to `clerk/fragment` with the following benefits: * stable react keys keep views from re-rendering * support assigning custom width per tap, fixes #178 In addition, this fixes a regression of not respecting the custom per-result / fragment budgets which as introduced with #443. --------- Co-authored-by: Martin Kavalar <[email protected]>
1 parent 0de032d commit c4f5fe1

File tree

7 files changed

+95
-83
lines changed

7 files changed

+95
-83
lines changed

notebooks/fragments.clj

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,11 @@
1010
(clerk/plotly {::clerk/width :full} {:data [{:y [1 3 2]}]})
1111
(clerk/html {::clerk/width :full} [:div.h-20.bg-amber-200])
1212
(clerk/fragment (clerk/html {::clerk/width :full} [:div.h-20.bg-amber-300])
13-
(clerk/html {::clerk/width :full} [:div.h-20.bg-amber-400])))
13+
(clerk/html {::clerk/width :full} [:div.h-20.bg-amber-400])
14+
(reduce (fn [acc i] (vector i acc)) :fin (range 200 0 -1))
15+
(reduce (fn [acc i] (vector i acc)) :fin (range 200 0 -1))
16+
(clerk/with-viewer {} {::clerk/budget 5}
17+
(reduce (fn [acc i] (vector i acc)) :fin (range 15 0 -1)))))
1418

1519
;; ## Collapsible Sections
1620
;; Fragments allow to hide (and in future versions of Clerk, probably fold) chunks of prose interspersed with results. That is, by using the usual visibility annotation

notebooks/pagination.clj

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,9 @@
6969
^{::clerk/budget 5}
7070
(reduce (fn [acc i] (vector i acc)) :fin (range 15 0 -1))
7171

72+
(clerk/with-viewer {}
73+
{::clerk/budget 5}
74+
(reduce (fn [acc i] (vector i acc)) :fin (range 15 0 -1)))
7275

7376
(clerk/html [:div
7477
[:h3 "Nesting Images inside " [:span.font-mono "clerk/html"]]
@@ -81,3 +84,9 @@
8184
[:p [:strong {:nextjournal/value (range 30)}]]]
8285
[:li "A slanted range:"
8386
[:p [:em {:nextjournal/value (range 100)}]]]]])
87+
88+
(clerk/html
89+
{::clerk/budget 5}
90+
[:div
91+
[:h3 "Configuring budget inside " [:span.font-mono "clerk/html"]]
92+
{:nextjournal/value (reduce (fn [acc i] (vector i acc)) :fin (range 15 0 -1))}])

src/nextjournal/clerk/render.cljs

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -305,7 +305,7 @@
305305
:nested-prose "w-full max-w-prose"
306306
"w-full max-w-prose px-8")])))
307307

308-
(defn render-result [{:as result :nextjournal/keys [fetch-opts hash presented]} {:as opts :keys [id auto-expand-results? path]}]
308+
(defn render-result [{:nextjournal/keys [fetch-opts hash presented]} {:keys [id auto-expand-results?]}]
309309
(let [!desc (hooks/use-state-with-deps presented [hash])
310310
!expanded-at (hooks/use-state (when (map? @!desc)
311311
(->expanded-at auto-expand-results? @!desc)))
@@ -359,10 +359,9 @@
359359
(< 1 (count xs)))
360360

361361
(defn inspect-children [opts]
362-
;; TODO: move update function onto viewer
363-
(map-indexed (fn [idx x]
364-
(cond-> [inspect-presented (update opts :path (fnil conj []) idx) x]
365-
(get-in x [:nextjournal/opts :id]) (with-meta {:key (str (get-in x [:nextjournal/opts :id]) "@" @!eval-counter)})))))
362+
(map (fn [x] (cond-> [inspect-presented opts x]
363+
(get-in x [:nextjournal/opts :id])
364+
(with-meta {:key (str (get-in x [:nextjournal/opts :id]) "@" @!eval-counter)})))))
366365

367366
(def expand-style
368367
["cursor-pointer"
@@ -719,9 +718,6 @@
719718
(when react-root
720719
(.render react-root (r/as-element [root]))))
721720

722-
(defn render-with-react-key [x {:as _opts :keys [id]}]
723-
(with-meta x {:key id}))
724-
725721
(defn html-render [markup]
726722
(r/as-element
727723
(if (string? markup)
@@ -822,7 +818,6 @@
822818
[:path {:fill-rule "evenodd" :d "M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" :clip-rule "evenodd"}]])
823819

824820
(defn render-code-block [code-string {:keys [id]}]
825-
^{:key id}
826821
[:div.viewer.code-viewer.w-full.max-w-wide {:data-block-id id}
827822
[code/render-code code-string]])
828823

@@ -858,7 +853,6 @@
858853
[:span.ml-4.opacity-0.translate-y-full.group-hover:opacity-100.group-hover:translate-y-0.transition-all.delay-150.hover:text-slate-500
859854
{:class "text-[10px]"}
860855
"evaluated in 0.2s"]]
861-
^{:key id}
862856
[:div.code-viewer.mb-2.relative.code-viewer.w-full.max-w-wide {:data-block-id id :style {:margin-top 0}}
863857
[render-code code-string]]])))
864858

src/nextjournal/clerk/render/code.cljs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -87,14 +87,13 @@
8787
(< pos to)
8888
(concat [(.sliceString text pos to)]))))))))
8989

90-
(defn render-code [^String code {:as _opts :keys [id]}]
90+
(defn render-code [^String code _]
9191
(let [builder (RangeSetBuilder.)
9292
_ (highlightTree (.. clojureLanguage -parser (parse code)) highlight-style
9393
(fn [from to style]
9494
(.add builder from to (.mark Decoration (j/obj :class style)))))
9595
decorations-rangeset (.finish builder)
9696
text (.of Text (.split code "\n"))]
97-
^{:key id}
9897
[:div.cm-editor
9998
[:cm-scroller
10099
(into [:div.cm-content.whitespace-pre]

src/nextjournal/clerk/tap.clj

Lines changed: 35 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,11 @@
44
(:require [clojure.core :as core]
55
[nextjournal.clerk :as clerk]
66
[nextjournal.clerk.viewer :as v])
7-
(:import (java.time LocalTime ZoneId)))
7+
(:import (java.time Instant LocalTime ZoneId)))
8+
9+
(defn inst->local-time-str [inst] (str (LocalTime/ofInstant inst (ZoneId/systemDefault))))
10+
(defn record-tap [x]
11+
{::val x ::tapped-at (Instant/now) ::key (str (gensym))})
812

913
(def switch-view
1014
(assoc v/viewer-eval-viewer
@@ -24,77 +28,54 @@
2428
^{::clerk/sync true ::clerk/viewer switch-view ::clerk/visibility {:result :show}}
2529
(defonce !view (atom :stream))
2630

27-
(defonce !taps (atom []))
31+
32+
(defonce !taps (atom ()))
2833

2934
(defn reset-taps! []
30-
(reset! !taps [])
35+
(reset! !taps ())
3136
(clerk/recompute!))
3237

33-
#_(reset-taps!)
34-
35-
(defn inst->local-time-str [inst]
36-
(str (LocalTime/ofInstant inst (ZoneId/systemDefault))))
37-
38-
#_(inst->local-time-str (Instant/now))
39-
40-
(def tap-viewer
41-
{:name `tap-viewer
42-
:render-fn '(fn [{:keys [val tapped-at key]} opts]
43-
(with-meta
44-
[:div.border-t.relative.py-3
45-
[:span.absolute.rounded-full.px-2.bg-gray-300.font-mono.top-0
46-
{:class "left-1/2 -translate-x-1/2 -translate-y-1/2 py-[1px] text-[9px]"} (:nextjournal/value tapped-at)]
47-
[:div.overflow-x-auto [nextjournal.clerk.render/inspect-presented val]]]
48-
{:key (:nextjournal/value key)}))
49-
:transform-fn (comp clerk/mark-preserve-keys
50-
(clerk/update-val #(update % :tapped-at inst->local-time-str)))})
51-
52-
(clerk/add-viewers! [tap-viewer])
53-
54-
(def taps-viewer
55-
{:render-fn '#(into [:div.flex.flex-col.pt-2] (nextjournal.clerk.render/inspect-children %2) %1)
56-
:transform-fn (clerk/update-val (fn [taps]
57-
(mapv (partial clerk/with-viewer `tap-viewer) (reverse taps))))})
58-
59-
^{::clerk/visibility {:result :show}
60-
::clerk/viewer (cond-> taps-viewer
61-
(= :latest @!view)
62-
(update :transform-fn (fn [orig] (comp orig (clerk/update-val (partial take-last 1))))))}
63-
@!taps
64-
6538
(defn tapped [x]
66-
(swap! !taps conj {:val x :tapped-at (java.time.Instant/now) :key (str (gensym))})
39+
(swap! !taps conj (record-tap x))
6740
(clerk/recompute!))
6841

69-
#_(tapped (rand-int 1000))
70-
71-
#_(reset! @(find-var 'clojure.core/tapset) #{})
42+
(defonce tap-setup
43+
(add-tap (fn [x] ((resolve `tapped) x))))
7244

73-
(defonce setup
74-
(add-tap tapped))
45+
(def tap-viewer
46+
{:pred (v/get-safe ::val)
47+
:render-fn '(fn [{::keys [val tapped-at]} opts]
48+
[:div.border-t.relative.py-3.mt-2
49+
[:span.absolute.rounded-full.px-2.bg-gray-300.font-mono.top-0
50+
{:class "left-1/2 -translate-x-1/2 -translate-y-1/2 py-[1px] text-[9px]"} (:nextjournal/value tapped-at)]
51+
[:div.overflow-x-auto [nextjournal.clerk.render/inspect-presented val]]])
52+
:transform-fn (fn [{:as wrapped-value :nextjournal/keys [value]}]
53+
(-> wrapped-value
54+
v/mark-preserve-keys
55+
(merge (v/->opts (v/ensure-wrapped (:val value)))) ;; preserve opts like ::clerk/width and ::clerk/css-class
56+
(assoc-in [:nextjournal/opts :id] (:key value)) ;; assign custom react key
57+
(update-in [:nextjournal/value ::tapped-at] inst->local-time-str)))})
7558

76-
#_ (remove-tap tapped)
7759

60+
^{::clerk/visibility {:result :show}
61+
::clerk/viewers (v/add-viewers [tap-viewer])}
62+
(clerk/fragment (cond->> @!taps
63+
(= :latest @!view) (take 1)))
7864

7965
(comment
8066
(last @!taps)
81-
8267
(dotimes [_i 5]
8368
(tap> (rand-int 1000)))
84-
8569
(tap> (shuffle (range (+ 20 (rand-int 200)))))
8670
(tap> (clerk/md "> The purpose of visualization is **insight**, not pictures."))
87-
(tap> (v/plotly {:data [{:z [[1 2 3]
88-
[3 2 1]]
89-
:type "surface"}]}))
90-
(tap> (javax.imageio.ImageIO/read (java.net.URL. "https://images.freeimages.com/images/large-previews/773/koldalen-4-1384902.jpg")))
91-
71+
(tap> (v/plotly {:data [{:z [[1 2 3] [3 2 1]] :type "surface"}]}))
72+
(tap> (clerk/html {::clerk/width :full} [:h1.w-full.border-2.border-amber-500.bg-amber-500.h-10]))
73+
(tap> (clerk/table {::clerk/width :full} [[1 2] [3 4]]))
74+
(tap> (clerk/plotly {::clerk/width :full} {:data [{:y [3 1 2]}]}))
75+
(tap> (clerk/image "trees.png"))
9276
(do (require 'rule-30)
9377
(tap> (clerk/with-viewers (clerk/add-viewers rule-30/viewers) rule-30/rule-30)))
94-
9578
(tap> (clerk/with-viewers (clerk/add-viewers rule-30/viewers) rule-30/board))
96-
9779
(tap> (clerk/html [:h1 "Fin. 👋"]))
98-
99-
(reset-taps!)
100-
)
80+
(tap> (reduce (fn [acc _] (vector acc)) :fin (range 200)))
81+
(reset-taps!))

src/nextjournal/clerk/viewer.cljc

Lines changed: 18 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -313,7 +313,7 @@
313313

314314
(defn into-markup [markup]
315315
(fn [{:as wrapped-value :nextjournal/keys [viewers opts]}]
316-
(-> (with-viewer {:name `markdown-node-viewer :render-fn 'nextjournal.clerk.render/render-with-react-key} wrapped-value)
316+
(-> (with-viewer {:name `markdown-node-viewer :render-fn 'identity} wrapped-value)
317317
mark-presented
318318
(update :nextjournal/value
319319
(fn [{:as node :keys [text content] ::keys [doc]}]
@@ -474,24 +474,26 @@
474474
(defn transform-result [{:as wrapped-value :keys [path]}]
475475
(let [{:as _cell :keys [form id] ::keys [result doc]} (:nextjournal/value wrapped-value)
476476
{:keys [auto-expand-results? inline-results? bundle?]} doc
477-
{:nextjournal/keys [value blob-id budget viewers]} result
477+
{:nextjournal/keys [value blob-id viewers]} result
478478
blob-mode (cond
479479
(and (not inline-results?) blob-id) :lazy-load
480480
bundle? :inline ;; TODO: provide a separte setting for this
481481
:else :file)
482482
#?(:clj blob-opts :cljs _) (assoc doc :blob-mode blob-mode :blob-id blob-id)
483483
opts-from-form-meta (-> result
484-
(select-keys [:nextjournal/css-class :nextjournal/width :nextjournal/opts])
484+
(select-keys [:nextjournal/css-class :nextjournal/width :nextjournal/opts :nextjournal/budget])
485485
(cond-> #_result
486486
(some? auto-expand-results?) (update :nextjournal/opts #(merge {:auto-expand-results? auto-expand-results?} %))))
487-
presented-result (->> (present (cond-> (merge (->opts wrapped-value)
488-
(ensure-wrapped-with-viewers (or viewers (get-viewers *ns*)) value))
489-
true (merge opts-from-form-meta)
490-
true (assoc-in [:nextjournal/opts :id] (processed-block-id (str id "-result") path))
491-
(seq path) (assoc-in [:nextjournal/opts :fragment-item?] true)
492-
(contains? result :nextjournal/budget) (assoc :nextjournal/budget budget)))
493-
#?(:clj (process-blobs blob-opts)))
494-
487+
presented-result (-> (present (merge (dissoc (->opts wrapped-value) :!budget)
488+
;; reset budget from top level form for fragment items to have their own
489+
(ensure-wrapped-with-viewers (or viewers (get-viewers *ns*)) value)
490+
opts-from-form-meta))
491+
(update :nextjournal/opts
492+
(fn [{:as opts existing-id :id}]
493+
(cond-> opts
494+
(seq path) (assoc :fragment-item? true)
495+
(not existing-id) (assoc :id (processed-block-id (str id "-result") path)))))
496+
#?(:clj (->> (process-blobs blob-opts))))
495497
viewer-eval-result? (-> presented-result :nextjournal/value viewer-eval?)]
496498
#_(prn :presented-result viewer-eval? presented-result)
497499
(-> wrapped-value
@@ -1379,12 +1381,14 @@
13791381
(:nextjournal/budget opts 200))
13801382

13811383
(defn make-!budget-opts [opts]
1382-
(when-let [budget (->budget opts)]
1383-
{:!budget (atom budget)}))
1384+
(let [budget (->budget opts)]
1385+
(cond-> {:nextjournal/budget budget}
1386+
budget (assoc :!budget (atom budget)))))
13841387

13851388
#_(make-!budget-opts {})
13861389
#_(make-!budget-opts {:nextjournal/budget 42})
13871390
#_(make-!budget-opts {:nextjournal/budget nil})
1391+
#_(make-!budget-opts (make-!budget-opts {:nextjournal/budget nil}))
13881392

13891393
(defn ^:private present-elision* [!path->wrapped-value {:as fetch-opts :keys [path]}]
13901394
(if-let [wrapped-value (@!path->wrapped-value path)]
@@ -1497,8 +1501,7 @@
14971501
(->opts (normalize-viewer-opts x)))
14981502
!path->wrapped-value (atom {})]
14991503
(-> (ensure-wrapped-with-viewers x)
1500-
(merge {:nextjournal/budget (->budget opts)
1501-
:store!-wrapped-value (fn [{:as wrapped-value :keys [path]}]
1504+
(merge {:store!-wrapped-value (fn [{:as wrapped-value :keys [path]}]
15021505
(swap! !path->wrapped-value assoc path wrapped-value))
15031506
:present-elision-fn (partial present-elision* !path->wrapped-value)
15041507
:path (:path opts [])}

test/nextjournal/clerk/viewer_test.clj

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -312,7 +312,29 @@
312312
(->> (eval/eval-string "1\n(nextjournal.clerk/fragment 2 3 (nextjournal.clerk/fragment 4 5))\n6")
313313
view/doc->viewer v/->value :blocks
314314
(tree-seq coll? seq)
315-
(filter (every-pred map? :nextjournal/presented :nextjournal/blob-id))))))))
315+
(filter (every-pred map? :nextjournal/presented :nextjournal/blob-id)))))))
316+
317+
(testing "customizing budget, user-facing"
318+
(is (= 5
319+
(count
320+
(->> (eval/eval-string "^{:nextjournal.clerk/budget 5}(reduce (fn [acc _i] (vector acc)) :fin (range 100 0 -1))")
321+
view/doc->viewer v/->value :blocks
322+
(tree-seq coll? seq)
323+
(filter (every-pred map? (comp #{'nextjournal.clerk.render/render-coll} :form :render-fn)))))))
324+
325+
(is (= 5
326+
(count
327+
(->> (eval/eval-string "(nextjournal.clerk/with-viewer {} {:nextjournal.clerk/budget 5} (reduce (fn [acc i] (vector acc)) :fin (range 15 0 -1)))")
328+
view/doc->viewer v/->value :blocks
329+
(tree-seq coll? seq)
330+
(filter (every-pred map? (comp #{'nextjournal.clerk.render/render-coll} :form :render-fn)))))))
331+
332+
(is (= 101
333+
(count
334+
(->> (eval/eval-string "^{:nextjournal.clerk/budget nil}(reduce (fn [acc i] (vector i acc)) :fin (range 101 0 -1))")
335+
view/doc->viewer v/->value :blocks
336+
(tree-seq coll? seq)
337+
(filter (every-pred map? (comp #{'nextjournal.clerk.render/render-coll} :form :render-fn)))))))))
316338

317339
(deftest ->edn
318340
(testing "normal symbols and keywords"

0 commit comments

Comments
 (0)