├── .editorconfig ├── .gitignore ├── .npmignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── lib ├── index.js ├── selector-formatter.js ├── template-parser.js └── variable-templating.js ├── package.json └── test ├── _run.js ├── base.test.js ├── color-types.test.js ├── selector-formatter.test.js ├── variable-syntax.test.js └── variable-templating.test.js /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 4 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.{json,yml}] 12 | indent_size = 2 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | npm-debug.log 3 | .idea 4 | coverage 5 | .nyc_output 6 | .dockerignore 7 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .gitignore 2 | .editorconfig 3 | 4 | node_modules/ 5 | npm-debug.log 6 | 7 | test.js 8 | .travis.yml 9 | .idea 10 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | sudo: false 3 | node_js: 4 | - 4 5 | - 6 6 | - 7 7 | 8 | cache: 9 | directories: 10 | - node_modules 11 | 12 | script: 13 | - npm test 14 | 15 | after_success: npm run coverage 16 | 17 | deploy: 18 | provider: npm 19 | email: lutien02@gmail.com 20 | api_key: $NPM_API_KEY 21 | on: 22 | tags: true 23 | repo: lutien/postcss-extract-value 24 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## v0.5.1 2 | 3 | * Replace AVA to Jest 4 | * Add support PostCSS 6.0 5 | 6 | ## v0.5.0 7 | 8 | * Refactoring variable templating 9 | * Add support keyword [selectorName] for template 10 | 11 | ## v0.4.0 12 | 13 | * Add support less and sass syntax for variables 14 | 15 | ## v0.3.0 16 | 17 | * Add templating for variables :art: 18 | 19 | ## v0.2.2 20 | 21 | * Fix hsl regexp 22 | * Add tests for color types 23 | 24 | ## v0.2.1 25 | 26 | * Fix case with default value in css variable 27 | 28 | ## v0.2.0 29 | 30 | * Add support several colors in one property 31 | * Fix regexp for different color types 32 | * Add support color keywords 33 | * Add opportunity to set custom selector, which will contain variables 34 | 35 | ## v0.1.0 36 | Initial release 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright 2016 lutien 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. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PostCSS Extract Value [![Build Status][ci-img]][ci] [![Coverage Status](https://coveralls.io/repos/github/lutien/postcss-extract-value/badge.svg)](https://coveralls.io/github/lutien/postcss-extract-value) 2 | 3 | [PostCSS] plugin to extract values from css properties and put them into variables. 4 | 5 | [PostCSS]: https://github.com/postcss/postcss 6 | [ci-img]: https://travis-ci.org/lutien/postcss-extract-value.svg 7 | [ci]: https://travis-ci.org/lutien/postcss-extract-value 8 | 9 | ```css 10 | .foo { 11 | width: 100px; 12 | color: #000; 13 | margin: 10px; 14 | } 15 | .bar { 16 | color: #000; 17 | margin: 15px; 18 | } 19 | ``` 20 | 21 | ```css 22 | :root { 23 | --width-1: 100px; 24 | --color-1: #000; 25 | --margin-1: 10px; 26 | --margin-2: 15px; 27 | } 28 | .foo { 29 | width: var(--width-1); 30 | color: var(--color-1); 31 | margin: var(--margin-1); 32 | } 33 | .bar { 34 | color: var(--color-1); 35 | margin: var(--margin-2); 36 | } 37 | ``` 38 | 39 | ## Usage ## 40 | 41 | ```js 42 | import postcssExtractValue from 'postcss-extract-value'; 43 | 44 | postcss([ 45 | postcssExtractValue(/* options */), 46 | // more plugins... 47 | ]) 48 | ``` 49 | 50 | ## Options ## 51 | 52 | #### filterByProps #### 53 | 54 | Type: `array`
55 | Required: `false`
56 | Default: `[]` 57 | 58 | You can add names of css properties and only from this properties will be extracted values. 59 | 60 | #### onlyColor #### 61 | 62 | Type: `boolean`
63 | Required: `false`
64 | Default: `false` 65 | 66 | If you set true, only colors (hex, rgb, hsl, color keywords) will be extracted from values. 67 | 68 | #### scope #### 69 | 70 | Type: `string`
71 | Required: `false`
72 | Default: `:root` 73 | 74 | You can set custom selector, which will contain variables. 75 | 76 | #### variableSyntax #### 77 | 78 | Type: `string`
79 | Required: `false`
80 | Default: `` 81 | 82 | By default it will be used css variables syntax, other available variants **less** and **sass**. 83 | 84 | #### templateVariableName #### 85 | 86 | Type: `string`
87 | Required: `false`
88 | Default: `` 89 | 90 | You can set template for variables using special words. 91 | See more information below. 92 | 93 | 94 | ## Usage templateVariableName ## 95 | 96 | ### With options _filterByProps_ or without any options by default: ### 97 | 98 | #### [propertyName] #### 99 | Name of css property (width, border, etc.). 100 | 101 | ```js 102 | postcss([ 103 | postcssExtractValue({ 104 | templateVariableName: 'theme[propertyName]' 105 | }), 106 | ]) 107 | ``` 108 | ```css 109 | .foo { 110 | width: 100px; 111 | } 112 | ``` 113 | 114 | ```css 115 | :root { 116 | --theme-width-1: 100px; 117 | } 118 | .foo { 119 | width: var(--theme-width-1); 120 | } 121 | ``` 122 | 123 | ### With options _onlyColor_: ### 124 | 125 | #### [colorKeyword] #### 126 | Color keyword of the nearest color. 127 | 128 | #### [tint] #### 129 | Deviation in the dark or light side from the nearest color. (light\dark) 130 | 131 | ```js 132 | postcss([ 133 | postcssExtractValue({ 134 | templateVariableName: 'theme[tint][colorKeyword]', 135 | }), 136 | ]) 137 | ``` 138 | ```css 139 | .foo { 140 | border: 2px solid #cc0000; 141 | color: #ff0000; 142 | background-color: rgb(255, 26, 26); 143 | } 144 | ``` 145 | 146 | ```css 147 | :root { 148 | --theme-dark-red-1: #cc0000; 149 | --theme-red-1: #ff0000; 150 | --theme-light-red-1: rgb(255, 26, 26); 151 | } 152 | .foo { 153 | border: 2px solid var(--theme-dark-red-1); 154 | color: var(--theme-red-1); 155 | background-color: var(--theme-light-red-1); 156 | } 157 | ``` 158 | 159 | ### Others ### 160 | 161 | #### [selectorName] #### 162 | Name of css selector (className, id, etc.) 163 | 164 | ```js 165 | postcss([ 166 | postcssExtractValue({ 167 | templateVariableName: 'theme[selectorName]' 168 | }), 169 | ]) 170 | ``` 171 | ```css 172 | .foo { 173 | width: 100px; 174 | } 175 | ``` 176 | 177 | ```css 178 | :root { 179 | --theme-foo-1: 100px; 180 | } 181 | .foo { 182 | width: var(--theme-foo-1); 183 | } 184 | ``` 185 | 186 | See [PostCSS] docs for examples for your environment. 187 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const postcss = require('postcss'); 4 | const colorObject = require('color-name'); 5 | const makeCSSVariable = require('./variable-templating'); 6 | const templateParser = require('./template-parser'); 7 | 8 | const colorList = {}; 9 | const colorNameList = Object.keys(colorObject); 10 | 11 | colorNameList.forEach((key) => { 12 | colorList[key] = { 13 | r: colorObject[key][0], 14 | g: colorObject[key][1], 15 | b: colorObject[key][2], 16 | }; 17 | }); 18 | 19 | function checkProp(filter, prop) { 20 | return filter.indexOf(prop) > -1; 21 | } 22 | 23 | function isColor(reColorKeywords, value) { 24 | const reCheck = new RegExp(`${/#\w+|rgba?|hsla?/.source}|${reColorKeywords.source}`, 'g'); 25 | return reCheck.test(value); 26 | } 27 | 28 | function extractColor(reExtract, value) { 29 | const resultArray = []; 30 | let result = reExtract.exec(value); 31 | 32 | while (result) { 33 | resultArray.push(result[0]); 34 | result = reExtract.exec(value); 35 | } 36 | return resultArray; 37 | } 38 | 39 | function addCSSVariable(variableSyntax, currentScope, value, prop) { 40 | if (variableSyntax) { 41 | currentScope.prepend({ prop, value }); 42 | } else { 43 | currentScope.append({ prop, value }); 44 | } 45 | } 46 | 47 | function hasVariable(variableSyntax, reCSSVariable, value) { 48 | const reTest = reCSSVariable[variableSyntax] || reCSSVariable.default; 49 | return reTest.test(value); 50 | } 51 | 52 | module.exports = postcss.plugin('postcss-extract-value', (opts) => { 53 | // Fix for Node 4 54 | const params = opts || {}; 55 | 56 | // Options 57 | const filterByProps = params.filterByProps; 58 | const onlyColor = params.onlyColor; 59 | const scope = params.scope || ':root'; 60 | const templateVariableName = params.templateVariableName || ''; 61 | const variableSyntax = params.variableSyntax || ''; 62 | const templateMap = templateParser(templateVariableName); 63 | 64 | 65 | const variablePrefix = { 66 | default: '--', 67 | less: '@', 68 | sass: '$', 69 | }; 70 | 71 | const variablesListCounter = {}; 72 | 73 | // Cache RegExp 74 | const reColorKeywords = new RegExp(colorNameList.join('|')); 75 | const reCSSVariable = { 76 | default: /^var\(-{2}\w{1}[\w+-]*/, 77 | sass: /\$\w{1}[\w+-]*/, 78 | less: /@\w{1}[\w+-]*/, 79 | }; 80 | const reHex = /#(\w{6}|\w{3})/; 81 | const reRgb = /rgba?\([\d,.\s]+\)/; 82 | const reHsl = /hsla?\(\s?[0-9]{1,3},\s?([0-9]{1,3}%,?\s?){2}([0-9.]+)?\)/; 83 | const reExtract = new RegExp(`${reHex.source}|${reRgb.source}|${reHsl.source}|${reColorKeywords.source}`, 'g'); 84 | 85 | return function parser(css) { 86 | const root = css.root(); 87 | let rootSel = {}; 88 | const storeProps = {}; 89 | let checkColorFilter = true; 90 | let checkPropFilter = true; 91 | let filteredValueList = []; 92 | let variableName = ''; 93 | const variablesList = {}; 94 | let positionValue = 0; 95 | 96 | css.walkRules((rule) => { 97 | if (rule.selector === scope) { 98 | rootSel = rule; 99 | } else { 100 | rule.walkDecls((decl) => { 101 | if (!hasVariable(variableSyntax, reCSSVariable, decl.value)) { 102 | checkColorFilter = !onlyColor || onlyColor 103 | && isColor(reColorKeywords, decl.value); 104 | 105 | checkPropFilter = (!filterByProps || filterByProps 106 | && checkProp(filterByProps, decl.prop)); 107 | 108 | if (checkColorFilter && checkPropFilter) { 109 | if (!storeProps[decl.prop]) { 110 | storeProps[decl.prop] = []; 111 | } 112 | 113 | if (onlyColor) { 114 | filteredValueList = extractColor(reExtract, decl.value); 115 | } else { 116 | filteredValueList = new Array(decl.value); 117 | } 118 | 119 | filteredValueList.forEach((filteredValue) => { 120 | positionValue = storeProps[decl.prop].indexOf(filteredValue); 121 | 122 | if (positionValue === -1) { 123 | storeProps[decl.prop].push(filteredValue); 124 | } 125 | if ({}.hasOwnProperty.call(variablesList, filteredValue)) { 126 | variableName = variablesList[filteredValue]; 127 | } else { 128 | positionValue = storeProps[decl.prop].indexOf(filteredValue) + 1; 129 | variableName = makeCSSVariable(templateMap, variablePrefix, 130 | variableSyntax, onlyColor, variablesListCounter, decl.prop, positionValue, 131 | filteredValue, rule); 132 | variablesList[filteredValue] = variableName; 133 | } 134 | if (variableSyntax) { 135 | decl.value = decl.value.replace(filteredValue, 136 | `${variableName}`); 137 | } else { 138 | decl.value = decl.value.replace(filteredValue, 139 | `var(${variableName})`); 140 | } 141 | }); 142 | } 143 | } 144 | }); 145 | } 146 | }); 147 | 148 | if (Object.keys(rootSel).length === 0) { 149 | if (variableSyntax) { 150 | rootSel = root; 151 | } else { 152 | rootSel = postcss.rule({ selector: scope }); 153 | root.prepend(rootSel); 154 | } 155 | } 156 | 157 | const varialbleListKeys = Object.keys(variablesList); 158 | if (variableSyntax) { 159 | varialbleListKeys.reverse(); 160 | } 161 | 162 | varialbleListKeys.forEach((value) => { 163 | addCSSVariable(variableSyntax, rootSel, value, variablesList[value]); 164 | }); 165 | }; 166 | }); 167 | -------------------------------------------------------------------------------- /lib/selector-formatter.js: -------------------------------------------------------------------------------- 1 | function selectorFormatter(selector) { 2 | return selector 3 | .replace(/^\.|^-|^#|\[|]|"|\)|$'/g, '') 4 | .replace(/\.|=|:|~|#|>|\(|\^|\+|,/g, '-') 5 | .replace(/[ \t]{2,}/g, ' ') 6 | .replace('*', 'all') 7 | .split(' ') 8 | .join('-') 9 | .replace(/-{2,}/g, '-') 10 | .replace(/^-/g, ''); 11 | } 12 | 13 | module.exports = selectorFormatter; 14 | -------------------------------------------------------------------------------- /lib/template-parser.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | function templateParser(template) { 4 | const arrLetters = template.split(''); 5 | const templateMap = new Map(); 6 | let str = ''; 7 | 8 | arrLetters.forEach((letter) => { 9 | if (letter === '[') { 10 | if (str) { 11 | templateMap.set(str, str); 12 | } 13 | str = letter; 14 | } else if (letter === ']') { 15 | str += letter; 16 | templateMap.set(str, str); 17 | str = ''; 18 | } else { 19 | str += letter; 20 | } 21 | }); 22 | 23 | if (str) { 24 | templateMap.set(str, str); 25 | } 26 | templateMap.set('[number]', '[number]'); 27 | return templateMap; 28 | } 29 | 30 | module.exports = templateParser; 31 | -------------------------------------------------------------------------------- /lib/variable-templating.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const parserColor = require('parse-color'); 4 | const nearestColor = require('nearest-color'); 5 | const colorObject = require('color-name'); 6 | const selectorFormatter = require('./selector-formatter'); 7 | 8 | const colorList = {}; 9 | const colorNameList = Object.keys(colorObject); 10 | 11 | colorNameList.forEach((key) => { 12 | colorList[key] = { 13 | r: colorObject[key][0], 14 | g: colorObject[key][1], 15 | b: colorObject[key][2], 16 | }; 17 | }); 18 | const findColor = nearestColor.from(colorList); 19 | 20 | 21 | function colorNameVariable(result, value) { 22 | let nearestColorValue = {}; 23 | const parsedColor = parserColor(value); 24 | 25 | if (parsedColor.hex) { 26 | nearestColorValue = parserColor(findColor(parsedColor.hex).value); 27 | } 28 | 29 | if (nearestColorValue) { 30 | if (result.has('[colorKeyword]')) { 31 | result.set('[colorKeyword]', nearestColorValue.keyword); 32 | } 33 | 34 | if (result.has('[tint]')) { 35 | if (nearestColorValue.hsl[2] > parsedColor.hsl[2]) { 36 | result.set('[tint]', 'dark'); 37 | } else if (nearestColorValue.hsl[2] < parsedColor.hsl[2]) { 38 | result.set('[tint]', 'light'); 39 | } else { 40 | result.delete('[tint]'); 41 | } 42 | } 43 | } 44 | 45 | return result; 46 | } 47 | 48 | function makeNameByTemplate(templateMap, onlyColor, value, prop, rule) { 49 | let result = new Map(templateMap); 50 | 51 | if (onlyColor) { 52 | result = colorNameVariable(result, value); 53 | } else if (templateMap.has('[propertyName]')) { 54 | result.set('[propertyName]', prop); 55 | } 56 | if (templateMap.has('[selectorName]')) { 57 | result.set('[selectorName]', selectorFormatter(rule.selector)); 58 | } 59 | return result; 60 | } 61 | 62 | function addVariablePrefix(variablePrefix, variableSyntax, variable) { 63 | const prefix = variablePrefix[variableSyntax]; 64 | return `${prefix || variablePrefix.default}${variable}`; 65 | } 66 | 67 | function makeCSSVariable(templateMap, variablePrefix, variableSyntax, onlyColor, 68 | variablesListCounter, prop, num, value, rule) { 69 | let variableName = ''; 70 | let result = new Map(templateMap); 71 | if (templateMap.size > 1) { 72 | result = makeNameByTemplate(templateMap, onlyColor, value, prop, rule); 73 | variableName = Array.from(result.values()).join('-'); 74 | 75 | if (!variablesListCounter[variableName]) { 76 | variablesListCounter[variableName] = 1; 77 | } 78 | result.set('[number]', `${variablesListCounter[variableName]}`); 79 | result = Array.from(result.values()).join('-'); 80 | 81 | variablesListCounter[variableName] += 1; 82 | } else { 83 | variableName = `${prop}-${num}`; 84 | result = variableName; 85 | } 86 | 87 | return addVariablePrefix(variablePrefix, variableSyntax, result); 88 | } 89 | 90 | module.exports = makeCSSVariable; 91 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "postcss-extract-value", 3 | "version": "0.5.1", 4 | "description": "PostCSS plugin to extract values from css properties and put them into variables.", 5 | "keywords": [ 6 | "postcss", 7 | "css", 8 | "postcss-plugin", 9 | "variable" 10 | ], 11 | "author": "lutien ", 12 | "license": "MIT", 13 | "repository": "lutien/postcss-extract-value", 14 | "bugs": { 15 | "url": "https://github.com/lutien/postcss-extract-value/issues" 16 | }, 17 | "homepage": "https://github.com/lutien/postcss-extract-value", 18 | "dependencies": { 19 | "color-name": "^1.1.1", 20 | "nearest-color": "^0.4.0", 21 | "parse-color": "^1.0.0", 22 | "postcss": "^6.0.1" 23 | }, 24 | "devDependencies": { 25 | "coveralls": "^2.11.12", 26 | "eslint": "^3.6.0", 27 | "eslint-config-airbnb-base": "^11.1.1", 28 | "eslint-plugin-import": "^2.2.0", 29 | "jest": "^19.0.2" 30 | }, 31 | "main": "./lib/index.js", 32 | "scripts": { 33 | "test": "jest --coverage && eslint ./lib/*.js", 34 | "coverage": "jest --no-cache --coverage && cat ./coverage/lcov.info | coveralls" 35 | }, 36 | "eslintConfig": { 37 | "extends": "airbnb-base", 38 | "rules": { 39 | "indent": [ 40 | "error", 41 | 4 42 | ], 43 | "import/no-extraneous-dependencies": [ 44 | "off" 45 | ], 46 | "strict": [ 47 | "off" 48 | ], 49 | "no-mixed-operators": [ 50 | "off" 51 | ], 52 | "max-len": [ 53 | "warn", 54 | 100 55 | ], 56 | "no-param-reassign": [ 57 | "off" 58 | ] 59 | }, 60 | "env": { 61 | "jest": true 62 | } 63 | }, 64 | "jest": { 65 | "testRegex": "/test/.*.test.js$", 66 | "coveragePathIgnorePatterns": ["/test/"] 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /test/_run.js: -------------------------------------------------------------------------------- 1 | const postcss = require('postcss'); 2 | const plugin = require('../lib/index'); 3 | 4 | module.exports = function run(input, output, opts) { 5 | const params = opts || {}; // Fix for Node 4 6 | return postcss([plugin(params)]).process(input) 7 | .then((result) => { 8 | expect(result.css).toEqual(output); 9 | expect(result.warnings().length).toBe(0); 10 | }); 11 | }; 12 | -------------------------------------------------------------------------------- /test/base.test.js: -------------------------------------------------------------------------------- 1 | const run = require('./_run'); 2 | 3 | const optDefault = { 4 | filterByProps: ['color', 'border-color'], 5 | }; 6 | 7 | it('default settings', () => { 8 | const input = `.foo { 9 | color: #000; 10 | width: 10px; 11 | display: block; 12 | }`; 13 | const output = `:root { 14 | --color-1: #000; 15 | --width-1: 10px; 16 | --display-1: block;\n}\n.foo { 17 | color: var(--color-1); 18 | width: var(--width-1); 19 | display: var(--display-1); 20 | }`; 21 | return run(input, output, { }); 22 | }); 23 | 24 | it('repeated values', () => { 25 | const input = `.foo { 26 | color: blue; 27 | } 28 | .bar { 29 | color: blue; 30 | }`; 31 | const output = `:root { 32 | --color-1: blue;\n} 33 | .foo { 34 | color: var(--color-1); 35 | } 36 | .bar { 37 | color: var(--color-1); 38 | }`; 39 | return run(input, output, optDefault); 40 | }); 41 | 42 | it('exist root element', () => { 43 | const input = `:root { 44 | --base-font-size: 16px; 45 | } 46 | .foo { 47 | color: #000; 48 | font-size: var(--base-font-size); 49 | }`; 50 | const output = `:root { 51 | --base-font-size: 16px; 52 | --color-1: #000; 53 | } 54 | .foo { 55 | color: var(--color-1); 56 | font-size: var(--base-font-size); 57 | }`; 58 | return run(input, output, { }); 59 | }); 60 | 61 | it('several colors in one property', () => { 62 | const input = `.foo { 63 | box-shadow: inset 0 2px 0px #dcffa6, 0 2px 5px #000; 64 | }`; 65 | const output = `:root { 66 | --box-shadow-1: #dcffa6; 67 | --box-shadow-2: #000;\n}\n.foo { 68 | box-shadow: inset 0 2px 0px var(--box-shadow-1), 0 2px 5px var(--box-shadow-2); 69 | }`; 70 | return run(input, output, { onlyColor: true }); 71 | }); 72 | 73 | it('custom element for css variables', () => { 74 | const input = `.foo { 75 | --color-1: black; 76 | } 77 | .bar { 78 | border-color: red; 79 | }`; 80 | const output = `.foo { 81 | --color-1: black; 82 | --border-color-1: red; 83 | } 84 | .bar { 85 | border-color: var(--border-color-1); 86 | }`; 87 | return run(input, output, { scope: '.foo' }); 88 | }); 89 | 90 | it('filter by color and props', () => { 91 | const input = `.foo { 92 | border: 1px solid #000; 93 | background-color: red; 94 | }`; 95 | const output = `:root { 96 | --border-1: #000;\n}\n.foo { 97 | border: 1px solid var(--border-1); 98 | background-color: red; 99 | }`; 100 | return run(input, output, { 101 | onlyColor: true, 102 | filterByProps: ['border'], 103 | }); 104 | }); 105 | 106 | it('default value in css variable', () => { 107 | const input = `:root { 108 | --base-color: #fff; 109 | } 110 | .foo { 111 | color: var(--base-color, #000); 112 | border: 1px solid #eee; 113 | }`; 114 | const output = `:root { 115 | --base-color: #fff; 116 | --border-1: #eee; 117 | } 118 | .foo { 119 | color: var(--base-color, #000); 120 | border: 1px solid var(--border-1); 121 | }`; 122 | return run(input, output, { onlyColor: true }); 123 | }); 124 | 125 | it('variable with several color values', () => { 126 | const input = `:root { 127 | --base-color: #fff; 128 | } 129 | .foo { 130 | border: 1px solid var(--base-color), 131 | 2px solid #000; 132 | }`; 133 | const output = `:root { 134 | --base-color: #fff; 135 | --border-1: #000; 136 | } 137 | .foo { 138 | border: 1px solid var(--base-color), 139 | 2px solid var(--border-1); 140 | }`; 141 | return run(input, output, { onlyColor: true }); 142 | }); 143 | -------------------------------------------------------------------------------- /test/color-types.test.js: -------------------------------------------------------------------------------- 1 | const run = require('./_run'); 2 | 3 | it('HSL', () => { 4 | const input = `.foo { 5 | border: 1px solid hsl(120, 100%, 50%); 6 | }`; 7 | const output = `:root { 8 | --border-1: hsl(120, 100%, 50%);\n}\n.foo { 9 | border: 1px solid var(--border-1); 10 | }`; 11 | return run(input, output, { onlyColor: true }); 12 | }); 13 | 14 | it('HSLA', () => { 15 | const input = `.foo { 16 | border: 1px solid hsla(120, 100%, 50%, 0.5); 17 | }`; 18 | const output = `:root { 19 | --border-1: hsla(120, 100%, 50%, 0.5);\n}\n.foo { 20 | border: 1px solid var(--border-1); 21 | }`; 22 | return run(input, output, { onlyColor: true }); 23 | }); 24 | 25 | it('color keyword', () => { 26 | const input = `.foo { 27 | border: 1px solid black; 28 | }`; 29 | const output = `:root { 30 | --border-1: black;\n}\n.foo { 31 | border: 1px solid var(--border-1); 32 | }`; 33 | return run(input, output, { onlyColor: true }); 34 | }); 35 | 36 | it('RGB', () => { 37 | const input = `.foo { 38 | border: 1px solid rgb(120, 100, 50); 39 | }`; 40 | const output = `:root { 41 | --border-1: rgb(120, 100, 50);\n}\n.foo { 42 | border: 1px solid var(--border-1); 43 | }`; 44 | return run(input, output, { onlyColor: true }); 45 | }); 46 | 47 | it('RGBA', () => { 48 | const input = `.foo { 49 | border: 1px solid rgba(120, 100, 50, 0.4); 50 | }`; 51 | const output = `:root { 52 | --border-1: rgba(120, 100, 50, 0.4);\n}\n.foo { 53 | border: 1px solid var(--border-1); 54 | }`; 55 | return run(input, output, { onlyColor: true }); 56 | }); 57 | 58 | it('short hex', () => { 59 | const input = `.foo { 60 | border: 1px solid #000; 61 | }`; 62 | const output = `:root { 63 | --border-1: #000;\n}\n.foo { 64 | border: 1px solid var(--border-1); 65 | }`; 66 | return run(input, output, { onlyColor: true }); 67 | }); 68 | 69 | it('long hex', () => { 70 | const input = `.foo { 71 | border: 1px solid #101113; 72 | }`; 73 | const output = `:root { 74 | --border-1: #101113;\n}\n.foo { 75 | border: 1px solid var(--border-1); 76 | }`; 77 | return run(input, output, { onlyColor: true }); 78 | }); 79 | -------------------------------------------------------------------------------- /test/selector-formatter.test.js: -------------------------------------------------------------------------------- 1 | const selectorFormatter = require('../lib/selector-formatter'); 2 | 3 | it('class', () => { 4 | expect(selectorFormatter('.foo')).toBe('foo'); 5 | }); 6 | 7 | it('id', () => { 8 | expect(selectorFormatter('#foo')).toBe('foo'); 9 | }); 10 | 11 | it('two classes', () => { 12 | expect(selectorFormatter('.foo.bar')).toBe('foo-bar'); 13 | }); 14 | 15 | it('two classes', () => { 16 | expect(selectorFormatter('.foo.bar')).toBe('foo-bar'); 17 | }); 18 | 19 | it('with pseudoclass', () => { 20 | expect(selectorFormatter('a:hover')).toBe('a-hover'); 21 | }); 22 | 23 | it('with pseudoelement', () => { 24 | expect(selectorFormatter('a::before')).toBe('a-before'); 25 | }); 26 | 27 | it('with +', () => { 28 | expect(selectorFormatter('div + p')).toBe('div-p'); 29 | }); 30 | 31 | it('with ~', () => { 32 | expect(selectorFormatter('div ~ p')).toBe('div-p'); 33 | }); 34 | 35 | it('with >', () => { 36 | expect(selectorFormatter('div > p')).toBe('div-p'); 37 | }); 38 | 39 | it('with :nth-child(n)', () => { 40 | expect(selectorFormatter(':nth-child(2n)')).toBe('nth-child-2n'); 41 | }); 42 | 43 | it('with attribute', () => { 44 | expect(selectorFormatter('[attribute^=value]')).toBe('attribute-value'); 45 | }); 46 | 47 | it('all elements', () => { 48 | expect(selectorFormatter('*')).toBe('all'); 49 | }); 50 | 51 | it('two elements', () => { 52 | expect(selectorFormatter('div, p')).toBe('div-p'); 53 | }); 54 | -------------------------------------------------------------------------------- /test/variable-syntax.test.js: -------------------------------------------------------------------------------- 1 | const run = require('./_run'); 2 | 3 | it('sass variable syntax', () => { 4 | const input = `.foo { 5 | border: 1px solid #000; 6 | background-color: red; 7 | }`; 8 | const output = `$theme-border-1: 1px solid #000;\n$theme-background-color-1: red;\n.foo { 9 | border: $theme-border-1; 10 | background-color: $theme-background-color-1; 11 | }`; 12 | return run(input, output, { 13 | templateVariableName: 'theme[propertyName]', 14 | variableSyntax: 'sass', 15 | }); 16 | }); 17 | 18 | it('less variable syntax', () => { 19 | const input = `.foo { 20 | border: 1px solid #000; 21 | background-color: red; 22 | } 23 | .bar { 24 | border: blue; 25 | }`; 26 | const output = `@theme-border-1: 1px solid #000; 27 | @theme-background-color-1: red; 28 | @theme-border-2: blue; 29 | .foo { 30 | border: @theme-border-1; 31 | background-color: @theme-background-color-1; 32 | } 33 | .bar { 34 | border: @theme-border-2; 35 | }`; 36 | return run(input, output, { 37 | templateVariableName: 'theme[propertyName]', 38 | variableSyntax: 'less', 39 | }); 40 | }); 41 | 42 | it('variable template with tint and sass syntax', () => { 43 | const input = `.foo { 44 | border: 2px solid #000; 45 | color: #020202; 46 | background-color: #0d0d0d; 47 | }`; 48 | const output = `$black-1: #000;\n$black-light-1: #020202;\n$black-light-2: #0d0d0d;\n.foo { 49 | border: 2px solid $black-1; 50 | color: $black-light-1; 51 | background-color: $black-light-2; 52 | }`; 53 | return run(input, output, { 54 | onlyColor: true, 55 | templateVariableName: '[colorKeyword][tint]', 56 | variableSyntax: 'sass', 57 | }); 58 | }); 59 | -------------------------------------------------------------------------------- /test/variable-templating.test.js: -------------------------------------------------------------------------------- 1 | const run = require('./_run'); 2 | 3 | it('variable template', () => { 4 | const input = `.foo { 5 | border: 2px solid #000; 6 | }`; 7 | const output = `:root { 8 | --theme-black-1: #000;\n}\n.foo { 9 | border: 2px solid var(--theme-black-1); 10 | }`; 11 | return run(input, output, { onlyColor: true, templateVariableName: 'theme[colorKeyword]' }); 12 | }); 13 | 14 | it('variable template with two black colors', () => { 15 | const input = `.foo { 16 | border: 2px solid #000; 17 | color: #020202; 18 | }`; 19 | const output = `:root { 20 | --theme-black-1: #000; 21 | --theme-black-2: #020202;\n}\n.foo { 22 | border: 2px solid var(--theme-black-1); 23 | color: var(--theme-black-2); 24 | }`; 25 | return run(input, output, { onlyColor: true, templateVariableName: 'theme[colorKeyword]' }); 26 | }); 27 | 28 | it('variable template with light and dark color', () => { 29 | const input = `.foo { 30 | border: 2px solid #cc0000; 31 | color: #ff0000; 32 | background-color: rgb(255, 26, 26); 33 | }`; 34 | const output = `:root { 35 | --theme-dark-red-1: #cc0000; 36 | --theme-red-1: #ff0000; 37 | --theme-light-red-1: rgb(255, 26, 26);\n}\n.foo { 38 | border: 2px solid var(--theme-dark-red-1); 39 | color: var(--theme-red-1); 40 | background-color: var(--theme-light-red-1); 41 | }`; 42 | return run(input, output, { 43 | onlyColor: true, 44 | templateVariableName: 'theme[tint][colorKeyword]', 45 | }); 46 | }); 47 | 48 | it('variable template with tint', () => { 49 | const input = `.foo { 50 | border: 2px solid #000; 51 | color: #020202; 52 | background-color: #0d0d0d; 53 | }`; 54 | const output = `:root { 55 | --black-1: #000; 56 | --black-light-1: #020202; 57 | --black-light-2: #0d0d0d;\n}\n.foo { 58 | border: 2px solid var(--black-1); 59 | color: var(--black-light-1); 60 | background-color: var(--black-light-2); 61 | }`; 62 | return run(input, output, { onlyColor: true, templateVariableName: '[colorKeyword][tint]' }); 63 | }); 64 | 65 | it('variable template without special word at the end', () => { 66 | const input = `.foo { 67 | border: 2px solid #000; 68 | color: #020202; 69 | background-color: #0d0d0d; 70 | }`; 71 | const output = `:root { 72 | --black-new-1: #000; 73 | --light-black-new-1: #020202; 74 | --light-black-new-2: #0d0d0d;\n}\n.foo { 75 | border: 2px solid var(--black-new-1); 76 | color: var(--light-black-new-1); 77 | background-color: var(--light-black-new-2); 78 | }`; 79 | return run(input, output, { 80 | onlyColor: true, 81 | templateVariableName: '[tint][colorKeyword]new', 82 | }); 83 | }); 84 | 85 | it('template with propertyName', () => { 86 | const input = `.foo { 87 | border: 1px solid #000; 88 | background-color: red; 89 | }`; 90 | const output = `:root { 91 | --theme-border-1: 1px solid #000; 92 | --theme-background-color-1: red;\n}\n.foo { 93 | border: var(--theme-border-1); 94 | background-color: var(--theme-background-color-1); 95 | }`; 96 | return run(input, output, { 97 | templateVariableName: 'theme[propertyName]', 98 | }); 99 | }); 100 | 101 | it('variable template with selector', () => { 102 | const input = `.foo { 103 | border: 2px solid #000; 104 | color: red; 105 | }`; 106 | const output = `:root { 107 | --theme-foo-1: #000; 108 | --theme-foo-2: red;\n}\n.foo { 109 | border: 2px solid var(--theme-foo-1); 110 | color: var(--theme-foo-2); 111 | }`; 112 | return run(input, output, { onlyColor: true, templateVariableName: 'theme[selectorName]' }); 113 | }); 114 | 115 | it('variable template with tint and selector', () => { 116 | const input = `.foo.bar { 117 | border: 2px solid #000; 118 | color: #020202; 119 | background-color: #0d0d0d; 120 | }`; 121 | const output = `:root { 122 | --foo-bar-black-1: #000; 123 | --foo-bar-black-light-1: #020202; 124 | --foo-bar-black-light-2: #0d0d0d;\n}\n.foo.bar { 125 | border: 2px solid var(--foo-bar-black-1); 126 | color: var(--foo-bar-black-light-1); 127 | background-color: var(--foo-bar-black-light-2); 128 | }`; 129 | return run(input, output, { 130 | onlyColor: true, 131 | templateVariableName: '[selectorName][colorKeyword][tint]', 132 | }); 133 | }); 134 | --------------------------------------------------------------------------------