postcss-css-variables
Version:
PostCSS plugin to transform CSS Custom Properties(CSS variables) syntax into a static representation
295 lines (252 loc) • 10.3 kB
JavaScript
// PostCSS CSS Variables (postcss-css-variables)
// v0.5.0
//
// https://github.com/MadLittleMods/postcss-css-variables
// For Debugging
//var nomo = require('node-monkey').start({port: 50501});
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 = /(--(.+))/;
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);
}
});
}
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;
}
}
}
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
};
module.exports = (options = {}) => {
var opts = extend({}, defaults, options);
// Work with opts here
return {
postcssPlugin: 'postcss-css-variables',
Once(css, { decl, result, rule }) {
// 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 = rule({ selector: ":root" });
css.root().prepend(variableRootRule);
// Add the variable decl to the root node
var varDecl = 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) {
if (rule.type === "rule" && rule.selectors.length > 1) {
// 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)
// duplicate rules would be probably merged with cssnano (cannot be sure about nested)
rule.selectors.reverse().forEach(function(selector) {
var ruleClone = rule.cloneAfter();
ruleClone.selector = selector;
return ruleClone;
});
// Rules will be added to list in the next traverse
rule.remove();
} else {
rulesThatHaveDeclarationsWithVariablesList.push(rule);
}
}
});
rulesThatHaveDeclarationsWithVariablesList.forEach(function(rule) {
// Resolve the declarations
rule.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);
}
/* */
}
};
};
module.exports.postcss = true;