mdlib-pure
Documentation pages
On this page

Web Components #

mdlib-pure/element defines two custom elements that work in plain HTML and in any framework:

  • <mdlib-markdown>: renders Markdown from its source attribute;
  • <mdlib-editor>: a Markdown editor with toolbar and live preview.

They are the base of the Blazor wrapper and of the Angular wrapper.

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.

import { defineElements } from 'mdlib-pure/element'
import 'mdlib-pure/theme.css' // with a bundler that handles CSS imports;
import 'mdlib-pure/editor.css' // otherwise <link> 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) } },
})
<mdlib-markdown class="md" source="# Hello" plugins="gfm math"></mdlib-markdown>
<mdlib-editor value="# Hello" plugins="all" style="block-size: 30rem"></mdlib-editor>

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 })
<mdlib-markdown> source the Markdown
<mdlib-editor> value initial (or externally set) Markdown
<mdlib-editor> view split (default), write, preview
<mdlib-editor> delay ms before input is dispatched (default 200)
<mdlib-editor> label, name accessible name of the text area; form field name
<mdlib-editor> disabled no typing or formatting, value left out of the form (as inside <fieldset disabled>)

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 <fieldset disabled>) makes it behave like a disabled <textarea>, while the view buttons keep working. Toolbar: bold, italic, heading, link, lists, quote, code (Ctrl+B, Ctrl+I, Ctrl+K). Math and diagrams load KaTeX and Mermaid only when a document uses them; diagrams already drawn are reused while typing.

For an editor with application-specific tags and toolbar actions, see the podcast sample. It builds a dedicated split editor with the public parser/render APIs; custom plugins and commands are not registration options of defineElements().

getMarkdown(plugins, config) returns the shared Markdown instance behind an attribute combination (e.g. to parse() the editor's text).

Without a bundler #

dist/element.js is an ES module. Load it with an import map or a relative path, then define the elements:

<link rel="stylesheet" href="/node_modules/mdlib-pure/styles/theme.css" />
<link rel="stylesheet" href="/node_modules/mdlib-pure/styles/editor.css" />
<script type="module">
  import { defineElements } from '/node_modules/mdlib-pure/dist/element.js'
  defineElements()
</script>

<mdlib-editor name="body" value="# Draft" plugins="gfm" style="block-size: 24rem"></mdlib-editor>

Reading the editor's value #

const editor = document.querySelector('mdlib-editor')
editor.addEventListener('input', (e) => save(e.target.value)) // debounced (delay)
editor.addEventListener('change', (e) => save(e.target.value)) // on blur
editor.value = '# Replaced' // set it from code

value works as a property or as an attribute, before or after the element is connected, and even before defineElements() runs: frameworks that set properties on custom elements (React 19, Vue) need nothing special. Inside a <form>, an editor with a name submits its text like a <textarea> would.

Frameworks that rewrite the page #

Blazor enhanced navigation, Turbo and htmx may empty or morph elements they did not create. Both elements repair themselves: the viewer renders again, the editor puts back its toolbar and panes and keeps the typed text. Call element.refresh() to render again by hand; every defineElements() call refreshes the elements already in the page (e.g. loaders registered late).

Frameworks #

  • Blazor: use MdlibPure.Blazor, which wraps these elements for every render mode and adds a C# AST service.
  • Angular: use ngx-mdlib-pure: standalone components with [(value)], a form control for ngModel and reactive forms, SSR with hydration, and a service for the AST.
  • React 19, Vue, Svelte: custom elements work as-is: <mdlib-editor value={text} onInput={(e) => setText(e.target.value)} /> in React 19 (which sets value as a property), :value in Vue. Pass source and plugins as attributes.

Testing #

Unit tests in jsdom (Vitest, Jest) work without polyfills: the editor skips the form value where ElementInternals.setFormValue() is missing. In jsdom, value and the preview are updated at once; the input event comes after delay ms (use fake timers), change on blur. KaTeX and Mermaid do not run there: test math and diagrams in a real browser.

Security #

Raw HTML and functions cannot be enabled from attributes: html, highlight, allowUrl and sanitize only come from named configs written in JavaScript. URLs follow the default allowlist; Mermaid runs with securityLevel: 'strict'. More in Security: Web Components.

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