Forge
csharp3407750f
1using System.Net;
2using System.Text.RegularExpressions;
3
4namespace AgentForge.Plugin.View;
5
6internal static partial class ForgeSyntaxHighlighter
7{
8 public static string? LanguageFromPath(string? path)
9 {
10 if (string.IsNullOrWhiteSpace(path))
11 return null;
12
13 return Path.GetExtension(path).ToLowerInvariant() switch
14 {
15 ".cs" => "csharp",
16 ".py" => "python",
17 ".json" => "json",
18 ".md" => "markdown",
19 ".xml" => "xml",
20 ".html" or ".htm" => "html",
21 ".js" or ".jsx" => "javascript",
22 ".ts" or ".tsx" => "typescript",
23 ".yaml" or ".yml" => "yaml",
24 ".sh" or ".bash" => "shell",
25 ".ps1" => "powershell",
26 ".sql" => "sql",
27 _ => null,
28 };
29 }
30
31 public static string HighlightLine(string line, string? language) =>
32 language switch
33 {
34 "csharp" => HighlightWithRules(line, CSharpRules()),
35 "python" => HighlightWithRules(line, PythonRules()),
36 "javascript" or "typescript" => HighlightWithRules(line, JavaScriptRules()),
37 "json" => HighlightJsonLine(line),
38 "markdown" => HighlightMarkdownLine(line),
39 "xml" or "html" => HighlightWithRules(line, XmlRules()),
40 "yaml" => HighlightYamlLine(line),
41 "shell" or "powershell" => HighlightWithRules(line, ShellRules()),
42 "sql" => HighlightWithRules(line, SqlRules()),
43 _ => Enc(line),
44 };
45
46 private static string HighlightJsonLine(string line)
47 {
48 var trimmed = line.TrimStart();
49 if (trimmed.StartsWith("//", StringComparison.Ordinal) || trimmed.StartsWith('#'))
50 return Cmt(line);
51
52 var keyMatch = JsonKeyRegex().Match(line);
53 if (keyMatch.Success)
54 {
55 return string.Concat(
56 Enc(line[..keyMatch.Index]),
57 Str(keyMatch.Groups["key"].Value),
58 Enc(line[(keyMatch.Index + keyMatch.Length)..]));
59 }
60
61 return HighlightWithRules(line, JsonValueRules());
62 }
63
64 private static string HighlightMarkdownLine(string line)
65 {
66 if (line.StartsWith("#", StringComparison.Ordinal))
67 return Kw(line);
68
69 if (line.TrimStart().StartsWith("```", StringComparison.Ordinal))
70 return Kw(line);
71
72 return Enc(line);
73 }
74
75 private static string HighlightYamlLine(string line)
76 {
77 var trimmed = line.TrimStart();
78 if (trimmed.StartsWith('#'))
79 return Cmt(line);
80
81 var keyMatch = YamlKeyRegex().Match(line);
82 if (keyMatch.Success)
83 {
84 return string.Concat(
85 Enc(line[..keyMatch.Index]),
86 Kw(keyMatch.Groups["key"].Value),
87 Enc(line[(keyMatch.Index + keyMatch.Length)..]));
88 }
89
90 return Enc(line);
91 }
92
93 private static string HighlightWithRules(string line, IReadOnlyList<HighlightRule> rules)
94 {
95 if (line.Length == 0)
96 return string.Empty;
97
98 var spans = new List<TextSpan> { new(0, line.Length, TextSpanKind.Plain) };
99 foreach (var rule in rules)
100 ApplyRule(line, spans, rule);
101
102 return RenderSpans(line, spans);
103 }
104
105 private static void ApplyRule(string line, List<TextSpan> spans, HighlightRule rule)
106 {
107 for (var i = 0; i < spans.Count; i++)
108 {
109 var span = spans[i];
110 if (span.Kind != TextSpanKind.Plain)
111 continue;
112
113 var segment = line.Substring(span.Start, span.Length);
114 var match = rule.Regex.Match(segment);
115 if (!match.Success)
116 continue;
117
118 var replacements = new List<TextSpan>();
119 var cursor = 0;
120 do
121 {
122 if (match.Index > cursor)
123 {
124 replacements.Add(new TextSpan(
125 span.Start + cursor,
126 match.Index - cursor,
127 TextSpanKind.Plain));
128 }
129
130 replacements.Add(new TextSpan(
131 span.Start + match.Index,
132 match.Length,
133 rule.Kind));
134
135 cursor = match.Index + match.Length;
136 match = match.Index + match.Length < segment.Length
137 ? rule.Regex.Match(segment, match.Index + match.Length)
138 : Match.Empty;
139 }
140 while (match.Success);
141
142 if (cursor < segment.Length)
143 replacements.Add(new TextSpan(span.Start + cursor, segment.Length - cursor, TextSpanKind.Plain));
144
145 spans.RemoveAt(i);
146 spans.InsertRange(i, replacements);
147 i += replacements.Count - 1;
148 }
149 }
150
151 private static string RenderSpans(string line, IReadOnlyList<TextSpan> spans)
152 {
153 var result = new System.Text.StringBuilder();
154 foreach (var span in spans)
155 {
156 var text = line.Substring(span.Start, span.Length);
157 result.Append(span.Kind switch
158 {
159 TextSpanKind.Keyword => Kw(text),
160 TextSpanKind.String => Str(text),
161 TextSpanKind.Comment => Cmt(text),
162 TextSpanKind.Number => Num(text),
163 TextSpanKind.Type => Typ(text),
164 _ => Enc(text),
165 });
166 }
167
168 return result.ToString();
169 }
170
171 private static string Enc(string value) => WebUtility.HtmlEncode(value);
172
173 private static string Kw(string value) => $"<span class=\"syn-kw\">{Enc(value)}</span>";
174
175 private static string Str(string value) => $"<span class=\"syn-str\">{Enc(value)}</span>";
176
177 private static string Cmt(string value) => $"<span class=\"syn-cmt\">{Enc(value)}</span>";
178
179 private static string Num(string value) => $"<span class=\"syn-num\">{Enc(value)}</span>";
180
181 private static string Typ(string value) => $"<span class=\"syn-type\">{Enc(value)}</span>";
182
183 private static HighlightRule[] CSharpRules() =>
184 [
185 new(StringRegex(), TextSpanKind.String),
186 new(CommentRegex(), TextSpanKind.Comment),
187 new(WordRegex(
188 "class", "struct", "record", "interface", "enum", "namespace", "using", "public", "private",
189 "protected", "internal", "static", "readonly", "async", "await", "return", "if", "else", "switch",
190 "case", "for", "foreach", "while", "do", "break", "continue", "new", "true", "false", "null",
191 "var", "void", "int", "string", "bool", "byte", "char", "decimal", "double", "float", "long",
192 "object", "dynamic", "typeof", "nameof", "throw", "try", "catch", "finally", "yield"), TextSpanKind.Keyword),
193 new(WordRegex(
194 "Task", "ValueTask", "List", "Dictionary", "HashSet", "IEnumerable", "IReadOnlyList", "StringBuilder",
195 "CancellationToken", "Exception", "DateTime", "DateTimeOffset", "Guid", "Uri", "Regex", "String",
196 "Int32", "Boolean"), TextSpanKind.Type),
197 new(TypeNameRegex(), TextSpanKind.Type),
198 new(NumberRegex(), TextSpanKind.Number),
199 ];
200
201 private static HighlightRule[] PythonRules() =>
202 [
203 new(StringRegex(), TextSpanKind.String),
204 new(CommentRegex(), TextSpanKind.Comment),
205 new(WordRegex(
206 "def", "class", "if", "elif", "else", "for", "while", "return", "import", "from", "as", "with",
207 "try", "except", "finally", "raise", "pass", "break", "continue", "True", "False", "None", "and",
208 "or", "not", "in", "is", "lambda", "yield", "async", "await"), TextSpanKind.Keyword),
209 new(NumberRegex(), TextSpanKind.Number),
210 ];
211
212 private static HighlightRule[] JavaScriptRules() =>
213 [
214 new(StringRegex(), TextSpanKind.String),
215 new(CommentRegex(), TextSpanKind.Comment),
216 new(WordRegex(
217 "function", "const", "let", "var", "if", "else", "for", "while", "return", "import", "export",
218 "class", "extends", "new", "true", "false", "null", "undefined", "async", "await", "try", "catch",
219 "finally", "throw", "switch", "case", "break", "continue", "typeof", "interface", "type"), TextSpanKind.Keyword),
220 new(NumberRegex(), TextSpanKind.Number),
221 ];
222
223 private static HighlightRule[] JsonValueRules() =>
224 [
225 new(StringRegex(), TextSpanKind.String),
226 new(WordRegex("true", "false", "null"), TextSpanKind.Keyword),
227 new(NumberRegex(), TextSpanKind.Number),
228 ];
229
230 private static HighlightRule[] XmlRules() =>
231 [
232 new(XmlTagRegex(), TextSpanKind.Keyword),
233 new(StringRegex(), TextSpanKind.String),
234 new(CommentRegex(), TextSpanKind.Comment),
235 ];
236
237 private static HighlightRule[] ShellRules() =>
238 [
239 new(StringRegex(), TextSpanKind.String),
240 new(CommentRegex(), TextSpanKind.Comment),
241 new(WordRegex("if", "then", "else", "fi", "for", "do", "done", "while", "case", "esac", "function", "return", "exit"), TextSpanKind.Keyword),
242 ];
243
244 private static HighlightRule[] SqlRules() =>
245 [
246 new(StringRegex(), TextSpanKind.String),
247 new(CommentRegex(), TextSpanKind.Comment),
248 new(WordRegex(
249 "select", "from", "where", "join", "inner", "left", "right", "on", "group", "by", "order", "having",
250 "insert", "into", "values", "update", "set", "delete", "create", "table", "alter", "drop", "and",
251 "or", "not", "null", "as", "distinct", "limit", "offset"), TextSpanKind.Keyword),
252 new(NumberRegex(), TextSpanKind.Number),
253 ];
254
255 private static Regex StringRegex() => new(
256 @"@""(?:""""|[^""])*""|""(?:\\.|[^""\\])*""|'(?:\\.|[^'\\])*'",
257 RegexOptions.Compiled);
258
259 private static Regex CommentRegex() => new(
260 @"//.*$|#.*$|/\*.*?\*/",
261 RegexOptions.Compiled);
262
263 private static Regex NumberRegex() => new(
264 @"\b\d+(?:\.\d+)?\b",
265 RegexOptions.Compiled);
266
267 private static Regex TypeNameRegex() => new(
268 @"\bI[A-Z][A-Za-z0-9]+\b|\b[A-Z][A-Za-z0-9]+\b",
269 RegexOptions.Compiled);
270
271 private static Regex WordRegex(params string[] words) =>
272 new($@"\b(?:{string.Join("|", words)})\b", RegexOptions.Compiled | RegexOptions.IgnoreCase);
273
274 private static Regex XmlTagRegex() => new(
275 @"</?[\w:.-]+|/?>",
276 RegexOptions.Compiled);
277
278 [GeneratedRegex(""""(?<key>"[^"]+")\s*:"""")]
279 private static partial Regex JsonKeyRegex();
280
281 [GeneratedRegex(@"^(?<key>[\w.-]+)\s*:")]
282 private static partial Regex YamlKeyRegex();
283
284 private enum TextSpanKind
285 {
286 Plain,
287 Keyword,
288 String,
289 Comment,
290 Number,
291 Type,
292 }
293
294 private readonly record struct TextSpan(int Start, int Length, TextSpanKind Kind);
295
296 private readonly record struct HighlightRule(Regex Regex, TextSpanKind Kind);
297}
298
View only · write via MCP/CIDE