Markdown
Safe markdown renderer that streams: chat answers and READMEs, with code, math and diagrams.
c2-markdown renders CommonMark and GitHub-flavoured markdown (tables, task lists, strikethrough, autolinks) plus TeX math and Mermaid diagrams, for model answers streaming in and for static documents alike.
Safe by construction.
- The output is built from the parser’s tokens as templates from a fixed list of elements. No markdown ever reaches
innerHTML. - Raw HTML in the source is shown as code, never run.
- Links and images keep only
http(s)and relative URLs; links also allowmailto:andtel:. An optionalurlTransformhook runs after that check, and can proxy a URL or drop it. - Images render as
c2-image.image-policy="click"makes each one wait for a click, except those fromimage-origins. That closes the image-URL channel a prompt injection uses to leak data out of a chat.
Streaming. Call appendText(chunk) while streaming is set.
- The text appears at a steady pace, with the same pacing as
c2-streaming-text. - Only the block still growing re-renders, so a reader’s selection survives.
- The unfinished end is repaired as it grows:
**boldalready renders bold, a half-typed link shows only its label, an unfinished image is held back, and an open code fence stays plain code until it closes. reveal-endfires once the stream has ended and the display has caught up.
Built from other c2 components. Closed code fences render through c2-code-viewer, mermaid fences through c2-mermaid, and $…$, $$…$$, \(…\) and \[…\] through c2-math. Each one loads on first use; diagrams need the optional mermaid package installed (npm install mermaid). math="bracket" turns off the dollar forms for text full of prices; prices such as $5 and $10 stay text anyway.
Import
import '@c2n/components/markdown'Usage
Install
Run npm install @c2n/components, then import what you use:
- Buttons and forms>
- Tables and charts>
- Your own variants>
Raw HTML such as
<b>this</b>is shown as code, never run.
The assistant attached a chart:
Stream an answer from any reader: fetch, server-sent events or an AI SDK.
const answer = document.querySelector('c2-markdown')!
answer.streaming = true
for await (const chunk of response.body!.pipeThrough(new TextDecoderStream())) answer.appendText(chunk)
answer.streaming = false
answer.addEventListener('reveal-end', () => showActions(), { once: true })Route internal links yourself, or proxy every URL:
answer.addEventListener('link-click', (event) => {
if (!event.detail.external) {
event.preventDefault()
router.navigate(event.detail.href)
}
})
answer.urlTransform = (url, kind) => (kind === 'image' ? `/image-proxy?url=${encodeURIComponent(url)}` : url)