remark-lint/doc/create-a-custom-rule.md
Titus 014fca79e0
Add improved docs
Closes GH-276.
2021-12-03 10:06:36 +01:00

7.3 KiB
Raw Blame History

Create a custom remark-lint rule

This guide is part of a step-by-step tutorial, and will help you getting started to create your first linting plugin for remark.

Contents

Set up the project

Create a new folder and enter it from your terminal. For this example I will be using Unix commands (macOS and Linux compatible). Then generate a package.json:

mkdir my-custom-rule
cd my-custom-rule
npm init -y

Now we can start installing our dependencies:

npm install remark-lint remark-cli

We will also use some utilities:

npm install unified-lint-rule unist-util-generated unist-util-visit

These will help us creating and managing our custom rules.

Set up remark

With everything installed, we can now create a .remarkrc.js that will contain the plugins well use.

For more info on configuration, see Examples in remark-lint.

touch .remarkrc.js
// .remarkrc.js
module.exports = {
  plugins: []
}

Then, in our package.json, add the following script to process all the markdown files in our project:

"scripts": {
  "lint": "remark ."
}

Lets create a doc.md, the markdown file we want to lint:

touch doc.md

…and copy/paste the following:

## Best pets! <3

Some funny images of our favorite pets

![a funny cat](funny-cat.gif)

![a lovely dog](lovely-dog.png)

At this point, we have a working remark configuration and a markdown file in the project.

If we run npm run lint we should expect to see in our terminal:

doc.md: no issues found

All good, the file has been processed, and because we havent specified any plugins nor lint rules, no issues are found.

The no-invalid-gif rule

Lets imagine we want to write a rule that checks whether a .gif file is used as an image. Given the content of our doc.md file declared above, we would expect an error or warning pointing to:

![a funny cat](funny-cat.gif)

Because the file extension .gif in the image violates our rule.

Create the custom rule

Lets create a new folder rules under the root directory, where we will place all of our custom rules, and create a new file in it named no-gif-allowed.js.

mkdir rules
cd rules
touch no-gif-allowed.js
cd .. # return to project root

Note: the name of folders and files, and where to place them within your project, is up to you.

In ./rules/no-gif-allowed.js, lets import unified-lint-rule.

We then export the result of calling rule by providing the namespace and rule name (remark-lint:no-gif-allowed) as the first argument, and our implementation of the rule (noGifAllowed) as the second argument.

// rules/no-gif-allowed.js
import {lintRule} from 'unified-lint-rule'

const remarkLintNoGifAllowed = lintRule(
  'remark-lint:no-gif-allowed',
  (tree, file, options) => {
    // Rule implementation
  }
)

export default remarkLintNoGifAllowed

Lets say you want all your custom rules to be defined as part of your project namespace. If your project was named my-project, then you can export your rule as:

const remarkLintNoGifAllowed = lintRule('my-project-name:no-gif-allowed', () => {})
// Or:
const remarkLintNoGifAllowed = lintRule('my-npm-published-package:no-gif-allowed', () => {})

This can help you when wanting to create a group of rules under the same namespace.

Rule arguments

Your rule function will receive three arguments:

(tree, file, options) => {}
  • tree (required): mdast
  • file (required): virtual file
  • options (optional): additional info passed to the rule by users

Rule implementation

Because we will be inspecting mdast, which is a markdown abstract syntax tree built upon unist, we can take advantage of the many existing unist utilities to inspect our trees nodes.

For this example, we will use unist-util-visit to recursively inspect all the image nodes, and unist-util-generated to ensure we are not inspecting nodes that we have generated ourselves and do not belong to the doc.md.

import {lintRule} from 'unified-lint-rule'
import {visit} from 'unist-visit-util'
import {generated} from 'unist-util-generated'

function isValidNode(node) {
  // Here we check whether the given node violates our rule.
  // Implementation details are not relevant to the scope of this example.
  // This is an overly simplified solution for demonstration purposes
  if (node.url && typeof node.url === 'string') {
    return !node.url.endsWith('.gif')
  }
}

const remarkLintNoGifAllowed = lintRule(
  'remark-lint:no-gif-allowed',
  (tree, file, options) => {
    visit(tree, 'image', (node) => {
      if (!generated(node)) {
        // This is an extremely simplified example of how to structure
        // the logic to check whether a node violates your rule.
        // You have complete freedom over how to visit/inspect the tree,
        // and on how to implement the validation logic for your node.
        const isValid = isValidNode(node)

        if (!isValid) {
          // Remember to provide the node as second argument to the message,
          // in order to obtain the position and column where the violation occurred.
          file.message(
            'Invalid image file extensions. Please do not use gifs',
            node
          )
        }
      }
    })
  }
)

export default remarkLintNoGifAllowed

Import the rule in your remark config

Now that our custom rule is defined and ready to be used we need to add it to our remark configuration.

You can do that by importing your rule and adding it in plugins array:

// .remarkrc.js
import remarkLintNoGifAllowed from './rules/no-gif-allowed.js'

const plugins = {
  plugins: [remarkLintNoGifAllowed]
}

const preset = {plugins}

export default preset

Apply the rule on the Markdown file

If you run npm run lint, you should see the following message in the terminal:

5:1-5:30  warning  Invalid image file extensions. Please do not use gifs  no-gif-allowed  remark-lint

Congratulations! The rule works!