@lobehub/ui
Version:
Lobe UI is an open-source UI component library for building AIGC web apps
1 lines • 9.71 kB
Source Map (JSON)
{"version":3,"file":"useHighlight.mjs","names":["codeToHtmlPromise: Promise<ICodeToHtml | null> | null","lobeTheme"],"sources":["../../src/hooks/useHighlight.ts"],"sourcesContent":["'use client';\n\nimport {\n transformerNotationDiff,\n transformerNotationErrorLevel,\n transformerNotationFocus,\n transformerNotationHighlight,\n transformerNotationWordHighlight,\n} from '@shikijs/transformers';\nimport { CSSProperties, useEffect, useMemo, useState } from 'react';\nimport type { BuiltinTheme, CodeToHastOptions, ThemedToken } from 'shiki';\nimport { Md5 } from 'ts-md5';\n\nimport { getCodeLanguageByInput } from '@/Highlighter/const';\nimport lobeTheme from '@/Highlighter/theme/lobe-theme';\n\n// Application-level cache to avoid repeated calculations\nexport const MD5_LENGTH_THRESHOLD = 10_000; // Use async MD5 for text exceeding this length\n\nexport type StreamingHighlightResult = {\n lines: ThemedToken[][];\n preStyle?: CSSProperties;\n};\n\n// Application-level cache for highlighted HTML\n// Key: cacheKey string, Value: Promise<string>\nconst highlightCache = new Map<string, Promise<string>>();\n\n// Maximum cache size to prevent memory leaks\nconst MAX_CACHE_SIZE = 1000;\n\n// Clean up old cache entries when limit is reached\nconst cleanupCache = () => {\n if (highlightCache.size > MAX_CACHE_SIZE) {\n // Remove oldest 20% of entries\n const entriesToRemove = Math.floor(MAX_CACHE_SIZE * 0.2);\n const keysToRemove = Array.from(highlightCache.keys()).slice(0, entriesToRemove);\n for (const key of keysToRemove) {\n highlightCache.delete(key);\n }\n }\n};\n\nexport type ICodeToHtml = (code: string, options: CodeToHastOptions) => Promise<string>;\nexport type ShikiModule = typeof import('shiki');\n\n// Use codeToHtml shorthand for better performance\n// It automatically manages highlighter instances and loads themes/languages on-demand\nlet codeToHtmlPromise: Promise<ICodeToHtml | null> | null = null;\n\nconst loadCodeToHtml = (): Promise<ICodeToHtml | null> => {\n if (typeof window === 'undefined') return Promise.resolve(null);\n\n if (!codeToHtmlPromise) {\n codeToHtmlPromise = import('shiki').then((mod) => mod.codeToHtml ?? null);\n }\n\n return codeToHtmlPromise;\n};\n\n// Export shikiModulePromise for useStreamHighlight compatibility\nconst loadShikiModule = (): Promise<ShikiModule | null> => {\n if (typeof window === 'undefined') return Promise.resolve(null);\n return import('shiki');\n};\nexport const shikiModulePromise = loadShikiModule();\n\n// Helper function: Safe HTML escaping\nexport const escapeHtml = (str: string): string => {\n return str\n .replaceAll('&', '&')\n .replaceAll('<', '<')\n .replaceAll('>', '>')\n .replaceAll('\"', '"')\n .replaceAll(\"'\", ''');\n};\n\n// Main highlight component - optimized version without SWR\nconst customThemes = {\n 'lobe-theme': lobeTheme,\n};\n\nexport const useHighlight = (\n text: string,\n {\n language,\n enableTransformer,\n theme: builtinTheme,\n streaming,\n }: { enableTransformer?: boolean; language: string; streaming?: boolean; theme?: BuiltinTheme },\n): string => {\n // Safely handle language and text with boundary checks\n const safeText = text ?? '';\n const lang = (language ?? 'plaintext').toLowerCase();\n\n // Match supported languages\n const matchedLanguage = useMemo(() => getCodeLanguageByInput(lang), [lang]);\n\n // Optimize transformer creation\n const transformers = useMemo(() => {\n if (!enableTransformer) return;\n return [\n transformerNotationDiff(),\n transformerNotationHighlight(),\n transformerNotationWordHighlight(),\n transformerNotationFocus(),\n transformerNotationErrorLevel(),\n ];\n }, [enableTransformer]);\n\n // Build cache key\n const cacheKey = useMemo((): string | null => {\n if (streaming) return null;\n // Use hash for long text\n const hash = safeText.length < MD5_LENGTH_THRESHOLD ? safeText : Md5.hashStr(safeText);\n return [matchedLanguage, builtinTheme, hash].filter(Boolean).join('-');\n }, [safeText, matchedLanguage, builtinTheme, streaming]);\n\n const [data, setData] = useState<string | undefined>();\n\n useEffect(() => {\n if (!cacheKey) {\n setData(undefined);\n return;\n }\n\n // Check cache first\n const cachedPromise = highlightCache.get(cacheKey);\n if (cachedPromise) {\n cachedPromise\n .then((html) => {\n setData(html);\n })\n .catch(() => {\n // Silently handle errors, fallback will be handled in the promise\n });\n return;\n }\n\n // Create new promise for highlighting\n // Using codeToHtml shorthand: automatically loads themes/languages on-demand\n const highlightPromise = (async (): Promise<string> => {\n try {\n // Try full rendering with transformers\n const shikiModule = await shikiModulePromise;\n if (!shikiModule) return safeText;\n\n const effectiveTheme = builtinTheme || 'lobe-theme';\n\n // Load custom theme if using slack-dark or slack-ochin\n if (!builtinTheme && effectiveTheme === 'lobe-theme') {\n const customTheme = customThemes[effectiveTheme];\n if (customTheme) {\n // Use getSingletonHighlighter to load custom theme\n const highlighter = await shikiModule.getSingletonHighlighter({\n langs: [matchedLanguage],\n themes: [customTheme as any],\n });\n\n const html = await highlighter.codeToHtml(safeText, {\n lang: matchedLanguage,\n theme: effectiveTheme,\n transformers,\n });\n\n return html;\n }\n }\n\n // Fallback to codeToHtml for builtin themes\n const codeToHtml = await loadCodeToHtml();\n if (!codeToHtml) return safeText;\n\n const html = await codeToHtml(safeText, {\n lang: matchedLanguage,\n theme: effectiveTheme,\n transformers,\n });\n\n return html;\n } catch (error_) {\n console.error('Advanced rendering failed:', error_);\n\n try {\n // Try simple rendering (without transformers)\n const codeToHtml = await loadCodeToHtml();\n if (!codeToHtml) return safeText;\n const html = await codeToHtml(safeText, {\n lang: matchedLanguage,\n theme: 'lobe-theme',\n });\n return html;\n } catch {\n // Fallback to plain text\n const fallbackHtml = `<pre class=\"fallback\"><code>${escapeHtml(safeText)}</code></pre>`;\n return fallbackHtml;\n }\n }\n })();\n\n // Cache the promise\n highlightCache.set(cacheKey, highlightPromise);\n cleanupCache();\n\n // Handle promise result\n highlightPromise\n .then((html) => {\n // Only update if this is still the current cache key\n if (highlightCache.get(cacheKey) === highlightPromise) {\n setData(html);\n }\n })\n .catch(() => {\n // Remove failed promise from cache\n if (highlightCache.get(cacheKey) === highlightPromise) {\n highlightCache.delete(cacheKey);\n }\n });\n }, [cacheKey, safeText, matchedLanguage, builtinTheme, transformers, customThemes]);\n\n return data || '';\n};\n"],"mappings":"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"}