diff --git a/README.md b/README.md index f7493981..e798b69f 100644 --- a/README.md +++ b/README.md @@ -26,20 +26,21 @@ markdownlint --help MarkdownLint Command Line Interface Options: - -V, --version output the version number - -c, --config [configFile] configuration file (JSON, JSONC, JS, YAML, or TOML) - -d, --dot include files/folders with a dot (for example `.github`) - -f, --fix fix basic errors (does not work with STDIN) - -i, --ignore [file|directory|glob] file(s) to ignore/exclude (default: []) - -j, --json write issues in json format - -o, --output [outputFile] write issues to file (no console) - -p, --ignore-path [file] path to file with ignore pattern(s) - -q, --quiet do not write issues to STDOUT - -r, --rules [file|directory|glob|package] include custom rule files (default: []) - -s, --stdin read from STDIN (does not work with files) - --enable [rules...] Enable certain rules, e.g. --enable MD013 MD041 -- - --disable [rules...] Disable certain rules, e.g. --disable MD013 MD041 -- - -h, --help display help for command + -V, --version output the version number + -c, --config configuration file (JSON, JSONC, JS, YAML, or TOML) + --configPointer JSON Pointer to object within configuration file (default: "") + -d, --dot include files/folders with a dot (for example `.github`) + -f, --fix fix basic errors (does not work with STDIN) + -i, --ignore file(s) to ignore/exclude (default: []) + -j, --json write issues in json format + -o, --output write issues to file (no console) + -p, --ignore-path path to file with ignore pattern(s) + -q, --quiet do not write issues to STDOUT + -r, --rules include custom rule files (default: []) + -s, --stdin read from STDIN (does not work with files) + --enable Enable certain rules, e.g. --enable MD013 MD041 -- + --disable Disable certain rules, e.g. --disable MD013 MD041 -- + -h, --help display help for command ``` 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 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**. A JS configuration file may internally `require` one or more npm packages as a way of reusing configuration across projects. +The `--configPointer` argument allows the use of [JSON Pointer][json-pointer] syntax to identify a sub-object within the configuration object (per above). +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`). + `--enable` and `--disable` override configuration files; if a configuration file disables `MD123` and you pass `--enable MD123`, it will be enabled. If a rule is passed to both `--enable` and `--disable`, it will be disabled. @@ -156,6 +160,7 @@ MIT © Igor Shubovych [actions-badge]: https://github.com/igorshubovych/markdownlint-cli/workflows/CI/badge.svg?branch=master [actions-url]: https://github.com/igorshubovych/markdownlint-cli/actions?query=workflow%3ACI [commander-variadic]: https://github.com/tj/commander.js#variadic-option +[json-pointer]: https://datatracker.ietf.org/doc/html/rfc6901 [markdownlint]: https://github.com/DavidAnson/markdownlint [markdownlint-cli2]: https://github.com/DavidAnson/markdownlint-cli2 [markdownlint-jsonc]: https://github.com/DavidAnson/markdownlint/blob/main/schema/.markdownlint.jsonc diff --git a/markdownlint.js b/markdownlint.js index d8868ecc..93f00b8e 100755 --- a/markdownlint.js +++ b/markdownlint.js @@ -12,6 +12,7 @@ const glob = require('glob'); const markdownlint = require('markdownlint'); const rc = require('run-con'); const minimatch = require('minimatch'); +const jsonpointer = require('jsonpointer'); const pkg = require('./package.json'); const options = program.opts(); @@ -55,8 +56,6 @@ const fsOptions = {encoding: 'utf8'}; const processCwd = process.cwd(); function readConfiguration(userConfigFile) { - const jsConfigFile = /\.c?js$/i.test(userConfigFile); - // Load from well-known config files let config = rc('markdownlint', {}); for (const projectConfigFile of projectConfigFiles) { @@ -71,9 +70,10 @@ function readConfiguration(userConfigFile) { } // Normally parsing this file is not needed, because it is already parsed by rc package. - // However I have to do it to overwrite configuration from .markdownlint.{json,yaml,yml}. + // However I have to do it to overwrite configuration from .markdownlint.{jsonc,json,yaml,yml}. if (userConfigFile) { try { + const jsConfigFile = /\.c?js$/i.test(userConfigFile); const userConfig = jsConfigFile ? require(path.resolve(processCwd, userConfigFile)) : markdownlint.readConfigSync(userConfigFile, configParsers); config = require('deep-extend')(config, userConfig); } catch (error) { @@ -199,7 +199,8 @@ program .version(pkg.version) .description(pkg.description) .usage('[options] ') - .option('-c, --config ', 'configuration file (JSON, JSONC, JS, or YAML)') + .option('-c, --config ', 'configuration file (JSON, JSONC, JS, YAML, or TOML)') + .option('--configPointer ', 'JSON Pointer to object within configuration file', '') .option('-d, --dot', 'include files/folders with a dot (for example `.github`)') .option('-f, --fix', 'fix basic errors (does not work with STDIN)') .option('-i, --ignore ', 'file(s) to ignore/exclude', concatArray, []) @@ -207,7 +208,7 @@ program .option('-o, --output ', 'write issues to file (no console)') .option('-p, --ignore-path ', 'path to file with ignore pattern(s)') .option('-q, --quiet', 'do not write issues to STDOUT') - .option('-r, --rules ', 'include custom rule files', concatArray, []) + .option('-r, --rules ', 'include custom rule files', concatArray, []) .option('-s, --stdin', 'read from STDIN (does not work with files)') .option('--enable ', 'Enable certain rules, e.g. --enable MD013 MD041 --') .option('--disable ', 'Disable certain rules, e.g. --disable MD013 MD041 --'); @@ -276,7 +277,8 @@ const diff = files.filter(file => !ignores.some(ignore => ignore.absolute === fi function lintAndPrint(stdin, files) { files ||= []; - const config = readConfiguration(options.config); + const configuration = readConfiguration(options.config); + const config = jsonpointer.get(configuration, options.configPointer) || {}; for (const rule of options.enable || []) { // Leave default values in place if rule is an object diff --git a/package-lock.json b/package-lock.json index a260d31e..d11ce505 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,6 +15,7 @@ "ignore": "~5.3.1", "js-yaml": "^4.1.0", "jsonc-parser": "~3.2.1", + "jsonpointer": "5.0.1", "markdownlint": "~0.34.0", "minimatch": "~9.0.4", "run-con": "~1.3.2", @@ -4712,6 +4713,14 @@ "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.1.tgz", "integrity": "sha512-AilxAyFOAcK5wA1+LeaySVBrHsGQvUFCDWXKpZjzaL0PqW+xfBOttn8GNtWKFWqneyMZj41MWF9Kl6iPWLwgOA==" }, + "node_modules/jsonpointer": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-5.0.1.tgz", + "integrity": "sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", @@ -11209,6 +11218,11 @@ "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.1.tgz", "integrity": "sha512-AilxAyFOAcK5wA1+LeaySVBrHsGQvUFCDWXKpZjzaL0PqW+xfBOttn8GNtWKFWqneyMZj41MWF9Kl6iPWLwgOA==" }, + "jsonpointer": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-5.0.1.tgz", + "integrity": "sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ==" + }, "levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", diff --git a/package.json b/package.json index 269a27ac..cd43aa5b 100644 --- a/package.json +++ b/package.json @@ -43,6 +43,7 @@ "ignore": "~5.3.1", "js-yaml": "^4.1.0", "jsonc-parser": "~3.2.1", + "jsonpointer": "5.0.1", "markdownlint": "~0.34.0", "minimatch": "~9.0.4", "run-con": "~1.3.2", diff --git a/test/nested-config.json b/test/nested-config.json new file mode 100644 index 00000000..db6df9fa --- /dev/null +++ b/test/nested-config.json @@ -0,0 +1,8 @@ +{ + "name": "unused", + + "key": { + "blanks-around-headings": false, + "commands-show-output": false + } +} diff --git a/test/nested-config.toml b/test/nested-config.toml new file mode 100644 index 00000000..ff208c82 --- /dev/null +++ b/test/nested-config.toml @@ -0,0 +1,7 @@ +name = "unused" + +[key] +other-name = "unused" + +[key.subkey] +commands-show-output = false diff --git a/test/test.js b/test/test.js index 72f45886..b9955ad6 100644 --- a/test/test.js +++ b/test/test.js @@ -489,6 +489,50 @@ test('.markdownlint.yaml in cwd is used instead of .markdownlint.yml', getCwdCon test('.markdownlint.json with JavaScript-style comments is handled', getCwdConfigFileTest('json-c')); +test('invalid JSON Pointer', async t => { + try { + await execa('../markdownlint.js', ['--config', 'nested-config.json', '--configPointer', 'INVALID', '**/*.md'], {stripFinalNewline: false}); + t.fail(); + } catch (error) { + t.is(error.stdout, ''); + t.regex(error.stderr, /Invalid JSON pointer\./); + t.is(error.exitCode, 4); + } +}); + +test('empty JSON Pointer', async t => { + try { + await execa('../markdownlint.js', ['--config', 'nested-config.json', '--configPointer', '/EMPTY', 'incorrect.md'], {stripFinalNewline: false}); + t.fail(); + } catch (error) { + t.is(error.stdout, ''); + t.is(error.stderr.match(errorPattern).length, 7); + t.is(error.exitCode, 1); + } +}); + +test('valid JSON Pointer with JSON configuration', async t => { + try { + await execa('../markdownlint.js', ['--config', 'nested-config.json', '--configPointer', '/key', 'incorrect.md'], {stripFinalNewline: false}); + t.fail(); + } catch (error) { + t.is(error.stdout, ''); + t.is(error.stderr.match(errorPattern).length, 1); + t.is(error.exitCode, 1); + } +}); + +test('valid JSON Pointer with TOML configuration', async t => { + try { + await execa('../markdownlint.js', ['--config', 'nested-config.toml', '--configPointer', '/key/subkey', 'incorrect.md'], {stripFinalNewline: false}); + t.fail(); + } catch (error) { + t.is(error.stdout, ''); + t.is(error.stderr.match(errorPattern).length, 3); + t.is(error.exitCode, 1); + } +}); + test('Custom rule from single file loaded', async t => { try { const input = '# Input\n';