-
Notifications
You must be signed in to change notification settings - Fork 7
/
index.js
144 lines (115 loc) · 4.6 KB
/
index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
import postcss from 'postcss';
import postcssNesting from 'postcss-nesting';
const nesting = postcssNesting();
// functional selector match
const functionalSelectorMatch = /(^|[^\w-])(%[_a-zA-Z]+[_a-zA-Z0-9-]*)([^\w-]|$)/i;
// plugin
export default postcss.plugin('postcss-extend-rule', rawopts => {
// options ( onFunctionalSelector, onRecursiveExtend, onUnusedExtend)
const opts = Object(rawopts);
const extendMatch = opts.name instanceof RegExp
? opts.name
: 'name' in opts
? new RegExp(`^${opts.name}$`, 'i')
: 'extend';
return (root, result) => {
const extendedAtRules = new WeakMap();
// for each extend at-rule
root.walkAtRules(extendMatch, extendAtRule => {
let parent = extendAtRule.parent;
while (parent.parent && parent.parent !== root) {
parent = parent.parent;
}
// do not revisit visited extend at-rules
if (!extendedAtRules.has(extendAtRule)) {
extendedAtRules.set(extendAtRule, true);
// selector identifier
const selectorIdMatch = getSelectorIdMatch(extendAtRule.params);
// extending rules
const 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
const cloneRoot = postcss.root().append(parent.clone());
nesting(cloneRoot);
parent.replaceWith(cloneRoot);
} else {
// manage unused extend at-rules
const 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
const 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, functionalRule => {
// manage encountered functional selectors
const 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 getExtendingRules(selectorIdMatch, extendAtRule) {
// extending rules
const extendingRules = [];
// for each rule found from root of the extend at-rule with a matching selector identifier
extendAtRule.root().walkRules(selectorIdMatch, matchingRule => {
// nesting selectors for the selectors matching the selector identifier
const nestingSelectors = matchingRule.selectors.filter(
selector => selectorIdMatch.test(selector)
).map(
selector => selector.replace(selectorIdMatch, '$1&$3')
).join(',');
// matching rule’s cloned nodes
const nestingNodes = matchingRule.clone().nodes;
// clone the matching rule as a nested rule
let 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
let 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
const escapedSelectorIds = postcss.list.comma(selectorIds).map(
selectorId => selectorId.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
).join('|');
// selector unattached to an existing selector
const selectorIdMatch = new RegExp(`(^|[^\\w-]!.!#)(${escapedSelectorIds})([^\\w-]|$)`, '');
return selectorIdMatch;
}