Create a remark plugin
This guide shows how to create a plugin for remark that turns emoji shortcodes (gemoji, such as :+1:
) into Unicode emoji (👍
). It looks for a regex in the text and replaces it.
Stuck? Have an idea for another guide? See
support.md
.
Contents
Case
Before we start, let’s first outline what we want to make. Say we have the following file:
Look, the moon :new_moon_with_face:
And we’d like to turn that into:
Look, the moon 🌚
In the next step we’ll write the code to use our plugin.
Setting up
Let’s set up a project. Create a folder, example
, enter it, and initialize a new project:
mkdir example
cd example
npm init -y
Then make sure the project is a module, so that import
and export
work, by changing package.json
:
--- a/package.json
+++ b/package.json
@@ -1,6 +1,7 @@
{
"name": "example",
"version": "1.0.0",
+ "type": "module",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
Make sure input.md
exists with:
Look, the moon :new_moon_with_face:
Now, let’s create an example.js
file that will process our file and report any found problems.
import fs from 'node:fs/promises'
import {remark} from 'remark'
import remarkGemoji from './plugin.js'
const document = await fs.readFile('input.md', 'utf8')
const file = await remark().use(remarkGemoji).process(document)
await fs.writeFile('output.md', String(file))
(alias) module "node:fs/promises"
import fs
(alias) const remark: Processor<Root, undefined, undefined, Root, string>
import remark
Create a new unified processor that already uses remark-parse
and remark-stringify
.
(alias) function remarkGemoji(): (tree: Root) => undefined
import remarkGemoji
const document: string
(alias) module "node:fs/promises"
import fs
function readFile(path: PathLike | fs.FileHandle, options: ({
encoding: BufferEncoding;
flag?: OpenMode | undefined;
} & EventEmitter<T extends EventMap<T> = DefaultEventMap>.Abortable) | BufferEncoding): Promise<string> (+2 overloads)
Asynchronously reads the entire contents of a file.
- @param path A path to a file. If a URL is provided, it must use the
file:
protocol. If aFileHandle
is provided, the underlying file will not be closed automatically. - @param options An object that may contain an optional flag. If a flag is not provided, it defaults to
'r'
.
const file: VFile
(alias) remark(): Processor<Root, undefined, undefined, Root, string>
import remark
Create a new unified processor that already uses remark-parse
and remark-stringify
.
(method) Processor<Root, undefined, undefined, Root, string>.use<[], Root, undefined>(plugin: Plugin<[], Root, undefined>, ...parameters: [] | [boolean]): Processor<Root, Root, undefined, Root, string> (+2 overloads)
Configure the processor to use a plugin, a list of usable values, or a preset.
If the processor is already using a plugin, the previous plugin configuration is changed based on the options that are passed in. In other words, the plugin is not added a second time.
Note:
use
cannot be called on frozen processors. Call the processor first to create a new unfrozen processor.
- @example There are many ways to pass plugins to
.use()
. This example gives an overview:import {unified} from 'unified' unified() // Plugin with options: .use(pluginA, {x: true, y: true}) // Passing the same plugin again merges configuration (to `{x: true, y: false, z: true}`): .use(pluginA, {y: false, z: true}) // Plugins: .use([pluginB, pluginC]) // Two plugins, the second with options: .use([pluginD, [pluginE, {}]]) // Preset with plugins and settings: .use({plugins: [pluginF, [pluginG, {}]], settings: {position: false}}) // Settings only: .use({settings: {position: false}})
- @template {Array} [Parameters=[]]
- @template {Node | string | undefined} [Input=undefined]
- @template [Output=Input]
- @overload
- @overload
- @overload
- @param value Usable value.
- @param parameters Parameters, when a plugin is given as a usable value.
- @returns Current processor.
(alias) function remarkGemoji(): (tree: Root) => undefined
import remarkGemoji
(method) Processor<Root, Root, undefined, Root, string>.process(file?: Compatible | undefined): Promise<VFile> (+1 overload)
Process the given file as configured on the processor.
Note:
process
freezes the processor if not already frozen.
Note:
process
performs the parse, run, and stringify phases.
- @overload
- @overload
- @param file File (optional); typically
string
orVFile
]; any value accepted asx
innew VFile(x)
. - @param done Callback (optional).
- @returns Nothing if
done
is given. Otherwise a promise, rejected with a fatal error or resolved with the processed file. The parsed, transformed, and compiled value is available atfile.value
(see note).Note: unified typically compiles by serializing: most compilers return
string
(orUint8Array
). Some compilers, such as the one configured withrehype-react
, return other values (in this case, a React tree). If you’re using a compiler that doesn’t serialize, expect different result values.To register custom results in TypeScript, add them to {@linkcode CompileResultMap}.
const document: string
(alias) module "node:fs/promises"
import fs
function writeFile(file: PathLike | fs.FileHandle, data: string | NodeJS.ArrayBufferView | Iterable<string | NodeJS.ArrayBufferView> | AsyncIterable<string | NodeJS.ArrayBufferView> | internal.Stream, options?: (ObjectEncodingOptions & {
mode?: Mode | undefined;
flag?: OpenMode | undefined;
flush?: boolean | undefined;
} & EventEmitter<T extends EventMap<...> = DefaultEventMap>.Abortable) | BufferEncoding | null): Promise<void>
Asynchronously writes data to a file, replacing the file if it already exists. data
can be a string, a buffer, an AsyncIterable, or an Iterable object.
The encoding
option is ignored if data
is a buffer.
If options
is a string, then it specifies the encoding.
The mode
option only affects the newly created file. See fs.open()
for more details.
Any specified FileHandle
has to support writing.
It is unsafe to use fsPromises.writeFile()
multiple times on the same file without waiting for the promise to be settled.
Similarly to fsPromises.readFile
- fsPromises.writeFile
is a convenience method that performs multiple write
calls internally to write the buffer passed to it. For performance sensitive code consider using fs.createWriteStream()
or filehandle.createWriteStream()
.
It is possible to use an AbortSignal
to cancel an fsPromises.writeFile()
. Cancelation is "best effort", and some amount of data is likely still to be written.
import { writeFile } from 'node:fs/promises';
import { Buffer } from 'node:buffer';
try {
const controller = new AbortController();
const { signal } = controller;
const data = new Uint8Array(Buffer.from('Hello Node.js'));
const promise = writeFile('message.txt', data, { signal });
// Abort the request before the promise settles.
controller.abort();
await promise;
} catch (err) {
// When a request is aborted - err is an AbortError
console.error(err);
}
Aborting an ongoing request does not abort individual operating system requests but rather the internal buffering fs.writeFile
performs.
- @since v10.0.0
- @param file filename or
FileHandle
- @return Fulfills with
undefined
upon success.
var String: StringConstructor
(value?: any) => string
Allows manipulation and formatting of text strings and determination and location of substrings within strings.
const file: VFile
Don’t forget to
npm install remark
!
If you read the guide on using unified, you’ll see some familiar statements. First, we load dependencies, then we read the file in. We process that file with the plugin we’ll create next and finally we write it out again.
Note that we directly depend on remark
. This is a package that exposes a unified
processor, and comes with the markdown parser and markdown compiler attached.
Now we’ve got everything set up except for the plugin itself. We’ll do that in the next section.
Plugin
We’ll need a plugin, and for our case also a transform. Let’s create them in our plugin file plugin.js
:
/**
* @import {Root} from 'mdast'
*/
/**
* Turn gemoji shortcodes (`:+1:`) into emoji (`👍`).
*
* @returns
* Transform.
*/
export default function remarkGemoji() {
/**
* @param {Root} tree
* @return {undefined}
*/
return function (tree) {
}
}
function remarkGemoji(): (tree: Root) => undefined
Turn gemoji shortcodes (:+1:
) into emoji (👍
).
- @returns Transform.
(parameter) tree: Root
- @param tree
That’s how most plugins start. A function that returns another function.
For this use case, we could walk the tree and replace nodes with unist-util-visit
, which is how many plugins work. But a different utility is even simpler: mdast-util-find-and-replace
. It looks for a regex and lets you then replace that match.
Let’s add that.
--- a/plugin.js
+++ b/plugin.js
@@ -2,6 +2,8 @@
* @import {Root} from 'mdast'
*/
+import {findAndReplace} from 'mdast-util-find-and-replace'
+
/**
* Turn gemoji shortcodes (`:+1:`) into emoji (`👍`).
*
@@ -14,5 +16,16 @@ export default function remarkGemoji() {
* @return {undefined}
*/
return function (tree) {
+ findAndReplace(tree, [
+ /:(\+1|[-\w]+):/g,
+ /**
+ * @param {string} _
+ * @param {string} $1
+ * @return {undefined}
+ */
+ function (_, $1) {
+ console.log(arguments)
+ }
+ ])
}
}
Don’t forget to
npm install mdast-util-find-and-replace
!
If we now run our example with Node.js, we’ll see that console.log
is called:
node example.js
[Arguments] {
'0': ':new_moon_with_face:',
'1': 'new_moon_with_face',
'2': {
index: 15,
input: 'Look, the moon :new_moon_with_face:',
stack: [ [Object], [Object], [Object] ]
}
}
This output shows that the regular expression matches the emoji shortcode. The second argument is the name of the emoji. That’s what we want.
We can look that name up to find the corresponding Unicode emoji. We can use the gemoji
package for that. It exposes a nameToEmoji
record.
--- a/plugin.js
+++ b/plugin.js
@@ -2,6 +2,7 @@
* @import {Root} from 'mdast'
*/
+import {nameToEmoji} from 'gemoji'
import {findAndReplace} from 'mdast-util-find-and-replace'
/**
@@ -21,10 +22,10 @@ export default function remarkGemoji() {
/**
* @param {string} _
* @param {string} $1
- * @return {undefined}
+ * @return {string | false}
*/
function (_, $1) {
- console.log(arguments)
+ return Object.hasOwn(nameToEmoji, $1) ? nameToEmoji[$1] : false
}
])
}
Don’t forget to
npm install gemoji
!
If we now run our example again with Node…
node example.js
…and open output.md
, we’ll see that the shortcode is replaced with the emoji!
Look, the moon 🌚
That’s it! For a complete version of this plugin, see remark-gemoji
.
If you haven’t already, check out the other articles in the learn section!