Skip to content

Commit ef2828d

Browse files
zampinomkphilippamarkovics
authored
clerk/fragment (#443)
Allow for splicing a seq of values into the document as if it were produced by results of individual cells. Useful when programmatically generating content. --------- Co-authored-by: Martin Kavalar <[email protected]> Co-authored-by: Philippa Markovics <[email protected]>
1 parent 9fa6b37 commit ef2828d

File tree

13 files changed

+277
-129
lines changed

13 files changed

+277
-129
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ Changes can be:
1111

1212
To see what's going on while waiting for a long-running computation, Clerk will now show an execution status bar on the top. For named cells (defining a var) it will show the name of the var, for anonymous expressions, a preview of the form.
1313

14+
* 🍕 `clerk/fragment` for splicing a seq of values into the document as if it were produced by results of individual cells. Useful when programmatically generating content.
15+
1416
* 🔌 Make websocket reconnect automatically on close to avoid having to reload the page
1517
* 💫 Cache expressions that return `nil` in memory
1618

notebooks/fragments.clj

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
;; # 🧩Fragments
2+
(ns fragments
3+
(:require [nextjournal.clerk :as clerk]))
4+
5+
;; With `clerk/fragment` we allow to embed a sequence of values into the document as if they were results of individual cells, nesting is allowed.
6+
7+
(clerk/fragment
8+
(clerk/table [[1 2] [3 4]])
9+
(clerk/image "trees.png")
10+
(clerk/plotly {::clerk/width :full} {:data [{:y [1 3 2]}]})
11+
(clerk/html {::clerk/width :full} [:div.h-20.bg-amber-200])
12+
(clerk/fragment (clerk/html {::clerk/width :full} [:div.h-20.bg-amber-300])
13+
(clerk/html {::clerk/width :full} [:div.h-20.bg-amber-400])))
14+
15+
;; ## Collapsible Sections
16+
;; Fragments allow to hide (and in future versions of Clerk, probably fold) chunks of prose interspersed with results. That is, by using the usual visibility annotation
17+
;;
18+
;; ^{::clerk/visibility {:code :hide :result :hide}}
19+
;;
20+
;; all the following section can be hidden.
21+
22+
^{::clerk/visibility {:code :hide}}
23+
(clerk/fragment
24+
(clerk/md "# Title")
25+
(clerk/code 123)
26+
123
27+
(clerk/md "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus condimentum accumsan lacus id laoreet. Maecenas scelerisque rutrum nunc, eu rutrum libero tincidunt eu. Etiam neque mi, sollicitudin in sodales nec, ornare nec dolor. Vivamus non vestibulum erat. Etiam sodales justo lacus, ac ullamcorper sem dignissim eu. Nunc a vehicula elit. Donec orci odio, bibendum ut imperdiet id, fermentum nec ex. Nunc venenatis est quis arcu elementum, non accumsan erat dictum. Donec vitae felis felis.
28+
## Some Section
29+
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus condimentum accumsan lacus id laoreet. Maecenas scelerisque rutrum nunc, eu rutrum libero tincidunt eu. Etiam neque mi, sollicitudin in sodales nec, ornare nec dolor. Vivamus non vestibulum erat. Etiam sodales justo lacus, ac ullamcorper sem dignissim eu. Nunc a vehicula elit. Donec orci odio, bibendum ut imperdiet id, fermentum nec ex. Nunc venenatis est quis arcu elementum, non accumsan erat dictum. Donec vitae felis felis.")
30+
(clerk/code '(clerk/plotly {:data [{:y [1 2 3]}]}))
31+
(clerk/plotly {:data [{:y [1 2 3]}]})
32+
(clerk/md "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus condimentum accumsan lacus id laoreet. Maecenas scelerisque rutrum nunc, eu rutrum libero tincidunt eu. Etiam neque mi, sollicitudin in sodales nec, ornare nec dolor. Vivamus non vestibulum erat. Etiam sodales justo lacus, ac ullamcorper sem dignissim eu. Nunc a vehicula elit. Donec orci odio, bibendum ut imperdiet id, fermentum nec ex. Nunc venenatis est quis arcu elementum, non accumsan erat dictum. Donec vitae felis felis.
33+
34+
---"))
35+
36+
;; And the above will look like as if produced by:
37+
;; # Title
38+
123
39+
;; Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus condimentum accumsan lacus id laoreet. Maecenas scelerisque rutrum nunc, eu rutrum libero tincidunt eu. Etiam neque mi, sollicitudin in sodales nec, ornare nec dolor. Vivamus non vestibulum erat. Etiam sodales justo lacus, ac ullamcorper sem dignissim eu. Nunc a vehicula elit. Donec orci odio, bibendum ut imperdiet id, fermentum nec ex. Nunc venenatis est quis arcu elementum, non accumsan erat dictum. Donec vitae felis felis.
40+
;; ## Some Section
41+
;; Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus condimentum accumsan lacus id laoreet. Maecenas scelerisque rutrum nunc, eu rutrum libero tincidunt eu. Etiam neque mi, sollicitudin in sodales nec, ornare nec dolor. Vivamus non vestibulum erat. Etiam sodales justo lacus, ac ullamcorper sem dignissim eu. Nunc a vehicula elit. Donec orci odio, bibendum ut imperdiet id, fermentum nec ex. Nunc venenatis est quis arcu elementum, non accumsan erat dictum. Donec vitae felis felis.
42+
(clerk/plotly {:data [{:y [1 2 3]}]})
43+
;; Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus condimentum accumsan lacus id laoreet. Maecenas scelerisque rutrum nunc, eu rutrum libero tincidunt eu. Etiam neque mi, sollicitudin in sodales nec, ornare nec dolor. Vivamus non vestibulum erat. Etiam sodales justo lacus, ac ullamcorper sem dignissim eu. Nunc a vehicula elit. Donec orci odio, bibendum ut imperdiet id, fermentum nec ex. Nunc venenatis est quis arcu elementum, non accumsan erat dictum. Donec vitae felis felis.
44+
;;
45+
;; ---

notebooks/viewers/markdown.clj

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,9 @@ It's [Markdown](https://daringfireball.net/projects/markdown/), like you know it
4848

4949
;; ## Soft vs. Hard Line Breaks
5050
;; This one ⇥
51-
;; ⇤ is a [soft break](https://spec.commonmark.org/0.30/#soft-line-breaks) and is rendered as a space, while this one ⇥\
51+
;; ⇤ is a [soft break](https://spec.commonmark.org/0.30/#soft-line-breaks) and is rendered as a space.
52+
;;
53+
;; This one instead ⇥\
5254
;; ⇤ is a [hard break](https://spec.commonmark.org/0.30/#hard-line-breaks) and is rendered as a newline.
5355

5456
;; ## Sidenotes

resources/stylesheets/viewer.css

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@
3737
h2 { @apply text-xl !important; }
3838
h3 { @apply text-lg !important; }
3939
}
40-
40+
4141
button { @apply focus:outline-none; }
4242
strong { @apply font-bold; }
4343
em { @apply italic; }
@@ -254,6 +254,9 @@
254254
.markdown-viewer blockquote p:last-of-type:after {
255255
@apply content-none;
256256
}
257+
.markdown-node-viewer.result-viewer.fragment-item {
258+
@apply mb-0 !important;
259+
}
257260

258261
/* Images */
259262
/* --------------------------------------------------------------- */
@@ -330,7 +333,7 @@
330333
@apply mt-1;
331334
}
332335
.sidenotes-layout .markdown-viewer {
333-
@apply pr-[205px];
336+
@apply pr-[241px];
334337
}
335338
.sidenote-container {
336339
@apply relative mb-0;
@@ -339,9 +342,9 @@
339342
@apply w-[756px] !important;
340343
}
341344
}
342-
.code-viewer + .viewer:not(.markdown-viewer):not(.code-viewer):not(.code-viewer-folded),
343-
.code-viewer-folded + .viewer:not(.markdown-viewer):not(.code-viewer):not(.code-viewer-folded),
344-
.result-viewer + .result-viewer {
345+
.code-viewer + .viewer:not(.code-viewer):not(.code-viewer-folded),
346+
.code-viewer-folded + .viewer:not(.code-viewer):not(.code-viewer-folded),
347+
.result-viewer:not(.markdown-node-viewer) + .result-viewer {
345348
@apply mt-2;
346349
}
347350
.code-viewer + .code-viewer-folded {
@@ -350,6 +353,9 @@
350353
.result-viewer {
351354
@apply leading-tight mb-6;
352355
}
356+
.code-viewer.fragment-item.result-viewer {
357+
@apply mb-0 !important;
358+
}
353359
.result-viewer figure {
354360
@apply mt-0 !important;
355361
}

src/nextjournal/clerk.clj

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -327,6 +327,13 @@
327327
[text content]
328328
(v/caption text content))
329329

330+
(defn fragment
331+
"A utility function to splice the given `xs` into individual results.
332+
333+
Useful when prgrammatically generating content."
334+
[& xs]
335+
(apply v/fragment xs))
336+
330337
(defn code
331338
"Displays `x` as syntax highlighted Clojure code.
332339

src/nextjournal/clerk/builder.clj

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,14 @@
2020
"notebooks/onwards.md"]
2121
(map #(str "notebooks/" % ".clj"))
2222
["cards"
23+
"controlling_width"
2324
"docs"
2425
"hello"
2526
"how_clerk_works"
2627
"exec_status"
2728
"eval_cljs"
2829
"example"
30+
"fragments"
2931
"hiding_clerk_metadata"
3032
"js_import"
3133
"multiviewer"

src/nextjournal/clerk/render.cljs

Lines changed: 56 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -114,27 +114,6 @@
114114

115115
(defonce !eval-counter (r/atom 0))
116116

117-
(defn render-processed-block [x]
118-
(let [{viewer-name :name} (viewer/->viewer x)
119-
viewer-css-class (viewer/css-class x)
120-
inner-viewer-name (some-> x viewer/->value viewer/->viewer :name)
121-
processed-block-id (get-in x [:nextjournal/opts :id])]
122-
^{:key (str processed-block-id "@" @!eval-counter)}
123-
[:div {:data-block-id processed-block-id
124-
:class (concat
125-
[(when (:nextjournal/open-graph-image-capture (viewer/->value x)) "open-graph-image-capture")]
126-
(if viewer-css-class
127-
(cond-> viewer-css-class
128-
(string? viewer-css-class) vector)
129-
["viewer"
130-
(when viewer-name (name viewer-name))
131-
(when inner-viewer-name (name inner-viewer-name))
132-
(case (or (viewer/width x) (case viewer-name (`viewer/code-viewer `viewer/code-folded-viewer) :wide :prose))
133-
:wide "w-full max-w-wide"
134-
:full "w-full"
135-
"w-full max-w-prose px-8")]))}
136-
[inspect-presented x]]))
137-
138117
(defn exec-status [{:keys [progress status]}]
139118
[:div.w-full.bg-purple-200.dark:bg-purple-900.rounded.z-20 {:class "h-0.5"}
140119
[:div.bg-purple-600.dark:bg-purple-400 {:class "h-0.5" :style {:width (str (* progress 100) "%")}}]
@@ -147,7 +126,9 @@
147126
{:style {:font-size "0.5rem"} :class "left-[35px] md:left-0 mt-[7px] md:mt-1"}
148127
status])
149128

150-
(defn render-notebook [{:as _doc xs :blocks :keys [bundle? css-class sidenotes? toc toc-visibility]}]
129+
(declare inspect-children)
130+
131+
(defn render-notebook [{:as _doc xs :blocks :keys [bundle? css-class sidenotes? toc toc-visibility]} opts]
151132
(r/with-let [local-storage-key "clerk-navbar"
152133
navbar-width 220
153134
!state (r/atom {:toc (toc-items (:children toc))
@@ -197,14 +178,19 @@
197178
{:class "z-10 fixed right-2 top-2 md:right-auto md:left-3 md:top-[7px] text-slate-400 font-sans text-xs hover:underline cursor-pointer flex items-center bg-white dark:bg-gray-900 py-1 px-3 md:p-0 rounded-full md:rounded-none border md:border-0 border-slate-200 dark:border-gray-500 shadow md:shadow-none dark:text-slate-400 dark:hover:text-white"}]
198179
[navbar/panel !state [navbar/navbar !state]]])
199180
[:div.flex-auto.w-screen.scroll-container
200-
[:> (.-div motion)
201-
{:key "notebook-viewer"
202-
:initial (when toc-visibility {:margin-left doc-inset})
203-
:animate (when toc-visibility {:margin-left doc-inset})
204-
:transition navbar/spring
205-
:class (str (or css-class "flex flex-col items-center notebook-viewer flex-auto ")
206-
(when sidenotes? "sidenotes-layout"))}
207-
(doall (map render-processed-block xs))]]])))
181+
(into
182+
[:> (.-div motion)
183+
{:key "notebook-viewer"
184+
:initial (when toc-visibility {:margin-left doc-inset})
185+
:animate (when toc-visibility {:margin-left doc-inset})
186+
:transition navbar/spring
187+
:class (str (or css-class "flex flex-col items-center notebook-viewer flex-auto ")
188+
(when sidenotes? "sidenotes-layout"))}]
189+
190+
;; TODO: restore react keys via block-id
191+
;; ^{:key (str processed-block-id "@" @!eval-counter)}
192+
193+
(inspect-children opts) xs)]])))
208194

209195
(defn opts->query [opts]
210196
(->> opts
@@ -298,7 +284,28 @@
298284
auto-expand? (-> viewer/assign-content-lengths)
299285
true (-> viewer/assign-expanded-at (get :nextjournal/expanded-at {}))))
300286

301-
(defn render-result [{:as result :nextjournal/keys [fetch-opts hash presented]} {:as opts :keys [auto-expand-results?]}]
287+
(defn result-css-class [x]
288+
(let [{viewer-name :name} (viewer/->viewer x)
289+
viewer-css-class (viewer/css-class x)
290+
inner-viewer-name (some-> x viewer/->value viewer/->viewer :name)]
291+
(if viewer-css-class
292+
(cond-> viewer-css-class
293+
(string? viewer-css-class) vector)
294+
["viewer"
295+
(when (get-in x [:nextjournal/opts :fragment-item?]) "fragment-item")
296+
(when viewer-name (name viewer-name))
297+
(when inner-viewer-name (name inner-viewer-name))
298+
(case (or (viewer/width x)
299+
(case viewer-name
300+
(`viewer/code-viewer) :wide
301+
(`viewer/markdown-node-viewer) :nested-prose
302+
:prose))
303+
:wide "w-full max-w-wide"
304+
:full "w-full"
305+
:nested-prose "w-full max-w-prose"
306+
"w-full max-w-prose px-8")])))
307+
308+
(defn render-result [{:as result :nextjournal/keys [fetch-opts hash presented]} {:as opts :keys [id auto-expand-results? path]}]
302309
(let [!desc (hooks/use-state-with-deps presented [hash])
303310
!expanded-at (hooks/use-state (when (map? @!desc)
304311
(->expanded-at auto-expand-results? @!desc)))
@@ -324,10 +331,11 @@
324331
(when @!desc
325332
[view-context/provide {:fetch-fn fetch-fn}
326333
[:> ErrorBoundary {:hash hash}
327-
[:div.relative
328-
[:div.overflow-x-auto
329-
{:ref ref-fn}
330-
[inspect-presented {:!expanded-at !expanded-at} @!desc]]]]])))
334+
[:div.result-viewer {:class (result-css-class @!desc) :data-block-id id :ref ref-fn}
335+
[:div.relative
336+
[:div.overflow-x-auto
337+
{:ref ref-fn}
338+
[inspect-presented {:!expanded-at !expanded-at} @!desc]]]]]])))
331339

332340
(defn toggle-expanded [!expanded-at path event]
333341
(.preventDefault event)
@@ -350,11 +358,11 @@
350358
(defn expandable? [xs]
351359
(< 1 (count xs)))
352360

353-
354361
(defn inspect-children [opts]
355362
;; TODO: move update function onto viewer
356363
(map-indexed (fn [idx x]
357-
(inspect-presented (update opts :path (fnil conj []) idx) x))))
364+
(cond-> [inspect-presented (update opts :path (fnil conj []) idx) x]
365+
(get-in x [:nextjournal/opts :id]) (with-meta {:key (str (get-in x [:nextjournal/opts :id]) "@" @!eval-counter)})))))
358366

359367
(def expand-style
360368
["cursor-pointer"
@@ -711,6 +719,9 @@
711719
(when react-root
712720
(.render react-root (r/as-element [root]))))
713721

722+
(defn render-with-react-key [x {:as _opts :keys [id]}]
723+
(with-meta x {:key id}))
724+
714725
(defn html-render [markup]
715726
(r/as-element
716727
(if (string? markup)
@@ -810,7 +821,12 @@
810821
[:svg {:xmlns "http://www.w3.org/2000/svg" :viewBox "0 0 20 20" :fill "currentColor" :width 12 :height 12}
811822
[:path {:fill-rule "evenodd" :d "M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" :clip-rule "evenodd"}]])
812823

813-
(defn render-folded-code [code-string]
824+
(defn render-code-block [code-string {:keys [id]}]
825+
^{:key id}
826+
[:div.viewer.code-viewer.w-full.max-w-wide {:data-block-id id}
827+
[code/render-code code-string]])
828+
829+
(defn render-folded-code-block [code-string {:keys [id]}]
814830
(let [!hidden? (hooks/use-state true)]
815831
(if @!hidden?
816832
[:div.relative.pl-12.font-sans.text-slate-400.cursor-pointer.flex.overflow-y-hidden.group
@@ -842,7 +858,8 @@
842858
[:span.ml-4.opacity-0.translate-y-full.group-hover:opacity-100.group-hover:translate-y-0.transition-all.delay-150.hover:text-slate-500
843859
{:class "text-[10px]"}
844860
"evaluated in 0.2s"]]
845-
[:div.code-viewer.mb-2.relative {:style {:margin-top 0}}
861+
^{:key id}
862+
[:div.code-viewer.mb-2.relative.code-viewer.w-full.max-w-wide {:data-block-id id :style {:margin-top 0}}
846863
[render-code code-string]]])))
847864

848865

src/nextjournal/clerk/render/code.cljs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,13 +87,14 @@
8787
(< pos to)
8888
(concat [(.sliceString text pos to)]))))))))
8989

90-
(defn render-code [^String code]
90+
(defn render-code [^String code {:as _opts :keys [id]}]
9191
(let [builder (RangeSetBuilder.)
9292
_ (highlightTree (.. clojureLanguage -parser (parse code)) highlight-style
9393
(fn [from to style]
9494
(.add builder from to (.mark Decoration (j/obj :class style)))))
9595
decorations-rangeset (.finish builder)
9696
text (.of Text (.split code "\n"))]
97+
^{:key id}
9798
[:div.cm-editor
9899
[:cm-scroller
99100
(into [:div.cm-content.whitespace-pre]

src/nextjournal/clerk/static_app.cljs

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -30,21 +30,22 @@
3030
:nextjournal/value hiccup})
3131

3232
(defn show [{:as view-data :git/keys [sha url] :keys [bundle? doc path url->path]}]
33-
(let [header [:div.mb-8.text-xs.sans-serif.text-gray-400.not-prose
34-
(when (not= "" path)
35-
[:<>
36-
[:a.hover:text-indigo-500.dark:hover:text-white.font-medium.border-b.border-dotted.border-gray-300
37-
{:href (doc-url view-data "")} "Back to index"]
38-
[:span.mx-1 "/"]])
39-
[:span
40-
"Generated with "
41-
[:a.hover:text-indigo-500.dark:hover:text-white.font-medium.border-b.border-dotted.border-gray-300
42-
{:href "https://github.com/nextjournal/clerk"} "Clerk"]
43-
(when (and url sha (contains? url->path path))
33+
(let [header [:div.viewer.w-full.max-w-prose.px-8
34+
[:div.mb-8.text-xs.sans-serif.text-gray-400.not-prose
35+
(when (not= "" path)
4436
[:<>
45-
" from "
4637
[:a.hover:text-indigo-500.dark:hover:text-white.font-medium.border-b.border-dotted.border-gray-300
47-
{:href (str url "/blob/" sha "/" (url->path path))} (url->path path) "@" [:span.tabular-nums (subs sha 0 7)]]])]]]
38+
{:href (doc-url view-data "")} "Back to index"]
39+
[:span.mx-1 "/"]])
40+
[:span
41+
"Generated with "
42+
[:a.hover:text-indigo-500.dark:hover:text-white.font-medium.border-b.border-dotted.border-gray-300
43+
{:href "https://github.com/nextjournal/clerk"} "Clerk"]
44+
(when (and url sha (contains? url->path path))
45+
[:<>
46+
" from "
47+
[:a.hover:text-indigo-500.dark:hover:text-white.font-medium.border-b.border-dotted.border-gray-300
48+
{:href (str url "/blob/" sha "/" (url->path path))} (url->path path) "@" [:span.tabular-nums (subs sha 0 7)]]])]]]]
4849
(render/set-state! {:doc (cond-> (assoc doc :bundle? bundle?)
4950
(vector? (get-in doc [:nextjournal/value :blocks]))
5051
(update-in [:nextjournal/value :blocks] (partial into [(hiccup header)])))})

0 commit comments

Comments
 (0)