hashbrown

Magic Text in Angular

1. Intro

Magic Text is Hashbrown's optimistic Markdown parser and renderer for streaming LLM output. It is designed to render partial Markdown while text is still arriving, then converge to the final structure as the stream completes.

Why this is useful in real apps:

  • Streaming stability: users see useful formatted output early, even before the full response is done.
  • Predictable rendering: content is parsed into trusted node types instead of arbitrary HTML.
  • Incremental updates: unchanged segments stay stable while new content streams in.
  • Animation-ready segmentation: text can be segmented (for example by word or grapheme), so you can animate newly arrived segments.
  • Citation support: markdown citations can become interactive source links in your UI.

Hashbrowns at Waffle House started as simple scattered potatoes when the first shop opened in 19551⁠; the now-iconic "scattered, smothered & covered" shorthand hit menus in February 19842⁠. Grill crews kept riffing until the all-the-way call piled on every topping — cheese, ham, tomatoes, jalapeños, mushrooms, chili, and sausage gravy — a salty love letter to late-night diners3⁠. Today cooks still sear the potatoes on the open griddle so cheese melts while onions stay just caramelized4⁠.

If you only need streaming Markdown, you can skip citations and move to exposeMarkdown().

When you do want source links, Magic Text supports a citations extension:

  • Inline reference: [^source-id]
  • Definition: [^source-id]: Source title https://example.com

Prompt pattern you can give your model:

system prompt excerpt
Write in Markdown.

When you make a factual claim that needs a source, add an inline citation like [^id].
At the end, add a definition line for each citation:
[^id]: Short source title https://full-url

Do not invent URLs. Omit citations if you are unsure.

For UI resources, exposeMarkdown can append this citation guidance automatically by setting citations: true.


2. exposeMarkdown()

exposeMarkdown() is the easiest way to expose streaming Markdown to model-generated UI. It exposes a constrained component where the model controls only children (the Markdown content).

Default renderer example:

ui-kit.ts
import { createUiKit, exposeMarkdown } from '@hashbrownai/angular';

export const uiKit = createUiKit({
  components: [
    exposeMarkdown({
      citations: true,
      options: { segmenter: { granularity: 'word' } },
      caret: true,
      className: 'chat-markdown',
      onLinkClick: ({ mouseEvent, url }) => {
        if (url.startsWith('http')) {
          return;
        }
        mouseEvent.preventDefault();
      },
      onCitationClick: ({ citation }) => {
        console.log('citation clicked', citation);
      },
    }),
  ],
});

Custom renderer example:

ui-kit.ts
import { createUiKit, exposeMarkdown } from '@hashbrownai/angular';
import { AppMarkdownRenderer } from './app-markdown-renderer';

export const uiKit = createUiKit({
  components: [
    exposeMarkdown({
      renderer: AppMarkdownRenderer,
      citations: true,
    }),
  ],
});

Important behavior:

  • With the built-in renderer, you can configure options, caret, className, onLinkClick, and onCitationClick.
  • If you pass renderer, those built-in settings are not supported. Your renderer owns display and interaction.
  • A custom renderer component must define both text and isComplete inputs.

You will typically pass exposeMarkdown() into:


3. MagicTextRenderer

The renderer component is exported as MagicText and used as <hb-magic-text>.

magic-text.component.ts
import { Component, input } from '@angular/core';
import { MagicText } from '@hashbrownai/angular';

@Component({
  selector: 'app-markdown',
  standalone: true,
  imports: [MagicText],
  template: `
    <hb-magic-text
      [text]="text()"
      [isComplete]="isComplete()"
      [options]="{ segmenter: { granularity: 'word' } }"
      [caret]="true"
      className="app-markdown"
    />
  `,
})
export class AppMarkdown {
  text = input.required<string>();
  isComplete = input(false);
}

Inputs and outputs:

  • text (required): current Markdown text (usually grows while streaming).
  • isComplete (default false): marks the current text as finalized.
  • options: partial parser options (segmenter, enableTables, enableAutolinks).
  • caret: true | false | TemplateRef to show/hide/customize streaming caret.
  • className: root class for styling.
  • linkClick: emitted for link and autolink clicks.
  • citationClick: emitted for citation clicks.

4. Custom Magic Text Renderer

Wrap <hb-magic-text> when you need to override node rendering, text segment rendering, or caret rendering.

app-markdown-renderer.ts
import { Component, input } from '@angular/core';
import {
  MagicText,
  MagicTextRenderNode,
  MagicTextRenderTextSegment,
  MagicTextRenderCaret,
} from '@hashbrownai/angular';

@Component({
  selector: 'app-markdown-renderer',
  standalone: true,
  imports: [
    MagicText,
    MagicTextRenderNode,
    MagicTextRenderTextSegment,
    MagicTextRenderCaret,
  ],
  template: `
    <hb-magic-text [text]="text()" [isComplete]="isComplete()" [caret]="true">
      <ng-template hbMagicTextRenderNode nodeType="citation" let-node="node">
        <sup class="citation">[{{ node.number ?? node.idRef }}]</sup>
      </ng-template>

      <ng-template hbMagicTextRenderTextSegment let-segment="segment">
        <span class="segment" [class.ws]="segment.isWhitespace">{{
          segment.text
        }}</span>
      </ng-template>

      <ng-template hbMagicTextRenderCaret let-depth="depth">
        <span class="caret" [attr.data-depth]="depth">|</span>
      </ng-template>
    </hb-magic-text>
  `,
})
export class AppMarkdownRenderer {
  text = input.required<string>();
  isComplete = input(false);
}

Node override directives:

  • Per-node override: ng-template[hbMagicTextRenderNode] with nodeType="..."
  • Text segment override: ng-template[hbMagicTextRenderTextSegment]
  • Caret override: ng-template[hbMagicTextRenderCaret]

All node types and the directive to use:

Node type valueStructural directive
documenthbMagicTextRenderNode
paragraphhbMagicTextRenderNode
headinghbMagicTextRenderNode
blockquotehbMagicTextRenderNode
listhbMagicTextRenderNode
list-itemhbMagicTextRenderNode
tablehbMagicTextRenderNode
table-rowhbMagicTextRenderNode
table-cellhbMagicTextRenderNode
emhbMagicTextRenderNode
stronghbMagicTextRenderNode
strikethroughhbMagicTextRenderNode
linkhbMagicTextRenderNode
code-blockhbMagicTextRenderNode
thematic-breakhbMagicTextRenderNode
texthbMagicTextRenderNode
inline-codehbMagicTextRenderNode
soft-breakhbMagicTextRenderNode
hard-breakhbMagicTextRenderNode
imagehbMagicTextRenderNode
autolinkhbMagicTextRenderNode
citationhbMagicTextRenderNode
node (fallback for any unhandled type)hbMagicTextRenderNode

To expose your custom renderer to the model, pass it to exposeMarkdown():

ui-kit.ts
import { createUiKit, exposeMarkdown } from '@hashbrownai/angular';
import { AppMarkdownRenderer } from './app-markdown-renderer';

export const uiKit = createUiKit({
  components: [
    exposeMarkdown({
      renderer: AppMarkdownRenderer,
      name: 'Markdown',
      description: 'Render Markdown content for the user.',
    }),
  ],
});
Magic Text in Angular1. Intro2. exposeMarkdown()3. MagicTextRenderer4. Custom Magic Text Renderer