UNPKG

@lobehub/ui

Version:

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

1 lines 9.82 kB
{"version":3,"file":"useHighlight.mjs","names":["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 { type 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('&', '&amp;')\n .replaceAll('<', '&lt;')\n .replaceAll('>', '&gt;')\n .replaceAll('\"', '&quot;')\n .replaceAll(\"'\", '&#039;');\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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"}