mirror of
https://github.com/DavidAnson/markdownlint.git
synced 2025-12-16 14:00:13 +01: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
|
### Config
|
||||||
|
|
||||||
The `options.config` configuration object is simple and can be loaded as JSON
|
The `options.config` configuration object is simple and can be stored in a file
|
||||||
in many cases. To take advantage of shared configuration where one file `extends`
|
for readability and easy reuse. The `readConfig` and `readConfigSync` functions
|
||||||
another, the following functions are useful.
|
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:
|
Asynchronous interface:
|
||||||
|
|
||||||
|
|
@ -411,10 +415,11 @@ Asynchronous interface:
|
||||||
* Read specified configuration file.
|
* Read specified configuration file.
|
||||||
*
|
*
|
||||||
* @param {String} file Configuration file name/path.
|
* @param {String} file Configuration file name/path.
|
||||||
|
* @param {Array} [parsers] Optional parsing function(s).
|
||||||
* @param {Function} callback Callback (err, result) function.
|
* @param {Function} callback Callback (err, result) function.
|
||||||
* @returns {void}
|
* @returns {void}
|
||||||
*/
|
*/
|
||||||
function readConfig(file, callback) { ... }
|
function readConfig(file, parsers, callback) { ... }
|
||||||
```
|
```
|
||||||
|
|
||||||
Synchronous interface:
|
Synchronous interface:
|
||||||
|
|
@ -424,16 +429,17 @@ Synchronous interface:
|
||||||
* Read specified configuration file synchronously.
|
* Read specified configuration file synchronously.
|
||||||
*
|
*
|
||||||
* @param {String} file Configuration file name/path.
|
* @param {String} file Configuration file name/path.
|
||||||
|
* @param {Array} [parsers] Optional parsing function(s).
|
||||||
* @returns {Object} Configuration object.
|
* @returns {Object} Configuration object.
|
||||||
*/
|
*/
|
||||||
function readConfigSync(file) { ... }
|
function readConfigSync(file, parsers) { ... }
|
||||||
```
|
```
|
||||||
|
|
||||||
#### file
|
#### file
|
||||||
|
|
||||||
Type: `String`
|
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`
|
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`
|
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
|
first, then those of `file` are applied on top (overriding any of the same keys
|
||||||
appearing in the referenced file).
|
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
|
#### callback
|
||||||
|
|
||||||
Type: `Function` taking (`Error`, `Object`)
|
Type: `Function` taking (`Error`, `Object`)
|
||||||
|
|
|
||||||
|
|
@ -422,6 +422,7 @@ function lintFile(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Lints files and strings
|
||||||
function lintInput(options, synchronous, callback) {
|
function lintInput(options, synchronous, callback) {
|
||||||
// Normalize inputs
|
// Normalize inputs
|
||||||
options = options || {};
|
options = options || {};
|
||||||
|
|
@ -517,34 +518,64 @@ function markdownlintSync(options) {
|
||||||
return results;
|
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.
|
* Read specified configuration file.
|
||||||
*
|
*
|
||||||
* @param {String} file Configuration file name/path.
|
* @param {String} file Configuration file name/path.
|
||||||
|
* @param {Array} [parsers] Optional parsing function(s).
|
||||||
* @param {Function} callback Callback (err, result) function.
|
* @param {Function} callback Callback (err, result) function.
|
||||||
* @returns {void}
|
* @returns {void}
|
||||||
*/
|
*/
|
||||||
function readConfig(file, callback) {
|
function readConfig(file, parsers, callback) {
|
||||||
|
if (!callback) {
|
||||||
|
// @ts-ignore
|
||||||
|
callback = parsers;
|
||||||
|
parsers = null;
|
||||||
|
}
|
||||||
// Read file
|
// Read file
|
||||||
fs.readFile(file, shared.utf8Encoding, function handleFile(err, content) {
|
fs.readFile(file, shared.utf8Encoding, (err, content) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
return callback(err);
|
return callback(err);
|
||||||
}
|
}
|
||||||
// Parse file
|
// Try to parse file
|
||||||
let config = null;
|
const { config, message } = parseConfiguration(file, content, parsers);
|
||||||
try {
|
if (!config) {
|
||||||
config = JSON.parse(content);
|
return callback(new Error(message));
|
||||||
} catch (ex) {
|
|
||||||
return callback(ex);
|
|
||||||
}
|
}
|
||||||
if (config.extends) {
|
// Extend configuration
|
||||||
// Extend configuration
|
const configExtends = config.extends;
|
||||||
const extendsFile = path.resolve(path.dirname(file), config.extends);
|
if (configExtends) {
|
||||||
readConfig(extendsFile, function handleConfig(errr, extendsConfig) {
|
delete config.extends;
|
||||||
|
const extendsFile = path.resolve(path.dirname(file), configExtends);
|
||||||
|
readConfig(extendsFile, parsers, (errr, extendsConfig) => {
|
||||||
if (errr) {
|
if (errr) {
|
||||||
return callback(errr);
|
return callback(errr);
|
||||||
}
|
}
|
||||||
delete config.extends;
|
|
||||||
callback(null, shared.assign(extendsConfig, config));
|
callback(null, shared.assign(extendsConfig, config));
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -557,17 +588,24 @@ function readConfig(file, callback) {
|
||||||
* Read specified configuration file synchronously.
|
* Read specified configuration file synchronously.
|
||||||
*
|
*
|
||||||
* @param {String} file Configuration file name/path.
|
* @param {String} file Configuration file name/path.
|
||||||
|
* @param {Array} [parsers] Optional parsing function(s).
|
||||||
* @returns {Object} Configuration object.
|
* @returns {Object} Configuration object.
|
||||||
*/
|
*/
|
||||||
function readConfigSync(file) {
|
function readConfigSync(file, parsers) {
|
||||||
// Parse file
|
// Read file
|
||||||
let config = JSON.parse(fs.readFileSync(file, shared.utf8Encoding));
|
const content = fs.readFileSync(file, shared.utf8Encoding);
|
||||||
if (config.extends) {
|
// Try to parse file
|
||||||
// Extend configuration
|
const { config, message } = parseConfiguration(file, content, parsers);
|
||||||
config = shared.assign(
|
if (!config) {
|
||||||
readConfigSync(path.resolve(path.dirname(file), config.extends)),
|
throw new Error(message);
|
||||||
config);
|
}
|
||||||
|
// Extend configuration
|
||||||
|
const configExtends = config.extends;
|
||||||
|
if (configExtends) {
|
||||||
delete config.extends;
|
delete config.extends;
|
||||||
|
return shared.assign(
|
||||||
|
readConfigSync(path.resolve(path.dirname(file), configExtends), parsers),
|
||||||
|
config);
|
||||||
}
|
}
|
||||||
return config;
|
return config;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -35,8 +35,10 @@
|
||||||
"eslint": "~4.19.1",
|
"eslint": "~4.19.1",
|
||||||
"glob": "~7.1.2",
|
"glob": "~7.1.2",
|
||||||
"istanbul": "~0.4.5",
|
"istanbul": "~0.4.5",
|
||||||
|
"js-yaml": "~3.11.0",
|
||||||
"nodeunit": "~0.11.2",
|
"nodeunit": "~0.11.2",
|
||||||
"rimraf": "~2.6.2",
|
"rimraf": "~2.6.2",
|
||||||
|
"toml": "~2.3.3",
|
||||||
"tv4": "~1.3.0",
|
"tv4": "~1.3.0",
|
||||||
"typescript": "~2.8.3",
|
"typescript": "~2.8.3",
|
||||||
"uglify-js": "~3.3.25"
|
"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",
|
"#### result",
|
||||||
"### Config",
|
"### Config",
|
||||||
"#### file",
|
"#### file",
|
||||||
|
"#### parsers",
|
||||||
"#### callback",
|
"#### callback",
|
||||||
"#### result",
|
"#### result",
|
||||||
"## Usage",
|
"## 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) {
|
module.exports.configSingleSync = function configSingleSync(test) {
|
||||||
test.expect(1);
|
test.expect(1);
|
||||||
const actual = markdownlint.readConfigSync("./test/config/config-child.json");
|
const actual = markdownlint.readConfigSync("./test/config/config-child.json");
|
||||||
|
|
@ -1530,8 +1597,10 @@ module.exports.configBadJsonSync = function configBadJsonSync(test) {
|
||||||
}, function testError(err) {
|
}, function testError(err) {
|
||||||
test.ok(err, "Did not get an error for bad JSON.");
|
test.ok(err, "Did not get an error for bad JSON.");
|
||||||
test.ok(err instanceof Error, "Error not instance of Error.");
|
test.ok(err instanceof Error, "Error not instance of Error.");
|
||||||
test.ok(err.message.match(/Unexpected token b in JSON at position \d+/),
|
test.ok(err.message.match(
|
||||||
"Error message unexpected.");
|
// eslint-disable-next-line max-len
|
||||||
|
/^Unable to parse '[^']*'; Unexpected token \S+ in JSON at position \d+$/
|
||||||
|
), "Error message unexpected.");
|
||||||
return true;
|
return true;
|
||||||
}, "Did not get exception for bad JSON.");
|
}, "Did not get exception for bad JSON.");
|
||||||
test.done();
|
test.done();
|
||||||
|
|
@ -1544,13 +1613,72 @@ module.exports.configBadChildJsonSync = function configBadChildJsonSync(test) {
|
||||||
}, function testError(err) {
|
}, function testError(err) {
|
||||||
test.ok(err, "Did not get an error for bad child JSON.");
|
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 instanceof Error, "Error not instance of Error.");
|
||||||
test.ok(err.message.match(/Unexpected token b in JSON at position \d+/),
|
test.ok(err.message.match(
|
||||||
"Error message unexpected.");
|
// eslint-disable-next-line max-len
|
||||||
|
/^Unable to parse '[^']*'; Unexpected token \S+ in JSON at position \d+$/
|
||||||
|
), "Error message unexpected.");
|
||||||
return true;
|
return true;
|
||||||
}, "Did not get exception for bad child JSON.");
|
}, "Did not get exception for bad child JSON.");
|
||||||
test.done();
|
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) {
|
module.exports.customRulesV0 = function customRulesV0(test) {
|
||||||
test.expect(4);
|
test.expect(4);
|
||||||
const customRulesMd = "./test/custom-rules.md";
|
const customRulesMd = "./test/custom-rules.md";
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue