Skip to content

Instantly share code, notes, and snippets.

@JohnnyJayJay
Last active May 25, 2024 17:40
Show Gist options
  • Save JohnnyJayJay/861cd1db0a66adf83fdf8fffa37d8808 to your computer and use it in GitHub Desktop.
Save JohnnyJayJay/861cd1db0a66adf83fdf8fffa37d8808 to your computer and use it in GitHub Desktop.
cryogen extension for AOT syntax highlighting
;; SPDX-License-Identifier: MIT
;; SPDX-FileCopyrightText: 2023 JohnnyJayJay
;;
;; AOT syntax highlighting for cryogen blogs using highlight.js and GraalVM polyglot features.
;; How to use:
;; 1. Add Graaljs as a dependency: https://mvnrepository.com/artifact/org.graalvm.js/js/23.0.1 (or build your blog on GraalVM directly)
;; 2. Place a minimal highlight.js distribution at the root of your cryogen project. You can get one by downloading without selecting any languages from https://highlightjs.org/download
;; 3. In your cryogen.core namespace, call `compile-assets-timed` with `{:update-article-fn highlight-code-in-article}`
;; 4. Remove the highlight.js scripts from your theme's templates - they're not needed anymore. A highlight.js *theme* is still needed though!
(ns cryogen.highlight
(:require [net.cgrand.enlive-html :as enlive]
[clojure.string :as str]
[clojure.java.io :as io])
(:import (org.graalvm.polyglot Context Source)))
;; "Wrong" language names that you use in your code blocks anyway, and their mapping to the "correct" language.
(def aliases
"Mapping from language alias -> actual language name, for cases where you specify
'wrong' languages in your code blocks that wouldn't normally be recognized by highlight.js."
{"clj" "clojure"
"js" "javascript"})
(def ^Context js-context
(doto (.build (Context/newBuilder (into-array ["js"])))
(.eval (.build (Source/newBuilder "js" (io/file "highlight.js"))))))
;; Atom keeping track of what languages have already been fetched.
(def loaded-languages
(atom #{}))
(def cache-dir
(io/file ".hljs-cache"))
;; Get the hljs grammar for a (recognized) language from a CDN. You can of course use a different CDN or rewrite this to load files from a local directory instead.
;; Caches fetched grammars in [[cache-dir]].
(defn fetch-grammar! [language]
(.mkdir cache-dir)
(let [cache-file (io/file cache-dir language)]
(if (.exists cache-file)
(slurp cache-file)
(try
(doto (slurp (format "https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.8.0/languages/%s.min.js" language))
(->> (spit cache-file)))
(catch java.io.FileNotFoundException _e
(printf "WARNING: A code block specifies '%s' as its language, which is not recognized. Using automatic highlighting instead.\n" language)
nil)))))
(defn ensure-language! [language]
(let [language (get aliases language language)]
(if (contains? @loaded-languages language)
language
(when-let [grammar (fetch-grammar! language)]
(.eval js-context "js" grammar)
(swap! loaded-languages conj language)
language))))
(defn highlight-code
"Highlights code using the given language, downloading and loading the language grammar if necessary.
Both `code` and the returned value are strings. Pass `nil` as the language for automatic highlighting."
[language code]
;; Locking because JS context is not thread safe.
(locking js-context
(let [language (ensure-language! language)]
(.. js-context (getBindings "js") (putMember "hlCode" code))
(.asString
(.eval
js-context
"js"
(if language
(format "hljs.highlight(hlCode, { language: '%s', ignoreIllegals: true }).value" language)
"hljs.highlightAuto(hlCode).value"))))))
(defn apply-highlighting-to-dom [dom]
(enlive/transform
dom
[:code]
(fn [selected-node]
(let [language-class (get-in selected-node [:attrs :class])]
(if (and language-class (not (str/includes? language-class "nohighlight")))
(-> selected-node
(update :content (comp enlive/html-snippet (partial highlight-code language-class) first))
(update-in [:attrs :class] str " hljs language-" language-class))
selected-node)))))
(defn highlight-code-in-article
"Function that can be passed as :update-article-fn and that updates the content-dom of an article to apply highlighting."
[article _]
(update article :content-dom apply-highlighting-to-dom))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment