Summary Update.

This commit is contained in:
2026-06-06 23:38:50 -04:00
parent dd75ca4b06
commit 2916d17868
30 changed files with 1231 additions and 21 deletions

View File

@@ -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 (&lt;div&gt;, &lt;/p&gt;) — 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;