mirror of
https://github.com/DavidAnson/markdownlint.git
synced 2025-09-21 21:30:47 +02:00
Add support for shareable/extendable configuration via "extends" and helper functions (fixes #33).
This commit is contained in:
parent
d826833a82
commit
7528295cae
11 changed files with 385 additions and 25 deletions
120
README.md
120
README.md
|
@ -28,8 +28,8 @@ tool for [Node.js](https://nodejs.org/) and [io.js](https://iojs.org/) with a
|
|||
library of rules to enforce standards and consistency for Markdown files. It
|
||||
was inspired by - and heavily influenced by - Mark Harrison's
|
||||
[markdownlint](https://github.com/mivok/markdownlint) for
|
||||
[Ruby](https://www.ruby-lang.org/). The rules, rule documentation, and test
|
||||
cases come directly from that project.
|
||||
[Ruby](https://www.ruby-lang.org/). The initial rules, rule documentation, and
|
||||
test cases came directly from that project.
|
||||
|
||||
### Related
|
||||
|
||||
|
@ -142,11 +142,13 @@ space * in * emphasis <!-- markdownlint-disable --> <!-- markdownlint-enable -->
|
|||
|
||||
## API
|
||||
|
||||
### Linting
|
||||
|
||||
Standard asynchronous interface:
|
||||
|
||||
```js
|
||||
/**
|
||||
* Lint specified Markdown files according to configurable rules.
|
||||
* Lint specified Markdown files.
|
||||
*
|
||||
* @param {Object} options Configuration options.
|
||||
* @param {Function} callback Callback (err, result) function.
|
||||
|
@ -159,7 +161,7 @@ Synchronous interface (for build scripts, etc.):
|
|||
|
||||
```js
|
||||
/**
|
||||
* Lint specified Markdown files according to configurable rules.
|
||||
* Lint specified Markdown files synchronously.
|
||||
*
|
||||
* @param {Object} options Configuration options.
|
||||
* @returns {Object} Result object.
|
||||
|
@ -167,13 +169,13 @@ Synchronous interface (for build scripts, etc.):
|
|||
function markdownlint.sync(options) { ... }
|
||||
```
|
||||
|
||||
### options
|
||||
#### options
|
||||
|
||||
Type: `Object`
|
||||
|
||||
Configures the function.
|
||||
|
||||
#### options.files
|
||||
##### options.files
|
||||
|
||||
Type: `Array` of `String`
|
||||
|
||||
|
@ -185,7 +187,7 @@ responsibility.
|
|||
|
||||
Example: `[ "one.md", "dir/two.md" ]`
|
||||
|
||||
#### options.strings
|
||||
##### options.strings
|
||||
|
||||
Type: `Object` mapping `String` to `String`
|
||||
|
||||
|
@ -204,7 +206,7 @@ Example:
|
|||
}
|
||||
```
|
||||
|
||||
#### options.frontMatter
|
||||
##### options.frontMatter
|
||||
|
||||
Type: `RegExp`
|
||||
|
||||
|
@ -232,7 +234,7 @@ title: Title
|
|||
---
|
||||
```
|
||||
|
||||
#### options.config
|
||||
##### options.config
|
||||
|
||||
Type: `Object` mapping `String` to `Boolean | Object`
|
||||
|
||||
|
@ -277,7 +279,46 @@ See the [style](style) directory for more samples.
|
|||
See [markdownlint-config-schema.json](schema/markdownlint-config-schema.json)
|
||||
for the [JSON Schema](http://json-schema.org/) of the `options.config` object.
|
||||
|
||||
#### options.resultVersion
|
||||
For more advanced scenarios, styles can reference and extend other styles. The
|
||||
`readConfig` and `readConfigSync` functions can be used to read such styles.
|
||||
|
||||
For example, assuming a `base.json` configuration file:
|
||||
|
||||
```json
|
||||
{
|
||||
"default": true
|
||||
}
|
||||
```
|
||||
|
||||
And a `custom.json` configuration file:
|
||||
|
||||
```json
|
||||
{
|
||||
"extends": "base.json",
|
||||
"line-length": false
|
||||
}
|
||||
```
|
||||
|
||||
Then code like the following:
|
||||
|
||||
```js
|
||||
var options = {
|
||||
"config": markdownlint.readConfigSync("./custom.json")
|
||||
};
|
||||
```
|
||||
|
||||
Merges `custom.json` and `base.json` and is equivalent to:
|
||||
|
||||
```js
|
||||
var options = {
|
||||
"config": {
|
||||
"default": true,
|
||||
"line-length": false
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
##### options.resultVersion
|
||||
|
||||
Type: `Number`
|
||||
|
||||
|
@ -291,13 +332,13 @@ Passing a `resultVersion` of `1` corresponds to a more detailed format where eac
|
|||
error includes information about the line number, rule name, alias, description,
|
||||
as well as any additional detail or context that is available.
|
||||
|
||||
### callback
|
||||
#### callback
|
||||
|
||||
Type: `Function` taking (`Error`, `Object`)
|
||||
|
||||
Standard completion callback.
|
||||
|
||||
### result
|
||||
#### result
|
||||
|
||||
Type: `Object`
|
||||
|
||||
|
@ -305,6 +346,61 @@ Call `result.toString()` for convenience or see below for an example of the
|
|||
structure of the `result` object. Passing the value `true` to `toString()`
|
||||
uses rule aliases (ex: `no-hard-tabs`) instead of names (ex: `MD010`).
|
||||
|
||||
### Config
|
||||
|
||||
The `options.config` configuration object is simple and can be loaded as JSON
|
||||
in many cases. To take advantage of shared configuration where one file `extends`
|
||||
another, the following functions are useful.
|
||||
|
||||
Asynchronous interface:
|
||||
|
||||
```js
|
||||
/**
|
||||
* Read specified configuration file.
|
||||
*
|
||||
* @param {String} file Configuration file name/path.
|
||||
* @param {Function} callback Callback (err, result) function.
|
||||
* @returns {void}
|
||||
*/
|
||||
function readConfig(file, callback) { ... }
|
||||
```
|
||||
|
||||
Synchronous interface:
|
||||
|
||||
```js
|
||||
/**
|
||||
* Read specified configuration file synchronously.
|
||||
*
|
||||
* @param {String} file Configuration file name/path.
|
||||
* @returns {Object} Configuration object.
|
||||
*/
|
||||
function readConfigSync(file) { ... }
|
||||
```
|
||||
|
||||
#### file
|
||||
|
||||
Type: `String`
|
||||
|
||||
Location of JSON configuration file to read.
|
||||
|
||||
The `file` is resolved relative to the current working directory. If an `extends`
|
||||
key is present once read, its value will be resolved as a path relative to `file`
|
||||
and loaded recursively. Settings from a file referenced by `extends` are applied
|
||||
first, then those of `file` are applied on top (overriding any of the same keys
|
||||
appearing in the referenced file).
|
||||
|
||||
#### callback
|
||||
|
||||
Type: `Function` taking (`Error`, `Object`)
|
||||
|
||||
Standard completion callback.
|
||||
|
||||
#### result
|
||||
|
||||
Type: `Object`
|
||||
|
||||
Configuration object.
|
||||
|
||||
## Usage
|
||||
|
||||
Invoke `markdownlint` and use the `result` object's `toString` method:
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
"use strict";
|
||||
|
||||
var fs = require("fs");
|
||||
var path = require("path");
|
||||
var md = require("markdown-it")({ "html": true });
|
||||
var rules = require("./rules");
|
||||
var shared = require("./shared");
|
||||
|
@ -329,7 +330,7 @@ function markdownlintSynchronousCallback() {
|
|||
}
|
||||
|
||||
/**
|
||||
* Lint specified Markdown files according to configurable rules.
|
||||
* Lint specified Markdown files.
|
||||
*
|
||||
* @param {Object} options Configuration options.
|
||||
* @param {Function} callback Callback (err, result) function.
|
||||
|
@ -387,7 +388,7 @@ function markdownlint(options, callback) {
|
|||
}
|
||||
|
||||
/**
|
||||
* Lint specified Markdown files according to configurable rules.
|
||||
* Lint specified Markdown files synchronously.
|
||||
*
|
||||
* @param {Object} options Configuration options.
|
||||
* @returns {Object} Result object.
|
||||
|
@ -396,6 +397,63 @@ function markdownlintSync(options) {
|
|||
return markdownlint(options, markdownlintSynchronousCallback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Read specified configuration file.
|
||||
*
|
||||
* @param {String} file Configuration file name/path.
|
||||
* @param {Function} callback Callback (err, result) function.
|
||||
* @returns {void}
|
||||
*/
|
||||
function readConfig(file, callback) {
|
||||
// Read file
|
||||
fs.readFile(file, shared.utf8Encoding, function handleFile(err, content) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
// Parse file
|
||||
var config = null;
|
||||
try {
|
||||
config = JSON.parse(content);
|
||||
} catch (ex) {
|
||||
return callback(ex);
|
||||
}
|
||||
if (config.extends) {
|
||||
// Extend configuration
|
||||
var extendsFile = path.resolve(path.dirname(file), config.extends);
|
||||
readConfig(extendsFile, function handleConfig(errr, extendsConfig) {
|
||||
if (errr) {
|
||||
return callback(errr);
|
||||
}
|
||||
delete config.extends;
|
||||
callback(null, shared.assign(extendsConfig, config));
|
||||
});
|
||||
} else {
|
||||
callback(null, config);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Read specified configuration file synchronously.
|
||||
*
|
||||
* @param {String} file Configuration file name/path.
|
||||
* @returns {Object} Configuration object.
|
||||
*/
|
||||
function readConfigSync(file) {
|
||||
// Parse file
|
||||
var config = JSON.parse(fs.readFileSync(file, shared.utf8Encoding));
|
||||
if (config.extends) {
|
||||
// Extend configuration
|
||||
config = shared.assign(
|
||||
readConfigSync(path.resolve(path.dirname(file), config.extends)),
|
||||
config);
|
||||
delete config.extends;
|
||||
}
|
||||
return config;
|
||||
}
|
||||
|
||||
// Export a/synchronous APIs
|
||||
module.exports = markdownlint;
|
||||
module.exports.sync = markdownlintSync;
|
||||
module.exports.readConfig = readConfig;
|
||||
module.exports.readConfigSync = readConfigSync;
|
||||
|
|
|
@ -13,6 +13,11 @@ var schema = {
|
|||
"description": "Default state for all rules",
|
||||
"type": "boolean",
|
||||
"default": true
|
||||
},
|
||||
"extends": {
|
||||
"description": "Path to configuration file to extend",
|
||||
"type": "string",
|
||||
"default": null
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
|
|
|
@ -7,6 +7,11 @@
|
|||
"type": "boolean",
|
||||
"default": true
|
||||
},
|
||||
"extends": {
|
||||
"description": "Path to configuration file to extend",
|
||||
"type": "string",
|
||||
"default": null
|
||||
},
|
||||
"MD001": {
|
||||
"description": "MD001/header-increment - Header levels should only increment by one level at a time",
|
||||
"type": "boolean",
|
||||
|
|
4
test/config-child.json
Normal file
4
test/config-child.json
Normal file
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"no-hard-tabs": false,
|
||||
"whitespace": false
|
||||
}
|
6
test/config-grandparent.json
Normal file
6
test/config-grandparent.json
Normal file
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"extends": "config-parent.json",
|
||||
"MD003": { "style": "atx_closed" },
|
||||
"MD007": { "indent": 2 },
|
||||
"no-hard-tabs": false
|
||||
}
|
7
test/config-parent.json
Normal file
7
test/config-parent.json
Normal file
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"extends": "config-child.json",
|
||||
"MD003": false,
|
||||
"MD007": { "indent": 4 },
|
||||
"no-hard-tabs": true,
|
||||
"line-length": { "line_length": 200 }
|
||||
}
|
4
test/config/config-badchildfile.json
Normal file
4
test/config/config-badchildfile.json
Normal file
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"extends": "config-badfile.json",
|
||||
"default": true
|
||||
}
|
4
test/config/config-badchildjson.json
Normal file
4
test/config/config-badchildjson.json
Normal file
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"extends": "config-badjson.json",
|
||||
"default": true
|
||||
}
|
3
test/config/config-badjson.json
Normal file
3
test/config/config-badjson.json
Normal file
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
bad json
|
||||
}
|
|
@ -87,7 +87,7 @@ module.exports.projectFiles = function projectFiles(test) {
|
|||
};
|
||||
markdownlint(options, function callback(err, actual) {
|
||||
test.ifError(err);
|
||||
var expected = { "README.md": {} };
|
||||
var expected = { "README.md": { "MD024": [ 392, 398 ] } };
|
||||
test.deepEqual(actual, expected, "Issue(s) with project files.");
|
||||
test.done();
|
||||
});
|
||||
|
@ -782,14 +782,19 @@ module.exports.readmeHeaders = function readmeHeaders(test) {
|
|||
"## Tags",
|
||||
"## Configuration",
|
||||
"## API",
|
||||
"### options",
|
||||
"#### options.files",
|
||||
"#### options.strings",
|
||||
"#### options.frontMatter",
|
||||
"#### options.config",
|
||||
"#### options.resultVersion",
|
||||
"### callback",
|
||||
"### result",
|
||||
"### Linting",
|
||||
"#### options",
|
||||
"##### options.files",
|
||||
"##### options.strings",
|
||||
"##### options.frontMatter",
|
||||
"##### options.config",
|
||||
"##### options.resultVersion",
|
||||
"#### callback",
|
||||
"#### result",
|
||||
"### Config",
|
||||
"#### file",
|
||||
"#### callback",
|
||||
"#### result",
|
||||
"## Usage",
|
||||
"## Browser",
|
||||
"## History"
|
||||
|
@ -798,7 +803,7 @@ module.exports.readmeHeaders = function readmeHeaders(test) {
|
|||
}
|
||||
}, function callback(err, result) {
|
||||
test.ifError(err);
|
||||
var expected = { "README.md": {} };
|
||||
var expected = { "README.md": { "MD024": [ 392, 398 ] } };
|
||||
test.deepEqual(result, expected, "Unexpected issues.");
|
||||
test.done();
|
||||
});
|
||||
|
@ -825,7 +830,7 @@ module.exports.filesArrayAsString = function filesArrayAsString(test) {
|
|||
"config": { "MD013": { "line_length": 150 } }
|
||||
}, function callback(err, actual) {
|
||||
test.ifError(err);
|
||||
var expected = { "README.md": {} };
|
||||
var expected = { "README.md": { "MD024": [ 392, 398 ] } };
|
||||
test.deepEqual(actual, expected, "Unexpected issues.");
|
||||
test.done();
|
||||
});
|
||||
|
@ -1109,3 +1114,166 @@ module.exports.trimPolyfills = function trimPolyfills(test) {
|
|||
});
|
||||
test.done();
|
||||
};
|
||||
|
||||
module.exports.configSingle = function configSingle(test) {
|
||||
test.expect(2);
|
||||
markdownlint.readConfig("./test/config-child.json",
|
||||
function callback(err, actual) {
|
||||
test.ifError(err);
|
||||
var expected = require("./config-child.json");
|
||||
test.deepEqual(actual, expected, "Config object not correct.");
|
||||
test.done();
|
||||
});
|
||||
};
|
||||
|
||||
module.exports.configAbsolute = function configAbsolute(test) {
|
||||
test.expect(2);
|
||||
markdownlint.readConfig(path.join(__dirname, "config-child.json"),
|
||||
function callback(err, actual) {
|
||||
test.ifError(err);
|
||||
var expected = require("./config-child.json");
|
||||
test.deepEqual(actual, expected, "Config object not correct.");
|
||||
test.done();
|
||||
});
|
||||
};
|
||||
|
||||
module.exports.configMultiple = function configMultiple(test) {
|
||||
test.expect(2);
|
||||
markdownlint.readConfig("./test/config-grandparent.json",
|
||||
function callback(err, actual) {
|
||||
test.ifError(err);
|
||||
var expected = shared.assign(shared.assign(shared.assign({},
|
||||
require("./config-child.json")),
|
||||
require("./config-parent.json")),
|
||||
require("./config-grandparent.json"));
|
||||
delete expected.extends;
|
||||
test.deepEqual(actual, expected, "Config object not correct.");
|
||||
test.done();
|
||||
});
|
||||
};
|
||||
|
||||
module.exports.configBadFile = function configBadFile(test) {
|
||||
test.expect(4);
|
||||
markdownlint.readConfig("./test/config/config-badfile.json",
|
||||
function callback(err, result) {
|
||||
test.ok(err, "Did not get an error for bad file.");
|
||||
test.ok(err instanceof Error, "Error not instance of Error.");
|
||||
test.equal(err.code, "ENOENT", "Error code for bad file not ENOENT.");
|
||||
test.ok(!result, "Got result for bad file.");
|
||||
test.done();
|
||||
});
|
||||
};
|
||||
|
||||
module.exports.configBadChildFile = function configBadChildFile(test) {
|
||||
test.expect(4);
|
||||
markdownlint.readConfig("./test/config/config-badchildfile.json",
|
||||
function callback(err, result) {
|
||||
test.ok(err, "Did not get an error for bad child file.");
|
||||
test.ok(err instanceof Error, "Error not instance of Error.");
|
||||
test.equal(err.code, "ENOENT",
|
||||
"Error code for bad child file not ENOENT.");
|
||||
test.ok(!result, "Got result for bad child file.");
|
||||
test.done();
|
||||
});
|
||||
};
|
||||
|
||||
module.exports.configBadJson = function configBadJson(test) {
|
||||
test.expect(3);
|
||||
markdownlint.readConfig("./test/config/config-badjson.json",
|
||||
function callback(err, result) {
|
||||
test.ok(err, "Did not get an error for bad JSON.");
|
||||
test.ok(err instanceof Error, "Error not instance of Error.");
|
||||
test.ok(!result, "Got result for bad JSON.");
|
||||
test.done();
|
||||
});
|
||||
};
|
||||
|
||||
module.exports.configBadChildJson = function configBadChildJson(test) {
|
||||
test.expect(3);
|
||||
markdownlint.readConfig("./test/config/config-badchildjson.json",
|
||||
function callback(err, result) {
|
||||
test.ok(err, "Did not get an error for bad child JSON.");
|
||||
test.ok(err instanceof Error, "Error not instance of Error.");
|
||||
test.ok(!result, "Got result for bad child JSON.");
|
||||
test.done();
|
||||
});
|
||||
};
|
||||
|
||||
module.exports.configSingleSync = function configSingleSync(test) {
|
||||
test.expect(1);
|
||||
var actual = markdownlint.readConfigSync("./test/config-child.json");
|
||||
var expected = require("./config-child.json");
|
||||
test.deepEqual(actual, expected, "Config object not correct.");
|
||||
test.done();
|
||||
};
|
||||
|
||||
module.exports.configAbsoluteSync = function configAbsoluteSync(test) {
|
||||
test.expect(1);
|
||||
var actual = markdownlint.readConfigSync(
|
||||
path.join(__dirname, "config-child.json"));
|
||||
var expected = require("./config-child.json");
|
||||
test.deepEqual(actual, expected, "Config object not correct.");
|
||||
test.done();
|
||||
};
|
||||
|
||||
module.exports.configMultipleSync = function configMultipleSync(test) {
|
||||
test.expect(1);
|
||||
var actual = markdownlint.readConfigSync("./test/config-grandparent.json");
|
||||
var expected = shared.assign(shared.assign(shared.assign({},
|
||||
require("./config-child.json")),
|
||||
require("./config-parent.json")),
|
||||
require("./config-grandparent.json"));
|
||||
delete expected.extends;
|
||||
test.deepEqual(actual, expected, "Config object not correct.");
|
||||
test.done();
|
||||
};
|
||||
|
||||
module.exports.configBadFileSync = function configBadFileSync(test) {
|
||||
test.expect(4);
|
||||
test.throws(function badFileCall() {
|
||||
markdownlint.readConfigSync("./test/config-badfile.json");
|
||||
}, function testError(err) {
|
||||
test.ok(err, "Did not get an error for bad file.");
|
||||
test.ok(err instanceof Error, "Error not instance of Error.");
|
||||
test.equal(err.code, "ENOENT", "Error code for bad file not ENOENT.");
|
||||
return true;
|
||||
}, "Did not get exception for bad file.");
|
||||
test.done();
|
||||
};
|
||||
|
||||
module.exports.configBadChildFileSync = function configBadChildFileSync(test) {
|
||||
test.expect(4);
|
||||
test.throws(function badChildFileCall() {
|
||||
markdownlint.readConfigSync("./test/config-badfile.json");
|
||||
}, function testError(err) {
|
||||
test.ok(err, "Did not get an error for bad child file.");
|
||||
test.ok(err instanceof Error, "Error not instance of Error.");
|
||||
test.equal(err.code, "ENOENT", "Error code for bad child file not ENOENT.");
|
||||
return true;
|
||||
}, "Did not get exception for bad child file.");
|
||||
test.done();
|
||||
};
|
||||
|
||||
module.exports.configBadJsonSync = function configBadJsonSync(test) {
|
||||
test.expect(3);
|
||||
test.throws(function badJsonCall() {
|
||||
markdownlint.readConfigSync("./test/config-badjson.json");
|
||||
}, function testError(err) {
|
||||
test.ok(err, "Did not get an error for bad JSON.");
|
||||
test.ok(err instanceof Error, "Error not instance of Error.");
|
||||
return true;
|
||||
}, "Did not get exception for bad JSON.");
|
||||
test.done();
|
||||
};
|
||||
|
||||
module.exports.configBadChildJsonSync = function configBadChildJsonSync(test) {
|
||||
test.expect(3);
|
||||
test.throws(function badChildJsonCall() {
|
||||
markdownlint.readConfigSync("./test/config-badchildjson.json");
|
||||
}, function testError(err) {
|
||||
test.ok(err, "Did not get an error for bad child JSON.");
|
||||
test.ok(err instanceof Error, "Error not instance of Error.");
|
||||
return true;
|
||||
}, "Did not get exception for bad child JSON.");
|
||||
test.done();
|
||||
};
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue