/
index.js
executable file
·185 lines (158 loc) · 5.46 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
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
'use strict';
const _ = require('lodash');
const isStandardSyntaxRule = require('../../utils/isStandardSyntaxRule');
const isStandardSyntaxSelector = require('../../utils/isStandardSyntaxSelector');
const keywordSets = require('../../reference/keywordSets');
const optionsMatches = require('../../utils/optionsMatches');
const parseSelector = require('../../utils/parseSelector');
const report = require('../../utils/report');
const resolvedNestedSelector = require('postcss-resolve-nested-selector');
const ruleMessages = require('../../utils/ruleMessages');
const specificity = require('specificity');
const validateOptions = require('../../utils/validateOptions');
const ruleName = 'selector-max-specificity';
const messages = ruleMessages(ruleName, {
expected: (selector, specificity) =>
`Expected "${selector}" to have a specificity no more than "${specificity}"`,
});
// Return an array representation of zero specificity. We need a new array each time so that it can mutated
const zeroSpecificity = () => [0, 0, 0, 0];
// Calculate the sum of given array of specificity arrays
const specificitySum = (specificities) => {
const sum = zeroSpecificity();
specificities.forEach((specificityArray) => {
specificityArray.forEach((value, i) => {
sum[i] += value;
});
});
return sum;
};
const rule = function(max, options) {
return (root, result) => {
const validOptions = validateOptions(
result,
ruleName,
{
actual: max,
possible: [
function(max) {
// Check that the max specificity is in the form "a,b,c"
const pattern = new RegExp('^\\d+,\\d+,\\d+$');
return pattern.test(max);
},
],
},
{
actual: options,
possible: {
ignoreSelectors: [_.isString, _.isRegExp],
},
optional: true,
},
);
if (!validOptions) {
return;
}
// Calculate the specificity of a simple selector (type, attribute, class, ID, or pseudos's own value)
const simpleSpecificity = (selector) => {
if (optionsMatches(options, 'ignoreSelectors', selector)) {
return zeroSpecificity();
}
return specificity.calculate(selector)[0].specificityArray;
};
// Calculate the the specificity of the most specific direct child
const maxChildSpecificity = (node) =>
node.reduce((max, child) => {
const childSpecificity = nodeSpecificity(child); // eslint-disable-line no-use-before-define
return specificity.compare(childSpecificity, max) === 1 ? childSpecificity : max;
}, zeroSpecificity());
// Calculate the specificity of a pseudo selector including own value and children
const pseudoSpecificity = (node) => {
// `node.toString()` includes children which should be processed separately,
// so use `node.value` instead
const ownValue = node.value;
const ownSpecificity =
ownValue === ':not' || ownValue === ':matches'
? // :not and :matches don't add specificity themselves, but their children do
zeroSpecificity()
: simpleSpecificity(ownValue);
return specificitySum([ownSpecificity, maxChildSpecificity(node)]);
};
const shouldSkipPseudoClassArgument = (node) => {
// postcss-selector-parser includes the arguments to nth-child() functions
// as "tags", so we need to ignore them ourselves.
// The fake-tag's "parent" is actually a selector node, whose parent
// should be the :nth-child pseudo node.
const parentNode = node.parent.parent;
if (parentNode && parentNode.value) {
const parentNodeValue = parentNode.value;
const normalisedParentNode = parentNodeValue.toLowerCase().replace(/:+/, '');
return (
parentNode.type === 'pseudo' &&
(keywordSets.aNPlusBNotationPseudoClasses.has(normalisedParentNode) ||
keywordSets.linguisticPseudoClasses.has(normalisedParentNode))
);
}
return false;
};
// Calculate the specificity of a node parsed by `postcss-selector-parser`
const nodeSpecificity = (node) => {
if (shouldSkipPseudoClassArgument(node)) {
return zeroSpecificity();
}
switch (node.type) {
case 'attribute':
case 'class':
case 'id':
case 'tag':
return simpleSpecificity(node.toString());
case 'pseudo':
return pseudoSpecificity(node);
case 'selector':
// Calculate the sum of all the direct children
return specificitySum(node.map(nodeSpecificity));
default:
return zeroSpecificity();
}
};
const maxSpecificityArray = ('0,' + max).split(',').map(parseFloat);
root.walkRules((rule) => {
if (!isStandardSyntaxRule(rule)) {
return;
}
// Using rule.selectors gets us each selector in the eventuality we have a comma separated set
rule.selectors.forEach((selector) => {
resolvedNestedSelector(selector, rule).forEach((resolvedSelector) => {
try {
// Skip non-standard syntax selectors
if (!isStandardSyntaxSelector(resolvedSelector)) {
return;
}
parseSelector(resolvedSelector, result, rule, (selectorTree) => {
// Check if the selector specificity exceeds the allowed maximum
if (
specificity.compare(maxChildSpecificity(selectorTree), maxSpecificityArray) === 1
) {
report({
ruleName,
result,
node: rule,
message: messages.expected(resolvedSelector, max),
word: selector,
});
}
});
} catch (e) {
result.warn('Cannot parse selector', {
node: rule,
stylelintType: 'parseError',
});
}
});
});
});
};
};
rule.ruleName = ruleName;
rule.messages = messages;
module.exports = rule;