|
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