Skip to content

Commit

Permalink
Support ranges within files and verbose comments; fixes #57
Browse files Browse the repository at this point in the history
  • Loading branch information
davidtheclark committed Aug 9, 2015
1 parent 9da9fa2 commit 6f5f27b
Show file tree
Hide file tree
Showing 19 changed files with 369 additions and 204 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
=== Head

* Support multiple definitions per file.
* Support comments to end definition enforcement.
* Support verbose comment syntax, e.g. `/* postcss-bem-linter: define ComponentName */`.

=== 0.5.0 (August 5, 2015)

* Add alternate signature for designating preset and preset options.
Expand Down
85 changes: 70 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ This plugin registers warnings via PostCSS. Therefore, you'll want to use it wit
**Weak mode**:

* While *initial* selector sequences (before combinators) must match the defined convention,
sequences *after* combinators are not held to any standard.
sequences *after combinators* are not held to any standard.

*Prior to 0.5.0, this plugin checked two other details: that `:root` rules only contain custom-properties; and that the `:root` selector is not grouped or combined with other selectors. These checks can now be performed by [stylelint](https://github.com/stylelint/stylelint). So from 0.5.0 onwards, this plugin leaves that business to stylelint to focus on its more unique task.*

Expand Down Expand Up @@ -68,8 +68,9 @@ The following preset patterns are available:

You can use a preset pattern and its options in two ways:
- pass the preset's name as the first argument, and, if needed, an `options` object as the second,
e.g. `bemLinter('suit', { namespace: 'twt' })`.
- pass an object as the first and only argument, with the preset's name as the `preset` property and, if need, `presetOptions`, e.g. `bemLinter({ preset: 'suit', presetOptions { namespace: 'twt' })`.
e.g. `bemLinter('suit', { namespace: 'twt' })`.
- pass an object as the first and only argument, with the preset's name as the `preset` property and,
if needed, `presetOptions`, e.g. `bemLinter({ preset: 'suit', presetOptions { namespace: 'twt' })`.

**`'suit'` is the default pattern**; so if you do not pass any `pattern` argument,
SUIT conventions will be enforced.
Expand All @@ -81,17 +82,17 @@ You can define a custom pattern by passing as your first and only argument an ob
- `componentName` (optional): A regular expression describing valid component names.
Default is `/[-_a-zA-Z0-9]+/`.
- `componentSelectors`: Either of the following:
- A single function that accepts a component name and returns a regular expression describing
- A *single function* that accepts a component name and returns a regular expression describing
all valid selector sequences for the stylesheet.
- An object consisting of two methods, `initial` and `combined`. Both methods accept a
- An *object consisting of two methods*, `initial` and `combined`. Both methods accept a
component name and return a regular expression. `initial` returns a description of valid
initial selector sequences — those occurring at the beginning of a selector, before any
combinators. `combined` returns a description of valid selector sequences allowed *after* combinators.
Two things to note: If you do not specify a combined pattern, it is assumed that combined
(Two things to note: If you do not specify a combined pattern, it is assumed that combined
sequences must match the same pattern as initial sequences.
And in weak mode, *any* combined sequences are accepted.
And in weak mode, *any* combined sequences are accepted.)
- `utilitySelectors`: A regular expression describing valid utility selectors. This will be use
if the stylesheet uses `/** @define utilities */`, as explained below.
if the stylesheet defines a group of utilities, as explained below.

So you might call the plugin in any of the following ways:

Expand Down Expand Up @@ -134,13 +135,18 @@ bemLinter({

### Defining a component

The plugin will only run against files that explicitly declare that they
are defining either a named component or utilities, using either
`/** @define ComponentName */` or `/** @define utilities */` in the first line
of the file.
The plugin will only run if it finds special comments that
define a named component or a group of utilities.

Weak mode is turned on by adding `; weak` to this definition,
e.g. `/** @define ComponentName; weak */`.
These definitions can be provided in two syntaxes: concise and verbose.

- Concise definition syntax: `/** @define ComponentName */` or `/** @define utilities */`
- Verbose definition syntax: `/* postcss-bem-linter: define ComponentName */` or `/* postcss-bem-linter: define utilities */`.

Weak mode is turned on by adding `; weak` to a definition,
e.g. `/** @define ComponentName; weak */` or `/* postcss-bem-linter: define ComponentName; weak */`.

Concise syntax:

```css
/** @define MyComponent */
Expand All @@ -154,6 +160,20 @@ e.g. `/** @define ComponentName; weak */`.
.MyComponent-other {}
```

Verbose syntax:

```css
/** postcss-bem-linter: define FancyComponent */

:root {
--FancyComponent-property: value;
}

.FancyComponent {}

.FancyComponent-other {}
```

Weak mode:

```css
Expand Down Expand Up @@ -181,9 +201,44 @@ Utilities:
If a component is defined and the component name does not match your `componentName` pattern,
the plugin will throw an error.

### Multiple definitions

It's recommended that you keep each defined group of rules in a distinct file,
with the definition at the top of the file. If, however, you have a good reason
for *multiple definitions within a single file*, you can do that.

Successive definitions override each other. So the following works:

```css
/* @define Foo */
.Foo {}

/* @define Bar */
.Bar {}

/* @define utilities */
.u-something {}
```

You can also deliberately *end the enforcement of a definition* with the following special comments:
`/* @end */` or `/* postcss-bem-linter: end */`.

```css
/* @define Foo */
.Foo {}
/* @end */

.something-something-something {}
```

One use-case for this functionality is when linting files *after* concatenation performed by a
CSS processor like Less or Sass, whose syntax is not always compatible with PostCSS.
See [issue #57](https://github.com/postcss/postcss-bem-linter/issues/57).

### Ignoring specific selectors

If you need to ignore a specific selector but do not want to ignore the entire stylesheet,
If you need to ignore a specific selector but do not want to ignore the entire stylesheet
or end the enforcement of a definition,
you can do so by preceding the selector with this comment: `/* postcss-bem-linter: ignore */`.

```css
Expand Down
118 changes: 72 additions & 46 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,70 +1,96 @@
var postcss = require('postcss');
var validateCustomProperties = require('./lib/validate-properties');
var validateCustomProperties = require('./lib/validate-custom-properties');
var validateUtilities = require('./lib/validate-utilities');
var validateSelectors = require('./lib/validate-selectors');
var presetPatterns = require('./lib/preset-patterns');
var generateConfig = require('./lib/generate-config');

var RE_DIRECTIVE = /\*\s*@define ([-_a-zA-Z0-9]+)\s*(?:;\s*(weak))?\s*/;
var DEFINE_VALUE = '([-_a-zA-Z0-9]+)\\s*(?:;\\s*(weak))?';
var DEFINE_DIRECTIVE = new RegExp(
'(?:\\*\\s*@define ' + DEFINE_VALUE + ')|' +
'(?:\\s*postcss-bem-linter: define ' + DEFINE_VALUE + ')\\s*'
);
var END_DIRECTIVE = new RegExp(
'(?:\\*\\s*@end\\s*)|' +
'(?:\\s*postcss-bem-linter: end)\\s*'
);
var UTILITIES_IDENT = 'utilities';
var WEAK_IDENT = 'weak';

/**
* Set things up and call the validators.
*
* If the input CSS does not have a
* If the input CSS does not have any
* directive defining a component name according to the specified pattern,
* do nothing -- or warn, if the directive is there but the name does not match.
*
* @param {Object|String} [primaryOptions = 'suit']
* @param {RegExp} [primaryOptions.componentName]
* @param {RegExp} [primaryOptions.utilitySelectors]
* @param {Object|Function} [primaryOptions.componentSelectors]
* @param {String} [primaryOptions.preset] - The same as passing a string for `primaryOptions`
* @param {Object} [primaryOptions.presetOptions] - Options that are can be used by
* a pattern (e.g. `namespace`)
* @param {Object} [secondaryOptions] - The same as `primaryOptions.presetOptions`
* @param {Object|String} primaryOptions
* @param {Object} [secondaryOptions]
*/
module.exports = postcss.plugin('postcss-bem-linter', function(primaryOptions, secondaryOptions) {
var patterns = primaryOptions || 'suit';
if (typeof patterns === 'string') {
patterns = presetPatterns[patterns];
} else if (patterns.preset) {
patterns = presetPatterns[patterns.preset];
}

var presetOptions = secondaryOptions || {};
if (primaryOptions && primaryOptions.presetOptions) {
presetOptions = primaryOptions.presetOptions;
}

var componentNamePattern = patterns.componentName || /[-_a-zA-Z0-9]+/;
var config = generateConfig(primaryOptions, secondaryOptions);

return function(root, result) {
var firstNode = root.nodes[0];
if (!firstNode || firstNode.type !== 'comment') return;
var ranges = findRanges(root);

var initialComment = firstNode.text;
if (!initialComment || !initialComment.match(RE_DIRECTIVE)) return;
root.eachRule(function(rule) {
var ruleStartLine = rule.source.start.line;
ranges.forEach(function(range) {
if (ruleStartLine < range.start) return;
if (range.end && ruleStartLine > range.end) return;
checkRule(rule, range);
})
});

var defined = initialComment.match(RE_DIRECTIVE)[1].trim();
var isUtilities = defined === UTILITIES_IDENT;
if (!isUtilities && !defined.match(componentNamePattern)) {
result.warn(
'Invalid component name in definition /*' + initialComment + '*/',
{ node: firstNode }
);
function checkRule(rule, range) {
if (range.defined === UTILITIES_IDENT) {
validateUtilities(rule, config.patterns.utilitySelectors, result);
return;
}
validateCustomProperties(rule, range.defined, result);
validateSelectors({
rule: rule,
componentName: range.defined,
weakMode: range.weakMode,
selectorPattern: config.patterns.componentSelectors,
selectorPatternOptions: config.presetOptions,
result: result,
});
}

var weakMode = initialComment.match(RE_DIRECTIVE)[2] === 'weak';
function findRanges(root) {
var ranges = [];
root.eachComment(function(comment) {
var startLine = comment.source.start.line;

if (isUtilities) {
validateUtilities(root, patterns.utilitySelectors, result);
return;
}
if (END_DIRECTIVE.test(comment.text)) {
endCurrentRange(startLine);
return;
}

validateSelectors(
root, defined, weakMode, patterns.componentSelectors, presetOptions, result
);
validateCustomProperties(root, defined, result);
console.log(result.messages)
var directiveMatch = comment.text.match(DEFINE_DIRECTIVE);
if (!directiveMatch) return;
var defined = (directiveMatch[1] || directiveMatch[3]).trim();
if (defined !== UTILITIES_IDENT && !defined.match(config.componentNamePattern)) {
result.warn(
'Invalid component name in definition /*' + comment + '*/',
{ node: comment }
);
}
endCurrentRange(startLine);
ranges.push({
defined: defined,
start: startLine,
weakMode: directiveMatch[2] === WEAK_IDENT,
});
});
return ranges;

function endCurrentRange(line) {
if (!ranges.length) return;
var lastRange = ranges[ranges.length - 1];
if (lastRange.end) return;
lastRange.end = line;
}
}
};
});
35 changes: 35 additions & 0 deletions lib/generate-config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
var presetPatterns = require('./preset-patterns');

/**
* Given some user options, put together a config object that
* the validators can use.
*
* @param {Object|String} [primaryOptions = 'suit']
* @param {RegExp} [primaryOptions.componentName]
* @param {RegExp} [primaryOptions.utilitySelectors]
* @param {Object|Function} [primaryOptions.componentSelectors]
* @param {String} [primaryOptions.preset] - The same as passing a string for `primaryOptions`
* @param {Object} [primaryOptions.presetOptions] - Options that are can be used by
* a pattern (e.g. `namespace`)
* @param {Object} [secondaryOptions] - The same as `primaryOptions.presetOptions`
* @return {Object} The configuration object
*/
module.exports = function(primaryOptions, secondaryOptions) {
var patterns = primaryOptions || 'suit';
if (typeof patterns === 'string') {
patterns = presetPatterns[patterns];
} else if (patterns.preset) {
patterns = presetPatterns[patterns.preset];
}

var presetOptions = secondaryOptions || {};
if (primaryOptions && primaryOptions.presetOptions) {
presetOptions = primaryOptions.presetOptions;
}

return {
patterns: patterns,
presetOptions: presetOptions,
componentNamePattern: patterns.componentName || /[-_a-zA-Z0-9]+/,
}
}
62 changes: 0 additions & 62 deletions lib/is-valid-selector.js

This file was deleted.

Loading

0 comments on commit 6f5f27b

Please sign in to comment.