postcss-advanced-variables-mod
Version:
Use Sass-like variables, conditionals, and iterators in CSS
771 lines (621 loc) • 23.3 kB
JavaScript
;
function _interopDefault (ex) { return (ex && (typeof ex === 'object') && 'default' in ex) ? ex['default'] : ex; }
var postcss = require('postcss');
var postcss__default = _interopDefault(postcss);
var path = _interopDefault(require('path'));
var resolve = _interopDefault(require('sass-import-resolve-no-scope'));
// return the closest variable from a node
function getClosestVariable(name, node, opts) {
var variables = getVariables(node);
var variable = variables[name];
if (requiresAncestorVariable(variable, node)) {
variable = getClosestVariable(name, node.parent, opts);
}
if (requiresFnVariable(variable, opts)) {
variable = getFnVariable(name, node, opts.variables);
}
return variable;
}
// return the variables object of a node
var getVariables = function getVariables(node) {
return Object(Object(node).variables);
};
// return whether the variable should be replaced using an ancestor variable
var requiresAncestorVariable = function requiresAncestorVariable(variable, node) {
return undefined === variable && node && node.parent;
};
// return whether variable should be replaced using a variables function
var requiresFnVariable = function requiresFnVariable(value, opts) {
return value === undefined && Object(opts).variables === Object(Object(opts).variables);
};
// return whether variable should be replaced using a variables function
var getFnVariable = function getFnVariable(name, node, variables) {
return 'function' === typeof variables ? variables(name, node) : variables[name];
};
function manageUnresolved(node, opts, word, message) {
if ('warn' === opts.unresolved) {
node.warn(opts.result, message, { word });
} else if ('ignore' !== opts.unresolved) {
throw node.error(message, { word });
}
}
// tooling
// return content with its variables replaced by the corresponding values of a node
function getReplacedString(string, node, opts) {
var replacedString = string.replace(matchVariables, function (match, before, name1, name2, name3) {
// conditionally return an (unescaped) match
if (before === '\\') {
return match.slice(1);
}
// the first matching variable name
var name = name1 || name2 || name3;
// the closest variable value
var value = getClosestVariable(name, node.parent, opts);
// if a variable has not been resolved
if (undefined === value) {
manageUnresolved(node, opts, name, `Could not resolve the variable "$${name}" within "${string}"`);
return match;
}
// the stringified value
var stringifiedValue = `${before}${stringify(value)}`;
return stringifiedValue;
});
return replacedString;
}
// match all $name, $(name), and #{$name} variables (and catch the character before it)
var matchVariables = /(.?)(?:\$([A-z][\w-]*)|\$\(([A-z][\w-]*)\)|#\{\$([A-z][\w-]*)\})/g;
// return a sass stringified variable
var stringify = function stringify(object) {
return Array.isArray(object) ? `(${object.map(stringify).join(',')})` : Object(object) === object ? `(${Object.keys(object).map(function (key) {
return `${key}:${stringify(object[key])}`;
}).join(',')})` : String(object);
};
// tooling
// set a variable on a node
function setVariable(node, name, value, opts) {
// if the value is not a default with a value already defined
if (!matchDefault.test(value) || getClosestVariable(name, node, opts) === undefined) {
// the value without a default suffix
var undefaultedValue = matchDefault.test(value) ? value.replace(matchDefault, '') : value;
// ensure the node has a variables object
node.variables = node.variables || {};
// set the variable
node.variables[name] = undefaultedValue;
}
}
// match anything ending with a valid !default
var matchDefault = /\s+!default$/;
// tooling
// transform declarations
function transformDecl(decl, opts) {
// update the declaration value with its variables replaced by their corresponding values
decl.value = getReplacedString(decl.value, decl, opts);
// if the declaration is a variable declaration
if (isVariableDeclaration(decl)) {
// set the variable on the parent of the declaration
setVariable(decl.parent, decl.prop.slice(1), decl.value, opts);
// remove the declaration
decl.remove();
}
}
// return whether the declaration property is a variable declaration
var isVariableDeclaration = function isVariableDeclaration(decl) {
return matchVariable.test(decl.prop);
};
// match a variable ($any-name)
var matchVariable = /^\$[\w-]+$/;
// tooling
// transform generic at-rules
function transformAtrule(rule, opts) {
// update the at-rule params with its variables replaced by their corresponding values
rule.params = getReplacedString(rule.params, rule, opts);
}
var slicedToArray = function () {
function sliceIterator(arr, i) {
var _arr = [];
var _n = true;
var _d = false;
var _e = undefined;
try {
for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) {
_arr.push(_s.value);
if (i && _arr.length === i) break;
}
} catch (err) {
_d = true;
_e = err;
} finally {
try {
if (!_n && _i["return"]) _i["return"]();
} finally {
if (_d) throw _e;
}
}
return _arr;
}
return function (arr, i) {
if (Array.isArray(arr)) {
return arr;
} else if (Symbol.iterator in Object(arr)) {
return sliceIterator(arr, i);
} else {
throw new TypeError("Invalid attempt to destructure non-iterable instance");
}
};
}();
var toArray = function (arr) {
return Array.isArray(arr) ? arr : Array.from(arr);
};
var toConsumableArray = function (arr) {
if (Array.isArray(arr)) {
for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) arr2[i] = arr[i];
return arr2;
} else {
return Array.from(arr);
}
};
// tooling
// return sass-like arrays as literal arrays ('(hello), (goodbye)' to [[hello], [goodbye]])
function getValueAsObject(value) {
var hasWrappingParens = matchWrappingParens.test(value);
var unwrappedValue = String(hasWrappingParens ? value.replace(matchWrappingParens, '$1') : value).replace(matchTrailingComma, '');
var separatedValue = postcss.list.comma(unwrappedValue);
var firstValue = separatedValue[0];
if (firstValue === value) {
return value;
} else {
var objectValue = {};
var arrayValue = [];
separatedValue.forEach(function (subvalue, index) {
var _ref = subvalue.match(matchDeclaration) || [],
_ref2 = slicedToArray(_ref, 3),
match = _ref2[0],
key = _ref2[1],
keyvalue = _ref2[2];
if (match) {
objectValue[key] = getValueAsObject(keyvalue);
} else {
arrayValue[index] = getValueAsObject(subvalue);
}
});
var transformedValue = Object.keys(objectValue).length > 0 ? Object.assign(objectValue, arrayValue) : arrayValue;
return transformedValue;
}
}
// match wrapping parentheses ((), (anything), (anything (anything)))
var matchWrappingParens = /^\(([\W\w]*)\)$/g;
// match a property name (any-possible_name)
var matchDeclaration = /^([\w-]+)\s*:\s*([\W\w]+)\s*$/;
// match a trailing comma
var matchTrailingComma = /\s*,\s*$/;
var waterfall = (function (items, asyncFunction) {
return items.reduce(function (lastPromise, item) {
return lastPromise.then(function () {
return asyncFunction(item);
});
}, Promise.resolve());
});
// tooling
// transform @each at-rules
function transformEachAtrule(rule, opts) {
// if @each is supported
if (opts.transform.indexOf('@each') !== -1) {
// @each options
var _getEachOpts = getEachOpts(rule, opts),
varname = _getEachOpts.varname,
incname = _getEachOpts.incname,
list = _getEachOpts.list;
var replacements = [];
var ruleClones = [];
Object.keys(list).forEach(function (key) {
// set the current variable
setVariable(rule, varname, list[key], opts);
// conditionally set the incremenator variable
if (incname) {
setVariable(rule, incname, key, opts);
}
// clone the @each at-rule
var clone = rule.clone({
parent: rule.parent,
variables: Object.assign({}, rule.variables)
});
ruleClones.push(clone);
});
return waterfall(ruleClones, function (clone) {
return transformNode(clone, opts).then(function () {
replacements.push.apply(replacements, toConsumableArray(clone.nodes));
});
}).then(function () {
// replace the @each at-rule with the replacements
rule.parent.insertBefore(rule, replacements);
rule.remove();
});
}
}
// return the @each statement options (@each NAME in LIST, @each NAME ITERATOR in LIST)
var getEachOpts = function getEachOpts(node, opts) {
var params = node.params.split(matchInOperator);
var args = (params[0] || '').trim().split(' ');
var varname = args[0].trim().slice(1);
var incname = args.length > 1 && args[1].trim().slice(1);
var rawlist = getValueAsObject(getReplacedString(params.slice(1).join(matchInOperator), node, opts));
var list = 'string' === typeof rawlist ? [rawlist] : rawlist;
return { varname, incname, list };
};
// match the opertor separating the name and iterator from the list
var matchInOperator = ' in ';
// tooling
// transform @if at-rules
function transformIfAtrule(rule, opts) {
// @if options
var isTruthy = isIfTruthy(rule, opts);
var next = rule.next();
var transformAndInsertBeforeParent = function transformAndInsertBeforeParent(node) {
return transformNode(node, opts).then(function () {
return node.parent.insertBefore(node, node.nodes);
});
};
return ifPromise(opts.transform.indexOf('@if') !== -1, function () {
return ifPromise(isTruthy, function () {
return transformAndInsertBeforeParent(rule);
}).then(function () {
rule.remove();
});
}).then(function () {
return ifPromise(opts.transform.indexOf('@else') !== -1 && isElseRule(next), function () {
return ifPromise(!isTruthy, function () {
return transformAndInsertBeforeParent(next);
}).then(function () {
next.remove();
});
});
});
}
var ifPromise = function ifPromise(condition, trueFunction) {
return Promise.resolve(condition && trueFunction());
};
// return whether the @if at-rule is truthy
var isIfTruthy = function isIfTruthy(node, opts) {
// @if statement options (@if EXPRESSION, @if LEFT OPERATOR RIGHT)
var params = postcss.list.space(node.params);
var left = getInterprettedString(getReplacedString(params[0] || '', node, opts));
var operator = params[1];
var right = getInterprettedString(getReplacedString(params[2] || '', node, opts));
// evaluate the expression
var isTruthy = !operator && left || operator === '==' && left === right || operator === '!=' && left !== right || operator === '<' && left < right || operator === '<=' && left <= right || operator === '>' && left > right || operator === '>=' && left >= right;
return isTruthy;
};
// return the value as a boolean, number, or string
var getInterprettedString = function getInterprettedString(value) {
return 'true' === value ? true : 'false' === value ? false : isNaN(value) ? value : Number(value);
};
// return whether the node is an else at-rule
var isElseRule = function isElseRule(node) {
return Object(node) === node && 'atrule' === node.type && 'else' === node.name;
};
// tooling
// transform @import at-rules
function transformImportAtrule(rule, opts) {
// if @import is supported
if (opts.transform.indexOf('@import') !== -1) {
// @import options
var _getImportOpts = getImportOpts(rule, opts),
id = _getImportOpts.id,
media = _getImportOpts.media,
cwf = _getImportOpts.cwf,
cwd = _getImportOpts.cwd;
if (opts.importFilter instanceof Function && opts.importFilter(id, media) || opts.importFilter instanceof RegExp && opts.importFilter.test(id)) {
var cwds = [cwd].concat(opts.importPaths);
// promise the resolved file and its contents using the file resolver
var importPromise = cwds.reduce(function (promise, thiscwd) {
return promise.catch(function () {
return opts.importResolve(id, thiscwd, opts);
});
}, Promise.reject());
return importPromise.then(
// promise the processed file
function (_ref) {
var file = _ref.file,
contents = _ref.contents;
return processor.process(contents, { from: file }).then(function (_ref2) {
var root = _ref2.root;
// push a dependency message
opts.result.messages.push({ type: 'dependency', file: file, parent: cwf });
// imported nodes
var nodes = root.nodes.slice(0);
// if media params were detected
if (media) {
// create a new media rule
var mediaRule = postcss__default.atRule({
name: 'media',
params: media,
source: rule.source
});
// append with the imported nodes
mediaRule.append(nodes);
// replace the @import at-rule with the @media at-rule
rule.replaceWith(mediaRule);
} else {
// replace the @import at-rule with the imported nodes
rule.replaceWith(nodes);
}
// transform all nodes from the import
return transformNode({ nodes }, opts);
});
}, function () {
// otherwise, if the @import could not be found
manageUnresolved(rule, opts, '@import', `Could not resolve the @import for "${id}"`);
});
}
}
}
var processor = postcss__default();
// return the @import statement options (@import ID, @import ID MEDIA)
var getImportOpts = function getImportOpts(node, opts) {
var _list$space = postcss.list.space(node.params),
_list$space2 = toArray(_list$space),
rawid = _list$space2[0],
medias = _list$space2.slice(1);
var id = getReplacedString(trimWrappingURL(rawid), node, opts);
var media = medias.join(' ');
// current working file and directory
var cwf = node.source && node.source.input && node.source.input.file || opts.result.from;
var cwd = cwf ? path.dirname(cwf) : opts.importRoot;
return { id, media, cwf, cwd };
};
// return a string with the wrapping url() and quotes trimmed
var trimWrappingURL = function trimWrappingURL(string) {
return trimWrappingQuotes(string.replace(/^url\(([\W\w]*)\)$/, '$1'));
};
// return a string with the wrapping quotes trimmed
var trimWrappingQuotes = function trimWrappingQuotes(string) {
return string.replace(/^("|')([\W\w]*)\1$/, '$2');
};
// tooling
// transform @include at-rules
function transformIncludeAtrule(rule, opts) {
// if @include is supported
if (opts.transform.indexOf('@include') !== -1) {
// @include options
var _getIncludeOpts = getIncludeOpts(rule),
name = _getIncludeOpts.name,
args = _getIncludeOpts.args;
// the closest @mixin variable
var mixin = getClosestVariable(`@mixin ${name}`, rule.parent, opts);
// if the @mixin variable exists
if (mixin) {
// set @mixin variables on the @include at-rule
mixin.params.forEach(function (param, index) {
var arg = index in args ? getReplacedString(args[index], rule, opts) : param.value;
setVariable(rule, param.name, arg, opts);
});
// clone the @mixin at-rule
var clone = mixin.rule.clone({
original: rule,
parent: rule.parent,
variables: rule.variables
});
// transform the clone children
return transformNode(clone, opts).then(function () {
// replace the @include at-rule with the clone children
rule.parent.insertBefore(rule, clone.nodes);
rule.remove();
});
} else {
// otherwise, if the @mixin variable does not exist
manageUnresolved(rule, opts, name, `Could not resolve the mixin for "${name}"`);
}
}
}
// return the @include statement options (@include NAME, @include NAME(ARGS))
var getIncludeOpts = function getIncludeOpts(node) {
// @include name and args
var _node$params$split = node.params.split(matchOpeningParen, 2),
_node$params$split2 = slicedToArray(_node$params$split, 2),
name = _node$params$split2[0],
sourceArgs = _node$params$split2[1];
var args = sourceArgs ? postcss.list.comma(sourceArgs.slice(0, -1)) : [];
return { name, args };
};
// match an opening parenthesis
var matchOpeningParen = '(';
// tooling
// transform @for at-rules
function transformForAtrule(rule, opts) {
// if @for is supported
if (opts.transform.indexOf('@for') !== -1) {
// @for options
var _getForOpts = getForOpts(rule, opts),
varname = _getForOpts.varname,
start = _getForOpts.start,
end = _getForOpts.end,
increment = _getForOpts.increment;
var direction = start <= end ? 1 : -1;
var replacements = [];
var ruleClones = [];
// for each iteration
for (var incrementor = start; incrementor * direction <= end * direction; incrementor += increment * direction) {
// set the current variable
setVariable(rule, varname, incrementor, opts);
// clone the @for at-rule
var clone = rule.clone({
parent: rule.parent,
variables: Object.assign({}, rule.variables)
});
ruleClones.push(clone);
}
return waterfall(ruleClones, function (clone) {
return transformNode(clone, opts).then(function () {
replacements.push.apply(replacements, toConsumableArray(clone.nodes));
});
}).then(function () {
// replace the @for at-rule with the replacements
rule.parent.insertBefore(rule, replacements);
rule.remove();
});
}
}
// return the @for statement options (@for NAME from START through END, @for NAME from START through END by INCREMENT)
var getForOpts = function getForOpts(node, opts) {
var params = postcss.list.space(node.params);
var varname = params[0].trim().slice(1);
var start = Number(getReplacedString(params[2], node, opts));
var end = Number(getReplacedString(params[4], node, opts));
var increment = 6 in params && Number(getReplacedString(params[6], node, opts)) || 1;
return { varname, start, end, increment };
};
// tooling
// transform @mixin at-rules
function transformMixinAtrule(rule, opts) {
// if @mixin is supported
if (opts.transform.indexOf('@mixin') !== -1) {
// @mixin options
var _getMixinOpts = getMixinOpts(rule),
name = _getMixinOpts.name,
params = _getMixinOpts.params;
// set the mixin as a variable on the parent of the @mixin at-rule
setVariable(rule.parent, `@mixin ${name}`, { params, rule }, opts);
// remove the @mixin at-rule
rule.remove();
}
}
// return the @mixin statement options (@mixin NAME, @mixin NAME(PARAMS))
var getMixinOpts = function getMixinOpts(node) {
// @mixin name and default params ([{ name, value }, ...])
var _node$params$split = node.params.split(matchOpeningParen$1, 2),
_node$params$split2 = slicedToArray(_node$params$split, 2),
name = _node$params$split2[0],
sourceParams = _node$params$split2[1];
var params = sourceParams && sourceParams.slice(0, -1).trim() ? postcss.list.comma(sourceParams.slice(0, -1).trim()).map(function (param) {
var parts = postcss.list.split(param, ':');
var paramName = parts[0].slice(1);
var paramValue = parts.length > 1 ? parts.slice(1).join(':') : undefined;
return { name: paramName, value: paramValue };
}) : [];
return { name, params };
};
// match an opening parenthesis
var matchOpeningParen$1 = '(';
// tooling
// transform rule nodes
function transformRule(rule, opts) {
// update the rule selector with its variables replaced by their corresponding values
rule.selector = getReplacedString(rule.selector, rule, opts);
}
// tooling
// transform @content at-rules
function transformContentAtrule(rule, opts) {
// if @content is supported
if (opts.transform.indexOf('@content') !== -1) {
// the closest @mixin at-rule
var mixin = getClosestMixin(rule);
// if the @mixin at-rule exists
if (mixin) {
// clone the @mixin at-rule
var clone = mixin.original.clone({
parent: rule.parent,
variables: rule.variables
});
// transform the clone children
return transformNode(clone, opts).then(function () {
// replace the @content at-rule with the clone children
rule.parent.insertBefore(rule, clone.nodes);
rule.remove();
});
} else {
// otherwise, if the @mixin at-rule does not exist
manageUnresolved(rule, opts, '@content', 'Could not resolve the mixin for @content');
}
}
}
// return the closest @mixin at-rule
var getClosestMixin = function getClosestMixin(node) {
return 'atrule' === node.type && 'mixin' === node.name ? node : node.parent && getClosestMixin(node.parent);
};
// tooling
function transformNode(node, opts) {
return waterfall(getNodesArray(node), function (child) {
return transformRuleOrDecl(child, opts).then(function () {
// conditionally walk the children of the attached child
if (child.parent) {
return transformNode(child, opts);
}
});
});
}
function transformRuleOrDecl(child, opts) {
return Promise.resolve().then(function () {
var type = child.type;
if ('atrule' === type) {
var name = child.name.toLowerCase();
if ('content' === name) {
// transform @content at-rules
return transformContentAtrule(child, opts);
} else if ('each' === name) {
// transform @each at-rules
return transformEachAtrule(child, opts);
} else if ('if' === name) {
// transform @if at-rules
return transformIfAtrule(child, opts);
} else if ('import' === name) {
return transformImportAtrule(child, opts);
} else if ('include' === name) {
// transform @include at-rules
return transformIncludeAtrule(child, opts);
} else if ('for' === name) {
// transform @for at-rules
return transformForAtrule(child, opts);
} else if ('mixin' === name) {
// transform mixin at-rules
return transformMixinAtrule(child, opts);
} else {
// transform all other at-rules
return transformAtrule(child, opts);
}
} else if ('decl' === type) {
// transform declarations
return transformDecl(child, opts);
} else if ('rule' === type) {
// transform rule
return transformRule(child, opts);
}
});
}
// return the children of a node as an array
var getNodesArray = function getNodesArray(node) {
return Array.from(Object(node).nodes || []);
};
// tooling
// plugin
var index = postcss__default.plugin('postcss-advanced-variables', function (opts) {
return function (root, result) {
// process options
var transformOpt = ['@content', '@each', '@else', '@if', '@include', '@import', '@for', '@mixin'].filter(function (feature) {
return !(String(Object(opts).disable || '').split(/\s*,\s*|\s+,?\s*|\s,?\s+/).indexOf(feature) !== -1);
});
var unresolvedOpt = String(Object(opts).unresolved || 'throw').toLowerCase();
var variablesOpt = Object(opts).variables;
var importCache = Object(Object(opts).importCache);
var importFilter = Object(opts).filter || function (id) {
return !matchProtocol.test(id);
};
var importPaths = [].concat(Object(opts).importPaths || []);
var importResolve = Object(opts).resolve || function (id, cwd) {
return resolve(id, { cwd, readFile: true, cache: importCache });
};
var importRoot = Object(opts).importRoot || process.cwd();
return transformNode(root, {
result,
importCache,
importFilter,
importPaths,
importResolve,
importRoot,
transform: transformOpt,
unresolved: unresolvedOpt,
variables: variablesOpt
});
};
});
var matchProtocol = /^(?:[A-z]+:)?\/\//;
module.exports = index;