├── test ├── imports │ ├── variables.css │ ├── import-1.css │ ├── mixins.css │ └── scss.scss ├── unresolved.css ├── unresolved.expect.css ├── import-variables.expect.css ├── unresolved-include.css ├── default.expect.css ├── default.var.expect.css ├── unresolved-include.expect.css ├── default.var-func.expect.css ├── import-mixins.css ├── default.css ├── import-variables.css ├── imports-media.css ├── imports-media.expect.css ├── imports-scss.scss ├── import-mixins.expect.css ├── imports-alt.css ├── scss.expect.scss ├── scss.result.scss ├── scss.scss ├── imports-alt.expect.css ├── imports.css ├── property.expect.css ├── imports.expect.css ├── atrules.expect.css ├── imports.no-from.expect.css ├── imports-scss.expect.scss ├── iterators.css ├── property.css ├── atrules.css ├── iterators.expect.css ├── conditionals.expect.css ├── mixins.expect.css ├── conditionals.disable-else.expect.css ├── variables.expect.css ├── mixed.css ├── mixins.css ├── variables.css ├── conditionals.disable-if.expect.css ├── conditionals.disable.expect.css ├── conditionals.css └── mixed.expect.css ├── src ├── lib │ ├── waterfall.js │ ├── manage-unresolved.js │ ├── transform-rule.js │ ├── transform-atrule.js │ ├── set-variable.js │ ├── transform-decl.js │ ├── get-closest-variable.js │ ├── transform-content-atrule.js │ ├── get-value-as-object.js │ ├── transform-mixin-atrule.js │ ├── get-replaced-string.js │ ├── transform-for-atrule.js │ ├── transform-include-atrule.js │ ├── transform-each-atrule.js │ ├── transform-if-atrule.js │ ├── transform-node.js │ └── transform-import-atrule.js └── index.js ├── .rollup.mjs ├── .gitignore ├── .editorconfig ├── .github └── workflows │ └── test.yml ├── package.json ├── CONTRIBUTING.md ├── CHANGELOG.md ├── .tape.js ├── LICENSE.md └── README.md /test/imports/variables.css: -------------------------------------------------------------------------------- 1 | $pass: "pass"; 2 | -------------------------------------------------------------------------------- /test/imports/import-1.css: -------------------------------------------------------------------------------- 1 | .import-1 { 2 | content: $pass; 3 | } 4 | -------------------------------------------------------------------------------- /test/unresolved.css: -------------------------------------------------------------------------------- 1 | .test { 2 | content: $unresolved; 3 | } 4 | -------------------------------------------------------------------------------- /test/unresolved.expect.css: -------------------------------------------------------------------------------- 1 | .test { 2 | content: $unresolved; 3 | } 4 | -------------------------------------------------------------------------------- /test/import-variables.expect.css: -------------------------------------------------------------------------------- 1 | .import-1 { 2 | content: "pass"; 3 | } 4 | -------------------------------------------------------------------------------- /test/unresolved-include.css: -------------------------------------------------------------------------------- 1 | .test { 2 | @include @test-unresolved; 3 | } 4 | -------------------------------------------------------------------------------- /test/default.expect.css: -------------------------------------------------------------------------------- 1 | .test-default { 2 | content: "default-value"; 3 | } 4 | -------------------------------------------------------------------------------- /test/default.var.expect.css: -------------------------------------------------------------------------------- 1 | .test-default { 2 | content: custom-value; 3 | } 4 | -------------------------------------------------------------------------------- /test/unresolved-include.expect.css: -------------------------------------------------------------------------------- 1 | .test { 2 | @include @test-unresolved; 3 | } 4 | -------------------------------------------------------------------------------- /test/default.var-func.expect.css: -------------------------------------------------------------------------------- 1 | .test-default { 2 | content: custom-fn-value; 3 | } 4 | -------------------------------------------------------------------------------- /test/import-mixins.css: -------------------------------------------------------------------------------- 1 | @import 'imports/mixins'; 2 | 3 | .test-1 { 4 | @include mixin-test-1; 5 | } 6 | -------------------------------------------------------------------------------- /test/default.css: -------------------------------------------------------------------------------- 1 | $default: "default-value" !default; 2 | 3 | .test-default { 4 | content: $default; 5 | } 6 | -------------------------------------------------------------------------------- /test/import-variables.css: -------------------------------------------------------------------------------- 1 | @import 'imports/variables.css'; 2 | 3 | .import-1 { 4 | content: $pass; 5 | } 6 | -------------------------------------------------------------------------------- /test/imports-media.css: -------------------------------------------------------------------------------- 1 | $pass: "pass"; 2 | 3 | @import "imports/import-1" screen and (orientation:landscape); 4 | -------------------------------------------------------------------------------- /test/imports-media.expect.css: -------------------------------------------------------------------------------- 1 | @media screen and (orientation:landscape) {.import-1 { 2 | content: "pass"; 3 | } 4 | } 5 | -------------------------------------------------------------------------------- /test/imports-scss.scss: -------------------------------------------------------------------------------- 1 | $pass: "pass"; 2 | 3 | @import "imports/scss"; 4 | 5 | .imported { 6 | content: "$(name)"; 7 | } 8 | -------------------------------------------------------------------------------- /test/import-mixins.expect.css: -------------------------------------------------------------------------------- 1 | .test-1 { 2 | -webkit-border-radius: 1em; 3 | -moz-border-radius: 1em; 4 | -ms-border-radius: 1em; 5 | border-radius: 1em; 6 | } 7 | -------------------------------------------------------------------------------- /test/imports/mixins.css: -------------------------------------------------------------------------------- 1 | @mixin mixin-test-1 { 2 | -webkit-border-radius: 1em; 3 | -moz-border-radius: 1em; 4 | -ms-border-radius: 1em; 5 | border-radius: 1em; 6 | } 7 | -------------------------------------------------------------------------------- /test/imports-alt.css: -------------------------------------------------------------------------------- 1 | $pass: "pass"; 2 | 3 | @import "import-1"; 4 | @import 'import-1'; 5 | @import url("import-1"); 6 | @import url('import-1'); 7 | @import url(import-1); 8 | -------------------------------------------------------------------------------- /test/scss.expect.scss: -------------------------------------------------------------------------------- 1 | .is-color-green { 2 | color: green; 3 | } 4 | 5 | .is-color-green { 6 | color: green; 7 | } 8 | 9 | .is-color-green { 10 | color: green; 11 | } 12 | -------------------------------------------------------------------------------- /test/scss.result.scss: -------------------------------------------------------------------------------- 1 | .is-color-green { 2 | color: green; 3 | } 4 | 5 | .is-color-green { 6 | color: green; 7 | } 8 | 9 | .is-color-green { 10 | color: green; 11 | } 12 | -------------------------------------------------------------------------------- /src/lib/waterfall.js: -------------------------------------------------------------------------------- 1 | export default (items, asyncFunction) => items.reduce( 2 | (lastPromise, item) => lastPromise.then( 3 | () => asyncFunction(item) 4 | ), 5 | Promise.resolve() 6 | ) 7 | -------------------------------------------------------------------------------- /.rollup.mjs: -------------------------------------------------------------------------------- 1 | export default { 2 | input: 'src/index.js', 3 | output: [ 4 | { file: 'index.js', format: 'cjs', exports: 'auto' }, 5 | { file: 'index.mjs', format: 'esm', exports: 'auto' } 6 | ], 7 | }; 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.log* 3 | *.result.css 4 | *.result.scss 5 | .* 6 | !.editorconfig 7 | !.gitignore 8 | !.rollup.mjs 9 | !.rollup.js 10 | !.tape.js 11 | !.travis.yml 12 | /index.js 13 | /index.mjs 14 | -------------------------------------------------------------------------------- /test/scss.scss: -------------------------------------------------------------------------------- 1 | $color: green !default; 2 | 3 | .is-color-$color { 4 | color: $color; 5 | } 6 | 7 | .is-color-$(color) { 8 | color: $(color); 9 | } 10 | 11 | .is-color-#{$color} { 12 | color: #{$color}; 13 | } 14 | -------------------------------------------------------------------------------- /test/imports-alt.expect.css: -------------------------------------------------------------------------------- 1 | .import-1 { 2 | content: "pass"; 3 | } 4 | .import-1 { 5 | content: "pass"; 6 | } 7 | .import-1 { 8 | content: "pass"; 9 | } 10 | .import-1 { 11 | content: "pass"; 12 | } 13 | .import-1 { 14 | content: "pass"; 15 | } 16 | -------------------------------------------------------------------------------- /test/imports/scss.scss: -------------------------------------------------------------------------------- 1 | @import "./mixins"; 2 | 3 | $name: name; 4 | 5 | .scss { 6 | .nested { 7 | // inline comment 8 | content: $pass; 9 | } 10 | 11 | .#{$name} { 12 | content: $pass; 13 | 14 | .mixin { 15 | @include mixin-test-1; 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /test/imports.css: -------------------------------------------------------------------------------- 1 | $pass: "pass"; 2 | 3 | @import "imports/import-1"; 4 | @import 'imports/import-1'; 5 | @import url("imports/import-1"); 6 | @import url('imports/import-1'); 7 | @import url(imports/import-1); 8 | @import url(https://necolas.github.io/normalize.css/7.0.0/normalize.css); 9 | -------------------------------------------------------------------------------- /test/property.expect.css: -------------------------------------------------------------------------------- 1 | --foo-replacement: "test-1"; 2 | --foo-replacement2: bar; 3 | --foo-bar: foo; 4 | 5 | --each-foo: foo; 6 | 7 | --each-bar: bar; 8 | 9 | --each-baz: baz; 10 | 11 | .class { 12 | --custom-property: black; 13 | color: var(--custom-property) 14 | } -------------------------------------------------------------------------------- /src/lib/manage-unresolved.js: -------------------------------------------------------------------------------- 1 | export default function manageUnresolved(node, opts, word, message) { 2 | if ('warn' === opts.unresolved) { 3 | node.warn(opts.result, message, { word }); 4 | } else if ('ignore' !== opts.unresolved) { 5 | throw node.error(message, { word }); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | indent_style = tab 7 | insert_final_newline = true 8 | trim_trailing_whitespace = true 9 | 10 | [*.md] 11 | trim_trailing_whitespace = false 12 | 13 | [*.{json,md,yml}] 14 | indent_size = 2 15 | indent_style = space 16 | -------------------------------------------------------------------------------- /test/imports.expect.css: -------------------------------------------------------------------------------- 1 | .import-1 { 2 | content: "pass"; 3 | } 4 | .import-1 { 5 | content: "pass"; 6 | } 7 | .import-1 { 8 | content: "pass"; 9 | } 10 | .import-1 { 11 | content: "pass"; 12 | } 13 | .import-1 { 14 | content: "pass"; 15 | } 16 | @import url(https://necolas.github.io/normalize.css/7.0.0/normalize.css); 17 | -------------------------------------------------------------------------------- /test/atrules.expect.css: -------------------------------------------------------------------------------- 1 | .is-color-green { 2 | color: green; 3 | } 4 | 5 | @media (min-width: 30em), handheld and (orientation: landscape) { 6 | 7 | .is-color-green { 8 | color: green; 9 | } 10 | } 11 | 12 | @keyframes fadeIn {} 13 | @-webkit-keyframes fadeIn {} 14 | @-moz-keyframes fadeIn {} 15 | @-o-keyframes fadeIn {} 16 | -------------------------------------------------------------------------------- /test/imports.no-from.expect.css: -------------------------------------------------------------------------------- 1 | .import-1 { 2 | content: "pass"; 3 | } 4 | .import-1 { 5 | content: "pass"; 6 | } 7 | .import-1 { 8 | content: "pass"; 9 | } 10 | .import-1 { 11 | content: "pass"; 12 | } 13 | .import-1 { 14 | content: "pass"; 15 | } 16 | @import url(https://necolas.github.io/normalize.css/7.0.0/normalize.css); 17 | -------------------------------------------------------------------------------- /src/lib/transform-rule.js: -------------------------------------------------------------------------------- 1 | // tooling 2 | import getReplacedString from './get-replaced-string'; 3 | 4 | // transform rule nodes 5 | export default function transformRule(rule, opts) { 6 | // update the rule selector with its variables replaced by their corresponding values 7 | rule.selector = getReplacedString(rule.selector, rule, opts); 8 | } 9 | -------------------------------------------------------------------------------- /src/lib/transform-atrule.js: -------------------------------------------------------------------------------- 1 | // tooling 2 | import getReplacedString from './get-replaced-string'; 3 | 4 | // transform generic at-rules 5 | export default function transformAtrule(rule, opts) { 6 | // update the at-rule params with its variables replaced by their corresponding values 7 | rule.params = getReplacedString(rule.params, rule, opts); 8 | } 9 | -------------------------------------------------------------------------------- /test/imports-scss.expect.scss: -------------------------------------------------------------------------------- 1 | .scss { 2 | .nested { 3 | // inline comment 4 | content: "pass"; 5 | } 6 | 7 | .name { 8 | content: "pass"; 9 | 10 | .mixin { 11 | -webkit-border-radius: 1em; 12 | -moz-border-radius: 1em; 13 | -ms-border-radius: 1em; 14 | border-radius: 1em; 15 | } 16 | } 17 | } 18 | 19 | .imported { 20 | content: "name"; 21 | } 22 | -------------------------------------------------------------------------------- /test/iterators.css: -------------------------------------------------------------------------------- 1 | @for $i from 1 to 5 { 2 | .for-$(i)-from-1-to-5 { 3 | content: "$i"; 4 | } 5 | } 6 | 7 | @for $i from 1 to 5 by 2 { 8 | .for-$(i)-from-1-to-5-by-2 { 9 | content: "$i"; 10 | } 11 | } 12 | 13 | @for $i from 6 to 1 by 2 { 14 | .for-$(i)-from-6-to-1-by-2 { 15 | content: "$i"; 16 | } 17 | } 18 | 19 | @each $i in (foo, bar, baz) { 20 | .each-$(i)-in(foo,bar,baz) { 21 | content: $i; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /test/property.css: -------------------------------------------------------------------------------- 1 | $test-var-1: foo; 2 | $test-var-2: bar; 3 | $test-var-3: (foo, bar, baz); 4 | $test-var-4: custom-property; 5 | $test-var-5: black; 6 | 7 | --$(test-var-1)-replacement: "test-1"; 8 | --$(test-var-1)-replacement2: $test-var-2; 9 | --$(test-var-1)-$(test-var-2): $test-var-1; 10 | 11 | @each $i in $test-var-3 { 12 | --each-$(i): $i; 13 | } 14 | 15 | .class { 16 | --$(test-var-4): $test-var-5; 17 | color: var(--$(test-var-4)) 18 | } -------------------------------------------------------------------------------- /test/atrules.css: -------------------------------------------------------------------------------- 1 | $color: green !default; 2 | 3 | .is-color-green { 4 | color: $color; 5 | } 6 | 7 | $min-width: 30em; 8 | $orientation: landscape; 9 | 10 | @media (min-width: $min-width), handheld and (orientation: $orientation) { 11 | $color: white !default; 12 | 13 | .is-color-green { 14 | color: $color; 15 | } 16 | } 17 | 18 | $animation-name: fadeIn; 19 | 20 | @keyframes $animation-name {} 21 | @-webkit-keyframes $animation-name {} 22 | @-moz-keyframes $animation-name {} 23 | @-o-keyframes $animation-name {} 24 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | full: 11 | name: Node.js Test Suite 12 | runs-on: ubuntu-latest 13 | 14 | strategy: 15 | matrix: 16 | node-version: [18, '*'] 17 | 18 | steps: 19 | - name: Checkout the repository 20 | uses: actions/checkout@v2 21 | - name: Install Node.js ${{ matrix.node-version }} 22 | uses: actions/setup-node@v2 23 | with: 24 | node-version: ${{ matrix.node-version }} 25 | - name: Install dependencies 26 | run: npm install 27 | - name: Run tests 28 | run: npm test 29 | -------------------------------------------------------------------------------- /src/lib/set-variable.js: -------------------------------------------------------------------------------- 1 | // tooling 2 | import getClosestVariable from './get-closest-variable'; 3 | 4 | // set a variable on a node 5 | export default function setVariable(node, name, value, opts) { 6 | // if the value is not a default with a value already defined 7 | if (!matchDefault.test(value) || getClosestVariable(name, node, opts) === undefined) { 8 | // the value without a default suffix 9 | const undefaultedValue = matchDefault.test(value) 10 | ? value.replace(matchDefault, '') 11 | : value; 12 | 13 | // ensure the node has a variables object 14 | node.variables = node.variables || {}; 15 | 16 | // set the variable 17 | node.variables[name] = undefaultedValue; 18 | } 19 | } 20 | 21 | // match anything ending with a valid !default 22 | const matchDefault = /\s+!default$/; 23 | -------------------------------------------------------------------------------- /test/iterators.expect.css: -------------------------------------------------------------------------------- 1 | 2 | .for-1-from-1-to-5 { 3 | content: "1"; 4 | } 5 | .for-2-from-1-to-5 { 6 | content: "2"; 7 | } 8 | .for-3-from-1-to-5 { 9 | content: "3"; 10 | } 11 | .for-4-from-1-to-5 { 12 | content: "4"; 13 | } 14 | .for-5-from-1-to-5 { 15 | content: "5"; 16 | } 17 | 18 | .for-1-from-1-to-5-by-2 { 19 | content: "1"; 20 | } 21 | 22 | .for-3-from-1-to-5-by-2 { 23 | content: "3"; 24 | } 25 | 26 | .for-5-from-1-to-5-by-2 { 27 | content: "5"; 28 | } 29 | 30 | .for-6-from-6-to-1-by-2 { 31 | content: "6"; 32 | } 33 | 34 | .for-4-from-6-to-1-by-2 { 35 | content: "4"; 36 | } 37 | 38 | .for-2-from-6-to-1-by-2 { 39 | content: "2"; 40 | } 41 | 42 | .each-foo-in(foo,bar,baz) { 43 | content: foo; 44 | } 45 | 46 | .each-bar-in(foo,bar,baz) { 47 | content: bar; 48 | } 49 | 50 | .each-baz-in(foo,bar,baz) { 51 | content: baz; 52 | } 53 | -------------------------------------------------------------------------------- /test/conditionals.expect.css: -------------------------------------------------------------------------------- 1 | 2 | .is-true-true { 3 | content: "pass"; 4 | } .is-false-false { 5 | content: "pass"; 6 | } 7 | 8 | .is-true-variable-true { 9 | content: "pass"; 10 | } 11 | 12 | .is-1-equal-to-1 { 13 | content: "pass"; 14 | } .is-1-not-equal-to-2 { 15 | content: "pass"; 16 | } 17 | 18 | .is-1-not-equal-to-2 { 19 | content: "pass"; 20 | } 21 | 22 | .is-1-less-than-2 { 23 | content: "pass"; 24 | } 25 | 26 | .is-1-greater-than-0 { 27 | content: "pass"; 28 | } 29 | 30 | .is-1-less-than-or-equal-to-1 { 31 | content: "pass"; 32 | } 33 | 34 | .is-1-less-than-or-equal-to-2 { 35 | content: "pass"; 36 | } 37 | 38 | .is-1-less-than-or-equal-to-0 { 39 | content: "pass"; 40 | } 41 | 42 | .is-1-less-than-or-equal-to-1 { 43 | content: "pass"; 44 | } .is-number-var-not-equal-to-1 { 45 | content: "pass"; 46 | } 47 | 48 | .is-number-equal-to-5 { 49 | content: "pass"; 50 | } 51 | -------------------------------------------------------------------------------- /src/lib/transform-decl.js: -------------------------------------------------------------------------------- 1 | // tooling 2 | import getReplacedString from './get-replaced-string'; 3 | import setVariable from './set-variable'; 4 | 5 | // transform declarations 6 | export default function transformDecl(decl, opts) { 7 | // update the declaration value with its variables replaced by their corresponding values 8 | decl.value = getReplacedString(decl.value, decl, opts); 9 | 10 | 11 | // if the declaration is a variable declaration 12 | if (isVariableDeclaration(decl)) { 13 | // set the variable on the parent of the declaration 14 | setVariable(decl.parent, decl.prop.slice(1), decl.value, opts); 15 | 16 | // remove the declaration 17 | decl.remove(); 18 | } else { 19 | decl.prop = getReplacedString(decl.prop, decl, opts); 20 | } 21 | } 22 | 23 | // return whether the declaration property is a variable declaration 24 | const isVariableDeclaration = decl => matchVariable.test(decl.prop); 25 | 26 | // match a variable ($any-name) 27 | const matchVariable = /^\$[\w-]+$/; 28 | -------------------------------------------------------------------------------- /test/mixins.expect.css: -------------------------------------------------------------------------------- 1 | .test-1 { 2 | -webkit-border-radius: 1em; 3 | -moz-border-radius: 1em; 4 | -ms-border-radius: 1em; 5 | border-radius: 1em; 6 | } 7 | 8 | .test-2 { 9 | -webkit-border-radius: 1em; 10 | -moz-border-radius: 1em; 11 | -ms-border-radius: 1em; 12 | border-radius: 1em; 13 | } 14 | 15 | .test-3a { 16 | -webkit-border-radius: 1em; 17 | -moz-border-radius: 1em; 18 | -ms-border-radius: 1em; 19 | border-radius: 1em; 20 | } 21 | 22 | .test-3b { 23 | -webkit-border-radius: 2em; 24 | -moz-border-radius: 2em; 25 | -ms-border-radius: 2em; 26 | border-radius: 2em; 27 | } 28 | 29 | .test-4a { 30 | -webkit-border-radius: 1em; 31 | -moz-border-radius: 1em; 32 | -ms-border-radius: 1em; 33 | border-radius: 1em; 34 | } 35 | 36 | .test-4b { 37 | -webkit-border-radius: 2em; 38 | -moz-border-radius: 2em; 39 | -ms-border-radius: 2em; 40 | border-radius: 2em; 41 | } 42 | 43 | .test-5a { 44 | @media (min-width: 30em) { 45 | min-width: 30em; 46 | } 47 | } 48 | 49 | .test-5b { 50 | @media (min-width: 60em) { 51 | min-width: 60em; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/lib/get-closest-variable.js: -------------------------------------------------------------------------------- 1 | // return the closest variable from a node 2 | export default function getClosestVariable(name, node, opts) { 3 | const variables = getVariables(node); 4 | 5 | let variable = variables[name]; 6 | 7 | if (requiresAncestorVariable(variable, node)) { 8 | variable = getClosestVariable(name, node.parent, opts); 9 | } 10 | 11 | if (requiresFnVariable(variable, opts)) { 12 | variable = getFnVariable(name, node, opts.variables); 13 | } 14 | 15 | return variable; 16 | } 17 | 18 | // return the variables object of a node 19 | const getVariables = node => Object(Object(node).variables); 20 | 21 | // return whether the variable should be replaced using an ancestor variable 22 | const requiresAncestorVariable = (variable, node) => undefined === variable && node && node.parent; 23 | 24 | // return whether variable should be replaced using a variables function 25 | const requiresFnVariable = (value, opts) => value === undefined && Object(opts).variables === Object(Object(opts).variables); 26 | 27 | // return whether variable should be replaced using a variables function 28 | const getFnVariable = (name, node, variables) => 'function' === typeof variables 29 | ? variables(name, node) 30 | : variables[name]; 31 | -------------------------------------------------------------------------------- /src/lib/transform-content-atrule.js: -------------------------------------------------------------------------------- 1 | // tooling 2 | import manageUnresolved from './manage-unresolved'; 3 | import transformNode from './transform-node'; 4 | 5 | // transform @content at-rules 6 | export default function transformContentAtrule(rule, opts) { 7 | // if @content is supported 8 | if (opts.transform.includes('@content')) { 9 | // the closest @mixin at-rule 10 | const mixin = getClosestMixin(rule); 11 | 12 | // if the @mixin at-rule exists 13 | if (mixin) { 14 | // clone the @mixin at-rule 15 | const clone = mixin.original.clone({ 16 | parent: rule.parent, 17 | variables: rule.variables 18 | }); 19 | 20 | // transform the clone children 21 | return transformNode(clone, opts).then(() => { 22 | // replace the @content at-rule with the clone children 23 | rule.parent.insertBefore(rule, clone.nodes); 24 | 25 | rule.remove(); 26 | }) 27 | 28 | } else { 29 | // otherwise, if the @mixin at-rule does not exist 30 | manageUnresolved(rule, opts, '@content', 'Could not resolve the mixin for @content'); 31 | } 32 | } 33 | } 34 | 35 | // return the closest @mixin at-rule 36 | const getClosestMixin = node => 'atrule' === node.type && 'mixin' === node.name 37 | ? node 38 | : node.parent && getClosestMixin(node.parent); 39 | -------------------------------------------------------------------------------- /test/conditionals.disable-else.expect.css: -------------------------------------------------------------------------------- 1 | 2 | .is-true-true { 3 | content: "pass"; 4 | } @else { 5 | .is-true-false { 6 | content: "fail"; 7 | } 8 | } @else { 9 | .is-false-false { 10 | content: "pass"; 11 | } 12 | } 13 | 14 | .is-true-variable-true { 15 | content: "pass"; 16 | } @else { 17 | .is-true-variable-false { 18 | content: "pass"; 19 | } 20 | } 21 | 22 | .is-1-equal-to-1 { 23 | content: "pass"; 24 | } @else { 25 | .is-1-not-equal-to-1 { 26 | content: "fail"; 27 | } 28 | } @else { 29 | .is-1-not-equal-to-2 { 30 | content: "pass"; 31 | } 32 | } 33 | 34 | .is-1-not-equal-to-2 { 35 | content: "pass"; 36 | } 37 | 38 | .is-1-less-than-2 { 39 | content: "pass"; 40 | } 41 | 42 | .is-1-greater-than-0 { 43 | content: "pass"; 44 | } 45 | 46 | .is-1-less-than-or-equal-to-1 { 47 | content: "pass"; 48 | } 49 | 50 | .is-1-less-than-or-equal-to-2 { 51 | content: "pass"; 52 | } 53 | 54 | .is-1-less-than-or-equal-to-0 { 55 | content: "pass"; 56 | } 57 | 58 | .is-1-less-than-or-equal-to-1 { 59 | content: "pass"; 60 | } @else { 61 | .is-number-var-not-equal-to-1 { 62 | content: "pass"; 63 | } 64 | } 65 | 66 | .is-number-equal-to-5 { 67 | content: "pass"; 68 | } @else { 69 | .is-number-not-equal-to-5 { 70 | content: "fail"; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /test/variables.expect.css: -------------------------------------------------------------------------------- 1 | .test-var-1 { 2 | font-family: "Helvetica Neue"; 3 | font-family: "Helvetica Neue" 4 | } 5 | 6 | .test-var-2 { 7 | font-family: ("Helvetica Neue"); 8 | font-family: "Helvetica Neue" 9 | } 10 | 11 | .test-var-3 { 12 | font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; 13 | font-family: "Helvetica Neue"; 14 | font-family: Helvetica; 15 | font-family: Arial; 16 | font-family: sans-serif 17 | } 18 | 19 | .test-var-4 { 20 | font-family: ("Helvetica Neue", Helvetica, Arial, sans-serif); 21 | font-family: "Helvetica Neue"; 22 | font-family: Helvetica; 23 | font-family: Arial; 24 | font-family: sans-serif 25 | } 26 | 27 | .test-var-5 { 28 | font-family: ("Helvetica Neue", (Helvetica, Arial), sans-serif); 29 | font-family: "Helvetica Neue"; 30 | font-family: (Helvetica,Arial); 31 | font-family: sans-serif 32 | } 33 | 34 | .test-var-6 { 35 | font-family: (foo: "Helvetica Neue", bar: Helvetica, baz: Arial, qux: sans-serif); 36 | font-family: "Helvetica Neue"; 37 | font-family: Helvetica; 38 | font-family: Arial; 39 | font-family: sans-serif 40 | } 41 | 42 | .test-var-7 { 43 | font-family: (foo: "Helvetica Neue", (barfoo: Helvetica, barbar: Arial), baz: sans-serif); 44 | font-family: (barfoo:Helvetica,barbar:Arial); 45 | font-family: "Helvetica Neue"; 46 | font-family: sans-serif 47 | } 48 | -------------------------------------------------------------------------------- /test/mixed.css: -------------------------------------------------------------------------------- 1 | $pass: green; 2 | $number: 3; 3 | 4 | @for $i from 1 to 5 by 2 { 5 | @for $j from 3 to 1 { 6 | .for-$(j)-from-3-to-1.for-$(j)-from-1-to-5-by-2 { 7 | color: $pass; 8 | } 9 | } 10 | } 11 | 12 | @each $i in ((foo, bar), (baz, qux)) { 13 | @each $j in $i { 14 | .each-$(j)-in($(i)).each-$(i)-in((foo,bar),(baz,qux)) { 15 | color: $pass; 16 | } 17 | } 18 | } 19 | 20 | @for $i from 1 to 5 by 2 { 21 | @if $i >= $number { 22 | .if-$(i)-is-atleast-$(number) { 23 | color: $pass; 24 | } 25 | } @else { 26 | .if-$(i)-is-not-atleast-$(number) { 27 | color: $pass; 28 | } 29 | } 30 | } 31 | 32 | $dir: assets/images; 33 | 34 | @each $i in (1, 2, 3) { 35 | @for $j from $i to 3 { 36 | @if $j >= 3 { 37 | .if-$(j)-is-atleast-3.for-$(j)-from-$(i)-to-3.each-$(i)-in(1,2,3) { 38 | background: url($dir/$j.png); 39 | } 40 | } 41 | } 42 | } 43 | 44 | @each $color $i in (red, white, blue) { 45 | :nth-child($i) { 46 | color: $color; 47 | } 48 | } 49 | 50 | @each $i in (1) { 51 | @for $j from $i to 3 { 52 | .test-i-$i { 53 | background-image: url($dir/$i$j.png); 54 | content: "\$dir"; 55 | } 56 | } 57 | } 58 | 59 | @mixin mixin-test-1($count: 3) { 60 | @for $i from 1 to $count { 61 | .for-$(i)-from-1-to$(count) { 62 | content: "$i $count"; 63 | } 64 | } 65 | } 66 | 67 | @include mixin-test-1; 68 | 69 | @include mixin-test-1(5); 70 | -------------------------------------------------------------------------------- /test/mixins.css: -------------------------------------------------------------------------------- 1 | $default-radius: 2em; 2 | 3 | @mixin mixin-test-1 { 4 | -webkit-border-radius: 1em; 5 | -moz-border-radius: 1em; 6 | -ms-border-radius: 1em; 7 | border-radius: 1em; 8 | } 9 | 10 | @mixin mixin-test-2() { 11 | -webkit-border-radius: 1em; 12 | -moz-border-radius: 1em; 13 | -ms-border-radius: 1em; 14 | border-radius: 1em; 15 | } 16 | 17 | @mixin mixin-test-3($radius: $default-radius) { 18 | -webkit-border-radius: $radius; 19 | -moz-border-radius: $radius; 20 | -ms-border-radius: $radius; 21 | border-radius: $radius; 22 | } 23 | 24 | @mixin mixin-test-4($radius: 1em) { 25 | -webkit-border-radius: $radius; 26 | -moz-border-radius: $radius; 27 | -ms-border-radius: $radius; 28 | border-radius: $radius; 29 | } 30 | 31 | @mixin mixin-test-5($min-width: 30em) { 32 | @media (min-width: $min-width) { 33 | @content; 34 | } 35 | } 36 | 37 | .test-1 { 38 | @include mixin-test-1; 39 | } 40 | 41 | .test-2 { 42 | @include mixin-test-2(); 43 | } 44 | 45 | .test-3a { 46 | @include mixin-test-3(1em); 47 | } 48 | 49 | .test-3b { 50 | @include mixin-test-3; 51 | } 52 | 53 | .test-4a { 54 | @include mixin-test-4; 55 | } 56 | 57 | .test-4b { 58 | @include mixin-test-4(2em); 59 | } 60 | 61 | .test-5a { 62 | @include mixin-test-5 { 63 | min-width: $min-width; 64 | } 65 | } 66 | 67 | .test-5b { 68 | @include mixin-test-5(60em) { 69 | min-width: $min-width; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "postcss-advanced-variables", 3 | "version": "5.0.0", 4 | "description": "Use Sass-like variables, conditionals, and iterators in CSS", 5 | "author": "Jonathan Neal ", 6 | "license": "CC0-1.0", 7 | "repository": "jonathantneal/postcss-advanced-variables", 8 | "homepage": "https://github.com/jonathantneal/postcss-advanced-variables#readme", 9 | "bugs": "https://github.com/jonathantneal/postcss-advanced-variables/issues", 10 | "main": "index.js", 11 | "module": "index.mjs", 12 | "files": [ 13 | "index.js", 14 | "index.mjs" 15 | ], 16 | "scripts": { 17 | "prepublishOnly": "npm test", 18 | "pretest": "npm run build", 19 | "build": "rollup -c .rollup.mjs --silent", 20 | "test": "postcss-tape" 21 | }, 22 | "engines": { 23 | "node": ">=18" 24 | }, 25 | "dependencies": { 26 | "@csstools/sass-import-resolve": "^1.0.0" 27 | }, 28 | "peerDependencies": { 29 | "postcss": "^8.4" 30 | }, 31 | "devDependencies": { 32 | "postcss": "^8.4", 33 | "postcss-scss": "^3.0.5", 34 | "postcss-tape": "^6.0.1", 35 | "rollup": "^4.24.0" 36 | }, 37 | "keywords": [ 38 | "postcss", 39 | "css", 40 | "postcss-plugin", 41 | "sass", 42 | "scss", 43 | "variables", 44 | "conditionals", 45 | "iterators", 46 | "fors", 47 | "eaches", 48 | "medias", 49 | "defaults" 50 | ] 51 | } 52 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | // tooling 2 | import transformNode from './lib/transform-node'; 3 | import resolve from '@csstools/sass-import-resolve'; 4 | 5 | const matchProtocol = /^(?:[A-z]+:)?\/\//; 6 | 7 | // plugin 8 | const plugin = opts => ({ 9 | postcssPlugin: "postcss-advanced-variables", 10 | Root(root, { result }) { 11 | // process options 12 | const transformOpt = ['@content', '@each', '@else', '@if', '@include', '@import', '@for', '@mixin'].filter( 13 | feature => !String(Object(opts).disable || '').split(/\s*,\s*|\s+,?\s*|\s,?\s+/).includes(feature) 14 | ); 15 | const unresolvedOpt = String(Object(opts).unresolved || 'throw').toLowerCase(); 16 | const variablesOpt = Object(opts).variables; 17 | const importCache = Object(Object(opts).importCache); 18 | const importFilter = Object(opts).importFilter || (id => { 19 | return !matchProtocol.test(id); 20 | }); 21 | const importPaths = [].concat(Object(opts).importPaths || []); 22 | const importResolve = Object(opts).importResolve || ( 23 | (id, cwd) => resolve(id, { cwd, readFile: true, cache: importCache }) 24 | ); 25 | const importRoot = Object(opts).importRoot || process.cwd(); 26 | 27 | return transformNode(root, { 28 | result, 29 | importCache, 30 | importFilter, 31 | importPaths, 32 | importResolve, 33 | importRoot, 34 | transform: transformOpt, 35 | unresolved: unresolvedOpt, 36 | variables: variablesOpt 37 | }); 38 | }, 39 | }); 40 | 41 | plugin.postcss = true; 42 | export default plugin; 43 | -------------------------------------------------------------------------------- /src/lib/get-value-as-object.js: -------------------------------------------------------------------------------- 1 | // tooling 2 | import { list } from 'postcss'; 3 | 4 | // return sass-like arrays as literal arrays ('(hello), (goodbye)' to [[hello], [goodbye]]) 5 | export default function getValueAsObject(value) { 6 | const hasWrappingParens = matchWrappingParens.test(value); 7 | const unwrappedValue = String(hasWrappingParens ? value.replace(matchWrappingParens, '$1') : value).replace(matchTrailingComma, ''); 8 | const separatedValue = list.comma(unwrappedValue); 9 | const firstValue = separatedValue[0]; 10 | 11 | if (firstValue === value) { 12 | return value; 13 | } else { 14 | const objectValue = {}; 15 | const arrayValue = []; 16 | 17 | separatedValue.forEach( 18 | (subvalue, index) => { 19 | const [ match, key, keyvalue ] = subvalue.match(matchDeclaration) || []; 20 | 21 | if (match) { 22 | objectValue[key] = getValueAsObject(keyvalue); 23 | } else { 24 | arrayValue[index] = getValueAsObject(subvalue); 25 | } 26 | } 27 | ); 28 | 29 | const transformedValue = Object.keys(objectValue).length > 0 30 | ? Object.assign(objectValue, arrayValue) 31 | : arrayValue; 32 | 33 | return transformedValue; 34 | } 35 | } 36 | 37 | // match wrapping parentheses ((), (anything), (anything (anything))) 38 | const matchWrappingParens = /^\(([\W\w]*)\)$/g; 39 | 40 | // match a property name (any-possible_name) 41 | const matchDeclaration = /^([\w-]+)\s*:\s*([\W\w]+)\s*$/; 42 | 43 | // match a trailing comma 44 | const matchTrailingComma = /\s*,\s*$/; 45 | -------------------------------------------------------------------------------- /src/lib/transform-mixin-atrule.js: -------------------------------------------------------------------------------- 1 | // tooling 2 | import { list } from 'postcss'; 3 | import getReplacedString from './get-replaced-string'; 4 | import setVariable from './set-variable'; 5 | 6 | // transform @mixin at-rules 7 | export default function transformMixinAtrule(rule, opts) { 8 | // if @mixin is supported 9 | if (opts.transform.includes('@mixin')) { 10 | // @mixin options 11 | const { name, params } = getMixinOpts(rule, opts); 12 | 13 | // set the mixin as a variable on the parent of the @mixin at-rule 14 | setVariable(rule.parent, `@mixin ${name}`, { params, rule }, opts); 15 | 16 | // remove the @mixin at-rule 17 | rule.remove(); 18 | } 19 | } 20 | 21 | // return the @mixin statement options (@mixin NAME, @mixin NAME(PARAMS)) 22 | const getMixinOpts = (node, opts) => { 23 | // @mixin name and default params ([{ name, value }, ...]) 24 | const [ name, sourceParams ] = node.params.split(matchOpeningParen, 2); 25 | const params = sourceParams && sourceParams.slice(0, -1).trim() 26 | ? list.comma(sourceParams.slice(0, -1).trim()).map( 27 | param => { 28 | const parts = list.split(param, ':'); 29 | const paramName = parts[0].slice(1); 30 | const paramValue = parts.length > 1 31 | ? getReplacedString(parts.slice(1).join(':'), node, opts) 32 | : undefined; 33 | 34 | return { name: paramName, value: paramValue }; 35 | } 36 | ) 37 | : []; 38 | 39 | return { name, params }; 40 | }; 41 | 42 | // match an opening parenthesis 43 | const matchOpeningParen = '('; 44 | -------------------------------------------------------------------------------- /test/variables.css: -------------------------------------------------------------------------------- 1 | $test-var-1: "Helvetica Neue"; 2 | $test-var-2: ("Helvetica Neue"); 3 | $test-var-3: "Helvetica Neue", Helvetica, Arial, sans-serif; 4 | $test-var-4: ("Helvetica Neue", Helvetica, Arial, sans-serif); 5 | $test-var-5: ("Helvetica Neue", (Helvetica, Arial), sans-serif); 6 | $test-var-6: (foo: "Helvetica Neue", bar: Helvetica, baz: Arial, qux: sans-serif); 7 | $test-var-7: (foo: "Helvetica Neue", (barfoo: Helvetica, barbar: Arial), baz: sans-serif); 8 | 9 | .test-var-1 { 10 | font-family: $test-var-1; 11 | 12 | @each $test-var-1-var in $test-var-1 { 13 | font-family: $test-var-1-var; 14 | } 15 | } 16 | 17 | .test-var-2 { 18 | font-family: $test-var-2; 19 | 20 | @each $test-var-2-var in $test-var-2 { 21 | font-family: $test-var-2-var; 22 | } 23 | } 24 | 25 | .test-var-3 { 26 | font-family: $test-var-3; 27 | 28 | @each $test-var-3-var in $test-var-3 { 29 | font-family: $test-var-3-var; 30 | } 31 | } 32 | 33 | .test-var-4 { 34 | font-family: $test-var-4; 35 | 36 | @each $test-var-4-var in $test-var-4 { 37 | font-family: $test-var-4-var; 38 | } 39 | } 40 | 41 | .test-var-5 { 42 | font-family: $test-var-5; 43 | 44 | @each $test-var-5-var in $test-var-5 { 45 | font-family: $test-var-5-var; 46 | } 47 | } 48 | 49 | .test-var-6 { 50 | font-family: $test-var-6; 51 | 52 | @each $test-var-6-var in $test-var-6 { 53 | font-family: $test-var-6-var; 54 | } 55 | } 56 | 57 | .test-var-7 { 58 | font-family: $test-var-7; 59 | 60 | @each $test-var-7-var in $test-var-7 { 61 | font-family: $test-var-7-var; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/lib/get-replaced-string.js: -------------------------------------------------------------------------------- 1 | // tooling 2 | import getClosestVariable from './get-closest-variable'; 3 | import manageUnresolved from './manage-unresolved'; 4 | 5 | // return content with its variables replaced by the corresponding values of a node 6 | export default function getReplacedString(string, node, opts) { 7 | const replacedString = string.replace( 8 | matchVariables, 9 | (match, before, name1, name2, name3) => { 10 | // conditionally return an (unescaped) match 11 | if (before === '\\') { 12 | return match.slice(1); 13 | } 14 | 15 | // the first matching variable name 16 | const name = name1 || name2 || name3; 17 | 18 | // the closest variable value 19 | const value = getClosestVariable(name, node.parent, opts); 20 | 21 | // if a variable has not been resolved 22 | if (undefined === value) { 23 | manageUnresolved(node, opts, name, `Could not resolve the variable "$${name}" within "${string}"`); 24 | 25 | return match; 26 | } 27 | 28 | // the stringified value 29 | const stringifiedValue = `${before}${stringify(value)}`; 30 | 31 | return stringifiedValue; 32 | } 33 | ); 34 | 35 | return replacedString; 36 | } 37 | 38 | // match all $name, $(name), and #{$name} variables (and catch the character before it) 39 | const matchVariables = /(.?)(?:\$([A-z][\w-]*)|\$\(([A-z][\w-]*)\)|#\{\$([A-z][\w-]*)\})/g; 40 | 41 | // return a sass stringified variable 42 | const stringify = object => Array.isArray(object) 43 | ? `(${object.map(stringify).join(',')})` 44 | : Object(object) === object 45 | ? `(${Object.keys(object).map( 46 | key => `${key}:${stringify(object[key])}` 47 | ).join(',')})` 48 | : String(object); 49 | -------------------------------------------------------------------------------- /src/lib/transform-for-atrule.js: -------------------------------------------------------------------------------- 1 | // tooling 2 | import { list } from 'postcss'; 3 | import getReplacedString from './get-replaced-string'; 4 | import transformNode from './transform-node'; 5 | import setVariable from './set-variable'; 6 | import waterfall from './waterfall'; 7 | 8 | // transform @for at-rules 9 | export default function transformForAtrule(rule, opts) { 10 | // if @for is supported 11 | if (opts.transform.includes('@for')) { 12 | // @for options 13 | const { varname, start, end, increment } = getForOpts(rule, opts); 14 | const direction = start <= end ? 1 : -1; 15 | const replacements = []; 16 | const ruleClones = []; 17 | 18 | // for each iteration 19 | for (let incrementor = start; incrementor * direction <= end * direction; incrementor += increment * direction) { 20 | // set the current variable 21 | setVariable(rule, varname, incrementor, opts); 22 | 23 | // clone the @for at-rule 24 | const clone = rule.clone({ 25 | parent: rule.parent, 26 | variables: Object.assign({}, rule.variables) 27 | }); 28 | 29 | ruleClones.push(clone) 30 | } 31 | 32 | return waterfall( 33 | ruleClones, 34 | clone => transformNode(clone, opts).then( 35 | () => { 36 | replacements.push(...clone.nodes); 37 | } 38 | ) 39 | ).then( 40 | () => { 41 | // replace the @for at-rule with the replacements 42 | rule.parent.insertBefore(rule, replacements); 43 | 44 | rule.remove(); 45 | } 46 | ) 47 | } 48 | } 49 | 50 | // return the @for statement options (@for NAME from START through END, @for NAME from START through END by INCREMENT) 51 | const getForOpts = (node, opts) => { 52 | const params = list.space(node.params); 53 | const varname = params[0].trim().slice(1); 54 | const start = Number(getReplacedString(params[2], node, opts)); 55 | const end = Number(getReplacedString(params[4], node, opts)); 56 | const increment = 6 in params && Number(getReplacedString(params[6], node, opts)) || 1; 57 | 58 | return { varname, start, end, increment }; 59 | }; 60 | -------------------------------------------------------------------------------- /src/lib/transform-include-atrule.js: -------------------------------------------------------------------------------- 1 | // tooling 2 | import { list } from 'postcss'; 3 | import getClosestVariable from './get-closest-variable'; 4 | import getReplacedString from './get-replaced-string'; 5 | import manageUnresolved from './manage-unresolved'; 6 | import setVariable from './set-variable'; 7 | import transformNode from './transform-node'; 8 | 9 | // transform @include at-rules 10 | export default function transformIncludeAtrule(rule, opts) { 11 | // if @include is supported 12 | if (opts.transform.includes('@include')) { 13 | // @include options 14 | const { name, args } = getIncludeOpts(rule); 15 | 16 | // the closest @mixin variable 17 | const mixin = getClosestVariable(`@mixin ${name}`, rule.parent, opts); 18 | 19 | // if the @mixin variable exists 20 | if (mixin) { 21 | // set @mixin variables on the @include at-rule 22 | mixin.params.forEach( 23 | (param, index) => { 24 | const arg = index in args 25 | ? getReplacedString(args[index], rule, opts) 26 | : param.value; 27 | 28 | setVariable(rule, param.name, arg, opts); 29 | } 30 | ); 31 | 32 | // clone the @mixin at-rule 33 | const clone = mixin.rule.clone({ 34 | original: rule, 35 | parent: rule.parent, 36 | variables: rule.variables 37 | }); 38 | 39 | // transform the clone children 40 | return transformNode(clone, opts).then(() => { 41 | // replace the @include at-rule with the clone children 42 | rule.parent.insertBefore(rule, clone.nodes); 43 | 44 | rule.remove(); 45 | }) 46 | } else { 47 | // otherwise, if the @mixin variable does not exist 48 | manageUnresolved(rule, opts, name, `Could not resolve the mixin for "${name}"`); 49 | } 50 | } 51 | } 52 | 53 | // return the @include statement options (@include NAME, @include NAME(ARGS)) 54 | const getIncludeOpts = node => { 55 | // @include name and args 56 | const [ name, sourceArgs ] = node.params.split(matchOpeningParen, 2); 57 | const args = sourceArgs 58 | ? list.comma(sourceArgs.slice(0, -1)) 59 | : []; 60 | 61 | return { name, args }; 62 | }; 63 | 64 | // match an opening parenthesis 65 | const matchOpeningParen = '('; 66 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to PostCSS Advanced Variables 2 | 3 | You want to help? You rock! Now, take a moment to be sure your contributions 4 | make sense to everyone else. 5 | 6 | ## Reporting Issues 7 | 8 | Found a problem? Want a new feature? 9 | 10 | - See if your issue or idea has [already been reported]. 11 | - Provide a [reduced test case] or a [live example]. 12 | 13 | Remember, a bug is a _demonstrable problem_ caused by _our_ code. 14 | 15 | ## Submitting Pull Requests 16 | 17 | Pull requests are the greatest contributions, so be sure they are focused in 18 | scope and avoid unrelated commits. 19 | 20 | 1. To begin; [fork this project], clone your fork, and add our upstream. 21 | ```bash 22 | # Clone your fork of the repo into the current directory 23 | git clone git@github.com:YOUR_USER/postcss-advanced-variables.git 24 | 25 | # Navigate to the newly cloned directory 26 | cd postcss-advanced-variables 27 | 28 | # Assign the original repo to a remote called "upstream" 29 | git remote add upstream git@github.com:jonathantneal/postcss-advanced-variables.git 30 | 31 | # Install the tools necessary for testing 32 | npm install 33 | ``` 34 | 35 | 2. Create a branch for your feature or fix: 36 | ```bash 37 | # Move into a new branch for your feature 38 | git checkout -b feature/thing 39 | ``` 40 | ```bash 41 | # Move into a new branch for your fix 42 | git checkout -b fix/something 43 | ``` 44 | 45 | 3. If your code follows our practices, then push your feature branch: 46 | ```bash 47 | # Test current code 48 | npm test 49 | ``` 50 | ```bash 51 | # Push the branch for your new feature 52 | git push origin feature/thing 53 | ``` 54 | ```bash 55 | # Or, push the branch for your update 56 | git push origin update/something 57 | ``` 58 | 59 | That’s it! Now [open a pull request] with a clear title and description. 60 | 61 | [already been reported]: ../../issues 62 | [fork this project]: ../../fork 63 | [live example]: https://codepen.io/pen 64 | [open a pull request]: https://help.github.com/articles/using-pull-requests/ 65 | [reduced test case]: https://css-tricks.com/reduced-test-cases/ 66 | -------------------------------------------------------------------------------- /test/conditionals.disable-if.expect.css: -------------------------------------------------------------------------------- 1 | @if true { 2 | .is-true-true { 3 | content: "pass"; 4 | } 5 | } 6 | 7 | @if false { 8 | .is-false-true { 9 | content: "fail"; 10 | } 11 | } .is-false-false { 12 | content: "pass"; 13 | } 14 | 15 | @if $if-boolean { 16 | .is-true-variable-true { 17 | content: "pass"; 18 | } 19 | } 20 | 21 | @if 1 == 1 { 22 | .is-1-equal-to-1 { 23 | content: "pass"; 24 | } 25 | } 26 | 27 | @if 1 == 2 { 28 | .is-1-equal-to-2 { 29 | content: "fail"; 30 | } 31 | } .is-1-not-equal-to-2 { 32 | content: "pass"; 33 | } 34 | 35 | @if 1 != 1 { 36 | .is-1-not-equal-to-1 { 37 | content: "fail"; 38 | } 39 | } 40 | 41 | @if 1 != 2 { 42 | .is-1-not-equal-to-2 { 43 | content: "pass"; 44 | } 45 | } 46 | 47 | @if 1 < 0 { 48 | .is-1-less-than-0 { 49 | content: "fail"; 50 | } 51 | } 52 | 53 | @if 1 < 1 { 54 | .is-1-less-than-1 { 55 | content: "fail"; 56 | } 57 | } 58 | 59 | @if 1 < 2 { 60 | .is-1-less-than-2 { 61 | content: "pass"; 62 | } 63 | } 64 | 65 | @if 1 > 0 { 66 | .is-1-greater-than-0 { 67 | content: "pass"; 68 | } 69 | } 70 | 71 | @if 1 > 1 { 72 | .is-1-greater-than-1 { 73 | content: "fail"; 74 | } 75 | } 76 | 77 | @if 1 > 2 { 78 | .is-1-greater-than-2 { 79 | content: "fail"; 80 | } 81 | } 82 | 83 | @if 1 <= 0 { 84 | .is-1-less-than-or-equal-to-0 { 85 | content: "fail"; 86 | } 87 | } 88 | 89 | @if 1 <= 1 { 90 | .is-1-less-than-or-equal-to-1 { 91 | content: "pass"; 92 | } 93 | } 94 | 95 | @if 1 <= 2 { 96 | .is-1-less-than-or-equal-to-2 { 97 | content: "pass"; 98 | } 99 | } 100 | 101 | @if 1 >= 0 { 102 | .is-1-less-than-or-equal-to-0 { 103 | content: "pass"; 104 | } 105 | } 106 | 107 | @if 1 >= 1 { 108 | .is-1-less-than-or-equal-to-1 { 109 | content: "pass"; 110 | } 111 | } 112 | 113 | @if 1 >= 2 { 114 | .is-1-less-than-or-equal-to-2 { 115 | content: "fail"; 116 | } 117 | } 118 | 119 | @if $number == 1 { 120 | .is-number-var-equal-to-1 { 121 | content: "fail"; 122 | } 123 | } .is-number-var-not-equal-to-1 { 124 | content: "pass"; 125 | } 126 | 127 | @if $number == 5 { 128 | .is-number-equal-to-5 { 129 | content: "pass"; 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /src/lib/transform-each-atrule.js: -------------------------------------------------------------------------------- 1 | // tooling 2 | import getReplacedString from './get-replaced-string'; 3 | import getValueAsObject from './get-value-as-object'; 4 | import setVariable from './set-variable'; 5 | import transformNode from './transform-node'; 6 | import waterfall from './waterfall'; 7 | 8 | // transform @each at-rules 9 | export default function transformEachAtrule(rule, opts) { 10 | // if @each is supported 11 | if (opts.transform.includes('@each')) { 12 | // @each options 13 | const { varname, incname, list } = getEachOpts(rule, opts); 14 | const replacements = []; 15 | const ruleClones = []; 16 | 17 | Object.keys(list).forEach( 18 | key => { 19 | // set the current variable 20 | setVariable(rule, varname, list[key], opts); 21 | 22 | // conditionally set the incremenator variable 23 | if (incname) { 24 | setVariable(rule, incname, key, opts); 25 | } 26 | 27 | // clone the @each at-rule 28 | const clone = rule.clone({ 29 | parent: rule.parent, 30 | variables: Object.assign({}, rule.variables) 31 | }); 32 | 33 | ruleClones.push(clone) 34 | } 35 | ); 36 | 37 | return waterfall( 38 | ruleClones, 39 | clone => transformNode(clone, opts).then( 40 | () => { 41 | replacements.push(...clone.nodes); 42 | } 43 | ) 44 | ).then( 45 | () => { 46 | // replace the @each at-rule with the replacements 47 | rule.parent.insertBefore(rule, replacements); 48 | 49 | rule.remove(); 50 | } 51 | ) 52 | } 53 | } 54 | 55 | // return the @each statement options (@each NAME in LIST, @each NAME ITERATOR in LIST) 56 | const getEachOpts = (node, opts) => { 57 | const params = node.params.split(matchInOperator); 58 | const args = (params[0] || '').trim().split(' '); 59 | const varname = args[0].trim().slice(1); 60 | const incname = args.length > 1 && args[1].trim().slice(1); 61 | const rawlist = getValueAsObject( 62 | getReplacedString( 63 | params.slice(1).join(matchInOperator), 64 | node, 65 | opts 66 | ) 67 | ); 68 | const list = 'string' === typeof rawlist ? [rawlist] : rawlist; 69 | 70 | return { varname, incname, list }; 71 | }; 72 | 73 | // match the opertor separating the name and iterator from the list 74 | const matchInOperator = ' in '; 75 | -------------------------------------------------------------------------------- /src/lib/transform-if-atrule.js: -------------------------------------------------------------------------------- 1 | // tooling 2 | import { list } from 'postcss'; 3 | import transformNode from './transform-node'; 4 | import getReplacedString from './get-replaced-string'; 5 | 6 | // transform @if at-rules 7 | export default function transformIfAtrule(rule, opts) { 8 | // @if options 9 | const isTruthy = isIfTruthy(rule, opts); 10 | const next = rule.next(); 11 | 12 | const transformAndInsertBeforeParent = node => transformNode(node, opts).then( 13 | () => node.parent.insertBefore(node, node.nodes) 14 | ) 15 | 16 | return ifPromise( 17 | opts.transform.includes('@if'), 18 | () => ifPromise( 19 | isTruthy, 20 | () => transformAndInsertBeforeParent(rule) 21 | ).then(() => { 22 | rule.remove(); 23 | }) 24 | ).then(() => ifPromise( 25 | opts.transform.includes('@else') && isElseRule(next), 26 | () => ifPromise( 27 | !isTruthy, 28 | () => transformAndInsertBeforeParent(next) 29 | ).then(() => { 30 | next.remove(); 31 | }) 32 | )) 33 | } 34 | 35 | const ifPromise = (condition, trueFunction) => Promise.resolve(condition && trueFunction()) 36 | 37 | // return whether the @if at-rule is truthy 38 | const isIfTruthy = (node, opts) => { 39 | // @if statement options (@if EXPRESSION, @if LEFT OPERATOR RIGHT) 40 | const params = list.space(node.params); 41 | const left = getInterprettedString(getReplacedString(params[0] || '', node, opts)); 42 | const operator = params[1]; 43 | const right = getInterprettedString(getReplacedString(params[2] || '', node, opts)); 44 | 45 | // evaluate the expression 46 | const isTruthy = !operator && left || 47 | operator === '==' && left === right || 48 | operator === '!=' && left !== right || 49 | operator === '<' && left < right || 50 | operator === '<=' && left <= right || 51 | operator === '>' && left > right || 52 | operator === '>=' && left >= right; 53 | 54 | return isTruthy; 55 | }; 56 | 57 | // return the value as a boolean, number, or string 58 | const getInterprettedString = value => 'true' === value 59 | ? true 60 | : 'false' === value 61 | ? false 62 | : isNaN(value) 63 | ? value 64 | : Number(value); 65 | 66 | // return whether the node is an else at-rule 67 | const isElseRule = node => Object(node) === node && 'atrule' === node.type && 'else' === node.name; 68 | -------------------------------------------------------------------------------- /src/lib/transform-node.js: -------------------------------------------------------------------------------- 1 | // tooling 2 | import transformDecl from './transform-decl'; 3 | import transformAtrule from './transform-atrule'; 4 | import transformEachAtrule from './transform-each-atrule'; 5 | import transformIfAtrule from './transform-if-atrule'; 6 | import transformImportAtrule from './transform-import-atrule'; 7 | import transformIncludeAtrule from './transform-include-atrule'; 8 | import transformForAtrule from './transform-for-atrule'; 9 | import transformMixinAtrule from './transform-mixin-atrule'; 10 | import transformRule from './transform-rule'; 11 | import transformContentAtrule from './transform-content-atrule'; 12 | import waterfall from './waterfall'; 13 | 14 | export default function transformNode(node, opts) { 15 | return waterfall( 16 | getNodesArray(node), 17 | child => transformRuleOrDecl(child, opts).then(() => { 18 | // conditionally walk the children of the attached child 19 | if (child.parent) { 20 | return transformNode(child, opts); 21 | } 22 | }) 23 | ); 24 | } 25 | 26 | function transformRuleOrDecl(child, opts) { 27 | return Promise.resolve().then(() => { 28 | const type = child.type; 29 | 30 | if ('atrule' === type) { 31 | const name = (child.name || '').toLowerCase(); 32 | 33 | if ('content' === name) { 34 | // transform @content at-rules 35 | return transformContentAtrule(child, opts); 36 | } else if ('each' === name) { 37 | // transform @each at-rules 38 | return transformEachAtrule(child, opts); 39 | } else if ('if' === name) { 40 | // transform @if at-rules 41 | return transformIfAtrule(child, opts); 42 | } else if ('import' === name) { 43 | return transformImportAtrule(child, opts); 44 | } else if ('include' === name) { 45 | // transform @include at-rules 46 | return transformIncludeAtrule(child, opts); 47 | } else if ('for' === name) { 48 | // transform @for at-rules 49 | return transformForAtrule(child, opts); 50 | } else if ('mixin' === name) { 51 | // transform mixin at-rules 52 | return transformMixinAtrule(child, opts); 53 | } else { 54 | // transform all other at-rules 55 | return transformAtrule(child, opts); 56 | } 57 | } else if ('decl' === type) { 58 | // transform declarations 59 | return transformDecl(child, opts); 60 | } else if ('rule' === type) { 61 | // transform rule 62 | return transformRule(child, opts); 63 | } 64 | }) 65 | } 66 | 67 | // return the children of a node as an array 68 | const getNodesArray = node => Array.from(Object(node).nodes || []); 69 | -------------------------------------------------------------------------------- /test/conditionals.disable.expect.css: -------------------------------------------------------------------------------- 1 | @if true { 2 | .is-true-true { 3 | content: "pass"; 4 | } 5 | } @else { 6 | .is-true-false { 7 | content: "fail"; 8 | } 9 | } 10 | 11 | @if false { 12 | .is-false-true { 13 | content: "fail"; 14 | } 15 | } @else { 16 | .is-false-false { 17 | content: "pass"; 18 | } 19 | } 20 | 21 | @if $if-boolean { 22 | .is-true-variable-true { 23 | content: "pass"; 24 | } 25 | } @else { 26 | .is-true-variable-false { 27 | content: "pass"; 28 | } 29 | } 30 | 31 | @if 1 == 1 { 32 | .is-1-equal-to-1 { 33 | content: "pass"; 34 | } 35 | } @else { 36 | .is-1-not-equal-to-1 { 37 | content: "fail"; 38 | } 39 | } 40 | 41 | @if 1 == 2 { 42 | .is-1-equal-to-2 { 43 | content: "fail"; 44 | } 45 | } @else { 46 | .is-1-not-equal-to-2 { 47 | content: "pass"; 48 | } 49 | } 50 | 51 | @if 1 != 1 { 52 | .is-1-not-equal-to-1 { 53 | content: "fail"; 54 | } 55 | } 56 | 57 | @if 1 != 2 { 58 | .is-1-not-equal-to-2 { 59 | content: "pass"; 60 | } 61 | } 62 | 63 | @if 1 < 0 { 64 | .is-1-less-than-0 { 65 | content: "fail"; 66 | } 67 | } 68 | 69 | @if 1 < 1 { 70 | .is-1-less-than-1 { 71 | content: "fail"; 72 | } 73 | } 74 | 75 | @if 1 < 2 { 76 | .is-1-less-than-2 { 77 | content: "pass"; 78 | } 79 | } 80 | 81 | @if 1 > 0 { 82 | .is-1-greater-than-0 { 83 | content: "pass"; 84 | } 85 | } 86 | 87 | @if 1 > 1 { 88 | .is-1-greater-than-1 { 89 | content: "fail"; 90 | } 91 | } 92 | 93 | @if 1 > 2 { 94 | .is-1-greater-than-2 { 95 | content: "fail"; 96 | } 97 | } 98 | 99 | @if 1 <= 0 { 100 | .is-1-less-than-or-equal-to-0 { 101 | content: "fail"; 102 | } 103 | } 104 | 105 | @if 1 <= 1 { 106 | .is-1-less-than-or-equal-to-1 { 107 | content: "pass"; 108 | } 109 | } 110 | 111 | @if 1 <= 2 { 112 | .is-1-less-than-or-equal-to-2 { 113 | content: "pass"; 114 | } 115 | } 116 | 117 | @if 1 >= 0 { 118 | .is-1-less-than-or-equal-to-0 { 119 | content: "pass"; 120 | } 121 | } 122 | 123 | @if 1 >= 1 { 124 | .is-1-less-than-or-equal-to-1 { 125 | content: "pass"; 126 | } 127 | } 128 | 129 | @if 1 >= 2 { 130 | .is-1-less-than-or-equal-to-2 { 131 | content: "fail"; 132 | } 133 | } 134 | 135 | @if $number == 1 { 136 | .is-number-var-equal-to-1 { 137 | content: "fail"; 138 | } 139 | } @else { 140 | .is-number-var-not-equal-to-1 { 141 | content: "pass"; 142 | } 143 | } 144 | 145 | @if $number == 5 { 146 | .is-number-equal-to-5 { 147 | content: "pass"; 148 | } 149 | } @else { 150 | .is-number-not-equal-to-5 { 151 | content: "fail"; 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /test/conditionals.css: -------------------------------------------------------------------------------- 1 | @if true { 2 | .is-true-true { 3 | content: "pass"; 4 | } 5 | } @else { 6 | .is-true-false { 7 | content: "fail"; 8 | } 9 | } 10 | 11 | @if false { 12 | .is-false-true { 13 | content: "fail"; 14 | } 15 | } @else { 16 | .is-false-false { 17 | content: "pass"; 18 | } 19 | } 20 | 21 | $if-boolean: true !default; 22 | 23 | @if $if-boolean { 24 | .is-$(if-boolean)-variable-true { 25 | content: "pass"; 26 | } 27 | } @else { 28 | .is-$(if-boolean)-variable-false { 29 | content: "pass"; 30 | } 31 | } 32 | 33 | @if 1 == 1 { 34 | .is-1-equal-to-1 { 35 | content: "pass"; 36 | } 37 | } @else { 38 | .is-1-not-equal-to-1 { 39 | content: "fail"; 40 | } 41 | } 42 | 43 | @if 1 == 2 { 44 | .is-1-equal-to-2 { 45 | content: "fail"; 46 | } 47 | } @else { 48 | .is-1-not-equal-to-2 { 49 | content: "pass"; 50 | } 51 | } 52 | 53 | @if 1 != 1 { 54 | .is-1-not-equal-to-1 { 55 | content: "fail"; 56 | } 57 | } 58 | 59 | @if 1 != 2 { 60 | .is-1-not-equal-to-2 { 61 | content: "pass"; 62 | } 63 | } 64 | 65 | @if 1 < 0 { 66 | .is-1-less-than-0 { 67 | content: "fail"; 68 | } 69 | } 70 | 71 | @if 1 < 1 { 72 | .is-1-less-than-1 { 73 | content: "fail"; 74 | } 75 | } 76 | 77 | @if 1 < 2 { 78 | .is-1-less-than-2 { 79 | content: "pass"; 80 | } 81 | } 82 | 83 | @if 1 > 0 { 84 | .is-1-greater-than-0 { 85 | content: "pass"; 86 | } 87 | } 88 | 89 | @if 1 > 1 { 90 | .is-1-greater-than-1 { 91 | content: "fail"; 92 | } 93 | } 94 | 95 | @if 1 > 2 { 96 | .is-1-greater-than-2 { 97 | content: "fail"; 98 | } 99 | } 100 | 101 | @if 1 <= 0 { 102 | .is-1-less-than-or-equal-to-0 { 103 | content: "fail"; 104 | } 105 | } 106 | 107 | @if 1 <= 1 { 108 | .is-1-less-than-or-equal-to-1 { 109 | content: "pass"; 110 | } 111 | } 112 | 113 | @if 1 <= 2 { 114 | .is-1-less-than-or-equal-to-2 { 115 | content: "pass"; 116 | } 117 | } 118 | 119 | @if 1 >= 0 { 120 | .is-1-less-than-or-equal-to-0 { 121 | content: "pass"; 122 | } 123 | } 124 | 125 | @if 1 >= 1 { 126 | .is-1-less-than-or-equal-to-1 { 127 | content: "pass"; 128 | } 129 | } 130 | 131 | @if 1 >= 2 { 132 | .is-1-less-than-or-equal-to-2 { 133 | content: "fail"; 134 | } 135 | } 136 | 137 | $number: 5 !default; 138 | 139 | @if $number == 1 { 140 | .is-number-var-equal-to-1 { 141 | content: "fail"; 142 | } 143 | } @else { 144 | .is-number-var-not-equal-to-1 { 145 | content: "pass"; 146 | } 147 | } 148 | 149 | @if $number == 5 { 150 | .is-number-equal-to-5 { 151 | content: "pass"; 152 | } 153 | } @else { 154 | .is-number-not-equal-to-5 { 155 | content: "fail"; 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /test/mixed.expect.css: -------------------------------------------------------------------------------- 1 | 2 | .for-3-from-3-to-1.for-3-from-1-to-5-by-2 { 3 | color: green; 4 | } 5 | .for-2-from-3-to-1.for-2-from-1-to-5-by-2 { 6 | color: green; 7 | } 8 | .for-1-from-3-to-1.for-1-from-1-to-5-by-2 { 9 | color: green; 10 | } 11 | .for-3-from-3-to-1.for-3-from-1-to-5-by-2 { 12 | color: green; 13 | } 14 | .for-2-from-3-to-1.for-2-from-1-to-5-by-2 { 15 | color: green; 16 | } 17 | .for-1-from-3-to-1.for-1-from-1-to-5-by-2 { 18 | color: green; 19 | } 20 | .for-3-from-3-to-1.for-3-from-1-to-5-by-2 { 21 | color: green; 22 | } 23 | .for-2-from-3-to-1.for-2-from-1-to-5-by-2 { 24 | color: green; 25 | } 26 | .for-1-from-3-to-1.for-1-from-1-to-5-by-2 { 27 | color: green; 28 | } 29 | 30 | .each-foo-in((foo,bar)).each-(foo,bar)-in((foo,bar),(baz,qux)) { 31 | color: green; 32 | } 33 | 34 | .each-bar-in((foo,bar)).each-(foo,bar)-in((foo,bar),(baz,qux)) { 35 | color: green; 36 | } 37 | 38 | .each-baz-in((baz,qux)).each-(baz,qux)-in((foo,bar),(baz,qux)) { 39 | color: green; 40 | } 41 | 42 | .each-qux-in((baz,qux)).each-(baz,qux)-in((foo,bar),(baz,qux)) { 43 | color: green; 44 | } 45 | 46 | .if-1-is-not-atleast-3 { 47 | color: green; 48 | } 49 | 50 | .if-3-is-atleast-3 { 51 | color: green; 52 | } 53 | 54 | .if-5-is-atleast-3 { 55 | color: green; 56 | } 57 | 58 | .if-3-is-atleast-3.for-3-from-1-to-3.each-1-in(1,2,3) { 59 | background: url(assets/images/3.png); 60 | } 61 | 62 | .if-3-is-atleast-3.for-3-from-2-to-3.each-2-in(1,2,3) { 63 | background: url(assets/images/3.png); 64 | } 65 | 66 | .if-3-is-atleast-3.for-3-from-3-to-3.each-3-in(1,2,3) { 67 | background: url(assets/images/3.png); 68 | } 69 | 70 | :nth-child(0) { 71 | color: red; 72 | } 73 | 74 | :nth-child(1) { 75 | color: white; 76 | } 77 | 78 | :nth-child(2) { 79 | color: blue; 80 | } 81 | 82 | .test-i-1 { 83 | background-image: url(assets/images/11.png); 84 | content: "$dir"; 85 | } 86 | 87 | .test-i-1 { 88 | background-image: url(assets/images/12.png); 89 | content: "$dir"; 90 | } 91 | 92 | .test-i-1 { 93 | background-image: url(assets/images/13.png); 94 | content: "$dir"; 95 | } 96 | 97 | .for-1-from-1-to3 { 98 | content: "1 3"; 99 | } 100 | 101 | .for-2-from-1-to3 { 102 | content: "2 3"; 103 | } 104 | 105 | .for-3-from-1-to3 { 106 | content: "3 3"; 107 | } 108 | 109 | .for-1-from-1-to5 { 110 | content: "1 5"; 111 | } 112 | 113 | .for-2-from-1-to5 { 114 | content: "2 5"; 115 | } 116 | 117 | .for-3-from-1-to5 { 118 | content: "3 5"; 119 | } 120 | 121 | .for-4-from-1-to5 { 122 | content: "4 5"; 123 | } 124 | 125 | .for-5-from-1-to5 { 126 | content: "5 5"; 127 | } 128 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changes to PostCSS Advanced Variables 2 | 3 | ### 5.0.0 4 | 5 | - Updated dependencies 6 | - Changed: Support for Node 18+ 7 | 8 | ### 4.0.0 9 | 10 | - Updated: `postcss` to ^8.4 (major) 11 | 12 | ### 3.0.1 (February 27, 2020) 13 | 14 | - Fixed: parsing the contents of imported stylesheets (#71) 15 | 16 | ### 3.0.0 (November 22, 2018) 17 | 18 | - Fixed: transform variables in default value of mixins 19 | - Updated: `postcss` to 7.0.6 (major) 20 | - Changed: Support for Node 6+ 21 | 22 | ### 2.3.3 (February 10, 2018) 23 | 24 | - Fixed: asynchronous transforms to allow for imported mixins and variables 25 | 26 | ### 2.3.2 (February 10, 2018) 27 | 28 | - Fixed: imports failing when `from` is missing 29 | 30 | ### 2.3.1 (February 10, 2018) 31 | 32 | - Added: `babel-plugin-array-includes` instead of `babel-polyfill` for publish 33 | - Fixed: `@mixin` rules to support being declared with empty parens 34 | - Noted: Recommend `postcss-scss-syntax` to best support variable interpolation 35 | 36 | ### 2.3.0 (January 6, 2018) 37 | 38 | - Added: `importFilter` option to accept or ignore imports by function or regex 39 | - Added: Support for media parameters after `@import` rules 40 | - Added: Support for case-insensitive at-rules 41 | - Fixed: Protocol and protocol-less imports are ignored 42 | 43 | ### 2.2.0 (January 2, 2018) 44 | 45 | - Added: Support for `@import` 46 | - Added: `disable` option to conditionally disable any feature(s) 47 | - Fixed: How iterator arrays and objects are treated 48 | 49 | ### 2.1.0 (January 1, 2018) 50 | 51 | - Added: Support for `@mixin`, `@include`, and `@content` 52 | 53 | ### 2.0.0 (December 31, 2017) 54 | 55 | - Completely rewritten 56 | - Added: `unresolved` option to throw errors or warnings on unresolved variables 57 | - Added: Support for the `#{$var}` syntax 58 | - Added: Support for iterators in `@each` at-rules 59 | - Added: Support for boolean `@if` at-rules 60 | (`@each $item $index in $array`) 61 | - Added: Support for variable replacement in all at-rules 62 | - Added: Support for neighboring variables `$a$b` 63 | - Fixed: Number comparison in `@if` at-rules 64 | 65 | ## 1.2.2 (October 21, 2015) 66 | 67 | - Removed: Old gulp file 68 | 69 | ## 1.2.1 (October 21, 2015) 70 | 71 | - Updated: PostCSS 5.0.10 72 | - Updated: Tests 73 | 74 | ## 1.2.0 (October 21, 2015) 75 | 76 | - Added: Global variables set in options 77 | 78 | ## 1.1.0 (September 8, 2015) 79 | 80 | - Added: Support for `!default` 81 | 82 | ## 1.0.0 (September 7, 2015) 83 | 84 | - Updated: PostCSS 5.0.4 85 | - Updated: Chai 3.2.0 86 | - Updated: ESLint 1.0 87 | - Updated: Mocha 2.1.3 88 | 89 | ## 0.0.4 (July 22, 2015) 90 | 91 | - Added: Support for vars in @media 92 | 93 | ## 0.0.3 (July 8, 2015) 94 | 95 | - Added: Support for @else statements 96 | 97 | ## 0.0.2 (July 7, 2015) 98 | 99 | - Fixed: Some variable evaluations 100 | - Added: Support for deep arrays 101 | 102 | ## 0.0.1 (July 7, 2015) 103 | 104 | - Pre-release 105 | -------------------------------------------------------------------------------- /src/lib/transform-import-atrule.js: -------------------------------------------------------------------------------- 1 | // tooling 2 | import postcss, { list } from 'postcss'; 3 | import getReplacedString from './get-replaced-string'; 4 | import path from 'path'; 5 | import transformNode from './transform-node'; 6 | import manageUnresolved from './manage-unresolved'; 7 | 8 | // transform @import at-rules 9 | export default function transformImportAtrule(rule, opts) { 10 | // if @import is supported 11 | if (opts.transform.includes('@import')) { 12 | // @import options 13 | const { id, media, cwf, cwd } = getImportOpts(rule, opts); 14 | 15 | // PostCSS options 16 | const options = opts.result.opts; 17 | const parser = options.parser || options.syntax && options.syntax.parse || null; 18 | 19 | if ( 20 | opts.importFilter instanceof Function && opts.importFilter(id, media) || 21 | opts.importFilter instanceof RegExp && opts.importFilter.test(id) 22 | ) { 23 | const cwds = [cwd].concat(opts.importPaths); 24 | 25 | // promise the resolved file and its contents using the file resolver 26 | const importPromise = cwds.reduce( 27 | (promise, thiscwd) => promise.catch( 28 | () => opts.importResolve(id, thiscwd, opts) 29 | ), 30 | Promise.reject() 31 | ); 32 | 33 | return importPromise.then( 34 | // promise the processed file 35 | ({ file, contents }) => processor.process(contents, { from: file, parser: parser }).then( 36 | ({ root }) => { 37 | // push a dependency message 38 | opts.result.messages.push({ type: 'dependency', file: file, parent: cwf }); 39 | 40 | // imported nodes 41 | const nodes = root.nodes.slice(0); 42 | 43 | // if media params were detected 44 | if (media) { 45 | // create a new media rule 46 | const mediaRule = postcss.atRule({ 47 | name: 'media', 48 | params: media, 49 | source: rule.source 50 | }); 51 | 52 | // append with the imported nodes 53 | mediaRule.append(nodes); 54 | 55 | // replace the @import at-rule with the @media at-rule 56 | rule.replaceWith(mediaRule); 57 | } else { 58 | // replace the @import at-rule with the imported nodes 59 | rule.replaceWith(nodes); 60 | } 61 | 62 | // transform all nodes from the import 63 | return transformNode({ nodes }, opts); 64 | } 65 | ), 66 | () => { 67 | // otherwise, if the @import could not be found 68 | manageUnresolved(rule, opts, '@import', `Could not resolve the @import for "${id}"`); 69 | } 70 | ) 71 | } 72 | } 73 | } 74 | 75 | const noopPlugin = () => { 76 | return { postcssPlugin: 'noop-plugin', Once() {} }; 77 | }; 78 | 79 | noopPlugin.postcss = true; 80 | 81 | const processor = postcss([noopPlugin()]); 82 | 83 | // return the @import statement options (@import ID, @import ID MEDIA) 84 | const getImportOpts = (node, opts) => { 85 | const [ rawid, ...medias ] = list.space(node.params); 86 | const id = getReplacedString(trimWrappingURL(rawid), node, opts); 87 | const media = medias.join(' '); 88 | 89 | // current working file and directory 90 | const cwf = node.source && node.source.input && node.source.input.file || opts.result.from; 91 | const cwd = cwf ? path.dirname(cwf) : opts.importRoot; 92 | 93 | return { id, media, cwf, cwd }; 94 | }; 95 | 96 | // return a string with the wrapping url() and quotes trimmed 97 | const trimWrappingURL = string => trimWrappingQuotes(string.replace(/^url\(([\W\w]*)\)$/, '$1')); 98 | 99 | // return a string with the wrapping quotes trimmed 100 | const trimWrappingQuotes = string => string.replace(/^("|')([\W\w]*)\1$/, '$2'); 101 | -------------------------------------------------------------------------------- /.tape.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | 'default': { 3 | message: 'supports !default usage' 4 | }, 5 | 'default:var': { 6 | message: 'supports !default { variables } usage', 7 | options: { 8 | variables: { 9 | default: 'custom-value' 10 | } 11 | } 12 | }, 13 | 'default:var-func': { 14 | message: 'supports !default { variables() } usage', 15 | options: { 16 | variables: () => 'custom-fn-value' 17 | } 18 | }, 19 | 'variables': { 20 | message: 'supports variables usage' 21 | }, 22 | 'conditionals': { 23 | message: 'supports conditionals (@if, @else) usage' 24 | }, 25 | 'conditionals:disable': { 26 | message: 'supports disabled @if and @else usage', 27 | options: { 28 | disable: '@if, @else' 29 | } 30 | }, 31 | 'conditionals:disable-if': { 32 | message: 'supports disabled @if usage', 33 | options: { 34 | disable: '@if' 35 | } 36 | }, 37 | 'conditionals:disable-else': { 38 | message: 'supports disabled @else usage', 39 | options: { 40 | disable: '@else' 41 | } 42 | }, 43 | 'iterators': { 44 | message: 'supports iterators (@for, @each) usage' 45 | }, 46 | 'atrules': { 47 | message: 'supports generic at-rules usage' 48 | }, 49 | 'mixins': { 50 | message: 'supports mixins usage' 51 | }, 52 | 'imports': { 53 | message: 'supports @import usage' 54 | }, 55 | 'imports:no-from': { 56 | message: 'supports @import usage with no `from`', 57 | processOptions: { 58 | from: null 59 | }, 60 | options: { 61 | importRoot: 'test' 62 | } 63 | }, 64 | 'imports-alt': { 65 | message: 'supports @import with { importPaths } usage', 66 | options: { 67 | importPaths: 'test/imports' 68 | } 69 | }, 70 | 'imports-media': { 71 | message: 'supports @import with media usage' 72 | }, 73 | 'import-mixins': { 74 | message: 'supports @import with mixin usage' 75 | }, 76 | 'import-variables': { 77 | message: 'supports @import with variable usage' 78 | }, 79 | 'mixed': { 80 | message: 'supports mixed usage' 81 | }, 82 | 'scss': { 83 | message: 'supports scss interpolation', 84 | source: 'scss.scss', 85 | expect: 'scss.expect.scss', 86 | result: 'scss.result.scss', 87 | processOptions: { 88 | syntax: require('postcss-scss') 89 | } 90 | }, 91 | 'import-scss': { 92 | message: 'supports @import with scss syntax', 93 | source: 'imports-scss.scss', 94 | expect: 'imports-scss.expect.scss', 95 | result: 'imports-scss.result.scss', 96 | processOptions: { 97 | syntax: require('postcss-scss') 98 | } 99 | }, 100 | 'unresolved:ignore': { 101 | message: 'supports { unresolved: "ignore" } option', 102 | expect: 'unresolved.expect.css', 103 | options: { 104 | unresolved: 'ignore' 105 | } 106 | }, 107 | 'unresolved-include:ignore': { 108 | message: 'supports { unresolved: "ignore" } option with mixins', 109 | expect: 'unresolved-include.expect.css', 110 | options: { 111 | unresolved: 'ignore' 112 | } 113 | }, 114 | 'unresolved:throw': { 115 | message: 'supports { unresolved: "throw" } option', 116 | expect: 'unresolved.expect.css', 117 | options: { 118 | unresolved: 'throw' 119 | }, 120 | error: { 121 | reason: /^Could not resolve the variable/ 122 | } 123 | }, 124 | 'unresolved-include:throw': { 125 | message: 'supports { unresolved: "throw" } option with mixins', 126 | expect: 'unresolved-include.expect.css', 127 | options: { 128 | unresolved: 'throw' 129 | }, 130 | error: { 131 | reason: /^Could not resolve the mixin/ 132 | } 133 | }, 134 | 'unresolved:warn': { 135 | message: 'supports { unresolved: "warn" } option', 136 | expect: 'unresolved.expect.css', 137 | options: { 138 | unresolved: 'warn' 139 | }, 140 | warnings: 1 141 | }, 142 | 'unresolved-include:warn': { 143 | message: 'supports { unresolved: "warn" } option with mixins', 144 | expect: 'unresolved-include.expect.css', 145 | options: { 146 | unresolved: 'warn' 147 | }, 148 | warnings: 1 149 | }, 150 | 'properties': { 151 | message: 'supports variable property names', 152 | source: 'property.css', 153 | expect: 'property.expect.css', 154 | result: 'property.result.css', 155 | processOptions: { 156 | syntax: require('postcss-scss') 157 | } 158 | }, 159 | }; 160 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # CC0 1.0 Universal 2 | 3 | ## Statement of Purpose 4 | 5 | The laws of most jurisdictions throughout the world automatically confer 6 | exclusive Copyright and Related Rights (defined below) upon the creator and 7 | subsequent owner(s) (each and all, an “owner”) of an original work of 8 | authorship and/or a database (each, a “Work”). 9 | 10 | Certain owners wish to permanently relinquish those rights to a Work for the 11 | purpose of contributing to a commons of creative, cultural and scientific works 12 | (“Commons”) that the public can reliably and without fear of later claims of 13 | infringement build upon, modify, incorporate in other works, reuse and 14 | redistribute as freely as possible in any form whatsoever and for any purposes, 15 | including without limitation commercial purposes. These owners may contribute 16 | to the Commons to promote the ideal of a free culture and the further 17 | production of creative, cultural and scientific works, or to gain reputation or 18 | greater distribution for their Work in part through the use and efforts of 19 | others. 20 | 21 | For these and/or other purposes and motivations, and without any expectation of 22 | additional consideration or compensation, the person associating CC0 with a 23 | Work (the “Affirmer”), to the extent that he or she is an owner of Copyright 24 | and Related Rights in the Work, voluntarily elects to apply CC0 to the Work and 25 | publicly distribute the Work under its terms, with knowledge of his or her 26 | Copyright and Related Rights in the Work and the meaning and intended legal 27 | effect of CC0 on those rights. 28 | 29 | 1. Copyright and Related Rights. A Work made available under CC0 may be 30 | protected by copyright and related or neighboring rights (“Copyright and 31 | Related Rights”). Copyright and Related Rights include, but are not limited 32 | to, the following: 33 | 1. the right to reproduce, adapt, distribute, perform, display, communicate, 34 | and translate a Work; 35 | 2. moral rights retained by the original author(s) and/or performer(s); 36 | 3. publicity and privacy rights pertaining to a person’s image or likeness 37 | depicted in a Work; 38 | 4. rights protecting against unfair competition in regards to a Work, 39 | subject to the limitations in paragraph 4(i), below; 40 | 5. rights protecting the extraction, dissemination, use and reuse of data in 41 | a Work; 42 | 6. database rights (such as those arising under Directive 96/9/EC of the 43 | European Parliament and of the Council of 11 March 1996 on the legal 44 | protection of databases, and under any national implementation thereof, 45 | including any amended or successor version of such directive); and 46 | 7. other similar, equivalent or corresponding rights throughout the world 47 | based on applicable law or treaty, and any national implementations 48 | thereof. 49 | 50 | 2. Waiver. To the greatest extent permitted by, but not in contravention of, 51 | applicable law, Affirmer hereby overtly, fully, permanently, irrevocably and 52 | unconditionally waives, abandons, and surrenders all of Affirmer’s Copyright 53 | and Related Rights and associated claims and causes of action, whether now 54 | known or unknown (including existing as well as future claims and causes of 55 | action), in the Work (i) in all territories worldwide, (ii) for the maximum 56 | duration provided by applicable law or treaty (including future time 57 | extensions), (iii) in any current or future medium and for any number of 58 | copies, and (iv) for any purpose whatsoever, including without limitation 59 | commercial, advertising or promotional purposes (the “Waiver”). Affirmer 60 | makes the Waiver for the benefit of each member of the public at large and 61 | to the detriment of Affirmer’s heirs and successors, fully intending that 62 | such Waiver shall not be subject to revocation, rescission, cancellation, 63 | termination, or any other legal or equitable action to disrupt the quiet 64 | enjoyment of the Work by the public as contemplated by Affirmer’s express 65 | Statement of Purpose. 66 | 67 | 3. Public License Fallback. Should any part of the Waiver for any reason be 68 | judged legally invalid or ineffective under applicable law, then the Waiver 69 | shall be preserved to the maximum extent permitted taking into account 70 | Affirmer’s express Statement of Purpose. In addition, to the extent the 71 | Waiver is so judged Affirmer hereby grants to each affected person a 72 | royalty-free, non transferable, non sublicensable, non exclusive, 73 | irrevocable and unconditional license to exercise Affirmer’s Copyright and 74 | Related Rights in the Work (i) in all territories worldwide, (ii) for the 75 | maximum duration provided by applicable law or treaty (including future time 76 | extensions), (iii) in any current or future medium and for any number of 77 | copies, and (iv) for any purpose whatsoever, including without limitation 78 | commercial, advertising or promotional purposes (the “License”). The License 79 | shall be deemed effective as of the date CC0 was applied by Affirmer to the 80 | Work. Should any part of the License for any reason be judged legally 81 | invalid or ineffective under applicable law, such partial invalidity or 82 | ineffectiveness shall not invalidate the remainder of the License, and in 83 | such case Affirmer hereby affirms that he or she will not (i) exercise any 84 | of his or her remaining Copyright and Related Rights in the Work or (ii) 85 | assert any associated claims and causes of action with respect to the Work, 86 | in either case contrary to Affirmer’s express Statement of Purpose. 87 | 88 | 4. Limitations and Disclaimers. 89 | 1. No trademark or patent rights held by Affirmer are waived, abandoned, 90 | surrendered, licensed or otherwise affected by this document. 91 | 2. Affirmer offers the Work as-is and makes no representations or warranties 92 | of any kind concerning the Work, express, implied, statutory or 93 | otherwise, including without limitation warranties of title, 94 | merchantability, fitness for a particular purpose, non infringement, or 95 | the absence of latent or other defects, accuracy, or the present or 96 | absence of errors, whether or not discoverable, all to the greatest 97 | extent permissible under applicable law. 98 | 3. Affirmer disclaims responsibility for clearing rights of other persons 99 | that may apply to the Work or any use thereof, including without 100 | limitation any person’s Copyright and Related Rights in the Work. 101 | Further, Affirmer disclaims responsibility for obtaining any necessary 102 | consents, permissions or other rights required for any use of the Work. 103 | 4. Affirmer understands and acknowledges that Creative Commons is not a 104 | party to this document and has no duty or obligation with respect to this 105 | CC0 or use of the Work. 106 | 107 | For more information, please see 108 | http://creativecommons.org/publicdomain/zero/1.0/. 109 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PostCSS Advanced Variables [PostCSS Logo][postcss] 2 | 3 | [![NPM Version][npm-img]][npm-url] 4 | [![Build Status][cli-img]][cli-url] 5 | [![Test Status][test-img]][test-url] 6 | [![Support Chat][git-img]][git-url] 7 | 8 | [PostCSS Advanced Variables] lets you use Sass-like variables, conditionals, 9 | and iterators in CSS. 10 | 11 | ```scss 12 | $dir: assets/icons; 13 | 14 | @each $icon in (foo, bar, baz) { 15 | .icon-$icon { 16 | background: url('$dir/$icon.png'); 17 | } 18 | } 19 | 20 | @for $count from 1 to 5 by 2 { 21 | @if $count > 2 { 22 | .col-$count { 23 | width: #{$count}0%; 24 | } 25 | } 26 | } 27 | 28 | @import "path/to/some-file"; 29 | 30 | /* after */ 31 | 32 | .icon-foo { 33 | background: url('assets/icons/foo.png'); 34 | } 35 | 36 | .icon-bar { 37 | background: url('assets/icons/bar.png'); 38 | } 39 | 40 | .icon-baz { 41 | background: url('assets/icons/baz.png'); 42 | } 43 | 44 | .col-3 { 45 | width: 30%; 46 | } 47 | 48 | .col-5 { 49 | width: 50%; 50 | } 51 | 52 | // the contents of "path/to/_some-file.scss" 53 | ``` 54 | 55 | ## Usage 56 | 57 | Add [PostCSS Advanced Variables] to your build tool: 58 | 59 | ```bash 60 | npm install postcss-advanced-variables --save-dev 61 | ``` 62 | 63 | #### Node 64 | 65 | Use [PostCSS Advanced Variables] to process your CSS: 66 | 67 | ```js 68 | require('postcss-advanced-variables').process(YOUR_CSS); 69 | ``` 70 | 71 | #### PostCSS 72 | 73 | Add [PostCSS] to your build tool: 74 | 75 | ```bash 76 | npm install postcss --save-dev 77 | ``` 78 | 79 | Use [PostCSS Advanced Variables] as a plugin: 80 | 81 | ```js 82 | postcss([ 83 | require('postcss-advanced-variables')(/* options */) 84 | ]).process(YOUR_CSS); 85 | ``` 86 | 87 | #### Gulp 88 | 89 | Add [Gulp PostCSS] to your build tool: 90 | 91 | ```bash 92 | npm install gulp-postcss --save-dev 93 | ``` 94 | 95 | Use [PostCSS Advanced Variables] in your Gulpfile: 96 | 97 | ```js 98 | var postcss = require('gulp-postcss'); 99 | 100 | gulp.task('css', function () { 101 | return gulp.src('./src/*.css').pipe( 102 | postcss([ 103 | require('postcss-advanced-variables')(/* options */) 104 | ]) 105 | ).pipe( 106 | gulp.dest('.') 107 | ); 108 | }); 109 | ``` 110 | 111 | #### Grunt 112 | 113 | Add [Grunt PostCSS] to your build tool: 114 | 115 | ```bash 116 | npm install grunt-postcss --save-dev 117 | ``` 118 | 119 | Use [PostCSS Advanced Variables] in your Gruntfile: 120 | 121 | ```js 122 | grunt.loadNpmTasks('grunt-postcss'); 123 | 124 | grunt.initConfig({ 125 | postcss: { 126 | options: { 127 | use: [ 128 | require('postcss-advanced-variables')(/* options */) 129 | ] 130 | }, 131 | dist: { 132 | src: '*.css' 133 | } 134 | } 135 | }); 136 | ``` 137 | 138 | --- 139 | 140 | ## Features 141 | 142 | ### $variables 143 | 144 | Variables let you store information to be reused anywhere in a stylesheet. 145 | 146 | Variables are set just like CSS properties, placing a `$` symbol before the 147 | name of the variable (`$var-name`). They may also be set placing a `$` symbol 148 | before two parentheses wrapping the name of the variable (`$(var-name)`), or by 149 | wrapping the `$` symbol and variable name in curly braces preceeded by a hash 150 | (`#{$var-name}`). 151 | 152 | ```scss 153 | $font-size: 1.25em; 154 | $font-stack: "Helvetica Neue", sans-serif; 155 | $primary-color: #333; 156 | 157 | body { 158 | font: $font-size $(font-stack); 159 | color: #{$primary-color}; 160 | } 161 | ``` 162 | 163 | *Note: To use `#{$var-name}` without issues, you will need to include the 164 | [PostCSS SCSS Syntax]. 165 | 166 | In that example, `$font-size`, `$font-stack`, and `$primary-color` are replaced 167 | with their values. 168 | 169 | ```css 170 | body { 171 | font: 1.25em "Helvetica Neue", sans-serif; 172 | color: #333; 173 | } 174 | ``` 175 | 176 | ### @if and @else Rules 177 | 178 | Conditionals like `@if` and `@else` let you use rules in a stylesheet if they 179 | evaluate true or false. 180 | 181 | Conditionals are set by writing `@if` before the expression you want to 182 | evaluate. If the expression is true, then its contents are included in the 183 | stylesheet. If the expression is false, then its contents are not included, but 184 | the contents of an `@else` that follows it are included. 185 | 186 | ```scss 187 | $type: monster; 188 | 189 | p { 190 | @if $type == ocean { 191 | color: blue; 192 | } @else { 193 | color: black; 194 | } 195 | } 196 | ``` 197 | 198 | In that example, `$type === ocean` is false, so the `@if` contents are ignored 199 | and the `@else` contents are used. 200 | 201 | ```css 202 | p { 203 | color: black; 204 | } 205 | ``` 206 | 207 | ### @for and @each Rules 208 | 209 | Iterators like `@for` and `@each` let you repeat content in a stylesheet. 210 | 211 | A `@for` statement repeats by a numerical counter defined as a variable. 212 | 213 | It can be written as `@for $counter from through ` where 214 | `$counter` is the name of the iterating variable, `` is the number to 215 | start with, and `` is the number to finish with. 216 | 217 | It can also be written as `@for $counter from to ` where 218 | `$counter` is still the name of the counter variable, `` is still the 219 | number to start with, but `` is now the number to finish 220 | *before, but not include*. 221 | 222 | When `` is greater than ``, the counter will decrement instead of 223 | increment. 224 | 225 | Either form of `@for` can be written as 226 | `@for $var from to by ` or 227 | `@for $var from through by ` 228 | where `` is the amount the counter variable will advance. 229 | 230 | ```scss 231 | @for $i from 1 through 5 by 2 { 232 | .width-#{$i} { 233 | width: #{$i}0em; 234 | } 235 | } 236 | 237 | @for $j from 1 to 5 by 2 { 238 | .height-#{$j} { 239 | height: #{$j}0em; 240 | } 241 | } 242 | ``` 243 | 244 | In that example, `$i` is repeated from 1 through 5 by 2, which means it is 245 | repeated 3 times (1, 3, and 5). Meanwhile, `$j` is repeated from 1 to 5 by 2, 246 | which means it is repeated 2 times (1 and 3). 247 | 248 | ```css 249 | .width-1 { 250 | width: 10em; 251 | } 252 | 253 | .width-3 { 254 | width: 30em; 255 | } 256 | 257 | .width-5 { 258 | width: 50em; 259 | } 260 | 261 | .height-1 { 262 | height: 10em; 263 | } 264 | 265 | .height-3 { 266 | height: 30em; 267 | } 268 | ``` 269 | 270 | An `@each` statement statement repeats through a list of values. 271 | 272 | It can be written as `@each $item in $list` where `$item` is the 273 | name of the iterating variable and `$list` is the list of values being looped 274 | over. 275 | 276 | ```scss 277 | @each $animal in (puma, sea-slug, egret, salamander) { 278 | .#{$animal}-icon { 279 | background-image: url("images/icon-#{$animal}.svg"); 280 | } 281 | } 282 | ``` 283 | 284 | In that example, a list of 4 animals is looped over to create 4 unique 285 | classnames. 286 | 287 | ```css 288 | .puma-icon { 289 | background-image: url("images/icon-puma.svg"); 290 | } 291 | 292 | .sea-slug-icon { 293 | background-image: url("images/icon-sea-slug.svg"); 294 | } 295 | 296 | .egret-icon { 297 | background-image: url("images/icon-egret.svg"); 298 | } 299 | 300 | .salamander-icon { 301 | background-image: url("images/icon-salamander.svg"); 302 | } 303 | ``` 304 | 305 | It can also be written as `@each $item $counter in $list` where `$item` is 306 | still the name of the iterating variable and `$list` is still the list of values 307 | being looped over, but now `$counter` is the numerical counter. 308 | 309 | ```scss 310 | @each $animal $i in (puma, sea-slug, egret, salamander) { 311 | .#{$animal}-icon { 312 | background-image: url("images/icon-#{$i}.svg"); 313 | } 314 | } 315 | ``` 316 | 317 | ```css 318 | .puma-icon { 319 | background-image: url("images/icon-1.svg"); 320 | } 321 | 322 | .sea-slug-icon { 323 | background-image: url("images/icon-2.svg"); 324 | } 325 | 326 | .egret-icon { 327 | background-image: url("images/icon-3.svg"); 328 | } 329 | 330 | .salamander-icon { 331 | background-image: url("images/icon-4.svg"); 332 | } 333 | ``` 334 | 335 | In that example, a list of 4 animals is looped over to create 4 unique 336 | classnames. 337 | 338 | ### @mixin, @include, and @content rules 339 | 340 | Mixins let you reuse rule in a stylesheet. A `@mixin` defines the content you 341 | want to reuse, while an `@include` rule includes it anywhere in your stylesheet. 342 | 343 | Mixins are set by writing `@mixin` before the name of the mixin you define. 344 | This can be (optionally) followed by comma-separated variables you 345 | want to use inside of it. Mixins are then used anywhere by writing `@include` 346 | before the name of the mixin you are using. This is (again, optionally) 347 | followed by some comma-separated arguments you want to pass into the mixin as 348 | the (aforementioned) variables. 349 | 350 | ```scss 351 | @mixin heading-text { 352 | color: #242424; 353 | font-size: 4em; 354 | } 355 | 356 | h1, h2, h3 { 357 | @include heading-text; 358 | } 359 | 360 | .some-heading-component > :first-child { 361 | @include heading-text; 362 | } 363 | ``` 364 | 365 | In that example, `@include heading-text` is replaced with its contents. 366 | 367 | ```css 368 | h1, h2, h3 { 369 | color: #242424; 370 | font-size: 4em; 371 | } 372 | 373 | .some-heading-component > :first-child { 374 | color: #242424; 375 | font-size: 4em; 376 | } 377 | ``` 378 | 379 | Remember, mixins can be followed by comma-separated variables you 380 | want to pass into the mixin as variables. 381 | 382 | ```scss 383 | @mixin heading-text($color: #242424, $font-size: 4em) { 384 | color: $color; 385 | font-size: $font-size; 386 | } 387 | 388 | h1, h2, h3 { 389 | @include heading-text; 390 | } 391 | 392 | .some-heading-component > :first-child { 393 | @include heading-text(#111111, 6em); 394 | } 395 | ``` 396 | 397 | In that example, `@include heading-text` is replaced with its contents, but 398 | this time some of their contents are customized with variables. 399 | 400 | ```css 401 | h1, h2, h3 { 402 | color: #242424; 403 | font-size: 4em; 404 | } 405 | 406 | .some-heading-component > :first-child { 407 | color: #111111; 408 | font-size: 6em; 409 | } 410 | ``` 411 | 412 | --- 413 | 414 | ## Options 415 | 416 | ### variables 417 | 418 | The `variables` option defines global variables used when they cannot be 419 | resolved automatically. 420 | 421 | ```js 422 | require('postcss-advanced-variables')({ 423 | variables: { 424 | 'site-width': '960px' 425 | } 426 | }); 427 | ``` 428 | 429 | The `variables` option also accepts a function, which is given 2 arguments; the 430 | name of the unresolved variable, and the PostCSS node that used it. 431 | 432 | ```js 433 | require('postcss-advanced-variables')({ 434 | variables(name, node) { 435 | if (name === 'site-width') { 436 | return '960px'; 437 | } 438 | 439 | return undefined; 440 | } 441 | }); 442 | ``` 443 | 444 | ```scss 445 | .hero { 446 | max-width: $site-width; 447 | } 448 | 449 | /* after */ 450 | 451 | .hero { 452 | max-width: 960px; 453 | } 454 | ``` 455 | 456 | ### unresolved 457 | 458 | The `unresolved` option defines how unresolved variables, mixins, and imports 459 | should be handled. The available options are `throw`, `warn`, and `ignore`. The 460 | default option is to `throw`. 461 | 462 | ```js 463 | require('postcss-advanced-variables')({ 464 | unresolved: 'ignore' // ignore unresolved variables 465 | }); 466 | ``` 467 | 468 | ### disable 469 | 470 | The `disable` option defines which features should be disabled in 471 | [PostCSS Advanced Variables]. 472 | 473 | The `disable` option can be a string or an array, and the features that can be 474 | disabled are `@content`, `@each`, `@else`, `@if`, `@include`, `@import`, `@for`, 475 | and `@mixin`. 476 | 477 | ```js 478 | require('postcss-advanced-variables')({ 479 | disable: '@mixin, @include, @content' // ignore @mixin, @include, and @content at-rules 480 | }); 481 | ``` 482 | 483 | ### Import Options 484 | 485 | These options only apply to the `@import` at-rule. 486 | 487 | #### importPaths 488 | 489 | The `importPaths` option defines a path or multiple paths used to lookup 490 | files when they cannot be found automatically. 491 | 492 | The `importPaths` option can be a string or an array. 493 | 494 | By default, imports are resolved using the [Sass Import Resolve Specification]. 495 | 496 | ```js 497 | require('postcss-advanced-variables')({ 498 | importPaths: ['path/to/files', 'another/path/to/files'] 499 | }); 500 | ``` 501 | 502 | #### importResolve 503 | 504 | The `importResolve` option defines the file resolver used by imports. It is a 505 | function given 3 arguments; the url id, the current working directory, and the 506 | options processed by [PostCSS Advanced Variables]. 507 | 508 | The `importResolve` function should return a Promise with an object containing 509 | the full path (`file`) and the contents of the file (`contents`). 510 | 511 | ```js 512 | const resolve = require('custom-resolver'); 513 | 514 | require('postcss-advanced-variables')({ 515 | // a resolver may work many ways, and this is just an example 516 | importResolve: (id, cwd, opts) => resolve({ id, cwd }); 517 | }); 518 | ``` 519 | 520 | #### importFilter 521 | 522 | The `importFilter` option determines whether an import will be inlined. 523 | 524 | The value can be a function or an regular expression. When 525 | providing a function, it is called with a single string argument `id` 526 | and returns true when the import should be inlined. When providing a 527 | regular expression, if the `id` matches the expression, the import will 528 | be inlined. 529 | 530 | By default, imports are ignored if they begin with a protocol or 531 | protocol-relative slashes (`//`). 532 | 533 | ```js 534 | require('postcss-advanced-variables')({ 535 | importFilter: (id) => { 536 | return ['ignore', 'these', 'imports'].contains(id); 537 | } 538 | }); 539 | ``` 540 | 541 | #### importRoot 542 | 543 | The `importRoot` option defines the root directory used by imports when the 544 | current directory cannot be detected. Its default value is `process.cwd()`. 545 | 546 | ```js 547 | require('postcss-advanced-variables')({ 548 | importRoot: 'path/to/root' 549 | }); 550 | ``` 551 | 552 | #### importCache 553 | 554 | The `importCache` option defines a cache made available to the options object 555 | that may be used by the [file resolver](#importResolve). 556 | 557 | ```js 558 | const sharedCache = {}; 559 | 560 | require('postcss-advanced-variables')({ 561 | importCache: sharedCache 562 | }); 563 | ``` 564 | 565 | [cli-img]: https://img.shields.io/travis/jonathantneal/postcss-advanced-variables.svg 566 | [cli-url]: https://travis-ci.org/jonathantneal/postcss-advanced-variables 567 | [test-img]: https://github.com/csstools/postcss-advanced-variables/actions/workflows/test.yml/badge.svg 568 | [test-url]: https://github.com/csstools/postcss-advanced-variables/actions/workflows/test.yml 569 | [git-img]: https://img.shields.io/badge/chat-gitter-blue.svg 570 | [git-url]: https://gitter.im/postcss/postcss 571 | [npm-img]: https://img.shields.io/npm/v/postcss-advanced-variables.svg 572 | [npm-url]: https://www.npmjs.com/package/postcss-advanced-variables 573 | 574 | [Gulp PostCSS]: https://github.com/postcss/gulp-postcss 575 | [Grunt PostCSS]: https://github.com/nDmitry/grunt-postcss 576 | [PostCSS]: https://github.com/postcss/postcss 577 | [PostCSS Advanced Variables]: https://github.com/jonathantneal/postcss-advanced-variables 578 | [PostCSS SCSS Syntax]: https://github.com/postcss/postcss-scss 579 | [Sass Import Resolve Specification]: https://jonathantneal.github.io/sass-import-resolve/ 580 | --------------------------------------------------------------------------------