diff --git a/src/nextjournal/clerk/viewer.cljc b/src/nextjournal/clerk/viewer.cljc index b040cbbd0..a311dd7a9 100644 --- a/src/nextjournal/clerk/viewer.cljc +++ b/src/nextjournal/clerk/viewer.cljc @@ -429,27 +429,46 @@ (defn var-from-def? [x] (var? (get-safe x :nextjournal.clerk/var-from-def))) +(def unwrap-var-value (some-fn :nextjournal.clerk/var-snapshot + (comp deref :nextjournal.clerk/var-from-def))) + (def var-from-def-viewer {:name `var-from-def-viewer :pred var-from-def? - :transform-fn (update-val (some-fn :nextjournal.clerk/var-snapshot - (comp deref :nextjournal.clerk/var-from-def)))}) + :transform-fn (update-val unwrap-var-value)}) (defn apply-viewer-unwrapping-var-from-def - "Applies the `viewer` (if set) to the given result `result`. In case - the `value` is a `var-from-def?` it will be unwrapped unless the - viewer opts out with a truthy `:nextjournal.clerk/var-from-def`." + "Applies the `viewer` (if set) to the given result `result`. By default + the `value` of a `var-from-def?` is unwrapped so that the viewer + operates on the def's value. A viewer that wants the raw wrapper + sets `:var-from-def? true` — on the viewer map directly, or on the + viewer map that a fn viewer returns (the fn will be called a second + time with the raw `var-from-def?` value in that case)." [{:as result :nextjournal/keys [value viewer]}] (if viewer - (let [value+viewer (if (or (var? viewer) (fn? viewer)) - (viewer value) - {:nextjournal/value value - :nextjournal/viewer (normalize-viewer viewer)}) - {unwrap-var :transform-fn var-from-def? :pred} var-from-def-viewer] - (assoc result :nextjournal/value (cond-> value+viewer - (and (var-from-def? value) - (-> value+viewer ->viewer :var-from-def? not)) - unwrap-var))) + (let [fn-viewer? (or (var? viewer) (fn? viewer)) + is-var? (var-from-def? value) + keep-wrapped? (and (map? viewer) (:var-from-def? viewer)) + apply-vw (fn [v] + (if fn-viewer? + (viewer v) + {:nextjournal/value v + :nextjournal/viewer (normalize-viewer viewer)})) + first-try (apply-vw (cond-> value + (and is-var? fn-viewer? (not keep-wrapped?)) + unwrap-var-value)) + return-keep-wrapped? (boolean (-> first-try ->viewer :var-from-def?)) + value+viewer (cond + ;; map viewer that doesn't want the wrapper -> unwrap + (and is-var? (not fn-viewer?) (not keep-wrapped?)) + (update first-try :nextjournal/value unwrap-var-value) + + ;; fn viewer returned a viewer that wants the wrapper -> re-run with it + (and is-var? fn-viewer? return-keep-wrapped?) + (apply-vw value) + + :else first-try)] + (assoc result :nextjournal/value value+viewer)) result)) #?(:clj diff --git a/test/nextjournal/clerk/viewer_test.clj b/test/nextjournal/clerk/viewer_test.clj index 20be9d3a2..5787408ff 100644 --- a/test/nextjournal/clerk/viewer_test.clj +++ b/test/nextjournal/clerk/viewer_test.clj @@ -212,19 +212,41 @@ (is (= [{:render-fn 'foo}] (v/get-viewers 'nextjournal.clerk.viewer-test.random-ns-name))))) (def my-test-var [:h1 "hi"]) +(def my-test-var2 1) + +(def apply+get-value #(-> % v/apply-viewer-unwrapping-var-from-def :nextjournal/value :nextjournal/value)) (deftest apply-viewer-unwrapping-var-from-def - (let [apply+get-value #(-> % v/apply-viewer-unwrapping-var-from-def :nextjournal/value :nextjournal/value)] - (testing "unwraps var when viewer doens't opt out" - (is (= my-test-var - (apply+get-value {:nextjournal/value [:h1 "hi"] :nextjournal/viewer v/html}) - (apply+get-value {:nextjournal/value {:nextjournal.clerk/var-from-def #'my-test-var} :nextjournal/viewer v/html}) - (apply+get-value {:nextjournal/value {:nextjournal.clerk/var-from-def #'my-test-var} :nextjournal/viewer v/html-viewer})))) - - (testing "leaves var wrapped when viewer opts out" - (is (= {:nextjournal.clerk/var-from-def #'my-test-var} - (apply+get-value {:nextjournal/value {:nextjournal.clerk/var-from-def #'my-test-var} - :nextjournal/viewer (assoc v/html-viewer :var-from-def? true)})))))) + (testing "unwraps var when viewer doens't opt out" + (is (= my-test-var + (apply+get-value {:nextjournal/value [:h1 "hi"] :nextjournal/viewer v/html}) + (apply+get-value {:nextjournal/value {:nextjournal.clerk/var-from-def #'my-test-var} :nextjournal/viewer v/html}) + (apply+get-value {:nextjournal/value {:nextjournal.clerk/var-from-def #'my-test-var} :nextjournal/viewer v/html-viewer})))) + + (testing "leaves var wrapped when viewer opts out" + (is (= {:nextjournal.clerk/var-from-def #'my-test-var} + (apply+get-value {:nextjournal/value {:nextjournal.clerk/var-from-def #'my-test-var} + :nextjournal/viewer (assoc v/html-viewer :var-from-def? true)})))) + + (testing "function viewer receives the deref'd value, not the var-from-def wrapper (fixes #770)" + (is (= [my-test-var2] + (apply+get-value {:nextjournal/value {:nextjournal.clerk/var-from-def #'my-test-var2 + :nextjournal.clerk/var-snapshot my-test-var2} + :nextjournal/viewer v/row})))) + + (testing "function viewer on a seq-valued def spreads across row cells" + (is (= [1 2 3] + (apply+get-value {:nextjournal/value {:nextjournal.clerk/var-from-def #'my-test-var + :nextjournal.clerk/var-snapshot [1 2 3]} + :nextjournal/viewer v/row})))) + + (testing "function viewer whose returned viewer opts out keeps the raw var-from-def (e.g. cx/slider via render-eval-viewer)" + (let [viewer-fn (fn [v] (v/with-viewer (assoc v/html-viewer :var-from-def? true) v))] + (is (= {:nextjournal.clerk/var-from-def #'my-test-var + :nextjournal.clerk/var-snapshot [:h1 "hi"]} + (apply+get-value {:nextjournal/value {:nextjournal.clerk/var-from-def #'my-test-var + :nextjournal.clerk/var-snapshot [:h1 "hi"]} + :nextjournal/viewer viewer-fn})))))) (deftest resolve-aliases