141 lines
4.6 KiB
JavaScript
141 lines
4.6 KiB
JavaScript
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;
|