/** * Lightweight syntax highlighter for JSX, HTML, and CSS. * Produces HTML strings with color spans. */ interface TokenRule { pattern: RegExp; className: string; } const JSX_RULES: TokenRule[] = [ // Multi-line comments { pattern: /\/\*[\s\S]*?\*\//g, className: 'syn-comment' }, // Single-line comments { pattern: /\/\/.*/g, className: 'syn-comment' }, // JSX self-closing tags: { pattern: /<\/?[A-Za-z][A-Za-z0-9.]*/g, className: 'syn-tag' }, // Closing bracket { pattern: /\/?>/g, className: 'syn-tag' }, // Strings (double and single quoted) { pattern: /"(?:[^"\\]|\\.)*"/g, className: 'syn-string' }, { pattern: /'(?:[^'\\]|\\.)*'/g, className: 'syn-string' }, // Template literals { pattern: /`(?:[^`\\]|\\.)*`/g, className: 'syn-string' }, // Keywords { pattern: /\b(export|function|return|const|let|var|import|from|default|if|else|for|while|switch|case|break|continue|new|this|class|extends|typeof|instanceof|void|null|undefined|true|false)\b/g, className: 'syn-keyword', }, // Attribute names (word followed by =) { pattern: /\b[a-zA-Z-]+(?==)/g, className: 'syn-attr' }, // Numbers { pattern: /\b\d+\.?\d*\b/g, className: 'syn-number' }, // Curly braces in JSX { pattern: /[{}]/g, className: 'syn-bracket' }, ]; const HTML_RULES: TokenRule[] = [ // Comments { pattern: //g, className: 'syn-comment' }, // Tags { pattern: /<\/?[a-zA-Z][a-zA-Z0-9-]*/g, className: 'syn-tag' }, { pattern: /\/?>/g, className: 'syn-tag' }, // Attribute values { pattern: /"(?:[^"\\]|\\.)*"/g, className: 'syn-string' }, { pattern: /'(?:[^'\\]|\\.)*'/g, className: 'syn-string' }, // Attribute names { pattern: /\b[a-zA-Z-]+(?==)/g, className: 'syn-attr' }, ]; const CSS_RULES: TokenRule[] = [ // Comments { pattern: /\/\*[\s\S]*?\*\//g, className: 'syn-comment' }, // Selectors (class/id/element) { pattern: /[.#]?[a-zA-Z_-][a-zA-Z0-9_-]*(?=\s*\{)/g, className: 'syn-tag' }, // Property names { pattern: /[a-zA-Z-]+(?=\s*:)/g, className: 'syn-attr' }, // Strings { pattern: /"(?:[^"\\]|\\.)*"/g, className: 'syn-string' }, { pattern: /'(?:[^'\\]|\\.)*'/g, className: 'syn-string' }, // Numbers with units { pattern: /\b\d+\.?\d*(px|em|rem|%|deg|vh|vw|s|ms)?\b/g, className: 'syn-number' }, // Colors { pattern: /#[0-9a-fA-F]{3,8}\b/g, className: 'syn-string' }, // Braces { pattern: /[{}]/g, className: 'syn-bracket' }, // Keywords { pattern: /\b(important|inherit|initial|unset|none|auto|solid|dashed|flex|grid|block|inline|absolute|relative|fixed|sticky)\b/g, className: 'syn-keyword', }, ]; function escapeHtml(text: string): string { return text .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"'); } interface Token { start: number; end: number; className: string; } function tokenize(code: string, rules: TokenRule[]): Token[] { const tokens: Token[] = []; for (const rule of rules) { const regex = new RegExp(rule.pattern.source, rule.pattern.flags); let match: RegExpExecArray | null; while ((match = regex.exec(code)) !== null) { tokens.push({ start: match.index, end: match.index + match[0].length, className: rule.className, }); } } // Sort by start position; earlier rules win ties (priority order) tokens.sort((a, b) => a.start - b.start); // Remove overlapping tokens (first match wins) const filtered: Token[] = []; let lastEnd = 0; for (const token of tokens) { if (token.start >= lastEnd) { filtered.push(token); lastEnd = token.end; } } return filtered; } function renderTokens(code: string, tokens: Token[]): string { let result = ''; let pos = 0; for (const token of tokens) { if (token.start > pos) { result += escapeHtml(code.slice(pos, token.start)); } result += `${escapeHtml(code.slice(token.start, token.end))}`; pos = token.end; } if (pos < code.length) { result += escapeHtml(code.slice(pos)); } return result; } const SWIFT_RULES: TokenRule[] = [ { pattern: /\/\/.*/g, className: 'syn-comment' }, { pattern: /\/\*[\s\S]*?\*\//g, className: 'syn-comment' }, { pattern: /"(?:[^"\\]|\\.)*"/g, className: 'syn-string' }, { pattern: /\b(import|struct|var|let|func|return|if|else|for|in|while|switch|case|break|some|self|true|false|nil|class|enum|protocol|extension|guard|throw|try|catch|async|await|private|public|internal|static|mutating|typealias|associatedtype|where)\b/g, className: 'syn-keyword', }, { pattern: /\b(View|Text|VStack|HStack|ZStack|Color|Font|Image|Rectangle|RoundedRectangle|Circle|Ellipse|Capsule|Path|Spacer|Divider|GeometryReader|ScrollView|Button|AsyncImage|LinearGradient|RadialGradient|Shape|some)\b/g, className: 'syn-tag', }, { pattern: /\.[a-zA-Z]+(?=\()/g, className: 'syn-attr' }, { pattern: /\b\d+\.?\d*\b/g, className: 'syn-number' }, { pattern: /[{}()]/g, className: 'syn-bracket' }, ]; const KOTLIN_RULES: TokenRule[] = [ { pattern: /\/\/.*/g, className: 'syn-comment' }, { pattern: /\/\*[\s\S]*?\*\//g, className: 'syn-comment' }, { pattern: /"(?:[^"\\]|\\.)*"/g, className: 'syn-string' }, { pattern: /\b(package|import|fun|val|var|return|if|else|for|in|while|when|is|class|object|interface|override|private|public|internal|companion|data|sealed|abstract|open|suspend|inline|annotation|true|false|null)\b/g, className: 'syn-keyword', }, { pattern: /\b(Composable|Modifier|Column|Row|Box|Text|Image|Icon|Canvas|Divider|Spacer|Surface|Card|Scaffold|LazyColumn|LazyRow|Color|FontWeight|FontStyle|TextAlign|TextDecoration|Arrangement|Alignment|RoundedCornerShape|CircleShape|Dp|ContentScale)\b/g, className: 'syn-tag', }, { pattern: /\.[a-zA-Z]+(?=\()/g, className: 'syn-attr' }, { pattern: /\b\d+\.?\d*(f|dp|sp)?\b/g, className: 'syn-number' }, { pattern: /@[A-Za-z]+/g, className: 'syn-attr' }, { pattern: /[{}()]/g, className: 'syn-bracket' }, ]; const DART_RULES: TokenRule[] = [ { pattern: /\/\/.*/g, className: 'syn-comment' }, { pattern: /\/\*[\s\S]*?\*\//g, className: 'syn-comment' }, { pattern: /"(?:[^"\\]|\\.)*"/g, className: 'syn-string' }, { pattern: /'(?:[^'\\]|\\.)*'/g, className: 'syn-string' }, { pattern: /\b(import|class|extends|implements|with|mixin|abstract|final|const|var|void|return|if|else|for|in|while|switch|case|break|continue|new|this|super|true|false|null|async|await|static|late|required|override|enum|typedef|try|catch|throw|dynamic)\b/g, className: 'syn-keyword', }, { pattern: /\b(Widget|StatelessWidget|StatefulWidget|State|BuildContext|Container|Column|Row|Stack|Positioned|Text|Image|SizedBox|Padding|Center|Expanded|Flexible|Scaffold|AppBar|Material|BoxDecoration|BorderRadius|Border|EdgeInsets|Color|TextStyle|FontWeight|FontStyle|TextAlign|BoxFit|MainAxisAlignment|CrossAxisAlignment|CustomPaint|ClipPath|Opacity|Transform|LinearGradient|RadialGradient)\b/g, className: 'syn-tag', }, { pattern: /\.[a-zA-Z]+(?=\()/g, className: 'syn-attr' }, { pattern: /@[a-zA-Z]+/g, className: 'syn-attr' }, { pattern: /\b\d+\.?\d*\b/g, className: 'syn-number' }, { pattern: /[{}()]/g, className: 'syn-bracket' }, ]; export type SyntaxLanguage = 'jsx' | 'html' | 'css' | 'swift' | 'kotlin' | 'dart'; export function highlightCode(code: string, language: SyntaxLanguage): string { const ruleMap: Record = { jsx: JSX_RULES, html: HTML_RULES, css: CSS_RULES, swift: SWIFT_RULES, kotlin: KOTLIN_RULES, dart: DART_RULES, }; const rules = ruleMap[language]; const tokens = tokenize(code, rules); return renderTokens(code, tokens); }