├── .editorconfig ├── .gitignore ├── .npmignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── index.js ├── package.json └── test ├── cases ├── result.css ├── source.css └── testCase.js └── 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 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .gitignore 2 | .editorconfig 3 | 4 | node_modules/ 5 | npm-debug.log 6 | 7 | *.test.js 8 | .travis.yml 9 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | cache: yarn 3 | node_js: 4 | - stable 5 | - "6" 6 | - "4" 7 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mcattx/postcss-define-function/1b9f8cb038119ef48c5f8a9afda83396fb7b7f1d/CHANGELOG.md -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright 2016 mcattx 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 define-function [![Build Status][ci-img]][ci] 2 | 3 | [PostCSS] plugin to implement sass @function. 4 | 5 | [PostCSS]: https://github.com/postcss/postcss 6 | [ci-img]: https://travis-ci.org/titancat/postcss-define-function.svg 7 | [ci]: https://travis-ci.org/titancat/postcss-define-function 8 | 9 | ```css 10 | @define-function rem($val) { 11 | @return $val / 640 * 10 * 1rem; 12 | } 13 | @callFn .foo { 14 | /* Input example */ 15 | height: rem(640); 16 | } 17 | ``` 18 | 19 | ```css 20 | .foo { 21 | /* Output example */ 22 | height: 10rem} 23 | ``` 24 | 25 | **It only supports the basic four mixed operations: `+`、`-`、`*`、`/`. If you want to be able to use more advanced features, you can use [mixins](https://github.com/postcss/postcss-mixins) instead or help me to improve it.** 26 | 27 | **Looking forward to your [issues](https://github.com/titancat/postcss-define-function/issues) and [pull requests](https://github.com/titancat/postcss-define-function/pulls)** 28 | 29 | ## Usage 30 | 31 | ```js 32 | postcss([ require('postcss-define-function') ]) 33 | ``` 34 | 35 | See [PostCSS] docs for examples for your environment. 36 | 37 | ## Options 38 | 39 | Call plugin function to set options: 40 | 41 | ```js 42 | postcss([ require('postcss-define-function')({ silent: true }) ]) 43 | ``` 44 | 45 | ### `silent` 46 | Remove unknown callFns and do not throw a error. Default is `false`. 47 | 48 | ## Test 49 | ```js 50 | // basic feature test 51 | npm test 52 | // preview visual test results 53 | npm run testCase 54 | ``` 55 | 56 | ## Thanks 57 | 58 | - [postcss](https://github.com/postcss/postcss) 59 | - [postcss-plugin-boilerplate](https://github.com/postcss/postcss-plugin-boilerplate) 60 | - [postcss-mixins](https://github.com/postcss/postcss-mixins) 61 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var postcss = require('postcss'); 2 | var path = require('path'); 3 | 4 | var fn = {}; 5 | // record all customize function name 6 | var fnNameList = []; 7 | 8 | var DEFINEFUNCTION_KEY = 'define-function'; 9 | var FUNCTION_KEY = 'function'; 10 | var RETURN_KEY = 'return'; 11 | var PARSEFUNCTION_KEY = 'callFn'; 12 | 13 | /** 14 | * [remove space in target string] 15 | * @param {[String]} str 16 | * @return {[String]} 17 | */ 18 | function removeSpace(str) { 19 | return str.replace(/\s+/g, ''); 20 | } 21 | 22 | /** 23 | * [replaceFn description] 24 | * @param {[type]} name [custom function name] 25 | * @param {[type]} args [custom function arguments] 26 | * @param {[type]} content [custom function content] 27 | * @return {[String]} 28 | * 29 | * 30 | * rem(640) => 640 / 640 * 10 rem; 31 | */ 32 | function replaceFn(name, args, content) { 33 | var newContent = ''; 34 | var oneVarRE = /(\$\w+)/g; 35 | var variableRE = /(\$\w+)([\+\-\*/@])/g; 36 | var content = removeSpace(content); 37 | // to get '$a' in the end of a content string beacuse of my bad Reg 38 | content = content + '@'; 39 | var length = args.length; 40 | 41 | if (length) { 42 | if(length === 1) { 43 | content = content.replace(oneVarRE, args[0]); 44 | } else { 45 | var i = 0; 46 | var replacer = function(str, $1, $2) { 47 | if(args[i]){ 48 | return args[i++] + $2; 49 | } else { 50 | return str; 51 | } 52 | 53 | } 54 | content = content.replace(variableRE, replacer); 55 | } 56 | 57 | } else { 58 | // no params 59 | content = content; 60 | } 61 | // delete '@' 62 | newContent = content.substr(0, content.length - 1); 63 | 64 | 65 | 66 | return newContent; 67 | } 68 | 69 | /** 70 | * [computeValue description] 71 | * @param {[String]} valueStr [css value to be calculated] 72 | * @return {[String]} 73 | * 74 | * '640 / 640 * 10 rem' => '10rem' 75 | */ 76 | function computeValue(valueStr) { 77 | var resultStr = ''; 78 | // length data unit 79 | var unit = ['%', 'in', 'cm', 'mm', 'em', 'ex', 'pt', 'pc', 'px', 'rem', 'vh', 'vw', 'vmin', 'vmax', 'ch']; 80 | var unitStr = unit.join('|'); 81 | var unitRE = new RegExp(unitStr, 'g'); 82 | var tempStr = valueStr.match(unitRE)[0]; 83 | if(tempStr) { 84 | valueStr = valueStr.replace(tempStr, ''); 85 | valueStr = eval(valueStr); 86 | resultStr = valueStr + tempStr; 87 | } 88 | return resultStr; 89 | } 90 | 91 | /** 92 | * refer to : https://github.com/postcss/postcss-mixins/blob/master/index.js 93 | * beacuse last version code is too ugly, I decide to refactor my codes. 94 | */ 95 | 96 | function insideDefine(rule) { 97 | var parent = rule.parent; 98 | if(!parent) { 99 | return false; 100 | } else if(parent.name === DEFINEFUNCTION_KEY) { 101 | return true; 102 | } else { 103 | return insideDefine(parent); 104 | } 105 | } 106 | 107 | function insertFn(root, fns, fnNames, rule, processFns, opts) { 108 | 109 | rule.walkDecls(function(i) { 110 | var name = detachCustomName(i.value); 111 | var args = detachCustomValues(i.value); 112 | 113 | if(name && name != '') { 114 | var selector = i.parent.params; 115 | var prop = i.prop; 116 | var meta = fns[name]; 117 | var fn = meta && meta.fn; 118 | 119 | 120 | if(!meta) { 121 | if(!opts.silent) { 122 | throw rule.error('Undefined @define-function ' + name); 123 | } else { 124 | i.parent.remove(); 125 | } 126 | 127 | } else{ 128 | var content = meta.content; 129 | if(fnNames.indexOf(name) !== -1) { 130 | var tempValue = replaceFn(name, args, content); 131 | i.value = computeValue(tempValue); 132 | 133 | // @callFn a {} => a {}; 134 | var proxy = postcss.parse('a{}'); 135 | proxy.type = 'rule'; 136 | proxy.selector = i.parent.params; 137 | proxy.nodes = i.parent.nodes; 138 | i.parent.replaceWith(proxy); 139 | } 140 | } 141 | 142 | } 143 | }) 144 | 145 | } 146 | 147 | function defineFn(result, fns, fnNames, rule) { 148 | var fn = detachProps(rule); 149 | var name = fn.name; 150 | var args = fn.args; 151 | var content = fn.content; 152 | 153 | fns[name] = { 154 | name: name, 155 | args: args, 156 | content: content 157 | }; 158 | 159 | fnNames.push(name); 160 | 161 | rule.remove(); 162 | } 163 | /** 164 | * [detach custom name in rule] 165 | * @param {[String]} ruleName 166 | * @return {[String]} 167 | * 168 | * rem(400) || rem($val) => rem 169 | */ 170 | function detachCustomName(ruleName) { 171 | ruleName = removeSpace(ruleName); 172 | var nameRE = /[-\w]+\(/g; 173 | var name = ''; 174 | 175 | if(ruleName.match(nameRE)) { 176 | name = ruleName.match(nameRE)[0]; 177 | name = name.substring(0, name.length - 1); 178 | } 179 | 180 | return name; 181 | } 182 | 183 | /** 184 | * [detachCustomValues description] 185 | * @param {[String]} ruleName [ruleName] 186 | * @return {[Array]} 187 | * 188 | * 'rem(a, b, c)' => ['a', 'b', 'c'] 189 | */ 190 | function detachCustomValues(ruleName) { 191 | ruleName = removeSpace(ruleName); 192 | var argsRE = /\(\S+\)/g; 193 | var args = []; 194 | if(ruleName.match(argsRE)) { 195 | args = ruleName.match(argsRE)[0].replace(/\(|\)/g, '').split(','); 196 | } 197 | 198 | return args; 199 | } 200 | 201 | /** 202 | * [detach props from function definition] 203 | * @param {[String]} rule [Postcss Rule] 204 | * @return {[Object]} 205 | * 206 | * @define-function rem($val) {@return $val / 640 * 10 * 1rem} => {name: 'rem', args: ['$val'], content: '$val / 640 * 10 * 1rem'} 207 | * 208 | */ 209 | function detachProps(rule) { 210 | var rs = {}; 211 | var params = removeSpace(rule.params); 212 | var argsRE = /\(\S+\)/g; 213 | 214 | var name = detachCustomName(params); 215 | 216 | var args = detachCustomValues(params); 217 | 218 | var content = removeSpace(rule.nodes[0].params); 219 | 220 | rs.name = name; 221 | rs.args = args; 222 | rs.content = content; 223 | return rs; 224 | } 225 | 226 | module.exports = postcss.plugin('postcss-precss-function', function(opts) { 227 | if (typeof opts === 'undefined') { 228 | opts = {}; 229 | } 230 | 231 | var cwd = process.cwd(); 232 | var globs = []; 233 | var fns = {}; 234 | var fnNames = []; 235 | 236 | return function(css, result, root) { 237 | var processFns = function(root) { 238 | root.walkAtRules(function(i) { 239 | if(i.name === PARSEFUNCTION_KEY) { 240 | if(!insideDefine(i)) { 241 | insertFn(root, fns, fnNames, i, processFns, opts); 242 | } 243 | } else if(i.name === DEFINEFUNCTION_KEY) { 244 | defineFn(result, fns, fnNames, i); 245 | } 246 | }) 247 | } 248 | 249 | processFns(css); 250 | 251 | } 252 | }); 253 | 254 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "postcss-define-function", 3 | "version": "0.1.2", 4 | "description": "PostCSS plugin : A plugin to implement sass @function directive", 5 | "keywords": [ 6 | "postcss", 7 | "css", 8 | "postcss-plugin", 9 | "define", 10 | "function", 11 | "directive", 12 | "sass" 13 | ], 14 | "author": "mcattx ", 15 | "license": "MIT", 16 | "repository": "titancat/postcss-define-function", 17 | "bugs": { 18 | "url": "https://github.com/titancat/postcss-define-function/issues" 19 | }, 20 | "homepage": "https://github.com/titancat/postcss-define-function", 21 | "dependencies": { 22 | "postcss": "^5.2.6" 23 | }, 24 | "devDependencies": { 25 | "ava": "^0.17.0", 26 | "eslint": "^4.18.2", 27 | "eslint-config-postcss": "^2.0.2" 28 | }, 29 | "scripts": { 30 | "test": "ava test/test.js", 31 | "testCase": "node ./test/cases/testCase.js" 32 | }, 33 | "eslintConfig": { 34 | "extends": "eslint-config-postcss/es5" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /test/cases/result.css: -------------------------------------------------------------------------------- 1 | a { 2 | width: 300px; 3 | height: 10rem; 4 | line-height: 100} 5 | 6 | b { 7 | color: red; 8 | width: 5rem} 9 | 10 | a { 11 | width: 8rem} 12 | b { 13 | width: 30px} 14 | a { 15 | color: red; 16 | height: 8cm} 17 | 18 | 19 | b { 20 | color: #fff; 21 | } 22 | 23 | @media screen and (min-width: 480px) { 24 | body { 25 | background-color: lightgreen; 26 | } 27 | } 28 | 29 | ul li { 30 | padding: 5px; 31 | } 32 | -------------------------------------------------------------------------------- /test/cases/source.css: -------------------------------------------------------------------------------- 1 | @define-function rem($val) { 2 | @return $val / 640 * 10 * 1rem; 3 | } 4 | 5 | @callFn a { 6 | width: 300px; 7 | height: rem(640); 8 | line-height: 100; 9 | } 10 | 11 | @define-function pe($val) { 12 | @return $val / 10 * 5 * 1rem; 13 | } 14 | 15 | @callFn b { 16 | color: red; 17 | width: pe(10); 18 | } 19 | 20 | @define-function pe($val) { 21 | @return $val / $val * ($val - 2)rem; 22 | } 23 | 24 | @callFn a { 25 | width: pe(10); 26 | } 27 | 28 | @define-function pe($a, $b) { 29 | @return $a + $b + 10px; 30 | } 31 | @callFn b{ 32 | width: pe(10, 10) 33 | } 34 | 35 | @define-function pe($n) { 36 | @return $n / $n * ($n - 2)cm; 37 | } 38 | @callFn a { 39 | color: red; 40 | height: pe(10); 41 | } 42 | 43 | 44 | b { 45 | color: #fff; 46 | } 47 | 48 | @media screen and (min-width: 480px) { 49 | body { 50 | background-color: lightgreen; 51 | } 52 | } 53 | 54 | ul li { 55 | padding: 5px; 56 | } 57 | -------------------------------------------------------------------------------- /test/cases/testCase.js: -------------------------------------------------------------------------------- 1 | var postcss = require('postcss'); 2 | var path = require('path'); 3 | var fs = require('fs'); 4 | 5 | // alias postcss-define-function -> fn 6 | var fn = require('../../index.js'); 7 | 8 | var opts = { 9 | silent: true 10 | }; 11 | 12 | fs.readFile('test/cases/source.css', 'utf8', (err, css) => { 13 | postcss(fn(opts)).process(css).then( 14 | result => { 15 | fs.writeFile('test/cases/result.css', result, (err) => { 16 | if(err) { 17 | console.log('Write file error: ' + err); 18 | } 19 | }) 20 | } 21 | ).catch((err) => { 22 | console.log('err: ' + err); 23 | }) 24 | }) 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | var postcss = require('postcss'); 2 | var path = require('path'); 3 | var test = require('ava'); 4 | 5 | // alias postcss-define-function -> fn 6 | var fn = require('../'); 7 | 8 | function run(t, input, output, opts) { 9 | return postcss(fn(opts)).process(input).then( 10 | result => { 11 | t.deepEqual(result.css, output); 12 | t.deepEqual(result.warnings().length, 0); 13 | } 14 | ) 15 | } 16 | 17 | // test cases 18 | 19 | test('test core feature', t => { 20 | return run(t, '@define-function rem($val) {@return $val / 640 * 10 * 1rem;} @callFn a {width: rem(640);}', 21 | 'a {width: 10rem}' 22 | ); 23 | }); 24 | 25 | test('test two decls', t => { 26 | return run(t, '@define-function pe($val) {@return $val / 10 * 5 * 1rem;} @callFn a {color: red;width: pe(10);}', 27 | 'a {color: red;width: 5rem}' 28 | ); 29 | }); 30 | 31 | test('test multiple decls', t => { 32 | return run(t, '@define-function pe($val) {@return $val / 10 * 1rem;} @callFn a {color: red;width: pe(10);height: 20px}', 33 | 'a {color: red;width: 1rem;height: 20px}' 34 | ); 35 | }); 36 | 37 | test('test multiple params in @return statement', t => { 38 | return run(t, '@define-function pe($val) {@return $val / $val * ($val - 2)rem;} @callFn a {color: red;width: pe(10);height: 20px}', 39 | 'a {color: red;width: 8rem;height: 20px}' 40 | ); 41 | }); 42 | 43 | test('test multiple params in define statement', t => { 44 | return run(t , '@define-function pe($a, $b) {@return $a + $b + 10px;} @callFn a {width: pe(10, 10)}', 45 | 'a {width: 30px}') 46 | }); 47 | 48 | test('test different css value units', t => { 49 | return run(t, '@define-function pe($n) {@return $n / $n * ($n - 2)cm;} @callFn a {color: red;width: pe(10);height: 20px}', 50 | 'a {color: red;width: 8cm;height: 20px}' 51 | ); 52 | }); 53 | 54 | // I decide not to support this feature, this function should be supported by postcss-simple-vars 55 | // test.skip('test variables', t => { 56 | // return run(t, '$column: 20; @define-function pe() {@return $colum * 3px} @callFn a {width: pe();}', 57 | // 'a {width: 60px}' 58 | // ); 59 | // }); 60 | 61 | test('throws error on unknown callFn', t => { 62 | return run(t, '@callFn a{width: te()}', 'Undefined @define-function te').catch(err => { 63 | t.deepEqual(err.reason, 'Undefined @define-function te'); 64 | }) 65 | }); 66 | 67 | test('remove unknown callFn on request', t => { 68 | return run(t, '@callFn a{width: te()} b{}', 'b{}', { silent: true }); 69 | }); 70 | --------------------------------------------------------------------------------