Files
sbox-public/engine/Sandbox.System/Utility/Parse.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

654 lines
12 KiB
C#

using System.Globalization;
namespace Sandbox
{
/// <summary>
/// A lightweight string parser with cursor-based navigation.
/// Designed for parsing text files, CSS, and other structured text formats.
/// Uses ref struct to stay stack-allocated for performance.
/// </summary>
internal ref struct Parse
{
/// <summary>Source file name for error reporting</summary>
public string FileName;
/// <summary>The text being parsed</summary>
public string Text;
/// <summary>Current position in the text</summary>
public int Pointer;
int lineOffset;
public Parse( string value, string filename = "nofile", int lineOffset = 0 ) : this()
{
FileName = filename;
Text = value;
this.lineOffset = lineOffset;
}
public int Length => Text.Length;
public bool IsEnd => Pointer >= Length;
public char Current => IsEnd ? '\0' : Text[Pointer];
public char Next => Pointer + 1 >= Length ? '\0' : Text[Pointer + 1];
public bool IsWhitespace => char.IsWhiteSpace( Current );
public bool IsNewline => Current == '\n' || Current == '\r';
public bool IsDigit => char.IsDigit( Current );
public bool IsLetter => char.IsLetter( Current );
public Parse JumpToEndOfLine( bool afterNewline )
{
var p = this;
while ( !p.IsEnd && !p.IsNewline )
{
p.Pointer++;
}
if ( afterNewline )
{
while ( !p.IsEnd && p.IsNewline )
{
p.Pointer++;
}
}
return p;
}
public bool IsOneOf( string chars )
{
if ( chars == null )
return false;
return chars.IndexOf( Current ) >= 0;
}
public string Read( int chars )
{
if ( chars <= 0 ) throw new System.Exception( $"Tried to read {chars} chars" );
var result = Text.Substring( Pointer, chars );
Pointer += chars;
return result;
}
public string ReadRemaining( bool acceptNone = false )
{
if ( IsEnd && acceptNone ) return string.Empty;
if ( IsEnd ) throw new System.Exception( $"Tried to ReadRemaining but we're at the end" );
var result = Text.Substring( Pointer );
Pointer = Length;
return result;
}
public Parse SkipWhitespaceAndNewlines( string andCharacters = null )
{
while ( !IsEnd )
{
if ( !IsWhitespace && !IsNewline && !IsOneOf( andCharacters ) )
return this;
Pointer++;
}
return this;
}
public string ReadUntilWhitespaceOrNewlineOrEnd( string andCharacters = null )
{
var p = this;
while ( true )
{
if ( p.IsEnd || p.IsNewline || p.IsWhitespace || IsOneOf( andCharacters ) )
return this.Read( p.Pointer - Pointer );
p.Pointer++;
}
}
public string ReadUntilWhitespaceOrNewlineOrEndAndObeyBrackets()
{
var p = this;
int inside = 0;
while ( true )
{
var lineEnder = p.IsNewline || p.IsWhitespace;
if ( inside > 0 ) lineEnder = false;
if ( p.IsEnd || lineEnder )
return this.Read( p.Pointer - Pointer );
if ( p.Is( '(' ) ) inside++;
if ( p.Is( ')' ) ) inside--;
p.Pointer++;
}
}
public string ReadInnerBrackets( char inner = '(', char outer = ')' )
{
int inside = 0;
int iStart = Pointer;
int iEnd;
while ( true )
{
if ( IsEnd )
return null;
if ( Is( inner ) )
{
if ( inside == 0 ) iStart = Pointer + 1;
inside++;
}
if ( Is( outer ) )
{
inside--;
if ( inside == 0 )
{
iEnd = Pointer;
Pointer++;
break;
}
}
Pointer++;
}
return Text.Substring( iStart, iEnd - iStart );
}
public string ReadWord( string endOnCharacter = null, bool readUntilEnd = false )
{
var p = this;
while ( true )
{
if ( p.IsEnd && !readUntilEnd )
return null;
if ( p.IsEnd || p.IsWhitespace || p.IsNewline || p.IsOneOf( endOnCharacter ) )
return this.Read( p.Pointer - Pointer );
p.Pointer++;
}
}
public string ReadChars( string chars = null, bool readUntilEnd = false )
{
var p = this;
while ( true )
{
if ( p.IsEnd && !readUntilEnd )
return null;
if ( p.IsEnd || !p.IsOneOf( chars ) )
{
if ( p.Pointer == Pointer ) return null;
return this.Read( p.Pointer - Pointer );
}
p.Pointer++;
}
}
/// <summary>
/// Reads a sentence until the next statement divided by ,
/// Returns the sentence
/// </summary>
public string ReadSentence()
{
var p = this;
while ( !p.Is( "," ) && !p.IsEnd )
{
if ( p.Is( "(" ) )
{
while ( !p.Is( ")" ) && !p.IsEnd )
{
p.Pointer++;
}
}
else
p.Pointer++;
}
return this.Read( p.Pointer - Pointer );
}
public string ReadUntil( string c1 )
{
var p = this;
while ( !p.IsEnd )
{
if ( p.IsOneOf( c1 ) )
return this.Read( p.Pointer - Pointer );
p.Pointer++;
}
return null;
}
public string ReadUntilOrEnd( string c1, bool acceptNone = false )
{
var p = this;
while ( !p.IsEnd )
{
if ( p.IsOneOf( c1 ) )
{
if ( p.Pointer == Pointer )
return string.Empty;
return this.Read( p.Pointer - Pointer );
}
p.Pointer++;
}
return this.ReadRemaining( acceptNone );
}
public (string, string) ReadKeyValue()
{
var key = ReadUntilOrEnd( ":" );
if ( string.IsNullOrWhiteSpace( key ) ) throw new System.Exception( $"Expected key {FileAndLine}" );
Pointer++;
if ( IsEnd ) throw new System.Exception( $"Expected value {FileAndLine}" );
var value = ReadUntilOrEnd( ";" );
if ( string.IsNullOrWhiteSpace( value ) ) throw new System.Exception( $"Expected value {FileAndLine}" );
Pointer++;
return (key.Trim(), value.Trim());
}
public bool TryReadTime( out float val )
{
val = 0;
var p = this;
p = p.SkipWhitespaceAndNewlines();
var numStart = p.Pointer;
while ( !p.IsEnd )
{
if ( p.IsDigit || p.Current == '.' )
{
p.Pointer++;
continue;
}
if ( p.Current == 's' || p.Current == 'S' )
{
var len = p.Pointer - numStart;
var str = p.Text.Substring( numStart, len );
if ( !float.TryParse( str, NumberStyles.Float, CultureInfo.InvariantCulture, out var parsed ) )
return false;
Pointer = p.Pointer + 1;
val = parsed * 1000.0f;
return true;
}
if ( p.Current == 'm' || p.Current == 'M' )
{
if ( p.Next != 's' && p.Next != 'S' )
return false;
var len = p.Pointer - numStart;
var str = p.Text.Substring( numStart, len );
if ( !float.TryParse( str, NumberStyles.Float, CultureInfo.InvariantCulture, out var parsed ) )
return false;
Pointer = p.Pointer + 2;
val = parsed;
return true;
}
return false;
}
return false;
}
internal bool TryReadLength( out Sandbox.UI.Length outval )
{
outval = 0;
var p = this;
if ( p.IsEnd ) return false;
p = p.SkipWhitespaceAndNewlines();
if ( p.IsEnd ) return false;
var numStart = p.Pointer;
var w = p.ReadWord( ")", true );
if ( w == null ) return false;
var v = Sandbox.UI.Length.Parse( w );
if ( !v.HasValue ) return false;
outval = v.Value;
Pointer = p.Pointer;
return true;
}
internal bool TryReadRepeat( out string outval )
{
outval = "";
var p = this;
p = p.SkipWhitespaceAndNewlines();
if ( !p.IsLetter )
return false;
var w = p.ReadWord( null, true );
switch ( w )
{
case "no-repeat":
case "repeat-x":
case "repeat-y":
case "repeat":
case "clamp":
outval = w;
break;
default:
return false;
}
Pointer += w.Length;
return true;
}
internal bool TryReadMaskMode( out string outval )
{
outval = "";
var p = this;
p = p.SkipWhitespaceAndNewlines();
if ( !p.IsLetter )
return false;
var w = p.ReadWord( null, true );
switch ( w )
{
case "match-source":
case "alpha":
case "luminance":
outval = w;
break;
default:
return false;
}
Pointer += w.Length;
return true;
}
internal bool TryReadLineStyle( out string outval )
{
outval = "";
var p = this;
p = p.SkipWhitespaceAndNewlines();
if ( !p.IsLetter )
return false;
var w = p.ReadWord( null, true );
switch ( w )
{
case "none":
case "solid":
// case "double":
// case "dotted":
// case "dashed":
// case "inset":
// case "outset":
// case "ridge":
// case "groove":
// case "hidden":
outval = w;
break;
default:
return false;
}
Pointer += w.Length;
return true;
}
public bool TryReadFloat( out float outval )
{
outval = 0;
var p = this;
if ( p.IsEnd ) return false;
p = p.SkipWhitespaceAndNewlines();
if ( p.IsEnd ) return false;
var w = p.ReadChars( "-0123456789.Ee", true );
if ( w == null )
return false;
if ( !float.TryParse( w, NumberStyles.Float, CultureInfo.InvariantCulture, out outval ) )
return false;
Pointer += w.Length;
// if it ends in f, skip it
if ( Current == 'f' )
Pointer++;
return true;
}
internal bool TryReadColor( out Color outval )
{
outval = default;
var p = this;
if ( p.IsEnd ) return false;
p = p.SkipWhitespaceAndNewlines();
if ( p.IsEnd ) return false;
int inside = 0;
var numStart = p.Pointer;
while ( !p.IsEnd )
{
if ( p.Current == '(' )
inside++;
if ( p.Current == ')' )
inside--;
if ( inside < 0 )
return false;
if ( inside == 0 && p.IsOneOf( " ;\t\n\r," ) )
break;
p.Pointer++;
}
if ( numStart == p.Pointer )
return false;
var c = p.Text.Substring( numStart, p.Pointer - numStart );
var color = Color.Parse( c );
if ( !color.HasValue ) return false;
Pointer = p.Pointer;
outval = color.Value;
return true;
}
/// <summary>
/// <para>
/// Typically used to parse shorthand position &amp; size combinations, like those seen inside
/// mask and background shorthands.
/// <code>&lt;position&gt; [ / &lt;size&gt; ]</code>
/// </para>
/// </summary>
internal bool TryReadPositionAndSize( out Sandbox.UI.Length positionX, out Sandbox.UI.Length positionY, out Sandbox.UI.Length sizeX, out Sandbox.UI.Length sizeY )
{
// Initial values
positionX = 0;
positionY = 0;
sizeX = UI.Length.Auto;
sizeY = UI.Length.Auto;
//
// <position>
//
if ( TryReadLength( out positionX ) )
{
if ( !TryReadLength( out positionY ) )
positionY = positionX;
SkipWhitespaceAndNewlines();
//
// [ / <size> ]?
//
if ( TrySkip( "/" ) )
{
// We have a size
if ( !TryReadLength( out sizeX ) )
return false; // Invalid - expected a length
if ( !TryReadLength( out sizeY ) )
return true; // We don't require a length here
}
SkipWhitespaceAndNewlines();
return true;
}
return false;
}
internal bool TryReadShadowInset( out bool isInset )
{
isInset = false;
var p = this;
p = p.SkipWhitespaceAndNewlines();
if ( !p.IsLetter )
return false;
var w = p.ReadWord( null, true );
switch ( w )
{
case "inset":
isInset = true;
break;
default:
return false;
}
Pointer += w.Length;
return true;
}
/// <summary>
/// Return true if the string at the pointer is this
/// </summary>
public bool Is( string v, int offset = 0, bool ignorecase = false )
{
var len = v.Length;
for ( int i = 0; i < len; i++ )
{
if ( !Is( v[i], i, ignorecase ) )
return false;
}
return true;
}
/// <summary>
/// Skip this string if it exists
/// </summary>
public bool TrySkip( string v, int offset = 0, bool ignorecase = false )
{
var len = v.Length;
for ( int i = 0; i < len; i++ )
{
if ( !Is( v[i], i, ignorecase ) )
return false;
}
Pointer += len;
return true;
}
/// <summary>
/// Skip comma and then possible whitespace
/// </summary>
public bool TrySkipCommaSeparation()
{
SkipWhitespaceAndNewlines();
if ( Current != ',' ) return false;
Read( 1 );
SkipWhitespaceAndNewlines();
return true;
}
/// <summary>
/// Return true if the char at the pointer is this
/// </summary>
public bool Is( char v, int offset = 0, bool ignorecase = false )
{
var ptr = Pointer + offset;
if ( ptr >= Length ) return false;
if ( ptr < 0 ) return false;
if ( ignorecase )
return char.ToLowerInvariant( Text[ptr] ) == char.ToLowerInvariant( v );
return Text[ptr] == v;
}
/// <summary>
/// Get the line we're currently on
/// </summary>
public int CurrentLine
{
get
{
var substr = Text.Substring( 0, Math.Min( Pointer, Text.Length ) );
var lines = substr.Count( x => x == '\n' );
return lines + lineOffset;
}
}
public string FileAndLine => $"[{FileName}:{CurrentLine}]";
}
}