Files
sbox-public/engine/Sandbox.Engine/Systems/UI/Parser/StyleParser.Sheet.cs
s&box team 71f266059a Open source release
This commit imports the C# engine code and game files, excluding C++ source code.

[Source-Commit: ceb3d758046e50faa6258bc3b658a30c97743268]
2025-11-24 09:05:18 +00:00

229 lines
5.6 KiB
C#

using Sandbox.Engine;
namespace Sandbox.UI;
internal static partial class StyleParser
{
[ThreadStatic]
static int IncludeLoops = 0;
public static StyleSheet ParseSheet( string content, string filename = "none", IEnumerable<(string, string)> variables = null )
{
IncludeLoops = 0;
StyleSheet sheet = new();
sheet.AddVariables( variables );
ParseToSheet( content, filename, sheet );
return sheet;
}
private static void ParseToSheet( string content, string filename, StyleSheet sheet )
{
IncludeLoops++;
filename ??= "none";
filename = filename.NormalizeFilename();
sheet.AddFilename( filename );
content = StripComments( content );
var p = new Parse( content, filename );
while ( !p.IsEnd )
{
p = p.SkipWhitespaceAndNewlines();
if ( p.IsEnd )
break;
if ( ParseVariable( ref p, sheet ) )
continue;
if ( ParseKeyframes( ref p, sheet ) )
continue;
if ( ParseImport( ref p, sheet, filename ) )
continue;
var selector = p.ReadUntilOrEnd( "{;$@" );
if ( selector is null )
throw new System.Exception( $"Parse Error, expected class name {p.FileAndLine}" );
if ( p.IsEnd ) throw new System.Exception( $"Parse Error, unexpected end {p.FileAndLine}" );
if ( p.Current != '{' ) throw new System.Exception( $"Parse Error, unexpected character {p.Current} {p.FileAndLine}" );
if ( p.Current == '{' )
{
ReadStyleBlock( ref p, selector, sheet, null );
}
}
IncludeLoops--;
}
private static bool ParseVariable( ref Parse p, StyleSheet sheet )
{
if ( p.Current != '$' )
return false;
// We want the key with the $
(string key, string value) = p.ReadKeyValue();
bool isDefault = value.EndsWith( "!default", StringComparison.OrdinalIgnoreCase );
if ( isDefault )
{
value = value[..^8].Trim();
}
// Console.WriteLine( $"Found [{key}] = [{value}] ({isDefault})" );
sheet.SetVariable( key, value, isDefault );
return true;
}
private static void TryImport( StyleSheet sheet, string filename, string includeFileAndLine )
{
if ( !GlobalContext.Current.FileMount.FileExists( filename ) )
throw new System.Exception( $"Missing import {filename} ({includeFileAndLine})" );
var text = GlobalContext.Current.FileMount.ReadAllText( filename );
ParseToSheet( text, filename, sheet );
}
private static bool ParseImport( ref Parse p, StyleSheet sheet, string filename )
{
if ( p.Current != '@' )
return false;
var word = p.ReadWord( " ", true );
if ( string.IsNullOrWhiteSpace( word ) )
throw new System.Exception( $"Expected word after @ {p.FileAndLine}" );
if ( word == "@import" )
{
if ( IncludeLoops > 10 )
throw new System.Exception( $"Possible infinite @import loop {p.FileAndLine}" );
var thisRoot = System.IO.Path.GetDirectoryName( filename );
var files = p.ReadUntilOrEnd( ";" );
if ( string.IsNullOrWhiteSpace( files ) )
throw new System.Exception( $"Expected files then ; after @import {p.FileAndLine}" );
// files could be
// 1. "file", "file", "file"
// 2. "file"
// 3. 'file'
foreach ( var file in files.Split( ',', StringSplitOptions.RemoveEmptyEntries ) )
{
var cleanFile = file.Trim( ' ', '\"', '\'' );
if ( cleanFile.StartsWith( "./" ) ) cleanFile = cleanFile.Substring( 2 );
while ( cleanFile.StartsWith( "../" ) || cleanFile.StartsWith( "..\\" ) )
{
thisRoot = System.IO.Path.GetDirectoryName( thisRoot );
cleanFile = cleanFile.Substring( 3 );
}
// if no extension clean it up as an include
if ( !System.IO.Path.HasExtension( cleanFile ) ) cleanFile = $"_{cleanFile}.scss";
// try to find file in local directory, if not found then fall back
var localPath = System.IO.Path.Combine( thisRoot, cleanFile ).ToLower();
if ( !GlobalContext.Current.FileMount.FileExists( localPath ) )
{
localPath = cleanFile.ToLower();
}
TryImport( sheet, localPath, p.FileAndLine );
}
if ( p.Is( ';' ) )
p.Pointer++;
return true;
}
throw new System.Exception( $"Unknown rule {word} {p.FileAndLine}" );
}
private static bool ParseKeyframes( ref Parse p, StyleSheet sheet )
{
var keyframe = KeyFrames.Parse( ref p );
if ( keyframe == null )
return false;
sheet.AddKeyFrames( keyframe );
return true;
}
static void ReadStyleBlock( ref Parse p, string selectors, StyleSheet sheet, StyleBlock parentNode )
{
if ( p.Current != '{' )
throw new System.Exception( $"Block doesn't start with {{ {p.FileAndLine}" );
p.Pointer++;
p = p.SkipWhitespaceAndNewlines();
var node = new StyleBlock();
node.LoadOrder = sheet.Nodes.Count();
node.FileName = p.FileName;
node.AbsolutePath = GlobalContext.Current.FileMount?.GetFullPath( p.FileName );
node.FileLine = p.CurrentLine;
node.SetSelector( selectors, parentNode );
var styles = new Styles();
while ( !p.IsEnd )
{
var content = p.ReadUntilOrEnd( ";{}" );
if ( content is null ) throw new System.Exception( $"Parse Error, expected class name {p.FileAndLine}" );
if ( p.Current == '{' )
{
ReadStyleBlock( ref p, content, sheet, node );
continue;
}
if ( p.Current == ';' )
{
try
{
content = sheet.ReplaceVariables( content );
}
catch ( System.Exception e )
{
throw new System.Exception( $"{e.Message} {p.FileAndLine}" );
}
styles.SetInternal( content, p.FileName, p.CurrentLine );
p.Pointer++;
p = p.SkipWhitespaceAndNewlines();
}
if ( p.Current == '}' )
{
p.Pointer++;
node.Styles = styles;
// Only add this node if it's not empty
if ( !node.IsEmpty )
{
sheet.Nodes.Add( node );
}
return;
}
}
throw new System.Exception( $"Unexpected end of block {p.FileAndLine}" );
}
}