├── .gitignore ├── .eslintrc ├── test ├── fixtures │ ├── pseudos-restricted.css │ ├── blacklist.css │ ├── blacklist.out.css │ ├── pseudos-restricted.out.css │ ├── pseudos.css │ ├── pseudos.out.css │ ├── prefix.out.css │ └── pseudos-combinations.out.css └── test.js ├── .prettierrc ├── .editorconfig ├── CHANGELOG.md ├── .github └── workflows │ └── test.yml ├── package.json ├── LICENSE ├── README.md └── index.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | npm-debug.log 3 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "eslint:recommended", 3 | "env": { 4 | "node": true 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /test/fixtures/pseudos-restricted.css: -------------------------------------------------------------------------------- 1 | :active {} 2 | .a:hover {} 3 | .a:focus:hover {} 4 | .a + .b:hover {} 5 | .a:before + .a:not(.c) {} 6 | .a:nth-child(2) {} 7 | .a:focus:hover:active {} 8 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "arrowParens": "avoid", 3 | "jsxSingleQuote": false, 4 | "quoteProps": "consistent", 5 | "semi": true, 6 | "singleQuote": true, 7 | "trailingComma": "none" 8 | } 9 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | This project adheres to [Semantic Versioning](http://semver.org/). 3 | 4 | ## 0.4 5 | 6 | * Moved to PostCSS 8.0. 7 | 8 | ## 0.2 9 | 10 | * Added `restrictTo` option. 11 | 12 | ## 0.1 13 | 14 | * Initial release. 15 | -------------------------------------------------------------------------------- /test/fixtures/blacklist.css: -------------------------------------------------------------------------------- 1 | :root { 2 | color: blue; 3 | } 4 | 5 | :host { 6 | color: blue; 7 | } 8 | 9 | :host-context(a) { 10 | color: blue; 11 | } 12 | 13 | :host([disabled]) { 14 | opacity: .4; 15 | } 16 | 17 | :host([enabled]) { 18 | opacity: 1; 19 | } 20 | 21 | a:hover, :host { 22 | color: blue; 23 | } 24 | -------------------------------------------------------------------------------- /test/fixtures/blacklist.out.css: -------------------------------------------------------------------------------- 1 | :root { 2 | color: blue; 3 | } 4 | 5 | :host { 6 | color: blue; 7 | } 8 | 9 | :host-context(a) { 10 | color: blue; 11 | } 12 | 13 | :host([disabled]) { 14 | opacity: .4; 15 | } 16 | 17 | :host([enabled]) { 18 | opacity: 1; 19 | } 20 | 21 | a:hover, :host, 22 | a.\:hover { 23 | color: blue; 24 | } 25 | -------------------------------------------------------------------------------- /test/fixtures/pseudos-restricted.out.css: -------------------------------------------------------------------------------- 1 | :active, 2 | .\:active {} 3 | .a:hover, 4 | .a.\:hover {} 5 | .a:focus:hover, 6 | .a:focus.\:hover {} 7 | .a + .b:hover, 8 | .a + .b.\:hover {} 9 | .a:before + .a:not(.c) {} 10 | .a:nth-child(2), 11 | .a.\:nth-child\(2\) {} 12 | .a:focus:hover:active, 13 | .a:focus:hover.\:active, 14 | .a:focus.\:hover:active, 15 | .a:focus.\:hover.\:active {} 16 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | on: 3 | push: 4 | branches: 5 | - master 6 | pull_request: 7 | permissions: 8 | contents: read 9 | jobs: 10 | test: 11 | runs-on: ubuntu-latest 12 | strategy: 13 | matrix: 14 | node-version: 15 | - 20 16 | - 18 17 | name: Node.js ${{ matrix.node-version }} Quick 18 | steps: 19 | - name: Checkout the repository 20 | uses: actions/checkout@v4 21 | - name: Install Node.js ${{ matrix.node-version }} 22 | uses: actions/setup-node@v3 23 | with: 24 | node-version: ${{ matrix.node-version }} 25 | cache: npm 26 | - name: Install dependencies 27 | run: npm install --frozen-lockfile --ignore-scripts --ignore-engines 28 | - name: Run unit tests 29 | run: npm test 30 | -------------------------------------------------------------------------------- /test/fixtures/pseudos.css: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Comment. 4 | */ 5 | 6 | a { 7 | text-decoration: none; 8 | } 9 | 10 | a:hover { 11 | text-decoration: underline; 12 | } 13 | 14 | a:active { 15 | font-weight: bold; 16 | } 17 | 18 | a:hover, 19 | a:focus { 20 | font-weight: bold; 21 | } 22 | 23 | a.lol:active { 24 | font-weight: normal; 25 | } 26 | 27 | a:nth-child(5) { 28 | border: 1px solid papayawhip; 29 | } 30 | 31 | a:hover:focus { 32 | font-weight: bold; 33 | } 34 | 35 | a:before { 36 | color: white; 37 | } 38 | 39 | a::before { 40 | color: white; 41 | } 42 | 43 | a:before, 44 | a::before { 45 | color: white; 46 | } 47 | 48 | a:hover::before { 49 | border-top-color: blue; 50 | } 51 | 52 | a:active:focus:hover::before { 53 | border-bottom-color: blue; 54 | } 55 | 56 | a:active:focus + div:hover { 57 | color: magenta; 58 | } 59 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "postcss-pseudo-classes", 3 | "version": "0.4.0", 4 | "description": "PostCSS plugin to convert pseudo-classes to classes for testing purposes", 5 | "keywords": [ 6 | "postcss", 7 | "css", 8 | "postcss-plugin", 9 | "css test", 10 | "pseudo classes" 11 | ], 12 | "author": "Giuseppe Gurgone", 13 | "license": "MIT", 14 | "repository": { 15 | "type": "git", 16 | "url": "git+https://github.com/giuseppeg/postcss-pseudo-classes.git" 17 | }, 18 | "bugs": { 19 | "url": "https://github.com/giuseppeg/postcss-pseudo-classes/issues" 20 | }, 21 | "homepage": "https://github.com/giuseppeg/postcss-pseudo-classes", 22 | "peerDependencies": { 23 | "postcss": "^8.0.0" 24 | }, 25 | "devDependencies": { 26 | "ava": "^5.3.1", 27 | "eslint": "^8.54.0", 28 | "postcss": "^8.4.31" 29 | }, 30 | "scripts": { 31 | "test": "ava test/test.js && eslint *.js test/*.js" 32 | }, 33 | "files": [ 34 | "index.js" 35 | ] 36 | } 37 | -------------------------------------------------------------------------------- /test/fixtures/pseudos.out.css: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Comment. 4 | */ 5 | 6 | a { 7 | text-decoration: none; 8 | } 9 | 10 | a:hover, 11 | a.\:hover { 12 | text-decoration: underline; 13 | } 14 | 15 | a:active, 16 | a.\:active { 17 | font-weight: bold; 18 | } 19 | 20 | a:hover, 21 | a:focus, 22 | a.\:hover, 23 | a.\:focus { 24 | font-weight: bold; 25 | } 26 | 27 | a.lol:active, 28 | a.lol.\:active { 29 | font-weight: normal; 30 | } 31 | 32 | a:nth-child(5), 33 | a.\:nth-child\(5\) { 34 | border: 1px solid papayawhip; 35 | } 36 | 37 | a:hover:focus, 38 | a.\:hover.\:focus { 39 | font-weight: bold; 40 | } 41 | 42 | a:before { 43 | color: white; 44 | } 45 | 46 | a::before { 47 | color: white; 48 | } 49 | 50 | a:before, 51 | a::before { 52 | color: white; 53 | } 54 | 55 | a:hover::before, 56 | a.\:hover::before { 57 | border-top-color: blue; 58 | } 59 | 60 | a:active:focus:hover::before, 61 | a.\:active.\:focus.\:hover::before { 62 | border-bottom-color: blue; 63 | } 64 | 65 | a:active:focus + div:hover, 66 | a.\:active.\:focus + div.\:hover { 67 | color: magenta; 68 | } 69 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright 2015-present Giuseppe Gurgone 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 | -------------------------------------------------------------------------------- /test/fixtures/prefix.out.css: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Comment. 4 | */ 5 | 6 | a { 7 | text-decoration: none; 8 | } 9 | 10 | a:hover, 11 | a.pseudo-class-hover { 12 | text-decoration: underline; 13 | } 14 | 15 | a:active, 16 | a.pseudo-class-active { 17 | font-weight: bold; 18 | } 19 | 20 | a:hover, 21 | a:focus, 22 | a.pseudo-class-hover, 23 | a.pseudo-class-focus { 24 | font-weight: bold; 25 | } 26 | 27 | a.lol:active, 28 | a.lol.pseudo-class-active { 29 | font-weight: normal; 30 | } 31 | 32 | a:nth-child(5), 33 | a.pseudo-class-nth-child\(5\) { 34 | border: 1px solid papayawhip; 35 | } 36 | 37 | a:hover:focus, 38 | a.pseudo-class-hover.pseudo-class-focus { 39 | font-weight: bold; 40 | } 41 | 42 | a:before { 43 | color: white; 44 | } 45 | 46 | a::before { 47 | color: white; 48 | } 49 | 50 | a:before, 51 | a::before { 52 | color: white; 53 | } 54 | 55 | a:hover::before, 56 | a.pseudo-class-hover::before { 57 | border-top-color: blue; 58 | } 59 | 60 | a:active:focus:hover::before, 61 | a.pseudo-class-active.pseudo-class-focus.pseudo-class-hover::before { 62 | border-bottom-color: blue; 63 | } 64 | 65 | a:active:focus + div:hover, 66 | a.pseudo-class-active.pseudo-class-focus + div.pseudo-class-hover { 67 | color: magenta; 68 | } 69 | -------------------------------------------------------------------------------- /test/fixtures/pseudos-combinations.out.css: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Comment. 4 | */ 5 | 6 | a { 7 | text-decoration: none; 8 | } 9 | 10 | a:hover, 11 | a.\:hover { 12 | text-decoration: underline; 13 | } 14 | 15 | a:active, 16 | a.\:active { 17 | font-weight: bold; 18 | } 19 | 20 | a:hover, 21 | a:focus, 22 | a.\:hover, 23 | a.\:focus { 24 | font-weight: bold; 25 | } 26 | 27 | a.lol:active, 28 | a.lol.\:active { 29 | font-weight: normal; 30 | } 31 | 32 | a:nth-child(5), 33 | a.\:nth-child\(5\) { 34 | border: 1px solid papayawhip; 35 | } 36 | 37 | a:hover:focus, 38 | a:hover.\:focus, 39 | a.\:hover:focus, 40 | a.\:hover.\:focus { 41 | font-weight: bold; 42 | } 43 | 44 | a:before { 45 | color: white; 46 | } 47 | 48 | a::before { 49 | color: white; 50 | } 51 | 52 | a:before, 53 | a::before { 54 | color: white; 55 | } 56 | 57 | a:hover::before, 58 | a.\:hover::before { 59 | border-top-color: blue; 60 | } 61 | 62 | a:active:focus:hover::before, 63 | a:active:focus.\:hover::before, 64 | a:active.\:focus:hover::before, 65 | a:active.\:focus.\:hover::before, 66 | a.\:active:focus:hover::before, 67 | a.\:active:focus.\:hover::before, 68 | a.\:active.\:focus:hover::before, 69 | a.\:active.\:focus.\:hover::before { 70 | border-bottom-color: blue; 71 | } 72 | 73 | a:active:focus + div:hover, 74 | a:active.\:focus + div:hover, 75 | a.\:active:focus + div:hover, 76 | a.\:active.\:focus + div:hover, 77 | a:active:focus + div.\:hover, 78 | a:active.\:focus + div.\:hover, 79 | a.\:active:focus + div.\:hover, 80 | a.\:active.\:focus + div.\:hover { 81 | color: magenta; 82 | } 83 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | /* eslint-env es6 */ 2 | 3 | const postcss = require('postcss'); 4 | const readFileSync = require('fs').readFileSync; 5 | const joinPath = require('path').join; 6 | const test = require('ava'); 7 | 8 | const plugin = require('../'); 9 | 10 | const read = fileName => readFileSync(joinPath(__dirname, fileName), 'utf-8'); 11 | const inCSS = read('./fixtures/pseudos.css'); 12 | 13 | function run(t, input, output, opts = {}) { 14 | return postcss([plugin(opts)]) 15 | .process(input.trim(), { from: undefined }) 16 | .then(result => { 17 | t.is(result.css, output.trim()); 18 | t.is(result.warnings().length, 0); 19 | }); 20 | } 21 | 22 | test('should add proper pseudoclass selectors', t => { 23 | const expectedOut = read('./fixtures/pseudos.out.css'); 24 | return run(t, inCSS, expectedOut, {}); 25 | }); 26 | 27 | test( 28 | 'should add all combinations (slower) of pseudoclass selectors ' + 29 | 'if the `allCombinations` option is set to `true`', 30 | t => { 31 | const expectedOut = read('./fixtures/pseudos-combinations.out.css'); 32 | return run(t, inCSS, expectedOut, { allCombinations: true }); 33 | } 34 | ); 35 | 36 | test('should add pseudoclass selectors from a list and ignore the rest', t => { 37 | const input = read('./fixtures/pseudos-restricted.css'); 38 | const expectedOut = read('./fixtures/pseudos-restricted.out.css'); 39 | const restrictTo = ['nth-child', ':hover', 'active']; 40 | return run(t, input, expectedOut, { allCombinations: true, restrictTo }); 41 | }); 42 | 43 | test('should ignore pseudoclass selections in the blacklist', t => { 44 | const input = read('./fixtures/blacklist.css'); 45 | const expectedOut = read('./fixtures/blacklist.out.css'); 46 | return run(t, input, expectedOut, { allCombinations: true }); 47 | }); 48 | 49 | test('optional prefixes', t => { 50 | const expectedOut = read('./fixtures/prefix.out.css'); 51 | 52 | return run(t, inCSS, expectedOut, { prefix: 'pseudo-class-' }); 53 | }); 54 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PostCSS Pseudo Classes 2 | 3 | [PostCSS] plugin to automatically add in companion classes 4 | where pseudo-selectors are used. 5 | This allows you to add the class name to force the styling of a pseudo-selector, 6 | which can be really helpful for testing or being able 7 | to concretely reach all style states. 8 | 9 | Example: 10 | 11 | ```css 12 | .some-selector:hover { 13 | text-decoration: underline; 14 | } 15 | ``` 16 | 17 | yields 18 | 19 | ```css 20 | .some-selector:hover, 21 | .some-selector.\:hover { 22 | text-decoration: underline; 23 | } 24 | ``` 25 | 26 | [PostCSS]: https://github.com/postcss/postcss 27 | 28 | ### Credits 29 | 30 | This plugin is a port of [rework-pseudo-classes](https://github.com/SlexAxton/rework-pseudo-classes) written by [Alex Sexton](https://twitter.com/SlexAxton). 31 | 32 | ## Installation 33 | 34 | ```bash 35 | $ npm install postcss-pseudo-classes 36 | ``` 37 | 38 | ## Options 39 | 40 | ```js 41 | require('postcss-pseudo-classes')({ 42 | // default contains `:root`. 43 | blacklist: [], 44 | 45 | // (optional) create classes for a restricted list of selectors 46 | // N.B. the colon (:) is optional 47 | restrictTo: [':nth-child', 'hover'], 48 | 49 | // default is `false`. If `true`, will output CSS 50 | // with all combinations of pseudo styles/pseudo classes. 51 | allCombinations: true, 52 | 53 | // default is `true`. If `false`, will generate 54 | // pseudo classes for `:before` and `:after` 55 | preserveBeforeAfter: false 56 | 57 | // default is `\:`. It will be added to pseudo classes. 58 | prefix: '\\:' 59 | }); 60 | ``` 61 | 62 | ## Edge cases 63 | 64 | * This plugin escapes parenthesis so `:nth-child(5)` would look like `.class.\:nth-child\(5\)` and can be used as a regular class: ``. 65 | * Pseudo-selectors with two colons are ignored entirely since they're a slightly different thing. 66 | * Chained pseudo-selectors just become chained classes: `:focus:hover` becomes `.\:focus.\:hover`. 67 | 68 | ## Tests 69 | 70 | ```bash 71 | $ npm test 72 | ``` 73 | 74 | ## Contributors 75 | 76 | [@ai](https://github.com/ai) 77 | 78 | ## License 79 | 80 | (MIT) 81 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var plugin = function (options) { 2 | options = options || {}; 3 | options.preserveBeforeAfter = options.preserveBeforeAfter || true; 4 | 5 | // Backwards compatibility--we always by default ignored `:root`. 6 | var blacklist = { 7 | ':root': true, 8 | ':host': true, 9 | ':host-context': true 10 | }; 11 | 12 | var prefix = options.prefix || '\\:'; 13 | 14 | (options.blacklist || []).forEach(function (blacklistItem) { 15 | blacklist[blacklistItem] = true; 16 | }); 17 | 18 | var restrictTo; 19 | 20 | if (Array.isArray(options.restrictTo) && options.restrictTo.length) { 21 | restrictTo = options.restrictTo.reduce(function (target, pseudoClass) { 22 | var finalClass = 23 | (pseudoClass.charAt(0) === ':' ? '' : ':') + 24 | pseudoClass.replace(/\(.*/g, ''); 25 | if (!Object.prototype.hasOwnProperty.call(target, finalClass)) { 26 | target[finalClass] = true; 27 | } 28 | return target; 29 | }, {}); 30 | } 31 | 32 | return { 33 | postcssPlugin: 'postcss-pseudo-classes', 34 | prepare: function () { 35 | var fixed = []; 36 | return { 37 | Rule: function (rule) { 38 | if (fixed.indexOf(rule) !== -1) { 39 | return; 40 | } 41 | fixed.push(rule); 42 | 43 | var combinations; 44 | 45 | rule.selectors.forEach(function (selector) { 46 | // Ignore some popular things that are never useful 47 | if (blacklist[selector]) { 48 | return; 49 | } 50 | 51 | if (!selector.includes(':')) { 52 | return; 53 | } 54 | 55 | var selectorParts = selector.split(' '); 56 | var pseudoedSelectorParts = []; 57 | 58 | selectorParts.forEach(function (selectorPart, index) { 59 | var pseudos = selectorPart.match(/::?([^:]+)/g); 60 | 61 | if (!pseudos) { 62 | if (options.allCombinations) { 63 | pseudoedSelectorParts[index] = [selectorPart]; 64 | } else { 65 | pseudoedSelectorParts.push(selectorPart); 66 | } 67 | return; 68 | } 69 | 70 | var baseSelector = selectorPart.substr( 71 | 0, 72 | selectorPart.length - pseudos.join('').length 73 | ); 74 | 75 | var classPseudos = pseudos.map(function (pseudo) { 76 | var pseudoToCheck = pseudo.replace(/\(.*/g, ''); 77 | // restrictTo a subset of pseudo classes 78 | if ( 79 | blacklist[pseudoToCheck] || 80 | (restrictTo && !restrictTo[pseudoToCheck]) 81 | ) { 82 | return pseudo; 83 | } 84 | 85 | // Ignore pseudo-elements! 86 | if (pseudo.match(/^::/)) { 87 | return pseudo; 88 | } 89 | 90 | // Ignore ':before' and ':after' 91 | if ( 92 | options.preserveBeforeAfter && 93 | [':before', ':after'].indexOf(pseudo) !== -1 94 | ) { 95 | return pseudo; 96 | } 97 | 98 | // Kill the colon 99 | pseudo = pseudo.substr(1); 100 | 101 | // Replace left and right parens 102 | pseudo = pseudo.replace(/\(/g, '\\('); 103 | pseudo = pseudo.replace(/\)/g, '\\)'); 104 | 105 | return '.' + prefix + pseudo; 106 | }); 107 | 108 | // Add all combinations of pseudo selectors/pseudo styles given a 109 | // selector with multiple pseudo styles. 110 | if (options.allCombinations) { 111 | combinations = createCombinations(pseudos, classPseudos); 112 | pseudoedSelectorParts[index] = []; 113 | 114 | combinations.forEach(function (combination) { 115 | pseudoedSelectorParts[index].push(baseSelector + combination); 116 | }); 117 | } else { 118 | pseudoedSelectorParts.push( 119 | baseSelector + classPseudos.join('') 120 | ); 121 | } 122 | }); 123 | 124 | if (options.allCombinations) { 125 | var serialCombinations = createSerialCombinations( 126 | pseudoedSelectorParts, 127 | appendWithSpace 128 | ); 129 | 130 | serialCombinations.forEach(function (combination) { 131 | addSelector(combination); 132 | }); 133 | } else { 134 | addSelector(pseudoedSelectorParts.join(' ')); 135 | } 136 | 137 | function addSelector(newSelector) { 138 | if (newSelector && newSelector !== selector) { 139 | rule.selector += ',\n' + newSelector; 140 | } 141 | } 142 | }); 143 | } 144 | }; 145 | } 146 | }; 147 | }; 148 | plugin.postcss = true; 149 | 150 | // a.length === b.length 151 | function createCombinations(a, b) { 152 | var combinations = ['']; 153 | var newCombinations; 154 | for (var i = 0, len = a.length; i < len; i += 1) { 155 | newCombinations = []; 156 | combinations.forEach(function (combination) { 157 | newCombinations.push(combination + a[i]); 158 | // Don't repeat work. 159 | if (a[i] !== b[i]) { 160 | newCombinations.push(combination + b[i]); 161 | } 162 | }); 163 | combinations = newCombinations; 164 | } 165 | return combinations; 166 | } 167 | 168 | // arr = [[list of 1st el], [list of 2nd el] ... etc] 169 | function createSerialCombinations(arr, fn) { 170 | var combinations = ['']; 171 | var newCombinations; 172 | arr.forEach(function (elements) { 173 | newCombinations = []; 174 | elements.forEach(function (element) { 175 | combinations.forEach(function (combination) { 176 | newCombinations.push(fn(combination, element)); 177 | }); 178 | }); 179 | combinations = newCombinations; 180 | }); 181 | return combinations; 182 | } 183 | 184 | function appendWithSpace(a, b) { 185 | if (a) { 186 | a += ' '; 187 | } 188 | return a + b; 189 | } 190 | 191 | module.exports = plugin; 192 | --------------------------------------------------------------------------------