UNPKG

@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
/** * 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; }