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 }): setsheading.id(GitHub-compatible slugs). Useprefix: '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 $6stays text). The closing$is the next unescaped one, wherever it is in the paragraph: in$6 per item, or `$` alonethe$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 ismeta.
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.