Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Wikilinks #553

Draft
wants to merge 14 commits into
base: main
Choose a base branch
from
7 changes: 7 additions & 0 deletions notebooks/document_linking.clj
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,10 @@
[:li [:a {:href (nextjournal.clerk.viewer/doc-url "notebooks/viewers/html")} "HTML"]]
[:li [:a {:href (nextjournal.clerk.viewer/doc-url "notebooks/markdown")} "Markdown"]]
[:li [:a {:href (nextjournal.clerk.viewer/doc-url "notebooks/viewer_api")} "Viewer API / Tables"]]]) nil)

;; ## Links in prose
;; * Link to a namespace `[[rule-30]]` renders as [[rule-30]]
;; * Link to a var `[[how-clerk-works/hashes]]` renders as [[how-clerk-works/hashes]]
;; * Link to a path `[[notebooks/viewers/image.clj]]` renders as [[notebooks/viewers/image.clj]]
;;
;; The href of regular links is processed in a similar fashion: `[Clerk Analyzer](how-clerk-works/hashes)` renders as [Clerk Analyzer](how-clerk-works/hashes).
16 changes: 12 additions & 4 deletions src/nextjournal/clerk.clj
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
(ns nextjournal.clerk
"Clerk's Public API."
"Clerk's Public API.

Further API:
* [Parsing](nextjournal.clerk.parser)
* [Viewers API](nextjournal.clerk.viewer)
* [Static analysis and caching](nextjournal.clerk.analyzer)"
(:require [babashka.fs :as fs]
[clojure.java.browse :as browse]
[clojure.java.io :as io]
Expand Down Expand Up @@ -222,13 +227,16 @@
([viewer-opts x] (v/html viewer-opts x)))

(defn md
"Displays `x` with the markdown viewer.
"Displays `x` with the markdown viewer. Accepts strings or a structure as returned by [[nextjournal.markdown/parse]].

Supports an optional first `viewer-opts` map arg with the following optional keys:

* `:nextjournal.clerk/width`: set the width to `:full`, `:wide`, `:prose`
* `:nextjournal.clerk/viewers`: a seq of viewers to use for presentation of this value and its children
* `:nextjournal.clerk/render-opts`: a map argument that will be passed as a secong arg to the viewers `:render-fn`"
* `:nextjournal.clerk/render-opts`: a map argument that will be passed as a secong arg to the viewers `:render-fn`.

See also [[nextjournal.clerk.viewer/markdown-viewer]]."

([x] (v/md x))
([viewer-opts x] (v/md viewer-opts x)))

Expand Down Expand Up @@ -553,7 +561,7 @@
#_(with-cache (do (Thread/sleep 4200) 42))

(defmacro defcached
"Like `clojure.core/def` but with Clerk's caching of the value."
"Like `clojure.core/def` but with Clerk's caching of the value. See also [[with-cache]]."
[name expr]
`(let [result# (-> ~(v/->edn expr) eval/eval-string :blob->result first val :nextjournal/value)]
(def ~name result#)))
Expand Down
73 changes: 60 additions & 13 deletions src/nextjournal/clerk/doc.clj
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,31 @@
{:nextjournal.clerk/visibility {:code :hide :result :hide}}
(:require [clojure.string :as str]
[nextjournal.clerk :as clerk]
[nextjournal.clerk.viewer :as viewer]))
[nextjournal.clerk.viewer :as viewer]
[nextjournal.markdown.transform :as md.transform]))

(clerk/eval-cljs
'(defn handle-click [{:keys [label var ns]} e]
(.stopPropagation e)
(.preventDefault e)
(when (resolve '!active-ns)
(let [scroll-to-target (fn []
(if var
(when-some [el (js/document.getElementById (name var))]
(.scrollIntoView el))
(when ns
(when-some [page (js/document.getElementById "main-column")]
(.scroll page (applied-science.js-interop/obj :top 0))))))]
(when ns
(if (not= @!active-ns (str ns))
(do (reset! !active-ns (str ns))
;; TODO: smarter
(js/setTimeout scroll-to-target 500))
(scroll-to-target)))))))

(clerk/eval-cljs
'(defn render-link [{:as info :keys [label]} _]
[:a {:href "#" :on-click (partial handle-click info)} label]))

(def render-input
'(fn [!query]
Expand Down Expand Up @@ -100,17 +124,6 @@
#_(ns-tree ns-matches)
#_(ns-tree ())

(defn parent-ns [ns-str]
(when (str/includes? ns-str ".")
(str/join "." (butlast (str/split ns-str #"\.")))))

(defn prepend-parent [nss]
(when-let [parent (parent-ns (first nss))]
(cons parent nss)))

(defn path-to-ns [ns-str]
(last (take-while some? (iterate prepend-parent [ns-str]))))

^{::clerk/visibility {:result :show}}
(clerk/html
(let [matches (try
Expand Down Expand Up @@ -161,7 +174,7 @@
(into [:div.text-sm.font-sans.px-5.mt-2]
(map render-ns)
(ns-tree (str-match-nss @!active-ns)))]])]]
[:div.flex-auto.max-h-screen.overflow-y-auto.px-8.py-5
[:div#main-column.flex-auto.max-h-screen.overflow-y-auto.px-8.py-5
(let [ns (some-> @!active-ns symbol find-ns)]
(cond
ns [:<>
Expand Down Expand Up @@ -195,3 +208,37 @@

#_(deref nextjournal.clerk.webserver/!doc)

(defn resolve-internal-link [link]
(viewer/resolve-internal-link (cond->> link
(and @!active-ns (not= :all @!active-ns)
(not (find-ns (symbol link)))
(not (qualified-symbol? (symbol link))))
(str @!active-ns "/"))))

(def get-info
(comp clerk/mark-presented
(fn [wv]
(let [{:as node :keys [type text attrs]} (-> wv :nextjournal/value)]
(when-some [{:as info :keys [ns var]}
(some-> (resolve-internal-link (case type :internal-link text :link (:href attrs)))
(viewer/update-if :ns ns-name)
(viewer/update-if :var symbol))]
(assoc info :label
(str (case type
:internal-link (or var ns)
:link (md.transform/->text node)))))))))

(def custom-markdown-viewers
[{:name :nextjournal.markdown/internal-link
:render-fn 'nextjournal.clerk.doc/render-link
:transform-fn get-info}
{:name :nextjournal.markdown/link
:render-fn 'nextjournal.clerk.doc/render-link
:transform-fn get-info}])

(def markdown-viewer
(update viewer/markdown-viewer :add-viewers viewer/add-viewers custom-markdown-viewers))

(viewer/add-viewers! [markdown-viewer])

#_(clerk/clear-cache!)
15 changes: 8 additions & 7 deletions src/nextjournal/clerk/parser.cljc
Original file line number Diff line number Diff line change
Expand Up @@ -287,14 +287,17 @@

(defn markdown-context []
(update markdown.parser/empty-doc
:text-tokenizers (partial map markdown.parser/normalize-tokenizer)))

#_(markdown-context)
:text-tokenizers
(comp (partial mapv markdown.parser/normalize-tokenizer)
(partial cons markdown.parser/internal-link-tokenizer))))

(defn parse-markdown
"Like `n.markdown.parser/parse` but allows to reuse the same context in successive calls"
[ctx md]
(markdown.parser/apply-tokens ctx (markdown/tokenize md)))
([md] (parse-markdown (markdown-context) md))
([ctx md]
(markdown.parser/apply-tokens ctx (markdown/tokenize md))))

#_(parse-markdown-string {:doc? true} "# Title\nSome [[internal-link]] to be followed.")

(defn update-markdown-blocks [{:as state :keys [md-context]} md]
(let [{::markdown.parser/keys [path]} md-context
Expand Down Expand Up @@ -348,9 +351,7 @@
state))))

#_(parse-clojure-string {:doc? true} "'code ;; foo\n;; bar")
#_(parse-clojure-string "'code , ;; foo\n;; bar")
#_(parse-clojure-string "'code\n;; foo\n;; bar")
#_(keys (parse-clojure-string {:doc? true} (slurp "notebooks/viewer_api.clj")))
#_(parse-clojure-string {:doc? true} ";; # Hello\n;; ## 👋 Section\n(do 123)\n;; ## 🤚🏽 Section")

(defn parse-markdown-cell [{:as state :keys [nodes]} opts]
Expand Down
2 changes: 1 addition & 1 deletion src/nextjournal/clerk/render.cljs
Original file line number Diff line number Diff line change
Expand Up @@ -865,7 +865,7 @@
[: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"}]])

(defn render-code-block [code-string {:as opts :keys [id]}]
[:div.viewer.code-viewer.w-full.max-w-wide {:data-block-id id}
[:div.viewer.code-viewer.w-full.max-w-wide {:id id}
[code/render-code code-string (assoc opts :language "clojure")]])

(defn render-folded-code-block [code-string {:as opts :keys [id]}]
Expand Down
79 changes: 66 additions & 13 deletions src/nextjournal/clerk/viewer.cljc
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@
[sci.lang]
[applied-science.js-interop :as j]])
[nextjournal.clerk.parser :as parser]
[nextjournal.markdown :as md]
[nextjournal.markdown.parser :as md.parser]
[nextjournal.markdown.transform :as md.transform])
#?(:clj (:import (com.pngencoder PngEncoder)
Expand All @@ -31,6 +30,8 @@
(java.nio.file Files StandardOpenOption)
(javax.imageio ImageIO))))

(declare doc-url)

(defrecord ViewerEval [form])

(defrecord ViewerFn [form #?(:cljs f)]
Expand Down Expand Up @@ -470,7 +471,9 @@
%)
presented-result)))

(defn get-default-viewers []
(defn get-default-viewers
"Returns viewers from the global scope when set, defaults to [[default-viewers]] (see also [[!viewers]])."
[]
(:default @!viewers default-viewers))

(defn datafy-scope [scope]
Expand Down Expand Up @@ -714,6 +717,57 @@
(doto (java.text.SimpleDateFormat. "yyyy-MM-dd'T'HH:mm:ss.SSS-00:00")
(.setTimeZone (java.util.TimeZone/getTimeZone "GMT")))))

#?(:clj (defn resolve-internal-link [link]
(if (fs/exists? link)
{:path link}
(let [sym (symbol link)]
(if (qualified-symbol? sym)
(when-some [var (try (requiring-resolve sym)
(catch Exception _ nil))]
(merge {:var var} (resolve-internal-link (-> var symbol namespace))))
(when-some [ns (try (require sym)
(find-ns sym)
(catch Exception _ nil))]
(cond-> {:ns ns}
(fs/exists? (analyzer/ns->file sym))
(assoc :path (analyzer/ns->file sym)))))))))

#_(resolve-internal-link "notebooks/hello.clj")
#_(resolve-internal-link "nextjournal.clerk.tap")
#_(resolve-internal-link "rule-30/board")

(defn process-internal-link [link]
#?(:clj
(let [{:keys [path var]} (resolve-internal-link link)]
{:path path
:fragment (when var (str (-> var symbol str) "-code"))
:title (or (when var (-> var symbol str))
(when path (:title (parser/parse-file {:doc? true} path)))
link)})
:cljs
{:path link :title link}))

(defn process-href [^String href]
#?(:cljs href
:clj (if (or (.getScheme (URI. href)) (str/starts-with? href "/"))
href
(let [{:keys [path fragment]} (process-internal-link href)]
(if (or path fragment) (doc-url path fragment) href)))))

#_(process-href "rule-30")
#_(process-href "#some-id")
#_(process-internal-link "#some-id")

#_(process-internal-link "notebooks/rule_30.clj")
#_(process-internal-link "viewers.html")
#_(process-internal-link "how-clerk-works/hashes")
#_(process-internal-link "rule-30/first-generation")

(defn update-if [m k f] (if (k m) (update m k f) m))
#_(update-if {:n "42"} :n #(Integer/parseInt %))

(declare html)

(def markdown-viewers
[{:name :nextjournal.markdown/doc
:transform-fn (into-markup (fn [{:keys [id]}] [:div.viewer.markdown-viewer.w-full.max-w-prose.px-8 {:data-block-id id}]))}
Expand Down Expand Up @@ -741,7 +795,12 @@
{:name :nextjournal.markdown/strong :transform-fn (into-markup [:strong])}
{:name :nextjournal.markdown/monospace :transform-fn (into-markup [:code])}
{:name :nextjournal.markdown/strikethrough :transform-fn (into-markup [:s])}
{:name :nextjournal.markdown/link :transform-fn (into-markup #(vector :a (:attrs %)))}
{:name :nextjournal.markdown/link :transform-fn (into-markup #(vector :a (update-if (:attrs %) :href process-href)))}
{:name :nextjournal.markdown/internal-link
:transform-fn (update-val
(fn [{:keys [text]}]
(let [{:keys [path fragment title]} (process-internal-link text)]
(html [:a.internal-link {:href (doc-url path fragment)} title]))))}

;; inlines
{:name :nextjournal.markdown/text :transform-fn (into-markup [:<>])}
Expand Down Expand Up @@ -937,12 +996,13 @@
{:name `vega-lite-viewer :render-fn 'nextjournal.clerk.render/render-vega-lite :transform-fn mark-presented})

(def markdown-viewer
"A clerk viewer for rendering markdown. See also [[nextjournal.clerk/md]]."
{:name `markdown-viewer
:add-viewers markdown-viewers
:transform-fn (fn [wrapped-value]
(-> wrapped-value
mark-presented
(update :nextjournal/value #(cond->> % (string? %) md/parse))
(update :nextjournal/value #(cond->> % (string? %) parser/parse-markdown))
(with-md-viewer)))})

(def code-viewer
Expand Down Expand Up @@ -1122,14 +1182,7 @@
(map (juxt #(list 'quote (symbol %)) #(->> % deref deref (list 'quote))))
(extract-sync-atom-vars doc)))))

(defn update-if [m k f]
(if (k m)
(update m k f)
m))

#_(update-if {:n "42"} :n #(Integer/parseInt %))

(declare html doc-url)
(declare html)

(defn home? [{:keys [nav-path]}]
(contains? #{"src/nextjournal/home.clj" "'nextjournal.clerk.home"} nav-path))
Expand Down Expand Up @@ -1288,7 +1341,7 @@
hide-result-viewer])

(defonce
^{:doc "atom containing a map of and per-namespace viewers or `:defaults` overridden viewers."}
^{:doc "An atom containing a map of per-namespace viewers or `:default` overridden viewers. See also how to [get default viewers](get-default-viewers)."}
!viewers
(#?(:clj atom :cljs ratom/atom) {}))

Expand Down
Loading