postcss-apply
Version:
PostCSS plugin enabling custom properties sets references
262 lines (204 loc) • 6.57 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 balanced = _interopDefault(require('balanced-match'));
function _defineProperty(obj, key, value) {
if (key in obj) {
Object.defineProperty(obj, key, {
value: value,
enumerable: true,
configurable: true,
writable: true
});
} else {
obj[key] = value;
}
return obj;
}
function _objectSpread(target) {
for (var i = 1; i < arguments.length; i++) {
var source = arguments[i] != null ? arguments[i] : {};
var ownKeys = Object.keys(source);
if (typeof Object.getOwnPropertySymbols === 'function') {
ownKeys = ownKeys.concat(Object.getOwnPropertySymbols(source).filter(function (sym) {
return Object.getOwnPropertyDescriptor(source, sym).enumerable;
}));
}
ownKeys.forEach(function (key) {
_defineProperty(target, key, source[key]);
});
}
return target;
}
function kebabify(prop) {
const upperToHyphen = (match, offset, string) => {
const addDash = offset && string.charAt(offset - 1) !== '-';
return (addDash ? '-' : '') + match.toLowerCase();
};
return prop.replace(/[A-Z]/g, upperToHyphen);
}
const isPlainObject = arg => Object.prototype.toString.call(arg) === '[object Object]';
const RE_PROP_SET = /^(--)([\w-]+)(\s*)([:]?)$/;
class Visitor {
constructor(options) {
_defineProperty(this, "cache", {});
_defineProperty(this, "result", {});
_defineProperty(this, "options", {});
_defineProperty(this, "defaults", {
preserve: false,
sets: {}
});
_defineProperty(this, "prepend", () => {
const {
sets
} = this.options;
Object.keys(sets).forEach(setName => {
const newRule = postcss__default.rule({
selector: `--${setName}`
});
const set = sets[setName];
if (typeof set === 'string') {
newRule.prepend(set);
} else if (isPlainObject(set)) {
Object.entries(set).forEach(([prop, value]) => {
newRule.prepend(postcss__default.decl({
prop: kebabify(prop),
value
}));
});
} else {
throw new Error(`Unrecognized set type \`${typeof set}\`, must be an object or string.`);
}
this.cache[setName] = newRule;
});
});
_defineProperty(this, "collect", rule => {
const matches = RE_PROP_SET.exec(rule.selector);
if (!matches) {
return;
}
const setName = matches[2];
const {
parent
} = rule;
if (parent.selector !== ':root') {
rule.warn(this.result, 'Custom property set ignored: not scoped to top-level `:root` ' + `(--${setName}` + `${parent.type === 'rule' ? ` declared in ${parent.selector}` : ''})`);
if (parent.type === 'root') {
rule.remove();
}
return;
} // Custom property sets override each other wholly,
// rather than cascading together like colliding style rules do.
// @see: https://tabatkins.github.io/specs/css-apply-rule/#defining
const newRule = rule.clone();
this.cache[setName] = newRule;
if (!this.options.preserve) {
removeCommentBefore(rule);
safeRemoveRule(rule);
}
if (!parent.nodes.length) {
parent.remove();
}
});
_defineProperty(this, "resolveNested", () => {
Object.keys(this.cache).forEach(rule => {
this.cache[rule].walkAtRules('apply', atRule => {
this.resolve(atRule); // @TODO honor `preserve` option.
atRule.remove();
});
});
});
_defineProperty(this, "resolve", atRule => {
let ancestor = atRule.parent;
while (ancestor && ancestor.type !== 'rule') {
ancestor = ancestor.parent;
}
if (!ancestor) {
atRule.warn(this.result, 'The @apply rule can only be declared inside Rule type nodes.');
atRule.remove();
return;
}
if (isDefinition(atRule.parent)) {
return;
}
const param = getParamValue(atRule.params);
const matches = RE_PROP_SET.exec(param);
if (!matches) {
return;
}
const setName = matches[2];
const {
parent
} = atRule;
if (!(setName in this.cache)) {
atRule.warn(this.result, `No custom property set declared for \`${setName}\`.`);
return;
}
const newRule = this.cache[setName].clone();
cleanIndent(newRule);
if (this.options.preserve) {
parent.insertBefore(atRule, newRule.nodes);
return;
}
atRule.replaceWith(newRule.nodes);
});
this.options = _objectSpread({}, this.defaults, options);
}
/**
* Prepend JS defined sets into the cache before parsing.
* This means CSS defined sets will overrides them if they share the same name.
*/
}
/**
* Helper: return whether the rule is a custom property set definition.
*/
function isDefinition(rule) {
return !!rule.selector && !!RE_PROP_SET.exec(rule.selector) && rule.parent && !!rule.parent.selector && rule.parent.selector === ':root';
}
/**
* Helper: allow parens usage in `@apply` AtRule declaration.
* This is for Polymer integration.
*/
function getParamValue(param) {
return /^\(/.test(param) ? balanced('(', ')', param).body : param;
}
/**
* Helper: remove excessive declarations indentation.
*/
function cleanIndent(rule) {
rule.walkDecls(decl => {
if (typeof decl.raws.before === 'string') {
decl.raws.before = decl.raws.before.replace(/[^\S\n\r]{2,}/, ' ');
}
});
}
/**
* Helper: correctly handle property sets removal and semi-colons.
* @See: postcss/postcss#1014
*/
function safeRemoveRule(rule) {
if (rule === rule.parent.last && rule.raws.ownSemicolon) {
rule.parent.raws.semicolon = true;
}
rule.remove();
}
/**
* Helper: remove immediate preceding comments.
*/
function removeCommentBefore(node) {
const previousNode = node.prev();
if (previousNode && previousNode.type === 'comment') {
previousNode.remove();
}
}
var name = "postcss-apply";
var index = postcss.plugin(name, options => (css, result) => {
const visitor = new Visitor(options);
visitor.result = result;
visitor.prepend();
css.walkRules(visitor.collect);
visitor.resolveNested();
css.walkAtRules('apply', visitor.resolve);
});
module.exports = index;