Skip to main content
Streaming mode allows you to parse markdown incrementally as new content arrives, making it ideal for processing LLM outputs in real-time. The parser buffers incomplete blocks and only emits finalized, stable nodes.

How streaming mode works

When stream: true is enabled, the parser:
  1. Maintains internal state across multiple parse() calls
  2. Buffers incomplete blocks until they’re fully closed
  3. Returns only stable, finalized blocks
  4. Continues parsing where it left off on the next call
In streaming mode, the parser keeps track of open blocks across calls. You must use the same parser instance for all chunks in a stream.

Basic streaming example

Here’s how to parse markdown as it streams in:
1

Create a parser instance

import { MarkdownParser } from "markdown-parser";

const parser = new MarkdownParser();
2

Parse the first chunk

// First chunk arrives
const nodes1 = parser.parse("# Hello World\nThis", { stream: true });

console.log(nodes1);
// [{ type: "heading", level: 1, children: [{ type: "text", text: "Hello World" }] }]
// The paragraph is NOT emitted - it's still open
3

Continue with more chunks

// More content arrives
const nodes2 = parser.parse(" is a paragraph\n\nThis is another paragraph.", { stream: true });

console.log(nodes2);
// [{ type: "paragraph", children: [{ type: "text", text: "This is a paragraph" }] }]
// First paragraph is now closed, but second one remains open
4

Finalize remaining blocks

// Close all remaining open blocks
const nodes3 = parser.parse("", { stream: false });

console.log(nodes3);
// [{ type: "paragraph", children: [{ type: "text", text: "This is another paragraph." }] }]

When blocks are emitted

Understanding when the parser emits blocks is crucial for streaming:
Block TypeEmitted When
HeadingImmediately (single line)
ParagraphAfter blank line or new block starts
Code block (fenced)After closing fence encountered
Code block (indented)After non-indented line
ListAfter blank line or content ends
BlockquoteAfter content ends
TableAfter non-table line
Thematic breakImmediately (single line)

Streaming LLM output

Here’s a practical example of parsing markdown from an LLM as it streams:
import { MarkdownParser } from "markdown-parser";

async function streamLLMResponse(stream: ReadableStream) {
  const parser = new MarkdownParser();
  const reader = stream.getReader();
  const decoder = new TextDecoder();

  try {
    while (true) {
      const { done, value } = await reader.read();
      
      if (done) {
        // Finalize any remaining open blocks
        const finalNodes = parser.parse("", { stream: false });
        if (finalNodes.length > 0) {
          displayNodes(finalNodes);
        }
        break;
      }

      // Parse the chunk with streaming enabled
      const chunk = decoder.decode(value, { stream: true });
      const nodes = parser.parse(chunk, { stream: true });
      
      // Only finalized blocks are returned
      if (nodes.length > 0) {
        displayNodes(nodes);
      }
    }
  } finally {
    reader.releaseLock();
  }
}

function displayNodes(nodes: BlockNode[]) {
  // Render or process the finalized nodes
  console.log("New finalized blocks:", nodes);
}

Handling code blocks

Code blocks require special attention in streaming mode:
const parser = new MarkdownParser();

// Start of code block
let nodes = parser.parse("```javascript\n", { stream: true });
console.log(nodes); // []

// Content inside code block
nodes = parser.parse("console.log('hello');\n", { stream: true });
console.log(nodes); // []

// Close code block
nodes = parser.parse("```\n", { stream: true });
console.log(nodes);
// [{ type: "code-block", content: "console.log('hello');\n", info: "javascript" }]

Managing list items

Lists in streaming mode emit items as they close:
const parser = new MarkdownParser();

// First list item starts
let nodes = parser.parse("1. First item\n", { stream: true });
console.log(nodes); // [] - list is still open

// Second item starts (closes first)
nodes = parser.parse("2. Second item\n", { stream: true });
console.log(nodes); // [] - list continues

// Blank line closes the list
nodes = parser.parse("\n", { stream: true });
console.log(nodes);
// [{ type: "list", kind: "ordered", items: [...], tight: true }]
Nested lists can remain open for multiple chunks. Always finalize with stream: false at the end.
The CommonMark spec allows link reference definitions to appear after their usage:
const parser = new MarkdownParser();

// Link reference before definition
let nodes = parser.parse("[link][ref]\n\n", { stream: true });
console.log(nodes);
// Link might not resolve yet if definition comes later

// Definition arrives in later chunk
nodes = parser.parse("[ref]: https://example.com\n", { stream: true });
// Parser resolves references in finalized blocks
When streaming, links may initially appear unresolved if their reference definition arrives in a later chunk.

Best practices

Call parse("", { stream: false }) at the end to close any remaining open blocks:
const parser = new MarkdownParser();

// ... parse streaming chunks ...

// Always finalize
const finalNodes = parser.parse("", { stream: false });
Don’t reuse parser instances across independent streams:
// ✅ Good: New parser for each stream
const parser1 = new MarkdownParser();
processStream1(parser1);

const parser2 = new MarkdownParser();
processStream2(parser2);

// ❌ Bad: Reusing parser
const parser = new MarkdownParser();
processStream1(parser);
processStream2(parser); // State is mixed!
For very small chunks, consider buffering to reduce parser overhead:
let buffer = "";
const MIN_CHUNK_SIZE = 50;

for await (const chunk of stream) {
  buffer += chunk;
  
  if (buffer.length >= MIN_CHUNK_SIZE || isEndOfStream) {
    const nodes = parser.parse(buffer, { stream: true });
    displayNodes(nodes);
    buffer = "";
  }
}

Streaming vs. non-streaming comparison

const parser = new MarkdownParser();

// All blocks finalized immediately
const nodes = parser.parse("# Title\nIncomplete paragraph");
// Returns both heading AND paragraph (finalized)

Next steps

React integration

Use streaming with React components

API reference

Explore the full MarkdownParser API

Build docs developers (and LLMs) love