Chat ยท Guides

Markdown Rendering

AI messages in @threadplane/chat can render full markdown -- headings, code blocks, tables, lists, blockquotes, and inline formatting. The renderMarkdown() utility does the work, and CHAT_MARKDOWN_STYLES handles the styling.

#How It Works

The markdown pipeline has two stages:

  1. Parse: The renderMarkdown() function converts markdown text to sanitized HTML using the marked library (dynamically imported). If marked is not installed, it falls back to plain text with <br> newline conversion.
  2. Style: The CHAT_MARKDOWN_STYLES constant provides CSS rules scoped under the .chat-md class, targeting all common markdown elements.

#The renderMarkdown() Function

import { renderMarkdown } from '@threadplane/chat';
import { DomSanitizer } from '@angular/platform-browser';
 
// In a component:
private sanitizer = inject(DomSanitizer);
 
renderMd(content: string): SafeHtml {
  return renderMarkdown(content, this.sanitizer);
}

Signature:

function renderMarkdown(content: string, sanitizer: DomSanitizer): SafeHtml
ParameterTypeDescription
contentstringRaw markdown text to render
sanitizerDomSanitizerAngular's DOM sanitizer for XSS protection

Returns: SafeHtml -- sanitized HTML that can be bound via [innerHTML].

Behavior:

  • When marked is installed, parses markdown to HTML, sanitizes it through Angular's SecurityContext.HTML, then marks the result as trusted.
  • When marked is not available, escapes HTML entities (&, <, >) and converts newlines to <br> tags.
Dynamic import

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.

#CHAT_MARKDOWN_STYLES

The CHAT_MARKDOWN_STYLES constant provides CSS rules for all standard markdown elements. It targets the .chat-md class using ::ng-deep for view encapsulation compatibility.

import { CHAT_MARKDOWN_STYLES } from '@threadplane/chat';
 
@Component({
  styles: [CHAT_MARKDOWN_STYLES],
  // ...
})
export class MyComponent {}

#Styled Elements

The stylesheet covers the following markdown elements:

ElementStyling
pBottom margin of 0.75em, last child has no bottom margin
code (inline)Background var(--ngaf-chat-surface-alt), padding, 4px radius, monospace font
preBackground var(--ngaf-chat-surface-alt), 12px 16px padding, horizontal scroll
pre codeNo extra background or padding (inherits from pre)
ul, ol0.5em vertical margin, 1.5em left padding
li0.25em vertical margin
aText color with underline
strongFont weight 600
blockquoteLeft border 3px solid, left padding 12px, muted text color
h11.25em font size, weight 600
h21.125em font size, weight 600
h31em font size, weight 600
tableCollapsed borders, full width
thAlt background, bold, 0.875em
tdStandard border and padding

All colors reference --ngaf-chat-* CSS custom properties, so markdown elements automatically respect the active chat theme.

#Using Markdown in Custom Components

Let's render markdown in a custom message template. Apply both the styles and the .chat-md class:

import { Component, inject } from '@angular/core';
import { DomSanitizer } from '@angular/platform-browser';
import {
  ChatMessageListComponent,
  MessageTemplateDirective,
  CHAT_MARKDOWN_STYLES,
  renderMarkdown,
} from '@threadplane/chat';
 
@Component({
  selector: 'app-chat-view',
  standalone: true,
  imports: [ChatMessageListComponent, MessageTemplateDirective],
  styles: [CHAT_MARKDOWN_STYLES],
  template: `
    <chat-message-list [agent]="chatRef">
      <ng-template chatMessageTemplate="ai" let-message>
        <div
          class="chat-md"
          [innerHTML]="renderMd(message.content)"
        ></div>
      </ng-template>
    </chat-message-list>
  `,
})
export class ChatViewComponent {
  private sanitizer = inject(DomSanitizer);
 
  // chatRef = injectAgent(); // with provideAgent({...}) in the component's providers
 
  renderMd(content: string | unknown) {
    if (typeof content !== 'string') return '';
    return renderMarkdown(content, this.sanitizer);
  }
}
The .chat-md class is required

The markdown styles are scoped to .chat-md. Make sure the container element receiving [innerHTML] has this class, otherwise the rendered HTML will appear unstyled.

#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 ViewRegistry via 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:

import { ApplicationConfig } from '@angular/core';
import { MARKDOWN_VIEW_REGISTRY, cacheplaneMarkdownViews } from '@threadplane/chat';
import { overrideViews } from '@threadplane/render';
import { MyCodeBlockComponent } from './my-code-block.component';
 
export const appConfig: ApplicationConfig = {
  providers: [
    {
      provide: MARKDOWN_VIEW_REGISTRY,
      useValue: overrideViews(cacheplaneMarkdownViews, {
        'code-block': MyCodeBlockComponent,
      }),
    },
  ],
};

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).

overrideViews vs withViews

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:

import { Component } from '@angular/core';
import { ChatStreamingMdComponent, cacheplaneMarkdownViews } from '@threadplane/chat';
import { overrideViews } from '@threadplane/render';
import { MyCodeBlockComponent } from './my-code-block.component';
 
@Component({
  selector: 'app-custom-chat',
  standalone: true,
  imports: [ChatStreamingMdComponent],
  template: `
    <chat-streaming-md [content]="content" [viewRegistry]="myRegistry" />
  `,
})
export class CustomChatComponent {
  content = '';
  myRegistry = overrideViews(cacheplaneMarkdownViews, {
    'code-block': MyCodeBlockComponent,
  });
}

#Node-Type Reference

cacheplaneMarkdownViews covers every node type emitted by @cacheplane/partial-markdown. Use these keys when calling overrideViews or withViews.

Use 'code-block', not 'code'

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'.

KeyDescription
'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>)
'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 --ngaf-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.

#Without marked

If you skip installing marked, markdown content renders as plain text with line breaks preserved. That's fine for simple chat apps that don't need rich formatting.

# Full markdown support:
npm install marked
 
# Or skip it -- plain text fallback works automatically