How to create a custom lint rule for Markdown and MDX using remark and ESLint
Daniele Tortora
Posted on April 7, 2021
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
- Set up the project
- Set up remark
- The
no-invalid-gif
rule - Create the custom rule
- Rule arguments
- Rule implementation
- Import the rule in your remark config
- Apply the rule on the Markdown file
- Markdown to MDX
- ESlint MDX and Remark
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
Now we can start installing our dependencies.
npm install remark-lint remark-cli
-
remark-lint
: Core lint plugin -
remark-cli
: Command-line interface
We will also need 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 we’ll use.
For more info on configuration, see Configuring remark-lint
.
touch .remarkrc.js
// .remarkrc.js
module.exports = {
plugins: [],
};
Then, in our package.json
, let's add the following script, which will process all the markdown file within our project:
"scripts": {
"lint": "remark ."
}
Let's 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 haven't specified any plugins nor lint rule, no issues are found.
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)
Because the file extension .gif
in the image tag violates our rule.
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
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);
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);
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.
function noGifAllowed(tree, file, options) {}
-
tree
(required): mdast -
file
(required): virtual file -
options
(optional): additional information 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 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);
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],
};
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
The rule works, congratulations!
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)
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
Ouch! it seems our .mdx
file is not seen or parsed by remark
and the rule is not applied! Lets' take care of that.
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
- 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
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,
},
};
Okay, now it's time to update our package.json
with a new lint
script:
"scripts": {
"lint": "eslint . --ext md,mdx"
}
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
Congratulation!
Your custom rule has been correctly applied both to Markdown and MDX!
Posted on April 7, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
April 7, 2021