├── test ├── res │ ├── nested │ │ └── example2.eaml │ └── example1.eaml └── eaml │ ├── core_test.clj │ ├── file_test.clj │ ├── fixtures │ └── simple_res.clj │ ├── test_helpers.clj │ ├── xml_test.clj │ ├── util_test.clj │ ├── scope_test.clj │ ├── parser_test.clj │ └── compiler_test.clj ├── .gitignore ├── src └── eaml │ ├── error.clj │ ├── transpile │ ├── simple_res.clj │ └── style.clj │ ├── compiler.clj │ ├── file.clj │ ├── core.clj │ ├── xml.clj │ ├── scope.clj │ ├── parser.clj │ └── util.clj ├── project.clj ├── resources └── grammar.bnf ├── doc ├── release_notes.md ├── 0.3.0 │ └── quickstart.md └── 0.4.0 │ └── quickstart.md ├── README.md └── LICENSE /test/res/nested/example2.eaml: -------------------------------------------------------------------------------- 1 | color foo: #123456; 2 | -------------------------------------------------------------------------------- /test/eaml/core_test.clj: -------------------------------------------------------------------------------- 1 | (ns eaml.core-test 2 | (:require [clojure.test :refer :all] 3 | [eaml.core :refer :all])) 4 | 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /classes 3 | /checkouts 4 | pom.xml 5 | pom.xml.asc 6 | *.jar 7 | *.class 8 | /.lein-* 9 | /.nrepl-port 10 | 11 | *.swp 12 | 13 | tmp/ 14 | 15 | .idea/ 16 | *.iml 17 | -------------------------------------------------------------------------------- /test/res/example1.eaml: -------------------------------------------------------------------------------- 1 | color red: #f00; 2 | color green: #0f0; 3 | color blue : #00f; 4 | 5 | dimen activity_margins { 6 | default: 8dp; 7 | land: 12dp; 8 | } 9 | 10 | dimen button_text_size: 12dp; 11 | 12 | style Button { 13 | android:textSize: button_text_size; 14 | android:background: red; 15 | v21 { 16 | android:background: @drawable/btn_ripple; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/eaml/error.clj: -------------------------------------------------------------------------------- 1 | (ns eaml.error 2 | (:require [clojure.pprint :refer [pprint]] 3 | [clojure.string :refer [join]])) 4 | 5 | (defn- ppr 6 | [obj] 7 | (with-out-str (clojure.pprint/pprint obj))) 8 | 9 | (defn raise! 10 | "Throws a RuntimeException with the given message as argument" 11 | [& messages] 12 | (throw (new IllegalStateException (join "" (map ppr messages))))) 13 | 14 | 15 | -------------------------------------------------------------------------------- /project.clj: -------------------------------------------------------------------------------- 1 | (defproject eaml "0.4.0" 2 | :description "Describe your android styles expressively with eaml, 3 | an Android XML styles pre-processor." 4 | :main eaml.core 5 | :url "http://github.com/fhur/eaml" 6 | :license {:name "Eclipse Public License" 7 | :url "http://www.eclipse.org/legal/epl-v10.html"} 8 | :plugins [[lein-cloverage "1.0.6"]] 9 | :profiles {:uberjar {:aot :all}} 10 | :dependencies [[org.clojure/clojure "1.6.0"] 11 | [instaparse "1.4.1"] 12 | [presto "0.2.0"] 13 | [org.clojure/tools.cli "0.3.3"]]) 14 | 15 | 16 | -------------------------------------------------------------------------------- /test/eaml/file_test.clj: -------------------------------------------------------------------------------- 1 | (ns eaml.file-test 2 | (:require [eaml.file :refer :all] 3 | [clojure.java.io :refer [as-file]] 4 | [presto.core :refer :all] 5 | [clojure.test :refer :all])) 6 | 7 | 8 | (expected-when "path-concat concatenates two paths" path-concat 9 | when ["foo" "bar"] = "foo/bar" 10 | when ["./foo" "./bar"] = "./foo/bar" 11 | when ["./" "bar"] = "./bar" 12 | when ["path" "foo/bar.baz"] = "path/foo/bar.baz" 13 | when ["foo/" "bar/"] = "foo/bar" 14 | when ["./foo/bar/" "./baz/"] = "./foo/bar/baz") 15 | 16 | (expected-when "filter-tree iterates recursively over a file system tree and 17 | returns all files that match the given filter" filter-tree 18 | when ["test/res" (extension-filter "eaml")] 19 | = [(as-file "test/res/nested/example2.eaml") 20 | (as-file "test/res/example1.eaml")] 21 | when ["test/res" (extension-filter "foo")] 22 | = []) 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /test/eaml/fixtures/simple_res.clj: -------------------------------------------------------------------------------- 1 | (ns eaml.fixtures.simple-res) 2 | 3 | (def fix-simple-colors 4 | "color red: #f00; 5 | color green: #0f0; 6 | color blue: #00f; 7 | color main_color: red;") 8 | 9 | (def fix-simple-dimen 10 | "dimen small_margins: 8dp; 11 | dimen medium_margins: 12dp; 12 | dimen large_margins: 24dp; 13 | dimen default_margins: medium_margins;") 14 | 15 | (def fix-simple-strings 16 | "string hello_world: \"Hello World!\"; 17 | string name: 'Pizza 123';") 18 | 19 | (def fix-simple-bools 20 | "bool is_true: true; 21 | bool aint_true: false; 22 | bool a_boolean: is_true;") 23 | 24 | (def fix-simple-res-with-configs 25 | "dimen padding { 26 | default: 12dp; 27 | v21: 24dp; 28 | land: 30dp; 29 | } 30 | 31 | string supports_ripples { 32 | default: 'nope'; 33 | v21: 'yes'; 34 | } 35 | 36 | color main_color: #f00; 37 | color button_color { 38 | default: main_color; 39 | v21: @drawable/btn_ripple; 40 | } 41 | ") 42 | -------------------------------------------------------------------------------- /src/eaml/transpile/simple_res.clj: -------------------------------------------------------------------------------- 1 | (ns eaml.transpile.simple-res 2 | (:require [eaml.scope :as scope])) 3 | 4 | (defn- mk-simple-res 5 | [nodetype id value] 6 | [nodetype {:name id} value]) 7 | 8 | (defn simple-res? 9 | "true if the given node id is a color, dimen, bool, integer or string" 10 | [id] 11 | (#{:color :dimen :bool :integer :string} id)) 12 | 13 | (defn transpile-simple-res 14 | "Given a simple res node, returns a map of config => [node {:name name} value] 15 | Example: 16 | {:node :dimen 17 | :id 'margins' 18 | :vals [{:config 'tablet' :value '24dp' 19 | :config 'default' :value '12dp'}]} 20 | 21 | Will return: 22 | {'default' [:color {:name 'margins'} 12dp] 23 | 'tablet' [:color {:name 'margins'} 24dp]}" 24 | [scope simple-res-node] 25 | (let [{vals :vals id :id nodetype :node} simple-res-node] 26 | (reduce (fn [config-map {config :config value :value}] 27 | (let [scoped-value (scope/resolve-expr :any value scope)] 28 | (assoc config-map config 29 | (mk-simple-res nodetype id scoped-value)))) 30 | {} vals))) 31 | -------------------------------------------------------------------------------- /test/eaml/test_helpers.clj: -------------------------------------------------------------------------------- 1 | (ns eaml.test-helpers 2 | "Place common helper functions for testing" 3 | (:require [clojure.string :refer [split-lines join]] 4 | [eaml.util :refer :all])) 5 | 6 | (defn cleanup-xml 7 | [xml-string] 8 | (->> (split-lines xml-string) 9 | (map #(.trim %)) 10 | (join))) 11 | 12 | 13 | (defn xml=? 14 | "Tests that two xml strings are semantically equal. 15 | This does not include new lines, indentation, etc." 16 | [expected actual] 17 | (= (cleanup-xml expected) 18 | (cleanup-xml actual))) 19 | 20 | (defmacro simpleres 21 | [type name value] 22 | [(keyword type) {:name name} value]) 23 | 24 | (defmacro resources 25 | [& nodes] 26 | (vec (cons* nodes :resources {}))) 27 | 28 | (defmacro color 29 | [name value] 30 | (simpleres :color name value)) 31 | 32 | (defmacro dimen 33 | [name value] 34 | (simpleres :dimen name value)) 35 | 36 | (defmacro string 37 | [name value] 38 | (simpleres :string name value)) 39 | 40 | (defmacro bool 41 | [name value] 42 | (simpleres :bool name value)) 43 | 44 | (defmacro item 45 | [name value] 46 | (simpleres :item name value)) 47 | 48 | (defmacro style 49 | [attrs & items] 50 | (vec (cons* items :style attrs))) 51 | 52 | 53 | -------------------------------------------------------------------------------- /test/eaml/xml_test.clj: -------------------------------------------------------------------------------- 1 | (ns eaml.xml-test 2 | (:require [eaml.xml :refer :all] 3 | [eaml.file :refer [read-file]] 4 | [eaml.test-helpers :refer :all] 5 | [presto.core :refer :all] 6 | [clojure.test :refer :all])) 7 | 8 | (expected-when "test render-xml" render-xml-string 9 | 10 | when [[:foo]] 11 | xml=? "" 12 | 13 | when [[:foo {:name "bar" :age "12"} "a string value"]] 14 | xml=? " 15 | a string value 16 | " 17 | 18 | when [[:nested {:foo "bar"} 19 | [:first {:name "first"} "first-val"] 20 | [:second {:name "second"} [:foo {:hi "ho"}]] 21 | [:third {:name "third"} "third-val"]]] 22 | xml=? " 23 | first-val 24 | 25 | 26 | 27 | third-val 28 | " 29 | 30 | when [[:a {:x "y"} 31 | [:b {:z "w"} 32 | [:c {:foo "bar"}]]]] 33 | xml=? " 34 | 35 | 36 | 37 | ") 38 | 39 | (deftest test-config-writer 40 | (let [text (str (rand-int 100) "text")] 41 | (with-open [writer (config-writer "tmp" :foo)] 42 | (.write writer text)) 43 | (is (= (str text "\n") 44 | (read-file "tmp/values-foo/values.xml"))))) 45 | -------------------------------------------------------------------------------- /src/eaml/compiler.clj: -------------------------------------------------------------------------------- 1 | (ns eaml.compiler 2 | (:require [eaml.error :refer :all] 3 | [eaml.util :refer :all] 4 | [eaml.xml :as xml] 5 | [eaml.scope :as scope] 6 | [eaml.transpile.simple-res :refer :all] 7 | [eaml.transpile.style :refer :all] 8 | [eaml.parser :as parser])) 9 | 10 | (defn- transpile-node 11 | [scope node] 12 | (let [type (:node node)] 13 | (cond (style? type) 14 | (transpile-style scope node) 15 | (simple-res? type) 16 | (transpile-simple-res scope node) 17 | :else (raise! "Unknown node type: " node)))) 18 | 19 | (defn- insert-resources-top-level 20 | [config-map] 21 | (loop [result-map config-map 22 | remaining-keys (keys result-map)] 23 | (if (empty? remaining-keys) 24 | result-map 25 | (let [key (first remaining-keys) 26 | value (get config-map key)] 27 | (recur (assoc result-map key (cons* value :resources {})) 28 | (rest remaining-keys)))))) 29 | 30 | (defn- skip-node? 31 | [{node-type :node}] 32 | (= node-type :mixin)) 33 | 34 | (defn transpile 35 | "Transpile the given AST into an XmlStruct. 36 | Returns a map of configuration => XmlStruct where 37 | configuration is a configuration name e.g. 'tablet'" 38 | [ast] 39 | (let [scope (scope/create ast) 40 | transpile-scoped #(transpile-node scope %) 41 | ast (filter (complement skip-node?) ast)] 42 | (->> (map transpile-scoped ast) 43 | (group-maps) 44 | (insert-resources-top-level)))) 45 | 46 | (defn transpile-str 47 | "Equivalent to transpile but takes an eaml program string as input instead 48 | of an AST and transpiles it." 49 | [string] 50 | (let [ast (parser/parse-str string)] 51 | (transpile ast))) 52 | 53 | 54 | -------------------------------------------------------------------------------- /src/eaml/file.clj: -------------------------------------------------------------------------------- 1 | (ns eaml.file 2 | (:require [clojure.java.io :refer :all]) 3 | (:import [java.io File])) 4 | 5 | (defn read-file 6 | "Reads the file located at file-path and returns the contents as a string" 7 | [file-path] 8 | (with-open [reader (clojure.java.io/reader file-path)] 9 | (loop [result "" 10 | line (.readLine reader)] 11 | (if (nil? line) 12 | result 13 | (recur (str result line "\n") 14 | (.readLine reader)))))) 15 | 16 | 17 | (defn extension-filter 18 | "Creates an anonymous function which takes a file as argument and returns 19 | true if the file has the given extension. 20 | Example: (extension-filter \"clj\") will match clojure files." 21 | [ext] 22 | (fn [file] 23 | (let [file-name (.getName file) 24 | match (re-find #".+\.(.+)" file-name)] 25 | (if (nil? match) 26 | false 27 | (= ext (last match)))))) 28 | 29 | 30 | (defn filter-tree 31 | "Reads a file tree recursively and returns a list of files that match the 32 | given filter function f. To match by extension see the extension-filter 33 | function." 34 | [root-dir-path f] 35 | (loop [files [(as-file root-dir-path)] 36 | result []] 37 | (if (empty? files) 38 | result 39 | (let [file (first files)] 40 | (if (.isDirectory file) 41 | (recur (into (rest files) (.listFiles file)) result) 42 | (recur (rest files) (if (f file) (conj result file) result))))))) 43 | 44 | 45 | (defn mkdirs! 46 | "Given a list of files or paths, creates any missing directories for every 47 | file or file-path." 48 | [& file-paths] 49 | (doseq [file-path file-paths] 50 | (let [file (as-file file-path)] 51 | (.mkdirs file)))) 52 | 53 | (defn path-concat 54 | "Concatenates two file paths, the first one being a parent 55 | of the second one." 56 | [path1 path2] 57 | (let [clean-path1 (str (File. path1)) 58 | clean-path2 (.replaceAll (str (File. path2)) "\\./" "") 59 | joined-path (str clean-path1 "/" clean-path2)] 60 | (str (File. joined-path)))) 61 | 62 | -------------------------------------------------------------------------------- /src/eaml/core.clj: -------------------------------------------------------------------------------- 1 | (ns eaml.core 2 | (:gen-class) 3 | (:require [eaml.compiler :as compiler] 4 | [eaml.xml :as xml] 5 | [eaml.util :refer [itp]] 6 | [eaml.parser :refer [parse-dir]] 7 | [clojure.string :refer [join]] 8 | [clojure.tools.cli :refer [parse-opts]])) 9 | 10 | (defn eaml 11 | [in-dir out-dir] 12 | (println (itp "Transpiling from #{in-dir} to #{out-dir}")) 13 | (let [ast (parse-dir in-dir) 14 | config-map (compiler/transpile ast)] 15 | (doseq [[config xmlstruct] config-map] 16 | (with-open [writer (xml/config-writer out-dir config)] 17 | (println (itp "Transpiling done, writing to #{out-dir}")) 18 | (xml/render-xml xmlstruct writer))))) 19 | 20 | 21 | (def cli-options 22 | [["-i" "--in DIR" "Input directory"] 23 | ["-o" "--out DIR" "Output directory"] 24 | ["-h" "--help" "Print a help message"]]) 25 | 26 | (defn exit 27 | "Print the given msg to stdout and terminate program with the given status code" 28 | [status msg] 29 | (println msg) 30 | (System/exit status)) 31 | 32 | (defn error-msg 33 | [errors] 34 | (str "The following errors occurred while parsing your command:\n\n" 35 | (join \newline errors))) 36 | 37 | (defn usage 38 | [summary] 39 | (->> ["eaml transpiler" 40 | "" 41 | "Reads all .eaml files in the input directory and subdirectories, transpiles and writes XML to the output directory" 42 | "" 43 | "Usage:" 44 | "" 45 | "eaml [--in DIR] [--out DIR] [--help]" 46 | "" 47 | summary 48 | ""] 49 | (join \newline))) 50 | 51 | (defn -main 52 | [& args] 53 | (let [{:keys [options arguments errors summary]} (parse-opts args cli-options)] 54 | ;; Handle help and error conditions 55 | (cond 56 | (:help options) 57 | (exit 0 (usage summary)) 58 | errors 59 | (exit 1 (error-msg errors)) 60 | (and (contains? options :in) (contains? options :out)) 61 | (eaml (:in options) (:out options)) 62 | :else 63 | (exit 1 (usage summary))))) 64 | -------------------------------------------------------------------------------- /resources/grammar.bnf: -------------------------------------------------------------------------------- 1 | = res-def* (* A program is composed of 1 or more resource definitions *) 2 | 3 | (* there are currently only 3 types of resources *) 4 | = simple-res-def 5 | | style-def 6 | | mixin-def 7 | 8 | (* simple resources *) 9 | simple-res-def = simple-res-type identifier <':'> simple-res-single-config <';'> 10 | | simple-res-type identifier <'{'> simple-res-configs <'}'> 11 | 12 | simple-res-single-config = simple-res-value 13 | simple-res-configs = ( simple-res-config )* 14 | simple-res-config = config-name <':'> simple-res-value <';'> 15 | = 'color' | 'dimen' | 'bool' | 'integer' | 'string' 16 | = string-literal 17 | | pointer 18 | | color-literal 19 | | dimen-literal 20 | | native-pointer 21 | | boolean-literal 22 | | integer-literal 23 | 24 | (* styles *) 25 | style-def = <'style'> identifier (<'<'> parent-iden)? <'{'> attrs <'}'> 26 | config-block = <'&:'> config-names <'{'> attr-def* <'}'> 27 | config-names = config-name (<','> config-name)* 28 | 29 | attrs = (attr-def | config-block | mixin-call)* 30 | attr-def = attr-name <':'> attr-value <';'> 31 | = string-literal 32 | | color-literal 33 | | dimen-literal 34 | | integer-literal 35 | | native-pointer 36 | | pointer 37 | 38 | (* mixins *) 39 | mixin-def = <'mixin'> identifier <'{'> mixin-attrs <'}'> 40 | mixin-attrs = (attr-def | config-block)* 41 | mixin-call = identifier <'('> <')'> <';'> 42 | 43 | (* tokens *) 44 | = #"[a-zA-Z]\w+" 45 | = #"[a-zA-Z][.\w]+" 46 | pointer = #"[a-zA-Z]\w+" 47 | boolean-literal = 'true' | 'false' 48 | string-literal = #'".*?"' | #"'.*?'" 49 | integer-literal = #"\d+" 50 | color-literal = #'#[a-fA-F0-9]{3}([a-fA-F0-9]{3})?' 51 | dimen-literal = #'\d+(sp|dp|px|dip)' 52 | config-name = #"[a-z][a-z0-9]*(-[a-z0-9]+)*" 53 | = #"[\w\d]+(:[\w\d]+)*" 54 | native-pointer = #"@(android:)?\w+(/\w[\w\.]+)?" 55 | -------------------------------------------------------------------------------- /test/eaml/util_test.clj: -------------------------------------------------------------------------------- 1 | (ns eaml.util-test 2 | (:require [eaml.util :refer :all] 3 | [presto.core :refer :all] 4 | [clojure.test :refer :all])) 5 | 6 | (expected-when "flat-coll flattens a coll of colls" flat-coll 7 | when [[]] = [] 8 | when [[[1] [2]]] = [1 2] 9 | when [[[] [1 2 3] [4 5]]] = [1 2 3 4 5]) 10 | 11 | 12 | (expected-when "group-maps joins values with the same key over different maps" group-maps 13 | when [[{:foo "foozie"} 14 | {:bar "bazie" 15 | :qux ["a" "list"]} 16 | {:foo "fun" 17 | :qux "quxie"}]] 18 | = {:foo ["foozie" "fun"] 19 | :bar ["bazie"] 20 | :qux [["a" "list"] "quxie"]}) 21 | 22 | (expected-when "cons* test" cons* 23 | when [[] 1 2] = [1 2] 24 | when [[3 4] 1 2] = [1 2 3 4] 25 | when [(list 3 4) 1 2] = [1 2 3 4] 26 | when [(seq [3 4]) 1 2] = [1 2 3 4] 27 | when [[:foo :bar] :baz] = [:baz :foo :bar]) 28 | 29 | (expected-when "case-match test" case-match 30 | when ["foobar" #"\Afoo" :foo] = :foo 31 | when ["foobar" #"\Abar" :bar] = nil 32 | when ["foobar" #"\Afoo" :foo 33 | #"\Afoobar" :foobar] = :foo) 34 | 35 | (expected-when "merge-lists test" merge-lists 36 | when [[] [1 2 3] identity] = [1 2 3] 37 | when [[1 2 3] [] identity] = [1 2 3] 38 | when [[] [] identity] = [] 39 | when [[1 2 3] [4 5 6] identity] = [1 2 3 4 5 6] 40 | when [[1 2 3] [3 4 5] identity] = [1 2 4 5 3] 41 | when [[{:a 1 :b 2} {:a 2 :b 3} {:foo :bar}] 42 | [{:a 1 :b 3} {:a 2 :b 6} {:a 4 :b 5}] 43 | :a] 44 | = [{:foo :bar} {:a 4 :b 5} {:a 1 :b 3} {:a 2 :b 6}]) 45 | 46 | (expected-when "find-first finds the first element that matches a predicate" find-first 47 | when [[1 2 3 4 5] #(> % 5)] = nil 48 | when [[1 2 3 4 5] #(> % 3)] = 4 49 | when [[] (fn [x] true)] = nil) 50 | 51 | (expected-when "singleton? returns true iff coll has only 1 element" singleton? 52 | when [nil] = false 53 | when [{}] = false 54 | when [[]] = false 55 | when [[1]] = true) 56 | 57 | (expected-when "remove-first-duplicates removes all duplicates and keeps the 58 | last one" remove-first-duplicates 59 | when [keys [{:b 0} {:a 1} {:a 2} {:b 1}]] = [{:a 2} {:b 1}] 60 | when [:a [{:a 0 :b 0} {:a 0 :b 1} {:a 1 :b 0} {:a 1 :b 1} {:a 0 :b "last one"}]] = 61 | [{:a 1 :b 1} {:a 0 :b "last one"}]) 62 | 63 | -------------------------------------------------------------------------------- /test/eaml/scope_test.clj: -------------------------------------------------------------------------------- 1 | (ns eaml.scope-test 2 | (:require [eaml.scope :refer :all] 3 | [eaml.parser :refer [parse-str]] 4 | [eaml.parser :refer [parse-str]] 5 | [presto.core :refer :all] 6 | [clojure.test :refer :all])) 7 | 8 | (def nodes (parse-str "color red: #f00; 9 | color main_color: red; 10 | integer foo: 5; 11 | string name: 'a name'; 12 | string my_name: name; 13 | string other_name: my_name; 14 | mixin RedText { 15 | android:textColor: red; 16 | } 17 | style Foo < Bar { 18 | android:textColor: main_color; 19 | android:foo: foo; 20 | android:text: my_name; 21 | }")) 22 | 23 | (def scope (create nodes)) 24 | 25 | (expected-when "obtain-type returns the resolved type of an expression 26 | in the current scope" #(obtain-type % scope) 27 | when ["red"] = :color 28 | when ["main_color"] = :color 29 | when ["foo"] = :integer 30 | when ["name"] = :string 31 | when ["my_name"] = :string 32 | when ["other_name"] = :string 33 | when ["5"] = :integer 34 | when ["5123"] = :integer 35 | when ["true"] = :bool 36 | when ["false"] = :bool 37 | when ["'foo'"] = :string 38 | when ["\"\""] = :string 39 | when ["@string/foo"] = :string 40 | when ["@bool/foo"] = :bool 41 | when ["''"] = :string) 42 | 43 | (expected-when "resolve-expr obtains the value of an expression in the 44 | current scope" #(resolve-expr :any % scope) 45 | when ["#f00"] = "#f00" 46 | when ["true"] = "true" 47 | when ["false"] = "false" 48 | when ["'foo'"] = "foo" 49 | when ["12sp"] = "12sp" 50 | when ["1234"] = "1234" 51 | when ["red"] = "@color/red" 52 | when ["main_color"] = "@color/main_color" 53 | when ["foo"] = "@integer/foo" 54 | when ["my_name"] = "@string/my_name") 55 | 56 | (expected-when "include? returns true iff the scope contains 57 | the given id" #(include? scope %) 58 | when ["red"] = true 59 | when ["main_color"] = true 60 | when ["RedText"] = true 61 | when ["Foo"] = true 62 | when ["other_name"] = true 63 | when ["akaljsdhf"] = false) 64 | 65 | (deftest obtain-type-throws-when-not-found 66 | (is (thrown? IllegalStateException (obtain-type "asdf" scope))) 67 | (is (thrown? IllegalStateException (obtain-type "a%1234" scope))) 68 | (is (thrown? IllegalStateException (obtain-type "#" scope))) 69 | (is (thrown? IllegalStateException (resolve-expr :string "main_color" scope))) 70 | (is (thrown? IllegalStateException (create (parse-str "integer foo: 1; color foo: #f00;"))))) 71 | -------------------------------------------------------------------------------- /doc/release_notes.md: -------------------------------------------------------------------------------- 1 | # Planned features 2 | 3 | - [ ] Remove `;` to indicate line termination. 4 | - [ ] Gradle integration 5 | - [ ] Android studio plugin 6 | - Syntax highlighting 7 | - Compile from IDE 8 | - Go to definition 9 | - [ ] Mixins with arguments e.g. 10 | ``` 11 | mixin coloredText(textColor, backgroundColor) { 12 | android:textColor: textColor; 13 | android:backgroundColor: backgroundColor; 14 | } 15 | ``` 16 | Which could then be invoked using `coloredText(#f00,#fff)` inside a Style. 17 | - [ ] Reverse compiler: cleanup your existing styles. 18 | - [ ] Detailed documentation 19 | - [ ] Type checking at compile time 20 | 21 | # eaml 0.5 - WIP 22 | 23 | - Eaml is now a command line app. To run: 24 | ```bash 25 | # will create an ubjerjar in target/{something}-standalone.jar 26 | lein uberjar 27 | # execute it and show --help banner 28 | java -jar target/{something}-standalone.jar --help 29 | ``` 30 | 31 | # eaml 0.4 - 1450616768 32 | 33 | - Added support for `@null` 34 | - Added mixins with no arguments: 35 | ``` 36 | mixin redText { 37 | android:textColor: #f00; 38 | } 39 | 40 | style RedButton < Button { 41 | redText(); 42 | } 43 | ``` 44 | - Mixins with arguments will come in the next version 45 | - Fixed several issues with native resource pointers (e.g. @foo/bar) 46 | 47 | # eaml 0.3.0 - 1448745045 48 | 49 | Main features: 50 | - Removed multiple inheritance. Implementation proved to be way to complicated. 51 | Added support for single inheritance via piggy-backing on the style's native 52 | `parent` attribute. 53 | - Added support for `bool`, `string` and `integer`. 54 | - Added support for multiple configurations for simple resources: 55 | ``` 56 | dimen margins { 57 | default: 8dp; 58 | land: 12dp; 59 | } 60 | ``` 61 | - Added support for multiple configurations for styles: 62 | ``` 63 | style Button < BaseButton { 64 | android:background: @drawable/btn_bkg; 65 | &:v21, v22 { 66 | android:background: @drawable/btn_ripple; 67 | } 68 | } 69 | ``` 70 | 71 | # eaml 0.2.0 - 1445287611 72 | 73 | First minimal working version: 74 | - Support for `color` and `dimens` e.g. `color primary_color: #f00;` 75 | - Multiple inheritance between styles 76 | ``` 77 | style BigRedButton < RedButton, BigButton { 78 | # your code here 79 | } 80 | ``` 81 | - Nested directories 82 | You can organize your files with nested directores: 83 | ``` 84 | ./styles/ 85 | ├── variables.eaml 86 | ├── common_styles.eaml 87 | └── buttons 88 | ├── buttons.eaml 89 | └── more_buttons.eaml 90 | ``` 91 | - One global scope. 92 | Currently there is only one scope: the global scope. This basically 93 | means that a `color foo` defined in one file is accesible everywhere. 94 | - No support for comments: no comments are supported yet. This feature 95 | coming soon. 96 | - No support for multiple configurations: Configuration support also 97 | coming soon. 98 | 99 | 100 | -------------------------------------------------------------------------------- /src/eaml/transpile/style.clj: -------------------------------------------------------------------------------- 1 | (ns eaml.transpile.style 2 | (:require [eaml.util :refer :all] 3 | [eaml.error :refer :all] 4 | [eaml.scope :refer [resolve-expr]])) 5 | 6 | (defn- mk-item 7 | [scope {name :name value :value}] 8 | [:item {:name name} (resolve-expr :any value scope)]) 9 | 10 | (defn- mk-style-res 11 | [scope id parent? attrs] 12 | (let [style-attrs {:name id} 13 | style-attrs (if parent? 14 | (assoc style-attrs :parent parent?) 15 | style-attrs) 16 | items (map #(mk-item scope %) attrs)] 17 | (cons* items :style style-attrs))) 18 | 19 | (defn style? 20 | [id] 21 | (= :style id)) 22 | 23 | (defn- mixin-attr? 24 | [attr] 25 | (contains-all? attr :mixin :args :config)) 26 | 27 | (defn regular-attr? 28 | [attr] 29 | (contains-all? attr :name :value :config)) 30 | 31 | (defn- expand-mixin 32 | [scope mixin] 33 | (:attrs (get scope (:mixin mixin)))) 34 | 35 | (defn- expand-mixins 36 | "Invokes all mixins and replaces them with the mixin's contents. Also removes 37 | any duplicate attributes that would have resulted by mixin expansion." 38 | [scope attrs] 39 | (->> (map (fn [attr] 40 | (cond (mixin-attr? attr) 41 | (expand-mixin scope attr) 42 | (regular-attr? attr) 43 | [attr] 44 | :else (raise! (itp "Unrecognized attr: #{attr}")))) 45 | attrs) 46 | (flat-coll) 47 | (remove-first-duplicates (fn [{name :name config :config}] 48 | [name config])))) 49 | 50 | (defn transpile-style 51 | "Return a map of config => style XmlStruct" 52 | ;; TODO: handle repeated attrs correctly! 53 | [scope {id :id parent :parent attrs :attrs}] 54 | (if (empty? attrs) 55 | ;; if there are no attrs, return an empty style 56 | (mk-style-res id parent []) 57 | (let [attrs (expand-mixins scope attrs) 58 | by-config (group-by :config attrs) 59 | ;; obtain a list of all configurations 60 | configs (keys by-config) 61 | ;; get the 'default' config attrs 62 | default-attrs (:default by-config) 63 | ;; merge the default config attrs into other configs. 64 | ;; Why? default config attrs should be present by default. 65 | by-config (reduce-kv (fn [by-config config attrs] 66 | (assoc by-config config 67 | (merge-lists default-attrs attrs :name))) 68 | {} by-config)] 69 | ;; iterate over all configs and construct 70 | ;; a map that assocs a config key to the XmlStruct 71 | ;; for the attrs that should be rendered in that config 72 | (reduce (fn [config-map config] 73 | (assoc config-map config 74 | (mk-style-res scope id parent (get by-config config)))) 75 | {} configs)))) 76 | 77 | 78 | -------------------------------------------------------------------------------- /src/eaml/xml.clj: -------------------------------------------------------------------------------- 1 | (ns eaml.xml 2 | (:require [clojure.string :refer [join]] 3 | [eaml.util :refer :all] 4 | [eaml.error :refer :all] 5 | [clojure.java.io :refer [writer]] 6 | [eaml.file :refer [path-concat mkdirs!]]) 7 | (:import [java.io StringWriter])) 8 | 9 | (declare render-xml-node-start 10 | render-xml-node-body 11 | render-xml-node-end) 12 | 13 | (defn string-writer 14 | "Initializes a new StringWriter" 15 | [] (StringWriter. )) 16 | 17 | (defn config-writer 18 | [root config] 19 | (let [config-name (name config) 20 | dir (if (= :default config) 21 | (itp "#{root}/values") 22 | (itp "#{root}/values-#{config-name}")) 23 | file-path (itp "#{dir}/values.xml")] 24 | (mkdirs! dir) 25 | (writer file-path))) 26 | 27 | (defn render-xml 28 | "Writes a tree of nodes to the given writer. 29 | node must be in the following form: 30 | [:node {:arg1 'val1' :arg2 'val2' ...} 31 | [:subnode1 {:arg1 'val1'} ...] 32 | [:subnode2 {:arg1 'val1'} ...] 33 | ...] 34 | 35 | Note: this function does not handle closing of the writer. 36 | You must do this yourself. 37 | 38 | Example: 39 | [:resources {} 40 | [:style {name 'Button' parent 'BaseButton'} 41 | [:item {name 'android:textSize'} '12sp']]] 42 | 43 | Will write 44 | 45 | 48 | " 49 | [node writer] 50 | (let [writer writer] 51 | (loop [node-queue (list node)] 52 | (if (empty? node-queue) 53 | writer 54 | (let [head (first node-queue) 55 | tail (rest node-queue)] 56 | (render-xml-node-start head writer) 57 | (render-xml-node-body head writer) 58 | (render-xml-node-end head writer) 59 | (recur tail)))))) 60 | 61 | (defn render-xml-string 62 | "Writes a tree of nodes to a string and returns that string. 63 | Use this method only for debbuging. 64 | Equivalent to render-xml using string-writer as a writer" 65 | [nodes] 66 | (let [writer (string-writer)] 67 | (render-xml nodes writer) 68 | (.toString writer))) 69 | 70 | (defn- render-xml-node-start 71 | [[node-type attrs & subnodes] writer] 72 | (.write writer "<") 73 | (.write writer (name node-type)) 74 | (.write writer " ") 75 | (.write writer (->> (map (fn [[k v]] 76 | (str (name k) "=\"" v \")) 77 | attrs) 78 | (join " "))) 79 | (.write writer ">")) 80 | 81 | (defn- render-xml-node-body 82 | [[node-type attrs & subnodes] writer] 83 | (when (seq subnodes) 84 | (doseq [node subnodes] 85 | (if (coll? node) 86 | (render-xml node writer) 87 | (.write writer node))))) 88 | 89 | (defn- render-xml-node-end 90 | [[node-type & _] writer] 91 | (.write writer "")) 94 | 95 | -------------------------------------------------------------------------------- /doc/0.3.0/quickstart.md: -------------------------------------------------------------------------------- 1 | Language QuickStart 2 | =================== 3 | 4 | This short guide will give you a basic understanding of most eaml features. 5 | 6 | ## Defining simple resources 7 | 8 | Simple resources include `color`, `dimen`, `bool`, `integer` and `string` 9 | 10 | ``` 11 | # Let's define some colors 12 | color red: #f00; 13 | color green: #0f0; 14 | color blue: #00f; 15 | 16 | # And some margins 17 | dimen small_margins: 4dp; 18 | dimen medium_margins: 8dp; 19 | dimen large_margins: 8dp; 20 | 21 | # color pointers 22 | color main_color: red; 23 | ``` 24 | 25 | ### Supporting multiple configurations 26 | 27 | Eaml simplifies supporting multiple configurations by providing a concise 28 | syntax to describe how resources behave on different configurations. 29 | 30 | ``` 31 | # Example: paddings that behave differently when the device is in landscape 32 | dimen button_paddings { 33 | default: 4dp; 34 | land: 8dp; 35 | } 36 | ``` 37 | 38 | ### Functions (TODO) 39 | 40 | Eaml supports several built-int functions which greatly facilitate calculating 41 | properties based on other properties. 42 | 43 | ``` 44 | color red: #f00; 45 | color red_dark: darker(10%, red); 46 | color red_darker: darker(20%, red); 47 | color red_light: lighten(10%, red); 48 | color red_lighter: lighten(20%, red); 49 | ``` 50 | 51 | #### Creating your own functions (TODO) 52 | 53 | You can create your own functions as follows: 54 | 55 | ``` 56 | func bigger(dimen d, percent p) { 57 | d*(1 + p); 58 | } 59 | 60 | func smaller(dimen d, percent p) { 61 | d*(1 - p); 62 | } 63 | ``` 64 | 65 | ## Defining styles 66 | 67 | Styles can be defined using the following syntax. 68 | ``` 69 | style Button { 70 | android:paddingLeft: 8dp; 71 | android:paddingRight: 8dp; 72 | android:paddingTop: 4dp; 73 | android:paddingBottom: 4dp; 74 | android:textSize: 12sp; 75 | } 76 | ``` 77 | 78 | ## Inheritance 79 | Eaml supports single inheritance via the `<` keyword. This is simply syntax sugar 80 | that piggy backs on android's `parent` attribute. 81 | ``` 82 | style BigButton < Button { 83 | android:paddingTop: 8dp; 84 | android:paddingBottom: 8dp; 85 | } 86 | ``` 87 | 88 | ## Mixins 89 | In cases where you would like to reuse functionality from more than one source 90 | you can use mixins. Mixins are for most purposes identical to styles except for 91 | the fact that they cannot inherit from other `mixins` or `styles`. 92 | 93 | ``` 94 | # A simple mixin that just sets the color to red. 95 | mixin redText { 96 | android:textColor: red; 97 | } 98 | 99 | style RedButton < Button { 100 | redText() 101 | } 102 | ``` 103 | 104 | Mixins can define behaviour for multiple configurations as follows: 105 | 106 | ``` 107 | mixin redText { 108 | android:textColor: red; 109 | &:v21 { 110 | android:textColor: fancy_red; 111 | } 112 | } 113 | 114 | style RedButton < Button { 115 | redText(); 116 | } 117 | ``` 118 | NOTE: you cannot invoke a mixin from inside a configuration block: 119 | 120 | ``` 121 | style RedButton < Button { 122 | &:v21 { 123 | redText(); # THIS WILL THROW AN ERROR! 124 | } 125 | } 126 | ``` 127 | 128 | ## Multiple configuration support 129 | Eaml styles also come with built in syntax to support multiple configurations 130 | as follows: 131 | 132 | ``` 133 | # A button that uses a ripple on v21 and a regular drawable by default 134 | style RedButton < Button { 135 | android:background { 136 | default: @drawable/btn_red 137 | v21: @drawable/btn_red_ripple 138 | } 139 | } 140 | ``` 141 | 142 | 143 | -------------------------------------------------------------------------------- /src/eaml/scope.clj: -------------------------------------------------------------------------------- 1 | (ns eaml.scope 2 | "This namespace provides the build-scope function whose 3 | purpose is two build the global scope which will later be used 4 | by the transpiler to resolve variables and type checking 5 | 6 | attr-value: a tuple where the first element is the type and 7 | the second element is the value. 8 | 9 | Example: [:literal \"#ff0000\"] => an attr-value for a color 10 | [:] 11 | " 12 | (:require [eaml.error :refer :all] 13 | [eaml.util :refer :all])) 14 | 15 | (defn literal? 16 | [parsed] 17 | (#{:color :dimen :bool :integer :string} parsed)) 18 | 19 | (defn pointer? 20 | [parsed] 21 | (= parsed :pointer)) 22 | 23 | (defn null? 24 | [parsed] 25 | (= parsed :native-null)) 26 | 27 | (defn native-res? 28 | [parsed] 29 | (= parsed :native-pointer)) 30 | 31 | (defn- remove-quotes 32 | [string] 33 | (.replaceAll string "[\"']" "")) 34 | 35 | (defn parse-expr 36 | [string] 37 | (case-match string 38 | #"\A@null" :native-null 39 | #"\A@.*?(/.*)" :native-pointer 40 | #"\A#[a-fA-F\d]{3}" :color 41 | #"\A#[a-fA-F\d]{6}" :color 42 | #"\A#[a-fA-F\d]{8}" :color 43 | #"\A\d+(sp|px|dp)" :dimen 44 | #"\Atrue|false" :bool 45 | #"\A\d+" :integer 46 | #"\A\".*?\"" :string 47 | #"\A'.*?'" :string 48 | #"\A[a-zA-Z]\w*" :pointer)) 49 | 50 | (defn create 51 | "Create the scope" 52 | [nodes] 53 | (reduce (fn [scope node] 54 | (let [id (:id node)] 55 | (if (contains? scope id) 56 | (raise! (itp "Scope already contains a mapping for #{id} => #{node}: #{(scope id)}")) 57 | (assoc scope id node)))) 58 | {} nodes)) 59 | 60 | (defn obtain-type 61 | [expr scope] 62 | (let [parsed (parse-expr expr)] 63 | (cond (null? parsed) 64 | parsed 65 | (literal? parsed) 66 | parsed 67 | (native-res? parsed) 68 | (->> (re-find #"@(.*?)/" expr) 69 | (last) 70 | (keyword)) 71 | (pointer? parsed) 72 | (if (contains? scope expr) 73 | (:node (get scope expr)) 74 | (raise! (itp "No mapping found for '#{expr}' in scope"))) 75 | :else 76 | (raise! (itp "Unkown parse result: '#{parsed}'"))))) 77 | 78 | (defn resolve-expr 79 | "Obtains the value for the given expr in the scope. 80 | If the resolved type is different from the expected type 81 | then return nil. 82 | If the given expr is not present in the scope, throw an exception. 83 | You may use :any as type to match any type" 84 | [type expr scope] 85 | (let [parsed (parse-expr expr)] 86 | (cond (or (literal? parsed) (native-res? parsed)) 87 | (if (= :string parsed) 88 | (remove-quotes expr) 89 | expr) 90 | (null? parsed) 91 | expr 92 | (pointer? parsed) 93 | (let [resolved-type (obtain-type expr scope)] 94 | (if (or (= type :any) (= type resolved-type)) 95 | (case resolved-type 96 | :dimen (itp "@dimen/#{expr}") 97 | :color (itp "@color/#{expr}") 98 | :bool (itp "@bool/#{expr}") 99 | :integer (itp "@integer/#{expr}") 100 | :string (itp "@string/#{expr}")) 101 | (raise! "Type mismatch: expected #{type} but got #{resolved-type}")))))) 102 | 103 | (defn include? 104 | "True if the given id exists for the current scope." 105 | [scope id] 106 | (contains? scope id)) 107 | -------------------------------------------------------------------------------- /src/eaml/parser.clj: -------------------------------------------------------------------------------- 1 | (ns eaml.parser 2 | (:require [instaparse.core :as insta] 3 | [eaml.file :refer :all] 4 | [eaml.util :refer :all] 5 | [eaml.error :refer :all])) 6 | 7 | (defn or-empty 8 | [coll-or-nil] 9 | (if (nil? coll-or-nil) 10 | [] coll-or-nil)) 11 | 12 | (defn normalize-literal 13 | [literal-value] 14 | literal-value) 15 | 16 | (defn normalize-pointer 17 | [value] 18 | value) 19 | 20 | (defn normalize-string 21 | [value] 22 | value) 23 | 24 | (defn normalize-simple-res-def 25 | [res-type id value] 26 | {:node (keyword res-type) 27 | :id id 28 | :vals value}) 29 | 30 | (defn normalize-simple-res-single-config 31 | [value] 32 | [{:config :default 33 | :value value}]) 34 | 35 | (defn normalize-simple-res-config 36 | [config value] 37 | {:value value 38 | :config config}) 39 | 40 | (defn normalize-style 41 | ([id parent attrs?] 42 | {:id id 43 | :node :style 44 | :parent parent 45 | :attrs (or-empty attrs?)}) 46 | ([id attrs?] 47 | (normalize-style id nil attrs?))) 48 | 49 | (defn normalize-mixin 50 | [id attrs?] 51 | {:id id 52 | :node :mixin 53 | :attrs (or-empty attrs?)}) 54 | 55 | 56 | (defn normalize-attr 57 | [id value] 58 | {:name id 59 | :value value 60 | :config :default}) 61 | 62 | 63 | (defn normalize-config-name 64 | [config-name] 65 | (keyword config-name)) 66 | 67 | (defn normalize-config-block 68 | [config-names & attrs] 69 | {:config-block (for [config-name config-names 70 | attr attrs] 71 | (assoc attr :config config-name))}) 72 | 73 | (defn normalize-style-attrs 74 | [& attrs] 75 | ;; TODO: rewrite this 76 | (reduce (fn [result attr] 77 | (if (:config-block attr) 78 | (into result (:config-block attr)) 79 | (conj result attr))) 80 | [] attrs)) 81 | 82 | (defn normalize-mixin-attrs 83 | [& attrs] 84 | ;; TODO: rewrite this 85 | (apply normalize-style-attrs attrs)) 86 | 87 | (defn normalize-mixin-call 88 | [mixin-id & args?] 89 | {:mixin mixin-id 90 | :config :default 91 | :args (or-empty args?)}) 92 | 93 | 94 | (defn normalize-nodes 95 | [ast-nodes] 96 | (insta/transform {:color-literal normalize-literal 97 | :dimen-literal normalize-literal 98 | :string-literal normalize-string 99 | :native-pointer normalize-literal 100 | :boolean-literal normalize-literal 101 | :integer-literal normalize-literal 102 | :pointer normalize-pointer 103 | :config-name normalize-config-name 104 | :config-names (fn [& args] args) 105 | :simple-res-def normalize-simple-res-def 106 | :simple-res-configs (fn [& args] args) 107 | :simple-res-config normalize-simple-res-config 108 | :simple-res-single-config normalize-simple-res-single-config 109 | :config-block normalize-config-block 110 | :mixin-call normalize-mixin-call 111 | :extends-expr (fn [& args] args) 112 | :mixin-def normalize-mixin 113 | :style-def normalize-style 114 | :attrs normalize-style-attrs 115 | :mixin-attrs normalize-mixin-attrs 116 | :attr-def normalize-attr} 117 | ast-nodes)) 118 | 119 | (def parser (insta/parser (clojure.java.io/resource "grammar.bnf") 120 | :auto-whitespace :standard)) 121 | 122 | (defn parse-str 123 | "Parses and normalizes a string. Raises an error if it is not possible to parse 124 | the given string." 125 | [string] 126 | (let [parsed (parser string)] 127 | (if (insta/failure? parsed) 128 | (do (instaparse.failure/pprint-failure (insta/get-failure parsed)) 129 | (raise! "Error while parsing")) 130 | (normalize-nodes parsed)))) 131 | 132 | 133 | (defn parse-dir 134 | "Finds all .eaml files under the given root-path (folder) recursively and returns 135 | an AST. See parse-str" 136 | [root-path] 137 | (->> (filter-tree root-path (extension-filter "eaml")) 138 | (map read-file) 139 | (map parse-str) 140 | (flat-coll))) 141 | -------------------------------------------------------------------------------- /src/eaml/util.clj: -------------------------------------------------------------------------------- 1 | (ns eaml.util 2 | (:require [clojure.set :refer [union intersection]])) 3 | 4 | (defn flat-coll 5 | "Given a coll of collections, flattens it by one level. 6 | Example: [[1] [2 3] [] [4 5 6]] will result in [1 2 3 4 5 6]" 7 | [colls] 8 | (apply concat colls)) 9 | 10 | 11 | (defn group-maps 12 | "Takes a collection of maps and groups their k,v pairs by key" 13 | [maps] 14 | (loop [grouped {} 15 | maps maps] 16 | (if (empty? maps) 17 | grouped 18 | (let [m (first maps)] 19 | (recur (reduce-kv (fn [reduction k v] 20 | (let [current-val (get reduction k)] 21 | (if current-val 22 | (assoc reduction k (conj current-val v)) 23 | (assoc reduction k [v])))) 24 | grouped m) 25 | (rest maps)))))) 26 | 27 | (defn cons* 28 | "equivalent to calling cons over the values in reverse order. 29 | Example: (cons* [4 5 6] 1 2 3) == '(1 2 3 4 5 6)" 30 | [coll & values] 31 | (if (empty? values) 32 | coll 33 | (cons (first values) 34 | (apply cons* coll (rest values))))) 35 | 36 | (defmacro itp 37 | "Interpolation macro. Similar to ruby interpolation. 38 | Does not provide any escaping mechanism. 39 | Syntax: (itp 'Hi #{name}, how are you?') 40 | Will expand to (str 'Hi ' name ', how are you?')" 41 | [string] 42 | (loop [string string 43 | result []] 44 | (let [[_ match-sym-anywhere] (re-find #"(.*?)#\{.*?\}" string) 45 | [regex-match match-symbol] (re-find #"\A#\{(.*?)\}" string)] 46 | (cond match-symbol 47 | (recur (.substring string (count regex-match)) 48 | (conj result (read-string match-symbol))) 49 | match-sym-anywhere 50 | (recur (.substring string (count match-sym-anywhere)) 51 | (conj result match-sym-anywhere)) 52 | :else 53 | (cons `str (conj result string)))))) 54 | 55 | (defn case-match 56 | "Matches string agains the given regex, one by one until a match 57 | is found. Returns the matched form arg. Returns nil if there are no matches. 58 | form-pairs => [regex arg]" 59 | [string & form-pairs] 60 | (if (empty? form-pairs) 61 | nil 62 | (let [[regex form] (take 2 form-pairs)] 63 | (if (re-find regex string) 64 | form 65 | (apply case-match string (rest (rest form-pairs))))))) 66 | 67 | 68 | (defn singleton? 69 | [coll] 70 | (= (count coll) 1)) 71 | 72 | (defn find-first 73 | "Return the first x in coll s.t. pred(x) = true" 74 | [coll pred] 75 | (first (filter pred coll))) 76 | 77 | 78 | (defn merge-lists 79 | "Preconditions: 80 | - f is a 1-1 function 81 | - Every element on both l1 and l2 is unique 82 | 83 | Merges the two lists into one. If for some a in l1 and b in l2 84 | (= (f a) (f b)) then only b is added." 85 | [l1 l2 f] 86 | (let [mapped-set1 (set (map f l1)) 87 | mapped-set2 (set (map f l2)) 88 | mapped-intersection (intersection mapped-set1 mapped-set2) 89 | l1-filtered (filter (fn [x] 90 | (if (contains? mapped-intersection (f x)) 91 | false true)) 92 | l1)] 93 | (loop [l2 l2 94 | clashes [] 95 | result []] 96 | (if (empty? l2) 97 | (concat l1-filtered result clashes) 98 | (let [x (first l2) 99 | tail (rest l2) 100 | fx (f x)] 101 | (if (contains? mapped-intersection fx) 102 | (recur tail (conj clashes x) result) 103 | (recur tail clashes (conj result x)))))))) 104 | 105 | (defn reduce-indexed 106 | "Equivalent to reduce but f takes three arguments: 107 | the reduction, index and value and value." 108 | [f init coll] 109 | (loop [reduction init 110 | coll coll 111 | index 0] 112 | (if (empty? coll) 113 | reduction 114 | (recur (f reduction index (first coll)) 115 | (rest coll) 116 | (inc index))))) 117 | 118 | (defn contains-all? 119 | "Returns true if the given map contains all the specified keys" 120 | [m & ks] 121 | (if (empty? ks) 122 | true 123 | (and (contains? m (first ks)) 124 | (contains-all? (rest ks))))) 125 | 126 | (defn remove-first-duplicates 127 | "Removes all duplicates from the given list, preserves the last 128 | duplicate. Two elements a and b are considered a duplicate if f(a) == f(b) 129 | Example (remove-first-duplicates keys [{:b 0} {:a 1} {:a 2} {:b 1}]) will result in 130 | [{:a 2} {:b 1}]" 131 | [f coll] 132 | (let [duplicates (reduce-indexed (fn [reduction index x] 133 | (assoc reduction (f x) [index x])) 134 | {} coll) 135 | without-dups (reduce-indexed (fn [reduction index x] 136 | (if (= (get duplicates (f x)) 137 | [index x]) 138 | (conj reduction x) 139 | reduction)) 140 | [] coll)] 141 | without-dups)) 142 | -------------------------------------------------------------------------------- /doc/0.4.0/quickstart.md: -------------------------------------------------------------------------------- 1 | Language QuickStart 2 | =================== 3 | 4 | This short guide will give you a basic understanding of most eaml features. 5 | 6 | ## Introduction 7 | 8 | Eaml is a simple language for defining styles in Android. Eaml is targeted at android 9 | developers who already understand the Android styling framework but are having trouble 10 | organizing and maintaining their styles as the project grows. 11 | 12 | Eaml's most interesting features include: 13 | - Support for nested directories. You can nest your `.eaml` files as deep as possible. 14 | - Support for `mixin`s. See the mixin section for more information. 15 | - Configuration selectors. Using the `&:` syntax you can specify how a style varies its 16 | behaviour depending on the device's configuration. 17 | 18 | *NOTE:* A good understanding of how Android styles work is required in order to use Eaml 19 | proficiently as Eaml transpiles to Android's XML styles. 20 | 21 | ## Defining simple resources 22 | 23 | Eaml provides a simplified syntax for defining simple resources. 24 | Simple resources include `color`, `dimen`, `bool`, `integer` and `string` 25 | 26 | ### Syntax 27 | ` : ;` 28 | 29 | Example: 30 | ``` 31 | # Let's define some colors 32 | color red: #f00; 33 | color green: #0f0; 34 | color blue: #00f; 35 | 36 | # And some dimen 37 | dimen small_margins: 4dp; 38 | dimen medium_margins: 8dp; 39 | dimen large_margins: 8dp; 40 | ``` 41 | 42 | ### Resource Pointers 43 | 44 | When a resource reference another resource, this reference is called a resource pointer. 45 | 46 | Eaml provides two types of pointers: 47 | 48 | - Native resource pointers e.g. `@color/red`. Use these type of pointers when referencing a resource 49 | defined in an xml file. This serves as a way of interop between existing xml styles and Eaml programs. 50 | All form of native resource pointers are supported including `@null`. 51 | - Eaml resource pointers e.g. `color main_color: red;`. Use these pointers when referencing a resource 52 | defined in Eaml. 53 | 54 | Example: 55 | ``` 56 | color a_cool_color: #f00; 57 | color some_other_color: @android:color/a_color; 58 | color my_favorite_color: a_cool_color; 59 | ``` 60 | 61 | ### Supporting multiple configurations 62 | 63 | Eaml simplifies supporting multiple configurations by providing a concise 64 | syntax to describe how resources behave on different configurations. 65 | 66 | #### Syntax: 67 | 68 | ``` 69 | { 70 | : ; 71 | : ; 72 | ... 73 | } 74 | ``` 75 | 76 | Example: 77 | ``` 78 | # Paddings that behave differently when the device is in landscape 79 | dimen button_paddings { 80 | default: 4dp; 81 | land: 8dp; 82 | } 83 | 84 | # Support for certain features 85 | bool has_ripples { 86 | v21: true; 87 | default: false; 88 | } 89 | ``` 90 | 91 | ## Defining styles 92 | 93 | Styles can be defined using the following syntax. 94 | ``` 95 | style (< )? { 96 | * 97 | } 98 | ``` 99 | 100 | Example: 101 | ``` 102 | style Button { 103 | android:paddingLeft: 8dp; 104 | android:paddingRight: 8dp; 105 | android:paddingTop: 4dp; 106 | android:paddingBottom: 4dp; 107 | android:textSize: 12sp; 108 | } 109 | ``` 110 | 111 | ## Inheritance 112 | Eaml supports single inheritance via the `<` keyword. This is simply syntax sugar 113 | that piggy backs on android's `parent` attribute. 114 | 115 | Example: 116 | ``` 117 | style BigButton < Button { 118 | android:paddingTop: 8dp; 119 | android:paddingBottom: 8dp; 120 | } 121 | ``` 122 | 123 | ## Multiple configuration support 124 | Styles also come with built in syntax to support multiple configurations 125 | as follows: 126 | 127 | ``` 128 | # A button that uses a ripple on v21 and a regular drawable by default 129 | style RedButton < Button { 130 | android:textColor: red; 131 | android:background: @drawable/btn_red; 132 | &:v21 { 133 | android:background: @drawable/btn_red_ripple; 134 | } 135 | } 136 | ``` 137 | 138 | In this case `&:v21 { ... }` is called a configuration block for `v21`. 139 | You can add as many attributes as you want inside a configuration block. 140 | *NOTE*: Eaml technically does not know about Android's configuration management system so 141 | whatever configuration identifier you specify will be used to create a folder with 142 | the given name e.g. `&:foo-bar { ... }` will create `res/values-foo-bar/styles.xml` 143 | which will never be picked up by the Android OS. 144 | 145 | ## Mixins 146 | In cases where you would like to reuse functionality from more than one source 147 | you can use mixins. Mixins are for most purposes identical to styles except for 148 | the fact that they cannot inherit from other `mixins` or `styles`. 149 | 150 | Example: 151 | ``` 152 | # A simple mixin that just sets the text color to red. 153 | mixin redText { 154 | android:textColor: red; 155 | } 156 | 157 | mixin bigText { 158 | android:textSize: 28sp; 159 | } 160 | 161 | style RedButton < Button { 162 | redText(); 163 | } 164 | 165 | style BigRedButton < Button { 166 | redText(); 167 | bigText(); 168 | } 169 | ``` 170 | 171 | Mixins can define behaviour for multiple configurations as follows: 172 | 173 | ``` 174 | mixin redText { 175 | android:textColor: red; 176 | &:v21 { 177 | android:textColor: fancy_red; 178 | } 179 | } 180 | 181 | style RedButton < Button { 182 | redText(); 183 | } 184 | ``` 185 | 186 | *NOTE*: you cannot invoke a mixin from inside a configuration block: 187 | ``` 188 | style RedButton < Button { 189 | &:v21 { 190 | redText(); # THIS WILL THROW AN ERROR! 191 | } 192 | } 193 | ``` 194 | 195 | 196 | -------------------------------------------------------------------------------- /test/eaml/parser_test.clj: -------------------------------------------------------------------------------- 1 | (ns eaml.parser-test 2 | (:require [eaml.parser :refer :all] 3 | [presto.core :refer :all] 4 | [clojure.test :refer :all])) 5 | 6 | (defn parse-first 7 | [string] 8 | (first (parse-str string))) 9 | 10 | (expected-when "parsing a single color" parse-first 11 | when ["color red: #ff0000;"] 12 | = {:id "red" 13 | :node :color 14 | :vals [{:config :default 15 | :value "#ff0000"}]} 16 | 17 | when ["color some_other_color: #f12;"] 18 | = {:id "some_other_color" 19 | :node :color 20 | :vals [{:config :default 21 | :value "#f12"}]} 22 | 23 | when ["color multi_config_color { 24 | default: #ff0000; 25 | v21: @drawable/btn_red_ripple; 26 | }"] 27 | = {:id "multi_config_color" 28 | :node :color 29 | :vals [{:config :default 30 | :value "#ff0000"} 31 | {:config :v21 32 | :value "@drawable/btn_red_ripple"}]} 33 | 34 | when ["color some_color: some_other;"] 35 | = {:id "some_color" 36 | :node :color 37 | :vals [{:config :default 38 | :value "some_other"}]}) 39 | 40 | (expected-when "parsing a boolean" parse-first 41 | when ["bool is_foo: true;"] 42 | = {:id "is_foo" 43 | :node :bool 44 | :vals [{:config :default 45 | :value "true"}]}) 46 | 47 | (expected-when "parsing a string" parse-first 48 | when ["string a_string: 'foobar';"] 49 | = {:id "a_string" 50 | :node :string 51 | :vals [{:config :default 52 | :value "'foobar'"}]}) 53 | 54 | (expected-when "parsing an integer" parse-first 55 | when ["integer foo: 123;"] 56 | = {:id "foo" 57 | :node :integer 58 | :vals [{:config :default 59 | :value "123"}]}) 60 | 61 | 62 | (expected-when "parsing a single dimens" parse-first 63 | when ["dimen small_margins: 12dp;"] 64 | = {:id "small_margins" 65 | :node :dimen 66 | :vals [{:config :default 67 | :value "12dp"}]} 68 | 69 | when ["dimen large_text: 12sp;"] 70 | = {:id "large_text" 71 | :node :dimen 72 | :vals [{:config :default 73 | :value "12sp"}]}) 74 | 75 | 76 | (expected-when "parsing a single style" parse-first 77 | when ["style Foo {}"] 78 | = {:id "Foo" 79 | :node :style 80 | :parent nil 81 | :attrs []} 82 | 83 | when ["style FooBar < Foo.Bar123.Baz {}"] 84 | = {:id "FooBar" 85 | :node :style 86 | :parent "Foo.Bar123.Baz" 87 | :attrs []} 88 | 89 | when ["style FooBar123 < Foo { 90 | colorPrimaryDark: #ff0000; 91 | android:textSize: 12sp; 92 | android:background: @null; 93 | foo: @android:foo/bar; 94 | bar123: @style/Text.Label2; 95 | }"] 96 | = {:id "FooBar123" 97 | :node :style 98 | :parent "Foo" 99 | :attrs [{:name "colorPrimaryDark" :value "#ff0000" :config :default} 100 | {:name "android:textSize" :value "12sp" :config :default} 101 | {:name "android:background" :value "@null" :config :default} 102 | {:name "foo" :value "@android:foo/bar" :config :default} 103 | {:name "bar123" :value "@style/Text.Label2" :config :default}]} 104 | 105 | when ["style Foo < Bar { 106 | android:textColor: #123; 107 | android:background: @drawable/foo_drawable; 108 | fooBar(); 109 | android:textSize: small_text; 110 | android:text: \"some text\"; 111 | qux(); 112 | }"] 113 | = {:id "Foo" 114 | :node :style 115 | :parent "Bar" 116 | :attrs [{:name "android:textColor" :value "#123" :config :default} 117 | {:name "android:background" :value "@drawable/foo_drawable" :config :default} 118 | {:mixin "fooBar" :args [] :config :default} 119 | {:name "android:textSize" :value "small_text" :config :default} 120 | {:name "android:text" :value "\"some text\"" :config :default} 121 | {:mixin "qux" :args [] :config :default}]}) 122 | 123 | (expected-when "Parsing a single mixin" parse-first 124 | when ["mixin Foo {}"] 125 | = {:id "Foo" 126 | :node :mixin 127 | :attrs []} 128 | 129 | when ["mixin FooBarBaz { 130 | android:textSize: 12sp; 131 | android:textColor: #123456; 132 | android:numFoo: 4; 133 | android:text: @string/a_string; 134 | &:foo-bar { 135 | android:textSize: 14sp; 136 | android:extraAttr: 'extra'; 137 | } 138 | }"] 139 | = {:id "FooBarBaz" 140 | :node :mixin 141 | :attrs [{:name "android:textSize" :value "12sp" :config :default} 142 | {:name "android:textColor" :value "#123456" :config :default} 143 | {:name "android:numFoo" :value "4" :config :default} 144 | {:name "android:text" :value "@string/a_string" :config :default} 145 | {:name "android:textSize" :value "14sp" :config :foo-bar} 146 | {:name "android:extraAttr" :value "'extra'" :config :foo-bar}]}) 147 | 148 | 149 | (expected-when "parsing several nodes" parse-str 150 | when ["color red: #f00; 151 | dimen normal_paddings: 12dp; 152 | style Foo < Bar { 153 | foo: 12dp; 154 | &:land { foo: 12dp; } 155 | }"] 156 | = [{:id "red" 157 | :node :color 158 | :vals [{:config :default 159 | :value "#f00"}]} 160 | {:id "normal_paddings" 161 | :node :dimen 162 | :vals [{:config :default 163 | :value "12dp"}]} 164 | {:id "Foo" 165 | :node :style 166 | :parent "Bar" 167 | :attrs [{:name "foo" :value "12dp" :config :default} 168 | {:name "foo" :value "12dp" :config :land}]}]) 169 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # eaml 2 | 3 | [![Circle CI](https://circleci.com/gh/fhur/eaml/tree/master.svg?style=svg)](https://circleci.com/gh/fhur/eaml/tree/master) 4 | 5 | eaml (pronounced e-mel) is the extended android modeling language. It is 6 | an XML preprocessor that will make your android resource definitions 7 | simple, readable, understandable and will greatly facilitate supporting 8 | several different configurations with a minimal code base. 9 | 10 | Read the [release notes](./doc/release_notes.md) to see what features 11 | are implemented in the current release. 12 | 13 | Language QuickStart 14 | =================== 15 | 16 | This short guide will give you a basic understanding of most eaml features. 17 | 18 | ## Introduction 19 | 20 | Eaml is a simple language for defining styles in Android. Eaml is targeted at android 21 | developers who already understand the Android styling framework but are having trouble 22 | organizing and maintaining their styles as the project grows. 23 | 24 | Eaml's most interesting features include: 25 | - Support for nested directories. You can nest your `.eaml` files as deep as possible. 26 | - Support for `mixin`s. See the mixin section for more information. 27 | - Configuration selectors. Using the `&:` syntax you can specify how a style varies its 28 | behaviour depending on the device's configuration. 29 | 30 | *NOTE:* A good understanding of how Android styles work is required in order to use Eaml 31 | proficiently as Eaml transpiles to Android's XML styles. 32 | 33 | ## Defining simple resources 34 | 35 | Eaml provides a simplified syntax for defining simple resources. 36 | Simple resources include `color`, `dimen`, `bool`, `integer` and `string` 37 | 38 | ### Syntax 39 | ` : ;` 40 | 41 | Example: 42 | ``` 43 | # Let's define some colors 44 | color red: #f00; 45 | color green: #0f0; 46 | color blue: #00f; 47 | 48 | # And some dimen 49 | dimen small_margins: 4dp; 50 | dimen medium_margins: 8dp; 51 | dimen large_margins: 8dp; 52 | ``` 53 | 54 | ### Resource Pointers 55 | 56 | When a resource reference another resource, this reference is called a resource pointer. 57 | 58 | Eaml provides two types of pointers: 59 | 60 | - Native resource pointers e.g. `@color/red`. Use these type of pointers when referencing a resource 61 | defined in an xml file. This serves as a way of interop between existing xml styles and Eaml programs. 62 | All form of native resource pointers are supported including `@null`. 63 | - Eaml resource pointers e.g. `color main_color: red;`. Use these pointers when referencing a resource 64 | defined in Eaml. 65 | 66 | Example: 67 | ``` 68 | color a_cool_color: #f00; 69 | color some_other_color: @android:color/a_color; 70 | color my_favorite_color: a_cool_color; 71 | ``` 72 | 73 | ### Supporting multiple configurations 74 | 75 | Eaml simplifies supporting multiple configurations by providing a concise 76 | syntax to describe how resources behave on different configurations. 77 | 78 | #### Syntax: 79 | 80 | ``` 81 | { 82 | : ; 83 | : ; 84 | ... 85 | } 86 | ``` 87 | 88 | Example: 89 | ``` 90 | # Paddings that behave differently when the device is in landscape 91 | dimen button_paddings { 92 | default: 4dp; 93 | land: 8dp; 94 | } 95 | ``` 96 | 97 | ## Defining styles 98 | 99 | Styles can be defined using the following syntax. 100 | ``` 101 | style (< )? { 102 | * 103 | } 104 | ``` 105 | 106 | Example: 107 | ``` 108 | style Button { 109 | android:paddingLeft: 8dp; 110 | android:paddingRight: 8dp; 111 | android:paddingTop: 4dp; 112 | android:paddingBottom: 4dp; 113 | android:textSize: 12sp; 114 | } 115 | ``` 116 | 117 | ## Inheritance 118 | Eaml supports single inheritance via the `<` keyword. This is simply syntax sugar 119 | that piggy backs on android's `parent` attribute. 120 | 121 | Example: 122 | ``` 123 | style BigButton < Button { 124 | android:paddingTop: 8dp; 125 | android:paddingBottom: 8dp; 126 | } 127 | ``` 128 | 129 | ## Multiple configuration support 130 | Styles also come with built in syntax to support multiple configurations 131 | as follows: 132 | 133 | ``` 134 | # A button that uses a ripple on v21 and a regular drawable by default 135 | style RedButton < Button { 136 | android:textColor: red; 137 | android:background: @drawable/btn_red; 138 | &:v21 { 139 | android:background: @drawable/btn_red_ripple; 140 | } 141 | } 142 | ``` 143 | 144 | ## Mixins 145 | In cases where you would like to reuse functionality from more than one source 146 | you can use mixins. Mixins are for most purposes identical to styles except for 147 | the fact that they cannot inherit from other `mixins` or `styles`. 148 | 149 | ``` 150 | # A simple mixin that just sets the text color to red. 151 | mixin redText { 152 | android:textColor: red; 153 | } 154 | 155 | style RedButton < Button { 156 | redText() 157 | } 158 | ``` 159 | 160 | Mixins can define behaviour for multiple configurations as follows: 161 | 162 | ``` 163 | mixin redText { 164 | android:textColor: red; 165 | &:v21 { 166 | android:textColor: fancy_red; 167 | } 168 | } 169 | 170 | style RedButton < Button { 171 | redText(); 172 | } 173 | ``` 174 | 175 | *NOTE*: you cannot invoke a mixin from inside a configuration block: 176 | ``` 177 | style RedButton < Button { 178 | &:v21 { 179 | redText(); # THIS WILL THROW AN ERROR! 180 | } 181 | } 182 | ``` 183 | 184 | #### Android Studio Support 185 | 186 | There is an Android Studio being developed in [fhur/eaml-idea](https://github.com/fhur/eaml-idea). 187 | The plugin is still under construction but it will offer easy gradle integration, syntax highlighting, 188 | auto completion and other nice features. 189 | 190 | #### Feature requests 191 | 192 | eaml is still in its infancy and we are very interested in understanding 193 | what problems android devs encounter when writing styles. 194 | 195 | Is there a feature that you really need but is not here? 196 | Please create a new issue explaining the feature + use case and 197 | it might end up on the next version :) 198 | 199 | ## License 200 | 201 | Copyright © 2015 fhur 202 | 203 | Distributed under the Eclipse Public License either version 1.0. 204 | -------------------------------------------------------------------------------- /test/eaml/compiler_test.clj: -------------------------------------------------------------------------------- 1 | (ns eaml.compiler-test 2 | (:require [eaml.compiler :refer :all] 3 | [eaml.test-helpers :refer :all] 4 | [eaml.fixtures.simple-res :refer :all] 5 | [presto.core :refer :all] 6 | [clojure.test :refer :all])) 7 | 8 | 9 | (expected-when "Transpiling simple resources" transpile-str 10 | when [fix-simple-colors] 11 | = {:default 12 | (resources 13 | (color "red" "#f00") 14 | (color "green" "#0f0") 15 | (color "blue" "#00f") 16 | (color "main_color" "@color/red"))} 17 | 18 | when [fix-simple-dimen] 19 | = {:default 20 | (resources 21 | (dimen "small_margins" "8dp") 22 | (dimen "medium_margins" "12dp") 23 | (dimen "large_margins" "24dp") 24 | (dimen "default_margins" "@dimen/medium_margins"))} 25 | 26 | when [fix-simple-strings] 27 | = {:default 28 | (resources 29 | (string "hello_world" "Hello World!") 30 | (string "name" "Pizza 123"))} 31 | 32 | when [fix-simple-bools] 33 | = {:default 34 | (resources 35 | (bool "is_true" "true") 36 | (bool "aint_true" "false") 37 | (bool "a_boolean" "@bool/is_true"))}) 38 | 39 | 40 | (expected-when "Transpiling simple resources that support multiple configs" transpile-str 41 | when [fix-simple-res-with-configs] 42 | = {:default (resources 43 | (dimen "padding" "12dp") 44 | (string "supports_ripples" "nope") 45 | (color "main_color" "#f00") 46 | (color "button_color" "@color/main_color")) 47 | :v21 (resources 48 | (dimen "padding" "24dp") 49 | (string "supports_ripples" "yes") 50 | (color "button_color" "@drawable/btn_ripple")) 51 | :land (resources 52 | (dimen "padding" "30dp"))}) 53 | 54 | (expected-when "Transpiling styles" transpile-str 55 | when ["color foo: #fff; 56 | mixin redColored { 57 | color: #f00; 58 | bar: 12dp; 59 | } 60 | style Foo { 61 | foo: foo; 62 | redColored(); 63 | }"] 64 | = {:default (resources 65 | (color "foo" "#fff") 66 | (style {:name "Foo"} 67 | (item "foo" "@color/foo") 68 | (item "color" "#f00") 69 | (item "bar" "12dp")))} 70 | 71 | when ["style AppTheme < Theme.AppCompat.Light.NoActionBar { 72 | android:windowBackground: @null; 73 | colorPrimary: @color/red_1; 74 | colorPrimaryDark: @android:color/black; 75 | foo123: @style/SpinnerItem; 76 | bar123qwe: @style/Foo.Bar.Baz123; 77 | }"] 78 | = {:default (resources 79 | (style {:name "AppTheme" :parent "Theme.AppCompat.Light.NoActionBar"} 80 | (item "android:windowBackground" "@null") 81 | (item "colorPrimary" "@color/red_1") 82 | (item "colorPrimaryDark" "@android:color/black") 83 | (item "foo123" "@style/SpinnerItem") 84 | (item "bar123qwe" "@style/Foo.Bar.Baz123")))} 85 | 86 | 87 | when ["style Button < BaseButton { 88 | android:textSize: 12dp; 89 | android:background: @drawable/btn_back; 90 | &:v21,v22 { 91 | android:background: @drawable/btn_ripple; 92 | } 93 | &:land { 94 | android:textSize: 10dp; 95 | } 96 | }"] 97 | = {:default (resources 98 | (style {:name "Button" :parent "BaseButton"} 99 | (item "android:textSize" "12dp") 100 | (item "android:background" "@drawable/btn_back"))) 101 | :v21 (resources 102 | (style {:name "Button" :parent "BaseButton"} 103 | (item "android:textSize" "12dp") 104 | (item "android:background" "@drawable/btn_ripple"))) 105 | :v22 (resources 106 | (style {:name "Button" :parent "BaseButton"} 107 | (item "android:textSize" "12dp") 108 | (item "android:background" "@drawable/btn_ripple"))) 109 | :land (resources 110 | (style {:name "Button" :parent "BaseButton"} 111 | (item "android:background" "@drawable/btn_back") 112 | (item "android:textSize" "10dp")))}) 113 | 114 | 115 | (expected-when "mixins override any style attrs set by the style" transpile-str 116 | when ["mixin mixinA { attr: 12dp; } 117 | mixin mixinB { 118 | attr: 14dp; 119 | &:v21 { attr: 16dp; } 120 | } 121 | style Foo { 122 | attr: 10dp; 123 | mixinA(); 124 | &:v21 { 125 | attr: 20dp; 126 | } 127 | mixinB(); 128 | }"] 129 | = {:default (resources 130 | (style {:name "Foo"} 131 | (item "attr" "14dp"))) 132 | :v21 (resources 133 | (style {:name "Foo"} 134 | (item "attr" "16dp")))}) 135 | 136 | (expected-when "mixin provide a form of including common style attributes" 137 | transpile-str 138 | when ["color main_color: #f0f0f0; 139 | color main_color_lighter: #f2f2f2; 140 | mixin mainBackgroundColored { 141 | android:background: main_color; 142 | &:v21 { 143 | android:background: main_color_lighter; 144 | } 145 | } 146 | style Foo { 147 | mainBackgroundColored(); 148 | }"] 149 | = {:default (resources 150 | (color "main_color" "#f0f0f0") 151 | (color "main_color_lighter" "#f2f2f2") 152 | (style {:name "Foo"} 153 | (item "android:background" "@color/main_color"))) 154 | :v21 (resources 155 | (style {:name "Foo"} 156 | (item "android:background" "@color/main_color_lighter")))}) 157 | 158 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE PUBLIC 2 | LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION OF THE PROGRAM 3 | CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT. 4 | 5 | 1. DEFINITIONS 6 | 7 | "Contribution" means: 8 | 9 | a) in the case of the initial Contributor, the initial code and 10 | documentation distributed under this Agreement, and 11 | 12 | b) in the case of each subsequent Contributor: 13 | 14 | i) changes to the Program, and 15 | 16 | ii) additions to the Program; 17 | 18 | where such changes and/or additions to the Program originate from and are 19 | distributed by that particular Contributor. A Contribution 'originates' from 20 | a Contributor if it was added to the Program by such Contributor itself or 21 | anyone acting on such Contributor's behalf. Contributions do not include 22 | additions to the Program which: (i) are separate modules of software 23 | distributed in conjunction with the Program under their own license 24 | agreement, and (ii) are not derivative works of the Program. 25 | 26 | "Contributor" means any person or entity that distributes the Program. 27 | 28 | "Licensed Patents" mean patent claims licensable by a Contributor which are 29 | necessarily infringed by the use or sale of its Contribution alone or when 30 | combined with the Program. 31 | 32 | "Program" means the Contributions distributed in accordance with this 33 | Agreement. 34 | 35 | "Recipient" means anyone who receives the Program under this Agreement, 36 | including all Contributors. 37 | 38 | 2. GRANT OF RIGHTS 39 | 40 | a) Subject to the terms of this Agreement, each Contributor hereby grants 41 | Recipient a non-exclusive, worldwide, royalty-free copyright license to 42 | reproduce, prepare derivative works of, publicly display, publicly perform, 43 | distribute and sublicense the Contribution of such Contributor, if any, and 44 | such derivative works, in source code and object code form. 45 | 46 | b) Subject to the terms of this Agreement, each Contributor hereby grants 47 | Recipient a non-exclusive, worldwide, royalty-free patent license under 48 | Licensed Patents to make, use, sell, offer to sell, import and otherwise 49 | transfer the Contribution of such Contributor, if any, in source code and 50 | object code form. This patent license shall apply to the combination of the 51 | Contribution and the Program if, at the time the Contribution is added by the 52 | Contributor, such addition of the Contribution causes such combination to be 53 | covered by the Licensed Patents. The patent license shall not apply to any 54 | other combinations which include the Contribution. No hardware per se is 55 | licensed hereunder. 56 | 57 | c) Recipient understands that although each Contributor grants the licenses 58 | to its Contributions set forth herein, no assurances are provided by any 59 | Contributor that the Program does not infringe the patent or other 60 | intellectual property rights of any other entity. Each Contributor disclaims 61 | any liability to Recipient for claims brought by any other entity based on 62 | infringement of intellectual property rights or otherwise. As a condition to 63 | exercising the rights and licenses granted hereunder, each Recipient hereby 64 | assumes sole responsibility to secure any other intellectual property rights 65 | needed, if any. For example, if a third party patent license is required to 66 | allow Recipient to distribute the Program, it is Recipient's responsibility 67 | to acquire that license before distributing the Program. 68 | 69 | d) Each Contributor represents that to its knowledge it has sufficient 70 | copyright rights in its Contribution, if any, to grant the copyright license 71 | set forth in this Agreement. 72 | 73 | 3. REQUIREMENTS 74 | 75 | A Contributor may choose to distribute the Program in object code form under 76 | its own license agreement, provided that: 77 | 78 | a) it complies with the terms and conditions of this Agreement; and 79 | 80 | b) its license agreement: 81 | 82 | i) effectively disclaims on behalf of all Contributors all warranties and 83 | conditions, express and implied, including warranties or conditions of title 84 | and non-infringement, and implied warranties or conditions of merchantability 85 | and fitness for a particular purpose; 86 | 87 | ii) effectively excludes on behalf of all Contributors all liability for 88 | damages, including direct, indirect, special, incidental and consequential 89 | damages, such as lost profits; 90 | 91 | iii) states that any provisions which differ from this Agreement are offered 92 | by that Contributor alone and not by any other party; and 93 | 94 | iv) states that source code for the Program is available from such 95 | Contributor, and informs licensees how to obtain it in a reasonable manner on 96 | or through a medium customarily used for software exchange. 97 | 98 | When the Program is made available in source code form: 99 | 100 | a) it must be made available under this Agreement; and 101 | 102 | b) a copy of this Agreement must be included with each copy of the Program. 103 | 104 | Contributors may not remove or alter any copyright notices contained within 105 | the Program. 106 | 107 | Each Contributor must identify itself as the originator of its Contribution, 108 | if any, in a manner that reasonably allows subsequent Recipients to identify 109 | the originator of the Contribution. 110 | 111 | 4. COMMERCIAL DISTRIBUTION 112 | 113 | Commercial distributors of software may accept certain responsibilities with 114 | respect to end users, business partners and the like. While this license is 115 | intended to facilitate the commercial use of the Program, the Contributor who 116 | includes the Program in a commercial product offering should do so in a 117 | manner which does not create potential liability for other Contributors. 118 | Therefore, if a Contributor includes the Program in a commercial product 119 | offering, such Contributor ("Commercial Contributor") hereby agrees to defend 120 | and indemnify every other Contributor ("Indemnified Contributor") against any 121 | losses, damages and costs (collectively "Losses") arising from claims, 122 | lawsuits and other legal actions brought by a third party against the 123 | Indemnified Contributor to the extent caused by the acts or omissions of such 124 | Commercial Contributor in connection with its distribution of the Program in 125 | a commercial product offering. The obligations in this section do not apply 126 | to any claims or Losses relating to any actual or alleged intellectual 127 | property infringement. In order to qualify, an Indemnified Contributor must: 128 | a) promptly notify the Commercial Contributor in writing of such claim, and 129 | b) allow the Commercial Contributor tocontrol, and cooperate with the 130 | Commercial Contributor in, the defense and any related settlement 131 | negotiations. The Indemnified Contributor may participate in any such claim 132 | at its own expense. 133 | 134 | For example, a Contributor might include the Program in a commercial product 135 | offering, Product X. That Contributor is then a Commercial Contributor. If 136 | that Commercial Contributor then makes performance claims, or offers 137 | warranties related to Product X, those performance claims and warranties are 138 | such Commercial Contributor's responsibility alone. Under this section, the 139 | Commercial Contributor would have to defend claims against the other 140 | Contributors related to those performance claims and warranties, and if a 141 | court requires any other Contributor to pay any damages as a result, the 142 | Commercial Contributor must pay those damages. 143 | 144 | 5. NO WARRANTY 145 | 146 | EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, THE PROGRAM IS PROVIDED ON 147 | AN "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER 148 | EXPRESS OR IMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR 149 | CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A 150 | PARTICULAR PURPOSE. Each Recipient is solely responsible for determining the 151 | appropriateness of using and distributing the Program and assumes all risks 152 | associated with its exercise of rights under this Agreement , including but 153 | not limited to the risks and costs of program errors, compliance with 154 | applicable laws, damage to or loss of data, programs or equipment, and 155 | unavailability or interruption of operations. 156 | 157 | 6. DISCLAIMER OF LIABILITY 158 | 159 | EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, NEITHER RECIPIENT NOR ANY 160 | CONTRIBUTORS SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, INCIDENTAL, 161 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING WITHOUT LIMITATION 162 | LOST PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 163 | CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 164 | ARISING IN ANY WAY OUT OF THE USE OR DISTRIBUTION OF THE PROGRAM OR THE 165 | EXERCISE OF ANY RIGHTS GRANTED HEREUNDER, EVEN IF ADVISED OF THE POSSIBILITY 166 | OF SUCH DAMAGES. 167 | 168 | 7. GENERAL 169 | 170 | If any provision of this Agreement is invalid or unenforceable under 171 | applicable law, it shall not affect the validity or enforceability of the 172 | remainder of the terms of this Agreement, and without further action by the 173 | parties hereto, such provision shall be reformed to the minimum extent 174 | necessary to make such provision valid and enforceable. 175 | 176 | If Recipient institutes patent litigation against any entity (including a 177 | cross-claim or counterclaim in a lawsuit) alleging that the Program itself 178 | (excluding combinations of the Program with other software or hardware) 179 | infringes such Recipient's patent(s), then such Recipient's rights granted 180 | under Section 2(b) shall terminate as of the date such litigation is filed. 181 | 182 | All Recipient's rights under this Agreement shall terminate if it fails to 183 | comply with any of the material terms or conditions of this Agreement and 184 | does not cure such failure in a reasonable period of time after becoming 185 | aware of such noncompliance. If all Recipient's rights under this Agreement 186 | terminate, Recipient agrees to cease use and distribution of the Program as 187 | soon as reasonably practicable. However, Recipient's obligations under this 188 | Agreement and any licenses granted by Recipient relating to the Program shall 189 | continue and survive. 190 | 191 | Everyone is permitted to copy and distribute copies of this Agreement, but in 192 | order to avoid inconsistency the Agreement is copyrighted and may only be 193 | modified in the following manner. The Agreement Steward reserves the right to 194 | publish new versions (including revisions) of this Agreement from time to 195 | time. No one other than the Agreement Steward has the right to modify this 196 | Agreement. The Eclipse Foundation is the initial Agreement Steward. The 197 | Eclipse Foundation may assign the responsibility to serve as the Agreement 198 | Steward to a suitable separate entity. Each new version of the Agreement will 199 | be given a distinguishing version number. The Program (including 200 | Contributions) may always be distributed subject to the version of the 201 | Agreement under which it was received. In addition, after a new version of 202 | the Agreement is published, Contributor may elect to distribute the Program 203 | (including its Contributions) under the new version. Except as expressly 204 | stated in Sections 2(a) and 2(b) above, Recipient receives no rights or 205 | licenses to the intellectual property of any Contributor under this 206 | Agreement, whether expressly, by implication, estoppel or otherwise. All 207 | rights in the Program not expressly granted under this Agreement are 208 | reserved. 209 | 210 | This Agreement is governed by the laws of the State of New York and the 211 | intellectual property laws of the United States of America. No party to this 212 | Agreement will bring a legal action under this Agreement more than one year 213 | after the cause of action arose. Each party waives its rights to a jury trial 214 | in any resulting litigation. 215 | --------------------------------------------------------------------------------