├── .npmignore ├── core.json ├── test ├── include-fixture.screess ├── javascript.coffee ├── include.coffee ├── conditional-operators.coffee ├── z-index.coffee ├── sources.coffee ├── conditional-statements.coffee ├── loops.coffee ├── layers.coffee ├── property-macro.coffee ├── utilities.coffee ├── filter.coffee ├── whitespace.coffee ├── value-macro.coffee └── value.coffee ├── .gitignore ├── example.sss ├── bin └── screess ├── source ├── values │ ├── AttributeReferenceValue.ts │ ├── Value.ts │ ├── FunctionValue.ts │ ├── ScopeValue.ts │ └── ColorValue.ts ├── Stack.ts ├── expressions │ ├── Expression.ts │ ├── LiteralExpression.ts │ ├── NotOperatorExpression.ts │ ├── TypeCheckOperatorExpression.ts │ ├── JavaScriptExpression.ts │ ├── ArrayExpression.ts │ ├── ScopeExpression.ts │ ├── NullCoalescingExpression.ts │ ├── TernaryExpression.ts │ ├── MacroReferenceExpression.ts │ ├── PropertyAccessExpression.ts │ ├── SetOperatorExpression.ts │ ├── StringExpression.ts │ ├── BooleanLogicExpression.ts │ ├── ArithmeticOperatorExpression.ts │ └── ComparisonOperatorExpression.ts ├── scopes │ ├── class.ts │ ├── object.ts │ ├── global.ts │ └── layer.ts ├── eval.ts ├── statements │ ├── ClassStatement.ts │ ├── Statement.ts │ ├── LayerStatement.ts │ ├── MacroDefinitionStatement.ts │ ├── ConditionalStatement.ts │ ├── JavascriptStatement.ts │ ├── PropertyStatement.ts │ ├── LoopStatement.ts │ └── MacroReferenceStatement.ts ├── utilities.ts ├── Macro.ts ├── ArgumentsDefinition.ts ├── utilities │ ├── main.ts │ ├── object.ts │ └── color.ts ├── ExpressionSet.ts ├── server.ts ├── CLI.ts ├── index.ts ├── Arguments.ts ├── Scope.ts └── parser.pegjs ├── tsd.json ├── Makefile ├── package.json ├── core.sss ├── example.json └── README.md /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | source/parser.js 3 | npm-debug.log 4 | -------------------------------------------------------------------------------- /core.json: -------------------------------------------------------------------------------- 1 | { 2 | "layers": [], 3 | "sources": {}, 4 | "transition": {} 5 | } 6 | -------------------------------------------------------------------------------- /test/include-fixture.screess: -------------------------------------------------------------------------------- 1 | magic-number: 17 2 | 3 | uppercase(value) = `value().toUpperCase()` -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | source/parser.js 3 | typescript_interfaces 4 | compiled 5 | npm-debug.log 6 | -------------------------------------------------------------------------------- /example.sss: -------------------------------------------------------------------------------- 1 | # { 2 | type: background 3 | background-color: #edd 4 | } 5 | 6 | hillshade(opacity: 0.1) 7 | contours() 8 | -------------------------------------------------------------------------------- /bin/screess: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var Path = require('path'); 4 | var FS = require('fs'); 5 | 6 | var dir = Path.dirname(FS.realpathSync(__filename)); 7 | require(Path.join(dir, '../compiled/CLI')); 8 | -------------------------------------------------------------------------------- /source/values/AttributeReferenceValue.ts: -------------------------------------------------------------------------------- 1 | import Value = require("./Value"); 2 | import Stack = require('../Stack'); 3 | 4 | class AttributeReferenceValue extends Value { 5 | constructor(public name:string) { super() } 6 | evaluate():string { return "{" + this.name + "}" } 7 | } 8 | 9 | export = AttributeReferenceValue; -------------------------------------------------------------------------------- /source/Stack.ts: -------------------------------------------------------------------------------- 1 | class Stack { 2 | 3 | getGlobalScope() { return this.scope[0] } 4 | getScope() { return this.scope[this.scope.length - 1] } 5 | 6 | public macros; 7 | public scope; 8 | 9 | constructor() { 10 | this.macros = [] 11 | this.scope = [] 12 | } 13 | 14 | } 15 | 16 | export = Stack 17 | -------------------------------------------------------------------------------- /source/expressions/Expression.ts: -------------------------------------------------------------------------------- 1 | import Value = require('../values/Value') 2 | 3 | class Expression { 4 | 5 | evaluateToIntermediate(scope, stack):any { 6 | throw new Error("Abstract method"); 7 | } 8 | 9 | evaluate(scope, stack):any { 10 | return Value.evaluate(this.evaluateToIntermediate(scope, stack)); 11 | } 12 | 13 | } 14 | 15 | export = Expression -------------------------------------------------------------------------------- /source/values/Value.ts: -------------------------------------------------------------------------------- 1 | import Stack = require('../Stack'); 2 | 3 | class Value { 4 | 5 | static evaluate(value:any):any { 6 | if (value && value.evaluate) { 7 | return value.evaluate(); 8 | } else { 9 | return value; 10 | } 11 | } 12 | 13 | evaluate():any { 14 | throw "Abstract method" 15 | } 16 | 17 | } 18 | 19 | export = Value; 20 | -------------------------------------------------------------------------------- /source/scopes/class.ts: -------------------------------------------------------------------------------- 1 | import Stack = require('../Stack'); 2 | import Scope = require('../Scope'); 3 | import assert = require('assert'); 4 | 5 | function evaluateClassScope(stack:Stack, properties:{}, layers:Scope[], classes:Scope[]):any { 6 | assert(layers.length == 0); 7 | assert(classes.length == 0); 8 | 9 | return properties; 10 | } 11 | 12 | export = evaluateClassScope; 13 | -------------------------------------------------------------------------------- /source/scopes/object.ts: -------------------------------------------------------------------------------- 1 | import Stack = require('../Stack'); 2 | import Scope = require('../Scope'); 3 | import assert = require('assert'); 4 | 5 | function evaluateObjectScope(stack:Stack, properties:{}, layers:Scope[], classes:Scope[]):any { 6 | assert(!layers || layers.length == 0); 7 | assert(classes.length == 0); 8 | 9 | return properties; 10 | } 11 | 12 | export = evaluateObjectScope; 13 | -------------------------------------------------------------------------------- /source/expressions/LiteralExpression.ts: -------------------------------------------------------------------------------- 1 | import Expression = require("./Expression"); 2 | 3 | class LiteralExpression extends Expression { 4 | 5 | static literalExpression(value) { 6 | return new LiteralExpression(value) 7 | } 8 | 9 | constructor(public value) { 10 | super() 11 | } 12 | 13 | evaluateToIntermediate():any { 14 | return this.value; 15 | } 16 | 17 | } 18 | 19 | export = LiteralExpression; 20 | -------------------------------------------------------------------------------- /source/eval.ts: -------------------------------------------------------------------------------- 1 | import VM = require('vm'); 2 | import Scope = require("./Scope"); 3 | import Stack = require("./Stack"); 4 | import _ = require('./utilities'); 5 | import ScreeSS = require("./index"); 6 | 7 | function _eval(source:string, scope:Scope, stack:Stack, sandbox:{} = {}): any { 8 | _.extend( 9 | sandbox, 10 | {scope: scope, stack: stack, console: console}, 11 | scope.getMacrosAsFunctions(stack), 12 | ScreeSS 13 | ); 14 | return VM.runInNewContext(source, sandbox); 15 | } 16 | 17 | export = _eval; 18 | -------------------------------------------------------------------------------- /source/statements/ClassStatement.ts: -------------------------------------------------------------------------------- 1 | import Stack = require('../Stack'); 2 | import Scope = require("../Scope"); 3 | import Statement = require("./Statement"); 4 | import assert = require("assert"); 5 | import _ = require("../utilities"); 6 | 7 | class ClassStatement extends Statement { 8 | 9 | constructor(public name:string, public body:Scope) { super(); } 10 | 11 | evaluate(scope:Scope, stack:Stack, layers, classes, properties) { 12 | classes.push(this.body.evaluate(Scope.Type.CLASS, stack)); 13 | } 14 | } 15 | 16 | export = ClassStatement; -------------------------------------------------------------------------------- /tsd.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "v4", 3 | "repo": "borisyankov/DefinitelyTyped", 4 | "ref": "master", 5 | "path": "typescript_interfaces", 6 | "bundle": "typescript_interfaces/index.d.ts", 7 | "installed": { 8 | "node/node.d.ts": { 9 | "commit": "07ae96e580d1ac9817d40717158fcafc875044b3" 10 | }, 11 | "commander/commander.d.ts": { 12 | "commit": "07ae96e580d1ac9817d40717158fcafc875044b3" 13 | }, 14 | "underscore/underscore.d.ts": { 15 | "commit": "7a31100a5aac155d9a3071eda99dcb47a10a5211" 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /source/expressions/NotOperatorExpression.ts: -------------------------------------------------------------------------------- 1 | import Expression = require("./Expression"); 2 | import assert = require("assert"); 3 | import _ = require("../utilities"); 4 | import Scope = require("../Scope"); 5 | import Stack = require("../Stack"); 6 | 7 | class SetOperatorExpression extends Expression { 8 | 9 | constructor(public expression:Expression) { super(); } 10 | 11 | evaluateToIntermediate(scope:Scope, stack:Stack):any { 12 | return ["none", this.expression.evaluate(scope, stack)]; 13 | } 14 | 15 | } 16 | 17 | export = SetOperatorExpression; 18 | 19 | -------------------------------------------------------------------------------- /source/statements/Statement.ts: -------------------------------------------------------------------------------- 1 | import Stack = require('../Stack'); 2 | import Scope = require("../Scope"); 3 | import assert = require("assert"); 4 | import _ = require("../utilities"); 5 | 6 | class Statement { 7 | 8 | constructor() {} 9 | 10 | eachPrimitiveStatement(scope:Scope, stack:Stack, callback:(scope:Scope, statement:Statement) => void):void { 11 | callback(scope, this); 12 | } 13 | 14 | evaluate(scope:Scope, stack:Stack, layers, classes, properties) { 15 | assert(false, "abstract method"); 16 | } 17 | 18 | } 19 | 20 | export = Statement; 21 | -------------------------------------------------------------------------------- /test/javascript.coffee: -------------------------------------------------------------------------------- 1 | assert = require("assert") 2 | _ = require('../compiled/utilities') 3 | 4 | Parser = require("../compiled/parser") 5 | parse = (source) -> Parser.parse(source).evaluate() 6 | 7 | describe "javascript", -> 8 | 9 | it "can be embedded as a statement", -> 10 | stylesheet = parse """ 11 | `properties.foo = 17` 12 | """ 13 | assert.equal stylesheet["foo"], 17 14 | 15 | it "can be embedded as a statement", -> 16 | stylesheet = parse """ 17 | `properties.foo = identity(42)` 18 | """ 19 | assert.equal stylesheet["foo"], 42 20 | -------------------------------------------------------------------------------- /source/statements/LayerStatement.ts: -------------------------------------------------------------------------------- 1 | import Stack = require('../Stack'); 2 | import Scope = require("../Scope"); 3 | import Statement = require("./Statement"); 4 | import assert = require("assert"); 5 | import _ = require("../utilities"); 6 | 7 | class LayerStatement extends Statement { 8 | 9 | constructor(public name:string, public body:Scope) { 10 | super(); 11 | this.body.name = name; 12 | } 13 | 14 | evaluate(scope:Scope, stack:Stack, layers, classes, properties) { 15 | layers.push(this.body.evaluate(Scope.Type.LAYER, stack)); 16 | } 17 | } 18 | 19 | export = LayerStatement; 20 | -------------------------------------------------------------------------------- /source/expressions/TypeCheckOperatorExpression.ts: -------------------------------------------------------------------------------- 1 | import Expression = require("./Expression"); 2 | import AttributeReferenceValue = require("../values/AttributeReferenceValue"); 3 | import assert = require("assert"); 4 | import Scope = require("../Scope"); 5 | import Stack = require("../Stack"); 6 | import _ = require("../utilities"); 7 | 8 | class TypeCheckExpression extends Expression { 9 | 10 | constructor(public type:Expression) { super() } 11 | 12 | evaluateToIntermediate(scope:Scope, stack:Stack):any { 13 | return ["==", "$type", this.type.evaluate(scope, stack)]; 14 | } 15 | } 16 | 17 | export = TypeCheckExpression 18 | -------------------------------------------------------------------------------- /source/expressions/JavaScriptExpression.ts: -------------------------------------------------------------------------------- 1 | import _ = require("../utilities"); 2 | import assert = require("assert"); 3 | import Expression = require("./Expression"); 4 | import Scope = require("../Scope"); 5 | import Stack = require("../Stack"); 6 | import eval = require("../eval"); 7 | 8 | var parse = require("../parser").parse; 9 | 10 | class JavascriptExpression extends Expression { 11 | 12 | constructor(public source:string) { 13 | super() 14 | } 15 | 16 | evaluateToIntermediate(scope: Scope, stack: Stack):any { 17 | return eval(this.source, scope, stack); 18 | } 19 | 20 | } 21 | 22 | export = JavascriptExpression; 23 | -------------------------------------------------------------------------------- /test/include.coffee: -------------------------------------------------------------------------------- 1 | assert = require("assert") 2 | _ = require('../compiled/utilities') 3 | 4 | Parser = require("../compiled/parser") 5 | parse = (source) -> Parser.parse(source).evaluate() 6 | 7 | describe "include", -> 8 | it "should load properties", -> 9 | stylesheet = parse """ 10 | include('test/include-fixture.screess') 11 | """ 12 | assert.equal stylesheet["magic-number"], 17 13 | 14 | it "should load value macros", -> 15 | stylesheet = parse """ 16 | include('test/include-fixture.screess') 17 | scree-test-meta: uppercase('baz') 18 | """ 19 | assert.equal stylesheet["scree-test-meta"], "BAZ" 20 | -------------------------------------------------------------------------------- /source/expressions/ArrayExpression.ts: -------------------------------------------------------------------------------- 1 | import Expression = require("./Expression"); 2 | import LiteralExpression = require("./LiteralExpression"); 3 | import Scope = require("../Scope"); 4 | import Stack = require("../Stack"); 5 | import _ = require("../utilities"); 6 | 7 | class ArrayExpression extends Expression { 8 | 9 | constructor(public expressions:Expression[]) { 10 | super(); 11 | } 12 | 13 | evaluateToIntermediate(scope:Scope, stack:Stack):any { 14 | return _.map(this.expressions, function(expression: Expression): any { 15 | return expression.evaluateToIntermediate(scope, stack); 16 | }); 17 | } 18 | 19 | } 20 | 21 | export = ArrayExpression; 22 | -------------------------------------------------------------------------------- /source/utilities.ts: -------------------------------------------------------------------------------- 1 | import underscore = require("underscore"); 2 | 3 | import color = require("./utilities/color"); 4 | var colorAny:any = new color.Utilities() 5 | underscore.mixin(colorAny); 6 | 7 | import main = require('./utilities/main'); 8 | var mainAny:any = new main.Utilities() 9 | underscore.mixin(mainAny); 10 | 11 | import object = require('./utilities/object'); 12 | var objectAny:any = new object.Utilities() 13 | underscore.mixin(objectAny); 14 | 15 | interface Utilities extends 16 | UnderscoreStatic, 17 | color.Utilities, 18 | main.Utilities, 19 | object.Utilities {} 20 | 21 | var underscoreAny:any = underscore; 22 | var utilities:Utilities = underscoreAny; 23 | export = utilities; -------------------------------------------------------------------------------- /source/expressions/ScopeExpression.ts: -------------------------------------------------------------------------------- 1 | import Expression = require("./Expression"); 2 | import ExpressionSet = require("../ExpressionSet"); 3 | import LiteralExpression = require("./LiteralExpression"); 4 | import Scope = require("../Scope"); 5 | import Stack = require("../Stack"); 6 | import assert = require("assert"); 7 | import _ = require("../utilities"); 8 | import ScopeValue = require('../values/ScopeValue'); 9 | 10 | class ScopeExpression extends Expression { 11 | 12 | constructor(public body:Scope) { 13 | super(); 14 | } 15 | 16 | evaluateToIntermediate(scope:Scope, stack:Stack):any { 17 | return new ScopeValue(this.body); 18 | } 19 | 20 | } 21 | 22 | export = ScopeExpression; 23 | -------------------------------------------------------------------------------- /test/conditional-operators.coffee: -------------------------------------------------------------------------------- 1 | {parse} = require("../compiled/parser") 2 | assert = require("assert") 3 | _ = require('../compiled/utilities') 4 | 5 | parseValue = (value, context = {}) -> 6 | stylesheet = parse("#layer { type: 'background'; scree-test-meta: #{value} }").evaluate() 7 | stylesheet.layers[0]['scree-test-meta'] 8 | 9 | describe "conditional operators", -> 10 | 11 | it "should parse conditional assignment operator", -> 12 | value = parseValue "true ? 1 : 2" 13 | assert.equal(value, 1) 14 | 15 | value = parseValue "false ? 1 : 2" 16 | assert.equal(value, 2) 17 | 18 | it "should parse null coalescing operator", -> 19 | value = parseValue "null ?? 1 ?? 2" 20 | assert.equal(value, 1) 21 | -------------------------------------------------------------------------------- /source/expressions/NullCoalescingExpression.ts: -------------------------------------------------------------------------------- 1 | import Expression = require("./Expression"); 2 | import Scope = require("../Scope"); 3 | import Stack = require("../Stack"); 4 | import _ = require("../utilities"); 5 | 6 | class NullCoalescingExpression extends Expression { 7 | 8 | constructor(public headExpression:Expression, public tailExpression:Expression) { super(); } 9 | 10 | evaluateToIntermediate(scope:Scope, stack:Stack):any { 11 | var headValue = this.headExpression.evaluate(scope, stack); 12 | if (headValue == null) { 13 | return this.tailExpression.evaluateToIntermediate(scope, stack); 14 | } else { 15 | return headValue; 16 | } 17 | } 18 | 19 | } 20 | 21 | export = NullCoalescingExpression; 22 | 23 | -------------------------------------------------------------------------------- /source/scopes/global.ts: -------------------------------------------------------------------------------- 1 | import Stack = require('../Stack'); 2 | import Scope = require('../Scope'); 3 | import _ = require("../utilities"); 4 | 5 | function evaluateGlobalScope(stack:Stack, properties:{}, layers:Scope[], classes:Scope[]):any { 6 | var sources = this.sources; 7 | 8 | var transition = { 9 | duration: properties["transition-delay"], 10 | delay: properties["transition-duration"] 11 | } 12 | delete properties["transition-delay"]; 13 | delete properties["transition-duration"]; 14 | 15 | stack.scope.pop(); 16 | 17 | return _.extend( 18 | properties, 19 | { 20 | version: 8, 21 | layers: layers, 22 | sources: sources, 23 | transition: transition 24 | } 25 | ) 26 | } 27 | 28 | export = evaluateGlobalScope; 29 | -------------------------------------------------------------------------------- /source/statements/MacroDefinitionStatement.ts: -------------------------------------------------------------------------------- 1 | import Stack = require('../Stack'); 2 | import Scope = require("../Scope"); 3 | import Statement = require("./Statement"); 4 | import assert = require("assert"); 5 | import ExpressionSet = require("../ExpressionSet"); 6 | import Value = require("../values/Value"); 7 | import _ = require("../utilities"); 8 | import ArgumentsDefinition = require('../ArgumentsDefinition') 9 | import Expression = require('../expressions/Expression'); 10 | 11 | class MacroDefinitionStatement extends Statement { 12 | 13 | constructor(public name:string, public argsDefinition:ArgumentsDefinition, public body:Expression) { 14 | super(); 15 | } 16 | 17 | evaluate(scope:Scope, stack:Stack, layers, classes, properties) {} 18 | 19 | } 20 | 21 | export = MacroDefinitionStatement 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: clean build-pegjs install-typescript-interfaces build-typescript test test-debug 2 | 3 | all: build-typescript 4 | 5 | clean: 6 | rm -r compiled typescript_interfaces 7 | 8 | build-pegjs: 9 | mkdir -p compiled 10 | pegjs \ 11 | --allowed-start-rules global,expression \ 12 | --cache \ 13 | source/parser.pegjs \ 14 | compiled/parser.js 15 | 16 | install-typescript-interfaces: 17 | mkdir -p typescript_interfaces 18 | tsd reinstall 19 | tsd rebundle 20 | 21 | build-typescript: build-pegjs install-typescript-interfaces 22 | tsc \ 23 | --sourceMap \ 24 | --target ES5 \ 25 | --module commonjs \ 26 | --outDir ./compiled \ 27 | source/*.ts source/**/*.ts 28 | 29 | test: all 30 | mocha --compilers coffee:coffee-script/register test 31 | 32 | test-debug: all 33 | mocha --debug-brk --compilers coffee:coffee-script/register test 34 | -------------------------------------------------------------------------------- /source/statements/ConditionalStatement.ts: -------------------------------------------------------------------------------- 1 | import Stack = require('../Stack'); 2 | import Scope = require("../Scope"); 3 | import Statement = require("./Statement"); 4 | import assert = require("assert"); 5 | import Expression = require("../expressions/Expression"); 6 | import _ = require("../utilities"); 7 | 8 | class ConditionalStatement extends Statement { 9 | 10 | constructor(public items: { condition: Expression; scope: Scope; }[]) { super(); } 11 | 12 | eachPrimitiveStatement(scope:Scope, stack:Stack, callback:(scope:Scope, statement:Statement) => void):void { 13 | for (var i in this.items) { 14 | var item = this.items[i]; 15 | if (item.condition.evaluateToIntermediate(scope, stack)) { 16 | item.scope.eachPrimitiveStatement(stack, callback); 17 | break; 18 | } 19 | } 20 | } 21 | } 22 | 23 | export = ConditionalStatement; -------------------------------------------------------------------------------- /source/statements/JavascriptStatement.ts: -------------------------------------------------------------------------------- 1 | import Stack = require('../Stack'); 2 | import Scope = require("../Scope"); 3 | import Statement = require("./Statement"); 4 | import assert = require("assert"); 5 | import _ = require("../utilities"); 6 | import eval = require("../eval"); 7 | 8 | class PropertyStatement extends Statement { 9 | 10 | constructor(public source:string) { 11 | super(); 12 | } 13 | 14 | evaluate(scope:Scope, stack:Stack, layers, classes, properties) { 15 | return eval(this.source, scope, stack, { 16 | scope: scope, 17 | stack: stack, 18 | layers: layers, 19 | classes: classes, 20 | properties: properties 21 | }); 22 | } 23 | 24 | eachPrimitiveStatement(scope:Scope, stack:Stack, callback:(scope:Scope, statement:Statement) => void):void { 25 | callback(scope, this); 26 | } 27 | } 28 | 29 | export = PropertyStatement 30 | -------------------------------------------------------------------------------- /source/expressions/TernaryExpression.ts: -------------------------------------------------------------------------------- 1 | import Expression = require("./Expression"); 2 | import Scope = require("../Scope"); 3 | import Stack = require("../Stack"); 4 | 5 | class TernaryExpression extends Expression { 6 | 7 | constructor(public conditionExpression:Expression, public trueExpression:Expression, public falseExpression:Expression) { super(); } 8 | 9 | evaluateToIntermediate(scope:Scope, stack:Stack):any { 10 | var conditionValue = this.conditionExpression.evaluate(scope, stack); 11 | 12 | if (conditionValue === true) { 13 | return this.trueExpression.evaluateToIntermediate(scope, stack) 14 | } else if (conditionValue === false) { 15 | return this.falseExpression.evaluateToIntermediate(scope, stack); 16 | } else { 17 | throw new Error("Compile-time condition could not be resolved.") 18 | } 19 | } 20 | 21 | } 22 | 23 | export = TernaryExpression; 24 | 25 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.1", 3 | "name": "screess", 4 | "description": "A stylesheet language that compiles down to a Mapbox GL stylesheet", 5 | "author": "Lucas Wojciechowski ", 6 | "main": "compiled/node/index", 7 | "bin": { 8 | "screess": "bin/screess" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "http://github.com/screemaps/screess.git" 13 | }, 14 | "dependencies": { 15 | "coffee-script": "^1.10.0", 16 | "commander": "^2.5.0", 17 | "express": "^4.13.3", 18 | "mapbox-gl-style-spec": "^8.1.0", 19 | "tsd": "^0.6.4", 20 | "underscore": "^1.8.3" 21 | }, 22 | "devDependencies": { 23 | "pegjs": "^0.9.0", 24 | "mocha": "^2.0.1", 25 | "browserify": "^6.3.2", 26 | "source-map-support": "^0.2.8", 27 | "typescript": "^1.5.3", 28 | "tsd": "^0.6.4" 29 | }, 30 | "scripts": { 31 | "prepublish": "make", 32 | "test": "make test" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /source/expressions/MacroReferenceExpression.ts: -------------------------------------------------------------------------------- 1 | import Expression = require("./Expression"); 2 | import ExpressionSet = require("../ExpressionSet"); 3 | import util = require('util'); 4 | import Scope = require("../Scope"); 5 | import Arguments = require('../Arguments'); 6 | import Stack = require("../Stack"); 7 | import _ = require("../utilities"); 8 | import assert = require("assert"); 9 | 10 | class MacroReferenceExpression extends Expression { 11 | 12 | constructor(public name:string, public argExpressions:ExpressionSet) { 13 | super(); 14 | } 15 | 16 | evaluateToIntermediate(argsScope:Scope, stack:Stack):any { 17 | assert(argsScope instanceof Scope); 18 | var args = this.argExpressions.toArguments(argsScope, stack); 19 | 20 | var macro = argsScope.getMacro(this.name, args, stack); 21 | if (!macro) { throw new Error("Could not find macro " + this.name); } 22 | 23 | return macro.evaluateToIntermediate(args, stack); 24 | } 25 | } 26 | 27 | export = MacroReferenceExpression; 28 | -------------------------------------------------------------------------------- /source/values/FunctionValue.ts: -------------------------------------------------------------------------------- 1 | import Value = require("./Value"); 2 | import Stack = require("../Stack") 3 | import _ = require("../utilities"); 4 | import assert = require("assert"); 5 | import Arguments = require("../Arguments"); 6 | 7 | class FunctionValue extends Value { 8 | 9 | static fromArguments(args:Arguments):FunctionValue { 10 | var stops = []; 11 | 12 | for (var key in args) { 13 | if (key == "base") continue; 14 | var stop = parseInt(key); 15 | assert(stop != NaN, "Malformed stop value"); 16 | stops.push([stop, args[key]]); 17 | } 18 | 19 | assert(stops.length > 0, "Function has no stops"); 20 | 21 | return new FunctionValue(args["base"], stops); 22 | } 23 | 24 | constructor(public base:number, public stops:[number, number][]) { super(); } 25 | 26 | evaluate():any { 27 | if (this.base) { 28 | return {base: Value.evaluate(this.base), stops: this.stops} 29 | } else { 30 | return {stops: this.stops} 31 | } 32 | } 33 | } 34 | 35 | export = FunctionValue; 36 | -------------------------------------------------------------------------------- /source/statements/PropertyStatement.ts: -------------------------------------------------------------------------------- 1 | import Stack = require('../Stack'); 2 | import Scope = require("../Scope"); 3 | import Statement = require("./Statement"); 4 | import assert = require("assert"); 5 | import Expression = require("../expressions/Expression"); 6 | import Value = require("../values/Value"); 7 | import _ = require("../utilities"); 8 | 9 | class PropertyStatement extends Statement { 10 | 11 | constructor(public name:string, public expression:Expression) { 12 | super(); 13 | assert(expression instanceof Expression); 14 | } 15 | 16 | evaluateValueToIntermediate(scope:Scope, stack:Stack) { 17 | return this.expression.evaluateToIntermediate(scope, stack); 18 | } 19 | 20 | evaluate(scope:Scope, stack:Stack, layers, classes, properties) { 21 | properties[this.name] = Value.evaluate(this.evaluateValueToIntermediate(scope, stack)); 22 | } 23 | 24 | eachPrimitiveStatement(scope:Scope, stack:Stack, callback:(scope:Scope, statement:Statement) => void):void { 25 | callback(scope, this); 26 | } 27 | } 28 | 29 | export = PropertyStatement 30 | -------------------------------------------------------------------------------- /source/values/ScopeValue.ts: -------------------------------------------------------------------------------- 1 | import Value = require("./Value"); 2 | import Scope = require("../Scope"); 3 | import Stack = require("../Stack"); 4 | import _ = require("../utilities"); 5 | import assert = require("assert"); 6 | import Statement = require("../statements/Statement"); 7 | import PropertyStatement = require("../statements/PropertyStatement"); 8 | 9 | class ScopeValue extends Value { 10 | 11 | constructor(public scope:Scope) { 12 | super(); 13 | } 14 | 15 | evaluate():any { 16 | return this.scope.evaluate(Scope.Type.OBJECT); 17 | } 18 | 19 | // TODO this is ugly, at least move to scope class 20 | toObject(stack:Stack = new Stack()) { 21 | stack.scope.push(this.scope); 22 | var output = {}; 23 | this.scope.eachPrimitiveStatement(stack, (scope: Scope, statement: Statement) => { 24 | if (statement instanceof PropertyStatement) { 25 | output[statement.name] = statement.evaluateValueToIntermediate(scope, stack); 26 | } 27 | }); 28 | stack.scope.pop(); 29 | return output; 30 | } 31 | } 32 | 33 | export = ScopeValue; 34 | -------------------------------------------------------------------------------- /test/z-index.coffee: -------------------------------------------------------------------------------- 1 | assert = require("assert") 2 | _ = require('../compiled/utilities') 3 | 4 | Parser = require("../compiled/parser") 5 | parse = (source) -> Parser.parse(source).evaluate() 6 | 7 | describe "z-index", -> 8 | 9 | it "should work on layers", -> 10 | 11 | stylesheet = parse """ 12 | #second { type: 'background' } 13 | #third { type: 'background' } 14 | #first { type: 'background'; z-index: -1 } 15 | """ 16 | assert.deepEqual stylesheet.layers[0].id, "first" 17 | assert.deepEqual stylesheet.layers[1].id, "second" 18 | assert.deepEqual stylesheet.layers[2].id, "third" 19 | 20 | it "should work on sublayers", -> 21 | 22 | stylesheet = parse """ 23 | # { 24 | #second { type: 'background' } 25 | #third { type: 'background' } 26 | #first { type: 'background'; z-index: -1 } 27 | } 28 | """ 29 | assert.deepEqual stylesheet.layers[0].layers[0].id, "first" 30 | assert.deepEqual stylesheet.layers[0].layers[1].id, "second" 31 | assert.deepEqual stylesheet.layers[0].layers[2].id, "third" 32 | -------------------------------------------------------------------------------- /source/expressions/PropertyAccessExpression.ts: -------------------------------------------------------------------------------- 1 | import Expression = require("./Expression"); 2 | import Scope = require("../Scope"); 3 | import Stack = require("../Stack"); 4 | import assert = require('assert'); 5 | import _ = require("underscore"); 6 | import ScopeValue = require('../values/ScopeValue'); 7 | 8 | class PropertyAccessExpression extends Expression { 9 | 10 | constructor(public baseExpression:Expression, public propertyExpression:Expression) { 11 | super() 12 | assert(this.baseExpression instanceof Expression); 13 | assert(this.propertyExpression instanceof Expression); 14 | } 15 | 16 | evaluateToIntermediate(scope:Scope, stack:Stack):any { 17 | var base = this.baseExpression.evaluateToIntermediate(scope, stack); 18 | var property = this.propertyExpression.evaluateToIntermediate(scope, stack); 19 | 20 | if (base instanceof ScopeValue) { 21 | base = base.toObject(); 22 | } 23 | 24 | assert(_.isString(property) || _.isNumber(property), "Property is of invalid type (" + property + ")"); 25 | 26 | return base[property]; 27 | } 28 | 29 | } 30 | 31 | export = PropertyAccessExpression; 32 | -------------------------------------------------------------------------------- /source/Macro.ts: -------------------------------------------------------------------------------- 1 | import Expression = require('./expressions/Expression') 2 | import ArgumentsDefinition = require('./ArgumentsDefinition') 3 | import Arguments = require('./Arguments') 4 | import Scope = require('./Scope') 5 | import LiteralExpression = require('./expressions/LiteralExpression') 6 | import assert = require('assert') 7 | import Stack = require('./Stack') 8 | import _ = require("./utilities") 9 | 10 | class Macro { 11 | 12 | constructor(public scope:Scope, public name:string, public argsDefinition:ArgumentsDefinition, public body:Expression) { 13 | assert(this.body instanceof Expression); 14 | } 15 | 16 | matches(name:string, args:Arguments):boolean { 17 | return name == this.name && args.matches(this.argsDefinition); 18 | } 19 | 20 | evaluateToIntermediate(args:Arguments, stack:Stack) { 21 | var scope = new Scope(this.scope); 22 | 23 | var argsObject = args.toObject(this.argsDefinition, stack); 24 | scope.addLiteralMacros(argsObject); 25 | scope.addLiteralMacros({arguments: argsObject}); 26 | 27 | stack.scope.push(scope); 28 | var evaluated = this.body.evaluateToIntermediate(scope, stack); 29 | stack.scope.pop(); 30 | 31 | return evaluated; 32 | } 33 | 34 | } 35 | 36 | export = Macro 37 | -------------------------------------------------------------------------------- /source/ArgumentsDefinition.ts: -------------------------------------------------------------------------------- 1 | import assert = require('assert') 2 | import Scope = require('./Scope') 3 | import _ = require('./utilities') 4 | import Expression = require('./expressions/Expression'); 5 | 6 | interface ValueDefinition { 7 | name: string; 8 | index?: number; 9 | expression?: Expression; 10 | } 11 | 12 | class ArgumentsDefinition { 13 | 14 | static ZERO:ArgumentsDefinition = new ArgumentsDefinition([], null); 15 | static WILDCARD:ArgumentsDefinition = new ArgumentsDefinition([], null); 16 | 17 | public named:{[name:string]: ValueDefinition}; 18 | public length:number; 19 | 20 | constructor(public definitions:ValueDefinition[], public scope:Scope) { 21 | if (this.definitions.length > 0) { 22 | assert(this.scope != null); 23 | } 24 | 25 | this.named = {}; 26 | 27 | for (var index in this.definitions) { 28 | var definition = this.definitions[index]; 29 | definition.index = index 30 | if (definition.name) { 31 | this.named[definition.name] = definition; 32 | } 33 | } 34 | 35 | this.length = this.definitions.length; 36 | } 37 | 38 | public isWildcard():boolean { 39 | return false; 40 | } 41 | } 42 | 43 | ArgumentsDefinition.WILDCARD.isWildcard = function() { return true }; 44 | 45 | export = ArgumentsDefinition; 46 | -------------------------------------------------------------------------------- /source/statements/LoopStatement.ts: -------------------------------------------------------------------------------- 1 | import Stack = require('../Stack'); 2 | import Scope = require("../Scope"); 3 | import Statement = require("./Statement"); 4 | import assert = require("assert"); 5 | import Expression = require("../expressions/Expression"); 6 | import _ = require("../utilities"); 7 | import ScopeValue = require('../values/ScopeValue'); 8 | 9 | class LoopStatement extends Statement { 10 | constructor( 11 | public body:Scope, 12 | public valueIdentifier:string, 13 | public keyIdentifier:string, 14 | public collectionExpression:Expression 15 | ) { super(); } 16 | 17 | eachPrimitiveStatement(scope:Scope, stack:Stack, callback:(scope:Scope, statement:Statement) => void):void { 18 | var collection = this.collectionExpression.evaluateToIntermediate(scope, stack); 19 | if (collection instanceof ScopeValue) { collection = collection.toObject(stack); } 20 | assert(_.isArray(collection) || _.isObject(collection)) 21 | 22 | for (var key in collection) { 23 | var value = collection[key]; 24 | this.body.addLiteralMacro(this.valueIdentifier, value); 25 | if (this.keyIdentifier) { this.body.addLiteralMacro(this.keyIdentifier, key); } 26 | this.body.eachPrimitiveStatement(stack, callback) 27 | } 28 | 29 | } 30 | } 31 | 32 | export = LoopStatement; 33 | -------------------------------------------------------------------------------- /test/sources.coffee: -------------------------------------------------------------------------------- 1 | assert = require("assert") 2 | _ = require('../compiled/utilities') 3 | 4 | Parser = require("../compiled/parser") 5 | parse = (source) -> Parser.parse(source).evaluate() 6 | 7 | describe "sources", -> 8 | it "should be respected", -> 9 | stylesheet = parse """ 10 | #layer { 11 | type: 'background' 12 | source-bar: "baz" 13 | } 14 | """ 15 | assert.deepEqual _.values(stylesheet.sources)[0], bar: "baz" 16 | assert stylesheet.layers[0].source 17 | assert _.keys(stylesheet.sources)[0] == stylesheet.layers[0].source 18 | 19 | it "should be omitted", -> 20 | stylesheet = parse """ 21 | #layer { 22 | type: 'background' 23 | } 24 | """ 25 | assert.deepEqual _.keys(stylesheet.sources).length, 0 26 | assert !stylesheet.layers[0].source 27 | 28 | it "should respect source layer", -> 29 | stylesheet = parse """ 30 | #layer { 31 | source-layer: 'bar' 32 | type: 'background' 33 | } 34 | """ 35 | assert.equal stylesheet.layers[0]["source-layer"], "bar" 36 | 37 | it "should rename tile-size to tileSize", -> 38 | stylesheet = parse """ 39 | #layer { 40 | type: 'background' 41 | source-tile-size: 17 42 | } 43 | """ 44 | assert.deepEqual _.values(stylesheet.sources)[0].tileSize, 17 -------------------------------------------------------------------------------- /source/expressions/SetOperatorExpression.ts: -------------------------------------------------------------------------------- 1 | import Expression = require("./Expression"); 2 | import assert = require("assert"); 3 | import AttributeReferenceValue = require("../values/AttributeReferenceValue"); 4 | import Value = require('../values/Value'); 5 | import Scope = require("../Scope"); 6 | import Stack = require("../Stack"); 7 | import _ = require("../utilities"); 8 | 9 | class SetOperatorExpression extends Expression { 10 | 11 | private static operators = { 12 | "in": (needle, haystack) => { return _.contains(haystack, needle) }, 13 | "!in": (needle, haystack) => { return !_.contains(haystack, needle) } 14 | } 15 | 16 | constructor(public needle:Expression, public operator:string, public haystack:Expression) { super() } 17 | 18 | evaluateToIntermediate(scope:Scope, stack:Stack):any { 19 | var needle = this.needle.evaluateToIntermediate(scope, stack); 20 | var haystack = this.haystack.evaluateToIntermediate(scope, stack); 21 | var operator = this.operator; 22 | 23 | haystack = Value.evaluate(haystack); 24 | assert(haystack instanceof Array); 25 | 26 | if (needle instanceof AttributeReferenceValue) { 27 | return [operator, needle.name].concat(haystack); 28 | } else { 29 | return SetOperatorExpression.operators[operator](needle, haystack) 30 | } 31 | 32 | } 33 | } 34 | 35 | export = SetOperatorExpression; -------------------------------------------------------------------------------- /core.sss: -------------------------------------------------------------------------------- 1 | include(file) = ` 2 | var includeeScope = Scope.createFromFile(file()); 3 | var includerScope = scope.parent; 4 | includerScope.macros = includerScope.macros.concat(includeeScope.macros); 5 | new ScopeValue(includeeScope); 6 | ` 7 | 8 | identity (value) = value 9 | 10 | range(start, stop, step) = `_.range(start(), stop(), step())` 11 | 12 | hsva(hue, saturation, value, alpha = 1) = `ColorValue.hsla(hue(), saturation(), value(), alpha())` 13 | hsla(hue, saturation, lightness, alpha = 1) = `ColorValue.hsla(hue(), saturation(), lightness(), alpha())` 14 | rgba(red, green, blue, alpha = 1) = `ColorValue.rgba(red(), green(), blue(), alpha())` 15 | 16 | color-mix(a, b, ratio = 0.5) = rgba( 17 | red: a.red * ratio + b.red * (1 - ratio) 18 | green: a.green * ratio + b.green * (1 - ratio) 19 | blue: a.blue * ratio + b.blue * (1 - ratio) 20 | alpha: a.alpha * ratio + b.alpha * (1 - ratio) 21 | ) 22 | 23 | function(*) = `FunctionValue.fromArguments(arguments())` 24 | 25 | background = "background" 26 | fill = "fill" 27 | line = "line" 28 | symbol = "symbol" 29 | raster = "raster" 30 | vector = "vector" 31 | raster = "raster" 32 | point = "point" 33 | line = "line" 34 | bevel = "bevel" 35 | round = "round" 36 | miter = "miter" 37 | butt = "butt" 38 | round = "round" 39 | square = "square" 40 | 41 | source(url, layer) = { 42 | source(vector url layer) 43 | } 44 | 45 | source(type, url, layer) = { 46 | source-type: type 47 | source-url: url 48 | source-layer: layer 49 | } 50 | -------------------------------------------------------------------------------- /source/statements/MacroReferenceStatement.ts: -------------------------------------------------------------------------------- 1 | import Stack = require('../Stack'); 2 | import Scope = require("../Scope"); 3 | import Statement = require("./Statement"); 4 | import assert = require("assert"); 5 | import ExpressionSet = require("../ExpressionSet"); 6 | import Value = require("../values/Value"); 7 | import ScopeValue = require("../values/ScopeValue"); 8 | import _ = require("../utilities"); 9 | import Macro = require('../Macro'); 10 | import Arguments = require('../Arguments'); 11 | 12 | class MacroReferenceStatement extends Statement { 13 | 14 | constructor(public name:string, public expressions:ExpressionSet) { 15 | super(); 16 | assert(this.expressions instanceof ExpressionSet); 17 | } 18 | 19 | eachPrimitiveStatement(parentScope:Scope, stack:Stack, callback:(scope:Scope, statement:Statement) => void):void { 20 | assert(parentScope instanceof Scope); 21 | var args:Arguments = this.expressions.toArguments(parentScope, stack); 22 | 23 | var macro = parentScope.getMacro(this.name, args, stack); 24 | if (!macro) { 25 | throw new Error("Could not find macro " + this.name); 26 | } 27 | 28 | var scopeValue:ScopeValue = macro.evaluateToIntermediate(args, stack); 29 | assert(scopeValue instanceof ScopeValue); 30 | 31 | var scope:Scope = scopeValue.scope; 32 | assert(scope instanceof Scope); 33 | 34 | var argsObject = args.toObject(macro.argsDefinition, stack) 35 | scope.addLiteralMacros(argsObject); 36 | scope.addLiteralMacros({arguments: argsObject}); 37 | 38 | scope.eachPrimitiveStatement(stack, callback); 39 | } 40 | } 41 | 42 | export = MacroReferenceStatement 43 | -------------------------------------------------------------------------------- /source/utilities/main.ts: -------------------------------------------------------------------------------- 1 | import _ = require('underscore'); 2 | 3 | export class Utilities { 4 | 5 | isArrayOf(array:any, klass:Function) { 6 | if (!_.isArray(array)) { return false } 7 | 8 | for (var i in array) { 9 | if (!(array[i] instanceof klass)) { return false } 10 | } 11 | 12 | return true 13 | } 14 | 15 | mapMethod( 16 | list:_.List, 17 | method:string, 18 | ...args:any[] 19 | ):_.List { 20 | return _.map( 21 | list, 22 | (value) => value[method].apply(value, args) 23 | ) 24 | } 25 | 26 | none( 27 | list: _.List, 28 | iterator: (value:T, key:number) => boolean = _.identity, 29 | stack: any = {} 30 | ):boolean { 31 | return !_.some(list, iterator, stack); 32 | } 33 | 34 | count( 35 | list: _.List, 36 | iterator: (value:T, key:number) => boolean = _.identity, 37 | stack: any = {} 38 | ):number { 39 | var count:number = 0; 40 | _.each( 41 | list, 42 | (value, key) => { if (iterator(value, key)) { count++ } } 43 | ) 44 | return count 45 | } 46 | 47 | hash(value:string):number { 48 | var hash = 0, i, chr, len; 49 | if (value.length == 0) return hash; 50 | for (i = 0, len = value.length; i < len; i++) { 51 | chr = value.charCodeAt(i); 52 | hash = ((hash << 5) - hash) + chr; 53 | hash |= 0; // Convert to 32bit integer 54 | } 55 | return hash; 56 | } 57 | 58 | startsWith(value: string, prefix: string):boolean { 59 | return value.slice(0, prefix.length) == prefix; 60 | } 61 | 62 | isWhitespace(value:string):boolean { 63 | return /^\s+$/.test(value) 64 | } 65 | } -------------------------------------------------------------------------------- /source/expressions/StringExpression.ts: -------------------------------------------------------------------------------- 1 | import Expression = require("./Expression"); 2 | import Scope = require("../Scope"); 3 | import Stack = require("../Stack"); 4 | import _ = require("../utilities"); 5 | var parse = require("../parser").parse; 6 | var assert = require("assert"); 7 | 8 | class StringExpression extends Expression { 9 | 10 | constructor(public body:string) { super() } 11 | 12 | evaluateToIntermediate(scope:Scope, stack:Stack):any { 13 | 14 | function parseExpression(input:string):Expression { 15 | return parse(input, { startRule: 'expression' }); 16 | } 17 | 18 | var input = this.body; 19 | var output = ''; 20 | var skip = false; 21 | 22 | for (var i = 0; i < input.length; i++) { 23 | 24 | if (!skip && input[i] == '\\') { 25 | skip = true; 26 | continue; 27 | 28 | } else if (!skip && input[i] == '{') { 29 | var expression = ''; 30 | while (input[i + 1] != '}') { 31 | assert(i < input.length); 32 | expression += input[++i]; 33 | } 34 | output += parseExpression(expression).evaluate(scope, stack).toString(); 35 | i++ // Skip the closing '}' 36 | 37 | } else if (!skip && input[i] == '@') { 38 | var expression = '@'; 39 | while (i + 1 < input.length && !_.isWhitespace(input[i + 1])) { 40 | expression += input[++i]; 41 | } 42 | assert(expression.length > 1); 43 | output += parseExpression(expression).evaluate(scope, stack).toString(); 44 | 45 | } else { 46 | output += input[i]; 47 | skip = false; 48 | } 49 | 50 | } 51 | 52 | return output; 53 | } 54 | } 55 | 56 | export = StringExpression; -------------------------------------------------------------------------------- /source/expressions/BooleanLogicExpression.ts: -------------------------------------------------------------------------------- 1 | import AttributeReferenceValue = require("../values/AttributeReferenceValue"); 2 | import assert = require("assert"); 3 | import Expression = require("./Expression"); 4 | import Scope = require("../Scope"); 5 | import Stack = require("../Stack"); 6 | import _ = require("../utilities"); 7 | 8 | function isFalse(value:any):boolean { return value === false } 9 | function isTrue(value:any):boolean { return value === true } 10 | 11 | class BooleanLogicExpression extends Expression { 12 | 13 | private static operators = { 14 | "||": "any", 15 | "&&": "all" 16 | }; 17 | 18 | constructor(public operator:string, public expressions:Expression[]) { 19 | super(); 20 | } 21 | 22 | evaluateToIntermediate(scope:Scope, stack:Stack):any { 23 | var operator = BooleanLogicExpression.operators[this.operator]; 24 | 25 | var values:any[] = _.map( 26 | this.expressions, 27 | (expression) => { return expression.evaluate(scope, stack) } 28 | ) 29 | 30 | if (operator == "any") { 31 | if (_.all(values, isFalse)) { return false } 32 | 33 | values = _.reject(values, isFalse) 34 | 35 | if (values.length === 0) { return true } 36 | else if (values.length === 1) { return values[0] } 37 | else if (_.any(values, isTrue)) { return true } 38 | 39 | } else if (operator == "all") { 40 | if (_.all(values, isTrue)) { return true } 41 | 42 | values = _.reject(values, isTrue) 43 | 44 | if (values.length === 0) { return true } 45 | else if (values.length === 1) { return values[0] } 46 | else if (_.any(values, isFalse)) { return false } 47 | 48 | } else { 49 | assert(false) 50 | } 51 | 52 | return [operator].concat(values) 53 | } 54 | 55 | } 56 | 57 | export = BooleanLogicExpression -------------------------------------------------------------------------------- /source/expressions/ArithmeticOperatorExpression.ts: -------------------------------------------------------------------------------- 1 | import assert = require("assert"); 2 | import Expression = require("./Expression"); 3 | import Scope = require("../Scope"); 4 | import Stack = require("../Stack"); 5 | import FunctionValue = require("../values/FunctionValue") 6 | import _ = require("../utilities"); 7 | 8 | class ArithmeticOperatorExpression extends Expression { 9 | 10 | constructor(public left:Expression, public operator:string, public right:Expression) { 11 | super(); 12 | } 13 | 14 | evaluateToIntermediate(scope:Scope, stack:Stack):any { 15 | var left = this.left.evaluateToIntermediate(scope, stack); 16 | var right = this.right.evaluateToIntermediate(scope, stack); 17 | 18 | function apply(left:number, operator:string, right:number):number { 19 | if (operator == '+') { return left + right } 20 | else if (operator == '-') { return left - right } 21 | else if (operator == '*') { return left * right } 22 | else if (operator == '/') { return left / right } 23 | } 24 | 25 | if (_.isNumber(left) && _.isNumber(right)) { 26 | return apply(left, this.operator, right); 27 | 28 | } else if (_.isNumber(left) && right instanceof FunctionValue) { 29 | return new FunctionValue(right.base, <[number, number][]> _.map(right.stops, (value) => { 30 | return [value[0], apply(left, this.operator, value[1])] 31 | })); 32 | 33 | } else if (left instanceof FunctionValue && _.isNumber(right)) { 34 | return new FunctionValue(left.base, <[number, number][]> _.map(left.stops, (value) => { 35 | return [value[0], apply(value[1], this.operator, right)] 36 | })); 37 | 38 | } else { 39 | assert(false, "Can't perform arithmetic on '" + left + "' and '" + right + "'"); 40 | } 41 | 42 | } 43 | 44 | } 45 | 46 | export = ArithmeticOperatorExpression -------------------------------------------------------------------------------- /source/ExpressionSet.ts: -------------------------------------------------------------------------------- 1 | import assert = require('assert'); 2 | import Arguments = require('./Arguments'); 3 | import Scope = require("./Scope"); 4 | import Stack = require("./Stack"); 5 | import _ = require('./utilities'); 6 | import Expression = require('./expressions/Expression'); 7 | import LiteralExpression = require('./expressions/LiteralExpression'); 8 | 9 | 10 | interface Item { 11 | name?:string; 12 | expression:Expression; 13 | } 14 | 15 | class ExpressionSet { 16 | 17 | static fromPositionalExpressions(expressions:Expression[]): ExpressionSet { 18 | return new ExpressionSet( _.map(expressions, (expression: Expression): Item => { 19 | return { expression: expression } 20 | })); 21 | } 22 | 23 | static fromPositionalValues(values:any[]):ExpressionSet { 24 | return new ExpressionSet( _.map(values, (value:any):Item => { 25 | return {expression: new LiteralExpression(value)}; 26 | })); 27 | } 28 | 29 | static ZERO:ExpressionSet = new ExpressionSet([]); 30 | 31 | private isNamed_:boolean = true; 32 | private isUnnamed_:boolean = true; 33 | 34 | constructor(public items:Item[]) { 35 | for (var i in items) { 36 | assert(items[i].expression); 37 | if (items[i].name) this.isUnnamed_ = false; 38 | else this.isNamed_ = false; 39 | } 40 | } 41 | 42 | isNamed():boolean { 43 | return this.isNamed_; 44 | } 45 | 46 | isUnnamed():boolean { 47 | return this.isUnnamed_; 48 | } 49 | 50 | toArray():Expression[] { 51 | return _.pluck(this.items, 'expression'); 52 | } 53 | 54 | toArguments(scope:Scope, stack:Stack):Arguments { 55 | assert(scope instanceof Scope); 56 | return new Arguments(_.map(this.items, (item:Item) => { 57 | return { 58 | value: item.expression.evaluateToIntermediate(scope, stack), 59 | name: item.name 60 | } 61 | })); 62 | } 63 | 64 | } 65 | 66 | export = ExpressionSet; 67 | -------------------------------------------------------------------------------- /test/conditional-statements.coffee: -------------------------------------------------------------------------------- 1 | assert = require("assert") 2 | _ = require('../compiled/utilities') 3 | 4 | # TODO create high level parser wrapper 5 | Parser = require("../compiled/parser") 6 | parse = (source) -> Parser.parse(source).evaluate() 7 | 8 | describe "conditional statements", -> 9 | 10 | describe "evaluation", -> 11 | 12 | it "should evaluate true if expressions", -> 13 | 14 | stylesheet = parse """ 15 | #test { 16 | if true { 17 | type: 'background' 18 | } 19 | } 20 | """ 21 | 22 | assert.equal stylesheet.layers[0].type, 'background' 23 | 24 | it "should not evaluate false if expressions", -> 25 | 26 | stylesheet = parse """ 27 | #test { 28 | type: 'background' 29 | if false { 30 | type: 'fill' 31 | } 32 | } 33 | """ 34 | 35 | assert.equal stylesheet.layers[0].type, 'background' 36 | 37 | it "should evaluate else expressions", -> 38 | stylesheet = parse """ 39 | #test { 40 | if false { 41 | type: 'fill' 42 | } else { 43 | type: 'background' 44 | } 45 | } 46 | """ 47 | 48 | assert.equal stylesheet.layers[0].type, 'background' 49 | 50 | it "should evaluate else if expressions", -> 51 | stylesheet = parse """ 52 | #test { 53 | if false { 54 | type: 'fill' 55 | } else if true { 56 | type: 'background' 57 | } else { 58 | type: line 59 | } 60 | } 61 | """ 62 | 63 | assert.equal stylesheet.layers[0].type, 'background' 64 | 65 | it "should respect logic operators", -> 66 | 67 | stylesheet = parse """ 68 | #test { 69 | if 1 == 1 { 70 | type: 'background' 71 | } 72 | } 73 | """ 74 | assert.equal stylesheet.layers[0].type, 'background' 75 | -------------------------------------------------------------------------------- /source/expressions/ComparisonOperatorExpression.ts: -------------------------------------------------------------------------------- 1 | import Expression = require("./Expression"); 2 | import assert = require("assert"); 3 | import AttributeReferenceValue = require("../values/AttributeReferenceValue"); 4 | import Value = require('../values/Value'); 5 | import _ = require("../utilities"); 6 | import Scope = require("../Scope"); 7 | import Stack = require("../Stack"); 8 | 9 | class ComparisonOperatorExpression extends Expression { 10 | 11 | private static operators:{[operator:string]: (left:any, right:any) => boolean} = { 12 | "==": (left, right) => { return left == right; }, 13 | ">=": (left, right) => { return left >= right; }, 14 | "<=": (left, right) => { return left <= right; }, 15 | "<": (left, right) => { return left < right; }, 16 | ">": (left, right) => { return left > right; }, 17 | "!=": (left, right) => { return left != right; } 18 | } 19 | 20 | private static operatorInverses = { 21 | "==": "==", 22 | ">=": "<=", 23 | "<=": ">=", 24 | "<": ">", 25 | ">": "<", 26 | "!=": "!=" 27 | } 28 | 29 | constructor(public left, public operator, public right) { super() } 30 | 31 | evaluateToIntermediate(scope:Scope, stack:Stack):any { 32 | var left = this.left.evaluateToIntermediate(scope, stack); 33 | var right = this.right.evaluateToIntermediate(scope, stack) 34 | var operator = this.operator 35 | 36 | if (right instanceof AttributeReferenceValue) { 37 | var temp = left; 38 | left = right; 39 | right = temp; 40 | operator = ComparisonOperatorExpression.operatorInverses[operator]; 41 | } 42 | 43 | if (left instanceof AttributeReferenceValue) { 44 | assert(!(right instanceof AttributeReferenceValue)) 45 | return [operator, left.name, Value.evaluate(right)]; 46 | } else { 47 | return ComparisonOperatorExpression.operators[operator](left, right); 48 | } 49 | } 50 | 51 | } 52 | 53 | export = ComparisonOperatorExpression -------------------------------------------------------------------------------- /source/values/ColorValue.ts: -------------------------------------------------------------------------------- 1 | import Value = require("./Value"); 2 | import assert = require("assert"); 3 | import Stack = require('../Stack'); 4 | import _ = require('../utilities'); 5 | 6 | class ColorValue extends Value { 7 | 8 | static hex(hex:string):ColorValue { 9 | var rgb = _.hex2rgb(hex) 10 | return new ColorValue(rgb[0], rgb[1], rgb[2], 1) 11 | } 12 | 13 | static hsva(hue:number, saturation:number, value:number, alpha:number):ColorValue { 14 | var rgb = _.hsv2rgb(hue, saturation, value) 15 | return new ColorValue(rgb[0], rgb[1], rgb[2], alpha) 16 | } 17 | 18 | static hsla(hue:number, saturation:number, lightness:number, alpha:number):ColorValue { 19 | var rgb = _.hsl2rgb(hue, saturation, lightness) 20 | return new ColorValue(rgb[0], rgb[1], rgb[2], alpha) 21 | } 22 | 23 | static rgba(red:number, green:number, blue:number, alpha:number):ColorValue { 24 | return new ColorValue(red, green, blue, alpha) 25 | } 26 | 27 | public lightness:number; 28 | public value:number; 29 | public hue:number; 30 | public saturation:number; 31 | 32 | constructor( 33 | public red:number, 34 | public green:number, 35 | public blue:number, 36 | public alpha:number 37 | ) { 38 | super(); 39 | 40 | assert(this.red >= 0 && this.red < 256); 41 | assert(this.blue >= 0 && this.blue < 256); 42 | assert(this.green >= 0 && this.green < 256); 43 | 44 | assert(this.alpha >= 0 && this.alpha <= 1); 45 | 46 | var hsv = _.rgb2hsv(red, green, blue); 47 | var hsl = _.rgb2hsv(red, green, blue); 48 | 49 | this.hue = hsv[0]; 50 | this.saturation = hsv[1]; 51 | this.value = hsv[2]; 52 | this.lightness = hsl[2]; 53 | } 54 | 55 | evaluate():string { 56 | if (this.alpha != 1) { 57 | return "rgba(" + this.red + ", " + this.green + ", " + this.blue + ", " + this.alpha + ")"; 58 | } else { 59 | return _.rgb2hex(this.red, this.green, this.blue); 60 | } 61 | } 62 | } 63 | 64 | export = ColorValue -------------------------------------------------------------------------------- /source/server.ts: -------------------------------------------------------------------------------- 1 | var Express = require('express'); 2 | 3 | function exported(file:string) { 4 | 5 | var server = Express(); 6 | 7 | server.get('/style.json', function(req, res) { 8 | res.sendFile(file); 9 | }); 10 | 11 | server.get('/', function(req, res) { 12 | 13 | res.send(` 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 25 | 26 | 27 | 28 |
29 | 58 | 59 | 60 | 61 | `); 62 | 63 | }); 64 | 65 | return server; 66 | 67 | } 68 | 69 | export = exported; 70 | -------------------------------------------------------------------------------- /source/CLI.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | import CLI = require('commander'); 4 | import Fs = require('fs'); 5 | import Path = require('path'); 6 | import ChildProcess = require('child_process') 7 | import assert = require('assert'); 8 | import PreviewServer = require('./server'); 9 | var Parser = require('./parser'); 10 | var Package = require('../package.json'); 11 | var MapboxGLStyleSpec = require('mapbox-gl-style-spec'); 12 | var Reload = require('reload'); 13 | 14 | var ENCODING = "utf8"; 15 | 16 | CLI.version(Package.version) 17 | CLI.usage('[=stdin [=stdout]]'); 18 | CLI.option('-p, --preview', 'Start a webserver to preview changes in GL JS'); 19 | CLI.option('-w, --watch', 'Monitor the source file for changes and automatically recompile'); 20 | CLI.parse(process.argv) 21 | 22 | compile(); 23 | 24 | if (CLI['watch']) { 25 | assert(CLI.args[0]) 26 | Fs.watchFile(CLI.args[0], {interval: 100}, () => { 27 | compile(); 28 | }); 29 | } 30 | 31 | if (CLI['preview']) { 32 | assert(CLI.args[1], 'You must provide an output filename with --preview'); 33 | var server = PreviewServer(Path.resolve(CLI.args[1])); 34 | server.listen(3000, function () { 35 | console.log('Server listening at http://localhost:3000'); 36 | }); 37 | } 38 | 39 | function compile() { 40 | console.log("\n\nStarting compilation"); 41 | var inputStream = CLI.args[0] ? Fs.createReadStream(CLI.args[0]) : process.stdin; 42 | var outputStream = CLI.args[1] ? Fs.createWriteStream(CLI.args[1]) : process.stdout; 43 | 44 | inputStream.setEncoding(ENCODING); 45 | 46 | inputStream.on("error", (error) => {throw error}); 47 | outputStream.on("error", (error) => {throw error}); 48 | 49 | var input:string = "" 50 | inputStream.on("data", (chunk) => {input += chunk}) 51 | 52 | inputStream.on("end", () => { 53 | try { 54 | var output = JSON.stringify(Parser.parse(input).evaluate(), null, 2) + "\n"; 55 | MapboxGLStyleSpec.validate(output); 56 | 57 | if (outputStream == process.stdout) { 58 | outputStream.write(output, ENCODING); 59 | } else { 60 | outputStream.end(output, ENCODING); 61 | } 62 | 63 | assert(CLI.args[1]); 64 | console.log("Done compilation"); 65 | } catch (e) { 66 | console.log(e.toString()) 67 | console.log(e.stack) 68 | } 69 | }); 70 | } 71 | -------------------------------------------------------------------------------- /source/utilities/object.ts: -------------------------------------------------------------------------------- 1 | import _ = require('underscore'); 2 | 3 | export class Utilities { 4 | 5 | objectMapValues( 6 | input:_.Dictionary, 7 | iterator:_.ObjectIterator, 8 | stack?:any 9 | ):_.Dictionary; 10 | 11 | objectMapValues( 12 | input:_.List, 13 | iterator:_.ListIterator, 14 | stack?:any 15 | ):_.Dictionary; 16 | 17 | objectMapValues(input, iterator, stack = {}):any { 18 | return this.objectMap( 19 | input, 20 | (value, key) => { return [key, iterator(value, key)] } 21 | ); 22 | } 23 | 24 | objectMapKeys( 25 | input:_.Dictionary, 26 | iterator:_.ObjectIterator, 27 | stack?:any 28 | ):_.Dictionary; 29 | 30 | objectMapKeys( 31 | input:_.List, 32 | iterator:_.ListIterator, 33 | stack?:any 34 | ):_.Dictionary; 35 | 36 | objectMapKeys(input, iterator, stack = {}):any { 37 | return this.objectMap( 38 | input, 39 | (value, key) => { return [iterator(value, key), value] }, 40 | stack 41 | ) 42 | } 43 | 44 | objectMap( 45 | input:_.Dictionary, 46 | iterator:_.ObjectIterator, 47 | stack?:any 48 | ):_.Dictionary; 49 | 50 | objectMap( 51 | input:_.List, 52 | iterator:_.ListIterator, 53 | stack?:any 54 | ):_.Dictionary; 55 | 56 | objectMap(input, iterator, stack = {}):any { 57 | var output = {} 58 | _.each(input, (inputValue, inputKey) => { 59 | var tuple = iterator(inputValue, inputKey); 60 | var outputKey = tuple[0]; 61 | var outputValue = tuple[1]; 62 | output[outputKey] = outputValue; 63 | }) 64 | return output; 65 | } 66 | 67 | objectZip( 68 | keys:string[], 69 | values:T[] 70 | ):{[key:string]: T} { 71 | var output:{[key:string]: T} = {} 72 | for (var i=0; i( 79 | input:_.Dictionary, 80 | iterator:(value:T, key:string) => boolean 81 | ):_.Dictionary { 82 | var output:_.Dictionary = {}; 83 | _.each(input, (value:T, key:string) => { 84 | if (iterator(value, key)) { 85 | output[key] = value; 86 | } 87 | }); 88 | return output; 89 | } 90 | 91 | objectCompact( 92 | input:_.Dictionary 93 | ):_.Dictionary { 94 | return this.objectFilter(input, (value) => { return !(value === undefined || value === null) }); 95 | } 96 | 97 | } -------------------------------------------------------------------------------- /test/loops.coffee: -------------------------------------------------------------------------------- 1 | assert = require("assert") 2 | _ = require('../compiled/utilities') 3 | 4 | Parser = require("../compiled/parser") 5 | parse = (source) -> Parser.parse(source).evaluate() 6 | 7 | describe "loops", -> 8 | 9 | it "should iterate over an array's values to create layers", -> 10 | stylesheet = parse """ 11 | for value in [0,1,2] { 12 | # { scree-test-meta: value } 13 | } 14 | """ 15 | assert.equal stylesheet.layers[0]['scree-test-meta'], 0 16 | assert.equal stylesheet.layers[1]['scree-test-meta'], 1 17 | assert.equal stylesheet.layers[2]['scree-test-meta'], 2 18 | 19 | it "should iterate over an object's values to create layers", -> 20 | stylesheet = parse """ 21 | for value in { zero:0; one:1; two:2 } { 22 | # { scree-test-meta: value } 23 | } 24 | """ 25 | assert.equal stylesheet.layers[0]['scree-test-meta'], 0 26 | assert.equal stylesheet.layers[1]['scree-test-meta'], 1 27 | assert.equal stylesheet.layers[2]['scree-test-meta'], 2 28 | 29 | it "should iterate over an object's keys and values to create layers", -> 30 | stylesheet = parse """ 31 | for key value in {zero:0; one:1; two:2} { 32 | # { scree-test-meta: [key, value] } 33 | } 34 | """ 35 | assert.deepEqual stylesheet.layers[0]['scree-test-meta'], ['zero', 0] 36 | assert.deepEqual stylesheet.layers[1]['scree-test-meta'], ['one', 1] 37 | assert.deepEqual stylesheet.layers[2]['scree-test-meta'], ['two', 2] 38 | 39 | it "should iterate over an array's keys and values to create layers", -> 40 | stylesheet = parse """ 41 | for key value in [2, 4, 6] { 42 | # { scree-test-meta: [key, value] } 43 | } 44 | """ 45 | assert.deepEqual stylesheet.layers[0]['scree-test-meta'], [0, 2] 46 | assert.deepEqual stylesheet.layers[1]['scree-test-meta'], [1, 4] 47 | assert.deepEqual stylesheet.layers[2]['scree-test-meta'], [2, 6] 48 | 49 | it "should place sublayers in the right order", -> 50 | stylesheet = parse """ 51 | # { scree-test-meta: 1 } 52 | for value in [1] { 53 | # { scree-test-meta: 0; z-index: -1 } 54 | # { scree-test-meta: 2 } 55 | # { scree-test-meta: 4; z-index: 1 } 56 | } 57 | # { scree-test-meta: 3 } 58 | """ 59 | assert.equal stylesheet.layers[0]['scree-test-meta'], 0 60 | assert.equal stylesheet.layers[1]['scree-test-meta'], 1 61 | assert.equal stylesheet.layers[2]['scree-test-meta'], 2 62 | assert.equal stylesheet.layers[3]['scree-test-meta'], 3 63 | assert.equal stylesheet.layers[4]['scree-test-meta'], 4 64 | -------------------------------------------------------------------------------- /test/layers.coffee: -------------------------------------------------------------------------------- 1 | assert = require("assert") 2 | _ = require('../compiled/utilities') 3 | 4 | Parser = require("../compiled/parser") 5 | parse = (source) -> Parser.parse(source).evaluate() 6 | 7 | describe "layers", -> 8 | 9 | it "should be in the stylesheet", -> 10 | stylesheet = parse "#test {type: 'background'}" 11 | assert stylesheet.layers[0] 12 | 13 | it "should be allowed to be anonymous", -> 14 | stylesheet = parse "# {type: 'background'}" 15 | assert.equal stylesheet.layers[0].type, "background" 16 | assert stylesheet.layers[0].id 17 | 18 | it "should respect its name", -> 19 | stylesheet = parse "#test {type: 'background'}" 20 | assert.equal stylesheet.layers[0].id, "test" 21 | 22 | it "should respect layout properties", -> 23 | stylesheet = parse "#test { type: 'line'; line-cap: 'square' }" 24 | assert.equal stylesheet.layers[0].layout['line-cap'], 'square' 25 | 26 | it "should respect paint properties", -> 27 | stylesheet = parse "#test { type: 'line'; line-color: 'red' }" 28 | assert.equal stylesheet.layers[0].paint['line-color'], 'red' 29 | 30 | it "should respect meta properties", -> 31 | stylesheet = parse "#test { type: 'background'; scree-test-meta: 'bar' }" 32 | assert.equal stylesheet.layers[0]['scree-test-meta'], "bar" 33 | 34 | it "should respect its filter", -> 35 | stylesheet = parse "#test { type: 'background'; filter: @name == 'foo' }" 36 | assert stylesheet.layers[0].filter 37 | 38 | describe "sublayers", -> 39 | 40 | it "should not have an empty layers array if no sublayers", -> 41 | stylesheet = parse "#test { type: 'background'; }" 42 | assert !stylesheet.layers[0].layers 43 | 44 | it "should allow sublayers", -> 45 | stylesheet = parse """ 46 | #parent { 47 | 48 | #child { 49 | type: 'background' 50 | background-color: 'red' 51 | } 52 | 53 | raster-opacity: 1; 54 | } 55 | """ 56 | assert.equal stylesheet.layers[0].type, 'raster' 57 | assert.equal stylesheet.layers[0].paint['raster-opacity'], 1 58 | assert.equal stylesheet.layers[0].layers.length, 1 59 | assert.equal stylesheet.layers[0].layers[0].type, 'background' 60 | assert.equal stylesheet.layers[0].layers[0].paint['background-color'], 'red' 61 | 62 | it "should fail if sublayers are defined but type is not 'raster'", -> 63 | 64 | assert.throws -> 65 | 66 | stylesheet = parse """ 67 | #parent { 68 | type: 'background' 69 | #child { 70 | type: 'background' 71 | } 72 | } 73 | """ -------------------------------------------------------------------------------- /test/property-macro.coffee: -------------------------------------------------------------------------------- 1 | assert = require("assert") 2 | 3 | Parser = require("../compiled/parser") 4 | parse = (source) -> Parser.parse(source).evaluate() 5 | 6 | describe "property macros", -> 7 | 8 | it "should shadow property macros in enclosing scopes", -> 9 | stylesheet = parse """ 10 | foo(value) = { scree-test-meta: value } 11 | #layer { 12 | foo = { foo(17) } 13 | type: 'background' 14 | foo() 15 | } 16 | """ 17 | assert.equal stylesheet.layers[0]['scree-test-meta'], 17 18 | 19 | it "should allow namespaced identifiers", -> 20 | stylesheet = parse """ 21 | baz::foo = { scree-test-meta: 17 } 22 | #layer { baz::foo() } 23 | """ 24 | assert.equal stylesheet.layers[0]['scree-test-meta'], 17 25 | 26 | describe "arguments", -> 27 | 28 | it "should accept no arguments with parenthesis", -> 29 | stylesheet = parse """ 30 | property = { scree-test-meta: "bar" } 31 | #layer { type: 'background'; property() } 32 | """ 33 | assert.equal stylesheet.layers[0]['scree-test-meta'], "bar" 34 | 35 | 36 | it "should accept property-style arguments", -> 37 | stylesheet = parse """ 38 | foo(one, two) = { scree-test-meta: two } 39 | #layer { type: 'background'; foo("baz" "bar") } 40 | """ 41 | assert.equal stylesheet.layers[0]['scree-test-meta'], "bar" 42 | 43 | it "should apply value macros", -> 44 | stylesheet = parse """ 45 | foo(value) = { scree-test-meta: identity(value) } 46 | #layer { type: 'background'; foo("bar") } 47 | """ 48 | assert.equal stylesheet.layers[0]['scree-test-meta'], "bar" 49 | 50 | it "should apply other property macros", -> 51 | stylesheet = parse """ 52 | inner(value) = { scree-test-meta: value } 53 | outer(value) = { inner(value) } 54 | #layer { type: 'background'; outer("bar") } 55 | """ 56 | assert.equal stylesheet.layers[0]['scree-test-meta'], "bar" 57 | 58 | it "should respect default arguments", -> 59 | stylesheet = parse """ 60 | foo(one, two = #fff) = { scree-test-meta: two } 61 | #layer {type: 'background'; foo(0)} 62 | """ 63 | assert.equal stylesheet.layers[0]['scree-test-meta'], '#ffffff' 64 | 65 | 66 | it "should select a property macro by the number of arguments supplied", -> 67 | stylesheet = parse """ 68 | foo = { scree-test-meta: 0 } 69 | foo(value) = { scree-test-meta: value } 70 | #layer { type: 'background'; foo(17) } 71 | """ 72 | assert.equal stylesheet.layers[0]['scree-test-meta'], 17 73 | 74 | it "should be able to contain layers", -> 75 | stylesheet = parse """ 76 | foo(value) = { # { scree-test-meta: value } } 77 | foo(17) 78 | """ 79 | assert.equal stylesheet.layers[0]['scree-test-meta'], 17 80 | -------------------------------------------------------------------------------- /test/utilities.coffee: -------------------------------------------------------------------------------- 1 | _ = require "../compiled/utilities" 2 | assert = require "assert" 3 | 4 | describe 'utilities', -> 5 | 6 | describe 'objectMap', -> 7 | it 'should map arrays', -> 8 | input = [16] 9 | output = _.objectMap input, (value, key) -> [key + 1, value + 1] 10 | assert.deepEqual(output, "1": 17) 11 | 12 | it 'should map objects', -> 13 | lowercase = {foo: 'bar'} 14 | uppercase = _.objectMap lowercase, (value, key) -> 15 | [key.toUpperCase(), value.toUpperCase()] 16 | assert.deepEqual(uppercase, FOO: "BAR") 17 | 18 | it 'should map empty arrays', -> 19 | assert.deepEqual _.objectMap([], _.identity), {} 20 | 21 | it 'should map empty objects', -> 22 | assert.deepEqual _.objectMap({}, _.identity), {} 23 | 24 | describe 'objectMapKeys', -> 25 | it 'should map objects', -> 26 | lowercase = {foo: 'bar'} 27 | uppercase = _.objectMapKeys lowercase, (value, key) -> key.toUpperCase() 28 | assert.deepEqual(uppercase, FOO: "bar") 29 | 30 | describe 'objectMapValues', -> 31 | it 'should map objects', -> 32 | lowercase = {foo: 'bar'} 33 | uppercase = _.objectMapValues lowercase, (value, key) -> value.toUpperCase() 34 | assert.deepEqual(uppercase, foo: "BAR") 35 | 36 | describe 'objectZip', -> 37 | it 'zip two arrays into an object', -> 38 | assert.deepEqual(_.objectZip(["foo"], ["bar"]), foo: "bar") 39 | 40 | it 'zip two empty arrays into an empty object', -> 41 | assert.deepEqual(_.objectZip([], []), {}) 42 | 43 | describe 'none', -> 44 | it 'should return true for an empty array', -> 45 | assert _.none([]) 46 | 47 | it 'should true for an array of only falses', -> 48 | assert _.none([false, false, false]) 49 | 50 | it 'should false for an array with one true', -> 51 | assert !_.none([false, true, false]) 52 | 53 | it 'should respect a custom predicate', -> 54 | assert _.none([true, true, true], (value) -> !value) 55 | 56 | describe 'map method', -> 57 | 58 | it 'should return an empty array for an empty array', -> 59 | assert.deepEqual _.mapMethod([] ,"foo"), [] 60 | 61 | it 'should run the method', -> 62 | assert.deepEqual _.mapMethod([{foo: => "bar"}] ,"foo"), ["bar"] 63 | 64 | it 'should run the method with args', -> 65 | assert.deepEqual _.mapMethod([{foo: (value) => value}] ,"foo", "bar"), ["bar"] 66 | 67 | describe 'count', -> 68 | it 'should return the number of trues', -> 69 | assert.equal _.count([true, false, true]), 2 70 | 71 | it 'should return 0 if there are no trues', -> 72 | assert.equal _.count([false, false]), 0 73 | 74 | it 'should return 0 for an empty array', -> 75 | assert.equal _.count([]), 0 76 | 77 | it 'should respect a custom predicate', -> 78 | assert.equal _.count([true, false, true], (value) -> !value), 1 79 | 80 | -------------------------------------------------------------------------------- /source/index.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | export var parser = require("./parser"); 4 | export import eval = require("./eval"); 5 | export import Scope = require("./Scope"); 6 | export import Stack = require("./Stack"); 7 | export import utilities = require("./utilities"); 8 | export import _ = require("./utilities"); 9 | export import ExpressionSet = require("./ExpressionSet"); 10 | export import Arguments = require("./Arguments"); 11 | export import ArgumentsDefinition = require("./ArgumentsDefinition"); 12 | export import Macro = require("./Macro"); 13 | 14 | export import Statement = require("./statements/Statement"); 15 | export import ClassStatement = require("./statements/ClassStatement"); 16 | export import ConditionalStatement = require("./statements/ConditionalStatement"); 17 | export import LayerStatement = require("./statements/LayerStatement"); 18 | export import LoopStatement = require("./statements/LoopStatement"); 19 | export import PropertyStatement = require("./statements/PropertyStatement"); 20 | export import MacroDefinitionStatement = require("./statements/MacroDefinitionStatement"); 21 | export import MacroReferenceStatement = require("./statements/MacroReferenceStatement"); 22 | export import JavascriptStatement = require("./statements/JavascriptStatement"); 23 | 24 | export import Expression = require("./expressions/Expression"); 25 | export import ArithmeticOperatorExpression = require("./expressions/ArithmeticOperatorExpression"); 26 | export import ArrayExpression = require("./expressions/ArrayExpression"); 27 | export import BooleanLogicExpression = require("./expressions/BooleanLogicExpression"); 28 | export import ComparisonOperatorExpression = require("./expressions/ComparisonOperatorExpression"); 29 | export import JavascriptExpression = require("./expressions/JavascriptExpression"); 30 | export import LiteralExpression = require("./expressions/LiteralExpression"); 31 | export import ScopeExpression = require("./expressions/ScopeExpression"); 32 | export import NotOperatorExpression = require("./expressions/NotOperatorExpression"); 33 | export import NullCoalescingExpression = require("./expressions/NullCoalescingExpression"); 34 | export import SetOperatorExpression = require("./expressions/SetOperatorExpression"); 35 | export import StringExpression = require("./expressions/StringExpression"); 36 | export import PropertyAccessExpression = require("./expressions/PropertyAccessExpression"); 37 | export import TernaryExpression = require("./expressions/TernaryExpression"); 38 | export import TypeCheckOperatorExpression = require("./expressions/TypeCheckOperatorExpression"); 39 | export import MacroReferenceExpression = require("./expressions/MacroReferenceExpression"); 40 | 41 | export import AttributeReferenceValue = require("./values/AttributeReferenceValue"); 42 | export import ColorValue = require("./values/ColorValue"); 43 | export import FunctionValue = require("./values/FunctionValue"); 44 | export import ScopeValue = require("./values/ScopeValue"); 45 | export import Value = require("./values/Value"); 46 | -------------------------------------------------------------------------------- /source/scopes/layer.ts: -------------------------------------------------------------------------------- 1 | import Stack = require('../Stack'); 2 | import Scope = require('../Scope'); 3 | import _ = require('../utilities'); 4 | import Value = require('../values/Value'); 5 | import assert = require('assert'); 6 | var MBGLStyleSpec = require('mapbox-gl-style-spec'); 7 | 8 | function evaluateLayerScope(stack:Stack, properties:{}, layers:Scope[], _classes:Scope[]):any { 9 | var metaProperties = { 'z-index': 0 }; 10 | var paintProperties = {}; 11 | var layoutProperties = {}; 12 | var source = {}; 13 | 14 | var type = properties['type'] || 'raster'; 15 | 16 | for (var name in properties) { 17 | var value = Value.evaluate(properties[name]); 18 | 19 | if (name == 'z-index') { 20 | metaProperties['z-index'] = value; 21 | 22 | } else if (name == "source-tile-size") { 23 | source["tileSize"] = value; 24 | 25 | } else if (_.startsWith(name, "source-") && name != "source-layer") { 26 | source[name.substr("source-".length)] = value; 27 | 28 | } else if (getPropertyType(name) == PropertyType.PAINT) { 29 | paintProperties[name] = value; 30 | 31 | } else if (getPropertyType(name) == PropertyType.LAYOUT) { 32 | layoutProperties[name] = value; 33 | 34 | } else if (getPropertyType(name) == PropertyType.META) { 35 | metaProperties[name] = value; 36 | 37 | } else { 38 | assert(false, "Property name '" + name + "' is unknown"); 39 | } 40 | } 41 | 42 | if (!_.isEmpty(source)) { 43 | metaProperties["source"] = stack.getGlobalScope().addSource(source); 44 | } 45 | 46 | if (layers.length) { 47 | if (metaProperties['type']) { 48 | assert.equal(metaProperties['type'], 'raster'); 49 | } else { 50 | metaProperties['type'] = 'raster'; 51 | } 52 | } else { 53 | layers = undefined; 54 | } 55 | 56 | var classes = _.objectMap(_classes, (scope) => { 57 | return ["paint." + scope.name, scope] 58 | }); 59 | 60 | return _.extend( 61 | { 62 | id: this.name || _.uniqueId('scope'), 63 | layers: layers, 64 | paint: paintProperties, 65 | layout: layoutProperties 66 | }, 67 | metaProperties, 68 | classes 69 | ); 70 | } 71 | 72 | enum PropertyType { PAINT, LAYOUT, META } 73 | 74 | function getPropertyType(name: string): PropertyType { 75 | if (name == 'scree-test-paint') return PropertyType.PAINT; 76 | else if (name == 'scree-test-layout') return PropertyType.LAYOUT; 77 | else if (name == 'scree-test-meta') return PropertyType.META; 78 | else { 79 | var spec = MBGLStyleSpec.latest; 80 | 81 | for (var i in spec["layout"]) { 82 | for (var name_ in spec[spec["layout"][i]]) { 83 | if (name == name_) return PropertyType.LAYOUT; 84 | } 85 | } 86 | 87 | for (var i in spec["paint"]) { 88 | for (var name_ in spec[spec["paint"][i]]) { 89 | if (name == name_) return PropertyType.PAINT; 90 | } 91 | } 92 | 93 | assert(spec["layer"][name]); 94 | return PropertyType.META; 95 | } 96 | } 97 | 98 | 99 | export = evaluateLayerScope; 100 | -------------------------------------------------------------------------------- /test/filter.coffee: -------------------------------------------------------------------------------- 1 | assert = require("assert") 2 | 3 | Parser = require("../compiled/parser") 4 | parse = (source) -> Parser.parse(source).evaluate() 5 | 6 | describe "filters", -> 7 | 8 | parseFilter = (filter) -> 9 | stylesheet = parse "#layer { type: 'background'; filter: #{filter} }" 10 | stylesheet.layers[0].filter 11 | 12 | describe "typecheck operator", -> 13 | it "should parse the 'is' operator", -> 14 | assert.deepEqual parseFilter("is 'LineString'"), ["==", "$type", "LineString"] 15 | 16 | describe "comparison operator", -> 17 | for operator in ["==", ">=", "<=", "<", ">", "!="] 18 | it "should parse the '#{operator}' operator with attribute references", -> 19 | assert.deepEqual parseFilter("@foo #{operator} 'bar'"), [operator, "foo", "bar"] 20 | 21 | it "should parse the '==' operator with literals", -> 22 | assert.equal parseFilter("1 == 1"), true 23 | assert.equal parseFilter("1 == 2"), false 24 | 25 | it "should parse the '>' operator with literals", -> 26 | assert.deepEqual parseFilter("1 > 0"), true 27 | assert.deepEqual parseFilter("0 > 0"), false 28 | 29 | describe "set operator expression", -> 30 | 31 | it "should parse the 'in' operator with an attribute reference", -> 32 | assert.deepEqual parseFilter("@foo in [1 2 3]"), ["in", "foo", 1, 2, 3] 33 | 34 | it "should parse the '!in' operator with an attribute reference", -> 35 | assert.deepEqual parseFilter("@foo !in [1 2 3]"), ["!in", "foo", 1, 2, 3] 36 | 37 | it "should parse the 'in' operator with a literal value", -> 38 | assert.equal parseFilter("1 in [1 2 3]"), true 39 | assert.equal parseFilter("0 in [1 2 3]"), false 40 | 41 | it "should parse the '!in' operator with a literal value", -> 42 | assert.equal parseFilter("0 !in [1 2 3]"), true 43 | assert.equal parseFilter("1 !in [1 2 3]"), false 44 | 45 | it "should throw an error if the rvalue isn't an array value", -> 46 | assert.throws -> 47 | parseFilter("@foo in 1") 48 | 49 | describe "boolean logic operator expression", -> 50 | 51 | it "should parse the '&&' operator with attribute references", -> 52 | assert.deepEqual( 53 | parseFilter("@foo == 1 && @bar == 2"), 54 | ["all", ["==", "foo", 1], ["==", "bar", 2]] 55 | ) 56 | 57 | it "should parse the '||' operator with attribute references", -> 58 | assert.deepEqual( 59 | parseFilter("@foo == 1 || @bar == 2"), 60 | ["any", ["==", "foo", 1], ["==", "bar", 2]] 61 | ) 62 | 63 | it "should parse the '!' operator with attribute references", -> 64 | assert.deepEqual( 65 | parseFilter("!@foo == 1"), 66 | ["none", ["==", "foo", 1]] 67 | ) 68 | 69 | it "should parse the '&&' operator with literals", -> 70 | assert.equal(parseFilter("1 == 1 && 2 == 2"), true) 71 | assert.equal(parseFilter("1 == 1 && 1 == 2"), false) 72 | 73 | it "should parse the '||' operator with literals", -> 74 | assert.equal(parseFilter("1 == 2 || 2 == 2"), true) 75 | assert.equal(parseFilter("1 == 2 || 1 == 2"), false) 76 | 77 | it "should parse the '&&' operator with literals and attribute references", -> 78 | assert.deepEqual( 79 | parseFilter("1 == 1 && @foo == 1 && @bar == 2"), 80 | ["all", ["==", "foo", 1], ["==", "bar", 2]] 81 | ) 82 | assert.deepEqual(parseFilter("1 == 1 && @foo == 1"), ["==", "foo", 1]) 83 | assert.equal(parseFilter("1 == 2 && @foo == 1"), false) 84 | -------------------------------------------------------------------------------- /example.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 8, 3 | "layers": [ 4 | { 5 | "id": "scope82", 6 | "paint": { 7 | "background-color": "#eedddd" 8 | }, 9 | "layout": {}, 10 | "z-index": 0, 11 | "type": "background" 12 | }, 13 | { 14 | "id": "scope83", 15 | "paint": { 16 | "fill-color": "#000000", 17 | "fill-opacity": 0.094 18 | }, 19 | "layout": {}, 20 | "z-index": 0, 21 | "type": "fill", 22 | "source-layer": "hillshade", 23 | "filter": [ 24 | "==", 25 | "level", 26 | 94 27 | ], 28 | "source": "680519890" 29 | }, 30 | { 31 | "id": "scope84", 32 | "paint": { 33 | "fill-color": "#000000", 34 | "fill-opacity": 0.09000000000000001 35 | }, 36 | "layout": {}, 37 | "z-index": 0, 38 | "type": "fill", 39 | "source-layer": "hillshade", 40 | "filter": [ 41 | "==", 42 | "level", 43 | 90 44 | ], 45 | "source": "680519890" 46 | }, 47 | { 48 | "id": "scope85", 49 | "paint": { 50 | "fill-color": "#000000", 51 | "fill-opacity": 0.08900000000000001 52 | }, 53 | "layout": {}, 54 | "z-index": 0, 55 | "type": "fill", 56 | "source-layer": "hillshade", 57 | "filter": [ 58 | "==", 59 | "level", 60 | 89 61 | ], 62 | "source": "680519890" 63 | }, 64 | { 65 | "id": "scope86", 66 | "paint": { 67 | "fill-color": "#000000", 68 | "fill-opacity": 0.07800000000000001 69 | }, 70 | "layout": {}, 71 | "z-index": 0, 72 | "type": "fill", 73 | "source-layer": "hillshade", 74 | "filter": [ 75 | "==", 76 | "level", 77 | 78 78 | ], 79 | "source": "680519890" 80 | }, 81 | { 82 | "id": "scope87", 83 | "paint": { 84 | "fill-color": "#000000", 85 | "fill-opacity": 0.067 86 | }, 87 | "layout": {}, 88 | "z-index": 0, 89 | "type": "fill", 90 | "source-layer": "hillshade", 91 | "filter": [ 92 | "==", 93 | "level", 94 | 67 95 | ], 96 | "source": "680519890" 97 | }, 98 | { 99 | "id": "scope88", 100 | "paint": { 101 | "fill-color": "#000000", 102 | "fill-opacity": 0.05600000000000001 103 | }, 104 | "layout": {}, 105 | "z-index": 0, 106 | "type": "fill", 107 | "source-layer": "hillshade", 108 | "filter": [ 109 | "==", 110 | "level", 111 | 56 112 | ], 113 | "source": "680519890" 114 | }, 115 | { 116 | "id": "scope89", 117 | "paint": { 118 | "line-opacity": 0.2, 119 | "line-width": 1, 120 | "line-color": "#333333" 121 | }, 122 | "layout": {}, 123 | "z-index": 0, 124 | "source-layer": "contour", 125 | "type": "line", 126 | "source": "680519890" 127 | }, 128 | { 129 | "id": "scope90", 130 | "paint": { 131 | "line-opacity": 0.2, 132 | "line-width": 1.5, 133 | "line-color": "#333333" 134 | }, 135 | "layout": {}, 136 | "z-index": 0, 137 | "source-layer": "contour", 138 | "type": "line", 139 | "filter": [ 140 | "==", 141 | "index", 142 | 5 143 | ], 144 | "source": "680519890" 145 | } 146 | ], 147 | "sources": { 148 | "680519890": { 149 | "type": "vector", 150 | "url": "mapbox://mapbox.mapbox-terrain-v2" 151 | } 152 | }, 153 | "transition": {} 154 | } 155 | -------------------------------------------------------------------------------- /source/Arguments.ts: -------------------------------------------------------------------------------- 1 | import assert = require('assert'); 2 | import ArgumentsDefinition = require('./ArgumentsDefinition'); 3 | import Value = require('./values/Value'); 4 | import Scope = require("./Scope"); 5 | import Stack = require("./Stack"); 6 | import _ = require('./utilities'); 7 | import Expression = require('./expressions/Expression'); 8 | 9 | interface Item { 10 | name?:string; 11 | value:any; 12 | } 13 | 14 | class Arguments { 15 | 16 | static fromPositionalValues(values:any[]):Arguments { 17 | return this.fromValues( _.map(values, (value:any):Item => { 18 | return {value: value} 19 | })); 20 | } 21 | 22 | static fromValues(values:Item[]):Arguments { 23 | return new Arguments(values); 24 | } 25 | 26 | static ZERO: Arguments = new Arguments([]); 27 | 28 | public length:number; 29 | public positional:any[]; 30 | public named:{[s:string]:any}; 31 | 32 | constructor(items:Item[]) { 33 | this.positional = []; 34 | this.named = {}; 35 | 36 | for (var i in items) { 37 | var item = items[i]; 38 | 39 | if (item.name !== undefined) { 40 | this.named[item.name] = item.value; 41 | } else { 42 | this.positional.push(item.value); 43 | } 44 | } 45 | 46 | this.length = this.positional.length + _.values(this.named).length; 47 | } 48 | 49 | matches(argsDefinition:ArgumentsDefinition):boolean { 50 | assert(argsDefinition != null); 51 | 52 | if (argsDefinition.isWildcard()) return true; 53 | 54 | var indicies = _.times(argsDefinition.length, () => { return false }); 55 | 56 | // Mark named arguments 57 | for (var name in this.named) { 58 | var value = this.named[name]; 59 | if (!argsDefinition.named[name]) { return false; } 60 | indicies[argsDefinition.named[name].index] = true 61 | } 62 | 63 | // Mark positional arguments 64 | var positionalIndex = -1 65 | for (var i in this.positional) { 66 | var value = this.positional[i] 67 | while (indicies[++positionalIndex] && positionalIndex < argsDefinition.definitions.length) {} 68 | if (positionalIndex >= argsDefinition.definitions.length) { return false } 69 | indicies[positionalIndex] = true; 70 | } 71 | 72 | // Mark default arguments 73 | for (var i in argsDefinition.definitions) { 74 | var definition = argsDefinition.definitions[i]; 75 | if (definition.expression) { 76 | indicies[definition.index] = true; 77 | } 78 | } 79 | 80 | return _.all(indicies); 81 | } 82 | 83 | toObject(argsDefinition:ArgumentsDefinition, stack:Stack):{[name:string]: any} { 84 | 85 | assert(this.matches(argsDefinition)); 86 | 87 | if (!argsDefinition || argsDefinition.isWildcard()) { 88 | return _.extend( 89 | _.objectMap( 90 | this.positional, 91 | (values, index) => { return [index.toString(), values]; } 92 | ), 93 | this.named 94 | ); 95 | 96 | } else { 97 | var args:{[s:string]: any} = {}; 98 | 99 | for (var name in this.named) { 100 | var value = this.named[name]; 101 | args[name] = value 102 | } 103 | 104 | var positionalIndex = 0 105 | for (var i in argsDefinition.definitions) { 106 | var definition = argsDefinition.definitions[i]; 107 | if (!args[definition.name]) { 108 | if (positionalIndex < this.positional.length) { 109 | args[definition.name] = this.positional[positionalIndex++] 110 | } else { 111 | args[definition.name] = definition.expression.evaluateToIntermediate(argsDefinition.scope, stack) 112 | } 113 | } 114 | } 115 | } 116 | 117 | return args; 118 | } 119 | } 120 | 121 | export = Arguments; 122 | -------------------------------------------------------------------------------- /test/whitespace.coffee: -------------------------------------------------------------------------------- 1 | assert = require("assert") 2 | _ = require("../compiled/utilities") 3 | 4 | Parser = require("../compiled/parser") 5 | parse = (source) -> Parser.parse(source).evaluate() 6 | 7 | describe "whitespace", -> 8 | 9 | it "should allow for a scope on one line", -> 10 | stylesheet = parse "#test { type: 'background'; background-color: 'red' }" 11 | assert.deepEqual stylesheet.layers[0].type, 'background' 12 | assert.deepEqual stylesheet.layers[0].paint['background-color'], 'red' 13 | 14 | it "should allow for a scope on one line with a trailing semicolon", -> 15 | stylesheet = parse "#test { type: 'background'; background-color: 'red'; }" 16 | assert.deepEqual stylesheet.layers[0].type, 'background' 17 | assert.deepEqual stylesheet.layers[0].paint['background-color'], 'red' 18 | 19 | it "should allow for a scope with seperated by newlines", -> 20 | stylesheet = parse ''' 21 | #test { 22 | type: 'background' 23 | background-color: 'red' 24 | } 25 | ''' 26 | assert.deepEqual stylesheet.layers[0].type, 'background' 27 | assert.deepEqual stylesheet.layers[0].paint['background-color'], 'red' 28 | 29 | it "should allow for a scope seperated by semicolons and newlines", -> 30 | stylesheet = parse ''' 31 | #test { 32 | type: 'background'; 33 | background-color: 'red' 34 | } 35 | ''' 36 | assert.deepEqual stylesheet.layers[0].type, 'background' 37 | assert.deepEqual stylesheet.layers[0].paint['background-color'], 'red' 38 | 39 | it "should allow for a scope seperated by semicolons and newlines with a trailing semicolon", -> 40 | stylesheet = parse ''' 41 | #test { 42 | type: 'background'; 43 | background-color: 'red'; 44 | } 45 | ''' 46 | assert.deepEqual stylesheet.layers[0].type, 'background' 47 | assert.deepEqual stylesheet.layers[0].paint['background-color'], 'red' 48 | 49 | it "should allow for a scope with empty lines", -> 50 | stylesheet = parse ''' 51 | #test { 52 | type: 'background'; 53 | } 54 | ''' 55 | assert.deepEqual stylesheet.layers[0].type, 'background' 56 | 57 | it "should allow for a value macro's arguments to span multiple lines", -> 58 | stylesheet = parse """ 59 | second(one two) = two 60 | #layer { 61 | type: second( 62 | 'fill' 63 | 'background' 64 | ) 65 | } 66 | """ 67 | assert.equal stylesheet.layers[0].type, 'background' 68 | 69 | it "should allow for an array's values to span multiple lines", -> 70 | stylesheet = parse """ 71 | #layer { 72 | type: 'background' 73 | scree-test-meta: [ 74 | 1 75 | 2 76 | ] 77 | } 78 | """ 79 | assert.deepEqual stylesheet.layers[0]['scree-test-meta'], [1, 2] 80 | 81 | it "should allow for a property macro's values to span multiple lines within parenthesis", -> 82 | stylesheet = parse """ 83 | macro(one, two) = { scree-test-meta: two } 84 | #layer { 85 | macro( 86 | 'fill' 87 | 'background' 88 | ) 89 | } 90 | """ 91 | assert.deepEqual stylesheet.layers[0]['scree-test-meta'], 'background' 92 | 93 | describe 'comments', -> 94 | it "should be ignored at the end of a line", -> 95 | stylesheet = parse ''' 96 | #test { // test 97 | type: 'background' // test 98 | background-color: 'red' // test 99 | } // test 100 | ''' 101 | assert.deepEqual stylesheet.layers[0].type, 'background' 102 | assert.deepEqual stylesheet.layers[0].paint['background-color'], 'red' 103 | 104 | it "should be ignored on its own line", -> 105 | stylesheet = parse ''' 106 | // test 107 | #test { 108 | // test 109 | type: 'background' 110 | // test 111 | background-color: 'red' 112 | // test 113 | } 114 | //test 115 | ''' 116 | assert.deepEqual stylesheet.layers[0].type, 'background' 117 | assert.deepEqual stylesheet.layers[0].paint['background-color'], 'red' 118 | -------------------------------------------------------------------------------- /source/utilities/color.ts: -------------------------------------------------------------------------------- 1 | export class Utilities { 2 | 3 | /** 4 | * Converts an RGB color value to HSL. Conversion formula 5 | * adapted from http://en.wikipedia.org/wiki/HSL_color_space. 6 | * Assumes r, g, and b are contained in the set [0, 255] and 7 | * returns h, s, and l in the set [0, 1]. 8 | * 9 | * @param Number r The red color value 10 | * @param Number g The green color value 11 | * @param Number b The blue color value 12 | * @return Array The HSL representation 13 | */ 14 | rgb2hsl(r, g, b){ 15 | r /= 255, g /= 255, b /= 255; 16 | var max = Math.max(r, g, b), min = Math.min(r, g, b); 17 | var h, s, l = (max + min) / 2; 18 | 19 | if (max == min){ 20 | h = s = 0; // achromatic 21 | } else { 22 | var d = max - min; 23 | s = l > 0.5 ? d / (2 - max - min) : d / (max + min); 24 | switch(max){ 25 | case r: h = (g - b) / d + (g < b ? 6 : 0); break; 26 | case g: h = (b - r) / d + 2; break; 27 | case b: h = (r - g) / d + 4; break; 28 | } 29 | h /= 6; 30 | } 31 | 32 | return [h, s, l]; 33 | } 34 | 35 | /** 36 | * Converts an HSL color value to RGB. Conversion formula 37 | * adapted from http://en.wikipedia.org/wiki/HSL_color_space. 38 | * Assumes h, s, and l are contained in the set [0, 1] and 39 | * returns r, g, and b in the set [0, 255]. 40 | * 41 | * @param Number h The hue 42 | * @param Number s The saturation 43 | * @param Number l The lightness 44 | * @return Array The RGB representation 45 | */ 46 | hsl2rgb(h, s, l){ 47 | var r, g, b; 48 | 49 | if (s == 0){ 50 | r = g = b = l; // achromatic 51 | } else { 52 | function hue2rgb(p, q, t) { 53 | if(t < 0) t += 1; 54 | if(t > 1) t -= 1; 55 | if(t < 1/6) return p + (q - p) * 6 * t; 56 | if(t < 1/2) return q; 57 | if(t < 2/3) return p + (q - p) * (2/3 - t) * 6; 58 | return p; 59 | } 60 | 61 | var q = l < 0.5 ? l * (1 + s) : l + s - l * s; 62 | var p = 2 * l - q; 63 | r = hue2rgb(p, q, h + 1/3); 64 | g = hue2rgb(p, q, h); 65 | b = hue2rgb(p, q, h - 1/3); 66 | } 67 | 68 | return [r * 255, g * 255, b * 255]; 69 | } 70 | 71 | /* 72 | * Converts an RGB color value to HSV. Conversion formula 73 | * adapted from http://en.wikipedia.org/wiki/HSV_color_space. 74 | * Assumes r, g, and b are contained in the set [0, 255] and 75 | * returns h, s, and v in the set [0, 1]. 76 | * 77 | * @param Number r The red color value 78 | * @param Number g The green color value 79 | * @param Number b The blue color value 80 | * @return Array The HSV representation 81 | */ 82 | 83 | rgb2hsv(r, g, b){ 84 | r = r/255, g = g/255, b = b/255; 85 | var max = Math.max(r, g, b), min = Math.min(r, g, b); 86 | var h, s, v = max; 87 | 88 | var d = max - min; 89 | s = max == 0 ? 0 : d / max; 90 | 91 | if(max == min){ 92 | h = 0; // achromatic 93 | }else{ 94 | switch(max){ 95 | case r: h = (g - b) / d + (g < b ? 6 : 0); break; 96 | case g: h = (b - r) / d + 2; break; 97 | case b: h = (r - g) / d + 4; break; 98 | } 99 | h /= 6; 100 | } 101 | 102 | return [h, s, v]; 103 | } 104 | 105 | /** 106 | * Converts an HSV color value to RGB. Conversion formula 107 | * adapted from http://en.wikipedia.org/wiki/HSV_color_space. 108 | * Assumes h, s, and v are contained in the set [0, 1] and 109 | * returns r, g, and b in the set [0, 255]. 110 | * 111 | * @param Number h The hue 112 | * @param Number s The saturation 113 | * @param Number v The value 114 | * @return Array The RGB representation 115 | */ 116 | hsv2rgb(h, s, v){ 117 | var r, g, b; 118 | 119 | var i = Math.floor(h * 6); 120 | var f = h * 6 - i; 121 | var p = v * (1 - s); 122 | var q = v * (1 - f * s); 123 | var t = v * (1 - (1 - f) * s); 124 | 125 | switch(i % 6){ 126 | case 0: r = v, g = t, b = p; break; 127 | case 1: r = q, g = v, b = p; break; 128 | case 2: r = p, g = v, b = t; break; 129 | case 3: r = p, g = q, b = v; break; 130 | case 4: r = t, g = p, b = v; break; 131 | case 5: r = v, g = p, b = q; break; 132 | } 133 | 134 | return [r * 255, g * 255, b * 255]; 135 | } 136 | 137 | rgb2hex(r, g, b) { 138 | return "#" + Math.round((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1); 139 | } 140 | 141 | hex2rgb(hex) { 142 | // Expand shorthand form (e.g. "03F") to full form (e.g. "0033FF") 143 | var shorthandRegex = /^#?([a-f\d])([a-f\d])([a-f\d])$/i; 144 | hex = hex.replace(shorthandRegex, function(m, r, g, b) { 145 | return r + r + g + g + b + b; 146 | }); 147 | 148 | var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); 149 | return result ? [ 150 | parseInt(result[1], 16), 151 | parseInt(result[2], 16), 152 | parseInt(result[3], 16) 153 | ] : null; 154 | } 155 | 156 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ScreeSS 2 | 3 | ScreeSS is a high level stylesheet language that compiles down to a [Mapbox GL style object](https://www.mapbox.com/mapbox-gl-style-spec/). It features a clean CSS-like syntax and powerful macro system. 4 | 5 | ## Installation 6 | 7 | To install ScreeSS, you must have `node` and `npm` installed on your system. 8 | 9 | Installation is via `npm` 10 | 11 | ```bash 12 | npm install -g screess 13 | ``` 14 | 15 | ## Writing a ScreeSS Stylesheet 16 | 17 | Create a layer called "water" 18 | ``` 19 | #water { } 20 | ``` 21 | 22 | Set the layer's source. 23 | ``` 24 | #water { 25 | source: source( 26 | type: vector 27 | url: "mapbox://mapbox.mapbox-streets-v5" 28 | layer: "water" 29 | ) 30 | } 31 | ``` 32 | 33 | Set the layer's type. 34 | 35 | ``` 36 | #water { 37 | source: source( 38 | type: vector 39 | url: "mapbox://mapbox.mapbox-streets-v5" 40 | ) 41 | type: fill 42 | } 43 | ``` 44 | 45 | You can add filters to the layer using a natural syntax 46 | 47 | ``` 48 | #water { 49 | source: source( 50 | type: vector 51 | url: "mapbox://mapbox.mapbox-streets-v5" 52 | layer: "water" 53 | ) 54 | type: "fill" 55 | filter: is polygon && @area > 1000 56 | 57 | } 58 | ``` 59 | 60 | Properties of the object being filtered or styled are prefixed with an `@`, as in `@area` above. Supported operators in filters are 61 | 62 | - comparison operators (`==`, `!=`, `>`, `<`, `>=`, `<=`) 63 | - typechecking operator (`is line`, `is polygon`, `is point`) 64 | - boolean logic operators (`&&`, `||`, `!`) 65 | 66 | Don't worry about top-level vs layout vs paint properties -- they will be differentiated automatically by the compiler. 67 | 68 | Style the layer with paint properties. Available paint properties depend on the layer `type` and documented in the [Mapbox GL style spec](https://www.mapbox.com/mapbox-gl-style-spec/) 69 | 70 | ``` 71 | #water { 72 | source: source( 73 | type: vector 74 | url: "mapbox://mapbox.mapbox-streets-v5" 75 | layer: "water" 76 | ) 77 | type: fill 78 | filter: is polygon && @area > 1000 79 | fill-color: #2491dd 80 | } 81 | ``` 82 | 83 | Operators work almost everywhere! Order of operations should work as expected. 84 | 85 | ``` 86 | #water { 87 | source: source( 88 | type: vector 89 | url: "mapbox://mapbox.mapbox-streets-v5" 90 | layer: "water" 91 | ) 92 | type: fill 93 | filter: is polygon && @area > 1000 94 | fill-color: #2491dd 95 | fill-translate: ((5 + 2) / 17) 3 * 5 96 | } 97 | ``` 98 | 99 | Function values are created using the special `function` value macro 100 | 101 | ``` 102 | #water { 103 | source: source( 104 | type: vector 105 | url: "mapbox://mapbox.mapbox-streets-v5" 106 | layer: "water" 107 | ) 108 | type: fill 109 | filter: is polygon && @area > 1000 110 | fill-color: function(0:#2491dd, 10:#196499) 111 | fill-translate: ((5 + 2) / 17) 3 * 5 112 | } 113 | ``` 114 | 115 | You may create `if` blocks and `for` blocks in your stylesheet to factor out structure. The below example also demonstrates using map objects and unnamed layers. 116 | 117 | ``` 118 | lake-types = { 119 | small: {area-min: 0 area-max: 1000 color: #2491dd} 120 | medium: {area-min: 1000 area-max: 10000 color: #1d73b0} 121 | large: {area-min: 10000 area-max: null color: #196499} 122 | } 123 | 124 | for lake-type in lake-types { 125 | # { 126 | source: source( 127 | type: vector 128 | url: "mapbox://mapbox.mapbox-streets-v5" 129 | layer: "water" 130 | ) 131 | type: fill 132 | filter: @area > lake-type.area-min && (lake-type.area-max == null || @area <= lake-type.area-max) 133 | fill-color: lake-type.color 134 | } 135 | } 136 | ``` 137 | 138 | ### Value Macros 139 | 140 | Values may be reused by assigning them to value macros 141 | 142 | ``` 143 | color-water = #2491dd 144 | 145 | #water { 146 | source: source( 147 | type: vector 148 | url: "mapbox://mapbox.mapbox-streets-v5" 149 | layer: "water" 150 | ) 151 | type: fill 152 | filter: is polygon && @area > 1000 153 | fill-color: color-water 154 | } 155 | ``` 156 | 157 | Value macros may take any number of arguments and invoke other value macros 158 | 159 | ``` 160 | color-water(depth) = darken(#2491dd, depth) 161 | 162 | #water { 163 | source: source( 164 | type: vector 165 | url: "mapbox://mapbox.mapbox-streets-v5" 166 | layer: "water" 167 | ) 168 | type: fill 169 | filter: is polygon && @area > 1000 170 | fill-color: color-water(0.5) 171 | } 172 | ``` 173 | 174 | Arguments to value macros may be optional 175 | 176 | ``` 177 | color-water(depth = 0) = darken(#2491dd, depth) 178 | 179 | #water { 180 | source: source( 181 | type: vector 182 | url: "mapbox://mapbox.mapbox-streets-v5" 183 | layer: "water" 184 | ) 185 | type: fill 186 | filter: is polygon && @area > 1000 187 | fill-color: color-water 188 | } 189 | ``` 190 | 191 | Arguments to value macros may be either named or positional, a la Python. 192 | 193 | ``` 194 | color-water(depth = 0) = darken(#2491dd, depth) 195 | 196 | #water { 197 | source: source( 198 | type: vector 199 | url: "mapbox://mapbox.mapbox-streets-v5" 200 | layer: "water" 201 | ) 202 | type: fill 203 | filter: is polygon && @area > 1000 204 | fill-color: color-water(depth: 0.5) 205 | } 206 | ``` 207 | 208 | Built-in macros include 209 | 210 | - `source(...)` 211 | - `hsv(h, s, v)` 212 | - `hsva(h, s, v, a)` 213 | - `hsl(h, s, l)` 214 | - `hsla(h, s, l, a)` 215 | - `rgb(r, g, b)` 216 | - `rgba(r, g, b, a)` 217 | - `function(base, ...)` 218 | - `range(start, stop, step = 1)` 219 | 220 | ### Property Macros 221 | 222 | Sets of properties may be reused by assigning them to a property macro 223 | ``` 224 | fill-water(depth) = { 225 | color = darken(#2491dd, depth) 226 | fill-color: color 227 | fill-antialias: true 228 | fill-outline-color: darken(color, 0.1) 229 | } 230 | 231 | #water { 232 | source: source( 233 | type: vector 234 | url: "mapbox://mapbox.mapbox-streets-v5" 235 | layer: "water" 236 | ) 237 | type: fill 238 | filter: is polygon && @area > 1000 239 | fill-water(0) 240 | } 241 | ``` 242 | -------------------------------------------------------------------------------- /test/value-macro.coffee: -------------------------------------------------------------------------------- 1 | assert = require("assert") 2 | 3 | Parser = require("../compiled/parser") 4 | parse = (source) -> Parser.parse(source).evaluate() 5 | 6 | describe "value macro", -> 7 | 8 | it "should allow namespaced identifiers", -> 9 | stylesheet = parse """ 10 | baz::foo = 17 11 | #layer { 12 | scree-test-meta: baz::foo() 13 | } 14 | """ 15 | assert.equal stylesheet.layers[0]['scree-test-meta'], 17 16 | 17 | describe "shadowing", -> 18 | 19 | it "should shadow a value macro in an enclosing scope", -> 20 | stylesheet = parse """ 21 | test(value) = value 22 | #layer { 23 | type: 'background'; 24 | test = test(17) 25 | scree-test-meta: test 26 | } 27 | """ 28 | assert.equal stylesheet.layers[0]['scree-test-meta'], 17 29 | 30 | it "should not turn into a literal string if undefined", -> 31 | assert.throws -> 32 | stylesheet = parse """ 33 | #layer { 34 | type: 'background' 35 | scree-test-meta: baz 36 | } 37 | """ 38 | 39 | it "should return values", -> 40 | stylesheet = parse """ 41 | identity(value) = value 42 | #layer { 43 | type: 'background' 44 | scree-test-meta: identity(17) 45 | } 46 | """ 47 | assert.equal stylesheet.layers[0]['scree-test-meta'], 17 48 | 49 | describe "argument evaluation", -> 50 | 51 | it "should evaluate without arguments and without parens", -> 52 | stylesheet = parse """ 53 | foo = 17 54 | #layer { 55 | type: 'background' 56 | scree-test-meta: foo 57 | } 58 | """ 59 | assert.equal stylesheet.layers[0]['scree-test-meta'], 17 60 | 61 | it "should evaluate without arguments and with parens", -> 62 | stylesheet = parse """ 63 | foo = 17 64 | #layer { 65 | type: 'background' 66 | scree-test-meta: foo() 67 | } 68 | """ 69 | assert.equal stylesheet.layers[0]['scree-test-meta'], 17 70 | 71 | it "should evaluate with positional arguments", -> 72 | stylesheet = parse """ 73 | foo(one, two, three, four) = three 74 | #layer { 75 | type: 'background' 76 | scree-test-meta: foo(1 2 3 4) 77 | } 78 | """ 79 | assert.equal stylesheet.layers[0]['scree-test-meta'], 3 80 | 81 | it "should evaluate with named arguments", -> 82 | stylesheet = parse """ 83 | foo(one, two, three, four) = three 84 | #layer { 85 | type: 'background' 86 | scree-test-meta: foo(four:4 three:3 two:2 one:1) 87 | } 88 | """ 89 | assert.equal stylesheet.layers[0]['scree-test-meta'], 3 90 | 91 | it "should evaluate with positional and named arguments", -> 92 | stylesheet = parse """ 93 | foo(foo bar) = bar 94 | #layer { 95 | type: 'background' 96 | scree-test-meta: foo(bar:17 10) 97 | } 98 | """ 99 | assert.equal stylesheet.layers[0]['scree-test-meta'], 17 100 | 101 | it "should evaluate using an optional argument", -> 102 | stylesheet = parse """ 103 | foo(one, two = 17) = two 104 | #layer { 105 | type: 'background' 106 | scree-test-meta: foo(0) 107 | } 108 | """ 109 | assert.equal stylesheet.layers[0]['scree-test-meta'], 17 110 | 111 | it "should override an optional argument", -> 112 | stylesheet = parse """ 113 | foo(one, two = 0) = two 114 | #layer { 115 | type: 'background' 116 | scree-test-meta: foo(0, 17) 117 | } 118 | """ 119 | assert.equal stylesheet.layers[0]['scree-test-meta'], 17 120 | 121 | describe "argument matching", -> 122 | 123 | it "should match by number of positional arguments", -> 124 | stylesheet = parse """ 125 | foo(one) = one 126 | foo(one, two) = two 127 | foo(one, two, three) = one 128 | #layer { 129 | type: 'background' 130 | scree-test-meta: foo(0, 17) 131 | } 132 | """ 133 | assert.equal stylesheet.layers[0]['scree-test-meta'], 17 134 | 135 | it "should match by names of named arguments", -> 136 | stylesheet = parse """ 137 | foo(foo, bar) = one 138 | foo(one, two) = two 139 | #layer { 140 | type: 'background' 141 | scree-test-meta: foo(two:17 0) 142 | } 143 | """ 144 | assert.equal stylesheet.layers[0]['scree-test-meta'], 17 145 | 146 | it "should match with optional arguments", -> 147 | stylesheet = parse """ 148 | foo(one, two, three=3) = two 149 | #layer { 150 | type: 'background' 151 | scree-test-meta: foo(0, 17) 152 | } 153 | """ 154 | assert.equal stylesheet.layers[0]['scree-test-meta'], 17 155 | 156 | it "should match with wildcard arguments", -> 157 | stylesheet = parse """ 158 | named(*) = arguments 159 | #layer { 160 | scree-test-meta: named('positional', name:'named') 161 | } 162 | """ 163 | assert.deepEqual stylesheet.layers[0]['scree-test-meta'], {0: "positional", name: 'named'} 164 | 165 | describe "scope", -> 166 | 167 | it "should apply recursively in arguments", -> 168 | stylesheet = parse """ 169 | identity(value) = value 170 | #layer { 171 | type: 'background' 172 | scree-test-meta: identity(identity(17)) 173 | } 174 | """ 175 | assert.equal stylesheet.layers[0]['scree-test-meta'], 17 176 | 177 | it "should apply other value macros to optional arguments", -> 178 | stylesheet = parse """ 179 | inner = 17 180 | outer(value=inner) = value 181 | #layer { 182 | type: 'background' 183 | scree-test-meta: outer 184 | } 185 | """ 186 | assert.equal stylesheet.layers[0]['scree-test-meta'], 17 187 | 188 | it "should apply other value macros", -> 189 | stylesheet = parse """ 190 | inner = 17 191 | outer = inner 192 | #layer { 193 | type: 'background' 194 | scree-test-meta: outer 195 | } 196 | """ 197 | assert.equal stylesheet.layers[0]['scree-test-meta'], 17 198 | 199 | it "should apply other value macros in the global scope", -> 200 | stylesheet = parse """ 201 | #layer { 202 | type: 'background' 203 | scree-test-meta: identity(17) 204 | } 205 | """ 206 | assert.equal stylesheet.layers[0]['scree-test-meta'], 17 207 | -------------------------------------------------------------------------------- /source/Scope.ts: -------------------------------------------------------------------------------- 1 | import FS = require("fs"); 2 | import Path = require("path"); 3 | import assert = require("assert") 4 | 5 | import _ = require("./utilities") 6 | import Arguments = require("./Arguments") 7 | import ArgumentsDefinition = require('./ArgumentsDefinition') 8 | import LiteralExpression = require('./expressions/LiteralExpression') 9 | import Stack = require('./Stack') 10 | import Expression = require('./expressions/Expression'); 11 | import Macro = require('./Macro'); 12 | import Statement = require('./statements/Statement'); 13 | import MacroDefinitionStatement = require('./statements/MacroDefinitionStatement'); 14 | import PropertyStatement = require('./statements/PropertyStatement'); 15 | import formatGlobalScope = require('./scopes/global'); 16 | import formatLayerScope = require('./scopes/layer'); 17 | import formatClassScope = require('./scopes/class'); 18 | import formatObjectScope = require('./scopes/object'); 19 | var Parser = require("./parser"); 20 | 21 | class Scope { 22 | 23 | private static coreLibrary:Scope = null; 24 | 25 | static createFromFile(file) { 26 | return Parser.parse(FS.readFileSync(file, "utf8")); 27 | } 28 | 29 | static createCoreLibrary():Scope { 30 | if (!this.coreLibrary) { 31 | this.coreLibrary = this.createFromFile(Path.join(__dirname, "../core.sss")); 32 | } 33 | return this.coreLibrary; 34 | } 35 | 36 | static createGlobal():Scope { 37 | var globalScope = new Scope(null) 38 | globalScope.name = "[global]"; 39 | return globalScope; 40 | } 41 | 42 | public sources:{} = {}; 43 | public version:number; 44 | public macros:Macro[] = []; 45 | public statements:Statement[] = []; 46 | public name:string = null; 47 | 48 | constructor(public parent:Scope) {} 49 | 50 | isGlobalScope():boolean { 51 | return !this.parent 52 | } 53 | 54 | getGlobalScope():Scope { 55 | return this.isGlobalScope() ? this : this.parent.getGlobalScope(); 56 | } 57 | 58 | ////////////////////////////////////////////////////////////////////////////// 59 | // Construction 60 | 61 | // TODO this belongs somewhere else, semantically. Maybe on the Stack object, renamed to "context"? 62 | addSource(source:{}):string { 63 | var hash = _.hash(JSON.stringify(source)).toString(); 64 | this.getGlobalScope().sources[hash] = source; 65 | return hash; 66 | } 67 | 68 | addStatement(statement:Statement) { 69 | this.statements.push(statement); 70 | 71 | if (statement instanceof MacroDefinitionStatement) { 72 | this.addMacro(statement.name, statement.argsDefinition, statement.body); 73 | } 74 | } 75 | 76 | addStatements(statements:Statement[]) { 77 | for (var i = 0; i < statements.length; i++) { 78 | this.addStatement(statements[i]); 79 | } 80 | } 81 | 82 | ////////////////////////////////////////////////////////////////////////////// 83 | // Macro Construction 84 | 85 | addLiteralMacros(macros:{[name:string]:any}):void { 86 | for (var identifier in macros) { 87 | var value = macros[identifier]; 88 | this.addLiteralMacro(identifier, value); 89 | } 90 | } 91 | 92 | addLiteralMacro(identifier:string, value:any) { 93 | this.addMacro(identifier, ArgumentsDefinition.ZERO, new LiteralExpression(value)); 94 | } 95 | 96 | addMacro(name:String, argsDefinition:ArgumentsDefinition, body:Expression) { 97 | var Macro_ = require("./Macro"); 98 | var macro = new Macro_(this, name, argsDefinition, body); 99 | this.macros.unshift(macro); 100 | } 101 | 102 | ////////////////////////////////////////////////////////////////////////////// 103 | // Evaluation Helpers 104 | 105 | getMacro(name:string, values:Arguments, stack:Stack):Macro { 106 | for (var i in this.macros) { 107 | var macro = this.macros[i]; 108 | 109 | if (macro.matches(name, values) && !_.contains(stack.macros, macro)) { 110 | return macro; 111 | } 112 | } 113 | 114 | if (this.parent) { 115 | return this.parent.getMacro(name, values, stack); 116 | } else { 117 | return null; 118 | } 119 | } 120 | 121 | eachMacro(callback:(macro:Macro) => void):void { 122 | for (var i in this.macros) { 123 | callback(this.macros[i]); 124 | } 125 | if (this.parent) this.parent.eachMacro(callback); 126 | } 127 | 128 | getMacrosAsFunctions(stack:Stack):{[name:string]:any} { 129 | var names = []; 130 | this.eachMacro((macro: Macro) => { names.push(macro.name); }); 131 | names = _.uniq(names); 132 | 133 | return _.objectMap(names, (name) => { 134 | var that = this; 135 | return [name, function() { 136 | var args = Arguments.fromPositionalValues(_.toArray(arguments)); 137 | var macro = that.getMacro(name, args, stack); 138 | if (!macro) return null; 139 | else return macro.evaluateToIntermediate(args, stack); 140 | }]; 141 | }); 142 | 143 | } 144 | 145 | // Properties, layers, classes 146 | eachPrimitiveStatement(stack:Stack, callback:(scope:Scope, statement:Statement) => void): void { 147 | var statements = this.statements; 148 | assert(stack != null); 149 | 150 | for (var i = 0; i < statements.length; i++) { 151 | statements[i].eachPrimitiveStatement(this, stack, callback); 152 | } 153 | } 154 | 155 | // TODO actually do a seperate pass over each primitive statement to extract this 156 | getVersion():number { 157 | return this.getGlobalScope().version || 7; 158 | } 159 | 160 | ////////////////////////////////////////////////////////////////////////////// 161 | // Evaluation 162 | 163 | evaluate(type:Scope.Type = Scope.Type.GLOBAL, stack:Stack = new Stack()):{} { 164 | stack.scope.push(this); 165 | 166 | var layers = []; 167 | var classes = []; 168 | var properties = {}; 169 | 170 | if (type == Scope.Type.GLOBAL) { 171 | this.version = parseInt(properties["version"], 10) || 7; 172 | this.macros = this.macros.concat(Scope.createCoreLibrary().macros); 173 | } 174 | 175 | this.eachPrimitiveStatement(stack, (scope: Scope, statement: Statement) => { 176 | statement.evaluate(scope, stack, layers, classes, properties); 177 | }); 178 | 179 | layers = _.sortBy(layers, 'z-index'); 180 | 181 | var formater; 182 | if (type == Scope.Type.GLOBAL) { 183 | formater = formatGlobalScope; 184 | } else if (type == Scope.Type.LAYER) { 185 | formater = formatLayerScope; 186 | } else if (type == Scope.Type.CLASS) { 187 | formater = formatClassScope; 188 | } else if (type == Scope.Type.OBJECT) { 189 | formater = formatObjectScope; 190 | } else { 191 | assert(false); 192 | } 193 | 194 | var output = formater.call(this, stack, properties, layers, classes) 195 | 196 | stack.scope.pop(); 197 | 198 | return output; 199 | } 200 | 201 | } 202 | 203 | module Scope { 204 | export enum Type { GLOBAL, LAYER, CLASS, OBJECT } 205 | } 206 | 207 | export = Scope 208 | -------------------------------------------------------------------------------- /test/value.coffee: -------------------------------------------------------------------------------- 1 | assert = require("assert") 2 | 3 | Parser = require("../compiled/parser") 4 | parse = (source) -> Parser.parse(source).evaluate() 5 | 6 | describe "value", -> 7 | 8 | parseValue = (value, context = {}) -> 9 | preface = "value-macro = 'foo'; property-macro = { foo: 'bar' }"; 10 | 11 | if context.filterLvalue 12 | stylesheet = parse "#{preface}; #layer { type: 'background'; scree-test-meta: #{value} == 1 }" 13 | stylesheet.layers[0]['scree-test-meta'][1] 14 | else if context.filterRvalue 15 | stylesheet = parse "#{preface}; #layer { type: 'background'; scree-test-meta: @test == #{value} }" 16 | stylesheet.layers[0]['scree-test-meta'][2] 17 | else 18 | stylesheet = parse "#{preface}; #layer { type: 'background'; scree-test-meta: #{value} }" 19 | stylesheet.layers[0]['scree-test-meta'] 20 | 21 | describe "javascript", -> 22 | 23 | it "can be embedded as an expression", -> 24 | assert.deepEqual parseValue("`'bar'.toUpperCase()`"), 'BAR' 25 | 26 | it "can access scope and stack", -> 27 | assert.deepEqual parseValue("`!!(scope && stack)`"), true 28 | 29 | it "can access value macros", -> 30 | stylesheet = parse """ 31 | value = "bar" 32 | scree-test-meta: `value().toUpperCase()` 33 | """ 34 | assert.equal(stylesheet["scree-test-meta"], "BAR") 35 | 36 | it "can access value macros with arguments", -> 37 | stylesheet = parse """ 38 | scree-test-meta: `identity('bar').toUpperCase()` 39 | """ 40 | assert.equal(stylesheet["scree-test-meta"], "BAR") 41 | 42 | describe "array", -> 43 | 44 | it "should parse with comma seperators", -> 45 | assert.deepEqual parseValue("[1,2 , 3]"), [1,2,3] 46 | 47 | it "should parse with space seperators", -> 48 | assert.deepEqual parseValue("[1 2 3]"), [1,2,3] 49 | 50 | it "should allow filters to be members", -> 51 | assert.deepEqual parseValue("[@class == 'footway']"), [["==", "class", "footway"]] 52 | 53 | it "should allow nested arrays", -> 54 | assert.deepEqual parseValue("[[[[ 1 ]]]]"), [[[[1]]]] 55 | 56 | describe "scope", -> 57 | 58 | it "should parse", -> 59 | assert.deepEqual parseValue("{one:1; two:2; three:3}"), {one: 1, two: 2, three: 3} 60 | 61 | it "should allow property access by dot notation", -> 62 | assert.deepEqual parseValue("{one:1; two:2; three:3}.three"), 3 63 | 64 | it "should allow property access by subscript notation", -> 65 | assert.deepEqual parseValue('{one:1; two:2; three:3}["three"]'), 3 66 | 67 | it "should allow maps inside maps", -> 68 | assert.deepEqual parseValue("{one:{two:{three: {four: 4}}}}"), {one: {two: {three: {four: 4}}}} 69 | 70 | it "shoud allow recursive property accesses", -> 71 | assert.deepEqual parseValue('{one:{two:{three: 3}}}.one["two"].three'), 3 72 | 73 | it "should allow filters to be members", -> 74 | assert.deepEqual parseValue("{filter: @class == 'footway'}"), {filter: ["==", "class", "footway"]} 75 | 76 | it "should allow keys to be language keywords", -> 77 | assert.deepEqual parseValue("{out: 0; in: 1}"), {out: 0, in: 1} 78 | 79 | it "should allow property macros", -> 80 | assert.deepEqual parseValue("{ property-macro() }"), { foo: 'bar' } 81 | 82 | describe "number", -> 83 | 84 | it "should parse an integer", -> 85 | assert.equal parseValue("12340"), 12340 86 | 87 | it "should parse a float with a leading integer", -> 88 | assert.equal parseValue("12340.12340"), 12340.1234 89 | 90 | it "should parse a float without a leading integer", -> 91 | assert.equal parseValue(".12340"), 0.1234 92 | 93 | it "should parse a float with a leading 0", -> 94 | assert.equal parseValue("0.12340"), 0.1234 95 | 96 | describe "boolean", -> 97 | 98 | it "should parse true", -> 99 | assert.equal parseValue("true"), true 100 | 101 | it "should parse false", -> 102 | assert.equal parseValue("false"), false 103 | 104 | describe "null", -> 105 | 106 | it "should parse", -> 107 | assert.equal parseValue("null"), null 108 | 109 | 110 | describe "attribute reference", -> 111 | 112 | it "should parse in a property", -> 113 | assert.equal parseValue("@foo"), "{foo}" 114 | 115 | it "should parse in a filter", -> 116 | assert.equal parseValue("@foo", filterLvalue:true), "foo" 117 | 118 | describe "string", -> 119 | 120 | it "should parse double quoted strings", -> 121 | assert.equal parseValue('"foo"'), "foo" 122 | 123 | it "should parse single quoted strings", -> 124 | assert.equal parseValue("'foo'"), "foo" 125 | 126 | it "should parse with literal values interpolated", -> 127 | assert.equal parseValue('"test {7} test"'), "test 7 test" 128 | 129 | it "should parse with macro value references interpolated", -> 130 | assert.equal parseValue('"test {value-macro} test"'), "test foo test" 131 | 132 | it "should parse with attribute reference values", -> 133 | assert.equal parseValue('"test @foo test"'), "test {foo} test" 134 | 135 | it "should parse with attribute reference values interpolated", -> 136 | assert.equal parseValue('"test {@foo} test"'), "test {foo} test" 137 | 138 | it "should parse with an escaped '{'", -> 139 | assert.equal parseValue('"\\{"'), "{" 140 | 141 | it "should parse with an escaped '{'", -> 142 | assert.equal parseValue('"\\@"'), "@" 143 | 144 | it "should parse with an escaped '\\'", -> 145 | assert.equal parseValue('"\\\\"'), "\\" 146 | 147 | it "should parse an empty string", -> 148 | assert.equal parseValue('""'), "" 149 | 150 | it "should fail for unclosed {}", -> 151 | assert.throws -> parseValue('"{"') 152 | 153 | it "should fail for malformed attribute reference", -> 154 | assert.throws -> parseValue('"@"') 155 | 156 | describe "function value", -> 157 | 158 | it "should apply with a base", -> 159 | assert.deepEqual( 160 | parseValue('function(base:0.5 0:100 5:50 10:25)'), 161 | stops: [[0, 100], [5, 50], [10, 25]], base:0.5 162 | ) 163 | 164 | it "should apply without a base", -> 165 | assert.deepEqual( 166 | parseValue('function(0:100 5:50 10:25)'), 167 | stops: [[0, 100], [5, 50], [10, 25]] 168 | ) 169 | 170 | describe "color", -> 171 | 172 | it "should parse 3 character hex with lowercase letters", -> 173 | assert.equal parseValue('#fff'), "#ffffff" 174 | 175 | it "should parse 3 character hex with capital letters", -> 176 | assert.equal parseValue('#FFF'), "#ffffff" 177 | 178 | it "should parse 6 character hex with lowercase letters", -> 179 | assert.equal parseValue('#ffffff'), "#ffffff" 180 | 181 | it "should parse 6 character hex with capital letters", -> 182 | assert.equal parseValue('#FFFFFF'), "#ffffff" 183 | 184 | it "should parse a color function", -> 185 | assert.equal parseValue('rgba(255 255 255 1)'), "#ffffff" 186 | 187 | it "should parse a color function with alpha", -> 188 | assert.equal parseValue('rgba(255 255 255 0.5)'), "rgba(255, 255, 255, 0.5)" 189 | 190 | describe "arithmetic operators", -> 191 | 192 | it "should apply '+'", -> 193 | stylesheet = parse """ 194 | #test { scree-test-meta: 1 + 1; } 195 | """ 196 | assert.equal stylesheet.layers[0]['scree-test-meta'], 2 197 | 198 | it "should apply '-'", -> 199 | stylesheet = parse """ 200 | #test { scree-test-meta: 3 - 1; } 201 | """ 202 | assert.equal stylesheet.layers[0]['scree-test-meta'], 2 203 | 204 | it "should apply '*'", -> 205 | stylesheet = parse """ 206 | #test { scree-test-meta: 2 * 2; } 207 | """ 208 | assert.equal stylesheet.layers[0]['scree-test-meta'], 4 209 | 210 | it "should apply '/'", -> 211 | stylesheet = parse """ 212 | #test { scree-test-meta: 4 / 2; } 213 | """ 214 | assert.equal stylesheet.layers[0]['scree-test-meta'], 2 215 | 216 | it "should apply to function values as rvalues", -> 217 | stylesheet = parse """ 218 | #test { scree-test-meta: 1 + function(base:0.5 1:1 2:2) } 219 | """ 220 | assert.deepEqual stylesheet.layers[0]['scree-test-meta'], { 221 | base: 0.5, 222 | stops: [[1, 2], [2, 3]] 223 | } 224 | 225 | it "should apply to function values as lvalues", -> 226 | stylesheet = parse """ 227 | #test { scree-test-meta: function(base:0.5 1:1 2:2) + 1 } 228 | """ 229 | assert.deepEqual stylesheet.layers[0]['scree-test-meta'], { 230 | base: 0.5, 231 | stops: [[1, 2], [2, 3]] 232 | } 233 | 234 | it "should support chaining", -> 235 | stylesheet = parse """ 236 | #test { scree-test-meta: 1 + 1 + 1 + 1; } 237 | """ 238 | assert.equal stylesheet.layers[0]['scree-test-meta'], 4 239 | 240 | it "should support order of operations", -> 241 | stylesheet = parse """ 242 | #test { scree-test-meta: 2 * 2 + 2 * 2; } 243 | """ 244 | assert.equal stylesheet.layers[0]['scree-test-meta'], 8 245 | 246 | it "should support parenthesis", -> 247 | stylesheet = parse """ 248 | #test { scree-test-meta: 2 * (2 + 2); } 249 | """ 250 | assert.equal stylesheet.layers[0]['scree-test-meta'], 8 251 | -------------------------------------------------------------------------------- /source/parser.pegjs: -------------------------------------------------------------------------------- 1 | { 2 | var _ = require('./utilities'); 3 | var assert = require('assert'); 4 | var ScreeSS = require("./index"); 5 | 6 | // UNEXPECTED GLOBAL VARS 7 | var scope = null; 8 | var globalScope = null; 9 | 10 | function pushScope(scopeNew) { 11 | scopeNew = scopeNew || new ScreeSS.Scope(scope); 12 | assert(scopeNew instanceof ScreeSS.Scope, "Malformed arguments to pushScope"); 13 | assert(scopeNew.parent == scope, "Next scope must be child of current scope"); 14 | scope = scopeNew; 15 | return scope; 16 | } 17 | 18 | function popScope(statements) { 19 | var scopeOld = scope; 20 | var scopeNew = scope.parent; 21 | scope = scopeNew; 22 | return scopeOld; 23 | } 24 | 25 | function rehead(head, body) { 26 | return [head].concat(_.pluck(body, 1)); 27 | } 28 | 29 | } 30 | 31 | identifier = a:[A-Za-z]b:[a-zA-Z0-9-_]* { return a + b.join(""); } 32 | namespacedIdentifier = head:identifier tail:( "::" identifier )* { return rehead(head, tail.join("")).join(""); } 33 | 34 | comment = "//" [^\n\r]* { return null; } 35 | 36 | whitespaceWeak = " " / "\t" { return null; } 37 | whitespaceStrong = comment? "\n" / "\r" { return null; } 38 | whitespace = whitespaceStrong / whitespaceWeak { return null; } 39 | 40 | expressionSeperator = ((whitespace* "," whitespace*) / whitespace+) { return null; } 41 | statementSeperator = whitespaceWeak* (";" / whitespaceStrong) whitespace* { return null; } 42 | 43 | //////////////////////////////////////////////////////////////////////////////// 44 | // // 45 | // Scope // 46 | // // 47 | //////////////////////////////////////////////////////////////////////////////// 48 | 49 | global = &{ globalScope = scope = globalScope || ScreeSS.Scope.createGlobal(); return true; } whitespace* statements:scopeBody whitespace* { globalScope.addStatements(statements); return globalScope; } 50 | 51 | scope = &{ pushScope(); return true; } "{" whitespace* body:scopeBody whitespace* "}" { scope.addStatements(body); return popScope(); } 52 | 53 | scopeBody = statements:(head:statement tail:(statementSeperator statement)* statementSeperator? whitespace* { return rehead(head, tail) })? comment? whitespace* { return _.compact(statements) || [] } 54 | 55 | //////////////////////////////////////////////////////////////////////////////// 56 | // // 57 | // Arguments // 58 | // // 59 | //////////////////////////////////////////////////////////////////////////////// 60 | 61 | arguments = 62 | head:argumentsItem tail:(expressionSeperator argumentsItem)* { return new ScreeSS.ExpressionSet(rehead(head, tail)); } 63 | 64 | argumentsItem = 65 | name:(identifier / integer) whitespace* ":" whitespace* expression:expression { return { name: name, expression: expression } } / 66 | expression:expression { return { expression: expression } } 67 | 68 | argumentsDefinition = 69 | "(" whitespace* "*" whitespace* ")" { return ScreeSS.ArgumentsDefinition.WILDCARD } / 70 | "(" whitespace* head:argumentsDefinitionItem tail:(expressionSeperator argumentsDefinitionItem)* whitespace* ")" { return new ScreeSS.ArgumentsDefinition(rehead(head, tail), scope) } / 71 | "" { return ScreeSS.ArgumentsDefinition.ZERO } 72 | 73 | argumentsDefinitionItem = 74 | name:identifier whitespace* "=" whitespace* expression:expression { return { name: name, expression: expression } } / 75 | name:identifier { return { name: name } } 76 | 77 | //////////////////////////////////////////////////////////////////////////////// 78 | // // 79 | // Statement // 80 | // // 81 | //////////////////////////////////////////////////////////////////////////////// 82 | 83 | statement = 84 | comment / 85 | macroDefinitionStatement / 86 | macroReferenceStatement / 87 | layerStatement / 88 | classStatement / 89 | loopStatement / 90 | conditionalStatement / 91 | propertyStatement / 92 | javascriptStatement 93 | 94 | propertyStatement = 95 | name:identifier whitespace* ":" whitespace* expression:expression { return new ScreeSS.PropertyStatement(name, expression) } 96 | 97 | macroDefinitionStatement = 98 | name:namespacedIdentifier whitespace* args:argumentsDefinition whitespace* "=" whitespace* body:expression { return new ScreeSS.MacroDefinitionStatement(name, args, body) } 99 | 100 | macroReferenceStatement = 101 | name:namespacedIdentifier whitespace* "(" whitespace* expressions:arguments whitespace* ")" { return new ScreeSS.MacroReferenceStatement(name, expressions) } / 102 | name:namespacedIdentifier whitespace* "(" whitespace* ")" { return new ScreeSS.MacroReferenceStatement(name, ScreeSS.ExpressionSet.ZERO) } 103 | 104 | loopStatement = 105 | "for" whitespace+ keyIdentifier:identifier expressionSeperator valueIdentifier:identifier whitespace+ "in" whitespace+ collection:expression whitespace* scope:scope { return new ScreeSS.LoopStatement(scope, valueIdentifier, keyIdentifier, collection) } / 106 | "for" whitespace+ valueIdentifier:identifier whitespace+ "in" whitespace+ collection:expression whitespace* scope:scope { return new ScreeSS.LoopStatement(scope, valueIdentifier, null, collection) } 107 | 108 | conditionalStatement = 109 | ifItem:conditionalStatementIf elseIfItems:( whitespace* conditionalStatementElseIf )* whitespace* elseItem:conditionalStatementElse? { return new ScreeSS.ConditionalStatement(_.compact(rehead(ifItem, elseIfItems).concat([elseItem]))) } 110 | 111 | conditionalStatementIf = 112 | "if" whitespace+ condition:expression whitespace* scope:scope { return {condition: condition, scope: scope}; } 113 | 114 | conditionalStatementElseIf = 115 | "else if" whitespace+ condition:expression whitespace* scope:scope { return {condition: condition, scope: scope}; } 116 | 117 | conditionalStatementElse = 118 | "else" whitespace* scope:scope { return {condition: new ScreeSS.LiteralExpression(true), scope: scope} } 119 | 120 | layerStatement = 121 | "#" name:identifier? whitespace* body:scope { return new ScreeSS.LayerStatement(name, body) } 122 | 123 | classStatement = 124 | "." name:identifier whitespace* body:scope { return new ScreeSS.ClassStatement(name, body) } 125 | 126 | javascriptStatement = 127 | "`" body:[^\`]* "`" { return new ScreeSS.JavascriptStatement(body.join("")) } 128 | 129 | //////////////////////////////////////////////////////////////////////////////// 130 | // // 131 | // Expression // 132 | // // 133 | //////////////////////////////////////////////////////////////////////////////// 134 | 135 | expression = 136 | filterExpression / 137 | arithmeticExpression 138 | 139 | ///////////////////////////////////////////// 140 | // Filter Expression // 141 | ///////////////////////////////////////////// 142 | 143 | filterExpression = 144 | notOperatorExpression / 145 | booleanLogicExpression / 146 | filterStrongExpression 147 | 148 | booleanLogicExpression = 149 | left:filterStrongExpression whitespace* operator:("||" / "&&") whitespace* right:expression { return new ScreeSS.BooleanLogicExpression(operator, [left, right]) } 150 | 151 | notOperatorExpression = 152 | "!" whitespace* expression:expression { return new ScreeSS.NotOperatorExpression(expression) } 153 | 154 | ///////////////////////////////////////////// 155 | // Filter Weak Expression // 156 | ///////////////////////////////////////////// 157 | 158 | filterStrongExpression = 159 | groupExpression / 160 | setOperatorExpression / 161 | comparisonOperatorExpression / 162 | typeCheckOperatorExpression 163 | 164 | groupExpression = 165 | whitespace* "(" expression:expression ")" { return expression } 166 | 167 | typeCheckOperatorExpression = 168 | whitespace* "is" whitespace* type:arithmeticExpression { return new ScreeSS.TypeCheckOperatorExpression(type) } 169 | 170 | comparisonOperatorExpression = 171 | whitespace* left:arithmeticExpression whitespace* operator:("==" / ">=" / "<=" / "<" / ">" / "!=") whitespace* right:arithmeticExpression { return new ScreeSS.ComparisonOperatorExpression(left, operator, right) } 172 | 173 | setOperatorExpression = 174 | whitespace* left:arithmeticExpression whitespace+ operator:("in" / "!in") whitespace+ right:arithmeticExpression { return new ScreeSS.SetOperatorExpression(left, operator, right) } 175 | 176 | ///////////////////////////////////////////// 177 | // Arithmetic Operator Expression // 178 | ///////////////////////////////////////////// 179 | 180 | arithmeticExpression = 181 | left:arithmeticStrongExpression whitespaceWeak* operator:("+" / "-") whitespaceWeak* right:arithmeticExpression { return new ScreeSS.ArithmeticOperatorExpression(left, operator, right) } / 182 | arithmeticStrongExpression 183 | 184 | arithmeticStrongExpression = 185 | left:propertyAccessExpression whitespaceWeak* operator:("/" / "*") whitespaceWeak* right:arithmeticStrongExpression { return new ScreeSS.ArithmeticOperatorExpression(left, operator, right) } / 186 | "(" whitespace* expression:arithmeticExpression whitespace* ")" { return expression } / 187 | conditionalExpression 188 | 189 | ///////////////////////////////////////////// 190 | // Conditional Expression // 191 | ///////////////////////////////////////////// 192 | 193 | conditionalExpression = 194 | head:propertyAccessExpression whitespaceWeak* '??' whitespaceWeak* tail:conditionalExpression { return new ScreeSS.NullCoalescingExpression(head, tail) } / 195 | condition:propertyAccessExpression whitespaceWeak* '?' whitespaceWeak* trueExpression:conditionalExpression whitespaceWeak* ':' whitespaceWeak* falseExpression:conditionalExpression { return new ScreeSS.TernaryExpression(condition, trueExpression, falseExpression) } / 196 | propertyAccessExpression 197 | 198 | ///////////////////////////////////////////// 199 | // Value Expression // 200 | ///////////////////////////////////////////// 201 | 202 | propertyAccessExpression = 203 | head:atomicExpression accesses:( 204 | "." (integer / identifier) / 205 | "[" expression "]" 206 | )* { 207 | var output = head; 208 | 209 | for (var i = 0; i < accesses.length; i++) { 210 | var access = accesses[i]; 211 | if (access[0] == ".") { 212 | output = new ScreeSS.PropertyAccessExpression(output, new ScreeSS.LiteralExpression(access[1])); 213 | } else if (access[0] == "[") { 214 | output = new ScreeSS.PropertyAccessExpression(output, access[1]); 215 | } 216 | } 217 | 218 | return output; 219 | } 220 | 221 | ///////////////////////////////////////////// 222 | // Weak Value Expression // 223 | ///////////////////////////////////////////// 224 | 225 | atomicExpression = 226 | javascriptExpression / 227 | literalExpression / 228 | macroReferenceExpression / 229 | stringExpression / 230 | scopeExpression / 231 | arrayExpression 232 | 233 | literalExpression = 234 | value:value { return new ScreeSS.LiteralExpression(value) } 235 | 236 | macroReferenceExpression = 237 | name:namespacedIdentifier whitespace* "(" whitespace* expressions:arguments whitespace* ")" { return new ScreeSS.MacroReferenceExpression(name, expressions) } / 238 | name:namespacedIdentifier (whitespace* "(" whitespace* ")")? { return new ScreeSS.MacroReferenceExpression(name, ScreeSS.ExpressionSet.ZERO) } 239 | 240 | stringExpression = 241 | "\"" body:[^\"]* "\"" { return new ScreeSS.StringExpression(body.join("")) } / 242 | "'" body:[^\']* "'" { return new ScreeSS.StringExpression(body.join("")) } 243 | 244 | arrayExpression = 245 | "[" whitespace* head:expression tail:(expressionSeperator expression)* whitespace* "]" { return new ScreeSS.ArrayExpression(_.map(rehead(head, tail))) } 246 | 247 | scopeExpression = 248 | body:scope { return new ScreeSS.ScopeExpression(body) } 249 | 250 | javascriptExpression = 251 | "`" body:[^\`]* "`" { return new ScreeSS.JavascriptExpression(body.join("")) } 252 | 253 | //////////////////////////////////////////////////////////////////////////////// 254 | // // 255 | // Value // 256 | // // 257 | //////////////////////////////////////////////////////////////////////////////// 258 | 259 | value = 260 | null / 261 | boolean / 262 | number / 263 | attributeReferenceValue / 264 | colorValue 265 | 266 | boolean = 267 | "true" { return true } / 268 | "false" { return false } 269 | 270 | null = 271 | "null" { return null } 272 | 273 | number = 274 | before:integer? "." after:([0-9]+) { return (before + parseFloat("." + after.join(""))) } / 275 | number:integer { return number } 276 | 277 | integer = 278 | head:[1-9-] tail:[0-9]* { return parseInt(head + tail.join("")) } / 279 | "0" { return 0 } 280 | 281 | attributeReferenceValue = 282 | "@" name:identifier { return new ScreeSS.AttributeReferenceValue(name) } 283 | 284 | colorValue = 285 | "#" color:[0-9a-fA-F]+ { return ScreeSS.ColorValue.hex(color.join("")) } 286 | --------------------------------------------------------------------------------