-
Notifications
You must be signed in to change notification settings - Fork 61
/
klipse.clj
152 lines (131 loc) · 5.36 KB
/
klipse.clj
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
(ns cryogen-core.klipse
(:require
[camel-snake-kebab.core :refer [->snake_case_string ->camelCaseString]]
[cheshire.core :as json]
[clojure.string :as string]
[cryogen-core.util :as util]
[net.cgrand.enlive-html :as enlive]))
;;;;;;;;;;;
;; utils
(defn map-keys
"Applies f to each key in m"
[f m]
(zipmap (map f (keys m)) (vals m)))
(defn update-existing
"Like clojure.core/update, but returns m untouched if it doesn't contain k"
[m k f & args]
(if (contains? m k)
(apply update m k f args)
m))
(def map-or-nil? (some-fn map? nil?))
(defn deep-merge
"Like clojure.core/merge, but also merges nested maps under the same key."
[& ms]
(apply merge-with
(fn [v1 v2]
(if (and (map-or-nil? v1) (map-or-nil? v2))
(deep-merge v1 v2)
v2))
ms))
(defn code-block-classes
"Takes a string of html and returns a sequence of
all the classes on all code blocks."
[html]
(->> html
(util/filter-html-elems (comp #{:code} :tag))
(keep (comp :class :attrs))
(mapcat #(string/split % #" "))))
;;;;;;;;;;;;
;; klipse
(defn eval-classes
"Takes the :settings map and returns all values that are css class selectors."
[settings]
(filter #(string/starts-with? % ".") (vals settings)))
(defn tag-nohighlight
"Takes html as an Enlive DOM and a coll of class-selectors and adds
the class `nohighlight` to all code blocks that includes one of them
so that the default code highlighter ignores them."
[html settings]
(letfn [(tag [h clas]
(enlive/transform h
[(keyword (str "code" clas))]
(fn [x]
(update-in x [:attrs :class] #(str % " nohighlight")))))]
(reduce tag html (eval-classes settings))))
(def defaults
{:js-src {:min "https://storage.googleapis.com/app.klipse.tech/plugin_prod/js/klipse_plugin.min.js"
:non-min "https://storage.googleapis.com/app.klipse.tech/plugin/js/klipse_plugin.js"}
:css-base "https://storage.googleapis.com/app.klipse.tech/css/codemirror.css"})
;; This needs to be updated whenever a new clojure selector is introduced.
;; It should only be necessary for react wrappers and the like, so not very often.
;; When (if?) self hosted cljs becomes compatible with advanced builds
;; this can be removed and we can just always use minified js.
(def clojure-selectors
"A set of selectors that imply clojure evaluation."
#{"selector" "selector_reagent"})
(defn clojure-eval-classes
"Takes settings and returns a set of the html classes that imply clojure eval."
[normalized-settings]
(reduce (fn [classes selector]
(if-let [klass (get normalized-settings selector)]
(conj classes (->> klass rest (apply str))) ;; Strip the leading .
classes))
#{} clojure-selectors))
(defn clojure-eval?
"Takes settings and html and returns whether there is any clojure eval."
[normalized-settings html]
(boolean (some (clojure-eval-classes normalized-settings) (code-block-classes html))))
(defn normalize-settings
"Transform the keys to the correct snake-case or camelCase strings."
[settings]
(-> (map-keys ->snake_case_string settings)
(update-existing "codemirror_options_in" (partial map-keys ->camelCaseString))
(update-existing "codemirror_options_out" (partial map-keys ->camelCaseString))))
(defn klipsify?
"Whether to klipsify a post based on the global and local configs."
[global local]
(boolean
(and local (or (seq global)
(and (map? local) (seq local))))))
(defn merge-configs
"Merges the defaults, global config and post config,
transforms lisp-case keywords into snake_case/camelCase strings
Returns nil if there's no post-config.
A post-config with the value true counts as an empty map."
[global local]
(let [local (if (true? local) {} local)]
(deep-merge defaults
(update-existing global :settings normalize-settings)
(update-existing local :settings normalize-settings))))
(defn infer-clojure-eval
"Infers whether there's clojure eval and returns the config with the
appropriate value assoc'd to :js.
Returns the config untouched if :js is already specified."
[config html]
(if (:js config)
config
(assoc config
:js
(if (clojure-eval? (:settings config) html) :non-min :min))))
(defn include-css [href]
(str "<link rel=\"stylesheet\" type=\"text/css\" href=" (pr-str href) ">"))
(defn include-js [src]
(str "<script src=" (pr-str src) "></script>"))
(defn emit
"Takes the final klipse config and returns the html to include on the bottom of the page."
[{:keys [settings js-src js css-base css-theme]}]
(str (include-css css-base) "\n"
(when css-theme (str (include-css css-theme) "\n"))
"<script>\n"
"window.klipse_settings = " (json/generate-string settings {:pretty true}) ";\n"
"</script>\n"
(include-js (js js-src))))
(defn klipsify
"Klipsifies (or not) a post depending on the global and local klipse configs."
[{:keys [klipse/global klipse/local content-dom] :as post}]
(if-not (klipsify? global local)
(dissoc post :klipse)
(let [cfg (-> (merge-configs global local) (infer-clojure-eval content-dom))]
(-> post
(assoc :klipse (emit cfg))
(update :content-dom tag-nohighlight (:settings cfg))))))