All questions

Streaming Markdown State Machine

Premium

Streaming Markdown State Machine

A streaming markdown renderer turns partially-received markdown into valid HTML at every step of a stream. When a language model streams an answer, you hold a growing prefix like **Bold te — a normal markdown parser renders that as a literal **Bold te because the closing marker has not arrived, so the reader watches raw asterisks flicker until the token finishes. This renderer instead treats an open marker as if it will close, so **Bold te reads as bold text the instant it arrives.

Implement streamingMarkdownStateMachine(md), returning an HTML string. Support a small subset and, crucially, close anything left open at the end of the input:

  • Bold**text** becomes <strong>text</strong>; an unclosed **text still renders as <strong>text</strong>.
  • Inline code — a backtick pair around text becomes <code>text</code>, and unclosed renders closed. Inside inline code, ** is literal.
  • Fenced code block — a fence of three backticks, an optional language, a newline, then code up to the closing fence, becomes <pre><code>code</code></pre>. An unclosed fence renders the code so far. Inside a fence, everything is literal.
  • Escaping — every <, >, and & in text is escaped, so the HTML is safe to inject.

Signature

function streamingMarkdownStateMachine(md: string): string;

Examples

streamingMarkdownStateMachine('**Bold** and `code`');
// '<strong>Bold</strong> and <code>code</code>'

streamingMarkdownStateMachine('**half'); // '<strong>half</strong>'  (closed at end)
streamingMarkdownStateMachine('```js\nx = 1\n```');
// '<pre><code>x = 1</code></pre>'  (the js info string is dropped)

Notes

  • Close on end-of-input — the defining behavior: a bold, code span, or fence still open when the string ends is closed in the output.
  • State decides meaning — inside inline code or a fence, a marker like ** is literal text, not formatting.
  • Always escape — escape <, >, and & everywhere, including inside code blocks.
  • Drop the info string — the text after the opening fence up to the newline (the language) is not rendered.
  • Out of scope — italics, links, headings, lists, nested emphasis, and double-backtick inline code.

FAQ

Why can't I just run a normal markdown parser on each chunk?
A normal parser sees `**bold` as a literal double asterisk because the closing marker has not arrived yet, so the reader watches raw asterisks flicker until the token completes. A streaming renderer instead treats an open marker as if it will close, so the text reads correctly as it arrives.
What does the state machine actually track?
Whether the scanner is currently inside a bold span, an inline code span, or a fenced code block. That state decides which characters are markers and which are literal text — inside code, a double asterisk is just text.
Why must the output always be escaped?
The result is injected into a page as HTML, so any `<`, `>`, or `&` in the source — especially inside a code block full of markup — must be escaped or it becomes an injection risk and breaks the render.

Unlock the solution & editor

  • Runnable editor + tests

    Solve in the browser with instant Jest feedback.

  • Detailed solutions

    Walkthroughs, edge cases, and complexity notes.

  • Multi-framework variants

    React, Vue, Vanilla, Angular — same question, different stacks.

Upgrade to Premium