Spaces:
Running
Running
| 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, '<') | |
| .replace(/>/g, '>'); | |
| const lines = escaped.split('\n'); | |
| return lines.map((line, i) => { | |
| // Step 1: Highlight strings (adds HTML markup) | |
| let processed = line.replace( | |
| /("(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'|`(?:[^`\\]|\\.)*`)/g, | |
| '<span class="text-green-400">$1</span>' | |
| ); | |
| // Step 2: Highlight numbers (must skip inside HTML tags added by steps above) | |
| processed = replaceOutsideTags(processed, /\b(\d+\.?\d*)\b/g, (_, num) => | |
| `<span class="text-amber-400">${num}</span>` | |
| ); | |
| // Step 3: Highlight comments (skip inside HTML tags) | |
| processed = replaceOutsideTags(processed, /(\/\/.*$|\/\*[\s\S]*?\*\/)/g, (_, comment) => | |
| `<span class="text-surface-500 italic">${comment}</span>` | |
| ); | |
| // 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) => | |
| `<span class="text-purple-400">${match}</span>` | |
| ); | |
| // Step 5: Highlight HTML tags/attributes (only for html/xml languages) | |
| if (language === 'html' || language === 'xml') { | |
| processed = processed.replace( | |
| /(<\/?)([\w-]+)/g, | |
| '$1<span class="text-blue-400">$2</span>' | |
| ); | |
| processed = processed.replace( | |
| /(\s)([\w-]+)(=)/g, | |
| '$1<span class="text-amber-300">$2</span>$3' | |
| ); | |
| } | |
| const lineNum = String(i + 1).padStart(4, ' '); | |
| return `<div class="flex"> | |
| <span class="text-surface-600 select-none text-right w-12 pr-4 flex-shrink-0 text-xs leading-6">${lineNum}</span> | |
| <span class="flex-1 text-xs leading-6" style="white-space: pre-wrap; word-break: break-all; overflow-wrap: break-word;">${processed || ' '}</span> | |
| </div>`; | |
| }).join('\n'); | |
| }, [code, language]); | |
| return ( | |
| <div className="p-0 font-mono text-sm"> | |
| <div className="bg-[#0a0e1a] p-0"> | |
| <div | |
| className="overflow-x-auto" | |
| dangerouslySetInnerHTML={{ __html: highlighted }} | |
| /> | |
| </div> | |
| </div> | |
| ); | |
| } | |