{"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: unknown = node.properties?.className;\n  if (Array.isArray(cn)) return cn.some((c) => String(c).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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"}