Skip to content

Commit

Permalink
Extend ignoreAtrules and ignoreProperties options to accept RegExp pa…
Browse files Browse the repository at this point in the history
…tterns (fixes #19, #45)
  • Loading branch information
lahmatiy committed Jan 30, 2023
1 parent ca24e5d commit a4f76aa
Show file tree
Hide file tree
Showing 5 changed files with 118 additions and 17 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
## next

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

## 2.0.0 (December 14, 2021)

Expand Down
22 changes: 17 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ Setup plugin in [stylelint config](http://stylelint.io/user-guide/configuration/
- [atrules](#atrules)
- [properties](#properties)
- [types](#types)
- [ignore](#ignore)
- [ignore](#ignore) (deprecated)
- [ignoreAtrules](#ignoreatrules)
- [ignoreProperties](#ignoreproperties)
- [ignoreValue](#ignorevalue)
Expand Down Expand Up @@ -175,10 +175,10 @@ Works the same as [`ignoreProperties`](#ignoreproperties) but **deprecated**, us

#### ignoreAtrules

Type: `Array<string>` or `false`
Type: `Array<string|RegExp>` or `false`
Default: `false`

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.
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.

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

#### ignoreProperties

Type: `Array<string>` or `false`
Type: `Array<string|RegExp>` or `false`
Default: `false`

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.
Expand All @@ -213,7 +213,7 @@ Defines a list of property names that should be ignored by the plugin. The names
}
```

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.
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.

#### ignoreValue

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

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.

## RegExp patterns

In some cases a more general match patterns are needed instead of exact name matching. In such cases a RegExp pattern can be used.

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.

To have a full control over a RegExp pattern, a regular RegExp instance or its stringified version (i.e. `"/pattern/flags?"`) can be used.

- `"foo|bar"` transforms into `/^(foo|bar)$/i`
- `"/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)
- `/foo|bar/` used as is (note: with no `i` flag a matching will be case-sensitive which makes no sense in CSS)

## License

MIT
50 changes: 41 additions & 9 deletions lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,21 +41,53 @@ const messages = utils.ruleMessages(ruleName, {
}
});

function createIgnoreMatcher(patterns) {
if (Array.isArray(patterns)) {
const names = new Set();
const regexpes = [];

for (let pattern of patterns) {
if (typeof pattern === 'string') {
const stringifiedRegExp = pattern.match(/^\/(.+)\/([a-z]*)/);

if (stringifiedRegExp) {
regexpes.push(new RegExp(stringifiedRegExp[1], stringifiedRegExp[2]));
} else if (/[^a-z0-9\-]/i.test(pattern)) {
regexpes.push(new RegExp(`^(${pattern})$`, 'i'));
} else {
names.add(pattern.toLowerCase());
}
} else if (isRegExp(pattern)) {
regexpes.push(pattern);
}
}

const matchRegExpes = regexpes.length
? name => regexpes.some(pattern => pattern.test(name))
: null;

if (names.size > 0) {
return matchRegExpes !== null
? name => names.has(name.toLowerCase()) || matchRegExpes(name)
: name => names.has(name.toLowerCase());
} else if (matchRegExpes !== null) {
return matchRegExpes;
}
}

return false;
}

const plugin = createPlugin(ruleName, function(options) {
options = options || {};

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

const ignoreValue = options.ignoreValue && (typeof options.ignoreValue === 'string' || isRegExp(options.ignoreValue))
? new RegExp(options.ignoreValue)
: false;
const ignoreProperties = Array.isArray(optionIgnoreProperties)
? new Set(optionIgnoreProperties.map(name => String(name).toLowerCase()))
: false;
const ignoreAtrules = Array.isArray(options.ignoreAtrules)
? new Set(options.ignoreAtrules.map(name => String(name).toLowerCase()))
: false;
const ignoreProperties = createIgnoreMatcher(options.ignoreProperties || options.ignore);
const ignoreAtrules = createIgnoreMatcher(options.ignoreAtrules);
const atrulesValidationDisabled = options.atrules === false;
const syntax = optionSyntaxExtension.has('less')
? optionSyntaxExtension.has('sass')
Expand Down Expand Up @@ -100,7 +132,7 @@ const plugin = createPlugin(ruleName, function(options) {
return;
}

if (ignoreAtrules !== false && ignoreAtrules.has(atrule.name)) {
if (ignoreAtrules !== false && ignoreAtrules(atrule.name)) {
ignoreAtruleNodes.add(atrule);
return;
}
Expand Down Expand Up @@ -159,7 +191,7 @@ const plugin = createPlugin(ruleName, function(options) {
}

// ignore properties from ignore list
if (ignoreProperties !== false && ignoreProperties.has(decl.prop.toLowerCase())) {
if (ignoreProperties !== false && ignoreProperties(decl.prop)) {
return;
}

Expand Down
49 changes: 48 additions & 1 deletion test/css.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,21 @@ css(null, function(tr) {
tr.notOk(' @unknown {}', unknownAtrule('unknown', 1, 3));
tr.notOk(' @media ??? {}', invalidPrelude('media', 1, 10));
});
css({ ignoreAtrules: ['unknown', 'import'] }, function(tr) {
css({ ignoreAtrules: ['unknown', 'IMPORT'] }, function(tr) {
tr.ok(' @UNKNOWN {}');
tr.ok(' @import {}');
tr.notOk(' @unknown-import {}', unknownAtrule('unknown-import', 1, 3));
tr.notOk(' @media ??? {}', invalidPrelude('media', 1, 10));
});
css({ ignoreAtrules: ['unknown|import'] }, function(tr) {
tr.ok(' @unknown {}');
tr.ok(' @import {}');
tr.notOk(' @unknown-import {}', unknownAtrule('unknown-import', 1, 3));
tr.notOk(' @media ??? {}', invalidPrelude('media', 1, 10));
});
css({ ignoreAtrules: ['unknown', 'very-unknown|import'] }, function(tr) {
tr.ok(' @unknown {}');
tr.ok(' @very-unknown {}');
tr.ok(' @import {}');
tr.notOk(' @media ??? {}', invalidPrelude('media', 1, 10));
});
Expand All @@ -65,9 +78,43 @@ css({ atrules: false }, function(tr) {
css({ ignoreProperties: ['foo', 'bar'] }, function(tr) {
tr.ok('.foo { foo: 1 }');
tr.ok('.foo { bar: 1 }');
tr.notOk('.foo { foobar: 1 }', unknownProperty('foobar'));
tr.ok('.foo { BAR: 1 }');
tr.notOk('.foo { baz: 1 }', unknownProperty('baz'));
});
css({ ignoreProperties: ['foo|bar'] }, function(tr) {
tr.ok('.foo { foo: 1 }');
tr.ok('.foo { bar: 1 }');
tr.notOk('.foo { foobar: 1 }', unknownProperty('foobar'));
tr.ok('.foo { BAR: 1 }');
tr.notOk('.foo { baz: 1 }', unknownProperty('baz'));
});
css({ ignoreProperties: ['/foo|bar/', '/qux/i'] }, function(tr) {
tr.ok('.foo { foo: 1 }');
tr.ok('.foo { bar: 1 }');
tr.ok('.foo { foobar: 1 }');
tr.notOk('.foo { BAR: 1 }', unknownProperty('BAR'));
tr.ok('.foo { QUX: 1; qux: 2 }');
});
css({ ignoreProperties: [/foo|bar/, /qux/i] }, function(tr) {
tr.ok('.foo { foo: 1 }');
tr.ok('.foo { bar: 1 }');
tr.ok('.foo { foobar: 1 }');
tr.notOk('.foo { BAR: 1 }', unknownProperty('BAR'));
tr.ok('.foo { QUX: 1; qux: 2 }');
});
css({ ignoreProperties: ['FOO', 'bar|QUX'] }, function(tr) {
tr.ok('.foo { foo: 1 }');
tr.ok('.foo { BAR: 1 }');
tr.ok('.foo { qux: 1 }');
tr.notOk('.foo { baz: 1 }', unknownProperty('baz'));
});
css({ ignoreProperties: ['token-\\d+'] }, function(tr) {
tr.ok('.foo { token-1: 1 }');
tr.ok('.foo { token-23: 1 }');
tr.notOk('.foo { token-1-postfix: 1 }', unknownProperty('token-1-postfix'));
tr.notOk('.foo { baz: 1 }', unknownProperty('baz'));
});

// should ignore by ignoreValue pattern
css({ ignoreValue: '^patternToIgnore$|=', ignoreProperties: ['bar'] }, function(tr) {
Expand Down
11 changes: 9 additions & 2 deletions test/utils/tester.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Reworked stylelint-rule-tester

import assert, { deepStrictEqual } from 'assert';
import { isRegExp } from 'util/types';
import postcss from 'postcss';

/**
Expand Down Expand Up @@ -35,9 +36,15 @@ function ruleTester(rule, ruleName, testerOptions) {
ruleSecondaryOptions = null;
}

const ruleOptionsString = rulePrimaryOptions ? JSON.stringify(rulePrimaryOptions) : '';
const ruleOptionsString = rulePrimaryOptions
? JSON.stringify(rulePrimaryOptions, (_, value) =>
isRegExp(value) ? 'regexp:' + String(value) : value
).replace(/"regexp:(.*?)"/g, '$1')
: '';
if (ruleOptionsString && ruleSecondaryOptions) {
ruleOptionsString += ', ' + JSON.stringify(ruleSecondaryOptions);
ruleOptionsString += ', ' + JSON.stringify(ruleSecondaryOptions, (_, value) =>
isRegExp(value) ? 'regexp:' + String(value) : value
).replace(/"regexp:(.*?)"/g, '$1');
}

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

0 comments on commit a4f76aa

Please sign in to comment.