import postcss from 'postcss'; import nesting from 'postcss-nesting'; // functional selector match var functionalSelectorMatch = /(^|[^\w-])(%[_a-zA-Z]+[_a-zA-Z0-9-]*)([^\w-]|$)/i; // plugin var index = postcss.plugin('postcss-extend-rule', function (rawopts) { // options ( onFunctionalSelector, onRecursiveExtend, onUnusedExtend) var opts = Object(rawopts); var extendMatch = opts.name instanceof RegExp ? opts.name : 'name' in opts ? new RegExp(`^${opts.name}$`, 'i') : 'extend'; return function (root, result) { // for each extend at-rule root.walkAtRules(extendMatch, function (extendAtRule) { // do not revisit visited extend at-rules if (!extendAtRule.__extendAtRuleVisited) { extendAtRule.__extendAtRuleVisited = true; // selector identifier var selectorIdMatch = getSelectorIdMatch(extendAtRule.params); // extending rules var extendingRules = getExtendingRules(selectorIdMatch, extendAtRule); // if there are extending rules if (extendingRules.length) { // replace the extend at-rule with the extending rules extendAtRule.replaceWith(extendingRules); // transform any nesting at-rules extendingRules.forEach(function (extendingRule) { transform(extendingRule); extendingRule.walk(transform); }); } else { // manage unused extend at-rules var unusedExtendMessage = `Unused extend at-rule "${extendAtRule.params}"`; if (opts.onUnusedExtend === 'throw') { throw extendAtRule.error(unusedExtendMessage, { word: extendAtRule.name }); } else if (opts.onUnusedExtend === 'warn') { extendAtRule.warn(result, unusedExtendMessage); } else if (opts.onUnusedExtend !== 'ignore') { extendAtRule.remove(); } } } else { // manage revisited extend at-rules var revisitedExtendMessage = `Revisited extend at-rule "${extendAtRule.params}"`; if (opts.onRecursiveExtend === 'throw') { throw extendAtRule.error(revisitedExtendMessage, { word: extendAtRule.name }); } else if (opts.onRecursiveExtend === 'warn') { extendAtRule.warn(result, revisitedExtendMessage); } else if (opts.onRecursiveExtend !== 'ignore') { extendAtRule.remove(); } } }); root.walkRules(functionalSelectorMatch, function (functionalRule) { // manage encountered functional selectors var functionalSelectorMessage = `Encountered functional selector "${functionalRule.selector}"`; if (opts.onFunctionalSelector === 'throw') { throw functionalRule.error(functionalSelectorMessage, { word: functionalRule.selector.match(functionalSelectorMatch)[1] }); } else if (opts.onFunctionalSelector === 'warn') { functionalRule.warn(result, functionalSelectorMessage); } else if (opts.onFunctionalSelector !== 'ignore') { functionalRule.remove(); } }); }; }); function transform(node) { return nesting()({ walk(transformer) { return transformer(node); } }); } function getExtendingRules(selectorIdMatch, extendAtRule) { // extending rules var extendingRules = []; // for each rule found from root of the extend at-rule with a matching selector identifier extendAtRule.root().walkRules(selectorIdMatch, function (matchingRule) { // nesting selectors for the selectors matching the selector identifier var nestingSelectors = matchingRule.selectors.filter(function (selector) { return selectorIdMatch.test(selector); }).map(function (selector) { return selector.replace(selectorIdMatch, '$1&$3'); }).join(','); // matching rule’s cloned nodes var nestingNodes = matchingRule.clone().nodes; // clone the matching rule as a nested rule var clone = extendAtRule.clone({ name: 'nest', params: nestingSelectors, nodes: nestingNodes, // empty the extending rules, as they are likely non-comforming raws: {} }); // preserve nesting of parent rules and at-rules var parent = matchingRule.parent; while (parent && (parent.type === 'rule' || parent.type === 'atrule')) { clone = parent.clone().removeAll().append([clone]); parent = parent.parent; } // push the matching rule to the extending rules extendingRules.push(clone); }); // return the extending rules return extendingRules; } function getSelectorIdMatch(selectorIds) { // escape the contents of the selector id to avoid being parsed as regex var escapedSelectorIds = postcss.list.comma(selectorIds).map(function (selectorId) { return selectorId.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); }).join('|'); // selector unattached to an existing selector var selectorIdMatch = new RegExp(`(^|[^\\w-])(${escapedSelectorIds})([^\\w-]|$)`, ''); return selectorIdMatch; } export default index;