import { codeToHtml, codeToTokens, type BundledTheme, type BundledLanguage } from 'shiki'; import { CODE_THEMES, type CodeThemeId } from '../types'; let highlighterReady = false; export async function highlightCode( code: string, language: string, theme: CodeThemeId ): Promise { try { const html = await codeToHtml(code, { lang: language as BundledLanguage, theme: theme as BundledTheme, }); highlighterReady = true; return html; } catch (error) { console.error('Highlighting error:', error); return `
${escapeHtml(code)}
`; } } export interface TokenInfo { content: string; color: string; } export interface LineTokens { tokens: TokenInfo[]; } export async function tokenizeCode( code: string, language: string, theme: CodeThemeId ): Promise { try { const result = await codeToTokens(code, { lang: language as BundledLanguage, theme: theme as BundledTheme, }); highlighterReady = true; return result.tokens.map(line => ({ tokens: line.map(token => ({ content: token.content, color: token.color || '#ffffff', })), })); } catch (error) { console.error('Tokenization error:', error); // Fallback: return plain text tokens return code.split('\n').map(line => ({ tokens: [{ content: line, color: '#ffffff' }], })); } } export function getThemeBackground(themeId: CodeThemeId): string { const theme = CODE_THEMES.find(t => t.id === themeId); return theme?.bg || '#1e1e2e'; } export function isLightTheme(themeId: CodeThemeId): boolean { const lightThemes = ['github-light', 'vitesse-light', 'min-light', 'solarized-light', 'light-plus']; return lightThemes.includes(themeId); } function escapeHtml(text: string): string { return text .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); } export function isHighlighterReady(): boolean { return highlighterReady; } // Language detection based on common patterns export function detectLanguage(code: string): string { // Kotlin / Jetpack Compose patterns (check first - primary language) // Look for @Composable, fun, val, var, class, object, companion object, etc. if (/@Composable|@Preview|@OptIn|@Suppress/.test(code)) { return 'kotlin'; } if (/^(fun|val|var|class|object|package|import|sealed|data\s+class|enum\s+class|interface)\s+/m.test(code)) { // Check for Kotlin-specific syntax if (/:\s*\w+(\s*[={()]|$|\s*,)/.test(code) || /\b(Unit|String|Int|Long|Boolean|Float|Double|List|Map|Set|suspend|inline|crossinline|noinline|reified)\b/.test(code) || /\bModifier\b|\bColumn\b|\bRow\b|\bBox\b|\bText\b|\bButton\b|\bScaffold\b/.test(code) || /\.copy\(|\.let\s*\{|\.apply\s*\{|\.also\s*\{|\.run\s*\{/.test(code) || /\blambda\b|->/.test(code)) { return 'kotlin'; } } // TypeScript/JavaScript patterns if (/^import\s+.*from\s+['"]|^export\s+(default\s+)?|const\s+\w+:\s*\w+/m.test(code)) { if (/:\s*(string|number|boolean|any|void|Promise|Array)\b/.test(code)) { return 'typescript'; } return 'javascript'; } // Java patterns (after Kotlin to avoid false positives) if (/^(public|private|protected)\s+(static\s+)?(class|void|int|String)/m.test(code) && !/@Composable/.test(code)) { return 'java'; } // Swift patterns if (/^(func|var|let|class|struct|enum|protocol|import\s+\w+)\s+/m.test(code) && /@State|@Binding|@Published|@ObservedObject|some\s+View/.test(code)) { return 'swift'; } // Dart/Flutter patterns if (/^(class|void|final|const|import\s+')/m.test(code) && /Widget|StatelessWidget|StatefulWidget|BuildContext|setState/.test(code)) { return 'dart'; } // Python patterns if (/^(def|class|import|from|if __name__|print\()/m.test(code) && !/[{};]/.test(code)) { return 'python'; } // Rust patterns if (/^(fn|let|mut|impl|struct|enum|use|pub)\s+/m.test(code) && /->/.test(code)) { return 'rust'; } // Go patterns if (/^(func|package|import|type|var|const)\s+/m.test(code) && /:=/.test(code)) { return 'go'; } // Groovy/Gradle patterns if (/^(plugins|dependencies|android|buildscript)\s*\{/m.test(code) || /implementation\s*\(|compile\s*\(/.test(code)) { return 'groovy'; } // XML patterns (for Android layouts) if (/^<\?xml|^