Markdown Rendering
AI messages in @threadplane/chat can render full markdown -- headings, code blocks, tables, lists, blockquotes, math, HTML nodes, and inline formatting. Use <chat-streaming-md> for the live node-based renderer. Use renderMarkdown() only when you need a small sanitized-HTML helper in a custom template.
#How It Works
The markdown pipeline has two stages:
- Streaming renderer:
<chat-streaming-md>parses markdown into node keys and renders those keys throughcacheplaneMarkdownViews. - Sanitized HTML helper:
renderMarkdown()converts markdown text to sanitized HTML using the requiredmarkedpeer dependency. Ifmarkedcannot be loaded at runtime, it falls back to escaped plain text with<br>newline conversion.
#The renderMarkdown() Function
Signature:
| Parameter | Type | Description |
|---|---|---|
content | string | Raw markdown text to render |
sanitizer | DomSanitizer | Angular's DOM sanitizer for XSS protection |
Returns: SafeHtml -- sanitized HTML that can be bound via [innerHTML].
Behavior:
- Parses markdown to HTML with
marked, sanitizes it through Angular'sSecurityContext.HTML, then marks the result as trusted. - If the dynamic
markedimport fails at runtime, escapes HTML entities (&,<,>) and converts newlines to<br>tags as a defensive fallback.
The marked library is loaded via a dynamic import('marked') at module initialization time. This means it does not block initial bundle loading and resolves before the first render in most cases.
#Using Markdown in Custom Components
For custom message templates, prefer <chat-streaming-md> so you get the same node registry, math, citation, table, and code-block behavior as the built-in chat components:
If you only need sanitized HTML, call renderMarkdown() directly and provide your own styling for the host element:
#Streaming Markdown with chat-streaming-md
<chat-streaming-md> is the component that renders AI message content token-by-token using the node-based rendering pipeline. It resolves each markdown node type against MARKDOWN_VIEW_REGISTRY โ a chat-internal DI token exported from @threadplane/chat.
By default the component provides cacheplaneMarkdownViews (the full 22-node registry) on its own component injector. You can override this at two levels:
- App-wide โ provide a custom registry in your root or feature providers.
- Per-instance โ pass a
ViewRegistryvia the[viewRegistry]input; the component uses that value instead of the DI tree.
#Overriding Markdown Components
#App-wide override
To replace a node-type renderer for every <chat-streaming-md> in your app, provide a custom MARKDOWN_VIEW_REGISTRY in your application config:
overrideViews(base, overrides) replaces every key listed in overrides and preserves all other entries from base. Import it from @threadplane/render (chat does not re-export it).
Use overrideViews when replacing an existing node type. Use withViews when adding a brand-new node type that cacheplaneMarkdownViews does not yet cover โ withViews is additive-only and the base registry wins on conflicts. See the render views API for full signatures.
#Per-instance override
Pass a ViewRegistry directly to a single <chat-streaming-md> via its [viewRegistry] input. The component uses the provided value and ignores the DI tree for that instance:
#Node-Type Reference
cacheplaneMarkdownViews covers every node type emitted by @cacheplane/partial-markdown. Use these keys when calling overrideViews or withViews.
The most common mistake is providing 'code' as an override key โ it does not match anything in the registry. The correct key for fenced code blocks is 'code-block'.
| Key | Description |
|---|---|
'document' | Root node wrapping the entire parsed document |
'paragraph' | Block-level paragraph (<p>) |
'heading' | Heading element (<h1> through <h6>) |
'blockquote' | Block-level quotation (<blockquote>) |
'list' | Ordered or unordered list (<ol> / <ul>) |
'list-item' | Individual list item (<li>) |
'code-block' | Fenced code block (<pre><code>) |
'thematic-break' | Horizontal rule (<hr>) |
'text' | Inline text run |
'emphasis' | Italic emphasis (<em>) |
'strong' | Bold emphasis (<strong>) |
'strikethrough' | Strikethrough text (<del>) |
'inline-code' | Inline code span (<code>) |
'math-inline' | Inline math |
'math-display' | Display math block |
'html-inline' | Inline HTML node |
'html-block' | Block HTML node |
'link' | Hyperlink (<a>) |
'autolink' | Auto-detected URL or email link |
'image' | Image (<img>) |
'soft-break' | Soft line break (space or newline within a paragraph) |
'hard-break' | Hard line break (<br>) |
'citation-reference' | In-text citation reference rendered by the chat pipeline |
'table' | Table container (<table>) |
'table-row' | Table row (<tr>) |
'table-cell' | Table header or data cell (<th> / <td>) |
#Theming Markdown Components
All built-in markdown view components consume the same --tplane-chat-* and --a2ui-* CSS custom properties as the rest of the chat UI. No extra tokens are needed โ changing the active theme automatically re-styles markdown output. See the chat theming guide for the full token reference.
#Defensive Plain-Text Fallback
marked is a required peer dependency for supported @threadplane/chat installs:
If the dynamic import('marked') still fails at runtime, renderMarkdown() falls back to escaped plain text with line breaks preserved. Treat that fallback as a resilience path, not as the recommended installation mode; the built-in chat experience expects marked to be present for rich markdown.