From 56a701b2b8019db0365044d7eeb1046563730180 Mon Sep 17 00:00:00 2001 From: flybot-nam-nguyenhoai <137136212+flybot-nam-nguyenhoai@users.noreply.github.com> Date: Fri, 4 Aug 2023 15:32:27 +0800 Subject: [PATCH] Add syntax highlighting for Markdown code blocks (#255) * Add code syntax highlighting using highlight.js * Highlight blocks without languages as plain text * Add themes for syntax highlighting * Fix CSS; support highlighting for more languages * Syntax highlight each post only once --- .../src/flybot/client/web/core/db/event.cljs | 7 + .../web/src/flybot/client/web/core/db/fx.cljs | 12 +- .../client/web/core/db/fx/highlight.cljs | 12 + .../flybot/client/web/core/dom/hiccup.cljs | 42 ++- .../flybot/client/web/core/dom/page/post.cljs | 20 +- deps.edn | 1 + resources/public/css/highlight.css | 270 ++++++++++++++++++ resources/public/css/style.css | 3 +- resources/public/index.html | 1 + 9 files changed, 353 insertions(+), 15 deletions(-) create mode 100644 client/web/src/flybot/client/web/core/db/fx/highlight.cljs create mode 100644 resources/public/css/highlight.css diff --git a/client/web/src/flybot/client/web/core/db/event.cljs b/client/web/src/flybot/client/web/core/db/event.cljs index ca290fda..d5a70f52 100644 --- a/client/web/src/flybot/client/web/core/db/event.cljs +++ b/client/web/src/flybot/client/web/core/db/event.cljs @@ -123,6 +123,13 @@ :fx [[:fx.app/set-theme-local-store next-theme] [:fx.app/toggle-css-class [cur-theme next-theme]]]}))) +;; Code syntax highlighting + +(rf/reg-event-fx + :evt.app/highlight-code + (fn [_ [_ html-id]] + {:fx [[:fx.app/highlight-code html-id]]})) + ;; ---------- Navbar ---------- (rf/reg-event-db diff --git a/client/web/src/flybot/client/web/core/db/fx.cljs b/client/web/src/flybot/client/web/core/db/fx.cljs index 4ad6d3f7..cd6d8759 100644 --- a/client/web/src/flybot/client/web/core/db/fx.cljs +++ b/client/web/src/flybot/client/web/core/db/fx.cljs @@ -1,9 +1,11 @@ (ns flybot.client.web.core.db.fx - (:require [clojure.edn :as edn] + (:require [cljsjs.highlight] + [clojure.edn :as edn] [clojure.string :as str] [flybot.client.common.db.fx] [flybot.client.common.utils :refer [cljs->js]] [flybot.client.web.core.db.class-utils :as cu] + [flybot.client.web.core.db.fx.highlight] [flybot.client.web.core.db.localstorage :as l-storage] [re-frame.core :as rf] [reagent.core :as reagent] @@ -64,6 +66,14 @@ (fn [next-theme] (l-storage/set-item :theme next-theme))) +;; code syntax highlighting + +(rf/reg-fx + :fx.app/highlight-code + (fn [id] + (.configure js/hljs #js {:cssSelector (str "#" id " pre code")}) + (.highlightAll js/hljs))) + ;; ----- Notification ------ ;; Pop-ups (toasts) diff --git a/client/web/src/flybot/client/web/core/db/fx/highlight.cljs b/client/web/src/flybot/client/web/core/db/fx/highlight.cljs new file mode 100644 index 00000000..e7594408 --- /dev/null +++ b/client/web/src/flybot/client/web/core/db/fx/highlight.cljs @@ -0,0 +1,12 @@ +(ns flybot.client.web.core.db.fx.highlight + (:require [cljsjs.highlight.langs.asciidoc] + [cljsjs.highlight.langs.bash] + [cljsjs.highlight.langs.clojure] + [cljsjs.highlight.langs.clojure-repl] + [cljsjs.highlight.langs.cpp] + [cljsjs.highlight.langs.dockerfile] + [cljsjs.highlight.langs.java] + [cljsjs.highlight.langs.javascript] + [cljsjs.highlight.langs.lisp] + [cljsjs.highlight.langs.markdown] + [cljsjs.highlight.langs.python])) diff --git a/client/web/src/flybot/client/web/core/dom/hiccup.cljs b/client/web/src/flybot/client/web/core/dom/hiccup.cljs index f0923bb9..7a2fce8e 100644 --- a/client/web/src/flybot/client/web/core/dom/hiccup.cljs +++ b/client/web/src/flybot/client/web/core/dom/hiccup.cljs @@ -1,26 +1,58 @@ (ns flybot.client.web.core.dom.hiccup - (:require [clojure.walk :refer [postwalk]] + (:require [clojure.string :as str] + [clojure.walk :refer [postwalk]] [markdown-to-hiccup.core :as mth] [re-frame.core :as rf])) +(defn- hiccup-with-properties + "Add an empty property map to Hiccup vectors without one." + [[tag & rest :as hiccup]] + (if (map? (first rest)) + hiccup + (into [tag {}] rest))) + ;; ---------- Post hiccup conversion logic ---------- (defn md-dark-image - "Extract the dark mode src from the markdown - and add it to the hiccup props." + "Extracts the dark mode src from the markdown + and add it to the hiccup props." [[tag {:keys [srcdark] :as props} value]] (if (and srcdark (= :dark @(rf/subscribe [:subs/pattern '{:app/theme ?x}]))) [tag (assoc props :src (:srcdark props)) value] [tag props value])) +(defn md-code-block + "Decorates code blocks: + + - Mark code blocks without any specified languages as plain text. + - Make code blocks responsive to dark and light themes." + [h] + (let [theme @(rf/subscribe [:subs/pattern '{:app/theme ?x}]) + [_ props content] (hiccup-with-properties h)] + (if (= :code (get content 0)) + (let [[_ code-props code-content] (hiccup-with-properties content)] + [:pre (if (= :dark theme) + (merge-with #(str/join " " %&) {:class "dark"} props) + props) + [:code (merge {:class "text"} code-props) + code-content]]) + h))) + (defn post-hiccup - "Given the hiccup, apply some customisation." + "Given the Hiccup content, applies customizations to images and code blocks: + + - Add dark versions to images. + - Mark code blocks without any specified languages as plain text." [hiccup] (->> hiccup (postwalk (fn [h] - (if (and (vector? h) (= :img (first h))) + (cond + (and (vector? h) (= :img (first h))) (md-dark-image h) + (and (vector? h) (= :pre (first h))) + (md-code-block h) + :else h))))) ;; ---------- Markdown to Hiccup ---------- diff --git a/client/web/src/flybot/client/web/core/dom/page/post.cljs b/client/web/src/flybot/client/web/core/dom/page/post.cljs index 3320d06b..10006d80 100644 --- a/client/web/src/flybot/client/web/core/dom/page/post.cljs +++ b/client/web/src/flybot/client/web/core/dom/page/post.cljs @@ -220,14 +220,18 @@ (user-info editor-name last-edit-date nil :editor)])]))) (defn post-view - [{:post/keys [css-class image-beside hiccup-content] :as post}] + [{:post/keys [id css-class image-beside hiccup-content] :as post}] (let [{:image/keys [src src-dark alt]} image-beside src (if (= :dark @(rf/subscribe [:subs/pattern '{:app/theme ?x}])) src-dark src) - full-content [[:div {:style {:height 0}} - [:a {:id (web.utils/post->url-identifier post)}]] + fragment-anchor [:div {:style {:height 0}} + [:a {:id (web.utils/post->url-identifier post)}]] + code-highlighting (fn [] (rf/dispatch [:evt.app/highlight-code + (str "post-" id)])) + full-content [fragment-anchor [post-authors post] - hiccup-content]] + hiccup-content + [code-highlighting]]] (if (seq src) ;; returns 2 hiccup divs to be displayed in 2 columns [:div.post-body @@ -254,7 +258,7 @@ (when-not (utils/temporary-id? id) [:div.post {:key id - :id id} + :id (str "post-" id)} [post-view post]])) (defn post-read @@ -264,7 +268,7 @@ :as post}] [:div.post {:key id - :id id} + :id (str "post-" id)} [:div.post-header [:form [edit-button id] @@ -279,7 +283,7 @@ [{:post/keys [id]}] [:div.post {:key id - :id id} + :id (str "post-" id)} [:div.post-header (when-not @(rf/subscribe [:subs/pattern '{:form/fields {:post/to-delete? ?x}}]) [:form @@ -339,7 +343,7 @@ (let [post-title (client.utils/post->title post)] [:div.post.short {:key id - :id id} + :id (str "post-" id)} [post-link post [:div.post-body {:class css-class} diff --git a/deps.edn b/deps.edn index 48be3065..d9349be2 100644 --- a/deps.edn +++ b/deps.edn @@ -36,6 +36,7 @@ :client {:extra-deps {com.bhauman/figwheel-main {:mvn/version "0.2.18"} org.clojure/clojurescript {:mvn/version "1.11.60"} reagent/reagent {:mvn/version "1.2.0"} + cljsjs/highlight {:mvn/version "11.7.0-0"} cljsjs/react {:mvn/version "18.2.0-1"} cljsjs/react-dom {:mvn/version "18.2.0-1"} cljsjs/react-toastify {:mvn/version "9.1.0-0"} diff --git a/resources/public/css/highlight.css b/resources/public/css/highlight.css new file mode 100644 index 00000000..89572966 --- /dev/null +++ b/resources/public/css/highlight.css @@ -0,0 +1,270 @@ +/* + Themes are obtained from the highlight.js repository at + https://github.com/highlightjs/highlight.js/tree/main/src/styles + */ + +pre code.hljs { + display: block; + overflow-x: auto; + padding: 1rem; +} + +code.hljs { + padding: 3px 5px; +} + +/* ------------------------------------------------------------------------- */ + +/*! + Theme: GitHub + Description: Light theme as seen on github.com + Author: github.com + Maintainer: @Hirse + Updated: 2021-05-15 + + Outdated base version: https://github.com/primer/github-syntax-light + Current colors taken from GitHub's CSS +*/ + +.hljs { + color: #24292e; + background: #ffffff; +} + +.hljs-doctag, +.hljs-keyword, +.hljs-meta .hljs-keyword, +.hljs-template-tag, +.hljs-template-variable, +.hljs-type, +.hljs-variable.language_ { + /* prettylights-syntax-keyword */ + color: #d73a49; +} + +.hljs-title, +.hljs-title.class_, +.hljs-title.class_.inherited__, +.hljs-title.function_ { + /* prettylights-syntax-entity */ + color: #6f42c1; +} + +.hljs-attr, +.hljs-attribute, +.hljs-literal, +.hljs-meta, +.hljs-number, +.hljs-operator, +.hljs-variable, +.hljs-selector-attr, +.hljs-selector-class, +.hljs-selector-id { + /* prettylights-syntax-constant */ + color: #005cc5; +} + +.hljs-regexp, +.hljs-string, +.hljs-meta .hljs-string { + /* prettylights-syntax-string */ + color: #032f62; +} + +.hljs-built_in, +.hljs-symbol { + /* prettylights-syntax-variable */ + color: #e36209; +} + +.hljs-comment, +.hljs-code, +.hljs-formula { + /* prettylights-syntax-comment */ + color: #6a737d; +} + +.hljs-name, +.hljs-quote, +.hljs-selector-tag, +.hljs-selector-pseudo { + /* prettylights-syntax-entity-tag */ + color: #22863a; +} + +.hljs-subst { + /* prettylights-syntax-storage-modifier-import */ + color: #24292e; +} + +.hljs-section { + /* prettylights-syntax-markup-heading */ + color: #005cc5; + font-weight: bold; +} + +.hljs-bullet { + /* prettylights-syntax-markup-list */ + color: #735c0f; +} + +.hljs-emphasis { + /* prettylights-syntax-markup-italic */ + color: #24292e; + font-style: italic; +} + +.hljs-strong { + /* prettylights-syntax-markup-bold */ + color: #24292e; + font-weight: bold; +} + +.hljs-addition { + /* prettylights-syntax-markup-inserted */ + color: #22863a; + background-color: #f0fff4; +} + +.hljs-deletion { + /* prettylights-syntax-markup-deleted */ + color: #b31d28; + background-color: #ffeef0; +} + +.hljs-char.escape_, +.hljs-link, +.hljs-params, +.hljs-property, +.hljs-punctuation, +.hljs-tag { + /* purposely ignored */ +} + +/* ------------------------------------------------------------------------- */ + +/*! + Theme: GitHub Dark + Description: Dark theme as seen on github.com + Author: github.com + Maintainer: @Hirse + Updated: 2021-05-15 + + Outdated base version: https://github.com/primer/github-syntax-dark + Current colors taken from GitHub's CSS +*/ + +.dark .hljs { + color: #c9d1d9; + background: #0d1117; +} + +.dark .hljs-doctag, +.dark .hljs-keyword, +.dark .hljs-meta .hljs-keyword, +.dark .hljs-template-tag, +.dark .hljs-template-variable, +.dark .hljs-type, +.dark .hljs-variable.language_ { + /* prettylights-syntax-keyword */ + color: #ff7b72; +} + +.dark .hljs-title, +.dark .hljs-title.class_, +.dark .hljs-title.class_.inherited__, +.dark .hljs-title.function_ { + /* prettylights-syntax-entity */ + color: #d2a8ff; +} + +.dark .hljs-attr, +.dark .hljs-attribute, +.dark .hljs-literal, +.dark .hljs-meta, +.dark .hljs-number, +.dark .hljs-operator, +.dark .hljs-variable, +.dark .hljs-selector-attr, +.dark .hljs-selector-class, +.dark .hljs-selector-id { + /* prettylights-syntax-constant */ + color: #79c0ff; +} + +.dark .hljs-regexp, +.dark .hljs-string, +.dark .hljs-meta .hljs-string { + /* prettylights-syntax-string */ + color: #a5d6ff; +} + +.dark .hljs-built_in, +.dark .hljs-symbol { + /* prettylights-syntax-variable */ + color: #ffa657; +} + +.dark .hljs-comment, +.dark .hljs-code, +.dark .hljs-formula { + /* prettylights-syntax-comment */ + color: #8b949e; +} + +.dark .hljs-name, +.dark .hljs-quote, +.dark .hljs-selector-tag, +.dark .hljs-selector-pseudo { + /* prettylights-syntax-entity-tag */ + color: #7ee787; +} + +.dark .hljs-subst { + /* prettylights-syntax-storage-modifier-import */ + color: #c9d1d9; +} + +.dark .hljs-section { + /* prettylights-syntax-markup-heading */ + color: #1f6feb; + font-weight: bold; +} + +.dark .hljs-bullet { + /* prettylights-syntax-markup-list */ + color: #f2cc60; +} + +.dark .hljs-emphasis { + /* prettylights-syntax-markup-italic */ + color: #c9d1d9; + font-style: italic; +} + +.dark .hljs-strong { + /* prettylights-syntax-markup-bold */ + color: #c9d1d9; + font-weight: bold; +} + +.dark .hljs-addition { + /* prettylights-syntax-markup-inserted */ + color: #aff5b4; + background-color: #033a16; +} + +.dark .hljs-deletion { + /* prettylights-syntax-markup-deleted */ + color: #ffdcd7; + background-color: #67060c; +} + +.dark .hljs-char.escape_, +.dark .hljs-link, +.dark .hljs-params, +.dark .hljs-property, +.dark .hljs-punctuation, +.dark .hljs-tag { + /* purposely ignored */ +} diff --git a/resources/public/css/style.css b/resources/public/css/style.css index 4766ed2b..90eab068 100644 --- a/resources/public/css/style.css +++ b/resources/public/css/style.css @@ -123,7 +123,8 @@ h6 { color: var(--text-primary-color); } -p { +p, +pre{ padding: 0.5rem 0rem; } diff --git a/resources/public/index.html b/resources/public/index.html index fd44bdb5..303644b9 100644 --- a/resources/public/index.html +++ b/resources/public/index.html @@ -34,6 +34,7 @@ +