@humanspeak/svelte-markdown
Version:
Markdown and HTML renderer for Svelte 5 — built for rendering streaming AI agent output from Claude Code, ChatGPT, and agentic workflows. XSS-safe defaults, streaming-aware sanitization, token caching, TypeScript types, and Svelte 5 runes.
88 lines (87 loc) • 3.5 kB
TypeScript
/**
* Incremental Markdown Parser for Streaming
*
* Optimizes streaming scenarios (LLM token-by-token updates) by performing
* a full re-parse but diffing the result against the previous token array.
* Only changed/appended tokens are returned as updates, allowing Svelte to
* skip re-rendering unchanged components.
*
* @module incremental-parser
*/
import type { SvelteMarkdownOptions } from '../types.js';
import type { Token } from './markdown-parser.js';
/**
* Result of an incremental parse update.
*/
export interface IncrementalUpdateResult {
/** The full new token array */
tokens: Token[];
/** Index of the first token that differs from the previous parse */
divergeAt: number;
}
/**
* Streaming-optimized parser that performs full re-parses but diffs results
* against the previous token array to minimize DOM updates.
*
* For append-only streaming (typical LLM use case), most tokens are identical
* between updates. By comparing `raw` strings, we identify which tokens changed
* so Svelte can skip re-rendering unchanged components.
*
* @example
* ```typescript
* const parser = new IncrementalParser({ gfm: true })
*
* // First update — all tokens are "new"
* const r1 = parser.update('# Hello')
* // r1.divergeAt === 0
*
* // Second update — heading unchanged, paragraph appended
* const r2 = parser.update('# Hello\n\nWorld')
* // r2.divergeAt === 1 (heading at index 0 unchanged)
* ```
*/
export declare class IncrementalParser {
/** Previous parse result for diffing */
private prevTokens;
/** Previous full source string for append-only tail reparsing */
private prevSource;
/** Parser options passed to the Marked lexer */
private options;
/** Whether caller-supplied parser hooks make tail-window reparsing unsafe */
private tailWindowDisabled;
/** True iff any token in `prevTokens` is an HTML opening (closed or
* unclosed) whose `.raw` is shorter than its actual source span. The
* tail-window's sum-of-raws offset arithmetic only works when token
* raws sum to the source length; this flag detects when that
* invariant is broken so we fall through to a full re-parse. Cached
* to keep `getTailWindowBoundary` O(1) on the hot path. */
private prevHasHtmlSpanMismatch;
/**
* Creates a new incremental parser instance.
*
* @param options - Svelte markdown parser options forwarded to Marked's Lexer
*/
constructor(options: SvelteMarkdownOptions);
private getTailWindowBoundary;
/**
* True for an HTML opening tag whose `.raw` is shorter than its
* actual source span. After token-cleanup, a non-self-closing html
* opening token's `.raw` is only the opening tag itself (e.g.
* `<div>`); its children and closing tag (if any) live on
* `.tokens`. The tail-window's `reparseOffset = sum(raws)` math is
* unsound any time such a token sits in `prevTokens`, regardless
* of whether the close has arrived yet.
*/
private hasHtmlSpanMismatch;
private isStableAtSourceEnd;
private hasAppendSensitiveReferenceSyntax;
private canUseTailWindow;
private parseSource;
/**
* Parses the full source and diffs against the previous result.
*
* @param source - The full accumulated markdown source string
* @returns The new tokens and the index where they diverge from the previous parse
*/
update: (source: string) => IncrementalUpdateResult;
}