Aquileo | unified - learnhttps://unifiedjs.com/Sun, 28 Jun 2026 10:48:33 GMT2026-06-28T10:48:33.854ZenenAquileo | Publish a pluginTitus Wormerhttps://unifiedjs.com/learn/guide/publish-plugin/https://unifiedjs.com/learn/guide/publish-plugin/Mon, 19 Aug 2024 00:00:00 GMT2024-08-19T00:00:00.000Z2024-08-19T00:00:00.000Z<div><h2 id="user-content-publish-a-plugin"><a href="https://unifiedjs.com/learn/guide/publish-plugin/#publish-a-plugin" class="" aria-label="Link to self"></a>Publish a plugin</h2><p>This guide shows how to share a plugin with the world.</p><blockquote><p>Stuck? Have an idea for another guide? See <a href="https://github.com/unifiedjs/.github/blob/main/support.md"><code>support.md</code></a>.</p></blockquote><h3 id="user-content-contents"><a href="https://unifiedjs.com/learn/guide/publish-plugin/#contents" class="" aria-label="Link to self"></a>Contents</h3><ul><li><a href="https://unifiedjs.com/learn/guide/publish-plugin/#intro">Intro</a></li><li><a href="https://unifiedjs.com/learn/guide/publish-plugin/#package-name">Package name</a></li><li><a href="https://unifiedjs.com/learn/guide/publish-plugin/#npm">npm</a></li><li><a href="https://unifiedjs.com/learn/guide/publish-plugin/#github">GitHub</a></li><li><a href="https://unifiedjs.com/learn/guide/publish-plugin/#export-map">Export map</a></li><li><a href="https://unifiedjs.com/learn/guide/publish-plugin/#export-default"><code>export default</code></a></li><li><a href="https://unifiedjs.com/learn/guide/publish-plugin/#docs">Docs</a></li><li><a href="https://unifiedjs.com/learn/guide/publish-plugin/#tests">Tests</a></li><li><a href="https://unifiedjs.com/learn/guide/publish-plugin/#license">License</a></li></ul><h3 id="user-content-intro"><a href="https://unifiedjs.com/learn/guide/publish-plugin/#intro" class="" aria-label="Link to self"></a>Intro</h3><p>You may have a <a href="https://unifiedjs.com/explore/package/unified/#plugin">plugin</a> locally, that you use in a project, and think others might find useful too.</p><p>Below you’ll find some rules on how to share plugins with the world.</p><h3 id="user-content-package-name"><a href="https://unifiedjs.com/learn/guide/publish-plugin/#package-name" class="" aria-label="Link to self"></a>Package name</h3><p>The package name must be prefixed with <code>rehype-</code>, <code>remark-</code>, <code>retext-</code>, and so on. The name of the ecosystem. Use this prefix <strong>only</strong> for plugins. For presets, use <code>rehype-preset-</code>, <code>remark-preset-</code>, <code>retext-preset-</code>, and so on.</p><h3 id="user-content-npm"><a href="https://unifiedjs.com/learn/guide/publish-plugin/#npm" class="" aria-label="Link to self"></a>npm</h3><p>Plugins should be published to npm. You are free to publish them elsewhere too.</p><p>You should use a keyword <code>rehype-plugin</code>, <code>remark-plugin</code>, <code>retext-plugin</code>, and so on, to tag your package.</p><h3 id="user-content-github"><a href="https://unifiedjs.com/learn/guide/publish-plugin/#github" class="" aria-label="Link to self"></a>GitHub</h3><p>You may use GitHub. Or an alternative.</p><p>Only GitHub is crawled for the <a href="https://unifiedjs.com/explore/">explore</a> page. You should use a topic <code>rehype-plugin</code>, <code>remark-plugin</code>, <code>retext-plugin</code>, and so on, to tag your repository.</p><h3 id="user-content-export-map"><a href="https://unifiedjs.com/learn/guide/publish-plugin/#export-map" class="" aria-label="Link to self"></a>Export map</h3><p>You must expose a plugin that can be imported from that package name. So, there must be a plugin at <code>rehype-some-plugin</code>.</p><p>There may be several plugins in a package. One example is <a href="https://unifiedjs.com/explore/package/rehype-mathjax/"><code>rehype-mathjax</code></a>, which has plugins exposed as <code>rehype-mathjax/browser</code>, <code>rehype-mathjax/chtml</code>, <code>rehype-mathjax/svg</code>, and <code>rehype-mathjax</code> (as an alias for <code>rehype-mathjax/svg</code>). The reason for different plugins in the same project is that they each use different code. Which impact performance and bundle size.</p><h3 id="user-content-export-default"><a href="https://unifiedjs.com/learn/guide/publish-plugin/#export-default" class="" aria-label="Link to self"></a><code>export default</code></h3><p>Your package must expose a plugin at the default export. It must be a function that must work when passed as <code>x</code> to <code>unified().use(x)</code>.</p><p>There may be different exports too. One example is <a href="https://unifiedjs.com/explore/package/remark-rehype/"><code>remark-rehype</code></a>, which also exposes useful things at <code>defaultFootnoteBackContent</code>, <code>defaultFootnoteBackLabel</code>, and <code>defaultHandlers</code>, but the plugin itself is exported as <code>default</code>.</p><h3 id="user-content-docs"><a href="https://unifiedjs.com/learn/guide/publish-plugin/#docs" class="" aria-label="Link to self"></a>Docs</h3><p>Write a good readme. Explain what the plugin does, when it should be used or not, how to configure it.</p><h3 id="user-content-tests"><a href="https://unifiedjs.com/learn/guide/publish-plugin/#tests" class="" aria-label="Link to self"></a>Tests</h3><p>Add thorough tests. Use a CI. You should test your project in all maintained versions of Node.js.</p><h3 id="user-content-license"><a href="https://unifiedjs.com/learn/guide/publish-plugin/#license" class="" aria-label="Link to self"></a>License</h3><p>Add a license. It should be MIT or ISC, as that aligns with most of the JavaScript ecosystem. It must be an open source license.</p></div>Aquileo | Intro to syntax treesTitus Wormerhttps://unifiedjs.com/learn/guide/introduction-to-syntax-trees/https://unifiedjs.com/learn/guide/introduction-to-syntax-trees/Wed, 14 Aug 2024 00:00:00 GMT2024-08-14T00:00:00.000Z2024-08-14T00:00:00.000Z<div><h2 id="user-content-introduction-to-syntax-trees"><a href="https://unifiedjs.com/learn/guide/introduction-to-syntax-trees/#introduction-to-syntax-trees" class="" aria-label="Link to self"></a>Introduction to syntax trees</h2><p><a href="https://unifiedjs.com/explore/package/unified/" class="">unified</a> uses abstract syntax trees (abbreviated as ASTs), that plugins can work on. This guide introduces what ASTs are and how to work with them.</p><h3 id="user-content-contents"><a href="https://unifiedjs.com/learn/guide/introduction-to-syntax-trees/#contents" class="" aria-label="Link to self"></a>Contents</h3><ul><li><a href="https://unifiedjs.com/learn/guide/introduction-to-syntax-trees/#what-is-an-ast">What is an AST?</a></li><li><a href="https://unifiedjs.com/learn/guide/introduction-to-syntax-trees/#what-is-unist">What is unist?</a></li><li><a href="https://unifiedjs.com/learn/guide/introduction-to-syntax-trees/#when-to-use-an-ast">When to use an AST?</a></li></ul><h3 id="user-content-what-is-an-ast"><a href="https://unifiedjs.com/learn/guide/introduction-to-syntax-trees/#what-is-an-ast" class="" aria-label="Link to self"></a>What is an AST?</h3><p>An abstract syntax tree (AST) is a tree representation of the syntax of programming languages. For us that’s typically markup languages.</p><p>As a JavaScript developer you may already know things that are like ASTs: The DOM and React’s virtual DOM. Or you may have heard of Babel, ESLint, PostCSS, Prettier, or TypeScript. They all use ASTs to inspect and transform code.</p><p>In <a href="https://unifiedjs.com/explore/package/unified/" class="">unified</a>, we support <em>several</em> ASTs. The reason for different ASTs is that each markup language has several aspects that do not translate 1-to-1 to other markup languages. Taking markdown and HTML as an example, in some cases markdown has more info than HTML: markdown has several ways to add a link (“autolinks”: <code>&#x3C;https://url></code>, resource links: <code>[label](url)</code>, and reference links with definitions: <code>[label][id]</code> and <code>[id]: url</code>). In other cases, HTML has more info than markdown. It has many tags, which add new meaning (semantics), that aren’t available in markdown. If there was one AST, it would be quite hard to do the tasks that several <a href="https://unifiedjs.com/explore/project/remarkjs/remark/" class="">remark</a> and <a href="https://unifiedjs.com/explore/project/rehypejs/rehype/" class="">rehype</a> plugins now do.</p><p>See <a href="https://unifiedjs.com/learn/recipe/build-a-syntax-tree/">“How to build a syntax tree”</a> for more info on how to make a tree. See <a href="https://unifiedjs.com/learn/guide/syntax-trees-typescript/">“Syntax trees in TypeScript”</a> on how to work with ASTs in TypeScript.</p><h3 id="user-content-what-is-unist"><a href="https://unifiedjs.com/learn/guide/introduction-to-syntax-trees/#what-is-unist" class="" aria-label="Link to self"></a>What is unist?</h3><p>But all our ASTs have things in common. The bit in common is called <a href="https://github.com/syntax-tree/unist" class="">unist</a>. By having a shared interface, we can also share tools that work on all ASTs. In practice, that means you can use for example <a href="https://unifiedjs.com/explore/package/unist-util-visit/"><code>unist-util-visit</code></a> to visit nodes in any supported AST.</p><p>See <a href="https://unifiedjs.com/learn/recipe/tree-traversal/">“Tree traversal”</a> for more info on <code>unist-util-visit</code>.</p><p><a href="https://github.com/syntax-tree/unist" class="">unist</a> is different from the ASTs used in other tools. Quite noticeable because it uses a particular set of names for things: <code>type</code>, <code>children</code>, <code>position</code>. But perhaps harder to see is that it’s compatible with JSON. It’s all objects and arrays. Strings, numbers. Where other tools use instances with methods, we use plain data. Years ago in <a href="https://unifiedjs.com/explore/project/retextjs/retext/" class="">retext</a> we started out like that too. But we found that we preferred to be able to read and write a tree from/to a JSON file, to treat ASTs as data, and use more functional utilities.</p><h3 id="user-content-when-to-use-an-ast"><a href="https://unifiedjs.com/learn/guide/introduction-to-syntax-trees/#when-to-use-an-ast" class="" aria-label="Link to self"></a>When to use an AST?</h3><p>You can use an AST when you want to inspect or transform content.</p><p>Say you wanted to count the number of headings in a markdown file. You could also do that with a regex:</p><div><pre><code class="language-js">const value = `# Pluto Pluto is a dwarf planet in the Kuiper belt. ## History ### Discovery In the 1840s, Urbain Le Verrier used Newtonian mechanics to predict the position of…` const expression = /^#+[^\r\n]+/gm const headings = [...value.matchAll(expression)].length console.log(headings) //=> 3 </code></pre></div><p>But what if the headings were in a code block? Or if Setext headings were used instead of ATX headings? The grammar of markdown is more complex than a regex can handle. That’s where an AST can help.</p><div><pre><code class="language-js">import {fromMarkdown} from 'mdast-util-from-markdown' import {visit} from 'unist-util-visit' const value = `# Pluto Pluto is a dwarf planet in the Kuiper belt. ## History ### Discovery In the 1840s, Urbain Le Verrier used Newtonian mechanics to predict the position of…` const tree = fromMarkdown(value) let headings = 0 visit(tree, 'heading', function () { headings++ }) console.log(headings) //=> 3 </code></pre></div><p>See <a href="https://unifiedjs.com/learn/recipe/tree-traversal/">“Tree traversal”</a> for more info on <code>unist-util-visit</code>.</p></div>Aquileo | Create a retext pluginTitus Wormerhttps://unifiedjs.com/learn/guide/create-a-retext-plugin/https://unifiedjs.com/learn/guide/create-a-retext-plugin/Tue, 13 Aug 2024 00:00:00 GMT2024-08-13T00:00:00.000Z2024-08-13T00:00:00.000Z<div><h2 id="user-content-create-a-retext-plugin"><a href="https://unifiedjs.com/learn/guide/create-a-retext-plugin/#create-a-retext-plugin" class="" aria-label="Link to self"></a>Create a retext plugin</h2><p>This guide shows how to create a plugin for <a href="https://unifiedjs.com/explore/project/retextjs/retext/" class="">retext</a> that checks the amount of spaces between sentences.</p><blockquote><p>Stuck? Have an idea for another guide? See <a href="https://github.com/unifiedjs/.github/blob/main/support.md"><code>support.md</code></a>.</p></blockquote><h3 id="user-content-contents"><a href="https://unifiedjs.com/learn/guide/create-a-retext-plugin/#contents" class="" aria-label="Link to self"></a>Contents</h3><ul><li><a href="https://unifiedjs.com/learn/guide/create-a-retext-plugin/#case">Case</a></li><li><a href="https://unifiedjs.com/learn/guide/create-a-retext-plugin/#setting-up">Setting up</a></li><li><a href="https://unifiedjs.com/learn/guide/create-a-retext-plugin/#plugin">Plugin</a></li><li><a href="https://unifiedjs.com/learn/guide/create-a-retext-plugin/#further-exercises">Further exercises</a></li></ul><h3 id="user-content-case"><a href="https://unifiedjs.com/learn/guide/create-a-retext-plugin/#case" class="" aria-label="Link to self"></a>Case</h3><p>Before we start, let’s first outline what we want to make. Say we have the following text file:</p><pre><code class="language-markdown">One sentence. Two sentences. One sentence. Two sentences. </code></pre><p>We want to get a warning for the second paragraph, saying that one space instead of two spaces should be used.</p><p>In the next step we’ll write the code to use our plugin.</p><h3 id="user-content-setting-up"><a href="https://unifiedjs.com/learn/guide/create-a-retext-plugin/#setting-up" class="" aria-label="Link to self"></a>Setting up</h3><p>Let’s set up a project. Create a folder, <code>example</code>, enter it, and initialize a new project:</p><pre><code class="language-sh">mkdir example cd example npm init -y </code></pre><p>Then make sure the project is a module, so that <code>import</code> and <code>export</code> work, by changing <code>package.json</code>:</p><pre><code class="language-diff">--- a/package.json +++ b/package.json @@ -1,6 +1,7 @@ { "name": "example", "version": "1.0.0", + "type": "module", "main": "noindex.js", "scripts": { "test": "echo \"Error: no test specified\" &#x26;&#x26; exit 1" </code></pre><p>Make sure <code>example.md</code> exists with:</p><pre><code class="language-markdown">One sentence. Two sentences. One sentence. Two sentences. </code></pre><p>Now, let’s create an <code>example.js</code> file that will process our text file and report any found problems.</p><div><pre><code class="language-js">import fs from 'node:fs/promises' import {retext} from 'retext' import {reporter} from 'vfile-reporter' import retextSentenceSpacing from './plugin.js' const document = await fs.readFile('example.md', 'utf8') const file = await retext() .use(retextSentenceSpacing) .process(document) console.error(reporter(file)) </code></pre></div><blockquote><p>Don’t forget to <code>npm install retext vfile-reporter</code>!</p></blockquote><p>If you read the guide on <a href="https://unifiedjs.com/learn/guide/using-unified/">using unified</a>, 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 report either a fatal error or any found linting messages.</p><p>Note that we directly depend on <code>retext</code>. This is a package that exposes a <code>unified</code> processor, and comes with the parser and compiler attached.</p><p>When running our example (it doesn’t work yet though) we want to see a message for the second paragraph, saying that one space instead of two spaces should be used.</p><p>Now we’ve got everything set up except for the plugin itself. We’ll do that in the next section.</p><h3 id="user-content-plugin"><a href="https://unifiedjs.com/learn/guide/create-a-retext-plugin/#plugin" class="" aria-label="Link to self"></a>Plugin</h3><p>We’ll need a plugin, and for our case also a transform which will inspect. Let’s create them in our plugin file <code>plugin.js</code>:</p><div><pre><code class="language-js">/** * @import {Root} from 'nlcst' * @import {VFile} from 'vfile' */ export default function retextSentenceSpacing() { /** * @param {Root} tree * @param {VFile} file * @return {undefined} */ return function (tree, file) { } } </code></pre></div><p>First things first, we need to check <code>tree</code> for a pattern. We can use a utility to help us to recursively walk our tree, namely <a href="https://unifiedjs.com/explore/package/unist-util-visit/"><code>unist-util-visit</code></a>. Let’s add that.</p><pre><code class="language-diff">--- a/plugin.js +++ b/plugin.js @@ -3,6 +3,8 @@ * @import {VFile} from 'vfile' */ +import {visit} from 'unist-util-visit' + export default function retextSentenceSpacing() { /** * @param {Root} tree @@ -10,5 +12,8 @@ export default function retextSentenceSpacing() { * @return {undefined} */ return function (tree, file) { + visit(tree, 'ParagraphNode', function (node) { + console.log(node) + }) } } </code></pre><blockquote><p>Don’t forget to <code>npm install unist-util-visit</code>.</p></blockquote><p>If we now run our example with Node.js, as follows, we’ll see that visitor is called with both paragraphs in our example:</p><pre><code class="language-sh">node example.js </code></pre><pre><code class="language-text">{ type: 'ParagraphNode', children: [ { type: 'SentenceNode', children: [Array], position: [Object] }, { type: 'WhiteSpaceNode', value: ' ', position: [Object] }, { type: 'SentenceNode', children: [Array], position: [Object] } ], position: { start: { line: 1, column: 1, offset: 0 }, end: { line: 1, column: 29, offset: 28 } } } { type: 'ParagraphNode', children: [ { type: 'SentenceNode', children: [Array], position: [Object] }, { type: 'WhiteSpaceNode', value: ' ', position: [Object] }, { type: 'SentenceNode', children: [Array], position: [Object] } ], position: { start: { line: 3, column: 1, offset: 30 }, end: { line: 3, column: 30, offset: 59 } } } no issues found </code></pre><p>This output already shows that paragraphs contain two types of nodes: <code>SentenceNode</code> and <code>WhiteSpaceNode</code>. The latter is what we want to check, but the former is important because we only warn about whitespace between sentences in this plugin (that could be another plugin though).</p><p>Let’s now loop through the children of each paragraph. Only checking whitespace between sentences. We use a small utility for checking node types: <a href="https://unifiedjs.com/explore/package/unist-util-is/"><code>unist-util-is</code></a>.</p><pre><code class="language-diff">--- a/plugin.js +++ b/plugin.js @@ -13,7 +13,23 @@ export default function retextSentenceSpacing() { */ return function (tree, file) { visit(tree, 'ParagraphNode', function (node) { - console.log(node) + let index = -1 + + while (++index &#x3C; node.children.length) { + const previous = node.children[index - 1] + const child = node.children[index] + const next = node.children[index + 1] + + if ( + previous &#x26;&#x26; + next &#x26;&#x26; + previous.type === 'SentenceNode' &#x26;&#x26; + child.type === 'WhiteSpaceNode' &#x26;&#x26; + next.type === 'SentenceNode' + ) { + console.log(child) + } + } }) } } </code></pre><p>If we now run our example with Node, as follows, we’ll see that only whitespace between sentences is logged.</p><pre><code class="language-sh">node example.js </code></pre><pre><code class="language-text">{ type: 'WhiteSpaceNode', value: ' ', position: { start: { line: 1, column: 14, offset: 13 }, end: { line: 1, column: 15, offset: 14 } } } { type: 'WhiteSpaceNode', value: ' ', position: { start: { line: 3, column: 14, offset: 43 }, end: { line: 3, column: 16, offset: 45 } } } no issues found </code></pre><p>Finally, let’s add a warning for the second whitespace, as it has more characters than needed. We can use <a href="https://unifiedjs.com/explore/package/vfile/#vfilemessagereason-options"><code>file.message()</code></a> to associate a message with the file.</p><pre><code class="language-diff">--- a/plugin.js +++ b/plugin.js @@ -25,9 +25,15 @@ export default function retextSentenceSpacing() { next &#x26;&#x26; previous.type === 'SentenceNode' &#x26;&#x26; child.type === 'WhiteSpaceNode' &#x26;&#x26; - next.type === 'SentenceNode' + next.type === 'SentenceNode' &#x26;&#x26; + child.value.length !== 1 ) { - console.log(child) + file.message( + 'Unexpected `' + + child.value.length + + '` spaces between sentences, expected `1` space', + child + ) } } }) </code></pre><p>If we now run our example one final time, we’ll see a message for our problem!</p><pre><code class="language-sh">$ node example.js 3:14-3:16 warning Unexpected `2` spaces between sentences, expected `1` space ⚠ 1 warning </code></pre><h3 id="user-content-further-exercises"><a href="https://unifiedjs.com/learn/guide/create-a-retext-plugin/#further-exercises" class="" aria-label="Link to self"></a>Further exercises</h3><p>One space between sentences isn’t for everyone. This plugin could receive the preferred amount of spaces instead of a hard-coded <code>1</code>.</p><p>If you want to warn for tabs or newlines between sentences, maybe create a plugin for that too?</p><p>If you haven’t already, check out the other articles in the <a href="https://unifiedjs.com/learn/">learn section</a>!</p></div>Aquileo | Create a remark pluginTitus Wormerhttps://unifiedjs.com/learn/guide/create-a-remark-plugin/https://unifiedjs.com/learn/guide/create-a-remark-plugin/Tue, 13 Aug 2024 00:00:00 GMT2024-08-13T00:00:00.000Z2024-08-13T00:00:00.000Z<div><h2 id="user-content-create-a-remark-plugin"><a href="https://unifiedjs.com/learn/guide/create-a-remark-plugin/#create-a-remark-plugin" class="" aria-label="Link to self"></a>Create a remark plugin</h2><p>This guide shows how to create a plugin for <a href="https://unifiedjs.com/explore/project/remarkjs/remark/" class="">remark</a> that turns emoji shortcodes (<a href="https://github.com/wooorm/gemoji/blob/main/support.md">gemoji</a>, such as <code>:+1:</code>) into Unicode emoji (<code>👍</code>). It looks for a regex in the text and replaces it.</p><blockquote><p>Stuck? Have an idea for another guide? See <a href="https://github.com/unifiedjs/.github/blob/main/support.md"><code>support.md</code></a>.</p></blockquote><h3 id="user-content-contents"><a href="https://unifiedjs.com/learn/guide/create-a-remark-plugin/#contents" class="" aria-label="Link to self"></a>Contents</h3><ul><li><a href="https://unifiedjs.com/learn/guide/create-a-remark-plugin/#case">Case</a></li><li><a href="https://unifiedjs.com/learn/guide/create-a-remark-plugin/#setting-up">Setting up</a></li><li><a href="https://unifiedjs.com/learn/guide/create-a-remark-plugin/#plugin">Plugin</a></li></ul><h3 id="user-content-case"><a href="https://unifiedjs.com/learn/guide/create-a-remark-plugin/#case" class="" aria-label="Link to self"></a>Case</h3><p>Before we start, let’s first outline what we want to make. Say we have the following file:</p><pre><code class="language-markdown">Look, the moon :new_moon_with_face: </code></pre><p>And we’d like to turn that into:</p><pre><code class="language-markdown">Look, the moon 🌚 </code></pre><p>In the next step we’ll write the code to use our plugin.</p><h3 id="user-content-setting-up"><a href="https://unifiedjs.com/learn/guide/create-a-remark-plugin/#setting-up" class="" aria-label="Link to self"></a>Setting up</h3><p>Let’s set up a project. Create a folder, <code>example</code>, enter it, and initialize a new project:</p><pre><code class="language-sh">mkdir example cd example npm init -y </code></pre><p>Then make sure the project is a module, so that <code>import</code> and <code>export</code> work, by changing <code>package.json</code>:</p><pre><code class="language-diff">--- a/package.json +++ b/package.json @@ -1,6 +1,7 @@ { "name": "example", "version": "1.0.0", + "type": "module", "main": "noindex.js", "scripts": { "test": "echo \"Error: no test specified\" &#x26;&#x26; exit 1" </code></pre><p>Make sure <code>input.md</code> exists with:</p><pre><code class="language-markdown">Look, the moon :new_moon_with_face: </code></pre><p>Now, let’s create an <code>example.js</code> file that will process our file and report any found problems.</p><div><pre><code class="language-js">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)) </code></pre></div><blockquote><p>Don’t forget to <code>npm install remark</code>!</p></blockquote><p>If you read the guide on <a href="https://unifiedjs.com/learn/guide/using-unified/">using unified</a>, 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.</p><p>Note that we directly depend on <code>remark</code>. This is a package that exposes a <code>unified</code> processor, and comes with the markdown parser and markdown compiler attached.</p><p>Now we’ve got everything set up except for the plugin itself. We’ll do that in the next section.</p><h3 id="user-content-plugin"><a href="https://unifiedjs.com/learn/guide/create-a-remark-plugin/#plugin" class="" aria-label="Link to self"></a>Plugin</h3><p>We’ll need a plugin, and for our case also a transform. Let’s create them in our plugin file <code>plugin.js</code>:</p><div><pre><code class="language-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) { } } </code></pre></div><p>That’s how most plugins start. A function that returns another function.</p><p>For this use case, we could walk the tree and replace nodes with <a href="https://unifiedjs.com/explore/package/unist-util-visit/"><code>unist-util-visit</code></a>, which is how many plugins work. But a different utility is even simpler: <a href="https://github.com/syntax-tree/mdast-util-find-and-replace"><code>mdast-util-find-and-replace</code></a>. It looks for a regex and lets you then replace that match.</p><p>Let’s add that.</p><pre><code class="language-diff">--- 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) + } + ]) } } </code></pre><blockquote><p>Don’t forget to <code>npm install mdast-util-find-and-replace</code>!</p></blockquote><p>If we now run our example with Node.js, we’ll see that <code>console.log</code> is called:</p><pre><code class="language-sh">node example.js </code></pre><pre><code class="language-text">[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] ] } } </code></pre><p>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.</p><p>We can look that name up to find the corresponding Unicode emoji. We can use the <a href="https://github.com/wooorm/gemoji/blob/main/support.md"><code>gemoji</code></a> package for that. It exposes a <code>nameToEmoji</code> record.</p><pre><code class="language-diff">--- 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 } ]) } </code></pre><blockquote><p>Don’t forget to <code>npm install gemoji</code>!</p></blockquote><p>If we now run our example again with Node…</p><pre><code class="language-sh">node example.js </code></pre><p>…and open <code>output.md</code>, we’ll see that the shortcode is replaced with the emoji!</p><pre><code class="language-markdown">Look, the moon 🌚 </code></pre><p>That’s it! For a complete version of this plugin, see <a href="https://unifiedjs.com/explore/package/remark-gemoji/"><code>remark-gemoji</code></a>.</p><p>If you haven’t already, check out the other articles in the <a href="https://unifiedjs.com/learn/">learn section</a>!</p></div>Aquileo | Create a rehype pluginTitus Wormerhttps://unifiedjs.com/learn/guide/create-a-rehype-plugin/https://unifiedjs.com/learn/guide/create-a-rehype-plugin/Tue, 13 Aug 2024 00:00:00 GMT2024-08-13T00:00:00.000Z2024-08-13T00:00:00.000Z<div><h2 id="user-content-create-a-rehype-plugin"><a href="https://unifiedjs.com/learn/guide/create-a-rehype-plugin/#create-a-rehype-plugin" class="" aria-label="Link to self"></a>Create a rehype plugin</h2><p>This guide shows how to create a plugin for <a href="https://unifiedjs.com/explore/project/rehypejs/rehype/" class="">rehype</a> that adds <code>id</code> attributes to headings.</p><blockquote><p>Stuck? Have an idea for another guide? See <a href="https://github.com/unifiedjs/.github/blob/main/support.md"><code>support.md</code></a>.</p></blockquote><h3 id="user-content-contents"><a href="https://unifiedjs.com/learn/guide/create-a-rehype-plugin/#contents" class="" aria-label="Link to self"></a>Contents</h3><ul><li><a href="https://unifiedjs.com/learn/guide/create-a-rehype-plugin/#case">Case</a></li><li><a href="https://unifiedjs.com/learn/guide/create-a-rehype-plugin/#setting-up">Setting up</a></li><li><a href="https://unifiedjs.com/learn/guide/create-a-rehype-plugin/#plugin">Plugin</a></li></ul><h3 id="user-content-case"><a href="https://unifiedjs.com/learn/guide/create-a-rehype-plugin/#case" class="" aria-label="Link to self"></a>Case</h3><p>Before we start, let’s first outline what we want to make. Say we have the following file:</p><pre><code class="language-html">&#x3C;h1>Solar System&#x3C;/h1> &#x3C;h2>Formation and evolution&#x3C;/h2> &#x3C;h2>Structure and composition&#x3C;/h2> &#x3C;h3>Orbits&#x3C;/h3> &#x3C;h3>Composition&#x3C;/h3> &#x3C;h3>Distances and scales&#x3C;/h3> &#x3C;h3>Interplanetary environment&#x3C;/h3> &#x3C;p>…&#x3C;/p> </code></pre><p>And we’d like to turn that into:</p><pre><code class="language-html">&#x3C;h1 id="solar-system">Solar System&#x3C;/h1> &#x3C;h2 id="formation-and-evolution">Formation and evolution&#x3C;/h2> &#x3C;h2 id="structure-and-composition">Structure and composition&#x3C;/h2> &#x3C;h3 id="orbits">Orbits&#x3C;/h3> &#x3C;h3 id="composition">Composition&#x3C;/h3> &#x3C;h3 id="distances-and-scales">Distances and scales&#x3C;/h3> &#x3C;h3 id="interplanetary-environment">Interplanetary environment&#x3C;/h3> &#x3C;p>…&#x3C;/p> </code></pre><p>In the next step we’ll write the code to use our plugin.</p><h3 id="user-content-setting-up"><a href="https://unifiedjs.com/learn/guide/create-a-rehype-plugin/#setting-up" class="" aria-label="Link to self"></a>Setting up</h3><p>Let’s set up a project. Create a folder, <code>example</code>, enter it, and initialize a new project:</p><pre><code class="language-sh">mkdir example cd example npm init -y </code></pre><p>Then make sure the project is a module, so that <code>import</code> and <code>export</code> work, by changing <code>package.json</code>:</p><pre><code class="language-diff">--- a/package.json +++ b/package.json @@ -1,6 +1,7 @@ { "name": "example", "version": "1.0.0", + "type": "module", "main": "noindex.js", "scripts": { "test": "echo \"Error: no test specified\" &#x26;&#x26; exit 1" </code></pre><p>Make sure <code>input.html</code> exists with:</p><pre><code class="language-html">&#x3C;h1>Solar System&#x3C;/h1> &#x3C;h2>Formation and evolution&#x3C;/h2> &#x3C;h2>Structure and composition&#x3C;/h2> &#x3C;h3>Orbits&#x3C;/h3> &#x3C;h3>Composition&#x3C;/h3> &#x3C;h3>Distances and scales&#x3C;/h3> &#x3C;h3>Interplanetary environment&#x3C;/h3> &#x3C;p>…&#x3C;/p> </code></pre><p>Now, let’s create an <code>example.js</code> file that will process our file and report any found problems.</p><div><pre><code class="language-js">import fs from 'node:fs/promises' import {rehype} from 'rehype' import rehypeSlug from './plugin.js' const document = await fs.readFile('input.html', 'utf8') const file = await rehype() .data('settings', {fragment: true}) .use(rehypeSlug) .process(document) await fs.writeFile('output.html', String(file)) </code></pre></div><blockquote><p>Don’t forget to <code>npm install rehype</code>!</p></blockquote><p>If you read the guide on <a href="https://unifiedjs.com/learn/guide/using-unified/">using unified</a>, 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.</p><p>Note that we directly depend on <code>rehype</code>. This is a package that exposes a <code>unified</code> processor, and comes with the HTML parser and HTML compiler attached.</p><p>Now we’ve got everything set up except for the plugin itself. We’ll do that in the next section.</p><h3 id="user-content-plugin"><a href="https://unifiedjs.com/learn/guide/create-a-rehype-plugin/#plugin" class="" aria-label="Link to self"></a>Plugin</h3><p>We’ll need a plugin and for our case also a transform. Let’s create them in our plugin file <code>plugin.js</code>:</p><div><pre><code class="language-js">/** * @import {Root} from 'hast' */ /** * Add `id`s to headings. * * @returns * Transform. */ export default function rehypeSlug() { /** * @param {Root} tree * @return {undefined} */ return function (tree) { } } </code></pre></div><p>That’s how most plugins start. A function that returns another function.</p><p>Next, for this use case, we can walk the tree and change nodes with <a href="https://unifiedjs.com/explore/package/unist-util-visit/"><code>unist-util-visit</code></a>. That’s how many plugins work.</p><p>Let’s start there, to use <code>unist-util-visit</code> to look for headings:</p><pre><code class="language-diff">--- a/plugin.js +++ b/plugin.js @@ -2,6 +2,8 @@ * @import {Root} from 'hast' */ +import {visit} from 'unist-util-visit' + /** * Add `id`s to headings. * @@ -14,5 +16,17 @@ export default function rehypeSlug() { * @return {undefined} */ return function (tree) { + visit(tree, 'element', function (node) { + if ( + node.tagName === 'h1' || + node.tagName === 'h2' || + node.tagName === 'h3' || + node.tagName === 'h4' || + node.tagName === 'h5' || + node.tagName === 'h6' + ) { + console.log(node) + } + }) } } </code></pre><blockquote><p>Don’t forget to <code>npm install unist-util-visit</code>!</p></blockquote><p>If we now run our example with Node.js, we’ll see that <code>console.log</code> is called:</p><pre><code class="language-sh">node example.js </code></pre><pre><code class="language-text">{ type: 'element', tagName: 'h1', properties: {}, children: [ { type: 'text', value: 'Solar System', position: [Object] } ], position: … } { type: 'element', tagName: 'h2', properties: {}, children: [ { type: 'text', value: 'Formation and evolution', position: [Object] } ], position: … } … </code></pre><p>This output shows that we find our heading element. That’s what we want.</p><p>Next we want to get a string representation of what is inside the headings. There’s another utility for that: <a href="https://unifiedjs.com/explore/package/hast-util-to-string/"><code>hast-util-to-string</code></a>.</p><pre><code class="language-diff">--- a/plugin.js +++ b/plugin.js @@ -2,6 +2,7 @@ * @import {Root} from 'hast' */ +import {toString} from 'hast-util-to-string' import {visit} from 'unist-util-visit' /** @@ -25,7 +26,8 @@ export default function rehypeSlug() { node.tagName === 'h5' || node.tagName === 'h6' ) { - console.log(node) + const value = toString(node) + console.log(value) } }) } </code></pre><blockquote><p>Don’t forget to <code>npm install hast-util-to-string</code>!</p></blockquote><p>If we now run our example with Node.js, we’ll see the text printed:</p><pre><code class="language-sh">node example.js </code></pre><pre><code class="language-text">Solar System Formation and evolution Structure and composition Orbits Composition Distances and scales Interplanetary environment </code></pre><p>Then we want to turn that text into slugs. You have many options here. For this case, we’ll use <a href="https://github.com/Flet/github-slugger"><code>github-slugger</code></a>.</p><pre><code class="language-diff">--- a/plugin.js +++ b/plugin.js @@ -3,6 +3,7 @@ */ import {toString} from 'hast-util-to-string' +import Slugger from 'github-slugger' import {visit} from 'unist-util-visit' /** @@ -17,6 +18,8 @@ export default function rehypeSlug() { * @return {undefined} */ return function (tree) { + const slugger = new Slugger() + visit(tree, 'element', function (node) { if ( node.tagName === 'h1' || @@ -27,7 +30,8 @@ export default function rehypeSlug() { node.tagName === 'h6' ) { const value = toString(node) - console.log(value) + const id = slugger.slug(value) + console.log(id) } }) } </code></pre><blockquote><p>Don’t forget to <code>npm install github-slugger</code>!</p></blockquote><p>The reason <code>const slugger = new Slugger()</code> is there, is because we want to create a new slugger for each document. If we’d create it outside of the function, we’d reuse the same slugger for each document, which would lead to slugs from different documents being mixed. That becomes a problem for documents with the same headings.</p><p>If we now run our example with Node.js, we’ll see the slugs printed:</p><pre><code class="language-sh">node example.js </code></pre><pre><code class="language-text">solar-system formation-and-evolution structure-and-composition orbits composition distances-and-scales interplanetary-environment </code></pre><p>Finally, we want to add the <code>id</code> to the heading elements. This is also a good time to make sure we don’t overwrite existing <code>id</code>s.</p><pre><code class="language-diff">--- a/plugin.js +++ b/plugin.js @@ -22,16 +22,17 @@ export default function rehypeSlug() { visit(tree, 'element', function (node) { if ( - node.tagName === 'h1' || - node.tagName === 'h2' || - node.tagName === 'h3' || - node.tagName === 'h4' || - node.tagName === 'h5' || - node.tagName === 'h6' + !node.properties.id &#x26;&#x26; + (node.tagName === 'h1' || + node.tagName === 'h2' || + node.tagName === 'h3' || + node.tagName === 'h4' || + node.tagName === 'h5' || + node.tagName === 'h6') ) { const value = toString(node) const id = slugger.slug(value) - console.log(id) + node.properties.id = id } }) } </code></pre><p>If we now run our example again with Node…</p><pre><code class="language-sh">node example.js </code></pre><p>…and open <code>output.html</code>, we’ll see that the IDs are there!</p><pre><code class="language-html">&#x3C;h1 id="solar-system">Solar System&#x3C;/h1> &#x3C;h2 id="formation-and-evolution">Formation and evolution&#x3C;/h2> &#x3C;h2 id="structure-and-composition">Structure and composition&#x3C;/h2> &#x3C;h3 id="orbits">Orbits&#x3C;/h3> &#x3C;h3 id="composition">Composition&#x3C;/h3> &#x3C;h3 id="distances-and-scales">Distances and scales&#x3C;/h3> &#x3C;h3 id="interplanetary-environment">Interplanetary environment&#x3C;/h3> &#x3C;p>…&#x3C;/p> </code></pre><p>That’s it! For a complete version of this plugin, see <a href="https://unifiedjs.com/explore/package/rehype-slug/"><code>rehype-slug</code></a>.</p><p>If you haven’t already, check out the other articles in the <a href="https://unifiedjs.com/learn/">learn section</a>!</p></div>Aquileo | unified in the browserTitus Wormerhttps://unifiedjs.com/learn/guide/unified-in-the-browser/https://unifiedjs.com/learn/guide/unified-in-the-browser/Fri, 09 Aug 2024 00:00:00 GMT2024-08-09T00:00:00.000Z2024-08-09T00:00:00.000Z<div><h2 id="user-content-unified-in-the-browser"><a href="https://unifiedjs.com/learn/guide/unified-in-the-browser/#unified-in-the-browser" class="" aria-label="Link to self"></a>unified in the browser</h2><p><a href="https://unifiedjs.com/explore/package/unified/" class="">unified</a> is many different projects that are maintained on GitHub and distributed mainly through npm. Almost all the projects can be used anywhere: in Bun, Deno, Node.js, on the edge, or in browsers. To use our projects in a browser, you need to do one or two things. And there’s different ways to go about it.</p><h3 id="user-content-contents"><a href="https://unifiedjs.com/learn/guide/unified-in-the-browser/#contents" class="" aria-label="Link to self"></a>Contents</h3><ul><li><a href="https://unifiedjs.com/learn/guide/unified-in-the-browser/#bundle">Bundle</a></li><li><a href="https://unifiedjs.com/learn/guide/unified-in-the-browser/#cdn">CDN</a></li></ul><h3 id="user-content-bundle"><a href="https://unifiedjs.com/learn/guide/unified-in-the-browser/#bundle" class="" aria-label="Link to self"></a>Bundle</h3><p>A common way to use <a href="https://unifiedjs.com/explore/package/unified/" class="">unified</a> in the browser is to bundle it with a bundler. You perhaps know bundlers already: webpack, Rollup, or esbuild. You might be using one. Or otherwise have a favorite. If not, <a href="https://esbuild.github.io/">esbuild</a> is a good choice.</p><p>Bundling is almost always a good idea. It gives end users a single file to download. Often minified.</p><p>Say we have some code using <a href="https://unifiedjs.com/explore/package/unified/" class="">unified</a> in <code>example.js</code>:</p><div><pre><code class="language-js">import rehypeStringify from 'rehype-stringify' import remarkParse from 'remark-parse' import remarkRehype from 'remark-rehype' import {unified} from 'unified' const file = await unified() .use(remarkParse) .use(remarkRehype) .use(rehypeStringify) .process('Hello, *world*!') console.log(String(file)) </code></pre></div><p>And you want to use that in some HTML called <code>index.html</code>:</p><pre><code class="language-html">&#x3C;!doctype html> &#x3C;meta charset=utf8> &#x3C;title>Example&#x3C;/title> &#x3C;script src=example.min.js type=module>&#x3C;/script> </code></pre><p>To make <code>example.js</code> work in the browser, you can bundle it with esbuild. First, set up a package. Go to the folder in your terminal and run:</p><pre><code class="language-sh">npm init --yes npm install esbuild --save-dev rehype-stringify remark-parse remark-rehype unified </code></pre><p>Then, bundle <code>example.js</code>:</p><pre><code class="language-sh">npx esbuild --bundle --format=esm --minify --outfile=example.min.js example.js </code></pre><p>Now, open <code>index.html</code> in a browser. When you open the console of your developer tools, you should see <code>Hello, &#x3C;em>world&#x3C;/em>!</code></p><p>You probably also want to configure the target environment for the browsers that you support. That way, JavaScript syntax which is too new for some browsers, will be transformed into older JavaScript syntax that works. Pass the <a href="https://esbuild.github.io/api/#target"><code>--target</code></a> flag to do this.</p><h3 id="user-content-cdn"><a href="https://unifiedjs.com/learn/guide/unified-in-the-browser/#cdn" class="" aria-label="Link to self"></a>CDN</h3><p>If you don’t want to bundle <a href="https://unifiedjs.com/explore/package/unified/" class="">unified</a> yourself, you can use a CDN.</p><p>A CDN hosts files for you. And they can process them for you as well. The nice thing is that you do not have to install and bundle things yourself. The downside is that you’re dependent on a particular server that you do not control.</p><p>One such CDN is <a href="https://esm.sh/">esm.sh</a>. Like the code above, you can use it in a browser like this:</p><pre><code class="language-html">&#x3C;!doctype html> &#x3C;meta charset=utf8> &#x3C;title>Example&#x3C;/title> &#x3C;script type=module> import rehypeStringify from 'https://esm.sh/rehype-stringify@10?bundle' import remarkParse from 'https://esm.sh/remark-parse@11?bundle' import remarkRehype from 'https://esm.sh/remark-rehype@11?bundle' import {unified} from 'https://esm.sh/unified@11?bundle' const file = await unified() .use(remarkParse) .use(remarkRehype) .use(rehypeStringify) .process('Hello, *world*!') console.log(String(file)) &#x3C;/script> </code></pre></div>Aquileo | Using pluginsTitus Wormerhttps://unifiedjs.com/learn/guide/using-plugins/https://unifiedjs.com/learn/guide/using-plugins/Thu, 08 Aug 2024 00:00:00 GMT2024-08-08T00:00:00.000Z2024-08-08T00:00:00.000Z<div><h2 id="user-content-using-plugins"><a href="https://unifiedjs.com/learn/guide/using-plugins/#using-plugins" class="" aria-label="Link to self"></a>Using plugins</h2><p>You can use plugins and presets to extend <a href="https://unifiedjs.com/explore/package/unified/" class="">unified</a> by calling <a href="https://unifiedjs.com/explore/package/unified/#processoruseplugin-options"><code>use</code></a> on a processor.</p><p>A small example is:</p><div><pre><code class="language-js">import rehypeDocument from 'rehype-document' import rehypeFormat from 'rehype-format' import rehypeStringify from 'rehype-stringify' import remarkParse from 'remark-parse' import remarkRehype from 'remark-rehype' import {read} from 'to-vfile' import {unified} from 'unified' const file = await read('example.md') await unified() .use(remarkParse) .use(remarkRehype) .use(rehypeDocument, {title: '👋🌍'}) .use(rehypeFormat) .use(rehypeStringify) .process(file) console.log(String(file)) </code></pre></div><p>This example shows how several plugins are used. It shows that the order in which plugins are used is important. And it shows that plugins can be configured by passing options to them. In this case, <code>rehypeDocument</code> receives a <code>title</code> field.</p><p>Using presets is similar:</p><div><pre><code class="language-js">import rehypeParse from 'rehype-parse' import rehypePresetMinify from 'rehype-preset-minify' import rehypeStringify from 'rehype-stringify' import {read} from 'to-vfile' import {unified} from 'unified' const file = await read('example.html') await unified() .use(rehypeParse) .use(rehypePresetMinify) .use(rehypeStringify) .process(file) console.log(String(file)) </code></pre></div><p>Presets themselves cannot receive options. Sometimes, you still want to pass options to a particular plugin in a preset. To configure a plugin in a preset, use it after the preset, with the correct options:</p><div><pre><code class="language-js">import rehypeMinifyWhitespace from 'rehype-minify-whitespace' import rehypeParse from 'rehype-parse' import rehypePresetMinify from 'rehype-preset-minify' import rehypeStringify from 'rehype-stringify' import {read} from 'to-vfile' import {unified} from 'unified' const file = await read('example.html') await unified() .use(rehypeParse) .use(rehypePresetMinify) .use(rehypeMinifyWhitespace, {newlines: true}) .use(rehypeStringify) .process(file) console.log(String(file)) </code></pre></div></div>Aquileo | HTML and remarkTitus Wormerhttps://unifiedjs.com/learn/recipe/remark-html/https://unifiedjs.com/learn/recipe/remark-html/Tue, 09 Mar 2021 00:00:00 GMT2021-03-09T00:00:00.000Z2024-08-02T00:00:00.000Z<div><h2 id="user-content-html-and-remark"><a href="https://unifiedjs.com/learn/recipe/remark-html/#html-and-remark" class="" aria-label="Link to self"></a>HTML and remark</h2><p><a href="https://unifiedjs.com/explore/project/remarkjs/remark/" class="">remark</a> is a markdown compiler. It’s focus is markdown. It’s concerned with HTML in two ways:</p><ol><li>markdown is often turned into HTML</li><li>markdown sometimes has embedded HTML</li></ol><p>When dealing with HTML and markdown, both <a href="https://unifiedjs.com/explore/project/remarkjs/remark/" class="">remark</a> and <a href="https://unifiedjs.com/explore/project/rehypejs/rehype/" class="">rehype</a> are used. This article shows some examples of how to do that.</p><h3 id="user-content-contents"><a href="https://unifiedjs.com/learn/recipe/remark-html/#contents" class="" aria-label="Link to self"></a>Contents</h3><ul><li><a href="https://unifiedjs.com/learn/recipe/remark-html/#how-to-turn-markdown-into-html">How to turn markdown into HTML</a></li><li><a href="https://unifiedjs.com/learn/recipe/remark-html/#how-to-turn-html-into-markdown">How to turn HTML into markdown</a></li><li><a href="https://unifiedjs.com/learn/recipe/remark-html/#how-to-allow-html-embedded-in-markdown">How to allow HTML embedded in markdown</a></li><li><a href="https://unifiedjs.com/learn/recipe/remark-html/#how-to-properly-support-html-inside-markdown">How to properly support HTML inside markdown</a></li></ul><h3 id="user-content-how-to-turn-markdown-into-html"><a href="https://unifiedjs.com/learn/recipe/remark-html/#how-to-turn-markdown-into-html" class="" aria-label="Link to self"></a>How to turn markdown into HTML</h3><p><a href="https://unifiedjs.com/explore/project/remarkjs/remark/" class="">remark</a> handles markdown: it can parse and serialize it. But it’s <strong>not</strong> for HTML. That’s what <a href="https://unifiedjs.com/explore/project/rehypejs/rehype/" class="">rehype</a> does, which exists to parse and serialize HTML.</p><p>To turn markdown into HTML, we need <a href="https://unifiedjs.com/explore/package/remark-parse/"><code>remark-parse</code></a>, <a href="https://unifiedjs.com/explore/package/remark-rehype/"><code>remark-rehype</code></a>, and <a href="https://unifiedjs.com/explore/package/rehype-stringify/"><code>rehype-stringify</code></a>:</p><div><pre><code class="language-js">import rehypeStringify from 'rehype-stringify' import remarkParse from 'remark-parse' import remarkRehype from 'remark-rehype' import {unified} from 'unified' const file = await unified() .use(remarkParse) // Parse markdown content to a syntax tree .use(remarkRehype) // Turn markdown syntax tree to HTML syntax tree, ignoring embedded HTML .use(rehypeStringify) // Serialize HTML syntax tree .process('*emphasis* and **strong**') console.log(String(file)) </code></pre></div><p>This turns <code>*emphasis* and **strong**</code> into <code>&#x3C;em>emphasis&#x3C;/em> and &#x3C;strong>strong&#x3C;/strong></code>, but it does not support HTML embedded inside markdown (such as <code>*emphasis* and &#x3C;strong>strong&#x3C;/strong></code>).</p><p>This solution <strong>is safe</strong>: content you don’t trust cannot cause an XSS vulnerability.</p><h3 id="user-content-how-to-turn-html-into-markdown"><a href="https://unifiedjs.com/learn/recipe/remark-html/#how-to-turn-html-into-markdown" class="" aria-label="Link to self"></a>How to turn HTML into markdown</h3><p>We can also do the inverse. To turn HTML into markdown, we need <a href="https://unifiedjs.com/explore/package/rehype-parse/"><code>rehype-parse</code></a>, <a href="https://unifiedjs.com/explore/package/rehype-remark/"><code>rehype-remark</code></a>, and <a href="https://unifiedjs.com/explore/package/remark-stringify/"><code>remark-stringify</code></a>:</p><div><pre><code class="language-js">import rehypeParse from 'rehype-parse' import rehypeRemark from 'rehype-remark' import remarkStringify from 'remark-stringify' import {unified} from 'unified' const file = await unified() .use(rehypeParse) // Parse HTML to a syntax tree .use(rehypeRemark) // Turn HTML syntax tree to markdown syntax tree .use(remarkStringify) // Serialize HTML syntax tree .process('&#x3C;em>emphasis&#x3C;/em> and &#x3C;strong>strong&#x3C;/strong>') console.log(String(file)) </code></pre></div><p>This turns <code>&#x3C;em>emphasis&#x3C;/em> and &#x3C;strong>strong&#x3C;/strong></code> into <code>*emphasis* and **strong**</code>.</p><h3 id="user-content-how-to-allow-html-embedded-in-markdown"><a href="https://unifiedjs.com/learn/recipe/remark-html/#how-to-allow-html-embedded-in-markdown" class="" aria-label="Link to self"></a>How to allow HTML embedded in markdown</h3><p>Markdown is a content format that’s great for the more basic things: it’s nicer to write <code>*emphasis*</code> than <code>&#x3C;em>emphasis&#x3C;/em></code>. But, it’s limited: only a couple things are supported with its terse syntax. Luckily, for more complex things, markdown allows HTML inside it. A common example of this is to include a <code>&#x3C;details></code> element.</p><p>HTML embedded in markdown can be allowed when going from markdown to HTML by configuring <a href="https://unifiedjs.com/explore/package/remark-rehype/"><code>remark-rehype</code></a> and <a href="https://unifiedjs.com/explore/package/rehype-stringify/"><code>rehype-stringify</code></a>:</p><div><pre><code class="language-js">import rehypeStringify from 'rehype-stringify' import remarkParse from 'remark-parse' import remarkRehype from 'remark-rehype' import {unified} from 'unified' const file = await unified() .use(remarkParse) .use(remarkRehype, {allowDangerousHtml: true}) // Pass raw HTML strings through. .use(rehypeStringify, {allowDangerousHtml: true}) // Serialize the raw HTML strings .process('*emphasis* and &#x3C;strong>strong&#x3C;/strong>') console.log(String(file)) </code></pre></div><p>This solution <strong>is not safe</strong>: content you don’t trust can cause XSS vulnerabilities.</p><h3 id="user-content-how-to-properly-support-html-inside-markdown"><a href="https://unifiedjs.com/learn/recipe/remark-html/#how-to-properly-support-html-inside-markdown" class="" aria-label="Link to self"></a>How to properly support HTML inside markdown</h3><p>To properly support HTML embedded inside markdown, we need another plugin: <a href="https://unifiedjs.com/explore/package/rehype-raw/"><code>rehype-raw</code></a>. This plugin will take the strings of HTML embedded in markdown and parse them with an actual HTML parser.</p><div><pre><code class="language-js">import rehypeRaw from 'rehype-raw' import rehypeStringify from 'rehype-stringify' import remarkParse from 'remark-parse' import remarkRehype from 'remark-rehype' import {unified} from 'unified' const file = await unified() .use(remarkParse) .use(remarkRehype, {allowDangerousHtml: true}) .use(rehypeRaw) // *Parse* the raw HTML strings embedded in the tree .use(rehypeStringify) .process('*emphasis* and &#x3C;strong>strong&#x3C;/strong>') console.log(String(file)) </code></pre></div><p>This solution <strong>is not safe</strong>: content you don’t trust can cause XSS vulnerabilities.</p><p>But because we now have a complete HTML syntax tree, we can sanitize that tree. For a safe solution, add <a href="https://unifiedjs.com/explore/package/rehype-sanitize/"><code>rehype-sanitize</code></a> right before <code>rehype-stringify</code>.</p></div>Aquileo | Support tables in remarkTitus Wormerhttps://unifiedjs.com/learn/recipe/remark-table/https://unifiedjs.com/learn/recipe/remark-table/Wed, 24 Feb 2021 00:00:00 GMT2021-02-24T00:00:00.000Z2024-08-02T00:00:00.000Z<div><h2 id="user-content-how-to-support-tables-in-remark"><a href="https://unifiedjs.com/learn/recipe/remark-table/#how-to-support-tables-in-remark" class="" aria-label="Link to self"></a>How to support tables in remark</h2><p>Tables are a non-standard feature in markdown: they are <strong>not</strong> defined in <a href="https://commonmark.org/">CommonMark</a> and will not work everywhere.</p><p>Tables are an extension that GitHub supports in their <a href="https://github.github.com/gfm/">GFM</a>. They work on <code>github.com</code> in most places: a readme, issue, PR, discussion, comment, etc.</p><p><a href="https://unifiedjs.com/explore/project/remarkjs/remark/" class="">remark</a> and <a href="https://unifiedjs.com/explore/package/unified/" class="">unified</a> can support them through a plugin: <a href="https://github.com/remarkjs/remark-gfm"><code>remark-gfm</code></a>.</p><h3 id="user-content-contents"><a href="https://unifiedjs.com/learn/recipe/remark-table/#contents" class="" aria-label="Link to self"></a>Contents</h3><ul><li><a href="https://unifiedjs.com/learn/recipe/remark-table/#what-are-tables">What are tables?</a></li><li><a href="https://unifiedjs.com/learn/recipe/remark-table/#how-to-write-tables">How to write tables</a></li><li><a href="https://unifiedjs.com/learn/recipe/remark-table/#how-to-support-tables">How to support tables</a></li><li><a href="https://unifiedjs.com/learn/recipe/remark-table/#how-to-support-tables-in-react-markdown">How to support tables in <code>react-markdown</code></a></li></ul><h3 id="user-content-what-are-tables"><a href="https://unifiedjs.com/learn/recipe/remark-table/#what-are-tables" class="" aria-label="Link to self"></a>What are tables?</h3><p>Tables in markdown are used for tabular data and look like this:</p><pre><code class="language-markdown">| Beep | No. | Boop | | :--- | :----: | -----: | | beep | 1024 | xyz | | boop | 338845 | tuv | | foo | 10106 | qrstuv | | bar | 45 | lmno | </code></pre><p>The result on a website would look something like this:</p><table><thead><tr><th align="left">Beep</th><th align="center">No.</th><th align="right">Boop</th></tr></thead><tbody><tr><td align="left">beep</td><td align="center">1024</td><td align="right">xyz</td></tr><tr><td align="left">boop</td><td align="center">338845</td><td align="right">tuv</td></tr><tr><td align="left">foo</td><td align="center">10106</td><td align="right">qrstuv</td></tr><tr><td align="left">bar</td><td align="center">45</td><td align="right">lmno</td></tr></tbody></table><h3 id="user-content-how-to-write-tables"><a href="https://unifiedjs.com/learn/recipe/remark-table/#how-to-write-tables" class="" aria-label="Link to self"></a>How to write tables</h3><p>Use pipe characters (<code>|</code>) between cells in a row. A new line starts a new row. You don’t have to align the pipes (<code>|</code>) to form a nice grid. But it does make the source more readable.</p><p>The first row is the <em>table header</em> and its cells are the labels for their respective column.</p><p>The second row is the <em>alignment row</em> and there must be as many cells in it as in the header row. Each “cell” must include a dash (<code>-</code>). A cell can be aligned left with a colon at the start (<code>:-</code>), aligned right with a colon at the end (<code>-:</code>), or aligned center with colons at the start and end (<code>:-:</code>). This alignment cell is used to align all corresponding cells in its column.</p><p>Further rows are the <em>table body</em> and are optional. Their cells are the table data.</p><h3 id="user-content-how-to-support-tables"><a href="https://unifiedjs.com/learn/recipe/remark-table/#how-to-support-tables" class="" aria-label="Link to self"></a>How to support tables</h3><p>As tables are non-standard, <a href="https://unifiedjs.com/explore/project/remarkjs/remark/" class="">remark</a> does not support them by default. But it can support them with a plugin: <a href="https://github.com/remarkjs/remark-gfm"><code>remark-gfm</code></a>. Let’s say we have some markdown with a GFM table, in an <code>example.md</code> file:</p><pre><code class="language-markdown"># Table | Branch | Commit | | ------- | ---------------- | | main | 0123456789abcdef | | staging | fedcba9876543210 | </code></pre><p>And a module set up to transform markdown with tables to HTML, <code>example.js</code>:</p><div><pre><code class="language-js">import fs from 'node:fs/promises' import rehypeStringify from 'rehype-stringify' import remarkGfm from 'remark-gfm' 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) // Parse markdown. .use(remarkGfm) // Support GFM (tables, autolinks, tasklists, strikethrough). .use(remarkRehype) // Turn it into HTML. .use(rehypeStringify) // Serialize HTML. .process(document) console.log(String(file)) </code></pre></div><p>Now, running <code>node example.js</code> yields:</p><pre><code class="language-html">&#x3C;h1>Table&#x3C;/h1> &#x3C;table> &#x3C;thead> &#x3C;tr> &#x3C;th>Branch&#x3C;/th> &#x3C;th>Commit&#x3C;/th> &#x3C;/tr> &#x3C;/thead> &#x3C;tbody> &#x3C;tr> &#x3C;td>main&#x3C;/td> &#x3C;td>0123456789abcdef&#x3C;/td> &#x3C;/tr> &#x3C;tr> &#x3C;td>staging&#x3C;/td> &#x3C;td>fedcba9876543210&#x3C;/td> &#x3C;/tr> &#x3C;/tbody> &#x3C;/table> </code></pre><h3 id="user-content-how-to-support-tables-in-react-markdown"><a href="https://unifiedjs.com/learn/recipe/remark-table/#how-to-support-tables-in-react-markdown" class="" aria-label="Link to self"></a>How to support tables in <code>react-markdown</code></h3><p>As tables are non-standard, <code>react-markdown</code> does not support them by default. But it can support them with a plugin: <a href="https://github.com/remarkjs/remark-gfm"><code>remark-gfm</code></a>.</p><p>Let’s say we have some markdown with a GFM table, in an <code>example.md</code> file:</p><div><pre><code class="language-js">/// &#x3C;reference lib="dom" /> // ---cut--- import React from 'react' import {createRoot} from 'react-dom/client' import Markdown from 'react-markdown' import remarkGfm from 'remark-gfm' const markdown = `| Branch | Commit | | - | - | | main | 0123456789abcdef |` createRoot(document.body).render( &#x3C;Markdown remarkPlugins={[remarkGfm]}>{markdown}&#x3C;/Markdown> ) </code></pre></div><p>Yields in JSX:</p><pre><code class="language-tsx">console.log( &#x3C;> &#x3C;h1>Table&#x3C;/h1> &#x3C;table> &#x3C;thead> &#x3C;tr> &#x3C;th>Branch&#x3C;/th> &#x3C;th>Commit&#x3C;/th> &#x3C;/tr> &#x3C;/thead> &#x3C;tbody> &#x3C;tr> &#x3C;td>main&#x3C;/td> &#x3C;td>0123456789abcdef&#x3C;/td> &#x3C;/tr> &#x3C;/tbody> &#x3C;/table> &#x3C;/> ) </code></pre></div>Aquileo | Remove a nodeTitus Wormerhttps://unifiedjs.com/learn/recipe/remove-node/https://unifiedjs.com/learn/recipe/remove-node/Mon, 15 Jun 2020 00:00:00 GMT2020-06-15T00:00:00.000Z2024-08-02T00:00:00.000Z<div><h2 id="user-content-how-to-remove-a-node"><a href="https://unifiedjs.com/learn/recipe/remove-node/#how-to-remove-a-node" class="" aria-label="Link to self"></a>How to remove a node</h2><p>Once you have found the node(s) you want to remove (see <a href="https://unifiedjs.com/learn/recipe/tree-traversal/">tree traversal</a>), you can remove them.</p><h3 id="user-content-contents"><a href="https://unifiedjs.com/learn/recipe/remove-node/#contents" class="" aria-label="Link to self"></a>Contents</h3><ul><li><a href="https://unifiedjs.com/learn/recipe/remove-node/#prerequisites">Prerequisites</a></li><li><a href="https://unifiedjs.com/learn/recipe/remove-node/#removing-a-node">Removing a node</a></li><li><a href="https://unifiedjs.com/learn/recipe/remove-node/#replacing-a-node-with-its-children">Replacing a node with its children</a></li></ul><h3 id="user-content-prerequisites"><a href="https://unifiedjs.com/learn/recipe/remove-node/#prerequisites" class="" aria-label="Link to self"></a>Prerequisites</h3><ul><li><a href="https://unifiedjs.com/learn/recipe/tree-traversal/">Tree traversal</a> — intro on how to walk trees and find specific nodes with <code>unist-util-visit</code></li></ul><h3 id="user-content-removing-a-node"><a href="https://unifiedjs.com/learn/recipe/remove-node/#removing-a-node" class="" aria-label="Link to self"></a>Removing a node</h3><p>For the most part, removing nodes has to do with finding them first (see <a href="https://unifiedjs.com/learn/recipe/tree-traversal/">tree traversal</a>), so let’s say we already have some code to find all <code>emphasis</code> nodes.</p><p>First, our <code>example.md</code> file:</p><pre><code class="language-md">Some text with *emphasis*. Another paragraph with **importance** (and *more emphasis*). </code></pre><p>And a module, <code>example.js</code>:</p><div><pre><code class="language-js">import fs from 'node:fs/promises' import remarkParse from 'remark-parse' import {unified} from 'unified' import {visit} from 'unist-util-visit' const document = await fs.readFile('example.md', 'utf8') const tree = unified().use(remarkParse).parse(document) visit(tree, 'emphasis', function (node) { console.log(node) }) </code></pre></div><p>Now, running <code>node example.js</code> yields (ignoring positions for brevity):</p><pre><code class="language-js">{ type: 'emphasis', children: [ { type: 'text', value: 'emphasis', position: [Object] } ] } { type: 'emphasis', children: [ { type: 'text', value: 'more emphasis', position: [Object] } ] } </code></pre><p>As the above log shows, nodes are objects. Each node is inside an array at the <code>children</code> property of another node. In other words, to remove a node, it must be removed from its parents <code>children</code>.</p><p>The problem then is to remove a value from an array. Standard JavaScript <a href="https://developer.mozilla.org/docs/JavaScript/Reference/Global_Objects/Array">Array functions</a> can be used: namely, <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/splice"><code>splice</code></a>.</p><p>We have the emphasis nodes, but we don’t have their parent, or the position in the parent’s <code>children</code> field they are in. Luckily, the function given to <code>visit</code> gets not only <code>node</code>, but also that <code>index</code> and <code>parent</code>:</p><pre><code class="language-diff">--- a/example.js +++ b/example.js @@ -7,6 +7,6 @@ const document = await fs.readFile('example.md', 'utf8') const tree = unified().use(remarkParse).parse(document) -visit(tree, 'emphasis', function (node) { - console.log(node) +visit(tree, 'emphasis', function (node, index, parent) { + console.log(node.type, index, parent?.type) }) </code></pre><p>Yields:</p><pre><code class="language-text">emphasis 1 paragraph emphasis 3 paragraph </code></pre><p><code>parent</code> is a reference to the parent of <code>node</code>, <code>index</code> is the position at which <code>node</code> is in <code>parent</code>’s <code>children</code>. With this information, and <code>splice</code>, we can now remove emphasis nodes:</p><pre><code class="language-diff">--- a/example.js +++ b/example.js @@ -8,5 +8,9 @@ const document = await fs.readFile('example.md', 'utf8') const tree = unified().use(remarkParse).parse(document) visit(tree, 'emphasis', function (node, index, parent) { - console.log(node.type, index, parent?.type) + if (typeof index !== 'number' || !parent) return + // Note: this is buggy, see next section. + parent.children.splice(index, 1) }) + +console.log(tree) </code></pre><p>Yields:</p><pre><code class="language-js">{ type: 'root', children: [ { type: 'paragraph', children: [ {type: 'text', value: 'Some text with '}, {type: 'text', value: '.'} ] }, { type: 'paragraph', children: [ {type: 'text', value: 'Another paragraph with '}, {type: 'strong', children: [Array]}, {type: 'text', value: ' (and '}, {type: 'text', value: ').'} ] } ] } </code></pre><p>This looks great, but beware of bugs. We are now changing the tree, while traversing it. That can cause bugs and performance problems.</p><p>When changing the tree, in most cases you should signal to <code>visit</code> how it should continue. More information on how to signal what to do next, is documented in <a href="https://unifiedjs.com/explore/package/unist-util-visit-parents/#visitparentstree-test-visitor-reverse"><code>unist-util-visit-parents</code></a>.</p><p>In this case, we don’t want the removed node to be traversed (we want to skip it). And we want to continue with the node that is now at the position where our removed node was. To do that: return that information from <code>visitor</code>:</p><pre><code class="language-diff">--- a/example.js +++ b/example.js @@ -1,7 +1,7 @@ import fs from 'node:fs/promises' import remarkParse from 'remark-parse' import {unified} from 'unified' -import {visit} from 'unist-util-visit' +import {SKIP, visit} from 'unist-util-visit' const document = await fs.readFile('example.md', 'utf8') @@ -9,8 +9,9 @@ const tree = unified().use(remarkParse).parse(document) visit(tree, 'emphasis', function (node, index, parent) { if (typeof index !== 'number' || !parent) return - // Note: this is buggy, see next section. parent.children.splice(index, 1) + // Do not traverse `node`, continue at the node *now* at `index`. + return [SKIP, index] }) console.log(tree) </code></pre><p>This yields the same output as before, but there’s no bug anymore. Nice, we can now remove nodes!</p><h3 id="user-content-replacing-a-node-with-its-children"><a href="https://unifiedjs.com/learn/recipe/remove-node/#replacing-a-node-with-its-children" class="" aria-label="Link to self"></a>Replacing a node with its children</h3><p>One more thing to make this example more useful: instead of dropping <code>emphasis</code> <em>and</em> its children, it might make more sense to replace the emphasis <em>with</em> its children.</p><p>To do that, we can do the following:</p><pre><code class="language-diff">--- a/example.js +++ b/example.js @@ -9,7 +9,7 @@ const tree = unified().use(remarkParse).parse(document) visit(tree, 'emphasis', function (node, index, parent) { if (typeof index !== 'number' || !parent) return - parent.children.splice(index, 1) + parent.children.splice(index, 1, ...node.children) // Do not traverse `node`, continue at the node *now* at `index`. return [SKIP, index] }) </code></pre><p>Yields:</p><pre><code class="language-js">{ type: 'root', children: [ { type: 'paragraph', children: [ {type: 'text', value: 'Some text with '}, {type: 'text', value: 'emphasis'}, {type: 'text', value: '.'} ] }, { type: 'paragraph', children: [ {type: 'text', value: 'Another paragraph with '}, {type: 'strong', children: [Array]}, {type: 'text', value: ' (and '}, {type: 'text', value: 'more emphasis'}, {type: 'text', value: ').'} ] } ] } </code></pre></div>