diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 403e42a16..5007e83aa 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -251,7 +251,7 @@ jobs: run: echo $(yarn global dir)/node_modules - name: ๐Ÿงช Build Rule 30 Notebook with SSR - run: NODE_PATH=$(yarn global dir)/node_modules clojure -J-Dclojure.main.report=stdout -X:demo:nextjournal/clerk :git/sha '"${{ github.sha }}"' :git/url '"https://github.com/nextjournal/clerk"' :index '"notebooks/rule_30.clj"' :paths [] :ssr true :compile-css true :exclude-js true + run: NODE_PATH=$(yarn global dir)/node_modules clojure -J-Dclojure.main.report=stdout -X:demo:nextjournal/clerk :git/sha '"${{ github.sha }}"' :git/url '"https://github.com/nextjournal/clerk"' :paths '["notebooks/rule_30.clj" "notebooks/viewers/katex.clj"]' :ssr true :compile-css true :exclude-js true - name: ๐Ÿ” Google Auth uses: google-github-actions/auth@v2.1.6 @@ -318,7 +318,7 @@ jobs: run: | bb test:static-app :sha ${{ github.sha }} :skip-install true bb test:static-app :skip-install true :url https://snapshots.nextjournal.com/clerk/book/${{ github.sha }}/book/index.html :index false :selector "h1:has-text(\"Book of Clerk\")" - bb test:static-app :skip-install true :url https://snapshots.nextjournal.com/clerk-ssr/build/${{ github.sha }}/index.html :index false :selector "h1:has-text(\"Rule 30\")" + bb test:static-app :skip-install true :url https://snapshots.nextjournal.com/clerk-ssr/build/${{ github.sha }}/index.html :index true deploy: needs: [build-and-upload-viewer-resources, test] diff --git a/bb.edn b/bb.edn index efc2c9548..8b55601d6 100644 --- a/bb.edn +++ b/bb.edn @@ -20,7 +20,7 @@ build:js {:doc "Builds JS" :depends [yarn-install] - :task (clojure "-M:sci:demo:dev release viewer")} + :task (apply clojure "-M:sci:demo:dev release viewer" *command-line-args*)} build+upload-viewer-resources {:doc "Refreshes assets stored on CDN (google storage)" :extra-paths ["src"] diff --git a/deps.edn b/deps.edn index 88f46fefb..f4030619e 100644 --- a/deps.edn +++ b/deps.edn @@ -7,7 +7,7 @@ weavejester/dependency {:mvn/version "0.2.1"} com.nextjournal/beholder {:mvn/version "1.0.3"} org.flatland/ordered {:mvn/version "1.15.12"} - io.github.nextjournal/markdown {:mvn/version "0.7.186"} + io.github.nextjournal/markdown {:mvn/version "0.7.189"} babashka/process {:mvn/version "0.4.16"} io.github.nextjournal/dejavu {:git/sha "7276cd9cec1bad001d595b52cee9e83a60d43bf0"} io.github.babashka/sci.nrepl {:mvn/version "0.0.2"} diff --git a/dev/nextjournal/clerk/ssr.clj b/dev/nextjournal/clerk/ssr.clj deleted file mode 100644 index a958483e2..000000000 --- a/dev/nextjournal/clerk/ssr.clj +++ /dev/null @@ -1,67 +0,0 @@ -(ns nextjournal.clerk.ssr - "Server-side-rendering using `reagent.dom.server` on GraalJS. - - Status: working in GraalJS `org.graalvm.js/js {:mvn/version \"22.3.0\"}` - - To try this ad the dep above to e.g. the `:sci` alias." - (:require [clojure.java.io :as io] - [clojure.edn :as edn] - [clojure.string :as str] - [nextjournal.clerk.config :as config]) - (:import (org.graalvm.polyglot Context Source))) - -(def context-builder - (doto (Context/newBuilder (into-array ["js"])) - (.option "js.timer-resolution" "1") - (.option "js.java-package-globals" "false") - (.out System/out) - (.err System/err) - (.allowAllAccess true) - (.allowNativeAccess true))) - -(def context (.build context-builder)) - -(defn execute-fn [context fn & args] - (let [fn-ref (.eval context "js" fn) - args (into-array Object args)] - (assert (.canExecute fn-ref) (str "cannot execute " fn)) - (.execute fn-ref args))) - -(defn viewer-js-path [] - (@config/!asset-map "/js/viewer.js") - ;; uncomment the following to test against a local js bundle - #_"build/viewer.js") - -(def viewer-js-source - ;; run `bb build:js` on shell to generate - (.build (Source/newBuilder "js" (str (slurp "https://gist.githubusercontent.com/Yaffle/5458286/raw/1aa5caa5cdd9938fe0fe202357db6c6b33af24f4/TextEncoderTextDecoder.js") ;; tiny utf8 only TextEncoder polyfill - "\n" - (slurp (viewer-js-path))) "viewer.mjs"))) - - -(def !eval-viewer-source - (delay (.eval context viewer-js-source))) - -(defn render [edn-string] - (force !eval-viewer-source) - (execute-fn context "nextjournal.clerk.sci_env.ssr" edn-string)) - - -(comment - (do - (require '[nextjournal.clerk :as clerk] - '[nextjournal.clerk.eval :as eval] - '[nextjournal.clerk.builder :as builder] - '[nextjournal.clerk.view :as view]) - - (defn file->static-app-opts [file] - (-> (eval/eval-file file) - (as-> doc (assoc doc :viewer (view/doc->viewer {} doc))) - (as-> doc+viewer (builder/build-static-app-opts (builder/process-build-opts {:index file}) [doc+viewer])))) - - (spit "build/static_app_state_hello.edn" (pr-str (file->static-app-opts "notebooks/hello.clj"))) - (spit "build/static_app_state_rule_30.edn" (pr-str (file->static-app-opts "notebooks/rule_30.clj"))) - - (time (render (slurp "build/static_app_state_hello.edn"))))) - - diff --git a/notebooks/viewers/katex.clj b/notebooks/viewers/katex.clj new file mode 100644 index 000000000..2c1456d5c --- /dev/null +++ b/notebooks/viewers/katex.clj @@ -0,0 +1,16 @@ +;; # Katex + +(ns katex + (:require [nextjournal.clerk] + [nextjournal.clerk.viewer])) + +;; Inline formula: $x^2$ + + +;; Block level formula: +;; $$x^2$$ + +;; Manual viewer: +(nextjournal.clerk/with-viewer + nextjournal.clerk.viewer/katex-viewer "x^2") + diff --git a/render/deps.edn b/render/deps.edn index a12ba3fda..9edde0445 100644 --- a/render/deps.edn +++ b/render/deps.edn @@ -8,7 +8,7 @@ io.github.babashka/sci.configs {:git/sha "8253c69a537bcc82e8ff122e5f905fe9d1e303f0" :exclusions [org.babashka/sci]} io.github.nextjournal/clojure-mode {:git/sha "1f55406087814a0dda6806396aa596dbe13ea302"} - thheller/shadow-cljs {:mvn/version "3.0.4"} + thheller/shadow-cljs {:mvn/version "3.2.0"} io.github.squint-cljs/cherry {;; :local/root "/Users/borkdude/dev/cherry" :git/sha "8de9f27" :git/tag "v0.4.26" diff --git a/shadow-cljs.edn b/shadow-cljs.edn index 01ac70f3d..425b2822c 100644 --- a/shadow-cljs.edn +++ b/shadow-cljs.edn @@ -8,7 +8,10 @@ :compiler-options {:source-map true} :dev {:modules {:viewer {:entries [devtools]}}} :modules {:viewer {:entries [nextjournal.clerk.sci-env - nextjournal.clerk.trim-image]}} + nextjournal.clerk.trim-image]} + :katex {:entries [katex nextjournal.clerk.render.katex] + :depends-on #{:viewer} + :exports {renderToString nextjournal.clerk.render.katex/renderToString}}} :js-options {:output-feature-set :es8} :build-hooks [(shadow.cljs.build-report/hook {:output-to "report.html" :print-table true})]}}} diff --git a/src/nextjournal/clerk/builder.clj b/src/nextjournal/clerk/builder.clj index fd6ea0ed2..32e44a6da 100644 --- a/src/nextjournal/clerk/builder.clj +++ b/src/nextjournal/clerk/builder.clj @@ -172,28 +172,46 @@ (defn download-text-file [url] (:body (http/get url))) +(defn local-js [url tmp-dir] + (if (str/starts-with? url "http") + (let [tmp (-> tmp-dir + (fs/file (-> (fs/file-name url) + (str/split #"\?") + (first) + (str/replace #".js$" ".mjs"))) + (fs/delete-on-exit) + str) + src (download-text-file url) + src (str/replace src "viewer.js" "viewer.mjs")] + (spit tmp src) + tmp) + url)) + (defn- node-ssr! [{:keys [viewer-js state] :or {viewer-js ;; for local REPL testing "./public/js/viewer.js"}}] - (let [viewer-js (if (str/starts-with? viewer-js "http") - (let [tmp (-> (fs/create-temp-file {:suffix ".mjs"}) - (fs/file) - (fs/delete-on-exit) - str) - src (download-text-file viewer-js)] - (spit tmp src) - tmp) - viewer-js) + (let [tmp-dir (fs/create-temp-dir) + katex? (-> state :doc :katex?) + [viewer-js katex-js] [(local-js viewer-js tmp-dir) + (when katex? + (local-js (str/replace viewer-js "viewer.js" "katex.js") tmp-dir))] in (str "import '" viewer-js "';" - "globalThis.CLERK_SSR = true;" - "console.log(nextjournal.clerk.sci_env.ssr(" (pr-str (pr-str state)) "))")] - (sh {:in in} - "node" - "--abort-on-uncaught-exception" - "--input-type=module" - "--trace-warnings"))) + (when katex? + (format (str "import * as katex from \"%s\";" + "globalThis.clerk$katex = katex;") + katex-js)) + "globalThis.CLERK_SSR = true; + new Promise((resolve) => { setTimeout(resolve, 2000)});\n" + "console.log(nextjournal.clerk.sci_env.ssr(" (pr-str (pr-str state )) "))")] + #_(spit "in.mjs" in) + (sh + {:out :string :in in :err :inherit} + "node" + "--abort-on-uncaught-exception" + "--input-type=module" + "--trace-warnings"))) (comment (declare so) ;; captured in REPL in ssr! function @@ -219,8 +237,10 @@ (throw (ex-info (str "Clerk ssr! failed\n" out "\n" err) result))))) (defn cleanup [build-opts] - (select-keys build-opts - [:package :render-router :path->doc :current-path :resource->url :exclude-js? :index :html])) + (cond-> (select-keys build-opts + [:package :render-router :path->doc :current-path :resource->url :exclude-js? :index :html]) + (-> build-opts :doc :katex?) + (assoc :katex? true))) (defn write-static-app! [opts docs] @@ -244,7 +264,7 @@ (dissoc :path->doc) cleanup)))))) (when browse? - (browse/browse-url (if-let [server-url (and (= out-path "public/build") (webserver/server-url))] + (browse/browse-url (if-let [server-url (and (= "public/build" out-path) (webserver/server-url))] (str server-url "/build/") (-> index-html fs/absolutize .toString path-to-url-canonicalize)))) {:docs docs @@ -386,11 +406,12 @@ (build-static-app! {:index "notebooks/document_linking.clj" :paths ["notebooks/viewers/html.clj" "notebooks/rule_30.clj"]}) + ;; document is not defined (build-static-app! {:ssr? true :exclude-js? true ;; test against cljs release `bb build:js` :resource->url {"/js/viewer.js" "./build/viewer.js"} - :index "notebooks/rule_30.clj"}) + :index "notebooks/viewers/katex.clj"}) (build-static-app! {:ssr? true :exclude-js? true diff --git a/src/nextjournal/clerk/render.cljs b/src/nextjournal/clerk/render.cljs index 9cc5b252a..f75e5f39f 100644 --- a/src/nextjournal/clerk/render.cljs +++ b/src/nextjournal/clerk/render.cljs @@ -981,9 +981,14 @@ default-loading-view)))) (defn render-katex [tex-string {:keys [inline?]}] - (let [katex (hooks/use-d3-require "katex@0.16.4")] + (let [katex (if (some-> (unchecked-get js/globalThis "process") + (unchecked-get "versions") + (unchecked-get "node")) + (unchecked-get js/globalThis "clerk$katex") + (hooks/use-d3-require "katex@0.16.4"))] (if katex - [:span {:dangerouslySetInnerHTML (r/unsafe-html (.renderToString katex tex-string (j/obj :displayMode (not inline?) :throwOnError false)))}] + (let [html (.renderToString katex tex-string (j/obj :displayMode (not inline?) :throwOnError false))] + [:span {:dangerouslySetInnerHTML (r/unsafe-html html)}]) default-loading-view))) (defn render-mathjax [value] diff --git a/src/nextjournal/clerk/render/katex.cljs b/src/nextjournal/clerk/render/katex.cljs new file mode 100644 index 000000000..962d90b2e --- /dev/null +++ b/src/nextjournal/clerk/render/katex.cljs @@ -0,0 +1,5 @@ +(ns nextjournal.clerk.render.katex + (:require ["katex" :as katex])) + +(defn renderToString [s] + (katex/renderToString s)) diff --git a/src/nextjournal/clerk/sci_env.cljs b/src/nextjournal/clerk/sci_env.cljs index 7e6fac134..5224e8bb7 100644 --- a/src/nextjournal/clerk/sci_env.cljs +++ b/src/nextjournal/clerk/sci_env.cljs @@ -8,7 +8,6 @@ ["@lezer/highlight" :as lezer-highlight] ["@nextjournal/lang-clojure" :as lang-clojure] ["framer-motion" :as framer-motion] - ["katex" :as katex] ["react" :as react] ["react-dom" :as react-dom] ["w3c-keyname" :as w3c-keyname] @@ -160,7 +159,7 @@ "@lezer/highlight" lezer-highlight "@nextjournal/lang-clojure" lang-clojure "framer-motion" framer-motion - "katex" katex + #_#_"katex" katex "react" react "react-dom" react-dom "w3c-keyname" w3c-keyname} diff --git a/src/nextjournal/clerk/view.clj b/src/nextjournal/clerk/view.clj index 0d2a41c54..f9d6dd2f1 100644 --- a/src/nextjournal/clerk/view.clj +++ b/src/nextjournal/clerk/view.clj @@ -2,15 +2,30 @@ (:require [clojure.java.io :as io] [clojure.string :as str] [hiccup.page :as hiccup] + [nextjournal.clerk.cljs-libs :as cljs-libs] [nextjournal.clerk.viewer :as v] - [nextjournal.clerk.cljs-libs :as cljs-libs]) + [nextjournal.clerk.walk :as w]) (:import (java.net URI))) +(defn viewer-names [state] + (let [!viewers (atom #{})] + (w/postwalk (fn [v] + (if-let [viewer (v/get-safe v :nextjournal/viewer)] + (do (swap! !viewers conj (:name viewer)) + v) + v)) + state) + (let [viewers @!viewers] + (assoc state :katex? (some viewers #{'nextjournal.clerk.viewer/katex-viewer + :nextjournal.markdown/formula + :nextjournal.markdown/block-formula}))))) + (defn doc->viewer ([doc] (doc->viewer {} doc)) ([opts {:as doc :keys [ns file]}] (binding [*ns* ns] - (-> (merge doc opts) v/notebook v/present (cljs-libs/prepend-required-cljs opts))))) + (-> (merge doc opts) v/notebook v/present (cljs-libs/prepend-required-cljs opts) + (viewer-names))))) #_(doc->viewer (nextjournal.clerk/eval-file "notebooks/hello.clj")) #_(nextjournal.clerk/show! "notebooks/test.clj") @@ -38,9 +53,14 @@ (str/replace #"require\(.*\)" ""))] [:style {:type "text/tailwindcss"} (slurp (io/resource "stylesheets/viewer.css"))]))) +(defn include-katex-css [state] + (when (:katex? state) + (hiccup/include-css "https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.12.0/katex.min.css"))) + (defn include-css+js [state] (list (include-viewer-css state) + (include-katex-css state) [:script {:type "module" :src (adjust-relative-path state (get-in state [:resource->url "/js/viewer.js"]))}] (hiccup/include-css "https://cdn.jsdelivr.net/npm/katex@0.13.13/dist/katex.min.css") [:link {:rel "preconnect" :href "https://fonts.bunny.net"}] @@ -66,7 +86,8 @@ }"]) (when current-path (v/open-graph-metas (-> state :path->doc (get current-path) v/->value :open-graph))) (if exclude-js? - (include-viewer-css state) + (list (include-viewer-css state) + (include-katex-css state)) (include-css+js state))] [:body.dark:bg-gray-900 [:div#clerk html] diff --git a/ui_tests/playwright_tests.cljs b/ui_tests/playwright_tests.cljs index 03883fa93..522c6d65a 100644 --- a/ui_tests/playwright_tests.cljs +++ b/ui_tests/playwright_tests.cljs @@ -46,23 +46,30 @@ (def console-errors (atom [])) +(def page-selectors {"notebooks/rule_30.clj" "h1:has-text(\"Rule 30\")" + "notebooks/viewers/katex.clj" "span.katex"}) + (defn test-notebook ([page url] (println "Visiting" url) (p/do (goto page url) (.waitForLoadState page "networkidle") - (p/let [selector (or (:selector @!opts) "div") + (p/let [selector (or (:selector @!opts) + "div") _ (prn :selector selector) loc (.locator page selector #js {:timeout 10000}) loc (.first loc #js {:timeout 10000}) _ (.waitFor loc #js {:state "visible"}) visible? (.isVisible loc)] (is visible?)))) + ;; called from index-page-test ([page url link] - (p/let [txt (.innerText link)] + (p/let [txt (.innerText link) + selector (or (get page-selectors txt) + "div")] (println "Visiting" (str url "#/" txt)) (p/do (.click link) - (p/let [loc (.locator page "div") + (p/let [loc (.locator page selector) loc (.first loc #js {:timeout 10000}) _ (.waitFor loc #js {:state "visible"}) visible? (.isVisible loc)] diff --git a/ui_tests/ssr.cljs b/ui_tests/ssr.cljs index 7651ba0d3..79fc3e0e0 100644 --- a/ui_tests/ssr.cljs +++ b/ui_tests/ssr.cljs @@ -3,12 +3,12 @@ Use this to iterate on it, then make sure the advanced bundle works in Graal via `nextjournal.clerk.ssr`." - (:require ["./../public/js/viewer.js" :as viewer] - ;; the above is the dev build, the one below the relase (generate it via `bb release:js`) + (:require ["./../public/js/viewer.js"] + ;; the above is the dev build, the one below the release (generate it via `bb release:js`) #_["./../build/viewer.js" :as viewer] [babashka.cli :as cli] - [promesa.core :as p] - [nbb.core :refer [slurp]])) + [nbb.core :refer [slurp]] + [promesa.core :as p])) (defn -main [& args] (p/let [{:keys [file edn url]} (:opts (cli/parse-args args {:alias {:u :url :f :file}})) @@ -16,7 +16,7 @@ edn edn)] (if edn-string (println (js/nextjournal.clerk.sci_env.ssr edn-string)) - (binding [*out* *err*] + (binding [*print-fn* *print-err-fn*] (println "must provide --file or --edn arg")))))