Adaptation of ideas described by Michiel Borkent in "Writing a Clojure Highlighter from Scratch". Uses rewrite-clj
to parse Clojure source code and forms, and leverages multimethods to allow for user-extensibility.
If you want to add syntax highlighting to your project without using a JavaScript library like Prism or Highlight.js and you use Hiccup to generate HTML, this library might help you. I wrote it in part because I wanted more control over how my source code was displayed in HTML and CSS while developing fabricate - which this code was factored out of - than was allowed by other tools.
Most source code highlighters focus on syntax. Adorn goes further than this and allows you the ability to highlight elements based on their meaning.
More broadly, I don't think Clojure should have to rely on other language ecosystems for good display of our source code. I think it can be done better in Clojure, because Clojure code is Clojure data and we have powerful facilities for working with it, especially with the widespread use of the excellent rewrite-clj library.
Adorn provides a minimal API in the site.fabricate.adorn
namespace. Use it to produce Hiccup elements - nested :span
vectors.
(require
'[site.fabricate.adorn :as adorn
:refer [clj->hiccup]])
You can pass quoted Clojure forms:
(clj->hiccup '[:vector {:map-key :map-val} symbol])
This results in the following data structure:
([:span
{:class "language-clojure vector"}
[:span {:class "bracket-open"} "["]
[:span
{:class "language-clojure keyword",
:data-java-class "clojure.lang.Keyword",
:data-clojure-keyword ":vector"}
":vector"]
[:span {:class "language-clojure whitespace"} " "]
[:span
{:class "language-clojure map"}
[:span {:class "brace-open"} "{"]
[:span
{:class "language-clojure keyword",
:data-java-class "clojure.lang.Keyword",
:data-clojure-keyword ":map-key"}
":map-key"]
[:span {:class "language-clojure whitespace"} " "]
[:span
{:class "language-clojure keyword",
:data-java-class "clojure.lang.Keyword",
:data-clojure-keyword ":map-val"}
":map-val"]
[:span {:class "brace-close"} "}"]]
[:span {:class "language-clojure whitespace"} " "]
[:span
{:class "language-clojure symbol",
:data-java-class "clojure.lang.Symbol",
:data-clojure-symbol "symbol"}
"symbol"]
[:span {:class "bracket-close"} "]"]])
When converted, it results in HTML like this:
[:vector {:map-key :map-val} symbol]
It also works on strings. A plain string will be assumed to contain one or more Clojure forms, and parsed with rewrite-clj.parser/parse-string-all
.
(clj->hiccup "[:vector {:map-key :map-val} 'symbol]")
And it works on rewrite-clj
nodes:
(require '[rewrite-clj.parser :as p])
(clj->hiccup (p/parse-string "[:vector {:map-key :map-val} 'symbol]"))
site.fabricate.adorn/clj->hiccup
uses the multimethod site.fabricate.adorn/form->hiccup
to dispatch, which means it can be extended to new form types.
For more information, see the API docs.
I included the resulting Hiccup form in the code examples because it demonstrates an important idea mentioned above: highlighting based on semantics. Adorn produces Hiccup elements, so if you display them by converting them to HTML you have all the power of CSS to display them as you see fit.
Say you want to assign a different color to definitions: def
, defn
, defmulti
, defprotocol
, and so on. Assigning different highlight rules to each of these terms is very difficult in other syntax highlighters - to the extent that it's possible at all.
Where appropriate, Adorn sets Clojure information, like the symbol in a form, as form-level data attributes. So you can simply assign rules based on this attribute.
span[data-clojure-symbol^="def"] {
font-weight: 900;
}
Now everything that begins with def
is covered - even if you forgot about a different one, like clojure.test/deftest
- you'll be covered.
I use a similar rule to highlight instances of the most important function in this intro: clj->hiccup
. I hope Adorn gives you the data you need to highlight what makes the most sense for the code you're displaying, rather than forcing you into a predetermined set of options.
- Glow is another server-side syntax highlighting library for Clojure. It only runs on the JVM because it uses ANTLR to parse Clojure. It also uses Enlive instead of Hiccup for its intermediate representation of parsed Clojure code.
- Clygments wraps the Pygments Python library, which obviously means this library introduces a dependency on Python.
Pre-alpha. Moving towards a stable API, but does not yet have clearly defined contracts. Not yet implemented across all target platforms, does not yet have a fully robust extension mechanism, and still requires a unified data model for "forms."
The site.fabricate.adorn.forms
namespace has a fairly complete set of functions that are used as building blocks.
- CLJC compatibility; generation of Hiccup forms using:
- Clojure
- ClojureScript
- Babashka
- provide sensible defaults and an example of styling using plain CSS
- including at least one useful Flexbox example
- provide an override mechanism for users who want to display particular forms in special ways
- provide a nested, map-based override mechanism for subforms in a Clojure form
- provide an extension mechanism for special symbols (e.g.
def
,defn
,def-my-custom-def
) - compatibility across Hiccup implementations
- Conversion of Hiccup to HTML. While this conversion will be necessary in order to verify the output of
adorn
, this will strictly be a dev-time dependency. What generates HTML from the Hiccup produced by adorn is up to the user. - Validation of output HTML.
- Support for other languages.
Licensed under the MIT license.
Assuming that the Clojure CLI is installed:
To execute the project's tests:
clojure -X:dev:test
Assuming that npm (globally) and shadow-cljs (locally) are installed:
To execute the project's tests:
npx shadow-cljs compile test
- Thanks to Michiel Borkent for the initial idea and excellent writeup
- Thanks to Lee Read, Michiel Borkent and the other contributors to
rewrite-clj
for such an excellent library, and for answering my questions about it - this library wouldn't be possible without the well-designed machinery thatrewrite-clj
provides. - Thanks to John Newman for answering my novice questions about CLJS.
- Oleksandr Yakushev has written a plethora of invaluable tools and information that helped me measure the performance of this code in a rigorous way.