File size: 4,316 Bytes
ce2d6ca
 
 
 
 
 
 
b38d231
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ce2d6ca
 
 
 
 
 
 
 
 
 
b38d231
ce2d6ca
 
 
 
b38d231
 
 
ce2d6ca
b38d231
 
 
ce2d6ca
b38d231
ce2d6ca
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
b38d231
 
 
 
860406b
 
 
 
 
 
 
 
 
 
ce2d6ca
 
 
 
70d86da
ce2d6ca
 
b38d231
ce2d6ca
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
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, '&amp;')
      .replace(/</g, '&lt;')
      .replace(/>/g, '&gt;');

    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(
          /(&lt;\/?)([\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>
  );
}