Extending AstroJS Markdown Processing With Remark and Rehype Plugins

Friedrich Kurz - Aug 17 - - Dev Community

AstroJS

AstroJS is a framework for static website building that has a lot of exciting features like

  • reducing page size (and therefore faster loading speed);
  • being framework agnostic (React, Svelte, what have you are all supported);
  • lowering client-side scripting (a lot of processing is done on the server and AstroJS ships pure HTML by default);
  • being component-based and supporting component de-hydration (components are loaded lazily when needed);
  • support for MDX (Markdown + JSX), TypeScript, CSS Pre-Processing, and CSS frameworks (like Tailwind CSS); and
  • integrated SEO automations (like automatic sitemap generation, RSS feeds, and pagination)

đź”— (astro.build) Introducing Astro

Another great aspect of AstroJS is that it supports extensions of Markdown processing via Remark and Rehype plugins.

Remark and Rehype

Both Remark and Rehype are frameworks to transform documents by first transforming them to abstract syntax trees (AST) and then applying pluggable functions to the AST before converting it to the target format.

Remark parses Markdown and MDX files and applies transformations. Rehype parses HTML and applies transformations. Both frameworks have a large ecosystem of community plugins available.

đź’ˇ Remark and Rehype may be combined if we export HTML after transformation with Remark, for example by using the remark-rehype plugin. AstroJS automatically does this for use, so we do not need to worry about the transformation and may directly write plugins for Remark and Rehype.

Extending AstroJS With Remark and Rehype

A Remark or Rehype plugin is a higher-order function—a function returning another function in this case—that takes an options argument for configuration and then returns a function that receives the abstract syntax tree (AST) of the parsed Markdown or HTML document.

The abstract syntax tree for both tools looks something like this:



{
  type: 'root',
  children: [
    { type: ..., children: [Array], position: [Object] },
    // ...
  ],
  position: {
    start: { line: 1, column: 1, offset: 0 },
    end: { line: 44, column: 11, offset: 1201 }
  }
}


Enter fullscreen mode Exit fullscreen mode

Plugins for both Remark and Rehype may be registered in the Markdown or MDX integrations in astro.config.mjs. Below, is an example configuration of the MDX integration with a Remark plugin theRemarkPlugin:



import { defineConfig } from "astro/config";
import theRemarkPlugin from "src/remark/the-remark-plugin"

// https://astro.build/config
export default defineConfig({
  integrations: [
    mdx({
      remarkPlugins: [theRemarkPlugin]
    })
  ]
});


Enter fullscreen mode Exit fullscreen mode

đź’ˇ If we want to add a Remark or Rehype plugin to the Markdown processor, we have to add it to markdown.remarkPlugins respectively markdown.rehypePlugins of the configuration object passed to defineConfig instead.

Example: Enabling rehype-mermaid in AstroJS

Lets, assume we want to add rendered Mermaid diagrams to our page by transforming the source code in fenced code blocks like the following:



```mermaid
flowchart LR

A[Hard] -->|Text| B(Round)
B --> C{Decision}
C -->|One| D[Result 1]
C -->|Two| E[Result 2]
```


Enter fullscreen mode Exit fullscreen mode

Thankfully, a Rehype plugin to generate Mermaid diagrams rehype-mermaid from HTML <pre>or <code> elements with Mermaid diagram source code already exists. Adding it to our Markdown or MDX processing pipeline is therefore as easy as installing it with NPM and adding it to the configuration as shown above.

đź”— (github.com) Rehype Mermaid Plugin

The plugin's documentation, however, states that it only converts <pre class="mermaid"> and <code class="language-mermaid"> elements to rendered diagrams in SVG format.

And, unfortunately, if we add a fenced code block with language mermaid to our post, the element produced by remark is a <pre> element with a class list like astro-code github-dark. To get our rendered Mermaid diagram, we have to add the class mermaid to this element.

With the tooling provided for Rehype via projects in the unist ecosystem, this is luckily rather straight forward. To help finding the nodes of the AST that we are concerned with and transforming them as need, we may use the visit function from unist-util-visit.

This function, may—for simple scenarios like ours—be defined with only two parameters

  1. ast (the AST's root) and
  2. a visitor function that applies our changes.

Omitting some implementation details, our Rehype plugin addMermaidClass to add the mermaid class to Mermaid code blocks then may be written as follows:



import { visit, CONTINUE } from "unist-util-visit"
import type { Plugin } from 'unified';
import type { Root, Element } from 'hast';

/* ... */

const visitor = (node: any) => {
    /* ... */
}

const addMermaidClass: Plugin<void[], Root> = () =>
  (ast: Root) => visit(ast, visitor)


export default addMermaidClass


Enter fullscreen mode Exit fullscreen mode

Let's now take a look at the implementation of the visitor function.



const dataLanguageMermaid = "mermaid"
const typeElement = "element"
const tagNamePre = "pre"
const classMermaid = dataLanguageMermaid

const isPreElement = (node: any) => typeof node.type !== undefined && node.type === typeElement
    && node.tagName !== undefined && node.tagName === tagNamePre
    && node.properties !== undefined && node.properties.dataLanguage === dataLanguageMermaid

const visitor = (node: any) => {
  if(!isPreElement(node)) {
    return CONTINUE
  }

  const element = node as Element
  const properties = element.properties
  const className = properties.className as Array<string>
  properties.className = [...className, classMermaid]

  return CONTINUE
}


Enter fullscreen mode Exit fullscreen mode

The important things to note are

  • the first conditional which assures that we are visiting the AST representation of a <pre> element and skips all other elements and
  • the in-place modification of the node.properties.className array.

đź’ˇ Note that the visit function should return an object of type Action instead of the visited node. In this case, we return the CONTINUE action that tells Remark to continue traversing the AST.

The last step is to import the plugin and add it to the configuration of the MDX integration before the rehypeMermaid plugin. My AstroJS at the time of writing looks like this.



import { defineConfig } from "astro/config";
import icon from "astro-icon";
import tailwind from "@astrojs/tailwind";
import mdx from "@astrojs/mdx";
import rehypeMermaid from 'rehype-mermaid'
import addMermaidClass from "src/rehype/add-mermaid-class"

// https://astro.build/config
export default defineConfig({
  integrations: [
    tailwind(),
    icon(),
    mdx({
      rehypePlugins: [addMermaidClass, rehypeMermaid]
    })
  ]
});


Enter fullscreen mode Exit fullscreen mode

đź’ˇ Adding the addMermaidClass plugin before the rehypeMermaid plugin is important since the array order defines the execution order of the plugins and we need to change the class name before passing the AST on to the rehypeMermaid plugin.

The rendered result of the Mermaid diagram is shown below:

Rendered Mermaid Diagram

. . . . .
Terabox Video Player