mirror of
https://github.com/DavidAnson/markdownlint.git
synced 2025-09-21 21:30:47 +02:00
Add parsers parameter to readConfig/Sync to support non-JSON formats like YAML (fixes #118).
This commit is contained in:
parent
2b4ecdced8
commit
101edd8496
10 changed files with 252 additions and 31 deletions
34
README.md
34
README.md
|
@ -400,9 +400,13 @@ 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.
|
||||
The `options.config` configuration object is simple and can be stored in a file
|
||||
for readability and easy reuse. The `readConfig` and `readConfigSync` functions
|
||||
load configuration settings and support the `extends` keyword for referencing
|
||||
other files (see above).
|
||||
|
||||
By default, configuration files are parsed as JSON (and named `.markdownlint.json`).
|
||||
Custom parsers can be provided to handle other formats like JSONC, YAML, and TOML.
|
||||
|
||||
Asynchronous interface:
|
||||
|
||||
|
@ -411,10 +415,11 @@ Asynchronous interface:
|
|||
* Read specified configuration file.
|
||||
*
|
||||
* @param {String} file Configuration file name/path.
|
||||
* @param {Array} [parsers] Optional parsing function(s).
|
||||
* @param {Function} callback Callback (err, result) function.
|
||||
* @returns {void}
|
||||
*/
|
||||
function readConfig(file, callback) { ... }
|
||||
function readConfig(file, parsers, callback) { ... }
|
||||
```
|
||||
|
||||
Synchronous interface:
|
||||
|
@ -424,16 +429,17 @@ Synchronous interface:
|
|||
* Read specified configuration file synchronously.
|
||||
*
|
||||
* @param {String} file Configuration file name/path.
|
||||
* @param {Array} [parsers] Optional parsing function(s).
|
||||
* @returns {Object} Configuration object.
|
||||
*/
|
||||
function readConfigSync(file) { ... }
|
||||
function readConfigSync(file, parsers) { ... }
|
||||
```
|
||||
|
||||
#### file
|
||||
|
||||
Type: `String`
|
||||
|
||||
Location of JSON configuration file to read.
|
||||
Location of 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`
|
||||
|
@ -441,6 +447,22 @@ 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).
|
||||
|
||||
#### parsers
|
||||
|
||||
Type: *Optional* `Array` of `Function` taking (`String`) and returning `Object`
|
||||
|
||||
Array of functions to parse configuration files.
|
||||
|
||||
The contents of a configuration file are passed to each parser function until one
|
||||
of them returns a value (vs. throwing an exception). Consequently, strict parsers
|
||||
should come before flexible parsers.
|
||||
|
||||
For example:
|
||||
|
||||
```js
|
||||
[ JSON.parse, require("toml").parse, require("js-yaml").safeLoad ]
|
||||
```
|
||||
|
||||
#### callback
|
||||
|
||||
Type: `Function` taking (`Error`, `Object`)
|
||||
|
|
|
@ -422,6 +422,7 @@ function lintFile(
|
|||
}
|
||||
}
|
||||
|
||||
// Lints files and strings
|
||||
function lintInput(options, synchronous, callback) {
|
||||
// Normalize inputs
|
||||
options = options || {};
|
||||
|
@ -517,34 +518,64 @@ function markdownlintSync(options) {
|
|||
return results;
|
||||
}
|
||||
|
||||
// Parses the content of a configuration file
|
||||
function parseConfiguration(name, content, parsers) {
|
||||
let config = null;
|
||||
let message = "";
|
||||
const errors = [];
|
||||
// Try each parser
|
||||
(parsers || [ JSON.parse ]).every((parser) => {
|
||||
try {
|
||||
config = parser(content);
|
||||
} catch (ex) {
|
||||
errors.push(ex.message);
|
||||
}
|
||||
return !config;
|
||||
});
|
||||
// Message if unable to parse
|
||||
if (!config) {
|
||||
errors.unshift(`Unable to parse '${name}'`);
|
||||
message = errors.join("; ");
|
||||
}
|
||||
return {
|
||||
config,
|
||||
message
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Read specified configuration file.
|
||||
*
|
||||
* @param {String} file Configuration file name/path.
|
||||
* @param {Array} [parsers] Optional parsing function(s).
|
||||
* @param {Function} callback Callback (err, result) function.
|
||||
* @returns {void}
|
||||
*/
|
||||
function readConfig(file, callback) {
|
||||
function readConfig(file, parsers, callback) {
|
||||
if (!callback) {
|
||||
// @ts-ignore
|
||||
callback = parsers;
|
||||
parsers = null;
|
||||
}
|
||||
// Read file
|
||||
fs.readFile(file, shared.utf8Encoding, function handleFile(err, content) {
|
||||
fs.readFile(file, shared.utf8Encoding, (err, content) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
// Parse file
|
||||
let config = null;
|
||||
try {
|
||||
config = JSON.parse(content);
|
||||
} catch (ex) {
|
||||
return callback(ex);
|
||||
// Try to parse file
|
||||
const { config, message } = parseConfiguration(file, content, parsers);
|
||||
if (!config) {
|
||||
return callback(new Error(message));
|
||||
}
|
||||
if (config.extends) {
|
||||
// Extend configuration
|
||||
const extendsFile = path.resolve(path.dirname(file), config.extends);
|
||||
readConfig(extendsFile, function handleConfig(errr, extendsConfig) {
|
||||
// Extend configuration
|
||||
const configExtends = config.extends;
|
||||
if (configExtends) {
|
||||
delete config.extends;
|
||||
const extendsFile = path.resolve(path.dirname(file), configExtends);
|
||||
readConfig(extendsFile, parsers, (errr, extendsConfig) => {
|
||||
if (errr) {
|
||||
return callback(errr);
|
||||
}
|
||||
delete config.extends;
|
||||
callback(null, shared.assign(extendsConfig, config));
|
||||
});
|
||||
} else {
|
||||
|
@ -557,17 +588,24 @@ function readConfig(file, callback) {
|
|||
* Read specified configuration file synchronously.
|
||||
*
|
||||
* @param {String} file Configuration file name/path.
|
||||
* @param {Array} [parsers] Optional parsing function(s).
|
||||
* @returns {Object} Configuration object.
|
||||
*/
|
||||
function readConfigSync(file) {
|
||||
// Parse file
|
||||
let 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);
|
||||
function readConfigSync(file, parsers) {
|
||||
// Read file
|
||||
const content = fs.readFileSync(file, shared.utf8Encoding);
|
||||
// Try to parse file
|
||||
const { config, message } = parseConfiguration(file, content, parsers);
|
||||
if (!config) {
|
||||
throw new Error(message);
|
||||
}
|
||||
// Extend configuration
|
||||
const configExtends = config.extends;
|
||||
if (configExtends) {
|
||||
delete config.extends;
|
||||
return shared.assign(
|
||||
readConfigSync(path.resolve(path.dirname(file), configExtends), parsers),
|
||||
config);
|
||||
}
|
||||
return config;
|
||||
}
|
||||
|
|
|
@ -35,8 +35,10 @@
|
|||
"eslint": "~4.19.1",
|
||||
"glob": "~7.1.2",
|
||||
"istanbul": "~0.4.5",
|
||||
"js-yaml": "~3.11.0",
|
||||
"nodeunit": "~0.11.2",
|
||||
"rimraf": "~2.6.2",
|
||||
"toml": "~2.3.3",
|
||||
"tv4": "~1.3.0",
|
||||
"typescript": "~2.8.3",
|
||||
"uglify-js": "~3.3.25"
|
||||
|
|
1
test/config/config-badcontent.txt
Normal file
1
test/config/config-badcontent.txt
Normal file
|
@ -0,0 +1 @@
|
|||
@
|
2
test/config/config-child.yaml
Normal file
2
test/config/config-child.yaml
Normal file
|
@ -0,0 +1,2 @@
|
|||
no-hard-tabs: false
|
||||
whitespace: false
|
6
test/config/config-grandparent-hybrid.yaml
Normal file
6
test/config/config-grandparent-hybrid.yaml
Normal file
|
@ -0,0 +1,6 @@
|
|||
extends: config-parent-hybrid.toml
|
||||
MD003:
|
||||
style: atx_closed
|
||||
MD007:
|
||||
indent: 2
|
||||
no-hard-tabs: false
|
6
test/config/config-grandparent.yaml
Normal file
6
test/config/config-grandparent.yaml
Normal file
|
@ -0,0 +1,6 @@
|
|||
extends: config-parent.yaml
|
||||
MD003:
|
||||
style: atx_closed
|
||||
MD007:
|
||||
indent: 2
|
||||
no-hard-tabs: false
|
9
test/config/config-parent-hybrid.toml
Normal file
9
test/config/config-parent-hybrid.toml
Normal file
|
@ -0,0 +1,9 @@
|
|||
extends = "config-child.json"
|
||||
MD003 = false
|
||||
no-hard-tabs = true
|
||||
|
||||
[MD007]
|
||||
indent = 4
|
||||
|
||||
[line-length]
|
||||
line_length = 200
|
7
test/config/config-parent.yaml
Normal file
7
test/config/config-parent.yaml
Normal file
|
@ -0,0 +1,7 @@
|
|||
extends: config-child.yaml
|
||||
MD003: false
|
||||
MD007:
|
||||
indent: 4
|
||||
no-hard-tabs: true
|
||||
line-length:
|
||||
line_length: 200
|
|
@ -924,6 +924,7 @@ module.exports.readmeHeadings = function readmeHeadings(test) {
|
|||
"#### result",
|
||||
"### Config",
|
||||
"#### file",
|
||||
"#### parsers",
|
||||
"#### callback",
|
||||
"#### result",
|
||||
"## Usage",
|
||||
|
@ -1466,6 +1467,72 @@ module.exports.configBadChildJson = function configBadChildJson(test) {
|
|||
});
|
||||
};
|
||||
|
||||
module.exports.configSingleYaml = function configSingleYaml(test) {
|
||||
test.expect(2);
|
||||
markdownlint.readConfig(
|
||||
"./test/config/config-child.yaml",
|
||||
[ require("js-yaml").safeLoad ],
|
||||
function callback(err, actual) {
|
||||
test.ifError(err);
|
||||
const expected = require("./config/config-child.json");
|
||||
test.deepEqual(actual, expected, "Config object not correct.");
|
||||
test.done();
|
||||
});
|
||||
};
|
||||
|
||||
module.exports.configMultipleYaml = function configMultipleYaml(test) {
|
||||
test.expect(2);
|
||||
markdownlint.readConfig(
|
||||
"./test/config/config-grandparent.yaml",
|
||||
[ require("js-yaml").safeLoad ],
|
||||
function callback(err, actual) {
|
||||
test.ifError(err);
|
||||
const expected = shared.assign(
|
||||
shared.assign(
|
||||
shared.assign({}, require("./config/config-child.json")),
|
||||
require("./config/config-parent.json")),
|
||||
require("./config/config-grandparent.json"));
|
||||
delete expected.extends;
|
||||
test.deepEqual(actual, expected, "Config object not correct.");
|
||||
test.done();
|
||||
});
|
||||
};
|
||||
|
||||
module.exports.configMultipleHybrid = function configMultipleHybrid(test) {
|
||||
test.expect(2);
|
||||
markdownlint.readConfig(
|
||||
"./test/config/config-grandparent-hybrid.yaml",
|
||||
[ JSON.parse, require("toml").parse, require("js-yaml").safeLoad ],
|
||||
function callback(err, actual) {
|
||||
test.ifError(err);
|
||||
const expected = shared.assign(
|
||||
shared.assign(
|
||||
shared.assign({}, require("./config/config-child.json")),
|
||||
require("./config/config-parent.json")),
|
||||
require("./config/config-grandparent.json"));
|
||||
delete expected.extends;
|
||||
test.deepEqual(actual, expected, "Config object not correct.");
|
||||
test.done();
|
||||
});
|
||||
};
|
||||
|
||||
module.exports.configBadHybrid = function configBadHybrid(test) {
|
||||
test.expect(4);
|
||||
markdownlint.readConfig(
|
||||
"./test/config/config-badcontent.txt",
|
||||
[ JSON.parse, require("toml").parse, require("js-yaml").safeLoad ],
|
||||
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(err.message.match(
|
||||
// eslint-disable-next-line max-len
|
||||
/^Unable to parse '[^']*'; Unexpected token \S+ in JSON at position \d+; Expected [^;]+ or end of input but "\S+" found.; end of the stream or a document separator is expected at line \d+, column \d+:[^;]*$/
|
||||
), "Error message unexpected.");
|
||||
test.ok(!result, "Got result for bad child JSON.");
|
||||
test.done();
|
||||
});
|
||||
};
|
||||
|
||||
module.exports.configSingleSync = function configSingleSync(test) {
|
||||
test.expect(1);
|
||||
const actual = markdownlint.readConfigSync("./test/config/config-child.json");
|
||||
|
@ -1530,8 +1597,10 @@ module.exports.configBadJsonSync = function configBadJsonSync(test) {
|
|||
}, function testError(err) {
|
||||
test.ok(err, "Did not get an error for bad JSON.");
|
||||
test.ok(err instanceof Error, "Error not instance of Error.");
|
||||
test.ok(err.message.match(/Unexpected token b in JSON at position \d+/),
|
||||
"Error message unexpected.");
|
||||
test.ok(err.message.match(
|
||||
// eslint-disable-next-line max-len
|
||||
/^Unable to parse '[^']*'; Unexpected token \S+ in JSON at position \d+$/
|
||||
), "Error message unexpected.");
|
||||
return true;
|
||||
}, "Did not get exception for bad JSON.");
|
||||
test.done();
|
||||
|
@ -1544,13 +1613,72 @@ module.exports.configBadChildJsonSync = function configBadChildJsonSync(test) {
|
|||
}, 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.");
|
||||
test.ok(err.message.match(/Unexpected token b in JSON at position \d+/),
|
||||
"Error message unexpected.");
|
||||
test.ok(err.message.match(
|
||||
// eslint-disable-next-line max-len
|
||||
/^Unable to parse '[^']*'; Unexpected token \S+ in JSON at position \d+$/
|
||||
), "Error message unexpected.");
|
||||
return true;
|
||||
}, "Did not get exception for bad child JSON.");
|
||||
test.done();
|
||||
};
|
||||
|
||||
module.exports.configSingleYamlSync = function configSingleYamlSync(test) {
|
||||
test.expect(1);
|
||||
const actual = markdownlint.readConfigSync(
|
||||
"./test/config/config-child.yaml", [ require("js-yaml").safeLoad ]);
|
||||
const expected = require("./config/config-child.json");
|
||||
test.deepEqual(actual, expected, "Config object not correct.");
|
||||
test.done();
|
||||
};
|
||||
|
||||
module.exports.configMultipleYamlSync = function configMultipleYamlSync(test) {
|
||||
test.expect(1);
|
||||
const actual = markdownlint.readConfigSync(
|
||||
"./test/config/config-grandparent.yaml", [ require("js-yaml").safeLoad ]);
|
||||
const expected = shared.assign(
|
||||
shared.assign(
|
||||
shared.assign({}, require("./config/config-child.json")),
|
||||
require("./config/config-parent.json")),
|
||||
require("./config/config-grandparent.json"));
|
||||
delete expected.extends;
|
||||
test.deepEqual(actual, expected, "Config object not correct.");
|
||||
test.done();
|
||||
};
|
||||
|
||||
module.exports.configMultipleHybridSync =
|
||||
function configMultipleHybridSync(test) {
|
||||
test.expect(1);
|
||||
const actual = markdownlint.readConfigSync(
|
||||
"./test/config/config-grandparent-hybrid.yaml",
|
||||
[ JSON.parse, require("toml").parse, require("js-yaml").safeLoad ]);
|
||||
const expected = shared.assign(
|
||||
shared.assign(
|
||||
shared.assign({}, require("./config/config-child.json")),
|
||||
require("./config/config-parent.json")),
|
||||
require("./config/config-grandparent.json"));
|
||||
delete expected.extends;
|
||||
test.deepEqual(actual, expected, "Config object not correct.");
|
||||
test.done();
|
||||
};
|
||||
|
||||
module.exports.configBadHybridSync = function configBadHybridSync(test) {
|
||||
test.expect(4);
|
||||
test.throws(function badHybridCall() {
|
||||
markdownlint.readConfigSync(
|
||||
"./test/config/config-badcontent.txt",
|
||||
[ JSON.parse, require("toml").parse, require("js-yaml").safeLoad ]);
|
||||
}, function testError(err) {
|
||||
test.ok(err, "Did not get an error for bad content.");
|
||||
test.ok(err instanceof Error, "Error not instance of Error.");
|
||||
test.ok(err.message.match(
|
||||
// eslint-disable-next-line max-len
|
||||
/^Unable to parse '[^']*'; Unexpected token \S+ in JSON at position \d+; Expected [^;]+ or end of input but "\S+" found.; end of the stream or a document separator is expected at line \d+, column \d+:[^;]*$/
|
||||
), "Error message unexpected.");
|
||||
return true;
|
||||
}, "Did not get exception for bad content.");
|
||||
test.done();
|
||||
};
|
||||
|
||||
module.exports.customRulesV0 = function customRulesV0(test) {
|
||||
test.expect(4);
|
||||
const customRulesMd = "./test/custom-rules.md";
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue