├── .dockerignore ├── .editorconfig ├── .gitattributes ├── .github ├── dependabot.yml └── workflows │ ├── ci.yml │ └── publish-container-image.yml ├── .gitignore ├── .npmignore ├── .npmrc ├── .pre-commit-hooks.yaml ├── Dockerfile ├── LICENSE ├── README.md ├── markdownlint.js ├── package-lock.json ├── package.json └── test ├── .folder ├── .file-with-dot.md └── incorrect-dot.md ├── base-config.json ├── config-files ├── json-c │ ├── .markdownlint.json │ └── heading-dollar.md ├── json-yaml-yml │ ├── .markdownlint.json │ ├── .markdownlint.yaml │ ├── .markdownlint.yml │ └── heading-dollar.md ├── json │ ├── .markdownlint.json │ └── heading-dollar.md ├── jsonc-json-yaml-yml │ ├── .markdownlint.json │ ├── .markdownlint.jsonc │ ├── .markdownlint.yaml │ ├── .markdownlint.yml │ └── heading-dollar.md ├── jsonc │ ├── .markdownlint.jsonc │ └── heading-dollar.md ├── yaml-yml │ ├── .markdownlint.yaml │ ├── .markdownlint.yml │ └── heading-dollar.md ├── yaml │ ├── .markdownlint.yaml │ └── heading-dollar.md └── yml │ ├── .markdownlint.yml │ └── heading-dollar.md ├── correct.md ├── custom-rules ├── files │ ├── test-rule-1.cjs │ ├── test-rule-2.cjs │ └── test-rule-3-4.cjs ├── markdownlint-cli-local-test-rule-other │ ├── index.js │ ├── package.json │ └── rule.js ├── markdownlint-cli-local-test-rule │ ├── index.js │ └── package.json ├── relative-to-cwd │ ├── node_modules │ │ └── markdownlint-cli-local-test-rule │ │ │ ├── index.js │ │ │ └── package.json │ └── package.json └── scoped-package │ ├── node_modules │ └── @scoped │ │ └── custom-rule │ │ ├── package.json │ │ └── scoped-rule.cjs │ └── scoped-test.md ├── default-false-config.yml ├── incorrect.md ├── inline-jsonc.md ├── inline-yaml.md ├── malformed-config.yaml ├── markdownlintignore ├── .ignorefile ├── .markdownlintignore ├── correct.md ├── incorrect.markdown ├── incorrect.md └── subdir │ └── incorrect.markdown ├── md043-config.cjs ├── md043-config.json ├── md043-config.md ├── md043-config.toml ├── md043-config.yaml ├── nested-config.json ├── nested-config.toml ├── subdir-correct ├── correct.markdown └── correct.md ├── subdir-incorrect ├── UPPER.MD ├── incorrect.markdown └── incorrect.md ├── test-config.json ├── test-config.toml ├── test-config.yaml ├── test.js └── workspace ├── commonjs ├── .markdownlint.cjs ├── .markdownlint.js ├── package.json └── test.md └── module ├── .markdownlint.cjs ├── package.json └── test.md /.dockerignore: -------------------------------------------------------------------------------- 1 | test 2 | node_modules 3 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | 10 | [*.js] 11 | insert_final_newline = true 12 | 13 | [*.cjs] 14 | insert_final_newline = true 15 | 16 | [*.mjs] 17 | insert_final_newline = true 18 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | markdownlint.js eol=lf 2 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "npm" 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" 7 | versioning-strategy: increase 8 | - package-ecosystem: "github-actions" 9 | directory: "/" 10 | schedule: 11 | interval: "weekly" 12 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches-ignore: 6 | - "dependabot/**" 7 | pull_request: 8 | 9 | jobs: 10 | test: 11 | runs-on: ${{ matrix.os }} 12 | 13 | strategy: 14 | fail-fast: false 15 | matrix: 16 | node: 17 | - 20 18 | - 22 19 | - 24 20 | os: 21 | - ubuntu-latest 22 | - macos-latest 23 | - windows-latest 24 | 25 | steps: 26 | - uses: actions/checkout@v4 27 | 28 | - name: Setup Node.js environment 29 | uses: actions/setup-node@v4 30 | with: 31 | node-version: ${{ matrix.node }} 32 | cache: npm 33 | 34 | - name: Install packages 35 | run: npm ci 36 | 37 | - name: Lint and run tests 38 | run: npm run precommit 39 | -------------------------------------------------------------------------------- /.github/workflows/publish-container-image.yml: -------------------------------------------------------------------------------- 1 | name: Publish a Docker container image to GitHub Packages 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | tags: 8 | - v* 9 | 10 | env: 11 | REGISTRY: ghcr.io 12 | IMAGE_NAME: ${{ github.repository }} 13 | 14 | jobs: 15 | publish: 16 | runs-on: ubuntu-latest 17 | permissions: 18 | contents: read 19 | packages: write 20 | 21 | steps: 22 | - name: Checkout repository 23 | uses: actions/checkout@v4 24 | 25 | - name: Set up QEMU 26 | uses: docker/setup-qemu-action@v3 27 | 28 | - name: Set up Docker Buildx 29 | uses: docker/setup-buildx-action@v3 30 | 31 | - name: Log in to the registry 32 | uses: docker/login-action@v3 33 | with: 34 | registry: ${{ env.REGISTRY }} 35 | username: ${{ github.actor }} 36 | password: ${{ secrets.GITHUB_TOKEN }} 37 | 38 | - name: Extract tags and labels 39 | id: meta 40 | uses: docker/metadata-action@v5 41 | with: 42 | images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} 43 | 44 | - name: Build and push container image 45 | uses: docker/build-push-action@v6 46 | with: 47 | context: . 48 | push: true 49 | tags: ${{ steps.meta.outputs.tags }} 50 | labels: ${{ steps.meta.outputs.labels }} 51 | platforms: linux/amd64,linux/arm64 52 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 18 | .grunt 19 | 20 | # node-waf configuration 21 | .lock-wscript 22 | 23 | # Compiled binary addons (http://nodejs.org/api/addons.html) 24 | build/Release 25 | 26 | # Dependency directory 27 | # https://docs.npmjs.com/misc/faq#should-i-check-my-node-modules-folder-into-git 28 | node_modules 29 | # Keep it for test 30 | !test/custom-rules/relative-to-cwd/node_modules 31 | !test/custom-rules/scoped-package/node_modules 32 | 33 | # Optional npm cache directory 34 | .npm 35 | 36 | # Optional REPL history 37 | .node_repl_history 38 | 39 | # IDEs and editors 40 | .vscode 41 | .idea 42 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .node_repl_history 2 | .vscode/ 3 | .idea/ 4 | test/ 5 | .editorconfig 6 | .travis.yml 7 | appveyor.yml -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | engine-strict=true 2 | -------------------------------------------------------------------------------- /.pre-commit-hooks.yaml: -------------------------------------------------------------------------------- 1 | - id: markdownlint 2 | name: markdownlint 3 | description: "Checks the style of Markdown/Commonmark files." 4 | entry: markdownlint 5 | language: node 6 | types: [markdown] 7 | minimum_pre_commit_version: 0.15.0 8 | - id: markdownlint-fix 9 | name: markdownlint-fix 10 | description: "Fixes the style of Markdown/Commonmark files." 11 | entry: markdownlint --fix 12 | language: node 13 | types: [markdown] 14 | minimum_pre_commit_version: 0.15.0 15 | - id: markdownlint-docker 16 | name: markdownlint-docker 17 | description: "Checks the style of Markdown/Commonmark files." 18 | entry: ghcr.io/igorshubovych/markdownlint-cli 19 | language: docker_image 20 | types: [markdown] 21 | minimum_pre_commit_version: 0.15.0 22 | - id: markdownlint-fix-docker 23 | name: markdownlint-fix-docker 24 | description: "Fixes the style of Markdown/Commonmark files." 25 | entry: ghcr.io/igorshubovych/markdownlint-cli --fix 26 | language: docker_image 27 | types: [markdown] 28 | minimum_pre_commit_version: 0.15.0 29 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:lts-alpine 2 | 3 | WORKDIR /app 4 | 5 | COPY . . 6 | 7 | RUN npm install --production 8 | 9 | RUN npm install --global 10 | 11 | WORKDIR /workdir 12 | 13 | ENTRYPOINT ["/usr/local/bin/markdownlint"] 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016-2020 Igor Shubovych 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # markdownlint-cli 2 | 3 | [![GitHub Actions Build Status][actions-badge]][actions-url] 4 | 5 | > Command Line Interface for [MarkdownLint][markdownlint] 6 | 7 | ## Installation 8 | 9 | ```bash 10 | npm install -g markdownlint-cli 11 | ``` 12 | 13 | On macOS you can install via [Homebrew](https://brew.sh/): 14 | 15 | ```bash 16 | brew install markdownlint-cli 17 | ``` 18 | 19 | ## Usage 20 | 21 | ```bash 22 | markdownlint --help 23 | 24 | Usage: markdownlint [options] [files|directories|globs...] 25 | 26 | MarkdownLint Command Line Interface 27 | 28 | Arguments: 29 | files|directories|globs files, directories, and/or globs to lint 30 | 31 | Options: 32 | -V, --version output the version number 33 | -c, --config configuration file (JSON, JSONC, JS, YAML, or TOML) 34 | --configPointer JSON Pointer to object within configuration file (default: "") 35 | -d, --dot include files/folders with a dot (for example `.github`) 36 | -f, --fix fix basic errors (does not work with STDIN) 37 | -i, --ignore file(s) to ignore/exclude (default: []) 38 | -j, --json write issues in json format 39 | -o, --output write issues to file (no console) 40 | -p, --ignore-path path to file with ignore pattern(s) 41 | -q, --quiet do not write issues to STDOUT 42 | -r, --rules include custom rule files (default: []) 43 | -s, --stdin read from STDIN (does not work with files) 44 | --enable Enable certain rules, e.g. --enable MD013 MD041 -- 45 | --disable Disable certain rules, e.g. --disable MD013 MD041 -- 46 | -h, --help display help for command 47 | ``` 48 | 49 | Or run using [Docker](https://www.docker.com) and [GitHub Packages](https://github.com/features/packages): 50 | 51 | ```bash 52 | docker run -v $PWD:/workdir ghcr.io/igorshubovych/markdownlint-cli:latest "*.md" 53 | ``` 54 | 55 | > **Note** 56 | > Because `--enable` and `--disable` are [variadic arguments that accept multiple values][commander-variadic], it is necessary to end the list by passing `--` before the `` argument like so: `markdownlint --disable MD013 -- README.md`. 57 | 58 | ### Globbing 59 | 60 | `markdownlint-cli` supports advanced globbing patterns like `**/*.md` ([more information][globprimer]). 61 | With shells like Bash, it may be necessary to quote globs so they are not interpreted by the shell. 62 | For example, `--ignore *.md` would be expanded by Bash to `--ignore a.md b.md ...` before invoking `markdownlint-cli`, causing it to ignore only the first file because `--ignore` takes a single parameter (though it can be used multiple times). 63 | Quoting the glob like `--ignore '*.md'` passes it through unexpanded and ignores the set of files. 64 | 65 | #### Globbing examples 66 | 67 | To lint all Markdown files in a Node.js project (excluding dependencies), the following commands might be used: 68 | 69 | Windows CMD: `markdownlint **/*.md --ignore node_modules` 70 | 71 | Linux Bash: `markdownlint '**/*.md' --ignore node_modules` 72 | 73 | ### Ignoring files 74 | 75 | If present in the current folder, a `.markdownlintignore` file will be used to ignore files and/or directories according to the rules for [gitignore][gitignore]. 76 | If the `-p`/`--ignore-path` option is present, the specified file will be used instead of `.markdownlintignore`. 77 | 78 | The order of operations is: 79 | 80 | - Enumerate files/directories/globs passed on the command line 81 | - Apply exclusions from `-p`/`--ignore-path` (if specified) or `.markdownlintignore` (if present) 82 | - Apply exclusions from any `-i`/`--ignore` option(s) that are specified 83 | 84 | ### Fixing errors 85 | 86 | When the `--fix` option is specified, `markdownlint-cli` tries to apply all fixes reported by the active rules and reports any errors that remain. 87 | Because this option makes changes to the input files, it is good to make a backup first or work with files under source control so any unwanted changes can be undone. 88 | 89 | > Because not all rules include fix information when reporting errors, fixes may overlap, and not all errors are fixable, `--fix` will not usually address all errors. 90 | 91 | ## Configuration 92 | 93 | `markdownlint-cli` reuses [the rules][rules] from `markdownlint` package. 94 | 95 | Configuration is stored in JSON, JSONC, YAML, INI, or TOML files in the same [config format][config]. 96 | 97 | A sample configuration file: 98 | 99 | ```json 100 | { 101 | "default": true, 102 | "MD003": { "style": "atx_closed" }, 103 | "MD007": { "indent": 4 }, 104 | "no-hard-tabs": false, 105 | "whitespace": false 106 | } 107 | ``` 108 | 109 | For more examples, see [.markdownlint.jsonc][markdownlint-jsonc], [.markdownlint.yaml][markdownlint-yaml], [test-config.toml](test/test-config.toml) or the [style folder][style-folder]. 110 | 111 | The CLI argument `--config` is not required. 112 | If it is not provided, `markdownlint-cli` looks for the file `.markdownlint.jsonc`/`.markdownlint.json`/`.markdownlint.yaml`/`.markdownlint.yml` in current folder, or for the file `.markdownlintrc` in the current or all parent folders. 113 | The algorithm is described in detail on the [`rc` package page][rc-standards]. 114 | Note that when relying on the lookup of a file named `.markdownlintrc` in the current or parent folders, the only syntaxes accepted are INI and JSON, and the file cannot have an extension. 115 | If the `--config` argument is provided, the file must be valid JSON, JSONC, JS, or YAML. 116 | JS configuration files contain JavaScript code, must have the `.js` or `.cjs` file extension, and must export (via `module.exports = ...`) a configuration object of the form shown above. 117 | 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**. 118 | A JS configuration file may internally `require` one or more npm packages as a way of reusing configuration across projects. 119 | 120 | The `--configPointer` argument allows the use of [JSON Pointer][json-pointer] syntax to identify a sub-object within the configuration object (per above). 121 | 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`). 122 | 123 | `--enable` and `--disable` override configuration files; if a configuration file disables `MD123` and you pass `--enable MD123`, it will be enabled. 124 | If a rule is passed to both `--enable` and `--disable`, it will be disabled. 125 | 126 | > - JS configuration files must be provided via the `--config` argument; they are not automatically loaded because running untrusted code is a security concern. 127 | > - TOML configuration files must be provided via the `--config` argument; they are not automatically loaded. 128 | 129 | ## Exit codes 130 | 131 | `markdownlint-cli` returns one of the following exit codes: 132 | 133 | - `0`: Program ran successfully 134 | - `1`: Linting errors 135 | - `2`: Unable to write `-o`/`--output` output file 136 | - `3`: Unable to load `-r`/`--rules` custom rule 137 | - `4`: Unexpected error (e.g. malformed config) 138 | 139 | ## Use with pre-commit 140 | 141 | To run `markdownlint-cli` as part of a [pre-commit][pre-commit] workflow, add something like the below to the `repos` list in the project's `.pre-commit-config.yaml`: 142 | 143 | ```yaml 144 | - repo: https://github.com/igorshubovych/markdownlint-cli 145 | rev: v0.45.0 146 | hooks: 147 | - id: markdownlint 148 | ``` 149 | 150 | > Depending on the environment this workflow runs in, it may be necessary to [override the language version of Node.js used by pre-commit][pre-commit-version]. 151 | 152 | ## Related 153 | 154 | - [markdownlint][markdownlint] - API for this module 155 | - [markdownlint-cli2][markdownlint-cli2] - Alternate CLI implementation 156 | - [glob][glob] - Pattern matching implementation 157 | - [ignore][ignore] - `.markdownlintignore` implementation 158 | 159 | ## License 160 | 161 | MIT © Igor Shubovych 162 | 163 | [actions-badge]: https://github.com/igorshubovych/markdownlint-cli/workflows/CI/badge.svg?branch=master 164 | [actions-url]: https://github.com/igorshubovych/markdownlint-cli/actions?query=workflow%3ACI 165 | [commander-variadic]: https://github.com/tj/commander.js#variadic-option 166 | [json-pointer]: https://datatracker.ietf.org/doc/html/rfc6901 167 | [markdownlint]: https://github.com/DavidAnson/markdownlint 168 | [markdownlint-cli2]: https://github.com/DavidAnson/markdownlint-cli2 169 | [markdownlint-jsonc]: https://github.com/DavidAnson/markdownlint/blob/main/schema/.markdownlint.jsonc 170 | [markdownlint-yaml]: https://github.com/DavidAnson/markdownlint/blob/main/schema/.markdownlint.yaml 171 | [rules]: https://github.com/DavidAnson/markdownlint/blob/main/doc/Rules.md 172 | [config]: https://github.com/DavidAnson/markdownlint#optionsconfig 173 | [style-folder]: https://github.com/DavidAnson/markdownlint/tree/main/style 174 | [rc-standards]: https://www.npmjs.com/package/rc#standards 175 | [glob]: https://github.com/isaacs/node-glob 176 | [globprimer]: https://github.com/isaacs/node-glob/blob/master/README.md#glob-primer 177 | [ignore]: https://github.com/kaelzhang/node-ignore 178 | [gitignore]: https://git-scm.com/docs/gitignore 179 | [pre-commit]: https://pre-commit.com/ 180 | [pre-commit-version]: https://pre-commit.com/#overriding-language-version 181 | [ESM-only]: https://gist.github.com/sindresorhus/a39789f98801d908bbc7ff3ecc99d99c 182 | -------------------------------------------------------------------------------- /markdownlint.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import fs from 'node:fs'; 4 | import path from 'node:path'; 5 | import Module from 'node:module'; 6 | import os from 'node:os'; 7 | import process from 'node:process'; 8 | import {program} from 'commander'; 9 | import {globSync} from 'tinyglobby'; 10 | import {applyFixes} from 'markdownlint'; 11 | import {lint, readConfig} from 'markdownlint/sync'; 12 | import rc from 'run-con'; 13 | import {minimatch} from 'minimatch'; 14 | import jsonpointer from 'jsonpointer'; 15 | 16 | const require = Module.createRequire(import.meta.url); 17 | const options = program.opts(); 18 | // The following two values are copied from package.json (and validated by tests) 19 | const version = '0.45.0'; 20 | const description = 'MarkdownLint Command Line Interface'; 21 | 22 | function markdownItFactory() { 23 | return require('markdown-it')({html: true}); 24 | } 25 | 26 | function posixPath(p) { 27 | return p.split(path.sep).join(path.posix.sep); 28 | } 29 | 30 | function jsoncParse(text) { 31 | const {parse, printParseErrorCode} = require('jsonc-parser'); 32 | const errors = []; 33 | const result = parse(text, errors, {allowTrailingComma: true}); 34 | if (errors.length > 0) { 35 | const aggregate = errors.map(error => `${printParseErrorCode(error.error)} (offset ${error.offset}, length ${error.length})`).join(', '); 36 | throw new Error(`Unable to parse JSON(C) content, ${aggregate}`); 37 | } 38 | 39 | return result; 40 | } 41 | 42 | function yamlParse(text) { 43 | return require('js-yaml').load(text); 44 | } 45 | 46 | function tomlParse(text) { 47 | return require('smol-toml').parse(text); 48 | } 49 | 50 | const exitCodes = { 51 | lintFindings: 1, 52 | failedToWriteOutputFile: 2, 53 | failedToLoadCustomRules: 3, 54 | unexpectedError: 4 55 | }; 56 | 57 | const projectConfigFiles = ['.markdownlint.jsonc', '.markdownlint.json', '.markdownlint.yaml', '.markdownlint.yml']; 58 | // TOML files can be (incorrectly) read by yamlParse (but not vice versa), so tomlParse needs to go before yamlParse 59 | const configParsers = [jsoncParse, tomlParse, yamlParse]; 60 | const fsOptions = {encoding: 'utf8'}; 61 | const processCwd = process.cwd(); 62 | 63 | function readConfiguration(userConfigFile) { 64 | // Load from well-known config files 65 | let config = rc('markdownlint', {}); 66 | for (const projectConfigFile of projectConfigFiles) { 67 | try { 68 | fs.accessSync(projectConfigFile); 69 | const projectConfig = readConfig(projectConfigFile, configParsers); 70 | config = {...config, ...projectConfig}; 71 | break; 72 | } catch { 73 | // Ignore failure 74 | } 75 | } 76 | 77 | // Normally parsing this file is not needed, because it is already parsed by rc package. 78 | // However I have to do it to overwrite configuration from .markdownlint.{jsonc,json,yaml,yml}. 79 | if (userConfigFile) { 80 | try { 81 | const jsConfigFile = /\.c?js$/i.test(userConfigFile); 82 | const userConfig = jsConfigFile ? require(path.resolve(processCwd, userConfigFile)) : readConfig(userConfigFile, configParsers); 83 | config = require('deep-extend')(config, userConfig); 84 | } catch (error) { 85 | console.error(`Cannot read or parse config file '${userConfigFile}': ${error.message}`); 86 | process.exitCode = exitCodes.unexpectedError; 87 | } 88 | } 89 | 90 | return config; 91 | } 92 | 93 | function prepareFileList(files, fileExtensions, previousResults) { 94 | const globOptions = { 95 | dot: Boolean(options.dot), 96 | onlyFiles: true, 97 | expandDirectories: false, 98 | caseSensitiveMatch: !(os.platform() === 'win32' || os.platform() === 'darwin') 99 | }; 100 | let extensionGlobPart = '*.'; 101 | if (!fileExtensions) { 102 | // Match everything 103 | extensionGlobPart = ''; 104 | } else if (fileExtensions.length === 1) { 105 | // Glob seems not to match patterns like 'foo.{js}' 106 | extensionGlobPart += fileExtensions[0]; 107 | } else { 108 | extensionGlobPart += '{' + fileExtensions.join(',') + '}'; 109 | } 110 | 111 | files = files.map(file => { 112 | try { 113 | if (fs.lstatSync(file).isDirectory()) { 114 | // Directory (file falls through to below) 115 | if (previousResults) { 116 | const matcher = new minimatch.Minimatch(posixPath(path.resolve(processCwd, path.join(file, '**', extensionGlobPart))), globOptions); 117 | return previousResults.filter(fileInfo => matcher.match(fileInfo.absolute)).map(fileInfo => fileInfo.original); 118 | } 119 | 120 | return globSync(posixPath(path.join(file, '**', extensionGlobPart)), globOptions); 121 | } 122 | } catch { 123 | // Not a directory, not a file, may be a glob 124 | if (previousResults) { 125 | const matcher = new minimatch.Minimatch(posixPath(path.resolve(processCwd, file)), globOptions); 126 | return previousResults.filter(fileInfo => matcher.match(fileInfo.absolute)).map(fileInfo => fileInfo.original); 127 | } 128 | 129 | return globSync(file, globOptions); 130 | } 131 | 132 | // File 133 | return file; 134 | }); 135 | return files.flat().map(file => ({ 136 | original: file, 137 | relative: path.relative(processCwd, file), 138 | absolute: path.resolve(file) 139 | })); 140 | } 141 | 142 | function printResult(lintResult) { 143 | const results = Object.keys(lintResult).flatMap(file => 144 | lintResult[file].map(result => { 145 | if (options.json) { 146 | return { 147 | fileName: file, 148 | ...result 149 | }; 150 | } 151 | 152 | return { 153 | file: file, 154 | lineNumber: result.lineNumber, 155 | column: (result.errorRange && result.errorRange[0]) || 0, 156 | names: result.ruleNames.join('/'), 157 | description: result.ruleDescription + (result.errorDetail ? ' [' + result.errorDetail + ']' : '') + (result.errorContext ? ' [Context: "' + result.errorContext + '"]' : '') 158 | }; 159 | }) 160 | ); 161 | 162 | let lintResultString = ''; 163 | if (results.length > 0) { 164 | if (options.json) { 165 | results.sort((a, b) => a.fileName.localeCompare(b.fileName) || a.lineNumber - b.lineNumber || a.ruleDescription.localeCompare(b.ruleDescription)); 166 | lintResultString = JSON.stringify(results, null, 2); 167 | } else { 168 | results.sort((a, b) => a.file.localeCompare(b.file) || a.lineNumber - b.lineNumber || a.names.localeCompare(b.names) || a.description.localeCompare(b.description)); 169 | 170 | lintResultString = results 171 | .map(result => { 172 | const {file, lineNumber, column, names, description} = result; 173 | const columnText = column ? `:${column}` : ''; 174 | return `${file}:${lineNumber}${columnText} ${names} ${description}`; 175 | }) 176 | .join('\n'); 177 | } 178 | 179 | // Note: process.exit(1) will end abruptly, interrupting asynchronous IO 180 | // streams (e.g., when the output is being piped). Just set the exit code 181 | // and let the program terminate normally. 182 | // @see {@link https://nodejs.org/dist/latest-v8.x/docs/api/process.html#process_process_exit_code} 183 | // @see {@link https://github.com/igorshubovych/markdownlint-cli/pull/29#issuecomment-343535291} 184 | process.exitCode = exitCodes.lintFindings; 185 | } 186 | 187 | if (options.output) { 188 | lintResultString = lintResultString.length > 0 ? lintResultString + os.EOL : lintResultString; 189 | try { 190 | fs.writeFileSync(options.output, lintResultString); 191 | } catch (error) { 192 | console.warn('Cannot write to output file ' + options.output + ': ' + error.message); 193 | process.exitCode = exitCodes.failedToWriteOutputFile; 194 | } 195 | } else if (lintResultString && !options.quiet) { 196 | console.error(lintResultString); 197 | } 198 | } 199 | 200 | function concatArray(item, array) { 201 | array.push(item); 202 | return array; 203 | } 204 | 205 | program 206 | .version(version) 207 | .description(description) 208 | .option('-c, --config ', 'configuration file (JSON, JSONC, JS, YAML, or TOML)') 209 | .option('--configPointer ', 'JSON Pointer to object within configuration file', '') 210 | .option('-d, --dot', 'include files/folders with a dot (for example `.github`)') 211 | .option('-f, --fix', 'fix basic errors (does not work with STDIN)') 212 | .option('-i, --ignore ', 'file(s) to ignore/exclude', concatArray, []) 213 | .option('-j, --json', 'write issues in json format') 214 | .option('-o, --output ', 'write issues to file (no console)') 215 | .option('-p, --ignore-path ', 'path to file with ignore pattern(s)') 216 | .option('-q, --quiet', 'do not write issues to STDOUT') 217 | .option('-r, --rules ', 'include custom rule files', concatArray, []) 218 | .option('-s, --stdin', 'read from STDIN (does not work with files)') 219 | .option('--enable ', 'Enable certain rules, e.g. --enable MD013 MD041 --') 220 | .option('--disable ', 'Disable certain rules, e.g. --disable MD013 MD041 --') 221 | .argument('[files|directories|globs...]', 'files, directories, and/or globs to lint'); 222 | 223 | program.parse(process.argv); 224 | 225 | function tryResolvePath(filepath) { 226 | try { 227 | if ((path.basename(filepath) === filepath || filepath.startsWith('@')) && path.extname(filepath) === '') { 228 | // Looks like a package name, resolve it relative to cwd 229 | // Get list of directories, where requested module can be. 230 | let paths = Module._nodeModulePaths(processCwd); 231 | // eslint-disable-next-line unicorn/prefer-spread 232 | paths = paths.concat(Module.globalPaths); 233 | if (require.resolve.paths) { 234 | // Node >= 8.9.0 235 | return require.resolve(filepath, {paths: paths}); 236 | } 237 | 238 | return Module._resolveFilename(filepath, {paths: paths}); 239 | } 240 | 241 | // Maybe it is a path to package installed locally 242 | return require.resolve(path.join(processCwd, filepath)); 243 | } catch { 244 | return filepath; 245 | } 246 | } 247 | 248 | function loadCustomRules(rules) { 249 | return rules.flatMap(rule => { 250 | try { 251 | const resolvedPath = [tryResolvePath(rule)]; 252 | const fileList = prepareFileList(resolvedPath, ['js', 'cjs', 'mjs']).flatMap(filepath => require(filepath.absolute)); 253 | if (fileList.length === 0) { 254 | throw new Error('No such rule'); 255 | } 256 | 257 | return fileList; 258 | } catch (error) { 259 | console.error('Cannot load custom rule ' + rule + ': ' + error.message); 260 | return process.exit(exitCodes.failedToLoadCustomRules); 261 | } 262 | }); 263 | } 264 | 265 | let ignorePath = '.markdownlintignore'; 266 | let {existsSync} = fs; 267 | if (options.ignorePath) { 268 | ignorePath = options.ignorePath; 269 | existsSync = () => true; 270 | } 271 | 272 | let ignoreFilter = () => true; 273 | if (existsSync(ignorePath)) { 274 | const ignoreText = fs.readFileSync(ignorePath, fsOptions); 275 | const ignore = require('ignore'); 276 | const ignoreInstance = ignore().add(ignoreText); 277 | ignoreFilter = fileInfo => !ignoreInstance.ignores(fileInfo.relative); 278 | } 279 | 280 | const files = prepareFileList(program.args, ['md', 'markdown']).filter(value => ignoreFilter(value)); 281 | const ignores = prepareFileList(options.ignore, null, files); 282 | const customRules = loadCustomRules(options.rules); 283 | const diff = files.filter(file => !ignores.some(ignore => ignore.absolute === file.absolute)).map(paths => paths.original); 284 | 285 | function lintAndPrint(stdin, files) { 286 | files ||= []; 287 | const configuration = readConfiguration(options.config); 288 | const config = jsonpointer.get(configuration, options.configPointer) || {}; 289 | 290 | for (const rule of options.enable || []) { 291 | // Leave default values in place if rule is an object 292 | config[rule] ||= true; 293 | } 294 | 295 | for (const rule of options.disable || []) { 296 | config[rule] = false; 297 | } 298 | 299 | const lintOptions = { 300 | markdownItFactory, 301 | config, 302 | configParsers, 303 | customRules, 304 | files 305 | }; 306 | if (stdin) { 307 | lintOptions.strings = { 308 | stdin 309 | }; 310 | } 311 | 312 | if (options.fix) { 313 | const fixOptions = {...lintOptions}; 314 | for (const file of files) { 315 | fixOptions.files = [file]; 316 | const fixResult = lint(fixOptions); 317 | const fixes = fixResult[file].filter(error => error.fixInfo); 318 | if (fixes.length > 0) { 319 | const originalText = fs.readFileSync(file, fsOptions); 320 | const fixedText = applyFixes(originalText, fixes); 321 | if (originalText !== fixedText) { 322 | fs.writeFileSync(file, fixedText, fsOptions); 323 | } 324 | } 325 | } 326 | } 327 | 328 | const lintResult = lint(lintOptions); 329 | printResult(lintResult); 330 | } 331 | 332 | try { 333 | if (files.length > 0 && !options.stdin) { 334 | lintAndPrint(null, diff); 335 | } else if (files.length === 0 && options.stdin && !options.fix) { 336 | import('node:stream/consumers').then(module => module.text(process.stdin)).then(lintAndPrint); 337 | } else { 338 | program.help(); 339 | } 340 | } catch (error) { 341 | console.error(error); 342 | process.exit(exitCodes.unexpectedError); 343 | } 344 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "markdownlint-cli", 3 | "version": "0.45.0", 4 | "description": "MarkdownLint Command Line Interface", 5 | "type": "module", 6 | "main": "markdownlint.js", 7 | "bin": { 8 | "markdownlint": "markdownlint.js" 9 | }, 10 | "engines": { 11 | "node": ">=20" 12 | }, 13 | "scripts": { 14 | "invalid": "node ./markdownlint.js --config test/test-config.json -- test/incorrect.md", 15 | "fix": "cp test/incorrect.md test/incorrect.copy.md && node ./markdownlint.js --fix test/incorrect.copy.md ; cat test/incorrect.copy.md && rm test/incorrect.copy.md", 16 | "test": "ava", 17 | "watch": "npm test --watch", 18 | "start": "node ./markdownlint.js", 19 | "precommit": "xo && npm test" 20 | }, 21 | "files": [ 22 | "markdownlint.js" 23 | ], 24 | "repository": { 25 | "type": "git", 26 | "url": "git+https://github.com/igorshubovych/markdownlint-cli.git" 27 | }, 28 | "keywords": [ 29 | "markdown", 30 | "markdownlint", 31 | "cli", 32 | "cli-app" 33 | ], 34 | "author": "Igor Shubovych ", 35 | "license": "MIT", 36 | "bugs": { 37 | "url": "https://github.com/igorshubovych/markdownlint-cli/issues" 38 | }, 39 | "homepage": "https://github.com/igorshubovych/markdownlint-cli#readme", 40 | "dependencies": { 41 | "commander": "~14.0.0", 42 | "deep-extend": "~0.6.0", 43 | "ignore": "~7.0.5", 44 | "js-yaml": "~4.1.0", 45 | "jsonc-parser": "~3.3.1", 46 | "jsonpointer": "~5.0.1", 47 | "markdownlint": "~0.38.0", 48 | "markdown-it": "~14.1.0", 49 | "minimatch": "~10.0.1", 50 | "run-con": "~1.3.2", 51 | "smol-toml": "~1.3.4", 52 | "tinyglobby": "~0.2.12" 53 | }, 54 | "devDependencies": { 55 | "ava": "^6.2.0", 56 | "markdownlint-cli-local-test-rule": "./test/custom-rules/markdownlint-cli-local-test-rule", 57 | "nano-spawn": "^1.0.2", 58 | "xo": "^1.0.5" 59 | }, 60 | "xo": { 61 | "prettier": true, 62 | "space": true, 63 | "rules": { 64 | "comma-dangle": 0, 65 | "linebreak-style": 0, 66 | "no-var": 0, 67 | "prefer-arrow-callback": 0, 68 | "promise/prefer-await-to-then": 0, 69 | "object-shorthand": 0, 70 | "unicorn/prefer-module": 0, 71 | "unicorn/prefer-ternary": 0, 72 | "unicorn/prefer-top-level-await": 0 73 | } 74 | }, 75 | "prettier": { 76 | "arrowParens": "avoid", 77 | "bracketSpacing": false, 78 | "endOfLine": "auto", 79 | "printWidth": 1000, 80 | "singleQuote": true, 81 | "trailingComma": "none" 82 | }, 83 | "ava": { 84 | "files": [ 85 | "test/**/*.js", 86 | "test/**/*.cjs", 87 | "test/**/*.mjs", 88 | "!test/custom-rules/**/*.js", 89 | "!test/custom-rules/**/*.cjs", 90 | "!test/custom-rules/**/*.mjs", 91 | "!test/md043-config.cjs" 92 | ], 93 | "failFast": true, 94 | "timeout": "1m", 95 | "workerThreads": false 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /test/.folder/.file-with-dot.md: -------------------------------------------------------------------------------- 1 | ## header 2 2 | # header 3 | 4 | ```fence 5 | $ code 6 | ``` 7 | 8 | text 9 | -------------------------------------------------------------------------------- /test/.folder/incorrect-dot.md: -------------------------------------------------------------------------------- 1 | ## header 2 2 | # header 3 | 4 | ```fence 5 | $ code 6 | ``` 7 | 8 | text 9 | 10 | ```fence 11 | $ code 12 | ``` 13 | 14 | text 15 | 16 | ```fence 17 | $ code 18 | ``` 19 | 20 | text 21 | 22 | ```fence 23 | $ code 24 | ``` 25 | 26 | text 27 | -------------------------------------------------------------------------------- /test/base-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "default": true, 3 | 4 | "MD003": { "style": "atx" }, 5 | "MD007": { "indent": 2 }, 6 | "MD013": { "line_length": 40 }, 7 | "MD033": false, 8 | "MD034": false, 9 | "no-hard-tabs": false, 10 | "whitespace": false 11 | } 12 | -------------------------------------------------------------------------------- /test/config-files/json-c/.markdownlint.json: -------------------------------------------------------------------------------- 1 | { 2 | // Inline comment 3 | "no-trailing-punctuation": { 4 | /* 5 | * Block comment 6 | */ 7 | "punctuation": "$" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /test/config-files/json-c/heading-dollar.md: -------------------------------------------------------------------------------- 1 | # Heading$ 2 | 3 | Text 4 | -------------------------------------------------------------------------------- /test/config-files/json-yaml-yml/.markdownlint.json: -------------------------------------------------------------------------------- 1 | { 2 | "no-trailing-punctuation": { 3 | "punctuation": "$" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /test/config-files/json-yaml-yml/.markdownlint.yaml: -------------------------------------------------------------------------------- 1 | no-trailing-punctuation: 2 | punctuation: "!" 3 | -------------------------------------------------------------------------------- /test/config-files/json-yaml-yml/.markdownlint.yml: -------------------------------------------------------------------------------- 1 | no-trailing-punctuation: 2 | punctuation: "!" 3 | -------------------------------------------------------------------------------- /test/config-files/json-yaml-yml/heading-dollar.md: -------------------------------------------------------------------------------- 1 | # Heading$ 2 | 3 | Text 4 | -------------------------------------------------------------------------------- /test/config-files/json/.markdownlint.json: -------------------------------------------------------------------------------- 1 | { 2 | "no-trailing-punctuation": { 3 | "punctuation": "$" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /test/config-files/json/heading-dollar.md: -------------------------------------------------------------------------------- 1 | # Heading$ 2 | 3 | Text 4 | -------------------------------------------------------------------------------- /test/config-files/jsonc-json-yaml-yml/.markdownlint.json: -------------------------------------------------------------------------------- 1 | { 2 | "no-trailing-punctuation": { 3 | "punctuation": "!" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /test/config-files/jsonc-json-yaml-yml/.markdownlint.jsonc: -------------------------------------------------------------------------------- 1 | { 2 | "no-trailing-punctuation": { 3 | "punctuation": "$" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /test/config-files/jsonc-json-yaml-yml/.markdownlint.yaml: -------------------------------------------------------------------------------- 1 | no-trailing-punctuation: 2 | punctuation: "!" 3 | -------------------------------------------------------------------------------- /test/config-files/jsonc-json-yaml-yml/.markdownlint.yml: -------------------------------------------------------------------------------- 1 | no-trailing-punctuation: 2 | punctuation: "!" 3 | -------------------------------------------------------------------------------- /test/config-files/jsonc-json-yaml-yml/heading-dollar.md: -------------------------------------------------------------------------------- 1 | # Heading$ 2 | 3 | Text 4 | -------------------------------------------------------------------------------- /test/config-files/jsonc/.markdownlint.jsonc: -------------------------------------------------------------------------------- 1 | { 2 | // Inline comment 3 | "no-trailing-punctuation": { 4 | /* 5 | * Block comment 6 | */ 7 | "punctuation": "$", 8 | }, 9 | } 10 | -------------------------------------------------------------------------------- /test/config-files/jsonc/heading-dollar.md: -------------------------------------------------------------------------------- 1 | # Heading$ 2 | 3 | Text 4 | -------------------------------------------------------------------------------- /test/config-files/yaml-yml/.markdownlint.yaml: -------------------------------------------------------------------------------- 1 | no-trailing-punctuation: 2 | punctuation: "$" 3 | -------------------------------------------------------------------------------- /test/config-files/yaml-yml/.markdownlint.yml: -------------------------------------------------------------------------------- 1 | no-trailing-punctuation: 2 | punctuation: "!" 3 | -------------------------------------------------------------------------------- /test/config-files/yaml-yml/heading-dollar.md: -------------------------------------------------------------------------------- 1 | # Heading$ 2 | 3 | Text 4 | -------------------------------------------------------------------------------- /test/config-files/yaml/.markdownlint.yaml: -------------------------------------------------------------------------------- 1 | no-trailing-punctuation: 2 | punctuation: "$" 3 | -------------------------------------------------------------------------------- /test/config-files/yaml/heading-dollar.md: -------------------------------------------------------------------------------- 1 | # Heading$ 2 | 3 | Text 4 | -------------------------------------------------------------------------------- /test/config-files/yml/.markdownlint.yml: -------------------------------------------------------------------------------- 1 | no-trailing-punctuation: 2 | punctuation: "$" 3 | -------------------------------------------------------------------------------- /test/config-files/yml/heading-dollar.md: -------------------------------------------------------------------------------- 1 | # Heading$ 2 | 3 | Text 4 | -------------------------------------------------------------------------------- /test/correct.md: -------------------------------------------------------------------------------- 1 | # header 2 | 3 | ## header 2 4 | 5 | text 6 | 7 | ```fence 8 | code 9 | ``` 10 | 11 | text 12 | 13 | ```fence 14 | $ code 15 | output 16 | ``` 17 | 18 | text 19 | 20 | ```fence 21 | code 22 | code 23 | ``` 24 | 25 | text 26 | -------------------------------------------------------------------------------- /test/custom-rules/files/test-rule-1.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | names: ['test-rule-1'], 3 | description: 'Test rule broken', 4 | tags: ['test'], 5 | function: (parameters, onError) => { 6 | onError({ 7 | lineNumber: 1 8 | }); 9 | } 10 | }; 11 | -------------------------------------------------------------------------------- /test/custom-rules/files/test-rule-2.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | names: ['test-rule-2'], 3 | description: 'Test rule 2 broken', 4 | tags: ['test'], 5 | function: (parameters, onError) => { 6 | onError({ 7 | lineNumber: 1 8 | }); 9 | } 10 | }; 11 | -------------------------------------------------------------------------------- /test/custom-rules/files/test-rule-3-4.cjs: -------------------------------------------------------------------------------- 1 | module.exports = [ 2 | { 3 | names: ['test-rule-3'], 4 | description: 'Test rule 3 broken', 5 | tags: ['test'], 6 | function: (parameters, onError) => { 7 | onError({ 8 | lineNumber: 1 9 | }); 10 | } 11 | }, 12 | { 13 | names: ['test-rule-4'], 14 | description: 'Test rule 4 broken', 15 | tags: ['test'], 16 | function: (parameters, onError) => { 17 | onError({ 18 | lineNumber: 1 19 | }); 20 | } 21 | } 22 | ]; 23 | -------------------------------------------------------------------------------- /test/custom-rules/markdownlint-cli-local-test-rule-other/index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./rule.js'); 2 | -------------------------------------------------------------------------------- /test/custom-rules/markdownlint-cli-local-test-rule-other/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "markdownlint-cli-local-test-rule-other", 3 | "main": "./index.js", 4 | "version": "0.0.1", 5 | "type": "commonjs", 6 | "private": true 7 | } -------------------------------------------------------------------------------- /test/custom-rules/markdownlint-cli-local-test-rule-other/rule.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | names: ['markdownlint-cli-local-test-rule-other'], 3 | description: 'Test rule package other broken', 4 | tags: ['test'], 5 | function: (parameters, onError) => { 6 | onError({ 7 | lineNumber: 1 8 | }); 9 | } 10 | }; 11 | -------------------------------------------------------------------------------- /test/custom-rules/markdownlint-cli-local-test-rule/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | names: ['markdownlint-cli-local-test-rule'], 3 | description: 'Test rule package broken', 4 | tags: ['test'], 5 | function: (parameters, onError) => { 6 | onError({ 7 | lineNumber: 1 8 | }); 9 | } 10 | }; 11 | -------------------------------------------------------------------------------- /test/custom-rules/markdownlint-cli-local-test-rule/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "markdownlint-cli-local-test-rule", 3 | "main": "./index.js", 4 | "version": "0.0.1", 5 | "type": "commonjs", 6 | "private": true 7 | } -------------------------------------------------------------------------------- /test/custom-rules/relative-to-cwd/node_modules/markdownlint-cli-local-test-rule/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | names: ['markdownlint-cli-local-test-rule'], 3 | description: 'Test rule package relative to cwd broken', 4 | tags: ['test'], 5 | function: (params, onError) => { 6 | onError({ 7 | lineNumber: 1 8 | }); 9 | } 10 | }; 11 | -------------------------------------------------------------------------------- /test/custom-rules/relative-to-cwd/node_modules/markdownlint-cli-local-test-rule/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "markdownlint-cli-local-test-rule", 3 | "main": "./index.js", 4 | "version": "0.0.1", 5 | "type": "commonjs", 6 | "private": true 7 | } -------------------------------------------------------------------------------- /test/custom-rules/relative-to-cwd/package.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /test/custom-rules/scoped-package/node_modules/@scoped/custom-rule/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "markdownlint-cli-test-scoped-package-rule", 3 | "main": "./scoped-rule.cjs", 4 | "version": "0.0.1", 5 | "private": true 6 | } 7 | -------------------------------------------------------------------------------- /test/custom-rules/scoped-package/node_modules/@scoped/custom-rule/scoped-rule.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | names: ['scoped-rule'], 3 | description: 'Scoped rule', 4 | tags: ['test'], 5 | function: (_, onError) => { 6 | onError({ 7 | lineNumber: 1 8 | }); 9 | } 10 | }; 11 | -------------------------------------------------------------------------------- /test/custom-rules/scoped-package/scoped-test.md: -------------------------------------------------------------------------------- 1 | # Scoped Test 2 | -------------------------------------------------------------------------------- /test/default-false-config.yml: -------------------------------------------------------------------------------- 1 | default: false 2 | -------------------------------------------------------------------------------- /test/incorrect.md: -------------------------------------------------------------------------------- 1 | ## header 2 2 | # header 3 | 4 | ```fence 5 | $ code 6 | ``` 7 | 8 | text 9 | 10 | ```fence 11 | $ code 12 | ``` 13 | 14 | text 15 | 16 | ```fence 17 | $ code 18 | ``` 19 | 20 | text 21 | 22 | ```fence 23 | $ code 24 | ``` 25 | 26 | text 27 | -------------------------------------------------------------------------------- /test/inline-jsonc.md: -------------------------------------------------------------------------------- 1 | # Inline 2 | 3 | 4 | 8 | -------------------------------------------------------------------------------- /test/inline-yaml.md: -------------------------------------------------------------------------------- 1 | # Inline 2 | 3 | 4 | 8 | -------------------------------------------------------------------------------- /test/malformed-config.yaml: -------------------------------------------------------------------------------- 1 | - foo 2 | - bar 3 | -------------------------------------------------------------------------------- /test/markdownlintignore/.ignorefile: -------------------------------------------------------------------------------- 1 | **/*.md 2 | subdir/* 3 | -------------------------------------------------------------------------------- /test/markdownlintignore/.markdownlintignore: -------------------------------------------------------------------------------- 1 | # comment followed by blank line 2 | 3 | # star-star pattern with trailing space 4 | **/*.markdown 5 | # negated star pattern 6 | !subd*/incorrect.markdown 7 | -------------------------------------------------------------------------------- /test/markdownlintignore/correct.md: -------------------------------------------------------------------------------- 1 | # header 2 | -------------------------------------------------------------------------------- /test/markdownlintignore/incorrect.markdown: -------------------------------------------------------------------------------- 1 | # header -------------------------------------------------------------------------------- /test/markdownlintignore/incorrect.md: -------------------------------------------------------------------------------- 1 | # header -------------------------------------------------------------------------------- /test/markdownlintignore/subdir/incorrect.markdown: -------------------------------------------------------------------------------- 1 | # header -------------------------------------------------------------------------------- /test/md043-config.cjs: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Export config object directly (as below) 4 | // -OR- 5 | // via require('some-npm-module-that-exports-config') 6 | module.exports = { 7 | MD012: false, 8 | MD043: { 9 | headings: ['# First', '## Second', '### Third'] 10 | } 11 | }; 12 | -------------------------------------------------------------------------------- /test/md043-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "MD012": false, 3 | "MD043": { 4 | "headings": [ 5 | "# First", 6 | "## Second", 7 | "### Third" 8 | ] 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /test/md043-config.md: -------------------------------------------------------------------------------- 1 | # First 2 | 3 | Text 4 | 5 | ## Second 6 | 7 | Text 8 | 9 | ### Third 10 | 11 | Extra newline at end of file is sentinel to ensure config file is used 12 | 13 | -------------------------------------------------------------------------------- /test/md043-config.toml: -------------------------------------------------------------------------------- 1 | MD012 = false 2 | MD043 = { headings = ["# First", "## Second", "### Third"] } 3 | -------------------------------------------------------------------------------- /test/md043-config.yaml: -------------------------------------------------------------------------------- 1 | MD012: false 2 | MD043: 3 | headings: 4 | - "# First" 5 | - "## Second" 6 | - "### Third" 7 | -------------------------------------------------------------------------------- /test/nested-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "unused", 3 | 4 | "key": { 5 | "blanks-around-headings": false, 6 | "commands-show-output": false 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /test/nested-config.toml: -------------------------------------------------------------------------------- 1 | name = "unused" 2 | 3 | [key] 4 | other-name = "unused" 5 | 6 | [key.subkey] 7 | commands-show-output = false 8 | -------------------------------------------------------------------------------- /test/subdir-correct/correct.markdown: -------------------------------------------------------------------------------- 1 | # header 2 | 3 | ## header 2 4 | 5 | text 6 | 7 | ```fence 8 | code 9 | ``` 10 | 11 | text 12 | 13 | ```fence 14 | $ code 15 | output 16 | ``` 17 | 18 | text 19 | 20 | ```fence 21 | code 22 | code 23 | ``` 24 | 25 | text 26 | -------------------------------------------------------------------------------- /test/subdir-correct/correct.md: -------------------------------------------------------------------------------- 1 | # header 2 | 3 | ## header 2 4 | 5 | text 6 | 7 | ```fence 8 | code 9 | ``` 10 | 11 | text 12 | 13 | ```fence 14 | $ code 15 | output 16 | ``` 17 | 18 | text 19 | 20 | ```fence 21 | code 22 | code 23 | ``` 24 | 25 | text 26 | -------------------------------------------------------------------------------- /test/subdir-incorrect/UPPER.MD: -------------------------------------------------------------------------------- 1 | ## header 2 2 | # header 3 | 4 | ```fence 5 | $ code 6 | ``` 7 | 8 | text 9 | 10 | ```fence 11 | $ code 12 | ``` 13 | 14 | text 15 | 16 | ```fence 17 | $ code 18 | ``` 19 | 20 | text 21 | 22 | ```fence 23 | $ code 24 | ``` 25 | 26 | text 27 | -------------------------------------------------------------------------------- /test/subdir-incorrect/incorrect.markdown: -------------------------------------------------------------------------------- 1 | # header 2 | 3 | ```fence 4 | $ code 5 | ``` 6 | 7 | text 8 | 9 | ```fence 10 | $ code 11 | ``` 12 | 13 | text 14 | -------------------------------------------------------------------------------- /test/subdir-incorrect/incorrect.md: -------------------------------------------------------------------------------- 1 | ## header 2 2 | # header 3 | 4 | ```fence 5 | $ code 6 | ``` 7 | 8 | text 9 | 10 | ```fence 11 | $ code 12 | ``` 13 | 14 | text 15 | 16 | ```fence 17 | $ code 18 | ``` 19 | 20 | text 21 | 22 | ```fence 23 | $ code 24 | ``` 25 | 26 | text 27 | -------------------------------------------------------------------------------- /test/test-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "base-config.json", 3 | 4 | "MD007": { "indent": 4 }, 5 | "MD013": { "line_length": 200 } 6 | } 7 | -------------------------------------------------------------------------------- /test/test-config.toml: -------------------------------------------------------------------------------- 1 | default = true 2 | no-hard-tabs = false 3 | whitespace = false 4 | MD033 = false 5 | MD034 = false 6 | 7 | MD013.line_length = 200 8 | 9 | [MD003] 10 | style = "atx" 11 | 12 | [MD007] 13 | indent = 4 14 | -------------------------------------------------------------------------------- /test/test-config.yaml: -------------------------------------------------------------------------------- 1 | default: true 2 | no-hard-tabs: false 3 | whitespace: false 4 | MD033: false 5 | MD034: false 6 | MD003: 7 | style: atx 8 | MD007: 9 | indent: 2 10 | MD013: 11 | line_length: 200 12 | 13 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | import fs from 'node:fs'; 2 | import fsPromises from 'node:fs/promises'; 3 | import path from 'node:path'; 4 | import os from 'node:os'; 5 | import process from 'node:process'; 6 | import {fileURLToPath} from 'node:url'; 7 | import test from 'ava'; 8 | import nanoSpawn from 'nano-spawn'; 9 | 10 | const spawn = (script, arguments_, options) => { 11 | return nanoSpawn('node', [script, ...arguments_], options).then(subprocess => ({ 12 | ...subprocess, 13 | exitCode: 0 14 | })); 15 | }; 16 | 17 | // Shims import.meta.filename on Node before 20.11.0 18 | const __filename = fileURLToPath(import.meta.url); 19 | 20 | // Shims import.meta.dirname on Node before 20.11.0 21 | const __dirname = path.dirname(__filename); 22 | 23 | // Avoids "ExperimentalWarning: Importing JSON modules is an experimental feature and might change at any time" 24 | const importWithTypeJson = async file => JSON.parse(await fsPromises.readFile(path.resolve(__dirname, file))); 25 | 26 | const packageJson = await importWithTypeJson('../package.json'); 27 | const errorPattern = /(\.md|\.markdown|\.mdf|stdin):\d+(:\d+)? MD\d{3}/gm; 28 | 29 | process.chdir('./test'); 30 | 31 | test('--version option', async t => { 32 | const result = await spawn('../markdownlint.js', ['--version']); 33 | t.is(result.stdout, packageJson.version); 34 | t.is(result.stderr, ''); 35 | t.is(result.exitCode, 0); 36 | }); 37 | 38 | test('--help option', async t => { 39 | const result = await spawn('../markdownlint.js', ['--help']); 40 | t.true(result.stdout.includes('markdownlint')); 41 | t.true(result.stdout.includes(packageJson.description)); 42 | t.true(result.stdout.includes('--version')); 43 | t.true(result.stdout.includes('--help')); 44 | t.is(result.stderr, ''); 45 | t.is(result.exitCode, 0); 46 | }); 47 | 48 | test('no files shows help', async t => { 49 | const result = await spawn('../markdownlint.js', []); 50 | t.true(result.stdout.includes('--help')); 51 | t.is(result.stderr, ''); 52 | t.is(result.exitCode, 0); 53 | }); 54 | 55 | test('files and --stdin shows help', async t => { 56 | const result = await spawn('../markdownlint.js', ['--stdin', 'correct.md']); 57 | t.true(result.stdout.includes('--help')); 58 | t.is(result.stderr, ''); 59 | t.is(result.exitCode, 0); 60 | }); 61 | 62 | test('--fix and --stdin shows help', async t => { 63 | const result = await spawn('../markdownlint.js', ['--fix', '--stdin', 'correct.md']); 64 | t.true(result.stdout.includes('--help')); 65 | t.is(result.stderr, ''); 66 | t.is(result.exitCode, 0); 67 | }); 68 | 69 | test('linting of correct Markdown file yields no output', async t => { 70 | const result = await spawn('../markdownlint.js', ['--config', 'test-config.json', 'correct.md']); 71 | t.is(result.stdout, ''); 72 | t.is(result.stderr, ''); 73 | t.is(result.exitCode, 0); 74 | }); 75 | 76 | test('linting of correct Markdown file yields no output with absolute path', async t => { 77 | const result = await spawn('../markdownlint.js', ['--config', path.resolve('test-config.json'), 'correct.md']); 78 | t.is(result.stdout, ''); 79 | t.is(result.stderr, ''); 80 | t.is(result.exitCode, 0); 81 | }); 82 | 83 | test('linting of correct Markdown file with inline JSONC configuration yields no output', async t => { 84 | const result = await spawn('../markdownlint.js', ['inline-jsonc.md']); 85 | t.is(result.stdout, ''); 86 | t.is(result.stderr, ''); 87 | t.is(result.exitCode, 0); 88 | }); 89 | 90 | test('linting of correct Markdown file with inline YAML configuration yields no output', async t => { 91 | const result = await spawn('../markdownlint.js', ['inline-yaml.md']); 92 | t.is(result.stdout, ''); 93 | t.is(result.stderr, ''); 94 | t.is(result.exitCode, 0); 95 | }); 96 | 97 | test('linting of incorrect Markdown file fails', async t => { 98 | try { 99 | await spawn('../markdownlint.js', ['--config', 'test-config.json', 'incorrect.md']); 100 | t.fail(); 101 | } catch (error) { 102 | t.is(error.stdout, ''); 103 | t.is(error.stderr.match(errorPattern).length, 7); 104 | t.is(error.exitCode, 1); 105 | } 106 | }); 107 | 108 | test('linting of incorrect Markdown file fails prints issues as json', async t => { 109 | try { 110 | await spawn('../markdownlint.js', ['--config', 'test-config.json', 'incorrect.md', '--json']); 111 | t.fail(); 112 | } catch (error) { 113 | t.is(error.stdout, ''); 114 | const issues = JSON.parse(error.stderr); 115 | t.is(issues.length, 7); 116 | const issue = issues[0]; 117 | // Property "ruleInformation" changes with library version 118 | t.true(issue.ruleInformation.length > 0); 119 | issue.ruleInformation = null; 120 | const expected = { 121 | fileName: 'incorrect.md', 122 | lineNumber: 1, 123 | ruleNames: ['MD041', 'first-line-heading', 'first-line-h1'], 124 | ruleDescription: 'First line in a file should be a top-level heading', 125 | ruleInformation: null, 126 | errorContext: '## header 2', 127 | errorDetail: null, 128 | errorRange: null, 129 | fixInfo: null 130 | }; 131 | t.deepEqual(issues[0], expected); 132 | t.is(error.exitCode, 1); 133 | } 134 | }); 135 | 136 | test('linting of incorrect Markdown file fails with absolute path', async t => { 137 | try { 138 | await spawn('../markdownlint.js', ['--config', 'test-config.json', path.resolve('incorrect.md')]); 139 | t.fail(); 140 | } catch (error) { 141 | t.is(error.stdout, ''); 142 | t.is(error.stderr.match(errorPattern).length, 7); 143 | t.is(error.exitCode, 1); 144 | } 145 | }); 146 | 147 | test('linting of unreadable Markdown file fails', async t => { 148 | const unreadablePath = '../unreadable.test.md'; 149 | fs.symlinkSync('nonexistent.dest.md', unreadablePath, 'file'); 150 | 151 | try { 152 | await spawn('../markdownlint.js', ['--config', 'test-config.json', unreadablePath]); 153 | t.fail(); 154 | } catch (error) { 155 | t.is(error.exitCode, 4); 156 | } finally { 157 | fs.unlinkSync(unreadablePath, {force: true}); 158 | } 159 | }); 160 | 161 | test('linting of incorrect Markdown via npm run file fails with eol', async t => { 162 | try { 163 | const {default: spawn} = await import('nano-spawn'); 164 | await spawn('npm', ['run', 'invalid']); 165 | t.fail(); 166 | } catch (error) { 167 | t.regex(error.stderr, /MD\d{3}.*((\nnpm ERR!)|($))/); 168 | } 169 | }); 170 | 171 | test('glob linting works with passing files', async t => { 172 | const result = await spawn('../markdownlint.js', ['--config', 'test-config.json', '**/correct.md']); 173 | t.is(result.stdout, ''); 174 | t.is(result.stderr, ''); 175 | t.is(result.exitCode, 0); 176 | }); 177 | 178 | test('glob linting works with failing files', async t => { 179 | try { 180 | await spawn('../markdownlint.js', ['--config', 'test-config.json', '**/*.md']); 181 | t.fail(); 182 | } catch (error) { 183 | t.is(error.stdout, ''); 184 | t.is(error.stderr.match(errorPattern).length, 15); 185 | t.is(error.exitCode, 1); 186 | } 187 | }); 188 | 189 | test('dir linting works with passing .markdown files', async t => { 190 | const result = await spawn('../markdownlint.js', ['--config', 'test-config.json', 'subdir-correct']); 191 | t.is(result.stdout, ''); 192 | t.is(result.stderr, ''); 193 | t.is(result.exitCode, 0); 194 | }); 195 | 196 | test('dir linting works with failing .markdown files', async t => { 197 | try { 198 | await spawn('../markdownlint.js', ['--config', 'test-config.json', 'subdir-incorrect']); 199 | t.fail(); 200 | } catch (error) { 201 | t.is(error.stdout, ''); 202 | t.is(error.stderr.match(errorPattern).length, 9); 203 | t.is(error.exitCode, 1); 204 | } 205 | }); 206 | 207 | test('dir linting works with failing .markdown files and absolute path', async t => { 208 | try { 209 | await spawn('../markdownlint.js', ['--config', 'test-config.json', path.resolve('subdir-incorrect')]); 210 | t.fail(); 211 | } catch (error) { 212 | t.is(error.stdout, ''); 213 | t.is(error.stderr.match(errorPattern).length, 9); 214 | t.is(error.exitCode, 1); 215 | } 216 | }); 217 | 218 | test('glob linting with failing files passes when failures ignored by glob', async t => { 219 | const result = await spawn('../markdownlint.js', ['--config', 'test-config.json', '**/i*.md', '--ignore', '**/incorrect.md']); 220 | t.is(result.stdout, ''); 221 | t.is(result.stderr, ''); 222 | t.is(result.exitCode, 0); 223 | }); 224 | 225 | test('glob linting with failing files passes when everything ignored by glob', async t => { 226 | const result = await spawn('../markdownlint.js', ['--config', 'test-config.json', '**/*.md', '--ignore', '**/*']); 227 | t.is(result.stdout, ''); 228 | t.is(result.stderr, ''); 229 | t.is(result.exitCode, 0); 230 | }); 231 | 232 | test('glob linting with failing files has fewer errors when ignored by dir', async t => { 233 | try { 234 | await spawn('../markdownlint.js', ['--config', 'test-config.json', '**/*.md', '--ignore', 'subdir-incorrect']); 235 | t.fail(); 236 | } catch (error) { 237 | t.is(error.stdout, ''); 238 | t.is(error.stderr.match(errorPattern).length, 8); 239 | t.is(error.exitCode, 1); 240 | } 241 | }); 242 | 243 | test('glob linting with failing files has fewer errors when ignored by dir and absolute path', async t => { 244 | try { 245 | await spawn('../markdownlint.js', ['--config', 'test-config.json', '**/*.md', '--ignore', path.resolve('subdir-incorrect')]); 246 | t.fail(); 247 | } catch (error) { 248 | t.is(error.stdout, ''); 249 | t.is(error.stderr.match(errorPattern).length, 8); 250 | t.is(error.exitCode, 1); 251 | } 252 | }); 253 | 254 | test('dir linting with failing files has fewer errors when ignored by file', async t => { 255 | try { 256 | await spawn('../markdownlint.js', ['--config', 'test-config.json', 'subdir-incorrect', '--ignore', 'subdir-incorrect/incorrect.md']); 257 | t.fail(); 258 | } catch (error) { 259 | t.is(error.stdout, ''); 260 | t.is(error.stderr.match(errorPattern).length, 2); 261 | t.is(error.exitCode, 1); 262 | } 263 | }); 264 | 265 | test('dir linting with failing files has fewer errors when ignored by file and absolute path', async t => { 266 | try { 267 | await spawn('../markdownlint.js', ['--config', 'test-config.json', path.resolve('subdir-incorrect'), '--ignore', 'subdir-incorrect/incorrect.md']); 268 | t.fail(); 269 | } catch (error) { 270 | t.is(error.stdout, ''); 271 | t.is(error.stderr.match(errorPattern).length, 2); 272 | t.is(error.exitCode, 1); 273 | } 274 | }); 275 | 276 | test('glob linting with failing files passes when ignored by multiple globs', async t => { 277 | const result = await spawn('../markdownlint.js', ['--config', 'test-config.json', 'subdir-incorrect', '--ignore', '**/*.md', '--ignore', '**/*.markdown', '--ignore', '**/*.MD']); 278 | t.is(result.stdout, ''); 279 | t.is(result.stderr, ''); 280 | t.is(result.exitCode, 0); 281 | }); 282 | 283 | test('glob linting with directory ignore applies to all files within', async t => { 284 | const result = await spawn('../markdownlint.js', ['subdir-incorrect/**', '--ignore', 'subdir-incorrect']); 285 | t.is(result.stdout, ''); 286 | t.is(result.stderr, ''); 287 | t.is(result.exitCode, 0); 288 | }); 289 | 290 | test('linting results are sorted by file/line/names/description', async t => { 291 | try { 292 | await spawn('../markdownlint.js', ['--config', 'test-config.json', 'incorrect.md']); 293 | t.fail(); 294 | } catch (error) { 295 | const expected = ['incorrect.md:1 MD022/blanks-around-headings Headings should be surrounded by blank lines [Expected: 1; Actual: 0; Below] [Context: "## header 2"]', 'incorrect.md:1 MD041/first-line-heading/first-line-h1 First line in a file should be a top-level heading [Context: "## header 2"]', 'incorrect.md:2 MD022/blanks-around-headings Headings should be surrounded by blank lines [Expected: 1; Actual: 0; Above] [Context: "# header"]', 'incorrect.md:5:1 MD014/commands-show-output Dollar signs used before commands without showing output [Context: "$ code"]', 'incorrect.md:11:1 MD014/commands-show-output Dollar signs used before commands without showing output [Context: "$ code"]', 'incorrect.md:17:1 MD014/commands-show-output Dollar signs used before commands without showing output [Context: "$ code"]', 'incorrect.md:23:1 MD014/commands-show-output Dollar signs used before commands without showing output [Context: "$ code"]'].join('\n'); 296 | t.is(error.stdout, ''); 297 | t.is(error.stderr, expected); 298 | t.is(error.exitCode, 1); 299 | } 300 | }); 301 | 302 | test('glob linting does not try to lint directories as files', async t => { 303 | try { 304 | await spawn('../markdownlint.js', ['--config', 'test-config.json', '**/*', '--ignore', '**/*.mdf']); 305 | t.fail(); 306 | } catch (error) { 307 | t.is(error.stdout, ''); 308 | t.true(error.stderr.match(errorPattern).length > 0); 309 | t.is(error.exitCode, 1); 310 | } 311 | }); 312 | 313 | test('--stdin with empty input has no output', async t => { 314 | const stdin = {string: ''}; 315 | const result = await spawn('../markdownlint.js', ['--stdin'], {stdin}); 316 | t.is(result.stdout, ''); 317 | t.is(result.stderr, ''); 318 | t.is(result.exitCode, 0); 319 | }); 320 | 321 | test('--stdin with valid input has no output', async t => { 322 | const stdin = {string: ['# Heading', '', 'Text', ''].join('\n')}; 323 | const result = await spawn('../markdownlint.js', ['--stdin'], {stdin}); 324 | t.is(result.stdout, ''); 325 | t.is(result.stderr, ''); 326 | t.is(result.exitCode, 0); 327 | }); 328 | 329 | test('--stdin with invalid input reports violations', async t => { 330 | const stdin = {string: ['Heading', '', 'Text ', ''].join('\n')}; 331 | try { 332 | await spawn('../markdownlint.js', ['--stdin'], {stdin}); 333 | t.fail(); 334 | } catch (error) { 335 | t.is(error.stdout, ''); 336 | t.is(error.stderr.match(errorPattern).length, 2); 337 | t.is(error.exitCode, 1); 338 | } 339 | }); 340 | 341 | test('stdin support does not interfere with file linting', async t => { 342 | const result = await spawn('../markdownlint.js', ['--config', 'md043-config.json', 'md043-config.md']); 343 | t.is(result.stdout, ''); 344 | t.is(result.stderr, ''); 345 | t.is(result.exitCode, 0); 346 | }); 347 | 348 | test('--output with empty input has empty output', async t => { 349 | const stdin = {string: ''}; 350 | const output = '../outputA.txt'; 351 | const result = await spawn('../markdownlint.js', ['--stdin', '--output', output], {stdin}); 352 | t.is(result.stdout, ''); 353 | t.is(result.stderr, ''); 354 | t.is(result.exitCode, 0); 355 | t.is(fs.readFileSync(output, 'utf8'), ''); 356 | fs.unlinkSync(output); 357 | }); 358 | 359 | test('--output with valid input has empty output', async t => { 360 | const stdin = {string: ['# Heading', '', 'Text', ''].join('\n')}; 361 | const output = '../outputB.txt'; 362 | const result = await spawn('../markdownlint.js', ['--stdin', '--output', output], {stdin}); 363 | t.is(result.stdout, ''); 364 | t.is(result.stderr, ''); 365 | t.is(result.exitCode, 0); 366 | t.is(fs.readFileSync(output, 'utf8'), ''); 367 | fs.unlinkSync(output); 368 | }); 369 | 370 | test('--output with invalid input outputs violations', async t => { 371 | const stdin = {string: ['Heading', '', 'Text ', ''].join('\n')}; 372 | const output = '../outputC.txt'; 373 | try { 374 | await spawn('../markdownlint.js', ['--stdin', '--output', output], {stdin}); 375 | t.fail(); 376 | } catch (error) { 377 | t.is(error.stdout, ''); 378 | t.is(error.stderr, ''); 379 | t.is(error.exitCode, 1); 380 | t.is(fs.readFileSync(output, 'utf8').match(errorPattern).length, 2); 381 | fs.unlinkSync(output); 382 | } 383 | }); 384 | 385 | test('--output with invalid input and --json outputs issues as json', async t => { 386 | const stdin = {string: ['Heading', '', 'Text ', ''].join('\n')}; 387 | const output = '../outputF.json'; 388 | try { 389 | await spawn('../markdownlint.js', ['--stdin', '--output', output, '--json'], {stdin}); 390 | t.fail(); 391 | } catch (error) { 392 | t.is(error.stdout, ''); 393 | t.is(error.stderr, ''); 394 | t.is(error.exitCode, 1); 395 | t.is(JSON.parse(fs.readFileSync(output)).length, 2); 396 | fs.unlinkSync(output); 397 | } 398 | }); 399 | 400 | test('--output with invalid path fails', async t => { 401 | const stdin = {string: ''}; 402 | const output = 'invalid/outputD.txt'; 403 | try { 404 | await spawn('../markdownlint.js', ['--stdin', '--output', output], {stdin}); 405 | t.fail(); 406 | } catch (error) { 407 | t.is(error.stdout, ''); 408 | t.is(error.stderr.replace(/: ENOENT[^]*$/, ''), 'Cannot write to output file ' + output); 409 | t.is(error.exitCode, 2); 410 | t.throws(() => fs.accessSync(output, 'utf8')); 411 | } 412 | }); 413 | 414 | test('configuration file can be YAML', async t => { 415 | const result = await spawn('../markdownlint.js', ['--config', 'md043-config.yaml', 'md043-config.md']); 416 | t.is(result.stdout, ''); 417 | t.is(result.stderr, ''); 418 | t.is(result.exitCode, 0); 419 | }); 420 | 421 | test('configuration file can be JavaScript', async t => { 422 | const result = await spawn('../markdownlint.js', ['--config', 'md043-config.cjs', 'md043-config.md']); 423 | t.is(result.stdout, ''); 424 | t.is(result.stderr, ''); 425 | t.is(result.exitCode, 0); 426 | }); 427 | 428 | test('configuration file can be TOML', async t => { 429 | const result = await spawn('../markdownlint.js', ['--config', 'md043-config.toml', 'md043-config.md']); 430 | t.is(result.stdout, ''); 431 | t.is(result.stderr, ''); 432 | t.is(result.exitCode, 0); 433 | }); 434 | 435 | test('linting using a toml configuration file works', async t => { 436 | try { 437 | await spawn('../markdownlint.js', ['--config', 'test-config.toml', '**/*.md']); 438 | t.fail(); 439 | } catch (error) { 440 | t.is(error.stdout, ''); 441 | t.is(error.stderr.match(errorPattern).length, 15); 442 | t.is(error.exitCode, 1); 443 | } 444 | }); 445 | 446 | test('linting using a yaml configuration file works', async t => { 447 | try { 448 | await spawn('../markdownlint.js', ['--config', 'test-config.yaml', '**/*.md']); 449 | t.fail(); 450 | } catch (error) { 451 | t.is(error.stdout, ''); 452 | t.is(error.stderr.match(errorPattern).length, 15); 453 | t.is(error.exitCode, 1); 454 | } 455 | }); 456 | 457 | test('error on configuration file not found', async t => { 458 | try { 459 | await spawn('../markdownlint.js', ['--config', 'non-existent-file-path.yaml', 'correct.md']); 460 | } catch (error) { 461 | t.is(error.stdout, ''); 462 | t.regex(error.stderr, /Cannot read or parse config file 'non-existent-file-path\.yaml': ENOENT: no such file or directory, open '.*non-existent-file-path\.yaml'/); 463 | t.is(error.exitCode, 4); 464 | } 465 | }); 466 | 467 | test('error on malformed YAML configuration file', async t => { 468 | try { 469 | await spawn('../markdownlint.js', ['--config', 'malformed-config.yaml', 'correct.md']); 470 | } catch (error) { 471 | t.is(error.stdout, ''); 472 | t.regex(error.stderr, /Cannot read or parse config file 'malformed-config.yaml': Unable to parse 'malformed-config.yaml'; Parser 0:/); 473 | t.is(error.exitCode, 4); 474 | } 475 | }); 476 | 477 | function getCwdConfigFileTest(extension) { 478 | return async t => { 479 | try { 480 | await spawn(path.resolve('..', 'markdownlint.js'), ['.'], { 481 | cwd: path.join(__dirname, 'config-files', extension) 482 | }); 483 | t.fail(); 484 | } catch (error) { 485 | const expected = ["heading-dollar.md:1:10 MD026/no-trailing-punctuation Trailing punctuation in heading [Punctuation: '$']"].join('\n'); 486 | t.is(error.stdout, ''); 487 | t.is(error.stderr, expected); 488 | t.is(error.exitCode, 1); 489 | } 490 | }; 491 | } 492 | 493 | test('.markdownlint.jsonc in cwd is used automatically', getCwdConfigFileTest('jsonc')); 494 | 495 | test('.markdownlint.json in cwd is used automatically', getCwdConfigFileTest('json')); 496 | 497 | test('.markdownlint.yaml in cwd is used automatically', getCwdConfigFileTest('yaml')); 498 | 499 | test('.markdownlint.yml in cwd is used automatically', getCwdConfigFileTest('yml')); 500 | 501 | test('.markdownlint.jsonc in cwd is used instead of .markdownlint.json or .markdownlint.yaml or .markdownlint.yml', getCwdConfigFileTest('jsonc-json-yaml-yml')); 502 | 503 | test('.markdownlint.json in cwd is used instead of .markdownlint.yaml or .markdownlint.yml', getCwdConfigFileTest('json-yaml-yml')); 504 | 505 | test('.markdownlint.yaml in cwd is used instead of .markdownlint.yml', getCwdConfigFileTest('yaml-yml')); 506 | 507 | test('.markdownlint.json with JavaScript-style comments is handled', getCwdConfigFileTest('json-c')); 508 | 509 | test('invalid JSON Pointer', async t => { 510 | try { 511 | await spawn('../markdownlint.js', ['--config', 'nested-config.json', '--configPointer', 'INVALID', '**/*.md']); 512 | t.fail(); 513 | } catch (error) { 514 | t.is(error.stdout, ''); 515 | t.regex(error.stderr, /Invalid JSON pointer\./); 516 | t.is(error.exitCode, 4); 517 | } 518 | }); 519 | 520 | test('empty JSON Pointer', async t => { 521 | try { 522 | await spawn('../markdownlint.js', ['--config', 'nested-config.json', '--configPointer', '/EMPTY', 'incorrect.md']); 523 | t.fail(); 524 | } catch (error) { 525 | t.is(error.stdout, ''); 526 | t.is(error.stderr.match(errorPattern).length, 7); 527 | t.is(error.exitCode, 1); 528 | } 529 | }); 530 | 531 | test('valid JSON Pointer with JSON configuration', async t => { 532 | try { 533 | await spawn('../markdownlint.js', ['--config', 'nested-config.json', '--configPointer', '/key', 'incorrect.md']); 534 | t.fail(); 535 | } catch (error) { 536 | t.is(error.stdout, ''); 537 | t.is(error.stderr.match(errorPattern).length, 1); 538 | t.is(error.exitCode, 1); 539 | } 540 | }); 541 | 542 | test('valid JSON Pointer with TOML configuration', async t => { 543 | try { 544 | await spawn('../markdownlint.js', ['--config', 'nested-config.toml', '--configPointer', '/key/subkey', 'incorrect.md']); 545 | t.fail(); 546 | } catch (error) { 547 | t.is(error.stdout, ''); 548 | t.is(error.stderr.match(errorPattern).length, 3); 549 | t.is(error.exitCode, 1); 550 | } 551 | }); 552 | 553 | test('Custom rule from single file loaded', async t => { 554 | try { 555 | const stdin = {string: '# Input\n'}; 556 | await spawn('../markdownlint.js', ['--rules', 'custom-rules/files/test-rule-1.cjs', '--stdin'], {stdin}); 557 | t.fail(); 558 | } catch (error) { 559 | const expected = ['stdin:1 test-rule-1 Test rule broken'].join('\n'); 560 | t.is(error.stdout, ''); 561 | t.is(error.stderr, expected); 562 | t.is(error.exitCode, 1); 563 | } 564 | }); 565 | 566 | test('Multiple custom rules from single file loaded', async t => { 567 | try { 568 | const stdin = {string: '# Input\n'}; 569 | await spawn('../markdownlint.js', ['--rules', 'custom-rules/files/test-rule-3-4.cjs', '--stdin'], {stdin}); 570 | t.fail(); 571 | } catch (error) { 572 | const expected = ['stdin:1 test-rule-3 Test rule 3 broken', 'stdin:1 test-rule-4 Test rule 4 broken'].join('\n'); 573 | t.is(error.stdout, ''); 574 | t.is(error.stderr, expected); 575 | t.is(error.exitCode, 1); 576 | } 577 | }); 578 | 579 | test('Custom rules from directory loaded', async t => { 580 | try { 581 | const stdin = {string: '# Input\n'}; 582 | await spawn('../markdownlint.js', ['--rules', 'custom-rules/files', '--stdin'], {stdin}); 583 | t.fail(); 584 | } catch (error) { 585 | const expected = ['stdin:1 test-rule-1 Test rule broken', 'stdin:1 test-rule-2 Test rule 2 broken', 'stdin:1 test-rule-3 Test rule 3 broken', 'stdin:1 test-rule-4 Test rule 4 broken'].join('\n'); 586 | t.is(error.stdout, ''); 587 | t.is(error.stderr, expected); 588 | t.is(error.exitCode, 1); 589 | } 590 | }); 591 | 592 | test('Custom rules from glob loaded', async t => { 593 | try { 594 | const stdin = {string: '# Input\n'}; 595 | await spawn('../markdownlint.js', ['--rules', 'custom-rules/files/**/*.cjs', '--stdin'], {stdin}); 596 | t.fail(); 597 | } catch (error) { 598 | const expected = ['stdin:1 test-rule-1 Test rule broken', 'stdin:1 test-rule-2 Test rule 2 broken', 'stdin:1 test-rule-3 Test rule 3 broken', 'stdin:1 test-rule-4 Test rule 4 broken'].join('\n'); 599 | t.is(error.stdout, ''); 600 | t.is(error.stderr, expected); 601 | t.is(error.exitCode, 1); 602 | } 603 | }); 604 | 605 | test('Custom rule from node_modules package loaded', async t => { 606 | try { 607 | const stdin = {string: '# Input\n'}; 608 | await spawn('../markdownlint.js', ['--rules', 'markdownlint-cli-local-test-rule', '--stdin'], {stdin}); 609 | t.fail(); 610 | } catch (error) { 611 | const expected = ['stdin:1 markdownlint-cli-local-test-rule Test rule package broken'].join('\n'); 612 | t.is(error.stdout, ''); 613 | t.is(error.stderr, expected); 614 | t.is(error.exitCode, 1); 615 | } 616 | }); 617 | 618 | test('Custom rule from node_modules package loaded relative to cwd', async t => { 619 | try { 620 | const stdin = {string: '# Input\n'}; 621 | await spawn(path.resolve('..', 'markdownlint.js'), ['--rules', 'markdownlint-cli-local-test-rule', '--stdin'], { 622 | stdin, 623 | cwd: path.join(__dirname, 'custom-rules', 'relative-to-cwd') 624 | }); 625 | t.fail(); 626 | } catch (error) { 627 | const expected = ['stdin:1 markdownlint-cli-local-test-rule Test rule package relative to cwd broken'].join('\n'); 628 | t.is(error.stdout, ''); 629 | t.is(error.stderr, expected); 630 | t.is(error.exitCode, 1); 631 | } 632 | }); 633 | 634 | test('Custom rule with scoped package name via --rules', async t => { 635 | try { 636 | await spawn(path.resolve('..', 'markdownlint.js'), ['--rules', '@scoped/custom-rule', 'scoped-test.md'], { 637 | cwd: path.join(__dirname, 'custom-rules', 'scoped-package') 638 | }); 639 | t.fail(); 640 | } catch (error) { 641 | const expected = ['scoped-test.md:1 scoped-rule Scoped rule'].join('\n'); 642 | t.is(error.stdout, ''); 643 | t.is(error.stderr, expected); 644 | t.is(error.exitCode, 1); 645 | } 646 | }); 647 | 648 | test('Custom rule from package loaded', async t => { 649 | try { 650 | const stdin = {string: '# Input\n'}; 651 | await spawn('../markdownlint.js', ['--rules', './custom-rules/markdownlint-cli-local-test-rule', '--stdin'], {stdin}); 652 | t.fail(); 653 | } catch (error) { 654 | const expected = ['stdin:1 markdownlint-cli-local-test-rule Test rule package broken'].join('\n'); 655 | t.is(error.stdout, ''); 656 | t.is(error.stderr, expected); 657 | t.is(error.exitCode, 1); 658 | } 659 | }); 660 | 661 | test('Custom rule from several packages loaded', async t => { 662 | try { 663 | const stdin = {string: '# Input\n'}; 664 | await spawn('../markdownlint.js', ['--rules', './custom-rules/markdownlint-cli-local-test-rule', '--rules', './custom-rules/markdownlint-cli-local-test-rule-other', '--stdin'], {stdin}); 665 | t.fail(); 666 | } catch (error) { 667 | const expected = ['stdin:1 markdownlint-cli-local-test-rule Test rule package broken', 'stdin:1 markdownlint-cli-local-test-rule-other Test rule package other broken'].join('\n'); 668 | t.is(error.stdout, ''); 669 | t.is(error.stderr, expected); 670 | t.is(error.exitCode, 1); 671 | } 672 | }); 673 | 674 | test('Invalid custom rule name reports error', async t => { 675 | try { 676 | const stdin = {string: '# Input\n'}; 677 | await spawn('../markdownlint.js', ['--rules', 'markdownlint-cli-local-test-rule', '--rules', 'invalid-package', '--stdin'], {stdin}); 678 | t.fail(); 679 | } catch (error) { 680 | const expected = ['Cannot load custom rule invalid-package: No such rule'].join('\n'); 681 | t.is(error.stdout, ''); 682 | t.is(error.stderr, expected); 683 | t.is(error.exitCode, 3); 684 | } 685 | }); 686 | 687 | test('fixing errors in a file yields fewer errors', async t => { 688 | const fixFileA = 'incorrect.a.mdf'; 689 | try { 690 | fs.copyFileSync('incorrect.md', fixFileA); 691 | await spawn('../markdownlint.js', ['--fix', '--config', 'test-config.json', fixFileA]); 692 | t.fail(); 693 | } catch (error) { 694 | const expected = [fixFileA + ':1 MD041/first-line-heading/first-line-h1 First line in a file should be a top-level heading [Context: "## header 2"]'].join('\n'); 695 | t.is(error.stdout, ''); 696 | t.is(error.stderr, expected); 697 | t.is(error.exitCode, 1); 698 | fs.unlinkSync(fixFileA); 699 | } 700 | }); 701 | 702 | test('fixing errors in a file with absolute path yields fewer errors', async t => { 703 | const fixFileB = 'incorrect.b.mdf'; 704 | try { 705 | fs.copyFileSync('incorrect.md', fixFileB); 706 | await spawn('../markdownlint.js', ['--fix', '--config', 'test-config.json', path.resolve(fixFileB)]); 707 | t.fail(); 708 | } catch (error) { 709 | t.is(error.stdout, ''); 710 | t.is(error.stderr.match(errorPattern).length, 1); 711 | t.is(error.exitCode, 1); 712 | fs.unlinkSync(fixFileB); 713 | } 714 | }); 715 | 716 | test('fixing errors with a glob yields fewer errors', async t => { 717 | const fixFileC = 'incorrect.c.mdf'; 718 | const fixSubFileC = 'subdir-incorrect/incorrect.c.mdf'; 719 | const fixFileGlob = '**/*.c.mdf'; 720 | try { 721 | fs.copyFileSync('incorrect.md', fixFileC); 722 | fs.copyFileSync('subdir-incorrect/incorrect.md', fixSubFileC); 723 | await spawn('../markdownlint.js', ['--fix', '--config', 'test-config.json', fixFileGlob]); 724 | t.fail(); 725 | } catch (error) { 726 | t.is(error.stdout, ''); 727 | t.is(error.stderr.match(errorPattern).length, 2); 728 | t.is(error.exitCode, 1); 729 | fs.unlinkSync(fixFileC); 730 | fs.unlinkSync(fixSubFileC); 731 | } 732 | }); 733 | 734 | test('.markdownlintignore is applied correctly', async t => { 735 | try { 736 | await spawn(path.resolve('..', 'markdownlint.js'), ['.'], { 737 | cwd: path.join(__dirname, 'markdownlintignore') 738 | }); 739 | t.fail(); 740 | } catch (error) { 741 | const expected = ['incorrect.md:1:8 MD047/single-trailing-newline Files should end with a single newline character', 'subdir/incorrect.markdown:1:8 MD047/single-trailing-newline Files should end with a single newline character'].join('\n'); 742 | t.is(error.stdout, ''); 743 | t.is(error.stderr.replaceAll('\\', '/'), expected); 744 | t.is(error.exitCode, 1); 745 | } 746 | }); 747 | 748 | test('.markdownlintignore works with semi-absolute paths', async t => { 749 | try { 750 | await spawn(path.resolve('..', 'markdownlint.js'), ['./incorrect.md'], { 751 | cwd: path.join(__dirname, 'markdownlintignore') 752 | }); 753 | t.fail(); 754 | } catch (error) { 755 | const expected = ['./incorrect.md:1:8 MD047/single-trailing-newline Files should end with a single newline character'].join('\n'); 756 | t.is(error.stdout, ''); 757 | t.is(error.stderr, expected); 758 | t.is(error.exitCode, 1); 759 | } 760 | }); 761 | 762 | test('--ignore-path works with .markdownlintignore', async t => { 763 | try { 764 | await spawn(path.resolve('..', 'markdownlint.js'), ['--ignore-path', '.markdownlintignore', '.'], { 765 | cwd: path.join(__dirname, 'markdownlintignore') 766 | }); 767 | t.fail(); 768 | } catch (error) { 769 | const expected = ['incorrect.md:1:8 MD047/single-trailing-newline Files should end with a single newline character', 'subdir/incorrect.markdown:1:8 MD047/single-trailing-newline Files should end with a single newline character'].join('\n'); 770 | t.is(error.stdout, ''); 771 | t.is(error.stderr.replaceAll('\\', '/'), expected); 772 | t.is(error.exitCode, 1); 773 | } 774 | }); 775 | 776 | test('--ignore-path works with .ignorefile', async t => { 777 | try { 778 | await spawn(path.resolve('..', 'markdownlint.js'), ['--ignore-path', '.ignorefile', '.'], { 779 | cwd: path.join(__dirname, 'markdownlintignore') 780 | }); 781 | t.fail(); 782 | } catch (error) { 783 | const expected = ['incorrect.markdown:1:8 MD047/single-trailing-newline Files should end with a single newline character'].join('\n'); 784 | t.is(error.stdout, ''); 785 | t.is(error.stderr, expected); 786 | t.is(error.exitCode, 1); 787 | } 788 | }); 789 | 790 | test('--ignore-path fails for missing file', async t => { 791 | const missingFile = 'missing-file'; 792 | try { 793 | await spawn(path.resolve('..', 'markdownlint.js'), ['--ignore-path', missingFile, '.'], { 794 | cwd: path.join(__dirname, 'markdownlintignore') 795 | }); 796 | t.fail(); 797 | } catch (error) { 798 | t.is(error.stdout, ''); 799 | t.regex(error.stderr, /enoent.*no such file or directory/i); 800 | t.is(error.exitCode, 1); 801 | } 802 | }); 803 | 804 | test('Linter text file --output must end with EOF newline', async t => { 805 | const output = '../outputE.txt'; 806 | const endOfLine = new RegExp(os.EOL + '$'); 807 | try { 808 | await spawn('../markdownlint.js', ['--config', 'test-config.json', '--output', output, 'incorrect.md']); 809 | t.fail(); 810 | } catch { 811 | t.regex(fs.readFileSync(output, 'utf8'), endOfLine); 812 | fs.unlinkSync(output); 813 | } 814 | }); 815 | 816 | test('--dot option to include folders/files with a dot', async t => { 817 | try { 818 | await spawn('../markdownlint.js', ['--config', 'test-config.json', '--dot', '**/incorrect-dot.md', '**/.file-with-dot.md', '**/correct.md']); 819 | t.fail(); 820 | } catch (error) { 821 | t.is(error.stdout, ''); 822 | t.is(error.stderr.match(errorPattern).length, 11); 823 | t.is(error.exitCode, 1); 824 | } 825 | }); 826 | 827 | test('without --dot option exclude folders/files with a dot', async t => { 828 | const result = await spawn('../markdownlint.js', ['--config', 'test-config.json', '**/incorrect-dot.md', '**/.file-with-dot.md', '**/correct.md']); 829 | t.is(result.stdout, ''); 830 | t.is(result.stderr, ''); 831 | t.is(result.exitCode, 0); 832 | }); 833 | 834 | test('with --quiet option does not print to stdout or stderr', async t => { 835 | try { 836 | await spawn('../markdownlint.js', ['--quiet', '--config', 'test-config.json', 'incorrect.md']); 837 | t.fail(); 838 | } catch (error) { 839 | t.is(error.stdout, ''); 840 | t.is(error.stderr, ''); 841 | t.is(error.exitCode, 1); 842 | } 843 | }); 844 | 845 | test('--enable flag', async t => { 846 | try { 847 | await spawn('../markdownlint.js', ['--enable', 'MD041', '--config', 'default-false-config.yml', 'incorrect.md']); 848 | t.fail(); 849 | } catch (error) { 850 | t.is(error.stdout, ''); 851 | t.is(error.stderr, 'incorrect.md:1 MD041/first-line-heading/first-line-h1 First line in a file should be a top-level heading [Context: "## header 2"]'); 852 | t.is(error.exitCode, 1); 853 | } 854 | }); 855 | 856 | test('--enable flag does not modify already enabled rules', async t => { 857 | try { 858 | await spawn('../markdownlint.js', ['--enable', 'MD043', '--config', 'md043-config.yaml', 'correct.md']); 859 | t.fail(); 860 | } catch (error) { 861 | t.is(error.stdout, ''); 862 | t.is(error.stderr, 'correct.md:1 MD043/required-headings Required heading structure [Expected: # First; Actual: # header]'); 863 | t.is(error.exitCode, 1); 864 | } 865 | }); 866 | 867 | test('--enable flag accepts rule alias', async t => { 868 | try { 869 | await spawn('../markdownlint.js', ['--enable', 'first-line-heading', '--config', 'default-false-config.yml', 'incorrect.md']); 870 | t.fail(); 871 | } catch (error) { 872 | t.is(error.stdout, ''); 873 | t.is(error.stderr, 'incorrect.md:1 MD041/first-line-heading/first-line-h1 First line in a file should be a top-level heading [Context: "## header 2"]'); 874 | t.is(error.exitCode, 1); 875 | } 876 | }); 877 | 878 | test('--disable flag', async t => { 879 | const result = await spawn('../markdownlint.js', ['--disable', 'MD014', 'MD022', 'MD041', '--', 'incorrect.md']); 880 | 881 | t.is(result.stdout, ''); 882 | t.is(result.stderr, ''); 883 | t.is(result.exitCode, 0); 884 | 885 | try { 886 | await spawn('../markdownlint.js', ['--disable', 'MD014', 'MD014', 'MD022', '--', 'incorrect.md']); 887 | t.fail(); 888 | } catch (error) { 889 | t.is(error.stdout, ''); 890 | t.is(error.stderr, 'incorrect.md:1 MD041/first-line-heading/first-line-h1 First line in a file should be a top-level heading [Context: "## header 2"]'); 891 | t.is(error.exitCode, 1); 892 | } 893 | }); 894 | 895 | test('--disable flag overrides --enable flag', async t => { 896 | const result = await spawn('../markdownlint.js', ['--disable', 'MD041', '--enable', 'MD041', '--config', 'default-false-config.yml', 'incorrect.md']); 897 | t.is(result.stdout, ''); 898 | t.is(result.stderr, ''); 899 | t.is(result.exitCode, 0); 900 | }); 901 | 902 | test('configuration can be .js in the CommonJS workspace', async t => { 903 | const result = await spawn('../../../markdownlint.js', ['--config', '.markdownlint.js', 'test.md'], {cwd: './workspace/commonjs'}); 904 | t.is(result.stdout, ''); 905 | t.is(result.stderr, ''); 906 | t.is(result.exitCode, 0); 907 | }); 908 | 909 | test('configuration can be .cjs in the CommonJS workspace', async t => { 910 | const result = await spawn('../../../markdownlint.js', ['--config', '.markdownlint.cjs', 'test.md'], {cwd: './workspace/commonjs'}); 911 | t.is(result.stdout, ''); 912 | t.is(result.stderr, ''); 913 | t.is(result.exitCode, 0); 914 | }); 915 | 916 | test('configuration can be .cjs in the ESM (module) workspace', async t => { 917 | const result = await spawn('../../../markdownlint.js', ['--config', '.markdownlint.cjs', 'test.md'], {cwd: './workspace/module'}); 918 | t.is(result.stdout, ''); 919 | t.is(result.stderr, ''); 920 | t.is(result.exitCode, 0); 921 | }); 922 | -------------------------------------------------------------------------------- /test/workspace/commonjs/.markdownlint.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | 'line-length': false, 3 | }; 4 | -------------------------------------------------------------------------------- /test/workspace/commonjs/.markdownlint.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | 'line-length': false, 3 | }; 4 | -------------------------------------------------------------------------------- /test/workspace/commonjs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "commonjs" 3 | } 4 | -------------------------------------------------------------------------------- /test/workspace/commonjs/test.md: -------------------------------------------------------------------------------- 1 | # Test file 2 | 3 | Long line length which should not fail, because the line length rule was disabled in the config file. 4 | -------------------------------------------------------------------------------- /test/workspace/module/.markdownlint.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | 'line-length': false, 3 | }; 4 | -------------------------------------------------------------------------------- /test/workspace/module/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "module" 3 | } 4 | -------------------------------------------------------------------------------- /test/workspace/module/test.md: -------------------------------------------------------------------------------- 1 | # Test file 2 | 3 | Long line length which should not fail, because the line length rule was disabled in the config file. 4 | --------------------------------------------------------------------------------