@@ -56,6 +56,7 @@ private enum State : byte
5656 { PseudoClassNames . Has , ctx => new HasFunctionState ( ctx ) } ,
5757 { PseudoClassNames . Matches , ctx => new MatchesFunctionState ( ctx , PseudoClassNames . Matches ) } ,
5858 { PseudoClassNames . Is , ctx => new MatchesFunctionState ( ctx , PseudoClassNames . Is ) } ,
59+ { PseudoClassNames . Where , ctx => new MatchesFunctionState ( ctx , PseudoClassNames . Where ) } ,
5960 { PseudoClassNames . HostContext , ctx => new HostContextFunctionState ( ctx ) }
6061 } ;
6162
@@ -375,7 +376,7 @@ private void Insert(ISelector selector)
375376
376377 var combinator = GetCombinator ( ) ;
377378 _complex . AppendSelector ( _temp , combinator ) ;
378- _temp = selector ;
379+ _temp = WrapPseudoElement ( selector ) ;
379380 }
380381 }
381382 else
@@ -385,6 +386,27 @@ private void Insert(ISelector selector)
385386 }
386387 }
387388
389+ // CssData.DoesSelectorMatch(CompoundSelector, box) is the only place that synthesizes
390+ // ::before/::after (and ::marker) child boxes as a match side effect, and it only runs for a
391+ // pseudo-element that's the last member of a CompoundSelector. A pseudo-element preceded by a
392+ // combinator (e.g. "E :after", with whitespace) would otherwise become a bare, unwrapped _temp
393+ // here - which CssData's ComplexSelector subject-match dispatches straight to the plain
394+ // DoesSelectorMatch(PseudoElementSelector, box) overload (which only checks whether the flag is
395+ // already set, never sets it), so the pseudo-element box would never actually get created.
396+ // Wrapping it in a single-member CompoundSelector as soon as it becomes a standalone selector
397+ // routes it through the one, already-correct synthesis path instead of needing a second
398+ // implementation of the same logic in CssData.
399+ //
400+ // Deliberately NOT applied in the other "_temp == null" branch above (a pseudo-element with no
401+ // combinator at all, i.e. it's the entire selector - "*:before" written as bare ":before"): that
402+ // shape has no ancestor restriction, so synthesizing on every match would mean synthesizing on
403+ // literally every element in the document. The UA stylesheet's own
404+ // ":before, :after { white-space: pre-line }" rule (CssDefaults.cs) is exactly this shape and
405+ // relies on the existing non-synthesizing behavior - it exists to style already-synthesized
406+ // pseudo-element boxes, not to create one on every element.
407+ private static ISelector WrapPseudoElement ( ISelector selector ) =>
408+ selector is PseudoElementSelector ? new CompoundSelector { selector } : selector ;
409+
388410 private Combinator GetCombinator ( )
389411 {
390412 while ( _combinators . Count > 1 && _combinators . Peek ( ) == Combinator . Descendent ) _combinators . Pop ( ) ;
@@ -478,7 +500,6 @@ private ISelector GetPseudoFunction(FunctionToken arguments)
478500 }
479501
480502 return null ;
481-
482503 }
483504
484505 private SelectorConstructor CreateChild ( )
@@ -682,7 +703,7 @@ private sealed class ContainsFunctionState : FunctionState
682703
683704 protected override bool OnToken ( Token token )
684705 {
685- if ( token . Type == TokenType . Ident || token . Type == TokenType . String )
706+ if ( ( token . Type == TokenType . Ident || token . Type == TokenType . String ) )
686707 {
687708 _value = token . Data ;
688709 }
@@ -787,16 +808,12 @@ protected override bool OnToken(Token token)
787808 {
788809 switch ( _state )
789810 {
790- case ParseState . Initial :
791- return OnInitial ( token ) ;
792- case ParseState . AfterInitialSign :
793- return OnAfterInitialSign ( token ) ;
794- case ParseState . Offset :
795- return OnOffset ( token ) ;
796- case ParseState . BeforeOf :
797- return OnBeforeOf ( token ) ;
798- default :
799- return OnAfter ( token ) ;
811+ case ParseState . Initial : return OnInitial ( token ) ;
812+ case ParseState . AfterInitialSign : return OnAfterInitialSign ( token ) ;
813+ case ParseState . Offset : return OnOffset ( token ) ;
814+ case ParseState . AfterOffsetSign : return OnAfterOffsetSign ( token ) ;
815+ case ParseState . BeforeOf : return OnBeforeOf ( token ) ;
816+ default : return OnAfter ( token ) ;
800817 }
801818 }
802819
@@ -866,6 +883,16 @@ private bool OnOffset(Token token)
866883 {
867884 case TokenType . Whitespace :
868885 return false ;
886+ case TokenType . Delim when token . Data . IsOneOf ( "+" , "-" ) :
887+ // The An+B offset's sign is a standalone delim token when whitespace separates it
888+ // from B - the "<n-dimension> ['+' | '-'] <signless-integer>" production of
889+ // <a-n-plus-b> (CSS Syntax 3 §6.2); §6.1 gives `3n + 1` as a valid example. The
890+ // compact form `10n+1` instead arrives as a single signed <number>, handled by the
891+ // Number case below. Without this branch a spaced sign fell through to OnBeforeOf
892+ // and invalidated the selector.
893+ _sign = token . Data == "-" ? - 1 : 1 ;
894+ _state = ParseState . AfterOffsetSign ;
895+ return false ;
869896 case TokenType . Number :
870897 _valid = _valid && ( ( NumberToken ) token ) . IsInteger && int . TryParse ( token . Data , out _offset ) ;
871898 _offset *= _sign ;
@@ -876,6 +903,26 @@ private bool OnOffset(Token token)
876903 }
877904 }
878905
906+ private bool OnAfterOffsetSign ( Token token )
907+ {
908+ switch ( token . Type )
909+ {
910+ case TokenType . Whitespace :
911+ return false ;
912+ case TokenType . Number when ! token . Data . StartsWith ( "+" , StringComparison . Ordinal ) && ! token . Data . StartsWith ( "-" , StringComparison . Ordinal ) :
913+ // The production requires a <signless-integer> here, defined as "a <number-token>
914+ // with its type flag set to integer, and no sign character" (CSS Syntax 3 §6.2), so
915+ // `10n + -1` and `10n + +1` are invalid - §6.1 lists `3n + -6` as an invalid example.
916+ _valid = _valid && ( ( NumberToken ) token ) . IsInteger && int . TryParse ( token . Data , out _offset ) ;
917+ _offset *= _sign ;
918+ _state = ParseState . BeforeOf ;
919+ return false ;
920+ default :
921+ _valid = false ;
922+ return token . Type == TokenType . RoundBracketClose ;
923+ }
924+ }
925+
879926 private bool OnInitial ( Token token )
880927 {
881928 if ( token . Type == TokenType . Whitespace ) return false ;
@@ -910,6 +957,7 @@ private enum ParseState : byte
910957 Initial ,
911958 AfterInitialSign ,
912959 Offset ,
960+ AfterOffsetSign ,
913961 BeforeOf ,
914962 AfterOf
915963 }
0 commit comments