Why another Markdown parser


I want to take the opportunity to explain why we build comark, another Markdown parser and renderer, and comark cms, another file-based CMS.

When creating Nuxt (a web framework on top of Vue.js) back in October 2016, we obviously needed to write a documentation, ideally git-based so it can live alongside our codebase, so the obvious choice was to leverage Markdown.

At that time, and still today, most docs are statically generated at build time, we created the ability to do this with the nuxt generate command.

This means two things that always itched me for years:
- the more pages you have, the longer the build will take
- when you update your content, even for a typo, you need to rebuild your whole website

Of course, your website is super fast, and can be hosted almost free on many static hosting platforms.

Nuxt (like many other meta frameworks) can have a (node.js) server running in production, so I always dreamed of having the ability to create an API on top of my GitHub repository and query it like I would query any other CMS in real-time to render the content.

What about using components in Markdown?

Well this is a very good question (lol)! Most solutions like MDX or VitePress actually compile the Markdown to a component at build time in order for you to use your project's components.

We don't want to bring our build toolchain in production or even on client-side just for having the ability to render our Markdown on-demand.

Introducing Comark

Comark is a set of tools on top of Markdown composed of:
- A parser that support plugins and convert the Markdown to minimal and JSON-serializable AST (Abstract Syntax Tree)
- Renderer for multiple UI librairies (Angular, React, Svelte, Vue) but also HTML, ANSI (terminal) and back to Markdown

Comark parser supports all standard CommonMark and GitHub Flavored Markdown (GFM) features by default, as well as frontmatter, components and attributes.

Let's see an example of the output created by comark:

import { parse } from 'comark'

const result = await parse(`---
title: Hello World
---

This is a **simple example**.`)

When inspecting the result variable, we can see the following object:


{
  "frontmatter": {  
    "title": "Hello World"
  },
  "meta": {},
  "nodes": [
    ["p", {}, "This is a ", ["strong", {}, "simple example"], "."]
  ]
}

The frontmatter is our parsed version of our yaml defined at the top of our Markdown.

The meta is empty here, it is used by plugins to inject metadata, think of a read time plugin that could inject a meta.readTime: "5 min" value for example.

The nodes is the actual representation of our Markdown, similar to a virtual DOM (if you are familiar with the Vue, it might remind you the arguments of the h() function!). Our Comark renderer will actually use these nodes to render to the corresponding components.

So how can I render my components?

Comark has a syntax inspired by Markdown directives :: to specify that you want to use a custom html element.

# Hello world

::callout{type="info"}
This is a `<Callout>` with type info.
::

Will become this equivalent:

<h1>Hello world</h1>

<Callout type="info">
This is a <code>Callout</code> with type info.
</Callout>

Why this new syntax?

Most Markdown parsers force you to stick with HTML inside HTML tags or to add blank lines around it when you want to use Markdown in it. With this new syntax, you can use Markdown inside the components, but there's more!

Let's say I have a <Card> component that uses slots:

interface CardProps {
  slotTitle?: React.ReactNode
  children?: React.ReactNode
}

export default function Card({ title, children }: CardProps) {
  return (
    <div className="card">
      {slotTitle && <h2>{slotTitle}</h2>}
      <div className="body">{children}</div>
    </div>
  )
}

Comark let's you define slots within the component using the #{slot} shorthand:

::card
#title
Welcome to **Comark**
#default
Checkout the [documentation](https://comark.dev.
::

This will be transformed to this AST:


{
  "frontmatter": {},
  "meta": {},
  "nodes": [
    [
      "card",
      {},
      [
        "template",
        { "name": "title" },
        "Welcome to ", ["strong", {}, "Comark"]
      ],
      [
        "template",
        { "name": "default" },
        "Checkout the ",
        ["a", { "href": "https://comark.dev" },
        "documentation."
      ],
    ]
  ]
}

So now, how can I render this AST to my web application?

We shipped a <ComarkRenderer> component into various UI librairies (Angular, React, Vue and Svelte) to do so:

import { createParse } from 'comark'
import type { ComarkTree } from 'comark'
import { ComarkRenderer } from '@comark/react'
import Callout from '@/components/Callout'
import Card from '@/components/Card'

export default function Page({ tree }: { tree: ComarkTree }) {
  return <ComarkRenderer tree={tree} components={{ callout: Callout, card: Card }} />
}



Sign in to leave a note.