unified

Learn/Guide/Use unified

Using unified

This guide shows how unified can be used to transform a markdown file to HTML. It also shows how to generate a table of contents and sidesteps into checking prose.

Stuck? Have an idea for another guide? See support.md.

Contents

Tree transformations

For this example we start out with markdown content and then turn it into HTML. We need something to parse markdown and something to compile (stringify) HTML for that. The relevant projects are respectively remark-parse and rehype-stringify. To transform between the two syntaxes we use remark-rehype. Finally, we use unified itself to glue these together.

First set up a project. Create a folder example, enter it, and initialize a new package:

mkdir example
cd example
npm init -y

Then make sure the project is a module so that import and export work by specifying "type": "module":

--- 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"

Now let’s install the needed dependencies with npm, which comes bundled with Node.js.

npm install rehype-stringify remark-parse remark-rehype unified

Now create a markdown file, example.md, that we’re going to transform.

# Pluto

Pluto is an dwarf planet in the Kuiper belt.

## Contents

## History

### Discovery

In the 1840s, Urbain Le Verrier used Newtonian mechanics to predict the
position of…

### Name and symbol

The name Pluto is for the Roman god of the underworld, from a Greek epithet for
Hades…

### Planet X disproved

Once Pluto was found, its faintness and lack of a viewable disc cast doubt…

## Orbit

Pluto’s orbital period is about 248 years…

Then create index.js as well. It transforms markdown to HTML:

import fs from 'node:fs/promises'
import rehypeStringify from 'rehype-stringify'
import remarkParse from 'remark-parse'
import remarkRehype from 'remark-rehype'
import {unified} from 'unified'

const document = await fs.readFile('example.md', 'utf8')

const file = await unified()
  .use(remarkParse)
  .use(remarkRehype)
  .use(rehypeStringify).process(document)

console.log(String(file))
(alias) module "node:fs/promises"
import fs
(alias) const rehypeStringify: Plugin<[(Options | null | undefined)?], Root, string>
import rehypeStringify

Plugin to add support for serializing as HTML.

  • @this processor.
  • @param Configuration (optional).
  • @returns Nothing.

(alias) const remarkParse: Plugin<[(Readonly<Options> | null | undefined)?], string, Root>
import remarkParse

Add support for parsing from markdown.

  • @this processor.
  • @param Configuration (optional).
  • @returns Nothing.

(alias) function remarkRehype(processor: Processor, options?: Readonly<Options> | null | undefined): TransformBridge (+2 overloads)
import remarkRehype

Turn markdown into HTML.

Notes
Signature
  • if a processor is given, runs the (rehype) plugins used on it with a hast tree, then discards the result (bridge mode)
  • otherwise, returns a hast tree, the plugins used after remarkRehype are rehype plugins (mutate mode)

👉 Note: It’s highly unlikely that you want to pass a processor.

HTML

Raw HTML is available in mdast as html nodes and can be embedded in hast as semistandard raw nodes. Most plugins ignore raw nodes but two notable ones don’t:

  • rehype-stringify also has an option allowDangerousHtml which will output the raw HTML. This is typically discouraged as noted by the option name but is useful if you completely trust authors
  • rehype-raw can handle the raw embedded HTML strings by parsing them into standard hast nodes (element, text, etc); this is a heavy task as it needs a full HTML parser, but it is the only way to support untrusted content
Footnotes

Many options supported here relate to footnotes. Footnotes are not specified by CommonMark, which we follow by default. They are supported by GitHub, so footnotes can be enabled in markdown with remark-gfm.

The options footnoteBackLabel and footnoteLabel define natural language that explains footnotes, which is hidden for sighted users but shown to assistive technology. When your page is not in English, you must define translated values.

Back references use ARIA attributes, but the section label itself uses a heading that is hidden with an sr-only class. To show it to sighted users, define different attributes in footnoteLabelProperties.

Clobbering

Footnotes introduces a problem, as it links footnote calls to footnote definitions on the page through id attributes generated from user content, which results in DOM clobbering.

DOM clobbering is this:

<p id=x></p>
<script>alert(x) // `x` now refers to the DOM `p#x` element</script>

Elements by their ID are made available by browsers on the window object, which is a security risk. Using a prefix solves this problem.

More information on how to handle clobbering and the prefix is explained in Example: headings (DOM clobbering) in rehype-sanitize.

Unknown nodes

Unknown nodes are nodes with a type that isn’t in handlers or passThrough. The default behavior for unknown nodes is:

  • when the node has a value (and doesn’t have data.hName, data.hProperties, or data.hChildren, see later), create a hast text node
  • otherwise, create a &#60div> element (which could be changed with data.hName), with its children mapped from mdast to hast as well

This behavior can be changed by passing an unknownHandler.

  • @overload
  • @overload
  • @overload
  • @param destination Processor or configuration (optional).
  • @param options When a processor was given, configuration (optional).
  • @returns Transform.

(alias) const unified: Processor<undefined, undefined, undefined, undefined, undefined>
import unified

Create a new processor.

  • @example This example shows how a new processor can be created (from remark) and linked to stdin(4) and stdout(4).
    import process from 'node:process'
    import concatStream from 'concat-stream'
    import {remark} from 'remark'
    
    process.stdin.pipe(
      concatStream(function (buf) {
        process.stdout.write(String(remark().processSync(buf)))
      })
    )
    
  • @returns New unfrozen processor (processor). This processor is configured to work the same as its ancestor. When the descendant processor is configured in the future it does not affect the ancestral processor.

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> = any>.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 a FileHandle 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) unified(): Processor<undefined, undefined, undefined, undefined, undefined>
import unified

Create a new processor.

  • @example This example shows how a new processor can be created (from remark) and linked to stdin(4) and stdout(4).
    import process from 'node:process'
    import concatStream from 'concat-stream'
    import {remark} from 'remark'
    
    process.stdin.pipe(
      concatStream(function (buf) {
        process.stdout.write(String(remark().processSync(buf)))
      })
    )
    
  • @returns New unfrozen processor (processor). This processor is configured to work the same as its ancestor. When the descendant processor is configured in the future it does not affect the ancestral processor.

(method) Processor<undefined, undefined, undefined, undefined, undefined>.use<[], string, Root>(plugin: Plugin<[], string, Root>, ...parameters: [] | [boolean]): Processor<Root, undefined, undefined, undefined, undefined> (+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) const remarkParse: Plugin<[(Readonly<Options> | null | undefined)?], string, Root>
import remarkParse

Add support for parsing from markdown.

  • @this processor.
  • @param Configuration (optional).
  • @returns Nothing.

(method) Processor<Root, undefined, undefined, undefined, undefined>.use<[], Root, Root>(plugin: Plugin<[], Root, Root>, ...parameters: [] | [boolean]): Processor<Root, Root, Root, undefined, undefined> (+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 remarkRehype(processor: Processor, options?: Readonly<Options> | null | undefined): TransformBridge (+2 overloads)
import remarkRehype

Turn markdown into HTML.

Notes
Signature
  • if a processor is given, runs the (rehype) plugins used on it with a hast tree, then discards the result (bridge mode)
  • otherwise, returns a hast tree, the plugins used after remarkRehype are rehype plugins (mutate mode)

👉 Note: It’s highly unlikely that you want to pass a processor.

HTML

Raw HTML is available in mdast as html nodes and can be embedded in hast as semistandard raw nodes. Most plugins ignore raw nodes but two notable ones don’t:

  • rehype-stringify also has an option allowDangerousHtml which will output the raw HTML. This is typically discouraged as noted by the option name but is useful if you completely trust authors
  • rehype-raw can handle the raw embedded HTML strings by parsing them into standard hast nodes (element, text, etc); this is a heavy task as it needs a full HTML parser, but it is the only way to support untrusted content
Footnotes

Many options supported here relate to footnotes. Footnotes are not specified by CommonMark, which we follow by default. They are supported by GitHub, so footnotes can be enabled in markdown with remark-gfm.

The options footnoteBackLabel and footnoteLabel define natural language that explains footnotes, which is hidden for sighted users but shown to assistive technology. When your page is not in English, you must define translated values.

Back references use ARIA attributes, but the section label itself uses a heading that is hidden with an sr-only class. To show it to sighted users, define different attributes in footnoteLabelProperties.

Clobbering

Footnotes introduces a problem, as it links footnote calls to footnote definitions on the page through id attributes generated from user content, which results in DOM clobbering.

DOM clobbering is this:

<p id=x></p>
<script>alert(x) // `x` now refers to the DOM `p#x` element</script>

Elements by their ID are made available by browsers on the window object, which is a security risk. Using a prefix solves this problem.

More information on how to handle clobbering and the prefix is explained in Example: headings (DOM clobbering) in rehype-sanitize.

Unknown nodes

Unknown nodes are nodes with a type that isn’t in handlers or passThrough. The default behavior for unknown nodes is:

  • when the node has a value (and doesn’t have data.hName, data.hProperties, or data.hChildren, see later), create a hast text node
  • otherwise, create a &#60div></code> element (which could be changed with <code>data.hName</code>), with its children mapped from mdast to hast as well</ul><p>This behavior can be changed by passing an <code>unknownHandler</code>.<ul><li><strong>@overload</strong><li><strong>@overload</strong><li><strong>@overload</strong><li><strong>@param</strong> destination Processor or configuration (optional).<li><strong>@param</strong> options When a processor was given, configuration (optional).<li><strong>@returns</strong> Transform.</ul></div></div><div class="rehype-twoslash-hover rehype-twoslash-popover"id=rehype-twoslash-rtciuuuc-14 popover><pre class=rehype-twoslash-popover-code><code class=language-ts>(<span class=pl-smi>method</span>) <span class=pl-smi>Processor</span><span class=pl-k>&lt;</span><span class=pl-smi>Root</span>, <span class=pl-smi>Root</span>, <span class=pl-smi>Root</span>, <span class=pl-c1>undefined</span>, <span class=pl-c1>undefined</span><span class=pl-k>></span>.<span class=pl-en>use</span>&#60[], <span class=pl-en>Root</span>, <span class=pl-c1>string</span>>(<span class=pl-smi>plugin</span>: <span class=pl-c1>Plugin</span><span class=pl-k>&lt;</span>[], <span class=pl-smi>Root</span>, <span class=pl-smi>string</span><span class=pl-k>></span>, <span class=pl-k>...</span><span class=pl-smi>parameters</span>: [] <span class=pl-k>|</span> [<span class=pl-smi>boolean</span>]): <span class=pl-en>Processor</span>&lt;<span class=pl-en>Root</span>, <span class=pl-en>Root</span>, <span class=pl-en>Root</span>, <span class=pl-en>Root</span>, <span class=pl-c1>string</span>> (<span class=pl-k>+</span><span class=pl-c1>2</span> <span class=pl-smi>overloads</span>)</code></pre><div class=rehype-twoslash-popover-description><p>Configure the processor to use a plugin, a list of usable values, or a preset.<p>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.<blockquote><p><strong>Note</strong>: <code>use</code> cannot be called on <em>frozen</em> processors. Call the processor first to create a new unfrozen processor.</blockquote><ul><li><strong>@example</strong> There are many ways to pass plugins to <code>.use()</code>. This example gives an overview:<pre><code class=language-js><span class=pl-k>import</span> {<span class=pl-smi>unified</span>} <span class=pl-k>from</span> <span class=pl-s><span class=pl-pds>'</span>unified<span class=pl-pds>'</span></span> <span class=pl-en>unified</span>() <span class=pl-c>// Plugin with options:</span> .<span class=pl-en>use</span>(pluginA, {x<span class=pl-k>:</span> <span class=pl-c1>true</span>, y<span class=pl-k>:</span> <span class=pl-c1>true</span>}) <span class=pl-c>// Passing the same plugin again merges configuration (to `{x: true, y: false, z: true}`):</span> .<span class=pl-en>use</span>(pluginA, {y<span class=pl-k>:</span> <span class=pl-c1>false</span>, z<span class=pl-k>:</span> <span class=pl-c1>true</span>}) <span class=pl-c>// Plugins:</span> .<span class=pl-en>use</span>([pluginB, pluginC]) <span class=pl-c>// Two plugins, the second with options:</span> .<span class=pl-en>use</span>([pluginD, [pluginE, {}]]) <span class=pl-c>// Preset with plugins and settings:</span> .<span class=pl-en>use</span>({plugins<span class=pl-k>:</span> [pluginF, [pluginG, {}]], settings<span class=pl-k>:</span> {position<span class=pl-k>:</span> <span class=pl-c1>false</span>}}) <span class=pl-c>// Settings only:</span> .<span class=pl-en>use</span>({settings<span class=pl-k>:</span> {position<span class=pl-k>:</span> <span class=pl-c1>false</span>}}) </code></pre><li><strong>@template</strong> {Array} [Parameters=[]]<li><strong>@template</strong> {Node | string | undefined} [Input=undefined]<li><strong>@template</strong> [Output=Input]<li><strong>@overload</strong><li><strong>@overload</strong><li><strong>@overload</strong><li><strong>@param</strong> value Usable value.<li><strong>@param</strong> parameters Parameters, when a plugin is given as a usable value.<li><strong>@returns</strong> Current processor.</ul></div></div><div class="rehype-twoslash-hover rehype-twoslash-popover"id=rehype-twoslash-rtciuuuc-15 popover><pre class=rehype-twoslash-popover-code><code class=language-ts>(<span class=pl-smi>alias</span>) <span class=pl-k>const</span> <span class=pl-c1>rehypeStringify</span><span class=pl-k>:</span> <span class=pl-en>Plugin</span>&#60[(<span class=pl-en>Options</span> <span class=pl-k>|</span> <span class=pl-c1>null</span> <span class=pl-k>|</span> <span class=pl-c1>undefined</span>)<span class=pl-k>?</span>], <span class=pl-en>Root</span>, <span class=pl-c1>string</span>> <span class=pl-k>import</span> <span class=pl-smi>rehypeStringify</span></code></pre><div class=rehype-twoslash-popover-description><p>Plugin to add support for serializing as <abbr title="Hypertext markup language">HTML</abbr>.<ul><li><strong>@this</strong> processor.<li><strong>@param</strong> Configuration (optional).<li><strong>@returns</strong> Nothing.</ul></div></div><div class="rehype-twoslash-hover rehype-twoslash-popover"id=rehype-twoslash-rtciuuuc-16 popover><pre class=rehype-twoslash-popover-code><code class=language-ts>(<span class=pl-smi>method</span>) <span class=pl-smi>Processor</span><span class=pl-k>&lt;</span><span class=pl-smi>Root</span>, <span class=pl-smi>Root</span>, <span class=pl-smi>Root</span>, <span class=pl-smi>Root</span>, <span class=pl-smi>string</span><span class=pl-k>></span>.<span class=pl-en>process</span>(<span class=pl-smi>file</span><span class=pl-k>?:</span> <span class=pl-smi>Compatible</span> <span class=pl-k>|</span> <span class=pl-c1>undefined</span>): <span class=pl-c1>Promise</span>&lt;<span class=pl-en>VFile</span>> (<span class=pl-k>+</span><span class=pl-c1>1</span> <span class=pl-smi>overload</span>)</code></pre><div class=rehype-twoslash-popover-description><p>Process the given file as configured on the processor.<blockquote><p><strong>Note</strong>: <code>process</code> freezes the processor if not already <em>frozen</em>.</blockquote><blockquote><p><strong>Note</strong>: <code>process</code> performs the parse, run, and stringify phases.</blockquote><ul><li><strong>@overload</strong><li><strong>@overload</strong><li><strong>@param</strong> file File (optional); typically <code>string</code> or <code>VFile</code>]; any value accepted as <code>x</code> in <code>new VFile(x)</code>.<li><strong>@param</strong> done Callback (optional).<li><strong>@returns</strong> Nothing if <code>done</code> is given. Otherwise a promise, rejected with a fatal error or resolved with the processed file. The parsed, transformed, and compiled value is available at <code>file.value</code> (see note).<blockquote><p><strong>Note</strong>: unified typically compiles by serializing: most compilers return <code>string</code> (or <code>Uint8Array</code>). Some compilers, such as the one configured with <a href=/explore/package/rehype-react/><code>rehype-react</code></a>, return other values (in this case, a React tree). If you’re using a compiler that doesn’t serialize, expect different result values.<p>To register custom results in TypeScript, add them to {@linkcode CompileResultMap}.</blockquote></ul></div></div><div class="rehype-twoslash-hover rehype-twoslash-popover"id=rehype-twoslash-rtciuuuc-17 popover><pre class=rehype-twoslash-popover-code><code class=language-ts><span class=pl-k>const</span> <span class=pl-c1>document</span><span class=pl-k>:</span> <span class=pl-c1>string</span></code></pre></div><div class="rehype-twoslash-hover rehype-twoslash-popover"id=rehype-twoslash-rtciuuuc-18 popover><pre class=rehype-twoslash-popover-code><code class=language-ts><span class=pl-k>var</span> <span class=pl-smi>console</span><span class=pl-k>:</span> <span class=pl-en>Console</span></code></pre></div><div class="rehype-twoslash-hover rehype-twoslash-popover"id=rehype-twoslash-rtciuuuc-19 popover><pre class=rehype-twoslash-popover-code><code class=language-ts>(<span class=pl-smi>method</span>) <span class=pl-c1>console</span>.<span class=pl-smi>Console</span>.<span class=pl-en>log</span>(<span class=pl-k>...</span><span class=pl-smi>data</span>: <span class=pl-smi>any</span>[]): <span class=pl-k>void</span></code></pre></div><div class="rehype-twoslash-hover rehype-twoslash-popover"id=rehype-twoslash-rtciuuuc-20 popover><pre class=rehype-twoslash-popover-code><code class=language-ts><span class=pl-k>var</span> <span class=pl-smi>String</span><span class=pl-k>:</span> <span class=pl-en>StringConstructor</span> (<span class=pl-v>value</span><span class=pl-k>?:</span> <span class=pl-c1>any</span>) <span class=pl-k>=></span> <span class=pl-smi>string</span></code></pre><div class=rehype-twoslash-popover-description><p>Allows manipulation and formatting of text strings and determination and location of substrings within strings.</div></div><div class="rehype-twoslash-hover rehype-twoslash-popover"id=rehype-twoslash-rtciuuuc-21 popover><pre class=rehype-twoslash-popover-code><code class=language-ts><span class=pl-k>const</span> <span class=pl-c1>file</span><span class=pl-k>:</span> <span class=pl-en>VFile</span></code></pre></div></div><p>Now, running our module with <a href=//nodejs.org/en rel="nofollow noopener noreferrer">Node</a>:<pre><code class=language-sh>node index.js </code></pre><p>…gives us an <code>example.html</code> file that looks as follows:<pre><code class=language-html>&lt;<span class=pl-ent>h1</span>>Pluto&#60/<span class=pl-ent>h1</span>> &lt;<span class=pl-ent>p</span>>Pluto is an dwarf planet in the Kuiper belt.&#60/<span class=pl-ent>p</span>> &lt;<span class=pl-ent>h2</span>>Contents&#60/<span class=pl-ent>h2</span>> &lt;<span class=pl-ent>h2</span>>History&#60/<span class=pl-ent>h2</span>> &lt;<span class=pl-ent>h3</span>>Discovery&#60/<span class=pl-ent>h3</span>> &lt;<span class=pl-ent>p</span>>In the 1840s, Urbain Le Verrier used Newtonian mechanics to predict the position of…&#60/<span class=pl-ent>p</span>> &lt;<span class=pl-ent>h3</span>>Name and symbol&#60/<span class=pl-ent>h3</span>> &lt;<span class=pl-ent>p</span>>The name Pluto is for the Roman god of the underworld, from a Greek epithet for Hades…&#60/<span class=pl-ent>p</span>> &lt;<span class=pl-ent>h3</span>>Planet X disproved&#60/<span class=pl-ent>h3</span>> &lt;<span class=pl-ent>p</span>>Once Pluto was found, its faintness and lack of a viewable disc cast doubt…&#60/<span class=pl-ent>p</span>> &lt;<span class=pl-ent>h2</span>>Orbit&#60/<span class=pl-ent>h2</span>> &lt;<span class=pl-ent>p</span>>Pluto’s orbital period is about 248 years…&#60/<span class=pl-ent>p</span>> </code></pre><blockquote><p>👉 Note that <a href=/explore/package/remark-rehype/><code>remark-rehype</code></a> doesn’t deal with <abbr title="Hypertext markup language">HTML</abbr> inside the markdown. See <a href=../../recipe/remark-html/><em><abbr title="Hypertext markup language">HTML</abbr> and remark</em></a> for more info.</blockquote><p>🎉 Nifty! It doesn’t do much yet. We’ll get there. In the next section, we make this more useful by introducing plugins.<h3 id=plugins><a href=#plugins class=anchor aria-label="Link to self"><svg class=icon height=18 role=img viewBox="0 0 16 16"width=18 aria-hidden=true><path d="M7.775 3.275C7.64252 3.41717 7.57039 3.60522 7.57382 3.79952C7.57725 3.99382 7.65596 4.1792 7.79337 4.31662C7.93079 4.45403 8.11617 4.53274 8.31047 4.53617C8.50477 4.5396 8.69282 4.46748 8.835 4.335L10.085 3.085C10.2708 2.89918 10.4914 2.75177 10.7342 2.65121C10.977 2.55064 11.2372 2.49888 11.5 2.49888C11.7628 2.49888 12.023 2.55064 12.2658 2.65121C12.5086 2.75177 12.7292 2.89918 12.915 3.085C13.1008 3.27082 13.2482 3.49142 13.3488 3.7342C13.4493 3.97699 13.5011 4.23721 13.5011 4.5C13.5011 4.76279 13.4493 5.023 13.3488 5.26579C13.2482 5.50857 13.1008 5.72917 12.915 5.915L10.415 8.415C10.2292 8.60095 10.0087 8.74847 9.76588 8.84911C9.52308 8.94976 9.26283 9.00157 9 9.00157C8.73716 9.00157 8.47691 8.94976 8.23411 8.84911C7.99132 8.74847 7.77074 8.60095 7.585 8.415C7.44282 8.28252 7.25477 8.21039 7.06047 8.21382C6.86617 8.21725 6.68079 8.29596 6.54337 8.43337C6.40596 8.57079 6.32725 8.75617 6.32382 8.95047C6.32039 9.14477 6.39252 9.33282 6.525 9.475C6.85001 9.80004 7.23586 10.0579 7.66052 10.2338C8.08518 10.4097 8.54034 10.5002 9 10.5002C9.45965 10.5002 9.91481 10.4097 10.3395 10.2338C10.7641 10.0579 11.15 9.80004 11.475 9.475L13.975 6.975C14.6314 6.31858 15.0002 5.4283 15.0002 4.5C15.0002 3.57169 14.6314 2.68141 13.975 2.025C13.3186 1.36858 12.4283 0.999817 11.5 0.999817C10.5717 0.999817 9.68141 1.36858 9.02499 2.025L7.775 3.275ZM3.085 12.915C2.89904 12.7292 2.75152 12.5087 2.65088 12.2659C2.55023 12.0231 2.49842 11.7628 2.49842 11.5C2.49842 11.2372 2.55023 10.9769 2.65088 10.7341C2.75152 10.4913 2.89904 10.2707 3.085 10.085L5.585 7.585C5.77074 7.39904 5.99132 7.25152 6.23411 7.15088C6.47691 7.05023 6.73716 6.99842 7 6.99842C7.26283 6.99842 7.52308 7.05023 7.76588 7.15088C8.00867 7.25152 8.22925 7.39904 8.415 7.585C8.55717 7.71748 8.74522 7.7896 8.93952 7.78617C9.13382 7.78274 9.3192 7.70403 9.45662 7.56662C9.59403 7.4292 9.67274 7.24382 9.67617 7.04952C9.6796 6.85522 9.60748 6.66717 9.475 6.525C9.14999 6.19995 8.76413 5.94211 8.33947 5.7662C7.91481 5.59029 7.45965 5.49974 7 5.49974C6.54034 5.49974 6.08518 5.59029 5.66052 5.7662C5.23586 5.94211 4.85001 6.19995 4.525 6.525L2.025 9.02499C1.36858 9.68141 0.999817 10.5717 0.999817 11.5C0.999817 12.4283 1.36858 13.3186 2.025 13.975C2.68141 14.6314 3.57169 15.0002 4.5 15.0002C5.4283 15.0002 6.31858 14.6314 6.975 13.975L8.225 12.725C8.35748 12.5828 8.4296 12.3948 8.42617 12.2005C8.42274 12.0062 8.34403 11.8208 8.20662 11.6834C8.0692 11.546 7.88382 11.4672 7.68952 11.4638C7.49522 11.4604 7.30717 11.5325 7.165 11.665L5.915 12.915C5.72925 13.1009 5.50867 13.2485 5.26588 13.3491C5.02308 13.4498 4.76283 13.5016 4.5 13.5016C4.23716 13.5016 3.97691 13.4498 3.73411 13.3491C3.49132 13.2485 3.27074 13.1009 3.085 12.915Z"fill=currentcolor /></svg></a>Plugins</h3><p>We’re still missing some things Notably a table of contents and proper <abbr title="Hypertext markup language">HTML</abbr> document structure.<p>We can use <a href=/explore/package/rehype-slug/><code>rehype-slug</code></a> and <a href=/explore/package/remark-toc/><code>remark-toc</code></a> for the former and <a href=/explore/package/rehype-document/><code>rehype-document</code></a> for the latter task.<pre><code class=language-sh>npm install rehype-document rehype-slug remark-toc </code></pre><p>Let’s now use those two as well, by modifying our <code>index.js</code> file:<pre><code class=language-diff><span class=pl-md>--- a/index.js</span> <span class=pl-mi1>+++ b/index.js</span> <span class=pl-mdr>@@ -1,14 +1,20 @@</span> import fs from 'node:fs/promises' <span class=pl-mi1>+import rehypeDocument from 'rehype-document'</span> <span class=pl-mi1>+import rehypeSlug from 'rehype-slug'</span> import rehypeStringify from 'rehype-stringify' import remarkParse from 'remark-parse' import remarkRehype from 'remark-rehype' <span class=pl-mi1>+import remarkToc from 'remark-toc'</span> import {unified} from 'unified' const document = await fs.readFile('example.md', 'utf8') const file = await unified() .use(remarkParse) <span class=pl-mi1>+ .use(remarkToc)</span> .use(remarkRehype) <span class=pl-mi1>+ .use(rehypeSlug)</span> <span class=pl-mi1>+ .use(rehypeDocument, {title: 'Pluto'})</span> .use(rehypeStringify) .process(document) </code></pre><p>We pass options to <code>rehype-document</code>. In this case, we use that to make sure we get a proper <code>&#60title></code> element in our <code>&#60head></code>, as required by the <abbr title="Hypertext markup language">HTML</abbr> specification. More options are accepted by <code>rehype-document</code>, such as which language tag to use. These are described in detail in its <a href=/explore/package/rehype-document/><code>readme.md</code></a>. Many other plugins accept options as well, so make sure to read through their docs to learn more.<blockquote><p>👉 Note that <a href=/explore/project/remarkjs/remark/ class=remark><span class=hl>re</span>mark</a> plugins work on a markdown tree. <a href=/explore/project/rehypejs/rehype/ class=rehype><span class=hl>re</span>hype</a> plugins work on an <abbr title="Hypertext markup language">HTML</abbr> tree. It’s important that you place your <code>.use</code> calls in the correct places: plugins are order sensitive!</blockquote><p>When running our module like before, we’d get the following <code>example.html</code> file:<pre><code class=language-html>&#60!doctype html> &lt;<span class=pl-ent>html</span> <span class=pl-e>lang</span>=<span class=pl-s><span class=pl-pds>"</span>en<span class=pl-pds>"</span></span>> &lt;<span class=pl-ent>head</span>> &lt;<span class=pl-ent>meta</span> <span class=pl-e>charset</span>=<span class=pl-s><span class=pl-pds>"</span>utf-8<span class=pl-pds>"</span></span>> &lt;<span class=pl-ent>title</span>>Pluto&#60/<span class=pl-ent>title</span>> &lt;<span class=pl-ent>meta</span> <span class=pl-e>content</span>=<span class=pl-s><span class=pl-pds>"</span>width=device-width, initial-scale=1<span class=pl-pds>"</span></span> <span class=pl-e>name</span>=<span class=pl-s><span class=pl-pds>"</span>viewport<span class=pl-pds>"</span></span>> &#60/<span class=pl-ent>head</span>> &lt;<span class=pl-ent>body</span>> &lt;<span class=pl-ent>h1</span> <span class=pl-e>id</span>=<span class=pl-s><span class=pl-pds>"</span>pluto<span class=pl-pds>"</span></span>>Pluto&#60/<span class=pl-ent>h1</span>> &lt;<span class=pl-ent>p</span>>Pluto is an dwarf planet in the Kuiper belt.&#60/<span class=pl-ent>p</span>> &lt;<span class=pl-ent>h2</span> <span class=pl-e>id</span>=<span class=pl-s><span class=pl-pds>"</span>contents<span class=pl-pds>"</span></span>>Contents&#60/<span class=pl-ent>h2</span>> &lt;<span class=pl-ent>ul</span>> &lt;<span class=pl-ent>li</span>>&lt;<span class=pl-ent>a</span> <span class=pl-e>href</span>=<span class=pl-s><span class=pl-pds>"</span>#history<span class=pl-pds>"</span></span>>History&#60/<span class=pl-ent>a</span>> &lt;<span class=pl-ent>ul</span>> &lt;<span class=pl-ent>li</span>>&lt;<span class=pl-ent>a</span> <span class=pl-e>href</span>=<span class=pl-s><span class=pl-pds>"</span>#discovery<span class=pl-pds>"</span></span>>Discovery&#60/<span class=pl-ent>a</span>>&#60/<span class=pl-ent>li</span>> &lt;<span class=pl-ent>li</span>>&lt;<span class=pl-ent>a</span> <span class=pl-e>href</span>=<span class=pl-s><span class=pl-pds>"</span>#name-and-symbol<span class=pl-pds>"</span></span>>Name and symbol&#60/<span class=pl-ent>a</span>>&#60/<span class=pl-ent>li</span>> &lt;<span class=pl-ent>li</span>>&lt;<span class=pl-ent>a</span> <span class=pl-e>href</span>=<span class=pl-s><span class=pl-pds>"</span>#planet-x-disproved<span class=pl-pds>"</span></span>>Planet X disproved&#60/<span class=pl-ent>a</span>>&#60/<span class=pl-ent>li</span>> &#60/<span class=pl-ent>ul</span>> &#60/<span class=pl-ent>li</span>> &lt;<span class=pl-ent>li</span>>&lt;<span class=pl-ent>a</span> <span class=pl-e>href</span>=<span class=pl-s><span class=pl-pds>"</span>#orbit<span class=pl-pds>"</span></span>>Orbit&#60/<span class=pl-ent>a</span>>&#60/<span class=pl-ent>li</span>> &#60/<span class=pl-ent>ul</span>> &lt;<span class=pl-ent>h2</span> <span class=pl-e>id</span>=<span class=pl-s><span class=pl-pds>"</span>history<span class=pl-pds>"</span></span>>History&#60/<span class=pl-ent>h2</span>> &lt;<span class=pl-ent>h3</span> <span class=pl-e>id</span>=<span class=pl-s><span class=pl-pds>"</span>discovery<span class=pl-pds>"</span></span>>Discovery&#60/<span class=pl-ent>h3</span>> &lt;<span class=pl-ent>p</span>>In the 1840s, Urbain Le Verrier used Newtonian mechanics to predict the position of…&#60/<span class=pl-ent>p</span>> &lt;<span class=pl-ent>h3</span> <span class=pl-e>id</span>=<span class=pl-s><span class=pl-pds>"</span>name-and-symbol<span class=pl-pds>"</span></span>>Name and symbol&#60/<span class=pl-ent>h3</span>> &lt;<span class=pl-ent>p</span>>The name Pluto is for the Roman god of the underworld, from a Greek epithet for Hades…&#60/<span class=pl-ent>p</span>> &lt;<span class=pl-ent>h3</span> <span class=pl-e>id</span>=<span class=pl-s><span class=pl-pds>"</span>planet-x-disproved<span class=pl-pds>"</span></span>>Planet X disproved&#60/<span class=pl-ent>h3</span>> &lt;<span class=pl-ent>p</span>>Once Pluto was found, its faintness and lack of a viewable disc cast doubt…&#60/<span class=pl-ent>p</span>> &lt;<span class=pl-ent>h2</span> <span class=pl-e>id</span>=<span class=pl-s><span class=pl-pds>"</span>orbit<span class=pl-pds>"</span></span>>Orbit&#60/<span class=pl-ent>h2</span>> &lt;<span class=pl-ent>p</span>>Pluto’s orbital period is about 248 years…&#60/<span class=pl-ent>p</span>> &#60/<span class=pl-ent>body</span>> &#60/<span class=pl-ent>html</span>> </code></pre><blockquote><p>👉 Note that the document isn’t formatted nicely. There’s a plugin for that though! Feel free to add <a href=/explore/package/rehype-format/><code>rehype-format</code></a> to the plugins. Right after <code>rehypeDocument</code>!</blockquote><p>💯 You’re acing it! This is getting pretty useful, right?<p>In the next section, we lay the groundwork for creating a report.<h3 id=reporting><a href=#reporting class=anchor aria-label="Link to self"><svg class=icon height=18 role=img viewBox="0 0 16 16"width=18 aria-hidden=true><path d="M7.775 3.275C7.64252 3.41717 7.57039 3.60522 7.57382 3.79952C7.57725 3.99382 7.65596 4.1792 7.79337 4.31662C7.93079 4.45403 8.11617 4.53274 8.31047 4.53617C8.50477 4.5396 8.69282 4.46748 8.835 4.335L10.085 3.085C10.2708 2.89918 10.4914 2.75177 10.7342 2.65121C10.977 2.55064 11.2372 2.49888 11.5 2.49888C11.7628 2.49888 12.023 2.55064 12.2658 2.65121C12.5086 2.75177 12.7292 2.89918 12.915 3.085C13.1008 3.27082 13.2482 3.49142 13.3488 3.7342C13.4493 3.97699 13.5011 4.23721 13.5011 4.5C13.5011 4.76279 13.4493 5.023 13.3488 5.26579C13.2482 5.50857 13.1008 5.72917 12.915 5.915L10.415 8.415C10.2292 8.60095 10.0087 8.74847 9.76588 8.84911C9.52308 8.94976 9.26283 9.00157 9 9.00157C8.73716 9.00157 8.47691 8.94976 8.23411 8.84911C7.99132 8.74847 7.77074 8.60095 7.585 8.415C7.44282 8.28252 7.25477 8.21039 7.06047 8.21382C6.86617 8.21725 6.68079 8.29596 6.54337 8.43337C6.40596 8.57079 6.32725 8.75617 6.32382 8.95047C6.32039 9.14477 6.39252 9.33282 6.525 9.475C6.85001 9.80004 7.23586 10.0579 7.66052 10.2338C8.08518 10.4097 8.54034 10.5002 9 10.5002C9.45965 10.5002 9.91481 10.4097 10.3395 10.2338C10.7641 10.0579 11.15 9.80004 11.475 9.475L13.975 6.975C14.6314 6.31858 15.0002 5.4283 15.0002 4.5C15.0002 3.57169 14.6314 2.68141 13.975 2.025C13.3186 1.36858 12.4283 0.999817 11.5 0.999817C10.5717 0.999817 9.68141 1.36858 9.02499 2.025L7.775 3.275ZM3.085 12.915C2.89904 12.7292 2.75152 12.5087 2.65088 12.2659C2.55023 12.0231 2.49842 11.7628 2.49842 11.5C2.49842 11.2372 2.55023 10.9769 2.65088 10.7341C2.75152 10.4913 2.89904 10.2707 3.085 10.085L5.585 7.585C5.77074 7.39904 5.99132 7.25152 6.23411 7.15088C6.47691 7.05023 6.73716 6.99842 7 6.99842C7.26283 6.99842 7.52308 7.05023 7.76588 7.15088C8.00867 7.25152 8.22925 7.39904 8.415 7.585C8.55717 7.71748 8.74522 7.7896 8.93952 7.78617C9.13382 7.78274 9.3192 7.70403 9.45662 7.56662C9.59403 7.4292 9.67274 7.24382 9.67617 7.04952C9.6796 6.85522 9.60748 6.66717 9.475 6.525C9.14999 6.19995 8.76413 5.94211 8.33947 5.7662C7.91481 5.59029 7.45965 5.49974 7 5.49974C6.54034 5.49974 6.08518 5.59029 5.66052 5.7662C5.23586 5.94211 4.85001 6.19995 4.525 6.525L2.025 9.02499C1.36858 9.68141 0.999817 10.5717 0.999817 11.5C0.999817 12.4283 1.36858 13.3186 2.025 13.975C2.68141 14.6314 3.57169 15.0002 4.5 15.0002C5.4283 15.0002 6.31858 14.6314 6.975 13.975L8.225 12.725C8.35748 12.5828 8.4296 12.3948 8.42617 12.2005C8.42274 12.0062 8.34403 11.8208 8.20662 11.6834C8.0692 11.546 7.88382 11.4672 7.68952 11.4638C7.49522 11.4604 7.30717 11.5325 7.165 11.665L5.915 12.915C5.72925 13.1009 5.50867 13.2485 5.26588 13.3491C5.02308 13.4498 4.76283 13.5016 4.5 13.5016C4.23716 13.5016 3.97691 13.4498 3.73411 13.3491C3.49132 13.2485 3.27074 13.1009 3.085 12.915Z"fill=currentcolor /></svg></a>Reporting</h3><p>Before we check some prose, let’s first switch up our <code>index.js</code> file to print a pretty report.<p>We can use <a href=/explore/package/to-vfile/><code>to-vfile</code></a> to read and write virtual files from the file system. Then we can use <a href=/explore/package/vfile-reporter/><code>vfile-reporter</code></a> to report messages relating to those files. Let’s install those.<pre><code class=language-sh>npm install to-vfile vfile-reporter </code></pre><p>…and then use <a href=/explore/package/vfile/ class=vfile><span class=hl>v</span>file</a> in our example instead, like so:<pre><code class=language-diff><span class=pl-md>--- a/index.js</span> <span class=pl-mi1>+++ b/index.js</span> <span class=pl-mdr>@@ -1,21 +1,24 @@</span> <span class=pl-md>-import fs from 'node:fs/promises'</span> import rehypeDocument from 'rehype-document' import rehypeSlug from 'rehype-slug' import rehypeStringify from 'rehype-stringify' import remarkParse from 'remark-parse' import remarkRehype from 'remark-rehype' import remarkToc from 'remark-toc' <span class=pl-mi1>+import {read, write} from 'to-vfile'</span> import {unified} from 'unified' <span class=pl-mi1>+import {reporter} from 'vfile-reporter'</span> <span class=pl-md>-const document = await fs.readFile('example.md', 'utf8')</span> <span class=pl-mi1>+const file = await read('example.md')</span> <span class=pl-md>-const file = await unified()</span> <span class=pl-mi1>+await unified()</span> .use(remarkParse) .use(remarkToc) .use(remarkRehype) .use(rehypeSlug) .use(rehypeDocument, {title: 'Pluto'}) .use(rehypeStringify) <span class=pl-md>- .process(document)</span> <span class=pl-mi1>+ .process(file)</span> <span class=pl-md>-console.log(String(file))</span> <span class=pl-mi1>+console.error(reporter(file))</span> <span class=pl-mi1>+file.extname = '.html'</span> <span class=pl-mi1>+await write(file)</span> </code></pre><p>If we now run our module on its own we get a report showing everything’s fine:<pre><code class=language-sh>$ node index.js example.md: no issues found </code></pre><p>But everything’s not fine: there’s a typo in the markdown! The next section shows how to detect prose errors by adding <a href=/explore/project/retextjs/retext/ class=retext><span class=hl>re</span>text</a>.<h3 id=checking-prose><a href=#checking-prose class=anchor aria-label="Link to self"><svg class=icon height=18 role=img viewBox="0 0 16 16"width=18 aria-hidden=true><path d="M7.775 3.275C7.64252 3.41717 7.57039 3.60522 7.57382 3.79952C7.57725 3.99382 7.65596 4.1792 7.79337 4.31662C7.93079 4.45403 8.11617 4.53274 8.31047 4.53617C8.50477 4.5396 8.69282 4.46748 8.835 4.335L10.085 3.085C10.2708 2.89918 10.4914 2.75177 10.7342 2.65121C10.977 2.55064 11.2372 2.49888 11.5 2.49888C11.7628 2.49888 12.023 2.55064 12.2658 2.65121C12.5086 2.75177 12.7292 2.89918 12.915 3.085C13.1008 3.27082 13.2482 3.49142 13.3488 3.7342C13.4493 3.97699 13.5011 4.23721 13.5011 4.5C13.5011 4.76279 13.4493 5.023 13.3488 5.26579C13.2482 5.50857 13.1008 5.72917 12.915 5.915L10.415 8.415C10.2292 8.60095 10.0087 8.74847 9.76588 8.84911C9.52308 8.94976 9.26283 9.00157 9 9.00157C8.73716 9.00157 8.47691 8.94976 8.23411 8.84911C7.99132 8.74847 7.77074 8.60095 7.585 8.415C7.44282 8.28252 7.25477 8.21039 7.06047 8.21382C6.86617 8.21725 6.68079 8.29596 6.54337 8.43337C6.40596 8.57079 6.32725 8.75617 6.32382 8.95047C6.32039 9.14477 6.39252 9.33282 6.525 9.475C6.85001 9.80004 7.23586 10.0579 7.66052 10.2338C8.08518 10.4097 8.54034 10.5002 9 10.5002C9.45965 10.5002 9.91481 10.4097 10.3395 10.2338C10.7641 10.0579 11.15 9.80004 11.475 9.475L13.975 6.975C14.6314 6.31858 15.0002 5.4283 15.0002 4.5C15.0002 3.57169 14.6314 2.68141 13.975 2.025C13.3186 1.36858 12.4283 0.999817 11.5 0.999817C10.5717 0.999817 9.68141 1.36858 9.02499 2.025L7.775 3.275ZM3.085 12.915C2.89904 12.7292 2.75152 12.5087 2.65088 12.2659C2.55023 12.0231 2.49842 11.7628 2.49842 11.5C2.49842 11.2372 2.55023 10.9769 2.65088 10.7341C2.75152 10.4913 2.89904 10.2707 3.085 10.085L5.585 7.585C5.77074 7.39904 5.99132 7.25152 6.23411 7.15088C6.47691 7.05023 6.73716 6.99842 7 6.99842C7.26283 6.99842 7.52308 7.05023 7.76588 7.15088C8.00867 7.25152 8.22925 7.39904 8.415 7.585C8.55717 7.71748 8.74522 7.7896 8.93952 7.78617C9.13382 7.78274 9.3192 7.70403 9.45662 7.56662C9.59403 7.4292 9.67274 7.24382 9.67617 7.04952C9.6796 6.85522 9.60748 6.66717 9.475 6.525C9.14999 6.19995 8.76413 5.94211 8.33947 5.7662C7.91481 5.59029 7.45965 5.49974 7 5.49974C6.54034 5.49974 6.08518 5.59029 5.66052 5.7662C5.23586 5.94211 4.85001 6.19995 4.525 6.525L2.025 9.02499C1.36858 9.68141 0.999817 10.5717 0.999817 11.5C0.999817 12.4283 1.36858 13.3186 2.025 13.975C2.68141 14.6314 3.57169 15.0002 4.5 15.0002C5.4283 15.0002 6.31858 14.6314 6.975 13.975L8.225 12.725C8.35748 12.5828 8.4296 12.3948 8.42617 12.2005C8.42274 12.0062 8.34403 11.8208 8.20662 11.6834C8.0692 11.546 7.88382 11.4672 7.68952 11.4638C7.49522 11.4604 7.30717 11.5325 7.165 11.665L5.915 12.915C5.72925 13.1009 5.50867 13.2485 5.26588 13.3491C5.02308 13.4498 4.76283 13.5016 4.5 13.5016C4.23716 13.5016 3.97691 13.4498 3.73411 13.3491C3.49132 13.2485 3.27074 13.1009 3.085 12.915Z"fill=currentcolor /></svg></a>Checking prose</h3><p>I did notice a typo in there. So let’s check some prose to prevent that from happening in the future. We can use <a href=/explore/project/retextjs/retext/ class=retext><span class=hl>re</span>text</a> and its ecosystem for our natural language parsing. As we’re writing in English, we use <a href=/explore/package/retext-english/><code>retext-english</code></a> specifically to parse English natural language. The problem in our <code>example.md</code> file is that it has <code>an dwarf planet</code> instead of <code>a dwarf planet</code>, which is conveniently checked for by <a href=/explore/package/retext-indefinite-article/><code>retext-indefinite-article</code></a>. To bridge from markup to prose we use <a href=/explore/package/remark-retext/><code>remark-retext</code></a>. Let’s install these dependencies as well.<pre><code class=language-sh>npm install remark-retext retext-english retext-indefinite-article </code></pre><p>…and change our <code>index.js</code> like so:<pre><code class=language-diff><span class=pl-md>--- a/index.js</span> <span class=pl-mi1>+++ b/index.js</span> <span class=pl-mdr>@@ -3,7 +3,10 @@</span> import rehypeSlug from 'rehype-slug' import rehypeStringify from 'rehype-stringify' import remarkParse from 'remark-parse' import remarkRehype from 'remark-rehype' <span class=pl-mi1>+import remarkRetext from 'remark-retext'</span> import remarkToc from 'remark-toc' <span class=pl-mi1>+import retextEnglish from 'retext-english'</span> <span class=pl-mi1>+import retextIndefiniteArticle from 'retext-indefinite-article'</span> import {read, write} from 'to-vfile' import {unified} from 'unified' import {reporter} from 'vfile-reporter' <span class=pl-mdr>@@ -12,6 +15,8 @@</span> const file = await read('example.md') await unified() .use(remarkParse) <span class=pl-mi1>+ // @ts-expect-error: fine.</span> <span class=pl-mi1>+ .use(remarkRetext, unified().use(retextEnglish).use(retextIndefiniteArticle))</span> .use(remarkToc) .use(remarkRehype) .use(rehypeSlug) </code></pre><p>As the code shows, <code>remark-retext</code> receives another <code>unified</code> pipeline. A natural language pipeline. The plugin will transform the origin syntax (markdown) with the parser defined on the given pipeline. Then it runs the attached plugins on the natural language syntax tree.<p>Now when running our module one final time:<pre><code class=language-sh>$ node index.js example.md 3:10-3:12 warning Unexpected article <span class=pl-s><span class=pl-pds>`</span>an<span class=pl-pds>`</span></span> before <span class=pl-s><span class=pl-pds>`</span>dwarf<span class=pl-pds>`</span></span>, expected <span class=pl-s><span class=pl-pds>`</span>a<span class=pl-pds>`</span></span> retext-indefinite-article retext-indefinite-article ⚠ 1 warning </code></pre><p>…we get a useful message.<p>💃 You’ve got a really cool system set up already. Nicely done! That’s a wrap though, check out the next section for further exercises and resources.<h3 id=further-exercises><a href=#further-exercises class=anchor aria-label="Link to self"><svg class=icon height=18 role=img viewBox="0 0 16 16"width=18 aria-hidden=true><path d="M7.775 3.275C7.64252 3.41717 7.57039 3.60522 7.57382 3.79952C7.57725 3.99382 7.65596 4.1792 7.79337 4.31662C7.93079 4.45403 8.11617 4.53274 8.31047 4.53617C8.50477 4.5396 8.69282 4.46748 8.835 4.335L10.085 3.085C10.2708 2.89918 10.4914 2.75177 10.7342 2.65121C10.977 2.55064 11.2372 2.49888 11.5 2.49888C11.7628 2.49888 12.023 2.55064 12.2658 2.65121C12.5086 2.75177 12.7292 2.89918 12.915 3.085C13.1008 3.27082 13.2482 3.49142 13.3488 3.7342C13.4493 3.97699 13.5011 4.23721 13.5011 4.5C13.5011 4.76279 13.4493 5.023 13.3488 5.26579C13.2482 5.50857 13.1008 5.72917 12.915 5.915L10.415 8.415C10.2292 8.60095 10.0087 8.74847 9.76588 8.84911C9.52308 8.94976 9.26283 9.00157 9 9.00157C8.73716 9.00157 8.47691 8.94976 8.23411 8.84911C7.99132 8.74847 7.77074 8.60095 7.585 8.415C7.44282 8.28252 7.25477 8.21039 7.06047 8.21382C6.86617 8.21725 6.68079 8.29596 6.54337 8.43337C6.40596 8.57079 6.32725 8.75617 6.32382 8.95047C6.32039 9.14477 6.39252 9.33282 6.525 9.475C6.85001 9.80004 7.23586 10.0579 7.66052 10.2338C8.08518 10.4097 8.54034 10.5002 9 10.5002C9.45965 10.5002 9.91481 10.4097 10.3395 10.2338C10.7641 10.0579 11.15 9.80004 11.475 9.475L13.975 6.975C14.6314 6.31858 15.0002 5.4283 15.0002 4.5C15.0002 3.57169 14.6314 2.68141 13.975 2.025C13.3186 1.36858 12.4283 0.999817 11.5 0.999817C10.5717 0.999817 9.68141 1.36858 9.02499 2.025L7.775 3.275ZM3.085 12.915C2.89904 12.7292 2.75152 12.5087 2.65088 12.2659C2.55023 12.0231 2.49842 11.7628 2.49842 11.5C2.49842 11.2372 2.55023 10.9769 2.65088 10.7341C2.75152 10.4913 2.89904 10.2707 3.085 10.085L5.585 7.585C5.77074 7.39904 5.99132 7.25152 6.23411 7.15088C6.47691 7.05023 6.73716 6.99842 7 6.99842C7.26283 6.99842 7.52308 7.05023 7.76588 7.15088C8.00867 7.25152 8.22925 7.39904 8.415 7.585C8.55717 7.71748 8.74522 7.7896 8.93952 7.78617C9.13382 7.78274 9.3192 7.70403 9.45662 7.56662C9.59403 7.4292 9.67274 7.24382 9.67617 7.04952C9.6796 6.85522 9.60748 6.66717 9.475 6.525C9.14999 6.19995 8.76413 5.94211 8.33947 5.7662C7.91481 5.59029 7.45965 5.49974 7 5.49974C6.54034 5.49974 6.08518 5.59029 5.66052 5.7662C5.23586 5.94211 4.85001 6.19995 4.525 6.525L2.025 9.02499C1.36858 9.68141 0.999817 10.5717 0.999817 11.5C0.999817 12.4283 1.36858 13.3186 2.025 13.975C2.68141 14.6314 3.57169 15.0002 4.5 15.0002C5.4283 15.0002 6.31858 14.6314 6.975 13.975L8.225 12.725C8.35748 12.5828 8.4296 12.3948 8.42617 12.2005C8.42274 12.0062 8.34403 11.8208 8.20662 11.6834C8.0692 11.546 7.88382 11.4672 7.68952 11.4638C7.49522 11.4604 7.30717 11.5325 7.165 11.665L5.915 12.915C5.72925 13.1009 5.50867 13.2485 5.26588 13.3491C5.02308 13.4498 4.76283 13.5016 4.5 13.5016C4.23716 13.5016 3.97691 13.4498 3.73411 13.3491C3.49132 13.2485 3.27074 13.1009 3.085 12.915Z"fill=currentcolor /></svg></a>Further exercises</h3><p>Finally, check out the lists of available plugins for <a href=//github.com/retextjs/retext/blob/HEAD/doc/plugins.md rel="nofollow noopener noreferrer">retext</a>, <a href=//github.com/remarkjs/remark/blob/HEAD/doc/plugins.md rel="nofollow noopener noreferrer">remark</a>, and <a href=//github.com/rehypejs/rehype/blob/HEAD/doc/plugins.md rel="nofollow noopener noreferrer">rehype</a>, and try some of them out.<p>If you haven’t already, check out the other articles in the <a href=../../>learn section</a>!</div></main><footer class=container><nav class="flex row-l"><ol class="flex row x-show-l"><li><a href=/ class=unified><span class=hl>uni</span>fied</a><li><a href=/explore/>Explore</a><li><a href=../../>Learn</a><li><a href=/community/>Community</a></ol><ol class="row justify-end-l"><li><a href=/rss.xml>RSS Feed</a><li><a href=//opencollective.com/unified>OpenCollective</a><li><a href=//github.com/unifiedjs>GitHub</a></ol></nav></footer>