Skip to content

Commit d414976

Browse files
authored
Embedded Language Support + LiveDoc (#30)
* allows clojure mode to operate when embedded in other languages (e.g. Markdown) * first sketch of a cljs Markdown notebook editor with eval support in Clerk's SCI context
1 parent c3f92ac commit d414976

File tree

26 files changed

+1706
-99
lines changed

26 files changed

+1706
-99
lines changed

.github/workflows/main.yml

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ jobs:
1111
java-version: '11.0.7'
1212

1313
- name: 🔧 Install clojure
14-
uses: DeLaGuardo/setup-clojure@master
14+
uses: DeLaGuardo/setup-clojure@9.0
1515
with:
1616
cli: '1.10.3.943'
1717

@@ -64,12 +64,22 @@ jobs:
6464
branch: gh-pages # The branch the action should deploy to.
6565
folder: public # The folder the action should deploy.
6666

67-
- name: ✅ Add success status to report with link to snapshot
67+
- name: ✅ Add link to Clojure Mode Demo
6868
uses: Sibz/github-status-action@v1
6969
with:
7070
authToken: ${{secrets.GITHUB_TOKEN}}
71-
context: 'CI / Static App / URL'
71+
context: 'CI / Static App / Clojure Mode'
7272
description: 'Ready'
7373
state: 'success'
7474
sha: ${{github.event.pull_request.head.sha || github.sha}}
7575
target_url: https://snapshots.nextjournal.com/clojure-mode/build/${{ github.sha }}
76+
77+
- name: ✅ Add link to LiveDoc Demo
78+
uses: Sibz/github-status-action@v1
79+
with:
80+
authToken: ${{secrets.GITHUB_TOKEN}}
81+
context: 'CI / Static App / LiveDoc'
82+
description: 'Ready'
83+
state: 'success'
84+
sha: ${{github.event.pull_request.head.sha || github.sha}}
85+
target_url: https://snapshots.nextjournal.com/clojure-mode/build/${{ github.sha }}/livedoc

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
.cpcache
66
.lsp
77
public/js
8+
public/livedoc/js
89
node_modules
910
public/test
1011
*.iml

bb.edn

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
{:min-bb-version "0.7.6"
2+
:paths ["bb"]
3+
:tasks
4+
{:requires ([clojure.edn :as edn]
5+
[clojure.string :as str]
6+
[babashka.deps :as deps]
7+
[babashka.fs :as fs]
8+
[babashka.process :as p])
9+
:init (do
10+
(defn viewer-css-path []
11+
(let [cp (str/trim (with-out-str (deps/clojure ["-A:dev:demo" "-Spath"])))]
12+
(str/trim (:out (shell {:out :string} (str "bb -cp " cp " -e '(println (.getPath (clojure.java.io/resource \"css/viewer.css\")))'")))))))
13+
14+
copy-viewer-css {:doc "Copies viewer stylesheet to resources."
15+
:task (fs/copy (viewer-css-path) "resources/stylesheets/viewer.css" #{:replace-existing})}}}

demo/deps.edn

Whitespace-only changes.

demo/notebooks/livedoc.md

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
# 👋 Hello LiveDoc
2+
3+
LiveDoc is a cljs notebook editor powered by [CodeMirror Markdown language support](https://github.com/codemirror/lang-markdown) and [nextjournal clojure mode](https://nextjournal.github.io/clojure-mode).
4+
5+
In this demo we're evaluating code in [Clerk](https://github.com/nextjournal/clerk)'s SCI context. In particular we're rendering _markdown_ cells in terms of Clerk's viewers. This allows e.g. to get inline $\LaTeX$ formulas as well as block ones
6+
7+
$$\hat{f}(x) = \int_{-\infty}^{+\infty} f(t)\exp^{-2\pi i x t}dt$$
8+
9+
Here's some of Clerk's API in action
10+
11+
```clojure
12+
(v/vl {:width 650 :height 400 :mark "geoshape"
13+
:data {:url "https://vega.github.io/vega-datasets/data/us-10m.json"
14+
:format {:type "topojson"
15+
:feature "counties"}}
16+
:transform
17+
[{:lookup "id"
18+
:from {:data {:url "https://vega.github.io/vega-datasets/data/unemployment.tsv"}
19+
:key "id" :fields ["rate"]}}]
20+
:projection {:type "albersUsa"}
21+
:encoding {:color {:field "rate" :type "quantitative"}}})
22+
```
23+
24+
Compose results layout with `v/row` and `v/col`
25+
26+
```clojure
27+
(def pie
28+
(v/plotly
29+
{:data [{:values [27 11 25 8 1 3 25]
30+
:labels ["US" "China" "European Union" "Russian Federation"
31+
"Brazil" "India" "Rest of World"]
32+
:text "CO2"
33+
:textposition "inside"
34+
:domain {:column 1}
35+
:hoverinfo "label+percent+name"
36+
:hole 0.4
37+
:type "pie"}]
38+
:layout {:showlegend false
39+
:width 200
40+
:height 200
41+
:annotations [{:font {:size 20} :showarrow false :x 0.5 :y 0.5 :text "CO2"}]}
42+
:config {:responsive true}}))
43+
44+
(def contour
45+
(v/plotly {:data [{:z [[10 10.625 12.5 15.625 20]
46+
[5.625 6.25 8.125 11.25 15.625]
47+
[2.5 3.125 5.0 8.125 12.5]
48+
[0.625 1.25 3.125 6.25 10.625]
49+
[0 0.625 2.5 5.625 10]]
50+
:type "contour"}]}))
51+
52+
(v/col
53+
;; FIXME: can't use nested v/html
54+
(v/with-viewer :html [:h1 "Plots"])
55+
(v/row pie contour))
56+
```
57+
58+
## Extending the Evaluation Context
59+
60+
The rendering of blocks and their evaluation is fully customizable, this makes it easy to bring your own SCI context. In this notebook Clerk's context is being augmented of some convenient helpers for loading and handling data in the notebook:
61+
62+
* `livedoc/with-fetch`
63+
* `csv/parse`
64+
* `observable/Plot`
65+
66+
```clojure
67+
(livedoc/with-fetch "https://gist.githubusercontent.com/netj/8836201/raw/6f9306ad21398ea43cba4f7d537619d0e07d5ae3/iris.csv"
68+
(fn [text]
69+
(v/table (take 10 (map js->clj (csv/parse text))))))
70+
```
71+
72+
The following example is taken from this [Observable notebook](https://observablehq.com/@observablehq/plot)
73+
74+
```clojure
75+
(livedoc/with-fetch "https://raw.githubusercontent.com/flother/rio2016/master/athletes.csv"
76+
(fn [data]
77+
(.. observable/Plot
78+
(dot (csv/parse data)
79+
(j/obj :x "weight" :y "height" :stroke "sex"))
80+
plot)))
81+
```
82+
```clojure
83+
(v/plotly {:data [{:y (shuffle (range -100 100))}]})
84+
```
85+
86+
## Usage
87+
88+
Use livedoc `editor` function as a reagent component in your cljs application
89+
90+
[nextjournal.clojure-mode.livedoc/editor opts]
91+
92+
this puts together an instance of CodeMirror with markdown and clojure mixed language support with a set of extensions configurable via an `opts` map with keys:
93+
94+
* `:doc` (required) a markdown string
95+
96+
* `:render` a function taking a reagent state atom, returning hiccup. Such state holds a map with:
97+
* `:text` the block's text
98+
* `:type` with values `:code` or `:markdown`
99+
* `:selected?`
100+
101+
* `:eval-fn!` will be called on selected block states when evaluation is triggered
102+
103+
* `:tooltip` customises tooltip view
104+
105+
* `:extensions` extra CodeMirror extensions to be added along livedoc ones
106+
107+
* `:focus?` should editor acquire focus when loaded
108+
109+
## Keybindings
110+
111+
* `ESC`: toggles edit-one / edit-all / preview & select block
112+
* `ALT`: pressed while in edit mode toggles a tooltip with eval-at-cursor results
113+
* Arrow keys move selection up/down
114+
* `CMD + Enter` : Evaluate selected cell or leave edit mode
115+
* `CMD + Shift + Enter`: Evaluates all cells
116+
117+
```clojure
118+
(defonce state (atom 0))
119+
```
120+
```clojure
121+
(defn the-answer
122+
"to all questions"
123+
[x]
124+
(inc x))
125+
```
126+
```clojure
127+
(swap! state inc)
128+
```
129+
```clojure
130+
(v/html [:h2 (str "The Answer is: " (the-answer @state))])
131+
```
132+
133+
## Todo
134+
- [ ] cannot click to move cursor in each editable section bottom lines (probably we need calls to `requestMeasure`)
135+
- [ ] scroll selected block into view when moving out of viewport
136+
- [ ] clicking on blocks not always results in an edit at the right place
137+
- [ ] avoid re-rendering _all_ previews when scrolling or clicking to edit one (probably connected to height computations)
138+
- [ ] use async SCI eval
139+
- [ ] don't eval code when rendering previews (but only when leaving edit mode)

demo/src/deps.cljs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
{:npm-deps
22
{"react" "^17.0.2"
3-
"react-dom" "^17.0.2"}}
3+
"react-dom" "^17.0.2"
4+
"framer-motion" "^6.2.8"}}

demo/src/nextjournal/clojure_mode/demo.cljs

Lines changed: 80 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4,23 +4,27 @@
44
["@codemirror/state" :refer [EditorState]]
55
["@codemirror/view" :as view :refer [EditorView]]
66
[nextjournal.clerk.sci-viewer :as sv]
7-
[nextjournal.clerk.viewer :as clerk.viewer]
7+
[nextjournal.clerk.viewer :as v]
88
[applied-science.js-interop :as j]
9+
[shadow.resource :as rc]
910
[clojure.string :as str]
1011
[nextjournal.clojure-mode :as cm-clj]
11-
[nextjournal.clojure-mode.demo.sci :as sci]
12+
[nextjournal.livedoc :as livedoc]
13+
[nextjournal.clojure-mode.demo.sci :as demo.sci]
1214
[nextjournal.clojure-mode.keymap :as keymap]
1315
[nextjournal.clojure-mode.live-grammar :as live-grammar]
1416
[nextjournal.clojure-mode.test-utils :as test-utils]
15-
[react]
17+
["react" :as react]
1618
[reagent.core :as r]
1719
[reagent.dom :as rdom]))
1820

1921
(def theme
2022
(.theme EditorView
2123
(j/lit {".cm-content" {:white-space "pre-wrap"
22-
:padding "10px 0"}
23-
"&.cm-focused" {:outline "none"}
24+
:padding "10px 0"
25+
:flex "1 1 0"}
26+
27+
"&.cm-focused" {:outline "0 !important"}
2428
".cm-line" {:padding "0 9px"
2529
:line-height "1.6"
2630
:font-size "16px"
@@ -51,15 +55,15 @@
5155

5256
(defn editor [source {:keys [eval?]}]
5357
(r/with-let [!view (r/atom nil)
54-
last-result (when eval? (r/atom (sci/eval-string source)))
58+
last-result (when eval? (r/atom (demo.sci/eval-string source)))
5559
mount! (fn [el]
5660
(when el
5761
(reset! !view (new EditorView
5862
(j/obj :state
5963
(test-utils/make-state
6064
(cond-> #js [extensions]
61-
eval? (.concat #js [(sci/extension {:modifier "Alt"
62-
:on-result (partial reset! last-result)})]))
65+
eval? (.concat #js [(demo.sci/extension {:modifier "Alt"
66+
:on-result (partial reset! last-result)})]))
6367
source)
6468
:parent el)))))]
6569
[:div
@@ -74,8 +78,27 @@
7478
(react/isValidElement result) result
7579
'else (sv/inspect-paginated result)))])]
7680
(finally
77-
(j/call @!view :destroy))))
78-
81+
(j/call @!view :destroy))))
82+
83+
;; Markdown editors
84+
(defn markdown-editor [{:keys [doc extensions]}]
85+
[:div {:ref (fn [^js el]
86+
(when el
87+
(some-> el .-editorView .destroy)
88+
(j/assoc! el :editorView
89+
(EditorView. (j/obj :parent el
90+
:state (.create EditorState
91+
(j/obj :doc (str/trim doc)
92+
:extensions (into-array
93+
(cond-> [(syntaxHighlighting defaultHighlightStyle)
94+
(foldGutter)
95+
(.of view/keymap cm-clj/complete-keymap)
96+
(history)
97+
(.of view/keymap historyKeymap)
98+
theme
99+
livedoc/markdown-language-support]
100+
(seq extensions)
101+
(concat extensions))))))))))}])
79102

80103
(defn samples []
81104
(into [:<>]
@@ -123,7 +146,7 @@
123146
(when-not (zero? i) [:span " + "])
124147
[:kbd.kbd k]]) keys))))
125148

126-
(defn key-bindings-table []
149+
(defn key-bindings-table [keymap]
127150
[:table.w-full.text-sm
128151
[:thead
129152
[:tr.border-t
@@ -132,8 +155,7 @@
132155
[:th.px-3.py-1.align-top.text-left.text-xs.uppercase.font-normal.black-50 "Alternate Binding"]
133156
[:th.px-3.py-1.align-top.text-left.text-xs.uppercase.font-normal.black-50 {:style {:min-width 290}} "Description"]]]
134157
(into [:tbody]
135-
(->> keymap/paredit-keymap*
136-
(merge (sci/keymap* "Alt"))
158+
(->> keymap
137159
(sort-by first)
138160
(map (fn [[command [{:keys [key shift doc]} & [{alternate-key :key}]]]]
139161
[:<>
@@ -152,17 +174,59 @@
152174

153175
(defn ^:dev/after-load render []
154176
(rdom/render [samples] (js/document.getElementById "editor"))
155-
156177
(.. (js/document.querySelectorAll "[clojure-mode]")
157178
(forEach #(when-not (.-firstElementChild %)
158179
(rdom/render [editor (str/trim (.-innerHTML %))] %))))
159-
160180
(let [mapping (key-mapping)]
161181
(.. (js/document.querySelectorAll ".mod,.alt,.ctrl")
162182
(forEach #(when-let [k (get mapping (.-innerHTML %))]
163183
(set! (.-innerHTML %) k)))))
164184

165-
(rdom/render [key-bindings-table] (js/document.getElementById "docs"))
185+
;; set viewer tailwind stylesheet
186+
(j/assoc! (js/document.getElementById "viewer-stylesheet")
187+
:innerHTML (rc/inline "stylesheets/viewer.css"))
188+
189+
(rdom/render [key-bindings-table (merge keymap/paredit-keymap* (demo.sci/keymap* "Alt"))] (js/document.getElementById "docs"))
190+
(rdom/render [:div.rounded-md.mb-0.text-sm.monospace.overflow-auto.relative.border.shadow-lg.bg-white
191+
[markdown-editor {:doc "# Hello Markdown
192+
193+
Lezer [mounted trees](https://lezer.codemirror.net/docs/ref/#common.MountedTree) allows to
194+
have an editor with ~~mono~~ _mixed language support_.
195+
196+
```clojure
197+
(defn the-answer
198+
\"to all questions\"
199+
[]
200+
(inc 41))
201+
```
202+
203+
## Todo
204+
- [x] resolve **inner nodes**
205+
- [x] fix extra spacing when autoformatting after paredit movements
206+
- [x] fix errors when entering a newline
207+
- [ ] fix extra space when entering a newline
208+
- [x] fix nonsense deletions hitting delete key
209+
- [x] limit the scope of autoformat (TAB)
210+
- [x] limit the scope of kill*
211+
- [x] limit the scope of eval-region
212+
- [ ] restore autoformat when deleting
213+
- [x] keep parens balanced when deleting backward
214+
- [x] fix errors on Ctrl-K
215+
- [ ] fix dark theme
216+
- [ ] fix demo error: CssSyntaxError: <css input>:62:15: The `font-inter` class does not exist
217+
"}]] (js/document.getElementById "markdown-editor"))
166218

167219
(when (linux?)
168220
(js/twemoji.parse (.-body js/document))))
221+
222+
(comment
223+
(let [ctx' (sci.core/fork @sv/!sci-ctx)
224+
ctx'' (sci.core/merge-opts ctx' {:namespaces {'foo {'bar "ahoi"}}})]
225+
226+
(demo.sci/eval-string ctx'' "(def o (j/assoc! #js {:a 1} :b 2))")
227+
(demo.sci/eval-string ctx'' "(j/lookup (j/assoc! #js {:a 1} :b 2))")
228+
(demo.sci/eval-string ctx'' "(j/get o :b)")
229+
(demo.sci/eval-string ctx'' "(into-array [1 2 3])")
230+
231+
;; this is not evaluable as-is in sci
232+
(demo.sci/eval-string ctx'' "(j/let [^:js {:keys [a b]} o] (map inc [a b]))")))

0 commit comments

Comments
 (0)