Skip to content

Commit

Permalink
Allow setting code cell & result visibility
Browse files Browse the repository at this point in the history
You can control visibility in Clerk by setting the `:nextjournal.clerk/visibility` which takes a keyword or a set of keywords. Valid values are:

 * `:show` (the default)
 * `:fold` displays the cells collapsed and allows to uncollapse the,
 * `:hide` completely hides the code cells

A declartion on the `ns` form let's all code cells in the notebook inherit the value. On the `ns` form you can also use `:fold-ns` or `:hide-ns` if you'd like an option to only apply to the namespace form. A `ns` form is treated special in that when hidden Clerk will also hide its `nil` result.

The `clerk/hide-result` viewer allows you to hide the result of a code cell.
  • Loading branch information
mk committed Nov 26, 2021
1 parent 1011d5c commit a6ee5c7
Show file tree
Hide file tree
Showing 7 changed files with 178 additions and 57 deletions.
2 changes: 1 addition & 1 deletion deps.edn
Expand Up @@ -25,7 +25,7 @@
borkdude/sci {:mvn/version "0.2.6"}
reagent/reagent {:mvn/version "1.1.0"}
io.github.nextjournal/viewers {:git/url "git@github.com:nextjournal/viewers.git"
:git/sha "a32acfedce17bfa65ef8ac2a077d87823d72c8f3"}
:git/sha "0ebf86bbb457f819a6f9e8b5a33f34fc62c5b2e5"}
metosin/reitit-frontend {:mvn/version "0.5.15"}}}

:dev {:extra-deps {arrowic/arrowic {:mvn/version "0.1.1"}
Expand Down
28 changes: 28 additions & 0 deletions notebooks/visibility.clj
@@ -0,0 +1,28 @@
;; # Controlling Visibility 🙈
;; You can control visibility in Clerk by setting the `:nextjournal.clerk/visibility` which takes a keyword or a set of keywords. Valid values are:
;; * `:show` (the default)
;; * `:hide` to hide the cells w
;; * `:fold` which shows the cells collapsed and lets users uncollapse them

;; A declartion on the `ns` form let's all code cells in the notebook inherit the value. On the `ns` form you can also use `:fold-ns` or `:hide-ns` if you'd like an option to only apply to the namespace form.
^{:nextjournal.clerk/visibility #{:fold}}
(ns visibility
(:require [clojure.string :as str]
[nextjournal.clerk :as clerk]))

;; So a cell will only show the result now while you can uncoallpse the code cell.
(+ 39 3)

;; If you want, you can override it. So the following cell is shown:
^{::clerk/visibility :show} (range 25)

;; While this one is completely hidden, without the ability to uncollapse it.
^{::clerk/visibility :hide} (shuffle (range 25))

;; In the rare case you'd like to hide the result of a cell, use `clerk/hide-result`.
^{::clerk/visibility :show}
(clerk/hide-result (range 500))

;; In a follow-up, we'll remove the `::clerk/visibility` metadata from the code cells to not distract from the essence.

;; Fin.
25 changes: 17 additions & 8 deletions src/nextjournal/clerk.clj
Expand Up @@ -27,7 +27,8 @@
(str (config/cache-dir) fs/file-separator hash))

(defn wrap-with-blob-id [result hash]
{:result result :blob-id (cond-> hash (not (string? hash)) multihash/base58)})
{:nextjournal/value result
:nextjournal/blob-id (cond-> hash (not (string? hash)) multihash/base58)})

#_(wrap-with-blob-id :test "foo")

Expand Down Expand Up @@ -61,15 +62,16 @@

#_(worth-caching? 0.1)

(defn read+eval-cached [results-last-run vars->hash code-string]
(defn read+eval-cached [results-last-run vars->hash doc-visibility code-string]
(let [form (hashing/read-string code-string)
{:as analyzed :keys [ns-effect? var]} (hashing/analyze form)
hash (hashing/hash vars->hash analyzed)
digest-file (->cache-file (str "@" hash))
no-cache? (or ns-effect? (hashing/no-cache? form))
cas-hash (when (fs/exists? digest-file)
(slurp digest-file))
cached? (boolean (and cas-hash (-> cas-hash ->cache-file fs/exists?)))]
cached? (boolean (and cas-hash (-> cas-hash ->cache-file fs/exists?)))
visibility (if-let [fv (hashing/->visibility form)] fv doc-visibility)]
#_(prn :cached? (cond no-cache? :no-cache
cached? true
(fs/exists? digest-file) :no-cas-file
Expand Down Expand Up @@ -97,7 +99,8 @@
no-cache?))
var-value (cond-> result (var? result) deref)]
(if (fn? var-value)
{:result var-value}
{:nextjournal/value var-value
::visibility visibility}
(do (when-not (or no-cache?
(instance? clojure.lang.IDeref var-value)
(instance? clojure.lang.MultiFn var-value)
Expand All @@ -108,9 +111,11 @@
(catch Exception _e
#_(prn :freeze-error e)
nil)))
(wrap-with-blob-id var-value (if no-cache? (view/->hash-str var-value) hash))))))))
(-> var-value
(wrap-with-blob-id (if no-cache? (view/->hash-str var-value) hash))
(assoc ::visibility visibility))))))))

#_(read+eval-cached {} {} "(subs (slurp \"/usr/share/dict/words\") 0 1000)")
#_(read+eval-cached {} {} #{:show} "(subs (slurp \"/usr/share/dict/words\") 0 1000)")

(defn clear-cache!
([]
Expand All @@ -128,11 +133,11 @@

#_(blob->result @nextjournal.clerk.webserver/!doc)

(defn +eval-results [results-last-run vars->hash doc]
(defn +eval-results [results-last-run vars->hash {:keys [doc visibility]}]
(let [doc (into [] (map (fn [{:as cell :keys [type text]}]
(cond-> cell
(= :code type)
(assoc :result (read+eval-cached results-last-run vars->hash text))))) doc)]
(assoc :result (read+eval-cached results-last-run vars->hash visibility text))))) doc)]
(with-meta doc (-> doc blob->result (assoc :ns *ns*)))))

#_(let [doc (+eval-results {} {} [{:type :markdown :text "# Hi"} {:type :code :text "[1]"} {:type :code :text "(+ 39 3)"}])
Expand All @@ -143,13 +148,15 @@
(hashing/parse-file {:markdown? true} file))

#_(parse-file "notebooks/elements.clj")
#_(parse-file "notebooks/visibility.clj")

(defn eval-file
([file] (eval-file {} file))
([results-last-run file]
(+eval-results results-last-run (hashing/hash file) (parse-file file))))

#_(eval-file "notebooks/rule_30.clj")
#_(eval-file "notebooks/visibility.clj")

(defonce !show-filter-fn (atom nil))
(defonce !last-file (atom nil))
Expand Down Expand Up @@ -195,6 +202,7 @@
(def code v/code)
(def table #'v/table)
(def use-headers #'v/use-headers)
(def hide-result #'v/hide-result)

(defmacro with-viewer
[viewer x]
Expand Down Expand Up @@ -270,6 +278,7 @@
"paren_soup"
#_"readme" ;; TODO: add back when we have Clojure cells in md
"rule_30"
"visibility"
"viewer_api"
"viewers/html"
"viewers/image"
Expand Down
82 changes: 59 additions & 23 deletions src/nextjournal/clerk/hashing.clj
Expand Up @@ -71,19 +71,71 @@
(defn remove-leading-semicolons [s]
(str/replace s #"^[;]+" ""))

(defn ns? [form]
(and (list? form) (= 'ns (first form))))

(defn ->visibility [form]
(when-let [visibility (-> form meta :nextjournal.clerk/visibility)]
(let [visibility-set (cond-> visibility (not (set? visibility)) hash-set)]
(when-not (every? #{:hide-ns :hide :show :fold} visibility-set)
(throw (ex-info "Invalid `:nextjournal.clerk/visibility`, valid values are `#{:hide-ns :hide :show :fold}`." {:visibility visibility :form form})))
(when (and (contains? visibility-set :hide-ns) (not (ns? form)))
(throw (ex-info "Cannot set `:nextjournal.clerk/visibility` to `:hide-ns` on non ns form." {:visibility visibility :form form})))
visibility-set)))

#_(->visibility '(foo :bar))
#_(->visibility (quote ^{:nextjournal.clerk/visibility :fold} (ns foo)))
#_(->visibility (quote ^{:nextjournal.clerk/visibility #{:hide-ns :fold}} (ns foo)))
#_(->visibility (quote ^{:nextjournal.clerk/visibility :hidden} (ns foo)))
#_(->visibility (quote ^{:nextjournal.clerk/visibility "bam"} (ns foo)))
#_(->visibility (quote ^{:nextjournal.clerk/visibility #{:hide-ns}} (do :foo)))

(defn ->doc-visibility [first-form]
(or (when (ns? first-form)
(-> first-form
->visibility
(disj :hide-ns)
not-empty))
#{:show}))

#_(->doc-visibility '^{:nextjournal.clerk/visibility :fold} (ns foo))
#_(->doc-visibility '^{:nextjournal.clerk/visibility :hide-ns} (ns foo))

(defn auto-resolves [ns]
(as-> (ns-aliases ns) $
(assoc $ :current (ns-name *ns*))
(zipmap (keys $)
(map ns-name (vals $)))))

#_(auto-resolves (find-ns 'rule-30))


(defn read-string [s]
(edamame/parse-string s {:all true
:auto-resolve (auto-resolves (or *ns* (find-ns 'user)))
:readers *data-readers*
:read-cond :allow
:features #{:clj}}))

#_(read-string "(ns rule-30 (:require [nextjournal.clerk.viewer :as v]))")

(defn parse-file
([file]
(parse-file {} file))
([{:as _opts :keys [markdown?]} file]
(loop [{:as state :keys [doc nodes]} {:nodes (:children (p/parse-file-all file))
:doc []}]
(loop [{:as state :keys [doc nodes visibility]} {:nodes (:children (p/parse-file-all file))
:doc []}]
(if-let [node (first nodes)]
(recur (cond
(#{:deref :map :meta :list :quote :reader-macro :set :token :var :vector} (n/tag node))
(-> state
(update :nodes rest)
(update :doc (fnil conj []) {:type :code :text (n/string node)}))
(cond-> (-> state
(update :nodes rest)
(update :doc (fnil conj []) (cond-> {:type :code :text (n/string node)}
(and (not visibility) (-> node n/string read-string ns?))
(assoc :ns? true))))

(and markdown? (not visibility))
(assoc :visibility (-> node n/string read-string ->doc-visibility)))

(and markdown? (n/comment? node))
(-> state
Expand All @@ -92,29 +144,13 @@
(take-while n/comment? nodes)))}))
:else
(update state :nodes rest)))
doc))))
(select-keys state [:doc :visibility])))))

#_(parse-file {:markdown? true} "notebooks/visibility.clj")
#_(parse-file "notebooks/elements.clj")
#_(parse-file {:markdown? true} "notebooks/rule_30.clj")
#_(parse-file "notebooks/src/demo/lib.cljc")

(defn auto-resolves [ns]
(as-> (ns-aliases ns) $
(assoc $ :current (ns-name *ns*))
(zipmap (keys $)
(map ns-name (vals $)))))

#_(auto-resolves (find-ns 'rule-30))


(defn read-string [s]
(edamame/parse-string s {:all true
:auto-resolve (auto-resolves (or *ns* (find-ns 'user)))
:readers *data-readers*
:read-cond :allow
:features #{:clj}}))
#_(read-string "(ns rule-30 (:require [nextjournal.clerk.viewer :as v]))")

(defn analyze-file
([file]
(analyze-file {} {:graph (dep/graph)} file))
Expand Down
23 changes: 22 additions & 1 deletion src/nextjournal/clerk/sci_viewer.cljs
Expand Up @@ -81,7 +81,7 @@
(str "viewer-" (name viewer)))
(when-let [inner-viewer-name (some-> x viewer/value viewer/viewer :name name)]
(str "viewer-" inner-viewer-name))
(case (or (viewer/width x) (case viewer :code :wide :prose))
(case (or (viewer/width x) (case viewer (:code :code-folded) :wide :prose))
:wide "w-full max-w-wide"
:full "w-full"
"w-full max-w-prose px-8")]}
Expand Down Expand Up @@ -839,6 +839,26 @@ black")}]))}
(def plotly-viewer (comp normalize-viewer plotly/viewer))
(def vega-lite-viewer (comp normalize-viewer vega-lite/viewer))

(def expand-icon
[:svg {:xmlns "http://www.w3.org/2000/svg" :viewBox "0 0 20 20" :fill "currentColor" :width 12 :height 12}
[: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"}]])

(defn foldable-code-viewer [code-string]
(r/with-let [!hidden? (r/atom true)]
(html (if @!hidden?
[:div.w-full.max-w-wide.sans-serif {:style {:background "var(--gray-panel-color)"}}
[:button.mx-auto.flex.items-center.rounded-sm.cursor-pointer.bg-indigo-200.hover:bg-indigo-300.leading-none
{:style {:font-size "11px" :padding "1px 3px"}
:on-click #(swap! !hidden? not)}
expand-icon " Show code…"]]
[:div.viewer-code.relative {:style {:margin-top 0}}
[inspect (code-viewer code-string)]
[:button.sans-serif.mx-auto.flex.items-center.rounded-t-sm.cursor-pointer.bg-indigo-200.hover:bg-indigo-300.leading-none.absolute.bottom-0
{:style {:font-size "11px" :padding "1px 3px" :left "50%" :transform "translateX(-50%)"}
:on-click #(swap! !hidden? not)}
[:span {:style {:transform "rotate(180deg)"}} expand-icon] " Hide code…"]]))))


(defn url-for [{:keys [blob-id]}]
(str "/_blob/" blob-id))

Expand All @@ -865,6 +885,7 @@ black")}]))}
'mathjax-viewer mathjax-viewer
'markdown-viewer markdown/viewer
'code-viewer code-viewer
'foldable-code-viewer foldable-code-viewer
'plotly-viewer plotly-viewer
'vega-lite-viewer vega-lite-viewer
'reagent-viewer reagent-viewer
Expand Down
54 changes: 39 additions & 15 deletions src/nextjournal/clerk/view.clj
Expand Up @@ -57,12 +57,12 @@

#_(->hash (range 104))

(defn ->result [ns {:keys [result blob-id]} lazy-load?]
(let [described-result (v/describe result {:viewers (v/get-viewers ns (v/viewers result))})]
(defn ->result [ns {:nextjournal/keys [value blob-id visibility]} lazy-load?]
(let [described-result (v/describe value {:viewers (v/get-viewers ns (v/viewers value))})]
(merge {:nextjournal/viewer :clerk/result
:nextjournal/value (cond-> (try {:nextjournal/edn (->edn described-result)}
(catch Throwable _e
{:nextjournal/string (pr-str result)}))
{:nextjournal/string (pr-str value)}))
lazy-load?
(assoc :nextjournal/fetch-opts {:blob-id blob-id}
:nextjournal/hash (->hash-str [blob-id described-result])))}
Expand All @@ -71,29 +71,53 @@

#_(nextjournal.clerk/show! "notebooks/hello.clj")

(defn ->display [{:as code-cell :keys [result ns?]}]
(let [{:nextjournal.clerk/keys [visibility]} result
result? (and (contains? code-cell :result)
(not= :hide-result (v/viewer (v/value result)))
(not (contains? visibility :hide-ns))
(not (and ns? (contains? visibility :hide))))
fold? (and (not (contains? visibility :hide-ns))
(or (contains? visibility :fold)
(contains? visibility :fold-ns)))
code? (or fold? (contains? visibility :show))]
{:result? result? :fold? fold? :code? code?}))

#_(->display {:result {:nextjournal.clerk/visibility #{:fold :hide-ns}}})
#_(->display {:result {:nextjournal.clerk/visibility #{:fold-ns}}})
#_(->display {:result {:nextjournal.clerk/visibility #{:hide}} :ns? false})
#_(->display {:result {:nextjournal.clerk/visibility #{:fold}} :ns? true})
#_(->display {:result {:nextjournal.clerk/visibility #{:fold}} :ns? false})
#_(->display {:result {:nextjournal.clerk/visibility #{:hide} :nextjournal/value {:nextjournal/viewer :hide-result}} :ns? false})
#_(->display {:result {:nextjournal.clerk/visibility #{:hide}} :ns? true})

(defn doc->viewer
([doc] (doc->viewer {} doc))
([{:keys [inline-results?] :or {inline-results? false}} doc]
(let [{:keys [ns]} (meta doc)]
(cond-> (into []
(mapcat (fn [{:as x :keys [type text result]}]
(mapcat (fn [{:as cell :keys [type text result ns?]}]
(case type
:markdown [(v/md text)]
:code (cond-> [(v/code text)]
(contains? x :result)
(conj (cond
(v/registration? (:result result))
(:result result)

:else
(->result ns result (and (not inline-results?)
(contains? result :blob-id)))))))))
:code (let [{:keys [code? fold? result?]} (->display cell)]
(cond-> []
code?
(conj (cond-> (v/code text) fold? (assoc :nextjournal/viewer :code-folded)))
result?
(conj (cond
(v/registration? (v/value result))
(v/value result)

:else
(->result ns result (and (not inline-results?)
(contains? result :nextjournal/blob-id))))))))))
doc)
true v/notebook
ns (assoc :scope (v/datafy-scope ns))))))

#_(meta (doc->viewer (nextjournal.clerk/eval-file "notebooks/hello.clj")))
#_(nextjournal.clerk/show! "notebooks/test.clj")
#_(nextjournal.clerk/show! "notebooks/visibility.clj")

(defonce ^{:doc "Load dynamic js from shadow or static bundle from cdn."}
live-js?
Expand All @@ -102,8 +126,8 @@

(def resource->static-url
{"/css/app.css" "https://storage.googleapis.com/nextjournal-cas-eu/data/8VxQBDwk3cvr1bt8YVL5m6bJGrFEmzrSbCrH1roypLjJr4AbbteCKh9Y6gQVYexdY85QA2HG5nQFLWpRp69zFSPDJ9"
"/css/viewer.css" "https://storage.googleapis.com/nextjournal-cas-eu/data/8VvwJaC11sRe6kkEea3iBnhgiVVqAwGdacXea7sAQ1EVVRPHVupsxACFP4xcpQtXJJ5CdBPBDxLGRNYcdyQzNDPCTE"
"/js/viewer.js" "https://storage.googleapis.com/nextjournal-cas-eu/data/8VxfW23mjJmtGoscWJBchRxZgMCZ5dJZgYBpbkrpnWSV4vLbYBjbokxMrR8Adpd5dHSPhyp9HzWK1o9GDxGEQzXvjJ"})
"/css/viewer.css" "https://storage.googleapis.com/nextjournal-cas-eu/data/8VvykE47cdahchdt8fxwHyYwJ7YSmEFcMSyqf4UNs61izpuF1xXpKA4HeZQctDkkU11B5iLVSBjpCQrk5f5mWXS9xv"
"/js/viewer.js" "https://storage.googleapis.com/nextjournal-cas-eu/data/8VwyMcfayNcoRj7RZhzLTBGyS2hAtk8Teieo3s5GyiNooj4R5JhZdgZ3GuNnSAye7STHZKzhUTfrSsVbEZx79JEFc2"})

(defn ->html [{:keys [conn-ws? live-js?] :or {conn-ws? true live-js? live-js?}} doc]
(hiccup/html5
Expand Down

0 comments on commit a6ee5c7

Please sign in to comment.