311 lines
13 KiB
C#
311 lines
13 KiB
C#
using System.Text.RegularExpressions;
|
|
|
|
namespace RelayClient.Helpers;
|
|
|
|
public static class SyntaxHighlighter
|
|
{
|
|
private static readonly Color DefaultColor = Color.FromArgb("#D4D4D4");
|
|
private static readonly Color KeywordColor = Color.FromArgb("#569CD6");
|
|
private static readonly Color StringColor = Color.FromArgb("#CE9178");
|
|
private static readonly Color NumberColor = Color.FromArgb("#B5CEA8");
|
|
private static readonly Color CommentColor = Color.FromArgb("#6A9955");
|
|
private static readonly Color TypeColor = Color.FromArgb("#4EC9B0");
|
|
private static readonly Color FunctionColor = Color.FromArgb("#DCDCAA");
|
|
private static readonly Color OperatorColor = Color.FromArgb("#D4D4D4");
|
|
private static readonly Color TagColor = Color.FromArgb("#569CD6");
|
|
private static readonly Color AttrColor = Color.FromArgb("#9CDCFE");
|
|
|
|
private const string FontFamily = "AnonymousProRegular";
|
|
|
|
private static readonly Dictionary<string, string> Aliases = new(StringComparer.OrdinalIgnoreCase)
|
|
{
|
|
["cs"] = "csharp",
|
|
["c#"] = "csharp",
|
|
["js"] = "javascript",
|
|
["jsx"] = "javascript",
|
|
["ts"] = "typescript",
|
|
["tsx"] = "typescript",
|
|
["py"] = "python",
|
|
["sh"] = "bash",
|
|
["shell"] = "bash",
|
|
["zsh"] = "bash",
|
|
["htm"] = "html",
|
|
["xml"] = "html",
|
|
["yml"] = "yaml"
|
|
};
|
|
|
|
private static readonly Dictionary<string, HashSet<string>> Keywords = new(StringComparer.OrdinalIgnoreCase)
|
|
{
|
|
["csharp"] = new(StringComparer.Ordinal)
|
|
{
|
|
"abstract","as","async","await","base","bool","break","byte","case","catch","char","checked",
|
|
"class","const","continue","decimal","default","delegate","do","double","else","enum","event",
|
|
"explicit","extern","false","finally","fixed","float","for","foreach","get","goto","if",
|
|
"implicit","in","int","interface","internal","is","lock","long","namespace","new","null",
|
|
"object","operator","out","override","params","partial","private","protected","public",
|
|
"readonly","record","ref","return","sbyte","sealed","set","short","sizeof","stackalloc",
|
|
"static","string","struct","switch","this","throw","true","try","typeof","uint","ulong",
|
|
"unchecked","unsafe","ushort","using","var","virtual","void","volatile","while","yield",
|
|
"nameof","when","where","global","init","required","file","scoped","with"
|
|
},
|
|
["javascript"] = new(StringComparer.Ordinal)
|
|
{
|
|
"async","await","break","case","catch","class","const","continue","debugger","default",
|
|
"delete","do","else","enum","export","extends","false","finally","for","from","function",
|
|
"get","if","implements","import","in","instanceof","let","new","null","of","package",
|
|
"private","protected","public","return","set","static","super","switch","this","throw",
|
|
"true","try","typeof","undefined","var","void","while","with","yield"
|
|
},
|
|
["typescript"] = new(StringComparer.Ordinal)
|
|
{
|
|
"any","as","async","await","boolean","break","case","catch","class","const","continue",
|
|
"debugger","declare","default","delete","do","else","enum","export","extends","false",
|
|
"finally","for","from","function","get","if","implements","import","in","instanceof",
|
|
"interface","is","keyof","let","namespace","never","new","null","number","of","package",
|
|
"private","protected","public","readonly","return","set","static","string","super",
|
|
"switch","this","throw","true","try","type","typeof","undefined","unknown","var","void",
|
|
"while","with","yield"
|
|
},
|
|
["python"] = new(StringComparer.Ordinal)
|
|
{
|
|
"and","as","assert","async","await","break","class","continue","def","del","elif","else",
|
|
"except","False","finally","for","from","global","if","import","in","is","lambda","None",
|
|
"nonlocal","not","or","pass","raise","return","True","try","while","with","yield","self",
|
|
"cls","match","case"
|
|
},
|
|
["sql"] = new(StringComparer.OrdinalIgnoreCase)
|
|
{
|
|
"select","from","where","insert","update","delete","create","alter","drop","table","index",
|
|
"view","join","inner","outer","left","right","full","cross","on","as","group","by","order",
|
|
"having","distinct","union","all","into","values","set","null","not","and","or","in","like",
|
|
"between","is","true","false","primary","key","foreign","references","default","limit",
|
|
"offset","with","case","when","then","else","end","exists","cast","begin","commit","rollback"
|
|
},
|
|
["bash"] = new(StringComparer.Ordinal)
|
|
{
|
|
"if","then","else","elif","fi","for","in","do","done","while","until","case","esac",
|
|
"function","return","break","continue","exit","echo","printf","export","local","readonly",
|
|
"source","alias","unset","trap","set","eval","exec","shift","let","declare","typeset"
|
|
},
|
|
["json"] = new(StringComparer.Ordinal) { "true","false","null" },
|
|
["yaml"] = new(StringComparer.Ordinal) { "true","false","null","yes","no","on","off" },
|
|
["css"] = new(StringComparer.OrdinalIgnoreCase)
|
|
{
|
|
"important","inherit","initial","unset","auto","none","normal","bold","italic","center",
|
|
"left","right","top","bottom","flex","grid","block","inline","absolute","relative","fixed",
|
|
"sticky","static"
|
|
}
|
|
};
|
|
|
|
private static readonly Dictionary<string, Regex> Tokenizers = new(StringComparer.Ordinal);
|
|
|
|
static SyntaxHighlighter()
|
|
{
|
|
const RegexOptions opts = RegexOptions.Compiled | RegexOptions.Singleline;
|
|
|
|
Tokenizers["csharp"] = new Regex(
|
|
@"(?<comment>//[^\n]*|/\*.*?\*/)" +
|
|
@"|(?<string>@""(?:""""|[^""])*""|\$""(?:\\.|[^""\\])*""|""(?:\\.|[^""\\])*""|'(?:\\.|[^'\\])*')" +
|
|
@"|(?<number>\b\d+(?:\.\d+)?[fFdDmMuUlL]*\b)" +
|
|
@"|(?<word>[A-Za-z_]\w*)",
|
|
opts);
|
|
|
|
Tokenizers["javascript"] = new Regex(
|
|
@"(?<comment>//[^\n]*|/\*.*?\*/)" +
|
|
@"|(?<string>""(?:\\.|[^""\\])*""|'(?:\\.|[^'\\])*'|`(?:\\.|[^`\\])*`)" +
|
|
@"|(?<number>\b\d+(?:\.\d+)?\b)" +
|
|
@"|(?<word>[A-Za-z_$][\w$]*)",
|
|
opts);
|
|
|
|
Tokenizers["typescript"] = Tokenizers["javascript"];
|
|
|
|
Tokenizers["python"] = new Regex(
|
|
@"(?<comment>\#[^\n]*)" +
|
|
@"|(?<string>""""""[\s\S]*?""""""|'''[\s\S]*?'''|""(?:\\.|[^""\\])*""|'(?:\\.|[^'\\])*')" +
|
|
@"|(?<number>\b\d+(?:\.\d+)?\b)" +
|
|
@"|(?<word>[A-Za-z_]\w*)",
|
|
opts);
|
|
|
|
Tokenizers["sql"] = new Regex(
|
|
@"(?<comment>--[^\n]*|/\*.*?\*/)" +
|
|
@"|(?<string>'(?:''|[^'])*')" +
|
|
@"|(?<number>\b\d+(?:\.\d+)?\b)" +
|
|
@"|(?<word>[A-Za-z_]\w*)",
|
|
opts);
|
|
|
|
Tokenizers["bash"] = new Regex(
|
|
@"(?<comment>\#[^\n]*)" +
|
|
@"|(?<string>""(?:\\.|[^""\\])*""|'[^']*')" +
|
|
@"|(?<number>\b\d+\b)" +
|
|
@"|(?<variable>\$\{?[A-Za-z_]\w*\}?)" +
|
|
@"|(?<word>[A-Za-z_][\w-]*)",
|
|
opts);
|
|
|
|
Tokenizers["json"] = new Regex(
|
|
@"(?<string>""(?:\\.|[^""\\])*"")" +
|
|
@"|(?<number>-?\b\d+(?:\.\d+)?(?:[eE][+-]?\d+)?\b)" +
|
|
@"|(?<word>true|false|null)",
|
|
opts);
|
|
|
|
Tokenizers["yaml"] = new Regex(
|
|
@"(?<comment>\#[^\n]*)" +
|
|
@"|(?<string>""(?:\\.|[^""\\])*""|'[^']*')" +
|
|
@"|(?<key>^[ \t]*[A-Za-z_][\w-]*(?=\s*:))" +
|
|
@"|(?<number>\b\d+(?:\.\d+)?\b)" +
|
|
@"|(?<word>[A-Za-z_][\w-]*)",
|
|
opts | RegexOptions.Multiline);
|
|
|
|
Tokenizers["html"] = new Regex(
|
|
@"(?<comment><!--.*?-->)" +
|
|
@"|(?<string>""[^""]*""|'[^']*')" +
|
|
@"|(?<tag></?[A-Za-z][A-Za-z0-9-]*)" +
|
|
@"|(?<attr>\b[A-Za-z_][\w-]*(?==))",
|
|
opts);
|
|
|
|
Tokenizers["css"] = new Regex(
|
|
@"(?<comment>/\*.*?\*/)" +
|
|
@"|(?<string>""[^""]*""|'[^']*')" +
|
|
@"|(?<number>-?\b\d+(?:\.\d+)?(?:px|em|rem|%|vh|vw|s|ms|deg)?\b)" +
|
|
@"|(?<selector>[.#]?[A-Za-z_][\w-]*(?=\s*[{,]))" +
|
|
@"|(?<prop>[A-Za-z-]+(?=\s*:))" +
|
|
@"|(?<word>[A-Za-z_][\w-]*)",
|
|
opts);
|
|
|
|
Tokenizers["diff"] = new Regex(
|
|
@"(?<add>^\+[^\n]*)" +
|
|
@"|(?<del>^-[^\n]*)" +
|
|
@"|(?<hunk>^@@[^\n]*)",
|
|
opts | RegexOptions.Multiline);
|
|
|
|
Tokenizers["markdown"] = new Regex(
|
|
@"(?<header>^#{1,6}[^\n]*)" +
|
|
@"|(?<bold>\*\*[^*\n]+\*\*|__[^_\n]+__)" +
|
|
@"|(?<italic>\*[^*\n]+\*|_[^_\n]+_)" +
|
|
@"|(?<code>`[^`\n]+`)" +
|
|
@"|(?<link>\[[^\]]+\]\([^)]+\))",
|
|
opts | RegexOptions.Multiline);
|
|
}
|
|
|
|
public static List<Span> Highlight(string code, string? language, double fontSize)
|
|
{
|
|
var lang = Resolve(language);
|
|
var spans = new List<Span>();
|
|
|
|
if (lang is null || !Tokenizers.TryGetValue(lang, out var tokenizer))
|
|
{
|
|
spans.Add(MakeSpan(code, DefaultColor, fontSize));
|
|
return spans;
|
|
}
|
|
|
|
var keywords = Keywords.GetValueOrDefault(lang);
|
|
int cursor = 0;
|
|
|
|
foreach (Match m in tokenizer.Matches(code))
|
|
{
|
|
if (m.Index > cursor)
|
|
spans.Add(MakeSpan(code[cursor..m.Index], DefaultColor, fontSize));
|
|
|
|
spans.Add(SpanForMatch(m, lang, keywords, fontSize));
|
|
cursor = m.Index + m.Length;
|
|
}
|
|
|
|
if (cursor < code.Length)
|
|
spans.Add(MakeSpan(code[cursor..], DefaultColor, fontSize));
|
|
|
|
return spans;
|
|
}
|
|
|
|
private static Span SpanForMatch(Match m, string lang, HashSet<string>? keywords, double fontSize)
|
|
{
|
|
if (m.Groups["comment"].Success)
|
|
return MakeSpan(m.Value, CommentColor, fontSize, italic: true);
|
|
|
|
if (m.Groups["string"].Success)
|
|
return MakeSpan(m.Value, StringColor, fontSize);
|
|
|
|
if (m.Groups["number"].Success)
|
|
return MakeSpan(m.Value, NumberColor, fontSize);
|
|
|
|
if (m.Groups["variable"].Success)
|
|
return MakeSpan(m.Value, AttrColor, fontSize);
|
|
|
|
if (m.Groups["tag"].Success)
|
|
return MakeSpan(m.Value, TagColor, fontSize);
|
|
|
|
if (m.Groups["attr"].Success)
|
|
return MakeSpan(m.Value, AttrColor, fontSize);
|
|
|
|
if (m.Groups["selector"].Success)
|
|
return MakeSpan(m.Value, TypeColor, fontSize);
|
|
|
|
if (m.Groups["prop"].Success)
|
|
return MakeSpan(m.Value, AttrColor, fontSize);
|
|
|
|
if (m.Groups["key"].Success)
|
|
return MakeSpan(m.Value, AttrColor, fontSize);
|
|
|
|
if (m.Groups["add"].Success)
|
|
return MakeSpan(m.Value, Color.FromArgb("#6A9955"), fontSize);
|
|
|
|
if (m.Groups["del"].Success)
|
|
return MakeSpan(m.Value, Color.FromArgb("#F48771"), fontSize);
|
|
|
|
if (m.Groups["hunk"].Success)
|
|
return MakeSpan(m.Value, KeywordColor, fontSize);
|
|
|
|
if (m.Groups["header"].Success)
|
|
return MakeSpan(m.Value, KeywordColor, fontSize, bold: true);
|
|
|
|
if (m.Groups["bold"].Success)
|
|
return MakeSpan(m.Value, DefaultColor, fontSize, bold: true);
|
|
|
|
if (m.Groups["italic"].Success)
|
|
return MakeSpan(m.Value, DefaultColor, fontSize, italic: true);
|
|
|
|
if (m.Groups["code"].Success)
|
|
return MakeSpan(m.Value, StringColor, fontSize);
|
|
|
|
if (m.Groups["link"].Success)
|
|
return MakeSpan(m.Value, AttrColor, fontSize);
|
|
|
|
if (m.Groups["word"].Success)
|
|
{
|
|
var word = m.Value;
|
|
var compareSet = keywords;
|
|
|
|
if (compareSet is not null && compareSet.Contains(word))
|
|
return MakeSpan(word, KeywordColor, fontSize);
|
|
|
|
if (lang is "csharp" or "javascript" or "typescript" && word.Length > 0 && char.IsUpper(word[0]))
|
|
return MakeSpan(word, TypeColor, fontSize);
|
|
|
|
return MakeSpan(word, DefaultColor, fontSize);
|
|
}
|
|
|
|
return MakeSpan(m.Value, DefaultColor, fontSize);
|
|
}
|
|
|
|
private static Span MakeSpan(string text, Color color, double fontSize, bool bold = false, bool italic = false)
|
|
{
|
|
var attrs = FontAttributes.None;
|
|
if (bold) attrs |= FontAttributes.Bold;
|
|
if (italic) attrs |= FontAttributes.Italic;
|
|
|
|
return new Span
|
|
{
|
|
Text = text,
|
|
TextColor = color,
|
|
FontSize = fontSize,
|
|
FontFamily = FontFamily,
|
|
FontAttributes = attrs
|
|
};
|
|
}
|
|
|
|
private static string? Resolve(string? language)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(language)) return null;
|
|
var lower = language.Trim().ToLowerInvariant();
|
|
return Aliases.GetValueOrDefault(lower, lower);
|
|
}
|
|
}
|