diff --git a/CHANGELOG.md b/CHANGELOG.md index 34873c0..adfa72b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/README.md b/README.md index 5e98b9d..e5b3815 100644 --- a/README.md +++ b/README.md @@ -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.* @@ -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. @@ -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: @@ -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 */ @@ -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 @@ -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 diff --git a/index.js b/index.js index a6b87a5..86a47c3 100644 --- a/index.js +++ b/index.js @@ -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; + } + } }; }); diff --git a/lib/generate-config.js b/lib/generate-config.js new file mode 100644 index 0000000..52463ec --- /dev/null +++ b/lib/generate-config.js @@ -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]+/, + } +} diff --git a/lib/is-valid-selector.js b/lib/is-valid-selector.js deleted file mode 100644 index 727c868..0000000 --- a/lib/is-valid-selector.js +++ /dev/null @@ -1,62 +0,0 @@ -var listSequences = require('./listSequences'); - -/** - * A SelectorPattern defines acceptable patterns for selector sequences - * in component stylesheets. - * - * `selector-patterns.js` contains pre-defined - * and tested SelectorPatterns that the user can reference by name. - * Alternately, users can provide their own SelectorPattern to - * enforce custom conventions. - * - * The default SelectorPattern is "suit". - * - * A SelectorPattern can fit one of the following models: - * - A function that accepts a component name and returns a regexp. - * The returned regexp will be used to test all selector sequences. - * - An object that has the following properties: - * - initial: A function that accepts a component name and returns - * a regexp. This regexp will be used to test selector sequences - * at the beginning of the selector (before any combinators). - * If no combined property is included, this function will also - * be used to test sequences after combinators. - * - combined: A function that accepts a component name and returns - * a regexp. This regexp will be used to test selector sequences - * that come after combinators, if they do not already - * match the initial pattern. - */ - -/** - * @param {String} selector - The selector to test - * @param {String} componentName - The component's name - * @param {Boolean} weakMode - * @param {String|SelectorPattern} [pattern="suit"] - Either a string - * identifying a pre-defined SelectorPattern to use, or a custom - * SelectorPattern, as described above - * @param {Object} [presetOptions] - Options to pass to the pattern functions - * @returns {Boolean} - */ -module.exports = function(selector, componentName, weakMode, pattern, presetOptions) { - // Don't bother with :root - if (selector === ':root') return true; - - var initialPattern = (pattern.initial) ? - pattern.initial(componentName, presetOptions) : - pattern(componentName, presetOptions); - var combinedPattern = (pattern.combined) ? - pattern.combined(componentName, presetOptions) : - initialPattern; - var sequences = listSequences(selector); - - // Error if an acceptable initialPattern does not begin the selector - if (!initialPattern.test(sequences[0])) return false; - - // Unless in weak mode, error if combined simple selectors do not match the - // combinedPattern - if (weakMode) return true; - - return sequences.slice(1).every(function(combinedSequence) { - return initialPattern.test(combinedSequence) - || combinedPattern.test(combinedSequence); - }); -} diff --git a/lib/is-valid-utility.js b/lib/is-valid-utility.js deleted file mode 100644 index efd249a..0000000 --- a/lib/is-valid-utility.js +++ /dev/null @@ -1,13 +0,0 @@ -var listSequences = require('./listSequences'); - -/** - * @param {String} selector - * @param {RegExp} pattern - * @returns {Boolean} - */ -module.exports = function(selector, pattern) { - var sequences = listSequences(selector); - return sequences.every(function(sequence) { - return pattern.test(sequence); - }); -} diff --git a/lib/listSequences.js b/lib/list-sequences.js similarity index 100% rename from lib/listSequences.js rename to lib/list-sequences.js diff --git a/lib/validate-custom-properties.js b/lib/validate-custom-properties.js new file mode 100644 index 0000000..70ebb83 --- /dev/null +++ b/lib/validate-custom-properties.js @@ -0,0 +1,19 @@ +/** + * @param {Rule} rule - PostCSS rule + * @param {String} componentName + * @param {Result} result - PostCSS Result, for registering warnings + */ +module.exports = function(rule, componentName, result) { + rule.eachDecl(function(declaration, i) { + var property = declaration.prop; + if (property.indexOf('--') !== 0) return; + + if (property.indexOf(componentName + '-') === 2) return; + result.warn( + 'Invalid custom property name "' + property + '": ' + + 'a component\'s custom properties must start with the ' + + 'component name', + { node: declaration } + ); + }); +} diff --git a/lib/validate-properties.js b/lib/validate-properties.js deleted file mode 100644 index 9379163..0000000 --- a/lib/validate-properties.js +++ /dev/null @@ -1,23 +0,0 @@ -/** - * @param {Object} styles - * @param {String} componentName - * @param {Result} result - PostCSS Result, for registering warnings - */ -module.exports = function(styles, componentName, result) { - styles.eachRule(function(rule) { - if (rule.selectors[0] !== ':root') return; - - rule.eachDecl(function(declaration, i) { - var property = declaration.prop; - - if (property.indexOf(componentName + '-') !== 2) { - result.warn( - 'Invalid custom property name "' + property + '": ' + - 'a component\'s custom properties must start with the ' + - 'component name', - { node: declaration } - ); - } - }); - }); -} diff --git a/lib/validate-selectors.js b/lib/validate-selectors.js index 3429781..a9a16cb 100644 --- a/lib/validate-selectors.js +++ b/lib/validate-selectors.js @@ -1,35 +1,58 @@ -var isValid = require('./is-valid-selector'); +var listSequences = require('./list-sequences'); + var IGNORE_COMMENT = 'postcss-bem-linter: ignore'; /** - * @param {Object} styles - * @param {String} componentName - * @param {Boolean} weakMode - * @param {Object} pattern - * @param {Object} presetOptions - * @param {Result} result - PostCSS Result, for registering warnings + * @param {Object} config + * @param {Rule} config.rule - PostCSS Rule + * @param {String} config.componentName + * @param {Boolean} config.weakMode + * @param {Object} config.selectorPattern + * @param {Object} config.selectorPatternOptions + * @param {Result} config.result - PostCSS Result, for registering warnings */ -module.exports = function(styles, componentName, weakMode, pattern, presetOptions, result) { - styles.eachRule(function(rule) { - if (rule.parent && rule.parent.name === 'keyframes') return; - - var prev = rule.prev(); - if ( - prev - && prev.type === 'comment' - && prev.text === IGNORE_COMMENT - ) return; - - var selectors = rule.selectors; - - selectors.forEach(function(selector) { - // selectors must start with the componentName class, or be `:root` - if (!isValid(selector, componentName, weakMode, pattern, presetOptions)) { - result.warn( - 'Invalid component selector "' + selector + '"', - { node: rule } - ); - } - }); +module.exports = function(config) { + if (config.rule.parent && config.rule.parent.name === 'keyframes') return; + + var prev = config.rule.prev(); + if ( + prev + && prev.type === 'comment' + && prev.text === IGNORE_COMMENT + ) return; + + var selectors = config.rule.selectors; + + selectors.forEach(function(selector) { + if (isValid(selector)) return; + config.result.warn( + 'Invalid component selector "' + selector + '"', + { node: config.rule } + ); }); + + function isValid(selector) { + // Don't bother with :root + if (selector === ':root') return true; + + var initialPattern = (config.selectorPattern.initial) ? + config.selectorPattern.initial(config.componentName, config.selectorPatternOptions) : + config.selectorPattern(config.componentName, config.selectorPatternOptions); + var combinedPattern = (config.selectorPattern.combined) ? + config.selectorPattern.combined(config.componentName, config.selectorPatternOptions) : + initialPattern; + var sequences = listSequences(selector); + + // Not valid if an initialPattern does not begin the selector + if (!initialPattern.test(sequences[0])) return false; + + // Unless in weak mode, not valid if combined simple selectors do not match the + // combinedPattern + if (config.weakMode) return true; + + return sequences.slice(1).every(function(combinedSequence) { + return initialPattern.test(combinedSequence) + || combinedPattern.test(combinedSequence); + }); + } } diff --git a/lib/validate-utilities.js b/lib/validate-utilities.js index 0ad24b2..4f229ac 100644 --- a/lib/validate-utilities.js +++ b/lib/validate-utilities.js @@ -1,19 +1,23 @@ -var isValid = require('./is-valid-utility'); +var listSequences = require('./list-sequences'); /** - * @param {Object} styles + * @param {Rule} rule - PostCSS Rule * @param {RegExp} pattern * @param {Result} result - PostCSS Result, for registering warnings */ -module.exports = function(styles, pattern, result) { - styles.eachRule(function(rule) { - rule.selectors.forEach(function(selector) { - if (!isValid(selector, pattern)) { - result.warn( - 'Invalid utility selector "' + selector + '"', - { node: rule } - ); - } - }); +module.exports = function(rule, pattern, result) { + rule.selectors.forEach(function(selector) { + if (isValid(selector, pattern)) return; + result.warn( + 'Invalid utility selector "' + selector + '"', + { node: rule } + ); + }); +} + +function isValid(selector, pattern) { + var sequences = listSequences(selector); + return sequences.every(function(sequence) { + return pattern.test(sequence); }); } diff --git a/test/fixtures/ranges-one-ended-valid-one-invalid.css b/test/fixtures/ranges-one-ended-valid-one-invalid.css new file mode 100644 index 0000000..478bef9 --- /dev/null +++ b/test/fixtures/ranges-one-ended-valid-one-invalid.css @@ -0,0 +1,12 @@ +/** @define utilities */ + +.u-foo {} + +/** @end */ + +.blah {} +.blergh {} + +/** @define Foo */ + +.Fooooo {} diff --git a/test/fixtures/ranges-one-ended-valid.css b/test/fixtures/ranges-one-ended-valid.css new file mode 100644 index 0000000..6253525 --- /dev/null +++ b/test/fixtures/ranges-one-ended-valid.css @@ -0,0 +1,8 @@ +/** @define Foo */ + +.Foo {} + +/** @end */ + +.bar {} +.baz {} diff --git a/test/fixtures/ranges-one-valid-one-invalid.css b/test/fixtures/ranges-one-valid-one-invalid.css new file mode 100644 index 0000000..c4a6e2f --- /dev/null +++ b/test/fixtures/ranges-one-valid-one-invalid.css @@ -0,0 +1,7 @@ +/** @define Foo */ + +.Foo {} + +/** @define Bar */ + +.Barrrr {} diff --git a/test/fixtures/ranges-two-valid-one-invalid.css b/test/fixtures/ranges-two-valid-one-invalid.css new file mode 100644 index 0000000..d8037b8 --- /dev/null +++ b/test/fixtures/ranges-two-valid-one-invalid.css @@ -0,0 +1,17 @@ +/** @define utilities */ + +.u-something {} +.u-somethingElse {} +.u-another {} + +/** @define Foo */ + +.Foooooo {} +.Foo-bar {} +.Foo-bar--baz {} + +/** @define Bar */ + +.Bar {} +.Bar-baz {} +.Bar-baz--fo {} diff --git a/test/fixtures/ranges-two-valid.css b/test/fixtures/ranges-two-valid.css new file mode 100644 index 0000000..f76d91b --- /dev/null +++ b/test/fixtures/ranges-two-valid.css @@ -0,0 +1,7 @@ +/** @define Foo */ + +.Foo {} + +/** @define Bar */ + +.Bar {} diff --git a/test/fixtures/verbose-directives.css b/test/fixtures/verbose-directives.css new file mode 100644 index 0000000..202b430 --- /dev/null +++ b/test/fixtures/verbose-directives.css @@ -0,0 +1,17 @@ +/* postcss-bem-linter: define Foo */ + +.Foo {} +.Foo-bar {} + +/* postcss-bem-linter: end */ + +.blahblah {} + +/* postcss-bem-linter: define utilities */ + +.u-something {} +.u-somethingElse {} + +/* postcss-bem-linter: end */ + +.blergh {} diff --git a/test/property-validation.js b/test/property-validation.js index cc17eb6..a0fc6b3 100644 --- a/test/property-validation.js +++ b/test/property-validation.js @@ -25,8 +25,4 @@ describe('property validation', function() { ); } ); - - it('rejects invalid root declarations', function(done) { - assertSingleFailure(done, invDef + ':root { color: green; }'); - }); }); diff --git a/test/ranges.js b/test/ranges.js new file mode 100644 index 0000000..44f2da8 --- /dev/null +++ b/test/ranges.js @@ -0,0 +1,31 @@ +var util = require('./test-util'); +var assertSuccess = util.assertSuccess; +var assertSingleFailure = util.assertSingleFailure; +var selectorTester = util.selectorTester; +var fixture = util.fixture; + +describe('ranges', function() { + it('two valid', function(done) { + assertSuccess(done, fixture('ranges-two-valid')); + }); + + it('one valid, one invalid', function(done) { + assertSingleFailure(done, fixture('ranges-one-valid-one-invalid')); + }) + + it('two valid, one invalid', function(done) { + assertSingleFailure(done, fixture('ranges-two-valid-one-invalid')); + }) + + it('one valid that is ended, then some extra stuff', function(done) { + assertSuccess(done, fixture('ranges-one-ended-valid')); + }); + + it('one valid that is ended, then some extra stuff, then one invalid', function(done) { + assertSingleFailure(done, fixture('ranges-one-ended-valid-one-invalid')); + }); + + it('with verbose directives', function(done) { + assertSuccess(done, fixture('verbose-directives')); + }) +});