UNPKG

@lobehub/ui

Version:

Lobe UI is an open-source UI component library for building AIGC web apps

1 lines 8.51 kB
{"version":3,"file":"rehypeStreamAnimated.mjs","names":[],"sources":["../../../src/Markdown/plugins/rehypeStreamAnimated.ts"],"sourcesContent":["import { type Element, type ElementContent, type Root } from 'hast';\nimport { type BuildVisitor } from 'unist-util-visit';\nimport { visit } from 'unist-util-visit';\n\nimport { getNow } from '@/utils/getNow';\n\nexport interface StreamAnimatedRuntime {\n births: number[];\n /**\n * Write-once per-char render cache, indexed like `births`:\n * `undefined` = char not rendered yet, `null` = born fully revealed,\n * string = inline style frozen at first render.\n * Freezing the style keeps span props referentially stable across the\n * tail block's re-renders, so React never rewrites `animation-delay`\n * on an in-flight fade (a rewrite restarts the CSS animation).\n */\n styles: (string | null | undefined)[];\n}\n\nexport interface StreamAnimatedOptions {\n births?: number[];\n fadeDuration?: number;\n /**\n * `'word'` wraps whitespace-delimited runs in one span instead of one\n * span per char. Every concurrent CSS animation keeps the compositor\n * producing frames and fires animationstart/end through React's root\n * event delegation, so animating ~5x fewer nodes is the main CPU lever —\n * char-level remains available for the finer-grained look.\n */\n granularity?: 'char' | 'word';\n nowMs?: number;\n revealed?: boolean;\n runtime?: StreamAnimatedRuntime;\n}\n\n// Intl.Segmenter splits CJK runs into words too — the whitespace regex\n// fallback would otherwise fade an entire unspaced CJK paragraph as one\n// unit.\nconst WORD_SEGMENT_RE = /\\s+|\\S+/g;\n\nconst wordSegmenter =\n typeof Intl !== 'undefined' && 'Segmenter' in Intl\n ? new Intl.Segmenter(undefined, { granularity: 'word' })\n : null;\n\nconst segmentWords = (value: string): string[] => {\n if (!wordSegmenter) return value.match(WORD_SEGMENT_RE) ?? [];\n\n const segments: string[] = [];\n for (const item of wordSegmenter.segment(value)) {\n segments.push(item.segment);\n }\n return segments;\n};\n\nconst BLOCK_TAGS = new Set(['p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'li']);\nconst SKIP_TAGS = new Set(['pre', 'code', 'table', 'svg']);\n\nfunction hasClass(node: Element, cls: string): boolean {\n const cn = node.properties?.className;\n if (Array.isArray(cn)) return cn.some((c) => String(c).includes(cls));\n if (typeof cn === 'string') return cn.includes(cls);\n return false;\n}\n\nexport const rehypeStreamAnimated = (options: StreamAnimatedOptions = {}) => {\n const {\n births,\n fadeDuration = 150,\n granularity = 'char',\n nowMs,\n revealed = false,\n runtime,\n } = options;\n // Legacy births/nowMs callers share the runtime path through a throwaway\n // cache: the plugin factory runs once per render, so their styles are\n // recomputed against the caller's nowMs each run, exactly as before.\n const resolvedRuntime = revealed\n ? undefined\n : (runtime ??\n (Array.isArray(births) && typeof nowMs === 'number' ? { births, styles: [] } : undefined));\n const nowOverride = runtime ? undefined : nowMs;\n\n return (tree: Root) => {\n let globalCharIndex = 0;\n const now = nowOverride ?? (resolvedRuntime ? getNow() : 0);\n\n const shouldSkip = (node: Element): boolean => {\n return SKIP_TAGS.has(node.tagName) || hasClass(node, 'katex');\n };\n\n const resolveStyle = (index: number): string | null => {\n const styles = resolvedRuntime!.styles;\n const cached = styles[index];\n if (cached !== undefined) return cached;\n\n const birthTs = resolvedRuntime!.births[index];\n let resolved: string | null;\n if (birthTs === undefined) {\n resolved = null;\n } else {\n const elapsed = now - birthTs;\n // Negative delay = already elapsed ms into the fade. Positive\n // delay = not started yet (char born in the future, i.e.\n // staggered within the same commit).\n resolved = elapsed >= fadeDuration ? null : `animation-delay:${-elapsed}ms`;\n }\n styles[index] = resolved;\n return resolved;\n };\n\n const buildSpan = (value: string, startIndex: number): ElementContent => {\n let className = 'stream-char';\n let style: string | undefined;\n\n if (revealed) {\n className = 'stream-char stream-char-revealed';\n } else if (resolvedRuntime) {\n const resolved = resolveStyle(startIndex);\n if (resolved === null) {\n className = 'stream-char stream-char-revealed';\n } else {\n style = resolved;\n }\n }\n\n const properties: Record<string, any> = { className };\n if (style !== undefined) {\n properties.style = style;\n }\n return {\n children: [{ type: 'text', value }],\n properties,\n tagName: 'span',\n type: 'element',\n };\n };\n\n const wrapText = (node: Element) => {\n const newChildren: ElementContent[] = [];\n for (const child of node.children) {\n if (child.type === 'text') {\n if (granularity === 'word') {\n for (const segment of segmentWords(child.value)) {\n const startIndex = globalCharIndex;\n for (const _char of segment) globalCharIndex++;\n\n if (segment.trim() === '') {\n newChildren.push({ type: 'text', value: segment });\n } else {\n newChildren.push(buildSpan(segment, startIndex));\n }\n }\n } else {\n for (const char of child.value) {\n newChildren.push(buildSpan(char, globalCharIndex));\n globalCharIndex++;\n }\n }\n } else if (child.type === 'element') {\n if (!shouldSkip(child)) {\n wrapText(child);\n }\n newChildren.push(child);\n } else {\n newChildren.push(child);\n }\n }\n node.children = newChildren;\n };\n\n visit(tree, 'element', ((node: Element) => {\n if (shouldSkip(node)) return 'skip';\n if (BLOCK_TAGS.has(node.tagName)) {\n wrapText(node);\n return 'skip';\n }\n }) as BuildVisitor<Root, 'element'>);\n };\n};\n"],"mappings":"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"}