├── .gitignore ├── History.md ├── Makefile ├── Readme.md ├── lib ├── adjusters.js ├── convert.js ├── index.js └── parse.js ├── package.json └── test ├── convert.js └── parse.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /History.md: -------------------------------------------------------------------------------- 1 | 1.3.3 - October 7, 2017 2 | ----------------------- 3 | * REVERT RELEASE: This removes the changes made in `#19` only for this release. Those changes will be added back and released in a 2.0 because they are breaking changes. 4 | 5 | 1.3.2 - October 4, 2017 6 | ----------------------- 7 | * WARNING: This contains a breaking change in #19. If you need the 1.x series, use 1.3.3. Otherwise, use the 2.x series. 8 | * Fixes Vulnerability - Regular Expression Denial of Service caused by debug package. `#31` 9 | 10 | 1.3.1 - July 14, 2017 11 | ----------------------- 12 | * WARNING: This contains a breaking change in #19. If you need the 1.x series, use 1.3.3. Otherwise, use the 2.x series. 13 | * Fixes Tint, Shade, and Contrast Alpha Interference Issue. `#26` 14 | 15 | 1.3.0 - January 5, 2016 16 | ----------------------- 17 | * upgrade `color` to `0.11.0` 18 | 19 | 1.2.1 - March 16, 2015 20 | ---------------------- 21 | * add coercing strings to numbers for rgba 22 | 23 | 1.2.0 - March 6, 2015 24 | --------------------- 25 | * add `contrast` support 26 | 27 | 1.1.2 - November 27, 2014 28 | ------------------------- 29 | * fix for nested color functions 30 | 31 | 1.1.1 - August 26, 2014 32 | ----------------------- 33 | * support nested color functions 34 | 35 | 1.1.0 - August 12, 2014 36 | ----------------------- 37 | * throw errors for unknown adjusters 38 | 39 | 1.0.0 - August 11, 2014 40 | ----------------------- 41 | * add support for nested color functions 42 | * add `whiteness` and `blackness` support 43 | 44 | 0.1.0 - December 25, 2013 45 | ------------------------- 46 | * fix syntax errors 47 | * add syntax error test 48 | 49 | 0.0.2 - December 16, 2013 50 | ------------------------- 51 | * fix percentage rgba logic 52 | 53 | 0.0.1 - December 16, 2013 54 | ------------------------- 55 | :sparkles: 56 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | 2 | node_modules: package.json 3 | @npm install 4 | 5 | test: node_modules 6 | @node_modules/.bin/mocha test/parse test/convert --reporter spec --bail 7 | 8 | .PHONY: test -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | 2 | # css-color-function 3 | 4 | A parser and converter for [Tab Atkins](https://github.com/tabatkins)'s [proposed color function](http://dev.w3.org/csswg/css-color/#modifying-colors) in CSS. 5 | 6 | ## Installation 7 | 8 | $ npm install css-color-function 9 | 10 | ## Example 11 | 12 | ```js 13 | var color = require('css-color-function'); 14 | 15 | color.convert('color(red tint(50%))'); 16 | // "rgb(255, 128, 128)" 17 | 18 | color.parse('color(red blue(+ 30))'); 19 | // { 20 | // type: 'function', 21 | // name: 'color', 22 | // arguments: [ 23 | // { 24 | // type: 'color', 25 | // value: 'red' 26 | // }, 27 | // { 28 | // type: 'function', 29 | // name: 'blue', 30 | // arguments: [ 31 | // { 32 | // type: 'modifier', 33 | // value: '+' 34 | // }, 35 | // { 36 | // type: 'number', 37 | // value: '30' 38 | // } 39 | // ] 40 | // } 41 | // ] 42 | // } 43 | ``` 44 | 45 | ## API 46 | 47 | ### color.convert(string) 48 | 49 | Convert a color function CSS `string` into an RGB color string. 50 | 51 | ### color.parse(string) 52 | 53 | Parse a color function CSS `string` and return an AST. 54 | 55 | ## License 56 | 57 | The MIT License (MIT) 58 | 59 | Copyright (c) 2013 Ian Storm Taylor <ian@segment.io> 60 | 61 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 62 | 63 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 64 | 65 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 66 | -------------------------------------------------------------------------------- /lib/adjusters.js: -------------------------------------------------------------------------------- 1 | 2 | var Color = require('color'); 3 | 4 | /** 5 | * Basic RGBA adjusters. 6 | */ 7 | 8 | exports.red = rgbaAdjuster('red'); 9 | exports.blue = rgbaAdjuster('blue'); 10 | exports.green = rgbaAdjuster('green'); 11 | exports.alpha = exports.a = rgbaAdjuster('alpha'); 12 | 13 | /** 14 | * RGB adjuster. 15 | */ 16 | 17 | exports.rgb = function () { 18 | // TODO 19 | }; 20 | 21 | /** 22 | * Basic HSLWB adjusters. 23 | */ 24 | 25 | exports.hue = exports.h = hslwbAdjuster('hue'); 26 | exports.saturation = exports.s = hslwbAdjuster('saturation'); 27 | exports.lightness = exports.l = hslwbAdjuster('lightness'); 28 | exports.whiteness = exports.w = hslwbAdjuster('whiteness'); 29 | exports.blackness = exports.b = hslwbAdjuster('blackness'); 30 | 31 | /** 32 | * Blend adjuster. 33 | * 34 | * @param {Color} color 35 | * @param {Object} args 36 | */ 37 | 38 | exports.blend = function (color, args) { 39 | var targetAlpha = color.alpha(); 40 | 41 | // Reset the alpha value to one. This is required because color.mix mixes 42 | // the alpha value as well as rgb values. For blend() purposes, that's not 43 | // what we want. 44 | color.alpha(1); 45 | 46 | var other = new Color(args[0].value); 47 | var percentage = 1 - parseInt(args[1].value, 10) / 100; 48 | 49 | // Finally set the alpha value of the mixed color to the target value. 50 | color.mix(other, percentage).alpha(targetAlpha); 51 | }; 52 | 53 | /** 54 | * Tint adjuster. 55 | * 56 | * @param {Color} color 57 | * @param {Object} args 58 | */ 59 | 60 | exports.tint = function (color, args) { 61 | args.unshift({ type: 'argument', value: 'white' }); 62 | exports.blend(color, args); 63 | }; 64 | 65 | /** 66 | * Share adjuster. 67 | * 68 | * @param {Color} color 69 | * @param {Object} args 70 | */ 71 | 72 | exports.shade = function (color, args) { 73 | args.unshift({ type: 'argument', value: 'black' }); 74 | exports.blend(color, args); 75 | }; 76 | 77 | /** 78 | * Contrast adjuster. 79 | * 80 | * @param {Color} color 81 | * @param {Object} args 82 | */ 83 | exports.contrast = function (color, args) { 84 | if (args.length == 0) args.push({ type: 'argument', value: '100%' }); 85 | var percentage = 1 - parseInt(args[0].value, 10) / 100; 86 | var max = color.luminosity() < .5 ? new Color({ h:color.hue(), w:100, b:0 }) : new Color({ h:color.hue(), w:0, b:100 }); 87 | var min = max; 88 | var minRatio = 4.5; 89 | if (color.contrast(max) > minRatio) { 90 | var min = binarySearchBWContrast(minRatio, color, max); 91 | var targetMinAlpha = min.alpha(); 92 | // Set the alpha to 1 to avoid mix()-ing the alpha value. 93 | min.alpha(1); 94 | // mixes the colors then sets the alpha back to the target alpha. 95 | min.mix(max, percentage).alpha(targetMinAlpha); 96 | } 97 | color.hwb(min.hwb()); 98 | }; 99 | 100 | /** 101 | * Generate a value or percentage of modifier. 102 | * 103 | * @param {String} prop 104 | * @return {Function} 105 | */ 106 | 107 | function rgbaAdjuster (prop) { 108 | return function (color, args) { 109 | var mod; 110 | if (args[0].type == 'modifier') mod = args.shift().value; 111 | 112 | var val = args[0].value; 113 | if (val.indexOf('%') != -1) { 114 | val = parseInt(val, 10) / 100; 115 | if (!mod) { 116 | val = val * (prop == 'alpha' ? 1 : 255); 117 | } else if (mod != '*') { 118 | val = color[prop]() * val; 119 | } 120 | } else { 121 | val = Number(val); 122 | } 123 | 124 | color[prop](modify(color[prop](), val, mod)); 125 | }; 126 | } 127 | 128 | /** 129 | * Generate a basic HSLWB adjuster. 130 | * 131 | * @param {String} prop 132 | * @return {Function} 133 | */ 134 | 135 | function hslwbAdjuster (prop) { 136 | return function (color, args) { 137 | var mod; 138 | if (args[0].type == 'modifier') mod = args.shift().value; 139 | var val = parseFloat(args[0].value, 10); 140 | color[prop](modify(color[prop](), val, mod)); 141 | }; 142 | } 143 | 144 | /** 145 | * Return the percentage of a `number` for a given percentage `string`. 146 | * 147 | * @param {Number} number 148 | * @param {String} string 149 | * @return {Number} 150 | */ 151 | 152 | function percentageOf (number, string) { 153 | var percent = parseInt(string, 10) / 100; 154 | return number * percent; 155 | } 156 | 157 | /** 158 | * Modify a `val` by an `amount` with an optional `modifier`. 159 | * 160 | * @param {Number} val 161 | * @param {Number} amount 162 | * @param {String} modifier (optional) 163 | */ 164 | 165 | function modify (val, amount, modifier) { 166 | switch (modifier) { 167 | case '+': return val + amount; 168 | case '-': return val - amount; 169 | case '*': return val * amount; 170 | default: return amount; 171 | } 172 | } 173 | 174 | /** 175 | * Return the color closest to `color` between `color` and `max` that has a contrast ratio higher than `minRatio` 176 | * assumes `color` and `max` have identical hue 177 | * 178 | * @param {Number} minRatio 179 | * @param {Color} color 180 | * @param {Color} max 181 | **/ 182 | 183 | function binarySearchBWContrast (minRatio, color, max) { 184 | var hue = color.hue(); 185 | var min = color.clone(); 186 | var minW = color.whiteness(); 187 | var minB = color.blackness(); 188 | var maxW = max.whiteness(); 189 | var maxB = max.blackness(); 190 | while (Math.abs(minW - maxW) > 1 || Math.abs(minB - maxB) > 1) { 191 | var midW = Math.round((maxW + minW) / 2); 192 | var midB = Math.round((maxB + minB) / 2); 193 | min.whiteness(midW); 194 | min.blackness(midB); 195 | if (min.contrast(color) > minRatio) { 196 | maxW = midW; 197 | maxB = midB; 198 | } else { 199 | minW = midW; 200 | minB = midB; 201 | } 202 | } 203 | return min 204 | } 205 | -------------------------------------------------------------------------------- /lib/convert.js: -------------------------------------------------------------------------------- 1 | 2 | var balanced = require('balanced-match'); 3 | var Color = require('color'); 4 | var parse = require('./parse'); 5 | var adjusters = require('./adjusters'); 6 | 7 | /** 8 | * Expose `convert`. 9 | */ 10 | 11 | module.exports = convert; 12 | 13 | /** 14 | * Convert a color function CSS `string` into an RGB color string. 15 | * 16 | * @param {String} string 17 | * @return {String} 18 | */ 19 | 20 | function convert (string) { 21 | var index = string.indexOf('color('); 22 | if (index == -1) return string; 23 | 24 | string = string.slice(index); 25 | string = balanced('(', ')', string); 26 | if (!string) throw new SyntaxError('Missing closing parenthese for \'' + string + '\''); 27 | var ast = parse('color(' + string.body + ')'); 28 | return toRGB(ast) + convert(string.post); 29 | } 30 | 31 | /** 32 | * Given a color `ast` return an RGB color string. 33 | * 34 | * @param {Object} ast 35 | * @return {String} 36 | */ 37 | 38 | function toRGB (ast) { 39 | var color = new Color(ast.arguments[0].type == "function" ? toRGB(ast.arguments[0]) : ast.arguments[0].value) 40 | var fns = ast.arguments.slice(1); 41 | 42 | fns.forEach(function (adjuster) { 43 | var name = adjuster.name; 44 | if (!adjusters[name]) throw new Error('Unknown \'' + name + '\''); 45 | 46 | // convert nested color functions 47 | adjuster.arguments.forEach(function (arg) { 48 | if (arg.type == 'function' && arg.name == 'color') { 49 | arg.value = toRGB(arg); 50 | arg.type = 'color'; 51 | delete arg.name; 52 | } 53 | }); 54 | 55 | // apply adjuster transformations 56 | adjusters[name](color, adjuster.arguments); 57 | }); 58 | 59 | return color.rgbString(); 60 | } 61 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | 2 | var convert = require('./convert'); 3 | var parse = require('./parse'); 4 | 5 | /** 6 | * Expose `convert`. 7 | */ 8 | 9 | exports.convert = convert; 10 | 11 | /** 12 | * Expose `parse`. 13 | */ 14 | 15 | exports.parse = parse; -------------------------------------------------------------------------------- /lib/parse.js: -------------------------------------------------------------------------------- 1 | var balanced = require('balanced-match'); 2 | var debug = require('debug')('css-color-function:parse'); 3 | 4 | /** 5 | * Expose `parse`. 6 | */ 7 | 8 | module.exports = parse; 9 | 10 | /** 11 | * Parse a CSS color function string. 12 | * 13 | * @param {String} string 14 | * @return {Array} 15 | */ 16 | 17 | function parse (string) { 18 | if ('string' != typeof string) string = string.toString(); 19 | debug('string %s', string); 20 | 21 | /** 22 | * Match the current position in the string against a `regexp`, returning the 23 | * match if one exists. 24 | * 25 | * @param {RegExp} regexp 26 | * @return {Undefined or Array} 27 | */ 28 | 29 | function match (regexp) { 30 | var m = regexp.exec(string); 31 | if (!m) return; 32 | string = string.slice(m[0].length); 33 | return m.slice(1); 34 | } 35 | 36 | /** 37 | * Match whitespace. 38 | */ 39 | 40 | function whitespace () { 41 | match(/^\s+/); 42 | } 43 | 44 | /** 45 | * Match a right parentheses. 46 | * 47 | * @return {Array or Undefined} 48 | */ 49 | 50 | function rparen () { 51 | var m = match(/^\)/); 52 | if (!m) return; 53 | debug('rparen'); 54 | return m; 55 | } 56 | 57 | /** 58 | * Match a modifier: '+' '-' '*'. 59 | * 60 | * @return {Object or Undefined} 61 | */ 62 | 63 | function modifier () { 64 | var m = match(/^([\+\-\*])/); 65 | if (!m) return; 66 | var ret = {}; 67 | ret.type = 'modifier'; 68 | ret.value = m[0]; 69 | debug('modifier %o', ret); 70 | return ret; 71 | } 72 | 73 | /** 74 | * Match a generic number function argument. 75 | * 76 | * @return {Object or Undefined} 77 | */ 78 | 79 | function number () { 80 | var m = match(/^([^\)\s]+)/); 81 | if (!m) return; 82 | var ret = {}; 83 | ret.type = 'number'; 84 | ret.value = m[0]; 85 | debug('number %o', ret); 86 | return ret; 87 | } 88 | 89 | /** 90 | * Match a function's arguments. 91 | * 92 | * @return {Array} 93 | */ 94 | 95 | function args () { 96 | var ret = []; 97 | var el; 98 | while (el = modifier() || fn() || number()) { 99 | ret.push(el); 100 | whitespace(); 101 | } 102 | debug('args %o', ret); 103 | return ret; 104 | } 105 | 106 | /** 107 | * Match an adjuster function. 108 | * 109 | * @return {Object or Undefined} 110 | */ 111 | 112 | function adjuster () { 113 | var m = match(/^(\w+)\(/); 114 | if (!m) return; 115 | whitespace(); 116 | var el; 117 | var ret = {}; 118 | ret.type = 'function'; 119 | ret.name = m[0]; 120 | ret.arguments = args(); 121 | rparen() 122 | debug('adjuster %o', ret); 123 | return ret; 124 | } 125 | 126 | /** 127 | * Match a color. 128 | * 129 | * @return {Object} 130 | */ 131 | 132 | function color () { 133 | var ret = {}; 134 | ret.type = 'color'; 135 | 136 | var col = match(/([^\)\s]+)/)[0]; 137 | if (col.indexOf('(') != -1) { 138 | var piece = match(/([^\)]*?\))/)[0]; 139 | col = col + piece; 140 | } 141 | 142 | ret.value = col; 143 | whitespace(); 144 | return ret; 145 | } 146 | 147 | /** 148 | * Match a color function, capturing the first color argument and any adjuster 149 | * functions after it. 150 | * 151 | * @return {Object or Undefined} 152 | */ 153 | 154 | function fn () { 155 | if (!string.match(/^color\(/)) return; 156 | 157 | var colorRef = balanced('(', ')', string) 158 | if (!colorRef) throw new SyntaxError('Missing closing parenthese for \'' + string + '\''); 159 | if (colorRef.body === '') throw new SyntaxError('color() function cannot be empty'); 160 | string = colorRef.body 161 | whitespace(); 162 | 163 | var ret = {}; 164 | ret.type = 'function'; 165 | ret.name = 'color'; 166 | ret.arguments = [fn() || color()]; 167 | debug('function arguments %o', ret.arguments); 168 | 169 | var el; 170 | while (el = adjuster()) { 171 | ret.arguments.push(el); 172 | whitespace(); 173 | } 174 | 175 | // pass the rest of the string in case of recursive color() 176 | string = colorRef.post 177 | whitespace(); 178 | debug('function %o', ret); 179 | 180 | return ret; 181 | } 182 | 183 | /** 184 | * Return the parsed color function. 185 | */ 186 | 187 | return fn(); 188 | } 189 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "css-color-function", 3 | "repository": "git://github.com/ianstormtaylor/css-color-function.git", 4 | "version": "1.3.3", 5 | "license": "MIT", 6 | "description": "A parser and converter for Tab Atkins's proposed color function in CSS.", 7 | "keywords": [ 8 | "color", 9 | "function", 10 | "css", 11 | "parse", 12 | "convert" 13 | ], 14 | "devDependencies": { 15 | "mocha": "*" 16 | }, 17 | "main": "lib/index.js", 18 | "dependencies": { 19 | "balanced-match": "0.1.0", 20 | "color": "^0.11.0", 21 | "debug": "^3.1.0", 22 | "rgb": "~0.1.0" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /test/convert.js: -------------------------------------------------------------------------------- 1 | 2 | var assert = require('assert'); 3 | var color = require('..'); 4 | 5 | function convert (string, expected) { 6 | string = 'color(' + string + ')'; 7 | assert.equal(color.convert(string), expected); 8 | } 9 | 10 | describe('#convert', function () { 11 | 12 | describe('red', function () { 13 | it('should set red', function () { 14 | convert('red red(25)', 'rgb(25, 0, 0)'); 15 | }); 16 | 17 | it('should set red by percentage', function () { 18 | convert('black red(10%)', 'rgb(26, 0, 0)'); 19 | }); 20 | 21 | it('should add red', function () { 22 | convert('rgb(100, 0, 0) red(+ 25)', 'rgb(125, 0, 0)'); 23 | }); 24 | 25 | it('should add red by percentage', function () { 26 | convert('rgb(50, 0, 0) red(+ 10%)', 'rgb(55, 0, 0)'); 27 | }); 28 | 29 | it('should subtract red', function () { 30 | convert('rgb(50, 0, 0) red(- 25)', 'rgb(25, 0, 0)'); 31 | }); 32 | 33 | it('should subtract red by percentage', function () { 34 | convert('rgb(50, 0, 0) red(- 10%)', 'rgb(45, 0, 0)'); 35 | }); 36 | 37 | it('should multiply red', function () { 38 | convert('rgb(250, 0, 0) red(* .1)', 'rgb(25, 0, 0)'); 39 | }); 40 | 41 | it('should multiply red by percentage', function () { 42 | convert('rgb(250, 0, 0) red(* 10%)', 'rgb(25, 0, 0)'); 43 | }); 44 | }); 45 | 46 | describe('green', function () { 47 | it('should set green', function () { 48 | convert('green green(25)', 'rgb(0, 25, 0)'); 49 | }); 50 | 51 | it('should set green by percentage', function () { 52 | convert('black green(10%)', 'rgb(0, 26, 0)'); 53 | }); 54 | 55 | it('should add green', function () { 56 | convert('rgb(0, 100, 0) green(+ 25)', 'rgb(0, 125, 0)'); 57 | }); 58 | 59 | it('should add green by percentage', function () { 60 | convert('rgb(0, 50, 0) green(+ 10%)', 'rgb(0, 55, 0)'); 61 | }); 62 | 63 | it('should subtract green', function () { 64 | convert('rgb(0, 50, 0) green(- 25)', 'rgb(0, 25, 0)'); 65 | }); 66 | 67 | it('should subtract green by percentage', function () { 68 | convert('rgb(0, 50, 0) green(- 10%)', 'rgb(0, 45, 0)'); 69 | }); 70 | 71 | it('should multiply green', function () { 72 | convert('rgb(0, 250, 0) green(* .1)', 'rgb(0, 25, 0)'); 73 | }); 74 | 75 | it('should multiply green by percentage', function () { 76 | convert('rgb(0, 250, 0) green(* 10%)', 'rgb(0, 25, 0)'); 77 | }); 78 | }); 79 | 80 | describe('blue', function () { 81 | it('should set blue', function () { 82 | convert('blue blue(25)', 'rgb(0, 0, 25)'); 83 | }); 84 | 85 | it('should set blue by percentage', function () { 86 | convert('blue blue(10%)', 'rgb(0, 0, 26)'); 87 | }); 88 | 89 | it('should add blue', function () { 90 | convert('rgb(0, 0, 100) blue(+ 25)', 'rgb(0, 0, 125)'); 91 | }); 92 | 93 | it('should add blue by percentage', function () { 94 | convert('rgb(0, 0, 50) blue(+ 10%)', 'rgb(0, 0, 55)'); 95 | }); 96 | 97 | it('should subtract blue', function () { 98 | convert('rgb(0, 0, 50) blue(- 25)', 'rgb(0, 0, 25)'); 99 | }); 100 | 101 | it('should subtract blue by percentage', function () { 102 | convert('rgb(0, 0, 50) blue(- 10%)', 'rgb(0, 0, 45)'); 103 | }); 104 | 105 | it('should multiply blue', function () { 106 | convert('rgb(0, 0, 250) blue(* .1)', 'rgb(0, 0, 25)'); 107 | }); 108 | 109 | it('should multiply blue by percentage', function () { 110 | convert('rgb(0, 0, 250) blue(* 10%)', 'rgb(0, 0, 25)'); 111 | }); 112 | }); 113 | 114 | describe('alpha', function () { 115 | it('should set alpha', function () { 116 | convert('black alpha(.5)', 'rgba(0, 0, 0, 0.5)'); 117 | }); 118 | 119 | it('should set alpha by percentage', function () { 120 | convert('black alpha(42%)', 'rgba(0, 0, 0, 0.42)'); 121 | }); 122 | 123 | it('should add alpha', function () { 124 | convert('rgba(0,0,0,0) alpha(+ .1)', 'rgba(0, 0, 0, 0.1)'); 125 | }); 126 | 127 | it('should add alpha by percentage', function () { 128 | convert('rgba(0,0,0,.5) alpha(+ 10%)', 'rgba(0, 0, 0, 0.55)'); 129 | }); 130 | 131 | it('should subtract alpha', function () { 132 | convert('black alpha(- .1)', 'rgba(0, 0, 0, 0.9)'); 133 | }); 134 | 135 | it('should subtract alpha by percentage', function () { 136 | convert('black alpha(- 10%)', 'rgba(0, 0, 0, 0.9)'); 137 | }); 138 | 139 | it('should multiply alpha', function () { 140 | convert('rgba(0,0,0,.2) alpha(* .5)', 'rgba(0, 0, 0, 0.1)'); 141 | }); 142 | 143 | it('should multiply alpha by percentage', function () { 144 | convert('rgba(0,0,0,.2) alpha(* 50%)', 'rgba(0, 0, 0, 0.1)'); 145 | }); 146 | }); 147 | 148 | describe('hue', function () { 149 | it('should set hue', function () { 150 | convert('hsl(34, 50%, 50%) hue(25)', 'rgb(191, 117, 64)'); 151 | }); 152 | 153 | it('should set hue greater than 360', function () { 154 | convert('hsl(34, 50%, 50%) hue(385)', 'rgb(191, 117, 64)'); 155 | }); 156 | 157 | it('should set hue less than 360', function () { 158 | convert('hsl(34, 50%, 50%) hue(-369)', 'rgb(191, 117, 64)'); 159 | }); 160 | 161 | it('should add hue', function () { 162 | convert('hsl(10, 50%, 50%) hue(+ 15)', 'rgb(191, 117, 64)'); 163 | }); 164 | 165 | it('should subtract hue', function () { 166 | convert('hsl(40, 50%, 50%) hue(- 15)', 'rgb(191, 117, 64)'); 167 | }); 168 | 169 | it('should multiply hue', function () { 170 | convert('hsl(10, 50%, 50%) hue(* 2.5)', 'rgb(191, 117, 64)'); 171 | }); 172 | 173 | it('should adjust hue greater than 360', function () { 174 | convert('hsl(240, 50%, 50%) hue(+ 240)', 'rgb(64, 191, 64)'); 175 | }); 176 | 177 | it('should adjust negative hue', function () { 178 | convert('hsl(120, 50%, 50%) hue(- 240)', 'rgb(64, 64, 191)'); 179 | }); 180 | }); 181 | 182 | describe('saturation', function () { 183 | it('should set saturation', function () { 184 | convert('hsl(25, 0%, 50%) saturation(50%)', 'rgb(191, 117, 64)'); 185 | }); 186 | 187 | it('should add saturation', function () { 188 | convert('hsl(25, 25%, 50%) saturation(+ 25%)', 'rgb(191, 117, 64)'); 189 | }); 190 | 191 | it('should substract saturation', function () { 192 | convert('hsl(25, 60%, 50%) saturation(- 10%)', 'rgb(191, 117, 64)'); 193 | }); 194 | 195 | it('should multiply saturation', function () { 196 | convert('hsl(25, 25%, 50%) saturation(* 2)', 'rgb(191, 117, 64)'); 197 | }); 198 | }); 199 | 200 | describe('lightness', function () { 201 | it('should set lightness', function () { 202 | convert('hsl(25, 50%, 0%) lightness(50%)', 'rgb(191, 117, 64)'); 203 | }); 204 | 205 | it('should add lightness', function () { 206 | convert('hsl(25, 50%, 25%) lightness(+ 25%)', 'rgb(191, 117, 64)'); 207 | }); 208 | 209 | it('should substract lightness', function () { 210 | convert('hsl(25, 50%, 60%) lightness(- 10%)', 'rgb(191, 117, 64)'); 211 | }); 212 | 213 | it('should multiply lightness', function () { 214 | convert('hsl(25, 50%, 25%) lightness(* 2)', 'rgb(191, 117, 64)'); 215 | }); 216 | }); 217 | 218 | describe('whiteness', function () { 219 | it('should set whiteness', function () { 220 | convert('hwb(0, 0%, 0%) whiteness(20%)', 'rgb(255, 51, 51)'); // hwb(0, 20%, 0%) 221 | }); 222 | 223 | it('should add whiteness', function () { 224 | convert('hwb(0, 75%, 0%) whiteness(+25%)', 'rgb(255, 255, 255)'); // hwb(0, 100%, 0%) 225 | }); 226 | 227 | it('should substract whiteness', function () { 228 | convert('hwb(0, 30%, 0%) whiteness(-10%)', 'rgb(255, 51, 51)'); // hwb(0, 20%, 0%) 229 | }); 230 | 231 | it('should multiply whiteness', function () { 232 | convert('hwb(0, 50%, 0%) whiteness(*2)', 'rgb(255, 255, 255)'); // hwb(0, 100%, 0%) 233 | }); 234 | }); 235 | 236 | describe('blackness', function () { 237 | it('should set blackness', function () { 238 | convert('hwb(0, 0%, 0%) blackness(20%)', 'rgb(204, 0, 0)'); // hwb(0, 0%, 20%) 239 | }); 240 | 241 | it('should add blackness', function () { 242 | convert('hwb(0, 0%, 75%) blackness(+25%)', 'rgb(0, 0, 0)'); // hwb(0, 0%, 100%) 243 | }); 244 | 245 | it('should substract blackness', function () { 246 | convert('hwb(0, 0%, 30%) blackness(-10%)', 'rgb(204, 0, 0)'); // hwb(0, 0%, 20%) 247 | }); 248 | 249 | it('should multiply blackness', function () { 250 | convert('hwb(0, 0%, 50%) blackness(*2)', 'rgb(0, 0, 0)'); // hwb(0, 0%, 100%) 251 | }); 252 | }); 253 | 254 | describe('blend', function () { 255 | it('should blend two colors', function () { 256 | convert('red blend(black 50%)', 'rgb(128, 0, 0)'); 257 | }); 258 | }); 259 | 260 | describe('tint', function () { 261 | it('should blend a color with white', function () { 262 | convert('red tint(50%)', 'rgb(255, 128, 128)'); 263 | }); 264 | }); 265 | 266 | describe('tint with alpha', function () { 267 | it('should blend a color with white and adjust the alpha', function () { 268 | convert('red a(40%) tint(50%)', 'rgba(255, 128, 128, 0.4)'); 269 | }); 270 | 271 | it('should blend a color with white and adjust the alpha', function () { 272 | convert('red tint(50%) a(20%)', 'rgba(255, 128, 128, 0.2)'); 273 | }); 274 | }); 275 | 276 | describe('shade', function () { 277 | it('should blend a color with black', function () { 278 | convert('red shade(50%)', 'rgb(128, 0, 0)'); 279 | }); 280 | }); 281 | 282 | describe('shade with alpha', function () { 283 | it('should blend a color with black and adjust the alpha', function () { 284 | convert('red a(40%) shade(50%)', 'rgba(128, 0, 0, 0.4)'); 285 | }); 286 | 287 | it('should blend a color with black and adjust the alpha', function () { 288 | convert('red shade(50%) a(25%)', 'rgba(128, 0, 0, 0.25)'); 289 | }); 290 | }); 291 | 292 | describe('contrast', function () { 293 | it('should darken the same hue with a light color', function () { 294 | convert('hwb(180, 10%, 0%) contrast(0%)', 'rgb(13, 115, 115)'); // hwb(180, 5%, 55%) 295 | }); 296 | 297 | it('should lighten the same hue with a dark color', function () { 298 | convert('hwb(0, 0%, 10%) contrast(0%)', 'rgb(252, 245, 245)'); // hwb(0, 96%, 1%) 299 | }); 300 | 301 | it('should go to black with a light color', function () { 302 | convert('hwb(180, 10%, 0%) contrast(100%)', 'rgb(0, 0, 0)'); // hwb(180, 0%, 100%) 303 | }); 304 | 305 | it('should go to white with a dark color', function () { 306 | convert('hwb(0, 0%, 10%) contrast(100%)', 'rgb(255, 255, 255)'); // hwb(0, 100%, 0%) 307 | }); 308 | }); 309 | 310 | describe('contrast with alpha', function () { 311 | it('should go to white with a dark color and the given alpha', function() { 312 | convert('black a(40%) contrast(99%)', 'rgba(255, 255, 255, 0.4)'); 313 | }); 314 | 315 | it('should go to black with a light color and the given alpha', function() { 316 | convert('white a(50%) contrast(99%)', 'rgba(0, 0, 0, 0.5)'); 317 | }); 318 | }); 319 | 320 | describe('nested color functions', function () { 321 | it('should convert nested color functions', function () { 322 | convert('color(rebeccapurple a(-10%)) a(-10%)', 'rgba(102, 51, 153, 0.81)'); 323 | convert('color(#4C5859 shade(25%)) blend(color(#4C5859 shade(40%)) 20%)', 'rgb(55, 63, 64)'); 324 | }); 325 | }); 326 | 327 | describe('errors', function () { 328 | it('should throw an error is color is unknown', function () { 329 | assert.throws(function () { 330 | convert('wtf'); 331 | }, /Unable to parse color from string/); 332 | }); 333 | 334 | it('should throw an error is modifier is unknown', function () { 335 | assert.throws(function () { 336 | convert('red WTF(+10%)'); 337 | }, /Unknown /); 338 | }); 339 | }); 340 | }); 341 | -------------------------------------------------------------------------------- /test/parse.js: -------------------------------------------------------------------------------- 1 | 2 | var assert = require('assert'); 3 | var color = require('..'); 4 | 5 | function parse (string, expected) { 6 | string = 'color(' + string + ')'; 7 | assert.deepEqual(color.parse(string), expected); 8 | } 9 | 10 | describe('#parse', function () { 11 | it('should parse a color', function () { 12 | parse('red', { 13 | type: 'function', 14 | name: 'color', 15 | arguments: [ 16 | { 17 | type: 'color', 18 | value: 'red' 19 | } 20 | ] 21 | }); 22 | }); 23 | 24 | it('should parse a complex color', function () { 25 | parse('rgba(0,0,0,0)', { 26 | type: 'function', 27 | name: 'color', 28 | arguments: [ 29 | { 30 | type: 'color', 31 | value: 'rgba(0,0,0,0)' 32 | } 33 | ] 34 | }); 35 | }); 36 | 37 | it('should parse a more complex color', function () { 38 | parse('rgba(0, 31, 231, .4)', { 39 | type: 'function', 40 | name: 'color', 41 | arguments: [ 42 | { 43 | type: 'color', 44 | value: 'rgba(0, 31, 231, .4)' 45 | } 46 | ] 47 | }); 48 | }); 49 | 50 | it('should parse a basic adjuster', function () { 51 | parse('red red(24)', { 52 | type: 'function', 53 | name: 'color', 54 | arguments: [ 55 | { 56 | type: 'color', 57 | value: 'red', 58 | }, 59 | { 60 | type: 'function', 61 | name: 'red', 62 | arguments: [ 63 | { 64 | type: 'number', 65 | value: '24' 66 | } 67 | ] 68 | } 69 | ] 70 | }); 71 | }); 72 | 73 | it('should parse an adjuster with a modifier', function () { 74 | parse('red red(+ 24)', { 75 | type: 'function', 76 | name: 'color', 77 | arguments: [ 78 | { 79 | type: 'color', 80 | value: 'red', 81 | }, 82 | { 83 | type: 'function', 84 | name: 'red', 85 | arguments: [ 86 | { 87 | type: 'modifier', 88 | value: '+' 89 | }, 90 | { 91 | type: 'number', 92 | value: '24' 93 | } 94 | ] 95 | } 96 | ] 97 | }); 98 | }); 99 | 100 | it('should parse multiple adjusters', function () { 101 | parse('red red(24) blue(27)', { 102 | type: 'function', 103 | name: 'color', 104 | arguments: [ 105 | { 106 | type: 'color', 107 | value: 'red', 108 | }, 109 | { 110 | type: 'function', 111 | name: 'red', 112 | arguments: [ 113 | { 114 | type: 'number', 115 | value: '24' 116 | } 117 | ] 118 | }, 119 | { 120 | type: 'function', 121 | name: 'blue', 122 | arguments: [ 123 | { 124 | type: 'number', 125 | value: '27' 126 | } 127 | ] 128 | } 129 | ] 130 | }); 131 | }); 132 | 133 | it('should parse adjusters with multiple arguments', function () { 134 | parse('red blend(white 50%)', { 135 | type: 'function', 136 | name: 'color', 137 | arguments: [ 138 | { 139 | type: 'color', 140 | value: 'red', 141 | }, 142 | { 143 | type: 'function', 144 | name: 'blend', 145 | arguments: [ 146 | { 147 | type: 'number', 148 | value: 'white' 149 | }, 150 | { 151 | type: 'number', 152 | value: '50%' 153 | } 154 | ] 155 | } 156 | ] 157 | }); 158 | }); 159 | 160 | it('should parse adjusters with nested color functions', function () { 161 | parse('red blend(color(red) 50%)', { 162 | type: 'function', 163 | name: 'color', 164 | arguments: [ 165 | { 166 | type: 'color', 167 | value: 'red', 168 | }, 169 | { 170 | type: 'function', 171 | name: 'blend', 172 | arguments: [ 173 | { 174 | type: 'function', 175 | name: 'color', 176 | arguments: [ 177 | { 178 | type: 'color', 179 | value: 'red' 180 | } 181 | ] 182 | }, 183 | { 184 | type: 'number', 185 | value: '50%' 186 | } 187 | ] 188 | } 189 | ] 190 | }); 191 | }); 192 | 193 | it('should parse nested color functions', function () { 194 | parse('color(hsl(0, 0%, 93%) l(-5%)) l(+10%)', { 195 | type: 'function', 196 | name: 'color', 197 | arguments: [ 198 | { 199 | type: 'function', 200 | name: 'color', 201 | arguments: [ 202 | { 203 | type: 'color', 204 | value: 'hsl(0, 0%, 93%)', 205 | }, 206 | { 207 | type: 'function', 208 | name: 'l', 209 | arguments: [ 210 | { 211 | type: 'modifier', 212 | value: '-' 213 | }, 214 | { 215 | type: 'number', 216 | value: '5%' 217 | } 218 | ] 219 | } 220 | ] 221 | }, 222 | { 223 | type: 'function', 224 | name: 'l', 225 | arguments: [ 226 | { 227 | type: 'modifier', 228 | value: '+' 229 | }, 230 | { 231 | type: 'number', 232 | value: '10%' 233 | } 234 | ] 235 | } 236 | ] 237 | }); 238 | }); 239 | 240 | 241 | it('should throw on syntax error', function () { 242 | assert.throws(function () { 243 | color.parse('color(red'); 244 | }, /Missing closing parenthese/); 245 | }); 246 | 247 | it('should throw on syntax error for adjuster', function () { 248 | assert.throws(function () { 249 | color.parse('color(red l(+5%)'); 250 | }, /Missing closing parenthese for/); 251 | }); 252 | 253 | it('should throw on syntax error if color() is empty', function () { 254 | assert.throws(function () { 255 | color.parse('color()'); 256 | }, /color\(\) function cannot be empty/); 257 | }); 258 | }); 259 | --------------------------------------------------------------------------------