chlorine1.5.3.1A naive Clojure to Javascript translator dependencies
| (this space intentionally left almost blank) | ||||||||||||||||||
(ns chlorine.js (:require [clojure.string :as str] [hiccup.core] [clojure.walk]) (:use [chlorine.reader] [slingshot.slingshot] [pathetic.core :only [normalize]] [chlorine.util :only [url? resource-path? to-resource unzip assert-args *cwd* *cpd* file-and-dir re? replace-map]])) | |||||||||||||||||||
(def ^:dynamic *print-pretty* false) | |||||||||||||||||||
(defmacro with-pretty-print [& body] `(binding [*print-pretty* true] ~@body)) | |||||||||||||||||||
(def ^:dynamic *indent* 0) | |||||||||||||||||||
(defmacro with-indent [[& increment] & body] `(binding [*indent* (+ *indent* (or ~increment 4))] ~@body)) | |||||||||||||||||||
(def ^:dynamic *in-block-exp?* false) | |||||||||||||||||||
(defmacro with-block [& body] `(binding [*in-block-exp?* true] ~@body)) | |||||||||||||||||||
(defn newline-indent [] (if *print-pretty* (do (newline) (print (apply str (repeat *indent* " ")))) (print " "))) | |||||||||||||||||||
(defmacro with-parens [[& [left right]] & body] `(do (print (or ~left "(")) ~@body (print (or ~right ")")))) | |||||||||||||||||||
(def ^:dynamic *inline-if* false) (def ^:dynamic *quoted* false) | |||||||||||||||||||
(def ^:dynamic *in-fn-toplevel* true) (def ^:dynamic *unique-return-expr* false) | |||||||||||||||||||
Chlorinejs transforms Clojure code/data to Javascript equivalents. Normal data such as strings, numbers, keywords, symbols, vectors, (quoted) lists are transformed by associated emitters of their type. Functions, macros and special forms (including javascript native ones) share the same looks: they are unquoted lists whose first element is the form name and require one more step: looking up by the names to detect their types. | |||||||||||||||||||
Detects macro/function/special form names from expressions for further processing. Used as dispatch function for chlorine.js/emit, the most hardworking multi-method in chlorine library. | (defn detect-form [expr] (let [expr (if (and (coll? expr) (seq expr)) (first expr) expr)] (if (symbol? expr) (name expr) expr))) | ||||||||||||||||||
Checks if a form is a property access or method call (a symbol starting with '.') | (defn member-form? [form-name] (and (symbol? form-name) (= \. (first (name form-name))))) | ||||||||||||||||||
Checks if a symbol is a new object call (a symbol ending with '.') | (defn new-object? [f] (and (symbol? f) (= \. (last (name f))))) | ||||||||||||||||||
Normalizes dot forms or new-object forms by removing "." from their beginnings or endings. | (defn normalize-dot-form [form] (cond (and (.startsWith (name form) ".") (< 1 (count (name form)))) (symbol (subs (name form) 1)) (and (.endsWith (name form) ".") (< 1 (count (name form)))) (symbol (apply str (drop-last (str form)))) :default form)) | ||||||||||||||||||
(declare emit-str) (declare tojs') | |||||||||||||||||||
Transforms symbol or keyword into object's property access form. | (defn sym->property [s] (binding [*quoted* true] (emit-str (if (member-form? s) (symbol (subs (name s) 1)) s)))) | ||||||||||||||||||
Receives forms, emits javascript expressions. | (defmulti emit detect-form) | ||||||||||||||||||
Emit sequences with delimiters. Useful to emit javascript arrays, function arguments etc. | (defn emit-delimited [delimiter args & [emitter]] (when-not (empty? args) ((or emitter emit) (first args)) (doseq [arg (rest args)] (print delimiter) ((or emitter emit) arg)))) | ||||||||||||||||||
Checks a map's key before emitting. Valid keys are elements which can be converted to strings. several functions to emit Clojure data of map, set, vector, regexp, symbol and keyword types | (defn valid-map-key? [key] (or (string? key) (keyword? key) (number? key) (symbol? key) (and (seq? key) (= 2 (count key)) (= 'quote (first key)) (symbol? (second key))))) | ||||||||||||||||||
Clojure maps are emitted to javascript key/value objects. Keys can only be strings. Keywords and quoted symbols don't really make sense in Chlorinejs and that's why they are emitted to plain strings. | (defn emit-map [expr] (with-parens ["{" "}"] (binding [*inline-if* true] (emit-delimited "," (seq expr) (fn [[key val]] (if (valid-map-key? key) (emit key) (throw+ {:known-error true :msg (str "Error emitting this map `" expr "`:\n" "Invalid map key: `" key "`.\n" "Valid keys are elements which can be" " converted to strings.") :causes [expr key]})) (print " : ") (emit val)))))) | ||||||||||||||||||
Clojure sets are emitted to javascript key/value objects whose
values are all | (defn emit-set [expr] (emit `(hash-set ~@(seq expr)))) | ||||||||||||||||||
Clojure vectors and quoted lists are emitted as javascript arrays. | (defn emit-vector [expr] (with-parens ["[" "]"] (binding [*inline-if* true] (emit-delimited "," (seq expr))))) | ||||||||||||||||||
(defn emit-re [expr] (let [[_ flags pattern] (re-find #"^(?:\(\?([idmsux]*)\))?(.*)" (str expr))] (print (str \/ (.replaceAll (re-matcher #"/" pattern) "\\\\/") \/ flags)))) | |||||||||||||||||||
Symbols are Chlorine's amazing pieces. We have a wide range of valid
characters for Chlorine just like Clojure. You can use Lisp-style naming
conventions such as "?" endings for predicate functions.
Because javascript doesn't allow such characters, the function
| |||||||||||||||||||
The mapping used to do the replacements | (def ^:dynamic *symbol-map* (array-map #"^boolean$" "boolean*" ;; `int` is a reserved symbol name in js. It's also a function ;; name in Clojure and Chlorine core library. ;; `int` (the function) is emitted as `int*` instead #"^int$" "int*" ;; Chlorine uses a Clojure-like syntax of `(require ...)` ;; to load nodejs/browserify. It's implemented as macro which ;; expands to the lower level `require*`. `require*` in turn ;; emitted as javascript `require()` #"^require\*$" "require" "$" "$USD$" "->" "$ARROW$" "=>" "$BARROW$" "<-" "$LARROW$" ">" "$GT$" "<" "$LT$" ">=" "$GE$" "=<" "$LE$" "-" "_" "'" "$QUOT$" "!" "$EXCL$" "?" "$QUEST$" "#" "$HASH$" "%" "$P100$" "&" "$AND$" "*" "$STAR$" "+" "$PLUS$" "=" "$EQ$" "|" "$PIPE$")) | ||||||||||||||||||
You can also specify "reserved symbols", which are NOT affected by
| (def ^:dynamic *reserved-symbols* [#"^\$.*" #"^\.\$.*"]) | ||||||||||||||||||
Emits Clojure symbols to javascript ones. If the symbol is quoted, emits its name as a string. Does some replacements with characters not supported by javascript if the symbol isn't marked as reserved ones. | (defn emit-symbol [expr] (let [sym (name expr)] (print (if *quoted* (str "'" (name expr) "'") (if (reserved-symbol? *reserved-symbols* sym) sym (replace-map sym *symbol-map*)))))) | ||||||||||||||||||
Emits Clojure keywords. Uses emit-symbol as backend. | (defn emit-keyword [expr] (binding [*quoted* true] (emit-symbol expr))) | ||||||||||||||||||
Some Chlorine forms are converted directly to javascript native operators: unary and infix ones. | |||||||||||||||||||
Unary operators in Chlorine: "!"
Infix operators are consist of
- Please use high-level functions from Chlorine's core library instead of low-level ones" | |||||||||||||||||||
(defn emit-unary-operator [op arg] (print (name op)) (emit arg)) | |||||||||||||||||||
(defn emit-infix-operator [op & args] (let [clj->js {"instance?" "instanceof" "and" "&&" "or" "||" "=*" "===" "+*" "+" "-*" "-" "**" "*" "rem" "%" "bit-and" "&" "bit-or" "|" "bit-xor" "^" "bit-not" "~" "bit-shift-left" "<<" "bit-shift-right" ">>" "bit-shift-right-zero-fill" ">>>"} js-op (get clj->js (name op) (name op))] (binding [*unique-return-expr* false *in-fn-toplevel* false] (with-parens [] (emit-delimited (str " " js-op " ") args))))) | |||||||||||||||||||
Removes | (defn property->member [property] (symbol (subs (name property) 1))) | ||||||||||||||||||
Emits a function call by simply emitting the function name and its arguments in parentheses. | (defn emit-function-call [fun & args] (emit fun) (with-parens [] (with-indent [] (emit-delimited ", " args)))) | ||||||||||||||||||
Like emit-function-call, but wraps the function in parentheses. Used to emit function calls where function is not a symbol but an other form instead. | (defn emit-invoke-function [fun & args] (with-parens [] (emit fun)) (with-parens [] (emit-delimited "," args))) | ||||||||||||||||||
All Clojure forms return something (even nil). Javascript is imperative
and its forms may or may not return values. Javascript function bodies
require a manual That's why we create this dynamic var with initial value | (def ^:dynamic *return-expr* false) | ||||||||||||||||||
Consumes return-expr | (defmacro with-return-expr [[& [new-val]] & body] `(binding [*return-expr* (if *return-expr* (do (print "return ") false) (or ~new-val false))] ~@body)) | ||||||||||||||||||
Emits function forms such as: unary and infix operator calls, applying keyword on a map, method calls, creating new object calls, and normal function calls. | (defn emit-function-form [form] (binding [*inline-if* true *unique-return-expr* false *in-fn-toplevel* false] (let [[fun & args] form] (cond ;; those are not normal function calls (unary-operator? fun) (apply emit-unary-operator form) (infix-operator? fun) (apply emit-infix-operator form) (keyword? fun) (let [[map & default] args] (emit `(get ~map ~fun ~@default))) (member-form? fun) (let [[object & margs] args] (emit `(. ~object ~(normalize-dot-form fun) ~@margs))) (new-object? fun) (emit `(new ~(normalize-dot-form fun) ~@args)) ;; Normal function calls: ;; - Ensures caller are in parentheses by using `emit-invoke-function` ;; instead of `emit-function-call` in case the caller is not simply ;; a symbol. (coll? fun) (apply emit-invoke-function form) true (apply emit-function-call form))))) | ||||||||||||||||||
Emits an expression with trailing | (defn emit-statement [expr] (try+ (binding [*inline-if* false] (if (and (coll? expr) (#{'defmacro 'load-js 'load-file 'load-file-macros} (first expr))) (emit expr) (do (newline-indent) (emit expr) (when-not (and (coll? expr) (#{'do 'let 'let*} (first expr))) (print ";"))))) (catch map? e (throw+ (merge e {:causes (conj (or (:causes e) []) expr)}))) (catch Throwable e (throw+ {:known-error false :msg (.getMessage e) :causes [expr] :trace e})))) | ||||||||||||||||||
(defn emit-statements [exprs] (doseq [expr exprs] (emit-statement expr))) | |||||||||||||||||||
Emits statements with the manual | (defn emit-statements-with-return [exprs] (binding [*return-expr* false] (doseq [expr (butlast exprs)] (emit-statement expr))) (when (not= 'nil (last exprs)) (emit-statement (last exprs)))) | ||||||||||||||||||
Lispers love macros. In fact, making a macro means writing a function
that returns some code. Chlorine macros are nearly Clojure ones:
you can write the function (by | |||||||||||||||||||
When defined, new macros are added to a ref holding a map. The map keys are macro names while the values are the macro functions (the one that generates code). | (def ^:dynamic *macros* (ref {})) | ||||||||||||||||||
Checks if a macro with that name is defined. | (defn macro? [n] (and (symbol? n) (contains? @*macros* (name n)))) | ||||||||||||||||||
Gets the macro function by its name in order to generate code. | (defn get-macro [n] (and (symbol? n) (get @*macros* (name n)))) | ||||||||||||||||||
Removes a macro from known macro list. | (defn undef-macro [n] (when (macro? n) (when *print-pretty* (println "// undefining macro" n)) (dosync (alter *macros* dissoc (name n))))) | ||||||||||||||||||
(defmethod emit "defmacro" [[_ mname & mdeclrs]] (try+ (let [mdeclrs (if (string? (first mdeclrs)) (rest mdeclrs) mdeclrs)] (when *print-pretty* (println "// defining macro" mname)) (dosync (alter *macros* conj {(name mname) (eval `(clojure.core/fn ~@mdeclrs))}))) (catch Throwable e (throw+ {:known-error true :msg (str "Error defining macro `" mname "`:\n" (.getMessage e)) :causes [`(~_ ~mname ~@mdeclrs)] :trace e}))) ;; returns `nil` because output are consumed by `with-out-str` nil) | |||||||||||||||||||
Many Clojure macros work the same in Chlorine. Use this function to reuse them instead of rewriting. | (defn borrow-macros [& syms] (doseq [sym syms] (dosync (alter *macros* conj {(name sym) (try+ (fn [& args#] (apply (resolve sym) (concat [nil nil] args#))) (catch Throwable e (throw+ {:known-error true :msg (str "Error borrowing macro `" sym "`:\n" (.getMessage e)) :causes [`(borrow-macros ~sym)] :trace e})))})))) | ||||||||||||||||||
Gets and executes macro function, returns the Chlorine code. | (defn expand-macro-1 [form] (let [[mac-name & args] form] (if-let [mac (get-macro mac-name)] (try+ (apply mac args) (catch Throwable e (throw+ {:known-error true :msg (str "Error expanding macro `" form "`:\n" (.getMessage e)) :causes [form] :trace e}))) form))) | ||||||||||||||||||
Repeatedly calls expand-macro-1 on form until it no longer represents a macro form, then returns it. Note neither expand-macro-1 nor expand-macro expand macros in subforms. | (defn expand-macro [form] (let [ex (expand-macro-1 form)] (if (identical? ex form) form (expand-macro-1 ex)))) | ||||||||||||||||||
Gets and executes macro function, emits the result as Chlorine code. | (defn emit-macro-expansion [form] (emit (expand-macro-1 form))) | ||||||||||||||||||
Prints docstrings as javascript comments. | (defn emit-docstring [docstring] (when *print-pretty* (let [lines (str/split-lines docstring)] (newline-indent) (print (str "/* " (first lines))) (doseq [line (rest lines)] (newline-indent) (print (str " " line))) (print " */")))) | ||||||||||||||||||
(defn- ignorable-arg? [n] (and (symbol? n) (.startsWith (name n) "_"))) | |||||||||||||||||||
ChlorineJS produces a lot of temporary javascript symbols. To ensure all these symbols are unique, we use this counter | (def ^:dynamic *temp-sym-count* nil) | ||||||||||||||||||
Generates an unique temporary symbol. | (defn tempsym [] (dosync (ref-set *temp-sym-count* (+ 1 @*temp-sym-count*)) (symbol (str "_temp_" @*temp-sym-count*)))) | ||||||||||||||||||
Chlorine supports the following Clojure binding forms: - Basic binding with just a single symbol - Destructuring binding with sequences or maps | |||||||||||||||||||
(defn- emit-simple-binding [vname val] (emit (if (ignorable-arg? vname) (tempsym) vname)) (print " = ") (binding [*inline-if* true] (emit val))) | |||||||||||||||||||
(declare emit-var-bindings emit-destructured-seq-binding emit-destructured-map-binding) | |||||||||||||||||||
(defn- emit-binding [vname val] (binding [*inline-if* true] (let [emitter (cond (vector? vname) emit-destructured-seq-binding (map? vname) emit-destructured-map-binding :else emit-simple-binding)] (emitter vname val)))) | |||||||||||||||||||
Note on choice of get/get* in destructuring:
- destructuring seq use | (defn- emit-destructured-seq-binding [vvec val] (let [temp (tempsym)] (print (str temp " = ")) (emit val) (loop [vseq vvec, i 0, seen-rest? false] (when (seq vseq) (let [vname (first vseq) vval (second vseq)] (print ", ") (condp = vname '& (cond seen-rest? (throw+ {:known-error true :msg (str "Unsupported binding form `" vvec "`:\n" "only `:as` can follow `&`") :causes [vvec]}) (not (symbol? vval)) (throw+ {:known-error true :msg (str "Unsupported binding form `" vvec "`:\n" "`&` must be followed by exactly one symbol") :causes [vvec]}) :else (do (emit-binding vval `(.slice ~temp ~i)) (recur (nnext vseq) (inc i) true))) :as (cond (not= (count (nnext vseq)) 0) (throw+ {:known-error true :msg (str "Unsupported binding form `" vvec "`:\n" "nothing may follow after `:as <binding>`") :causes [vvec]}) (not (symbol? vval)) (throw+ {:known-error true :msg (str "Unsupported binding form, `" vvec "`:\n" "`:as` must be followed by a symbol") :causes [vvec]}) :else (emit-binding vval temp)) (do (emit-binding vname `(get* ~temp ~i)) (recur (next vseq) (inc i) seen-rest?)))))))) | ||||||||||||||||||
(defn- emit-destructured-map-binding [vmap val] (let [temp (or (:as vmap) (tempsym)) defaults (get vmap :or) keysmap (reduce #(assoc %1 %2 (keyword %2)) {} (mapcat vmap [:keys :strs :syms])) vmap (merge (dissoc vmap :as :or :keys :strs :syms) keysmap)] (print (str temp " = ")) (emit val) (doseq [[vname vkey] vmap] (print ", ") (cond (not (and (binding-form? vname) (or (some #(% vkey) #{keyword? number? binding-form?})))) (throw+ {:known-error true :msg (str "Unsupported binding form `" vmap "`:\n" "binding symbols must be followed by keywords or numbers") :causes [vmap]}) :else (if-let [[_ default] (find defaults vname)] (emit-binding vname `(get ~temp ~vkey ~default)) (emit-binding vname `(get ~temp ~vkey))))))) | |||||||||||||||||||
(defn- emit-var-bindings [bindings] (binding [*return-expr* false] (emit-delimited ", " (partition 2 bindings) (fn [[vname val]] (emit-binding vname val))))) | |||||||||||||||||||
(defn- emit-function [fdecl] (let [[fname fdecl] (if (symbol? (first fdecl)) [(first fdecl) (rest fdecl)] [nil fdecl]) docstring (if (string? (first fdecl)) (first fdecl) nil) fdecl (if (string? (first fdecl)) (rest fdecl) fdecl) args (first fdecl) dargs? (or (some destructuring-form? args) (some binding-special? args) (some ignorable-arg? args)) body (rest fdecl)] (assert-args fn (vector? args) "a vector for its bindings") (if dargs? (do (print "function ") (if fname (do (emit-symbol fname) (print " "))) (print "() {") (with-indent [] (newline-indent) (print "var ") (emit-binding args '(Array.prototype.slice.call arguments)) (print ";"))) (do (print "function ") (if fname (do (emit-symbol fname) (print " "))) (print "(") (binding [*return-expr* false] (emit-delimited ", " args)) (print ") {"))) (with-indent [] (when docstring (emit-docstring docstring)) (binding [*return-expr* true *unique-return-expr* (when (= 1 (count body)) true) *in-fn-toplevel* false] (emit-statements-with-return body))) (newline-indent) (print "}"))) | |||||||||||||||||||
We define local vars with | (defmethod emit "def" [[_ name value]] (print "var ") (emit-symbol name) (print " = ") (binding [*inline-if* true] (emit value))) | ||||||||||||||||||
Macro expansions are useful in REPL. macroexpand-1 and macroexpand work the like in Clojure except: - they're special forms, not functions and receive unquoted Chlorine forms instead of quoted ones like in Clojure. - they print out code as strings because javascript is not a Lisp. - namespaces don't make sense in ChlorineJS so they're automatically removed. | |||||||||||||||||||
Removes all namespaces in forms using clojure.walk/postwalk. | (defn remove-namespaces [forms] (clojure.walk/postwalk (fn [x] (if (symbol? x) (symbol (name x)) x)) forms)) | ||||||||||||||||||
(defmethod emit "macroexpand-1" [[_ form]] (emit (pr-str (remove-namespaces (expand-macro-1 form))))) | |||||||||||||||||||
(defmethod emit "macroexpand" [[_ form]] (emit (pr-str (remove-namespaces (expand-macro form))))) | |||||||||||||||||||
Low-level function form. Please use | (defmethod emit "fn*" [[_ & fdecl]] (with-return-expr [] (with-block (emit-function fdecl)))) | ||||||||||||||||||
Javascript's [1]: Javascript's | |||||||||||||||||||
(defn emit-inline-if [test consequent alternate] (with-return-expr [] (with-parens [] (emit test) (print " ? ") (emit consequent) (print " : ") (emit alternate)))) | |||||||||||||||||||
If 'no', traditional javascript | (defn emit-block-if [test consequent alternate] (print "if (") (binding [*return-expr* false *inline-if* true] (emit test)) (print ") {") (with-block (with-indent [] (emit-statement consequent))) (newline-indent) (print "}") ;; alternate might be `0`, which js equates as `nil` (when-not (or (nil? alternate) (= '(clojure.core/cond) alternate)) (print " else {") (with-block (with-indent [] (emit-statement alternate))) (newline-indent) (print "}"))) | ||||||||||||||||||
(defmethod emit "if" [[_ test consequent & [alternate]]] ;; emit consequent directly without printing checks ;; used to optimize `cond` macro output (if (and *inline-if* consequent) (emit-inline-if test consequent alternate) (emit-block-if test consequent alternate))) | |||||||||||||||||||
Clojure/ChlorineJS | |||||||||||||||||||
(defn emit-case [e clauses] (binding [*unique-return-expr* false *in-fn-toplevel* false] (let [pairs (partition 2 clauses)] (print "switch (") (binding [*return-expr* false] (emit e)) (print ") {") (doseq [[k v] pairs] (with-indent [] (newline-indent) (print "case " ) (binding [*return-expr* false] (emit k)) (print ":") (with-block (with-indent [] (emit-statement v) (newline-indent) (when-not *return-expr* (print "break;"))))))) (when (odd? (count clauses)) (with-indent [] (newline-indent) (print "default:") (with-block (with-indent [] (emit-statement (last clauses))))))) (newline-indent) (print "}")) | |||||||||||||||||||
(defmethod emit "case" [[_ e & clauses]] (if *inline-if* (do (print "(function(){") (binding [*return-expr* true] (with-indent [] (newline-indent) (emit-case e clauses))) (print "})()")) (emit-case e clauses))) | |||||||||||||||||||
(defmethod emit "do" [[_ & exprs]] (if *inline-if* (do (print "(function(){") (binding [*return-expr* true] (with-indent [] (newline-indent) (emit-statements-with-return exprs))) (print "})()")) (emit-statements-with-return exprs))) | |||||||||||||||||||
Chlorine implements the same behavior of | |||||||||||||||||||
(defmethod emit "let" [[_ bindings & exprs]] (let [emit-var-decls (fn [] (print "var ") (binding [*return-expr* false] (with-block (emit-var-bindings bindings)) (print ";")) (emit-statements-with-return exprs)) emit-let-fun (fn [] (print "(function () {") (with-indent [] (newline-indent) (binding [*return-expr* true] (emit-var-decls))) (newline-indent) (print " })()"))] (cond *inline-if* (with-return-expr [] (emit-let-fun)) *unique-return-expr* ;; *in-fn-toplevel* (binding [*unique-return-expr* false] (emit-var-decls)) *return-expr* (with-return-expr [] (emit-let-fun)) :default (do (emit-let-fun) (print ";"))))) | |||||||||||||||||||
"Leaky" versions of | (defmethod emit "let!" [[_ & bindings]] (binding [*return-expr* false] (with-block (emit-var-bindings bindings)) (print ";"))) | ||||||||||||||||||
(defmethod emit "let*" [[_ & bindings]] (print "var ") (binding [*return-expr* false] (with-block (emit-var-bindings bindings)) (print ";"))) | |||||||||||||||||||
(defmethod emit "new" [[_ class & args]] (with-return-expr [] (binding [*inline-if* true] (print "new ") (emit class) (with-parens [] (emit-delimited "," args))))) | |||||||||||||||||||
(defmethod emit "delete" [[_ item]] (with-return-expr [] (binding [*inline-if* true] (print "delete ") (emit item)))) | |||||||||||||||||||
(defmethod emit "return" [[_ value]] (print "return ") (emit value)) | |||||||||||||||||||
Low-level form to directly access object properties/array indexes.
Use | (defmethod emit "get*" [[_ map key]] (with-return-expr [] (emit map) (print "[") (emit key) (print "]"))) | ||||||||||||||||||
(defmethod emit "." [[_ object key & args]] (with-return-expr [] (emit object) (print ".") (cond (symbol? key) (if (.startsWith (name key) "-") (do (print (property->member key))) (do (print (name key)) (with-parens [] (with-indent [] (emit-delimited ", " args))))) (coll? key) (do (emit (first key)) (with-parens [] (with-indent [] (emit-delimited ", " (rest key)))))))) | |||||||||||||||||||
(defmethod emit "set!" [[_ & apairs]] (binding [*return-expr* false *in-fn-toplevel* false *unique-return-expr* false *inline-if* true] (let [apairs (partition 2 apairs)] (emit-delimited " = " (first apairs)) (doseq [apair (rest apairs)] (print ";") (newline-indent) (emit-delimited " = " apair))))) | |||||||||||||||||||
(defmethod emit "try" [[_ expr & clauses]] (print "try {") (with-indent [] (with-block (emit-statement expr))) (newline-indent) (print "}") (doseq [[clause & body] clauses] (case clause catch (let [[evar expr] body] (with-block (print " catch (") (emit-symbol evar) (print ") {") (with-indent [] (emit-statement expr)) (newline-indent) (print "}"))) finally (with-block (print " finally {") (with-indent [] (doseq [expr body] (emit-statement expr))) (newline-indent) (print "}"))))) | |||||||||||||||||||
(def ^:dynamic *loop-vars* nil) | |||||||||||||||||||
(defmethod emit "loop" [[_ bindings & body]] (let [emit-for-block (fn [] (print "for (var ") (binding [*return-expr* false *in-block-exp?* false] (emit-var-bindings bindings)) (print "; true;) {") (with-indent [] (binding [*loop-vars* (first (unzip bindings))] (emit-statements-with-return body)) (newline-indent) (print "break;")) (newline-indent) (print "}"))] (if (or *in-fn-toplevel* *unique-return-expr*) (binding [*unique-return-expr* false *in-fn-toplevel* false] (emit-for-block)) (with-return-expr [] (print "(function () {") (binding [*return-expr* true] (with-indent [] (newline-indent) (emit-for-block)) (newline-indent)) (print "}).call(this)"))))) | |||||||||||||||||||
(defmethod emit "recur" [[_ & args]] (binding [*return-expr* false] (let [tmp (tempsym)] (print "var" (emit-str tmp) "= ") (emit-vector args) (println ";") (emit-statements (map (fn [lvar i] `(set! ~lvar (get* ~tmp ~i))) *loop-vars* (range (count *loop-vars*)))))) (newline-indent) (print "continue")) | |||||||||||||||||||
(defmethod emit "dokeys" [[_ [lvar hash] & body]] (binding [*return-expr* false] (print "for (var ") (emit lvar) (print " in ") (emit hash) (print ") {") (with-indent [] (emit-statements body)) (newline-indent) (print "}"))) | |||||||||||||||||||
(defmethod emit "while" [[_ test & body]] (binding [*return-expr* false] (print "while (") (emit test) (print ") {") (with-indent [] (emit-statements body)) (newline-indent) (print "}"))) | |||||||||||||||||||
(defmethod emit "do-while" [[_ test & body]] (binding [*return-expr* false] (print "do {") (with-indent [] (emit-statements body)) (newline-indent) (print "}") (print " while (") (emit test) (print ")"))) | |||||||||||||||||||
(defmethod emit "dofor" [[_ [init-bindings test update] & body]] (let [init (if (vector? init-bindings) `(let* ~@init-bindings) init-bindings)] (binding [*return-expr* false] (print "for (") (emit-statements [init test update]) (print ") {") (with-indent [] (emit-statements body)) (newline-indent) (print "}")))) | |||||||||||||||||||
(defmethod emit "inline" [[_ js]] (with-return-expr [] (print js))) | |||||||||||||||||||
(defmethod emit "quote" [[_ expr]] (binding [*quoted* true] (emit expr))) | |||||||||||||||||||
(defmethod emit "throw" [[_ expr]] (binding [*return-expr* false] (print "throw ") (emit expr))) | |||||||||||||||||||
(defmethod emit :default [expr] (if (and (coll? expr) (not *quoted*) (macro? (first expr))) (emit-macro-expansion expr) (with-return-expr [] (cond (map? expr) (emit-map expr) (set? expr) (emit-set expr) (vector? expr) (emit-vector expr) (re? expr) (emit-re expr) (keyword? expr) (emit-keyword expr) (string? expr) (pr expr) (symbol? expr) (emit-symbol expr) (char? expr) (print (format "'%c'" expr)) (and *quoted* (coll? expr)) (emit-vector expr) (coll? expr) (emit-function-form expr) (nil? expr) (print "void(0)") true (print expr))))) | |||||||||||||||||||
(defn emit-str [expr] (binding [*return-expr* false *inline-if* true] (with-out-str (emit expr)))) | |||||||||||||||||||
(defn js-emit [expr] (emit expr)) | |||||||||||||||||||
Translate the Clojure subset `exprs' to a string of javascript code. | (defmacro js [& exprs] (let [exprs# `(quote ~exprs)] `(binding [*temp-sym-count* (ref 999)] (with-out-str (if (< 1 (count ~exprs#)) (emit-statements ~exprs#) (js-emit (first ~exprs#))))))) | ||||||||||||||||||
Bind Clojure environment values to named vars of a cljs block, and translate the Clojure subset `exprs' to a string of javascript code. | (defmacro js-let [bindings & exprs] (let [form# 'fn* [formals# actuals#] (unzip bindings)] `(with-out-str (emit-statement (list '(~form# ~(vec formals#) ~@exprs) ~@actuals#))))) | ||||||||||||||||||
Bind Clojure environment values to named vars of a quoted cljs block, and translate the Clojure subset `exprs' to a string of javascript code. | (defmacro let-js [bindings quoted-expr] (let [body# `(let ~bindings ~quoted-expr)] `(with-out-str (js-emit ~body#)))) | ||||||||||||||||||
(declare raw-script) | |||||||||||||||||||
Chlorine doesn't support an official way to modularize code like Clojure
with namespaces. Instead, Chlorine provides a basic syntax to load code
from other files into the current file as if they are one. This can be
done with | |||||||||||||||||||
(defmethod emit "load-file" [[_ & files]] ;(print (str (apply tojs' files))) (doseq [file files] (when *print-pretty* (println "// <-- Starts loading file: " file)) (if-let [content (tojs' file)] (print (str content))) (when *print-pretty* (println "// Ends loading file: " file " -->")))) | |||||||||||||||||||
Sometimes you only want to load macros from an outside file and print out
nothing. Use | (defmethod emit "load-file-macros" [[_ & files]] (doseq [file files] (when *print-pretty* (println "// Loads macros from file: " file)) (tojs' file))) | ||||||||||||||||||
Inlines raw javascript from files instead of Chlorine ones. | (defmethod emit "load-js" [[_ & files]] (doseq [file files] (when *print-pretty* (println "// <-- Starts Javascipt file: " file)) (if-let [content (raw-script file)] (print (str content))) (when *print-pretty* (println "// Ends Javascript file: " file " -->")))) | ||||||||||||||||||
(defn raw-script [& scripts] (with-out-str (doseq [script scripts] (let [[file dir] (file-and-dir script) f (cond (resource-path? file) (to-resource file) (or (url? file) (.isFile (clojure.java.io/file file))) file)] (print (slurp f)))))) | |||||||||||||||||||
The low-level, stateful way to compile Chlorine source files. This function varies depending on states such as macros, temporary symbol count etc. | (defn tojs' [& scripts] (with-out-str (doseq [script scripts] ;; converts to absolute paths: (let [[file dir] (file-and-dir script) f (cond (resource-path? file) (to-resource file) (or (url? file) (.isFile (clojure.java.io/file file))) file)] (binding [*cwd* dir] (try+ (if (nil? f) (throw+ {:known-error true :msg "File not found `" file "`" :causes [file]})) (with-open [in (sexp-reader f)] (loop [expr (read in false :eof)] (when (not= expr :eof) (when-let [s (emit-statement expr)] (print s)) (recur (read in false :eof))))) (catch map? e (throw+ (merge e {:causes (conj (or (:causes e) []) file)}))) (catch RuntimeException e (if (= (.getMessage e) "EOF while reading") (throw+ {:known-error true :msg (str "EOF while reading file " file "\n" "Maybe you've got mismatched parentheses," " brackets or braces.") :causes [file] :trace e}) (throw+ {:known-error false :msg (.getMessage e) :causes [file] :trace e}))) (catch Throwable e (throw+ {:known-error false :msg (.getMessage e) :causes [file] :trace e})))))))) | ||||||||||||||||||
The top-level, stateless way to compile Chlorine source files. Loads and compiles a list of cl2 scripts into javascript, and returns them in a string. This function starts its own temporary symbol count and macro memory. | (defn tojs [& scripts] (binding [*temp-sym-count* (ref 999) *macros* (ref {})] (apply tojs' scripts))) | ||||||||||||||||||
(ns chlorine.page (:use [chlorine.js :only [js js-let]] [chlorine.util :only [unzip]])) | |||||||||||||||||||
Similar to the (js ...) form, but wraps the javascript in a [:script ] element, which can be passed to hiccup.core/html. | (defmacro script [& forms] `[:script {:type "text/javascript"} (js ~@forms)]) | ||||||||||||||||||
Similar to the (js-let ...) form, but wraps the javascript in a [:script ] element, which can be passed to hiccup.core/html. | (defmacro script-let [bindings & forms] `[:script {:type "text/javascript"} (js-let ~bindings ~@forms)]) | ||||||||||||||||||
Similar to the (js ...) form, but wraps the javascript in a [:script ] element which is invoked on a jQuery document.ready event. | (defmacro jq [& forms] (let [fnform# 'fn] `[:script {:type "text/javascript"} (js (.ready ($ document) (~fnform# [] ~@forms)))])) | ||||||||||||||||||
Similar to the (js-let ...) form, but wraps the javascript in a [:script ] element which is invoked on a jQuery document.ready event. | (defmacro jq-let [bindings & forms] (let [fnform# 'fn [formals# actuals#] (unzip bindings)] `[:script {:type "text/javascript"} "$(document).ready(function () {" (js-let ~bindings ~@forms) "});"])) | ||||||||||||||||||
(ns chlorine.reader (:use [clojure.java.io :only [reader]] [chlorine.util :only [re?]])) | |||||||||||||||||||
Wrap | (defn sexp-reader [source] (new java.io.PushbackReader (reader source))) | ||||||||||||||||||
(defn unary-operator? [op] (and (symbol? op) (contains? #{"++" "--" "!"} (name op)))) | |||||||||||||||||||
(defn infix-operator? [op] (and (symbol? op) (contains? #{"and" "or" "bit-and" "bit-or" "bit-xor" "bit-not" "bit-shift-left" "bit-shift-right" "bit-shift-right-zero-fill" "rem" "+*" "-*" "**" "/" ">" ">=" "<" "<=" "==" "===" "!=" "!==" "in" "instance?" "instanceof"} (name op)))) | |||||||||||||||||||
(defn destructuring-form? [form] (or (map? form) (vector? form))) | |||||||||||||||||||
(defn binding-form? [form] (or (symbol? form) (destructuring-form? form))) | |||||||||||||||||||
(defn binding-special? [form] (contains? #{'& :as} form)) | |||||||||||||||||||
Checks if a string is specified in reserved symbol list. | (defn reserved-symbol? [v s] (let [strings (filter string? v) regexps (filter re? v)] (or (contains? (set strings) s) (some #(re-matches % s) regexps)))) | ||||||||||||||||||