Streaming API
autoCloseMarkdown(source)
Automatically closes unclosed markdown inline syntax and Comark components. Built for streaming scenarios where content arrives incrementally and may be incomplete at any point.
Parameters:
source- The markdown content (potentially partial/incomplete)
Returns: string — the source with all unclosed syntax closed
Example:
import { autoCloseMarkdown } from 'comark'
autoCloseMarkdown('**bold text')'**bold text**'autoCloseMarkdown is also available as a parse option — set autoClose: true (default) in parse() or createParse() to apply it automatically.Parser Integration
autoClose is enabled by default in parse() and createParse(). You can disable it if you want to handle incomplete syntax yourself:
import { parse } from 'comark'
const result = await parse(content, {
autoClose: true // default
})import { parse } from 'comark'
const result = await parse(content, {
autoClose: false
})import { autoCloseMarkdown, parse } from 'comark'
const closed = autoCloseMarkdown(content)
const result = await parse(closed, { autoClose: false })Supported Syntax
Inline Markdown
| Syntax | Example | Auto-closed |
|---|---|---|
| Bold | **text | **text** |
| Italic | *text | *text* |
| Code | `code | `code` |
| Strikethrough | ~~text | ~~text~~ |
| Link | [text](url | [text](url) |
| Image |  |
Comark Components
Block components are closed based on their marker count:
// 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:
// 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:
import { autoCloseMarkdown, parse } 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 parse(closed)
renderContent(result.nodes)
})
socket.on('end', async () => {
const result = await parse(accumulated)
renderFinalContent(result.nodes)
})AI SDK
Build a streaming AI chat with live Comark rendering in just a few lines.
The <Comark> 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.
Install dependencies
npm install ai @ai-sdk/vueCreate a server route
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 <Comark> component to parse and render the content as it arrives. Use isPartStreaming(part) for per-part streaming detection:
<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>
<Comark :markdown="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>The caret prop on <Comark> appends a blinking cursor to the last text node while streaming. See the Vue rendering docs for custom caret styling.
Real-time Editor
Show a live preview while the user types:
import { autoCloseMarkdown, parse } from 'comark'
import { renderHTML } from '@comark/html'
editor.addEventListener('input', async (e) => {
const closed = autoCloseMarkdown(e.target.value)
const result = await parse(closed)
preview.innerHTML = await renderHTML(result)
})Incremental File Upload
Parse content progressively as a file uploads:
import { autoCloseMarkdown, parse } 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 parse(closed)
updateProgress({
percent: (offset / file.size) * 100,
preview: result.nodes
})
offset += chunkSize
}
return parse(accumulated)
}Performance
Call autoCloseMarkdown once per chunk on the accumulated content — not on every character:
// ✅ Good: call once per chunk
for await (const chunk of stream) {
accumulated += chunk
const closed = autoCloseMarkdown(accumulated)
render(await parse(closed))
}
// ❌ Avoid: calling for every character
for (const char of text) {
accumulated += char
const closed = autoCloseMarkdown(accumulated) // too frequent
render(await parse(closed))
}