DEV Community

Cover image for How to create a custom lint rule for Markdown and MDX using remark and ESLint
Daniele Tortora
Daniele Tortora

Posted on

How to create a custom lint rule for Markdown and MDX using remark and ESLint

Everyone loves Markdown. It's an exceptional tool to create text documents, blog posts, documentation articles, and it allows us to do so without having to worry about formatting, font styles, or having to set up HTML boilerplate.

There is a myriad of solutions out there to convert our Markdown into HTML pages or to scaffold entire websites out of our documents.

In the last years, modern web development architectures based on client-side JavaScript, reusable APIs and prebuilt Markup (JAMstack), and new web frameworks (Gatsby, Gridsome or Next.js), have gained increased popularity amongst developers, and even allowed us to start using JSX within our Markdown (MDX).

As these solutions scale, and more content writers and developers start contributing to these documents, teams are encouraged to adopt linting programs to shape best practices around markdown and MDX, and enforcing styles and conventions.

In this article, we'll go through how to setup your own custom lint rule for a JavaScript project using Markdown and MDX, starting from scratch.

Lets' get started!

Contents

Fork this repository with the complete tutorial, if you don't want to start from scratch.

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).
Now we can generate our package.json

 mkdir my-custom-rule

 cd my-custom-rule

 npm init -y
Enter fullscreen mode Exit fullscreen mode

Now we can start installing our dependencies.

 npm install remark-lint remark-cli
Enter fullscreen mode Exit fullscreen mode

We will also need some utilities:

 npm install unified-lint-rule unist-util-generated unist-util-visit
Enter fullscreen mode Exit fullscreen mode

These will help us creating and managing our custom rules.

Back to Top

Set up remark

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

For more info on configuration, see Configuring remark-lint.

 touch .remarkrc.js
Enter fullscreen mode Exit fullscreen mode
// .remarkrc.js

module.exports = {
  plugins: [],
};
Enter fullscreen mode Exit fullscreen mode

Then, in our package.json, let's add the following script, which will process all the markdown file within our project:

"scripts": {
  "lint": "remark ."
}
Enter fullscreen mode Exit fullscreen mode

Let's create a doc.md, the markdown file we want to lint:

 touch doc.md
Enter fullscreen mode Exit fullscreen mode

...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)
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

All good, the file has been processed, and because we haven't specified any plugins nor lint rule, no issues are found.

Back to Top

The no-invalid-gif rule

Let’s 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)
Enter fullscreen mode Exit fullscreen mode

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

Back to Top

Create the custom rule

Let's 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
Enter fullscreen mode Exit fullscreen mode

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, let's 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

var rule = require("unified-lint-rule");
function noGifAllowed(tree, file, options) {
  // rule implementation
}
module.exports = rule("remark-lint:no-gif-allowed", noGifAllowed);
Enter fullscreen mode Exit fullscreen mode

Let's 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:

module.exports = rule("my-project-name:no-gif-allowed", noGifAllowed);
// or
module.exports = rule("my-npm-published-package:no-gif-allowed", noGifAllowed);
Enter fullscreen mode Exit fullscreen mode

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

Back to Top

Rule arguments

Your rule function will receive three arguments.

function noGifAllowed(tree, file, options) {}
Enter fullscreen mode Exit fullscreen mode
  • tree (required): mdast
  • file (required): virtual file
  • options (optional): additional information passed to the rule by users

Back to Top

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 tree’s 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.

const rule = require("unified-lint-rule");
const visit = require("unist-visit-util");
const generated = require("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");
  }
}
function noGifAllowed(tree, file, options) {
  visit(tree, "image", visitor);
  function visitor(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 extentions. Please do not use gifs`,
          node
        );
      }
    }
  }
}
module.exports = rule("remark-lint:no-gif-allowed", noGifAllowed);
Enter fullscreen mode Exit fullscreen mode

Back to Top

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
const noGifAllowed = require("./rules/no-gif-allowed.js");

module.exports = {
  plugins: [noGifAllowed],
};
Enter fullscreen mode Exit fullscreen mode

Back to Top

Apply the rule on the Markdown file

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

 5:1-5:30  warning  Invalid image file extentions. Please do not use gifs  no-gif-allowed  remark-lint
Enter fullscreen mode Exit fullscreen mode

The rule works, congratulations!

Back to Top

 Markdown to MDX

Hold on, we are now told that we need to start supporting into our project also MDX files, and that our rules must apply to those as well.

A new file is created in the project, doc.mdx,to start using our new ParagraphComponent inside MDX.

## Best pets! <3

<ParagraphComponent text="I am a new paragraph" />

Some funny images of our favourite pets

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

![a lovely dog](lovely-dog.png)
Enter fullscreen mode Exit fullscreen mode

Fine, we now run our npm run lint again and check the terminal output:

doc.md
  5:1-5:30  warning  Invalid image file extentions. Please do not use gifs  no-gif-allowed  remark-lint
Enter fullscreen mode Exit fullscreen mode

Ouch! it seems our .mdx file is not seen or parsed by remark and the rule is not applied! Lets' take care of that.

Back to Top

ESlint MDX and Remark

In order to correctly parse and lint MDX files, we will need a parser. A great solution for this is eslint-mdx, so let's install it.

  npm install eslint eslint-plugin-mdx
Enter fullscreen mode Exit fullscreen mode
  • ESLint: the most popular tool for linting JavaScript code.
  • ESLint MDX: an ESLint plugin/parser for MDX files.

We will need to create a ESLint config to pass the settings for MDX and configure the plugin.

Let's create a .eslintrc.js in the root of our project,

 touch .eslintrc.js
Enter fullscreen mode Exit fullscreen mode

extends the eslint-plugin-mdx settings, and enable the mdx/remark rule.

ESLint will use the MDX plugin to parse and process our markdown/mdx files, and will also pull in any remark configuration we have declared in our project.

module.exports = {
  extends: ["plugin:mdx/recommended"],
  parserOptions: {
    ecmaVersion: 2015,
  },
  settings: {
    // Integration with remark-lint plugins,
    // it will read remark's configuration automatically via .remarkrc.js
    "mdx/remark": true,
  },
};
Enter fullscreen mode Exit fullscreen mode

Okay, now it's time to update our package.json with a new lint script:

"scripts": {
  "lint": "eslint . --ext md,mdx"
}
Enter fullscreen mode Exit fullscreen mode

We are configuring ESLint to parse and process all the files in our project with either a .md or .mdx extension.

If we now run npm run lint we should see in the terminal:

 eslint . --ext md,mdx

doc.md
  5:1  warning  Invalid image file extentions. Please do not use gifs  remark-lint-no-gif-allowed

doc.mdx
  7:1  warning  Invalid image file extentions. Please do not use gifs  remark-lint-no-gif-allowed
Enter fullscreen mode Exit fullscreen mode

Congratulation!

Your custom rule has been correctly applied both to Markdown and MDX!

Back to Top

Top comments (2)

Collapse
 
dieman profile image
Alessandro

Nice post! Keep it up!

Collapse
 
floroz profile image
Daniele Tortora

Thank you Alessandro!