├── .dir-locals.el ├── .gitignore ├── LICENSE ├── README.md ├── deps.edn ├── dev └── user.clj ├── notebooks └── simple_slideshow.clj └── src └── nextjournal └── clerk_slideshow.clj /.dir-locals.el: -------------------------------------------------------------------------------- 1 | ;;; Directory Local Variables 2 | ;;; For more information see (info "(emacs) Directory Variables") 3 | 4 | ((clojure-mode . ((cider-clojure-cli-aliases . ":dev:nextjournal/clerk")))) 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .cache/ 3 | .clerk/ 4 | .cpcache/ 5 | .lsp/ 6 | node_modules 7 | notebooks/scratch*.clj 8 | /public/js/ 9 | /.shadow-cljs/ 10 | /target/ 11 | .idea/ 12 | clerk-slideshow.iml 13 | public/girouette.css 14 | public/images/ 15 | yarn.lock 16 | *~ 17 | .work 18 | /build 19 | /public/build 20 | .nrepl-port 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2021 Nextjournal GmbH. 2 | 3 | Permission to use, copy, modify, and/or distribute this software for any purpose 4 | with or without fee is hereby granted, provided that the above copyright notice 5 | and this permission notice appear in all copies. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 8 | REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND 9 | FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 10 | INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS 11 | OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER 12 | TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF 13 | THIS SOFTWARE. 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 🎠 clerk-slideshow 2 | 3 | Build your slideshows using Clerk notebooks! 4 | 5 | https://user-images.githubusercontent.com/1944/171370671-6a4844ce-389c-4ba4-8b4b-908316b8b9ae.mp4 6 | 7 | ## How does it work? 8 | 9 | Simply require `clerk-slideshow`… 10 | 11 | ```clojure 12 | (ns simple_slideshow 13 | (:require [nextjournal.clerk :as clerk] 14 | [nextjournal.clerk-slideshow :as slideshow])) 15 | ``` 16 | 17 | …and add it to Clerk’s viewers using `clerk/add-viewers!`: 18 | 19 | ```clojure 20 | (clerk/add-viewers! [slideshow/viewer]) 21 | ``` 22 | 23 | With that in place, you can use Markdown comments to write your slides’ content. Use Markdown rulers (`---`) to separate your slides. You can use everything that you’ll normally use in your Clerk notebooks: Markdown, plots, code blocks, you name it. 24 | 25 | Press `←` and `→` to navigate between slides or `Escape` to get an overview. 26 | -------------------------------------------------------------------------------- /deps.edn: -------------------------------------------------------------------------------- 1 | {:aliases 2 | {:dev {:extra-paths ["dev"]} 3 | :nextjournal/clerk 4 | {:extra-deps {io.github.nextjournal/clerk {:git/sha "b7207926bacb10e6af17075efd71df3363b74d21"}} 5 | :extra-paths ["notebooks"] 6 | :exec-fn nextjournal.clerk/build-static-app! 7 | :exec-args {:paths ["notebooks/simple_slideshow.clj"]}}}} 8 | -------------------------------------------------------------------------------- /dev/user.clj: -------------------------------------------------------------------------------- 1 | (ns user 2 | (:require [nextjournal.clerk :as clerk])) 3 | 4 | (clerk/serve! {:browse? true :port 7779}) 5 | -------------------------------------------------------------------------------- /notebooks/simple_slideshow.clj: -------------------------------------------------------------------------------- 1 | ;; # Hello there 👋 2 | ;; 3 | ;; `clerk-slideshow` enables you to create beautiful interactive slide decks 4 | ;; using Clerk notebooks. 5 | 6 | ;; --- 7 | ;; ## How does it work? 8 | ;; 9 | ;; Simply require `clerk-slideshow` 10 | 11 | (ns simple-slideshow 12 | (:require [nextjournal.clerk :as clerk] 13 | [nextjournal.clerk-slideshow :as slideshow])) 14 | 15 | ;; and add it to Clerk’s existing viewers 16 | 17 | ^{::clerk/visibility {:code :hide :result :hide}} 18 | (clerk/add-viewers! [slideshow/viewer]) 19 | 20 | ;; --- 21 | ;; ## What now? 22 | ;; 23 | ;; With that in place, you can use Markdown comments to write your slides’ content. 24 | ;; Use Markdown rulers (`---`) to separate your slides. You can use everything that 25 | ;; you’ll normally use in your Clerk notebooks: 26 | ;; Markdown, plots, code blocks, you name it. 27 | ;; 28 | ;; Press `←` and `→` to navigate between slides or `Escape` to get an overview. 29 | ;; 30 | ;; Now some demos 👉 31 | 32 | ;; --- 33 | ;; ## 📊 A Plotly graph 34 | ^{::clerk/visibility {:code :hide}} 35 | (clerk/plotly {:data [{:z [[1 2 3] [3 2 1]] :type "surface"}]}) 36 | 37 | ;; --- 38 | ;; ## 📈 A Vega Lite graph 39 | ^{::clerk/visibility {:code :hide}} 40 | (clerk/vl {:width 650 :height 400 :data {:url "https://vega.github.io/vega-datasets/data/us-10m.json" 41 | :format {:type "topojson" :feature "counties"}} 42 | :transform [{:lookup "id" :from {:data {:url "https://vega.github.io/vega-datasets/data/unemployment.tsv"} 43 | :key "id" :fields ["rate"]}}] 44 | :projection {:type "albersUsa"} :mark "geoshape" :encoding {:color {:field "rate" :type "quantitative"}}}) 45 | 46 | ;; --- 47 | ;; ## 🖼️ An Image 48 | ^{::clerk/visibility {:code :hide}} 49 | (clerk/image "https://images.unsplash.com/photo-1532879311112-62b7188d28ce?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8") 50 | 51 | ;; --- 52 | ;; ## And that’s it for now! 👋 53 | ;; 54 | ;; More demos will follow soon! 55 | -------------------------------------------------------------------------------- /src/nextjournal/clerk_slideshow.clj: -------------------------------------------------------------------------------- 1 | ;; # 🎠 Clerk Slideshow 2 | ;; --- 3 | (ns nextjournal.clerk-slideshow 4 | (:require [nextjournal.clerk.viewer :as v] 5 | [nextjournal.clerk :as clerk])) 6 | 7 | ;; With a custom viewer and some helper functions, we can turn a Clerk notebooks into a presentation. 8 | ;; 9 | ;; `slide-viewer` wraps a collection of blocks into markup suitable for rendering a slide. 10 | 11 | (def slide-viewer 12 | {:render-fn '(fn [blocks opts] 13 | [:div.flex.flex-col.justify-center 14 | {:style {:min-block-size "100vh"}} 15 | (into [:div.text-xl.p-20 {:class ["prose max-w-none prose-h1:mb-0 prose-h2:mb-8 prose-h3:mb-8 prose-h4:mb-8" 16 | "prose-h1:text-6xl prose-h2:text-5xl prose-h3:text-3xl prose-h4:text-2xl"]}] 17 | (nextjournal.clerk.render/inspect-children opts) 18 | blocks)])}) 19 | 20 | ;; We need a simpler code viewer than the default one, one that adapts to the full width of the slideshow. 21 | (def code-viewer 22 | {:render-fn '(fn [code] [:div.code-viewer [nextjournal.clerk.render.code/render-code code {:language "clojure"}]]) 23 | :transform-fn (comp v/mark-presented (v/update-val :text-without-meta))}) 24 | 25 | ;; --- 26 | ;; The `doc->slides` helper function takes Clerk notebook data and partitions its blocks into slides by occurrences of markdown rulers. 27 | (defn doc->slides [{:as doc :keys [blocks]}] 28 | (sequence (comp (mapcat (partial v/with-block-viewer doc)) 29 | (mapcat #(cond 30 | (= `v/markdown-viewer (v/->viewer %)) (map v/md (-> % v/->value :content)) 31 | (= `v/code-block-viewer (v/->viewer %)) [(assoc % :nextjournal/viewer code-viewer)] 32 | :else [%])) 33 | (partition-by (comp #{:ruler} :type v/->value)) 34 | (remove (comp #{:ruler} :type v/->value first)) 35 | (map (partial v/with-viewer slide-viewer))) 36 | blocks)) 37 | ;; --- 38 | ;; We can then override Clerk’s default notebook viewer with a custom slideshow viewer which can be required and set using 39 | ;; `clerk/add-viewers!`. 40 | (def viewer 41 | (assoc v/notebook-viewer 42 | :transform-fn (v/update-val doc->slides) 43 | :render-fn '(fn [slides] 44 | (reagent.core/with-let [!state (reagent.core/atom {:current-slide 0 45 | :grid? false 46 | :viewport-width js/innerWidth 47 | :viewport-height js/innerHeight}) 48 | ref-fn (fn [el] 49 | (when el 50 | (swap! !state assoc :stage-el el) 51 | (js/addEventListener "resize" 52 | #(swap! !state assoc 53 | :viewport-width js/innerWidth 54 | :viewport-height js/innerHeight)) 55 | (js/document.addEventListener "keydown" 56 | (fn [e] 57 | (case (.-key e) 58 | "Escape" (swap! !state update :grid? not) 59 | "ArrowRight" (when-not (:grid? !state) 60 | (swap! !state update :current-slide #(min (dec (count slides)) (inc %)))) 61 | "ArrowLeft" (when-not (:grid? !state) 62 | (swap! !state update :current-slide #(max 0 (dec %)))) 63 | nil))))) 64 | default-transition {:type :spring :duration 0.4 :bounce 0.1}] 65 | (let [{:keys [grid? current-slide viewport-width viewport-height]} @!state] 66 | [:div.overflow-hidden.relative.bg-slate-50.dark:bg-slate-800 67 | {:ref ref-fn :id "stage" :style {:width viewport-width :height viewport-height}} 68 | (into [:> (.. framer-motion -motion -div) 69 | {:style {:width (if grid? viewport-width (* (count slides) viewport-width))} 70 | :initial false 71 | :animate {:x (if grid? 0 (* -1 current-slide viewport-width))} 72 | :transition default-transition}] 73 | (map-indexed 74 | (fn [i slide] 75 | (let [width 250 76 | height 150 77 | gap 40 78 | slides-per-row (int (/ viewport-width (+ gap width))) 79 | col (mod i slides-per-row) 80 | row (int (/ i slides-per-row))] 81 | [:> (.. framer-motion -motion -div) 82 | {:initial false 83 | :class ["absolute left-0 top-0 overflow-x-hidden bg-white" 84 | (when grid? 85 | "rounded-lg shadow-lg overflow-y-hidden cursor-pointer ring-1 ring-slate-200 hover:ring hover:ring-blue-500/50 active:ring-blue-500")] 86 | :animate {:width (if grid? width viewport-width) 87 | :height (if grid? height viewport-height) 88 | :x (if grid? (+ gap (* (+ gap width) col)) (* i viewport-width)) 89 | :y (if grid? (+ gap (* (+ gap height) row)) 0)} 90 | :transition default-transition 91 | :on-click #(when grid? (swap! !state assoc :current-slide i :grid? false))} 92 | [:> (.. framer-motion -motion -div) 93 | {:style {:width viewport-width 94 | :height viewport-height 95 | :transformOrigin "left top"} 96 | :initial false 97 | :animate {:scale (if grid? (/ width viewport-width) 1)} 98 | :transition default-transition} 99 | [nextjournal.clerk.render/inspect-presented slide]]])) 100 | slides))]))))) 101 | 102 | (comment 103 | (clerk/add-viewers! [viewer]) 104 | (clerk/reset-viewers! clerk/default-viewers)) 105 | --------------------------------------------------------------------------------