---
title: Unterminated Block Parsing
description: Learn how Streamdown handles incomplete Markdown syntax during AI streaming with remend.
type: conceptual
summary: Graceful handling of unterminated code fences, lists, and other incomplete Markdown during streaming.
related:
  - /docs/animation
  - /docs/carets
---

# Unterminated Block Parsing



One of Streamdown's most powerful features is its ability to intelligently parse and style incomplete Markdown blocks using the [remend](https://www.npmjs.com/package/remend) package. This feature, called **unterminated block parsing**, ensures that your streaming content looks polished even before the AI finishes its response.

## About Remend

Remend is a lightweight, standalone preprocessor that completes incomplete Markdown syntax. Streamdown integrates remend by default, but you can also use it independently in your own projects. See the [remend documentation](https://www.npmjs.com/package/remend) for standalone usage.

## The Challenge

When AI models stream Markdown content token-by-token, the content often arrives incomplete:

```
**This is bold text
```

Without proper handling, this would either:

* Not render any formatting at all
* Display the raw Markdown syntax
* Break the layout

Streamdown solves this by detecting incomplete patterns and intelligently completing them for rendering purposes.

## How It Works

Remend (Streamdown's preprocessing layer) analyzes the incoming Markdown and identifies common patterns that might be incomplete. When detected, it automatically adds the closing syntax so the content renders correctly, then seamlessly updates when the actual closing syntax arrives.

The preprocessing happens before the markdown is passed into the unified/remark pipeline, operating on the raw string level for maximum performance.

### Supported Incomplete Patterns

#### Bold Text (`**text`)

```markdown
**This is bold text that hasn't been closed yet
```

The parser detects the opening `**` and adds a closing `**` to ensure the text renders as bold.

#### Italic Text (`*text` or `_text`)

```markdown
*This is italic text
_This is also italic
```

Single asterisks or underscores are completed automatically.

#### Bold Italic (`***text`)

```markdown
***This is bold and italic
```

Triple asterisks for combined formatting are handled.

#### Inline Code (`` `code ``)

```markdown
`const foo = "bar
```

Incomplete inline code blocks are closed with a backtick.

#### Strikethrough (`~~text`)

```markdown
~~This text is being crossed out
```

Strikethrough formatting is completed automatically.

#### Single Tilde Escape (`20~25`)

```markdown
20~25°C
```

Single `~` characters between word characters (letters, numbers) are escaped to prevent GFM from misinterpreting them as strikethrough markers. For example, `20~25°C` renders as `20~25°C` instead of applying strikethrough to the text between the tildes. This does not affect intentional `~~double tilde~~` strikethrough syntax.

#### Links (`[text](url)`)

Streamdown handles several link scenarios:

**Incomplete link text:**

```markdown
[Click here
```

Completes to: `[Click here](streamdown:incomplete-link)`

**Incomplete URL:**

```markdown
[Click here](https://exampl
```

Completes to: `[Click here](streamdown:incomplete-link)`

The special `streamdown:incomplete-link` URL ensures the link renders visually but doesn't navigate anywhere.

**Text-only mode:**

If you prefer to display just the link text without any link markup during streaming (for better compatibility with other markdown renderers like `react-markdown`), use the `linkMode` option:

```tsx
<Streamdown remend={{ linkMode: 'text-only' }}>
  {markdown}
</Streamdown>
```

With `linkMode: 'text-only'`:

* `[Click here` → `Click here` (plain text)
* `[Click here](https://exampl` → `Click here` (plain text)

The link renders as a proper link once complete.

**Custom component override:**

For full control over how incomplete links render, you can override the `a` component. This approach lets you customize styling, add loading indicators, or implement custom behavior:

```tsx
<Streamdown
  components={{
    a: ({ href, children, ...props }) => {
      const isIncomplete = href === 'streamdown:incomplete-link';

      if (isIncomplete) {
        // Render as plain text with custom styling
        return <span className="text-muted-foreground">{children}</span>;
      }

      return (
        <a href={href} target="_blank" rel="noreferrer" {...props}>
          {children}
        </a>
      );
    },
  }}
>
  {markdown}
</Streamdown>
```

For `react-markdown` users consuming the `remend` package directly:

```tsx
import ReactMarkdown from 'react-markdown';
import remend from 'remend';

<ReactMarkdown
  components={{
    a: ({ href, children, ...props }) => {
      if (href === 'streamdown:incomplete-link') {
        return <>{children}</>;
      }
      return <a href={href} {...props}>{children}</a>;
    },
  }}
>
  {remend(streamingText)}
</ReactMarkdown>
```

#### Images

For incomplete images, Streamdown removes them entirely rather than showing broken image placeholders:

```markdown
![Alt text that's incomplete
```

This prevents visual clutter during streaming.

#### Mathematical Expressions

Block-level KaTeX expressions are completed:

```markdown
$$
E = mc^2
```

The parser adds the closing `$$` to ensure proper math rendering.

## Configuration

### Within Streamdown

Unterminated block parsing is enabled by default in Streamdown. You can disable the remend preprocessor if needed:

```tsx
import { Streamdown } from 'streamdown';

export default function Page() {
  return (
    <Streamdown parseIncompleteMarkdown={false}>
      {markdown}
    </Streamdown>
  );
}
```

However, disabling this feature will result in incomplete Markdown syntax being displayed literally, which is generally not desirable for user-facing applications.

### Using Remend Standalone

You can use remend independently in your own markdown rendering pipeline:

```typescript
import remend from 'remend';

const partialMarkdown = "This is **incomplete bold";
const completed = remend(partialMarkdown);
// Result: "This is **incomplete bold**"
```

For links, you can use the `linkMode` option to control how incomplete links are handled:

```typescript
import remend from 'remend';

// Default behavior: use placeholder URL
remend("[Click here](http://exampl");
// Result: "[Click here](streamdown:incomplete-link)"

// Text-only mode: display plain text
remend("[Click here](http://exampl", { linkMode: 'text-only' });
// Result: "Click here"
```

See the [remend package](https://www.npmjs.com/package/remend) for more details on standalone usage.

### Custom Handlers

You can extend remend with custom handlers to complete your own markers during streaming. This is useful for domain-specific syntax like custom tags or markers that your AI might output.

```typescript
import remend, { type RemendHandler } from 'remend';

const jokeHandler: RemendHandler = {
  name: 'joke',
  handle: (text) => {
    // Complete <<<JOKE>>> marks that aren't closed
    const match = text.match(/<<<JOKE>>>([^<]*)$/);
    if (match && !text.endsWith('<<</JOKE>>>')) {
      return `${text}<<</JOKE>>>`;
    }
    return text;
  },
  priority: 80, // Runs after most built-ins (-10 to 70)
};

const result = remend(content, { handlers: [jokeHandler] });
```

#### Handler Interface

Each handler has three properties:

* `name` - Unique identifier for the handler
* `handle` - Transform function that receives text and returns modified text
* `priority` - Optional execution order (lower runs first, default: 100)

#### Execution Order

Built-in handlers use priorities 0-75:

| Handler               | Priority |
| --------------------- | -------- |
| `singleTilde`         | 0        |
| `comparisonOperators` | 5        |
| `htmlTags`            | 10       |
| `setextHeadings`      | 15       |
| `links`               | 20       |
| `boldItalic`          | 30       |
| `bold`                | 35       |
| `italic`              | 40-42    |
| `inlineCode`          | 50       |
| `strikethrough`       | 60       |
| `katex`               | 70       |

Custom handlers default to priority 100, running after all built-ins. Set a lower priority to run before specific built-ins.

**Example: Running Before Bold Handling**

To process content before bold formatting is completed (priority 30), set a lower priority:

```typescript
const preprocessHandler: RemendHandler = {
  name: 'preprocess',
  handle: (text) => {
    // Transform content before bold is completed
    return text.replace(/\{\{highlight\}\}/g, '**');
  },
  priority: 25, // Runs before bold (35) and after links (20)
};
```

**Example: Running After All Built-ins**

For post-processing, use the default priority or higher:

```typescript
const postprocessHandler: RemendHandler = {
  name: 'postprocess',
  handle: (text) => {
    // Clean up after all built-in handlers
    return text.trim();
  },
  priority: 150, // Runs after all built-ins
};
```

#### Context Utilities

Remend exports utilities to help custom handlers detect context:

```typescript
import {
  isWithinCodeBlock,
  isWithinMathBlock,
  isWithinLinkOrImageUrl,
  isWordChar,
} from 'remend';

const handler: RemendHandler = {
  name: 'custom',
  handle: (text) => {
    // Skip processing inside code blocks
    if (isWithinCodeBlock(text, text.length - 1)) {
      return text;
    }
    // Your completion logic here
    return text;
  },
};
```

## Smart Behavior

Remend includes intelligent rules to avoid false positives:

### List Item Detection

The parser won't close formatting markers that appear at the start of list items:

```markdown
- **
- Item with bold marker only
```

This prevents prematurely closing bold formatting that might be intentional list structure.

### Code Block Awareness

Formatting within complete code blocks is left untouched:

````markdown
```python
def foo():
    # This **won't** be treated as incomplete bold
    return "bar"
```
````

### Math Block Protection

Underscores within math blocks are not treated as italic markers:

```markdown
$$
E = m \times c^2
x_i = y_j
$$
```

### Single Tilde Protection

Single `~` characters between word characters are escaped so that GFM does not treat them as strikethrough:

```markdown
20~25°C
```

This prevents text like temperature ranges or numeric expressions from being rendered with strikethrough styling, while `~~double tilde~~` strikethrough still works as expected.

### Word-Internal Characters

Asterisks and underscores within words (like variable names or between alphanumeric characters) are preserved:

```markdown
const user_name = "john_doe";
234234*123
hello*world
```

This ensures that text like product codes or mathematical expressions with asterisks are not mistakenly interpreted as italic formatting.

## Performance Considerations

Remend is highly optimized for streaming scenarios:

* **Direct string iteration** - Avoids regex splits and allocations
* **ASCII fast-path** - Optimized character checking for common cases
* **Early returns** - Stops processing when conditions aren't met
* **Zero dependencies** - Pure TypeScript implementation
* **Block-Level Processing** - Streamdown splits content into blocks for parallel processing

## Examples

### Bold Text Streaming

As content streams in:

1. `**This` → Renders nothing (too short)
2. `**This is bol` → Renders as **This is bol**
3. `**This is bold**` → Renders as **This is bold**

### Link Streaming

With default `linkMode: 'protocol'`:

1. `[Cli` → Renders nothing
2. `[Click here` → Renders as [Click here](streamdown:incomplete-link)
3. `[Click here](https://` → Renders as [Click here](streamdown:incomplete-link)
4. `[Click here](https://example.com)` → Renders as [Click here](https://example.com)

With `linkMode: 'text-only'`:

1. `[Cli` → Renders nothing
2. `[Click here` → Renders as plain text: Click here
3. `[Click here](https://` → Renders as plain text: Click here
4. `[Click here](https://example.com)` → Renders as [Click here](https://example.com)

### Code Streaming

1. `` `const `` → Renders as `const`
2. `` `const foo = `` → Renders as `const foo =`
3. `` `const foo = "bar"` `` → Renders as `const foo = "bar"`


---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)