Skip to content

Commit

Permalink
Fix #138: #html literals (#139)
Browse files Browse the repository at this point in the history
  • Loading branch information
borkdude authored Sep 12, 2024
1 parent a0d53a2 commit 7f84c11
Show file tree
Hide file tree
Showing 6 changed files with 175 additions and 50 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
[Cherry](https://github.com/squint-cljs/cherry): Experimental ClojureScript to ES6 module compiler

## 0.3.20 (2024-09-12)

- [#138](https://github.com/squint-cljs/cherry/issues/138): Support `#html` literals, ported from squint

## 0.2.19 (2024-07-04)

- [#135](https://github.com/squint-cljs/cherry/issues/135): Fix UMD build
Expand Down
3 changes: 2 additions & 1 deletion deps.edn
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
org.babashka/sci {:mvn/version "0.3.32"}
io.github.squint-cljs/squint
#_{:local/root "/Users/borkdude/dev/squint"}
{:git/sha "3774802024e5b272e78c6978446bc972e5a4f6d8"}}
{:git/sha "8df5c90ec37ab7d8ddf1c6d73ab7d96e0597a2cf"}
funcool/promesa {:mvn/version "11.0.678"}}

:aliases
{:cljs {:extra-paths ["test"]
Expand Down
5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"type": "module",
"name": "cherry-cljs",
"sideEffects": false,
"version": "0.2.19",
"version": "0.3.20",
"files": [
"cljs.core.js",
"lib",
Expand All @@ -12,6 +12,9 @@
"bin": {
"cherry": "node_cli.js"
},
"dependencies": {
"squint-cljs": "0.8.113"
},
"devDependencies": {
"@babel/core": "^7.19.0",
"@babel/preset-react": "^7.18.6",
Expand Down
82 changes: 35 additions & 47 deletions src/cherry/compiler.cljc
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
[squint.compiler-common :as cc :refer [#?(:cljs Exception)
#?(:cljs format)
*aliases* *imported-vars* *public-vars* comma-list emit emit-args emit-infix
emit-return escape-jsx expr-env infix-operator? prefix-unary?
emit-return escape-jsx infix-operator? prefix-unary?

statement suffix-unary?]]
[squint.defclass :as defclass])
Expand All @@ -34,12 +34,18 @@
`(alter-var-root (var ~the-var) (constantly ~value))))

(defn emit-keyword [expr env]
(swap! *imported-vars* update "cherry-cljs/lib/cljs.core.js" (fnil conj #{}) 'keyword)
(emit-return (str (format "%skeyword(%s)"
(if-let [core-alias (:core-alias env)]
(str core-alias ".")
"")
(pr-str (subs (str expr) 1)))) env))
(let [js? (:js env)] ;; js is used for emitting CSS literals
(if (or (:html-attr env)
js?)
(cond-> (name expr)
js? (pr-str))
(do
(swap! *imported-vars* update "cherry-cljs/lib/cljs.core.js" (fnil conj #{}) 'keyword)
(emit-return (format "%skeyword(%s)"
(if-let [core-alias (:core-alias env)]
(str core-alias ".")
"")
(pr-str (subs (str expr) 1))) env)))))

(def special-forms (set ['var '. 'if 'funcall 'fn 'fn* 'quote 'set!
'return 'delete 'new 'do 'aget 'while
Expand All @@ -49,7 +55,8 @@
'recur 'js* 'case* 'deftype*
;; prefixed to avoid conflicts
'clava-compiler-jsx
'squint.defclass/defclass* 'squint.defclass/super*]))
'squint.defclass/defclass* 'squint.defclass/super*
'squint-compiler-html]))

(def built-in-macros (merge {'-> macros/core->
'->> macros/core->>
Expand Down Expand Up @@ -208,7 +215,7 @@
new-expr (apply macro expr {} (rest expr))]
(emit new-expr env))
(cond
(and (= (.charAt head-str 0) \.)
(and (= \. (.charAt head-str 0))
(> (count head-str) 1)
(not (= ".." head-str)))
(cc/emit-special '. env
Expand Down Expand Up @@ -237,42 +244,12 @@
:statement s
:return s))

(defn emit-vector [expr env]
(if (and (:jsx env)
(let [f (first expr)]
(or (keyword? f)
(symbol? f))))
(let [v expr
tag (first v)
attrs (second v)
attrs (when (map? attrs) attrs)
elts (if attrs (nnext v) (next v))
tag-name (symbol tag)
tag-name (if (= '<> tag-name)
(symbol "")
tag-name)
tag-name (emit tag-name (expr-env (dissoc env :jsx)))]
(emit-return (format "<%s%s>%s</%s>"
tag-name
(cc/jsx-attrs attrs env)
(let [env (expr-env env)]
(str/join "" (map #(emit % env) elts)))
tag-name) env))
(if (::js (meta expr))
(emit-return (format "[%s]"
(str/join ", " (emit-args env expr))) env)
(emit-return (format "%svector(%s)"
(if-let [core-alias (:core-alias env)]
(str core-alias ".")
"")
(str/join ", " (emit-args env expr))) env))))

(defn emit-map [expr env]
(let [env* env
env (dissoc env :jsx)
expr-env (assoc env :context :expr)
map-fn
(when-not (::js (meta expr))
(when-not (::cc/js (meta expr))
(if (<= (count expr) 8)
'array_map
'hash_map))
Expand Down Expand Up @@ -316,7 +293,7 @@
(symbol (str (if sym (munge sym)
"G__") next-id)))))
:emit {::cc/list emit-list
::cc/vector emit-vector
::cc/vector cc/emit-vector
::cc/map emit-map
::cc/keyword emit-keyword
::cc/set emit-set
Expand All @@ -327,6 +304,9 @@
(defn jsx [form]
(list 'clava-compiler-jsx form))

(defn html [form]
(list 'squint-compiler-html form))

(defmethod emit-special 'clava-compiler-jsx [_ env [_ form]]
(set! *jsx* true)
(let [env (assoc env :jsx true)]
Expand All @@ -337,8 +317,9 @@
{:all true
:end-location false
:location? seq?
:readers {'js #(vary-meta % assoc ::js true)
'jsx jsx}
:readers {'js #(vary-meta % assoc ::cc/js true)
'jsx jsx
'html html}
:read-cond :allow
:features #{:cljs :cherry}}))

Expand Down Expand Up @@ -368,7 +349,8 @@
(binding [cc/*core-package* "cherry-cljs/cljs.core.js"
*jsx* false
cc/*repl* (:repl opts cc/*repl*)]
(let [opts (merge {:ns-state (atom {})} opts)
(let [need-html-import (atom false)
opts (merge {:ns-state (atom {})} opts)
imported-vars (atom {})
public-vars (atom #{})
aliases (atom {core-alias cc/*core-package*})
Expand All @@ -385,13 +367,19 @@
cc/*cljs-ns* (:ns opts cc/*cljs-ns*)]
(let [transpiled (f x (assoc opts
:core-alias core-alias
:imports imports))
:imports imports
:need-html-import need-html-import))
_ (when @need-html-import
(swap! imports str
(if cc/*repl*
"var squint_html = await import('squint-cljs/src/squint/html.js');\n"
"import * as squint_html from 'squint-cljs/src/squint/html.js';\n")))
imports (when-not elide-imports @imports)
exports (when-not elide-exports
(str (when-let [vars (disj @public-vars "default$")]
(when (seq vars)
(str (format "\nexport { %s }\n"
(str/join ", " vars)))))
(format "\nexport { %s }\n"
(str/join ", " vars))))
(when (contains? @public-vars "default$")
"export default default$\n")))]
{:imports imports
Expand Down
4 changes: 3 additions & 1 deletion test/cherry/compiler_test.cljs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
["fs" :as fs]
[cherry.compiler :as cherry]
[cherry.jsx-test]
[cherry.html-test]
[cherry.squint-and-cherry-test]
[cherry.test-utils :refer [js! jss! jsv!]]
[clojure.string :as str]
Expand Down Expand Up @@ -489,4 +490,5 @@ IReset (-reset! [this v]
(deref x)"))))

(defn init []
(cljs.test/run-tests 'cherry.compiler-test 'cherry.jsx-test 'cherry.squint-and-cherry-test))
(cljs.test/run-tests 'cherry.compiler-test 'cherry.jsx-test 'cherry.squint-and-cherry-test
'cherry.html-test))
127 changes: 127 additions & 0 deletions test/cherry/html_test.cljs
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
(ns cherry.html-test
(:require
[clojure.test :as t :refer [deftest is]]
[cherry.test-utils :refer [jss!]]
[cherry.compiler :as squint]
[clojure.string :as str]
[promesa.core :as p]))

(defn html= [x y]
(= (str x) (str y)))

(deftest html-test
(t/async done
(is (str/includes?
(jss! "#html [:div \"Hello\"]")
"`<div>Hello</div>"))
(is (str/includes?
(jss! "#html ^foo/bar [:div \"Hello\"]")
"foo.bar`<div>Hello</div>"))
(let [{:keys [imports body]} (cherry.compiler/compile-string* "(defn foo [x] #html [:div \"Hello\" x])")]
(is (str/includes? imports "import * as squint_html from 'squint-cljs/src/squint/html.js'"))
(is (str/includes? body "squint_html.tag`<div>Hello${x}</div>")))
(let [js (cherry.compiler/compile-string "
(defn li [x] #html [:li x])
(defn foo [x] #html [:ul (map #(li %) (range x))]) (foo 5)" {:repl true :elide-exports true :context :return})
js (str/replace "(async function() { %s } )()" "%s" js)]
(-> (js/eval js)
(.then
#(is (html= "<ul><li>0</li><li>1</li><li>2</li><li>3</li><li>4</li></ul>" %)))
(.catch #(is false "nooooo"))
(.finally done)))))

(deftest html-attrs-test
(t/async done
(let [js (cherry.compiler/compile-string
"#html [:div {:class \"foo\" :id (+ 1 2 3)
:style {:color :green}}]"
{:repl true :elide-exports true :context :return})
js (str/replace "(async function() { %s } )()" "%s" js)]
(-> (js/eval js)
(.then
#(is (html= "<div class=\"foo\" id=\"6\" style=\"color:green;\"></div>" %)))
(.catch #(is false "nooooo"))
(.finally done)))))

(deftest html-nil-test
(t/async done
(let [js (cherry.compiler/compile-string
"(let [p nil] #html [:div p])"
{:repl true :elide-exports true :context :return})
js (str/replace "(async function() { %s } )()" "%s" js)]
(-> (js/eval js)
(.then
#(is (html= "<div>undefined</div>" %)))
(.catch #(is false "nooooo"))
(.finally done)))))

(deftest html-props-test
(t/async done
(let [js (cherry.compiler/compile-string
"(let [m {:a 1 :b 2}] #html [:div {:& m :a 2 :style {:color :red}} \"Hello\"])"
{:repl true :elide-exports true :context :return})
js (str/replace "(async function() { %s } )()" "%s" js)]
(-> (js/eval js)
(.then
#(is (html= "<div a=\"1\" style=\"color:red;\" b=\"2\">Hello</div>" %)))
(.catch #(is false "nooooo"))
(.finally done)))))

(deftest html-dynamic-css-test
(t/async done
(let [js (cherry.compiler/compile-string
"(let [m {:color :green}] #html [:div {:style (clj->js (assoc m :width \"200\"))} \"Hello\"])"
{:repl true :elide-exports true :context :return})
js (str/replace "(async function() { %s } )()" "%s" js)]
(-> (js/eval js)
(.then
#(is (html= "<div style=\"color:green; width:200;\">Hello</div>" %)))
(.catch #(is false "nooooo"))
(.finally done)))))

(deftest html-fragment-test
(t/async done
(let [js (cherry.compiler/compile-string
"#html [:div [:<> \"Hello\"]]"
{:repl true :elide-exports true :context :return})
js (str/replace "(async function() { %s } )()" "%s" js)]
(-> (js/eval js)
(.then
#(is (html= "<div>Hello</div>" %)))
(.catch #(do
(js/console.log %)
(is false "nooooo")))
(.finally done)))))

(defn compile-html [s]
(let [js (cherry.compiler/compile-string s
{:repl true :elide-exports true :context :return})
js (str/replace "(async function() { %s } )()" "%s" js)]
js))

(deftest html-safe-test
(t/async done
(->
(p/do
(p/let [js (compile-html
"(defn foo [x] #html [:div x]) (foo \"<>\")")
v (js/eval js)
_ (is (html= "<div>&lt;&gt;</div>" v))])
(p/let [js (compile-html
"(defn foo [x] #html [:div x]) (defn bar [] #html [:div (foo \"<script>\")])
(bar)")
v (js/eval js)
_ (is (html= "<div><div>&lt;script&gt;</div></div>" v))]))
(p/catch #(is false "nooooo"))
(p/finally done))))

(deftest html5-test
(t/async done
(->
(p/do
(p/let [js (compile-html "#html [:div [:br]]")
v (js/eval js)
_ (is (html= "<div><br></div>" v))])
)
(p/catch #(is false "nooooo"))
(p/finally done))))

0 comments on commit 7f84c11

Please sign in to comment.