# mdlib-pure: full documentation > Lightweight, zero-dependency Markdown renderer for the browser: 100% CommonMark 0.31.2 (652/652 spec examples), GitHub Flavored Markdown as an opt-in plugin (54/54 extension examples), an ~11 KB gzipped core, safe by default, linear time on any input. Extensible through plugins, styled with semantic CSS tokens, output as HTML, DOM, React/Preact/Vue elements, or the and Web Components, which the MdlibPure.Blazor (.NET 10) and ngx-mdlib-pure (Angular) wrappers build on. Not on a package registry yet (no npm, no NuGet): the Install section of each guide says how to install it today. This file concatenates the documentation pages of https://michelepolo.github.io/mdlib-pure/: the repository's README.md, docs/api.md, docs/elements.md, bindings/blazor/README.md, bindings/angular/README.md, docs/tokens.md, docs/podcast.md, docs/security.md, AGENTS.md and CHANGELOG.md, plus the site's page for AI agents. Version 0.1.1. # mdlib-pure Lightweight Markdown renderer for the browser. **Website, docs and playground: ** · for AI agents: [llms.txt](https://michelepolo.github.io/mdlib-pure/llms.txt) - **100% CommonMark 0.31.2**: all 652 spec examples pass - **GFM** as an opt-in plugin (~4 KB): tables, strikethrough, autolinks, task lists, footnotes, alerts. 54/54 GFM extension examples pass - **Zero dependencies**, ~11 KB gzipped core - **Safe by default**: raw HTML off, `javascript:` URLs blocked, DOM output without HTML parsing - **Extensible**: block, inline and delimiter rules, AST transforms, render overrides - **Framework-neutral output**: HTML string, DOM nodes, or React/Vue/Preact elements - **Tailwind-friendly**: semantic HTML with no classes by default, or a class map per node type - **Linear time** on adversarial input - MIT licensed ## Install **Not on the npm registry yet**: `npm install mdlib-pure` does not work. Until the first release there, install the tarball attached to the [GitHub release](https://github.com/MichelePolo/mdlib-pure/releases/tag/mdlib-pure-v0.1.1): ```sh npm install https://github.com/MichelePolo/mdlib-pure/releases/download/mdlib-pure-v0.1.1/mdlib-pure-0.1.1.tgz ``` npm records the URL in your `package.json` and the tarball's checksum in `package-lock.json`, so `npm ci` works on CI and for your colleagues. Imports are the same as they will be from the registry (`from 'mdlib-pure'`, `from 'mdlib-pure/gfm'`…). To build the tarball yourself instead (Node 22.18 or later): ```sh git clone https://github.com/MichelePolo/mdlib-pure.git cd mdlib-pure npm ci # dev tools only: the package itself has no dependencies npm pack # builds dist/, writes mdlib-pure-0.1.1.tgz cd ../my-app npm install ../mdlib-pure/mdlib-pure-0.1.1.tgz ``` The framework wrappers are not on a registry either, and have GitHub releases too: [Blazor](blazor.md) (the `.nupkg`, not on nuget.org) and [Angular](angular.md) (a tarball, not on npm). See [Framework wrappers](#framework-wrappers). ## Usage ```js import { createMarkdown } from 'mdlib-pure' const md = createMarkdown() md.render('# Hello *world*') // '

Hello world

\n' md.renderTo(element, '# Hello') // builds DOM nodes directly md.parse('# Hello') // mdast-style AST ``` ### Options ```js createMarkdown({ plugins: [], // see "Plugins" html: false, // recognize raw HTML (trusted input only, or with sanitize) sanitize: undefined, // (html) => html, e.g. DOMPurify.sanitize allowUrl: isSafeUrl, // (url, 'link' | 'image') => boolean decodeEntity: undefined,// named-entity decoder (default: the browser's DOM) classes: {}, // CSS classes per node type attributes: undefined, // (node) => extra attributes, e.g. data-sourcepos highlight: undefined, // (code, lang, meta) => html | VNodes | null positions: false, // line/column/offset on block nodes }) ``` Full reference: [docs/api.md](api.md). ### GitHub Flavored Markdown ```js import { createMarkdown } from 'mdlib-pure' import { gfm } from 'mdlib-pure/gfm' const md = createMarkdown({ plugins: gfm() }) // turn parts off: gfm({ footnotes: false, alerts: false }) ``` Also exported one by one: `table`, `strikethrough`, `autolink`, `tasklist`, `footnotes`, `tagfilter`, `alerts`. ### Syntax highlighting Bring your own highlighter through `highlight(code, lang, meta)`: ```js import hljs from 'highlight.js/lib/core' createMarkdown({ // a string is trusted HTML for inside highlight: (code, lang) => (lang && hljs.getLanguage(lang) ? hljs.highlight(code, { language: lang }).value : null), }) // shiki returns a whole
: it replaces the block
createMarkdown({ highlight: (code, lang) => shiki.codeToHtml(code, { lang: lang ?? 'text', theme: 'github-dark' }) })
```

Return `null` for the default rendering, or VNodes (`h('span', { class: 'kw' }, 'let')`)
to stay on the DOM-safe path of `renderTo()`. Strings are treated as trusted
HTML: with untrusted input, use a highlighter that escapes code (all of the
above do) or set `sanitize`.

### React, Vue, Preact

```jsx
import { createElement } from 'react'
import { toElements } from 'mdlib-pure/adapter'

{toElements(md.toVNodes(src), createElement, 'react')}
// Vue: toElements(vnodes, h, 'vue') · Preact: toElements(vnodes, h, 'preact') ``` The adapter applies each framework's conventions (`className`, uncontrolled checkboxes, Vue's array children, `dangerouslySetInnerHTML`/`innerHTML` for trusted raw HTML from `highlight`). Tested against the real React 19 on all CommonMark and GFM examples. With `html: true`, inline HTML arrives in fragments: use `md.render()` + `sanitize` on a container instead. ### Node, SSR, workers Named entities (`©`, `–`…) are decoded through the DOM to keep the core small. Outside the browser, import the full table: ```js import { decodeEntity } from 'mdlib-pure/entities' const md = createMarkdown({ decodeEntity }) ``` ### Styling **Theme with semantic tokens.** Import `mdlib-pure/theme.css` and put `class="md"` on the container. Everything is customizable through `--md-*` tokens; neutrals follow your text color and dark mode follows `color-scheme`. See [docs/tokens.md](tokens.md). ```css :root { --md-color-link: oklch(55% 0.2 260); --md-font-heading: "Inter", sans-serif; } ``` **Tailwind CSS v4.** The integration file loads the theme under your utilities and exposes the tokens as theme values (`text-md-link`, `bg-md-surface`…). Tailwind's preflight changes nothing inside `.md`: list markers, link underlines and inline images stay as in the plain theme. ```css @import "tailwindcss"; @import "mdlib-pure/tailwind.css"; ``` Or skip the theme: default output is plain semantic HTML, so `@tailwindcss/typography` works as is: ```html
…
``` Or add utility classes per node type: ```js createMarkdown({ classes: { heading: 'font-semibold tracking-tight', heading1: 'text-3xl', paragraph: 'my-4 leading-7', link: 'text-primary underline-offset-4 hover:underline', inlineCode: 'rounded bg-muted px-1 font-mono text-sm', code: 'rounded-lg bg-muted p-4 overflow-x-auto', }, }) ``` > Tailwind scans your sources for class names, so define the map in your own > code (as above), not in a file under `node_modules`. ## Plugins ```js const mark = { name: 'mark', delimiters: [{ char: '=', type: 'mark', sizes: [2] }], render: { mark: (node, r) => r.h('mark', null, r.children(node)) }, } createMarkdown({ plugins: [mark] }).render('==highlighted==') // '

highlighted

\n' ``` A plugin can provide: | Field | For | |---|---| | `block` | new block syntax (math fences, containers, tables) | | `inline` | syntax starting at a trigger character (`@mention`, `:emoji:`) | | `delimiters` | symmetric runs handled like emphasis (`~~`, `==`) | | `transform` | changes to the AST after parsing (heading ids, TOC) | | `render` | HTML output per node type, including core nodes | For a complete application example, see the [podcast plugin guide](podcast.md) and [copyable source](https://github.com/MichelePolo/mdlib-pure/blob/main/templates/podcast/README.md): a split editor with participant completion, planned section durations, interactive tasks and linked supporting documents, using fictional people and content. Try the [interactive sample](https://michelepolo.github.io/mdlib-pure/podcast/). First-party plugins: - `mdlib-pure/headings`: GitHub-style heading ids, optional `#` permalinks, and `toc(root)` for a table of contents. For untrusted input, use `headings({ prefix: 'user-content-' })` (see docs/security.md). - `mdlib-pure/container`: fenced containers (`::: warning Title` … `:::`, nestable with longer fences). - `mdlib-pure/math`: `$inline$`, `$$display$$` and `$$` blocks. The output works as-is with KaTeX auto-render or MathJax, or pass `math({ render: (tex, display) => katex.renderToString(tex, { displayMode: display }) })`. - `mdlib-pure/mark`: `==highlight==` → ``. - `mdlib-pure/mermaid`: ` ```mermaid ` diagrams, with no dependency on Mermaid: `renderMermaid(el, { load: () => import('mermaid') })` loads it only when a page has diagrams, themed from the tokens (light and dark). - `mdlib-pure/element`: `` and `` Web Components (an editor with live preview, styled by `mdlib-pure/editor.css`), every plugin included: [docs/elements.md](elements.md). - `mdlib-pure/deflist`: definition lists (`Term` / `: Definition`, Pandoc and PHP Markdown Extra style) → `
`. - `mdlib-pure/frontmatter`: YAML (`---`) / TOML (`+++`) front matter, kept out of the output; `getFrontmatter(root)` returns the raw text. The full guide, written for both humans and AI agents, is [`AGENTS.md`](extending.md). [`templates/plugin`](https://github.com/MichelePolo/mdlib-pure/tree/main/templates/plugin) is a complete, tested plugin to copy. ## Framework wrappers Both wrap the Web Components of `mdlib-pure/element`: the Blazor package bundles them, the Angular package depends on `mdlib-pure`. Neither is on a public registry yet. They offer the same features with each framework's idioms: a new plugin or element reaches both (the [agents guide](extending.md), rule 11). ### Blazor (.NET 10) [`MdlibPure.Blazor`](blazor.md) wraps the Web Components for every render mode: ``, `` and an AST service. **Not on nuget.org**: download the `.nupkg` from the [GitHub release](https://github.com/MichelePolo/mdlib-pure/releases/tag/blazor-v0.2.0) and add it from a local package feed. Version 0.1.1 adds [`PodcastEditor`](blazor.md#podcasteditor), with typed episode binding, configurable participants, supporting documents and planned duration. ### Angular (21, 22) [`ngx-mdlib-pure`](angular.md) wraps the Web Components as standalone components: `` (also a form control for `ngModel` and reactive forms), ``, `provideMdlibPure()` and a `MarkdownService` for the AST, the table of contents and HTML; `ngx-mdlib-pure/podcast` adds [`PodcastEditor`](angular.md#mdlib-podcast-editor-podcasteditor) (since 0.2.0). Server-side rendering and hydration work. **Not on npm**, like mdlib-pure itself: install both tarballs from the GitHub releases ([`angular-v0.2.0`](https://github.com/MichelePolo/mdlib-pure/releases/tag/angular-v0.2.0), [`mdlib-pure-v0.1.1`](https://github.com/MichelePolo/mdlib-pure/releases/tag/mdlib-pure-v0.1.1)). ## Performance Node 22, render to HTML string, median of 15 runs ([full method and results](https://github.com/MichelePolo/mdlib-pure/blob/main/bench/RESULTS.md); differences under ~10% are noise): | | CommonMark examples (exact) | spec.txt MB/s | README MB/s | min + gzip | |---|---:|---:|---:|---:| | **mdlib-pure** | **652/652** | 43.5 | 36.4 | **10.9 KB** | | marked | 503/652 | 44.6 | 32.7 | 14.0 KB | | markdown-it | 649/652 | 35.6 | 22.0 | 40.6 KB | | commonmark.js | 652/652 | 47.4 | 48.2 | 46.8 KB | With GFM: mdlib-pure + `gfm()` 39.6 / 25.1 MB/s, marked 38.8 / 27.2, markdown-it 28.7 / 17.4 (mdlib-pure also does footnotes, task lists and alerts). Reproduce: `npm run build && npm install --prefix bench && node bench/run.js`. ## Security - `html: false` (default): HTML in the source is not recognized and renders as text. - Every `href`/`src` passes through `allowUrl`. The default is an allowlist: relative URLs, `http:`, `https:`, `mailto:`, `tel:`, `sms:`, plus `data:image/{png,gif,jpeg,webp}` for images only. - `renderTo()` builds DOM with `createElement`/`createTextNode`. Only when raw HTML is present (you enabled `html`, or a plugin emits raw HTML) does it assign `innerHTML`, after `sanitize`. - Linear-time parsing, nesting caps and output-amplification limits. Details and threat model: [docs/security.md](security.md). ## Development ```sh npm test # 1000+ tests, TypeScript runs natively in Node ≥ 22.18 npm run test:browser # DOM path, theme and Tailwind build in Chromium npm run spec # CommonMark conformance report npm run verify # typecheck + tests + invariants + size budgets npm run build # dist/ ``` Live demo: `npm run build`, serve the repository root with any static server, open `/demo/`. ## License MIT © Michele Polo. Spec fixtures in `test/fixtures` keep their own licenses (test data only, not published): see [test/fixtures/README.md](https://github.com/MichelePolo/mdlib-pure/blob/main/test/fixtures/README.md). --- # API reference Entry points (all ES modules, all tree-shakable): For a complete application built through the plugin API, see the [podcast sample](podcast.md): 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` | `` and `` 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](tokens.md)) | | `mdlib-pure/tailwind.css` | Tailwind CSS v4 bridge | ## createMarkdown(options?) ```ts 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](#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](security.md#url-policy)) | | `decodeEntity` | `(name: string) => string \| undefined` | DOM-based | use `decodeEntity` from `mdlib-pure/entities` outside browsers | | `classes` | `Record` | — | 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 `` or a whole `
`; 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)

```js
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

```ts
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
}
```

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](extending.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
`
`. ## 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): ```html \(e^{i\pi}\) \[\sum_k a_k\]
\[\int_0^1 x\,dx\]
``` The `\(…\)`/`\[…\]` delimiters are the defaults of **KaTeX auto-render** and **MathJax**, so either works with no configuration (tested with KaTeX in Chromium): ```js 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: ```js 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: ```js 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 }` → ``. Same flanking rules as `*`; `Title\n==` stays a setext heading. ## mdlib-pure/mermaid `mermaid({ render? })` turns ` ```mermaid ` fences into `mermaid { value }` nodes, rendered as `
SOURCE
`: the markup Mermaid looks for, readable without JavaScript. mdlib-pure does not depend on Mermaid (~215 KB gzip at first load for Mermaid 12). ```js 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 ``, ``) | | `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: `
`, `
`, `
`; tight lists render definitions without `

`. ## 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, `` (viewer) and `` (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](elements.md)**. Framework wrappers: [Blazor](blazor.md), [Angular](angular.md). ## mdlib-pure/adapter ```ts toElements(vnodes, h, preset?: 'react' | 'preact' | 'vue'): Array ``` Converts `md.toVNodes(src)` with the framework's own `h`/`createElement`, applying its conventions. Top-level elements get a `key`. --- # Web Components `mdlib-pure/element` defines two custom elements that work in plain HTML and in any framework: - ``: renders Markdown from its `source` attribute; - ``: a Markdown editor with toolbar and live preview. They are the base of the [Blazor wrapper](blazor.md) and of the [Angular wrapper](angular.md). ## Quick start Core and every plugin are included: ~20 KB gzip. KaTeX and Mermaid are not: pass loaders, and they are fetched only when a document uses them. ```js import { defineElements } from 'mdlib-pure/element' import 'mdlib-pure/theme.css' // with a bundler that handles CSS imports; import 'mdlib-pure/editor.css' // otherwise them (see below) defineElements({ loaders: { mermaid: () => import('mermaid'), katex: () => import('katex/contrib/auto-render').then((m) => m.default), katexStylesheet: '/css/katex.min.css', }, configs: { docs: { highlight: (code, lang) => myHighlighter(code, lang) } }, }) ``` ```html ``` **Attributes** | Element | Attribute | | |---|---|---| | both | `plugins` | space-separated names or `all`: `gfm headings math mermaid container deflist mark frontmatter` | | both | `config` | name of options registered with `defineElements({ configs })` | | `` | `source` | the Markdown | | `` | `value` | initial (or externally set) Markdown | | `` | `view` | `split` (default), `write`, `preview` | | `` | `delay` | ms before `input` is dispatched (default 200) | | `` | `label`, `name` | accessible name of the text area; form field name | | `` | `disabled` | no typing or formatting, value left out of the form (as inside `

`) | Options that are functions (`highlight`, `allowUrl`, `sanitize`) and `html` can only be set through named configs, never from attributes: content cannot switch raw HTML on. **Editor behavior.** The preview renders on every keystroke, in the browser. Listeners get a debounced `input` event and `change` on blur, with the text in `event.target.value` (the text area's own events do not escape). While the user types, an attribute set to a value the editor already sent is treated as a late echo and ignored, so a slow server never overwrites typing; a value set while the editor is not focused (a Reset button) is applied. The element is form-associated: with `name`, its value is submitted with the form; `disabled` (or an enclosing `
`) makes it behave like a disabled `