Skip to content

Commit e14c358

Browse files
committed
Add a “create a remark plugin” guide
1 parent 61e6149 commit e14c358

File tree

3 files changed

+623
-0
lines changed

3 files changed

+623
-0
lines changed

dictionary.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ performant
1212
plugable
1313
programmatically
1414
readme
15+
shortcodes
16+
shortcode
1517
stringify
1618
syntaxes
1719
whitespace
@@ -30,6 +32,7 @@ MDX
3032
MacBook
3133
Otander
3234
Preact
35+
gemoji
3336
mdast
3437
nlcst
3538
npm

doc/learn/create-a-remark-plugin.md

Lines changed: 277 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,277 @@
1+
---
2+
authorGithub: wooorm
3+
authorTwitter: wooorm
4+
author: Titus Wormer
5+
description: Guide that shows how to create a remark plugin
6+
group: guide
7+
modified: 2024-08-13
8+
published: 2024-08-13
9+
tags:
10+
- mdast
11+
- plugin
12+
- remark
13+
title: Create a remark plugin
14+
---
15+
16+
## Create a remark plugin
17+
18+
This guide shows how to create a plugin for remark that turns emoji
19+
shortcodes ([gemoji][], such as `:+1:`) into Unicode emoji (`👍`).
20+
It looks for a regex in the text and replaces it.
21+
22+
> Stuck?
23+
> Have an idea for another guide?
24+
> See [`support.md`][support].
25+
26+
### Contents
27+
28+
* [Case](#case)
29+
* [Setting up](#setting-up)
30+
* [Plugin](#plugin)
31+
32+
### Case
33+
34+
Before we start, let’s first outline what we want to make.
35+
Say we have the following text file:
36+
37+
```markdown
38+
Look, the moon :new_moon_with_face:
39+
```
40+
41+
And we’d like to turn that into:
42+
43+
```markdown
44+
Look, the moon 🌚
45+
```
46+
47+
In the next step we’ll write the code to use our plugin.
48+
49+
### Setting up
50+
51+
Let’s set up a project.
52+
Create a folder, `example`, enter it, and initialize a new project:
53+
54+
```sh
55+
mkdir example
56+
cd example
57+
npm init -y
58+
```
59+
60+
Then make sure the project is a module, so that `import` and `export` work,
61+
by changing `package.json`:
62+
63+
```diff
64+
--- a/package.json
65+
+++ b/package.json
66+
@@ -1,6 +1,7 @@
67+
{
68+
"name": "example",
69+
"version": "1.0.0",
70+
+ "type": "module",
71+
"main": "index.js",
72+
"scripts": {
73+
"test": "echo \"Error: no test specified\" && exit 1"
74+
```
75+
76+
Make sure `input.md` exists with:
77+
78+
```markdown
79+
Look, the moon :new_moon_with_face:
80+
```
81+
82+
Now, let’s create an `example.js` file that will process our file and report
83+
any found problems.
84+
85+
```js twoslash
86+
// @filename: plugin.d.ts
87+
import type {Root} from 'mdast'
88+
export default function remarkGemoji(): (tree: Root) => undefined;
89+
// @filename: example.js
90+
/// <reference types="node" />
91+
// ---cut---
92+
import fs from 'node:fs/promises'
93+
import {remark} from 'remark'
94+
import remarkGemoji from './plugin.js'
95+
96+
const document = await fs.readFile('input.md', 'utf8')
97+
98+
const file = await remark().use(remarkGemoji).process(document)
99+
100+
await fs.writeFile('output.md', String(file))
101+
```
102+
103+
> Don’t forget to `npm install remark`!
104+
105+
If you read the guide on [using unified][use],
106+
you’ll see some familiar statements.
107+
First,
108+
we load dependencies,
109+
then we read the file in.
110+
We process that file with the plugin we’ll create next and finally we write
111+
it out again.
112+
113+
Note that we directly depend on `remark`.
114+
This is a package that exposes a `unified` processor, and comes with the
115+
markdown parser and markdown compiler attached.
116+
117+
Now we’ve got everything set up except for the plugin itself.
118+
We’ll do that in the next section.
119+
120+
### Plugin
121+
122+
We’ll need a plugin, and for our case also a transform.
123+
Let’s create them in our plugin file `plugin.js`:
124+
125+
```js twoslash
126+
/**
127+
* @import {Root} from 'mdast'
128+
*/
129+
130+
/**
131+
* Turn gemoji shortcodes (`:+1:`) into emoji (`👍`).
132+
*
133+
* @returns
134+
* Transform.
135+
*/
136+
export default function remarkGemoji() {
137+
/**
138+
* @param {Root} tree
139+
* @return {undefined}
140+
*/
141+
return function (tree) {
142+
}
143+
}
144+
```
145+
146+
That’s how most plugins start.
147+
A function that returns another function.
148+
149+
For this use case,
150+
we could walk the tree and replace nodes with
151+
[`unist-util-visit`][visit],
152+
which is how many plugins work.
153+
But a different utility is even simpler:
154+
[`mdast-util-find-and-replace`][find-and-replace].
155+
It looks for a regex and lets you then replace that match.
156+
157+
Let’s add that.
158+
159+
```diff
160+
--- a/plugin.js
161+
+++ b/plugin.js
162+
@@ -2,6 +2,8 @@
163+
* @import {Root} from 'mdast'
164+
*/
165+
166+
+import {findAndReplace} from 'mdast-util-find-and-replace'
167+
+
168+
/**
169+
* Turn gemoji shortcodes (`:+1:`) into emoji (`👍`).
170+
*
171+
@@ -14,5 +16,16 @@ export default function remarkGemoji() {
172+
* @return {undefined}
173+
*/
174+
return function (tree) {
175+
+ findAndReplace(tree, [
176+
+ /:(\+1|[-\w]+):/g,
177+
+ /**
178+
+ * @param {string} _
179+
+ * @param {string} $1
180+
+ * @return {undefined}
181+
+ */
182+
+ function (_, $1) {
183+
+ console.log(arguments)
184+
+ }
185+
+ ])
186+
}
187+
}
188+
```
189+
190+
> Don’t forget to `npm install mdast-util-find-and-replace`!
191+
192+
If we now run our example with Node.js,
193+
we’ll see that `console.log` is called:
194+
195+
```sh
196+
node example.js
197+
```
198+
199+
```txt
200+
[Arguments] {
201+
'0': ':new_moon_with_face:',
202+
'1': 'new_moon_with_face',
203+
'2': {
204+
index: 15,
205+
input: 'Look, the moon :new_moon_with_face:',
206+
stack: [ [Object], [Object], [Object] ]
207+
}
208+
}
209+
```
210+
211+
This output shows that the regular expression matches the emoji shortcode.
212+
The second argument is the name of the emoji.
213+
That’s what we want.
214+
215+
We can look that name up to find the corresponding Unicode emoji.
216+
We can use the [`gemoji`][gemoji] package for that.
217+
It exposes a `nameToEmoji` record.
218+
219+
```diff
220+
--- a/plugin.js
221+
+++ b/plugin.js
222+
@@ -2,6 +2,7 @@
223+
* @import {Root} from 'mdast'
224+
*/
225+
226+
+import {nameToEmoji} from 'gemoji'
227+
import {findAndReplace} from 'mdast-util-find-and-replace'
228+
229+
/**
230+
@@ -21,10 +22,10 @@ export default function remarkGemoji() {
231+
/**
232+
* @param {string} _
233+
* @param {string} $1
234+
- * @return {undefined}
235+
+ * @return {string | false}
236+
*/
237+
function (_, $1) {
238+
- console.log(arguments)
239+
+ return Object.hasOwn(nameToEmoji, $1) ? nameToEmoji[$1] : false
240+
}
241+
])
242+
}
243+
```
244+
245+
> Don’t forget to `npm install gemoji`!
246+
247+
If we now run our example again with Node…
248+
249+
```sh
250+
node example.js
251+
```
252+
253+
…and open `output.md`,
254+
we’ll see that the shortcode is replaced with the emoji!
255+
256+
```markdown
257+
Look, the moon 🌚
258+
```
259+
260+
That’s it!
261+
262+
If you haven’t already, check out the other articles in the
263+
[learn section][learn]!
264+
265+
<!--Definitions-->
266+
267+
[support]: https://github.com/unifiedjs/.github/blob/main/support.md
268+
269+
[find-and-replace]: https://github.com/syntax-tree/mdast-util-find-and-replace
270+
271+
[gemoji]: https://github.com/wooorm/gemoji/blob/main/support.md
272+
273+
[visit]: https://github.com/syntax-tree/unist-util-visit
274+
275+
[learn]: /learn/
276+
277+
[use]: /learn/guide/using-unified/

0 commit comments

Comments
 (0)