Summary Update.
This commit is contained in:
@@ -2,21 +2,50 @@ using System.Text.RegularExpressions;
|
||||
|
||||
namespace RelayClient.Helpers;
|
||||
|
||||
/// <summary>
|
||||
/// Discord-style syntax highlighting for ```lang...``` fenced code blocks. Builds a list of
|
||||
/// MAUI Spans (with colors from the VS Code Dark+ palette) that the caller drops into a
|
||||
/// FormattedString.
|
||||
///
|
||||
/// How it works:
|
||||
/// - The opening fence captures an optional language tag (e.g. ```cs, ```python).
|
||||
/// - Aliases resolves "cs" → "csharp", "js" → "javascript", etc.
|
||||
/// - Tokenizers[lang] is a compiled regex with named groups (comment/string/number/word/…).
|
||||
/// - For each match, SpanForMatch picks a colour based on which group matched + whether
|
||||
/// a "word" hit a language keyword set.
|
||||
///
|
||||
/// Adding a new language: register an alias (if needed), a Keywords set, and a tokenizer regex.
|
||||
/// </summary>
|
||||
public static class SyntaxHighlighter
|
||||
{
|
||||
/// <summary>Fallback identifier color (light grey). Used for any token we don't recognise.</summary>
|
||||
private static readonly Color DefaultColor = Color.FromArgb("#D4D4D4");
|
||||
/// <summary>Language keywords (if, for, return, etc.) — VS Code's "control flow" blue.</summary>
|
||||
private static readonly Color KeywordColor = Color.FromArgb("#569CD6");
|
||||
/// <summary>String literals — orange/salmon.</summary>
|
||||
private static readonly Color StringColor = Color.FromArgb("#CE9178");
|
||||
/// <summary>Numeric literals — soft green.</summary>
|
||||
private static readonly Color NumberColor = Color.FromArgb("#B5CEA8");
|
||||
/// <summary>Comments — green, rendered italic.</summary>
|
||||
private static readonly Color CommentColor = Color.FromArgb("#6A9955");
|
||||
/// <summary>Type names (heuristic: uppercase-start words in C#/JS/TS) — teal.</summary>
|
||||
private static readonly Color TypeColor = Color.FromArgb("#4EC9B0");
|
||||
/// <summary>Function names — yellow. Currently unused (we don't disambiguate function calls).</summary>
|
||||
private static readonly Color FunctionColor = Color.FromArgb("#DCDCAA");
|
||||
/// <summary>Operators — same as default. Reserved for future use.</summary>
|
||||
private static readonly Color OperatorColor = Color.FromArgb("#D4D4D4");
|
||||
/// <summary>HTML tag names (<div>, </p>) — blue.</summary>
|
||||
private static readonly Color TagColor = Color.FromArgb("#569CD6");
|
||||
/// <summary>HTML/CSS attribute names, YAML keys, bash variables — light blue.</summary>
|
||||
private static readonly Color AttrColor = Color.FromArgb("#9CDCFE");
|
||||
|
||||
/// <summary>Monospace font registered in MauiProgram. Used for all code-block spans.</summary>
|
||||
private const string FontFamily = "AnonymousProRegular";
|
||||
|
||||
/// <summary>
|
||||
/// Short language tags → canonical names. So users can write ```cs (instead of ```csharp),
|
||||
/// ```py instead of ```python, etc. Case-insensitive.
|
||||
/// </summary>
|
||||
private static readonly Dictionary<string, string> Aliases = new(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
["cs"] = "csharp",
|
||||
@@ -34,6 +63,11 @@ public static class SyntaxHighlighter
|
||||
["yml"] = "yaml"
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Per-language keyword sets. A token in a "word" match-group that hits one of these
|
||||
/// gets rendered with KeywordColor. Case-sensitivity matches the language — Ordinal
|
||||
/// for most languages, OrdinalIgnoreCase for SQL and CSS.
|
||||
/// </summary>
|
||||
private static readonly Dictionary<string, HashSet<string>> Keywords = new(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
["csharp"] = new(StringComparer.Ordinal)
|
||||
@@ -97,6 +131,11 @@ public static class SyntaxHighlighter
|
||||
}
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Per-language compiled token regex. Each pattern uses named groups (comment/string/
|
||||
/// number/word/tag/attr/…) which SpanForMatch dispatches on. Initialised lazily in the
|
||||
/// static constructor so the heavy regex compilation is paid once at startup.
|
||||
/// </summary>
|
||||
private static readonly Dictionary<string, Regex> Tokenizers = new(StringComparer.Ordinal);
|
||||
|
||||
static SyntaxHighlighter()
|
||||
@@ -186,6 +225,11 @@ public static class SyntaxHighlighter
|
||||
opts | RegexOptions.Multiline);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Entry point. Walks every regex match in the code, emits plain spans for the gaps and
|
||||
/// styled spans for the matches. If the language is unknown (or not specified), returns a
|
||||
/// single default-colored span — code still renders in the monospace font, just no colors.
|
||||
/// </summary>
|
||||
public static List<Span> Highlight(string code, string? language, double fontSize)
|
||||
{
|
||||
var lang = Resolve(language);
|
||||
@@ -215,6 +259,11 @@ public static class SyntaxHighlighter
|
||||
return spans;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Maps a regex Match to a colored Span by inspecting which named group succeeded. Words
|
||||
/// fall through to a keyword-set lookup; in C#/JS/TS, uppercase-start words that aren't
|
||||
/// keywords are treated as type names (a cheap heuristic that works surprisingly well).
|
||||
/// </summary>
|
||||
private static Span SpanForMatch(Match m, string lang, HashSet<string>? keywords, double fontSize)
|
||||
{
|
||||
if (m.Groups["comment"].Success)
|
||||
@@ -285,6 +334,7 @@ public static class SyntaxHighlighter
|
||||
return MakeSpan(m.Value, DefaultColor, fontSize);
|
||||
}
|
||||
|
||||
/// <summary>Helper: build a Span with the monospace code font and the given colour + bold/italic flags.</summary>
|
||||
private static Span MakeSpan(string text, Color color, double fontSize, bool bold = false, bool italic = false)
|
||||
{
|
||||
var attrs = FontAttributes.None;
|
||||
@@ -301,6 +351,7 @@ public static class SyntaxHighlighter
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>Normalises a user-supplied language tag through the Aliases table. Returns null for empty/whitespace input.</summary>
|
||||
private static string? Resolve(string? language)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(language)) return null;
|
||||
|
||||
Reference in New Issue
Block a user