├── .github └── workflows │ ├── codeql-analysis.yml │ └── node.js.yml ├── .gitignore ├── .npmignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── babel.config.json ├── package-lock.json ├── package.json └── src ├── __tests__ └── index.mjs ├── index.mjs └── lib ├── helpers.mjs └── transformer.mjs /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | name: "CodeQL" 7 | 8 | on: 9 | push: 10 | branches: [master] 11 | pull_request: 12 | # The branches below must be a subset of the branches above 13 | branches: [master] 14 | schedule: 15 | - cron: '0 1 * * 0' 16 | 17 | jobs: 18 | analyze: 19 | name: Analyze 20 | runs-on: ubuntu-latest 21 | 22 | strategy: 23 | fail-fast: false 24 | matrix: 25 | # Override automatic language detection by changing the below list 26 | # Supported options are ['csharp', 'cpp', 'go', 'java', 'javascript', 'python'] 27 | language: ['javascript'] 28 | # Learn more... 29 | # https://docs.github.com/en/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#overriding-automatic-language-detection 30 | 31 | steps: 32 | - name: Checkout repository 33 | uses: actions/checkout@v2 34 | 35 | # Initializes the CodeQL tools for scanning. 36 | - name: Initialize CodeQL 37 | uses: github/codeql-action/init@v1 38 | with: 39 | languages: ${{ matrix.language }} 40 | # If you wish to specify custom queries, you can do so here or in a config file. 41 | # By default, queries listed here will override any specified in a config file. 42 | # Prefix the list here with "+" to use these queries and those in the config file. 43 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 44 | 45 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 46 | # If this step fails, then you should remove it and run the build manually (see below) 47 | - name: Autobuild 48 | uses: github/codeql-action/autobuild@v1 49 | 50 | # ℹ️ Command-line programs to run using the OS shell. 51 | # 📚 https://git.io/JvXDl 52 | 53 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 54 | # and modify them (or add more) to build your code if your project 55 | # uses a compiled language 56 | 57 | #- run: | 58 | # make bootstrap 59 | # make release 60 | 61 | - name: Perform CodeQL Analysis 62 | uses: github/codeql-action/analyze@v1 63 | -------------------------------------------------------------------------------- /.github/workflows/node.js.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: Node.js CI 5 | 6 | on: 7 | push: 8 | branches: [ master ] 9 | pull_request: 10 | branches: [ master ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | strategy: 18 | matrix: 19 | node-version: [12.x, 14.x] 20 | 21 | steps: 22 | - uses: actions/checkout@v2 23 | - name: Use Node.js ${{ matrix.node-version }} 24 | uses: actions/setup-node@v1 25 | with: 26 | node-version: ${{ matrix.node-version }} 27 | - run: npm ci 28 | - run: npm test 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | npm-debug.log 3 | .nyc_output/ 4 | dest/ 5 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .gitignore 2 | 3 | node_modules/ 4 | 5 | babel.config.json 6 | src/ 7 | dest/__tests__/ 8 | .github/ 9 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 4.0.2 - 2020-11-08 2 | * Update to PostCSS 8. 3 | * Remove `glob` option. 4 | 5 | # 3.0.0 - 2017-08-13 6 | * Rewrite in ES6. 7 | * Add Babel. 8 | * Replace Mocha with AVA. 9 | * Update dependencies. 10 | * Add ability to use functions in selectors. 11 | 12 | # 2.1.1 - 2016-10-08 13 | * Only apply promises if function is asynchronous. Fixes out-of-memory issues. 14 | 15 | # 2.1.0 - 2016-01-28 16 | * Replace undocumented extend method with object-assign 17 | * Replace reduce-function-call with postcss-value-parser 18 | * Add support for promises 19 | 20 | # 2.0.0 - 2015-11-09 21 | * Update to PostCSS 5.x 22 | 23 | # 1.0.0 - 2015-08-04 24 | * Initial release 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright 2015 Andy Jansson 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # postcss-functions 2 | 3 | [PostCSS] plugin for exposing JavaScript functions. 4 | 5 | [PostCSS]: https://github.com/postcss/postcss 6 | 7 | ## Installation 8 | 9 | ```js 10 | npm install --save-dev postcss postcss-functions 11 | ``` 12 | 13 | ## Usage 14 | 15 | ```js 16 | import fs from 'fs'; 17 | import postcss from 'postcss'; 18 | import functions from 'postcss-functions'; 19 | 20 | const options = { 21 | //options 22 | }; 23 | 24 | const css = fs.readFileSync('input.css', 'utf8'); 25 | 26 | postcss() 27 | .use(functions(options)) 28 | .process(css) 29 | .then((result) => { 30 | const output = result.css; 31 | }); 32 | ``` 33 | 34 | **Example** of a function call: 35 | 36 | ```css 37 | body { 38 | prop: foobar(); 39 | } 40 | ``` 41 | 42 | ## Options 43 | 44 | ### `functions` 45 | 46 | Type: `Object` 47 | 48 | An object containing functions. The function name will correspond with the object key. 49 | 50 | **Example:** 51 | 52 | ```js 53 | import postcssFunctions from 'postcss-functions'; 54 | import { fromString, fromRgb } from 'css-color-converter'; 55 | ``` 56 | 57 | ```js 58 | function darken(value, frac) { 59 | const darken = 1 - parseFloat(frac); 60 | const rgba = fromString(value).toRgbaArray(); 61 | const r = rgba[0] * darken; 62 | const g = rgba[1] * darken; 63 | const b = rgba[2] * darken; 64 | return fromRgb([r,g,b]).toHexString(); 65 | } 66 | ``` 67 | 68 | ```js 69 | postcssFunctions({ 70 | functions: { darken } 71 | }); 72 | ``` 73 | 74 | ```css 75 | .foo { 76 | /* make 10% darker */ 77 | color: darken(blue, 0.1); 78 | } 79 | ``` 80 | 81 | #### Hey, what happened to `glob`? 82 | 83 | Versions prior to 4.0.0 had a globbing feature built in, but I've since decided to remove this feature from `postcss-functions`. This means one less dependency and a smaller package size. For people still interested in this feature, you are free to pair `postcss-functions` with the globbing library of your choice and pass the `import`ed JavaScript files to the `functions` option as described above. 84 | -------------------------------------------------------------------------------- /babel.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/preset-env"], 3 | "plugins": [ 4 | "add-module-exports", 5 | ["module-extension", { 6 | "mjs": "js" 7 | }] 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "postcss-functions", 3 | "version": "4.0.2", 4 | "description": "PostCSS plugin for exposing JavaScript functions", 5 | "main": "dest/index.js", 6 | "scripts": { 7 | "prepublish": "npm run build", 8 | "build": "babel src -d dest", 9 | "pretest": "eslint --ext .mjs src", 10 | "test": "ava src/__tests__/index.mjs" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "https://github.com/andyjansson/postcss-functions.git" 15 | }, 16 | "keywords": [ 17 | "postcss", 18 | "postcss-plugin", 19 | "javascript", 20 | "function", 21 | "functions" 22 | ], 23 | "author": "Andy Jansson", 24 | "license": "MIT", 25 | "bugs": { 26 | "url": "https://github.com/andyjansson/postcss-functions/issues" 27 | }, 28 | "homepage": "https://github.com/andyjansson/postcss-functions", 29 | "dependencies": { 30 | "postcss-value-parser": "^4.0.0" 31 | }, 32 | "peerDependencies": { 33 | "postcss": "^8.0.0" 34 | }, 35 | "devDependencies": { 36 | "@babel/cli": "^7.15.7", 37 | "@babel/core": "^7.15.5", 38 | "@babel/preset-env": "^7.15.6", 39 | "ava": "^3.15.0", 40 | "babel-plugin-add-module-exports": "^1.0.4", 41 | "babel-plugin-module-extension": "^0.1.3", 42 | "eslint": "^7.28.0", 43 | "eslint-config-i-am-meticulous": "^12.0.0", 44 | "postcss": "^8.0.0" 45 | }, 46 | "eslintConfig": { 47 | "extends": "eslint-config-i-am-meticulous" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/__tests__/index.mjs: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import postcss from 'postcss'; 3 | 4 | import functions from '../index.mjs' 5 | 6 | async function testFixture(t, fixture, expected = null, opts = {}) { 7 | if (expected === null) 8 | expected = fixture 9 | 10 | const out = await postcss(functions(opts)).process(fixture, { from: undefined }); 11 | t.deepEqual(out.css, expected); 12 | } 13 | 14 | test( 15 | 'should invoke a recognized function', 16 | testFixture, 17 | 'a{foo:bar()}', 18 | 'a{foo:baz}', 19 | { 20 | functions: { 21 | 'bar': function () { 22 | return 'baz'; 23 | } 24 | } 25 | } 26 | ); 27 | 28 | test( 29 | 'should accept deferred functions', 30 | testFixture, 31 | 'a{foo:bar()}', 32 | 'a{foo:baz}', 33 | { 34 | functions: { 35 | 'bar': function () { 36 | return Promise.resolve('baz'); 37 | } 38 | } 39 | } 40 | ); 41 | 42 | test( 43 | 'should invoke multiple functions', 44 | testFixture, 45 | 'a{foo:bar() baz()}', 46 | 'a{foo:bat qux}', 47 | { 48 | functions: { 49 | 'bar': function () { 50 | return 'bat'; 51 | }, 52 | 'baz': function () { 53 | return 'qux'; 54 | } 55 | } 56 | } 57 | ); 58 | 59 | test( 60 | 'should ignore unrecognized functions', 61 | testFixture, 62 | 'a{foo:bar()}' 63 | ); 64 | 65 | test( 66 | 'should be able to pass arguments to functions', 67 | testFixture, 68 | 'a{foo:bar(qux, norf)}', 69 | 'a{foo:qux-norf}', 70 | { 71 | functions: { 72 | 'bar': function (baz, bat) { 73 | return baz + '-' + bat; 74 | } 75 | } 76 | } 77 | ); 78 | 79 | test( 80 | 'should be able to pass arguments with spaces to functions', 81 | testFixture, 82 | 'a{foo:bar(hello world)}', 83 | 'a{foo:hello-world}', 84 | { 85 | functions: { 86 | 'bar': function (baz) { 87 | return baz.replace(' ', '-'); 88 | } 89 | } 90 | } 91 | ); 92 | 93 | test( 94 | 'should invoke a function in an at-rule', 95 | testFixture, 96 | '@foo bar(){bat:qux}', 97 | '@foo baz{bat:qux}', 98 | { 99 | functions: { 100 | 'bar': function () { 101 | return 'baz'; 102 | } 103 | } 104 | } 105 | ); 106 | 107 | test( 108 | 'should invoke a function in a rule', 109 | testFixture, 110 | 'foo:nth-child(bar()){}', 111 | 'foo:nth-child(baz){}', 112 | { 113 | functions: { 114 | 'bar': function () { 115 | return 'baz'; 116 | } 117 | } 118 | } 119 | ); 120 | 121 | test( 122 | 'should invoke nested functions', 123 | testFixture, 124 | 'a{foo:bar(baz())}', 125 | 'a{foo:batqux}', 126 | { 127 | functions: { 128 | 'bar': function (arg) { 129 | return 'bat' + arg; 130 | }, 131 | 'baz': function () { 132 | return Promise.resolve('qux'); 133 | } 134 | } 135 | } 136 | ); 137 | 138 | test( 139 | 'should not pass empty arguments', 140 | t => { 141 | return postcss(functions({ 142 | functions: { 143 | 'bar': function () { 144 | t.deepEqual(arguments.length, 0); 145 | } 146 | } 147 | })).process('a{foo:bar()}', { from: undefined }); 148 | } 149 | ); 150 | -------------------------------------------------------------------------------- /src/index.mjs: -------------------------------------------------------------------------------- 1 | import { transformAtRule, transformDecl, transformRule } from './lib/transformer.mjs'; 2 | 3 | function plugin(opts = {}) { 4 | const functions = opts.functions || {}; 5 | 6 | return { 7 | postcssPlugin: 'postcss-functions', 8 | AtRule(node) { 9 | return transformAtRule(node, functions); 10 | }, 11 | Declaration(node) { 12 | return transformDecl(node, functions); 13 | }, 14 | Rule(node) { 15 | return transformRule(node, functions); 16 | } 17 | } 18 | } 19 | 20 | plugin.postcss = true; 21 | 22 | export default plugin; 23 | -------------------------------------------------------------------------------- /src/lib/helpers.mjs: -------------------------------------------------------------------------------- 1 | export function isPromise(obj) { 2 | return !!obj && (typeof obj === 'object' || typeof obj === 'function') && typeof obj.then === 'function'; 3 | } 4 | 5 | export function hasPromises(arr) { 6 | return arr.length && arr.some(item => isPromise(item)); 7 | } 8 | 9 | export function then (promiseOrResult, onFulfilled) { 10 | if (isPromise(promiseOrResult)) 11 | return promiseOrResult.then(onFulfilled); 12 | 13 | return onFulfilled(promiseOrResult); 14 | } 15 | -------------------------------------------------------------------------------- /src/lib/transformer.mjs: -------------------------------------------------------------------------------- 1 | import valueParser from 'postcss-value-parser'; 2 | 3 | import { hasPromises, then } from './helpers.mjs'; 4 | 5 | function transformString(str, functions) { 6 | let promises = []; 7 | 8 | const values = valueParser(str).walk(part => { 9 | promises.push(transformNode(part, functions)); 10 | }); 11 | 12 | if (hasPromises(promises)) 13 | promises = Promise.all(promises); 14 | 15 | return then(promises, () => { 16 | return values.toString(); 17 | }); 18 | } 19 | 20 | function transformNode(node, functions) { 21 | if (node.type !== 'function' || !Object.prototype.hasOwnProperty.call(functions, node.value)) 22 | return node; 23 | 24 | const func = functions[node.value]; 25 | return then(extractArgs(node.nodes, functions), args => { 26 | const invocation = func.apply(func, args); 27 | 28 | return then(invocation, val => { 29 | node.type = 'word'; 30 | node.value = val; 31 | return node; 32 | }); 33 | }); 34 | } 35 | 36 | function extractArgs(nodes, functions) { 37 | nodes = nodes.map(node => transformNode(node, functions)); 38 | 39 | if (hasPromises(nodes)) 40 | nodes = Promise.all(nodes); 41 | 42 | return then(nodes, values => { 43 | const args = []; 44 | const last = values.reduce((prev, node) => { 45 | if (node.type === 'div' && node.value === ',') { 46 | args.push(prev); 47 | return ''; 48 | } 49 | return prev + valueParser.stringify(node); 50 | }, ''); 51 | 52 | if (last) 53 | args.push(last); 54 | 55 | return args; 56 | }); 57 | } 58 | 59 | export function transformDecl(node, functions) { 60 | return then(transformString(node.value, functions), value => { 61 | node.value = value; 62 | }); 63 | } 64 | 65 | export function transformAtRule(node, functions) { 66 | return then(transformString(node.params, functions), value => { 67 | node.params = value; 68 | }); 69 | } 70 | 71 | export function transformRule(node, functions) { 72 | return then(transformString(node.selector, functions), value => { 73 | node.selector = value; 74 | }); 75 | } 76 | --------------------------------------------------------------------------------