Part II: ESLint for Markdown

ESLint can be used to validate files other than JavaScript. Markdown is a very good example, because you can apply Prettier rules (eg: ensure consistent formatting) and linting rules (eg: validate that titles are not duplicated).

In this post I'm going to assume that you have ESLint+Prettier already working.

Linting Markdown

First, you need to install the plugin

npm install --save-dev eslint-plugin-md

Then, extend it from your .eslintrc config file. The plugin exposes two presets: one to use standalone and another one to use with Prettier. In this case we'll use the latter:

{
    "extends": [
        "plugin:prettier/recommended",
        "plugin:md/prettier"
    ]
}

And that's pretty much it for the basic setup. This should give you error when there are style or format issues in your Markdown files.

A thing worth mentioning is that since ESLInt 7.0 you don't have to specify the extensions anymore. Before 7.0 you had to run eslint --ext .js,.md to lint JavaScript and Markdown files. Since 7.0, ESLint will verify all extensions for which you have an override in .eslintrc. And because the preset plugin:md/prettier defines an override for *.md , you only need to run eslint to get linting for Markdown working.

Rule configuration

The plugin exposes a new rule called prettier/prettier, but it has severity "warning" by default. If you want to change it to "error", you have to:

{
  "overrides": [
    {
      "files": ["*.md"],
      "parser": "markdown-eslint-parser",
      "rules": {
        "prettier/prettier": [
          "error",
          {
            // Tells prettier to use `markdown` parser for .md files
            "parser": "markdown"
          }
        ]
      }
    }
  ]
}

The plugin also uses remark-lint, a linter specialized in Markdown files. By default it uses the preset markdown-style-guide. It exposes the results under a new eslint rule called md/remark.

Customize that rule is a bit tricky, as ESLint doesn't support composing rules configuration. Instead you have to manually compose the rule configuration:

{
  overrides: [
    files: [ '*.md' ],
    parser: 'markdown-eslint-parser',
    rules: {
        'md/remark': [
            'error',
            {
                plugins: [
                    // This is the original ruleset from `plugin:md/prettier`.
                    ...require( 'eslint-plugin-md' ).configs.prettier.rules[ 'md/remark' ][ 1 ].plugins,

                    // List of disabled rules form the preset
                    [ 'lint-maximum-heading-length', false ],
                    [ 'lint-no-duplicate-headings', false ],
                ],
            },
        ],
    },
  ]
}

Skipping rules

Another tricky bit is how to add support for disabling rules directly from Markdown files (i.e. the equivalent of //eslint-disable-next-line). It can be done by configuring the special rule message-control.

{
    overrides: [
        {
            files: [ '*.md' ],
            parser: 'markdown-eslint-parser',
            rules: {
                'prettier/prettier': [
                    'error',
                    {
                        // Tells prettier to use `markdown` parser for .md files
                        parser: 'markdown',
                    },
                ],
                'md/remark': [
                    'error',
                    {
                        plugins: [
                            // This is the original ruleset from `plugin:md/prettier`.
                            // We need to include it again because eslint doesn't compose overrides
                            ...require( 'eslint-plugin-md' ).configs.prettier.rules[ 'md/remark' ][ 1 ].plugins,

                            // List of disabled rules form the preset
                            [ 'lint-maximum-heading-length', false ],
                            [ 'lint-no-duplicate-headings', false ],

                            // This special plugin is used to allow the syntax <!--eslint ignore <rule>-->.
                            // It has to come last!
                            [ 'message-control', { name: 'eslint', source: 'remark-lint' } ],
                        ],
                    },
                ],
            },
        },
    ],

Linting code blocks

The above configuration gives you something really nice by default: it uses ESLint to lint fenced code blocks tagged as js or javascript.

The way it works is quite clever: it uses ESLint preprocessors to split a the Markdown into "virtual" files: one the Markdown minus the code blocks, and one file for each code block, using the tagged language as the extension. So, in the example above, ESLint sees a README.md without code blocks, and a README.md.js with the content of the code block. It lint each file individually and then the plugin aggregate the errors back so they make sense.

The names are not really important, they are only used to match the set of rules to apply. By default, a js code block will be linted with the same rules as any JavaScript file in our project. But probably you want to relax some rules, as code blocks are usually examples and not all rules make sense there.

To customize it, we can use an override:

{
    overrides: [
        {
            files: [ '*.md.js', '*.md.javascript' ],
            rules: {
                // These are ok for examples
                'no-console': 'off',
                'no-redeclare': 'off',
                'no-restricted-imports': 'off',
                'no-undef': 'off',
                'no-unused-vars': 'off',
            },
        },
    ],
}

Just make sure the override is in the right order: another override down the line that matches *.js could overwrite some of those rules.


I really hope you find this useful. In my opinion, linting Markdown files (both regular text and code blocks) is critical for a project, as documentation is usually the first point of contact for new contributors and users.

Usually the very first thing they see is your README.md, it is the first impression they get form your project. It is quite bad to see a README file with sloppy code examples or messed up heading. Similarly, having internal documentation with examples that don't follow the code style is a source of frustration for new contributors that like to copy+paste from the doc.