diff --git a/.clj-kondo/config.edn b/.clj-kondo/config.edn index 7eb679f6f..42ed11e51 100644 --- a/.clj-kondo/config.edn +++ b/.clj-kondo/config.edn @@ -7,7 +7,8 @@ taoensso.encore/defonce clojure.core/defonce taoensso.encore/defalias clojure.core/def promesa.core/let clojure.core/let - shadow.cljs.modern/defclass clojure.core/defprotocol} + shadow.cljs.modern/defclass clojure.core/defprotocol + nextjournal.clerk.utils/if-bb clojure.core/if} :linters {:clojure-lsp/unused-public-var {:level :off} :consistent-alias {:aliases {datomic.api datomic clojure.string str diff --git a/bb-runtime.edn b/bb-runtime.edn new file mode 100644 index 000000000..fa47427c6 --- /dev/null +++ b/bb-runtime.edn @@ -0,0 +1,31 @@ +{:min-bb-version "1.12.200" + :paths ["src" "notebooks" "resources"] + :deps {io.github.nextjournal/dejavu {:git/sha "4980e0cc18c9b09fb220874ace94ba6b57a749ca"} + io.github.nextjournal/cas-client {:git/sha "d9f838937ebc8b645fe5764949e72a6df8e344de"} + mvxcvi/multiformats {:git/url "https://github.com/greglook/clj-multiformats" + :git/sha "1189f1fb26db180cd8dcfd50518cdf553c0ff9e1"} + hiccup/hiccup {:mvn/version "2.0.0-alpha2"} + io.github.nextjournal/markdown {:mvn/version "0.6.157"} + io.github.nextjournal/clerk-slideshow {:git/sha "562f634494a1e1a9149ed78d5d39fd9486cc00ba"} + weavejester/dependency {:mvn/version "0.2.1"} + io.github.babashka/sci.nrepl {:mvn/version "0.0.2"}} + :tasks + {dev + {:requires ([babashka.fs :as fs] + [babashka.nrepl.server :as srv] + [nextjournal.clerk :as clerk] + [babashka.cli :as cli]) + :task (do (srv/start-server! {:host "localhost" :port 1339}) + (spit ".nrepl-port" "1339") + (nextjournal.clerk/serve! (cli/parse-opts *command-line-args*)) + (-> (Runtime/getRuntime) + (.addShutdownHook (Thread. (fn [] + (nextjournal.clerk/halt!) + (fs/delete ".nrepl-port"))))) + (deref (promise)))} + + build + {:requires ([nextjournal.clerk :as clerk] + [babashka.cli :as cli]) + :task (let [spec (-> (resolve 'nextjournal.clerk/build!) meta :org.babashka/cli)] + (clerk/build! (cli/parse-opts *command-line-args* spec)))}}} diff --git a/src/nextjournal/beholder.bb b/src/nextjournal/beholder.bb new file mode 100644 index 000000000..e6d96e0c9 --- /dev/null +++ b/src/nextjournal/beholder.bb @@ -0,0 +1,5 @@ +(ns nextjournal.beholder + "Babashka runtime no-op stubs") + +(defn watch [cb & args] nil) +(defn stop [w] nil) diff --git a/src/nextjournal/clerk/always_array_map.clj b/src/nextjournal/clerk/always_array_map.clj index 908aed0e0..6ec46abdc 100644 --- a/src/nextjournal/clerk/always_array_map.clj +++ b/src/nextjournal/clerk/always_array_map.clj @@ -1,6 +1,7 @@ (ns nextjournal.clerk.always-array-map "A persistent data structure that is based on array-map, but doesn't turn into a hash-map by using assoc etc. - Prints like a normal Clojure map in the order of insertion.") + Prints like a normal Clojure map in the order of insertion." + (:require [nextjournal.clerk.utils :as utils])) (set! *warn-on-reflection* true) @@ -9,52 +10,54 @@ (declare ->AlwaysArrayMap) -(deftype AlwaysArrayMap [^clojure.lang.PersistentArrayMap the-map] - clojure.lang.ILookup - (valAt [_ k] - (get the-map k)) +(utils/if-bb + nil + (deftype AlwaysArrayMap [^clojure.lang.PersistentArrayMap the-map] + clojure.lang.ILookup + (valAt [_ k] + (get the-map k)) - clojure.lang.Seqable - (seq [_] - (seq the-map)) + clojure.lang.Seqable + (seq [_] + (seq the-map)) - clojure.lang.IPersistentMap - (assoc [_ k v] - (if (< (count the-map) 8) - (->AlwaysArrayMap (assoc the-map k v)) - (->AlwaysArrayMap (assoc-after the-map k v)))) + clojure.lang.IPersistentMap + (assoc [_ k v] + (if (< (count the-map) 8) + (->AlwaysArrayMap (assoc the-map k v)) + (->AlwaysArrayMap (assoc-after the-map k v)))) - (assocEx [_ _k _v] - (throw (ex-info "Not implemented" {}))) + (assocEx [_ _k _v] + (throw (ex-info "Not implemented" {}))) - (without [_ k] - (->AlwaysArrayMap (dissoc the-map k))) + (without [_ k] + (->AlwaysArrayMap (dissoc the-map k))) - clojure.lang.Associative - (containsKey [_ k] - (contains? the-map k)) + clojure.lang.Associative + (containsKey [_ k] + (contains? the-map k)) - clojure.lang.IPersistentCollection - (equiv [_ other] - (= the-map other)) - (count [_] - (count the-map)) + clojure.lang.IPersistentCollection + (equiv [_ other] + (= the-map other)) + (count [_] + (count the-map)) - java.lang.Iterable - (iterator [_] - (.iterator the-map)) + java.lang.Iterable + (iterator [_] + (.iterator the-map)) - clojure.lang.IMeta - (meta [_] - (meta the-map)) + clojure.lang.IMeta + (meta [_] + (meta the-map)) - clojure.lang.IObj - (withMeta [_ meta] - (->AlwaysArrayMap (with-meta the-map meta))) + clojure.lang.IObj + (withMeta [_ meta] + (->AlwaysArrayMap (with-meta the-map meta))) - Object - (toString [_] - "")) + Object + (toString [_] + ""))) (defn assoc-before [aam k v] (->AlwaysArrayMap (apply array-map (list* k v (interleave (keys aam) (vals aam)))))) @@ -62,19 +65,21 @@ (defn always-array-map [& kvs] (->AlwaysArrayMap (apply array-map kvs))) -(defmethod print-method AlwaysArrayMap - [v ^java.io.Writer writer] - (.write writer "{") - (let [write-kv! (fn [k v] - (.write writer (pr-str k)) - (.write writer " ") - (.write writer (pr-str v)))] - (doseq [[k v] (butlast v)] - (write-kv! k v) - (.write writer ", ")) - (let [[k v] (last v)] - (write-kv! k v))) - (.write writer "}")) +(utils/if-bb + nil + (defmethod print-method AlwaysArrayMap + [v ^java.io.Writer writer] + (.write writer "{") + (let [write-kv! (fn [k v] + (.write writer (pr-str k)) + (.write writer " ") + (.write writer (pr-str v)))] + (doseq [[k v] (butlast v)] + (write-kv! k v) + (.write writer ", ")) + (let [[k v] (last v)] + (write-kv! k v))) + (.write writer "}"))) (comment (pr-str (always-array-map 1 2)) diff --git a/src/nextjournal/clerk/analyzer.bbold b/src/nextjournal/clerk/analyzer.bbold new file mode 100644 index 000000000..3fbe94d7a --- /dev/null +++ b/src/nextjournal/clerk/analyzer.bbold @@ -0,0 +1,6 @@ +(ns nextjournal.clerk.analyzer + "Babashka runtime no-op stubs") + +;; TODO: consider using this in eval +(defn valuehash [val] "valuehash") +(defn ->hash-str [val] (valuehash val)) diff --git a/src/nextjournal/clerk/analyzer.clj b/src/nextjournal/clerk/analyzer.clj index 1e6ff5b88..a548a65eb 100644 --- a/src/nextjournal/clerk/analyzer.clj +++ b/src/nextjournal/clerk/analyzer.clj @@ -12,10 +12,13 @@ [nextjournal.clerk.classpath :as cp] [nextjournal.clerk.config :as config] [nextjournal.clerk.parser :as parser] + [nextjournal.clerk.utils :as utils] [nextjournal.clerk.walk :as walk] - [taoensso.nippy :as nippy] [weavejester.dependency :as dep])) +(when-not utils/bb? + (require '[taoensso.nippy :as nippy])) + (set! *warn-on-reflection* true) (defn deref? [form] @@ -646,10 +649,12 @@ (let [digest-fn (case hash-type :sha1 sha1-base58 :sha512 sha2-base58)] - (binding [nippy/*incl-metadata?* false] - (-> value - nippy/fast-freeze - digest-fn))))) + (utils/if-bb (-> value digest-fn) + #_{:clj-kondo/ignore [:unresolved-namespace]} + (binding [nippy/*incl-metadata?* false] + (-> value + nippy/fast-freeze + digest-fn)))))) #_(valuehash (range 100)) #_(valuehash :sha1 (range 100)) diff --git a/src/nextjournal/clerk/builder.clj b/src/nextjournal/clerk/builder.clj index 7b58de6a9..03c161d37 100644 --- a/src/nextjournal/clerk/builder.clj +++ b/src/nextjournal/clerk/builder.clj @@ -5,7 +5,6 @@ [clojure.java.browse :as browse] [clojure.java.io :as io] [clojure.string :as str] - [nextjournal.clerk.analyzer :as analyzer] [nextjournal.clerk.builder-ui :as builder-ui] [nextjournal.clerk.config :as config] [nextjournal.clerk.eval :as eval] @@ -301,7 +300,7 @@ {state :result duration :time-ms} (eval/time-ms (mapv (comp (partial parser/parse-file {:doc? true}) :file) state)) _ (report-fn {:stage :parsed :state state :duration duration}) {state :result duration :time-ms} (eval/time-ms (reduce (fn [state doc] - (try (conj state (-> doc analyzer/build-graph analyzer/hash)) + (try (conj state (eval/analyze-doc doc)) (catch Exception e (reduced {:error e :file (:file doc)})))) [] diff --git a/src/nextjournal/clerk/eval.bb b/src/nextjournal/clerk/eval.bb new file mode 100644 index 000000000..28ad406f3 --- /dev/null +++ b/src/nextjournal/clerk/eval.bb @@ -0,0 +1,168 @@ +(ns nextjournal.clerk.eval + "Clerk's incremental evaluation (Babashka Edition) with in-memory caching layer." + (:refer-clojure :exclude [read-string]) + (:require [clojure.string :as str] + [edamame.core :as edamame] + [nextjournal.clerk.config :as config] + [nextjournal.clerk.parser :as parser] + [nextjournal.clerk.viewer :as v])) + +(defn wrapped-with-metadata [value hash] + (cond-> {:nextjournal/value value} + ;; TODO: maybe fix hash for blob serving + hash (assoc :nextjournal/blob-id (cond-> hash (not (string? hash)) str #_ multihash/base58)))) + +#_(wrap-with-blob-id :test "foo") + +(defn elapsed-ms [from] + (/ (double (- (. System (nanoTime)) from)) 1000000.0)) + +(defmacro time-ms + "Pure version of `clojure.core/time`. Returns a map with `:result` and `:time-ms` keys." + [expr] + `(let [start# (System/nanoTime) + ret# ~expr] + {:result ret# + :time-ms (elapsed-ms start#)})) + +(defn ^:private var-from-def [var] + (let [resolved-var (cond (var? var) + var + + (symbol? var) + (find-var var) + + :else + (throw (ex-info "Unable to resolve into a variable" {:data var})))] + {:nextjournal.clerk/var-from-def resolved-var})) + +(defn ^:private eval-form [{:keys [form var no-cache?]} hash] + (try + (let [{:keys [result]} (time-ms (binding [config/*in-clerk* true] + (eval form))) + result (if (and (nil? result) var (= 'defonce (first form))) + (find-var var) + result) + var-value (cond-> result (and var (var? result)) deref) + no-cache? (or no-cache? config/cache-disabled?)] + (let [blob-id (cond no-cache? "valuehash" #_#_ TODO?/valuehash (analyzer/->hash-str var-value) + (fn? var-value) nil + :else hash) + result (if var (var-from-def var) result)] + (wrapped-with-metadata result blob-id))) + (catch Throwable t + (throw (ex-info (ex-message t) (Throwable->map t)))))) + +(defn maybe-eval-viewers [{:as opts :nextjournal/keys [viewer viewers]}] + (cond-> opts + viewer + (update :nextjournal/viewer eval) + viewers + (update :nextjournal/viewers eval))) + +(defn read+eval-cached [{:as _doc :keys [blob->result]} {:as codeblock :keys [form vars var ns-effect? no-cache?]}] + (let [no-cache? (or ns-effect? no-cache?) + hash (.encodeToString (java.util.Base64/getEncoder) (.getBytes (str form))) + opts-from-form-meta (-> (meta form) + (select-keys [:nextjournal.clerk/viewer :nextjournal.clerk/viewers :nextjournal.clerk/width :nextjournal.clerk/opts]) + v/normalize-viewer-opts + maybe-eval-viewers)] + (cond-> (or (when-let [cached-result (and (not no-cache?) (get-in blob->result [hash :nextjournal/value]))] + (wrapped-with-metadata cached-result hash)) + (eval-form codeblock hash)) + (seq opts-from-form-meta) + (merge opts-from-form-meta)))) + +(defn eval-analyzed-doc [{:as analyzed-doc :keys [ns blocks]}] + (let [{:as evaluated-doc :keys [blob-ids]} + (reduce (fn [state {:as cell :keys [type]}] + (let [{:as result :nextjournal/keys [blob-id]} (when (= :code type) (read+eval-cached state cell))] + (cond-> (update state :blocks conj (cond-> cell result (assoc :result result))) + blob-id (update :blob-ids conj blob-id) + blob-id (assoc-in [:blob->result blob-id] result)))) + (assoc analyzed-doc :blocks [] :blob-ids #{}) + blocks)] + (-> evaluated-doc + (cond-> (not ns) (assoc :ns (find-ns 'user))) + (update :blob->result select-keys blob-ids) + (dissoc :blob-ids)))) + +(defn read-string [s] + (edamame/parse-string s + {:all true + :readers *data-readers* + :read-cond :allow + :regex #(list `re-pattern %) + :features #{:clj} + :auto-resolve (as-> (ns-aliases (or *ns* (find-ns 'user))) $ + (zipmap (keys $) (map ns-name (vals $))) + (assoc $ :current (ns-name *ns*)))})) + +(defn deflike? [form] (and (seq? form) (symbol? (first form)) (str/starts-with? (name (first form)) "def"))) +#_(deflike? (read-string "(def ^{:doc \"aloha\"} foo 123)")) +#_(deflike? (read-string "(def ^{:doc \"aloha\"} foo 123)")) +(defn no-cache-from-meta [form] + (when (contains? (meta form) :nextjournal.clerk/no-cache) + (-> form meta :nextjournal.clerk/no-cache))) +(defn no-cache? [& subjects] (or (some no-cache-from-meta subjects) false)) +(defn deref? [form] + (and (seq? form) + (= (first form) `deref) + (= 2 (count form)))) + +(defn read-forms [doc] + (binding [*ns* *ns*] + (reduce (fn [doc {:as b :keys [type text]}] + (let [form (read-string text) + ns? (= 'ns (when (list? form) (first form))) + var (when (and (deflike? form) (symbol? (second form))) (second form))] + (when ns? (eval form)) + (-> doc + (cond-> (and ns? (not (:ns doc))) (assoc :ns *ns*)) + (update :blocks conj + (cond-> b + (= :code type) (assoc :form form) + (or ns? (deref? form) (no-cache? form var *ns*)) (assoc :no-cache? true) + var (assoc :var (symbol (name (ns-name *ns*)) (name var)))))))) + (assoc doc :blocks []) + (:blocks doc)))) + +#_(read-forms + (parser/parse-file "notebooks/hello.clj")) + +;; used in builder +(def analyze-doc read-forms) + +(defn +eval-results + "Evaluates the given `parsed-doc` using the `in-memory-cache` and augments it with the results." + [in-memory-cache parsed-doc] + (let [{:as doc :keys [ns]} (read-forms parsed-doc)] + (binding [*ns* (or ns *ns*)] + (-> doc + (assoc :blob->result in-memory-cache) + eval-analyzed-doc)))) + +(defn eval-doc + "Evaluates the given `doc`." + ([doc] (eval-doc {} doc)) + ([in-memory-cache doc] (+eval-results in-memory-cache doc))) + +(defn eval-file + "Reads given `file` (using `slurp`) and evaluates it." + ([file] (eval-file {} file)) + ([in-memory-cache file] + (->> file + (parser/parse-file {:doc? true}) + (eval-doc in-memory-cache)))) + +#_(eval-file "notebooks/hello.clj") +#_(eval-file "notebooks/rule_30.clj") +#_(eval-file "notebooks/visibility.clj") + +(defn eval-string + "Evaluated the given `code-string` using the optional `in-memory-cache` map." + ([code-string] (eval-string {} code-string)) + ([in-memory-cache code-string] + (eval-doc in-memory-cache (parser/parse-clojure-string {:doc? true} code-string)))) + +#_(eval-string "(+ 39 3)") diff --git a/src/nextjournal/clerk/eval.clj b/src/nextjournal/clerk/eval.clj index 4989bd098..454f29a70 100644 --- a/src/nextjournal/clerk/eval.clj +++ b/src/nextjournal/clerk/eval.clj @@ -261,6 +261,9 @@ (boolean (and (string? (:file doc)) (str/ends-with? (:file doc) ".cljs")))) +;; TODO: used in builder to drop analyzer dependency, cfr. below +(defn analyze-doc [doc] (-> doc analyzer/build-graph analyzer/hash)) + (defn +eval-results "Evaluates the given `parsed-doc` using the `in-memory-cache` and augments it with the results." [in-memory-cache {:as parsed-doc :keys [set-status-fn no-cache]}] @@ -313,5 +316,4 @@ (eval-doc in-memory-cache (parser/parse-clojure-string code-string)))) #_(eval-string "(+ 39 3)") - #_(nextjournal.clerk/show! "notebooks/hello.md") diff --git a/src/nextjournal/clerk/parser.cljc b/src/nextjournal/clerk/parser.cljc index 5f5e9e56d..994c018d8 100644 --- a/src/nextjournal/clerk/parser.cljc +++ b/src/nextjournal/clerk/parser.cljc @@ -1,7 +1,10 @@ (ns nextjournal.clerk.parser "Clerk's Parser turns Clojure & Markdown files and strings into Clerk documents." (:refer-clojure :exclude [read-string]) - (:require #?@(:clj [[clojure.tools.reader :as tools.reader] + (:require #?@(:bb [[clojure.tools.reader :as tools.reader] + [multiformats.base.b58 :as b58] + [multiformats.hash :as hash]] + :clj [[clojure.tools.reader :as tools.reader] [taoensso.nippy :as nippy] [multiformats.base.b58 :as b58] [multiformats.hash :as hash]] @@ -366,7 +369,8 @@ (guess-var form))] var (let [hash-fn (fn [x] - #?(:clj (-> x nippy/fast-freeze sha1-base58) + #?(:bb (sha1-base58 x) + :clj (-> x nippy/fast-freeze sha1-base58) :cljs (hash-sha1 x)))] (symbol (str *ns*) (case type diff --git a/src/nextjournal/clerk/utils.clj b/src/nextjournal/clerk/utils.clj new file mode 100644 index 000000000..7371bba89 --- /dev/null +++ b/src/nextjournal/clerk/utils.clj @@ -0,0 +1,14 @@ +(ns nextjournal.clerk.utils) + +(def bb? (System/getProperty "babashka.version")) + +(defmacro if-bb [then else] + (if bb? then else)) + +(defmacro when-bb [& body] + (when bb? + `(do ~@body))) + +(defmacro when-not-bb [& body] + (when (not bb?) + `(do ~@body))) diff --git a/src/nextjournal/clerk/viewer.cljc b/src/nextjournal/clerk/viewer.cljc index 424abc856..7124ee25a 100644 --- a/src/nextjournal/clerk/viewer.cljc +++ b/src/nextjournal/clerk/viewer.cljc @@ -1,14 +1,13 @@ (ns nextjournal.clerk.viewer (:refer-clojure :exclude [var?]) - (:require [clojure.string :as str] + (:require [clojure.datafy :as datafy] [clojure.pprint :as pprint] - [clojure.datafy :as datafy] [clojure.set :as set] + [clojure.string :as str] [flatland.ordered.map :as omap :refer [ordered-map]] #?@(:clj [[babashka.fs :as fs] [clojure.repl :refer [demunge]] [clojure.tools.reader :as tools.reader] - [editscript.edit] [nextjournal.clerk.config :as config] [nextjournal.clerk.analyzer :as analyzer]] :cljs [[goog.crypt] @@ -18,12 +17,19 @@ [sci.impl.vars] [sci.lang] [applied-science.js-interop :as j]]) + #?@(:bb [] + :clj [[editscript.edit]]) [nextjournal.clerk.parser :as parser] [nextjournal.clerk.walk :as w] [nextjournal.markdown :as md] - [nextjournal.markdown.utils :as md.utils] - [nextjournal.markdown.transform :as md.transform]) - #?(:clj (:import (com.pngencoder PngEncoder) + [nextjournal.markdown.transform :as md.transform] + [nextjournal.markdown.utils :as md.utils]) + #?(:bb (:import (clojure.lang IDeref IAtom) + (java.nio.file Files StandardOpenOption) + (java.net URI URL) + (java.util Base64) + (java.lang Throwable)) + :clj (:import (com.pngencoder PngEncoder) (clojure.lang IDeref IAtom) (java.lang Throwable) (java.awt.image BufferedImage) @@ -39,7 +45,8 @@ (-invoke [_ x y] (@f x y))])) ;; Make sure `RenderFn` is changed atomically -#?(:clj +#?(:bb nil + :clj (extend-protocol editscript.edit/IType RenderFn (get-type [_] :val))) @@ -89,6 +96,11 @@ (.write w (if-let [opts (not-empty (dissoc (into {} v) :f :form))] (str "#clerk/render-fn+opts " [opts (:form v)]) (str "#clerk/render-fn " (:form v)))))) + +#?(:bb + (defn ordered-map-reader-bb [coll] + (omap/ordered-map coll))) + #?(:cljs (defn ordered-map-reader-cljs [coll] (omap/ordered-map (vec coll)))) @@ -97,7 +109,8 @@ {'clerk/render-fn ->render-fn 'clerk/render-fn+opts ->render-fn+opts 'clerk/unreadable-edn eval - 'ordered/map #?(:clj omap/ordered-map-reader-clj + 'ordered/map #?(:bb ordered-map-reader-bb + :clj omap/ordered-map-reader-clj :cljs ordered-map-reader-cljs)}) #_(binding [*data-readers* {'render-fn ->render-fn}] @@ -377,6 +390,10 @@ (= x (-> x str tools.reader/read-string)) (catch Exception _e false)))) +#?(:bb (defn print-simple [o, ^java.io.Writer w] + #_(print-meta o w) + (.write w (str o)))) + #?(:clj (defmethod print-method clojure.lang.Keyword [o w] (if (roundtrippable? o) @@ -599,7 +616,8 @@ (= :single-file package) (data-uri-base64-encode (fs/read-all-bytes src) (Files/probeContentType (fs/path src))) :else (str "/_fs/" src)))) -#?(:clj +#?(:bb nil + :clj (defn read-image [image-or-url] (ImageIO/read (if (string? image-or-url) @@ -614,8 +632,9 @@ (defn md-image->viewer [doc block-id idx {:keys [attrs]}] (with-viewer `html-viewer #?(:clj {:nextjournal/render-opts {:id (processed-block-id block-id [idx])} - :nextjournal/width (try (image-width (read-image (:src attrs))) - (catch Throwable _ :prose))}) + :nextjournal/width #?(:bb :prose + :clj (try (image-width (read-image (:src attrs))) + (catch Throwable _ :prose)))}) [:div.flex.flex-col.items-center.not-prose.mb-4 [:img (update attrs :src process-image-source doc)]])) @@ -944,7 +963,24 @@ (partial present-ex-data wrapped-value) datafy/datafy))))}) -#?(:clj +(def buffered-image-viewer #?(:bb {} + :cljs nil + :clj {:pred #(instance? BufferedImage %) + :transform-fn (fn [{image :nextjournal/value}] + (let [w (.getWidth image) + h (.getHeight image) + r (float (/ w h))] + (-> {:nextjournal/value (.. (PngEncoder.) + (withBufferedImage image) + (withCompressionLevel 1) + (toBytes)) + :nextjournal/content-type "image/png" + :nextjournal/width (if (and (< 2 r) (< 900 w)) :full :wide)} + mark-presented))) + :render-fn '(fn [blob] (v/html [:figure.flex.flex-col.items-center.not-prose [:img {:src (v/url-for blob)}]]))})) + +#?(:bb nil + :clj (defn buffered-image->bytes [^BufferedImage image] (.. (PngEncoder.) (withBufferedImage image) @@ -952,7 +988,8 @@ (toBytes)))) (def image-viewer - {#?@(:clj [:pred #(instance? BufferedImage %) + {#?@(:bb [] + :clj [:pred #(instance? BufferedImage %) :transform-fn (fn [{image :nextjournal/value}] (-> {:nextjournal/value (buffered-image->bytes image) :nextjournal/content-type "image/png" @@ -964,16 +1001,18 @@ :cljs blob-or-url)}]])}) (def ideref-viewer - {:name `ideref-viewer - :pred #(#?(:clj instance? :cljs satisfies?) IDeref %) - :transform-fn (update-val (fn [ideref] - (with-viewer `tagged-value-viewer - {:tag "object" - :value (vector (symbol (pr-str (type ideref))) - #?(:clj (with-viewer `number-hex-viewer (System/identityHashCode ideref))) - (if-let [deref-as-map (resolve 'clojure.core/deref-as-map)] - (deref-as-map ideref) - (deref ideref)))})))}) + #?(:bb {} + :default + {:name `ideref-viewer + :pred #(#?(:clj instance? :cljs satisfies?) IDeref %) + :transform-fn (update-val (fn [ideref] + (with-viewer `tagged-value-viewer + {:tag "object" + :value (vector (symbol (pr-str (type ideref))) + #?(:clj (with-viewer `number-hex-viewer (System/identityHashCode ideref))) + (if-let [deref-as-map (resolve 'clojure.core/deref-as-map)] + (deref-as-map ideref) + (deref ideref)))})))})) (def regex-viewer {:name `regex-viewer @@ -1879,7 +1918,9 @@ ([image-or-url] (image {} image-or-url)) ([viewer-opts image-or-url] (with-viewer (:name image-viewer) viewer-opts - #?(:cljs image-or-url :clj (read-image image-or-url))))) + #?(:cljs image-or-url + :bb image-or-url + :clj (read-image image-or-url))))) (defn caption [text content] (col diff --git a/src/nextjournal/clerk/webserver.clj b/src/nextjournal/clerk/webserver.clj index 610116077..34dfcad68 100644 --- a/src/nextjournal/clerk/webserver.clj +++ b/src/nextjournal/clerk/webserver.clj @@ -6,16 +6,25 @@ [clojure.pprint :as pprint] [clojure.set :as set] [clojure.string :as str] - [editscript.core :as editscript] [nextjournal.clerk.config :as config] [nextjournal.clerk.git :as git] [nextjournal.clerk.paths :as paths] + [nextjournal.clerk.utils :as u] [nextjournal.clerk.view :as view] [nextjournal.clerk.viewer :as v] + [nextjournal.markdown :as md] [org.httpkit.server :as httpkit] [sci.nrepl.browser-server :as sci.nrepl]) (:import (java.nio.file Files))) +(u/if-bb + (require '[editscript.core :as-alias editscript]) + (require '[editscript.core :as editscript])) + +(def help-doc +{:ns *ns* +:blocks [{:type :markdown :doc (md/parse "Use `nextjournal.clerk/show!` to make your notebook appear…")}]}) + (defonce !clients (atom #{})) (defonce !doc (atom nil)) (defonce !last-sender-ch (atom nil)) @@ -153,7 +162,7 @@ presented)) (defn update-doc! [{:as doc :keys [nav-path fragment skip-history?]}] - (broadcast! (if (and (:ns @!doc) (= (:ns @!doc) (:ns doc))) + (broadcast! (if (and (:ns @!doc) (= (:ns @!doc) (:ns doc)) (not u/bb?)) {:type :patch-state! :patch (editscript/get-edits (editscript/diff (meta @!doc) (present+reset! doc) {:algo :quick}))} (cond-> {:type :set-state! :doc (present+reset! doc)} diff --git a/src/nextjournal/markdown.bb b/src/nextjournal/markdown.bb new file mode 100644 index 000000000..293b9c9ea --- /dev/null +++ b/src/nextjournal/markdown.bb @@ -0,0 +1,87 @@ +(ns nextjournal.markdown + "Babashka runtime stubs" + (:require [babashka.fs :as fs] + [babashka.process :as p] + [cheshire.core :as json] + [clojure.java.io :as io] + [clojure.string :as str] + [nextjournal.markdown.transform :as md.transform] + [nextjournal.markdown.utils :as utils] + )) + +(defn assert-quickjs! [] (assert (= 0 (:exit @(p/process '[which qjs]))) "QuickJS needs to be installed (brew install quickjs)")) +(def !md-mod-temp-dir (atom nil)) +(defn md-mod-temp-dir [] + (or @!md-mod-temp-dir + (let [tempdir (fs/create-temp-dir)] + (assert-quickjs!) + (spit (fs/file tempdir "markdown.mjs") (slurp (io/resource "js/markdown.mjs"))) + (reset! !md-mod-temp-dir (str tempdir))))) + +(defn escape [t] (-> t (str/replace "\\" "\\\\\\") (str/replace "`" "\\`") (str/replace "'" "\\'"))) +(defn tokenize [text] + (some-> (p/shell {:out :string :err :string :dir (md-mod-temp-dir)} + (str "qjs -e 'import(\"./markdown.mjs\").then((mod) => {print(mod.default.tokenizeJSON(`" (escape text) "`))})" + ".catch((e) => {import(\"std\").then((std) => { std.err.puts(\"cant find markdown module\"); std.exit(1)})})'")) + deref :out not-empty + (json/parse-string true))) + +(defn parse* [& args] + ::TODO) + +(defn parse + [md] (some->> md tokenize + ;; TODO + ((requiring-resolve 'nextjournal.markdown.impl/parse)))) + +;; (defn re-groups* [m] (let [g (re-groups m)] (cond-> g (not (vector? g)) vector))) + +;; (defn re-idx-seq +;; "Takes a regex and a string, returns a seq of triplets comprised of match groups followed by indices delimiting each match." +;; [re text] +;; (let [m (re-matcher re text)] +;; (take-while some? (repeatedly #(when (.find m) [(re-groups* m) (.start m) (.end m)]))))) + +;; (defn split-by-emoji [s] +;; (let [[match start end] (first (re-idx-seq emoji/regex s))] +;; (if match +;; [(subs s start end) (str/trim (subs s end))] +;; [nil s]))) + +;; (defn text->id+emoji [text] +;; (when (string? text) +;; (let [[emoji text'] (split-by-emoji (str/trim text))] +;; (cond-> {:id (apply str (map (comp str/lower-case (fn [c] (case c (\space \_) \- c))) text'))} +;; emoji (assoc :emoji emoji))))) + +(def empty-doc + {:type :doc + :content [] + :toc {:type :toc} + :footnotes [] + :text-tokenizers [] + ;; Node -> {id : String, emoji String}, dissoc from context to opt-out of ids + :text->id+emoji-fn (comp utils/text->id+emoji md.transform/->text) + + ;; private + ;; Id -> Nat, to disambiguate ids for nodes with the same textual content + :nextjournal.markdown.impl/id->index {} + ;; allow to swap between :doc or :footnotes + :nextjournal.markdown.impl/root :doc}) + +(comment + (assert-quickjs!) + (md-mod-temp-dir) + (tokenize "# Hello") + (parse "# Hello +* `this` +* _is_ Some $\\mathfrak{M}$ formula +* crazy as [hello](https://hell.is) + +--- +```clojure +and this is code +``` +") + (try (parse (slurp "notebooks/markdown.md")) + (catch Exception e (:err (ex-data e)))))