├── .gitignore ├── .gitmodules ├── LICENSE ├── README.md ├── TRADEOFFS.md ├── compiler ├── .gitignore ├── bsconfig.json ├── fixtures │ ├── asdfasdfsdf.ess │ └── simple.ess ├── package.json ├── scripts │ └── copy-pervasives.js └── src │ ├── Ast.re │ ├── Blah.re │ ├── Common.re │ ├── Compilation.re │ ├── Compiler.re │ ├── Eir.re │ ├── Index.re │ ├── Lexer.mll │ ├── Parser.mly │ ├── SheetParser.re │ ├── Typecheck.re │ ├── Utils.re │ └── Watchman.re ├── docs └── design.md ├── package.json └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | .bsb.lock 2 | .merlin 3 | .opam/ 4 | npm-debug.log 5 | yarn-error.log 6 | package-lock.json 7 | 8 | node_modules/ 9 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "compiler/bucklescript"] 2 | path = compiler/bucklescript 3 | url = git@github.com:rtsao/bucklescript.git 4 | branch = lib 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Ryan Tsao 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ESS: element style sheets 2 | 3 | > Note: this project is still a work in progress and many features are incomplete, missing, or broken. 4 | 5 | ESS is a language that compiles to typed, stateless JSX components and an associated static CSS bundle. 6 | 7 | ESS is designed to be a *replacement* for CSS-in-JS while providing an abstraction similar to [styled-components](https://github.com/styled-components/styled-components) et al. 8 | 9 | ## Motivation 10 | 11 | ### Performance overhead of CSS-in-JS 12 | 13 | Abstraction in JS comes at a cost ([prepack](https://github.com/facebook/prepack) notwithstanding), so CSS-in-JS can incur a significant performance penalty. Composition, a primary concern for UI styling, necessitates dead code and overhead in JS. To make matters worse, the JS tooling ecosystem doesn't provide an great way to address this overhead and solutions tend to be hacky or introduce unnatural limitations on JS. 14 | 15 | ### Subpar type safety of CSS-in-JS 16 | 17 | Object literals can be typed, but CSS involves lots of "stringly" typed values so even when using TypeScript or Flow, there isn't a great typing experience. 18 | 19 | ### Awkward semantics and syntax of CSS-in-JS 20 | 21 | Authoring styling code in JS tends to be rather verbose. 22 | 23 | ## Features 24 | 25 | ### Static types and pattern matching 26 | 27 | Pattern matching makes styling logic incredibly elegant. 28 | 29 | Additionally, the JSX components outputted by the ESS are typed. 30 | 31 | ### No cascade, selectors, or specificity 32 | 33 | ESS has no concept of selectors or specificity whatsoever. Instead, element styling is expressed as a pure function. 34 | 35 | ### No inheritance by default 36 | 37 | Elements will always be styled the same regardless of their location in DOM or existing CSS. In other words, element styles do not depend on their parent/ancestors. Instead, composition of styles must be explicit within ESS. 38 | 39 | ### Optimized, static CSS output 40 | 41 | There are no truly "dynamic" values in ESS, therefore a static CSS bundle for all possible styles can be produced. This also means RTL and vendor prefixing can be performed at build-time. Furthermore, ESS produces an optimized, atomic CSS stylesheet with no caveats. 42 | 43 | ## Trade-offs 44 | See [TRADEOFFS.md](https://github.com/rtsao/ess/blob/master/TRADEOFFS.md) 45 | 46 | ## Other motivation 47 | 48 | **Teaching myself** 49 | 50 | I am using ESS as a project to gain more experience with: 51 | - ReasonML and OCaml 52 | - [Language server protocol](https://github.com/Microsoft/language-server-protocol) (LSP) 53 | - Compiler design and implementation 54 | 55 | **Teaching others** 56 | 57 | I hope ESS can eventually serve as a useful reference implementation of a production-grade programming language built with modern tools, but for an extremely simple programming language so it is approachable. 58 | 59 | To that end, my goal is for ESS to have all the expected bells and whistles of a modern language including: 60 | - IDE integration via LSP 61 | - Automatic AST-based code formatting like [prettier](https://github.com/prettier/prettier) or refmt 62 | - Syntax highlighting via [tree-sitter](https://github.com/tree-sitter/tree-sitter) grammar 63 | - Fast, native build performance 64 | - Web-based REPL-like playground 65 | - Human-friendly compiler errors 66 | - Watch-compile mode with incremental compilation 67 | -------------------------------------------------------------------------------- /TRADEOFFS.md: -------------------------------------------------------------------------------- 1 | ## Creating a new language instead of extending CSS 2 | ### Upsides 3 | - Significant improvements over CSS syntax and semantics (type safety, pattern matching, etc.) 4 | - No uncanny valley 5 | ### Downsides 6 | - Users have to install yet another tool 7 | - Tooling has to be built and maintained 8 | - Docs have to be written and maintained 9 | - Not already well-established, will be something new to learn 10 | - Second-class citizenship in Chrome DevTools. Extra tooling, such as custom extension may be necessary to fill gaps. Clean mapping for source maps might be a challenge 11 | 12 | ## Multiple app runtime targets 13 | ### Upsides 14 | - Interoperability and portability 15 | ### Downsides 16 | - Targeting multiple app runtimes means adopting latest and greatest of platforms may be slower 17 | - May be a lot of work to maintain multiple compiler backends, unique optimizations may be required 18 | - Language semantics and abstractions may not be tenable if targets platforms diverge or change 19 | -------------------------------------------------------------------------------- /compiler/.gitignore: -------------------------------------------------------------------------------- 1 | /lib/ 2 | /src/Parser.mli 3 | /src/Parser.ml 4 | /src/Lexer.ml 5 | Parser.conflicts 6 | /fixtures/*.ess.js 7 | -------------------------------------------------------------------------------- /compiler/bsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ess", 3 | "refmt": 3, 4 | "bsc-flags": ["-bs-super-errors"], 5 | "ocamlfind-dependencies": [ 6 | "lwt", 7 | "lwt.unix", 8 | "lwt.ppx", 9 | "menhirlib", 10 | "visitors.ppx", 11 | "visitors.runtime", 12 | "ppx_deriving", 13 | "yojson", 14 | "ppx_deriving_yojson" 15 | ], 16 | "entries": [ 17 | { 18 | "backend": "native", 19 | "main-module": "Index" 20 | } 21 | ], 22 | "sources": [ 23 | { 24 | "dir": "src", 25 | "generators": [ 26 | { 27 | "name": "ocamllex", 28 | "edge": ["Lexer.ml", ":", "Lexer.mll"] 29 | }, 30 | { 31 | "name": "menhir", 32 | "edge": ["Parser.ml", "Parser.mli", ":", "Parser.mly"] 33 | } 34 | ] 35 | } 36 | ], 37 | "generators": [ 38 | { 39 | "name": "ocamllex", 40 | "command": "ocamllex $in" 41 | }, 42 | { 43 | "name": "menhir", 44 | "command": "menhir --explain --table $in" 45 | } 46 | ] 47 | } 48 | -------------------------------------------------------------------------------- /compiler/fixtures/asdfasdfsdf.ess: -------------------------------------------------------------------------------- 1 | Baz100 ( 2 | @foo: boolean 3 | @notcool: "evenmore" | "lame" 4 | ) [ 5 | Other1 6 | , 7 | @foo { 8 | true => [Other2,Other3], 9 | false => [ 10 | Other4 11 | Other5 12 | ], 13 | (true, false) => [Other6], 14 | }, 15 | Other7, 16 | ] 17 | 18 | Baz [ 19 | blah asdf 20 | foo (@foo { 21 | true => @foo { 22 | true => green 23 | } 24 | }) 25 | Other8, 26 | arbitrary ( 27 | hello: world, 28 | hello2: world2, 29 | hello3: world3 30 | 31 | ) 32 | asdf ( 33 | 34 | 35 | ) 36 | ] 37 | 38 | Bar [ 39 | background @foo { 40 | true => red 41 | } 42 | ] 43 | -------------------------------------------------------------------------------- /compiler/fixtures/simple.ess: -------------------------------------------------------------------------------- 1 | Button ( 2 | @foo: boolean 3 | ) [ 4 | @foo { 5 | true => [color red], 6 | false => [ 7 | color blue 8 | color green 9 | ], 10 | }, 11 | ] 12 | -------------------------------------------------------------------------------- /compiler/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@ess/compiler", 3 | "version": "0.0.0", 4 | "scripts": { 5 | "prepare": "bsb && node scripts/copy-pervasives.js", 6 | "build": "bsb", 7 | "watch": "bsb -w", 8 | "clean": "bsb -clean-world", 9 | "start": "./lib/bs/native/index.native" 10 | }, 11 | "devDependencies": { 12 | "bsb-native": "4.0.6", 13 | "resolve-pkg": "^1.0.0" 14 | }, 15 | "license": "MIT" 16 | } 17 | -------------------------------------------------------------------------------- /compiler/scripts/copy-pervasives.js: -------------------------------------------------------------------------------- 1 | const resolvePkg = require('resolve-pkg'); 2 | const fs = require('fs'); 3 | const path = require('path'); 4 | 5 | const bsPath = resolvePkg('bsb-native', {cwd: __dirname}); 6 | 7 | const files = ['cmi', 'cmj', 'cmt', 'cmti', 'ml', 'mli'].map( 8 | ext => `pervasives.${ext}`, 9 | ); 10 | 11 | const dest = path.resolve(__dirname, '../lib/bs/native/ocaml'); 12 | 13 | fs.mkdir(dest, err => { 14 | if (err) throw err; 15 | files.forEach(file => { 16 | fs.copyFile( 17 | path.join(bsPath, 'lib/ocaml', file), 18 | path.join(dest, file), 19 | err => { 20 | if (err) throw err; 21 | }, 22 | ); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /compiler/src/Ast.re: -------------------------------------------------------------------------------- 1 | type loc = (Lexing.position, Lexing.position); 2 | 3 | [@deriving visitors({variety: "iter"})] 4 | type stylesheet = 5 | | Stylesheet(list(statement)) 6 | and statement = 7 | | ElementDeclaration([@opaque] loc, string, list(parameter), block) 8 | | VariableDeclaration([@opaque] loc) 9 | and block = 10 | | Block([@opaque] loc, list(rule)) 11 | and rule = 12 | | AttributeRule([@opaque] loc, string, expression) 13 | | CompositionRule([@opaque] loc, string) 14 | | MatchRule([@opaque] loc, list(parameter), list(match_rule_clause)) 15 | and expression = 16 | | MatchExpression( 17 | [@opaque] loc, 18 | list(parameter), 19 | list(match_expression_clause), 20 | ) 21 | | RecordExpression([@opaque] loc, list(record_field)) 22 | | LiteralExpression([@opaque] loc) 23 | | UnknownExpression([@opaque] loc) 24 | and record_field = 25 | | RecordField([@opaque] loc, string, literal) 26 | /* need to type check named expression assignment. Must only be primitive value */ 27 | and literal = 28 | | ReferenceLiteral([@opaque] loc) 29 | | TextLiteral([@opaque] loc) 30 | | HexLiteral([@opaque] loc) 31 | | PixelLiteral([@opaque] loc) 32 | | PercentLiteral([@opaque] loc) 33 | | NumberLiteral([@opaque] loc) 34 | and match_rule_clause = 35 | | MatchRuleClause([@opaque] loc, list(pattern), block) 36 | and match_expression_clause = 37 | | MatchExpressionClause([@opaque] loc, list(pattern), expression) 38 | and prop_value_type = 39 | | StringEnumType([@opaque] loc, list(string)) 40 | | BooleanType([@opaque] loc) 41 | | NumberType([@opaque] loc) 42 | and parameter = 43 | | Parameter([@opaque] loc, string) 44 | and pattern = 45 | | NumberPattern([@opaque] loc, int) 46 | | NumberRangePattern([@opaque] loc, (int, option(int))) 47 | | BooleanPattern([@opaque] loc, bool) 48 | | StringPattern([@opaque] loc, list(string)) 49 | | FallthroughPattern([@opaque] loc); 50 | -------------------------------------------------------------------------------- /compiler/src/Blah.re: -------------------------------------------------------------------------------- 1 | let str = {| 2 | Baz100 ( 3 | @foo: boolean 4 | @notcool: "evenmore" | "lame" 5 | ) [ 6 | Other1 7 | , 8 | @foo { 9 | true => [Other2,Other3], 10 | false => [ 11 | Other4 12 | Other5 13 | ], 14 | (true, false) => [Other6], 15 | }, 16 | Other7, 17 | ] 18 | 19 | Baz [ 20 | blah asdf 21 | foo (@foo { 22 | true => @foo { 23 | true => green 24 | } 25 | }) 26 | Other8, 27 | arbitrary ( 28 | hello: world, 29 | hello2: world2, 30 | hello3: world3 31 | 32 | ) 33 | asdf ( 34 | 35 | 36 | ) 37 | ] 38 | 39 | Bar [ 40 | background @foo { 41 | true => red 42 | } 43 | ] 44 | 45 | |}; 46 | 47 | let result = SheetParser.process(str); 48 | 49 | Compiler.maybe_log_error(result); 50 | 51 | switch (result) { 52 | | SheetParser.Success(parsed) => 53 | /* let yo = Typecheck.typecheck(parsed); */ 54 | let thang = Compilation.resolve(parsed); 55 | print_endline(string_of_int(Compilation.SymbolTable.length(thang.table))); 56 | /* print_endline(string_of_int(yo)); */ 57 | print_endline("dope!"); 58 | | _ => () 59 | }; 60 | -------------------------------------------------------------------------------- /compiler/src/Common.re: -------------------------------------------------------------------------------- 1 | module StrHash = 2 | Hashtbl.Make({ 3 | type t = string; 4 | let hash = Hashtbl.hash; 5 | let equal = (==); 6 | }); 7 | 8 | module StrSet = 9 | Set.Make({ 10 | type t = string; 11 | let compare = Pervasives.compare; 12 | }); 13 | -------------------------------------------------------------------------------- /compiler/src/Compilation.re: -------------------------------------------------------------------------------- 1 | type reference_kind = 2 | | Element; 3 | type symbol_key = { 4 | name: string, 5 | kind: reference_kind, 6 | }; 7 | 8 | module SymbolTable = { 9 | module H = 10 | Hashtbl.Make({ 11 | type t = symbol_key; 12 | let hash = Hashtbl.hash; 13 | let equal = (==); 14 | }); 15 | include H; 16 | }; 17 | 18 | type traverse_state = {table: SymbolTable.t(Eir.node)}; 19 | 20 | let resolve = (e: Ast.stylesheet) : traverse_state => { 21 | let state = {table: SymbolTable.create(1000)}; 22 | let v = { 23 | as _; 24 | inherit class Ast.iter(_) as super; 25 | pub! visit_ElementDeclaration = (env, loc, name, param_list, block) => { 26 | print_endline("an element:"); 27 | print_endline(name); 28 | let interface = Eir.SM.singleton("foo", Eir.NumberType); 29 | 30 | List.iter( 31 | param => { 32 | let param_name = 33 | switch (param) { 34 | | Ast.Parameter(_, x) => x 35 | }; 36 | Eir.SM.add(param_name, Eir.NumberType, interface); 37 | (); 38 | }, 39 | param_list, 40 | ); 41 | 42 | SymbolTable.add( 43 | env.table, 44 | {name, kind: Element}, 45 | Eir.Element({interface: interface}), 46 | ); 47 | 48 | super#visit_ElementDeclaration(env, loc, name, param_list, block); 49 | } 50 | }; 51 | v#visit_stylesheet(state, e); 52 | state; 53 | }; 54 | -------------------------------------------------------------------------------- /compiler/src/Compiler.re: -------------------------------------------------------------------------------- 1 | let maybe_log_error = result => 2 | switch (result) { 3 | | SheetParser.Success(ast) => print_endline("success") 4 | | SheetParser.Failure(state, pos) => 5 | let msg = "FAKE"; 6 | /* let msg = ParseErrors.message(state); */ 7 | let line = pos.pos_lnum; 8 | let col = pos.pos_cnum - pos.pos_bol + 1; 9 | print_endline( 10 | "Error: " 11 | ++ msg 12 | ++ " at " 13 | ++ string_of_int(line) 14 | ++ ":" 15 | ++ string_of_int(col), 16 | ); 17 | | SheetParser.UnknownError(msg) => print_endline("Error: " ++ msg) 18 | }; 19 | -------------------------------------------------------------------------------- /compiler/src/Eir.re: -------------------------------------------------------------------------------- 1 | /* ESS Intermediate Representation */ 2 | module SS = Set.Make(String); 3 | module SM = Map.Make(String); 4 | 5 | type param_type = 6 | | StringEnumType(SS.t) 7 | | NumberType 8 | | BooleanType 9 | and element_body = {interface: SM.t(param_type)} 10 | and node = 11 | | Element(element_body) 12 | and attribute = 13 | | BorderAttribute 14 | | PaddingAttribute 15 | | MarginAttribute 16 | | BackgroundAttribute 17 | and color = 18 | | RGBA(int, int, int, int) 19 | and length = 20 | | Px(float) 21 | and margin = { 22 | top: margin_value, 23 | right: margin_value, 24 | bottom: margin_value, 25 | left: margin_value, 26 | } 27 | and margin_value = 28 | | Margin(length) 29 | and border = { 30 | top: border_value, 31 | right: border_value, 32 | bottom: border_value, 33 | left: border_value, 34 | } 35 | and border_value = 36 | | Border(border_pattern, length, color) 37 | /* https://www.w3.org/TR/CSS2/tables.html#border-conflict-resolution */ 38 | | NoBorder 39 | | HiddenBorder 40 | and border_pattern = 41 | | DashedBorder 42 | | SolidBorder; 43 | -------------------------------------------------------------------------------- /compiler/src/Index.re: -------------------------------------------------------------------------------- 1 | open SheetParser; 2 | open Compilation; 3 | 4 | let read_file = filename => 5 | Lwt_io.with_file(~mode=Lwt_io.Input, filename, Lwt_io.read); 6 | let write_file = (filename, contents) => 7 | Lwt_io.with_file(~mode=Lwt_io.Output, filename, ch => 8 | Lwt_io.write(ch, contents) 9 | ); 10 | 11 | let print_files = (files: list(string), dir: string) => { 12 | let%lwt sources = Lwt_list.map_s(read_file, files); 13 | let results = List.map(str => SheetParser.process(str), sources); 14 | List.iter( 15 | result => 16 | switch (result) { 17 | | SheetParser.Success(parsed) => 18 | /* let yo = Typecheck.typecheck(parsed); */ 19 | let resolved = Compilation.resolve(parsed); 20 | print_endline( 21 | string_of_int(Compilation.SymbolTable.length(resolved.table)), 22 | ); 23 | /* print_endline(string_of_int(yo)); */ 24 | /* print_endline("dope!"); */ 25 | | _ => () 26 | }, 27 | results, 28 | ); 29 | 30 | Lwt_list.map_s( 31 | file => write_file(file ++ ".js", "// the js implementation\n"), 32 | files, 33 | ); 34 | }; 35 | 36 | let foo = Watchman.run_query(print_files); 37 | -------------------------------------------------------------------------------- /compiler/src/Lexer.mll: -------------------------------------------------------------------------------- 1 | { 2 | open Parser 3 | 4 | exception Error of string 5 | 6 | let get = Lexing.lexeme 7 | 8 | let hex_to_int hex_char1 hex_char2 = int_of_string ("0x" ^ (String.make 1 hex_char1) ^ (String.make 1 hex_char2)) 9 | let hex2_to_int hex_char = int_of_string ("0x" ^ (String.make 2 hex_char)) 10 | 11 | let int_of_maybe_string str = 12 | match str with 13 | | "" -> None 14 | | _ -> Some (int_of_string str) 15 | } 16 | 17 | let digit = ['0'-'9'] 18 | 19 | let hex = ['A'-'F' 'a'-'f' '0'-'9'] 20 | 21 | let alpha = ['A'-'Z' '-' 'a'-'z'] 22 | 23 | let id_chars = ['A'-'Z' '_' 'a'-'z' '0' - '9'] 24 | let attr_chars = ['a'-'z' '-'] 25 | 26 | rule token = parse 27 | | '\n' { Lexing.new_line lexbuf; NEWLINE } 28 | | '_' { UNDERSCORE } 29 | | '=' '>' { ARROW } 30 | | '=' { EQ } 31 | | ':' { COLON } 32 | | ',' { COMMA } 33 | | '{' { LBRACE } 34 | | '}' { RBRACE } 35 | | '[' { LBRACKET } 36 | | ']' { RBRACKET } 37 | | '(' { LPAREN } 38 | | ')' { RPAREN } 39 | | '|' { PIPE } 40 | | "VARDEC" { VARDEC } 41 | | "/*" { comment lexbuf; token lexbuf } 42 | | [' ' '\t'] { token lexbuf } 43 | | '@' (['a'-'z']+ as id) { PROP (id)} 44 | | "boolean" { BOOLEAN } 45 | | "false" { FALSE } 46 | | "true" { TRUE } 47 | | '"' (['a'-'z']+ as str) '"' { STRING(str) } 48 | | ['A'-'Z']+ id_chars* { ELEMENT_ID(get lexbuf) } 49 | | attr_chars+ { IDENTIFIER(get lexbuf) } 50 | | ['0'-'9']+ "px" { PIXEL } 51 | | (['0'-'9']+ as start_num)'.' '.' (['0'-'9']* as end_num) 52 | { RANGE(int_of_string start_num, int_of_maybe_string end_num) } 53 | | '#' (hex as r) (hex as g) (hex as b) 54 | { COLOR_SHORTHEX(hex2_to_int r, hex2_to_int g, hex2_to_int b) } 55 | | '#' (hex as r1) (hex as r2) (hex as g1) (hex as g2) (hex as b1) (hex as b2) 56 | { COLOR_HEX(hex_to_int r1 r2, hex_to_int g1 g2, hex_to_int b1 b2) } 57 | | _ { token lexbuf } 58 | | eof { EOF } 59 | 60 | and comment = parse 61 | | "/*" { comment lexbuf; comment lexbuf } 62 | | "*/" { () } 63 | | _ { comment lexbuf } 64 | | eof { failwith "lexer: comment not terminated" } 65 | -------------------------------------------------------------------------------- /compiler/src/Parser.mly: -------------------------------------------------------------------------------- 1 | %token LBRACE 2 | %token RBRACE 3 | %token VARDEC 4 | %token LPAREN 5 | %token RPAREN 6 | %token LBRACKET 7 | %token RBRACKET 8 | %token COMMA 9 | %token COLON 10 | %token PIPE 11 | %token BOOLEAN 12 | %token TRUE FALSE 13 | %token NEWLINE 14 | %token UNDERSCORE 15 | 16 | %token EQ 17 | %token RANGE 18 | %token PIXEL 19 | %token COLOR_SHORTHEX 20 | %token COLOR_HEX 21 | %token ARROW 22 | %token IDENTIFIER 23 | %token ELEMENT_ID 24 | %token PROP 25 | %token STRING 26 | 27 | %token EOF 28 | 29 | %start input 30 | %type input 31 | 32 | %% 33 | 34 | input: 35 | | NEWLINE* s = program EOF { Ast.Stylesheet s } 36 | 37 | program: 38 | | sl = statement_list? { Utils.list_maybe sl } 39 | 40 | statement_list: 41 | | s = _statement NEWLINE* { [s] } 42 | | s = _statement NEWLINE+ sl = statement_list { s :: sl } 43 | 44 | _statement: 45 | | e = element { e } 46 | 47 | element: 48 | | id = ELEMENT_ID ep = element_params_specifier? b = block 49 | { Ast.ElementDeclaration(($startpos, $endpos), id, Utils.list_maybe(ep), b) } 50 | 51 | element_params_specifier: 52 | | LPAREN NEWLINE* RPAREN 53 | { [] } 54 | | LPAREN e = element_params RPAREN 55 | { e } 56 | | LPAREN NEWLINE+ e = element_params RPAREN 57 | { e } 58 | 59 | element_params: 60 | | f = element_param { [f] } 61 | | f = element_param c = element_params { f :: c } 62 | 63 | element_param: 64 | | r = element_param_entry NEWLINE* { r } 65 | | r = element_param_entry COMMA NEWLINE* { r } 66 | | r = element_param_entry NEWLINE+ COMMA NEWLINE* { r } 67 | 68 | element_param_entry: 69 | | p = parameter NEWLINE* COLON NEWLINE* parameter_typedef 70 | { p } 71 | 72 | block: 73 | | LBRACKET NEWLINE* br = block_rules? RBRACKET 74 | { Ast.Block(($startpos, $endpos), Utils.list_maybe(br)) } 75 | 76 | block_rules: 77 | | e = block_entry { [e] } 78 | | e = block_entry b = block_rules { e :: b } 79 | 80 | block_entry: 81 | | r = rule NEWLINE* { r } 82 | | r = rule COMMA NEWLINE* { r } 83 | | r = rule NEWLINE+ COMMA NEWLINE* { r } 84 | 85 | rule: 86 | | r = attribute_rule { r } 87 | | r = composition_rule { r } 88 | | r = match_rule { r } 89 | 90 | attribute_rule: 91 | | attr = IDENTIFIER exp = expression { Ast.AttributeRule(($startpos, $endpos), attr, exp) } 92 | 93 | composition_rule: 94 | | id = ELEMENT_ID { Ast.CompositionRule(($startpos, $endpos), id) } 95 | 96 | match_rule: 97 | | parameter_list LBRACE NEWLINE* RBRACE 98 | { Ast.MatchRule(($startpos, $endpos), $1, []) } 99 | | parameter_list LBRACE NEWLINE+ match_rule_body RBRACE 100 | { Ast.MatchRule(($startpos, $endpos), $1, $4) } 101 | 102 | match_rule_body: 103 | | match_rule_clause NEWLINE* { [$1] } 104 | | match_rule_clause NEWLINE+ match_rule_body { $1 :: $3 } 105 | 106 | match_rule_clause: 107 | | p = pattern_def ARROW b = block COMMA? 108 | { Ast.MatchRuleClause(($startpos, $endpos), p, b) } 109 | 110 | expression: 111 | | e = plain_expression { e } 112 | | e = match_expression { e } 113 | | e = record_expression { e } 114 | | LPAREN e = expression RPAREN { e } 115 | 116 | plain_expression: 117 | | IDENTIFIER { Ast.LiteralExpression(($startpos, $endpos)) } 118 | 119 | record_expression: 120 | | LPAREN e = record_entries? RPAREN 121 | { Ast.RecordExpression(($startpos, $endpos), Utils.list_maybe(e)) } 122 | | LPAREN NEWLINE+ e = record_entries RPAREN 123 | { Ast.RecordExpression(($startpos, $endpos), e) } 124 | | LPAREN NEWLINE+ RPAREN 125 | { Ast.RecordExpression(($startpos, $endpos), []) } 126 | 127 | record_entries: 128 | | f = record_entry { [f] } 129 | | f = record_entry c = record_entries { f :: c } 130 | 131 | record_entry: 132 | | r = record_field NEWLINE* { r } 133 | | r = record_field COMMA NEWLINE* { r } 134 | | r = record_field NEWLINE+ COMMA NEWLINE* { r } 135 | 136 | record_field: 137 | | f = IDENTIFIER NEWLINE* COLON NEWLINE* v = IDENTIFIER 138 | { Ast.RecordField(($startpos, $endpos), f, Ast.TextLiteral($startpos, $endpos))} 139 | 140 | match_expression: 141 | | parameter_list LBRACE NEWLINE* RBRACE 142 | { Ast.MatchExpression(($startpos, $endpos), $1, []) } 143 | | parameter_list LBRACE NEWLINE+ match_expression_body RBRACE 144 | { Ast.MatchExpression(($startpos, $endpos), $1, $4) } 145 | 146 | match_expression_body: 147 | | match_expression_clause NEWLINE* { [$1] } 148 | | match_expression_clause NEWLINE+ match_expression_body { $1 :: $3 } 149 | 150 | match_expression_clause: 151 | | pattern_def ARROW expression 152 | { Ast.MatchExpressionClause(($startpos, $endpos), $1, $3) } 153 | 154 | parameter_list: 155 | | parameter { [$1] } 156 | | delimited(LPAREN, separated_nonempty_list(COMMA, parameter), RPAREN) { $1 } 157 | 158 | parameter: 159 | | p = PROP { Ast.Parameter(($startpos, $endpos), p) } 160 | 161 | pattern_def: 162 | | p = pattern { [p] } 163 | | p = delimited(LPAREN, separated_nonempty_list(COMMA, pattern), RPAREN) { p } 164 | 165 | pattern: 166 | | ls = separated_nonempty_list(PIPE, STRING) 167 | { Ast.StringPattern(($startpos, $endpos), ls) } 168 | | TRUE 169 | { Ast.BooleanPattern(($startpos, $endpos), true) } 170 | | FALSE 171 | { Ast.BooleanPattern(($startpos, $endpos), false) } 172 | | RANGE 173 | { Ast.NumberRangePattern(($startpos, $endpos), $1) } 174 | | UNDERSCORE 175 | { Ast.FallthroughPattern(($startpos, $endpos)) } 176 | 177 | parameter_typedef: 178 | | ls = separated_nonempty_list(PIPE, STRING) 179 | { Ast.StringPattern(($startpos, $endpos), ls) } 180 | | BOOLEAN 181 | { Ast.FallthroughPattern(($startpos, $endpos)) } 182 | 183 | %% 184 | -------------------------------------------------------------------------------- /compiler/src/SheetParser.re: -------------------------------------------------------------------------------- 1 | module I = Parser.MenhirInterpreter; 2 | 3 | type parseResult = 4 | | Success(Ast.stylesheet) 5 | | Failure(int, Lexing.position) 6 | | UnknownError(string); 7 | 8 | let succeed = (sheet: Ast.stylesheet) => Success(sheet); 9 | 10 | let stack = checkpoint => 11 | switch (checkpoint) { 12 | | I.HandlingError(env) => I.stack(env) 13 | | _ => assert(false) 14 | }; 15 | 16 | let state = checkpoint : int => { 17 | let result = Lazy.force(stack(checkpoint)); 18 | switch (result) { 19 | | MenhirLib.General.Nil => 0 20 | | MenhirLib.General.Cons(I.Element(s, _, _, _), _) => I.number(s) 21 | }; 22 | }; 23 | 24 | let fail = (lexbuf, checkpoint: I.checkpoint(Ast.stylesheet)) => { 25 | let pos = Lexing.lexeme_start_p(lexbuf); 26 | Failure(state(checkpoint), pos); 27 | }; 28 | 29 | let loop = (lexbuf, result) => { 30 | let supplier = I.lexer_lexbuf_to_supplier(Lexer.token, lexbuf); 31 | I.loop_handle(succeed, fail(lexbuf), supplier, result); 32 | }; 33 | 34 | let process = (token: string) => { 35 | let lexbuf = Lexing.from_string(token); 36 | try (loop(lexbuf, Parser.Incremental.input(lexbuf.lex_curr_p))) { 37 | | Lexer.Error(msg) => UnknownError(msg) 38 | }; 39 | }; 40 | -------------------------------------------------------------------------------- /compiler/src/Typecheck.re: -------------------------------------------------------------------------------- 1 | type numy = 2 | | Foo 3 | | Bar 4 | | Baz; 5 | 6 | type thingy = {asdf: numy}; 7 | 8 | let typecheck = (e: Ast.stylesheet) : int => { 9 | let v = { 10 | as _; 11 | val mutable count = 0; 12 | pub count = count; 13 | inherit class Ast.iter(_) as super; 14 | pub! visit_AttributeRule = (env, e0, e1) => { 15 | count = count + 1; 16 | print_endline(e1); 17 | let yolo = 18 | switch (e1) { 19 | | "foo" => Baz 20 | | "blah" => Bar 21 | | _ => Foo 22 | }; 23 | 24 | super#visit_AttributeRule({asdf: yolo}, e0, e1); 25 | }; 26 | pub! visit_expression = (env, x) => { 27 | /* pattern match against value types, based on env */ 28 | switch (env.asdf) { 29 | | Foo => print_endline("expression: Foo") 30 | | Bar => print_endline("expression: Bar") 31 | | Baz => print_endline("expression: Baz") 32 | }; 33 | super#visit_expression(env, x); 34 | } 35 | }; 36 | v#visit_stylesheet({asdf: Foo}, e); 37 | v#count; 38 | }; 39 | -------------------------------------------------------------------------------- /compiler/src/Utils.re: -------------------------------------------------------------------------------- 1 | let list_maybe = l => 2 | switch (l) { 3 | | Some(l) => l 4 | | None => [] 5 | }; 6 | -------------------------------------------------------------------------------- /compiler/src/Watchman.re: -------------------------------------------------------------------------------- 1 | type io = { 2 | input: Lwt_io.channel(Lwt_io.input), 3 | output: Lwt_io.channel(Lwt_io.output), 4 | }; 5 | 6 | [@deriving yojson] 7 | type sockname_response = { 8 | version: string, 9 | sockname: string, 10 | } 11 | and watch_project_response = { 12 | watcher: string, 13 | watch: string, 14 | relative_path: [@default ""] string, 15 | version: string, 16 | } 17 | and query_response = { 18 | files: list(string), 19 | clock: string, 20 | is_fresh_instance: bool, 21 | version: string, 22 | } 23 | and subscribe_response = { 24 | clock: string, 25 | subscribe: string, 26 | version: string, 27 | } 28 | and subscription_message = { 29 | unilateral: bool, 30 | subscription: string, 31 | root: string, 32 | files: list(string), 33 | version: string, 34 | since: [@default ""] string, 35 | clock: string, 36 | is_fresh_instance: bool, 37 | } 38 | and expression = 39 | | [@name "match"] Match(string) 40 | | [@name "allof"] Allof2(expression, expression) 41 | | [@name "allof"] Allof3(expression, expression, expression) 42 | and command = 43 | | [@name "query"] Query(string, query_schema) 44 | | [@name "watch-project"] WatchProject(string) 45 | | [@name "subscribe"] Subscribe(string, string, subscribe_schema) 46 | and response = 47 | | QueryResponse(query_response) 48 | | WatchProjectResponse(watch_project_response) 49 | | SubscribeResponse(subscribe_response) 50 | and subscribe_schema = { 51 | expression, 52 | path: list(string), 53 | fields: list(string), 54 | } 55 | and query_schema = { 56 | case_sensitive: bool, 57 | path: list(string), 58 | fields: list(string), 59 | expression, 60 | }; 61 | 62 | let result = a => 63 | switch (a) { 64 | | Result.Ok(b) => b 65 | }; 66 | 67 | let sock_addr_of_json = json_str => { 68 | let sockname = 69 | result(sockname_response_of_yojson(Yojson.Safe.from_string(json_str))). 70 | sockname; 71 | Unix.ADDR_UNIX(sockname); 72 | }; 73 | 74 | let socket_of_addr = sockaddr => { 75 | let socket = Lwt_unix.socket(Unix.PF_UNIX, Unix.SOCK_STREAM, 0); 76 | Lwt_unix.connect(socket, sockaddr); 77 | socket; 78 | }; 79 | 80 | let subscription_message_of_str = line => 81 | result(subscription_message_of_yojson(Yojson.Safe.from_string(line))); 82 | 83 | let print_files = (files: list(string)) => 84 | List.iter( 85 | file => { 86 | Lwt_io.printf("File: %s\n", file); 87 | (); 88 | }, 89 | files, 90 | ); 91 | 92 | let rec handle_changed_files = (stream, dir, handler) : Lwt.t(_) => { 93 | let%lwt line = Lwt_stream.last_new(stream); 94 | let res = subscription_message_of_str(line); 95 | let changed = List.map(file => Filename.concat(res.root, file), res.files); 96 | handler(changed, dir); 97 | handle_changed_files(stream, dir, handler); 98 | }; 99 | 100 | let read_line = ch => { 101 | let%lwt line = Lwt_io.read_line(ch); 102 | let json = Yojson.Safe.from_string(line); 103 | Lwt.return(json); 104 | }; 105 | 106 | let command_to_string = (cmd: command) => 107 | Yojson.Safe.to_string(command_to_yojson(cmd)); 108 | 109 | let send_cmd = ({input, output}, cmd: command) => { 110 | Lwt_io.write_line(output, command_to_string(cmd)); 111 | let%lwt line = read_line(input); 112 | Lwt.return(line); 113 | }; 114 | 115 | let send_watch_project = (io, dir) => { 116 | let%lwt res = send_cmd(io, WatchProject(dir)); 117 | Lwt.return(result(watch_project_response_of_yojson(res))); 118 | }; 119 | 120 | let send_subscribe = (io, root, name, args) => { 121 | let%lwt res = send_cmd(io, Subscribe(root, name, args)); 122 | Lwt.return(result(subscribe_response_of_yojson(res))); 123 | }; 124 | 125 | let send_query = (io, root, args) => { 126 | let%lwt res = send_cmd(io, Query(root, args)); 127 | Lwt.return(result(query_response_of_yojson(res))); 128 | }; 129 | 130 | let get_io = () => { 131 | let%lwt addr = 132 | Lwt_process.pread(("", [|"watchman", "get-sockname", "--no-pretty"|])); 133 | let sockaddr = sock_addr_of_json(addr); 134 | let socket = socket_of_addr(sockaddr); 135 | let input = Lwt_io.of_fd(~mode=Lwt_io.input, socket); 136 | let output = Lwt_io.of_fd(~mode=Lwt_io.output, socket); 137 | Lwt.return({input, output}); 138 | }; 139 | 140 | let startup_watchman = dir => { 141 | let%lwt io = get_io(); 142 | let%lwt watch_res = send_watch_project(io, dir); 143 | Lwt.return((io, watch_res)); 144 | }; 145 | 146 | let query = (dir, handler) => { 147 | let%lwt (io, watch_res) = startup_watchman(dir); 148 | let%lwt query_res = 149 | send_query( 150 | io, 151 | watch_res.watch, 152 | { 153 | path: [watch_res.relative_path], 154 | case_sensitive: false, 155 | expression: Match("*.ess"), 156 | fields: ["name"], 157 | }, 158 | ); 159 | 160 | let files = 161 | List.map( 162 | file => Filename.concat(watch_res.watch, file), 163 | query_res.files, 164 | ); 165 | 166 | handler(files, dir); 167 | }; 168 | 169 | let watch = (dir, handler) => { 170 | let%lwt (io, watch_res) = startup_watchman(dir); 171 | let%lwt sub_res = 172 | send_subscribe( 173 | io, 174 | watch_res.watch, 175 | "mysubscriptionname", 176 | { 177 | path: [watch_res.relative_path], 178 | expression: Match("*.ess"), 179 | fields: ["name"], 180 | }, 181 | ); 182 | 183 | Lwt_io.printf("clock: %s\n", sub_res.clock); 184 | let stream = Lwt_io.read_lines(io.input); 185 | let avail = Lwt_stream.get_available(stream); 186 | List.iter( 187 | line => { 188 | let res = subscription_message_of_str(line); 189 | let files = 190 | List.map(file => Filename.concat(res.root, file), res.files); 191 | Lwt_io.printf("starting files:\n"); 192 | handler(files, dir); 193 | }, 194 | avail, 195 | ); 196 | handle_changed_files(stream, dir, handler); 197 | }; 198 | 199 | let run_watch = handler => Lwt_main.run(watch(Sys.getcwd(), handler)); 200 | let run_query = handler => Lwt_main.run(query(Sys.getcwd(), handler)); 201 | -------------------------------------------------------------------------------- /docs/design.md: -------------------------------------------------------------------------------- 1 | 2 | ## Basic example 3 | 4 | ```elm 5 | Foo [ 6 | @size "small" | "medium" | "large" 7 | 8 | color red 9 | border (x: (color: red, width: 2)) 10 | shadow @size { 11 | "small" => (offset: 15, size: 15, color: red) 12 | "medium" => (offset: 25, size: 25, color: red) 13 | "large" => (offset: 35, size: 35, color: red) 14 | } 15 | ] 16 | ``` 17 | 18 | ## Composition 19 | 20 | ```elm 21 | Foo [ 22 | color red 23 | ] 24 | 25 | Bar [ 26 | include Foo 27 | 28 | background green 29 | ] 30 | ``` 31 | 32 | ## Pattern matching 33 | 34 | #### Value expressions 35 | ```elm 36 | Foo [ 37 | @prop boolean 38 | 39 | color @prop { 40 | true => red 41 | false => blue 42 | } 43 | ] 44 | ``` 45 | 46 | #### Block expressions 47 | ```elm 48 | Foo [ 49 | @prop boolean 50 | 51 | @prop { 52 | true => [ 53 | color white 54 | background red 55 | ] 56 | false => background blue 57 | } 58 | ] 59 | ``` 60 | 61 | ### Exhaustiveness 62 | ```elm 63 | Foo [ 64 | @a bool 65 | @b number 66 | @c "x" | "y" | "z" 67 | 68 | color @a { 69 | true => red 70 | false => blue 71 | } 72 | color @b { 73 | 1..10 => red 74 | 10..50 => blue 75 | _ => green 76 | } 77 | color @c { 78 | "x" => red 79 | "y" | "z" => blue 80 | } 81 | ] 82 | ``` 83 | 84 | ### Tuple matching 85 | 86 | ```elm 87 | Hello [ 88 | @foo boolean 89 | @bar boolean 90 | 91 | (@foo, @bar) { 92 | (true, true) => color red 93 | (false, false) => color blue 94 | (_, _) => color green 95 | } 96 | ] 97 | ``` 98 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@ess/monorepo", 3 | "version": "1.0.0", 4 | "private": true, 5 | "workspaces": ["compiler", "example-flow-webpack", "webpack-loader"], 6 | "license": "MIT" 7 | } 8 | -------------------------------------------------------------------------------- /yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | bsb-native@4.0.6: 6 | version "4.0.6" 7 | resolved "https://registry.yarnpkg.com/bsb-native/-/bsb-native-4.0.6.tgz#5c0289e24e0a3375ebf8f69a17510b884cace1ca" 8 | integrity sha512-T1MkndnmA4StiKK2UvEftrqANN+h9StlLghBbwmKa833d97D86np+2wmIOw5Rqy6rNW3D076A2G51mooWTUwfQ== 9 | dependencies: 10 | yauzl "^2.9.1" 11 | 12 | buffer-crc32@~0.2.3: 13 | version "0.2.13" 14 | resolved "https://registry.yarnpkg.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz#0d333e3f00eac50aa1454abd30ef8c2a5d9a7242" 15 | integrity sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI= 16 | 17 | fd-slicer@~1.0.1: 18 | version "1.0.1" 19 | resolved "https://registry.yarnpkg.com/fd-slicer/-/fd-slicer-1.0.1.tgz#8b5bcbd9ec327c5041bf9ab023fd6750f1177e65" 20 | integrity sha1-i1vL2ewyfFBBv5qwI/1nUPEXfmU= 21 | dependencies: 22 | pend "~1.2.0" 23 | 24 | pend@~1.2.0: 25 | version "1.2.0" 26 | resolved "https://registry.yarnpkg.com/pend/-/pend-1.2.0.tgz#7a57eb550a6783f9115331fcf4663d5c8e007a50" 27 | integrity sha1-elfrVQpng/kRUzH89GY9XI4AelA= 28 | 29 | resolve-from@^2.0.0: 30 | version "2.0.0" 31 | resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-2.0.0.tgz#9480ab20e94ffa1d9e80a804c7ea147611966b57" 32 | integrity sha1-lICrIOlP+h2egKgEx+oUdhGWa1c= 33 | 34 | resolve-pkg@^1.0.0: 35 | version "1.0.0" 36 | resolved "https://registry.yarnpkg.com/resolve-pkg/-/resolve-pkg-1.0.0.tgz#e19a15e78aca2e124461dc92b2e3943ef93494d9" 37 | integrity sha1-4ZoV54rKLhJEYdySsuOUPvk0lNk= 38 | dependencies: 39 | resolve-from "^2.0.0" 40 | 41 | yauzl@^2.9.1: 42 | version "2.9.1" 43 | resolved "https://registry.yarnpkg.com/yauzl/-/yauzl-2.9.1.tgz#a81981ea70a57946133883f029c5821a89359a7f" 44 | integrity sha1-qBmB6nCleUYTOIPwKcWCGok1mn8= 45 | dependencies: 46 | buffer-crc32 "~0.2.3" 47 | fd-slicer "~1.0.1" 48 | --------------------------------------------------------------------------------