mdlib-pure

Samples

Each sample shows its Markdown next to the output, rendered when this site was built by the same library you would install. In your browser, KaTeX typesets the math and Mermaid draws the diagrams, and the last sample is a live editor; everything else is static HTML.

Podcast rundown: a complete custom-plugin sample →
A split editor with participant completion, planned durations and separate supporting documents. Implementation guide.

CommonMark basics

Headings, emphasis, links, nested lists, block quotes, code, entities and escapes: the core alone, no plugins. Raw HTML stays text because html is off by default.

Configuration: createMarkdown()

Markdown

# A first-level heading

A paragraph with *emphasis*, **strong emphasis**, `inline code` and a
[link with a title](https://commonmark.org "CommonMark"). Adjacent lines
make one paragraph; two trailing spaces  
make a hard line break.

## Lists

1. Ordered lists
2. may contain
   - bullet lists,
   - nested

- A [reference link][spec]
- An autolink: <https://spec.commonmark.org/0.31.2/>
- Entities: &copy; &mdash; &frac12;
- Escapes: \*not emphasis\*

> A block quote can hold **any** block.
>
> ```js
> const md = createMarkdown()
> ```

    Code indented by four spaces.

***

<b>Raw HTML</b> stays text: the `html` option is off by default.

[spec]: https://spec.commonmark.org "The CommonMark spec"

Result

A first-level heading

A paragraph with emphasis, strong emphasis, inline code and a link with a title. Adjacent lines make one paragraph; two trailing spaces
make a hard line break.

Lists

  1. Ordered lists
  2. may contain
    • bullet lists,
    • nested

A block quote can hold any block.

const md = createMarkdown()
Code indented by four spaces.

<b>Raw HTML</b> stays text: the html option is off by default.

GitHub Flavored Markdown

Aligned tables, task lists, strikethrough, extended autolinks, footnotes and GitHub alerts, with gfm().

Configuration: createMarkdown({ plugins: gfm() })

Markdown

## Tables

| Syntax | Markdown | Result |
|:--|:--:|--:|
| Emphasis | `*a*` | *a* |
| Strong | `**a**` | **a** |
| Strikethrough | `~~a~~` | ~~a~~ |

## Task lists

- [x] CommonMark 0.31.2
- [x] GFM extensions
- [ ] Your plugin

## Strikethrough and autolinks

The ~~old~~ new renderer. Links without brackets: www.commonmark.org,
https://github.github.com/gfm/ and an address like info@example.com.

## Footnotes

Parsing stays linear on adversarial input.[^adversarial]

[^adversarial]: Checked by `test/pathological.test.ts` in the repository.

## Alerts

> [!NOTE]
> Alerts are blockquotes whose first line is `[!KIND]`.

> [!TIP]
> Neutral colors derive from `currentColor`.

> [!WARNING]
> `tagfilter` is not a sanitizer.

Result

Tables

Syntax Markdown Result
Emphasis *a* a
Strong **a** a
Strikethrough ~~a~~ a

Task lists

  • CommonMark 0.31.2
  • GFM extensions
  • Your plugin

Strikethrough and autolinks

The old new renderer. Links without brackets: www.commonmark.org, https://github.github.com/gfm/ and an address like info@example.com.

Footnotes

Parsing stays linear on adversarial input.1

Alerts

Note

Alerts are blockquotes whose first line is [!KIND].

Tip

Neutral colors derive from currentColor.

Warning

tagfilter is not a sanitizer.

  1. Checked by test/pathological.test.ts in the repository. ↩

Math with KaTeX

The math plugin outputs \(…\) and \[…\], the default delimiters of KaTeX auto-render: here KaTeX typesets the formulas in the browser with no configuration.

Configuration: createMarkdown({ plugins: [math()] })

Markdown

Euler's identity, $e^{i\pi} + 1 = 0$, sits in a line of text.
Double dollars make display math: $$\sum_{k=1}^{n} k = \frac{n(n+1)}{2}$$

$$
\int_0^1 x^2 \, dx = \frac{1}{3}
$$

$$
\begin{pmatrix} a & b \\ c & d \end{pmatrix}
\begin{pmatrix} x \\ y \end{pmatrix}
=
\begin{pmatrix} ax + by \\ cx + dy \end{pmatrix}
$$

Prices like $5 and $6 stay text: a dollar sign after a space never
closes the math.

Result

Euler's identity, \(e^{i\pi} + 1 = 0\), sits in a line of text. Double dollars make display math: \[\sum_{k=1}^{n} k = \frac{n(n+1)}{2}\]

\[\int_0^1 x^2 \, dx = \frac{1}{3}\]
\[\begin{pmatrix} a & b \\ c & d \end{pmatrix} \begin{pmatrix} x \\ y \end{pmatrix} = \begin{pmatrix} ax + by \\ cx + dy \end{pmatrix}\]

Prices like $5 and $6 stay text: a dollar sign after a space never closes the math.

Mermaid diagrams

The mermaid plugin turns ```mermaid fences into <pre class="mermaid">. Here renderMermaid() loads Mermaid only because this page has diagrams, and themes them from the --md-* tokens: switch the color scheme and they are redrawn. Without JavaScript you would see the source.

Configuration: createMarkdown({ plugins: [mermaid()] })

Markdown

How mdlib-pure turns Markdown into HTML:

```mermaid
flowchart TD
  src[Markdown] --> parse[Block and inline parsers]
  parse --> ast[(AST)]
  ast -->|plugins| transforms{{Transforms}}
  transforms --> vnodes[VNodes]
  vnodes --> html[HTML string]
  vnodes --> dom[DOM nodes]
```

And who calls whom when a page renders a diagram:

```mermaid
sequenceDiagram
  participant Page
  participant mdlib as mdlib-pure
  participant Mermaid
  Page->>mdlib: renderTo(el, markdown)
  mdlib-->>Page: <pre class="mermaid">
  Page->>mdlib: renderMermaid(el, { load })
  mdlib->>Mermaid: load(), only if there are diagrams
  Mermaid-->>Page: SVG, colored from the tokens
```

Result

How mdlib-pure turns Markdown into HTML:

flowchart TD
  src[Markdown] --> parse[Block and inline parsers]
  parse --> ast[(AST)]
  ast -->|plugins| transforms{{Transforms}}
  transforms --> vnodes[VNodes]
  vnodes --> html[HTML string]
  vnodes --> dom[DOM nodes]

And who calls whom when a page renders a diagram:

sequenceDiagram
  participant Page
  participant mdlib as mdlib-pure
  participant Mermaid
  Page->>mdlib: renderTo(el, markdown)
  mdlib-->>Page: <pre class="mermaid">
  Page->>mdlib: renderMermaid(el, { load })
  mdlib->>Mermaid: load(), only if there are diagrams
  Mermaid-->>Page: SVG, colored from the tokens

Definition lists

Terms and definitions, Pandoc and PHP Markdown Extra style, rendered as <dl>, <dt> and <dd>.

Configuration: createMarkdown({ plugins: [deflist()] })

Markdown

CommonMark
: The reference specification of Markdown syntax.

GFM
: GitHub Flavored Markdown: CommonMark plus tables, strikethrough,
  autolinks, task lists, footnotes and alerts.

Parser
Renderer
:   Two names, two phases of the same job.

    A second paragraph, indented to the content column.

Result

CommonMark

The reference specification of Markdown syntax.

GFM

GitHub Flavored Markdown: CommonMark plus tables, strikethrough, autolinks, task lists, footnotes and alerts.

Parser
Renderer

Two names, two phases of the same job.

A second paragraph, indented to the content column.

Containers

Fenced ::: blocks with a kind and an optional title, nestable with longer fences. The kind becomes the class md-container-KIND; the look is up to the site.

Configuration: createMarkdown({ plugins: [container()] })

Markdown

::: note
A container without a title takes any block, **even** lists:

- one
- two
:::

::: warning Before you publish
Run `npm run verify`.
:::

:::: details Nested containers
To nest, use more colons on the outside.

::: tip
A container inside another one.
:::
::::

Result

A container without a title takes any block, even lists:

  • one
  • two

Before you publish

Run npm run verify.

Nested containers

To nest, use more colons on the outside.

A container inside another one.

Highlighted text

==text== becomes <mark>, with the flanking rules of emphasis. A == line under a paragraph is still a setext heading.

Configuration: createMarkdown({ plugins: [mark()] })

Markdown

==Highlighted== text becomes `<mark>`, with the same rules as
emphasis: it nests in **==strong==** and *==emphasis==*, and works
inside wo==r==ds too.

These stay text: =one=, ===three===, and == with spaces ==.

A setext heading
==

Result

Highlighted text becomes <mark>, with the same rules as emphasis: it nests in strong and emphasis, and works inside words too.

These stay text: =one=, ===three===, and == with spaces ==.

A setext heading

Front matter

YAML or TOML metadata at the top of a document: kept out of the output, readable with getFrontmatter().

Configuration: createMarkdown({ plugins: [frontmatter()] })

Markdown

---
title: Release notes
date: 2026-10-28
tags: [markdown, commonmark]
---

# Front matter is not rendered

The YAML block at the top stays out of the HTML: `getFrontmatter(root)`
returns its raw text, with no YAML parser (zero dependencies).

Result

Front matter is not rendered

The YAML block at the top stays out of the HTML: getFrontmatter(root) returns its raw text, with no YAML parser (zero dependencies).

getFrontmatter(md.parse(source))

{
  "type": "yaml",
  "value": "title: Release notes\ndate: 2026-10-28\ntags: [markdown, commonmark]"
}

Syntax highlighting hook

The highlight(code, lang, meta) option takes any highlighter. This site uses a tokenizer of a few lines that returns VNodes, so the output stays on the safe path of renderTo().

Configuration

import { createMarkdown, h } from 'mdlib-pure'
import { grammars, tokenize } from './highlight.js' // ~60 lines, site/lib/highlight.js

const md = createMarkdown({
  // VNodes keep renderTo() on its DOM-safe path; null means default rendering
  highlight: (code, lang) =>
    grammars.has(lang)
      ? tokenize(code, grammars.get(lang)).map(([cls, text]) => (cls ? h('span', { class: `tok-${cls}` }, text) : text))
      : null,
})

Markdown

```js
// A minimal highlighter: returns VNodes, not HTML
const md = createMarkdown({
  highlight: (code, lang) => (lang === 'js' ? tokens(code) : null),
})
md.render('# Hello')
```

```css
:root {
  --md-color-link: oklch(55% 0.2 260);
}
```

```sh
npm install mdlib-pure
```

Result

// A minimal highlighter: returns VNodes, not HTML
const md = createMarkdown({
  highlight: (code, lang) => (lang === 'js' ? tokens(code) : null),
})
md.render('# Hello')
:root {
  --md-color-link: oklch(55% 0.2 260);
}
npm install mdlib-pure

Tailwind classes map

Without the theme, the classes option gives each node type its utility classes. The preview uses a real Tailwind CSS v4 build, isolated in a shadow root.

Configuration

createMarkdown({
  classes: {
    heading: 'font-semibold tracking-tight',
    heading2: 'mb-3 text-2xl',
    paragraph: 'my-3 leading-7',
    link: 'font-medium text-(--md-color-link) underline decoration-2 underline-offset-4',
    inlineCode: 'rounded bg-current/10 px-1 font-mono text-sm',
    list: 'my-3 list-disc pl-6 marker:text-(--md-color-link)',
    blockquote: 'my-3 border-l-4 border-(--md-color-link) pl-4 italic',
  },
})

Markdown

## A heading made of utilities

Every node type gets its own classes: no theme, just **Tailwind**,
[links](https://tailwindcss.com) and `inline code` included.

- first item
- second item

> A block quote with a left border.

Result (Tailwind CSS v4 utilities, no theme)

React adapter

mdlib-pure/adapter turns VNodes into React, Preact or Vue elements. This preview was produced by react-dom/server when the site was built.

Configuration

import { createElement } from 'react'
import { createMarkdown } from 'mdlib-pure'
import { gfm } from 'mdlib-pure/gfm'
import { toElements } from 'mdlib-pure/adapter'

const md = createMarkdown({ plugins: gfm() })

export function Markdown({ source }) {
  return <article className="md">{toElements(md.toVNodes(source), createElement, 'react')}</article>
}

Markdown

## From Markdown to React

`toElements()` turns VNodes into React elements with `createElement`.

- [x] `className` instead of `class`
- [x] uncontrolled checkboxes (`defaultChecked`)
- [x] a `key` on top-level elements

Result (react-dom/server)

From Markdown to React

toElements() turns VNodes into React elements with createElement.

  • className instead of class
  • uncontrolled checkboxes (defaultChecked)
  • a key on top-level elements

Web Components, live

mdlib-pure/element defines <mdlib-markdown> and <mdlib-editor>. Both are live below, defined in your browser from the HTML above: the viewer renders its source attribute, the editor renders its preview on every keystroke and sends a debounced input event. KaTeX and Mermaid come from this site, loaded only when the text needs them.

Configuration

import { defineElements } from 'mdlib-pure/element'

// This site's own KaTeX and Mermaid, fetched only on first use
// (simplified from site/client/elements.js)
defineElements({
  loaders: {
    katex: () => loadKatexAutoRender(), // resolves to renderMathInElement
    katexStylesheet: '/assets/katex/katex.min.css',
    mermaid: () => import('/assets/mermaid/mermaid.js'),
  },
})

// Debounced: 200 ms after the last keystroke (the delay attribute)
document.querySelector('mdlib-editor').addEventListener('input', (e) => save(e.target.value))

HTML

<mdlib-markdown class="md" plugins="gfm math"
  source="**Rendered in your browser** from the `source` attribute,
  with GFM and math: ~~strikethrough~~, www.commonmark.org
  and $a^2 + b^2 = c^2$."></mdlib-markdown>

<mdlib-editor plugins="all" label="Sample document" style="block-size: 34rem"
  value="## Edit me

This is a real `<mdlib-editor>`: the preview renders **on every keystroke**, in your browser.
Select a word and press Ctrl+B, or use the toolbar; *Write* and *Preview* switch the view.

- [x] every plugin, with `plugins` set to `all`
- [ ] your own text

KaTeX is loaded on first use: $e^{i\pi} + 1 = 0$.

::: tip Diagrams
Mermaid loads only when a diagram appears, and a diagram is drawn again only when its source changes.
:::

```mermaid
flowchart LR
  type[You type] --> preview[Live preview]
  preview -->|input event, debounced| app[Your app]
```
"></mdlib-editor>

Result (live Web Components)

input events from the editor: none yet