├── .gitignore ├── .npmignore ├── .travis.yml ├── src ├── index.js ├── lib │ ├── process.js │ ├── convert.js │ └── evaluate.js └── __tests__ │ └── index.js ├── .babelrc ├── LICENSE ├── CHANGELOG.md ├── package.json ├── README.md └── parser.jison /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | npm-debug.log 3 | dist -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .gitignore 2 | 3 | node_modules/ 4 | 5 | test/ 6 | .travis.yml 7 | 8 | gulpfile.js 9 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | node_js: 4 | - '6' 5 | - '4' 6 | 7 | matrix: 8 | fast_finish: true -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import {plugin} from 'postcss'; 2 | 3 | import process from './lib/process'; 4 | 5 | export default plugin('postcss-conditionals', () => { 6 | return process; 7 | }); -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [["env", { 3 | "targets": { 4 | "node": 4 5 | } 6 | }]], 7 | "plugins": ["add-module-exports"], 8 | "env": { 9 | "development": { 10 | "sourceMaps": "inline" 11 | }, 12 | "test": { 13 | "plugins": ["istanbul"] 14 | } 15 | } 16 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright 2015 Andy Jansson 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /src/lib/process.js: -------------------------------------------------------------------------------- 1 | import evaluate from './evaluate'; 2 | 3 | function processIfRule(rule) { 4 | processExpression(rule, rule.params); 5 | } 6 | 7 | function processExpression(node, expr) { 8 | process(node); 9 | if (!expr) 10 | throw node.error('Missing condition', { plugin: 'postcss-conditionals' }); 11 | 12 | const passed = evaluate(expr); 13 | if (passed) 14 | node.before(node.nodes); 15 | 16 | processNextNode(node, !passed); 17 | node.remove(); 18 | } 19 | 20 | function processNextNode(rule, evaluateNext) { 21 | const node = rule.next(); 22 | 23 | if (typeof node === 'undefined') 24 | return; 25 | 26 | if (node.type !== 'atrule') 27 | return; 28 | 29 | if (node.name !== 'else') 30 | return; 31 | 32 | if (evaluateNext) { 33 | if (node.params.substr(0, 2) === 'if') 34 | processElseIfRule(node); 35 | else 36 | processElseRule(node); 37 | } 38 | else 39 | processNextNode(node, false); 40 | 41 | node.remove(); 42 | } 43 | 44 | function processElseIfRule(rule) { 45 | processExpression(rule, rule.params.substr(3)); 46 | } 47 | 48 | function processElseRule(rule) { 49 | process(rule); 50 | rule.before(rule.nodes); 51 | } 52 | 53 | export default function process(node) { 54 | node.walkAtRules('if', processIfRule); 55 | } -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 3.0.0 - Unreleased 2 | * Update dependencies to latest version. 3 | * Rewrite in ES2015 (transpiled with babel). 4 | * Rewrite tests in AVA. 5 | 6 | # 2.1.0 - 2016-12-21 7 | * Add support for double-quoted strings [#16](https://github.com/andyjansson/postcss-conditionals/issues/16). 8 | 9 | # 2.0.3 - 2016-11-10 10 | * Fix string token to include dots, enabling use of class names (among other 11 | things) in expressions 12 | 13 | # 2.0.2 - 2016-03-02 14 | * Fix parsing of strings with escape character used for escaping characters 15 | other than single quotes [#12](https://github.com/andyjansson/postcss-conditionals/issues/12) 16 | 17 | # 2.0.1 - 2016-01-20 18 | * Allow for quoted empty strings 19 | 20 | # 2.0.0 - 2015-09-01 21 | * Update to PostCSS v5. 22 | 23 | # 1.4.0 - 2015-09-01 24 | * Fix value type being incorrectly transformed for certain unit types. 25 | * Expand mixed-type operations to include things such as comparisons of 26 | differently typed values. 27 | 28 | # 1.3.1 - 2015-08-24 29 | * Fix `@else` incorrectly being invoked when any statement other than the one 30 | directly preceding the `@else` is true. 31 | 32 | # 1.3.0 - 2015-08-19 33 | * Add support for nested if statements 34 | * Add boolean data type 35 | 36 | # 1.2.0 - 2015-05-27 37 | * Add transformations and comparisons of color values 38 | 39 | # 1.1.2 - 2015-05-26 40 | * Fix parsing of negative values [#2](https://github.com/andyjansson/postcss-conditionals/issues/2) 41 | 42 | # 1.1.1 - 2015-05-18 43 | * Add arithmetics for CSS units 44 | 45 | # 1.0.0 - 2015-05-05 46 | * Initial release 47 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "postcss-conditionals", 3 | "version": "3.0.0", 4 | "description": "PostCSS plugin that enables @if statements in your CSS", 5 | "keywords": [ 6 | "postcss", 7 | "css", 8 | "postcss-plugin", 9 | "conditional", 10 | "statement", 11 | "if-statements", 12 | "if-statement", 13 | "if", 14 | "else" 15 | ], 16 | "author": "Andy Jansson", 17 | "license": "MIT", 18 | "repository": { 19 | "type": "git", 20 | "url": "https://github.com/andyjansson/postcss-conditionals.git" 21 | }, 22 | "dependencies": { 23 | "css-color-converter": "^1.0.2", 24 | "css-unit-converter": "^1.0.0", 25 | "postcss": "^6.0.1" 26 | }, 27 | "devDependencies": { 28 | "ava": "^0.18.2", 29 | "babel-cli": "^6.18.0", 30 | "babel-core": "^6.21.0", 31 | "babel-eslint": "^7.1.1", 32 | "babel-plugin-add-module-exports": "^0.2.1", 33 | "babel-preset-env": "^1.4.0", 34 | "babel-register": "^6.18.0", 35 | "cross-env": "^3.1.4", 36 | "del-cli": "^0.2.1", 37 | "eslint": "^3.12.2", 38 | "eslint-config-i-am-meticulous": "^6.0.1", 39 | "eslint-plugin-babel": "^4.0.0", 40 | "eslint-plugin-import": "^2.2.0", 41 | "jison": "^0.4.17" 42 | }, 43 | "scripts": { 44 | "prepublish": "npm run build && del-cli dist/__tests__", 45 | "build": "del-cli dist && cross-env BABEL_ENV=publish babel src --out-dir dist && jison parser.jison -o dist/lib/parser.js", 46 | "pretest": "eslint src && npm run build", 47 | "test": "ava dist/__tests__/" 48 | }, 49 | "main": "dist/index.js", 50 | "files": [ 51 | "dist" 52 | ], 53 | "eslintConfig": { 54 | "parser": "babel-eslint", 55 | "extends": "eslint-config-i-am-meticulous" 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # postcss-conditionals [![Build Status][ci-img]][ci] 2 | 3 | [PostCSS] plugin that enables ```@if``` statements in your CSS. 4 | 5 | [PostCSS]: https://github.com/postcss/postcss 6 | [ci-img]: https://travis-ci.org/andyjansson/postcss-conditionals.svg 7 | [ci]: https://travis-ci.org/andyjansson/postcss-conditionals 8 | 9 | ## Installation 10 | 11 | ```js 12 | npm install postcss-conditionals 13 | ``` 14 | 15 | ## Usage 16 | 17 | ```js 18 | var fs = require('fs'); 19 | var postcss = require('postcss'); 20 | var conditionals = require('postcss-conditionals'); 21 | 22 | var css = fs.readFileSync('input.css', 'utf8'); 23 | 24 | var output = postcss() 25 | .use(conditionals) 26 | .process(css) 27 | .css; 28 | ``` 29 | 30 | Using this ```input.css```: 31 | 32 | ```css 33 | .foo { 34 | @if 3 < 5 { 35 | background: green; 36 | } 37 | @else { 38 | background: blue; 39 | } 40 | } 41 | ``` 42 | 43 | you will get: 44 | 45 | ```css 46 | .foo { 47 | background: green; 48 | } 49 | ``` 50 | 51 | Also works well with [postcss-simple-vars]: 52 | 53 | ```css 54 | $type: monster; 55 | p { 56 | @if $type == ocean { 57 | color: blue; 58 | } @else if $type == matador { 59 | color: red; 60 | } @else if $type == monster { 61 | color: green; 62 | } @else { 63 | color: black; 64 | } 65 | } 66 | ``` 67 | [postcss-simple-vars]: https://github.com/postcss/postcss-simple-vars 68 | 69 | and with [postcss-for]: 70 | 71 | ```css 72 | @for $i from 1 to 3 { 73 | .b-$i { 74 | width: $i px; 75 | @if $i == 2 { 76 | color: green; 77 | } 78 | } 79 | } 80 | ``` 81 | 82 | [postcss-for]: https://github.com/antyakushev/postcss-for 83 | 84 | ## Development 85 | 1. Clone repository 86 | 2. Install dependencies `npm install` 87 | 3. If `parser.jison` file has been changed, regenerate `parser.js` by running `npm run gen-parser` 88 | -------------------------------------------------------------------------------- /src/lib/convert.js: -------------------------------------------------------------------------------- 1 | import color from 'css-color-converter'; 2 | import convertUnits from 'css-unit-converter'; 3 | 4 | function convertNodes(left, right) { 5 | switch (left.type) { 6 | case 'Value': 7 | return convertValue(left, right); 8 | case 'ColorValue': 9 | return convertColorValue(left, right); 10 | case 'LengthValue': 11 | case 'AngleValue': 12 | case 'TimeValue': 13 | case 'FrequencyValue': 14 | case 'ResolutionValue': 15 | return convertAbsoluteLengthValue(left, right); 16 | case 'EmValue': 17 | case 'ExValue': 18 | case 'ChValue': 19 | case 'RemValue': 20 | case 'VhValue': 21 | case 'VwValue': 22 | case 'VminValue': 23 | case 'VmaxValue': 24 | case 'PercentageValue': 25 | return convertRelativeLengthValue(left, right); 26 | default: 27 | return { left, right }; 28 | } 29 | } 30 | 31 | function convertValue(left, right) { 32 | if (right.type === 'Value') 33 | return { left, right }; 34 | 35 | const nodes = convertNodes(right, left); 36 | return { 37 | left: nodes.right, 38 | right: nodes.left 39 | }; 40 | } 41 | 42 | function convertColorValue(left, right) { 43 | left.value = color(left.value).toHexString(); 44 | if (right.type === 'Value') { 45 | right = { 46 | type: 'ColorValue', 47 | value: color().fromRgb([ 48 | right.value, 49 | right.value, 50 | right.value 51 | ]).toHexString() 52 | }; 53 | } 54 | else if (right.type === 'ColorValue') { 55 | right.value = color(right.value).toHexString(); 56 | } 57 | 58 | return { left, right }; 59 | } 60 | 61 | function convertAbsoluteLengthValue(left, right) { 62 | switch (right.type) { 63 | case left.type: { 64 | right = { 65 | type: left.type, 66 | value: convertUnits(right.value, right.unit, left.unit), 67 | unit: left.unit 68 | }; 69 | break; 70 | } 71 | case 'Value': { 72 | right = { 73 | type: left.type, 74 | value: right.value, 75 | unit: left.unit 76 | }; 77 | break; 78 | } 79 | } 80 | return { left, right }; 81 | } 82 | 83 | function convertRelativeLengthValue(left, right) { 84 | if (right.type === 'Value') { 85 | right = { 86 | type: left.type, 87 | value: right.value, 88 | unit: left.unit 89 | }; 90 | } 91 | return { left, right }; 92 | } 93 | 94 | export default convertNodes; 95 | -------------------------------------------------------------------------------- /src/lib/evaluate.js: -------------------------------------------------------------------------------- 1 | import color from 'css-color-converter'; 2 | 3 | import {parser} from './parser' // eslint-disable-line 4 | import convert from './convert'; 5 | 6 | function evaluate(ast) { 7 | switch (ast.type) { 8 | case 'LogicalExpression': 9 | return evaluateLogicalExpression(ast); 10 | case 'BinaryExpression': 11 | return evaluateBinaryExpression(ast); 12 | case 'MathematicalExpression': 13 | return evaluateMathematicalExpression(ast); 14 | case 'UnaryExpression': 15 | return evaluateUnaryExpression(ast); 16 | default: 17 | return ast; 18 | } 19 | } 20 | 21 | function evaluateLogicalExpression(ast) { 22 | const left = evaluate(ast.left); 23 | const right = evaluate(ast.right); 24 | 25 | if (left.type !== 'BooleanValue') 26 | throw new Error('Unexpected node type'); 27 | 28 | if (right.type !== 'BooleanValue') 29 | throw new Error('Unexpected node type'); 30 | 31 | const value = ast.operator === 'AND' 32 | ? left.value && right.value 33 | : left.value || right.value; 34 | 35 | return { 36 | type: 'BooleanValue', 37 | value: value 38 | }; 39 | } 40 | 41 | function compare (val1, val2) { 42 | return val1 === val2 ? 0 : val1 > val2 ? 1 : -1; 43 | } 44 | 45 | function evaluateBinaryExpression(ast) { 46 | const { left, right } = convert(evaluate(ast.left), evaluate(ast.right)); 47 | const operator = ast.operator; 48 | 49 | const cmp = () => { 50 | const comparison = compare(left.value, right.value); 51 | switch (operator) { 52 | case '==': 53 | if (left.type !== right.type) 54 | return false; 55 | return comparison === 0; 56 | case '!=': 57 | if (left.type !== right.type) 58 | return true; 59 | return comparison !== 0; 60 | case '>=': 61 | if (left.type !== right.type) 62 | throw new Error('Node type mismatch'); 63 | return comparison >= 0; 64 | case '>': 65 | if (left.type !== right.type) 66 | throw new Error('Node type mismatch'); 67 | return comparison > 0; 68 | case '<=': 69 | if (left.type !== right.type) 70 | throw new Error('Node type mismatch'); 71 | return comparison <= 0; 72 | case '<': 73 | if (left.type !== right.type) 74 | throw new Error('Node type mismatch'); 75 | return comparison < 0; 76 | } 77 | }; 78 | 79 | return { 80 | type: 'BooleanValue', 81 | value: cmp() 82 | }; 83 | } 84 | 85 | function evaluateMathematicalExpression(ast) { 86 | const { left, right } = convert(evaluate(ast.left), evaluate(ast.right)); 87 | const operator = ast.operator; 88 | 89 | if (left.type !== right.type) { 90 | throw new Error('Node type mismatch'); 91 | } 92 | 93 | if (left.type === 'ColorValue') 94 | return evaluateColorMath(left, right, operator); 95 | 96 | switch (operator) { 97 | case '+': 98 | left.value = left.value + right.value; 99 | break; 100 | case '-': 101 | left.value = left.value - right.value; 102 | break; 103 | case '*': 104 | left.value = left.value * right.value; 105 | break; 106 | case '/': 107 | left.value = left.value / right.value; 108 | break; 109 | } 110 | return left; 111 | } 112 | 113 | function evaluateColorMath(left, right, op) { 114 | const val1 = color(left.value).toRgbaArray(); 115 | const val2 = color(right.value).toRgbaArray(); 116 | 117 | if (val1[3] !== val2[3]) { 118 | throw new Error('Alpha channels must be equal'); 119 | } 120 | 121 | let [r, g, b] = val1; 122 | const a = val1[3]; 123 | 124 | switch (op) { 125 | case '+': 126 | r = Math.min(r + val2[0], 255); 127 | g = Math.min(g + val2[1], 255); 128 | b = Math.min(b + val2[2], 255); 129 | break; 130 | case '-': 131 | r = Math.max(r - val2[0], 0); 132 | g = Math.max(g - val2[1], 0); 133 | b = Math.max(b - val2[2], 0); 134 | break; 135 | case '*': 136 | r = Math.min(r * val2[0], 255); 137 | g = Math.min(g * val2[1], 255); 138 | b = Math.min(b * val2[2], 255); 139 | break; 140 | case '/': 141 | r = Math.max(r / val2[0], 0); 142 | g = Math.max(g / val2[1], 0); 143 | b = Math.max(b / val2[2], 0); 144 | break; 145 | } 146 | 147 | return { 148 | type: 'ColorValue', 149 | value: color().fromRgba([r, g, b, a]).toHexString() 150 | }; 151 | } 152 | 153 | function evaluateUnaryExpression(ast) { 154 | const node = evaluate(ast.argument); 155 | if (node.type !== 'BooleanValue') { 156 | throw new Error('Node type mismatch'); 157 | } 158 | 159 | return { 160 | type: 'BooleanValue', 161 | value: !node.value 162 | }; 163 | } 164 | 165 | export default (expr) => { 166 | let ast = null; 167 | try { 168 | ast = parser.parse(expr); 169 | } catch (e) { 170 | throw new Error('Failed to parse expression'); 171 | } 172 | const result = evaluate(ast); 173 | switch (result.type) { 174 | case 'LogicalExpression': 175 | case 'BinaryExpression': 176 | case 'MathematicalExpression': 177 | case 'UnaryExpression': 178 | throw new Error('Could not evaluate expression'); 179 | default: 180 | return Boolean(result.value); 181 | } 182 | }; 183 | -------------------------------------------------------------------------------- /src/__tests__/index.js: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import postcss from 'postcss'; 3 | 4 | import conditionals from '..'; 5 | 6 | function testFixture(t, fixture, expected = null) { 7 | if (expected === null) 8 | expected = fixture 9 | 10 | const out = postcss(conditionals).process(fixture); 11 | t.deepEqual(out.css, expected) 12 | } 13 | 14 | test( 15 | 'equality (1)', 16 | testFixture, 17 | '@if a == a { success {} }', 18 | ' success {}'); 19 | 20 | test( 21 | 'equality (2)', 22 | testFixture, 23 | '@if a == b { success {} }', 24 | ''); 25 | 26 | test( 27 | 'equality (3)', 28 | testFixture, 29 | '@if a != b { success {} }', 30 | ' success {}'); 31 | 32 | test( 33 | 'equality (4)', 34 | testFixture, 35 | '@if a == b { success {} } @else { branch {} }', 36 | 'branch {}'); 37 | 38 | test( 39 | 'equality (5)', 40 | testFixture, 41 | '@if a != a { success {} }', 42 | ''); 43 | 44 | test( 45 | 'equality (6)', 46 | testFixture, 47 | '@if a != 0 { success {} }', 48 | ' success {}'); 49 | 50 | test( 51 | 'equality (7)', 52 | testFixture, 53 | '@if a == 0 { success {} }', 54 | ''); 55 | 56 | test( 57 | 'comparisons (1)', 58 | testFixture, 59 | '@if 2 > 1 { success {} }', 60 | ' success {}'); 61 | 62 | test( 63 | 'comparisons (2)', 64 | testFixture, 65 | '@if 1 > 2 { success {} }', 66 | ''); 67 | 68 | test( 69 | 'comparisons (3)', 70 | testFixture, 71 | '@if 2 >= 2 { success {} }', 72 | ' success {}'); 73 | 74 | test( 75 | 'comparisons (4)', 76 | testFixture, 77 | '@if 1 >= 2 { success {} }', 78 | ''); 79 | 80 | test( 81 | 'comparisons (5)', 82 | testFixture, 83 | '@if 1 < 2 { success {} }', 84 | ' success {}'); 85 | 86 | test( 87 | 'comparisons (6)', 88 | testFixture, 89 | '@if 2 < 1 { success {} }', 90 | ''); 91 | 92 | test( 93 | 'comparisons (7)', 94 | testFixture, 95 | '@if 2 <= 2 { success {} }', 96 | ' success {}'); 97 | 98 | test( 99 | 'comparisons (8)', 100 | testFixture, 101 | '@if 2 <= 1 { success {} }', 102 | ''); 103 | 104 | test( 105 | 'complex expressions (1)', 106 | testFixture, 107 | '@if a == a AND b == b { success {} }', 108 | ' success {}'); 109 | 110 | test( 111 | 'complex expressions (2)', 112 | testFixture, 113 | '@if a == a AND a == b { success {} }', 114 | ''); 115 | 116 | test( 117 | 'complex expressions (3)', 118 | testFixture, 119 | '@if a == b OR b == b { success {} }', 120 | ' success {}'); 121 | 122 | test( 123 | 'complex expressions (4)', 124 | testFixture, 125 | '@if a == b OR b == c { success {} }', 126 | ''); 127 | 128 | test( 129 | 'complex expressions (5)', 130 | testFixture, 131 | '@if a == b OR b == c { success {} }', 132 | ''); 133 | 134 | test( 135 | 'complex expressions (6)', 136 | testFixture, 137 | '@if a == a OR (c == b AND b == d) { success {} }', 138 | ' success {}'); 139 | 140 | test( 141 | 'unaries', 142 | testFixture, 143 | '@if NOT (a == b) { success {} }', 144 | ' success {}'); 145 | 146 | test( 147 | 'addition (1)', 148 | testFixture, 149 | '@if 1 + 1 == 2 { success {} }', 150 | ' success {}'); 151 | 152 | test( 153 | 'addition (2)', 154 | testFixture, 155 | '@if (1 + 2) + 3 == 6 { success {} }', 156 | ' success {}'); 157 | 158 | test( 159 | 'addition (3)', 160 | testFixture, 161 | '@if (1 + 2) + 3 == 7 { success {} }', 162 | ''); 163 | 164 | test( 165 | 'addition (4)', 166 | testFixture, 167 | '@if 1px + 2 == 3px { success {} }', 168 | ' success {}'); 169 | 170 | test( 171 | 'addition (5)', 172 | testFixture, 173 | '@if 1 + 1px == 2px { success {} }', 174 | ' success {}'); 175 | 176 | test( 177 | 'addition (6)', 178 | testFixture, 179 | '@if (3 + 2) + 1 == 3 { success {} }', 180 | ''); 181 | 182 | test( 183 | 'substraction (1)', 184 | testFixture, 185 | '@if 3 - 2 == 1 { success {} }', 186 | ' success {}'); 187 | 188 | test( 189 | 'substraction (2)', 190 | testFixture, 191 | '@if 3px - 2px == 1px { success {} }', 192 | ' success {}'); 193 | 194 | test( 195 | 'substraction (3)', 196 | testFixture, 197 | '@if 2 - 1px == 1px { success {} }', 198 | ' success {}'); 199 | 200 | test( 201 | 'addition and substraction', 202 | testFixture, 203 | '@if (3 - 2) + 1 == 2 { success {} }', 204 | ' success {}'); 205 | 206 | test( 207 | 'multiplication (1)', 208 | testFixture, 209 | '@if 1 * 2 == 2 { success {} }', 210 | ' success {}'); 211 | 212 | test( 213 | 'multiplication (2)', 214 | testFixture, 215 | '@if (1 * 2) * 3 == 6 { success {} }', 216 | ' success {}'); 217 | 218 | test( 219 | 'multiplication (3)', 220 | testFixture, 221 | '@if (1 * 2) * 3 == 7 { success {} }', 222 | ''); 223 | 224 | test( 225 | 'multiplication (4)', 226 | testFixture, 227 | '@if 3px * 2px == 6px { success {} }', 228 | ' success {}'); 229 | 230 | test( 231 | 'multiplication (5)', 232 | testFixture, 233 | '@if 2 * 2px == 4px { success {} }', 234 | ' success {}'); 235 | 236 | test( 237 | 'division (1)', 238 | testFixture, 239 | '@if 2 / 2 == 1 { success {} }', 240 | ' success {}'); 241 | 242 | test( 243 | 'division (2)', 244 | testFixture, 245 | '@if (4 / 2) / 2 == 1 { success {} }', 246 | ' success {}'); 247 | 248 | test( 249 | 'division (3)', 250 | testFixture, 251 | '@if 4 / (2 / 2) == 1 { success {} }', 252 | ''); 253 | 254 | test( 255 | 'division (4)', 256 | testFixture, 257 | '@if (4 / 2) * 2 == 2 { success {} }', 258 | ''); 259 | 260 | test( 261 | 'division (5)', 262 | testFixture, 263 | '@if 4px / 2px == 2px { success {} }', 264 | ' success {}'); 265 | 266 | test( 267 | 'division (6)', 268 | testFixture, 269 | '@if 4 / 2px == 2px { success {} }', 270 | ' success {}'); 271 | 272 | test( 273 | 'colors (1)', 274 | testFixture, 275 | '@if aqua - blue == lime { success {} }', 276 | ' success {}'); 277 | 278 | test( 279 | 'colors (2)', 280 | testFixture, 281 | '@if lime + rgb(0, 0, 255) == #00ffff { success {} }', 282 | ' success {}'); 283 | 284 | test( 285 | 'colors (3)', 286 | testFixture, 287 | '@if #0ff == #00ffff { success {} }', 288 | ' success {}'); 289 | 290 | test( 291 | 'colors (4)', 292 | testFixture, 293 | '@if #0ff == #00ffffff { success {} }', 294 | ' success {}'); 295 | 296 | test( 297 | 'colors (5)', 298 | testFixture, 299 | '@if #0fff == #00ffff { success {} }', 300 | ' success {}'); 301 | 302 | test( 303 | 'colors (6)', 304 | testFixture, 305 | '@if #0fff == #00ffffff { success {} }', 306 | ' success {}'); 307 | 308 | test( 309 | 'colors (7)', 310 | testFixture, 311 | '@if hsl(0, 0%, 100%) == white { success {} }', 312 | ' success {}'); 313 | 314 | test( 315 | 'colors (8)', 316 | testFixture, 317 | '@if hsla(0, 0%, 100%, .5) == white { success {} }', 318 | ''); 319 | 320 | test( 321 | 'colors (9)', 322 | testFixture, 323 | '@if rgb(0, 255, 255) == aqua { success {} }', 324 | ' success {}'); 325 | 326 | test( 327 | 'colors (10)', 328 | testFixture, 329 | '@if rgba(0, 255, 255, .5) == aqua { success {} }', 330 | ''); 331 | 332 | test( 333 | 'colors (11)', 334 | testFixture, 335 | '@if rgb(0, 255, 255) == rgb(0, 100%, 100%) { success {} }', 336 | ' success {}'); 337 | 338 | test( 339 | 'colors (12)', 340 | testFixture, 341 | '@if rgba(0, 100%, 100%, 1) == rgb(0, 100%, 100%) { success {} }', 342 | ' success {}'); 343 | 344 | test( 345 | 'nesting (1)', 346 | testFixture, 347 | '@if a == a { @if b == b { success {} } }', 348 | ' success {}'); 349 | 350 | test( 351 | 'nesting (2)', 352 | testFixture, 353 | '@if a == a { @if b != b { success {} } @else { branch {} } }', 354 | ' branch {}'); 355 | 356 | test( 357 | 'nesting (3)', 358 | testFixture, 359 | '@if a != a { @if b == b { success {} } }', 360 | ''); 361 | 362 | test( 363 | 'nesting (4)', 364 | testFixture, 365 | '@if a == a { @if b != b { success {} } }', 366 | ''); 367 | 368 | test( 369 | 'nesting (5)', 370 | testFixture, 371 | '@if a == a { @if b == b { @if c==c { success {} } } }', 372 | ' success {}'); 373 | 374 | test( 375 | 'booleans (1)', 376 | testFixture, 377 | '@if true { success {} }', 378 | ' success {}'); 379 | 380 | test( 381 | 'booleans (2)', 382 | testFixture, 383 | '@if false { success {} }', 384 | ''); 385 | 386 | test( 387 | 'booleans (3)', 388 | testFixture, 389 | '@if true AND true { success {} }', 390 | ' success {}'); 391 | 392 | test( 393 | 'booleans (4)', 394 | testFixture, 395 | '@if true OR true { success {} }', 396 | ' success {}' 397 | ); 398 | 399 | test( 400 | 'booleans (5)', 401 | testFixture, 402 | '@if true OR false { success {} }', 403 | ' success {}'); 404 | 405 | test( 406 | 'booleans (6)', 407 | testFixture, 408 | '@if false OR true { success {} }', 409 | ' success {}'); 410 | 411 | test( 412 | 'booleans (7)', 413 | testFixture, 414 | '@if false AND false { success {} }', 415 | ''); 416 | 417 | test( 418 | 'booleans (8)', 419 | testFixture, 420 | '@if false OR false { success {} }', 421 | ''); 422 | 423 | test( 424 | 'booleans (9)', 425 | testFixture, 426 | '@if true == true { success {} }', 427 | ' success {}'); 428 | 429 | test( 430 | 'booleans (10)', 431 | testFixture, 432 | '@if true == false { success {} }', 433 | ''); 434 | 435 | test( 436 | 'booleans (11)', 437 | testFixture, 438 | '@if true != false { success {} }', 439 | ' success {}'); 440 | 441 | test( 442 | 'booleans (12)', 443 | testFixture, 444 | '@if true { foo: bar } @else if false { bar: baz } @else { bat: quux }', 445 | ' foo: bar'); 446 | 447 | test( 448 | 'booleans (13)', 449 | testFixture, 450 | '@if false { foo: bar } @else if true { bar: baz } @else { bat: quux }', 451 | 'bar: baz'); 452 | 453 | test( 454 | 'booleans (14)', 455 | testFixture, 456 | '@if false { foo: bar } @else if false { bar: baz } @else { bat: quux }', 457 | 'bat: quux'); 458 | 459 | test( 460 | 'booleans (15)', 461 | testFixture, 462 | '@if 1 { success {} }', 463 | ' success {}'); 464 | 465 | test( 466 | 'booleans (16)', 467 | testFixture, 468 | '@if 0 { success {} }', 469 | ''); 470 | 471 | test( 472 | 'strings (1)', 473 | testFixture, 474 | '@if \'\' == \'\' { foo: bar }', 475 | ' foo: bar'); 476 | 477 | test( 478 | 'strings (2)', 479 | testFixture, 480 | '@if "" == "" { foo: bar }', 481 | ' foo: bar'); 482 | 483 | test( 484 | 'strings (3)', 485 | testFixture, 486 | '@if \'\' == "" { foo: bar }', 487 | ' foo: bar'); 488 | 489 | test( 490 | 'strings (4)', 491 | testFixture, 492 | '@if "" == \'\' { foo: bar }', 493 | ' foo: bar'); 494 | 495 | test( 496 | 'strings (5)', 497 | testFixture, 498 | '@if \'foo\\bar\' == \'foo\\bar\' { foo: bar }', 499 | ' foo: bar'); 500 | 501 | test( 502 | 'strings (6)', 503 | testFixture, 504 | '@if .foo == .foo { foo: bar }', ' foo: bar'); 505 | 506 | test( 507 | 'strings (7)', 508 | testFixture, 509 | '@if .foo == .bar { foo: bar }', ''); 510 | -------------------------------------------------------------------------------- /parser.jison: -------------------------------------------------------------------------------- 1 | /* description: Parses expressions. */ 2 | 3 | /* lexical grammar */ 4 | %lex 5 | %% 6 | \s+ /* skip whitespace */ 7 | "true" return 'BOOL'; 8 | "TRUE" return 'BOOL'; 9 | "false" return 'BOOL'; 10 | "FALSE" return 'BOOL'; 11 | "AND" return 'OP'; 12 | "and" yytext = yytext.toUpperCase(); return 'OP'; 13 | "OR" return 'OP'; 14 | "or" yytext = yytext.toUpperCase(); return 'OP'; 15 | "NOT" return 'NOT'; 16 | "not" yytext = yytext.toUpperCase(); return 'NOT'; 17 | "*" return 'MUL'; 18 | "/" return 'DIV'; 19 | "+" return 'ADD'; 20 | "-" return 'SUB'; 21 | rgb\(\s*[0-9]+\%?\s*\,\s*[0-9]+\%?\s*\,\s*[0-9]+\%?\s*\) return 'COLOR'; 22 | hsl\(\s*[0-9]+\s*\,\s*[0-9]+\%\s*\,\s*[0-9]+\%\s*\) return 'COLOR'; 23 | rgba\(\s*[0-9]+\%?\s*\,\s*[0-9]+\%?\s*\,\s*[0-9]+\%?\s*\,\s*([0-1]|0?\.[0-9]+)\s*\) return 'COLOR'; 24 | hsla\(\s*[0-9]+\s*\,\s*[0-9]+\%\s*\,\s*[0-9]+\%\s*\,\s*([0-1]|0?\.[0-9]+)\s*\) return 'COLOR'; 25 | \#[0-9a-fA-F]{6}([0-9a-fA-F]{2})?\b return 'COLOR'; 26 | \#[0-9a-fA-F]{3}([0-9a-fA-F])?\b return 'COLOR'; 27 | "aliceblue" return 'COLOR'; 28 | "antiquewhite" return 'COLOR'; 29 | "aqua" return 'COLOR'; 30 | "aquamarine" return 'COLOR'; 31 | "azure" return 'COLOR'; 32 | "beige" return 'COLOR'; 33 | "bisque" return 'COLOR'; 34 | "black" return 'COLOR'; 35 | "blanchedalmond" return 'COLOR'; 36 | "blue" return 'COLOR'; 37 | "blueviolet" return 'COLOR'; 38 | "brown" return 'COLOR'; 39 | "burlywood" return 'COLOR'; 40 | "cadetblue" return 'COLOR'; 41 | "chartreuse" return 'COLOR'; 42 | "chocolate" return 'COLOR'; 43 | "coral" return 'COLOR'; 44 | "cornflowerblue" return 'COLOR'; 45 | "cornsilk" return 'COLOR'; 46 | "crimson" return 'COLOR'; 47 | "cyan" return 'COLOR'; 48 | "darkblue" return 'COLOR'; 49 | "darkcyan" return 'COLOR'; 50 | "darkgoldenrod" return 'COLOR'; 51 | "darkgray" return 'COLOR'; 52 | "darkgreen" return 'COLOR'; 53 | "darkgrey" return 'COLOR'; 54 | "darkkhaki" return 'COLOR'; 55 | "darkmagenta" return 'COLOR'; 56 | "darkolivegreen" return 'COLOR'; 57 | "darkorange" return 'COLOR'; 58 | "darkorchid" return 'COLOR'; 59 | "darkred" return 'COLOR'; 60 | "darksalmon" return 'COLOR'; 61 | "darkseagreen" return 'COLOR'; 62 | "darkslateblue" return 'COLOR'; 63 | "darkslategray" return 'COLOR'; 64 | "darkslategrey" return 'COLOR'; 65 | "darkturquoise" return 'COLOR'; 66 | "darkviolet" return 'COLOR'; 67 | "deeppink" return 'COLOR'; 68 | "deepskyblue" return 'COLOR'; 69 | "dimgray" return 'COLOR'; 70 | "dimgrey" return 'COLOR'; 71 | "dodgerblue" return 'COLOR'; 72 | "firebrick" return 'COLOR'; 73 | "floralwhite" return 'COLOR'; 74 | "forestgreen" return 'COLOR'; 75 | "fuchsia" return 'COLOR'; 76 | "gainsboro" return 'COLOR'; 77 | "ghostwhite" return 'COLOR'; 78 | "gold" return 'COLOR'; 79 | "goldenrod" return 'COLOR'; 80 | "gray" return 'COLOR'; 81 | "green" return 'COLOR'; 82 | "greenyellow" return 'COLOR'; 83 | "grey" return 'COLOR'; 84 | "honeydew" return 'COLOR'; 85 | "hotpink" return 'COLOR'; 86 | "indianred" return 'COLOR'; 87 | "indigo" return 'COLOR'; 88 | "ivory" return 'COLOR'; 89 | "khaki" return 'COLOR'; 90 | "lavender" return 'COLOR'; 91 | "lavenderblush" return 'COLOR'; 92 | "lawngreen" return 'COLOR'; 93 | "lemonchiffon" return 'COLOR'; 94 | "lightblue" return 'COLOR'; 95 | "lightcoral" return 'COLOR'; 96 | "lightcyan" return 'COLOR'; 97 | "lightgoldenrodyellow" return 'COLOR'; 98 | "lightgray" return 'COLOR'; 99 | "lightgreen" return 'COLOR'; 100 | "lightgrey" return 'COLOR'; 101 | "lightpink" return 'COLOR'; 102 | "lightsalmon" return 'COLOR'; 103 | "lightseagreen" return 'COLOR'; 104 | "lightskyblue" return 'COLOR'; 105 | "lightslategray" return 'COLOR'; 106 | "lightslategrey" return 'COLOR'; 107 | "lightsteelblue" return 'COLOR'; 108 | "lightyellow" return 'COLOR'; 109 | "lime" return 'COLOR'; 110 | "limegreen" return 'COLOR'; 111 | "linen" return 'COLOR'; 112 | "magenta" return 'COLOR'; 113 | "maroon" return 'COLOR'; 114 | "mediumaquamarine" return 'COLOR'; 115 | "mediumblue" return 'COLOR'; 116 | "mediumorchid" return 'COLOR'; 117 | "mediumpurple" return 'COLOR'; 118 | "mediumseagreen" return 'COLOR'; 119 | "mediumslateblue" return 'COLOR'; 120 | "mediumspringgreen" return 'COLOR'; 121 | "mediumturquoise" return 'COLOR'; 122 | "mediumvioletred" return 'COLOR'; 123 | "midnightblue" return 'COLOR'; 124 | "mintcream" return 'COLOR'; 125 | "mistyrose" return 'COLOR'; 126 | "moccasin" return 'COLOR'; 127 | "navajowhite" return 'COLOR'; 128 | "navy" return 'COLOR'; 129 | "oldlace" return 'COLOR'; 130 | "olive" return 'COLOR'; 131 | "olivedrab" return 'COLOR'; 132 | "orange" return 'COLOR'; 133 | "orangered" return 'COLOR'; 134 | "orchid" return 'COLOR'; 135 | "palegoldenrod" return 'COLOR'; 136 | "palegreen" return 'COLOR'; 137 | "paleturquoise" return 'COLOR'; 138 | "palevioletred" return 'COLOR'; 139 | "papayawhip" return 'COLOR'; 140 | "peachpuff" return 'COLOR'; 141 | "peru" return 'COLOR'; 142 | "pink" return 'COLOR'; 143 | "plum" return 'COLOR'; 144 | "powderblue" return 'COLOR'; 145 | "purple" return 'COLOR'; 146 | "rebeccapurple" return 'COLOR'; 147 | "red" return 'COLOR'; 148 | "rosybrown" return 'COLOR'; 149 | "royalblue" return 'COLOR'; 150 | "saddlebrown" return 'COLOR'; 151 | "salmon" return 'COLOR'; 152 | "sandybrown" return 'COLOR'; 153 | "seagreen" return 'COLOR'; 154 | "seashell" return 'COLOR'; 155 | "sienna" return 'COLOR'; 156 | "silver" return 'COLOR'; 157 | "skyblue" return 'COLOR'; 158 | "slateblue" return 'COLOR'; 159 | "slategray" return 'COLOR'; 160 | "slategrey" return 'COLOR'; 161 | "snow" return 'COLOR'; 162 | "springgreen" return 'COLOR'; 163 | "steelblue" return 'COLOR'; 164 | "tan" return 'COLOR'; 165 | "teal" return 'COLOR'; 166 | "thistle" return 'COLOR'; 167 | "tomato" return 'COLOR'; 168 | "turquoise" return 'COLOR'; 169 | "violet" return 'COLOR'; 170 | "wheat" return 'COLOR'; 171 | "white" return 'COLOR'; 172 | "whitesmoke" return 'COLOR'; 173 | "yellow" return 'COLOR'; 174 | "yellowgreen" return 'COLOR'; 175 | [0-9]+("."[0-9]+)?px\b return 'LENGTH'; 176 | [0-9]+("."[0-9]+)?cm\b return 'LENGTH'; 177 | [0-9]+("."[0-9]+)?mm\b return 'LENGTH'; 178 | [0-9]+("."[0-9]+)?in\b return 'LENGTH'; 179 | [0-9]+("."[0-9]+)?pt\b return 'LENGTH'; 180 | [0-9]+("."[0-9]+)?pc\b return 'LENGTH'; 181 | [0-9]+("."[0-9]+)?deg\b return 'ANGLE'; 182 | [0-9]+("."[0-9]+)?grad\b return 'ANGLE'; 183 | [0-9]+("."[0-9]+)?rad\b return 'ANGLE'; 184 | [0-9]+("."[0-9]+)?turn\b return 'ANGLE'; 185 | [0-9]+("."[0-9]+)?s\b return 'TIME'; 186 | [0-9]+("."[0-9]+)?ms\b return 'TIME'; 187 | [0-9]+("."[0-9]+)?Hz\b return 'FREQ'; 188 | [0-9]+("."[0-9]+)?kHz\b return 'FREQ'; 189 | [0-9]+("."[0-9]+)?dpi\b return 'RES'; 190 | [0-9]+("."[0-9]+)?dpcm\b return 'RES'; 191 | [0-9]+("."[0-9]+)?dppx\b return 'RES'; 192 | [0-9]+("."[0-9]+)?em\b return 'EMS'; 193 | [0-9]+("."[0-9]+)?ex\b return 'EXS'; 194 | [0-9]+("."[0-9]+)?ch\b return 'CHS'; 195 | [0-9]+("."[0-9]+)?rem\b return 'REMS'; 196 | [0-9]+("."[0-9]+)?vw\b return 'VHS'; 197 | [0-9]+("."[0-9]+)?vh\b return 'VWS'; 198 | [0-9]+("."[0-9]+)?vmin\b return 'VMINS'; 199 | [0-9]+("."[0-9]+)?vmax\b return 'VMAXS'; 200 | [0-9]+("."[0-9]+)?\% return 'PERCENTAGE'; 201 | [0-9]+("."[0-9]+)?\b return 'NUMBER'; 202 | [a-zA-Z0-9-_.]+\b return 'STRING'; 203 | \'(\\[^\']|[^\'\\])*\' yytext = yytext.slice(1,-1); return 'STRING'; 204 | \"(\\[^\"]|[^\"\\])*\" yytext = yytext.slice(1,-1); return 'STRING'; 205 | "(" return 'LPAREN'; 206 | ")" return 'RPAREN'; 207 | "==" return 'RELOP'; 208 | "!=" return 'RELOP'; 209 | ">=" return 'RELOP'; 210 | ">" return 'RELOP'; 211 | "<=" return 'RELOP'; 212 | "<" return 'RELOP'; 213 | <> return 'EOF'; 214 | 215 | /lex 216 | 217 | %left ADD SUB 218 | %left MUL DIV 219 | %left OP 220 | %left NOT 221 | %left STRING 222 | %left RELOP 223 | %left UPREC 224 | 225 | %start expression 226 | 227 | %% 228 | 229 | expression 230 | : expr EOF { return $1; } 231 | ; 232 | 233 | expr 234 | : logical_expression { $$ = $1; } 235 | | LPAREN logical_expression RPAREN { $$ = $2; } 236 | | binary_expression { $$ = $1; } 237 | | LPAREN binary_expression RPAREN { $$ = $2; } 238 | | unary_expression { $$ = $1; } 239 | | LPAREN unary_expression RPAREN { $$ = $2; } 240 | | math_expression { $$ = $1; } 241 | | bool_value { $$ = $1; } 242 | | string { $$ = $1; } 243 | ; 244 | 245 | binary_expression 246 | : expr RELOP expr %prec UPREC { $$ = { type: 'BinaryExpression', operator: $2, left: $1, right: $3 }; } 247 | ; 248 | 249 | unary_expression 250 | : NOT css_value { $$ = { type: 'UnaryExpression', operator: $1, argument: $2 }; } 251 | | NOT value { $$ = { type: 'UnaryExpression', operator: $1, argument: $2 }; } 252 | | NOT string { $$ = { type: 'UnaryExpression', operator: $1, argument: $2 }; } 253 | | NOT LPAREN expr RPAREN { $$ = { type: 'UnaryExpression', operator: $1, argument: $3 }; } 254 | ; 255 | 256 | logical_expression 257 | : expr OP expr { $$ = { type: 'LogicalExpression', operator: $2, left: $1, right: $3 }; } 258 | ; 259 | 260 | math_expression 261 | : math_expression ADD math_expression { $$ = { type: 'MathematicalExpression', operator: $2, left: $1, right: $3 }; } 262 | | math_expression SUB math_expression { $$ = { type: 'MathematicalExpression', operator: $2, left: $1, right: $3 }; } 263 | | math_expression MUL math_expression { $$ = { type: 'MathematicalExpression', operator: $2, left: $1, right: $3 }; } 264 | | math_expression DIV math_expression { $$ = { type: 'MathematicalExpression', operator: $2, left: $1, right: $3 }; } 265 | | LPAREN math_expression RPAREN { $$ = $2; } 266 | | css_value { $$ = $1; } 267 | | color_value { $$ = $1; } 268 | | value { $$ = $1; } 269 | ; 270 | 271 | bool_value 272 | : BOOL { $$ = { type: 'BooleanValue', value: $1.toLowerCase() == "true" }; } 273 | | LPAREN bool_value RPAREN { $$ = $2; } 274 | ; 275 | 276 | value 277 | : NUMBER { $$ = { type: 'Value', value: parseFloat($1) }; } 278 | | SUB NUMBER { $$ = { type: 'Value', value: -parseFloat($2) }; } 279 | ; 280 | 281 | css_value 282 | : LENGTH { $$ = { type: 'LengthValue', value: parseFloat($1), unit: /[a-z]+/.exec($1)[0] }; } 283 | | ANGLE { $$ = { type: 'AngleValue', value: parseFloat($1), unit: /[a-z]+/.exec($1)[0] }; } 284 | | TIME { $$ = { type: 'TimeValue', value: parseFloat($1), unit: /[a-z]+/.exec($1)[0] }; } 285 | | FREQ { $$ = { type: 'FrequencyValue', value: parseFloat($1), unit: /[a-z]+/.exec($1)[0] }; } 286 | | RES { $$ = { type: 'ResolutionValue', value: parseFloat($1), unit: /[a-z]+/.exec($1)[0] }; } 287 | | EMS { $$ = { type: 'EmValue', value: parseFloat($1) }; } 288 | | EXS { $$ = { type: 'ExValue', value: parseFloat($1) }; } 289 | | CHS { $$ = { type: 'ChValue', value: parseFloat($1) }; } 290 | | REMS { $$ = { type: 'RemValue', value: parseFloat($1) }; } 291 | | VHS { $$ = { type: 'VhValue', value: parseFloat($1) }; } 292 | | VWS { $$ = { type: 'VwValue', value: parseFloat($1) }; } 293 | | VMINS { $$ = { type: 'VminValue', value: parseFloat($1) }; } 294 | | VMAXS { $$ = { type: 'VmaxValue', value: parseFloat($1) }; } 295 | | PERCENTAGE { $$ = { type: 'PercentageValue', value: parseFloat($1) }; } 296 | | SUB css_value { var prev = $2; prev.value *= -1; $$ = prev; } 297 | ; 298 | 299 | color_value 300 | : COLOR { $$ = { type: 'ColorValue', value: $1 }; } 301 | ; 302 | 303 | string 304 | : STRING { $$ = { type: 'String', value: $1 }; } 305 | ; 306 | --------------------------------------------------------------------------------