remark-lint/doc/create-a-custom-rule.md
2023-11-13 16:14:01 +01:00

291 lines
7.2 KiB
Markdown
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Create a custom `remark-lint` rule
This guide is part of [a step-by-step tutorial][tutorial], and will help you
getting started to create your first linting plugin for `remark`.
## Contents
* [Set up the project](#set-up-the-project)
* [Set up remark](#set-up-remark)
* [The `no-invalid-gif` rule](#the-no-invalid-gif-rule)
* [Create the custom rule](#create-the-custom-rule)
* [Rule arguments](#rule-arguments)
* [Rule implementation](#rule-implementation)
* [Import the rule in your remark config](#import-the-rule-in-your-remark-config)
* [Apply the rule on the Markdown file](#apply-the-rule-on-the-markdown-file)
## 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`:
```sh
mkdir my-custom-rule
cd my-custom-rule
npm init -y
```
Now we can start installing our dependencies:
```sh
npm install remark-lint remark-cli
```
* [`remark-lint`][remark-lint]
— core lint plugin
* [`remark-cli`][remark-cli]
— command line interface
We will also use some utilities:
```sh
npm install unified-lint-rule 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`][examples].
```sh
touch .remarkrc.js
```
```js
// .remarkrc.js
/**
* @typedef {import('unified').Preset} Preset
*/
/** @type {Preset} */
const preset = {plugins: []}
export default preset
```
Then, in our `package.json`, add the following script to process all the
markdown files in our project:
```json
"scripts": {
"lint": "remark ."
}
```
Lets create a `doc.md`, the markdown file we want to lint:
```sh
touch doc.md
```
…and copy/paste the following:
```markdown
## 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:
```sh
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:
```markdown
![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`.
```sh
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.
```js
// rules/no-gif-allowed.js
import {lintRule} from 'unified-lint-rule'
const remarkLintNoGifAllowed = lintRule(
'remark-lint:no-gif-allowed',
function (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:
```js
const remarkLintNoGifAllowed = lintRule(
'my-project-name:no-gif-allowed',
function () {}
)
// Or:
const remarkLintNoGifAllowed = lintRule(
'my-npm-published-package:no-gif-allowed',
function () {}
)
```
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:
```js
function rule(tree, file, options) {}
```
* `tree` (*required*): [mdast][]
* `file` (*required*): [virtual file][vfile]
* `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][unist-util] to inspect our trees nodes.
For this example, we will use [`unist-util-visit`][unist-util-visit] to
recursively inspect all the image nodes, and
[`unist-util-generated`][unist-util-generated] to ensure we are not inspecting
nodes that we have generated ourselves and do not belong to the `doc.md`.
```js
/**
* @typedef {import('mdast').Root} Root
*/
import {lintRule} from 'unified-lint-rule'
import {visit} from 'unist-util-visit'
const remarkLintNoGifAllowed = lintRule(
'remark-lint:no-gif-allowed',
/**
* @param {Root} tree
* Tree.
* @returns {undefined}
* Nothing.
*/
function (tree, file, options) {
visit(tree, 'image', function (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 = !node.url.endsWith('.gif')
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:
```js
// .remarkrc.js
/**
* @typedef {import('unified').Preset} Preset
*/
import remarkLintNoGifAllowed from './rules/no-gif-allowed.js'
/** @type {Preset} */
const preset = {plugins: [remarkLintNoGifAllowed]}
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:
```text
5:1-5:30 warning Invalid image file extensions. Please do not use gifs no-gif-allowed remark-lint
```
**Congratulations!
The rule works!**
[tutorial]: https://dev.to/floroz/how-to-create-a-custom-lint-rule-for-markdown-and-mdx-using-remark-and-eslint-2jim
[remark-lint]: https://github.com/remarkjs/remark-lint
[remark-cli]: https://github.com/remarkjs/remark/tree/main/packages/remark-cli
[examples]: https://github.com/remarkjs/remark-lint#examples
[mdast]: https://github.com/syntax-tree/mdast
[vfile]: https://github.com/vfile/vfile
[unist]: https://github.com/syntax-tree/unist
[unist-util]: https://github.com/syntax-tree/unist#utilities
[unist-util-visit]: https://github.com/syntax-tree/unist-util-visit
[unist-util-generated]: https://github.com/syntax-tree/unist-util-generated