Code/TailBox/Domain/Candidates/TailBoxCandidateParser.cs
using System;
using System.Collections.Generic;
using System.Linq;
namespace Sandbox.TailBox;
internal static class TailBoxCandidateParser
{
private static readonly Dictionary<string, string> PseudoVariants = new( StringComparer.Ordinal )
{
["hover"] = ":hover",
["active"] = ":active",
["focus"] = ":focus",
["intro"] = ":intro",
["outro"] = ":outro"
};
private static readonly HashSet<string> BrowserSelectorVariants = new( StringComparer.Ordinal )
{
"first", "last", "only", "odd", "even", "first-of-type", "last-of-type", "only-of-type",
"visited", "target", "open", "default", "checked", "indeterminate", "placeholder-shown",
"autofill", "optional", "required", "valid", "invalid", "user-valid", "user-invalid",
"in-range", "out-of-range", "read-only", "empty", "focus-within", "focus-visible",
"enabled", "disabled", "inert", "group", "peer", "has", "not", "aria", "data",
"nth", "nth-last", "nth-of-type", "nth-last-of-type", "*", "**", "before", "after",
"placeholder", "selection", "marker", "file", "backdrop", "first-letter", "first-line"
};
public static bool TryParse( string className, out TailBoxCandidate candidate, out TailBoxCompileDiagnostic skipped )
{
candidate = null;
skipped = null;
if ( string.IsNullOrWhiteSpace( className ) )
{
skipped = Skip( className, TailBoxSkipReason.InvalidCandidate, "Class name is empty." );
return false;
}
var segments = TailBoxText.Segment( className.Trim(), ':' ).Where( x => x.Length > 0 ).ToList();
if ( segments.Count == 0 )
{
skipped = Skip( className, TailBoxSkipReason.InvalidCandidate, "Class name did not contain a utility segment." );
return false;
}
var baseSegment = segments[^1];
var important = false;
if ( baseSegment.StartsWith( "!", StringComparison.Ordinal ) )
{
important = true;
baseSegment = baseSegment[1..];
}
if ( baseSegment.EndsWith( "!", StringComparison.Ordinal ) )
{
important = true;
baseSegment = baseSegment[..^1];
}
var negative = false;
if ( baseSegment.StartsWith( "-", StringComparison.Ordinal ) )
{
negative = true;
baseSegment = baseSegment[1..];
}
if ( string.IsNullOrWhiteSpace( baseSegment ) )
{
skipped = Skip( className, TailBoxSkipReason.InvalidCandidate, "Utility segment is empty after flags were parsed." );
return false;
}
var result = new TailBoxCandidate
{
Original = className,
Negative = negative,
Important = important
};
for ( var i = 0; i < segments.Count - 1; i++ )
{
result.Variants.Add( ParseVariant( segments[i] ) );
}
if ( baseSegment.StartsWith( "[", StringComparison.Ordinal ) && baseSegment.EndsWith( "]", StringComparison.Ordinal ) )
{
var propertyBody = baseSegment[1..^1];
var propertySegments = TailBoxText.Segment( propertyBody, ':' );
if ( propertySegments.Count < 2 )
{
skipped = Skip( className, TailBoxSkipReason.InvalidCandidate, "Arbitrary property must look like [property:value]." );
return false;
}
result.IsArbitraryProperty = true;
result.ArbitraryProperty = propertySegments[0];
result.ArbitraryValue = TailBoxText.DecodeArbitraryValue( string.Join( ":", propertySegments.Skip( 1 ) ) );
result.Base = baseSegment;
candidate = result;
return true;
}
var modifierSegments = TailBoxText.Segment( baseSegment, '/' );
if ( modifierSegments.Count > 2 )
{
skipped = Skip( className, TailBoxSkipReason.InvalidCandidate, "Utility contains more than one modifier separator." );
return false;
}
if ( modifierSegments.Count == 2 && !LooksLikeFractionUtility( modifierSegments[0], modifierSegments[1] ) )
{
result.Base = modifierSegments[0];
result.Modifier = DecodeModifier( modifierSegments[1] );
}
else
{
result.Base = baseSegment;
result.Modifier = null;
}
candidate = result;
return true;
}
private static TailBoxVariant ParseVariant( string raw )
{
if ( PseudoVariants.TryGetValue( raw, out var pseudo ) )
{
return new TailBoxVariant
{
Raw = raw,
Kind = TailBoxVariantKind.Pseudo,
SelectorSuffix = pseudo
};
}
if ( raw.StartsWith( "[", StringComparison.Ordinal ) && raw.EndsWith( "]", StringComparison.Ordinal ) )
{
return new TailBoxVariant
{
Raw = raw,
Kind = TailBoxVariantKind.Selector,
Detail = "Arbitrary selector variants are not emitted because nested selector support must be verified in s&box."
};
}
var root = raw;
var dash = root.IndexOf( '-', StringComparison.Ordinal );
if ( dash > 0 )
root = root[..dash];
if ( BrowserSelectorVariants.Contains( raw ) || BrowserSelectorVariants.Contains( root ) )
{
return new TailBoxVariant
{
Raw = raw,
Kind = TailBoxVariantKind.Selector,
Detail = $"Selector variant '{raw}' is browser-oriented and has not been verified against s&box."
};
}
return new TailBoxVariant
{
Raw = raw,
Kind = TailBoxVariantKind.Unsupported,
Detail = $"Unsupported TailBox variant '{raw}'."
};
}
private static string DecodeModifier( string modifier )
{
if ( modifier.StartsWith( "[", StringComparison.Ordinal ) && modifier.EndsWith( "]", StringComparison.Ordinal ) )
return TailBoxText.DecodeArbitraryValue( modifier[1..^1] );
return modifier;
}
private static bool LooksLikeFractionUtility( string valueBeforeSlash, string valueAfterSlash )
{
if ( string.IsNullOrWhiteSpace( valueBeforeSlash ) || string.IsNullOrWhiteSpace( valueAfterSlash ) )
return false;
var lastDash = valueBeforeSlash.LastIndexOf( "-", StringComparison.Ordinal );
if ( lastDash < 0 || lastDash == valueBeforeSlash.Length - 1 )
return false;
var numerator = valueBeforeSlash[(lastDash + 1)..];
return int.TryParse( numerator, out _ ) && int.TryParse( valueAfterSlash, out _ );
}
private static TailBoxCompileDiagnostic Skip( string className, TailBoxSkipReason reason, string detail )
{
return new TailBoxCompileDiagnostic
{
ClassName = className ?? "",
Reason = reason,
Detail = detail
};
}
}