|
1 | 1 | --- |
| 2 | +archive: true |
2 | 3 | authorGithub: wooorm |
3 | 4 | authorTwitter: wooorm |
4 | 5 | author: Titus Wormer |
5 | | -description: Guide that shows how to create a (retext) plugin |
| 6 | +description: Guide that shows how to create a plugin |
6 | 7 | group: guide |
7 | | -modified: 2024-08-06 |
| 8 | +modified: 2024-08-13 |
8 | 9 | published: 2017-05-03 |
9 | 10 | tags: |
10 | 11 | - plugin |
11 | | - - retext |
12 | 12 | title: Create a plugin |
13 | 13 | --- |
14 | 14 |
|
15 | | -## Creating a plugin with unified |
16 | | - |
17 | | -This guide shows how to create a plugin for retext that checks the amount of |
18 | | -spaces between sentences. |
19 | | -The concepts here apply to the other syntaxes of unified as well. |
20 | | - |
21 | | -> Stuck? |
22 | | -> Have an idea for another guide? |
23 | | -> See [`support.md`][support]. |
24 | | -
|
25 | | -### Contents |
26 | | - |
27 | | -* [Plugin basics](#plugin-basics) |
28 | | -* [Case](#case) |
29 | | -* [Setting up](#setting-up) |
30 | | -* [Plugin](#plugin) |
31 | | -* [Further exercises](#further-exercises) |
32 | | - |
33 | | -### Plugin basics |
34 | | - |
35 | | -A unified plugin changes the way the applied-on processor works, in several |
36 | | -ways. |
37 | | -In this guide we’ll review how to inspect syntax trees. |
38 | | - |
39 | | -Plugins can contain two parts: an **attacher**, which is a function that is |
40 | | -called when someone calls `.use`, and a **transformer**, which is an optional |
41 | | -function called each time a file is processed with a syntax tree and a virtual |
42 | | -file. |
43 | | - |
44 | | -In this case, we want to check the syntax tree of each processed file, so we do |
45 | | -specify a transformer. |
46 | | - |
47 | | -Now you know the basics of plugins in unified. |
48 | | -On to our case! |
49 | | - |
50 | | -### Case |
51 | | - |
52 | | -Before we start, let’s first outline what we want to make. |
53 | | -Say we have the following text file: |
54 | | - |
55 | | -```markdown |
56 | | -One sentence. Two sentences. |
57 | | - |
58 | | -One sentence. Two sentences. |
59 | | -``` |
60 | | - |
61 | | -We want to get a warning for the second paragraph, saying that one space instead |
62 | | -of two spaces should be used. |
63 | | - |
64 | | -In the next step we’ll write the code to use our plugin. |
65 | | - |
66 | | -### Setting up |
67 | | - |
68 | | -Let’s set up a project. |
69 | | -Create a folder, `example`, enter it, and initialize a new project: |
70 | | - |
71 | | -```sh |
72 | | -mkdir example |
73 | | -cd example |
74 | | -npm init -y |
75 | | -``` |
76 | | - |
77 | | -Then make sure the project is a module, so that `import` and `export` work, |
78 | | -by changing `package.json`: |
79 | | - |
80 | | -```diff |
81 | | ---- a/package.json |
82 | | -+++ b/package.json |
83 | | -@@ -1,6 +1,7 @@ |
84 | | - { |
85 | | - "name": "example", |
86 | | - "version": "1.0.0", |
87 | | -+ "type": "module", |
88 | | - "main": "index.js", |
89 | | - "scripts": { |
90 | | - "test": "echo \"Error: no test specified\" && exit 1" |
91 | | -``` |
92 | | - |
93 | | -Make sure `example.md` exists with: |
94 | | - |
95 | | -```markdown |
96 | | -One sentence. Two sentences. |
97 | | - |
98 | | -One sentence. Two sentences. |
99 | | -``` |
100 | | - |
101 | | -Now, let’s create an `example.js` file that will process our text file and |
102 | | -report any found problems. |
103 | | - |
104 | | -```js twoslash |
105 | | -// @filename: plugin.d.ts |
106 | | -import type {Root} from 'nlcst' |
107 | | -import type {VFile} from 'vfile' |
108 | | -export default function retextSentenceSpacing(): (tree: Root, file: VFile) => undefined; |
109 | | -// @filename: example.js |
110 | | -/// <reference types="node" /> |
111 | | -// ---cut--- |
112 | | -import fs from 'node:fs/promises' |
113 | | -import {retext} from 'retext' |
114 | | -import {reporter} from 'vfile-reporter' |
115 | | -import retextSentenceSpacing from './plugin.js' |
116 | | - |
117 | | -const document = await fs.readFile('example.md', 'utf8') |
118 | | - |
119 | | -const file = await retext() |
120 | | - .use(retextSentenceSpacing) |
121 | | - .process(document) |
122 | | - |
123 | | -console.error(reporter(file)) |
124 | | -``` |
125 | | - |
126 | | -> Don’t forget to `npm install retext vfile-reporter`! |
127 | | -
|
128 | | -If you read the guide on [using unified][use], you’ll see some familiar |
129 | | -statements. |
130 | | -First, we load dependencies, then we read the file in. |
131 | | -We process that file with the plugin we’ll create in a second, and finally we |
132 | | -report either a fatal error or any found linting messages. |
133 | | - |
134 | | -Note that we directly depend on retext. |
135 | | -This is a package that exposes a unified processor, and comes with the parser |
136 | | -and compiler attached. |
137 | | - |
138 | | -When running our example (it doesn’t work yet though) we want to see a message |
139 | | -for the second paragraph, saying that one space instead of two spaces should be |
140 | | -used. |
141 | | - |
142 | | -Now we’ve got everything set up except for the plugin itself. |
143 | | -We’ll do that in the next section. |
144 | | - |
145 | | -### Plugin |
146 | | - |
147 | | -As we read in Plugin Basics, we’ll need a plugin, and for our case also a |
148 | | -transformer. |
149 | | -Let’s create them in our plugin file `plugin.js`: |
150 | | - |
151 | | -```js twoslash |
152 | | -/** |
153 | | - * @import {Root} from 'nlcst' |
154 | | - * @import {VFile} from 'vfile' |
155 | | - */ |
156 | | - |
157 | | -export default function retextSentenceSpacing() { |
158 | | - /** |
159 | | - * @param {Root} tree |
160 | | - * @param {VFile} file |
161 | | - * @return {undefined} |
162 | | - */ |
163 | | - return function (tree, file) { |
164 | | - } |
165 | | -} |
166 | | -``` |
167 | | - |
168 | | -First things first, we need to check `tree` for a pattern. |
169 | | -We can use a utility to help us to recursively walk our tree, namely |
170 | | -[`unist-util-visit`][visit]. |
171 | | -Let’s add that. |
172 | | - |
173 | | -```diff |
174 | | ---- a/plugin.js |
175 | | -+++ b/plugin.js |
176 | | -@@ -3,6 +3,8 @@ |
177 | | - * @import {VFile} from 'vfile' |
178 | | - */ |
179 | | - |
180 | | -+import {visit} from 'unist-util-visit' |
181 | | -+ |
182 | | - export default function retextSentenceSpacing() { |
183 | | - /** |
184 | | - * @param {Root} tree |
185 | | -@@ -10,5 +12,8 @@ export default function retextSentenceSpacing() { |
186 | | - * @return {undefined} |
187 | | - */ |
188 | | - return function (tree, file) { |
189 | | -+ visit(tree, 'ParagraphNode', function (node) { |
190 | | -+ console.log(node) |
191 | | -+ }) |
192 | | - } |
193 | | - } |
194 | | -``` |
195 | | - |
196 | | -> Don’t forget to `npm install unist-util-visit`. |
197 | | -
|
198 | | -If we now run our example with Node.js, as follows, we’ll see that visitor is |
199 | | -called with both paragraphs in our example: |
200 | | - |
201 | | -```sh |
202 | | -node example.js |
203 | | -``` |
204 | | - |
205 | | -```txt |
206 | | -{ |
207 | | - type: 'ParagraphNode', |
208 | | - children: [ |
209 | | - { type: 'SentenceNode', children: [Array], position: [Object] }, |
210 | | - { type: 'WhiteSpaceNode', value: ' ', position: [Object] }, |
211 | | - { type: 'SentenceNode', children: [Array], position: [Object] } |
212 | | - ], |
213 | | - position: { |
214 | | - start: { line: 1, column: 1, offset: 0 }, |
215 | | - end: { line: 1, column: 29, offset: 28 } |
216 | | - } |
217 | | -} |
218 | | -{ |
219 | | - type: 'ParagraphNode', |
220 | | - children: [ |
221 | | - { type: 'SentenceNode', children: [Array], position: [Object] }, |
222 | | - { type: 'WhiteSpaceNode', value: ' ', position: [Object] }, |
223 | | - { type: 'SentenceNode', children: [Array], position: [Object] } |
224 | | - ], |
225 | | - position: { |
226 | | - start: { line: 3, column: 1, offset: 30 }, |
227 | | - end: { line: 3, column: 30, offset: 59 } |
228 | | - } |
229 | | -} |
230 | | -no issues found |
231 | | -``` |
232 | | - |
233 | | -This output already shows that paragraphs contain two types of nodes: |
234 | | -`SentenceNode` and `WhiteSpaceNode`. |
235 | | -The latter is what we want to check, but the former is important because we only |
236 | | -warn about whitespace between sentences in this plugin (that could be another |
237 | | -plugin though). |
238 | | - |
239 | | -Let’s now loop through the children of each paragraph. |
240 | | -Only checking whitespace between sentences. |
241 | | -We use a small utility for checking node types: [`unist-util-is`][is]. |
242 | | - |
243 | | -```diff |
244 | | ---- a/plugin.js |
245 | | -+++ b/plugin.js |
246 | | -@@ -13,7 +13,23 @@ export default function retextSentenceSpacing() { |
247 | | - */ |
248 | | - return function (tree, file) { |
249 | | - visit(tree, 'ParagraphNode', function (node) { |
250 | | -- console.log(node) |
251 | | -+ let index = -1 |
252 | | -+ |
253 | | -+ while (++index < node.children.length) { |
254 | | -+ const previous = node.children[index - 1] |
255 | | -+ const child = node.children[index] |
256 | | -+ const next = node.children[index + 1] |
257 | | -+ |
258 | | -+ if ( |
259 | | -+ previous && |
260 | | -+ next && |
261 | | -+ previous.type === 'SentenceNode' && |
262 | | -+ child.type === 'WhiteSpaceNode' && |
263 | | -+ next.type === 'SentenceNode' |
264 | | -+ ) { |
265 | | -+ console.log(child) |
266 | | -+ } |
267 | | -+ } |
268 | | - }) |
269 | | - } |
270 | | - } |
271 | | -``` |
272 | | - |
273 | | -If we now run our example with Node, as follows, we’ll see that only whitespace |
274 | | -between sentences is logged. |
275 | | - |
276 | | -```sh |
277 | | -node example.js |
278 | | -``` |
279 | | - |
280 | | -```txt |
281 | | -{ |
282 | | - type: 'WhiteSpaceNode', |
283 | | - value: ' ', |
284 | | - position: { |
285 | | - start: { line: 1, column: 14, offset: 13 }, |
286 | | - end: { line: 1, column: 15, offset: 14 } |
287 | | - } |
288 | | -} |
289 | | -{ |
290 | | - type: 'WhiteSpaceNode', |
291 | | - value: ' ', |
292 | | - position: { |
293 | | - start: { line: 3, column: 14, offset: 43 }, |
294 | | - end: { line: 3, column: 16, offset: 45 } |
295 | | - } |
296 | | -} |
297 | | -no issues found |
298 | | -``` |
299 | | - |
300 | | -Finally, let’s add a warning for the second whitespace, as it has more |
301 | | -characters than needed. |
302 | | -We can use [`file.message()`][message] to associate a message with the file. |
303 | | - |
304 | | -```diff |
305 | | ---- a/plugin.js |
306 | | -+++ b/plugin.js |
307 | | -@@ -25,9 +25,15 @@ export default function retextSentenceSpacing() { |
308 | | - next && |
309 | | - previous.type === 'SentenceNode' && |
310 | | - child.type === 'WhiteSpaceNode' && |
311 | | -- next.type === 'SentenceNode' |
312 | | -+ next.type === 'SentenceNode' && |
313 | | -+ child.value.length !== 1 |
314 | | - ) { |
315 | | -- console.log(child) |
316 | | -+ file.message( |
317 | | -+ 'Unexpected `' + |
318 | | -+ child.value.length + |
319 | | -+ '` spaces between sentences, expected `1` space', |
320 | | -+ child |
321 | | -+ ) |
322 | | - } |
323 | | - } |
324 | | - }) |
325 | | -``` |
326 | | - |
327 | | -If we now run our example one final time, we’ll see a message for our problem! |
328 | | - |
329 | | -```sh |
330 | | -$ node example.js |
331 | | -3:14-3:16 warning Unexpected `2` spaces between sentences, expected `1` space |
332 | | - |
333 | | -⚠ 1 warning |
334 | | -``` |
335 | | - |
336 | | -### Further exercises |
337 | | - |
338 | | -One space between sentences isn’t for everyone. |
339 | | -This plugin could receive the preferred amount of spaces instead of a hard-coded |
340 | | -`1`. |
341 | | - |
342 | | -If you want to warn for tabs or newlines between sentences, maybe create a |
343 | | -plugin for that too? |
344 | | - |
345 | | -If you haven’t already, check out the other articles in the |
346 | | -[learn section][learn]! |
347 | | - |
348 | | -<!--Definitions--> |
349 | | - |
350 | | -[support]: https://github.com/unifiedjs/.github/blob/main/support.md |
351 | | - |
352 | | -[visit]: https://github.com/syntax-tree/unist-util-visit |
353 | | - |
354 | | -[is]: https://github.com/syntax-tree/unist-util-is |
355 | | - |
356 | | -[message]: https://github.com/vfile/vfile#vfilemessagereason-position-origin |
357 | | - |
358 | | -[learn]: /learn/ |
359 | | - |
360 | | -[use]: /learn/guide/using-unified/ |
| 15 | +This guide is archived. |
| 16 | +See [“Create a retext plugin”](/learn/guide/create-a-retext-plugin/) for the |
| 17 | +current version. |
| 18 | +See also [“Create a rehype plugin”](/learn/guide/create-a-rehype-plugin/) and |
| 19 | +[“Create a remark plugin”](/learn/guide/create-a-remark-plugin/) |
| 20 | +for the other similar guides. |
0 commit comments