diff --git a/CHANGELOG.md b/CHANGELOG.md index b15beda10..d040635c5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,16 @@ 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. + + 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 +57,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) 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/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/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. 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 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/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 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.clj b/src/nextjournal/clerk.clj index f56073408..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] @@ -63,7 +65,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/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)) diff --git a/src/nextjournal/clerk/doc.clj b/src/nextjournal/clerk/doc.clj new file mode 100644 index 000000000..2e1d629c8 --- /dev/null +++ b/src/nextjournal/clerk/doc.clj @@ -0,0 +1,202 @@ +(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 + (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 (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 + :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 + (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.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) + 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)]]) 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/render.cljs b/src/nextjournal/clerk/render.cljs index 4c65bd53c..a5cead563 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]) @@ -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)) 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/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))) diff --git a/src/nextjournal/clerk/webserver.clj b/src/nextjournal/clerk/webserver.clj index 011fa2093..8c6000b6d 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] @@ -102,7 +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!))) @@ -197,7 +204,8 @@ (defn maybe-add-extension [nav-path] (if (and (string? nav-path) (or (str/starts-with? nav-path "'") - (fs/exists? nav-path))) + (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"])))) @@ -250,6 +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-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/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))))))) + 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])") 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")))))))