Skip to content

Commit 051e4f2

Browse files
zampinomk
andauthored
Polyglot Code Highlighting (#500)
Add polyglot code syntax highlighting for markdown code fences that specify a language. We support all of the [codemirror languages](https://github.com/codemirror/language-data). Co-authored-by: Martin Kavalar <[email protected]>
1 parent 1f6c533 commit 051e4f2

File tree

11 files changed

+169
-46
lines changed

11 files changed

+169
-46
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@ Changes can be:
4242

4343
* 💫 Support non-evaluated clojure code listings in markdown documents by specifying `{:nextjournal.clerk/code-listing true}` after the language ([#482](https://github.com/nextjournal/clerk/issues/482)).
4444

45+
* 🏳️‍🌈 Syntax highlighting for code listings in all [languages supported by codemirror](https://github.com/codemirror/language-data) ([#500](https://github.com/nextjournal/clerk/issues/500)).
46+
4547
* 🐜 Turn off analyzer pass for validation of `:type` tags, fixes [#488](https://github.com/nextjournal/clerk/issues/488) @craig-latacora
4648

4749
* 🐜 Strip `:type` metadata from forms before printing them to hash, fixes [#489](https://github.com/nextjournal/clerk/issues/489) @craig-latacora

book.clj

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@
9595

9696
;; In Emacs, add the following to your config:
9797

98-
;; ```elisp
98+
;; ```el
9999
;; (defun clerk-show ()
100100
;; (interactive)
101101
;; (when-let
@@ -121,7 +121,7 @@
121121

122122
;; With [neovim](https://neovim.io/) + [conjure](https://github.com/Olical/conjure/) one can use the following vimscript function to save the file and show it with Clerk:
123123

124-
;; ```
124+
;; ```vimscript
125125
;; function! ClerkShow()
126126
;; exe "w"
127127
;; exe "ConjureEval (nextjournal.clerk/show! \"" . expand("%:p") . "\")"
@@ -251,7 +251,7 @@
251251

252252
;; ### 🎼 Code
253253

254-
;; The code viewer uses
254+
;; By default the code viewer uses
255255
;; [clojure-mode](https://nextjournal.github.io/clojure-mode/) for
256256
;; syntax highlighting.
257257
(clerk/code (macroexpand '(when test
@@ -262,6 +262,24 @@
262262

263263
(clerk/code "(defn my-fn\n \"This is a Doc String\"\n [args]\n 42)")
264264

265+
;; You can specify the language for syntax highlighting via `::clerk/opts`.
266+
(clerk/code {::clerk/opts {:language "python"}} "
267+
class Foo(object):
268+
def __init__(self):
269+
pass
270+
def do_this(self):
271+
return 1")
272+
273+
;; Or use a code fence with a language in a markdown.
274+
275+
(clerk/md "```c++
276+
#include <iostream>
277+
int main() {
278+
std::cout << \" Hello, world! \" << std::endl
279+
return 0
280+
}
281+
```")
282+
265283
;; ### 🏞 Images
266284

267285
;; Clerk now has built-in support for the

notebooks/cherry.clj

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -70,11 +70,10 @@
7070
[:div
7171
[:div.flex
7272
[:div.viewer-code.flex-auto.w-80.mb-2 [nextjournal.clerk.render.code/editor !input]]
73-
[:button.flex-none.bg-slate-100.mb-2.pl-2.pr-2
74-
{:on-click click-handler}
75-
"Compile!"]]
76-
[:div.bg-slate-50
77-
[nextjournal.clerk.render/render-code @!compiled]]
73+
[:button.flex-none.rounded-md.border.border-slate-200.bg-slate-100.mb-2.pl-2.pr-2.font-sans
74+
{:on-click click-handler} "Compile!"]]
75+
[:div.bg-slate-100.p-2.border.border-slate-200
76+
[nextjournal.clerk.render/render-code @!compiled {:language "js"}]]
7877
[nextjournal.clerk.render/inspect
7978
(try (js/eval @!compiled)
8079
(catch :default e e))]])))}

notebooks/markdown_fences.md

Lines changed: 47 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,69 @@
11
# 🤺 Markdown Fences
2+
## Handling Clojure blocks
23

34
```
45
'(evaluated :and "highlighted")
56
```
67

8+
```clj
9+
'(evaluated :and "highlighted" :language clj)
10+
```
11+
712
```clojure
8-
'(evaluated :and "highlighted")
13+
'(evaluated :and "highlighted" :language clojure)
914
```
1015

16+
Use `{:nextjournal.clerk/code-listing true}` in the fence info to signal that a block should not be evaluated.
17+
1118
```clojure {:nextjournal.clerk/code-listing true}
12-
'(1 2 "not evaluated" :but-still-highlighted)
19+
(1 2 "not evaluated" :but-still-highlighted)
1320
```
1421

15-
```clojure {:nextjournal.clerk/code-listing true}
16-
'(1 2 "not evaluated" :but-still-highlighted)
22+
## 🏳️‍🌈 Polyglot Highlighting
23+
24+
EDN
25+
26+
```edn
27+
(1 2 "not evaluated" :but-still-highlighted)
1728
```
1829

30+
Javascript
31+
1932
```js
2033
() => {
2134
if (true) {
2235
return 'not evaluated'
2336
} else {
24-
return 'what'
37+
return 123
2538
}
2639
}
2740
```
41+
42+
Python
43+
44+
```py
45+
class Foo(object):
46+
def __init__(self):
47+
pass
48+
def do_this(self):
49+
return 1
50+
```
51+
52+
C++
53+
54+
```c++
55+
#include <iostream>
56+
57+
int main() {
58+
std::cout << "Hello, world!" << std::endl;
59+
return 0;
60+
}
61+
```
62+
63+
## Indented Code Blocks
64+
[Indented code blocks](https://spec.commonmark.org/0.30/#indented-code-blocks) default to clojure highlighting
65+
66+
(no (off) :fence)
67+
(but "highlighted")
68+
69+
fin.

notebooks/viewers/code.clj

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,22 @@ with quite
3636
some
3737
whitespace ")
3838

39+
;; Code in some other language, say Rust:
40+
(clerk/code {::clerk/opts {:language "rust"}}
41+
"fn calculate_factorial(n: u32, result: &mut u32) {
42+
if n == 0 {
43+
*result = 1;
44+
} else {
45+
*result = n * calculate_factorial(n - 1, result);
46+
}
47+
}
48+
fn main() {
49+
let number = 5;
50+
let mut result = 0;
51+
calculate_factorial(number, &mut result);
52+
println!(\"The factorial of {} is: {}\", number, result);
53+
}")
54+
3955
;; Editable code viewer
4056
(clerk/with-viewer
4157
'(fn [code-str _] [:div.viewer-code [nextjournal.clerk.render.code/editor (reagent.core/atom code-str)]])

notebooks/viewers/markdown.clj

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,8 @@ It's [Markdown](https://daringfireball.net/projects/markdown/), like you know it
3333
;; > — Special Forms
3434

3535
;; ## Code Listings
36-
37-
;; ```
36+
;; Clojure
37+
;; ```clj
3838
;; {:name :code,
3939
;; :render-fn 'nextjournal.clerk.render/render-code,
4040
;; :transform-fn
@@ -45,7 +45,19 @@ It's [Markdown](https://daringfireball.net/projects/markdown/), like you know it
4545
;; [v]
4646
;; (if (string? v) v (str/trim (with-out-str (pprint/pprint v)))))))}
4747
;; ```
48-
48+
;; APL
49+
;; ```apl
50+
;; numbers ← 1 2 3 4 5
51+
;; sum ← 0
52+
;; n ← ≢numbers ⍝ Get the number of elements in the array
53+
;;
54+
;; :For i :In ⍳n
55+
;; sum ← sum + numbers[i]
56+
;; :End
57+
;;
58+
;; sum
59+
;; ```
60+
;;
4961
;; ## Soft vs. Hard Line Breaks
5062
;; This one ⇥
5163
;; ⇤ is a [soft break](https://spec.commonmark.org/0.30/#soft-line-breaks) and is rendered as a space.

src/nextjournal/clerk/builder.clj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
(into ["CHANGELOG.md"
2121
"README.md"
2222
"notebooks/markdown.md"
23+
"notebooks/markdown_fences.md"
2324
"notebooks/onwards.md"]
2425
(map #(str "notebooks/" % ".clj"))
2526
["cards"

src/nextjournal/clerk/parser.cljc

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -357,21 +357,26 @@
357357
:nodes (rest nodes)
358358
::md-slice []))
359359

360-
(defn fenced-clojure-code-block? [{:as block :keys [type info language]}]
360+
(defn runnable-code-block? [{:as block :keys [info language]}]
361361
(and (code? block)
362362
info
363363
(or (empty? language)
364364
(re-matches #"clj(c?)|clojure" language))
365-
(not (:nextjournal.clerk/code-listing (let [parsed (p/parse-string-all (subs info (count language)))]
366-
(when (n/sexpr-able? parsed)
367-
(n/sexpr parsed)))))))
365+
(not (:nextjournal.clerk/code-listing
366+
(when-some [parsed (when (and (seq language) (str/starts-with? info language))
367+
(p/parse-string-all (subs info (count language))))]
368+
(when (n/sexpr-able? parsed)
369+
(n/sexpr parsed)))))))
370+
371+
#_(runnable-code-block? {:type :code :language "clojure" :info "clojure"})
372+
#_(runnable-code-block? {:type :code :language "clojure" :info "clojure {:nextjournal.clerk/code-listing true}"})
368373

369374
(defn parse-markdown-string [{:as opts :keys [doc?]} s]
370375
(let [{:as ctx :keys [content]} (parse-markdown (markdown-context) s)]
371376
(loop [{:as state :keys [nodes] ::keys [md-slice]} {:blocks [] ::md-slice [] :nodes content :md-context ctx}]
372377
(if-some [node (first nodes)]
373378
(recur
374-
(if (fenced-clojure-code-block? node)
379+
(if (runnable-code-block? node)
375380
(-> state
376381
(update :blocks #(cond-> % (seq md-slice) (conj {:type :markdown :doc {:type :doc :content md-slice}})))
377382
(parse-markdown-cell opts))

src/nextjournal/clerk/render.cljs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -917,9 +917,9 @@
917917

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

922-
(defn render-folded-code-block [code-string {:keys [id]}]
922+
(defn render-folded-code-block [code-string {:as opts :keys [id]}]
923923
(let [!hidden? (hooks/use-state true)]
924924
(if @!hidden?
925925
[:div.relative.pl-12.font-sans.text-slate-400.cursor-pointer.flex.overflow-y-hidden.group
@@ -952,7 +952,7 @@
952952
{:class "text-[10px]"}
953953
"evaluated in 0.2s"]]
954954
[:div.code-viewer.mb-2.relative.code-viewer.w-full.max-w-wide {:data-block-id id :style {:margin-top 0}}
955-
[render-code code-string]]])))
955+
[render-code code-string (assoc opts :language "clojure")]]])))
956956

957957

958958
(defn url-for [{:as src :keys [blob-id]}]

src/nextjournal/clerk/render/code.cljs

Lines changed: 43 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
(ns nextjournal.clerk.render.code
2-
(:require ["@codemirror/language" :refer [HighlightStyle syntaxHighlighting]]
3-
["@codemirror/state" :refer [EditorState RangeSetBuilder Text]]
2+
(:require ["@codemirror/language" :refer [HighlightStyle syntaxHighlighting LanguageDescription]]
3+
["@codemirror/state" :refer [EditorState RangeSet RangeSetBuilder Text]]
44
["@codemirror/view" :refer [EditorView Decoration]]
55
["@lezer/highlight" :refer [tags highlightTree]]
66
["@nextjournal/lang-clojure" :refer [clojureLanguage]]
77
[applied-science.js-interop :as j]
88
[clojure.string :as str]
99
[nextjournal.clerk.render.hooks :as hooks]
10-
[nextjournal.clojure-mode :as clojure-mode]))
10+
[nextjournal.clojure-mode :as clojure-mode]
11+
[shadow.esm]))
1112

1213
(def highlight-style
1314
(.define HighlightStyle
@@ -87,23 +88,47 @@
8788
(< pos to)
8889
(concat [(.sliceString text pos to)]))))))))
8990

90-
(defn lang->deco-range [lang code]
91-
(let [builder (RangeSetBuilder.)]
92-
(when lang
93-
(highlightTree (.. lang -parser (parse code)) highlight-style
94-
(fn [from to style]
95-
(.add builder from to (.mark Decoration (j/obj :class style))))))
96-
(.finish builder)))
91+
(defn import-matching-language-parser [language]
92+
(.. (shadow.esm/dynamic-import "https://cdn.skypack.dev/@codemirror/[email protected]")
93+
(then (fn [^js mod]
94+
(when-some [langs (.-languages mod)]
95+
(when-some [^js matching (or (.matchLanguageName LanguageDescription langs language)
96+
(.matchFilename LanguageDescription langs (str "code." language)))]
97+
(.load matching)))))
98+
(then (fn [^js lang-support] (when lang-support (.. lang-support -language -parser))))
99+
(catch (fn [err] (js/console.warn (str "Cannot load language parser for: " language) err)))))
97100

98-
(defn render-code [^String code {:keys [language]}]
101+
(defn add-style-ranges! [range-builder syntax-tree]
102+
(highlightTree syntax-tree highlight-style
103+
(fn [from to style]
104+
(.add range-builder from to (.mark Decoration (j/obj :class style))))))
105+
106+
(defn clojure-style-rangeset [code]
107+
(.finish (doto (RangeSetBuilder.)
108+
(add-style-ranges! (.. ^js clojureLanguage -parser (parse code))))))
109+
110+
(defn syntax-highlight [{:keys [code style-rangeset]}]
99111
(let [text (.of Text (.split code "\n"))]
100-
[:div.cm-editor
101-
[:cm-scroller
102-
(into [:div.cm-content.whitespace-pre]
103-
(map (partial style-line
104-
;; TODO: use-promise hook resolving to language data according to @codemirror/language-data
105-
(lang->deco-range (when (= "clojure" language) clojureLanguage) code) text))
106-
(range 1 (inc (.-lines text))))]]))
112+
(into [:div.cm-content.whitespace-pre]
113+
(map (partial style-line style-rangeset text))
114+
(range 1 (inc (.-lines text))))))
115+
116+
(defn highlight-imported-language [{:keys [code language]}]
117+
(let [^js builder (RangeSetBuilder.)
118+
^js parser (hooks/use-promise (import-matching-language-parser language))]
119+
(when parser (add-style-ranges! builder (.parse parser code)))
120+
[syntax-highlight {:code code :style-rangeset (.finish builder)}]))
121+
122+
(defn render-code [^String code {:keys [language]}]
123+
[:div.cm-editor
124+
[:cm-scroller
125+
(cond
126+
(not language)
127+
[syntax-highlight {:code code :style-rangeset (.-empty RangeSet)}]
128+
(#{"clojure" "clojurescript" "clj" "cljs" "cljc" "edn"} language)
129+
[syntax-highlight {:code code :style-rangeset (clojure-style-rangeset code)}]
130+
:else
131+
[highlight-imported-language {:code code :language language}])]])
107132

108133
;; editable code viewer
109134
(def theme

0 commit comments

Comments
 (0)