Skip to content

Commit

Permalink
Add syntaxExtensions option
Browse files Browse the repository at this point in the history
  • Loading branch information
lahmatiy committed Dec 14, 2021
1 parent e3c7c42 commit c256b39
Show file tree
Hide file tree
Showing 5 changed files with 212 additions and 113 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
- Added at-rule validation for name, prelude and descriptors
- Added `atrules` option to extend or alter at-rule syntax definition dictionary or disable at-rule validation when `false` is passed as a value for the option
- Added `ignoreAtrules` option to specify at-rule names which should not be validated
- Added `syntaxExtensions` option to specify syntax extensions, i.e. `sass` or/and `less`
- Used `isStandardSyntax*()` helpers from `stylelint` to reduce failures for non-standard syntax (e.g. Less or Sass)
- Added support for Less & Sass namespaces, a value with such constructions are ignored now instead of failure (#39)
- Added a column to mismatch error details
Expand Down
31 changes: 30 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

A [stylelint](http://stylelint.io/) plugin based on [csstree](https://github.com/csstree/csstree) to examinate CSS syntax. It examinates at-rules and declaration values to match W3C specs and browsers extensions. It might be extended in future to validate other parts of CSS.

> ⚠️ Warning ⚠️: The plugin is designed to validate CSS syntax only. However `stylelint` may be configured to use for other syntaxes like Less or Sass. In this case, the plugin avoids examination of expressions containing non-standard syntax.
> ⚠️ Warning ⚠️: The plugin is designed to validate CSS syntax only. However `stylelint` may be configured to use for other syntaxes like Less or Sass. In this case, the plugin avoids examination of expressions containing non-standard syntax, but you need specify which preprocessor is used with the [`syntaxExtensions`](#syntaxextensions) option.
## Install

Expand All @@ -31,13 +31,42 @@ Setup plugin in [stylelint config](http://stylelint.io/user-guide/configuration/

### Options

- [syntaxExtensions](#syntaxextensions)
- [atrules](#atrules)
- [properties](#properties)
- [types](#types)
- [ignore](#ignore)
- [ignoreAtrules](#ignoreatrules)
- [ignoreProperties](#ignoreproperties)
- [ignoreValue](#ignorevalue)

#### syntaxExtensions

Type: `Array<'sass' | 'less'>` or `false`
Default: `false`

Since the plugin focuses on CSS syntax validation it warns on a syntax which is introducing by preprocessors like Less or Sass. The `syntaxExtensions` option allows to specify that some preprocessor's syntaxes are used for styles so the plugin may avoid warnings when met such a syntax.

By default the plugin exams styles as pure CSS. To specify that a preprocessor's syntax is used, you must specify an array with the names of these extensions. Currently supported:

- `sass` – declaration values with Sass syntax will be ignored as well as custom at-rules introduced by Saas (e.g. `@if`, `@else`, `@mixin` etc)
- `less` – declaration values with Sass syntax will be ignored as well as `@plugin` at-rule introduced by Less

Using both syntax extensions is also possible:

```json
{
"plugins": [
"stylelint-csstree-validator"
],
"rules": {
"csstree/validator": {
"syntaxExtensions": ["sass", "less"]
}
}
}
```

#### atrules

Type: `Object`, `false` or `null`
Expand Down
91 changes: 63 additions & 28 deletions lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,37 +3,49 @@ import isStandardSyntaxAtRule from 'stylelint/lib/utils/isStandardSyntaxAtRule.j
import isStandardSyntaxDeclaration from 'stylelint/lib/utils/isStandardSyntaxDeclaration.js';
import isStandardSyntaxProperty from 'stylelint/lib/utils/isStandardSyntaxProperty.js';
import isStandardSyntaxValue from 'stylelint/lib/utils/isStandardSyntaxValue.js';
import { fork } from 'css-tree';
import { fork, lexer, parse } from 'css-tree';
import { less, sass } from './syntax-extension/index.js';

const { utils, createPlugin } = stylelint;
const csstree = fork(less).fork(sass);
const isRegExp = value => toString.call(value) === '[object RegExp]';
const getRaw = (node, name) => (node.raws && node.raws[name]) || '';
const allowedSyntaxExtensions = new Set(['less', 'sass']);
const lessExtendedSyntax = fork(less);
const sassExtendedSyntax = fork(sass);
const syntaxExtensions = {
none: { fork, lexer, parse },
less: lessExtendedSyntax,
sass: sassExtendedSyntax,
all: fork(less).fork(sass)
};

const ruleName = 'csstree/validator';
const messages = utils.ruleMessages(ruleName, {
parseError: function(value) {
csstree(value) {
return value;
},
parseError(value) {
return 'Can\'t parse value "' + value + '"';
},
invalidValue: function(property) {
return 'Invalid value for `' + property + '`';
unknownAtrule(atrule) {
return 'Unknown at-rule `@' + atrule + '`';
},
invalidPrelude: function(atrule) {
invalidPrelude(atrule) {
return 'Invalid prelude for `@' + atrule + '`';
},
unknownProperty(property) {
return 'Unknown property `' + property + '`';
},
invalidValue(property) {
return 'Invalid value for "' + property + '"';
}
});

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

const optionIgnoreProperties = options.ignoreProperties || options.ignore;

if (options.ignore && !seenOptions.has(options)) {
console.warn(`[${ruleName}] "ignore" option is deprecated and will be removed in future versions, use "ignoreProperties" instead`);
seenOptions.add(options);
}
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)
Expand All @@ -45,9 +57,16 @@ const plugin = createPlugin(ruleName, function(options) {
? new Set(options.ignoreAtrules.map(name => String(name).toLowerCase()))
: false;
const atrulesValidationDisabled = options.atrules === false;
const syntax = !options.properties && !options.types && !options.atrules
? csstree.lexer // default syntax
: csstree.fork({
const syntax = optionSyntaxExtension.has('less')
? optionSyntaxExtension.has('sass')
? syntaxExtensions.all
: syntaxExtensions.less
: optionSyntaxExtension.has('sass')
? syntaxExtensions.sass
: syntaxExtensions.none;
const lexer = !options.properties && !options.types && !options.atrules
? syntax.lexer // default syntax
: syntax.fork({
properties: options.properties,
types: options.types,
atrules: options.atrules
Expand All @@ -56,11 +75,22 @@ const plugin = createPlugin(ruleName, function(options) {
return function(root, result) {
const ignoreAtruleNodes = new WeakSet();

stylelint.utils.validateOptions(result, ruleName, {
actual: {
ignore: options.ignore,
syntaxExtensions: [...optionSyntaxExtension]
},
possible: {
ignore: value => value === undefined,
syntaxExtensions: value => allowedSyntaxExtensions.has(value)
}
});

root.walkAtRules(function(atrule) {
let error;

// ignore non-standard at-rules
if (!isStandardSyntaxAtRule(atrule)) {
if (syntax !== syntaxExtensions.none && !isStandardSyntaxAtRule(atrule)) {
return;
}

Expand All @@ -75,25 +105,27 @@ const plugin = createPlugin(ruleName, function(options) {
return;
}

if (error = syntax.checkAtruleName(atrule.name)) {
if (error = lexer.checkAtruleName(atrule.name)) {
ignoreAtruleNodes.add(atrule);
utils.report({
ruleName,
result,
message: error.message,
message: messages.csstree(error.message),
node: atrule
});

return;
}

if (error = syntax.matchAtrulePrelude(atrule.name, atrule.params).error) {
if (error = lexer.matchAtrulePrelude(atrule.name, atrule.params).error) {
let message = error.rawMessage || error.message;
let index = 2 + atrule.name.length + getRaw('afterName').length;

if (message === 'Mismatch') {
message = messages.invalidPrelude(atrule.name);
index += error.mismatchOffset;
} else {
message = messages.csstree(message);
}

utils.report({
Expand All @@ -118,16 +150,17 @@ const plugin = createPlugin(ruleName, function(options) {
}

// ignore declarations with non-standard syntax (Less, Sass, etc)
if (!isStandardSyntaxDeclaration(decl) ||
!isStandardSyntaxProperty(decl.prop) ||
!isStandardSyntaxValue(decl.value)) {
return;
if (syntax !== syntaxExtensions.none) {
if (!isStandardSyntaxDeclaration(decl) ||
!isStandardSyntaxProperty(decl.prop) ||
!isStandardSyntaxValue(decl.value)) {
return;
}
}

try {
csstree.parse(decl.value, {
context: 'value',
property: decl.prop
syntax.parse(decl.value, {
context: 'value'
});
} catch (e) {
// ignore values with preprocessor's extensions
Expand All @@ -149,8 +182,8 @@ const plugin = createPlugin(ruleName, function(options) {
}

const { error } = decl.parent.type === 'atrule'
? syntax.matchAtruleDescriptor(decl.parent.name, decl.prop, decl.value)
: syntax.matchProperty(decl.prop, decl.value);
? lexer.matchAtruleDescriptor(decl.parent.name, decl.prop, decl.value)
: lexer.matchProperty(decl.prop, decl.value);

if (error) {
let message = error.rawMessage || error.message || error;
Expand All @@ -170,6 +203,8 @@ const plugin = createPlugin(ruleName, function(options) {

message = messages.invalidValue(decl.prop);
index = decl.prop.length + getRaw(decl, 'between').length + error.mismatchOffset;
} else {
message = messages.csstree(message);
}

utils.report({
Expand Down
Loading

0 comments on commit c256b39

Please sign in to comment.