mdlib-pure
Documentation pages
On this page

API reference #

Entry points (all ES modules, all tree-shakable):

For a complete application built through the plugin API, see the podcast sample: custom tags, an AST-derived duration total and a dedicated split editor. Its plugin is copyable source, not a package entry point.

Import Contents
mdlib-pure createMarkdown, rendering helpers, types
mdlib-pure/entities decodeEntity: full HTML entity table (Node, SSR, workers)
mdlib-pure/gfm gfm() and the single GFM extensions
mdlib-pure/headings headings(), toc(), slug(), slugger(), headingText()
mdlib-pure/container container() for ::: blocks
mdlib-pure/adapter toElements() for React, Preact, Vue
mdlib-pure/element <mdlib-markdown> and <mdlib-editor> Web Components, all plugins included
mdlib-pure/editor.css styles for the two elements
mdlib-pure/math math() for $…$, $$…$$ and $$ blocks
mdlib-pure/mark mark() for ==…==
mdlib-pure/deflist deflist() for definition lists
mdlib-pure/mermaid mermaid(), renderMermaid() for diagrams
mdlib-pure/frontmatter frontmatter(), getFrontmatter()
mdlib-pure/theme.css default theme (tokens)
mdlib-pure/tailwind.css Tailwind CSS v4 bridge

createMarkdown(options?) #

function createMarkdown(options?: Options): Markdown

Builds a configured instance. Rule tables are compiled once here: create the instance once and reuse it.

Options #

Option Type Default
plugins Plugin[] [] see Plugins
html boolean false recognize raw HTML; otherwise it renders as text
sanitize (html: string) => string — applied to render()/renderTo() output and to each raw node of toVNodes()
allowUrl (url, kind: 'link' | 'image') => boolean isSafeUrl rejected URLs become "" (policy)
decodeEntity (name: string) => string | undefined DOM-based use decodeEntity from mdlib-pure/entities outside browsers
classes Record<string, string> — classes per node type; heading1…heading6 for levels
attributes (node) => Props | null — extra attributes per node, merged (class appended)
highlight (code, lang, meta) => string | VChild | VChild[] | null — string = trusted HTML for <code> or a whole <pre>; VNodes = safe
positions boolean false add position to block nodes

Markdown #

Method Returns
parse(src) Root Markdown → AST (plugin transforms applied)
render(src | root) string HTML, CommonMark-exact serialization
renderTo(el, src | root) void replaces el's content; builds DOM directly unless raw HTML is present
toVNodes(src | root) VChild[] for custom serializers and toElements()

render and renderTo accept an AST, so you can parse once, inspect or modify the tree, then render.

AST #

mdast-compatible plain objects. Every node has type; parents have children; with positions, block nodes have position: { start: Point, end: Point } where Point = { line, column, offset } (1-based line and column; end is the point after the node; the end of a block is the end of its last line). Inline nodes carry no position.

Type Fields Notes
root children
paragraph children
heading depth: 1–6, children id with the headings plugin
thematicBreak
blockquote children
list ordered, start: number | null, spread, children spread = loose
listItem spread, children checked with GFM task lists
code lang, meta, value lang/meta from the info string
html value, block?: true only with html: true
text value soft line breaks are \n inside text
emphasis, strong children
inlineCode value
break hard line break
link url, title: string | null, children references are resolved
image url, title, alt

GFM adds table { align } › tableRow › tableCell, delete, footnoteReference, footnoteDefinition, footnotes, taskCheckbox, alert { kind }. Plugins document their own nodes in their header comment.

Rendering helpers #

Export
h(tag, props?, ...children) creates a VNode element; text children are plain strings
raw(html, block?) trusted HTML VNode
toHtml(vnodes) VNodes → HTML string
toDom(vnodes, doc?) VNodes → DocumentFragment (throws on raw HTML)
coreRenderers the default render functions, to wrap or reuse
visit(node, fn) depth-first walk; return false to skip children
isSafeUrl(url, kind) the default URL policy
normalizeLabel(s), normalizeUrl(s) CommonMark label matching and URL encoding
escapeHtml(s) escapes & < > "
MAX_NESTING container/inline nesting cap (100)

Recipe: source positions in the HTML (editors, scroll sync) #

createMarkdown({
  positions: true,
  attributes: (n) => n.position && { 'data-sourcepos': `${n.position.start.line}:${n.position.start.column}-${n.position.end.line}:${n.position.end.column - 1}` },
})

Plugins #

interface Plugin {
  name: string
  block?: BlockRule[]          // block syntax (lines → blocks)
  inline?: InlineRule[]        // syntax starting at trigger characters
  delimiters?: DelimiterRule[] // symmetric runs like ~~ or ==
  transform?(root, ctx): Root | void
  render?: Record<string, RenderFn>
}

Render functions receive (node, r) where r is a RenderContext: r.h, r.raw, r.children(node), r.render(node), r.parent, r.url(url, kind). Transforms receive ctx.parseInline(text), ctx.references, ctx.env.

The complete contracts (parser state, return values, ordering, performance rules) are in AGENTS.md; src/types.ts has them as JSDoc.

mdlib-pure/gfm #

gfm(options?) returns all extensions; each can be disabled: { table, strikethrough, autolink, tasklist, footnotes, tagfilter, alerts } (all true by default). The functions table(), strikethrough(), autolink(), tasklist(), footnotes(), tagfilter(), alerts() are also exported.

mdlib-pure/headings #

  • headings({ prefix?: string, permalink?: boolean }): sets heading.id (GitHub-compatible slugs). Use prefix: 'user-content-' for untrusted input.
  • toc(root) → { depth, id, text }[] (after parsing with the plugin).
  • slug(text), slugger() (de-duplicating), headingText(node).

mdlib-pure/container #

container(): ::: kind Title … ::: blocks, nestable with longer fences. Node container { kind, title, children }, rendered as <div class="md-container md-container-KIND">.

mdlib-pure/math #

math({ render? }).

  • Inline $x^2$ (pandoc rules: no space after the opening $, the closing $ not preceded by a space nor followed by a digit, so $5 and $6 stays text). The closing $ is the next unescaped one, wherever it is in the paragraph: in $6 per item, or `$` alone the $ inside the code span closes math opened at $6. Escape stray dollars (\$6) in text with math.
  • $$…$$ inside a paragraph is display math.
  • A $$ line opens a block (like a code fence); text after it is meta.

Nodes: inlineMath { value, display? }, math { value, meta }. Default HTML (pandoc style, TeX escaped):

<span class="math math-inline">\(e^{i\pi}\)</span>
<span class="math math-display">\[\sum_k a_k\]</span>   <!-- $$…$$ in a paragraph -->
<div class="math math-display">\[\int_0^1 x\,dx\]</div> <!-- $$ block -->

The \(…\)/\[…\] delimiters are the defaults of KaTeX auto-render and MathJax, so either works with no configuration (tested with KaTeX in Chromium):

md.renderTo(el, src)
renderMathInElement(el) // KaTeX auto-render; or MathJax.typesetPromise([el])

Or render at build time with render(tex, display), which returns trusted HTML (string), VNodes or null; its output replaces the delimited TeX:

import katex from 'katex'
math({ render: (tex, display) => katex.renderToString(tex, { displayMode: display, throwOnError: false }) })

Localizing generated text #

Plugins write a few English strings: GFM alert titles (Note, Tip…), the footnote back-link label (Back to reference 1) and the headings permalink label (Permalink: …). Translate them with a render override, which takes precedence over the plugin's renderer when the plugin comes first:

const titles = { note: 'Nota', tip: 'Suggerimento', important: 'Importante', warning: 'Attenzione', caution: 'Cautela' }
createMarkdown({
  plugins: [
    ...gfm(),
    { name: 'it', render: {
      alert: (n, r) => r.h('div', { class: `markdown-alert markdown-alert-${n.kind}` },
        r.h('p', { class: 'markdown-alert-title' }, titles[n.kind]), r.children(n)),
    } },
  ],
})

mdlib-pure/mark #

mark(): ==text== → mark { children } → <mark>. Same flanking rules as *; Title\n== stays a setext heading.

mdlib-pure/mermaid #

mermaid({ render? }) turns ```mermaid fences into mermaid { value } nodes, rendered as <pre class="mermaid">SOURCE</pre>: the markup Mermaid looks for, readable without JavaScript. mdlib-pure does not depend on Mermaid (~215 KB gzip at first load for Mermaid 12).

import { mermaid, renderMermaid } from 'mdlib-pure/mermaid'

const md = createMarkdown({ plugins: [mermaid()] })
md.renderTo(el, src)
const stop = await renderMermaid(el, { load: () => import('mermaid') })
// later, if el is replaced: stop()

renderMermaid(root, options):

Option Default
load required returns Mermaid (module or default export); called only if root has diagrams
theme 'tokens' the --md-* tokens, flattened onto the page background, as Mermaid's base theme; or a Mermaid theme name ('dark', 'neutral'…)
securityLevel 'strict' 'sandbox' renders each diagram in an iframe: consider it for untrusted input
watch true with 'tokens' redraw when the color scheme changes (media query, class/style/data-* on <html>, <meta name="color-scheme">)
config — extra mermaid.initialize() settings

Invalid diagrams keep their source, with the class mermaid-error. The returned function stops watching.

Pre-rendered diagrams (e.g. SVG generated at build time with your own tooling): mermaid({ render: (source) => svgFor(source) }). A string is trusted HTML, VNodes are safe, null keeps the default markup.

mdlib-pure/deflist #

deflist():

Term
: Definition (continuation lines may be lazy)
: Another definition

Term A
Term B
:   Definition with blocks

    Second paragraph, indented to the content column.

The paragraph right above a : line becomes the terms (one per line). One blank line between terms and : is allowed (Pandoc); it makes the list loose. Nodes: definitionList { spread } › definitionTerm (inline) and definitionDescription { spread } (blocks). HTML: <dl>, <dt>, <dd>; tight lists render definitions without <p>.

mdlib-pure/frontmatter #

frontmatter(): YAML (--- … ---/...) or TOML (+++ … +++) on the first line of the document becomes yaml/toml { value }, first child of root, not rendered. Unclosed front matter is not front matter (the document parses as without the plugin). getFrontmatter(root) returns { type, value } | null; the value is raw text (no YAML parser: zero dependencies).

mdlib-pure/element #

defineElements(options) defines two Web Components, <mdlib-markdown> (viewer) and <mdlib-editor> (editor with live preview), with every plugin included (~20 KB gzip); getMarkdown(plugins, config) returns the shared instance behind an attribute combination. Styles: mdlib-pure/editor.css. Attributes, events, loaders, named configs and the editor's behavior: elements.md. Framework wrappers: Blazor, Angular.

mdlib-pure/adapter #

toElements(vnodes, h, preset?: 'react' | 'preact' | 'vue'): Array<Element | string>

Converts md.toVNodes(src) with the framework's own h/createElement, applying its conventions. Top-level elements get a key.

Rendered by mdlib-pure at build time from docs/api.md in the repository. Markdown version.