---
title: "Streaming API"
description: "Render incomplete Markdown correctly during streaming. autoCloseMarkdown() closes unterminated syntax so AI output displays at every frame."
canonical_url: "https://comark.dev/reference/auto-close"
---
# Streaming API

> Render incomplete Markdown correctly during streaming. autoCloseMarkdown() closes unterminated syntax so AI output displays at every frame.

## `autoCloseMarkdown(source, options?)`{lang="ts"}

Automatically closes unclosed markdown inline syntax and Comark components. Built for streaming scenarios where content arrives incrementally and may be incomplete at any point. CommonMark/GFM healing follows the behavioral SPEC in `packages/comark/SPEC/auto-close.md`.

**Parameters:**

- `source` - The markdown content (potentially partial/incomplete)
- `options` (optional) - Feature flags and Comark settings:
  - **Comark:** `frontmatter: true` completes an unclosed leading frontmatter block; `syntax: false` skips component-fence closing; `attributes` controls `{...}` attribute scopes (defaults to `syntax`)
  - **Links / images:** `linkMode: 'protocol' | 'text-only'` (default `'protocol'`), `incompleteLinkPlaceholder` (default `comark:incomplete-link`), `incompleteImagePlaceholder` (default `comark:incomplete-image`)
  - **Math:** inline `$…$` and block `$$…$$` off by default on bare `autoCloseMarkdown`; pass `math: true` (or use `parseMarkdown`, which enables it)
  - **Streaming:** `dropTrailingOpeners: true` drops a trailing opener after whitespace (`hello *` → `hello`) so half-typed markers do not flash. Enabled automatically when parsing with `streaming: true`.

**Returns:** `string`, the source with all unclosed syntax closed

**Example:**

<code-group>
```typescript [auto-close.ts]
import { autoCloseMarkdown } from 'comark'

autoCloseMarkdown('**bold text')
// '**bold text**'

autoCloseMarkdown('[partial link')
// '[partial link](comark:incomplete-link)'

autoCloseMarkdown('$x = 5')
// '$x = 5'

autoCloseMarkdown('$x = 5', { math: true })
// '$x = 5$'

autoCloseMarkdown('hello *', { dropTrailingOpeners: true })
// 'hello'
```


```typescript [Output]
'**bold text**'
```
</code-group>

<tip>
`autoCloseMarkdown` is also available as a parse option: set `autoClose: true` (default) in `parseMarkdown()` or `createMarkdownParser()` to apply it automatically.
</tip>

### Parser integration

`autoClose` is enabled by default in `parseMarkdown()` and `createMarkdownParser()`. You can disable it or provide a custom completion function:

<code-group>
```typescript [Enabled (default)]
import { parseMarkdown } from 'comark'

const result = await parseMarkdown(content, {
  autoClose: true // default
})
```


```typescript [Disabled]
import { parseMarkdown } from 'comark'

const result = await parseMarkdown(content, {
  autoClose: false
})
```


```typescript [Manual]
import { autoCloseMarkdown, parseMarkdown } from 'comark'

const closed = autoCloseMarkdown(content)
const result = await parseMarkdown(closed, { autoClose: false })
```


```typescript [Custom]
import { createMarkdownParser } from 'comark'

const parse = createMarkdownParser({
  autoClose: markdown => completeMarkdown(markdown)
})
```
</code-group>

During incremental parsing with `{ streaming: true }`, the custom function receives only the unstable tail that Comark still needs to parse. On a regular parse, it receives the full input. The function must return the markdown string to tokenize.

### Supported syntax

#### Inline Markdown

| Syntax           | Example           | Auto-closed                       |
| ---------------- | ----------------- | --------------------------------- |
| Bold             | `**text`          | `**text**`                        |
| Italic           | `*text` / `_text` | `*text*` / `_text_`               |
| Bold-italic      | `***text`         | `***text***`                      |
| Code             | ```code``         | ```code```                        |
| Strikethrough    | `~~text`          | `~~text~~`                        |
| Link (protocol)  | `[text](url`      | `[text](comark:incomplete-link)`  |
| Link (text-only) | `[text`           | `text`                            |
| Image            | `![alt](url`      | `![alt](comark:incomplete-image)` |
| Block math       | `$$x`             | `$$x$$`                           |
| Inline math      | `$x`              | `$x$` (`math: true`)              |

#### Comark components

Block components are closed based on their marker count:

```typescript [auto-close.ts]
// Double marker (block component)
autoCloseMarkdown('::alert\nContent')
// '::alert\nContent\n::'

// Triple marker (nested component)
autoCloseMarkdown(':::card\nContent')
// ':::card\nContent\n:::'

// Nested components
autoCloseMarkdown('::::outer\n:::inner\n::component')
// '::::outer\n:::inner\n::component\n::\n:::\n::::'
```

#### Props and attributes

Components with props are handled correctly:

```typescript [auto-close.ts]
// Inline props
autoCloseMarkdown('::alert{type="info" title="Note"}')
// '::alert{type="info" title="Note"}\n::'

// YAML props
autoCloseMarkdown(`::component\n---\nkey: value\n---\nContent`)
// '::component\n---\nkey: value\n---\nContent\n::'
```

---

## Use cases

### AI streaming

When streaming AI-generated markdown, content arrives in chunks and may be incomplete at any point:

```typescript [chat.ts]
import { autoCloseMarkdown, parseMarkdown } from 'comark'

let accumulated = ''

socket.on('chunk', async (chunk) => {
  accumulated += chunk

  // Auto-close before parsing to ensure valid AST at every chunk
  const closed = autoCloseMarkdown(accumulated)
  const result = await parseMarkdown(closed)

  renderContent(result.nodes)
})

socket.on('end', async () => {
  const result = await parseMarkdown(accumulated)
  renderFinalContent(result.nodes)
})
```

### AI SDK

Build a streaming AI chat with live Comark rendering in a few lines.
The [`<Markdown>`](https://comark.dev/rendering/vue) component applies `autoCloseMarkdown` on every render by default, so the AST stays valid at every chunk and you get smooth incremental rendering without any extra wiring.

<steps level="4">
#### Install dependencies


```bash
npm install ai @ai-sdk/vue
```


#### Create a server route


```typescript [server/api/chat.post.ts]
import { convertToModelMessages, streamText } from 'ai'

export default defineEventHandler(async (event) => {
  const { messages } = await readBody(event)

  const result = streamText({
    model: 'anthropic/claude-sonnet-4.6',
    system: 'You are a helpful assistant. Always respond using Comark syntax.',
    messages: await convertToModelMessages(messages),
  })

  return result.toUIMessageStreamResponse()
})
```


#### Render on the client


Use the `Chat` class from `@ai-sdk/vue` and the `<Markdown>` component to parse and render the content as it arrives. Use `isPartStreaming(part)` for per-part streaming detection:


```vue [app/pages/chat.vue]
<script setup lang="ts">
import { Chat } from '@ai-sdk/vue'
import { isTextUIPart } from 'ai'
import { isPartStreaming } from '@nuxt/ui/utils/ai'

const chat = new Chat({})
const input = ref('')

function onSubmit() {
  chat.sendMessage({ text: input.value })
  input.value = ''
}
</script>

<template>
  <UChatMessages
    should-auto-scroll
    :messages="chat.messages"
    :status="chat.status"
  >
    <template #indicator>
      <UChatShimmer text="Thinking..." />
    </template>

    <template #content="{ message }">
      <template
        v-for="(part, index) in message.parts"
        :key="`${message.id}-${part.type}-${index}`"
      >
        <template v-if="isTextUIPart(part)">
          <p v-if="message.role === 'user'" class="whitespace-pre-wrap">
            {{ part.text }}
          </p>
          <Suspense v-else>
            <Markdown :value="part.text" :streaming="isPartStreaming(part)" caret />
          </Suspense>
        </template>
      </template>
    </template>
  </UChatMessages>

  <UChatPrompt v-model="input" placeholder="Ask something…" @submit="onSubmit">
    <UChatPromptSubmit
      :status="chat.status"
      @stop="chat.stop()"
      @reload="chat.regenerate()"
    />
  </UChatPrompt>
</template>
```
</steps>

<callout icon="i-lucide-square-play" color="info" to="https://github.com/comarkdown/comark/tree/main/examples/4.ai/nuxt-ai-sdk">
See the full working example with server route and chat UI wired together.
</callout>

The `caret` prop on `<Markdown>` appends a blinking cursor to the last text node while streaming. See the [Vue rendering docs](https://comark.dev/rendering/vue#streaming) for custom caret styling.

### Real-time editor

Show a live preview while the user types:

```typescript [editor.ts]
import { autoCloseMarkdown, parseMarkdown } from 'comark'
import { renderHtmlFromDocument } from '@comark/html'

editor.addEventListener('input', async (e) => {
  const closed = autoCloseMarkdown(e.target.value)
  const result = await parseMarkdown(closed)
  preview.innerHTML = await renderHtmlFromDocument(result)
})
```

### Incremental file upload

Parse content progressively as a file uploads:

```typescript [upload.ts]
import { autoCloseMarkdown, parseMarkdown } from 'comark'

async function uploadAndParse(file: File) {
  const chunkSize = 64 * 1024 // 64KB
  let offset = 0
  let accumulated = ''

  while (offset < file.size) {
    const chunk = file.slice(offset, offset + chunkSize)
    accumulated += await chunk.text()

    const closed = autoCloseMarkdown(accumulated)
    const result = await parseMarkdown(closed)

    updateProgress({
      percent: (offset / file.size) * 100,
      preview: result.nodes
    })

    offset += chunkSize
  }

  return parseMarkdown(accumulated)
}
```

### Performance

Call `autoCloseMarkdown` once per chunk on the accumulated content, not on every character:

```typescript [stream.ts]
// ✅ Good: call once per chunk
for await (const chunk of stream) {
  accumulated += chunk
  const closed = autoCloseMarkdown(accumulated)
  render(await parseMarkdown(closed))
}

// ❌ Avoid: calling for every character
for (const char of text) {
  accumulated += char
  const closed = autoCloseMarkdown(accumulated) // too frequent
  render(await parseMarkdown(closed))
}
```

---

---

- [Parse API](https://comark.dev/reference/parse)
- [Render API](https://comark.dev/reference/render)


## Sitemap

See the full [sitemap](https://comark.dev/sitemap.md) for all pages.
