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.

107 lines (106 loc) 3.98 kB
/** * Parse and Cache Utility * * Handles markdown parsing with intelligent caching. * Separates parsing logic from component code for better testability. * * @module parse-and-cache */ import { tokenCache } from './token-cache.js'; import { shrinkHtmlTokens } from './token-cleanup.js'; import { Lexer, Marked } from 'marked'; /** * Lexes markdown source and cleans the resulting tokens. Shared by sync and async paths. * * @param source - Raw markdown string to lex * @param options - Parser options forwarded to the Marked lexer * @param isInline - When true, uses inline tokenization (no block elements) * @returns Cleaned token array with HTML tokens properly nested * * @example * ```typescript * import { lexAndClean } from './parse-and-cache.js' * * const tokens = lexAndClean('# Hello **world**', { gfm: true }, false) * ``` * * @internal */ export const lexAndClean = (source, options, isInline) => { const lexer = new Lexer(options); const parsedTokens = isInline ? lexer.inlineTokens(source) : lexer.lex(source); return shrinkHtmlTokens(parsedTokens); }; /** * Parses markdown source with caching (synchronous path). * Checks cache first, parses on miss, stores result, and returns tokens. * * @param source - Raw markdown string to parse * @param options - Svelte markdown parser options * @param isInline - Whether to parse as inline markdown (no block elements) * @returns Cleaned and cached token array * * @example * ```typescript * import { parseAndCacheTokens } from './parse-and-cache.js' * * // Parse markdown with caching * const tokens = parseAndCacheTokens('# Hello World', { gfm: true }, false) * * // Second call with same input returns cached result (<1ms) * const cachedTokens = parseAndCacheTokens('# Hello World', { gfm: true }, false) * ``` */ export const parseAndCacheTokens = (source, options, isInline) => { // Check cache first - avoids expensive parsing const cached = tokenCache.getTokens(source, options); if (cached) { return cached; } // Cache miss - parse and store const cleanedTokens = lexAndClean(source, options, isInline); if (typeof options.walkTokens === 'function') { cleanedTokens.forEach(options.walkTokens); } // Cache the cleaned tokens for next time tokenCache.setTokens(source, options, cleanedTokens); return cleanedTokens; }; /** * Parses markdown source with caching (async path). * Uses Marked's recursive walkTokens with Promise.all to properly * handle async walkTokens callbacks (e.g. marked-code-format). * * @param source - Raw markdown string to parse * @param options - Svelte markdown parser options * @param isInline - Whether to parse as inline markdown (no block elements) * @returns Promise resolving to cleaned and cached token array * * @example * ```typescript * import { parseAndCacheTokensAsync } from './parse-and-cache.js' * * const tokens = await parseAndCacheTokensAsync('# Hello', opts, false) * ``` */ export const parseAndCacheTokensAsync = async (source, options, isInline) => { // Check cache first - avoids expensive parsing const cached = tokenCache.getTokens(source, options); if (cached) { return cached; } // Cache miss - parse and store const cleanedTokens = lexAndClean(source, options, isInline); if (typeof options.walkTokens === 'function') { // Use Marked's recursive walkTokens which handles tables, lists, // nested tokens, and extension childTokens. Await all returned // promises so async walkTokens callbacks complete before caching. const marked = new Marked(); marked.defaults = { ...marked.defaults, ...options }; const results = marked.walkTokens(cleanedTokens, options.walkTokens); await Promise.all(results); } // Cache the cleaned tokens for next time tokenCache.setTokens(source, options, cleanedTokens); return cleanedTokens; };