Skip to content

Commit

Permalink
First cut of Clerk CLJS Editor component (#527)
Browse files Browse the repository at this point in the history
Adds `nextjournal.clerk.render.editor`, a clojure-mode based editor for Clerk documents in the browser. Uses completions based on the sci environment. Parses and evaluates the doc as a Clerk doc in ClojureScript.

https://snapshots.nextjournal.com/clerk/build/465f5351161eb28ad631bee197b34d6e0849bd6f/editor.html

---------

Co-authored-by: Philippa Markovics <philippa@markovics.com>
  • Loading branch information
mk and philippamarkovics authored Jun 26, 2023
1 parent 70af0c7 commit d801870
Show file tree
Hide file tree
Showing 14 changed files with 539 additions and 68 deletions.
4 changes: 3 additions & 1 deletion .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,9 @@ jobs:
uses: google-github-actions/setup-gcloud@v0.3.0

- name: 📓 Build Clerk Book
run: clojure -J-Dclojure.main.report=stderr -X:demo:nextjournal/clerk :git/sha '"${{ github.sha }}"' :git/url '"https://github.com/nextjournal/clerk"' :paths '["book.clj" "CHANGELOG.md"]'
run: |
cp notebooks/editor.clj editor.clj
clojure -J-Dclojure.main.report=stderr -X:demo:nextjournal/clerk :git/sha '"${{ github.sha }}"' :git/url '"https://github.com/nextjournal/clerk"' :paths '["book.clj" "CHANGELOG.md" "editor.clj"]'
- name: 🏗 Build Clerk Static App with default Notebooks
run: clojure -J-Dclojure.main.report=stderr -X:demo:nextjournal/clerk :git/sha '"${{ github.sha }}"' :git/url '"https://github.com/nextjournal/clerk"' :bundle true
Expand Down
9 changes: 9 additions & 0 deletions notebooks/editor.clj
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
(ns editor
{:nextjournal.clerk/visibility {:code :hide}
:nextjournal.clerk/doc-css-class [:overflow-hidden :p-0]}
(:require [nextjournal.clerk :as clerk]))

(clerk/with-viewer
{:render-fn 'nextjournal.clerk.render.editor/view
:transform-fn clerk/mark-presented}
(slurp "notebooks/rule_30.clj"))
3 changes: 2 additions & 1 deletion notebooks/rule_30.clj
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@
{:pred (every-pred list? (partial every? (some-fn number? vector?)))
:render-fn '#(into [:div.flex.flex-col] (nextjournal.clerk.render/inspect-children %2) %1)}
{:pred (every-pred vector? (complement map-entry?) (partial every? number?))
:render-fn '#(into [:div.flex.inline-flex] (nextjournal.clerk.render/inspect-children %2) %1)}])
:render-fn '#(into [:div.flex.inline-flex] (nextjournal.clerk.render/inspect-children %2) %1)}
{:pred var? :transform-fn (clerk/update-val deref)}])

(clerk/add-viewers! viewers)

Expand Down
2 changes: 1 addition & 1 deletion render/deps.edn
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
org.babashka/sci {:mvn/version "0.7.39"}
reagent/reagent {:mvn/version "1.2.0"}
io.github.babashka/sci.configs {:git/sha "0702ea5a21ad92e6d7cca6d36de84271083ea68f"}
io.github.nextjournal/clojure-mode {:git/sha "ac038ebf6e5da09dd2b8a31609e9ff4a65e36852"}
io.github.nextjournal/clojure-mode {:git/sha "1f55406087814a0dda6806396aa596dbe13ea302"}
thheller/shadow-cljs {:mvn/version "2.23.1"}
io.github.squint-cljs/cherry {;; :local/root "/Users/borkdude/dev/cherry"
:git/sha "ac89d93f136ee8fab91f62949de5b5822ba08b3c"}}}
5 changes: 1 addition & 4 deletions src/nextjournal/clerk/analyzer.clj
Original file line number Diff line number Diff line change
Expand Up @@ -310,9 +310,6 @@
(throw (ex-info (str "The var `#'" missing-dep "` is being referenced, but Clerk can't find it in the namespace's source code. Did you remove it? This validation can fail when the namespace is mutated programmatically (e.g. using `clojure.core/intern` or side-effecting macros). You can turn off this check by adding `{:nextjournal.clerk/error-on-missing-vars :off}` to the namespace metadata.")
{:var-name missing-dep :form form :file file #_#_:defined defined }))))))))

(defn filter-code-blocks-without-form [doc]
(update doc :blocks #(filterv (some-fn :form (complement parser/code?)) %)))

(defn ns-resolver [notebook-ns]
(if notebook-ns
(into {} (map (juxt key (comp ns-name val))) (ns-aliases notebook-ns))
Expand Down Expand Up @@ -363,7 +360,7 @@
(-> doc :blocks count range))
doc? (-> parser/add-block-settings
parser/add-open-graph-metadata
filter-code-blocks-without-form))))))
parser/filter-code-blocks-without-form))))))

#_(let [parsed (nextjournal.clerk.parser/parse-clojure-string "clojure.core/dec")]
(build-graph (analyze-doc parsed)))
Expand Down
1 change: 1 addition & 0 deletions src/nextjournal/clerk/builder.clj
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
"controlling_width"
"docs"
"document_linking"
"editor"
"hello"
"how_clerk_works"
"exec_status"
Expand Down
3 changes: 3 additions & 0 deletions src/nextjournal/clerk/parser.cljc
Original file line number Diff line number Diff line change
Expand Up @@ -372,6 +372,9 @@
#_(runnable-code-block? {:type :code :language "clojure" :info "clojure"})
#_(runnable-code-block? {:type :code :language "clojure" :info "clojure {:nextjournal.clerk/code-listing true}"})

(defn filter-code-blocks-without-form [doc]
(update doc :blocks #(filterv (some-fn :form (complement code?)) %)))

(defn parse-markdown-string [{:as opts :keys [doc?]} s]
(let [{:as ctx :keys [content]} (parse-markdown (markdown-context) s)]
(loop [{:as state :keys [nodes] ::keys [md-slice]} {:blocks [] ::md-slice [] :nodes content :md-context ctx}]
Expand Down
31 changes: 6 additions & 25 deletions src/nextjournal/clerk/render.cljs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
[nextjournal.clerk.render.code :as code]
[nextjournal.clerk.render.context :as view-context]
[nextjournal.clerk.render.hooks :as hooks]
[nextjournal.clerk.render.localstorage :as localstorage]
[nextjournal.clerk.render.navbar :as navbar]
[nextjournal.clerk.render.panel :as panel]
[nextjournal.clerk.viewer :as viewer]
Expand All @@ -34,12 +33,12 @@
(defn reagent-atom? [x]
(satisfies? ratom/IReactiveAtom x))

(defn dark-mode-toggle [!dark-mode?]
(defn dark-mode-toggle []
(let [spring {:type :spring :stiffness 200 :damping 10}]
[:div.relative.dark-mode-toggle
[:button.text-slate-400.hover:text-slate-600.dark:hover:text-white.cursor-pointer
{:on-click #(swap! !dark-mode? not)}
(if @!dark-mode?
{:on-click #(swap! code/!dark-mode? not)}
(if @code/!dark-mode?
[:> (.-svg motion)
{:xmlns "http://www.w3.org/2000/svg"
:class "w-5 h-5 md:w-4 md:h-4"
Expand Down Expand Up @@ -79,23 +78,6 @@
[:circle {:cx "20.9101" :cy "6.8555" :r "1.71143" :transform "rotate(-120 20.9101 6.8555)" :fill "currentColor"}]
[:circle {:cx "12" :cy "1.71143" :r "1.71143" :fill "currentColor"}]]])]]))

(def local-storage-dark-mode-key "clerk-darkmode")

(defn set-dark-mode! [dark-mode?]
(let [class-list (.-classList (js/document.querySelector "html"))]
(if dark-mode?
(.add class-list "dark")
(.remove class-list "dark")))
(localstorage/set-item! local-storage-dark-mode-key dark-mode?))

(defn setup-dark-mode! [!dark-mode?]
(add-watch !dark-mode? ::dark-mode-watch
(fn [_ _ old dark-mode?]
(when (not= old dark-mode?)
(set-dark-mode! dark-mode?))))
(when @!dark-mode?
(set-dark-mode! @!dark-mode?)))

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

(defn exec-status [{:keys [progress cell-progress status]}]
Expand Down Expand Up @@ -149,10 +131,9 @@

(defn render-notebook [{:as doc xs :blocks :keys [bundle? doc-css-class sidenotes? toc toc-visibility header footer]}
{:as render-opts :keys [!expanded-at expandable-toc?]}]
(r/with-let [!dark-mode? (r/atom (localstorage/get-item local-storage-dark-mode-key))
root-ref-fn (fn [el]
(r/with-let [root-ref-fn (fn [el]
(when (and el (exists? js/document))
(setup-dark-mode! !dark-mode?)
(code/setup-dark-mode!)
(when-some [heading (when (and (exists? js/location) (not bundle?))
(try (some-> js/location .-hash not-empty js/decodeURI (subs 1) js/document.getElementById)
(catch js/Error _
Expand All @@ -163,7 +144,7 @@
[:div.flex
{:ref root-ref-fn}
[:div.fixed.top-2.left-2.md:left-auto.md:right-2.z-10
[dark-mode-toggle !dark-mode?]]
[dark-mode-toggle]]
(when (and toc toc-visibility)
[navbar/view toc (assoc render-opts :set-hash? (not bundle?) :toc-visibility toc-visibility)])
[:div.flex-auto.w-screen.scroll-container
Expand Down
53 changes: 49 additions & 4 deletions src/nextjournal/clerk/render/code.cljs
Original file line number Diff line number Diff line change
@@ -1,15 +1,37 @@
(ns nextjournal.clerk.render.code
(:require ["@codemirror/language" :refer [HighlightStyle syntaxHighlighting LanguageDescription]]
["@codemirror/state" :refer [EditorState RangeSet RangeSetBuilder Text]]
["@codemirror/state" :refer [Compartment EditorState RangeSet RangeSetBuilder Text]]
["@codemirror/view" :refer [EditorView Decoration]]
["@lezer/highlight" :refer [tags highlightTree]]
["@nextjournal/lang-clojure" :refer [clojureLanguage]]
[applied-science.js-interop :as j]
[clojure.string :as str]
[nextjournal.clerk.render.hooks :as hooks]
[nextjournal.clerk.render.localstorage :as localstorage]
[nextjournal.clojure-mode :as clojure-mode]
[reagent.core :as r]
[shadow.esm]))

(def local-storage-dark-mode-key "clerk-darkmode")

(def !dark-mode?
(r/atom (boolean (localstorage/get-item local-storage-dark-mode-key))))

(defn set-dark-mode! [dark-mode?]
(let [class-list (.-classList (js/document.querySelector "html"))]
(if dark-mode?
(.add class-list "dark")
(.remove class-list "dark")))
(localstorage/set-item! local-storage-dark-mode-key dark-mode?))

(defn setup-dark-mode! []
(add-watch !dark-mode? ::dark-mode-watch
(fn [_ _ old dark-mode?]
(when (not= old dark-mode?)
(set-dark-mode! dark-mode?))))
(when @!dark-mode?
(set-dark-mode! @!dark-mode?)))

(def highlight-style
(.define HighlightStyle
(clj->js [{:tag (.-meta tags) :class "cmt-meta"}
Expand Down Expand Up @@ -131,7 +153,7 @@
[highlight-imported-language {:code code :language language}])]])

;; editable code viewer
(def theme
(defn get-theme []
(.theme EditorView
(j/lit {"&.cm-focused" {:outline "none"}
".cm-line" {:padding "0"
Expand All @@ -151,7 +173,22 @@
:overflow "hidden"}
".cm-tooltip > ul > li" {:padding "3px 10px 3px 0 !important"}
".cm-tooltip > ul > li:first-child" {:border-top-left-radius "3px"
:border-top-right-radius "3px"}})))
:border-top-right-radius "3px"}
".cm-tooltip.cm-tooltip-autocomplete" {:border "0"
:border-radius "6px"
:box-shadow "0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1)"
"& > ul" {:font-size "12px"
:font-family "'Fira Code', monospace"
:background "rgb(241 245 249)"
:border "1px solid rgb(203 213 225)"
:border-radius "6px"}}
".cm-tooltip-autocomplete ul li[aria-selected]" {:background "rgb(79 70 229)"
:color "#fff"}
".cm-tooltip.cm-tooltip-hover" {:background "rgb(241 245 249)"
:border-radius "6px"
:border "1px solid rgb(203 213 225)"
:box-shadow "0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1)"
:max-width "550px"}}) #js {:dark @!dark-mode?}))

(def read-only (.. EditorView -editable (of false)))

Expand All @@ -161,10 +198,17 @@
(when (.-docChanged tr) (f (.. tr -state sliceDoc)))
#js {}))))

(def theme (Compartment.))

(defn use-dark-mode [!view]
(hooks/use-effect (fn []
(add-watch !dark-mode? ::dark-mode #(.dispatch @!view #js {:effects (.reconfigure theme (get-theme))}))
#(remove-watch !dark-mode? ::dark-mode))))

(def ^:export default-extensions
#js [clojure-mode/default-extensions
(syntaxHighlighting highlight-style)
theme])
(.of theme (get-theme))])

(defn make-state [doc extensions]
(.create EditorState (j/obj :doc doc :extensions extensions)))
Expand Down Expand Up @@ -196,4 +240,5 @@
(j/lit {:changes [{:insert @!code-str
:from 0 :to (.. state -doc -length)}]}))))))
[@!code-str])
(use-dark-mode !view)
[:div {:ref !container-el}])))
Loading

0 comments on commit d801870

Please sign in to comment.