-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathindex.js
208 lines (178 loc) · 5.5 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
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
'use strict';
const stylelint = require('stylelint');
const valueParser = require('postcss-value-parser');
/** plugin name - prefixes all rules as per stylelint requirements */
const PLUGIN_NAME = 'rem-over-px';
/** rule name for rem-over-px */
const ruleName = `${PLUGIN_NAME}/rem-over-px`;
/** rule messages */
const messages = stylelint.utils.ruleMessages(ruleName, {
/** Report message for prefer rem over px */
remOverPx(val = '') {
return `Expected px unit in "${val}" to be rem.`;
},
});
/** default secondary options */
const defaultSecondaryOptions = {
/** properties to ignore */
ignore: ['1px'],
/** functions to ignore */
ignoreFunctions: ['url'],
/** @ rules to ignore */
ignoreAtRules: ['media'],
/** Base font size - used by autofix to convert px to rem */
fontSize: 16,
/** New: disable auto-fixing */
disableFix: false,
};
/** Regex to match pixels declarations in a string */
// eslint-disable-next-line
const regexPX = new RegExp(/(\d+\.?\d*)px/, 'g');
/** Converts a string with px units to rem */
const _pxToRem = (CSSString = '', fontSize = defaultSecondaryOptions.fontSize || 16) =>
CSSString.replace(regexPX, (match, n) => `${n / fontSize}rem`);
/** checks if prop is in ignore list */
const _propInIgnoreList = (prop, list) =>
prop && list.some((item) => prop.indexOf(item) > -1);
/** checks if prop is in ignore list with px */
const _propAddXpxInIgnoreList = (prop, list, px) => {
const reg = new RegExp(`\\s${px}`);
return (
prop &&
list.some(
(item) => reg.test(item) && prop.indexOf(item.replace(reg, '')) > -1,
)
);
};
/**
* check if a value has forbidden `px`
*/
const _hasForbiddenPX = (node, options) => {
const { type } = node;
/** value to check */
const value = type === 'decl' ? node.value : node.params;
/** prop to check */
const prop = type === 'decl' ? node.prop : null;
/** parsed value */
const parsed = valueParser(value);
/* parse secondaryOptions */
const {
ignore = defaultSecondaryOptions.ignore,
ignoreFunctions = defaultSecondaryOptions.ignoreFunctions,
ignoreAtRules = defaultSecondaryOptions.ignoreAtRules,
} = options;
/** Whether we matched px declarations */
let hasPX = false;
/** early exit and ignore */
if (
/* ignore atRules */
(type === 'atrule' && ignoreAtRules.indexOf(node.name) !== -1) ||
/* ignore declarations that are children of atrules - eg: keyframes */
(type === 'decl' && node?.parent?.parent?.type === 'atrule' && ignoreAtRules.indexOf(node?.parent?.parent?.name) !== -1) ||
/* ignore declarations ignored by props */
(type === 'decl' && _propInIgnoreList(node.prop, ignore))
) {
return;
}
/** Walk through the parsed tree and match - return boolean, true if we matched an issue */
parsed.walk((currNode) => {
// if currNode is `url(xxx)`, prevent the traversal
if (
currNode.type === 'function' &&
ignoreFunctions.indexOf(currNode.value) !== -1
) {
return false;
}
/** whether a px value was matched */
let matched;
if (
currNode.type === 'word' &&
(matched = currNode.value.match(/^([-,+]?\d+(\.\d+)?px)$/))
) {
/** matched px value. eg: '10px' */
const px = matched[1];
/** handled 0px edge case */
if (px === '0px') {
return;
}
/** check if prop is in ignore list, else -> set hasPX since an issue was matched */
if (
!_propAddXpxInIgnoreList(prop, ignore, px) &&
ignore.indexOf(px) === -1
) {
hasPX = true;
}
} else if (
/* handle string case, eg: mixins etc... */
currNode.type === 'string' &&
/(@\{[\w-]+\})px\b/.test(currNode.value)
) {
// eg. ~'@{width}px'
hasPX = true;
}
});
return hasPX;
};
/** rem-over-px plugin handler */
const pluginHandler =
(
primaryOption,
secondaryOptionObject = defaultSecondaryOptions,
context = {},
) =>
(root, result) => {
/** no primary option was provided or null, rule is disabled */
if (!primaryOption) {
return;
}
const { disableFix, fontSize } = secondaryOptionObject;
/* check for declarations */
root.walkDecls((declaration) => {
if (_hasForbiddenPX(declaration, secondaryOptionObject)) {
/* handle fixing */
if (context.fix && !disableFix) {
// Apply fixes using PostCSS API
declaration.value = _pxToRem(
declaration.value,
fontSize,
);
// Return and don't report a problem
return;
}
/* handle reporting */
stylelint.utils.report({
ruleName,
result,
node: declaration,
message: messages.remOverPx(declaration),
});
}
});
// check for rules
root.walkAtRules((atRule) => {
if (_hasForbiddenPX(atRule, secondaryOptionObject)) {
/* handle fixing */
if (context.fix && !disableFix) {
// Apply fixes using PostCSS API
atRule.value = _pxToRem(atRule.value, fontSize);
// Return and don't report a problem
return;
}
/* handle reporting */
stylelint.utils.report({
ruleName,
result,
node: atRule,
message: messages.remOverPx(atRule),
});
}
});
};
/**
* Stylelint plugin rem-over-px
*
* Enforces the usage of rem units over px units.
*/
module.exports = stylelint.createPlugin(ruleName, pluginHandler);
module.exports.ruleName = ruleName;
module.exports.messages = messages;