├── .gitignore ├── .travis.yml ├── History.md ├── Readme.md ├── index.js ├── package.json └── test ├── fixtures ├── case-sensitive.css ├── case-sensitive.out.css ├── js-variables.css ├── js-variables.out.css ├── media-query.css ├── media-query.out.css ├── preserve-variables.css ├── preserve-variables.out.css ├── remove-properties.css ├── remove-properties.out.css ├── substitution-defined.css ├── substitution-defined.out.css ├── substitution-empty.css ├── substitution-fallback.css ├── substitution-fallback.out.css ├── substitution-malformed.css ├── substitution-overwrite.css ├── substitution-overwrite.out.css └── substitution-undefined.css └── test.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.10" 4 | -------------------------------------------------------------------------------- /History.md: -------------------------------------------------------------------------------- 1 | 3.1.1 / 2014-06-19 2 | ================== 3 | 4 | * fix resolution of variables that contain a CSS function in their fallback 5 | 6 | 3.1.0 / 2014-06-19 7 | ================== 8 | 9 | * remove Component(1) support 10 | * add the option to preserve variables in the output 11 | 12 | 3.0.0 / 2014-04-17 13 | ================== 14 | 15 | * update syntax from `var-*` to `--*` to match spec 16 | 17 | 2.0.3 / 2014-02-11 18 | ================== 19 | 20 | * fix persistent vars map 21 | 22 | 2.0.2 / 2013-12-18 23 | ================== 24 | 25 | * fix `var-*` property stripping from output 26 | 27 | 2.0.1 / 2013-12-18 28 | ================== 29 | 30 | * fix the plugin throwing errors when processing anything that isn't a basic rule 31 | 32 | 2.0.0 / 2013-12-18 33 | ================== 34 | 35 | * limit variable declarations to `:root` 36 | * determine the value of variables before replacement 37 | * disallow variable declarations within `@media` and `@supports` 38 | 39 | 1.1.0 / 2013-12-01 40 | ================== 41 | 42 | * add support for fallback values 43 | * add support for overwriting variable values 44 | * add stripping of old `var-*` properties from output 45 | 46 | 1.0.1 / 2013-07-23 47 | ================== 48 | 49 | * fix for comments nested inside selectors 50 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # rework-vars [![Build Status](https://travis-ci.org/reworkcss/rework-vars.png)](https://travis-ci.org/reworkcss/rework-vars) 2 | 3 | A [Rework](https://github.com/reworkcss/rework) plugin to add support for the 4 | [W3C-style CSS variables](http://www.w3.org/TR/css-variables/) syntax. 5 | 6 | **N.B.** This is _not_ a polyfill. This plugin aims to provide a future-proof 7 | way of using a _limited subset_ of the features provided by native CSS variables. 8 | 9 | ## Installation 10 | 11 | ``` 12 | npm install rework-vars 13 | ``` 14 | 15 | ## Use 16 | 17 | As a Rework plugin: 18 | 19 | ```js 20 | // dependencies 21 | var fs = require('fs'); 22 | var rework = require('rework'); 23 | var vars = require('rework-vars'); 24 | 25 | // css to be processed 26 | var css = fs.readFileSync('build/build.css', 'utf8').toString(); 27 | 28 | // process css using rework-vars 29 | var options = {}; 30 | var out = rework(css).use(vars(options)).toString(); 31 | ``` 32 | 33 | ### Options 34 | 35 | #### `map` 36 | 37 | Optionally, you may pass an object of variables - `map` - to the JavaScript 38 | function. 39 | 40 | ```js 41 | var map = { 42 | 'app-bg-color': 'white' 43 | } 44 | 45 | var out = rework(css).use(vars({map: map})).toString(); 46 | ``` 47 | 48 | #### `preserve` (default: `false`) 49 | 50 | Setting `preserve` to `true` will preserve the variable definitions and 51 | references in the output, so that they can be used by supporting browsers. 52 | 53 | ```js 54 | var out = rework(css).use(vars({preserve: true})).toString(); 55 | ``` 56 | 57 | ## Supported features 58 | 59 | Variables can be declared as custom CSS properties on the `:root` element, 60 | prefixed with `--`: 61 | 62 | ```css 63 | :root { 64 | --my-color: red; 65 | } 66 | ``` 67 | 68 | Variables are applied using the `var()` function, taking the name of a variable 69 | as the first argument: 70 | 71 | ```css 72 | :root { 73 | --my-color: red; 74 | } 75 | 76 | div { 77 | color: var(--my-color); 78 | } 79 | ``` 80 | 81 | Fallback values are supported and are applied if a variable has not been 82 | declared: 83 | 84 | ```css 85 | :root { 86 | --my-color: red; 87 | } 88 | 89 | div { 90 | color: var(--other-color, green); 91 | } 92 | ``` 93 | 94 | Fallbacks can be "complex". Anything after the first comma in the `var()` 95 | function will act as the fallback value – `var(name, fallback)`. Nested 96 | variables are also supported: 97 | 98 | ```css 99 | :root { 100 | --my-color: red; 101 | } 102 | 103 | div { 104 | background: var(--my-other-color, linear-gradient(var(--my-color), rgba(255,0,0,0.5))); 105 | } 106 | ``` 107 | 108 | ## What to expect 109 | 110 | Variables can _only_ be declared for, and scoped to the `:root` element. All 111 | other variable declarations are left untouched. Any known variables used as 112 | values are replaced. 113 | 114 | ```css 115 | :root { 116 | --color-one: red; 117 | --color-two: green; 118 | } 119 | 120 | :root, 121 | div { 122 | --color-two: purple; 123 | color: var(--color-two); 124 | } 125 | 126 | div { 127 | --color-three: blue; 128 | } 129 | 130 | span { 131 | --color-four: yellow; 132 | } 133 | ``` 134 | 135 | yields: 136 | 137 | ```css 138 | :root, 139 | div { 140 | --color-two: purple; 141 | color: green; 142 | } 143 | 144 | div { 145 | --color-three: blue; 146 | } 147 | 148 | span { 149 | --color-four: yellow; 150 | } 151 | ``` 152 | 153 | Variables are not dynamic; they are replaced with normal CSS values. The value 154 | of a defined variable is determined by the last declaration of that variable 155 | for `:root`. 156 | 157 | ```css 158 | :root { 159 | --brand-color: green; 160 | } 161 | 162 | .brand { 163 | color: var(--brand-color); 164 | } 165 | 166 | :root { 167 | --brand-color: red; 168 | } 169 | ``` 170 | 171 | yields: 172 | 173 | ```css 174 | .brand { 175 | color: red; 176 | } 177 | ``` 178 | 179 | Variables declared within `@media` or `@supports` are not currently supported 180 | and will also be ignored. 181 | 182 | ```css 183 | @media (min-width: 320px) { 184 | :root { 185 | --brand-color: red; 186 | } 187 | } 188 | ``` 189 | 190 | yields: 191 | 192 | ```css 193 | @media (min-width: 320px) { 194 | :root { 195 | --brand-color: red; 196 | } 197 | } 198 | ``` 199 | 200 | ## License 201 | 202 | MIT 203 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Module dependencies. 3 | */ 4 | 5 | var balanced = require('balanced-match'); 6 | var visit = require('rework-visit'); 7 | 8 | /** 9 | * Constants. 10 | */ 11 | 12 | var VAR_PROP_IDENTIFIER = '--'; 13 | var VAR_FUNC_IDENTIFIER = 'var'; 14 | 15 | /** 16 | * Module export. 17 | */ 18 | 19 | module.exports = function (options) { 20 | 21 | return function vars(style) { 22 | options = options || {}; 23 | 24 | var map = options.map || {}; 25 | var preserve = (options.preserve === true ? true : false); 26 | 27 | // define variables 28 | style.rules.forEach(function (rule) { 29 | var varNameIndices = []; 30 | 31 | if (rule.type !== 'rule') return; 32 | // only variables declared for `:root` are supported 33 | if (rule.selectors.length !== 1 || rule.selectors[0] !== ':root') return; 34 | 35 | rule.declarations.forEach(function (decl, i) { 36 | var prop = decl.property; 37 | var value = decl.value; 38 | 39 | if (prop && prop.indexOf(VAR_PROP_IDENTIFIER) === 0) { 40 | map[prop] = value; 41 | varNameIndices.push(i); 42 | } 43 | }); 44 | 45 | // optionally remove `--*` properties from the rule 46 | if (!preserve) { 47 | for (var i = varNameIndices.length - 1; i >= 0; i--) { 48 | rule.declarations.splice(varNameIndices[i], 1); 49 | } 50 | } 51 | }); 52 | 53 | // resolve variables 54 | visit(style, function (declarations, node) { 55 | var decl; 56 | var resolvedValue; 57 | var value; 58 | 59 | for (var i = 0; i < declarations.length; i++) { 60 | decl = declarations[i]; 61 | value = decl.value; 62 | 63 | // skip comments 64 | if (decl.type !== 'declaration') { 65 | continue; 66 | } 67 | 68 | // skip values that don't contain variable functions 69 | if (!value || value.indexOf(VAR_FUNC_IDENTIFIER + '(') === -1) { 70 | continue; 71 | } 72 | 73 | resolvedValue = resolveValue(value, map); 74 | 75 | if (!preserve) { 76 | decl.value = resolvedValue; 77 | } else { 78 | declarations.splice(i, 0, { 79 | type: decl.type, 80 | property: decl.property, 81 | value: resolvedValue 82 | }); 83 | // skip ahead of preserved declaration 84 | i++; 85 | } 86 | } 87 | }); 88 | }; 89 | }; 90 | 91 | /** 92 | * Resolve CSS variables in a value 93 | * 94 | * The second argument to a CSS variable function, if provided, is a fallback 95 | * value, which is used as the substitution value when the referenced variable 96 | * is invalid. 97 | * 98 | * var(name[, fallback]) 99 | * 100 | * @param {String} value A property value known to contain CSS variable functions 101 | * @param {Object} map A map of variable names and values 102 | * @return {String} A property value with all CSS variables substituted. 103 | */ 104 | 105 | function resolveValue(value, map) { 106 | // matches `name[, fallback]`, captures 'name' and 'fallback' 107 | var RE_VAR = /([\w-]+)(?:\s*,\s*)?(.*)?/; 108 | var balancedParens = balanced('(', ')', value); 109 | var varStartIndex = value.indexOf('var('); 110 | var varRef = balanced('(', ')', value.substring(varStartIndex)).body; 111 | 112 | if (!balancedParens) { 113 | throw new Error( 114 | 'rework-vars: missing closing ")" in the value "' + value + '"' 115 | ); 116 | } 117 | 118 | if (varRef === '') { 119 | throw new Error('rework-vars: var() must contain a non-whitespace string'); 120 | } 121 | 122 | var varFunc = VAR_FUNC_IDENTIFIER + '(' + varRef + ')'; 123 | 124 | var varResult = varRef.replace(RE_VAR, function (_, name, fallback) { 125 | var replacement = map[name]; 126 | 127 | if (!replacement && !fallback) { 128 | throw new Error('rework-vars: variable "' + name + '" is undefined'); 129 | } 130 | 131 | if (!replacement && fallback) { 132 | return fallback; 133 | } 134 | 135 | return replacement; 136 | }); 137 | 138 | // resolve the variable 139 | value = value.split(varFunc).join(varResult); 140 | 141 | // recursively resolve any remaining variables in the value 142 | if (value.indexOf(VAR_FUNC_IDENTIFIER) !== -1) { 143 | value = resolveValue(value, map); 144 | } 145 | 146 | return value; 147 | } 148 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rework-vars", 3 | "version": "3.1.1", 4 | "description": "CSS spec style variables for Rework", 5 | "dependencies": { 6 | "rework-visit": "1.0.0", 7 | "balanced-match": "~0.1.0" 8 | }, 9 | "devDependencies": { 10 | "mocha": "~1.14.0", 11 | "rework": "^1.0.0" 12 | }, 13 | "files": [ 14 | "index.js" 15 | ], 16 | "scripts": { 17 | "test": "mocha --no-colors", 18 | "watch": "mocha --slow 30 --reporter spec --watch" 19 | }, 20 | "repository": { 21 | "type": "git", 22 | "url": "https://github.com/reworkcss/rework-vars.git" 23 | }, 24 | "license": "MIT", 25 | "keywords": [ 26 | "css", 27 | "rework", 28 | "rework-plugin", 29 | "variables", 30 | "vars" 31 | ] 32 | } 33 | -------------------------------------------------------------------------------- /test/fixtures/case-sensitive.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --TEST-color: red; 3 | --tESt-COLOR: green; 4 | } 5 | 6 | div { 7 | color: var(--TEST-color); 8 | color: var(--tESt-COLOR); 9 | } 10 | -------------------------------------------------------------------------------- /test/fixtures/case-sensitive.out.css: -------------------------------------------------------------------------------- 1 | div { 2 | color: red; 3 | color: green; 4 | } 5 | -------------------------------------------------------------------------------- /test/fixtures/js-variables.css: -------------------------------------------------------------------------------- 1 | div { 2 | color: var(--color); 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/js-variables.out.css: -------------------------------------------------------------------------------- 1 | div { 2 | color: red; 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/media-query.css: -------------------------------------------------------------------------------- 1 | @media screen and (min-width: 320px) { 2 | :root { 3 | --error: red; 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /test/fixtures/media-query.out.css: -------------------------------------------------------------------------------- 1 | @media screen and (min-width: 320px) { 2 | :root { 3 | --error: red; 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /test/fixtures/preserve-variables.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --color-one: red; 3 | --color-two: blue; 4 | } 5 | 6 | .atthebeginning { 7 | color: var(--color-one); 8 | prop: after; 9 | } 10 | 11 | .attheend { 12 | prop: before; 13 | color: var(--color-two); 14 | } 15 | 16 | .surrounded { 17 | prop: before; 18 | color: var(--undefined-color, green); 19 | otherprop: after; 20 | } 21 | -------------------------------------------------------------------------------- /test/fixtures/preserve-variables.out.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --color-one: red; 3 | --color-two: blue; 4 | } 5 | 6 | .atthebeginning { 7 | color: red; 8 | color: var(--color-one); 9 | prop: after; 10 | } 11 | 12 | .attheend { 13 | prop: before; 14 | color: blue; 15 | color: var(--color-two); 16 | } 17 | 18 | .surrounded { 19 | prop: before; 20 | color: green; 21 | color: var(--undefined-color, green); 22 | otherprop: after; 23 | } 24 | -------------------------------------------------------------------------------- /test/fixtures/remove-properties.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --test-one: test; 3 | --test-two: test; 4 | } 5 | 6 | div { 7 | color: red; 8 | } 9 | 10 | :root { 11 | --test-three: test; 12 | --test-four: test; 13 | } 14 | -------------------------------------------------------------------------------- /test/fixtures/remove-properties.out.css: -------------------------------------------------------------------------------- 1 | div { 2 | color: red; 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/substitution-defined.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Test comment 3 | */ 4 | 5 | :root { 6 | --test-one: green; 7 | --test-two: blue; 8 | --test-three: yellow; 9 | } 10 | 11 | :root, 12 | span { 13 | --untouched: red; 14 | } 15 | 16 | div { 17 | --untouched: red; 18 | /* single variable */ 19 | color: var(--test-one); 20 | /* single variable with tail */ 21 | color: var(--test-one) !important; 22 | /* multiple variables */ 23 | color: var(--test-one), var(--test-two); 24 | /* variable with function in fallback */ 25 | border: var(--test-one, 1px solid rgba(0, 0, 0, 0.1)); 26 | /* multiple variables within a function */ 27 | background: linear-gradient(to top, var(--test-one), var(--test-two)); 28 | } 29 | -------------------------------------------------------------------------------- /test/fixtures/substitution-defined.out.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Test comment 3 | */ 4 | 5 | 6 | 7 | :root, 8 | span { 9 | --untouched: red; 10 | } 11 | 12 | div { 13 | --untouched: red; 14 | /* single variable */ 15 | color: green; 16 | /* single variable with tail */ 17 | color: green !important; 18 | /* multiple variables */ 19 | color: green, blue; 20 | /* variable with function in fallback */ 21 | border: green; 22 | /* multiple variables within a function */ 23 | background: linear-gradient(to top, green, blue); 24 | } 25 | -------------------------------------------------------------------------------- /test/fixtures/substitution-empty.css: -------------------------------------------------------------------------------- 1 | div { 2 | color: var(); 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/substitution-fallback.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --nested: green; 3 | } 4 | 5 | div { 6 | /* simple fallback */ 7 | color: var(--missing, green); 8 | /* comma-separated fallback */ 9 | color: var(--missing, green, blue); 10 | /* fallback is a function */ 11 | background: var(--missing, linear-gradient(to top, #000, #111)); 12 | /* fallback contains a function */ 13 | background: var(--missing, 1px solid rgba(0, 0, 0, 0.1)); 14 | /* fallback is a function containing a function */ 15 | background: var(--missing, linear-gradient(to top, #000, rgba(0, 0, 0, 0.5))); 16 | /* fallback contains a defined variable */ 17 | background: var(--missing, var(--nested)); 18 | /* fallback contains a defined variable within a function */ 19 | background: var(--missing, linear-gradient(to top, #000, var(--nested))); 20 | /* fallback contains an undefined variable with a fallack */ 21 | background: var(--missing, var(--also-missing, green)); 22 | } 23 | -------------------------------------------------------------------------------- /test/fixtures/substitution-fallback.out.css: -------------------------------------------------------------------------------- 1 | div { 2 | /* simple fallback */ 3 | color: green; 4 | /* comma-separated fallback */ 5 | color: green, blue; 6 | /* fallback is a function */ 7 | background: linear-gradient(to top, #000, #111); 8 | /* fallback contains a function */ 9 | background: 1px solid rgba(0, 0, 0, 0.1); 10 | /* fallback is a function containing a function */ 11 | background: linear-gradient(to top, #000, rgba(0, 0, 0, 0.5)); 12 | /* fallback contains a defined variable */ 13 | background: green; 14 | /* fallback contains a defined variable within a function */ 15 | background: linear-gradient(to top, #000, green); 16 | /* fallback contains an undefined variable with a fallack */ 17 | background: green; 18 | } 19 | -------------------------------------------------------------------------------- /test/fixtures/substitution-malformed.css: -------------------------------------------------------------------------------- 1 | div { 2 | /* missing closing ')' */ 3 | color: var(--test, rgba(0,0,0,0.5); 4 | } 5 | -------------------------------------------------------------------------------- /test/fixtures/substitution-overwrite.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --test-override: red; 3 | } 4 | 5 | div { 6 | background: var(--test-override); 7 | color: var(--test-override); 8 | } 9 | 10 | :root { 11 | --test-override: green; 12 | } 13 | -------------------------------------------------------------------------------- /test/fixtures/substitution-overwrite.out.css: -------------------------------------------------------------------------------- 1 | div { 2 | background: green; 3 | color: green; 4 | } 5 | -------------------------------------------------------------------------------- /test/fixtures/substitution-undefined.css: -------------------------------------------------------------------------------- 1 | div { 2 | color: var(--test); 3 | } 4 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'); 2 | var fs = require('fs'); 3 | var rework = require('rework'); 4 | var vars = require('..'); 5 | 6 | function fixture(name) { 7 | return fs.readFileSync('test/fixtures/' + name + '.css', 'utf8').trim(); 8 | } 9 | 10 | function compareFixtures(name, options) { 11 | var actual = rework(fixture(name)).use(vars(options)).toString().trim(); 12 | var expected = fixture(name + '.out'); 13 | return assert.equal(actual, expected); 14 | } 15 | 16 | describe('rework-vars', function () { 17 | it('removes variable properties from the output', function () { 18 | compareFixtures('remove-properties'); 19 | }); 20 | 21 | it('throws an error when a variable function is empty', function () { 22 | var output = function () { 23 | return rework(fixture('substitution-empty')).use(vars()).toString(); 24 | }; 25 | assert.throws(output, Error, 'rework-vars: var() must contain a non-whitespace string'); 26 | }); 27 | 28 | it('throws an error when a variable function references an undefined variable', function () { 29 | var output = function () { 30 | return rework(fixture('substitution-undefined')).use(vars()).toString(); 31 | }; 32 | assert.throws(output, Error, 'rework-vars: variable "--test" is undefined'); 33 | }); 34 | 35 | it('throws an error when a variable function is malformed', function () { 36 | var output = function () { 37 | return rework(fixture('substitution-malformed')).use(vars()).toString(); 38 | }; 39 | assert.throws(output, Error, 'rework-vars: missing closing ")" in the value "var(--test, rgba(0,0,0,0.5)"'); 40 | }); 41 | 42 | it('ignores variables defined in a media query', function () { 43 | compareFixtures('media-query'); 44 | }); 45 | 46 | it('substitutes defined variables in `:root` only', function () { 47 | compareFixtures('substitution-defined'); 48 | }); 49 | 50 | it('overwrites variables correctly', function () { 51 | compareFixtures('substitution-overwrite'); 52 | }); 53 | 54 | it('substitutes undefined variables if there is a fallback', function () { 55 | compareFixtures('substitution-fallback'); 56 | }); 57 | 58 | it('supports case-sensitive variables', function () { 59 | compareFixtures('case-sensitive'); 60 | }); 61 | 62 | it('accepts variable definitions from JavaScript', function () { 63 | compareFixtures('js-variables', { map: { '--color': 'red' } }); 64 | }); 65 | 66 | it('preserves variables when `preserve` is `true`', function () { 67 | compareFixtures('preserve-variables', { preserve: true }); 68 | }); 69 | }); 70 | --------------------------------------------------------------------------------