Skip to content

Commit a4f76aa

Browse files
committed
Extend ignoreAtrules and ignoreProperties options to accept RegExp patterns (fixes #19, #45)
1 parent ca24e5d commit a4f76aa

File tree

5 files changed

+118
-17
lines changed

5 files changed

+118
-17
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
## next
22

33
- Bumped `css-tree` to `^2.3.1`
4+
- Extended `ignoreAtrules` and `ignoreProperties` options to accept [RegExp patterns](README.md#regexp-patterns) (#19, #45)
5+
- Fixed Sass's `@else` at-rule to allow have no a prelude (#46)
6+
- Changed at-rule prelude validation to emit no warnings when a prelude contains Sass/Less syntax extensions (#44)
47

58
## 2.0.0 (December 14, 2021)
69

README.md

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ Setup plugin in [stylelint config](http://stylelint.io/user-guide/configuration/
3535
- [atrules](#atrules)
3636
- [properties](#properties)
3737
- [types](#types)
38-
- [ignore](#ignore)
38+
- [ignore](#ignore) (deprecated)
3939
- [ignoreAtrules](#ignoreatrules)
4040
- [ignoreProperties](#ignoreproperties)
4141
- [ignoreValue](#ignorevalue)
@@ -175,10 +175,10 @@ Works the same as [`ignoreProperties`](#ignoreproperties) but **deprecated**, us
175175

176176
#### ignoreAtrules
177177

178-
Type: `Array<string>` or `false`
178+
Type: `Array<string|RegExp>` or `false`
179179
Default: `false`
180180

181-
Defines a list of at-rules names that should be ignored by the plugin. Ignorance for an at-rule means no validation for its name, prelude or descriptors. The names provided are used for full case-insensitive matching, i.e. a vendor prefix is mandatory and prefixed names should be provided as well if you need to ignore them.
181+
Defines a list of at-rules names that should be ignored by the plugin. Ignorance for an at-rule means no validation for its name, prelude or descriptors. The names provided are used for full case-insensitive matching, i.e. a vendor prefix is mandatory and prefixed names should be provided as well if you need to ignore them. You can use [RegExp patterns](#regexp-patterns) in the list as well.
182182

183183
```json
184184
{
@@ -195,7 +195,7 @@ Defines a list of at-rules names that should be ignored by the plugin. Ignorance
195195

196196
#### ignoreProperties
197197

198-
Type: `Array<string>` or `false`
198+
Type: `Array<string|RegExp>` or `false`
199199
Default: `false`
200200

201201
Defines a list of property names that should be ignored by the plugin. The names provided are used for full case-insensitive matching, i.e. a vendor prefix is mandatory and prefixed names should be provided as well if you need to ignore them.
@@ -213,7 +213,7 @@ Defines a list of property names that should be ignored by the plugin. The names
213213
}
214214
```
215215

216-
In this example, plugin will not test declarations with a property name `composes`, `mask` or `-webkit-mask`, i.e. no warnings for these declarations would be raised.
216+
In this example, plugin will not test declarations with a property name `composes`, `mask` or `-webkit-mask`, i.e. no warnings for these declarations would be raised. You can use [RegExp patterns](#regexp-patterns) in the list as well.
217217

218218
#### ignoreValue
219219

@@ -237,6 +237,18 @@ Defines a pattern for values that should be ignored by the validator.
237237

238238
For this example, the plugin will not report warnings for values which is matched the given pattern. However, warnings will still be reported for unknown properties.
239239

240+
## RegExp patterns
241+
242+
In some cases a more general match patterns are needed instead of exact name matching. In such cases a RegExp pattern can be used.
243+
244+
Since CSS names are an indentifiers which can't contain any RegExp special character, distiguish between a regular name and RegExp is a trivial problem. When the plugins meets a string in a ignore pattern list which contains any character other than `a-z` (case-insensitive), `0-9` or `-`, it produce a RegExp using the expression `new RegExp('^(' + pattern + ')$', 'i')`. In other words, the pattern should be fully matched case-insensitive.
245+
246+
To have a full control over a RegExp pattern, a regular RegExp instance or its stringified version (i.e. `"/pattern/flags?"`) can be used.
247+
248+
- `"foo|bar"` transforms into `/^(foo|bar)$/i`
249+
- `"/foo|bar/i"` transforms into `/foo|bar/i` (note: it's not the same as previous RegExp, since not requires a full match with a name)
250+
- `/foo|bar/` used as is (note: with no `i` flag a matching will be case-sensitive which makes no sense in CSS)
251+
240252
## License
241253

242254
MIT

lib/index.js

Lines changed: 41 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -41,21 +41,53 @@ const messages = utils.ruleMessages(ruleName, {
4141
}
4242
});
4343

44+
function createIgnoreMatcher(patterns) {
45+
if (Array.isArray(patterns)) {
46+
const names = new Set();
47+
const regexpes = [];
48+
49+
for (let pattern of patterns) {
50+
if (typeof pattern === 'string') {
51+
const stringifiedRegExp = pattern.match(/^\/(.+)\/([a-z]*)/);
52+
53+
if (stringifiedRegExp) {
54+
regexpes.push(new RegExp(stringifiedRegExp[1], stringifiedRegExp[2]));
55+
} else if (/[^a-z0-9\-]/i.test(pattern)) {
56+
regexpes.push(new RegExp(`^(${pattern})$`, 'i'));
57+
} else {
58+
names.add(pattern.toLowerCase());
59+
}
60+
} else if (isRegExp(pattern)) {
61+
regexpes.push(pattern);
62+
}
63+
}
64+
65+
const matchRegExpes = regexpes.length
66+
? name => regexpes.some(pattern => pattern.test(name))
67+
: null;
68+
69+
if (names.size > 0) {
70+
return matchRegExpes !== null
71+
? name => names.has(name.toLowerCase()) || matchRegExpes(name)
72+
: name => names.has(name.toLowerCase());
73+
} else if (matchRegExpes !== null) {
74+
return matchRegExpes;
75+
}
76+
}
77+
78+
return false;
79+
}
80+
4481
const plugin = createPlugin(ruleName, function(options) {
4582
options = options || {};
4683

47-
const optionIgnoreProperties = options.ignoreProperties || options.ignore;
4884
const optionSyntaxExtension = new Set(Array.isArray(options.syntaxExtensions) ? options.syntaxExtensions : []);
4985

5086
const ignoreValue = options.ignoreValue && (typeof options.ignoreValue === 'string' || isRegExp(options.ignoreValue))
5187
? new RegExp(options.ignoreValue)
5288
: false;
53-
const ignoreProperties = Array.isArray(optionIgnoreProperties)
54-
? new Set(optionIgnoreProperties.map(name => String(name).toLowerCase()))
55-
: false;
56-
const ignoreAtrules = Array.isArray(options.ignoreAtrules)
57-
? new Set(options.ignoreAtrules.map(name => String(name).toLowerCase()))
58-
: false;
89+
const ignoreProperties = createIgnoreMatcher(options.ignoreProperties || options.ignore);
90+
const ignoreAtrules = createIgnoreMatcher(options.ignoreAtrules);
5991
const atrulesValidationDisabled = options.atrules === false;
6092
const syntax = optionSyntaxExtension.has('less')
6193
? optionSyntaxExtension.has('sass')
@@ -100,7 +132,7 @@ const plugin = createPlugin(ruleName, function(options) {
100132
return;
101133
}
102134

103-
if (ignoreAtrules !== false && ignoreAtrules.has(atrule.name)) {
135+
if (ignoreAtrules !== false && ignoreAtrules(atrule.name)) {
104136
ignoreAtruleNodes.add(atrule);
105137
return;
106138
}
@@ -159,7 +191,7 @@ const plugin = createPlugin(ruleName, function(options) {
159191
}
160192

161193
// ignore properties from ignore list
162-
if (ignoreProperties !== false && ignoreProperties.has(decl.prop.toLowerCase())) {
194+
if (ignoreProperties !== false && ignoreProperties(decl.prop)) {
163195
return;
164196
}
165197

test/css.js

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,8 +51,21 @@ css(null, function(tr) {
5151
tr.notOk(' @unknown {}', unknownAtrule('unknown', 1, 3));
5252
tr.notOk(' @media ??? {}', invalidPrelude('media', 1, 10));
5353
});
54-
css({ ignoreAtrules: ['unknown', 'import'] }, function(tr) {
54+
css({ ignoreAtrules: ['unknown', 'IMPORT'] }, function(tr) {
55+
tr.ok(' @UNKNOWN {}');
56+
tr.ok(' @import {}');
57+
tr.notOk(' @unknown-import {}', unknownAtrule('unknown-import', 1, 3));
58+
tr.notOk(' @media ??? {}', invalidPrelude('media', 1, 10));
59+
});
60+
css({ ignoreAtrules: ['unknown|import'] }, function(tr) {
61+
tr.ok(' @unknown {}');
62+
tr.ok(' @import {}');
63+
tr.notOk(' @unknown-import {}', unknownAtrule('unknown-import', 1, 3));
64+
tr.notOk(' @media ??? {}', invalidPrelude('media', 1, 10));
65+
});
66+
css({ ignoreAtrules: ['unknown', 'very-unknown|import'] }, function(tr) {
5567
tr.ok(' @unknown {}');
68+
tr.ok(' @very-unknown {}');
5669
tr.ok(' @import {}');
5770
tr.notOk(' @media ??? {}', invalidPrelude('media', 1, 10));
5871
});
@@ -65,9 +78,43 @@ css({ atrules: false }, function(tr) {
6578
css({ ignoreProperties: ['foo', 'bar'] }, function(tr) {
6679
tr.ok('.foo { foo: 1 }');
6780
tr.ok('.foo { bar: 1 }');
81+
tr.notOk('.foo { foobar: 1 }', unknownProperty('foobar'));
82+
tr.ok('.foo { BAR: 1 }');
83+
tr.notOk('.foo { baz: 1 }', unknownProperty('baz'));
84+
});
85+
css({ ignoreProperties: ['foo|bar'] }, function(tr) {
86+
tr.ok('.foo { foo: 1 }');
87+
tr.ok('.foo { bar: 1 }');
88+
tr.notOk('.foo { foobar: 1 }', unknownProperty('foobar'));
6889
tr.ok('.foo { BAR: 1 }');
6990
tr.notOk('.foo { baz: 1 }', unknownProperty('baz'));
7091
});
92+
css({ ignoreProperties: ['/foo|bar/', '/qux/i'] }, function(tr) {
93+
tr.ok('.foo { foo: 1 }');
94+
tr.ok('.foo { bar: 1 }');
95+
tr.ok('.foo { foobar: 1 }');
96+
tr.notOk('.foo { BAR: 1 }', unknownProperty('BAR'));
97+
tr.ok('.foo { QUX: 1; qux: 2 }');
98+
});
99+
css({ ignoreProperties: [/foo|bar/, /qux/i] }, function(tr) {
100+
tr.ok('.foo { foo: 1 }');
101+
tr.ok('.foo { bar: 1 }');
102+
tr.ok('.foo { foobar: 1 }');
103+
tr.notOk('.foo { BAR: 1 }', unknownProperty('BAR'));
104+
tr.ok('.foo { QUX: 1; qux: 2 }');
105+
});
106+
css({ ignoreProperties: ['FOO', 'bar|QUX'] }, function(tr) {
107+
tr.ok('.foo { foo: 1 }');
108+
tr.ok('.foo { BAR: 1 }');
109+
tr.ok('.foo { qux: 1 }');
110+
tr.notOk('.foo { baz: 1 }', unknownProperty('baz'));
111+
});
112+
css({ ignoreProperties: ['token-\\d+'] }, function(tr) {
113+
tr.ok('.foo { token-1: 1 }');
114+
tr.ok('.foo { token-23: 1 }');
115+
tr.notOk('.foo { token-1-postfix: 1 }', unknownProperty('token-1-postfix'));
116+
tr.notOk('.foo { baz: 1 }', unknownProperty('baz'));
117+
});
71118

72119
// should ignore by ignoreValue pattern
73120
css({ ignoreValue: '^patternToIgnore$|=', ignoreProperties: ['bar'] }, function(tr) {

test/utils/tester.js

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// Reworked stylelint-rule-tester
22

33
import assert, { deepStrictEqual } from 'assert';
4+
import { isRegExp } from 'util/types';
45
import postcss from 'postcss';
56

67
/**
@@ -35,9 +36,15 @@ function ruleTester(rule, ruleName, testerOptions) {
3536
ruleSecondaryOptions = null;
3637
}
3738

38-
const ruleOptionsString = rulePrimaryOptions ? JSON.stringify(rulePrimaryOptions) : '';
39+
const ruleOptionsString = rulePrimaryOptions
40+
? JSON.stringify(rulePrimaryOptions, (_, value) =>
41+
isRegExp(value) ? 'regexp:' + String(value) : value
42+
).replace(/"regexp:(.*?)"/g, '$1')
43+
: '';
3944
if (ruleOptionsString && ruleSecondaryOptions) {
40-
ruleOptionsString += ', ' + JSON.stringify(ruleSecondaryOptions);
45+
ruleOptionsString += ', ' + JSON.stringify(ruleSecondaryOptions, (_, value) =>
46+
isRegExp(value) ? 'regexp:' + String(value) : value
47+
).replace(/"regexp:(.*?)"/g, '$1');
4148
}
4249

4350
const ok = Object.assign(createOkAssertFactory(it), {

0 commit comments

Comments
 (0)