Add support for external rules

*   Add example rule, and rule object file, to
    `test/external`;
*   Add fixtures for external tests;
*   Move `mdast.js`, `mdast.min.js` to `mdast-lint.js`,
    `mdast-lint.min.js`;
*   Add docs for external rules.

Closes GH-14.
This commit is contained in:
Titus Wormer 2015-06-12 22:16:50 +02:00
parent 6d2ba6557e
commit 5162a0982b
12 changed files with 308 additions and 8 deletions

View File

@ -8,6 +8,7 @@ fix their warnings.
* [Rules](#rules)
* [externals](#externals)
* [reset](#reset)
* [blockquote-indentation](#blockquote-indentation)
* [code-block-style](#code-block-style)
@ -69,10 +70,27 @@ Remember that rules can always be turned off by
passing false. In addition, when reset is given, values can
be null or undefined in order to be ignored.
### externals
````md
<!-- Load more rules -->
```json
{
"externals": ["foo", "bar", "baz"]
}
```
````
Externals contains a list of extra rules to load.
These are, or refer to, an object mapping `ruleId`s to rules.
Note that in node.js, a string can be given (a module
name or a file), but in the browser an object must be passed in.
### reset
````md
<!-- Explicitly activate rules: -->
<!-- Explicitly activate rules: -->
```json
{
"reset": true,

View File

@ -14,10 +14,20 @@
var range = require('mdast-range');
var zone = require('mdast-zone');
var rules = require('./rules');
var internals = require('./rules');
var sort = require('./sort');
var filter = require('./filter');
/*
* Needed for plug-in resolving.
*/
var path = require('path');
var fs = require('fs');
var exists = fs && fs.existsSync;
var resolve = path && path.resolve;
var cwd = process && process.cwd();
/**
* Factory to create a plugin from a rule.
*
@ -85,6 +95,78 @@ function attachFactory(id, rule, options) {
return attach;
}
/**
* Require an external. Checks, in this order:
*
* - `$cwd/$pathlike`;
* - `$cwd/$pathlike.js`;
* - `$cwd/node_modules/$pathlike`;
* - `$pathlike`.
*
* Where `$cwd` is the current working directory.
*
* @example
* var plugin = findPlugin('foo');
*
* @throws {Error} - Fails when `pathlike` cannot be
* resolved.
* @param {string} pathlike - Reference to external.
* @return {Object} - Result of `require`ing external.
*/
function loadExternal(pathlike) {
var local = resolve(cwd, pathlike);
var current = resolve(cwd, 'node_modules', pathlike);
var plugin;
if (exists(local) || exists(local + '.js')) {
plugin = local;
/* istanbul ignore else - for globals */
} else if (exists(current)) {
plugin = current;
} else {
plugin = pathlike;
}
return require(plugin);
}
/**
* Load all externals. Merges them into a single rule
* object.
*
* In node, accepts externals as strings, otherwise,
* externals should be a list of objects.
*
* @param {Array.<string|Object>} externals
* @return {Array.<Object>}
* @throws {Error} - When an external cannot be resolved.
*/
function loadExternals(externals) {
var index = -1;
var rules = {};
var external;
var ruleId;
var mapping = externals ? externals.concat() : [];
var length;
mapping.push(internals);
length = mapping.length;
while (++index < length) {
external = mapping[index];
if (typeof external === 'string') {
external = loadExternal(external);
}
for (ruleId in external) {
rules[ruleId] = external[ruleId];
}
}
return rules;
}
/**
* Lint attacher.
*
@ -105,6 +187,7 @@ function attachFactory(id, rule, options) {
function lint(mdast, options) {
var settings = options || {};
var reset = settings.reset;
var rules = loadExternals(settings.external);
var id;
var setting;

View File

@ -1,4 +1,4 @@
(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.mdast = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){
(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.mdastLint = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){
'use strict';
module.exports = require('./lib');
@ -94,10 +94,20 @@ module.exports = attacher;
var range = require('mdast-range');
var zone = require('mdast-zone');
var rules = require('./rules');
var internals = require('./rules');
var sort = require('./sort');
var filter = require('./filter');
/*
* Needed for plug-in resolving.
*/
var path = require('path');
var fs = require('fs');
var exists = fs && fs.existsSync;
var resolve = path && path.resolve;
var cwd = process && process.cwd();
/**
* Factory to create a plugin from a rule.
*
@ -165,6 +175,78 @@ function attachFactory(id, rule, options) {
return attach;
}
/**
* Require an external. Checks, in this order:
*
* - `$cwd/$pathlike`;
* - `$cwd/$pathlike.js`;
* - `$cwd/node_modules/$pathlike`;
* - `$pathlike`.
*
* Where `$cwd` is the current working directory.
*
* @example
* var plugin = findPlugin('foo');
*
* @throws {Error} - Fails when `pathlike` cannot be
* resolved.
* @param {string} pathlike - Reference to external.
* @return {Object} - Result of `require`ing external.
*/
function loadExternal(pathlike) {
var local = resolve(cwd, pathlike);
var current = resolve(cwd, 'node_modules', pathlike);
var plugin;
if (exists(local) || exists(local + '.js')) {
plugin = local;
/* istanbul ignore else - for globals */
} else if (exists(current)) {
plugin = current;
} else {
plugin = pathlike;
}
return require(plugin);
}
/**
* Load all externals. Merges them into a single rule
* object.
*
* In node, accepts externals as strings, otherwise,
* externals should be a list of objects.
*
* @param {Array.<string|Object>} externals
* @return {Array.<Object>}
* @throws {Error} - When an external cannot be resolved.
*/
function loadExternals(externals) {
var index = -1;
var rules = {};
var external;
var ruleId;
var mapping = externals ? externals.concat() : [];
var length;
mapping.push(internals);
length = mapping.length;
while (++index < length) {
external = mapping[index];
if (typeof external === 'string') {
external = loadExternal(external);
}
for (ruleId in external) {
rules[ruleId] = external[ruleId];
}
}
return rules;
}
/**
* Lint attacher.
*
@ -185,6 +267,7 @@ function attachFactory(id, rule, options) {
function lint(mdast, options) {
var settings = options || {};
var reset = settings.reset;
var rules = loadExternals(settings.external);
var id;
var setting;
@ -322,7 +405,7 @@ function lint(mdast, options) {
module.exports = lint;
},{"./filter":2,"./rules":18,"./sort":58,"mdast-range":64,"mdast-zone":65}],4:[function(require,module,exports){
},{"./filter":2,"./rules":18,"./sort":58,"fs":undefined,"mdast-range":64,"mdast-zone":65,"path":undefined}],4:[function(require,module,exports){
/**
* @author Titus Wormer
* @copyright 2015 Titus Wormer. All rights reserved.

1
mdast-lint.min.js vendored Normal file

File diff suppressed because one or more lines are too long

1
mdast.min.js vendored

File diff suppressed because one or more lines are too long

View File

@ -55,8 +55,8 @@
"make": "npm run lint && npm run test-coverage",
"build-rules": "node script/build-rule-documentation.js",
"build-md": "mdast . LICENSE --output",
"build-bundle": "browserify index.js -s mdast > mdast.js",
"postbuild-bundle": "esmangle mdast.js > mdast.min.js",
"build-bundle": "browserify index.js --bare -s mdastLint > mdast-lint.js",
"postbuild-bundle": "esmangle mdast-lint.js > mdast-lint.min.js",
"build": "npm run build-rules && npm run build-md && npm run build-bundle",
"prepublish": "npm run build"
}

View File

@ -2,5 +2,9 @@
"reset": {
"description": "By default, all rules are turned on unless explicitly set to `false`.\nWhen `reset: true`, the opposite is true: all rules are turned off,\nunless when given a non-nully and non-false value.\n\nOptions: `boolean`, default: `false`.",
"example": " <!-- Explicitly activate rules: -->\n ```json\n {\n \"reset\": true,\n \"final-newline\": true\n }\n ```\n"
},
"externals": {
"description": "Externals contains a list of extra rules to load.\nThese are, or refer to, an object mapping `ruleId`s to rules.\n\nNote that in node.js, a string can be given (a module\nname or a file), but in the browser an object must be passed in.",
"example": " <!-- Load more rules -->\n ```json\n {\n \"externals\": [\"foo\", \"bar\", \"baz\"]\n }\n ```\n"
}
}

16
test/external/index.js vendored Normal file
View File

@ -0,0 +1,16 @@
/**
* @author Titus Wormer
* @copyright 2015 Titus Wormer. All rights reserved.
* @module External
* @fileoverview Map of example external rules.
*/
'use strict';
/*
* Expose.
*/
module.exports = {
'no-lorem': require('./no-lorem')
};

40
test/external/no-lorem.js vendored Normal file
View File

@ -0,0 +1,40 @@
/**
* @author Titus Wormer
* @copyright 2015 Titus Wormer. All rights reserved.
* @module no-tabs
* @fileoverview
* Warn when `lorem` is used in a document.
* @example
* <!-- Invalid: -->
* lorem
*
* <!-- Valid: -->
* ipsum
*/
'use strict';
/**
* Warn when `lorem` is used in a document.
*
* @param {Node} ast - Root node.
* @param {File} file - Virtual file.
* @param {*} preferred - Ignored.
* @param {Function} done - Callback.
*/
function noLorem(ast, file, preferred, done) {
var content = file.toString();
var expression = /\blorem\b/gi;
while (expression.exec(content)) {
file.warn('Do not use lorem', file.offsetToPosition(expression.lastIndex));
}
done();
}
/*
* Expose.
*/
module.exports = noLorem;

1
test/fixtures/lorem-invalid.md vendored Normal file
View File

@ -0,0 +1 @@
Lorem.

1
test/fixtures/lorem-valid.md vendored Normal file
View File

@ -0,0 +1 @@
Ipsum.

View File

@ -206,6 +206,60 @@ describe('mdast-lint', function () {
});
});
/*
* Validate external rules.
*/
describe('External', function () {
describe('Load external rules with require', function () {
assertFile('lorem-invalid.md', [
'lorem-invalid.md:1:1: Do not use lorem'
], null, {
'external': ['test/external']
});
});
describe('Load external rules with require and without `.js` extension', function () {
assertFile('lorem-invalid.md', [
'lorem-invalid.md:1:1: Do not use lorem'
], null, {
'external': ['test/external/index']
});
});
describe('Load external rules with require and with `.js` extension', function () {
assertFile('lorem-invalid.md', [
'lorem-invalid.md:1:1: Do not use lorem'
], null, {
'external': ['test/external/index.js']
});
});
describe('Load local external rules', function () {
it('should fail on invalid external rules', function () {
assert.throws(function () {
process('lorem-valid.md', {
'external': ['mdast']
});
});
});
});
describe('Load external rules by passing in', function () {
var external = require('./external');
assertFile('lorem-invalid.md', [
'lorem-invalid.md:1:1: Do not use lorem'
], null, {
'external': [external]
});
});
});
/*
* Validate inline en- and disabling.
*/
describe('Comments', function () {
describe('Disable and re-enable rules based on markers', function () {
assertFile('comments-disable.md', [