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.

119 lines (118 loc) 5.02 kB
const isReusableStreamingNode = (value) => typeof value === 'object' && value !== null && !Array.isArray(value); const isReusableStreamingNodeArray = (value) => Array.isArray(value) && value.every((item) => Array.isArray(item) ? isReusableStreamingNodeArray(item) : isReusableStreamingNode(item)); const haveSameStableArrayIdentity = (previousArray, nextArray) => { if (previousArray.length !== nextArray.length) return false; return previousArray.every((previousItem, index) => { const nextItem = nextArray[index]; if (Array.isArray(previousItem) || Array.isArray(nextItem)) { return (Array.isArray(previousItem) && Array.isArray(nextItem) && haveSameStableArrayIdentity(previousItem, nextItem)); } return isSameStableNode(previousItem, nextItem); }); }; const haveSameStableChildIdentity = (previousNode, nextNode) => { const keys = new Set([...Object.keys(previousNode), ...Object.keys(nextNode)]); const previousRecord = previousNode; const nextRecord = nextNode; for (const key of keys) { const previousValue = previousRecord[key]; const nextValue = nextRecord[key]; const previousIsChildArray = isReusableStreamingNodeArray(previousValue); const nextIsChildArray = isReusableStreamingNodeArray(nextValue); if (!previousIsChildArray && !nextIsChildArray) continue; if (!previousIsChildArray || !nextIsChildArray) return false; if (!haveSameStableArrayIdentity(previousValue, nextValue)) return false; } return true; }; /** Returns whether two nodes and every nested token array have one stable identity. */ export const isSameStableNode = (previousNode, nextNode) => { if (previousNode.type !== nextNode.type) return false; if (typeof previousNode.raw === 'string' || typeof nextNode.raw === 'string') { if (previousNode.raw !== nextNode.raw) return false; } else if (typeof previousNode.text === 'string' || typeof nextNode.text === 'string') { if (previousNode.text !== nextNode.text) return false; } else { return false; } return haveSameStableChildIdentity(previousNode, nextNode); }; const reuseStableNodeArray = (previousArray, nextArray) => { const limit = Math.min(previousArray.length, nextArray.length); let reusedArray; for (let index = 0; index < limit; index++) { const previousItem = previousArray[index]; const nextItem = nextArray[index]; let reusedItem = nextItem; if (Array.isArray(previousItem) && Array.isArray(nextItem)) { reusedItem = reuseStableNodeArray(previousItem, nextItem); } else if (isReusableStreamingNode(previousItem) && isReusableStreamingNode(nextItem)) { reusedItem = reuseStableNode(previousItem, nextItem); } if (reusedItem !== nextItem) { reusedArray ??= nextArray.slice(); reusedArray[index] = reusedItem; } } return reusedArray ?? nextArray; }; const reuseStableNode = (previousNode, nextNode) => { if (isSameStableNode(previousNode, nextNode)) return previousNode; let mergedNode; const previousRecord = previousNode; const nextRecord = nextNode; for (const key of Object.keys(nextNode)) { const previousValue = previousRecord[key]; const nextValue = nextRecord[key]; if (!isReusableStreamingNodeArray(previousValue) || !isReusableStreamingNodeArray(nextValue)) { continue; } const reusedValue = reuseStableNodeArray(previousValue, nextValue); if (reusedValue !== nextValue) { mergedNode ??= { ...nextNode }; const mergedRecord = mergedNode; mergedRecord[key] = reusedValue; } } return mergedNode ?? nextNode; }; /** * Reuses stable token objects from a previous streaming parse to preserve * component identity across incremental updates. */ export const reuseStableTokenArray = (previousTokens, nextTokens, divergeAt) => { const reuseCount = Math.min(divergeAt, previousTokens.length, nextTokens.length); let reusedTokens; if (reuseCount > 0) { reusedTokens = new Array(nextTokens.length); for (let index = 0; index < reuseCount; index++) { reusedTokens[index] = previousTokens[index]; } for (let index = reuseCount; index < nextTokens.length; index++) { reusedTokens[index] = nextTokens[index]; } } if (reuseCount < previousTokens.length && reuseCount < nextTokens.length) { const reusedToken = reuseStableNode(previousTokens[reuseCount], nextTokens[reuseCount]); if (reusedToken !== nextTokens[reuseCount]) { reusedTokens ??= nextTokens.slice(); reusedTokens[reuseCount] = reusedToken; } } return reusedTokens ?? nextTokens; };