diff --git a/index.js b/index.js index 70c8282..23534e4 100644 --- a/index.js +++ b/index.js @@ -6,288 +6,287 @@ // For Debugging //var nomo = require('node-monkey').start({port: 50501}); -var postcss = require('postcss'); -var extend = require('extend'); - -var shallowCloneNode = require('./lib/shallow-clone-node'); -var resolveValue = require('./lib/resolve-value'); -var resolveDecl = require('./lib/resolve-decl'); +var postcss = require("postcss"); +var extend = require("extend"); +var shallowCloneNode = require("./lib/shallow-clone-node"); +var resolveValue = require("./lib/resolve-value"); +var resolveDecl = require("./lib/resolve-decl"); // A custom property is any property whose name starts with two dashes (U+002D HYPHEN-MINUS) // `--foo` // See: http://dev.w3.org/csswg/css-variables/#custom-property -var RE_VAR_PROP = (/(--(.+))/); - - - +var RE_VAR_PROP = /(--(.+))/; function eachCssVariableDeclaration(css, cb) { - // Loop through all of the declarations and grab the variables and put them in the map - css.walkDecls(function(decl) { - // If declaration is a variable - if(RE_VAR_PROP.test(decl.prop)) { - cb(decl); - } - }); + // Loop through all of the declarations and grab the variables and put them in the map + css.walkDecls(function(decl) { + // If declaration is a variable + if (RE_VAR_PROP.test(decl.prop)) { + cb(decl); + } + }); } - - function cleanUpNode(node) { - // If we removed all of the declarations in the rule(making it empty), - // then just remove it - var nodeToPossiblyCleanUp = node; - while(nodeToPossiblyCleanUp && nodeToPossiblyCleanUp.nodes.length <= 0) { - var nodeToRemove = nodeToPossiblyCleanUp.type !== 'root' ? nodeToPossiblyCleanUp : null; - - if(nodeToRemove) { - // Get a reference to it before we remove - // and lose reference to the child after removing it - nodeToPossiblyCleanUp = nodeToRemove.parent; - - nodeToRemove.remove(); - } - else { - nodeToPossiblyCleanUp = null; - } - } + // If we removed all of the declarations in the rule(making it empty), + // then just remove it + var nodeToPossiblyCleanUp = node; + while (nodeToPossiblyCleanUp && nodeToPossiblyCleanUp.nodes.length <= 0) { + var nodeToRemove = + nodeToPossiblyCleanUp.type !== "root" ? nodeToPossiblyCleanUp : null; + + if (nodeToRemove) { + // Get a reference to it before we remove + // and lose reference to the child after removing it + nodeToPossiblyCleanUp = nodeToRemove.parent; + + nodeToRemove.remove(); + } else { + nodeToPossiblyCleanUp = null; + } + } } - var defaults = { - // Allows you to preserve custom properties & var() usage in output. - // `true`, `false`, or `'computed'` - preserve: false, - // Define variables via JS - // Simple key-value pair - // or an object with a `value` property and an optional `isImportant` bool property - variables: {}, - // Preserve variables injected via JS with the `variables` option above - // before serializing to CSS (`false` will remove these variables from output) - preserveInjectedVariables: true, - // Will write media queries in the same order as in the original file. - // Currently defaulted to false for legacy behavior. We can update to `true` in a major version - preserveAtRulesOrder: false + // Allows you to preserve custom properties & var() usage in output. + // `true`, `false`, or `'computed'` + preserve: false, + // Define variables via JS + // Simple key-value pair + // or an object with a `value` property and an optional `isImportant` bool property + variables: {}, + // Preserve variables injected via JS with the `variables` option above + // before serializing to CSS (`false` will remove these variables from output) + preserveInjectedVariables: true, + // Will write media queries in the same order as in the original file. + // Currently defaulted to false for legacy behavior. We can update to `true` in a major version + preserveAtRulesOrder: false }; -module.exports = postcss.plugin('postcss-css-variables', function(options) { - - var opts = extend({}, defaults, options); +module.exports = postcss.plugin("postcss-css-variables", function(options) { + var opts = extend({}, defaults, options); - // Work with opts here + // Work with opts here - return function (css, result) { - // Transform CSS AST here + return function(css, result) { + // Transform CSS AST here - /* * / + /* * / try { /* */ - // List of nodes that if empty, will be removed - // We use this because we don't want to modify the AST when we still need to reference these later on - var nodesToRemoveAtEnd = []; - - // Keep track of the injected from `opts.variables` to remove at the end - // if user passes `opts.preserveInjectedVariables = false` - var injectedDeclsToRemoveAtEnd = []; - - // Map of variable names to a list of declarations - var map = {}; - - // Add the js defined variables `opts.variables` to the map - map = extend( - map, - Object.keys(opts.variables).reduce(function(prevVariableMap, variableName) { - var variableEntry = opts.variables[variableName]; - // Automatically prefix any variable with `--` (CSS custom property syntax) if it doesn't have it already - variableName = variableName.slice(0, 2) === '--' ? variableName : '--' + variableName; - var variableValue = (variableEntry || {}).value || variableEntry; - var isImportant = (variableEntry || {}).isImportant || false; - - - // Add a root node to the AST - var variableRootRule = postcss.rule({ selector: ':root' }); - css.root().prepend(variableRootRule); - // Add the variable decl to the root node - var varDecl = postcss.decl({ - prop: variableName, - value: variableValue, - important: isImportant - }); - variableRootRule.append(varDecl); - - // Collect JS-injected variables for removal if `opts.preserveInjectedVariables = false` - if (!opts.preserveInjectedVariables) { - injectedDeclsToRemoveAtEnd.push(varDecl); - } - - // Add the entry to the map - prevVariableMap[variableName] = (prevVariableMap[variableName] || []).concat({ - decl: varDecl, - prop: variableName, - calculatedInPlaceValue: variableValue, - isImportant: isImportant, - variablesUsed: [], - parent: variableRootRule, - isUnderAtRule: false - }); - - return prevVariableMap; - }, {}) - ); - - - // Chainable helper function to log any messages (warnings) - var logResolveValueResult = function(valueResult) { - // Log any warnings that might of popped up - var warningList = [].concat(valueResult.warnings); - warningList.forEach(function(warningArgs) { - warningArgs = [].concat(warningArgs); - result.warn.apply(result, warningArgs); - }); - - // Keep the chain going - return valueResult; - }; - - - // Collect all of the variables defined - // --------------------------------------------------------- - // --------------------------------------------------------- - //console.log('Collecting variables defined START'); - eachCssVariableDeclaration(css, function(decl) { - var declParentRule = decl.parent; - - var valueResults = logResolveValueResult(resolveValue(decl, map)); - // Split out each selector piece into its own declaration for easier logic down the road - decl.parent.selectors.forEach(function(selector) { - // Create a detached clone - var splitOutRule = shallowCloneNode(decl.parent); - splitOutRule.selector = selector; - splitOutRule.parent = decl.parent.parent; - - var declClone = decl.clone(); - splitOutRule.append(declClone); - - var prop = decl.prop; - map[prop] = (map[prop] || []).concat({ - decl: declClone, - prop: prop, - calculatedInPlaceValue: valueResults.value, - isImportant: decl.important || false, - variablesUsed: valueResults.variablesUsed, - parent: splitOutRule, - // variables inside root or at-rules (eg. @media, @support) - isUnderAtRule: splitOutRule.parent.type === 'atrule' - }); - }); - - let preserveDecl; - if (typeof opts.preserve === "function") { - preserveDecl = opts.preserve(decl); - } else { - preserveDecl = opts.preserve; - } - // Remove the variable declaration because they are pretty much useless after we resolve them - if(!preserveDecl) { - decl.remove(); - } - // Or we can also just show the computed value used for that variable - else if(preserveDecl === 'computed') { - decl.value = valueResults.value; - } - // Otherwise just keep them as var declarations - //else {} - - // We add to the clean up list if we removed some variable declarations to make it become an empty rule - // We clean up later on because we don't want to modify the AST when we still need to reference these later on - if(declParentRule.nodes.length <= 0) { - nodesToRemoveAtEnd.push(declParentRule); - } - }); - //console.log('Collecting variables defined END'); - - - - - - // Resolve variables everywhere - // --------------------------------------------------------- - // --------------------------------------------------------- - - // Collect all the rules that have declarations that use variables - var rulesThatHaveDeclarationsWithVariablesList = []; - css.walkRules(function(rule) { - var doesRuleUseVariables = rule.nodes.some(function(node) { - if(node.type === 'decl') { - var decl = node; - // If it uses variables - // and is not a variable declarations that we may be preserving from earlier - if(resolveValue.RE_VAR_FUNC.test(decl.value) && !RE_VAR_PROP.test(decl.prop)) { - return true; - } - } - - return false; - }); - - if(doesRuleUseVariables) { - rulesThatHaveDeclarationsWithVariablesList.push(rule); - } - }); - - rulesThatHaveDeclarationsWithVariablesList.forEach(function(rule) { - var rulesToWorkOn = [].concat(rule); - // Split out the rule into each comma separated selector piece - // We only need to split if is actually comma separted(selectors > 1) - if(rule.selectors.length > 1) { - // Reverse the selectors so that we can cloneAfter in the same comma separated order - rulesToWorkOn = rule.selectors.reverse().map(function(selector) { - var ruleClone = rule.cloneAfter(); - ruleClone.selector = selector; - - return ruleClone; - }); - - rule.remove(); - } - - // Resolve the declarations - rulesToWorkOn.forEach(function(ruleToWorkOn) { - ruleToWorkOn.nodes.slice(0).forEach(function(node) { - if(node.type === 'decl') { - var decl = node; - resolveDecl(decl, map, opts.preserve, opts.preserveAtRulesOrder, logResolveValueResult); - } - }); - }); - - }); - - - - - - // Clean up any nodes we don't want anymore - // We clean up at the end because we don't want to modify the AST when we still need to reference these later on - nodesToRemoveAtEnd.forEach(cleanUpNode); - - // Clean up JS-injected variables marked for removal - injectedDeclsToRemoveAtEnd.forEach(function(injectedDecl) { - injectedDecl.remove(); - }); - - - //console.log('map', map); - - /* * / + // List of nodes that if empty, will be removed + // We use this because we don't want to modify the AST when we still need to reference these later on + var nodesToRemoveAtEnd = []; + + // Keep track of the injected from `opts.variables` to remove at the end + // if user passes `opts.preserveInjectedVariables = false` + var injectedDeclsToRemoveAtEnd = []; + + // Map of variable names to a list of declarations + var map = {}; + + // Add the js defined variables `opts.variables` to the map + map = extend( + map, + Object.keys(opts.variables).reduce(function( + prevVariableMap, + variableName + ) { + var variableEntry = opts.variables[variableName]; + // Automatically prefix any variable with `--` (CSS custom property syntax) if it doesn't have it already + variableName = + variableName.slice(0, 2) === "--" + ? variableName + : "--" + variableName; + var variableValue = (variableEntry || {}).value || variableEntry; + var isImportant = (variableEntry || {}).isImportant || false; + + // Add a root node to the AST + var variableRootRule = postcss.rule({ selector: ":root" }); + css.root().prepend(variableRootRule); + // Add the variable decl to the root node + var varDecl = postcss.decl({ + prop: variableName, + value: variableValue, + important: isImportant + }); + variableRootRule.append(varDecl); + + // Collect JS-injected variables for removal if `opts.preserveInjectedVariables = false` + if (!opts.preserveInjectedVariables) { + injectedDeclsToRemoveAtEnd.push(varDecl); + } + + // Add the entry to the map + prevVariableMap[variableName] = ( + prevVariableMap[variableName] || [] + ).concat({ + decl: varDecl, + prop: variableName, + calculatedInPlaceValue: variableValue, + isImportant: isImportant, + variablesUsed: [], + parent: variableRootRule, + isUnderAtRule: false + }); + + return prevVariableMap; + }, + {}) + ); + + // Chainable helper function to log any messages (warnings) + var logResolveValueResult = function(valueResult) { + // Log any warnings that might of popped up + var warningList = [].concat(valueResult.warnings); + warningList.forEach(function(warningArgs) { + warningArgs = [].concat(warningArgs); + result.warn.apply(result, warningArgs); + }); + + // Keep the chain going + return valueResult; + }; + + // Collect all of the variables defined + // --------------------------------------------------------- + // --------------------------------------------------------- + //console.log('Collecting variables defined START'); + eachCssVariableDeclaration(css, function(decl) { + var declParentRule = decl.parent; + + var valueResults = logResolveValueResult(resolveValue(decl, map)); + // Split out each selector piece into its own declaration for easier logic down the road + decl.parent.selectors.forEach(function(selector) { + // Create a detached clone + var splitOutRule = shallowCloneNode(decl.parent); + splitOutRule.selector = selector; + splitOutRule.parent = decl.parent.parent; + + var declClone = decl.clone(); + splitOutRule.append(declClone); + + var prop = decl.prop; + map[prop] = (map[prop] || []).concat({ + decl: declClone, + prop: prop, + calculatedInPlaceValue: valueResults.value, + isImportant: decl.important || false, + variablesUsed: valueResults.variablesUsed, + parent: splitOutRule, + // variables inside root or at-rules (eg. @media, @support) + isUnderAtRule: splitOutRule.parent.type === "atrule" + }); + }); + + let preserveDecl; + if (typeof opts.preserve === "function") { + preserveDecl = opts.preserve(decl); + } else { + preserveDecl = opts.preserve; + } + // Remove the variable declaration because they are pretty much useless after we resolve them + if (!preserveDecl) { + decl.remove(); + } + // Or we can also just show the computed value used for that variable + else if (preserveDecl === "computed") { + decl.value = valueResults.value; + } + // Otherwise just keep them as var declarations + //else {} + + // We add to the clean up list if we removed some variable declarations to make it become an empty rule + // We clean up later on because we don't want to modify the AST when we still need to reference these later on + if (declParentRule.nodes.length <= 0) { + nodesToRemoveAtEnd.push(declParentRule); + } + }); + //console.log('Collecting variables defined END'); + + // Resolve variables everywhere + // --------------------------------------------------------- + // --------------------------------------------------------- + + // Collect all the rules that have declarations that use variables + var rulesThatHaveDeclarationsWithVariablesList = []; + css.walk(function(rule) { + // We're only interested in Containers with children. + if (rule.nodes === undefined) return; + + var doesRuleUseVariables = rule.nodes.some(function(node) { + if (node.type === "decl") { + var decl = node; + // If it uses variables + // and is not a variable declarations that we may be preserving from earlier + if ( + resolveValue.RE_VAR_FUNC.test(decl.value) && + !RE_VAR_PROP.test(decl.prop) + ) { + return true; + } + } + + return false; + }); + + if (doesRuleUseVariables) { + rulesThatHaveDeclarationsWithVariablesList.push(rule); + } + }); + + rulesThatHaveDeclarationsWithVariablesList.forEach(function(rule) { + var rulesToWorkOn = [].concat(rule); + // Split out the rule into each comma separated selector piece + // We only need to split if it's actually a Rule with multiple selectors (comma separated) + if (rule.type === "rule" && rule.selectors.length > 1) { + // Reverse the selectors so that we can cloneAfter in the same comma separated order + rulesToWorkOn = rule.selectors.reverse().map(function(selector) { + var ruleClone = rule.cloneAfter(); + ruleClone.selector = selector; + + return ruleClone; + }); + + rule.remove(); + } + + // Resolve the declarations + rulesToWorkOn.forEach(function(ruleToWorkOn) { + ruleToWorkOn.nodes.slice(0).forEach(function(node) { + if (node.type === "decl") { + var decl = node; + resolveDecl( + decl, + map, + opts.preserve, + opts.preserveAtRulesOrder, + logResolveValueResult + ); + } + }); + }); + }); + + // Clean up any nodes we don't want anymore + // We clean up at the end because we don't want to modify the AST when we still need to reference these later on + nodesToRemoveAtEnd.forEach(cleanUpNode); + + // Clean up JS-injected variables marked for removal + injectedDeclsToRemoveAtEnd.forEach(function(injectedDecl) { + injectedDecl.remove(); + }); + + //console.log('map', map); + + /* * / } catch(e) { //console.log('e', e.message); console.log('e', e.message, e.stack); } /* */ - - }; + }; }); diff --git a/test/fixtures/at-rules-containing-properties.css b/test/fixtures/at-rules-containing-properties.css new file mode 100644 index 0000000..93e895b --- /dev/null +++ b/test/fixtures/at-rules-containing-properties.css @@ -0,0 +1,8 @@ +:root { + --font-name: 'my-font-family-name'; +} + +@font-face { + font-family: var(--font-name); + src: url('myfont.woff2') format('woff2'); +} diff --git a/test/fixtures/at-rules-containing-properties.expected.css b/test/fixtures/at-rules-containing-properties.expected.css new file mode 100644 index 0000000..cc99efc --- /dev/null +++ b/test/fixtures/at-rules-containing-properties.expected.css @@ -0,0 +1,4 @@ +@font-face { + font-family: 'my-font-family-name'; + src: url('myfont.woff2') format('woff2'); +} diff --git a/test/fixtures/nested-at-rules-containing-properties.css b/test/fixtures/nested-at-rules-containing-properties.css new file mode 100644 index 0000000..54ee93f --- /dev/null +++ b/test/fixtures/nested-at-rules-containing-properties.css @@ -0,0 +1,13 @@ +:root { + --color: red; +} + +/* + Prince XML at-rules. + https://www.princexml.com/doc/11/at-rules/ +*/ +@page { + @footnote { + background-color: var(--color); + } +} diff --git a/test/fixtures/nested-at-rules-containing-properties.expected.css b/test/fixtures/nested-at-rules-containing-properties.expected.css new file mode 100644 index 0000000..2371346 --- /dev/null +++ b/test/fixtures/nested-at-rules-containing-properties.expected.css @@ -0,0 +1,9 @@ +/* + Prince XML at-rules. + https://www.princexml.com/doc/11/at-rules/ +*/ +@page { + @footnote { + background-color: red; + } +} diff --git a/test/test.js b/test/test.js index a3704fa..45a2f33 100644 --- a/test/test.js +++ b/test/test.js @@ -175,6 +175,15 @@ describe("postcss-css-variables", function() { { preserveAtRulesOrder: true } ); + test( + "should work with at-rules containing properties", + "at-rules-containing-properties" + ); + test( + "should work with nested at-rules containing properties", + "nested-at-rules-containing-properties" + ); + test("should cascade to nested rules", "cascade-on-nested-rules"); test(