// // // Selectors = [selector], [selector], [selector] // Selector = [rule] [rule] [rule] [rule] // rule = ELEMENT.Class.Class.Class:hover:and:stuff // namespace Sandbox.UI; internal static partial class StyleParser { /// /// Here we divide the selectors into groups /// .fucker, .cocks, .hairy /// public static List Selector( string rule_string, StyleBlock parent = null ) { var list = new List(); var p = new Parse( rule_string ); while ( !p.IsEnd ) { p = p.SkipWhitespaceAndNewlines(); if ( p.IsEnd ) break; if ( p.Current == ',' ) throw new System.Exception( $"Invalid Selector: {rule_string}" ); var group = p.ReadUntilOrEnd( "," ); group = group.Trim(); var ss = ParseSelector( group, parent ); if ( ss == null ) return null; list.Add( ss ); if ( !p.IsEnd && p.Current == ',' ) p.Pointer++; } return list; } public static StyleSelector ParseSelector( string rule_string, StyleBlock parent = null ) { var p = new Parse( rule_string ); StyleSelector lastRule = null; while ( !p.IsEnd ) { p = p.SkipWhitespaceAndNewlines(); if ( p.IsEnd ) break; bool immediateParent = false; if ( p.Is( '>' ) ) { p.Pointer++; p = p.SkipWhitespaceAndNewlines(); immediateParent = true; } var selector = p.ReadUntilWhitespaceOrNewlineOrEndAndObeyBrackets(); var rule = ParseSingleSelector( selector, parent ); if ( rule == null ) return null; rule.Parent = lastRule; rule.ImmediateParent = immediateParent; lastRule = rule; } if ( lastRule != null ) { lastRule.AsString = rule_string.Trim(); if ( parent != null ) { var parentRules = string.Join( ", ", parent.Selectors.Select( x => x.AsString ) ); if ( lastRule.AsString.StartsWith( '&' ) ) { lastRule.AsString = parentRules + lastRule.AsString.Substring( 1 ); } else { lastRule.AsString = parentRules + " " + lastRule.AsString; } } } return lastRule; } /// /// Parse a single rule, which as "panel.closed.error:hover" /// /// public static StyleSelector ParseSingleSelector( string rule_string, StyleBlock parent ) { var seperators = ".:"; var rule = new StyleSelector(); rule.AsString = rule_string.Trim(); var p = new Parse( rule_string ); p = p.SkipWhitespaceAndNewlines(); List ruleClasses = null; // // If our selector starts with & we need to match any of the parent block's selectors // if ( p.Current == '&' ) { p.Pointer++; if ( parent == null ) throw new System.Exception( $"Starts with & but has no parent block \"{rule_string}\"" ); rule.AnyOf = parent.Selectors; } else if ( p.Current == '>' ) { p.Pointer++; if ( parent == null ) throw new System.Exception( $"Starts with > but has no parent block \"{rule_string}\"" ); rule.DecendantOf = parent.Selectors; rule.ImmediateParent = true; } else if ( parent != null ) { // // If we have a parent block, our parent needs to conform to its rules // rule.DecendantOf = parent.Selectors; } while ( !p.IsEnd ) { // // Class // if ( p.Current == '.' ) { p.Pointer++; if ( p.IsEnd || p.IsOneOf( seperators ) ) throw new System.Exception( $"Invalid Rule \"{rule_string}\"" ); var classname = p.ReadUntilOrEnd( ".:#" ).ToLowerInvariant(); ruleClasses ??= new(); ruleClasses.Add( classname ); } else if ( p.Current == '#' ) { p.Pointer++; if ( p.IsEnd || p.IsOneOf( seperators ) ) throw new System.Exception( $"Invalid Rule \"{rule_string}\"" ); var id = p.ReadUntilOrEnd( ".:#" ).ToLower(); rule.Id = id; } else if ( p.Current == ':' ) { // there might be 2, skip them all while ( p.Current == ':' ) p.Pointer++; if ( p.IsEnd || p.IsOneOf( seperators ) ) throw new System.Exception( $"Invalid Rule \"{rule_string}\"" ); ReadPseudoClass( rule, ref p ); } else if ( p.Current == '*' ) { p.Pointer++; rule.UniversalSelector = true; if ( !p.IsEnd && !p.IsOneOf( seperators ) ) throw new System.Exception( $"Invalid Rule \"{rule_string}\"" ); } else { rule.Element = p.ReadUntilOrEnd( ".:#" ).ToLower(); } } if ( ruleClasses != null ) { rule.SetClasses( ruleClasses.ToArray() ); } return rule; } private static void ReadPseudoClass( StyleSelector rule, ref Parse p ) { if ( p.Is( "has(", 0, true ) ) { p.Pointer += 3; var inner = p.ReadInnerBrackets(); rule.Has = ParseHasSelectors( inner ); return; } if ( p.Is( "not(", 0, true ) ) { p.Pointer += 3; var inner = p.ReadInnerBrackets(); if ( string.IsNullOrEmpty( inner ) ) return; rule.Not = ParseSelector( inner ); return; } if ( p.Is( "nth-child(", 0, true ) ) { p.Pointer += "nth-child".Length; var inner = p.ReadInnerBrackets(); if ( string.IsNullOrEmpty( inner ) ) return; ParseNthChild( rule, inner.Trim() ); return; } var flagname = p.ReadUntilOrEnd( ".:" ).ToLower(); switch ( flagname ) { case "hover": rule.Flags |= PseudoClass.Hover; break; case "active": rule.Flags |= PseudoClass.Active; break; case "focus": rule.Flags |= PseudoClass.Focus; break; case "intro": rule.Flags |= PseudoClass.Intro; break; case "outro": rule.Flags |= PseudoClass.Outro; break; case "empty": rule.Flags |= PseudoClass.Empty; break; case "first-child": rule.Flags |= PseudoClass.FirstChild; break; case "last-child": rule.Flags |= PseudoClass.LastChild; break; case "only-child": rule.Flags |= PseudoClass.OnlyChild; break; case "before": rule.Flags |= PseudoClass.Before; break; case "after": rule.Flags |= PseudoClass.After; break; default: throw new System.Exception( $"Unsupported Pseudo Class \"{flagname}\"" ); } } private static void ParseNthChild( StyleSelector rule, string inner ) { if ( int.TryParse( inner, out int intValue ) ) { rule.NthChild = ( p ) => (p.SiblingIndex + 1) == intValue; return; } if ( string.Equals( inner, "odd", StringComparison.OrdinalIgnoreCase ) ) { rule.NthChild = ( p ) => p.SiblingIndex % 2 == 0; return; } if ( string.Equals( inner, "even", StringComparison.OrdinalIgnoreCase ) ) { rule.NthChild = ( p ) => p.SiblingIndex % 2 == 1; return; } throw new System.Exception( $"unsupported NthChild \"{inner}\"" ); } private static StyleSelector[] ParseHasSelectors( string inner ) { if ( string.IsNullOrWhiteSpace( inner ) ) return null; var selectors = new List(); foreach ( var part in inner.Split( ',' ) ) { var trimmed = part.Trim(); if ( string.IsNullOrEmpty( trimmed ) ) continue; var selector = ParseHasSelector( trimmed ); if ( selector == null ) continue; selectors.Add( selector ); } if ( selectors.Count == 0 ) return null; return selectors.ToArray(); } private static StyleSelector ParseHasSelector( string selectorString ) { if ( string.IsNullOrWhiteSpace( selectorString ) ) return null; var p = new Parse( selectorString ); p = p.SkipWhitespaceAndNewlines(); // Handle different combinator types at the start bool isDirectChild = false; bool isAdjacent = false; bool isGeneral = false; if ( !p.IsEnd && p.Current == '>' ) { isDirectChild = true; p.Pointer++; p = p.SkipWhitespaceAndNewlines(); } else if ( !p.IsEnd && p.Current == '+' ) { isAdjacent = true; p.Pointer++; p = p.SkipWhitespaceAndNewlines(); } else if ( !p.IsEnd && p.Current == '~' ) { isGeneral = true; p.Pointer++; p = p.SkipWhitespaceAndNewlines(); } if ( p.IsEnd ) return null; var remaining = p.ReadRemaining(); if ( string.IsNullOrWhiteSpace( remaining ) ) return null; var selector = ParseSingleSelector( remaining, null ); if ( selector != null ) { if ( isDirectChild ) selector.ImmediateParent = true; else if ( isAdjacent ) selector.AdjacentSibling = true; else if ( isGeneral ) selector.GeneralSibling = true; } return selector; } }