From ac52785b19ecc62e391e73e8605f55729b894ce2 Mon Sep 17 00:00:00 2001 From: Aleksander Madland Stapnes Date: Thu, 19 Jan 2017 14:33:38 -0300 Subject: [PATCH 1/4] Automatic klipse integration --- project.clj | 1 + src/cryogen_core/compiler.clj | 7 ++- src/cryogen_core/klipse.clj | 92 +++++++++++++++++++++++++++++++ test/cryogen_core/klipse_test.clj | 43 +++++++++++++++ 4 files changed, 141 insertions(+), 2 deletions(-) create mode 100644 src/cryogen_core/klipse.clj create mode 100644 test/cryogen_core/klipse_test.clj diff --git a/project.clj b/project.clj index e29657d..57d65b7 100644 --- a/project.clj +++ b/project.clj @@ -4,6 +4,7 @@ :license {:name "Eclipse Public License" :url "http://www.eclipse.org/legal/epl-v10.html"} :dependencies [[org.clojure/clojure "1.8.0"] + [camel-snake-kebab "0.4.0"] [cheshire "5.7.0"] [clj-rss "0.2.3"] [clj-text-decoration "0.0.3"] diff --git a/src/cryogen_core/compiler.clj b/src/cryogen_core/compiler.clj index 053d1ba..4729618 100644 --- a/src/cryogen_core/compiler.clj +++ b/src/cryogen_core/compiler.clj @@ -8,6 +8,7 @@ [selmer.util :refer [set-custom-resource-path!]] [text-decoration.core :refer :all] [cryogen-core.io :as cryogen-io] + [cryogen-core.klipse :as klipse] [cryogen-core.markup :as m] [cryogen-core.rss :as rss] [cryogen-core.sass :as sass] @@ -111,7 +112,8 @@ (merge (merge-meta-and-content file-name page-meta content) {:uri (page-uri file-name :page-root-uri config) - :page-index (:page-index page-meta)}))) + :page-index (:page-index page-meta) + :klipse (klipse/emit (:klipse config) (:klipse page-meta))}))) (defn parse-post "Return a map with the given post's information." @@ -128,7 +130,8 @@ :formatted-archive-group formatted-group :parsed-archive-group (.parse archive-fmt formatted-group) :uri (page-uri file-name :post-root-uri config) - :tags (set (:tags page-meta))})))) + :tags (set (:tags page-meta)) + :klipse (klipse/emit (:klipse config) (:klipse page-meta))})))) (defn read-posts "Returns a sequence of maps representing the data from markdown files of posts. diff --git a/src/cryogen_core/klipse.clj b/src/cryogen_core/klipse.clj new file mode 100644 index 0000000..2ea5705 --- /dev/null +++ b/src/cryogen_core/klipse.clj @@ -0,0 +1,92 @@ +(ns cryogen-core.klipse + (:require + [camel-snake-kebab.core :refer [->snake_case_string ->camelCaseString]] + [cheshire.core :as json])) + +;;;;;;;;;;; +;; 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)) + +;;;;;;;;;;;; +;; klipse + +(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? + "Does the configuration include any keys that imply clojure eval?" + [normalized-cfg] + (some clojure-selectors (keys normalized-cfg))) + +(defn normalize-settings + "Transform the keys to the correct snake-case or camelCase strings." + [cfg] + (-> (map-keys ->snake_case_string cfg) + (update-existing "codemirror_options_in" (partial map-keys ->camelCaseString)) + (update-existing "codemirror_options_out" (partial map-keys ->camelCaseString)))) + +(defn merge-configs [global-config post-config] + (when post-config + (let [post-config (if (true? post-config) {} post-config) + merged-config (deep-merge defaults + (update-existing global-config :settings normalize-settings) + (update-existing post-config :settings normalize-settings))] + (when (:settings merged-config) + (if (:js merged-config) + merged-config + (assoc merged-config :js (if (clojure-eval? (:settings merged-config)) + :non-min + :min))))))) + +(defn include-css [href] + (str "")) + +(defn include-js [src] + (str "")) + +(defn emit [global-config post-config] + (when-let [{:keys [settings js-src js css-base css-theme]} + (merge-configs global-config post-config)] + + (assert (#{:min :non-min} js) + (str ":js needs to be one of :min or :non-min but was: " js)) + + (str (include-css css-base) "\n" + (when css-theme (str (include-css css-theme) "\n")) + "\n" + (include-js (js js-src))))) diff --git a/test/cryogen_core/klipse_test.clj b/test/cryogen_core/klipse_test.clj new file mode 100644 index 0000000..5e92a19 --- /dev/null +++ b/test/cryogen_core/klipse_test.clj @@ -0,0 +1,43 @@ +(ns cryogen-core.klipse-test + (:require [cryogen-core.klipse :refer :all] + [clojure.test :refer [deftest testing is are]])) + +(deftest map-keys-test + (is (= {"a" 1 "b" 2} (map-keys name {:a 1 :b 2})))) + +(deftest update-existing-test + (is (= {:a 1 :b 2} (update-existing {:a 1 :b 1} :b inc))) + (is (= {:a 1} (update-existing {:a 1} :b (constantly 2))))) + +(deftest deep-merge-test + (is (= {:a {:b 1 :c 2}} (deep-merge {:a {:b 1}} {:a {:c 2}}))) + (is (= {:a {:b 1}} (deep-merge {:a {:b 1}} {:a nil}))) + (is (= {:a {:b 1 :c 3}} (deep-merge {:a {:b 1 :c 2}} {:a {:c 3}})))) + +(deftest normalize-settings-test + (is (= {"selector_reagent" ".reagent" + "codemirror_options_in" {"lineNumbers" true}} + (normalize-settings + {:selector-reagent ".reagent" + :codemirror-options-in {:line-numbers true}})))) + +(deftest merge-configs-test + (testing "Things are merged correctly, and :js :non-min is inferred from :selector." + (is (= (merge defaults + {:settings {"selector" ".clojure" + "codemirror_options_in" {"lineNumbers" true}} + :js :non-min}) + (merge-configs {:settings {:codemirror-options-in {:line-numbers true}}} + {:settings {:selector ".clojure"}})))) + + (testing "If it's all set up in config.edn, in the post it can be just :klipse true" + (is (= (merge defaults + {:settings {"selector_js" ".javascript"} + :js :min}) + (merge-configs {:settings {:selector-js ".javascript"}} true)))) + + (testing "Returns nil if there's nothing in the blog post" + (is (nil? (merge-configs {:settings {:selector ".clojure"}} nil)))) + + (testing "If there's no :settings, returns nil" + (is (nil? (merge-configs {:css-base "/css/base.css"} {:css-theme "/css/theme.css"}))))) From c5d995b2626c61f33d470b69898e6121b6d77a4e Mon Sep 17 00:00:00 2001 From: Aleksander Madland Stapnes Date: Thu, 19 Jan 2017 20:33:40 -0300 Subject: [PATCH 2/4] Add some docstrings --- src/cryogen_core/klipse.clj | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/src/cryogen_core/klipse.clj b/src/cryogen_core/klipse.clj index 2ea5705..f997d5f 100644 --- a/src/cryogen_core/klipse.clj +++ b/src/cryogen_core/klipse.clj @@ -58,12 +58,22 @@ (update-existing "codemirror_options_in" (partial map-keys ->camelCaseString)) (update-existing "codemirror_options_out" (partial map-keys ->camelCaseString)))) -(defn merge-configs [global-config post-config] +(defn merge-configs + "Merges the defaults, global config and post config, + transforms lisp-case keywords into snake_case/camelCase strings + and infers whether to use minified or non-minified js + (this can be overridden by supplying a :js key). + If the post config is just true, uses only the global config. + Returns nil if either there's no post-config or if there's no settings after merging." + [global-config post-config] (when post-config (let [post-config (if (true? post-config) {} post-config) merged-config (deep-merge defaults (update-existing global-config :settings normalize-settings) (update-existing post-config :settings normalize-settings))] + ;; It would make more sense to return nil if there's no selector specified + ;; instead of no settings, but that would be too much of a + ;; maintenance burden, as there are new selectors added all the time. (when (:settings merged-config) (if (:js merged-config) merged-config @@ -77,7 +87,10 @@ (defn include-js [src] (str "")) -(defn emit [global-config post-config] +(defn emit + "Takes the :klipse config from config.edn and the :klipse config from the + current post, and returns the html to include on the bottom of the page." + [global-config post-config] (when-let [{:keys [settings js-src js css-base css-theme]} (merge-configs global-config post-config)] From f7f02f74331687ff3779b538e9fd60f629fc34a9 Mon Sep 17 00:00:00 2001 From: Aleksander Madland Stapnes Date: Fri, 20 Jan 2017 16:16:01 -0300 Subject: [PATCH 3/4] Change confusing local names. cfg -> settings --- src/cryogen_core/klipse.clj | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/cryogen_core/klipse.clj b/src/cryogen_core/klipse.clj index f997d5f..18d95bd 100644 --- a/src/cryogen_core/klipse.clj +++ b/src/cryogen_core/klipse.clj @@ -47,14 +47,14 @@ #{"selector" "selector_reagent"}) (defn clojure-eval? - "Does the configuration include any keys that imply clojure eval?" - [normalized-cfg] - (some clojure-selectors (keys normalized-cfg))) + "Do the settings include any keys that imply clojure eval?" + [normalized-settings] + (some clojure-selectors (keys normalized-settings))) (defn normalize-settings "Transform the keys to the correct snake-case or camelCase strings." - [cfg] - (-> (map-keys ->snake_case_string cfg) + [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)))) From 70c95b41d89962f51e486f6fea69b65dc8180aa2 Mon Sep 17 00:00:00 2001 From: Aleksander Madland Stapnes Date: Fri, 27 Jan 2017 02:57:33 -0300 Subject: [PATCH 4/4] Look at the content as well as the config to infer clojure eval Before only the config was looked at to determine whether to include the js minified of not, now look at the html as well. This makes it more convenient to set up all klipse stuff once in config.edn without having to manually specify which js to include. --- src/cryogen_core/compiler.clj | 12 +++-- src/cryogen_core/klipse.clj | 78 +++++++++++++++++++++---------- test/cryogen_core/klipse_test.clj | 56 +++++++++++++++++----- 3 files changed, 105 insertions(+), 41 deletions(-) diff --git a/src/cryogen_core/compiler.clj b/src/cryogen_core/compiler.clj index 4729618..6403cca 100644 --- a/src/cryogen_core/compiler.clj +++ b/src/cryogen_core/compiler.clj @@ -113,7 +113,7 @@ (merge-meta-and-content file-name page-meta content) {:uri (page-uri file-name :page-root-uri config) :page-index (:page-index page-meta) - :klipse (klipse/emit (:klipse config) (:klipse page-meta))}))) + :klipse (klipse/merge-configs (:klipse config) (:klipse page-meta))}))) (defn parse-post "Return a map with the given post's information." @@ -131,7 +131,7 @@ :parsed-archive-group (.parse archive-fmt formatted-group) :uri (page-uri file-name :post-root-uri config) :tags (set (:tags page-meta)) - :klipse (klipse/emit (:klipse config) (:klipse page-meta))})))) + :klipse (klipse/merge-configs (:klipse config) (:klipse page-meta))})))) (defn read-posts "Returns a sequence of maps representing the data from markdown files of posts. @@ -477,11 +477,15 @@ (println (green "compiling assets...")) (let [{:keys [^String site-url blog-prefix rss-name recent-posts sass-dest keep-files ignored-files previews? author-root-uri theme] :as config} (read-config) - posts (add-prev-next (read-posts config)) + posts (map (fn [{:keys [klipse content] :as post}] + (assoc post :klipse (klipse/emit klipse content))) + (add-prev-next (read-posts config))) posts-by-tag (group-by-tags posts) posts (tag-posts posts config) latest-posts (->> posts (take recent-posts) vec) - pages (read-pages config) + pages (map (fn [{:keys [klipse content] :as page}] + (assoc page :klipse (klipse/emit klipse content))) + (read-pages config)) home-page (->> pages (filter #(boolean (:home? %))) (first)) diff --git a/src/cryogen_core/klipse.clj b/src/cryogen_core/klipse.clj index 18d95bd..3735c10 100644 --- a/src/cryogen_core/klipse.clj +++ b/src/cryogen_core/klipse.clj @@ -1,7 +1,8 @@ (ns cryogen-core.klipse - (:require - [camel-snake-kebab.core :refer [->snake_case_string ->camelCaseString]] - [cheshire.core :as json])) + (:require [clojure.string :as str] + [camel-snake-kebab.core :refer [->snake_case_string ->camelCaseString]] + [cheshire.core :as json] + [net.cgrand.enlive-html :as enlive])) ;;;;;;;;;;; ;; utils @@ -28,6 +29,25 @@ v2)) ms)) +(defn filter-html-elems + "Recursively walks a sequence of enlive-style html elements depth first + and returns a flat sequence of the elements where (pred elem)" + [pred html-elems] + (reduce (fn [acc {:keys [content] :as elem}] + (into (if (pred elem) (conj acc elem) acc) + (filter-html-elems pred content))) + [] html-elems)) + +(defn code-block-classes + "Takes a string of html and returns a sequence of + all the classes on all code blocks." + [html] + (->> html + enlive/html-snippet + (filter-html-elems (comp #{:code} :tag)) + (keep (comp :class :attrs)) + (mapcat #(str/split % #" ")))) + ;;;;;;;;;;;; ;; klipse @@ -46,10 +66,19 @@ "A set of selectors that imply clojure evaluation." #{"selector" "selector_reagent"}) -(defn clojure-eval? - "Do the settings include any keys that imply clojure eval?" +(defn clojure-eval-classes + "Takes settings and returns a set of the html classes that imply clojure eval." [normalized-settings] - (some clojure-selectors (keys 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." @@ -61,25 +90,24 @@ (defn merge-configs "Merges the defaults, global config and post config, transforms lisp-case keywords into snake_case/camelCase strings - and infers whether to use minified or non-minified js - (this can be overridden by supplying a :js key). - If the post config is just true, uses only the global config. - Returns nil if either there's no post-config or if there's no settings after merging." + Returns nil if there's no post-config. + A post-config with the value true counts as an empty map." [global-config post-config] (when post-config - (let [post-config (if (true? post-config) {} post-config) - merged-config (deep-merge defaults - (update-existing global-config :settings normalize-settings) - (update-existing post-config :settings normalize-settings))] - ;; It would make more sense to return nil if there's no selector specified - ;; instead of no settings, but that would be too much of a - ;; maintenance burden, as there are new selectors added all the time. - (when (:settings merged-config) - (if (:js merged-config) - merged-config - (assoc merged-config :js (if (clojure-eval? (:settings merged-config)) - :non-min - :min))))))) + (let [post-config (if (true? post-config) {} post-config)] + (deep-merge defaults + (update-existing global-config :settings normalize-settings) + (update-existing post-config :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 "")) @@ -90,9 +118,9 @@ (defn emit "Takes the :klipse config from config.edn and the :klipse config from the current post, and returns the html to include on the bottom of the page." - [global-config post-config] + [config html] (when-let [{:keys [settings js-src js css-base css-theme]} - (merge-configs global-config post-config)] + (infer-clojure-eval config html)] (assert (#{:min :non-min} js) (str ":js needs to be one of :min or :non-min but was: " js)) diff --git a/test/cryogen_core/klipse_test.clj b/test/cryogen_core/klipse_test.clj index 5e92a19..33b15f6 100644 --- a/test/cryogen_core/klipse_test.clj +++ b/test/cryogen_core/klipse_test.clj @@ -14,6 +14,44 @@ (is (= {:a {:b 1}} (deep-merge {:a {:b 1}} {:a nil}))) (is (= {:a {:b 1 :c 3}} (deep-merge {:a {:b 1 :c 2}} {:a {:c 3}})))) +;; For testing convenience. +(defn elt + "Returns an enlive style html element." + ([tag] (elt tag nil)) + ([tag attrs & content] + {:tag tag, :attrs attrs, :content content})) + +(deftest filter-html-elems-test + (is (= [(elt :div {:class "x"} :content [(elt :div {:class "x"} "foo")]) + (elt :div {:class "x"} "foo")]) + (filter-html-elems (comp #{"x"} :class :attrs) + [(elt :h1 {:class "y"} "things!") + (elt :div {:class "x"} (elt :div {:class "x"} "foo"))]))) + +(deftest code-block-classes-test + (is (= ["clojure" "ruby"] + (code-block-classes + "

stuff

+
(def x 42)
+
123
"))))
+
+(deftest clojure-eval-classes-test
+  (is (= #{"eval-cljs" "eval-reagent"}
+         (clojure-eval-classes {"selector" ".eval-cljs"
+                                "selector_reagent" ".eval-reagent"
+                                "selector_eval_ruby" ".eval-ruby"}))))
+
+(deftest clojure-eval?-test
+  (is (clojure-eval? {"selector" ".eval-cljs"}
+                     "

stuff

+
(def x 42)
+
123
"))
+
+  (is (not (clojure-eval? {"selector" ".eval-cljs"
+                           "selector_eval_ruby" ".eval-ruby"}
+                          "

stuff

+
123
"))))
+
 (deftest normalize-settings-test
   (is (= {"selector_reagent" ".reagent"
           "codemirror_options_in" {"lineNumbers" true}}
@@ -22,22 +60,16 @@
            :codemirror-options-in {:line-numbers true}}))))
 
 (deftest merge-configs-test
-  (testing "Things are merged correctly, and :js :non-min is inferred from :selector."
+  (testing "Things are merged correctly"
     (is (= (merge defaults
-                  {:settings {"selector" ".clojure"
-                              "codemirror_options_in" {"lineNumbers" true}}
-                   :js :non-min})
+                  {:settings {"selector" ".clojure-eval"
+                              "codemirror_options_in" {"lineNumbers" true}}})
            (merge-configs {:settings {:codemirror-options-in {:line-numbers true}}}
-                          {:settings {:selector ".clojure"}}))))
+                          {:settings {:selector ".clojure-eval"}}))))
 
   (testing "If it's all set up in config.edn, in the post it can be just :klipse true"
-    (is (= (merge defaults
-                  {:settings {"selector_js" ".javascript"}
-                   :js :min})
+    (is (= (merge defaults {:settings {"selector_js" ".javascript"}})
            (merge-configs {:settings {:selector-js ".javascript"}} true))))
 
   (testing "Returns nil if there's nothing in the blog post"
-    (is (nil? (merge-configs {:settings {:selector ".clojure"}} nil))))
-
-  (testing "If there's no :settings, returns nil"
-    (is (nil? (merge-configs {:css-base "/css/base.css"} {:css-theme "/css/theme.css"})))))
+    (is (nil? (merge-configs {:settings {:selector ".clojure-eval"}} nil)))))