Build a Streaming Markdown Code Fence Parser
CursorSoftware Engineer · Technical ScreenLast updated
Tests streaming string processing, finite state machine design, and edge-case handling for incrementally received Markdown code fences.
Streaming Markdown Code Fence Parser
In generative AI interfaces (such as Cursor), text streams in token by token. To render syntax-highlighted code blocks in real time, the client must detect when a code block begins, streams content, and closes—even when fence delimiters are split across consecutive chunks.
Implement a class StreamingCodeFenceParser that processes streaming text chunks and yields structured events.
Markdown Code Fence Specifications
- Opening Fence:
- Starts at the beginning of the stream or immediately after a newline (
\n). - May be preceded by
0to3spaces (which are ignored). - Consists of a run of
3or more consecutive backticks (`) or tildes (~). - The delimiter character (
or~`) cannot be mixed. - Any remaining text on the same line after the fence (excluding leading/trailing whitespace) is the info string / language tag.
- For backtick fences, the info string cannot contain any backtick characters.
- Starts at the beginning of the stream or immediately after a newline (
- Code Content:
- All characters following the opening fence's newline up until the start of the closing fence.
- Closing Fence:
- Starts on a new line, optionally preceded by
0to3spaces. - Consists of the same delimiter character as the opening fence.
- Must have at least as many delimiter characters as the opening fence.
- May only be followed by optional spaces and a newline (
\n) or the end of the stream (no info string allowed).
- Starts on a new line, optionally preceded by
- Normal Text:
- Any content outside of a code block.
Class Interface
class StreamingCodeFenceParser:
def __init__(self):
pass
def feed(self, chunk: str) -> list[dict]:
"""
Process an incoming chunk of text and return zero or more events.
"""
pass
def flush(self) -> list[dict]:
"""
Signal the end of the stream. Flush any buffered content as text
or unclosed code content, returning final events.
"""
pass
Event Format
Emit events as dictionaries:
{"type": "text", "content": str}: Emitted for non-code text as soon as it is confirmed not to be part of a fence.{"type": "code_start", "language": str}: Emitted when an opening fence line is fully resolved (languageis empty string""if unspecified).{"type": "code_chunk", "content": str}: Emitted for content inside the code block as it becomes available.{"type": "code_end"}: Emitted when a valid closing fence is confirmed.
Constraints
- Chunks can be of arbitrary length (from single characters to full documents).
- A closing fence cannot close a block opened with a different character (e.g.,
~~~cannot close```). - Content should be emitted as early as possible without prematurely committing to a state that could change with subsequent characters.
Example 1
parser = StreamingCodeFenceParser()
parser.feed("Here is code:\n```py")
# Returns: [{"type": "text", "content": "Here is code:\n"}]
# ("```py" is buffered because the opening fence line is not complete yet)
parser.feed("thon\nprint(1)\n```\nDone")
# Returns:
# [
# {"type": "code_start", "language": "python"},
# {"type": "code_chunk", "content": "print(1)\n"},
# {"type": "code_end"},
# {"type": "text", "content": "Done"}
# ]
Example 2 (Delimiter Split Across Chunks)
parser = StreamingCodeFenceParser()
parser.feed("``")
# Returns: [] (could be start of a fence or plain text)
parser.feed("`\nhello\n```")
# Returns:
# [
# {"type": "code_start", "language": ""},
# {"type": "code_chunk", "content": "hello\n"}
# ]
# (Trailing "```" is buffered because it might be extended or followed by text)
parser.flush()
# "```" at end of stream acts as valid closing fence
# Returns: [{"type": "code_end"}]
Hints
Hint 1
Model the parser as an explicit state machine with states like OUTSIDE_CODE, PENDING_OPEN_FENCE, INSIDE_CODE, and PENDING_CLOSE_FENCE.
Hint 2
Because fences are line-oriented, you can buffer characters until a newline is encountered or until you can definitively prove the buffered characters cannot be a fence.
Hint 3
When inside a code block, you cannot emit a line immediately if it looks like a potential closing fence; buffer the candidate line until you confirm whether it closes the block or is just code content.
Follow-up questions
- How would you handle nested code fences, such as displaying Markdown documentation containing 3-backtick code fences inside a 4-backtick fence?
- If the stream terminates unexpectedly without a closing fence, how should the parser handle the open code block to ensure the user interface does not break?
- How would you optimize memory usage and string allocations if chunk sizes are in the order of hundreds of kilobytes?
Solution walkthrough is for members
Get the full walkthrough with reference code and complexity analysis, plus Kitty Coach at 300 messages a day. Hints, the code runner and community answers stay free.