# Markdown

> Render a reply's Markdown, including Markdown that is still arriving, without loading anything the text points at.

Source: https://ai.honestui.com/docs/components/markdown

```tsx
"use client"

import * as React from "react"

import { Markdown } from "@/components/ai/markdown"
import { StreamingText, useSmoothStream } from "@/components/ai/streaming-text"

// Sample reply.
const sampleReply = `Rounding each row **before** the sum is what causes the drift. Three rows of 0.336 show it:

| Approach | Total |
| --- | --- |
| Round each row, then sum | 1.02 |
| Sum, then round once | 1.01 |

To fix the export:

1. Sum the raw amounts.
2. Round the total once, with \`roundTotal\`.

\`\`\`ts
function exportTotal(rows: Row[]) {
  const total = rows.reduce((sum, row) => sum + row.amount, 0)
  return roundTotal(total)
}
\`\`\`
`

// Cut into uneven pieces, so syntax arrives half-written as it does from a model.
const sampleChunks = sampleReply.match(/[\s\S]{1,23}/g) ?? []

const CHUNK_MILLISECONDS = 160

/**
 * No model streams into this preview. A timer delivers the sample reply a
 * piece at a time, and Markdown renders whatever has arrived. Try again
 * restarts the timer.
 */
export function MarkdownDemo() {
  const [chunkCount, setChunkCount] = React.useState(0)
  const hasAllChunks = chunkCount >= sampleChunks.length
  const stream = useSmoothStream(sampleChunks.slice(0, chunkCount).join(""), {
    charactersPerSecond: 160,
  })
  const isComplete = hasAllChunks && stream.isSettled

  React.useEffect(() => {
    if (hasAllChunks) return
    const timer = window.setTimeout(() => setChunkCount((count) => count + 1), CHUNK_MILLISECONDS)
    return () => window.clearTimeout(timer)
  }, [chunkCount, hasAllChunks])

  return (
    <div className="w-full max-w-md min-w-0">
      <StreamingText
        status={isComplete ? "complete" : "streaming"}
        // Paced text changes many times a second, which is too much to announce.
        announce={false}
        copyText={sampleReply}
        onRetry={() => setChunkCount(0)}
      >
        <Markdown isStreaming={!isComplete}>{stream.text}</Markdown>
      </StreamingText>
    </div>
  )
}

```

## Overview [#overview]

Models write Markdown, and they deliver it in pieces. Rendered as it arrives, half-written syntax flashes on screen as stray asterisks and backticks until its other half shows up. Markdown renders the text you have so far the way it will read once it is whole.

Pass the Markdown source as a string. Set `isStreaming` while more text is on the way. A fenced code block becomes a [Code View](/docs/components/code-view), so it has a copy button and line numbers.

Markdown parses text and requests nothing. HTML in the source is shown as text, not run. An image is shown as a link, because loading an image sends a request to a URL the model chose.

It is built on `react-markdown` with GitHub Flavored Markdown: tables, task lists, strikethrough, and bare links. Its text is styled by [Prose](/docs/prose), a stylesheet you can also use without this component.

A fence can also become one of your components. See [Blocks](#blocks).

## Anatomy [#anatomy]

<Anatomy title="Markdown">
  <AnatomyItem name="Text" description="Headings, paragraphs, lists, quotes, links, and inline code" />

  <AnatomyItem name="Code block" description="A Code View, titled with the block's language" />

  <AnatomyItem name="Table" description="A table inside a region that scrolls sideways" />

  <AnatomyItem name="Block" description="A fenced block drawn by a component you map to its language" />

  <AnatomyItem name="Caret" description="Marks the end of the text while it streams" />
</Anatomy>

Installing `markdown` brings three registry items with it: [Code View](/docs/components/code-view), [Prose](/docs/prose), and `repair-markdown`, the function behind `isStreaming`. It also installs `react-markdown` and `remark-gfm`.

## Behavior [#behavior]

### Streaming [#streaming]

With `isStreaming`, Markdown repairs the last block of the text before it parses it. A block ends at a blank line, so everything before the last blank line is left as written.

| Arrived so far                         | Rendered as                                                 |
| -------------------------------------- | ----------------------------------------------------------- |
| `a **bold`                             | "bold" in bold                                              |
| `an *italic`, `an _italic`, `~~struck` | The style, closed at the end of the text                    |
| ``use `npm ru``                        | "npm ru" as inline code                                     |
| `see [the docs](https://exa`           | "see the docs", as plain text until the address is complete |
| `![chart](https://exa`                 | Nothing, until the address is complete                      |
| A code fence with no closing fence     | A code block that runs to the end of the text               |

A marker with nothing after it yet, such as a lone `**` at the end, is held back until text follows it.

Some syntax cannot be recognized until more of it arrives, and Markdown does not guess at it. A table is a paragraph of pipes until its divider row is complete. A link's label shows its opening bracket until the closing bracket arrives. Both correct themselves on the next piece of text.

When `isStreaming` is false, the text is parsed exactly as given.

### Caret [#caret]

While streaming, a caret sits after the last words, inside the paragraph, heading, or list item they belong to. It is not drawn after a code block or a table.

Inside a [Streaming Text](/docs/components/streaming-text), Markdown's caret replaces Streaming Text's own, which would otherwise land on a line of its own under the last block. Set `isStreaming` on Markdown whenever the Streaming Text around it has `status="streaming"`.

### Code blocks [#code-blocks]

A fenced block renders as a Code View. Its title is the first of these the fence has:

1. A title after the language, written `title="export.ts"` or `filename="export.ts"`.
2. The language.
3. "Code".

Markdown does not highlight syntax. Pass `renderCodeLine` to color each line with the highlighter you already have. It receives the line, the language, and the line's index with the whole block, for a highlighter that reads across lines. [Syntax highlighting](#syntax-highlighting) has one built on Shiki.

`renderCodeActions` adds controls to a code block's header, before Copy. It receives the block's code, language, and title. Return a button for each action, and only for an action your application handles.

### Blocks [#blocks]

`blocks` maps a fence's language to a function that draws the fence's text as a component. A reply can then carry a [Flowchart](/docs/components/flowchart), a table of records, or a chart, written as data:

````md
```flowchart
{ "label": "Invoice follow-up", "steps": [{ "kind": "Trigger", "title": "Invoice becomes overdue" }] }
```
````

```tsx
const blocks = {
  flowchart(body: string) {
    const { label, steps } = flowchartSchema.parse(JSON.parse(body))
    return (
      <Flowchart label={label}>
        {steps.map((step) => (
          <FlowStep key={step.title} kind={step.kind} title={step.title} />
        ))}
      </Flowchart>
    )
  },
}

<Markdown blocks={blocks}>{reply.text}</Markdown>
```

The model wrote the text, so check it before you draw it. Three rules keep a block truthful:

* **While it arrives, it is held back.** With `isStreaming`, a block whose closing fence has not arrived shows "Waiting for the rest of this block…" instead. Your function is not called with half of its data.
* **When it does not fit, it is shown as code.** If your function throws, as a schema's `parse` does, Markdown shows the fence's text in a Code View. Nothing is lost, and nothing untrue is drawn. An error thrown later, while a component you returned renders, is not caught.
* **It keeps its own styles.** A block is wrapped in `not-prose`, so the text styles do not reach into it.

Markdown does not tell the model which blocks exist. Describe each block's language and data in your prompt.

Define `blocks` outside your component, or memoize it. A new object on every render makes every block render again.

### Styling [#styling]

The root element has the `honest-prose` class. Change the text size, line height, and spacing with [Prose's variables](/docs/prose#variables), on Markdown or on anything around it:

```tsx
<Markdown className="[--prose-size:13px] [--prose-flow:8px]">{reply.text}</Markdown>
```

A utility class for a property Prose sets, such as `text-sm`, has no effect, because Prose's rules sit outside Tailwind's layers.

### Links, images, and HTML [#links-images-and-html]

Links render as links. `react-markdown` drops an address whose scheme is not `http`, `https`, `mailto`, or another safe one, so a `javascript:` link is not clickable.

An image renders as a link to the image, with the image's description as the link text. To show images, map `img` in `components`, and load them only from hosts you trust.

Raw HTML in the source appears as text.

### Your own elements [#your-own-elements]

`components` replaces how any element is rendered. It is passed to `react-markdown` after Markdown's own mappings for `pre`, `img`, `table`, and `input`, so yours win. Mapping `pre` replaces code blocks and `blocks` together.

`remarkPlugins` and `rehypePlugins` add plugins. Parser plugins run after GitHub Flavored Markdown. Tree plugins run before the caret is placed. A plugin that turns raw HTML into elements, such as `rehype-raw`, removes the protection described above.

### Cost [#cost]

Each change to the text parses all of it again. That is fine for a reply. For a document of many thousands of lines that streams, render finished sections in their own `Markdown` and stream only the last one.

## Add-ons [#add-ons]

Two optional files extend Markdown. Each installs on its own and brings its packages with it.

### Math [#math]

`markdown-math` renders math with KaTeX.

<CliBlock commands="[&#x22;@honestai/markdown-math&#x22;]" />

```tsx
import "katex/dist/katex.min.css"

import { mathPlugins, normalizeMathDelimiters } from "@/lib/markdown-math"

<Markdown {...mathPlugins}>{normalizeMathDelimiters(reply.text)}</Markdown>
```

Math is written between `$$`: inside a sentence for inline math, or with each `$$` on a line of its own for a displayed equation. Models more often write `\( … \)` and `\[ … \]`. `normalizeMathDelimiters` rewrites those as `$$` and leaves code alone.

A single `$` is text by default, so "$5 and $10" stays a pair of prices. To read `$x$` as math as well, make your own plugins once, outside your components:

```tsx
const mathPlugins = createMathPlugins({ singleDollar: true })
```

Three limits to know:

* While it streams, an equation shows as its source until its closing delimiter arrives.
* KaTeX shows an expression it cannot parse as its source, in its error color.
* `normalizeMathDelimiters` treats every `\[ … \]` pair outside code as math, including one that was meant as escaped brackets.

### Syntax highlighting [#syntax-highlighting]

`markdown-shiki` makes a `renderCodeLine` that colors code with Shiki.

<CliBlock commands="[&#x22;@honestai/markdown-shiki&#x22;]" />

```tsx
import { createShikiCodeLine } from "@/lib/markdown-shiki"

// Once, outside your components.
const renderCodeLine = createShikiCodeLine()

<Markdown renderCodeLine={renderCodeLine}>{reply.text}</Markdown>
```

Shiki and each language load the first time a block needs them. Until then, and for a language Shiki does not know, the code is plain text. A block is highlighted whole, so a comment or string that spans lines is colored correctly.

`createShikiCodeLine({ light, dark })` takes the two themes. The dark one applies under Tailwind's `dark` variant. The defaults are GitHub's high-contrast themes.

A block that is still arriving is highlighted again each time it grows. That is fine for a reply. For code of many hundreds of lines that streams, pass `renderCodeLine` only once the text is complete.

## Accessibility [#accessibility]

Markdown renders native elements: headings, lists, tables, links, and block quotes keep their roles.

A heading in the source keeps its level. A reply that starts at `#` puts a level-one heading in the middle of your page. If that breaks your page's outline, map `h1` to `h3` in `components`.

A table sits in a group named "Table" that can take focus, so a wide table can be scrolled from the keyboard. A task list item's checkbox is disabled and named "Done" or "Not done". The caret is hidden from assistive technology.

A component drawn by `blocks` brings its own roles and names. Markdown adds none around it. Controls from `renderCodeActions` come before Copy in the tab order.

Markdown is not a live region. To have a streamed reply announced, place it in a [Streaming Text](/docs/components/streaming-text) or a [Chat Thread](/docs/components/chat-thread).

Every string comes from `labels`.

The preview has been checked with a keyboard and with axe. It has not been checked with a screen reader yet.

| Key            | Action                                     |
| -------------- | ------------------------------------------ |
| <kbd>Tab</kbd> | Move across links, code blocks, and tables |
| Arrow keys     | Scroll a focused code block or table       |

## Installation [#installation]


  

  
    <CliBlock commands="[&#x22;@honestai/markdown&#x22;]" />
  

  
    
      
        Add the items it is built from.
      

      <CliBlock commands="[&#x22;@honestai/code-view&#x22;, &#x22;@honestai/prose&#x22;, &#x22;@honestai/repair-markdown&#x22;]" />

      
        Install the parser.
      

      ```bash
      npm install react-markdown remark-gfm
      ```

      
        Copy and paste this file into your project.
      

      ### components/ai/markdown.tsx

```tsx
"use client"

import * as React from "react"
import ReactMarkdown, { type Components, type ExtraProps, type Options } from "react-markdown"
import remarkGfm from "remark-gfm"

import { cn } from "@/lib/utils"
import { CodeView } from "@/components/ai/code-view"
import { repairMarkdown } from "@/lib/repair-markdown"

export type MarkdownLabels = {
  /** Titles a code block that names no language. */
  code: string
  table: string
  /** The link text for an image that has no description. */
  image: string
  taskDone: string
  taskOpen: string
  /** Stands in for a block from `blocks` until its closing fence arrives. */
  blockPending: string
}

export const defaultMarkdownLabels: MarkdownLabels = {
  code: "Code",
  table: "Table",
  image: "Image",
  taskDone: "Done",
  taskOpen: "Not done",
  blockPending: "Waiting for the rest of this block…",
}

// The part of a syntax tree this file reads. Declared here so installing the
// component does not also install a package of types.
type TreeNode = {
  type: string
  tagName?: string
  value?: string
  properties?: Record<string, unknown>
  children?: TreeNode[]
}

const caretContainers = new Set(["ul", "ol", "li", "blockquote", "p", "h1", "h2", "h3", "h4", "h5", "h6"])
const caretHolders = new Set(["p", "li", "h1", "h2", "h3", "h4", "h5", "h6"])
const caretBlockers = new Set(["pre", "table", "hr"])

const caret: TreeNode = {
  type: "element",
  tagName: "span",
  properties: {
    ariaHidden: "true",
    dataSlot: "markdown-caret",
    className:
      "ms-0.5 inline-block h-[1em] w-0.5 translate-y-[0.15em] rounded-full bg-[var(--hui-color-foreground-base-primary)]",
  },
  children: [],
}

/** Puts a caret after the last words of the text, wherever they are nested. */
function rehypeStreamingCaret() {
  return (tree: TreeNode) => {
    let parent = tree
    let last: TreeNode | undefined

    for (;;) {
      last = parent.children?.findLast((child) => child.type !== "text" || child.value?.trim() !== "")
      if (last?.type !== "element" || !caretContainers.has(last.tagName ?? "")) break
      parent = last
    }

    const isEmpty = parent === tree && last === undefined
    const endsInText = caretHolders.has(parent.tagName ?? "") && !caretBlockers.has(last?.tagName ?? "")
    if (isEmpty || endsInText) parent.children?.push(caret)
  }
}

/** A fenced code block, as its renderers receive it. */
export type MarkdownCodeBlock = {
  code: string
  /** The word after the opening fence, such as `ts`. */
  language: string | undefined
  /** From `title="…"` or `filename="…"` after the language. */
  title: string | undefined
}

export type MarkdownCodeLine = {
  /** The line's place in the block, from zero. */
  index: number
  /** The whole block, for a highlighter that reads across lines. */
  code: string
}

/**
 * Draws one fenced block from its text. Throw when the text is not what the
 * block expects, and the source is shown as code instead.
 */
export type MarkdownBlockRenderer = (body: string) => React.ReactNode

type MarkdownSettings = {
  source: string
  isStreaming: boolean
  labels: MarkdownLabels
  blocks: Record<string, MarkdownBlockRenderer> | undefined
  renderCodeLine: MarkdownProps["renderCodeLine"]
  renderCodeActions: MarkdownProps["renderCodeActions"]
}

// The element renderers below are defined once, so a block keeps its state
// while the text around it changes. They read the current props from here.
const MarkdownSettingsContext = React.createContext<MarkdownSettings | null>(null)

function useMarkdownSettings() {
  const settings = React.useContext(MarkdownSettingsContext)
  if (settings === null) throw new Error("This element must be rendered inside Markdown.")
  return settings
}

const FENCE_META_TITLE = /(?:^|\s)(?:title|filename)=(?:"([^"]*)"|'([^']*)'|(\S+))/
const OPENING_FENCE_LINE = /^[\s>]*(`{3,}|~{3,})/
const CLOSING_FENCE_LINE = /^[\s>]*(`{3,}|~{3,})\s*$/

type FenceNode = NonNullable<ExtraProps["node"]>

/** Whether the fence that produced this node has its closing line in the text. */
function isFenceClosed(node: FenceNode | undefined, source: string) {
  const start = node?.position?.start.offset
  const end = node?.position?.end.offset
  if (start === undefined || end === undefined) return true

  const lines = source.slice(start, end).trimEnd().split("\n")
  const opening = OPENING_FENCE_LINE.exec(lines[0])?.[1]
  const closing = CLOSING_FENCE_LINE.exec(lines[lines.length - 1])?.[1]
  return (
    lines.length > 1 &&
    opening !== undefined &&
    closing !== undefined &&
    opening[0] === closing[0] &&
    closing.length >= opening.length
  )
}

/** Runs a block's renderer. The text is the model's, so one that does not fit is expected, not an error. */
function drawBlock(render: MarkdownBlockRenderer, body: string) {
  try {
    return { content: render(body) }
  } catch {
    // The caller shows the text as code instead, which loses nothing.
    return null
  }
}

function MarkdownPre({ children, node }: React.ComponentProps<"pre"> & ExtraProps) {
  const { source, isStreaming, labels, blocks, renderCodeLine, renderCodeActions } = useMarkdownSettings()

  const codeElement = React.Children.toArray(children).find(
    (child): child is React.ReactElement<{ className?: string; children?: React.ReactNode }> =>
      React.isValidElement(child)
  )
  const codeNode = node?.children.find((child) => child.type === "element" && child.tagName === "code")
  const meta = (codeNode?.data as { meta?: string | null } | undefined)?.meta ?? ""
  const titleMatch = FENCE_META_TITLE.exec(meta)

  const block: MarkdownCodeBlock = {
    // The parser ends a block with a line break, which would list an empty last line.
    code: React.Children.toArray(codeElement?.props.children).join("").replace(/\n$/, ""),
    language: /language-(\S+)/.exec(codeElement?.props.className ?? "")?.[1],
    title: titleMatch ? (titleMatch[1] ?? titleMatch[2] ?? titleMatch[3]) : undefined,
  }

  // `hasOwn`, so a language such as "constructor" does not find a method of every object.
  const renderBlock =
    block.language !== undefined && blocks && Object.hasOwn(blocks, block.language)
      ? blocks[block.language]
      : undefined
  if (renderBlock) {
    // Half a block's data would fail to parse, or draw something untrue.
    if (isStreaming && !isFenceClosed(node, source)) {
      return (
        <p
          data-slot="markdown-block-pending"
          className="text-[var(--hui-color-foreground-base-secondary)]"
        >
          {labels.blockPending}
        </p>
      )
    }

    const drawn = drawBlock(renderBlock, block.code)
    if (drawn) {
      return (
        <div data-slot="markdown-block" data-language={block.language} className="not-prose">
          {drawn.content}
        </div>
      )
    }
  }

  const { code, language } = block
  return (
    <CodeView
      filename={block.title ?? language ?? labels.code}
      code={code}
      renderLine={renderCodeLine && ((text, index) => renderCodeLine(text, language, { index, code }))}
      actions={renderCodeActions?.(block)}
    />
  )
}

// Loading an image sends a request to a URL the text chose, which can carry
// data out of the conversation. A link leaves that request to the person.
function MarkdownImage({ src, alt }: React.ComponentProps<"img">) {
  const { labels } = useMarkdownSettings()
  const text = alt || labels.image
  return typeof src === "string" && src !== "" ? <a href={src}>{text}</a> : <span>{text}</span>
}

function MarkdownTable({ children }: React.ComponentProps<"table">) {
  const { labels } = useMarkdownSettings()
  return (
    // Focusable so a keyboard user can scroll a wide table.
    <div
      role="group"
      aria-label={labels.table}
      tabIndex={0}
      data-slot="markdown-table"
      className="prose-scroll outline-none focus-visible:[outline:var(--hui-focus-ring)]"
    >
      <table>{children}</table>
    </div>
  )
}

// A task list item's checkbox is a record of the text, not a control.
function MarkdownTaskCheckbox({ type, checked }: React.ComponentProps<"input">) {
  const { labels } = useMarkdownSettings()
  return (
    <input
      type={type}
      checked={checked}
      disabled
      readOnly
      aria-label={checked ? labels.taskDone : labels.taskOpen}
    />
  )
}

const defaultComponents: Components = {
  pre: MarkdownPre,
  img: MarkdownImage,
  table: MarkdownTable,
  input: MarkdownTaskCheckbox,
}

export type MarkdownProps = Omit<React.ComponentProps<"div">, "children"> & {
  /** The Markdown source, all of it or as much as has arrived. */
  children: string
  /** Set while text is still arriving. Closes unfinished syntax and shows a caret. */
  isStreaming?: boolean
  /**
   * Draws a fenced block as a component, by the fence's language. A block is
   * held back until its closing fence arrives.
   */
  blocks?: Record<string, MarkdownBlockRenderer>
  /** Colors one line of a code block. Without it, code is plain text. */
  renderCodeLine?: (
    text: string,
    language: string | undefined,
    line: MarkdownCodeLine
  ) => React.ReactNode
  /** Controls for a code block's header, placed before Copy. */
  renderCodeActions?: (block: MarkdownCodeBlock) => React.ReactNode
  /** Replaces how an element is rendered, such as `a` or `img`. */
  components?: Components
  /** Parser plugins that run after GitHub Flavored Markdown. */
  remarkPlugins?: NonNullable<Options["remarkPlugins"]>
  /** Tree plugins that run before the caret is placed. */
  rehypePlugins?: NonNullable<Options["rehypePlugins"]>
  labels?: Partial<MarkdownLabels>
}

/**
 * Renders Markdown, including Markdown that is still arriving. It parses the
 * text it is given and requests nothing: HTML in the source is shown as text,
 * and an image becomes a link, so a reply cannot make the browser load a URL.
 * Its text is styled by the `honest-prose` class, from `prose.css`.
 */
function Markdown({
  children,
  isStreaming = false,
  blocks,
  renderCodeLine,
  renderCodeActions,
  components: componentsProp,
  remarkPlugins: remarkPluginsProp,
  rehypePlugins: rehypePluginsProp,
  labels: labelsProp,
  className,
  ...props
}: MarkdownProps) {
  const source = isStreaming ? repairMarkdown(children) : children

  const settings = React.useMemo<MarkdownSettings>(
    () => ({
      source,
      isStreaming,
      labels: { ...defaultMarkdownLabels, ...labelsProp },
      blocks,
      renderCodeLine,
      renderCodeActions,
    }),
    [source, isStreaming, labelsProp, blocks, renderCodeLine, renderCodeActions]
  )
  const components = React.useMemo<Components>(
    () => ({ ...defaultComponents, ...componentsProp }),
    [componentsProp]
  )
  const remarkPlugins = React.useMemo(
    () => [remarkGfm, ...(remarkPluginsProp ?? [])],
    [remarkPluginsProp]
  )
  const rehypePlugins = React.useMemo(
    () => [...(rehypePluginsProp ?? []), ...(isStreaming ? [rehypeStreamingCaret] : [])],
    [rehypePluginsProp, isStreaming]
  )

  return (
    <div
      data-slot="markdown"
      data-streaming={isStreaming ? "" : undefined}
      className={cn("honest-prose", className)}
      {...props}
    >
      <MarkdownSettingsContext.Provider value={settings}>
        <ReactMarkdown remarkPlugins={remarkPlugins} rehypePlugins={rehypePlugins} components={components}>
          {source}
        </ReactMarkdown>
      </MarkdownSettingsContext.Provider>
    </div>
  )
}

export { Markdown, repairMarkdown }

```

      
        Update the import paths to match your project setup.
      
    
  


Then import Prose in your global stylesheet, after Tailwind. Without it, the text is unstyled.

```css
@import "../styles/prose.css";
```

## Usage [#usage]

```tsx
import { Markdown } from "@/components/ai/markdown"
import { StreamingText } from "@/components/ai/streaming-text"

<StreamingText status={reply.isDone ? "complete" : "streaming"} copyText={reply.text}>
  <Markdown isStreaming={!reply.isDone}>{reply.text}</Markdown>
</StreamingText>
```

On its own, for text that is already complete:

```tsx
<Markdown renderCodeLine={(text, language) => highlight(text, language)}>
  {document.body}
</Markdown>
```

### Repairing text yourself [#repairing-text-yourself]

`repairMarkdown` is the function behind `isStreaming`. It is a file of its own with no React in it, so you can install it without Markdown and use it when another renderer draws the text:

<CliBlock commands="[&#x22;@honestai/repair-markdown&#x22;]" />

```tsx
import { repairMarkdown } from "@/lib/repair-markdown"

const safeToParse = repairMarkdown(textSoFar)
```

## Examples [#examples]

### Complete text [#complete-text]

A finished document with a heading, struck text, a task list, a quote, and an image shown as a link.

```tsx
import { Markdown } from "@/components/ai/markdown"

// Sample document.
const sampleDocument = `## Release checklist

The export fix is ready. ~~Ship on Friday~~ Ship on Monday, after the backfill.

- [x] Round the total once
- [x] Add a test for three rows of 0.336
- [ ] Backfill the 1,204 affected invoices

> Totals already sent to customers are not changed by the backfill.

The chart of the drift is an image, so it is shown as a link:
![Drift per export, by month](https://example.com/drift.png)
`

export function MarkdownStaticExample() {
  return (
    <div className="w-full max-w-md min-w-0">
      <Markdown>{sampleDocument}</Markdown>
    </div>
  )
}

```

### A block drawn as a component [#a-block-drawn-as-a-component]

A `flowchart` fence drawn as a [Flowchart](/docs/components/flowchart). While the fence is open, the reply shows that it is waiting.

```tsx
"use client"

import * as React from "react"
import { z } from "zod"

import { Flowchart, FlowStep } from "@/components/ai/flowchart"
import { Markdown, type MarkdownBlockRenderer } from "@/components/ai/markdown"
import { StreamingText } from "@/components/ai/streaming-text"

const flowchartSchema = z.object({
  label: z.string(),
  steps: z.array(z.object({ kind: z.string(), title: z.string(), caption: z.string().optional() })).min(1),
})

// The fence's text comes from the model, so it is checked before it is drawn.
// When it does not fit the schema, `parse` throws and Markdown shows the text as code.
const blocks: Record<string, MarkdownBlockRenderer> = {
  flowchart(body) {
    const { label, steps } = flowchartSchema.parse(JSON.parse(body))
    return (
      <Flowchart label={label}>
        {steps.map((step) => (
          <FlowStep key={step.title} kind={step.kind} title={step.title} caption={step.caption} />
        ))}
      </Flowchart>
    )
  },
}

// Sample reply.
const sampleReply = `The follow-up runs as **three steps**:

\`\`\`flowchart
{
  "label": "Invoice follow-up workflow",
  "steps": [
    { "kind": "Trigger", "title": "Invoice becomes overdue", "caption": "Runs the day after the due date." },
    { "kind": "If / Else", "title": "Amount is over 1,000", "caption": "Larger invoices go to a person first." },
    { "kind": "Action", "title": "Draft a reminder email", "caption": "Waits for approval." }
  ]
}
\`\`\`

Nothing is sent until someone approves the draft.
`

// Cut into uneven pieces, so the block's data arrives half-written as it does from a model.
const sampleChunks = sampleReply.match(/[\s\S]{1,31}/g) ?? []

const CHUNK_MILLISECONDS = 140

/**
 * No model streams into this preview. A timer delivers the sample reply a
 * piece at a time. Try again restarts the timer.
 */
export function MarkdownBlocksExample() {
  const [chunkCount, setChunkCount] = React.useState(0)
  const isComplete = chunkCount >= sampleChunks.length

  React.useEffect(() => {
    if (isComplete) return
    const timer = window.setTimeout(() => setChunkCount((count) => count + 1), CHUNK_MILLISECONDS)
    return () => window.clearTimeout(timer)
  }, [chunkCount, isComplete])

  return (
    <div className="w-full max-w-md min-w-0">
      <StreamingText
        status={isComplete ? "complete" : "streaming"}
        // The text changes several times a second, which is too much to announce.
        announce={false}
        onRetry={() => setChunkCount(0)}
      >
        <Markdown isStreaming={!isComplete} blocks={blocks}>
          {sampleChunks.slice(0, chunkCount).join("")}
        </Markdown>
      </StreamingText>
    </div>
  )
}

```

### Math, highlighting, a title, and an action [#math-highlighting-a-title-and-an-action]

Both add-ons, a code block titled by its fence, and a Download action in its header.

```tsx
"use client"

import "katex/dist/katex.min.css"

import { Download as DownloadIcon } from "honestui/icons"

import { Button } from "@/components/ui/button"
import { Markdown, type MarkdownCodeBlock } from "@/components/ai/markdown"
import { mathPlugins, normalizeMathDelimiters } from "@/lib/markdown-math"
import { createShikiCodeLine } from "@/lib/markdown-shiki"

const renderCodeLine = createShikiCodeLine()

function downloadCode({ code, title }: MarkdownCodeBlock) {
  const url = URL.createObjectURL(new Blob([code], { type: "text/plain" }))
  const link = document.createElement("a")
  link.href = url
  link.download = title ?? "code.txt"
  link.click()
  URL.revokeObjectURL(url)
}

function renderCodeActions(block: MarkdownCodeBlock) {
  return (
    <Button variant="link" size="sm" onClick={() => downloadCode(block)}>
      <DownloadIcon aria-hidden="true" />
      Download
    </Button>
  )
}

// Sample document. The math uses the LaTeX delimiters models tend to write.
const sampleDocument = String.raw`Rounding each of \(n\) rows can move the total by up to half a cent per row:

\[
\left| \sum_{i=1}^{n} \operatorname{round}(a_i) - \operatorname{round}\Bigl(\sum_{i=1}^{n} a_i\Bigr) \right| \le \frac{n + 1}{2} \cdot 0.01
\]

Summing first removes the drift. A price such as $5 is left as text.

${"```"}ts title="export.ts"
function exportTotal(rows: Row[]) {
  const total = rows.reduce((sum, row) => sum + row.amount, 0)
  return roundTotal(total)
}
${"```"}
`

const source = normalizeMathDelimiters(sampleDocument)

export function MarkdownAddOnsExample() {
  return (
    <div className="w-full max-w-md min-w-0">
      <Markdown {...mathPlugins} renderCodeLine={renderCodeLine} renderCodeActions={renderCodeActions}>
        {source}
      </Markdown>
    </div>
  )
}

```

## API reference [#api-reference]

### Markdown [#markdown]

Accepts native div props beyond those listed.

| Prop                | Type                                                                                 | Default  | Description                                                                                              |
| ------------------- | ------------------------------------------------------------------------------------ | -------- | -------------------------------------------------------------------------------------------------------- |
| `children`          | string                                                                               | Required | The Markdown source, all of it or as much as has arrived.                                                |
| `isStreaming`       | boolean                                                                              | `false`  | Closes unfinished syntax and shows a caret.                                                              |
| `blocks`            | `Record<string, MarkdownBlockRenderer>`                                              | None     | Draws a fenced block as a component, by the fence's language.                                            |
| `renderCodeLine`    | `(text: string, language: string \| undefined, line: MarkdownCodeLine) => ReactNode` | None     | Colors one line of a code block.                                                                         |
| `renderCodeActions` | `(block: MarkdownCodeBlock) => ReactNode`                                            | None     | Controls for a code block's header, before Copy.                                                         |
| `components`        | `Components` from `react-markdown`                                                   | None     | Replaces how an element is rendered.                                                                     |
| `remarkPlugins`     | Plugin list                                                                          | None     | Parser plugins, run after GitHub Flavored Markdown.                                                      |
| `rehypePlugins`     | Plugin list                                                                          | None     | Tree plugins, run before the caret is placed.                                                            |
| `labels`            | `Partial<MarkdownLabels>`                                                            | English  | The code block title, the table name, the image link text, the task names, and the waiting block's text. |

### repairMarkdown [#repairmarkdown]

`repairMarkdown(source: string): string` returns the text with the last block's unfinished syntax closed. It returns complete text unchanged. It is exported from `lib/repair-markdown`, and again from the component's file.

### markdown-math [#markdown-math]

| Export                                            | Description                                                                        |
| ------------------------------------------------- | ---------------------------------------------------------------------------------- |
| `mathPlugins`                                     | `remarkPlugins` and `rehypePlugins` to spread onto Markdown. A single `$` is text. |
| `createMathPlugins({ singleDollar })`             | Makes the plugins, with `$x$` read as math when `singleDollar` is true.            |
| `normalizeMathDelimiters(source: string): string` | Rewrites `\( … \)` and `\[ … \]` as `$$`.                                          |

### markdown-shiki [#markdown-shiki]

| Export                                 | Description                                                             |
| -------------------------------------- | ----------------------------------------------------------------------- |
| `createShikiCodeLine({ light, dark })` | Returns a `renderCodeLine`. Both themes are optional Shiki theme names. |

### Types [#types]

| Export                  | Shape                                                            |
| ----------------------- | ---------------------------------------------------------------- |
| `MarkdownLabels`        | `code`, `table`, `image`, `taskDone`, `taskOpen`, `blockPending` |
| `MarkdownBlockRenderer` | `(body: string) => ReactNode`. Throw when the text does not fit. |
| `MarkdownCodeBlock`     | `code`, `language`, `title`                                      |
| `MarkdownCodeLine`      | `index`, from zero, and `code`, the whole block                  |
