From 369cab04144497ae6c4d2c0fc3217ffbf90ab6cb Mon Sep 17 00:00:00 2001 From: Vedang Manerikar Date: Sun, 20 Aug 2023 23:54:58 +0530 Subject: [PATCH 01/26] Fix minor typo on the index page (#546) `show` -> `show!` --- src/nextjournal/clerk/home.clj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/nextjournal/clerk/home.clj b/src/nextjournal/clerk/home.clj index 8f4409dda..891a207eb 100644 --- a/src/nextjournal/clerk/home.clj +++ b/src/nextjournal/clerk/home.clj @@ -176,7 +176,7 @@ [:div.rounded-lg.border-2.border-amber-100.bg-amber-50.dark:border-slate-600.dark:bg-slate-800.dark:text-slate-100.px-8.py-4.mx-auto.text-center.font-sans.mt-6.md:mt-4 [:div [:span.font-medium "💡 Tip:"] " Show the " [:a {:href "/'nextjournal.clerk.tap"} "🚰 Tap Inspector"] " to inspect values using " (code-highlight {:class "text-sm" }"tap>") "."] [:div.mt-2.text-xs - (code-highlight {:class "text-sm"} "(nextjournal.clerk/show 'nextjournal.clerk.tap)")]] + (code-highlight {:class "text-sm"} "(nextjournal.clerk/show! 'nextjournal.clerk.tap)")]] #_[:div.mt-6 (clerk/with-viewer filter-input-viewer `!filter)]]) From 02dd0fc5f5e62471c2eaaca6a21250a621a56a89 Mon Sep 17 00:00:00 2001 From: Matt Kelly Date: Thu, 24 Aug 2023 05:41:56 -0400 Subject: [PATCH 02/26] Add missing action verb to Book of Clerk (#533) --- book.clj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/book.clj b/book.clj index 1ee18209a..6af1303ac 100644 --- a/book.clj +++ b/book.clj @@ -940,7 +940,7 @@ v/table-viewer ;; Also notably, there is a `:compile-css` option which compiles a css ;; file containing only the used CSS classes from the generated ;; markup. (Otherwise, Clerk is using Tailwind's Play CDN script which -;; can the page flicker, initially.) +;; can make the page flicker, initially.) ;; If set, the `:ssr` option will use React's server-side-rendering to ;; include the generated markup in the build HTML. From 4e61e0e9bb5c90501e8b4d5d735a482cd2a5bb09 Mon Sep 17 00:00:00 2001 From: Andrea Amantini Date: Thu, 24 Aug 2023 16:13:47 +0200 Subject: [PATCH 03/26] Fix preserving expanded-at on clerk/show (#548) We are resetting a result's expansion state when its `:nextjourna/hash` is changing. The hash is computed starting from the presented value using nippy/freeze. It turns out that adding pagination continuation functions (#421) as metadata on the presented result broke nippy serialization and we'd get a fresh hash (a gensym) on each call to show as a fallback. We can fix this by excluding metadata when nippy freezing. --- src/nextjournal/clerk/analyzer.clj | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/nextjournal/clerk/analyzer.clj b/src/nextjournal/clerk/analyzer.clj index f1289a217..687a2f1f9 100644 --- a/src/nextjournal/clerk/analyzer.clj +++ b/src/nextjournal/clerk/analyzer.clj @@ -609,9 +609,10 @@ (let [digest-fn (case hash-type :sha1 sha1-base58 :sha512 sha2-base58)] - (-> value - nippy/fast-freeze - digest-fn)))) + (binding [nippy/*incl-metadata?* false] + (-> value + nippy/fast-freeze + digest-fn))))) #_(valuehash (range 100)) #_(valuehash :sha1 (range 100)) From 0846e106ce8b22202232a529f8c76bb85a95763b Mon Sep 17 00:00:00 2001 From: Martin Kavalar Date: Fri, 25 Aug 2023 16:28:21 +0200 Subject: [PATCH 04/26] Preserve in-memory cache when eval error occurs (#550) Fixes #549 --- src/nextjournal/clerk.clj | 2 +- test/nextjournal/clerk/eval_test.clj | 15 ++++++++++++++- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/src/nextjournal/clerk.clj b/src/nextjournal/clerk.clj index f56073408..7c506d9c4 100644 --- a/src/nextjournal/clerk.clj +++ b/src/nextjournal/clerk.clj @@ -63,7 +63,7 @@ {:keys [blob->result]} @webserver/!doc {:keys [result time-ms]} (try (eval/time-ms (eval/+eval-results blob->result (assoc doc :set-status-fn webserver/set-status!))) (catch Exception e - (throw (ex-info (str "`nextjournal.clerk/show!` encountered an eval error with: `" (pr-str file-or-ns) "`") {::doc doc} e))))] + (throw (ex-info (str "`nextjournal.clerk/show!` encountered an eval error with: `" (pr-str file-or-ns) "`") {::doc (assoc doc :blob->result blob->result)} e))))] (println (str "Clerk evaluated '" file "' in " time-ms "ms.")) (webserver/update-doc! result)) (catch Exception e diff --git a/test/nextjournal/clerk/eval_test.clj b/test/nextjournal/clerk/eval_test.clj index 913592a7a..a27c7233e 100644 --- a/test/nextjournal/clerk/eval_test.clj +++ b/test/nextjournal/clerk/eval_test.clj @@ -6,7 +6,8 @@ [nextjournal.clerk.eval :as eval] [nextjournal.clerk.parser :as parser] [nextjournal.clerk.view :as view] - [nextjournal.clerk.viewer :as viewer])) + [nextjournal.clerk.viewer :as viewer] + [nextjournal.clerk.webserver :as webserver])) (deftest eval-string (testing "hello 42" @@ -227,3 +228,15 @@ (testing "class is not cachable" (is (not (#'eval/cachable-value? java.lang.String))) (is (not (#'eval/cachable-value? {:foo java.lang.String}))))) + +(deftest show!-test + (testing "in-memory cache is preserved when exception is thrown (#549)" + (let [code "{:f inc :n (rand-int 100000)}" + get-result #(:blob->result @webserver/!doc)] + (clerk/show! (java.io.StringReader. code)) + (let [result-first-run (get-result)] + (try (clerk/show! (java.io.StringReader. (str code " (throw (ex-info \"boom\" {}))"))) + (catch Exception _ nil)) + (clerk/show! (java.io.StringReader. code)) + (is (= result-first-run (get-result))))))) + From f6d9d890e45fd7bffb69e6203743188d933ad74f Mon Sep 17 00:00:00 2001 From: Martin Kavalar Date: Mon, 28 Aug 2023 10:00:00 +0200 Subject: [PATCH 05/26] Add regression test for stable hashes in presentations --- test/nextjournal/clerk/viewer_test.clj | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/test/nextjournal/clerk/viewer_test.clj b/test/nextjournal/clerk/viewer_test.clj index b527a8c96..ca9cea512 100644 --- a/test/nextjournal/clerk/viewer_test.clj +++ b/test/nextjournal/clerk/viewer_test.clj @@ -279,6 +279,11 @@ :out-path builder/default-out-path} test-doc) #"_data/.+\.png"))))) + (testing "presentations are pure, result hashes are stable" + (let [test-doc (eval/eval-string "(range 100)")] + (is (= (view/doc->viewer {} test-doc) + (view/doc->viewer {} test-doc))))) + (testing "Setting custom options on results via metadata" (is (= :full (-> (eval/eval-string "^{:nextjournal.clerk/width :full} (nextjournal.clerk/html [:div])") From a7a191ce08d12656cf74d8da1a997cc9156a58d7 Mon Sep 17 00:00:00 2001 From: Philippa Markovics Date: Wed, 30 Aug 2023 10:35:56 +0200 Subject: [PATCH 06/26] API Doc Browser (#552) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Improvements to Clerk’s api docs notebook: * move it to the classpath so it's accessible using `(nextjournal.clerk/show! 'nextjournal.clerk.doc)` * New user interface with improved navigation (tree view, show descendant namespaces) * Listing of public vars for the active namespace, clicking them scrolls to their respective doc * Search is now uncoupled from active namespace * Improved dark mode support * Improved regex search with error handling --------- Co-authored-by: Andrea Amantini Co-authored-by: Martin Kavalar --- notebooks/doc.clj | 83 -------------- src/nextjournal/clerk/doc.clj | 197 ++++++++++++++++++++++++++++++++++ 2 files changed, 197 insertions(+), 83 deletions(-) delete mode 100644 notebooks/doc.clj create mode 100644 src/nextjournal/clerk/doc.clj diff --git a/notebooks/doc.clj b/notebooks/doc.clj deleted file mode 100644 index e1d30e52c..000000000 --- a/notebooks/doc.clj +++ /dev/null @@ -1,83 +0,0 @@ -;; # 📓 Doc Browser -(ns doc - {:nextjournal.clerk/visibility {:code :hide :result :hide}} - (:require [clojure.string :as str] - [nextjournal.clerk :as clerk] - [nextjournal.clerk.viewer :as viewer])) - -(def render-input - '(fn [!query] - (prn :query !query) - [:div.my-1.relative - [:input {:type :text - :auto-correct "off" - :spell-check "false" - :placeholder "Filter namespaces…" - :value @!query - :class "px-3 py-2 relative bg-white bg-white rounded text-base font-sans border border-slate-200 shadow-inner outline-none focus:outline-none focus:ring w-full" - :on-input #(reset! !query (.. % -target -value))}] - [:button.absolute.right-2.text-xl.cursor-pointer - {:class "top-1/2 -translate-y-1/2" - :on-click #(reset! !query (clojure.string/join "." (drop-last (clojure.string/split @!query #"\."))))} "⏮"]])) - -^{::clerk/sync true} -(defonce !ns-query (atom "nextjournal.clerk")) -#_(reset! !ns-query "nextjournal.clerk") - - -!ns-query - -^{::clerk/visibility {:result :show} - ::clerk/viewer {:render-fn render-input - :transform-fn clerk/mark-presented}} -(viewer/->viewer-eval `!ns-query) - -^{::clerk/viewers (clerk/add-viewers - [{:pred seq? - :render-fn '#(into [:div.border.rounded-md.bg-white.shadow.flex.flex-col.mb-1] - (nextjournal.clerk.render/inspect-children %2) %1) :page-size 20} - {:pred string? - :render-fn '(fn [ns] [:button.text-xs.font-medium.font-sans.cursor-pointer.px-3.py-2.hover:bg-blue-100.text-slate-700.text-left - {:on-click #(reset! doc/!ns-query ns)} ns])}])} - -^{::clerk/visibility {:result :show}} -(def ns-matches - (filter (partial re-find (re-pattern @!ns-query)) (sort (map str (all-ns))))) - -(defn var->doc-viewer - "Takes a clojure `var` and returns a Clerk viewer to display its documentation." - [var] - (let [{:keys [doc name arglists]} (meta var)] - (clerk/html - [:div.border-t.border-slate-200.pt-6.mt-6 - [:h2 {:style {:margin 0}} name] - (when (seq arglists) - [:div.pt-4 - (clerk/code (str/join "\n" (mapv (comp pr-str #(concat [name] %)) arglists)))]) - (when doc - [:div.mt-4.viewer-markdown.prose - (clerk/md doc)])]))) - -#_(var->doc-viewer #'var->doc-viewer) - -(defn namespace->doc-viewer [ns] - (clerk/html - [:div.text-sm.mt-6 - [:h1 {:style {:margin 0}} (ns-name ns)] - (when-let [doc (-> ns meta :doc)] - [:div.mt-4.leading-normal.viewer-markdown.prose - (clerk/md doc)]) - (into [:<>] - (map (comp :nextjournal/value var->doc-viewer val)) - (into (sorted-map) (-> ns ns-publics)))])) - -(def ns-doc-viewer {:pred #(instance? clojure.lang.Namespace %) - :transform-fn (clerk/update-val namespace->doc-viewer)}) - -^{::clerk/visibility {:result :show}} -(when-let [ns-name (first ns-matches)] - (clerk/with-viewer ns-doc-viewer (find-ns (symbol ns-name)))) - - - -#_(deref nextjournal.clerk.webserver/!doc) diff --git a/src/nextjournal/clerk/doc.clj b/src/nextjournal/clerk/doc.clj new file mode 100644 index 000000000..f18f61ccf --- /dev/null +++ b/src/nextjournal/clerk/doc.clj @@ -0,0 +1,197 @@ +(ns nextjournal.clerk.doc + "Clerk's documentation browser." + {:nextjournal.clerk/visibility {:code :hide :result :hide}} + (:require [clojure.string :as str] + [nextjournal.clerk :as clerk] + [nextjournal.clerk.viewer :as viewer])) + +(def render-input + '(fn [!query] + (nextjournal.clerk.render.hooks/use-effect + (fn [] + (let [keydown-handler (fn [event] + (when (and (.-metaKey event) (= "k" (.-key event))) + (.focus (js/document.getElementById "search-nss"))))] + (js/document.addEventListener "keydown" keydown-handler) + #(js/document.removeEventListener "keydown" keydown-handler)))) + [:div.my-1.relative + [:input#search-nss.px-2.py-1.relative.bg-white.dark:bg-slate-900.bg-white.rounded.border.dark:border-slate-700.shadow-inner.outline-none.focus:outline-none.focus:ring-2.focus.ring-indigo-500.hover:border-slate-400.focus:hover:border-slate-200.w-full.text-xs.font-sans.dark:hover:border-slate-600.dark:focus:border-sslate-700 + {:type :text + :auto-correct "off" + :spell-check "false" + :placeholder "Search namespaces via Regex…" + :value @!query + :on-input #(reset! !query (.. % -target -value))}] + [:div.text-xs.absolute.right-2.text-slate-400.dark:text-slate-400.font-inter.tracking-widest.pointer-events-none + {:class "top-1/2 -translate-y-1/2"} "⌘K"]])) + +^{::clerk/sync true} +(defonce !ns-query (atom "")) +#_(reset! !ns-query "") + +^{::clerk/sync true} +(defonce !active-ns (atom "")) +#_(reset! !active-ns "nextjournal.clerk") + +(defn str-match-nss [s] + (filter #(str/includes? % s) (sort (map str (all-ns))))) + +(defn match-nss [re] + (filter (partial re-find (re-pattern re)) (sort (map str (all-ns))))) + +(defn var->doc-viewer + "Takes a clojure `var` and returns a Clerk viewer to display its documentation." + [var] + (let [{:keys [doc arglists] var-name :name} (meta var)] + (clerk/html + [:div.border-t.dark:border-slate-800.pt-6.mt-6 + {:id (name (symbol var))} + [:div.font-sans.font-bold.text-base {:style {:margin 0}} var-name] + (when (seq arglists) + [:div.pt-4 + (clerk/code (str/join "\n" (mapv (comp pr-str #(concat [var-name] %)) arglists)))]) + (when doc + [:div.mt-4.viewer-markdown.prose + (clerk/md doc)])]))) + +(defn render-ns [{:keys [name nss vars]}] + [:div.mt-1 + [:div.hover:underline.cursor-pointer.hover:text-indigo-600.dark:hover:text-white.whitespace-nowrap + {:class (when (= @!active-ns name) "font-bold") + :on-click (viewer/->viewer-eval `(fn [] + (reset! !active-ns ~name) + (reset! !ns-query "")))} name] + (when (and vars (= @!active-ns name)) + [:<> + (into [:div.text-xs.font-sans.mt-1.ml-3.mb-3] + (map (fn [var] + [:div.mt-1.hover:text-indigo-600.dark:hover:text-white.cursor-pointer.hover:underline + {:on-click (viewer/->viewer-eval `(fn [] + (when-some [el (js/document.getElementById ~(str var))] + (.scrollIntoView el))))} + var])) + vars) + (when nss + [:div.border-b.dark:border-slate-800.mb-3])]) + (when nss + (into [:div.ml-3] (map render-ns) nss))]) + +(defn ns-node-with-branches [nss-map ns-name] + (let [sub-nss (get nss-map ns-name) + vars (some-> ns-name symbol find-ns ns-publics not-empty keys vec sort)] + (cond-> {:name ns-name} + sub-nss (assoc :nss (mapv (partial ns-node-with-branches nss-map) sub-nss)) + vars (assoc :vars vars)))) + +(defn ns-tree + ([ns-matches] + (ns-tree (update-keys (group-by #(butlast (clojure.string/split % #"\.")) ns-matches) + (partial clojure.string/join ".")) + ns-matches + [])) + ([nss-map ns-matches acc] + (if-some [ns-name (first ns-matches)] + (recur nss-map + (remove (some-fn #{ns-name} #(str/starts-with? % (str ns-name "."))) + ns-matches) + (conj acc (ns-node-with-branches nss-map ns-name))) + acc))) + +#_(ns-tree ns-matches) +#_(ns-tree ()) + +(defn parent-ns [ns-str] + (when (str/includes? ns-str ".") + (str/join "." (butlast (str/split ns-str #"\."))))) + +(defn prepend-parent [nss] + (when-let [parent (parent-ns (first nss))] + (cons parent nss))) + +(defn path-to-ns [ns-str] + (last (take-while some? (iterate prepend-parent [ns-str])))) + +^{::clerk/visibility {:result :show}} +(clerk/html + (let [matches (try + (match-nss @!ns-query) + (catch Exception _ :error))] + [:<> + [:style ".markdown-viewer { padding: 0 !important; }"] + [:div.w-screen.h-screen.flex.fixed.left-0.top-0.bg-white.dark:bg-slate-950 + [:div.border-r.dark:border-slate-800.flex-shrink-0.flex.flex-col {:class "w-[300px]"} + [:div.px-3.py-3.border-b.dark:border-slate-800 + (clerk/with-viewer {:render-fn render-input + :transform-fn clerk/mark-presented} (viewer/->viewer-eval `!ns-query)) + (when (= matches :error) + [:div.text-red-600.dark:text-red-400.mt-2.font-sans.px-2.text-xs + "😖 Invalid or incomplete Regex pattern."])] + [:div.pb-5.flex-auto.overflow-y-auto + (cond (not (str/blank? @!ns-query)) + [:div + [:div.tracking-wider.uppercase.text-slate-500.dark:text-slate-400.px-5.font-sans.text-xs.mt-5 "Search results"] + (if (and (not= :error matches) (seq matches)) + (into [:div.text-sm.font-sans.px-5.mt-3] + (map render-ns) + (ns-tree matches)) + [:div.px-5.mt-3.font-sans.text-sm "Nothing found."])] + (= @!active-ns :all) + [:div + [:div.tracking-wider.uppercase.text-slate-500.dark:text-slate-400.px-5.font-sans.text-xs.mt-5 "All namespaces"] + (into [:div.text-sm.font-sans.px-5.mt-2] + (map render-ns) + (ns-tree (sort (map (comp str ns-name) (all-ns)))))] + :else + [:<> + [:div + [:div + [:div.tracking-wider.uppercase.text-slate-500.dark:text-slate-400.px-5.font-sans.text-xs.mt-5.mb-2 "Nav"] + (when-some [ns-name (some-> (str/join "." (butlast (str/split @!active-ns #"\."))) symbol find-ns ns-name str)] + [:div.px-5.font-sans.text-xs.mt-1.hover:text-indigo-600.dark:hover:text-white.hover:underline.cursor-pointer + {:on-click (viewer/->viewer-eval `(fn [] + (reset! !active-ns ~ns-name) + (reset! !ns-query "")))} + "One level up"]) + [:div.px-5.font-sans.text-xs.mt-1.hover:text-indigo-600.dark:hover:text-white.hover:underline.cursor-pointer + {:on-click (viewer/->viewer-eval `(fn [] + (reset! !active-ns :all) + (reset! !ns-query "")))} + "All namespaces"]] + [:div.tracking-wider.uppercase.text-slate-500.dark:text-slate-400.px-5.font-sans.text-xs.mt-5 "Current namespace"] + (into [:div.text-sm.font-sans.px-5.mt-2] + (map render-ns) + (ns-tree (str-match-nss @!active-ns)))]])]] + [:div.flex-auto.max-h-screen.overflow-y-auto.px-8.py-5 + (let [ns (some-> @!active-ns symbol find-ns)] + (cond + ns [:<> + [:div.font-bold.font-sans.text-xl {:style {:margin 0}} (ns-name ns)] + (when-let [doc (-> ns meta :doc)] + [:div.mt-4.leading-normal.viewer-markdown.prose + (clerk/md doc)]) + (into [:<>] + (map (comp :nextjournal/value var->doc-viewer val)) + (into (sorted-map) (-> ns ns-publics)))] + @!active-ns [:<> + [:div.font-bold.font-sans.text-xl {:style {:margin 0}} (if (= @!active-ns :all) + "All namespaces in classpath" + @!active-ns)] + (into [:div.mt-2] + (map (fn [ns-str] + [:div.pt-5.mt-5.border-t.dark:border-slate-800.hover:text-indigo-600.cursor-pointer.group + {:on-click (viewer/->viewer-eval `(fn [] + (reset! !active-ns ~ns-str) + (reset! !ns-query "")))} + [:div.font-sans.text-base.font-bold.group-hover:underline + {:style {:margin 0}} + ns-str] + (when-let [doc (some-> ns-str symbol find-ns meta :doc)] + [:div.mt-2.leading-normal.viewer-markdown.prose.text-sm + (clerk/md doc)])])) + (if (= :all @!active-ns) + (sort (map :name (ns-tree (map (comp str ns-name) (all-ns))))) + (str-match-nss @!active-ns)))] + :else [:div "No namespaces found."]))]]])) + +#_(deref nextjournal.clerk.webserver/!doc) + From 9f336d7d424d39bd6b4aa6c9fad19dccd09f9956 Mon Sep 17 00:00:00 2001 From: Philippa Markovics Date: Thu, 31 Aug 2023 11:13:46 +0200 Subject: [PATCH 07/26] Make sure connection status is visible above all else --- src/nextjournal/clerk/render.cljs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/nextjournal/clerk/render.cljs b/src/nextjournal/clerk/render.cljs index 4c65bd53c..a7a07d407 100644 --- a/src/nextjournal/clerk/render.cljs +++ b/src/nextjournal/clerk/render.cljs @@ -92,7 +92,7 @@ [:div.bg-sky-500.dark:bg-purple-400 {:class "h-[2px]" :style {:width (str (* cell-progress 100) "%")}}]])])5 (defn connection-status [status] - [:div.absolute.text-red-600.dark:text-white.text-xs.font-sans.ml-1.bg-white.dark:bg-red-800.rounded-full.shadow.z-20.font-bold.px-2.border.border-red-400 + [:div.absolute.text-red-600.dark:text-white.text-xs.font-sans.ml-1.bg-white.dark:bg-red-800.rounded-full.shadow.z-30.font-bold.px-2.border.border-red-400 {:style {:font-size "0.5rem"} :class "left-[35px] md:left-0 mt-[7px] md:mt-1"} status]) From 4e627e8f495ed23035fb8c88fbb807b931abd691 Mon Sep 17 00:00:00 2001 From: Andrea Amantini Date: Mon, 11 Sep 2023 20:35:28 +0200 Subject: [PATCH 08/26] Fix navigation in ToC (#556) * skip default handling of clicks on links in ToC * skip handling browser history-pop when events are pushed by ToC Was a regression introduced with b7207926bacb10e6af17075efd71df3363b74d21. --- src/nextjournal/clerk/render.cljs | 14 +++++++------- src/nextjournal/clerk/render/navbar.cljs | 1 + 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/src/nextjournal/clerk/render.cljs b/src/nextjournal/clerk/render.cljs index a7a07d407..a5cead563 100644 --- a/src/nextjournal/clerk/render.cljs +++ b/src/nextjournal/clerk/render.cljs @@ -722,8 +722,8 @@ (j/call js/history (if replace? :replaceState :pushState) (clj->js opts) "" (str (.. js/document -location -origin) "/" path (when fragment (str "#" fragment)))))) -(defn handle-history-popstate [state ^js e] - (when-let [{:as opts :keys [path]} (js->clj (.-state e) :keywordize-keys true)] +(defn handle-history-popstate [^js e] + (when-some [path (:path (js->clj (.-state e) :keywordize-keys true))] (.preventDefault e) (clerk-eval (list 'nextjournal.clerk.webserver/navigate! {:nav-path path :skip-history? true})))) @@ -733,7 +733,7 @@ (when-some [doc (get path->doc url)] (set-state! {:doc doc})))) -(defn handle-anchor-click [{:as state :keys [path->doc url->path]} ^js e] +(defn handle-anchor-click [^js e] (when-some [url (some-> e .-target closest-anchor-parent .-href ->URL)] (when-not (ignore-anchor-click? e url) (.preventDefault e) @@ -742,7 +742,7 @@ (seq (.-hash url)) (assoc :fragment (subs (.-hash url) 1)))))))) -(defn handle-initial-load [state ^js _e] +(defn handle-initial-load [^js _e] (history-push-state {:path (subs js/location.pathname 1) :replace? true})) (defn setup-router! [state] @@ -754,9 +754,9 @@ (cond (and (static-app? state) (:bundle? state)) [(gevents/listen js/window gevents/EventType.HASHCHANGE (partial handle-hashchange state) false)] (not (static-app? state)) - [(gevents/listen js/document gevents/EventType.CLICK (partial handle-anchor-click state) false) - (gevents/listen js/window gevents/EventType.POPSTATE (partial handle-history-popstate state) false) - (gevents/listen js/window gevents/EventType.LOAD (partial handle-initial-load state) false)]))))) + [(gevents/listen js/document gevents/EventType.CLICK handle-anchor-click false) + (gevents/listen js/window gevents/EventType.POPSTATE handle-history-popstate false) + (gevents/listen js/window gevents/EventType.LOAD handle-initial-load false)]))))) (defn ^:export mount [] diff --git a/src/nextjournal/clerk/render/navbar.cljs b/src/nextjournal/clerk/render/navbar.cljs index 183bd789b..3dada1142 100644 --- a/src/nextjournal/clerk/render/navbar.cljs +++ b/src/nextjournal/clerk/render/navbar.cljs @@ -40,6 +40,7 @@ [_ hash] (some-> search (.split "#"))] (when (or (and search hash (= path-name current-path-name)) anchor-only?) (let [anchor (if anchor-only? path-name (str "#" hash))] + (.stopPropagation event) (.preventDefault event) (when set-hash? (.pushState js/history #js {} "" anchor)) From f35635fdec255a23bf8c0346deb917e5a420858a Mon Sep 17 00:00:00 2001 From: Philippa Markovics Date: Mon, 11 Sep 2023 21:31:40 +0200 Subject: [PATCH 09/26] =?UTF-8?q?=F0=9F=A6=9C=20Cache=20remote=20assets=20?= =?UTF-8?q?using=20a=20ServiceWorker=20in=20the=20browser=20(#555)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This introduces a ServiceWorker to intercept and cache network requests to remote assets in the browser. It works for Clerk's js bundle, its tailwind css script, fonts and as well as javascript dynamically loaded using d3-require like Clerk's Vega and Plotly viewers. To use it, you need to open Clerk in the browser when online to populate the cache. Viewers that are dynamically loaded (e.g. Vega or Plotly) need to be used once while offline to be cached. We're considering loading them on worker init in a follow up. We currently apply it to the following urls: * https://fonts.bunny.net * https://cdn.skypack.dev * https://cdn.tailwindcss.com * https://storage.clerk.garden * https://cdn.jsdelivr.net * https://vega.github.io Co-authored-by: Martin Kavalar --- resources/public/clerk_service_worker.js | 54 ++++++++++++++++++++++++ src/nextjournal/clerk/view.clj | 8 ++++ src/nextjournal/clerk/webserver.clj | 3 ++ 3 files changed, 65 insertions(+) create mode 100644 resources/public/clerk_service_worker.js diff --git a/resources/public/clerk_service_worker.js b/resources/public/clerk_service_worker.js new file mode 100644 index 000000000..15ecc0e31 --- /dev/null +++ b/resources/public/clerk_service_worker.js @@ -0,0 +1,54 @@ +const cacheName = 'clerk-browser-cache-v2'; + +const hosts = [ + 'https://fonts.bunny.net', + 'https://cdn.skypack.dev', + 'https://cdn.tailwindcss.com', + 'https://storage.clerk.garden', + 'https://cdn.jsdelivr.net', + 'https://vega.github.io' +]; + +self.addEventListener('install', function(event) { + //console.log('install', event); + self.skipWaiting(); +}); + +self.addEventListener('activate', function(event) { + //console.log('activate', event); + + // Remove unwanted caches + event.waitUntil( + caches.keys().then(function(cacheNames) { + return Promise.all( + cacheNames.map(function(cache) { + if (cache !== cacheName) { + console.log("Service Worker: Clearing old cache"); + return caches.delete(cache); + } + })); + })); + + return self.clients.claim() +}); + +self.addEventListener('fetch', function(event) { + //console.log(event); + + event.respondWith( + caches.match(event.request).then(function(response) { + return response || fetch(event.request).then(function(response) { + + hosts.map(function(host) { + if (event.request.url.indexOf(host) === 0) { + var clonedResponse = response.clone(); + caches.open(cacheName).then(function(cache) { + cache.put(event.request, clonedResponse); + }); + } + }); + return response; + }); + }) + ); +}); diff --git a/src/nextjournal/clerk/view.clj b/src/nextjournal/clerk/view.clj index a7eb4b754..1eeb34743 100644 --- a/src/nextjournal/clerk/view.clj +++ b/src/nextjournal/clerk/view.clj @@ -55,6 +55,14 @@ [:head [:meta {:charset "UTF-8"}] [:meta {:name "viewport" :content "width=device-width, initial-scale=1"}] + (when conn-ws? + [:script {:type "text/javascript"} + "if ('serviceWorker' in navigator) { + navigator.serviceWorker + .register('/clerk_service_worker.js') + //.then(function() { console.log('Service Worker: Registered') }) + .catch(function(error) { console.log('Service Worker: Error', error) }) + }"]) (when current-path (v/open-graph-metas (-> state :path->doc (get current-path) v/->value :open-graph))) (if exclude-js? (include-viewer-css state) diff --git a/src/nextjournal/clerk/webserver.clj b/src/nextjournal/clerk/webserver.clj index d31b04c7f..f8ffe7cb5 100644 --- a/src/nextjournal/clerk/webserver.clj +++ b/src/nextjournal/clerk/webserver.clj @@ -4,6 +4,7 @@ [clojure.pprint :as pprint] [clojure.set :as set] [clojure.string :as str] + [clojure.java.io :as io] [editscript.core :as editscript] [nextjournal.clerk.config :as config] [nextjournal.clerk.view :as view] @@ -104,6 +105,7 @@ #_(serve-file "public" {:uri "/js/viewer.js"}) + (defn sync-atom-changed [key atom old-state new-state] (eval '(nextjournal.clerk/recompute!))) @@ -251,6 +253,7 @@ (case (get (re-matches #"/([^/]*).*" uri) 1) "_blob" (serve-blob @!doc (extract-blob-opts req)) ("build" "js" "css") (serve-file uri (str "public" uri)) + "clerk_service_worker.js" (serve-file uri (fs/path (io/resource "public/clerk_service_worker.js"))) ("_fs") (serve-file uri (str/replace uri "/_fs/" "")) "_ws" {:status 200 :body "upgrading..."} "favicon.ico" {:status 404} From 0dae460df026080b1b3c923784af579ce7a6bc00 Mon Sep 17 00:00:00 2001 From: Martin Kavalar Date: Tue, 12 Sep 2023 10:53:38 +0200 Subject: [PATCH 10/26] Fix serving service worker from jar --- src/nextjournal/clerk/webserver.clj | 9 +++++++-- test/nextjournal/clerk/webserver_test.clj | 15 ++++++++++++++- 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/src/nextjournal/clerk/webserver.clj b/src/nextjournal/clerk/webserver.clj index f8ffe7cb5..8c6000b6d 100644 --- a/src/nextjournal/clerk/webserver.clj +++ b/src/nextjournal/clerk/webserver.clj @@ -103,8 +103,13 @@ :body (fs/read-all-bytes file)} {:status 404}))) -#_(serve-file "public" {:uri "/js/viewer.js"}) +(defn serve-resource [resource] + (cond-> {:status 200 + :body (io/input-stream resource)} + (= "js" (fs/extension (fs/file (.getFile resource)))) + (assoc :headers {"Content-Type" "text/javascript"}))) +#_(serve-resource (io/resource "public/clerk_service_worker.js")) (defn sync-atom-changed [key atom old-state new-state] (eval '(nextjournal.clerk/recompute!))) @@ -253,7 +258,7 @@ (case (get (re-matches #"/([^/]*).*" uri) 1) "_blob" (serve-blob @!doc (extract-blob-opts req)) ("build" "js" "css") (serve-file uri (str "public" uri)) - "clerk_service_worker.js" (serve-file uri (fs/path (io/resource "public/clerk_service_worker.js"))) + "clerk_service_worker.js" (serve-resource (io/resource "public/clerk_service_worker.js")) ("_fs") (serve-file uri (str/replace uri "/_fs/" "")) "_ws" {:status 200 :body "upgrading..."} "favicon.ico" {:status 404} diff --git a/test/nextjournal/clerk/webserver_test.clj b/test/nextjournal/clerk/webserver_test.clj index 14da28667..c50b83398 100644 --- a/test/nextjournal/clerk/webserver_test.clj +++ b/test/nextjournal/clerk/webserver_test.clj @@ -1,5 +1,6 @@ (ns nextjournal.clerk.webserver-test - (:require [clojure.test :refer [deftest is testing]] + (:require [clojure.java.io :as io] + [clojure.test :refer [deftest is testing]] [nextjournal.clerk.eval :as eval] [nextjournal.clerk.view :as view] [nextjournal.clerk.webserver :as webserver])) @@ -20,3 +21,15 @@ (is body) (is (= (-> body webserver/read-msg :nextjournal/value first :nextjournal/value) 20))))) +(deftest serve-file-test + (testing "serving a file resource" + (is (= 200 (:status (webserver/serve-file "public/clerk_service_worker.js" "resources/public/clerk_service_worker.js"))) + (= {"Content-Type" "text/javascript"} (:headers (webserver/serve-file "public/clerk_service_worker.js" "resources/public/clerk_service_worker.js")))))) + +(deftest serve-resource-test + (testing "serving a file resource" + (is (= 200 (:status (webserver/serve-resource (io/resource "public/clerk_service_worker.js")))) + (= {"Content-Type" "text/javascript"} (:headers (webserver/serve-resource (io/resource "public/clerk_service_worker.js")))))) + + (testing "serving a resource from a jar" + (is (= 200 (:status (webserver/serve-resource (io/resource "weavejester/dependency.cljc"))))))) From d6fd90c8d028f5f9bd56549673ca8279ee18e18e Mon Sep 17 00:00:00 2001 From: Martin Kavalar Date: Thu, 28 Sep 2023 13:12:23 +0200 Subject: [PATCH 11/26] Update CHANGELOG --- CHANGELOG.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b15beda10..ee28049c2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,12 @@ Changes can be: ## Unreleased +* 🔌 Offline support + + Support working fully offline by adding a ServiceWorker to intercept and cache network requests to remote assets in the browser. It works for Clerk's js bundle, its tailwind css script, fonts and as well as javascript dynamically loaded using d3-require like Clerk's Vega and Plotly viewers. + + To use it, you need to open Clerk in the browser when online to populate the cache. Viewers that are dynamically loaded (e.g. Vega or Plotly) need to be used once while offline to be cached. We're considering loading them on worker init in a follow up. + * 👁️ Improve viewer customization * Simplify customization of number of rows displayed for table viewer using viewer-opts, e.g. `(clerk/table {::clerk/page-size 7})`. Pass `{::clerk/page-size nil}` to display elisions. Can also be passed a form metadata. Fixes [#406](https://github.com/nextjournal/clerk/issues/406). @@ -47,6 +53,8 @@ Changes can be: * 💫 Assign `:name` to every viewer in `default-viewers` +* 🐜 Ensure `var->location` returns a string path location fixing `Cannot open <#object[sun.nio.fs.UnixPath ,,,> as an InputStream` errors + * 🐞 Don't run existing files through `fs/glob`, fixes [#504](https://github.com/nextjournal/clerk/issues/504). Also improves performance of homepage. * 🐞 Show correct non-var return value for deflike form, fixes [#499](https://github.com/nextjournal/clerk/issues/499) From 9c38ff3ef240c9bd21e596792adb2ebdbb5a738d Mon Sep 17 00:00:00 2001 From: Martin Kavalar Date: Thu, 28 Sep 2023 13:13:04 +0200 Subject: [PATCH 12/26] v0.15.957 --- CHANGELOG.md | 4 ++++ README.md | 2 +- resources/META-INF/nextjournal/clerk/meta.edn | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ee28049c2..d040635c5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,10 @@ Changes can be: ## Unreleased +... + +## 0.15.957 (2023-09-28) + * 🔌 Offline support Support working fully offline by adding a ServiceWorker to intercept and cache network requests to remote assets in the browser. It works for Clerk's js bundle, its tailwind css script, fonts and as well as javascript dynamically loaded using d3-require like Clerk's Vega and Plotly viewers. diff --git a/README.md b/README.md index 5c54ec55d..e5ea1e72f 100644 --- a/README.md +++ b/README.md @@ -46,7 +46,7 @@ To use Clerk in your project, you'll need Java 11+ and [`clojure`](https://cloju following dependency to your `deps.edn`: ```edn -{:deps {io.github.nextjournal/clerk {:mvn/version "0.14.919"}}} +{:deps {io.github.nextjournal/clerk {:mvn/version "0.15.957"}}} ``` Require and start Clerk as part of your system start, e.g. in `user.clj`: diff --git a/resources/META-INF/nextjournal/clerk/meta.edn b/resources/META-INF/nextjournal/clerk/meta.edn index 2c8111227..55020c00c 100644 --- a/resources/META-INF/nextjournal/clerk/meta.edn +++ b/resources/META-INF/nextjournal/clerk/meta.edn @@ -1 +1 @@ -{:version {:major 0, :minor 14, :rev-count 919}} \ No newline at end of file +{:version {:major 0, :minor 15, :rev-count 957}} \ No newline at end of file From f41a9cf68e7eba3d9d2dad4ab8759a4d1511b6d6 Mon Sep 17 00:00:00 2001 From: Philippa Markovics Date: Tue, 10 Oct 2023 10:18:15 +0200 Subject: [PATCH 13/26] Clerk Docs Formatting (#554) * Adapt styles for code listings and content widths in doc browser-rendered markdown * Add missing formatting to function doc strings --- src/nextjournal/clerk.clj | 2 ++ src/nextjournal/clerk/doc.clj | 15 ++++++++++----- src/nextjournal/clerk/parser.cljc | 2 +- src/nextjournal/clerk/viewer.cljc | 2 +- 4 files changed, 14 insertions(+), 7 deletions(-) diff --git a/src/nextjournal/clerk.clj b/src/nextjournal/clerk.clj index 7c506d9c4..7f6c99096 100644 --- a/src/nextjournal/clerk.clj +++ b/src/nextjournal/clerk.clj @@ -23,11 +23,13 @@ Accepts ns using a quoted symbol or a `clojure.lang.Namespace`, calls `slurp` on all other arguments, e.g.: + ```clj (nextjournal.clerk/show! \"notebooks/vega.clj\") (nextjournal.clerk/show! 'nextjournal.clerk.tap) (nextjournal.clerk/show! (find-ns 'nextjournal.clerk.tap)) (nextjournal.clerk/show! \"https://raw.githubusercontent.com/nextjournal/clerk-demo/main/notebooks/rule_30.clj\") (nextjournal.clerk/show! (java.io.StringReader. \";; # Notebook from String 👋\n(+ 41 1)\")) + ``` " ([file-or-ns] (show! {} file-or-ns)) ([opts file-or-ns] diff --git a/src/nextjournal/clerk/doc.clj b/src/nextjournal/clerk/doc.clj index f18f61ccf..2e1d629c8 100644 --- a/src/nextjournal/clerk/doc.clj +++ b/src/nextjournal/clerk/doc.clj @@ -51,7 +51,7 @@ [:div.pt-4 (clerk/code (str/join "\n" (mapv (comp pr-str #(concat [var-name] %)) arglists)))]) (when doc - [:div.mt-4.viewer-markdown.prose + [:div.mt-4.viewer-markdown (clerk/md doc)])]))) (defn render-ns [{:keys [name nss vars]}] @@ -117,8 +117,13 @@ (match-nss @!ns-query) (catch Exception _ :error))] [:<> - [:style ".markdown-viewer { padding: 0 !important; }"] - [:div.w-screen.h-screen.flex.fixed.left-0.top-0.bg-white.dark:bg-slate-950 + [:style (str ".markdown-viewer { padding: 0 !important; } " + ".notebook-viewer .max-w-prose { max-width: 100vw !important; }" + ".markdown-viewer p, .markdown-viewer ul, .markdown-viewer ol, .markdown-viewer blockquote { max-width: 65ch; }" + ".doc-viewer .markdown-viewer .code-viewer { background: transparent; }" + ".notebook-viewer .doc-viewer .code-listing { width: auto !important; }" + ".doc-viewer .cm-editor { max-width: 100% !important; overflow-x: auto; }")] + [:div.w-screen.h-screen.flex.fixed.left-0.top-0.bg-white.dark:bg-slate-950.doc-viewer [:div.border-r.dark:border-slate-800.flex-shrink-0.flex.flex-col {:class "w-[300px]"} [:div.px-3.py-3.border-b.dark:border-slate-800 (clerk/with-viewer {:render-fn render-input @@ -167,7 +172,7 @@ ns [:<> [:div.font-bold.font-sans.text-xl {:style {:margin 0}} (ns-name ns)] (when-let [doc (-> ns meta :doc)] - [:div.mt-4.leading-normal.viewer-markdown.prose + [:div.mt-4.leading-normal.viewer-markdown (clerk/md doc)]) (into [:<>] (map (comp :nextjournal/value var->doc-viewer val)) @@ -186,7 +191,7 @@ {:style {:margin 0}} ns-str] (when-let [doc (some-> ns-str symbol find-ns meta :doc)] - [:div.mt-2.leading-normal.viewer-markdown.prose.text-sm + [:div.mt-2.leading-normal.viewer-markdown.text-sm (clerk/md doc)])])) (if (= :all @!active-ns) (sort (map :name (ns-tree (map (comp str ns-name) (all-ns))))) diff --git a/src/nextjournal/clerk/parser.cljc b/src/nextjournal/clerk/parser.cljc index 3607cc4ab..cab9af535 100644 --- a/src/nextjournal/clerk/parser.cljc +++ b/src/nextjournal/clerk/parser.cljc @@ -236,7 +236,7 @@ (defn root-location [zloc] (last (take-while some? (iterate z/up zloc)))) (defn remove-clerk-keys - "Takes a map zipper location, returns a new location representing the input map node with all ::clerk namespaced keys removed. + "Takes a map zipper location, returns a new location representing the input map node with all `::clerk` namespaced keys removed. Whitespace is preserved when possible." [map-loc] (loop [loc (z/down map-loc) parent map-loc] diff --git a/src/nextjournal/clerk/viewer.cljc b/src/nextjournal/clerk/viewer.cljc index 9b4f48bb3..f8d388ad7 100644 --- a/src/nextjournal/clerk/viewer.cljc +++ b/src/nextjournal/clerk/viewer.cljc @@ -1782,7 +1782,7 @@ (prn "`hide-result` has been deprecated, please put `^{:nextjournal.clerk/visibility {:result :hide}}` metadata on the form instead.")))) (defn hide-result - "Deprecated, please put ^{:nextjournal.clerk/visibility {:result :hide}} metadata on the form instead." + "Deprecated, please put `^{:nextjournal.clerk/visibility {:result :hide}}` metadata on the form instead." {:deprecated "0.10"} ([x] (print-hide-result-deprecation-warning) (with-viewer hide-result-viewer {} x)) ([viewer-opts x] (print-hide-result-deprecation-warning) (with-viewer hide-result-viewer viewer-opts x))) From 3fce7eac08b20a37aaeeab100f2063090144d82c Mon Sep 17 00:00:00 2001 From: Sohalt Date: Mon, 16 Oct 2023 19:20:20 +0200 Subject: [PATCH 14/26] bump cas-client (#562) Update cas-client to work with new CAS deployment. --- bb.edn | 2 +- deps.edn | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/bb.edn b/bb.edn index 887c091a4..84f6cdcb9 100644 --- a/bb.edn +++ b/bb.edn @@ -1,7 +1,7 @@ {:min-bb-version "0.9.159" :paths ["bb"] :deps {io.github.nextjournal/dejavu {:git/sha "4980e0cc18c9b09fb220874ace94ba6b57a749ca"} - io.github.nextjournal/cas-client {:git/sha "84ab35c3321c1e51a589fddbeee058aecd055bf8"}} + io.github.nextjournal/cas-client {:git/sha "22ef8360689cd3938e43a3223023ab1b9711818f"}} :tasks {:requires ([tasks :as t] diff --git a/deps.edn b/deps.edn index f3de62143..2efd7dc4f 100644 --- a/deps.edn +++ b/deps.edn @@ -42,7 +42,7 @@ binaryage/devtools {:mvn/version "1.0.3"} cider/cider-nrepl {:mvn/version "0.29.0"} com.clojure-goes-fast/clj-async-profiler {:mvn/version "1.0.3"} - io.github.nextjournal/cas-client {:git/sha "84ab35c3321c1e51a589fddbeee058aecd055bf8"} + io.github.nextjournal/cas-client {:git/sha "22ef8360689cd3938e43a3223023ab1b9711818f"} org.slf4j/slf4j-nop {:mvn/version "2.0.7"} org.babashka/cli {:mvn/version "0.5.40"}} :extra-paths ["dev" "notebooks"] @@ -74,7 +74,7 @@ io.github.nextjournal/clerk-slideshow {:git/sha "11a83fea564da04b9d17734f2031a4921d917893"}}} :build {:deps {io.github.nextjournal/clerk {:local/root "."} - io.github.nextjournal/cas-client {:git/sha "84ab35c3321c1e51a589fddbeee058aecd055bf8"} + io.github.nextjournal/cas-client {:git/sha "22ef8360689cd3938e43a3223023ab1b9711818f"} io.github.clojure/tools.build {:git/tag "v0.6.1" :git/sha "515b334"} io.github.slipset/deps-deploy {:git/sha "b4359c5d67ca002d9ed0c4b41b710d7e5a82e3bf"}} :extra-paths ["bb" "src" "resources"] ;; for loading lookup-url in build From 424bf35040a266f71d00d8a581ba7cdfcfcd4a75 Mon Sep 17 00:00:00 2001 From: Martin Kavalar Date: Mon, 23 Oct 2023 09:13:36 +0200 Subject: [PATCH 15/26] Allow to disable welcome page (#561) When passing the same `:paths` or `:paths-fn` arg as for `build!` to `serve!` Clerk will no longer the built-in welcome page and instead serve the generated or user-specified index page on root. This is a first step towards enabling easy sharing fully interactive Clerk documents including pagination &c. Co-authored-by: Andrea Amantini --- deps.edn | 4 + garden.edn | 1 + notebooks/intern.clj | 2 - src/nextjournal/clerk.clj | 9 +- src/nextjournal/clerk/builder.clj | 119 +----------------- src/nextjournal/clerk/git.clj | 40 ++++++ src/nextjournal/clerk/home.clj | 8 +- src/nextjournal/clerk/index.clj | 11 +- src/nextjournal/clerk/paths.clj | 156 ++++++++++++++++++++++++ src/nextjournal/clerk/viewer.cljc | 38 +++--- src/nextjournal/clerk/webserver.clj | 70 ++++++----- test/nextjournal/clerk/builder_test.clj | 54 -------- test/nextjournal/clerk/paths_test.clj | 60 +++++++++ 13 files changed, 348 insertions(+), 224 deletions(-) create mode 100644 garden.edn create mode 100644 src/nextjournal/clerk/git.clj create mode 100644 src/nextjournal/clerk/paths.clj create mode 100644 test/nextjournal/clerk/paths_test.clj diff --git a/deps.edn b/deps.edn index 2efd7dc4f..5973eda1c 100644 --- a/deps.edn +++ b/deps.edn @@ -73,6 +73,10 @@ :exclusions [org.babashka/sci]} io.github.nextjournal/clerk-slideshow {:git/sha "11a83fea564da04b9d17734f2031a4921d917893"}}} + :nextjournal/garden {:exec-fn nextjournal.clerk/serve! + :exec-args {:index "book.clj"} + :nextjournal.garden/aliases [:demo]} + :build {:deps {io.github.nextjournal/clerk {:local/root "."} io.github.nextjournal/cas-client {:git/sha "22ef8360689cd3938e43a3223023ab1b9711818f"} io.github.clojure/tools.build {:git/tag "v0.6.1" :git/sha "515b334"} diff --git a/garden.edn b/garden.edn new file mode 100644 index 000000000..ef60faf70 --- /dev/null +++ b/garden.edn @@ -0,0 +1 @@ +{:project "book-of-clerk"} diff --git a/notebooks/intern.clj b/notebooks/intern.clj index 79a3ff1cc..98a146b66 100644 --- a/notebooks/intern.clj +++ b/notebooks/intern.clj @@ -33,8 +33,6 @@ c (ns-unmap *ns* 'variable) (ns-unmap (find-ns 'foreign) 'variable) - (reset! nextjournal.clerk.webserver/!doc nextjournal.clerk.webserver/help-doc) - ;; inspect recorded interns (-> @nextjournal.clerk.webserver/!doc :blocks (->> (mapcat (comp :nextjournal/interned :result)))) diff --git a/src/nextjournal/clerk.clj b/src/nextjournal/clerk.clj index 7f6c99096..a5a9c3e0f 100644 --- a/src/nextjournal/clerk.clj +++ b/src/nextjournal/clerk.clj @@ -11,6 +11,7 @@ [nextjournal.clerk.config :as config] [nextjournal.clerk.eval :as eval] [nextjournal.clerk.parser :as parser] + [nextjournal.clerk.paths :as paths] [nextjournal.clerk.viewer :as v] [nextjournal.clerk.webserver :as webserver])) @@ -51,7 +52,10 @@ :else file-or-ns) - doc (try (merge opts + doc (try (merge (webserver/get-build-opts) + opts + (when-let [path (paths/path-in-cwd file-or-ns)] + {:file-path path}) {:nav-path (webserver/->nav-path file-or-ns)} (parser/parse-file {:doc? true} file)) (catch java.io.FileNotFoundException _e @@ -63,7 +67,8 @@ e)))) _ (reset! !last-file file) {:keys [blob->result]} @webserver/!doc - {:keys [result time-ms]} (try (eval/time-ms (eval/+eval-results blob->result (assoc doc :set-status-fn webserver/set-status!))) + {:keys [result time-ms]} (try (eval/time-ms (binding [paths/*build-opts* (webserver/get-build-opts)] + (eval/+eval-results blob->result (assoc doc :set-status-fn webserver/set-status!)))) (catch Exception e (throw (ex-info (str "`nextjournal.clerk/show!` encountered an eval error with: `" (pr-str file-or-ns) "`") {::doc (assoc doc :blob->result blob->result)} e))))] (println (str "Clerk evaluated '" file "' in " time-ms "ms.")) diff --git a/src/nextjournal/clerk/builder.clj b/src/nextjournal/clerk/builder.clj index 8523b5aa9..b203e3e35 100644 --- a/src/nextjournal/clerk/builder.clj +++ b/src/nextjournal/clerk/builder.clj @@ -2,19 +2,18 @@ "Clerk's Static App Builder." (:require [babashka.fs :as fs] [babashka.process :refer [sh]] - [clojure.edn :as edn] [clojure.java.browse :as browse] [clojure.java.io :as io] [clojure.string :as str] [nextjournal.clerk.analyzer :as analyzer] [nextjournal.clerk.builder-ui :as builder-ui] + [nextjournal.clerk.config :as config] [nextjournal.clerk.eval :as eval] [nextjournal.clerk.parser :as parser] + [nextjournal.clerk.paths :as paths] [nextjournal.clerk.view :as view] [nextjournal.clerk.viewer :as viewer] - [nextjournal.clerk.webserver :as webserver] - [nextjournal.clerk.config :as config]) - (:import (java.net URL))) + [nextjournal.clerk.webserver :as webserver])) (def clerk-docs (into ["CHANGELOG.md" @@ -127,84 +126,6 @@ (def default-out-path (str "public" fs/file-separator "build")) -(defn ^:private ensure-not-empty [build-opts {:as opts :keys [error expanded-paths]}] - (if error - opts - (if (empty? expanded-paths) - (merge {:error "nothing to build" :expanded-paths expanded-paths} (select-keys build-opts [:paths :paths-fn :index])) - opts))) - -(defn ^:private maybe-add-index [{:as build-opts :keys [index]} {:as opts :keys [expanded-paths]}] - (if-not (contains? build-opts :index) - opts - (if (and (not (instance? URL index)) - (not (symbol? index)) - (or (not (string? index)) (not (fs/exists? index)))) - {:error "`:index` must be either an instance of java.net.URL or a string and point to an existing file" - :index index} - (cond-> opts - (and index (not (contains? (set expanded-paths) index))) - (update :expanded-paths conj index))))) - -#_(maybe-add-index {:index "book.clj"} {:expanded-paths ["README.md"]}) -#_(maybe-add-index {:index 'book.clj} {:expanded-paths ["README.md"]}) - -(defn resolve-paths [{:as build-opts :keys [paths paths-fn index]}] - (when (and paths paths-fn) - (binding [*out* *err*] - (println "[info] both `:paths` and `:paths-fn` are set, `:paths` will take precendence."))) - (if (not (or paths paths-fn index)) - {:error "must set either `:paths`, `:paths-fn` or `:index`." - :build-opts build-opts} - (cond paths (if (sequential? paths) - {:resolved-paths paths} - {:error "`:paths` must be sequential" :paths paths}) - paths-fn (let [ex-msg "`:path-fn` must be a qualified symbol pointing at an existing var."] - (if-not (qualified-symbol? paths-fn) - {:error ex-msg :paths-fn paths-fn} - (if-some [resolved-var (try (requiring-resolve paths-fn) - (catch Exception _e nil))] - (let [{:as opts :keys [error paths]} - (try {:paths (cond-> @resolved-var (fn? @resolved-var) (apply []))} - (catch Exception e - {:error (str "An error occured invoking `" (pr-str resolved-var) "`: " (ex-message e)) - :paths-fn paths-fn}))] - (if error - opts - (if-not (sequential? paths) - {:error (str "`:paths-fn` must compute to a sequential value.") - :paths-fn paths-fn :resolved-paths paths} - {:resolved-paths paths}))) - {:error ex-msg :paths-fn paths-fn}))) - index {:resolved-paths []}))) - -#_(resolve-paths {:paths ["notebooks/di*.clj"]}) -#_(resolve-paths {:paths-fn 'clojure.core/inc}) -#_(resolve-paths {:paths-fn 'nextjournal.clerk.builder/clerk-docs}) - -(defn expand-paths [build-opts] - (let [{:as opts :keys [error resolved-paths]} (resolve-paths build-opts)] - (if error - opts - (->> resolved-paths - (mapcat (fn [path] (if (fs/exists? path) - [path] - (fs/glob "." path)))) - (filter (complement fs/directory?)) - (mapv (comp str fs/file)) - (hash-map :expanded-paths) - (maybe-add-index build-opts) - (ensure-not-empty build-opts))))) - -#_(expand-paths {:paths ["notebooks/di*.clj"] :index "src/nextjournal/clerk/index.clj"}) -#_(expand-paths {:paths ['notebooks/rule_30.clj]}) -#_(expand-paths {:index "book.clj"}) -#_(expand-paths {:paths-fn `clerk-docs}) -#_(expand-paths {:paths-fn `clerk-docs-2}) -#_(do (defn my-paths [] ["notebooks/h*.clj"])§ - (expand-paths {:paths-fn `my-paths})) -#_(expand-paths {:paths ["notebooks/viewers**"]}) - (def builtin-index (io/resource "nextjournal/clerk/index.clj")) @@ -216,17 +137,15 @@ (let [opts+index (cond-> opts index (assoc :index (str index))) {:as opts' :keys [expanded-paths]} (cond-> opts+index - expand-paths? (merge (expand-paths opts+index)))] + expand-paths? (merge (paths/expand-paths opts+index)))] (-> opts' (update :resource->url #(merge {} %2 %1) @config/!resource->url) (cond-> #_opts' expand-paths? (dissoc :expand-paths?) - (and (not index) (= 1 (count expanded-paths))) - (assoc :index (first expanded-paths)) (and (not index) (< 1 (count expanded-paths)) (every? (complement viewer/index-path?) expanded-paths)) (as-> opts - (-> opts (assoc :index builtin-index) (update :expanded-paths conj builtin-index)))))))) + (-> opts (assoc :index builtin-index) (update :expanded-paths conj builtin-index)))))))) #_(process-build-opts {:index 'book.clj :expand-paths? true}) #_(process-build-opts {:paths ["notebooks/rule_30.clj"] :expand-paths? true}) @@ -336,31 +255,6 @@ (str (viewer/relative-root-prefix-from (viewer/map-index opts file)) path (when fragment (str "#" fragment)))))) -(defn read-opts-from-deps-edn! [] - (if (fs/exists? "deps.edn") - (let [deps-edn (edn/read-string (slurp "deps.edn"))] - (if-some [clerk-alias (get-in deps-edn [:aliases :nextjournal/clerk])] - (get clerk-alias :exec-args - {:error (str "No `:exec-args` found in `:nextjournal/clerk` alias.")}) - {:error (str "No `:nextjournal/clerk` alias found in `deps.edn`.")})) - {:error (str "No `deps.edn` found in project.")})) - -(def ^:dynamic ^:private *build-opts* nil) -(def build-help-link "\n\nLearn how to [set up your static build](https://book.clerk.vision/#static-building).") -(defn index-paths - ([] (index-paths (or *build-opts* (read-opts-from-deps-edn!)))) - ([{:as opts :keys [index error]}] - (if error - (update opts :error str build-help-link) - (let [{:as result :keys [expanded-paths error]} (expand-paths opts)] - (if error - (update result :error str build-help-link) - {:paths (remove #{index "index.clj"} expanded-paths)}))))) - -#_(index-paths) -#_(index-paths {:paths ["CHANGELOG.md"]}) -#_(index-paths {:paths-fn "boom"}) - (defn build-static-app! [{:as opts :keys [bundle?]}] (let [{:as opts :keys [download-cache-fn upload-cache-fn report-fn compile-css? expanded-paths error]} (process-build-opts (assoc opts :expand-paths? true)) @@ -391,11 +285,10 @@ (let [{result :result duration :time-ms} (eval/time-ms (try (binding [*ns* *ns* - *build-opts* opts + paths/*build-opts* opts viewer/doc-url (partial doc-url opts file)] (let [doc (eval/eval-analyzed-doc doc)] (assoc doc :viewer (view/doc->viewer (assoc opts - :static-build? true :nav-path (if (instance? java.net.URL file) (str "'" (:ns doc)) (str file))) diff --git a/src/nextjournal/clerk/git.clj b/src/nextjournal/clerk/git.clj new file mode 100644 index 000000000..78984a6c7 --- /dev/null +++ b/src/nextjournal/clerk/git.clj @@ -0,0 +1,40 @@ +(ns nextjournal.clerk.git + "Clerk's Git integration for backlinks to source code repos." + (:require [babashka.process :as p] + [clojure.string :as str])) + +(defn ^:private shell-out-str + "Shell helper, calls a cmd and returns it output string trimmed." + [cmd] + (str/trim (:out (p/shell {:out :string} cmd)))) + +#_(shell-out-str "git rev-parse HEAD") +#_(shell-out-str "zonk") + +(defn ->github-project [remote-url] + (second (re-find #"^git@github\.com:(.*)\.git$" remote-url))) + +(defn ->https-git-url + "Takes a git `remote-url` and tries to convert it into a https url for + backlinks. Currently only works for github, should be extended for + gitlab, etc." + [remote-url] + (cond + (str/starts-with? remote-url "https://") + (str/replace remote-url #"\.git$" "") + + (->github-project remote-url) + (str "https://github.com/%s" (->github-project remote-url)))) + +#_(->https-git-url "https://github.com/nextjournal/clerk.git") +#_(->https-git-url "git@github.com:nextjournal/clerk.git") + +(defn read-git-attrs [] + (try {:git/sha (shell-out-str "git rev-parse HEAD") + :git/url (some ->https-git-url + (map #(shell-out-str (str "git remote get-url " %)) + (str/split-lines (shell-out-str "git remote"))))} + (catch Exception _ + {}))) + +#_(read-git-attrs) diff --git a/src/nextjournal/clerk/home.clj b/src/nextjournal/clerk/home.clj index 891a207eb..64e84c09d 100644 --- a/src/nextjournal/clerk/home.clj +++ b/src/nextjournal/clerk/home.clj @@ -1,9 +1,9 @@ (ns nextjournal.clerk.home {:nextjournal.clerk/visibility {:code :hide :result :hide}} - (:require [clojure.string :as str] - [babashka.fs :as fs] + (:require [babashka.fs :as fs] + [clojure.string :as str] [nextjournal.clerk :as clerk] - [nextjournal.clerk.builder :as builder] + [nextjournal.clerk.paths :as paths] [nextjournal.clerk.viewer :as v])) (defn glob-notebooks [] @@ -192,7 +192,7 @@ (when-not (seq (:query @!filter)) [:div {:class "w-1/2 pt-6 pl-6"} [:h4.text-lg "Static Build Index"] - (let [{:keys [paths error]} (builder/index-paths)] + (let [{:keys [paths error]} (paths/index-paths)] (cond error [:div {:class "-mx-8"} (clerk/md error)] paths (let [{:keys [query]} @!filter] diff --git a/src/nextjournal/clerk/index.clj b/src/nextjournal/clerk/index.clj index 79a728bae..ed89c99d7 100644 --- a/src/nextjournal/clerk/index.clj +++ b/src/nextjournal/clerk/index.clj @@ -1,12 +1,13 @@ (ns nextjournal.clerk.index - {:nextjournal.clerk/visibility {:code :hide :result :hide}} + {:nextjournal.clerk/visibility {:code :hide :result :hide} + :nextjournal.clerk/no-cache true} (:require [babashka.fs :as fs] [clojure.string :as str] [nextjournal.clerk :as clerk] - [nextjournal.clerk.viewer :as v] - [nextjournal.clerk.builder :as builder])) + [nextjournal.clerk.paths :as paths] + [nextjournal.clerk.viewer :as v])) -(def !paths (delay (builder/index-paths))) +(def !paths (delay (paths/index-paths))) (def index-item-viewer {:pred string? @@ -41,4 +42,4 @@ (clerk/html [:div.text-xs.text-slate-400.font-sans.mb-8.not-prose [:span.block.font-medium "This index page was automatically generated by Clerk."] - "You can customize it by adding a index.clj file to your project’s root directory. See " [:a.text-blue-600.dark:text-blue-300.hover:underline {:href "https://book.clerk.vision/#static-building"} "Static Publishing"] " in the " [:a.text-blue-600.dark:text-blue-300.hover:underline {:href "http://book.clerk.vision"} "Book of Clerk"] "."]) + "You can customize it by adding an index.clj file to your project’s root directory. See " [:a.text-blue-600.dark:text-blue-300.hover:underline {:href "https://book.clerk.vision/#static-building"} "Static Publishing"] " in the " [:a.text-blue-600.dark:text-blue-300.hover:underline {:href "http://book.clerk.vision"} "Book of Clerk"] "."]) diff --git a/src/nextjournal/clerk/paths.clj b/src/nextjournal/clerk/paths.clj new file mode 100644 index 000000000..d877b0a34 --- /dev/null +++ b/src/nextjournal/clerk/paths.clj @@ -0,0 +1,156 @@ +(ns nextjournal.clerk.paths + "Clerk's paths expansion and paths-fn handling." + (:require [babashka.fs :as fs] + [clojure.edn :as edn] + [clojure.string :as str] + [nextjournal.clerk.git :as git]) + (:import [java.net URL])) + +(defn ^:private ensure-not-empty [build-opts {:as opts :keys [error expanded-paths]}] + (if error + opts + (if (empty? expanded-paths) + (merge {:error "nothing to build" :expanded-paths expanded-paths} (select-keys build-opts [:paths :paths-fn :index])) + opts))) + +(defn ^:private maybe-add-index [{:as build-opts :keys [index]} {:as opts :keys [expanded-paths]}] + (if-not (contains? build-opts :index) + opts + (if (and (not (instance? URL index)) + (not (symbol? index)) + (or (not (string? index)) (not (fs/exists? index)))) + {:error "`:index` must be either an instance of java.net.URL or a string and point to an existing file" + :index index} + (cond-> opts + (and index (not (contains? (set expanded-paths) index))) + (update :expanded-paths conj index))))) + +#_(maybe-add-index {:index "book.clj"} {:expanded-paths ["README.md"]}) +#_(maybe-add-index {:index 'book.clj} {:expanded-paths ["README.md"]}) + +(defn resolve-paths [{:as build-opts :keys [paths paths-fn index]}] + (when (and paths paths-fn) + (binding [*out* *err*] + (println "[info] both `:paths` and `:paths-fn` are set, `:paths` will take precendence."))) + (if (not (or paths paths-fn index)) + {:error "must set either `:paths`, `:paths-fn` or `:index`." + :build-opts build-opts} + (cond paths (if (sequential? paths) + {:resolved-paths paths} + {:error "`:paths` must be sequential" :paths paths}) + paths-fn (let [ex-msg "`:path-fn` must be a qualified symbol pointing at an existing var."] + (if-not (qualified-symbol? paths-fn) + {:error ex-msg :paths-fn paths-fn} + (if-some [resolved-var (try (requiring-resolve paths-fn) + (catch Exception _e nil))] + (let [{:as opts :keys [error paths]} + (try {:paths (cond-> @resolved-var (fn? @resolved-var) (apply []))} + (catch Exception e + {:error (str "An error occured invoking `" (pr-str resolved-var) "`: " (ex-message e)) + :paths-fn paths-fn}))] + (if error + opts + (if-not (sequential? paths) + {:error (str "`:paths-fn` must compute to a sequential value.") + :paths-fn paths-fn :resolved-paths paths} + {:resolved-paths paths}))) + {:error ex-msg :paths-fn paths-fn}))) + index {:resolved-paths []}))) + +#_(resolve-paths {:paths ["notebooks/di*.clj"]}) +#_(resolve-paths {:paths-fn 'clojure.core/inc}) +#_(resolve-paths {:paths-fn 'nextjournal.clerk.builder/clerk-docs}) + +(defn set-index-when-single-path [{:as opts :keys [expanded-paths]}] + (cond-> opts + (and (not (contains? opts :index)) + (= 1 (count expanded-paths))) + (assoc :index (first expanded-paths)))) + +#_(set-index-when-single-path {:expanded-paths ["notebooks/rule_30.clj"]}) +#_(set-index-when-single-path {:expanded-paths ["notebooks/rule_30.clj" "book.clj"]}) + +(defn expand-paths [build-opts] + (let [{:as opts :keys [error resolved-paths]} (resolve-paths build-opts)] + (if error + opts + (->> resolved-paths + (mapcat (fn [path] (if (fs/exists? path) + [path] + (fs/glob "." path)))) + (filter (complement fs/directory?)) + (mapv (comp str fs/file)) + (hash-map :expanded-paths) + (maybe-add-index build-opts) + (set-index-when-single-path) + (ensure-not-empty build-opts))))) + +#_(expand-paths {:paths ["notebooks/di*.clj"] :index "src/nextjournal/clerk/index.clj"}) +#_(expand-paths {:paths ['notebooks/rule_30.clj]}) +#_(expand-paths {:index "book.clj"}) +#_(expand-paths {:paths-fn `nextjournal.clerk.builder/clerk-docs}) +#_(expand-paths {:paths-fn `clerk-docs-2}) +#_(do (defn my-paths [] ["notebooks/h*.clj"])§ + (expand-paths {:paths-fn `my-paths})) +#_(expand-paths {:paths ["notebooks/viewers**"]}) + + +(defn read-opts-from-deps-edn! [] + (if (fs/exists? "deps.edn") + (let [deps-edn (edn/read-string (slurp "deps.edn"))] + (if-some [clerk-alias (get-in deps-edn [:aliases :nextjournal/clerk])] + (get clerk-alias :exec-args + {:error (str "No `:exec-args` found in `:nextjournal/clerk` alias.")}) + {:error (str "No `:nextjournal/clerk` alias found in `deps.edn`.")})) + {:error (str "No `deps.edn` found in project.")})) + +(def ^:dynamic *build-opts* nil) + +(def build-help-link "\n\nLearn how to [set up your static build](https://book.clerk.vision/#static-building).") + +(defn index-paths + ([] (index-paths (or *build-opts* (read-opts-from-deps-edn!)))) + ([{:as opts :keys [index error]}] + (if error + (update opts :error str build-help-link) + (let [{:as result :keys [expanded-paths error]} (if (contains? opts :expanded-paths) opts (expand-paths opts))] + (if error + (update result :error str build-help-link) + {:paths (remove #{index "index.clj"} expanded-paths)}))))) + +#_(index-paths) +#_(index-paths {:paths ["CHANGELOG.md"]}) +#_(index-paths {:paths-fn "boom"}) + +(defn process-paths [{:as opts :keys [paths paths-fn index]}] + (merge (if (or paths paths-fn index) + (expand-paths opts) + opts) + (git/read-git-attrs))) + +#_(process-paths {:paths ["notebooks/rule_30.clj"]}) +#_(process-paths {:paths ["notebooks/no_rule_30.clj"]}) +#_(v/route-index? (process-paths @!server)) +#_(route-index (process-paths @!server) "") + + +(defn path-in-cwd + "Turns `file` into a unixified (forward slashed) path if the is in the cwd, + returns `nil` otherwise." + [file] + (when (and (string? file) + (fs/exists? file)) + (let [rel (fs/relativize (fs/cwd) (fs/canonicalize file #{:nofollow-links}))] + (when-not (str/starts-with? (str rel) "..") + (fs/unixify rel))))) + +#_(path-in-cwd "notebooks/rule_30.clj") +#_(path-in-cwd "/tmp/foo.clj") +#_(path-in-cwd "../scratch/rule_30.clj") + +(defn drop-extension [file] + (cond-> file + (fs/extension file) + (str/replace (re-pattern (format ".%s$" (fs/extension file))) ""))) + +#_(drop-extension "notebooks/rule_30.clj") diff --git a/src/nextjournal/clerk/viewer.cljc b/src/nextjournal/clerk/viewer.cljc index f8d388ad7..d673922d3 100644 --- a/src/nextjournal/clerk/viewer.cljc +++ b/src/nextjournal/clerk/viewer.cljc @@ -1134,22 +1134,28 @@ (defn home? [{:keys [nav-path]}] (contains? #{"src/nextjournal/home.clj" "'nextjournal.clerk.home"} nav-path)) -(defn index? [{:as opts :keys [nav-path index]}] - (when nav-path - (or (= "'nextjournal.clerk.index" nav-path) - (= (str index) nav-path) - (re-matches #"(^|.*/)(index\.(clj|cljc|md))$" nav-path)))) +(defn route-index? + "Should the index router be enabled?" + [{:keys [expanded-paths]}] + (boolean (seq expanded-paths))) -(defn index-path [{:keys [static-build? index]}] + +(defn index? [{:as opts :keys [file index ns]}] + (or (= (some-> ns ns-name) 'nextjournal.clerk.index) + (some->> file str (re-matches #"(^|.*/)(index\.(clj|cljc|md))$")) + (and index (= file index)))) + +(defn index-path [{:as opts :keys [index]}] #?(:cljs "" - :clj (if static-build? + :clj (if (route-index? opts) "" (if (fs/exists? "index.clj") "index.clj" "'nextjournal.clerk.index")))) -(defn header [{:as opts :keys [nav-path static-build?] :git/keys [url sha]}] +(defn header [{:as opts :keys [file file-path nav-path static-build? ns] :git/keys [url sha]}] (html [:div.viewer.w-full.max-w-prose.px-8.not-prose.mt-3 [:div.mb-8.text-xs.sans-serif.text-slate-400 - (when (and (not static-build?) (not (home? opts))) + (when (and (not (route-index? opts)) + (not (home? opts))) [:<> [:a.font-medium.border-b.border-dotted.border-slate-300.hover:text-indigo-500.hover:border-indigo-500.dark:border-slate-500.dark:hover:text-white.dark:hover:border-white.transition {:href (doc-url "'nextjournal.clerk.home")} "Home"] @@ -1163,12 +1169,14 @@ (if static-build? "Generated with " "Served from ") [:a.font-medium.border-b.border-dotted.border-slate-300.hover:text-indigo-500.hover:border-indigo-500.dark:border-slate-500.dark:hover:text-white.dark:hover:border-white.transition {:href "https://clerk.vision"} "Clerk"] - " from " - (let [default-index? (str/ends-with? (str nav-path) "src/nextjournal/clerk/index.clj")] - [:a.font-medium.border-b.border-dotted.border-slate-300.hover:text-indigo-500.hover:border-indigo-500.dark:border-slate-500.dark:hover:text-white.dark:hover:border-white.transition - {:href (when (and url sha) (if default-index? (str url "/tree/" sha) (str url "/blob/" sha "/" nav-path)))} - (if (and url default-index?) #?(:clj (subs (.getPath (URL. url)) 1) :cljs url) nav-path) - (when sha [:<> "@" [:span.tabular-nums (subs sha 0 7)]])])]]])) + (let [default-index? (= 'nextjournal.clerk.index (some-> ns ns-name))] + (when (or file-path default-index?) + [:<> + " from " + [:a.font-medium.border-b.border-dotted.border-slate-300.hover:text-indigo-500.hover:border-indigo-500.dark:border-slate-500.dark:hover:text-white.dark:hover:border-white.transition + {:href (when (and url sha) (if default-index? (str url "/tree/" sha) (str url "/blob/" sha "/" file-path)))} + (if (and url default-index?) #?(:clj (subs (.getPath (URL. url)) 1) :cljs url) (or file-path nav-path)) + (when sha [:<> "@" [:span.tabular-nums (subs sha 0 7)]])]]))]]])) (def header-viewer {:name `header-viewer diff --git a/src/nextjournal/clerk/webserver.clj b/src/nextjournal/clerk/webserver.clj index 8c6000b6d..8359291b8 100644 --- a/src/nextjournal/clerk/webserver.clj +++ b/src/nextjournal/clerk/webserver.clj @@ -1,33 +1,24 @@ (ns nextjournal.clerk.webserver (:require [babashka.fs :as fs] [clojure.edn :as edn] + [clojure.java.io :as io] [clojure.pprint :as pprint] [clojure.set :as set] [clojure.string :as str] - [clojure.java.io :as io] [editscript.core :as editscript] [nextjournal.clerk.config :as config] + [nextjournal.clerk.paths :as paths] [nextjournal.clerk.view :as view] [nextjournal.clerk.viewer :as v] [org.httpkit.server :as httpkit]) (:import (java.nio.file Files))) -(defn help-hiccup [] - [:p "Call " [:span.code "nextjournal.clerk/show!"] " from your REPL" - (when-let [watch-paths (seq (:paths @@(resolve 'nextjournal.clerk/!watcher)))] - (into [:<> " or save a file in "] - (interpose " or " (map #(vector :span.code %) watch-paths)))) - " to make your notebook appear…"]) - -(defn help-doc [] - {:blocks [{:type :code - :visibility {:code :hide, :result :show} - :result {:nextjournal/value (v/html (help-hiccup))}}]}) - (defonce !clients (atom #{})) (defonce !doc (atom nil)) (defonce !last-sender-ch (atom nil)) +(defonce !server (atom nil)) + #_(view/doc->viewer @!doc) #_(reset! !doc nil) @@ -182,19 +173,22 @@ (declare present+reset!) +(defn get-build-opts [] + (paths/process-paths @!server)) + (defn ->nav-path [file-or-ns] - (cond (or (symbol? file-or-ns) (instance? clojure.lang.Namespace file-or-ns)) + (cond (or (= 'nextjournal.clerk.index file-or-ns) + (= (:index (get-build-opts)) file-or-ns)) + "" + + (or (symbol? file-or-ns) (instance? clojure.lang.Namespace file-or-ns)) (str "'" file-or-ns) (string? file-or-ns) - (when (fs/exists? file-or-ns) - (fs/unixify (cond->> (fs/strip-ext file-or-ns) - (and (fs/absolute? file-or-ns) - (not (str/starts-with? (fs/relativize (fs/cwd) file-or-ns) ".."))) - (fs/relativize (fs/cwd))))) - - :else (str file-or-ns))) + (paths/drop-extension (or (paths/path-in-cwd file-or-ns) file-or-ns)))) +#_(->nav-path (str (fs/file (fs/cwd) "notebooks/rule_30.clj"))) +#_(->nav-path 'nextjournal.clerk.index) #_(->nav-path "notebooks/rule_30.clj") #_(->nav-path 'nextjournal.clerk.home) @@ -223,13 +217,28 @@ (defn show! [opts file-or-ns] ((resolve 'nextjournal.clerk/show!) opts file-or-ns)) +(defn route-index + "A routing function" + [{:as opts :keys [index expanded-paths]} nav-path] + (if (str/blank? nav-path) + (or index + (get (set expanded-paths) (maybe-add-extension "index")) + "'nextjournal.clerk.index") + nav-path)) + +(defn maybe-route-index [opts path] + (cond->> path + (v/route-index? opts) (route-index opts))) + (defn navigate! [{:as opts :keys [nav-path]}] - (show! opts (->file-or-ns (maybe-add-extension nav-path)))) + (let [route-opts (get-build-opts)] + (show! (merge route-opts opts) (->file-or-ns (maybe-add-extension (maybe-route-index route-opts nav-path)))))) (defn prefetch-request? [req] (= "prefetch" (-> req :headers (get "purpose")))) (defn serve-notebook [{:as req :keys [uri]}] - (let [nav-path (subs uri 1)] + (let [opts (paths/process-paths @!server) + nav-path (maybe-route-index opts (subs uri 1))] (cond (prefetch-request? req) {:status 404} @@ -240,7 +249,9 @@ (->nav-path 'nextjournal.clerk.home))}} :else (if-let [file-or-ns (->file-or-ns (maybe-add-extension nav-path))] - (do (try (show! {:skip-history? true} file-or-ns) + (do (try (show! (merge {:skip-history? true} + (select-keys opts [:expanded-paths :index :git/sha :git/url])) + file-or-ns) (catch Exception _)) {:status 200 :headers {"Content-Type" "text/html" "Cache-Control" "no-store"} @@ -285,7 +296,7 @@ (broadcast-status! status))) (defn set-status! [status] - (swap! !doc (fn [doc] (-> (or doc (help-doc)) + (swap! !doc (fn [doc] (-> (or doc {}) (vary-meta assoc :status status) (vary-meta update ::!send-status-future broadcast-status-debounced! status))))) @@ -295,8 +306,6 @@ ;; * load notebook without results ;; * allow page reload -(defonce !server (atom nil)) - (defn halt! [] (when-let [{:keys [port instance]} @!server] @(httpkit/server-stop! instance) @@ -305,10 +314,10 @@ #_(halt!) -(defn serve! [{:keys [host port] :or {host "localhost" port 7777}}] +(defn serve! [{:as opts :keys [host port] :or {host "localhost" port 7777}}] (halt!) (try - (reset! !server {:host host :port port :instance (httpkit/run-server #'app {:ip host :port port :legacy-return-value? false})}) + (reset! !server (assoc opts :instance (httpkit/run-server #'app {:ip host :port port :legacy-return-value? false}))) (println (format "Clerk webserver started on http://%s:%s ..." host port )) (catch java.net.BindException e (let [msg (format "Clerk webserver could not be started because port %d is not available. Stop what's running on port %d or specify a different port." port port)] @@ -317,4 +326,7 @@ (throw (ex-info msg {:port port} e)))))) #_(serve! {:port 7777}) +#_(serve! {:port 7777 :paths ["notebooks/rule_30.clj"]}) +#_(serve! {:port 7777 :paths ["notebooks/rule_30.clj" "book.clj"]}) +#_(serve! {:port 7777 :paths ["notebooks/rule_30.clj" "notebooks/links.md" "notebooks/markdown.md" "index.clj"]}) #_(serve! {:port 7777 :host "0.0.0.0"}) diff --git a/test/nextjournal/clerk/builder_test.clj b/test/nextjournal/clerk/builder_test.clj index a86633bae..baa6d9c65 100644 --- a/test/nextjournal/clerk/builder_test.clj +++ b/test/nextjournal/clerk/builder_test.clj @@ -30,60 +30,6 @@ (testing "*ns* isn't changed (#506)" (is (= original-*ns* *ns*)))))) -(def test-paths ["boo*.clj"]) -(def test-paths-fn (fn [] ["boo*.clj"])) - -(deftest expand-paths - (testing "expands glob patterns" - (let [{paths :expanded-paths} (builder/expand-paths {:paths ["notebooks/*clj"]})] - (is (> (count paths) 25)) - (is (every? #(str/ends-with? % ".clj") paths)))) - - (testing "supports index" - (is (= {:expanded-paths ["book.clj"]} - (builder/expand-paths {:index "book.clj"})))) - - (testing "supports paths" - (is (= {:expanded-paths ["book.clj"]} - (builder/expand-paths {:paths ["book.clj"]})))) - - (testing "supports paths-fn" - (is (= {:expanded-paths ["book.clj"]} - (builder/expand-paths {:paths-fn `test-paths}))) - (is (= {:expanded-paths ["book.clj"]} - (builder/expand-paths {:paths-fn `test-paths-fn})))) - - (testing "deduplicates index + paths" - (is (= {:expanded-paths [(str (fs/file "notebooks" "rule_30.clj"))]} - (builder/expand-paths {:paths ["notebooks/rule_**.clj"] - :index (str (fs/file "notebooks" "rule_30.clj"))})))) - - (testing "supports absolute paths (#504)" - (is (= {:expanded-paths [(str (fs/file (fs/cwd) "book.clj"))]} - (builder/expand-paths {:paths [(str (fs/file (fs/cwd) "book.clj"))]})))) - - (testing "invalid args" - (is (match? {:error #"must set either"} - (builder/expand-paths {}))) - (is (match? {:error #"must be a qualified symbol pointing at an existing var"} - (builder/expand-paths {:paths-fn :foo}))) - (is (match? {:error #"must be a qualified symbol pointing at an existing var"} - (builder/expand-paths {:paths-fn 'foo}))) - (is (match? {:error #"must be a qualified symbol pointing at an existing var"} - (builder/expand-paths {:paths-fn 'clerk.test.non-existant-name-space/bar}))) - (is (match? {:error #"must be a qualified symbol pointing at an existing var"} - (builder/expand-paths {:paths-fn 'clojure.core/non-existant-var}))) - (is (match? {:error #"must be a qualified symbol pointing at an existing var"} - (builder/expand-paths {:paths-fn "hi"}))) - (is (match? {:error #"nothing to build"} - (builder/expand-paths {:paths []}))) - (is (match? {:error #"An error occured invoking"} - (builder/expand-paths {:paths-fn 'clojure.core/inc}))) - (is (match? {:error #"must compute to a sequential value."} - (builder/expand-paths {:paths-fn 'clojure.core/+}))) - (is (match? {:error "`:index` must be either an instance of java.net.URL or a string and point to an existing file"} - (builder/expand-paths {:index ["book.clj"]}))))) - (deftest build-static-app! (testing "error when paths are empty (issue #339)" (is (thrown-with-msg? ExceptionInfo #"nothing to build" (builder/build-static-app! {:paths []})))) diff --git a/test/nextjournal/clerk/paths_test.clj b/test/nextjournal/clerk/paths_test.clj new file mode 100644 index 000000000..466fa27de --- /dev/null +++ b/test/nextjournal/clerk/paths_test.clj @@ -0,0 +1,60 @@ +(ns nextjournal.clerk.paths-test + (:require [babashka.fs :as fs] + [clojure.string :as str] + [clojure.test :refer [deftest is testing]] + [matcher-combinators.test] + [nextjournal.clerk.paths :as paths])) + +(def test-paths ["boo*.clj"]) +(def test-paths-fn (fn [] ["boo*.clj"])) + +(deftest expand-paths + (testing "expands glob patterns" + (let [{paths :expanded-paths} (paths/expand-paths {:paths ["notebooks/*clj"]})] + (is (> (count paths) 25)) + (is (every? #(str/ends-with? % ".clj") paths)))) + + (testing "supports index" + (is (= ["book.clj"] + (:expanded-paths (paths/expand-paths {:index "book.clj"}))))) + + (testing "supports paths" + (is (= ["book.clj"] + (:expanded-paths (paths/expand-paths {:paths ["book.clj"]}))))) + + (testing "supports paths-fn" + (is (= ["book.clj"] + (:expanded-paths (paths/expand-paths {:paths-fn `test-paths})))) + (is (= ["book.clj"] + (:expanded-paths (paths/expand-paths {:paths-fn `test-paths-fn}))))) + + (testing "deduplicates index + paths" + (is (= [(str (fs/file "notebooks" "rule_30.clj"))] + (:expanded-paths (paths/expand-paths {:paths ["notebooks/rule_**.clj"] + :index (str (fs/file "notebooks" "rule_30.clj"))}))))) + + (testing "supports absolute paths (#504)" + (is (= [(str (fs/file (fs/cwd) "book.clj"))] + (:expanded-paths (paths/expand-paths {:paths [(str (fs/file (fs/cwd) "book.clj"))]}))))) + + (testing "invalid args" + (is (match? {:error #"must set either"} + (paths/expand-paths {}))) + (is (match? {:error #"must be a qualified symbol pointing at an existing var"} + (paths/expand-paths {:paths-fn :foo}))) + (is (match? {:error #"must be a qualified symbol pointing at an existing var"} + (paths/expand-paths {:paths-fn 'foo}))) + (is (match? {:error #"must be a qualified symbol pointing at an existing var"} + (paths/expand-paths {:paths-fn 'clerk.test.non-existant-name-space/bar}))) + (is (match? {:error #"must be a qualified symbol pointing at an existing var"} + (paths/expand-paths {:paths-fn 'clojure.core/non-existant-var}))) + (is (match? {:error #"must be a qualified symbol pointing at an existing var"} + (paths/expand-paths {:paths-fn "hi"}))) + (is (match? {:error #"nothing to build"} + (paths/expand-paths {:paths []}))) + (is (match? {:error #"An error occured invoking"} + (paths/expand-paths {:paths-fn 'clojure.core/inc}))) + (is (match? {:error #"must compute to a sequential value."} + (paths/expand-paths {:paths-fn 'clojure.core/+}))) + (is (match? {:error "`:index` must be either an instance of java.net.URL or a string and point to an existing file"} + (paths/expand-paths {:index ["book.clj"]}))))) From 78d2ffa7a1eff8439838696d8a131bbfaf805867 Mon Sep 17 00:00:00 2001 From: Leah Neukirchen Date: Wed, 25 Oct 2023 15:40:35 +0200 Subject: [PATCH 16/26] git: use remote-url of current branch instead of guessing (#566) Previously, the first remote that followed a github-style URL schema was used; now we use the actual remote of the branch. --- src/nextjournal/clerk/git.clj | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/nextjournal/clerk/git.clj b/src/nextjournal/clerk/git.clj index 78984a6c7..a4b5f18a9 100644 --- a/src/nextjournal/clerk/git.clj +++ b/src/nextjournal/clerk/git.clj @@ -5,8 +5,8 @@ (defn ^:private shell-out-str "Shell helper, calls a cmd and returns it output string trimmed." - [cmd] - (str/trim (:out (p/shell {:out :string} cmd)))) + [& cmd] + (str/trim (:out (apply p/shell {:out :string} cmd)))) #_(shell-out-str "git rev-parse HEAD") #_(shell-out-str "zonk") @@ -24,17 +24,19 @@ (str/replace remote-url #"\.git$" "") (->github-project remote-url) - (str "https://github.com/%s" (->github-project remote-url)))) + (str "https://github.com/" (->github-project remote-url)))) #_(->https-git-url "https://github.com/nextjournal/clerk.git") #_(->https-git-url "git@github.com:nextjournal/clerk.git") (defn read-git-attrs [] (try {:git/sha (shell-out-str "git rev-parse HEAD") - :git/url (some ->https-git-url - (map #(shell-out-str (str "git remote get-url " %)) - (str/split-lines (shell-out-str "git remote"))))} - (catch Exception _ + :git/url (let [branch (shell-out-str "git symbolic-ref --short HEAD") + remote (shell-out-str "git" "config" (str "branch." branch ".remote")) + remote-url (shell-out-str "git" "remote" "get-url" remote)] + (->https-git-url remote-url))} + (catch Exception e + (prn e) {}))) #_(read-git-attrs) From 0fc5203587a8db92bd58560920eea34509768232 Mon Sep 17 00:00:00 2001 From: Andrea Amantini Date: Mon, 30 Oct 2023 15:46:04 +0100 Subject: [PATCH 17/26] Bump nextjournal.markdown with fixed multi-threaded access --- deps.edn | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deps.edn b/deps.edn index 5973eda1c..ae365279b 100644 --- a/deps.edn +++ b/deps.edn @@ -9,7 +9,7 @@ com.nextjournal/beholder {:mvn/version "1.0.2"} org.flatland/ordered {:mvn/version "1.15.11"} - io.github.nextjournal/markdown {:mvn/version "0.5.144"} + io.github.nextjournal/markdown {:mvn/version "0.5.146"} babashka/process {:mvn/version "0.4.16"} io.github.nextjournal/dejavu {:git/sha "4980e0cc18c9b09fb220874ace94ba6b57a749ca"} From b95bcd799567755ebceb753b2828a99e5fad9340 Mon Sep 17 00:00:00 2001 From: Martin Kavalar Date: Sat, 4 Nov 2023 14:36:24 +0000 Subject: [PATCH 18/26] Fix `row` and `col` viewers not showing a first map arg, fixes #567 --- CHANGELOG.md | 3 ++- src/nextjournal/clerk/viewer.cljc | 4 +++- test/nextjournal/clerk/viewer_test.clj | 9 +++++++++ 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d040635c5..0a8ca9b71 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,8 @@ Changes can be: ## Unreleased -... +* 💫 Allow to disable welcome page in `serve!` +* 🐞 Fix `row` and `col` viewers not showing a first map argument, fixes [#567](https://github.com/nextjournal/clerk/issues/567) @teodorlu ## 0.15.957 (2023-09-28) diff --git a/src/nextjournal/clerk/viewer.cljc b/src/nextjournal/clerk/viewer.cljc index d673922d3..88ee5e0f3 100644 --- a/src/nextjournal/clerk/viewer.cljc +++ b/src/nextjournal/clerk/viewer.cljc @@ -213,7 +213,9 @@ (defn with-viewer-extracting-opts [viewer & opts+items] ;; TODO: maybe support sequantial & viewer-opts? (cond - (and (map? (first opts+items)) (not (wrapped-value? (first opts+items)))) + (and (map? (first opts+items)) + (not (wrapped-value? (first opts+items))) + (seq (set/intersection parser/block-settings (set (keys (first opts+items))) ))) (with-viewer viewer (first opts+items) (rest opts+items)) (and (sequential? (first opts+items)) (= 1 (count opts+items))) diff --git a/test/nextjournal/clerk/viewer_test.clj b/test/nextjournal/clerk/viewer_test.clj index ca9cea512..f13f079e1 100644 --- a/test/nextjournal/clerk/viewer_test.clj +++ b/test/nextjournal/clerk/viewer_test.clj @@ -398,3 +398,12 @@ (-> (eval/eval-string "(ns test.removed-metadata\n(:require [nextjournal.clerk :as c]))\n\n^::c/no-cache (do 'this)") view/doc->viewer v/->value :blocks second v/->value)))) + +(deftest col-viewer-map-args + (testing "extracts first arg as viewer-opts" + (is (= [{:foo :bar}] + (v/->value (v/col {:nextjournal.clerk/width :wide} {:foo :bar}))))) + + (testing "doesn't treat plain map as viewer-opts" + (is (= [{:foo :bar} {:bar :baz}] + (v/->value (v/col {:foo :bar} {:bar :baz})))))) From c3eedb6d7cf3768971fbf0b58993be6096653f66 Mon Sep 17 00:00:00 2001 From: Martin Kavalar Date: Sat, 4 Nov 2023 16:00:00 +0000 Subject: [PATCH 19/26] Fix caching behavior of `clerk/image`, support override --- CHANGELOG.md | 1 + src/nextjournal/clerk/viewer.cljc | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0a8ca9b71..c2e6ea95a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ Changes can be: ## Unreleased * 💫 Allow to disable welcome page in `serve!` +* 🐞 Fix caching behaviour of `clerk/image` and support overriding image-viewer by name * 🐞 Fix `row` and `col` viewers not showing a first map argument, fixes [#567](https://github.com/nextjournal/clerk/issues/567) @teodorlu ## 0.15.957 (2023-09-28) diff --git a/src/nextjournal/clerk/viewer.cljc b/src/nextjournal/clerk/viewer.cljc index 88ee5e0f3..31fa1b0e6 100644 --- a/src/nextjournal/clerk/viewer.cljc +++ b/src/nextjournal/clerk/viewer.cljc @@ -1771,7 +1771,7 @@ (defn image ([image-or-url] (image {} image-or-url)) ([viewer-opts image-or-url] - (with-viewer image-viewer viewer-opts + (with-viewer (:name image-viewer) viewer-opts #?(:cljs image-or-url :clj (read-image image-or-url))))) (defn caption [text content] From d1a3e57467ab9f8a23d3f5613045b42badff4760 Mon Sep 17 00:00:00 2001 From: Martin Kavalar Date: Sun, 5 Nov 2023 16:25:14 +0100 Subject: [PATCH 20/26] Bump nippy to 3.3 (#569) --- CHANGELOG.md | 7 +++++++ deps.edn | 2 +- test/nextjournal/clerk/analyzer_test.clj | 6 +++--- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c2e6ea95a..74b5cb7af 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,14 @@ Changes can be: ## Unreleased * 💫 Allow to disable welcome page in `serve!` + +* 🛠 Bump depdendencies + + * `com.taoensso/nippy` to `3.3` + * `io.github.nextjournal/markdown` to `0.5.146` + * 🐞 Fix caching behaviour of `clerk/image` and support overriding image-viewer by name + * 🐞 Fix `row` and `col` viewers not showing a first map argument, fixes [#567](https://github.com/nextjournal/clerk/issues/567) @teodorlu ## 0.15.957 (2023-09-28) diff --git a/deps.edn b/deps.edn index ae365279b..81de08c90 100644 --- a/deps.edn +++ b/deps.edn @@ -13,7 +13,7 @@ babashka/process {:mvn/version "0.4.16"} io.github.nextjournal/dejavu {:git/sha "4980e0cc18c9b09fb220874ace94ba6b57a749ca"} - com.taoensso/nippy {:mvn/version "3.2.0"} + com.taoensso/nippy {:mvn/version "3.3.0"} mvxcvi/multiformats {:mvn/version "0.3.107"} com.pngencoder/pngencoder {:mvn/version "0.13.1"} diff --git a/test/nextjournal/clerk/analyzer_test.clj b/test/nextjournal/clerk/analyzer_test.clj index b762f34ca..ebb23879b 100644 --- a/test/nextjournal/clerk/analyzer_test.clj +++ b/test/nextjournal/clerk/analyzer_test.clj @@ -255,11 +255,11 @@ (deftest add-block-ids (testing "assigns block ids" - (is (= '[foo/anon-expr-5dtWXL41Ee4Yz8oTFQbUqiXhcj3prd + (is (= '[foo/anon-expr-5drWeEoPhxNKWWwpUt2dwAnqZuPwbA foo/bar foo/bar#2 - foo/anon-expr-5dqqrgB1pptKQdEw2L1k2mHBKHRw4P - foo/anon-expr-5dqqrgB1pptKQdEw2L1k2mHBKHRw4P#2] + foo/anon-expr-5dqrDAxc79ReQDs9Q6RVbPs3XirbAk + foo/anon-expr-5dqrDAxc79ReQDs9Q6RVbPs3XirbAk#2] (->> "(ns foo {:nextjournal.clerk/visibility {:code :fold}}) (def bar :baz) (def bar :baz) (rand-int 42) (rand-int 42)" analyze-string :blocks (mapv :id)))))) From 6d3451829cba8ca20f43f43cdb87c064b23c554e Mon Sep 17 00:00:00 2001 From: Martin Kavalar Date: Sun, 5 Nov 2023 20:52:38 +0100 Subject: [PATCH 21/26] Bump nippy for better freezable? impl (#570) See https://github.com/taoensso/nippy/commit/fb6f75e4d7db2b567fdf3a8ffa841e290eb9e924 --- CHANGELOG.md | 2 +- deps.edn | 2 +- src/nextjournal/clerk/eval.clj | 15 ++++++++------- test/nextjournal/clerk/eval_test.clj | 17 +++++++++-------- 4 files changed, 19 insertions(+), 17 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 74b5cb7af..ce23cce9b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,7 +12,7 @@ Changes can be: * 🛠 Bump depdendencies - * `com.taoensso/nippy` to `3.3` + * `com.taoensso/nippy` to `3.4.0-beta1` * `io.github.nextjournal/markdown` to `0.5.146` * 🐞 Fix caching behaviour of `clerk/image` and support overriding image-viewer by name diff --git a/deps.edn b/deps.edn index 81de08c90..034ae0c55 100644 --- a/deps.edn +++ b/deps.edn @@ -13,7 +13,7 @@ babashka/process {:mvn/version "0.4.16"} io.github.nextjournal/dejavu {:git/sha "4980e0cc18c9b09fb220874ace94ba6b57a749ca"} - com.taoensso/nippy {:mvn/version "3.3.0"} + com.taoensso/nippy {:mvn/version "3.4.0-beta1"} mvxcvi/multiformats {:mvn/version "0.3.107"} com.pngencoder/pngencoder {:mvn/version "0.13.1"} diff --git a/src/nextjournal/clerk/eval.clj b/src/nextjournal/clerk/eval.clj index a7d34fc4a..c021a5441 100644 --- a/src/nextjournal/clerk/eval.clj +++ b/src/nextjournal/clerk/eval.clj @@ -25,7 +25,6 @@ #_(-> [(clojure.java.io/file "notebooks") (find-ns 'user)] nippy/freeze nippy/thaw) - (defn ->cache-file [hash] (str config/cache-dir fs/file-separator hash)) @@ -87,7 +86,7 @@ hash))) -(defn ^:private cachable-value? [value] +(defn cachable? [value] (and (some? value) (try (and (not (analyzer/exceeds-bounded-count-limit? value)) @@ -97,10 +96,12 @@ (catch Exception _ false)))) -#_(cachable-value? (vec (range 100))) -#_(cachable-value? (range)) -#_(cachable-value? (map inc (range))) -#_(cachable-value? [{:hello (map inc (range))}]) +#_(cachable? (vec (range 100))) +#_(cachable? (range)) +#_(cachable? java.lang.String) +#_(cachable? (map inc (range))) +#_(cachable? [{:hello (map inc (range))}]) +#_(cachable? {:foo (javax.imageio.ImageIO/read (clojure.java.io/file "trees.png"))}) (defn ^:private cache! [digest-file var-value] @@ -138,7 +139,7 @@ (when (and (not no-cache?) (not ns-effect?) freezable? - (cachable-value? var-value) + (cachable? var-value) (or (not var) var-from-def?)) (cache! digest-file var-value)) (let [blob-id (cond no-cache? (analyzer/->hash-str var-value) diff --git a/test/nextjournal/clerk/eval_test.clj b/test/nextjournal/clerk/eval_test.clj index a27c7233e..8d157db21 100644 --- a/test/nextjournal/clerk/eval_test.clj +++ b/test/nextjournal/clerk/eval_test.clj @@ -1,5 +1,6 @@ (ns nextjournal.clerk.eval-test - (:require [clojure.string :as str] + (:require [clojure.java.io :as io] + [clojure.string :as str] [clojure.test :refer [deftest is testing]] [matcher-combinators.test :refer [match?]] [nextjournal.clerk :as clerk] @@ -219,15 +220,15 @@ (deftest cacheable-value?-test (testing "finite sequence is cacheable" - (is (#'eval/cachable-value? (vec (range 100))))) - (testing "nippy doesn't know how to freeze instances of clojure.lang.Iterate" - (is (not (#'eval/cachable-value? (range 100))))) + (is (eval/cachable? (vec (range 100))))) (testing "infinite sequences can't be cached" - (is (not (#'eval/cachable-value? (range)))) - (is (not (#'eval/cachable-value? (map inc (range)))))) + (is (not (eval/cachable? (range)))) + (is (not (eval/cachable? (map inc (range)))))) (testing "class is not cachable" - (is (not (#'eval/cachable-value? java.lang.String))) - (is (not (#'eval/cachable-value? {:foo java.lang.String}))))) + (is (not (eval/cachable? java.lang.String))) + (is (not (eval/cachable? {:foo java.lang.String})))) + (testing "image is cachable" + (is (eval/cachable? (javax.imageio.ImageIO/read (io/file "trees.png")))))) (deftest show!-test (testing "in-memory cache is preserved when exception is thrown (#549)" From 287bd833fec5734e127741c3c6577e0abe576a59 Mon Sep 17 00:00:00 2001 From: Martin Kavalar Date: Tue, 7 Nov 2023 17:43:34 +0100 Subject: [PATCH 22/26] Don't serve forbidden paths when `:paths` are closed (#572) --- src/nextjournal/clerk/paths.clj | 5 +++-- src/nextjournal/clerk/webserver.clj | 34 +++++++++++++++++++---------- 2 files changed, 26 insertions(+), 13 deletions(-) diff --git a/src/nextjournal/clerk/paths.clj b/src/nextjournal/clerk/paths.clj index d877b0a34..63e8ef2b9 100644 --- a/src/nextjournal/clerk/paths.clj +++ b/src/nextjournal/clerk/paths.clj @@ -21,7 +21,7 @@ (or (not (string? index)) (not (fs/exists? index)))) {:error "`:index` must be either an instance of java.net.URL or a string and point to an existing file" :index index} - (cond-> opts + (cond-> (merge (select-keys build-opts [:paths :paths-fn :index]) opts) (and index (not (contains? (set expanded-paths) index))) (update :expanded-paths conj index))))) @@ -86,7 +86,7 @@ (ensure-not-empty build-opts))))) #_(expand-paths {:paths ["notebooks/di*.clj"] :index "src/nextjournal/clerk/index.clj"}) -#_(expand-paths {:paths ['notebooks/rule_30.clj]}) +#_(expand-paths {:paths ['notebooks/rule_30.clj] :index "notebooks/markdown.md"}) #_(expand-paths {:index "book.clj"}) #_(expand-paths {:paths-fn `nextjournal.clerk.builder/clerk-docs}) #_(expand-paths {:paths-fn `clerk-docs-2}) @@ -129,6 +129,7 @@ (git/read-git-attrs))) #_(process-paths {:paths ["notebooks/rule_30.clj"]}) +#_(process-paths {:paths ["notebooks/rule_30.clj"] :index "notebooks/links.md"}) #_(process-paths {:paths ["notebooks/no_rule_30.clj"]}) #_(v/route-index? (process-paths @!server)) #_(route-index (process-paths @!server) "") diff --git a/src/nextjournal/clerk/webserver.clj b/src/nextjournal/clerk/webserver.clj index 8359291b8..2a8b527cc 100644 --- a/src/nextjournal/clerk/webserver.clj +++ b/src/nextjournal/clerk/webserver.clj @@ -176,6 +176,8 @@ (defn get-build-opts [] (paths/process-paths @!server)) +#_(get-build-opts) + (defn ->nav-path [file-or-ns] (cond (or (= 'nextjournal.clerk.index file-or-ns) (= (:index (get-build-opts)) file-or-ns)) @@ -214,8 +216,14 @@ (str/starts-with? nav-path "'") (symbol (subs nav-path 1)) (re-find #"\.(cljc?|md)$" nav-path) nav-path)) +(defn forbidden-path? [file-or-ns] + (if-let [expanded-paths (:expanded-paths (get-build-opts))] + (not (contains? (conj (set expanded-paths) 'nextjournal.clerk.index) file-or-ns)) + false)) + (defn show! [opts file-or-ns] - ((resolve 'nextjournal.clerk/show!) opts file-or-ns)) + (when-not (forbidden-path? file-or-ns) + ((resolve 'nextjournal.clerk/show!) opts file-or-ns))) (defn route-index "A routing function" @@ -248,16 +256,19 @@ :headers {"Location" (or (:nav-path @!doc) (->nav-path 'nextjournal.clerk.home))}} :else - (if-let [file-or-ns (->file-or-ns (maybe-add-extension nav-path))] - (do (try (show! (merge {:skip-history? true} - (select-keys opts [:expanded-paths :index :git/sha :git/url])) - file-or-ns) - (catch Exception _)) - {:status 200 - :headers {"Content-Type" "text/html" "Cache-Control" "no-store"} - :body (view/->html {:doc (view/doc->viewer @!doc) - :resource->url @config/!resource->url - :conn-ws? true})}) + (if-let [file-or-ns (let [file-or-ns (->file-or-ns (maybe-add-extension nav-path))] + (when-not (forbidden-path? file-or-ns) + file-or-ns))] + (do + (try (show! (merge {:skip-history? true} + (select-keys opts [:expanded-paths :index :git/sha :git/url])) + file-or-ns) + (catch Exception _)) + {:status 200 + :headers {"Content-Type" "text/html" "Cache-Control" "no-store"} + :body (view/->html {:doc (view/doc->viewer @!doc) + :resource->url @config/!resource->url + :conn-ws? true})}) {:status 404 :headers {"Content-Type" "text/plain"} :body (format "Could not find notebook at %s." (pr-str nav-path))})))) @@ -327,6 +338,7 @@ #_(serve! {:port 7777}) #_(serve! {:port 7777 :paths ["notebooks/rule_30.clj"]}) +#_(serve! {:port 7777 :paths ["notebooks/rule_30.clj"] :index "notebooks/links.md"}) #_(serve! {:port 7777 :paths ["notebooks/rule_30.clj" "book.clj"]}) #_(serve! {:port 7777 :paths ["notebooks/rule_30.clj" "notebooks/links.md" "notebooks/markdown.md" "index.clj"]}) #_(serve! {:port 7777 :host "0.0.0.0"}) From fe69278ff66e1cade156cba09ac01518dfb46464 Mon Sep 17 00:00:00 2001 From: Martin Kavalar Date: Tue, 7 Nov 2023 20:27:46 +0100 Subject: [PATCH 23/26] Fix serving 404 page for non-existent paths --- src/nextjournal/clerk/git.clj | 4 ++-- src/nextjournal/clerk/webserver.clj | 5 ++++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/nextjournal/clerk/git.clj b/src/nextjournal/clerk/git.clj index a4b5f18a9..e9b7d79da 100644 --- a/src/nextjournal/clerk/git.clj +++ b/src/nextjournal/clerk/git.clj @@ -34,9 +34,9 @@ :git/url (let [branch (shell-out-str "git symbolic-ref --short HEAD") remote (shell-out-str "git" "config" (str "branch." branch ".remote")) remote-url (shell-out-str "git" "remote" "get-url" remote)] - (->https-git-url remote-url))} + (->https-git-url remote-url))} (catch Exception e - (prn e) + #_(prn e) {}))) #_(read-git-attrs) diff --git a/src/nextjournal/clerk/webserver.clj b/src/nextjournal/clerk/webserver.clj index 2a8b527cc..c7608cae7 100644 --- a/src/nextjournal/clerk/webserver.clj +++ b/src/nextjournal/clerk/webserver.clj @@ -203,11 +203,13 @@ (and (fs/exists? nav-path) (not (fs/directory? nav-path))))) nav-path - (find-first-existing-file (map #(str (fs/file nav-path) "." %) ["md" "clj" "cljc"])))) + (or (find-first-existing-file (map #(str (fs/file nav-path) "." %) ["md" "clj" "cljc"])) + nav-path))) #_(maybe-add-extension "notebooks/rule_30") #_(maybe-add-extension "notebooks/rule_30.clj") #_(maybe-add-extension "notebooks/markdown") +#_(maybe-add-extension "asdf") #_(maybe-add-extension "'nextjournal.clerk.home") (defn ->file-or-ns [nav-path] @@ -338,6 +340,7 @@ #_(serve! {:port 7777}) #_(serve! {:port 7777 :paths ["notebooks/rule_30.clj"]}) +#_(serve! {:port 7777 :paths ["notebooks/rule_30.clj" "notebooks/links.md"]}) #_(serve! {:port 7777 :paths ["notebooks/rule_30.clj"] :index "notebooks/links.md"}) #_(serve! {:port 7777 :paths ["notebooks/rule_30.clj" "book.clj"]}) #_(serve! {:port 7777 :paths ["notebooks/rule_30.clj" "notebooks/links.md" "notebooks/markdown.md" "index.clj"]}) From 8b804e7c40f6e61cf65218e7d017e5864c559400 Mon Sep 17 00:00:00 2001 From: Philippa Markovics Date: Thu, 9 Nov 2023 16:14:51 +0100 Subject: [PATCH 24/26] Fix overlapping sidenotes (#573) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Sidenotes that are longer than the content they are anchored to will now push their subsequent content down instead of overlapping with it. Note: This uses `display: flex` now instead of positioning the sidenotes absolutely, thus preventing margin collapse with subsequent nodes that don’t have sidenotes. The layout adjusts for this now. Closes #564. --- CHANGELOG.md | 2 ++ notebooks/viewers/markdown.clj | 18 ++++++++++++++++-- resources/stylesheets/viewer.css | 22 +++++++++++++++++++--- src/nextjournal/clerk/viewer.cljc | 30 +++++++++++++++++++++--------- 4 files changed, 58 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ce23cce9b..2869c69ca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,8 @@ Changes can be: * 🐞 Fix `row` and `col` viewers not showing a first map argument, fixes [#567](https://github.com/nextjournal/clerk/issues/567) @teodorlu +* 🐞 Fix long sidenotes overlapping with subsequent content, fixes [#564](https://github.com/nextjournal/clerk/issues/564) @hlship + ## 0.15.957 (2023-09-28) * 🔌 Offline support diff --git a/notebooks/viewers/markdown.clj b/notebooks/viewers/markdown.clj index db7d10bbc..5f90db964 100644 --- a/notebooks/viewers/markdown.clj +++ b/notebooks/viewers/markdown.clj @@ -78,9 +78,11 @@ It's [Markdown](https://daringfireball.net/projects/markdown/), like you know it ;; on the [Tufte CSS website](https://edwardtufte.github.io/tufte-css/). ;; ;; Sidenotes are a great example of the web not being like print. On sufficiently -;; large viewports, Tufte CSS uses the margin for sidenotes, margin notes, and +;; large viewports, Tufte CSS^[Cascading Style Sheets] uses the margin for sidenotes, margin notes, and ;; small figures. On smaller viewports, elements that would go in the margin are -;; hidden until the user toggles them into view. The goal is to present related +;; hidden until the user toggles them into view. +;; +;; The goal is to present related ;; but not necessary information such as asides or citations as close as possible ;; to the text that references them. At the same time, this secondary information ;; should stay out of the way of the eye, not interfering with the progression of @@ -98,6 +100,18 @@ It's [Markdown](https://daringfireball.net/projects/markdown/), like you know it ;; And it can be followed by lists so the list layout also has to adapt to the ;; new content width once a sidenote is present in the document: ;; +;; ### Long sidenotes +;; +;; Sometimes sidenotes can be longer than the content they are anchored to. [^long] +;; +;; [^long]: In this case they should push their proximate content down instead of +;; overlapping with it. This would be especially glaring when the subsequent element +;; is a code cell, which spans both the main contentn col and sidenote col. + +;; ```clj +;; (comment "Here is a Clojure code listing to be pushed down by the sidenote.") +;; ``` + ;; Things to do: ;; * Hire two private investigators. Get them to follow each other. diff --git a/resources/stylesheets/viewer.css b/resources/stylesheets/viewer.css index 8c8046f27..f4b8a3eb7 100644 --- a/resources/stylesheets/viewer.css +++ b/resources/stylesheets/viewer.css @@ -14,7 +14,7 @@ .font-condensed { font-family: "Fira Sans Condensed", sans-serif; } .font-inter { font-family: "Inter", sans-serif; } body { - @apply font-serif antialiased text-gray-900 sm:overscroll-y-none; + @apply font-serif antialiased text-gray-900 sm:overscroll-y-none pb-8; } code, .code { @apply font-mono text-sm text-gray-900 bg-slate-50 px-0.5 py-px rounded dark:bg-gray-800; @@ -320,10 +320,20 @@ .sidenote-container { @apply mb-4; } +.sidenote-main-col *:last-child { + @apply mb-0; +} +.sidenote-main-col { + @apply w-full; +} +.sidenote-main-col > ul > li:first-child, +.sidenote-main-col > ol > li:first-child { + @apply mt-0; +} @media (min-width: 860px) { .sidenote sup { @apply inline; } .sidenote-column { - @apply w-[165px] absolute right-0 top-0 -mr-[205px]; + @apply w-[165px] -mr-[205px] flex-shrink-0; } .sidenote { @apply bg-transparent dark:bg-transparent p-0; @@ -335,7 +345,13 @@ @apply pr-[241px]; } .sidenote-container { - @apply relative mb-0; + @apply relative flex justify-between; + } + .sidenote-container > *:not(.sidenote-column) { + margin-right: 40px; + } + .sidenote:last-of-type { + } .sidenotes-layout h1 { @apply w-[756px] !important; diff --git a/src/nextjournal/clerk/viewer.cljc b/src/nextjournal/clerk/viewer.cljc index 31fa1b0e6..c75ff9521 100644 --- a/src/nextjournal/clerk/viewer.cljc +++ b/src/nextjournal/clerk/viewer.cljc @@ -333,6 +333,15 @@ (throw (ex-info "no type given for with-md-viewer" {:wrapped-value wrapped-value}))) (with-viewer (keyword "nextjournal.markdown" (name type)) wrapped-value))) +(defn apply-viewers-to-md [viewers doc x] + (-> (ensure-wrapped-with-viewers viewers (assoc x ::doc doc)) + (with-md-viewer) + (apply-viewers) + (as-> w + (if (= `markdown-node-viewer (:name (->viewer w))) + (->value w) + [(inspect-fn) (process-wrapped-value w)])))) + (defn into-markup [markup] (fn [{:as wrapped-value :nextjournal/keys [viewers render-opts]}] (-> (with-viewer {:name `markdown-node-viewer :render-fn 'identity} wrapped-value) @@ -341,14 +350,7 @@ (fn [{:as node :keys [text content] ::keys [doc]}] (into (cond-> markup (fn? markup) (apply [(merge render-opts node)])) (cond text [text] - content (mapv #(-> (ensure-wrapped-with-viewers viewers (assoc % ::doc doc)) - (with-md-viewer) - (apply-viewers) - (as-> w - (if (= `markdown-node-viewer (:name (->viewer w))) - (->value w) - [(inspect-fn) (process-wrapped-value w)]))) - content)))))))) + content (mapv (partial apply-viewers-to-md viewers doc) content)))))))) ;; A hack for making Clerk not fail in the precense of ;; programmatically generated keywords or symbols that cannot be read. @@ -780,7 +782,17 @@ {:name :nextjournal.markdown/toc :transform-fn (into-markup [:div.toc])} ;; sidenotes - {:name :nextjournal.markdown/sidenote-container :transform-fn (into-markup [:div.sidenote-container])} + {:name :nextjournal.markdown/sidenote-container + :transform-fn (fn [{:as wrapped-value :nextjournal/keys [viewers render-opts]}] + (-> (with-viewer {:name `markdown-node-viewer :render-fn 'identity} wrapped-value) + mark-presented + (update :nextjournal/value + (fn [{:as node :keys [text content] ::keys [doc]}] + [:div.sidenote-container + (into [:div.sidenote-main-col] + (map (partial apply-viewers-to-md viewers doc)) + (drop-last content)) + (apply-viewers-to-md viewers doc (last content))]))))} {:name :nextjournal.markdown/sidenote-column :transform-fn (into-markup [:div.sidenote-column])} {:name :nextjournal.markdown/sidenote :transform-fn (into-markup (fn [{:keys [ref]}] From b4e201fff6a5fd51d1eec87405c1a61517077b57 Mon Sep 17 00:00:00 2001 From: Philippa Markovics Date: Sat, 11 Nov 2023 12:55:09 +0100 Subject: [PATCH 25/26] Markdown Captions & Book Update: clerk/image, clerk/caption (#574) This updates the book to use `clerk/image` for all image examples and also provides examples of `clerk/caption`. In addition, the PR adds support to use Markdown syntax for the caption text, e.g. for easily adding links to captions. --------- Co-authored-by: Martin Kavalar --- CHANGELOG.md | 2 ++ book.clj | 41 ++++++++++++++++++++++--------- notebooks/viewers/caption.clj | 13 ++++++++++ resources/stylesheets/viewer.css | 7 ++++++ src/nextjournal/clerk/render.cljs | 2 +- src/nextjournal/clerk/viewer.cljc | 2 +- 6 files changed, 53 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2869c69ca..c82f8b7a3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,8 @@ Changes can be: * 💫 Allow to disable welcome page in `serve!` +* 💫 Support using Markdown syntax in `clerk/caption` text + * 🛠 Bump depdendencies * `com.taoensso/nippy` to `3.4.0-beta1` diff --git a/book.clj b/book.clj index 6af1303ac..972ec737f 100644 --- a/book.clj +++ b/book.clj @@ -299,20 +299,15 @@ int main() { ;; ### 🏞 Images -;; Clerk now has built-in support for the -;; `java.awt.image.BufferedImage` class, which is the native image -;; format of the JVM. -;; -;; When combined with `javax.imageio.ImageIO/read`, one can easily -;; load images in a variety of formats from a `java.io.File`, an -;; `java.io.InputStream`, or any resource that a `java.net.URL` can -;; address. +;; Clerk offers the `clerk/image` viewer to create a buffered image +;; from a string or anything `javax.imageio.ImageIO/read` can take +;; (URL, File or InputStream). ;; ;; For example, we can fetch a photo of De zaaier, Vincent van Gogh's ;; famous painting of a farmer sowing a field from Wiki Commons like ;; this: -(ImageIO/read (URL. "https://upload.wikimedia.org/wikipedia/commons/thumb/3/31/The_Sower.jpg/1510px-The_Sower.jpg")) +(clerk/image "https://upload.wikimedia.org/wikipedia/commons/thumb/3/31/The_Sower.jpg/1510px-The_Sower.jpg") ;; We've put some effort into making the default image rendering ;; pleasing. The viewer uses the dimensions and aspect ratio of each @@ -320,11 +315,31 @@ int main() { ;; fashion. For example, an image larger than 900px wide with an ;; aspect ratio larger then two will be displayed full width: -(ImageIO/read (URL. "https://images.unsplash.com/photo-1532879311112-62b7188d28ce?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8")) +(clerk/image "https://images.unsplash.com/photo-1532879311112-62b7188d28ce?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8") ;; On the other hand, smaller images are centered and shown using their intrinsic dimensions: -(ImageIO/read (URL. "https://nextjournal.com/data/QmSJ6eu6kUFeWrqXyYaiWRgJxAVQt2ivaoNWc1dtTEADCf?filename=thermo.png&content-type=image/png")) +(clerk/image "https://nextjournal.com/data/QmSJ6eu6kUFeWrqXyYaiWRgJxAVQt2ivaoNWc1dtTEADCf?filename=thermo.png&content-type=image/png") + +;; You can use `clerk/image` together with `clerk/caption` which will render a simple caption under the image: + +(clerk/caption + "Implements of the Paper Printing Industry" + (clerk/image "https://nextjournal.com/data/QmX99isUndwqBz7nj8fdG7UoDakNDSH1TZcvY2Y6NUTe6o?filename=image.gif&content-type=image/gif")) + +;; Captions aren't limited to images and work together with any arbitrary content that you provide, e.g. a table: + +^{::clerk/visibility {:code :fold}} +(clerk/caption + "Modern Symmetrical Unary(7) in [Solresol](https://wiki.xxiivv.com/site/solresol.html)" + (clerk/table {:head ["Solfège" "French IPA" "English IPA" "Meaning"] + :rows [["Do" "/do/" "/doʊ/" "no"] + ["Re" "/ʁɛ/" "/ɹeɪ/" "and, also"] + ["Mi" "/mi/" "/miː/" "or"] + ["Fa" "/fa/" "/fɑː/" "at, to"] + ["Sol" "/sɔl/" "/soʊl/" "but, if"] + ["La" "/la/" "/lɑː/" "the, then"] + ["Si" "/si/" "/siː/" "yes"]]})) ;; ### 📒 Markdown @@ -363,13 +378,15 @@ int main() { ;; nice captions: (defn caption [text] - (clerk/html [:span.text-slate-500.text-xs.text-center.font-sans text])) + (clerk/html [:figcaption.text-center.mt-1 text])) (clerk/row (clerk/col image-1 (caption "Figure 1: Decorative A")) (clerk/col image-2 (caption "Figure 2: Decorative B")) (clerk/col image-3 (caption "Figure 3: Decorative C"))) +;; Note: the caption example is _exactly_ how `clerk/caption` is implemented in Clerk. + ;; **Alternative notations** ;; ;; By default, `row` and `col` operate on `& rest` so you can pass any diff --git a/notebooks/viewers/caption.clj b/notebooks/viewers/caption.clj index 44841842e..e02ae108b 100644 --- a/notebooks/viewers/caption.clj +++ b/notebooks/viewers/caption.clj @@ -30,3 +30,16 @@ (clerk/image "https://nextjournal.com/data/QmX99isUndwqBz7nj8fdG7UoDakNDSH1TZcvY2Y6NUTe6o?filename=image.gif&content-type=image/gif") (clerk/image "https://nextjournal.com/data/QmV8UanpZwTaLvLnKgJkR9etvVH9YPZX3rMFHN7YHbSGbv?filename=image.gif&content-type=image/gif") (clerk/image "https://nextjournal.com/data/QmPzBy1GBTAJf8Zzwhx5yyCfHqX5h7Wgx9geRpzgghyoEZ?filename=image.gif&content-type=image/gif"))) + +;; Captions can also include Markdown, e.g. to render links: + +(clerk/caption + "Modern Symmetrical Unary(7) in [Solresol](https://wiki.xxiivv.com/site/solresol.html)" + (clerk/table {:head ["Solfège" "French IPA" "English IPA" "Meaning"] + :rows [["Do" "/do/" "/doʊ/" "no"] + ["Re" "/ʁɛ/" "/ɹeɪ/" "and, also"] + ["Mi" "/mi/" "/miː/" "or"] + ["Fa" "/fa/" "/fɑː/" "at, to"] + ["Sol" "/sɔl/" "/soʊl/" "but, if"] + ["La" "/la/" "/lɑː/" "the, then"] + ["Si" "/si/" "/siː/" "yes"]]})) diff --git a/resources/stylesheets/viewer.css b/resources/stylesheets/viewer.css index f4b8a3eb7..bcea2aac5 100644 --- a/resources/stylesheets/viewer.css +++ b/resources/stylesheets/viewer.css @@ -259,6 +259,13 @@ @apply mb-0 !important; } +figcaption .markdown-viewer { + @apply text-xs text-slate-500; +} + +figcaption .markdown-viewer a { + @apply underline; +} /* Images */ /* --------------------------------------------------------------- */ diff --git a/src/nextjournal/clerk/render.cljs b/src/nextjournal/clerk/render.cljs index a5cead563..1c65bb235 100644 --- a/src/nextjournal/clerk/render.cljs +++ b/src/nextjournal/clerk/render.cljs @@ -871,7 +871,7 @@ (defn render-folded-code-block [code-string {:as opts :keys [id]}] (let [!hidden? (hooks/use-state true)] (if @!hidden? - [:div.relative.pl-12.font-sans.text-slate-400.cursor-pointer.flex.overflow-y-hidden.group + [:div.relative.font-sans.text-slate-400.cursor-pointer.flex.overflow-y-hidden.group [:span.hover:text-slate-500 {:class "text-[10px]" :on-click #(swap! !hidden? not)} diff --git a/src/nextjournal/clerk/viewer.cljc b/src/nextjournal/clerk/viewer.cljc index c75ff9521..b0375ea7e 100644 --- a/src/nextjournal/clerk/viewer.cljc +++ b/src/nextjournal/clerk/viewer.cljc @@ -1789,7 +1789,7 @@ (defn caption [text content] (col content - (html [:figcaption.text-xs.text-slate-500.text-center.mt-1 text]))) + (html [:figcaption.text-center.mt-1 (md text)]))) (defn ^:dynamic doc-url ([path] (doc-url path nil)) From ad8e5a2c19aa55921ea357fa9005d563f80c53be Mon Sep 17 00:00:00 2001 From: Martin Kavalar Date: Sat, 25 Nov 2023 15:28:24 +0100 Subject: [PATCH 26/26] Add clerk/comment --- CHANGELOG.md | 2 ++ src/nextjournal/clerk.clj | 9 +++++++++ 2 files changed, 11 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c82f8b7a3..a9881439b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,8 @@ Changes can be: * 💫 Allow to disable welcome page in `serve!` +* 💫 Add `clerk/comment` that behaves like `clojure.core/comment` outside of Clerk but shows the results like regular top-level forms in Clerk. + * 💫 Support using Markdown syntax in `clerk/caption` text * 🛠 Bump depdendencies diff --git a/src/nextjournal/clerk.clj b/src/nextjournal/clerk.clj index a5a9c3e0f..b0ecbf6a0 100644 --- a/src/nextjournal/clerk.clj +++ b/src/nextjournal/clerk.clj @@ -1,5 +1,6 @@ (ns nextjournal.clerk "Clerk's Public API." + (:refer-clojure :exclude [comment]) (:require [babashka.fs :as fs] [clojure.java.browse :as browse] [clojure.java.io :as io] @@ -389,6 +390,14 @@ (defn doc-url [& args] (apply v/doc-url args)) +(defmacro comment + "Evaluates the expressions in `body` showing the results in Clerk. + + Does nothing outside of Clerk, like `clojure.core/comment`." + [& body] + (when nextjournal.clerk.config/*in-clerk* + `(nextjournal.clerk/fragment ~(vec body)))) + (defmacro example "Evaluates the expressions in `body` showing code next to results in Clerk.