Code/Generation/SuiNameSanitizer.cs
using System.Linq;
using System.Text;
namespace SboxUiDesigner.Generation;
/// <summary>
/// Name sanitizers for the generator. Three flavours:
///
/// 1. <see cref="ToCssClass"/> — turn an arbitrary string into a CSS class name
/// (lowercase, hyphens for separators, no leading digit, no special chars).
/// 2. <see cref="ToCSharpIdentifier"/> — turn an arbitrary string into a valid
/// C# identifier (PascalCase, ASCII only, leading underscore for digits).
/// Used for the generated PanelComponent class name and namespaces.
/// 3. <see cref="EscapeRazorText"/> — escape text content so it's safe to
/// embed in Razor markup as plain text. Stops `@`, `<`, `&` etc.
/// from being interpreted as Razor directives or HTML.
/// </summary>
public static class SuiNameSanitizer
{
// ─────────────────────────────────────────────────────────────────────
// CSS class
// ─────────────────────────────────────────────────────────────────────
public static string ToCssClass( string raw )
{
if ( string.IsNullOrEmpty( raw ) ) return "_";
var sb = new StringBuilder( raw.Length );
foreach ( var ch in raw )
{
if ( char.IsLetterOrDigit( ch ) ) sb.Append( char.ToLowerInvariant( ch ) );
else if ( ch == '-' || ch == '_' ) sb.Append( '-' );
else if ( char.IsWhiteSpace( ch ) ) sb.Append( '-' );
}
var s = sb.ToString().Trim( '-' );
if ( s.Length == 0 ) return "_";
if ( !char.IsLetter( s[0] ) ) s = "x" + s;
// Collapse runs of '--' into a single '-'.
while ( s.Contains( "--" ) ) s = s.Replace( "--", "-" );
return s;
}
// ─────────────────────────────────────────────────────────────────────
// C# identifier
// ─────────────────────────────────────────────────────────────────────
public static string ToCSharpIdentifier( string raw )
{
if ( string.IsNullOrEmpty( raw ) ) return "_";
var sb = new StringBuilder( raw.Length );
var capitalizeNext = true;
foreach ( var ch in raw )
{
if ( char.IsLetterOrDigit( ch ) )
{
sb.Append( capitalizeNext ? char.ToUpperInvariant( ch ) : ch );
capitalizeNext = false;
}
else
{
// underscores, spaces, hyphens, etc. all act as word separators
capitalizeNext = sb.Length > 0;
}
}
var s = sb.ToString();
if ( s.Length == 0 ) return "_";
if ( char.IsDigit( s[0] ) ) s = "_" + s;
// Avoid colliding with C# reserved keywords by prefixing if needed.
if ( IsCSharpKeyword( s ) ) s = "@" + s;
return s;
}
private static bool IsCSharpKeyword( string s ) => s switch
{
"abstract" or "as" or "base" or "bool" or "break" or "byte" or "case"
or "catch" or "char" or "checked" or "class" or "const" or "continue"
or "decimal" or "default" or "delegate" or "do" or "double" or "else"
or "enum" or "event" or "explicit" or "extern" or "false" or "finally"
or "fixed" or "float" or "for" or "foreach" or "goto" or "if" or "implicit"
or "in" or "int" or "interface" or "internal" or "is" or "lock" or "long"
or "namespace" or "new" or "null" or "object" or "operator" or "out"
or "override" or "params" or "private" or "protected" or "public"
or "readonly" or "ref" or "return" or "sbyte" or "sealed" or "short"
or "sizeof" or "stackalloc" or "static" or "string" or "struct" or "switch"
or "this" or "throw" or "true" or "try" or "typeof" or "uint" or "ulong"
or "unchecked" or "unsafe" or "ushort" or "using" or "virtual" or "void"
or "volatile" or "while" => true,
_ => false,
};
// ─────────────────────────────────────────────────────────────────────
// Razor / HTML text escape
// ─────────────────────────────────────────────────────────────────────
/// <summary>
/// Escape arbitrary user text so it's safe to embed in Razor markup as a
/// plain text node. Replaces `@` (Razor directive) with `@`, `<` /
/// `&` so HTML parsers don't reinterpret it.
/// </summary>
public static string EscapeRazorText( string raw )
{
if ( string.IsNullOrEmpty( raw ) ) return "";
var sb = new StringBuilder( raw.Length + 8 );
foreach ( var ch in raw )
{
switch ( ch )
{
case '@': sb.Append( "@" ); break;
case '<': sb.Append( "<" ); break;
case '>': sb.Append( ">" ); break;
case '&': sb.Append( "&" ); break;
case '"': sb.Append( """ ); break;
default: sb.Append( ch ); break;
}
}
return sb.ToString();
}
}