Skip to content

Commit 3acb04e

Browse files
committed
Add --configPointer argument to allow embedding the configuration object in files like package.json and pyproject.toml (fixes #113, fixes #458).
1 parent 899e6a8 commit 3acb04e

7 files changed

+101
-20
lines changed

README.md

+19-14
Original file line numberDiff line numberDiff line change
@@ -26,20 +26,21 @@ markdownlint --help
2626
MarkdownLint Command Line Interface
2727

2828
Options:
29-
-V, --version output the version number
30-
-c, --config [configFile] configuration file (JSON, JSONC, JS, YAML, or TOML)
31-
-d, --dot include files/folders with a dot (for example `.github`)
32-
-f, --fix fix basic errors (does not work with STDIN)
33-
-i, --ignore [file|directory|glob] file(s) to ignore/exclude (default: [])
34-
-j, --json write issues in json format
35-
-o, --output [outputFile] write issues to file (no console)
36-
-p, --ignore-path [file] path to file with ignore pattern(s)
37-
-q, --quiet do not write issues to STDOUT
38-
-r, --rules [file|directory|glob|package] include custom rule files (default: [])
39-
-s, --stdin read from STDIN (does not work with files)
40-
--enable [rules...] Enable certain rules, e.g. --enable MD013 MD041 --
41-
--disable [rules...] Disable certain rules, e.g. --disable MD013 MD041 --
42-
-h, --help display help for command
29+
-V, --version output the version number
30+
-c, --config <configFile> configuration file (JSON, JSONC, JS, YAML, or TOML)
31+
--configPointer <pointer> JSON Pointer to object within configuration file (default: "")
32+
-d, --dot include files/folders with a dot (for example `.github`)
33+
-f, --fix fix basic errors (does not work with STDIN)
34+
-i, --ignore <file|directory|glob> file(s) to ignore/exclude (default: [])
35+
-j, --json write issues in json format
36+
-o, --output <outputFile> write issues to file (no console)
37+
-p, --ignore-path <file> path to file with ignore pattern(s)
38+
-q, --quiet do not write issues to STDOUT
39+
-r, --rules <file|directory|glob|package> include custom rule files (default: [])
40+
-s, --stdin read from STDIN (does not work with files)
41+
--enable <rules...> Enable certain rules, e.g. --enable MD013 MD041 --
42+
--disable <rules...> Disable certain rules, e.g. --disable MD013 MD041 --
43+
-h, --help display help for command
4344
```
4445

4546
Or run using [Docker](https://www.docker.com) and [GitHub Packages](https://github.com/features/packages):
@@ -113,6 +114,9 @@ JS configuration files contain JavaScript code, must have the `.js` or `.cjs` fi
113114
If your workspace _(project)_ is [ESM-only] _(`"type": "module"` set in the root `package.json` file)_, then the configuration file **should end with `.cjs` file extension**.
114115
A JS configuration file may internally `require` one or more npm packages as a way of reusing configuration across projects.
115116

117+
The `--configPointer` argument allows the use of [JSON Pointer][json-pointer] syntax to identify a sub-object within the configuration object (per above).
118+
This argument can be used with any configuration file type and makes it possible to nest a configuration object within another file like `package.json` or `pyproject.toml` (e.g., via `/key` or `/key/subkey`).
119+
116120
`--enable` and `--disable` override configuration files; if a configuration file disables `MD123` and you pass `--enable MD123`, it will be enabled.
117121
If a rule is passed to both `--enable` and `--disable`, it will be disabled.
118122

@@ -156,6 +160,7 @@ MIT © Igor Shubovych
156160
[actions-badge]: https://github.com/igorshubovych/markdownlint-cli/workflows/CI/badge.svg?branch=master
157161
[actions-url]: https://github.com/igorshubovych/markdownlint-cli/actions?query=workflow%3ACI
158162
[commander-variadic]: https://github.com/tj/commander.js#variadic-option
163+
[json-pointer]: https://datatracker.ietf.org/doc/html/rfc6901
159164
[markdownlint]: https://github.com/DavidAnson/markdownlint
160165
[markdownlint-cli2]: https://github.com/DavidAnson/markdownlint-cli2
161166
[markdownlint-jsonc]: https://github.com/DavidAnson/markdownlint/blob/main/schema/.markdownlint.jsonc

markdownlint.js

+8-6
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ const glob = require('glob');
1212
const markdownlint = require('markdownlint');
1313
const rc = require('run-con');
1414
const minimatch = require('minimatch');
15+
const jsonpointer = require('jsonpointer');
1516
const pkg = require('./package.json');
1617

1718
const options = program.opts();
@@ -55,8 +56,6 @@ const fsOptions = {encoding: 'utf8'};
5556
const processCwd = process.cwd();
5657

5758
function readConfiguration(userConfigFile) {
58-
const jsConfigFile = /\.c?js$/i.test(userConfigFile);
59-
6059
// Load from well-known config files
6160
let config = rc('markdownlint', {});
6261
for (const projectConfigFile of projectConfigFiles) {
@@ -71,9 +70,10 @@ function readConfiguration(userConfigFile) {
7170
}
7271

7372
// Normally parsing this file is not needed, because it is already parsed by rc package.
74-
// However I have to do it to overwrite configuration from .markdownlint.{json,yaml,yml}.
73+
// However I have to do it to overwrite configuration from .markdownlint.{jsonc,json,yaml,yml}.
7574
if (userConfigFile) {
7675
try {
76+
const jsConfigFile = /\.c?js$/i.test(userConfigFile);
7777
const userConfig = jsConfigFile ? require(path.resolve(processCwd, userConfigFile)) : markdownlint.readConfigSync(userConfigFile, configParsers);
7878
config = require('deep-extend')(config, userConfig);
7979
} catch (error) {
@@ -199,15 +199,16 @@ program
199199
.version(pkg.version)
200200
.description(pkg.description)
201201
.usage('[options] <files|directories|globs>')
202-
.option('-c, --config <configFile>', 'configuration file (JSON, JSONC, JS, or YAML)')
202+
.option('-c, --config <configFile>', 'configuration file (JSON, JSONC, JS, YAML, or TOML)')
203+
.option('--configPointer <pointer>', 'JSON Pointer to object within configuration file', '')
203204
.option('-d, --dot', 'include files/folders with a dot (for example `.github`)')
204205
.option('-f, --fix', 'fix basic errors (does not work with STDIN)')
205206
.option('-i, --ignore <file|directory|glob>', 'file(s) to ignore/exclude', concatArray, [])
206207
.option('-j, --json', 'write issues in json format')
207208
.option('-o, --output <outputFile>', 'write issues to file (no console)')
208209
.option('-p, --ignore-path <file>', 'path to file with ignore pattern(s)')
209210
.option('-q, --quiet', 'do not write issues to STDOUT')
210-
.option('-r, --rules <file|directory|glob|package>', 'include custom rule files', concatArray, [])
211+
.option('-r, --rules <file|directory|glob|package>', 'include custom rule files', concatArray, [])
211212
.option('-s, --stdin', 'read from STDIN (does not work with files)')
212213
.option('--enable <rules...>', 'Enable certain rules, e.g. --enable MD013 MD041 --')
213214
.option('--disable <rules...>', 'Disable certain rules, e.g. --disable MD013 MD041 --');
@@ -276,7 +277,8 @@ const diff = files.filter(file => !ignores.some(ignore => ignore.absolute === fi
276277

277278
function lintAndPrint(stdin, files) {
278279
files ||= [];
279-
const config = readConfiguration(options.config);
280+
const configuration = readConfiguration(options.config);
281+
const config = jsonpointer.get(configuration, options.configPointer) || {};
280282

281283
for (const rule of options.enable || []) {
282284
// Leave default values in place if rule is an object

package-lock.json

+14
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+1
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
"ignore": "~5.3.1",
4444
"js-yaml": "^4.1.0",
4545
"jsonc-parser": "~3.2.1",
46+
"jsonpointer": "5.0.1",
4647
"markdownlint": "~0.34.0",
4748
"minimatch": "~9.0.4",
4849
"run-con": "~1.3.2",

test/nested-config.json

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"name": "unused",
3+
4+
"key": {
5+
"blanks-around-headings": false,
6+
"commands-show-output": false
7+
}
8+
}

test/nested-config.toml

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
name = "unused"
2+
3+
[key]
4+
other-name = "unused"
5+
6+
[key.subkey]
7+
commands-show-output = false

test/test.js

+44
Original file line numberDiff line numberDiff line change
@@ -489,6 +489,50 @@ test('.markdownlint.yaml in cwd is used instead of .markdownlint.yml', getCwdCon
489489

490490
test('.markdownlint.json with JavaScript-style comments is handled', getCwdConfigFileTest('json-c'));
491491

492+
test('invalid JSON Pointer', async t => {
493+
try {
494+
await execa('../markdownlint.js', ['--config', 'nested-config.json', '--configPointer', 'INVALID', '**/*.md'], {stripFinalNewline: false});
495+
t.fail();
496+
} catch (error) {
497+
t.is(error.stdout, '');
498+
t.regex(error.stderr, /Invalid JSON pointer\./);
499+
t.is(error.exitCode, 4);
500+
}
501+
});
502+
503+
test('empty JSON Pointer', async t => {
504+
try {
505+
await execa('../markdownlint.js', ['--config', 'nested-config.json', '--configPointer', '/EMPTY', 'incorrect.md'], {stripFinalNewline: false});
506+
t.fail();
507+
} catch (error) {
508+
t.is(error.stdout, '');
509+
t.is(error.stderr.match(errorPattern).length, 7);
510+
t.is(error.exitCode, 1);
511+
}
512+
});
513+
514+
test('valid JSON Pointer with JSON configuration', async t => {
515+
try {
516+
await execa('../markdownlint.js', ['--config', 'nested-config.json', '--configPointer', '/key', 'incorrect.md'], {stripFinalNewline: false});
517+
t.fail();
518+
} catch (error) {
519+
t.is(error.stdout, '');
520+
t.is(error.stderr.match(errorPattern).length, 1);
521+
t.is(error.exitCode, 1);
522+
}
523+
});
524+
525+
test('valid JSON Pointer with TOML configuration', async t => {
526+
try {
527+
await execa('../markdownlint.js', ['--config', 'nested-config.toml', '--configPointer', '/key/subkey', 'incorrect.md'], {stripFinalNewline: false});
528+
t.fail();
529+
} catch (error) {
530+
t.is(error.stdout, '');
531+
t.is(error.stderr.match(errorPattern).length, 3);
532+
t.is(error.exitCode, 1);
533+
}
534+
});
535+
492536
test('Custom rule from single file loaded', async t => {
493537
try {
494538
const input = '# Input\n';

0 commit comments

Comments
 (0)