import { useMemo } from 'react'; interface SyntaxHighlightProps { code: string; language: string; } // Apply a regex replacement only on text outside of HTML tags function replaceOutsideTags(text: string, regex: RegExp, fn: (match: string, ...args: any[]) => string): string { let result = ''; let lastIndex = 0; const tagRe = /<[^>]*>/g; let tagMatch: RegExpExecArray | null; while ((tagMatch = tagRe.exec(text)) !== null) { result += text.substring(lastIndex, tagMatch.index).replace(regex, fn as any); result += tagMatch[0]; lastIndex = tagMatch.index + tagMatch[0].length; } result += text.substring(lastIndex).replace(regex, fn as any); return result; } // A lightweight syntax highlighter without heavy dependencies export function SyntaxHighlight({ code, language }: SyntaxHighlightProps) { const highlighted = useMemo(() => { const escaped = code .replace(/&/g, '&') .replace(//g, '>'); const lines = escaped.split('\n'); return lines.map((line, i) => { // Step 1: Highlight strings (adds HTML markup) let processed = line.replace( /("(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'|`(?:[^`\\]|\\.)*`)/g, '$1' ); // Step 2: Highlight numbers (must skip inside HTML tags added by steps above) processed = replaceOutsideTags(processed, /\b(\d+\.?\d*)\b/g, (_, num) => `${num}` ); // Step 3: Highlight comments (skip inside HTML tags) processed = replaceOutsideTags(processed, /(\/\/.*$|\/\*[\s\S]*?\*\/)/g, (_, comment) => `${comment}` ); // Step 4: Highlight keywords (skip inside HTML tags) const keywords = [ '\\bconst\\b', '\\blet\\b', '\\bvar\\b', '\\bfunction\\b', '\\breturn\\b', '\\bif\\b', '\\belse\\b', '\\bfor\\b', '\\bwhile\\b', '\\bdo\\b', '\\bswitch\\b', '\\bcase\\b', '\\bbreak\\b', '\\bcontinue\\b', '\\btry\\b', '\\bcatch\\b', '\\bfinally\\b', '\\bthrow\\b', '\\bnew\\b', '\\bclass\\b', '\\bextends\\b', '\\bsuper\\b', '\\bimport\\b', '\\bexport\\b', '\\bfrom\\b', '\\brequire\\b', '\\bmodule\\b', '\\bexports\\b', '\\bdefault\\b', '\\btypeof\\b', '\\binstanceof\\b', '\\bthis\\b', '\\btrue\\b', '\\bfalse\\b', '\\bnull\\b', '\\bundefined\\b', '\\bawait\\b', '\\basync\\b', '\\byield\\b', '\\bdelete\\b', '\\bin\\b', '\\bof\\b', '\\bpublic\\b', '\\bprivate\\b', '\\bprotected\\b', '\\binternal\\b', '\\bclass\\b', '\\bstruct\\b', '\\binterface\\b', '\\benum\\b', '\\bnamespace\\b', '\\busing\\b', '\\basync\\b', '\\bawait\\b', '\\bvoid\\b', '\\bint\\b', '\\bstring\\b', '\\bbool\\b', '\\bvar\\b', '\\bget\\b', '\\bset\\b', '\\bvalue\\b', '\\bpartial\\b', '\\bstatic\\b', '\\breadonly\\b', '\\bvirtual\\b', '\\boverride\\b', '\\babstract\\b', '\\bsealed\\b', ]; const keywordPattern = new RegExp(keywords.join('|'), 'g'); processed = replaceOutsideTags(processed, keywordPattern, (match) => `${match}` ); // Step 5: Highlight HTML tags/attributes (only for html/xml languages) if (language === 'html' || language === 'xml') { processed = processed.replace( /(<\/?)([\w-]+)/g, '$1$2' ); processed = processed.replace( /(\s)([\w-]+)(=)/g, '$1$2$3' ); } const lineNum = String(i + 1).padStart(4, ' '); return `
${lineNum} ${processed || ' '}
`; }).join('\n'); }, [code, language]); return (
); }