@humanspeak/svelte-markdown
v1.4.1
Published
Fast, customizable markdown renderer for Svelte with built-in caching, TypeScript support, and Svelte 5 runes
Maintainers
Readme
@humanspeak/svelte-markdown
A powerful, customizable markdown renderer for Svelte with TypeScript support. Built as a successor to the original svelte-markdown package by Pablo Berganza, now maintained and enhanced by Humanspeak, Inc.
Features
- 🔒 Secure HTML parsing via HTMLParser2 with XSS protection
- 🚀 Full markdown syntax support through Marked
- 💪 Complete TypeScript support with strict typing
- 🔄 Svelte 5 runes compatibility
- ✂️ Inline snippet overrides — customize renderers without separate files
- 🎨 Customizable component rendering system
- ♿ WCAG 2.1 accessibility compliance
- 🎯 GitHub-style slug generation for headers
- 🧪 Comprehensive test coverage (vitest and playwright)
- 🧩 First-class marked extensions support via
extensionsprop (e.g., KaTeX math, alerts) - ⚡ Intelligent token caching (50-200x faster re-renders)
- 📡 LLM streaming mode with incremental rendering (~1.6ms avg per update)
- 🖼️ Smart image lazy loading with fade-in animation
Installation
npm i -S @humanspeak/svelte-markdownOr with your preferred package manager:
pnpm add @humanspeak/svelte-markdown
yarn add @humanspeak/svelte-markdownBasic Usage
<script lang="ts">
import SvelteMarkdown from '@humanspeak/svelte-markdown'
const source = `
# This is a header
This is a paragraph with **bold** and <em>mixed HTML</em>.
* List item with \`inline code\`
* And a [link](https://svelte.dev)
* With nested items
* Supporting full markdown
`
</script>
<SvelteMarkdown {source} />TypeScript Support
The package is written in TypeScript and includes full type definitions:
import type {
Renderers,
Token,
TokensList,
SvelteMarkdownOptions,
MarkedExtension
} from '@humanspeak/svelte-markdown'Exports for programmatic overrides
You can import renderer maps and helper keys to selectively override behavior.
import SvelteMarkdown, {
// Maps
defaultRenderers, // markdown renderer map
Html, // HTML renderer map
// Keys
rendererKeys, // markdown renderer keys (excludes 'html')
htmlRendererKeys, // HTML renderer tag names
// Utility components
Unsupported, // markdown-level unsupported fallback
UnsupportedHTML // HTML-level unsupported fallback
} from '@humanspeak/svelte-markdown'
// Example: override a subset
const customRenderers = {
...defaultRenderers,
link: CustomLink,
html: {
...Html,
span: CustomSpan
}
}
// Optional: iterate keys when building overrides dynamically
for (const key of rendererKeys) {
// if (key === 'paragraph') customRenderers.paragraph = MyParagraph
}
for (const tag of htmlRendererKeys) {
// if (tag === 'div') customRenderers.html.div = MyDiv
}Notes
rendererKeysintentionally excludeshtml. UsehtmlRendererKeysfor HTML tag overrides.UnsupportedandUnsupportedHTMLare available if you want a pass-through fallback strategy.
Helper utilities for allow/deny strategies
These helpers make it easy to either allow only a subset or exclude only a subset of renderers without writing huge maps by hand.
- HTML helpers
buildUnsupportedHTML(): returns a map where every HTML tag usesUnsupportedHTML.allowHtmlOnly(allowed): enable only the provided tags; others useUnsupportedHTML.- Accepts tag names like
'strong'or tuples like['div', MyDiv]to plug in custom components.
- Accepts tag names like
excludeHtmlOnly(excluded, overrides?): disable only the listed tags (mapped toUnsupportedHTML), with optional overrides for non-excluded tags using tuples.
- Markdown helpers (non-HTML)
buildUnsupportedRenderers(): returns a map where all markdown renderers (excepthtml) useUnsupported.allowRenderersOnly(allowed): enable only the provided markdown renderer keys; others useUnsupported.- Accepts keys like
'paragraph'or tuples like['paragraph', MyParagraph]to plug in custom components.
- Accepts keys like
excludeRenderersOnly(excluded, overrides?): disable only the listed markdown renderer keys, with optional overrides for non-excluded keys using tuples.
HTML helpers in context
The HTML helpers return an HtmlRenderers map to be used inside the html key of the overall renderers map. They do not replace the entire renderers object by themselves.
Basic: keep markdown defaults, allow only a few HTML tags (others become UnsupportedHTML):
import SvelteMarkdown, { defaultRenderers, allowHtmlOnly } from '@humanspeak/svelte-markdown'
const renderers = {
...defaultRenderers, // keep markdown defaults
html: allowHtmlOnly(['strong', 'em', 'a']) // restrict HTML
}Allow a custom component for one tag while allowing others with defaults:
import SvelteMarkdown, { defaultRenderers, allowHtmlOnly } from '@humanspeak/svelte-markdown'
const renderers = {
...defaultRenderers,
html: allowHtmlOnly([['div', MyDiv], 'a'])
}Exclude just a few HTML tags; keep all other HTML tags as defaults:
import SvelteMarkdown, { defaultRenderers, excludeHtmlOnly } from '@humanspeak/svelte-markdown'
const renderers = {
...defaultRenderers,
html: excludeHtmlOnly(['span', 'iframe'])
}
// Or exclude 'span', but override 'a' to CustomA
const renderersWithOverride = {
...defaultRenderers,
html: excludeHtmlOnly(['span'], [['a', CustomA]])
}Disable all HTML quickly (markdown defaults unchanged):
import SvelteMarkdown, { defaultRenderers, buildUnsupportedHTML } from '@humanspeak/svelte-markdown'
const renderers = {
...defaultRenderers,
html: buildUnsupportedHTML()
}Markdown-only (non-HTML) scenarios
Allow only paragraph and link with defaults, disable others:
import { allowRenderersOnly } from '@humanspeak/svelte-markdown'
const md = allowRenderersOnly(['paragraph', 'link'])Exclude just link; keep others as defaults:
import { excludeRenderersOnly } from '@humanspeak/svelte-markdown'
const md = excludeRenderersOnly(['link'])Disable all markdown renderers (except html) quickly:
import { buildUnsupportedRenderers } from '@humanspeak/svelte-markdown'
const md = buildUnsupportedRenderers()Combine HTML and Markdown helpers
You can combine both maps in renderers for SvelteMarkdown.
<script lang="ts">
import SvelteMarkdown, { allowRenderersOnly, allowHtmlOnly } from '@humanspeak/svelte-markdown'
const renderers = {
// Only allow a minimal markdown set
...allowRenderersOnly(['paragraph', 'link']),
// Configure HTML separately (only strong/em/a)
html: allowHtmlOnly(['strong', 'em', 'a'])
}
const source = `# Title\n\nThis has <strong>HTML</strong> and [a link](https://example.com).`
</script>
<SvelteMarkdown {source} {renderers} />Custom Renderer Example
Here's a complete example of a custom renderer with TypeScript support:
<script lang="ts">
import type { Snippet } from 'svelte'
interface Props {
children?: Snippet
href?: string
title?: string
}
const { href = '', title = '', children }: Props = $props()
</script>
<a {href} {title} class="custom-link">
{@render children?.()}
</a>If you would like to extend other renderers please take a look inside the renderers folder for the default implentation of them. If you would like feature additions please feel free to open an issue!
Snippet Overrides (Svelte 5)
For simple tweaks — adding a class, changing an attribute, wrapping in a div — you can override renderers inline with Svelte 5 snippets instead of creating separate component files:
<script lang="ts">
import SvelteMarkdown from '@humanspeak/svelte-markdown'
const source = '# Hello\n\nA paragraph with [a link](https://example.com).'
</script>
<SvelteMarkdown {source}>
{#snippet paragraph({ children })}
<p class="prose">{@render children?.()}</p>
{/snippet}
{#snippet heading({ depth, children })}
{#if depth === 1}
<h1 class="title">{@render children?.()}</h1>
{:else}
<h2>{@render children?.()}</h2>
{/if}
{/snippet}
{#snippet link({ href, title, children })}
<a {href} {title} target="_blank" rel="noopener noreferrer">
{@render children?.()}
</a>
{/snippet}
{#snippet code({ lang, text })}
<pre class="highlight {lang}"><code>{text}</code></pre>
{/snippet}
</SvelteMarkdown>How it works
- Container renderers (paragraph, heading, blockquote, list, etc.) receive a
childrensnippet for nested content - Leaf renderers (code, image, hr, br) receive only data props — no
children - Precedence: snippet > component renderer > default. If both a snippet and a
renderers.paragraphcomponent are provided, the snippet wins
HTML tag snippets
HTML tag snippets use an html_ prefix to avoid collisions with markdown renderer names:
<SvelteMarkdown {source}>
{#snippet html_div({ attributes, children })}
<div class="custom-wrapper" {...attributes}>{@render children?.()}</div>
{/snippet}
{#snippet html_a({ attributes, children })}
<a {...attributes} target="_blank" rel="noopener noreferrer">
{@render children?.()}
</a>
{/snippet}
</SvelteMarkdown>All HTML snippets share a uniform props interface: { attributes?: Record<string, any>, children?: Snippet }.
Custom HTML Tags
You can render arbitrary (non-standard) HTML tags like <click>, <tooltip>, or any custom element by providing a renderer or snippet for the tag name. The parsing pipeline accepts any tag name — you just need to tell SvelteMarkdown how to render it.
Component renderer approach:
<script lang="ts">
import SvelteMarkdown from '@humanspeak/svelte-markdown'
import ClickButton from './ClickButton.svelte'
const source = '<click>Click Me</click>'
const renderers = { html: { click: ClickButton } }
</script>
<SvelteMarkdown {source} {renderers} />Snippet override approach:
<SvelteMarkdown source={'<click data-action="submit">Click Me</click>'}>
{#snippet html_click({ attributes, children })}
<button {...attributes} class="custom-btn">{@render children?.()}</button>
{/snippet}
</SvelteMarkdown>Both approaches work for any tag name. Snippet overrides take precedence over component renderers when both are provided.
Marked Extensions
Use third-party marked extensions via the extensions prop. The component handles registering tokenizers internally — you just provide renderers for the custom token types.
KaTeX Math Rendering
npm install marked-katex-extension katexComponent renderer approach:
<script lang="ts">
import SvelteMarkdown from '@humanspeak/svelte-markdown'
import type { RendererComponent, Renderers } from '@humanspeak/svelte-markdown'
import markedKatex from 'marked-katex-extension'
import KatexRenderer from './KatexRenderer.svelte'
interface KatexRenderers extends Renderers {
inlineKatex: RendererComponent
blockKatex: RendererComponent
}
const renderers: Partial<KatexRenderers> = {
inlineKatex: KatexRenderer,
blockKatex: KatexRenderer
}
</script>
<svelte:head>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/dist/katex.min.css" crossorigin="anonymous" />
</svelte:head>
<SvelteMarkdown
source="Euler's identity: $e^{{i\pi}} + 1 = 0$"
extensions={[markedKatex({ throwOnError: false })]}
{renderers}
/>Where KatexRenderer.svelte is:
<script lang="ts">
import katex from 'katex'
interface Props {
text: string
displayMode?: boolean
}
const { text, displayMode = false }: Props = $props()
const html = $derived(katex.renderToString(text, { throwOnError: false, displayMode }))
</script>
{@html html}Snippet override approach (no separate component file needed):
<script lang="ts">
import SvelteMarkdown from '@humanspeak/svelte-markdown'
import katex from 'katex'
import markedKatex from 'marked-katex-extension'
</script>
<svelte:head>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/dist/katex.min.css" crossorigin="anonymous" />
</svelte:head>
<SvelteMarkdown
source="Euler's identity: $e^{{i\pi}} + 1 = 0$"
extensions={[markedKatex({ throwOnError: false })]}
>
{#snippet inlineKatex(props)}
{@html katex.renderToString(props.text, { displayMode: false })}
{/snippet}
{#snippet blockKatex(props)}
{@html katex.renderToString(props.text, { displayMode: true })}
{/snippet}
</SvelteMarkdown>Mermaid Diagrams (Async Rendering)
The package includes built-in markedMermaid and MermaidRenderer helpers for Mermaid diagram support. Install mermaid as an optional peer dependency:
npm install mermaidThen use the built-in helpers — no boilerplate needed:
<script lang="ts">
import SvelteMarkdown from '@humanspeak/svelte-markdown'
import type { RendererComponent, Renderers } from '@humanspeak/svelte-markdown'
import { markedMermaid, MermaidRenderer } from '@humanspeak/svelte-markdown/extensions'
// markdown containing fenced mermaid code blocks
let { source } = $props()
interface MermaidRenderers extends Renderers {
mermaid: RendererComponent
}
const renderers: Partial<MermaidRenderers> = {
mermaid: MermaidRenderer
}
</script>
<SvelteMarkdown {source} extensions={[markedMermaid()]} {renderers} />markedMermaid() is a zero-dependency tokenizer that converts ```mermaid code blocks into custom tokens. MermaidRenderer lazy-loads mermaid in the browser, renders SVG asynchronously, and automatically re-renders when dark/light mode changes.
You can also use snippet overrides to wrap MermaidRenderer with custom markup:
<SvelteMarkdown source={markdown} extensions={[markedMermaid()]}>
{#snippet mermaid(props)}
<div class="my-diagram-wrapper">
<MermaidRenderer text={props.text} />
</div>
{/snippet}
</SvelteMarkdown>Since Mermaid rendering is async, the snippet delegates to MermaidRenderer rather than calling mermaid.render() directly. This pattern works for any async extension — keep the async logic in a component and use the snippet for layout customization.
GitHub Alerts
Built-in support for GitHub-style alerts/admonitions. Five alert types are supported: NOTE, TIP, IMPORTANT, WARNING, and CAUTION.
<script lang="ts">
import SvelteMarkdown from '@humanspeak/svelte-markdown'
import type { RendererComponent, Renderers } from '@humanspeak/svelte-markdown'
import { markedAlert, AlertRenderer } from '@humanspeak/svelte-markdown/extensions'
const source = `
> [!NOTE]
> Useful information that users should know.
> [!WARNING]
> Urgent info that needs immediate attention.
`
interface AlertRenderers extends Renderers {
alert: RendererComponent
}
const renderers: Partial<AlertRenderers> = {
alert: AlertRenderer
}
</script>
<SvelteMarkdown {source} extensions={[markedAlert()]} {renderers} />AlertRenderer renders a <div class="markdown-alert markdown-alert-{type}"> with a title — no inline styles, so you can theme it with your own CSS. You can also use snippet overrides:
<SvelteMarkdown source={markdown} extensions={[markedAlert()]}>
{#snippet alert(props)}
<div class="my-alert my-alert-{props.alertType}">
<strong>{props.alertType}</strong>
<p>{props.text}</p>
</div>
{/snippet}
</SvelteMarkdown>Footnotes
Built-in support for footnote references and definitions. Footnote references ([^id]) render as superscript links, and definitions ([^id]: content) render as a numbered list at the end of the document with back-links.
<script lang="ts">
import SvelteMarkdown from '@humanspeak/svelte-markdown'
import type { RendererComponent, Renderers } from '@humanspeak/svelte-markdown'
import {
markedFootnote,
FootnoteRef,
FootnoteSection
} from '@humanspeak/svelte-markdown/extensions'
const source = `
Here is a statement[^1] with a footnote.
Another claim[^note] that needs a source.
[^1]: This is the first footnote.
[^note]: This is a named footnote.
`
interface FootnoteRenderers extends Renderers {
footnoteRef: RendererComponent
footnoteSection: RendererComponent
}
const renderers: Partial<FootnoteRenderers> = {
footnoteRef: FootnoteRef,
footnoteSection: FootnoteSection
}
</script>
<SvelteMarkdown {source} extensions={[markedFootnote()]} {renderers} />FootnoteRef renders <sup><a href="#fn-{id}">{id}</a></sup> and FootnoteSection renders an <ol> with bidirectional links (ref to definition and back). You can also use snippet overrides for custom rendering.
How It Works
Marked extensions define custom token types with a name property (e.g., inlineKatex, blockKatex, alert). When you pass extensions via the extensions prop, SvelteMarkdown automatically extracts these token type names and makes them available as both component renderer keys and snippet override names.
To find the token type names for any extension, check its source or documentation for the name field in its extensions array:
// Example: marked-katex-extension registers tokens named "inlineKatex" and "blockKatex"
// → use renderers={{ inlineKatex: ..., blockKatex: ... }}
// → or {#snippet inlineKatex(props)} and {#snippet blockKatex(props)}
// Example: a custom alert extension registers a token named "alert"
// → use renderers={{ alert: AlertComponent }}
// → or {#snippet alert(props)}Each snippet/component receives the token's properties as props (e.g., text, displayMode for KaTeX; text, level for alerts).
See the full documentation and interactive demo.
TypeScript
All snippet prop types are exported for use in external components:
import type {
ParagraphSnippetProps,
HeadingSnippetProps,
LinkSnippetProps,
CodeSnippetProps,
HtmlSnippetProps,
SnippetOverrides,
HtmlSnippetOverrides
} from '@humanspeak/svelte-markdown'Advanced Features
Table Support with Mixed Content
The package excels at handling complex nested structures and mixed content:
| Type | Content |
| ---------- | --------------------------------------- |
| Nested | <div>**bold** and _italic_</div> |
| Mixed List | <ul><li>Item 1</li><li>Item 2</li></ul> |
| Code | <code>`inline code`</code> |HTML in Markdown
Seamlessly mix HTML and Markdown:
<div style="color: blue">
### This is a Markdown heading inside HTML
And here's some **bold** text too!
</div>
<details>
<summary>Click to expand</summary>
- This is a markdown list
- Inside an HTML details element
- Supporting **bold** and _italic_ text
</details>Performance
Intelligent Token Caching
Parsed tokens are automatically cached using an LRU strategy, providing 50-200x faster re-renders for previously seen content (< 1ms vs 50-200ms). The cache uses FNV-1a hashing keyed on source + options, with LRU eviction (default 50 documents) and TTL expiration (default 5 minutes). No configuration required.
import { tokenCache, TokenCache } from '@humanspeak/svelte-markdown'
// Manual cache management
tokenCache.clearAllTokens()
tokenCache.deleteTokens(markdown, options)
// Custom cache instance
const myCache = new TokenCache({ maxSize: 100, ttl: 10 * 60 * 1000 })Smart Image Lazy Loading
Images automatically lazy load using native loading="lazy" and IntersectionObserver prefetching, with a smooth fade-in animation and error state handling. To disable lazy loading, provide a custom Image renderer:
<!-- EagerImage.svelte -->
<script lang="ts">
let { href = '', title = undefined, text = '' } = $props()
</script>
<img src={href} {title} alt={text} loading="eager" /><script lang="ts">
import SvelteMarkdown from '@humanspeak/svelte-markdown'
import EagerImage from './EagerImage.svelte'
const renderers = { image: EagerImage }
</script>
<SvelteMarkdown source={markdown} {renderers} />LLM Streaming
For real-time rendering of AI responses from ChatGPT, Claude, Gemini, and other LLMs, enable the streaming prop. This uses a smart diff algorithm that re-parses the full source for correctness but only updates changed DOM nodes, keeping render times constant regardless of document size.
The preferred API is now imperative: bind the component instance and call writeChunk() as chunks arrive. This avoids prop reactivity edge cases like identical consecutive string chunks being coalesced.
<script lang="ts">
import SvelteMarkdown from '@humanspeak/svelte-markdown'
import type { StreamingChunk } from '@humanspeak/svelte-markdown'
let markdown:
| {
writeChunk: (chunk: StreamingChunk) => void
resetStream: (nextSource?: string) => void
}
| undefined
async function streamResponse() {
const response = await fetch('/api/chat', { method: 'POST', body: '...' })
const reader = response.body.getReader()
const decoder = new TextDecoder()
while (true) {
const { done, value } = await reader.read()
if (done) break
markdown?.writeChunk(decoder.decode(value, { stream: true }))
}
}
</script>
<SvelteMarkdown bind:this={markdown} source="" streaming={true} />For websocket-style offset patches, pass an object chunk instead:
markdown?.writeChunk({ value: 'world', offset: 6 })Object chunks overwrite the internal buffer at offset. This is overwrite semantics, not insert semantics: the chunk replaces characters starting at that index and preserves any trailing content after the overwritten span.
If offset skips ahead, missing positions are padded with spaces. There is no delete or truncate behavior in offset mode.
Typical websocket-style usage can arrive out of order:
markdown?.writeChunk({ value: ' world', offset: 5 })
markdown?.writeChunk({ value: 'Hello', offset: 0 })The internal buffer converges as later patches fill earlier gaps.
You can reset the internal streaming buffer at any time:
markdown?.resetStream('')
markdown?.resetStream('# Seeded response')The first successful write after a reset locks the stream into one input mode:
stringchunks: append mode{ value, offset }chunks: offset mode
Switching modes before resetStream() or a source prop reset logs a warning and drops the chunk. Offset chunks must use a non-negative safe integer offset.
Changing the source prop also resets the imperative buffer, seeds a new baseline value, and unlocks the input mode.
Appending directly to source is still supported:
<script lang="ts">
import SvelteMarkdown from '@humanspeak/svelte-markdown'
let source = $state('')
function onChunk(chunk: string) {
source += chunk
}
</script>
<SvelteMarkdown {source} streaming={true} />Performance (measured at 100 characters/sec, character mode):
| Metric | Standard Mode | Streaming Mode | | -------------- | :-----------: | :------------: | | Average render | ~3.6ms | ~1.6ms | | Peak render | ~21ms | ~10ms | | Dropped frames | 0 | 0 |
When streaming is false (default), existing behavior is unchanged. The streaming prop skips cache lookups (always a miss during streaming) and uses in-place token array mutation so Svelte only re-renders components for tokens that actually changed.
Note: streaming is automatically disabled when async extensions (e.g., markedMermaid) are used. A console warning is logged in this case.
See the full streaming documentation and interactive demo.
Available Renderers
text- Text within other elementsparagraph- Paragraph (<p>)em- Emphasis (<em>)strong- Strong/bold (<strong>)hr- Horizontal rule (<hr>)blockquote- Block quote (<blockquote>)del- Deleted/strike-through (<del>)link- Link (<a>)image- Image (<img>)table- Table (<table>)tablehead- Table head (<thead>)tablebody- Table body (<tbody>)tablerow- Table row (<tr>)tablecell- Table cell (<td>/<th>)list- List (<ul>/<ol>)listitem- List item (<li>)heading- Heading (<h1>-<h6>)codespan- Inline code (<code>)code- Block of code (<pre><code>)html- HTML noderawtext- All other text that is going to be included in an object above
Optional List Renderers
For fine-grained styling:
orderedlistitem- Items in ordered listsunorderedlistitem- Items in unordered lists
HTML Renderers
The html renderer is special and can be configured separately to handle HTML elements:
| Element | Description |
| -------- | -------------------- |
| div | Division element |
| span | Inline container |
| table | HTML table structure |
| thead | Table header group |
| tbody | Table body group |
| tr | Table row |
| td | Table data cell |
| th | Table header cell |
| ul | Unordered list |
| ol | Ordered list |
| li | List item |
| code | Code block |
| em | Emphasized text |
| strong | Strong text |
| a | Anchor/link |
| img | Image |
You can customize HTML rendering by providing your own components:
import type { HtmlRenderers } from '@humanspeak/svelte-markdown'
const customHtmlRenderers: Partial<HtmlRenderers> = {
div: YourCustomDivComponent,
span: YourCustomSpanComponent
}Events
The component emits a parsed event when tokens are calculated:
<script lang="ts">
import SvelteMarkdown from '@humanspeak/svelte-markdown'
const handleParsed = (tokens: Token[] | TokensList) => {
console.log('Parsed tokens:', tokens)
}
</script>
<SvelteMarkdown {source} parsed={handleParsed} />Props
| Prop | Type | Description |
| ---------- | ----------------------- | ------------------------------------------------ |
| source | string \| Token[] | Markdown content or pre-parsed tokens |
| streaming | boolean | Enable incremental rendering for LLM streaming |
| renderers | Partial<Renderers> | Custom component overrides |
| options | SvelteMarkdownOptions | Marked parser configuration |
| isInline | boolean | Toggle inline parsing mode |
| extensions | MarkedExtension[] | Third-party marked extensions (e.g., KaTeX math) |
Security
This package takes a defense-in-depth approach to security:
- Secure HTML parsing - All HTML is parsed through HTMLParser2's streaming parser rather than
innerHTML, preventing script injection - XSS protection - HTML entities are safely handled; malicious markdown injection is neutralized during parsing
- Granular HTML control - Use
allowHtmlOnly()/excludeHtmlOnly()to restrict which HTML tags are rendered (see Helper utilities) - Full HTML lockdown - Call
buildUnsupportedHTML()to block all raw HTML rendering - Markdown renderer control - Use
allowRenderersOnly()/excludeRenderersOnly()to limit which markdown token types are rendered - No built-in sanitizer - By design, the package does not bundle a sanitizer. Integrate your own (e.g., DOMPurify) if you accept untrusted input
License
MIT © Humanspeak, Inc.
Credits
Made with ❤️ by Humanspeak
