UNPKG

postcss-apply

Version:

PostCSS plugin enabling custom properties sets references

262 lines (204 loc) 6.57 kB
'use strict'; 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;