Browse Source

fcl-css: parse pseudo elements as unary or binary elements, resolver: pseudo element

mattias 5 months ago
parent
commit
d322143566

+ 62 - 13
packages/fcl-css/src/fpcssparser.pp

@@ -86,8 +86,9 @@ Type
     function ParseHashIdentifier : TCSSHashIdentifierElement; virtual;
     function ParseClassName : TCSSClassNameElement; virtual;
     function ParseParenthesis: TCSSElement; virtual;
-    function ParsePseudo: TCSSElement; virtual;
-    Function ParseRuleBody(aRule: TCSSRuleElement; aIsAt : Boolean = False) : integer; virtual;
+    function ParsePseudoClass: TCSSElement; virtual;
+    function ParsePseudoElement: TCSSElement; virtual;
+    function ParseRuleBody(aRule: TCSSRuleElement; aIsAt : Boolean = False) : integer; virtual;
     function ParseInteger: TCSSElement; virtual;
     function ParseFloat: TCSSElement; virtual;
     function ParseString: TCSSElement; virtual;
@@ -928,7 +929,7 @@ begin
   GetNextToken;
 end;
 
-function TCSSParser.ParsePseudo: TCSSElement;
+function TCSSParser.ParsePseudoClass: TCSSElement;
 
 Var
   aPseudo : TCSSPseudoClassElement;
@@ -947,6 +948,20 @@ begin
   end;
 end;
 
+function TCSSParser.ParsePseudoElement: TCSSElement;
+begin
+  if CurrentToken<>ctkDOUBLECOLON then
+    raise Exception.Create('20250224201230');
+  GetNextToken;
+  case CurrentToken of
+  ctkIDENTIFIER: Result:=ParseIdentifier;
+  ctkFUNCTION: Result:=ParseCall('',false);
+  else
+    DoWarnExpectedButGot('pseudo element name');
+    Result:=nil;
+  end;
+end;
+
 function TCSSParser.ParseRuleBody(aRule: TCSSRuleElement; aIsAt: Boolean = false): integer;
 
 Var
@@ -1162,7 +1177,7 @@ begin
     ctkEOF: exit(nil);
     ctkLPARENTHESIS: Result:=ParseParenthesis;
     ctkURL: Result:=ParseURL;
-    ctkPSEUDO: Result:=ParsePseudo;
+    ctkPSEUDO: Result:=ParsePseudoClass;
     ctkLBRACE: Result:=ParseRule;
     ctkLBRACKET: Result:=ParseArray(Nil);
     ctkMinus,
@@ -1191,6 +1206,28 @@ end;
 
 function TCSSParser.ParseSelector: TCSSElement;
 
+  function ParseBinaryPseudoElement(var El: TCSSElement): boolean;
+  var
+    Bin: TCSSBinaryElement;
+  begin
+    Bin:=TCSSBinaryElement(CreateElement(CSSBinaryElementClass));
+    Bin.Left:=El;
+    El:=Bin;
+    Bin.Operation:=boDoubleColon;
+    Bin.Right:=ParsePseudoElement;
+    Result:=Bin.Right<>nil;
+  end;
+
+  function ParseUnaryPseudoElement: TCSSElement;
+  var
+    Un: TCSSUnaryElement;
+  begin
+    Un:=TCSSUnaryElement(CreateElement(CSSUnaryElementClass));
+    Result:=Un;
+    Un.Operation:=uoDoubleColon;
+    Un.Right:=ParsePseudoElement;
+  end;
+
   function ParseSub: TCSSElement;
   begin
     Result:=nil;
@@ -1200,8 +1237,9 @@ function TCSSParser.ParseSelector: TCSSElement;
       ctkHASH : Result:=ParseHashIdentifier;
       ctkCLASSNAME : Result:=ParseClassName;
       ctkLBRACKET: Result:=ParseAttributeSelector;
-      ctkPSEUDO: Result:=ParsePseudo;
+      ctkPSEUDO: Result:=ParsePseudoClass;
       ctkPSEUDOFUNCTION: Result:=ParseCall('',true);
+      ctkDOUBLECOLON: Result:=ParseUnaryPseudoElement;
     else
       DoWarn(SErrUnexpectedToken ,[
                GetEnumName(TypeInfo(TCSSToken),Ord(CurrentToken)),
@@ -1218,8 +1256,8 @@ function TCSSParser.ParseSelector: TCSSElement;
 
 var
   ok, OldReturnWhiteSpace: Boolean;
-  Bin: TCSSBinaryElement;
-  El: TCSSElement;
+  Bin, PseudoBin: TCSSBinaryElement;
+  El, Sub: TCSSElement;
   List: TCSSListElement;
 begin
   Result:=nil;
@@ -1234,15 +1272,18 @@ begin
   Scanner.ReturnWhiteSpace:=true;
   try
     repeat
-      {$IFDEF VerbosecSSParser}
+      {$IFDEF VerboseCSSParser}
       writeln('TCSSParser.ParseSelector LIST START ',CurrentToken,' ',CurrentTokenString);
       {$ENDIF}
       // read list
       List:=nil;
       El:=ParseSub;
-      {$IFDEF VerbosecSSParser}
+      {$IFDEF VerboseCSSParser}
       writeln('TCSSParser.ParseSelector LIST NEXT ',CurrentToken,' ',CurrentTokenString,' El=',GetCSSObj(El));
       {$ENDIF}
+      if El=nil then
+        exit;
+
       while CurrentToken in [ctkSTAR,ctkHASH,ctkIDENTIFIER,ctkCLASSNAME,ctkLBRACKET,ctkPSEUDO,ctkPSEUDOFUNCTION] do
         begin
         if List=nil then
@@ -1251,10 +1292,16 @@ begin
           List.AddChild(El);
           El:=List;
           end;
-        List.AddChild(ParseSub);
+        Sub:=ParseSub;
+        if Sub=nil then break;
+        List.AddChild(Sub);
         end;
       List:=nil;
 
+      // read postfix pseudo elements
+      while CurrentToken=ctkDOUBLECOLON do
+        if not ParseBinaryPseudoElement(El) then break;
+
       // use element
       if Bin<>nil then
         Bin.Right:=El
@@ -1263,7 +1310,7 @@ begin
       El:=nil;
 
       SkipWhiteSpace;
-      {$IFDEF VerbosecSSParser}
+      {$IFDEF VerboseCSSParser}
       writeln('TCSSParser.ParseSelector LIST END ',CurrentToken,' ',CurrentTokenString);
       {$ENDIF}
 
@@ -1282,7 +1329,7 @@ begin
         end;
       ctkSTAR,ctkHASH,ctkIDENTIFIER,ctkCLASSNAME,ctkLBRACKET,ctkPSEUDO,ctkPSEUDOFUNCTION:
         begin
-        // decendant combinator
+        // descendant combinator
         Bin:=TCSSBinaryElement(CreateElement(CSSBinaryElementClass));
         Bin.Left:=Result;
         Result:=Bin;
@@ -1297,6 +1344,9 @@ begin
     Scanner.ReturnWhiteSpace:=OldReturnWhiteSpace;
     if not ok then
       begin
+      if Result=Bin then Bin:=nil;
+      if El=List then List:=nil;
+      if Result=El then El:=nil;
       Result.Free;
       El.Free;
       List.Free;
@@ -1470,7 +1520,6 @@ var
   l : Integer;
   aValue: TCSSElement;
 begin
-  if IsSelector then ;
   aCall:=TCSSCallElement(CreateElement(CSSCallElementClass));
   try
     if (aName='') then

+ 154 - 62
packages/fcl-css/src/fpcssresolver.pas

@@ -140,6 +140,16 @@ type
     cssoUser,
     cssoAuthor
     );
+const
+  CSSOriginToSpecifity: array[TCSSOrigin] of TCSSNumericalID = (
+    CSSSpecificityUserAgent,
+    CSSSpecificityUser,
+    CSSSpecificityAuthor
+    );
+
+type
+
+  { ECSSResolver }
 
   ECSSResolver = class(ECSSException)
   end;
@@ -159,22 +169,28 @@ type
     function GetCSSID: TCSSString;
     function GetCSSTypeName: TCSSString;
     function GetCSSTypeID: TCSSNumericalID;
-    function HasCSSClass(const aClassName: TCSSString): boolean;
-    function GetCSSAttributeClass: TCSSString; // get the 'class' attribute
+    function GetCSSPseudoElementName: TCSSString;
+    function GetCSSPseudoElementID: TCSSNumericalID;
+    // parent
     function GetCSSParent: ICSSNode;
+    function GetCSSDepth: integer;
     function GetCSSIndex: integer; // node index in parent's children
+    // siblings
     function GetCSSNextSibling: ICSSNode;
     function GetCSSPreviousSibling: ICSSNode;
-    function GetCSSChildCount: integer;
-    function GetCSSChild(const anIndex: integer): ICSSNode;
     function GetCSSNextOfType: ICSSNode;
     function GetCSSPreviousOfType: ICSSNode;
+    // children
+    function GetCSSEmpty: boolean;
+    function GetCSSChildCount: integer;
+    function GetCSSChild(const anIndex: integer): ICSSNode;
+    // attributes
+    function HasCSSClass(const aClassName: TCSSString): boolean;
+    function GetCSSAttributeClass: TCSSString; // get the 'class' attribute
     function GetCSSCustomAttribute(const AttrID: TCSSNumericalID): TCSSString;
     function HasCSSExplicitAttribute(const AttrID: TCSSNumericalID): boolean; // e.g. if the HTML has the attribute
     function GetCSSExplicitAttribute(const AttrID: TCSSNumericalID): TCSSString;
     function HasCSSPseudoClass(const AttrID: TCSSNumericalID): boolean;
-    function GetCSSEmpty: boolean;
-    function GetCSSDepth: integer;
   end;
 
 type
@@ -386,9 +402,11 @@ type
     function SelectorIdentifierMatches(Identifier: TCSSResolvedIdentifierElement; const TestNode: ICSSNode; OnlySpecificity: boolean): TCSSSpecificity; virtual;
     function SelectorHashIdentifierMatches(Identifier: TCSSHashIdentifierElement; const TestNode: ICSSNode; OnlySpecificity: boolean): TCSSSpecificity; virtual;
     function SelectorClassNameMatches(aClassName: TCSSClassNameElement; const TestNode: ICSSNode; OnlySpecificity: boolean): TCSSSpecificity; virtual;
-    function SelectorPseudoClassMatches(aPseudoClass: TCSSResolvedPseudoClassElement; var TestNode: ICSSNode; OnlySpecificity: boolean): TCSSSpecificity; virtual;
+    function SelectorPseudoClassMatches(aPseudoClass: TCSSResolvedPseudoClassElement; const TestNode: ICSSNode; OnlySpecificity: boolean): TCSSSpecificity; virtual;
     function SelectorListMatches(aList: TCSSListElement; const TestNode: ICSSNode; OnlySpecificity: boolean): TCSSSpecificity; virtual;
+    function SelectorUnaryMatches(aUnary: TCSSUnaryElement; const TestNode: ICSSNode; OnlySpecificity: boolean): TCSSSpecificity; virtual;
     function SelectorBinaryMatches(aBinary: TCSSBinaryElement; const TestNode: ICSSNode; OnlySpecificity: boolean): TCSSSpecificity; virtual;
+    function SelectorPseudoElementMatches(aLeft, aRight: TCSSElement; const TestNode: ICSSNode): TCSSSpecificity; virtual;
     function SelectorArrayMatches(anArray: TCSSArrayElement; const TestNode: ICSSNode; OnlySpecificity: boolean): TCSSSpecificity; virtual;
     function SelectorArrayBinaryMatches(aBinary: TCSSBinaryElement; const TestNode: ICSSNode): TCSSSpecificity; virtual;
     function SelectorCallMatches(aCall: TCSSResolvedCallElement; const TestNode: ICSSNode; OnlySpecificity: boolean): TCSSSpecificity; virtual;
@@ -401,7 +419,7 @@ type
     function GetSiblingOfIndex(SiblingIDs: TIntegerDynArray; Index: integer): integer; virtual;
     function ComputeValue(El: TCSSElement): TCSSString; virtual;
     function SameValueText(const A, B: TCSSString): boolean; virtual;
-    function SameValueText(A: PAnsiChar; ALen: integer; B: PAnsiChar; BLen: integer): boolean; virtual;
+    function SameValueText(A: PCSSChar; ALen: integer; B: PCSSChar; BLen: integer): boolean; virtual;
     function PosSubString(const SearchStr, Str: TCSSString): integer; virtual;
     function PosWord(const SearchWord, Words: TCSSString): integer; virtual;
     function GetSiblingCount(aNode: ICSSNode): integer; virtual;
@@ -1160,15 +1178,6 @@ end;
 
 function TCSSResolver.SelectorMatches(aSelector: TCSSElement;
   const TestNode: ICSSNode; OnlySpecificity: boolean): TCSSSpecificity;
-
-  procedure MatchPseudo;
-  var
-    aNode: ICSSNode;
-  begin
-    aNode:=TestNode;
-    Result:=SelectorPseudoClassMatches(TCSSResolvedPseudoClassElement(aSelector),aNode,OnlySpecificity);
-  end;
-
 var
   C: TClass;
 begin
@@ -1182,7 +1191,9 @@ begin
   else if C=TCSSClassNameElement then
     Result:=SelectorClassNameMatches(TCSSClassNameElement(aSelector),TestNode,OnlySpecificity)
   else if C=TCSSResolvedPseudoClassElement then
-    MatchPseudo
+    Result:=SelectorPseudoClassMatches(TCSSResolvedPseudoClassElement(aSelector),TestNode,OnlySpecificity)
+  else if C=TCSSUnaryElement then
+    Result:=SelectorUnaryMatches(TCSSUnaryElement(aSelector),TestNode,OnlySpecificity)
   else if C=TCSSBinaryElement then
     Result:=SelectorBinaryMatches(TCSSBinaryElement(aSelector),TestNode,OnlySpecificity)
   else if C=TCSSArrayElement then
@@ -1256,9 +1267,8 @@ begin
   //writeln('TCSSResolver.SelectorClassNameMatches ',aValue,' ',Result);
 end;
 
-function TCSSResolver.SelectorPseudoClassMatches(
-  aPseudoClass: TCSSResolvedPseudoClassElement; var TestNode: ICSSNode;
-  OnlySpecificity: boolean): TCSSSpecificity;
+function TCSSResolver.SelectorPseudoClassMatches(aPseudoClass: TCSSResolvedPseudoClassElement;
+  const TestNode: ICSSNode; OnlySpecificity: boolean): TCSSSpecificity;
 var
   PseudoID: TCSSNumericalID;
 begin
@@ -1336,10 +1346,6 @@ begin
       Log(etWarning,20240625154031,'Type selector must be first',aList);
       {$ENDIF}
       exit(CSSSpecificityInvalid);
-    end
-    else if C=TCSSResolvedPseudoClassElement then
-    begin
-      Specificity:=SelectorPseudoClassMatches(TCSSResolvedPseudoClassElement(El),aNode,OnlySpecificity);
     end else
       Specificity:=SelectorMatches(El,aNode,OnlySpecificity);
     if Specificity<0 then
@@ -1348,11 +1354,35 @@ begin
   end;
 end;
 
+function TCSSResolver.SelectorUnaryMatches(aUnary: TCSSUnaryElement; const TestNode: ICSSNode;
+  OnlySpecificity: boolean): TCSSSpecificity;
+begin
+  Result:=CSSSpecificityInvalid;
+  case aUnary.Operation of
+  uoDoubleColon:
+    begin
+      // ::PseudoElement
+      if OnlySpecificity then
+        // treat as Type::PseudoElement
+        Result:=CSSSpecificityType+FSourceSpecificity
+               +CSSSpecificityType+FSourceSpecificity
+      else
+        Result:=SelectorPseudoElementMatches(nil,aUnary.Right,TestNode);
+    end;
+  else
+    // already warned by parser
+    {$IFDEF VerboseCSSResolver}
+    Log(etWarning,20250225103026,'Invalid CSS unary selector '+UnaryOperators[aUnary.Operation],aUnary);
+    {$ENDIF}
+  end;
+end;
+
 function TCSSResolver.SelectorBinaryMatches(aBinary: TCSSBinaryElement;
   const TestNode: ICSSNode; OnlySpecificity: boolean): TCSSSpecificity;
 var
   aParent, Sibling: ICSSNode;
   aSpecificity: TCSSSpecificity;
+  PseudoEl: TCSSElement;
 begin
   if OnlySpecificity then
   begin
@@ -1411,24 +1441,26 @@ begin
     end;
   boWhiteSpace:
     begin
-    // descendant combinator
-    Result:=SelectorMatches(aBinary.Right,TestNode,false);
-    if Result<0 then exit;
-    aParent:=TestNode;
-    repeat
-      aParent:=aParent.GetCSSParent;
-      if aParent=nil then
-        exit(CSSSpecificityNoMatch);
-      aSpecificity:=SelectorMatches(aBinary.Left,aParent,false);
-      if aSpecificity>=0 then
-      begin
-        inc(Result,aSpecificity);
-        exit;
-      end
-      else if aSpecificity=CSSSpecificityInvalid then
-        exit(CSSSpecificityInvalid);
-    until false;
-    end
+      // descendant combinator
+      Result:=SelectorMatches(aBinary.Right,TestNode,false);
+      if Result<0 then exit;
+      aParent:=TestNode;
+      repeat
+        aParent:=aParent.GetCSSParent;
+        if aParent=nil then
+          exit(CSSSpecificityNoMatch);
+        aSpecificity:=SelectorMatches(aBinary.Left,aParent,false);
+        if aSpecificity>=0 then
+        begin
+          inc(Result,aSpecificity);
+          exit;
+        end
+        else if aSpecificity=CSSSpecificityInvalid then
+          exit(CSSSpecificityInvalid);
+      until false;
+    end;
+  boDoubleColon:
+    Result:=SelectorPseudoElementMatches(aBinary.Left,aBinary.Right,TestNode);
   else
     // already warned by parser
     {$IFDEF VerboseCSSResolver}
@@ -1437,6 +1469,65 @@ begin
   end;
 end;
 
+function TCSSResolver.SelectorPseudoElementMatches(aLeft, aRight: TCSSElement;
+  const TestNode: ICSSNode): TCSSSpecificity;
+// pseudo element (function)
+var
+  ID: TCSSNumericalID;
+  aParent: ICSSNode;
+  aSpecificity: TCSSSpecificity;
+begin
+  Result:=CSSSpecificityInvalid;
+  if aRight is TCSSResolvedIdentifierElement then
+  begin
+    // pseudo element
+    ID:=TCSSResolvedIdentifierElement(aRight).NumericalID;
+    if ID<=0 then
+    begin
+      // already warned by parser
+      {$IFDEF VerboseCSSResolver}
+      Log(etWarning,20250224211914,'Invalid CSS pseudo element',aRight);
+      {$ENDIF}
+      exit;
+    end;
+    if ID<>TestNode.GetCSSPseudoElementID then
+      exit(CSSSpecificityNoMatch);
+    Result:=CSSSpecificityIdentifier;
+  end else if aRight is TCSSResolvedCallElement then begin
+    // pseudo element function
+    ID:=TCSSResolvedCallElement(aRight).NameNumericalID;
+    if ID<0 then
+    begin
+      // already warned by parser
+      {$IFDEF VerboseCSSResolver}
+      Log(etWarning,20250224212143,'Invalid CSS pseudo element function',aRight);
+      {$ENDIF}
+      exit;
+    end;
+    if ID<>TestNode.GetCSSPseudoElementID then
+      exit(CSSSpecificityNoMatch);
+    // todo: check parameters
+    Result:=CSSSpecificityIdentifier;
+  end else begin
+    // already warned by parser
+    {$IFDEF VerboseCSSResolver}
+    Log(etWarning,20250224212301,'Invalid CSS pseudo element',aRight);
+    {$ENDIF}
+  end;
+
+  if aLeft=nil then
+    exit; // unary ::Name
+
+  // test left side
+  aParent:=TestNode.GetCSSParent;
+  if aParent=nil then
+    exit(CSSSpecificityNoMatch);
+  aSpecificity:=SelectorMatches(aLeft,aParent,false);
+  if aSpecificity<0 then
+    exit(aSpecificity);
+  inc(Result,aSpecificity);
+end;
+
 function TCSSResolver.SelectorArrayMatches(anArray: TCSSArrayElement;
   const TestNode: ICSSNode; OnlySpecificity: boolean): TCSSSpecificity;
 var
@@ -1976,10 +2067,10 @@ begin
     Result:=A=B;
 end;
 
-function TCSSResolver.SameValueText(A: PAnsiChar; ALen: integer; B: PAnsiChar;
-  BLen: integer): boolean;
+function TCSSResolver.SameValueText(A: PCSSChar; ALen: integer; B: PCSSChar; BLen: integer
+  ): boolean;
 var
-  AC, BC: AnsiChar;
+  AC, BC: TCSSChar;
   i: Integer;
 begin
   if ALen<>BLen then exit(false);
@@ -1990,8 +2081,13 @@ begin
     begin
       AC:=A^;
       BC:=B^;
-      if (AC<>BC) and (UpCase(AC)<>UpCase(BC)) then
-        exit(false);
+      if (AC<>BC) then
+      begin
+        if (AC in ['a'..'z']) then AC:=TCSSChar(ord(AC)-32);
+        if (BC in ['a'..'z']) then BC:=TCSSChar(ord(BC)-32);
+        if AC<>BC then
+          exit(false);
+      end;
       inc(A);
       inc(B);
     end;
@@ -2004,22 +2100,24 @@ function TCSSResolver.PosSubString(const SearchStr, Str: TCSSString): integer;
 var
   SearchLen: SizeInt;
   i: Integer;
-  SearchP, StrP: PAnsiChar;
-  AC, BC: AnsiChar;
+  SearchP, StrP: PCSSChar;
+  AC, BC: TCSSChar;
 begin
   Result:=0;
   if SearchStr='' then exit;
   if Str='' then exit;
   if StringComparison=crscCaseInsensitive then
   begin
-    SearchP:=PAnsiChar(SearchStr);
-    StrP:=PAnsiChar(Str);
+    SearchP:=PCSSChar(SearchStr);
+    StrP:=PCSSChar(Str);
     SearchLen:=length(SearchStr);
     AC:=SearchP^;
+    if AC in ['a'..'z'] then AC:=TCSSChar(ord(AC)-32);
     for i:=0 to length(Str)-SearchLen do
     begin
       BC:=StrP^;
-      if (upcase(AC)=upcase(BC)) and SameValueText(SearchP,SearchLen,StrP,SearchLen) then
+      if BC in ['a'..'z'] then BC:=TCSSChar(ord(BC)-32);
+      if (AC=BC) and SameValueText(SearchP,SearchLen,StrP,SearchLen) then
         exit(i+1);
       inc(StrP);
     end;
@@ -2689,9 +2787,9 @@ begin
   begin
     // not yet resolved
     aName:=El.Name;
-    if Kind=nikPseudoClass then
+    if Kind in [nikPseudoClass,nikPseudoElement] then
     begin
-      // pseudo attributes are ASCII case insensitive
+      // pseudo attributes and elements are ASCII case insensitive
       System.Delete(aName,1,1);
       aName:=lowercase(aName);
     end;
@@ -2903,13 +3001,7 @@ begin
   // find all matching rules in all stylesheets
   for aLayerIndex:=0 to length(FLayers)-1 do
     with FLayers[aLayerIndex] do begin
-      case Origin of
-      cssoUserAgent: FSourceSpecificity:=CSSSpecificityUserAgent;
-      cssoUser: FSourceSpecificity:=CSSSpecificityUser;
-      else
-        FSourceSpecificity:=CSSSpecificityAuthor;
-      end;
-
+      FSourceSpecificity:=CSSOriginToSpecifity[Origin];
       for i:=0 to ElementCount-1 do
         ComputeElement(Elements[i].Element);
     end;

+ 234 - 22
packages/fcl-css/src/fpcssresparser.pas

@@ -119,6 +119,7 @@ type
   TCSSNumericalIDKind = (
     nikAttribute,
     nikPseudoClass, // e.g. "hover" of ":hover"
+    nikPseudoElement, // e.g. "first-line" of "::first-line"
     nikPseudoFunction, // e.g. "is" of ":is()"
     nikType,
     nikKeyword,
@@ -132,6 +133,7 @@ const
   CSSNumericalIDKindNames: array[TCSSNumericalIDKind] of TCSSString = (
     'Type',
     'PseudoClass',
+    'PseudoElement',
     'PseudoFunction',
     'Attribute',
     'Keyword',
@@ -309,22 +311,6 @@ type
     Index: TCSSNumericalID;
   end;
 
-  { TCSSPseudoClassDesc }
-
-  TCSSPseudoClassDesc = class(TCSSRegistryNamedItem)
-  public
-  end;
-  TCSSPseudoClassDescClass = class of TCSSPseudoClassDesc;
-  TCSSPseudoClassDescArray = array of TCSSPseudoClassDesc;
-
-  { TCSSTypeDesc }
-
-  TCSSTypeDesc = class(TCSSRegistryNamedItem)
-  public
-  end;
-  TCSSTypeDescClass = class of TCSSTypeDesc;
-  TCSSTypeDescArray = array of TCSSTypeDesc;
-
   { TCSSAttributeKeyData }
 
   TCSSAttributeKeyData = class(TCSSElementOwnedData)
@@ -355,11 +341,37 @@ type
       // used by the cascade algorithm to delete all overwritten properties
     OnCheck: TCheckEvent; // called by the parser after reading a declaration and there is no var()
       // return false if invalid, so the resolver skips this declaration
-    OnSplitShorthand: TSplitShorthandEvent; // called by resolver after resolving var(), if any value is empty, the initialvalue is used
+    OnSplitShorthand: TSplitShorthandEvent; // called by resolver after resolving var(), if any value is empty, the InitialValue is used
   end;
   TCSSAttributeDescClass = class of TCSSAttributeDesc;
   TCSSAttributeDescArray = array of TCSSAttributeDesc;
 
+  { TCSSPseudoClassDesc }
+
+  TCSSPseudoClassDesc = class(TCSSRegistryNamedItem)
+  public
+  end;
+  TCSSPseudoClassDescClass = class of TCSSPseudoClassDesc;
+  TCSSPseudoClassDescArray = array of TCSSPseudoClassDesc;
+
+  { TCSSPseudoElementDesc }
+
+  TCSSPseudoElementDesc = class(TCSSRegistryNamedItem)
+  public
+    Attributes: array of TCSSAttributeDesc; // allowed attributes
+    IsFunction: boolean;
+  end;
+  TCSSPseudoElementDescClass = class of TCSSPseudoElementDesc;
+  TCSSPseudoElementDescArray = array of TCSSPseudoElementDesc;
+
+  { TCSSTypeDesc }
+
+  TCSSTypeDesc = class(TCSSRegistryNamedItem)
+  public
+  end;
+  TCSSTypeDescClass = class of TCSSTypeDesc;
+  TCSSTypeDescArray = array of TCSSTypeDesc;
+
   { TCSSRegistry }
 
   TCSSRegistry = class
@@ -369,6 +381,7 @@ type
     FHashLists: array[TCSSNumericalIDKind] of TFPHashList; // name to TCSSRegistryNamedItem
     FKeywordCount: TCSSNumericalID;
     FPseudoClassCount: TCSSNumericalID;
+    FPseudoElementCount: TCSSNumericalID;
     FPseudoFunctionCount: TCSSNumericalID;
     FStamp, FModifiedStamp: TCSSNumericalID;
     FTypeCount: TCSSNumericalID;
@@ -401,7 +414,7 @@ type
       TopLeftID, TopRightID, BottomLeftID, BottomRightID: TCSSNumericalID; const Found: TCSSStringArray); overload;
     property AttributeCount: TCSSNumericalID read FAttributeCount;
   public
-    // pseudo classes
+    // pseudo classes, e.g. :hover
     PseudoClasses: TCSSPseudoClassDescArray; // Note: PseudoClasses[0] is nil to spot bugs easily
     PseudoClass_ClassOf: TCSSPseudoClassDescClass;
     function AddPseudoClass(aPseudo: TCSSPseudoClassDesc): TCSSPseudoClassDesc; overload;
@@ -410,13 +423,22 @@ type
     function IndexOfPseudoClassName(const aName: TCSSString): TCSSNumericalID; overload;
     property PseudoClassCount: TCSSNumericalID read FPseudoClassCount;
   public
-    // pseudo functions lowercase (they are parsed case insensitive)
+    // pseudo element, e.g. ::first-line
+    PseudoElements: TCSSPseudoElementDescArray;
+    PseudoElement_ClassOf: TCSSPseudoElementDescClass;
+    function AddPseudoElement(aPseudo: TCSSPseudoElementDesc): TCSSPseudoElementDesc; overload;
+    function AddPseudoElement(const aName: TCSSString; aClass: TCSSPseudoElementDescClass = nil): TCSSPseudoElementDesc; overload;
+    function FindPseudoElement(const aName: TCSSString): TCSSPseudoElementDesc; overload;
+    function IndexOfPseudoElementName(const aName: TCSSString): TCSSNumericalID; overload;
+    property PseudoElementCount: TCSSNumericalID read FPseudoElementCount;
+  public
+    // pseudo functions lowercase (they are parsed case insensitive), e.g. :not()
     PseudoFunctions: TCSSStringArray; // Note: PseudoFunctions[0] is nil to spot bugs easily
     function AddPseudoFunction(const aName: TCSSString): TCSSNumericalID; overload;
     function IndexOfPseudoFunction(const aName: TCSSString): TCSSNumericalID; overload;
     property PseudoFunctionCount: TCSSNumericalID read FPseudoFunctionCount;
   public
-    // types
+    // types, e.g. div
     Types: TCSSTypeDescArray; // Note: Types[0] is nil to spot bugs easily
     Type_ClassOf: TCSSTypeDescClass;
     function AddType(aType: TCSSTypeDesc): TCSSTypeDesc; overload;
@@ -436,7 +458,7 @@ type
     function GetKeywordColor(KeywordID: TCSSNumericalID): TCSSAlphaColor; virtual; overload;
     property KeywordCount: TCSSNumericalID read FKeywordCount;
   public
-    // attribute functions
+    // attribute functions, e.g. var()
     AttrFunctions: TCSSStringArray; // Note: AttrFunctions[0] is nil to spot bugs easily
     const afVar = CSSAttrFuncVar;
     function AddAttrFunction(const aName: TCSSString): TCSSNumericalID; overload;
@@ -575,6 +597,8 @@ type
     function GetAttributeDesc(AttrID: TCSSNumericalID): TCSSAttributeDesc; virtual;
     function GetTypeID(const aName: TCSSString): TCSSNumericalID; virtual;
     function GetPseudoClassID(const aName: TCSSString): TCSSNumericalID; virtual;
+    function GetPseudoElementID(const aName: TCSSString): TCSSNumericalID; virtual;
+    function GetPseudoElFuncID(const aName: TCSSString): TCSSNumericalID; virtual;
     function GetPseudoFunctionID(const aName: TCSSString): TCSSNumericalID; virtual;
 
     property CSSRegistry: TCSSRegistry read FCSSRegistry write SetCSSRegistry;
@@ -592,13 +616,17 @@ type
     function ResolveAttribute(El: TCSSResolvedIdentifierElement): TCSSNumericalID; virtual;
     function ResolveType(El: TCSSResolvedIdentifierElement): TCSSNumericalID; virtual;
     function ResolvePseudoClass(El: TCSSResolvedPseudoClassElement): TCSSNumericalID; virtual;
+    function ResolvePseudoElement(El: TCSSResolvedIdentifierElement): TCSSNumericalID; virtual;
+    function ResolvePseudoElementFunction(El: TCSSResolvedCallElement): TCSSNumericalID; virtual;
     function ResolvePseudoFunction(El: TCSSResolvedCallElement): TCSSNumericalID; virtual;
     function ParseCall(aName: TCSSString; IsSelector: boolean): TCSSCallElement; override;
     function ParseDeclaration(aIsAt: Boolean): TCSSDeclarationElement; override;
+    function ParsePseudoElement: TCSSElement; override;
     function ParseSelector: TCSSElement; override;
     procedure CheckSelector(El: TCSSElement); virtual;
     procedure CheckSelectorArray(anArray: TCSSArrayElement); virtual;
     procedure CheckSelectorArrayBinary(aBinary: TCSSBinaryElement); virtual;
+    procedure CheckSelectorUnary(aUnary: TCSSUnaryElement); virtual;
     procedure CheckSelectorBinary(aBinary: TCSSBinaryElement); virtual;
     procedure CheckSelectorList(aList: TCSSListElement); virtual;
     procedure CheckNthChildParams(aCall: TCSSResolvedCallElement); virtual;
@@ -653,6 +681,8 @@ begin
     Attribute_ClassOf:=TCSSAttributeDesc;
   if PseudoClass_ClassOf=nil then
     PseudoClass_ClassOf:=TCSSPseudoClassDesc;
+  if PseudoElement_ClassOf=nil then
+    PseudoElement_ClassOf:=TCSSPseudoElementDesc;
   if Type_ClassOf=nil then
     Type_ClassOf:=TCSSTypeDesc;
 
@@ -666,6 +696,11 @@ begin
   for i:=0 to length(PseudoClasses)-1 do PseudoClasses[i]:=nil;
   FPseudoClassCount:=1; // index 0 is CSSIDNone
 
+  // init pseudo elements
+  SetLength(PseudoElements,32);
+  for i:=0 to length(PseudoElements)-1 do PseudoElements[i]:=nil;
+  FPseudoElementCount:=1; // index 0 is CSSIDNone
+
   // init pseudo functions
   SetLength(PseudoFunctions,16);
   FPseudoFunctionCount:=1; // index 0 is CSSIDNone
@@ -712,6 +747,9 @@ begin
   if AddPseudoClass('only-of-type').Index<>CSSPseudoID_OnlyOfType then
     raise Exception.Create('20240623170609');
 
+  // init pseudo elements
+  // none by default
+
   // init pseudo functions
   if AddPseudoFunction('not')<>CSSCallID_Not then
     raise Exception.Create('20240625183757');
@@ -776,6 +814,12 @@ begin
   PseudoClasses:=nil;
   FPseudoClassCount:=0;
 
+  // pseudo elements
+  for i:=1 to PseudoElementCount-1 do
+    FreeAndNil(PseudoElements[i]);
+  PseudoElements:=nil;
+  FPseudoElementCount:=0;
+
   // types
   for i:=1 to TypeCount-1 do
     FreeAndNil(Types[i]);
@@ -827,6 +871,7 @@ var
   TypeDesc: TCSSTypeDesc;
   i: Integer;
   aName: TCSSString;
+  PseudoElementDesc: TCSSPseudoElementDesc;
 begin
   if AttributeCount>length(Attributes) then
     raise Exception.Create('20240629102438');
@@ -885,6 +930,27 @@ begin
       raise Exception.Create('20240629101227 pseudo class ID='+IntToStr(ID)+' "'+aName+'" IndexOf failed: '+IntToStr(ID2));
   end;
 
+  if PseudoElementCount>length(PseudoElements) then
+    raise Exception.Create('20250220140108');
+  for ID:=1 to PseudoElementCount-1 do
+  begin
+    PseudoElementDesc:=PseudoElements[ID];
+    if PseudoElementDesc=nil then
+      raise Exception.Create('20250220140126 pseudo element ID='+IntToStr(ID)+' Desc=nil');
+    aName:=PseudoElementDesc.Name;
+    if aName='' then
+      raise Exception.Create('20250220140201 pseudo element ID='+IntToStr(ID)+' missing name');
+    if length(aName)>255 then
+      raise Exception.Create('20250220140202 pseudo element ID='+IntToStr(ID)+' name too long "'+aName+'"');
+    if aName[1]=':' then
+      raise Exception.Create('20250220140204 pseudo element ID='+IntToStr(ID)+' invalid name "'+aName+'"');
+    if PseudoElementDesc.Index<>ID then
+      raise Exception.Create('20250220140205 pseudo element ID='+IntToStr(ID)+' Desc.Index='+IntToStr(PseudoElementDesc.Index)+' "'+aName+'"');
+    ID2:=IndexOfPseudoElementName(PseudoElementDesc.Name);
+    if ID2<>ID then
+      raise Exception.Create('20250220140207 pseudo element ID='+IntToStr(ID)+' "'+aName+'" IndexOf failed: '+IntToStr(ID2));
+  end;
+
   if PseudoFunctionCount>length(PseudoFunctions) then
     raise Exception.Create('20240629103430');
   for ID:=1 to PseudoFunctionCount-1 do
@@ -1152,6 +1218,8 @@ function TCSSRegistry.AddPseudoClass(const aName: TCSSString;
 begin
   if aName='' then
     raise ECSSParser.Create('missing name');
+  if aName<>lowercase(aName) then
+    raise ECSSParser.Create('name not lowercase');
   if FindPseudoClass(aName)<>nil then
     raise ECSSParser.Create('duplicate pseudo class "'+aName+'"');
   if aClass=nil then
@@ -1180,6 +1248,62 @@ begin
     Result:=-1;
 end;
 
+function TCSSRegistry.AddPseudoElement(aPseudo: TCSSPseudoElementDesc): TCSSPseudoElementDesc;
+begin
+  Result:=aPseudo;
+  if aPseudo.Name='' then
+    raise ECSSParser.Create('missing name');
+  if FindPseudoElement(aPseudo.Name)<>nil then
+    raise ECSSParser.Create('duplicate pseudo element "'+aPseudo.Name+'"');
+
+  if PseudoElementCount=length(PseudoElements) then
+  begin
+    if PseudoElementCount<32 then
+      SetLength(PseudoElements,32)
+    else
+      SetLength(PseudoElements,2*PseudoElementCount);
+    FillByte(PseudoElements[PseudoElementCount],SizeOf(Pointer)*(length(PseudoElements)-PseudoElementCount),0);
+  end;
+  PseudoElements[PseudoElementCount]:=aPseudo;
+  aPseudo.Index:=PseudoElementCount;
+  FHashLists[nikPseudoElement].Add(aPseudo.Name,aPseudo);
+  inc(FPseudoElementCount);
+  ChangeStamp;
+end;
+
+function TCSSRegistry.AddPseudoElement(const aName: TCSSString; aClass: TCSSPseudoElementDescClass
+  ): TCSSPseudoElementDesc;
+begin
+  if aName='' then
+    raise ECSSParser.Create('missing name');
+  if aName<>lowercase(aName) then
+    raise ECSSParser.Create('name not lowercase');
+  if FindPseudoElement(aName)<>nil then
+    raise ECSSParser.Create('duplicate pseudo element "'+aName+'"');
+  if aClass=nil then
+    aClass:=PseudoElement_ClassOf;
+
+  Result:=aClass.Create;
+  Result.Name:=aName;
+  AddPseudoElement(Result);
+end;
+
+function TCSSRegistry.FindPseudoElement(const aName: TCSSString): TCSSPseudoElementDesc;
+begin
+  Result:=TCSSPseudoElementDesc(FHashLists[nikPseudoElement].Find(aName));
+end;
+
+function TCSSRegistry.IndexOfPseudoElementName(const aName: TCSSString): TCSSNumericalID;
+var
+  aPseudo: TCSSPseudoElementDesc;
+begin
+  aPseudo:=TCSSPseudoElementDesc(FHashLists[nikPseudoElement].Find(aName));
+  if aPseudo<>nil then
+    Result:=aPseudo.Index
+  else
+    Result:=-1;
+end;
+
 function TCSSRegistry.AddPseudoFunction(const aName: TCSSString
   ): TCSSNumericalID;
 begin
@@ -2121,6 +2245,20 @@ begin
   Result:=CSSRegistry.IndexOfPseudoClassName(aName);
 end;
 
+function TCSSBaseResolver.GetPseudoElementID(const aName: TCSSString): TCSSNumericalID;
+begin
+  Result:=CSSRegistry.IndexOfPseudoElementName(aName);
+  if (Result>=0) and CSSRegistry.PseudoElements[Result].IsFunction then
+    Result:=-1;
+end;
+
+function TCSSBaseResolver.GetPseudoElFuncID(const aName: TCSSString): TCSSNumericalID;
+begin
+  Result:=CSSRegistry.IndexOfPseudoElementName(aName);
+  if (Result>=0) and not CSSRegistry.PseudoElements[Result].IsFunction then
+    Result:=-1;
+end;
+
 function TCSSBaseResolver.GetPseudoFunctionID(const aName: TCSSString): TCSSNumericalID;
 begin
   Result:=CSSRegistry.IndexOfPseudoFunction(aName);
@@ -2186,6 +2324,50 @@ begin
     El.NumericalID:=Result;
 end;
 
+function TCSSResolverParser.ResolvePseudoElement(El: TCSSResolvedIdentifierElement
+  ): TCSSNumericalID;
+var
+  aName: TCSSString;
+begin
+  // pseudo elements are ASCII case insensitive
+  aName:=lowercase(El.Name);
+
+  if El.NumericalID<>CSSIDNone then
+    raise Exception.Create('20250224203646');
+
+  El.Kind:=nikPseudoElement;
+  Result:=Resolver.GetPseudoElementID(aName);
+  //writeln('TCSSResolverParser.ResolvePseudoElement ',aName,' ID=',Result);
+  if Result<=CSSIDNone then
+  begin
+    El.NumericalID:=-1;
+    Log(etWarning,20250224203703,'unknown pseudo element "'+aName+'"',El);
+  end else
+    El.NumericalID:=Result;
+end;
+
+function TCSSResolverParser.ResolvePseudoElementFunction(El: TCSSResolvedCallElement
+  ): TCSSNumericalID;
+var
+  aName: TCSSString;
+begin
+  // pseudo elements are ASCII case insensitive
+  aName:=lowercase(El.Name);
+
+  if El.NameNumericalID<>CSSIDNone then
+    raise Exception.Create('20250224210628');
+
+  El.Kind:=nikPseudoElement;
+  Result:=Resolver.GetPseudoElFuncID(aName);
+  //writeln('TCSSResolverParser.ResolvePseudoElement ',aName,' ID=',Result);
+  if Result<=CSSIDNone then
+  begin
+    El.NameNumericalID:=-1;
+    Log(etWarning,20250224203703,'unknown pseudo element function "'+aName+'"',El);
+  end else
+    El.NameNumericalID:=Result;
+end;
+
 function TCSSResolverParser.ResolvePseudoFunction(El: TCSSResolvedCallElement
   ): TCSSNumericalID;
 var
@@ -2300,6 +2482,17 @@ begin
   end;
 end;
 
+function TCSSResolverParser.ParsePseudoElement: TCSSElement;
+begin
+  Result:=inherited ParsePseudoElement;
+  if Result is TCSSResolvedIdentifierElement then
+    ResolvePseudoElement(TCSSResolvedIdentifierElement(Result))
+  else if Result is TCSSResolvedCallElement then
+    ResolvePseudoElementFunction(TCSSResolvedCallElement(Result))
+  else
+    Log(etWarning,20250224210802,'Unknown CSS selector pseudo element',Result);
+end;
+
 function TCSSResolverParser.ParseSelector: TCSSElement;
 begin
   Result:=inherited ParseSelector;
@@ -2321,6 +2514,8 @@ begin
   else if C=TCSSResolvedPseudoClassElement then
     // e.g. :pseudoclass {}
     ResolvePseudoClass(TCSSResolvedPseudoClassElement(El))
+  else if C=TCSSUnaryElement then
+    CheckSelectorUnary(TCSSUnaryElement(El))
   else if C=TCSSBinaryElement then
     CheckSelectorBinary(TCSSBinaryElement(El))
   else if C=TCSSArrayElement then
@@ -2430,13 +2625,30 @@ begin
   end;
 end;
 
+procedure TCSSResolverParser.CheckSelectorUnary(aUnary: TCSSUnaryElement);
+begin
+  case aUnary.Operation of
+  uoDoubleColon:
+    ; // right side was done in ParsePseudoElement
+  else
+    Log(etWarning,20250225103443,'Invalid CSS unary selector '+UnaryOperators[aUnary.Operation],aUnary);
+  end;
+end;
+
 procedure TCSSResolverParser.CheckSelectorBinary(aBinary: TCSSBinaryElement);
 begin
   case aBinary.Operation of
   boGT,
   boPlus,
   boTilde,
-  boWhiteSpace: ;
+  boWhiteSpace:
+    ;
+  boDoubleColon:
+    begin
+    CheckSelector(aBinary.Left);
+    // right side was done in ParsePseudoElement
+    exit;
+    end
   else
     Log(etWarning,20240625153307,'Invalid CSS binary selector '+BinaryOperators[aBinary.Operation],aBinary);
   end;

+ 1 - 6
packages/fcl-css/src/fpcssscanner.pp

@@ -952,12 +952,7 @@ begin
       if csoDisablePseudo in Options then
         CharToken(ctkCOLON)
       else if (TokenStr[1]=':') then
-        begin
-        if (TokenStr[2] in AlNumIden) then
-          Result:=DoIdentifierLike
-        else
-          Result:=ctkDoubleCOLON
-        end
+        TwoCharsToken(ctkDoubleCOLON)
       else if (TokenStr[1] in AlNumIden) then
         Result:=DoIdentifierLike
       else

+ 1 - 2
packages/fcl-css/tests/tccssparser.pp

@@ -581,14 +581,13 @@ var
   List : TCSSListElement;
 
 begin
-  R:=ParseRule('input:enabled:read-write:-webkit-any(:focus,:hover)::-webkit-clear-button {  }');
+  R:=ParseRule('input:enabled:read-write:-webkit-any(:focus,:hover) {  }');
   AssertEquals('No rule children',0,R.ChildCount);
   AssertEquals('selector count',1,R.SelectorCount);
   List:=TCSSListElement(CheckClass('List',TCSSListElement,R.Selectors[0]));
   CheckList(List,0,'input');
   CheckList(List,1,':enabled');
   CheckList(List,2,':read-write');
-  CheckList(List,4,'::-webkit-clear-button');
 end;
 
 procedure TTestCSSParser.TestQueryPrefixedEmptyRule;

+ 238 - 10
packages/fcl-css/tests/tccssresolver.pp

@@ -216,7 +216,7 @@ type
     // computed by resolver:
     Rules: TCSSSharedRuleList; // owned by resolver
     Values: TCSSAttributeValues;
-
+    // explicit attributes: can be queried by CSS, e.g. div[foo=3px]
     ExplicitAttributes: array[TDemoNodeAttribute] of TCSSString;
 
     constructor Create(AOwner: TComponent); override;
@@ -232,22 +232,24 @@ type
     function GetCSSID: TCSSString; virtual;
     function GetCSSTypeName: TCSSString;
     function GetCSSTypeID: TCSSNumericalID;
-    function HasCSSClass(const aClassName: TCSSString): boolean; virtual;
+    function GetCSSPseudoElementName: TCSSString; virtual;
+    function GetCSSPseudoElementID: TCSSNumericalID; virtual;
     function GetCSSParent: ICSSNode; virtual;
+    function GetCSSDepth: integer; virtual;
     function GetCSSIndex: integer; virtual;
     function GetCSSNextSibling: ICSSNode; virtual;
     function GetCSSPreviousSibling: ICSSNode; virtual;
-    function GetCSSChildCount: integer; virtual;
-    function GetCSSChild(const anIndex: integer): ICSSNode; virtual;
     function GetCSSNextOfType: ICSSNode; virtual;
     function GetCSSPreviousOfType: ICSSNode; virtual;
+    function GetCSSEmpty: boolean; virtual;
+    function GetCSSChildCount: integer; virtual;
+    function GetCSSChild(const anIndex: integer): ICSSNode; virtual;
+    function HasCSSClass(const aClassName: TCSSString): boolean; virtual;
     function GetCSSAttributeClass: TCSSString; virtual;
     function GetCSSCustomAttribute(const AttrID: TCSSNumericalID): TCSSString; virtual;
     function HasCSSExplicitAttribute(const AttrID: TCSSNumericalID): boolean; virtual;
     function GetCSSExplicitAttribute(const AttrID: TCSSNumericalID): TCSSString; virtual;
     function HasCSSPseudoClass(const {%H-}AttrID: TCSSNumericalID): boolean; virtual;
-    function GetCSSEmpty: boolean; virtual;
-    function GetCSSDepth: integer; virtual;
 
     property Parent: TDemoNode read FParent write SetParent;
     property NodeCount: integer read GetNodeCount;
@@ -275,6 +277,35 @@ type
   end;
   TDemoNodeClass = class of TDemoNode;
 
+  { TDemoPseudoElement }
+
+  TDemoPseudoElement = class(TDemoNode)
+  public
+    constructor Create(AOwner: TComponent); override;
+    function GetCSSTypeName: TCSSString;
+    function GetCSSTypeID: TCSSNumericalID;
+    function GetCSSParent: ICSSNode; override;
+    function GetCSSIndex: integer; override;
+    function GetCSSNextSibling: ICSSNode; override;
+    function GetCSSPreviousSibling: ICSSNode; override;
+    function GetCSSNextOfType: ICSSNode; override;
+    function GetCSSPreviousOfType: ICSSNode; override;
+    function GetCSSEmpty: boolean; override;
+    function GetCSSChildCount: integer; override;
+    function GetCSSChild(const anIndex: integer): ICSSNode; override;
+    function HasCSSClass(const aClassName: TCSSString): boolean; override;
+    function GetCSSAttributeClass: TCSSString; override;
+  end;
+
+  { TDemoFirstLine }
+
+  TDemoFirstLine = class(TDemoPseudoElement)
+  public
+    class var DemoFirstLineID: TCSSNumericalID;
+    function GetCSSPseudoElementName: TCSSString; override;
+    function GetCSSPseudoElementID: TCSSNumericalID; override;
+  end;
+
   { TDemoDiv }
 
   TDemoDiv = class(TDemoNode)
@@ -342,7 +373,7 @@ type
   { TCustomTestNewCSSResolver }
 
   TCustomTestNewCSSResolver = class(TTestCase)
-  Private
+  private
     FDoc: TDemoDocument;
   protected
     procedure SetUp; override;
@@ -429,7 +460,7 @@ type
     // var()
     procedure Test_Var_NoDefault;
     procedure Test_Var_Inline_NoDefault;
-      procedure Test_Var_Defaults;
+    procedure Test_Var_Defaults;
 
     // skipping for forward compatibility
     // ToDo: invalid token in selector makes selector invalid
@@ -439,7 +470,10 @@ type
     // test skip invalid value  color: 3 red;
     // test skip invalid attribute  color: 3;
 
-    // pseudo elements
+    // pseudo elements (works like child combinator)
+    procedure Test_PseudoElement;
+    procedure Test_PseudoElement_Unary;
+    procedure Test_PseudoElement_PostfixSelectNothing;
   end;
 
 function LinesToStr(const Args: array of const): TCSSString;
@@ -833,6 +867,10 @@ begin
   if FindPseudoClass(DemoPseudoClassNames[pcHover]).Index<>DemoPseudoClassIDBase+ord(pcHover) then
     raise Exception.Create('20231008232201');
 
+  // register demo pseudo elements
+  TDemoFirstLine.DemoFirstLineID:=AddPseudoElement('first-line').Index;
+  AddPseudoElement('selection');
+
   // register demo element types
   for aType in TDemoElementType do
     AddDemoType(aType);
@@ -859,7 +897,7 @@ begin
   kwLTR:=AddKeyword('ltr');
   kwRTL:=AddKeyword('rtl');
 
-  // check parameters - - - - - - - - - - - - - - - - - - - - - - - -
+  // check attribute values - - - - - - - - - - - - - - - - - - - - - - - -
 
   // border-color
   DemoAttrs[naBorderColor].OnCheck:=@OnCheck_BorderColor;
@@ -1023,6 +1061,7 @@ begin
   Clear;
   FreeAndNil(FNodes);
   FreeAndNil(FCSSClasses);
+  FParent:=nil;
   inherited Destroy;
 end;
 
@@ -1300,11 +1339,109 @@ begin
   Result:=GetClassCSSTypeID;
 end;
 
+function TDemoNode.GetCSSPseudoElementName: TCSSString;
+begin
+  Result:='';
+end;
+
+function TDemoNode.GetCSSPseudoElementID: TCSSNumericalID;
+begin
+  Result:=CSSIDNone;
+end;
+
 class function TDemoNode.GetCSSTypeStyle: TCSSString;
 begin
   Result:='';
 end;
 
+{ TDemoPseudoElement }
+
+constructor TDemoPseudoElement.Create(AOwner: TComponent);
+begin
+  inherited Create(AOwner);
+  if not (AOwner is TDemoNode) then
+    raise Exception.Create('20250224153414');
+end;
+
+function TDemoPseudoElement.GetCSSTypeName: TCSSString;
+begin
+  Result:='';
+end;
+
+function TDemoPseudoElement.GetCSSTypeID: TCSSNumericalID;
+begin
+  Result:=CSSIDNone;
+end;
+
+function TDemoPseudoElement.GetCSSParent: ICSSNode;
+begin
+  Result:=TDemoNode(Owner);
+end;
+
+function TDemoPseudoElement.GetCSSIndex: integer;
+begin
+  Result:=-1;
+end;
+
+function TDemoPseudoElement.GetCSSNextSibling: ICSSNode;
+begin
+  Result:=nil;
+end;
+
+function TDemoPseudoElement.GetCSSPreviousSibling: ICSSNode;
+begin
+  Result:=nil;
+end;
+
+function TDemoPseudoElement.GetCSSNextOfType: ICSSNode;
+begin
+  Result:=nil;
+end;
+
+function TDemoPseudoElement.GetCSSPreviousOfType: ICSSNode;
+begin
+  Result:=nil;
+end;
+
+function TDemoPseudoElement.GetCSSEmpty: boolean;
+begin
+  Result:=true;
+end;
+
+function TDemoPseudoElement.GetCSSChildCount: integer;
+begin
+  Result:=0;
+end;
+
+function TDemoPseudoElement.GetCSSChild(const anIndex: integer): ICSSNode;
+begin
+  Result:=nil;
+  if anIndex=0 then ;
+end;
+
+function TDemoPseudoElement.HasCSSClass(const aClassName: TCSSString): boolean;
+begin
+  Result:=false;
+  if aClassName='' then ;
+end;
+
+function TDemoPseudoElement.GetCSSAttributeClass: TCSSString;
+begin
+  Result:='';
+end;
+
+{ TDemoFirstLine }
+
+function TDemoFirstLine.GetCSSPseudoElementName: TCSSString;
+begin
+  Result:='first-line';
+end;
+
+function TDemoFirstLine.GetCSSPseudoElementID: TCSSNumericalID;
+begin
+  Result:=DemoFirstLineID;
+end;
+
 { TCustomTestNewCSSResolver }
 
 procedure TCustomTestNewCSSResolver.SetUp;
@@ -2734,6 +2871,97 @@ begin
   AssertEquals('Div1.Color','',Div1.Color);
 end;
 
+procedure TTestNewCSSResolver.Test_PseudoElement;
+var
+  Div1: TDemoDiv;
+  FirstLine: TDemoFirstLine;
+begin
+  Doc.Root:=TDemoNode.Create(nil);
+
+  Div1:=TDemoDiv.Create(nil);
+  Div1.Name:='Div1';
+  Div1.Parent:=Doc.Root;
+
+  Doc.Style:=LinesToStr([
+  'div {',
+  '  border-color:red;',
+  '}',
+  '#Div1::first-line {',
+  '  color:red;',
+  '  border-color:white;',
+  '}',
+  'div {',
+  '  color: blue;',
+  '}']);
+  ApplyStyle;
+  FirstLine:=TDemoFirstLine.Create(Div1);
+  FirstLine.ApplyCSS(Doc.CSSResolver);
+
+  AssertEquals('Div1.BorderColor','red',Div1.BorderColor);
+  AssertEquals('Div1.Color','blue',Div1.Color);
+  AssertEquals('Div1::first-line.BorderColor','white',FirstLine.BorderColor);
+  AssertEquals('Div1::first-line.Color','red',FirstLine.Color);
+end;
+
+procedure TTestNewCSSResolver.Test_PseudoElement_Unary;
+var
+  Div1: TDemoDiv;
+  FirstLine: TDemoFirstLine;
+begin
+  Doc.Root:=TDemoNode.Create(nil);
+
+  Div1:=TDemoDiv.Create(nil);
+  Div1.Name:='Div1';
+  Div1.Parent:=Doc.Root;
+
+  Doc.Style:=LinesToStr([
+  '::first-line {',
+  '  color:red;',
+  '}',
+  'div {',
+  '  color: blue;',
+  '}']);
+  ApplyStyle;
+  FirstLine:=TDemoFirstLine.Create(Div1);
+  FirstLine.ApplyCSS(Doc.CSSResolver);
+
+  AssertEquals('Div1.Color','blue',Div1.Color);
+  AssertEquals('Div1::first-line.Color','red',FirstLine.Color);
+end;
+
+procedure TTestNewCSSResolver.Test_PseudoElement_PostfixSelectNothing;
+var
+  Div1: TDemoDiv;
+  FirstLine: TDemoFirstLine;
+begin
+  Doc.Root:=TDemoNode.Create(nil);
+
+  Div1:=TDemoDiv.Create(nil);
+  Div1.Name:='Div1';
+  Div1.Parent:=Doc.Root;
+  Div1.CSSClasses.Add('Big');
+
+  Doc.Style:=LinesToStr([
+  'div::first-line#Bird {',
+  '  color:red;',
+  '}',
+  'div::first-line.Big {',
+  '  border-color:red;',
+  '}',
+  'div {',
+  '  color: blue;',
+  '  border-color: blue;',
+  '}']);
+  ApplyStyle;
+  FirstLine:=TDemoFirstLine.Create(Div1);
+  FirstLine.ApplyCSS(Doc.CSSResolver);
+
+  AssertEquals('Div1.Color','blue',Div1.Color);
+  AssertEquals('Div1.BorderColor','blue',Div1.BorderColor);
+  AssertEquals('Div1::first-line.Color','',FirstLine.Color);
+  AssertEquals('Div1::first-line.BorderColor','',FirstLine.BorderColor);
+end;
+
 initialization
   RegisterTests([TTestNewCSSResolver]);
 

+ 3 - 5
packages/fcl-css/tests/tccssscanner.pp

@@ -519,7 +519,7 @@ end;
 
 procedure TTestCSSScanner.TestPSEUDO2;
 begin
-  CheckToken(ctkPSEUDO,'::name');
+  CheckTokens('::name',[ctkDOUBLECOLON,ctkIDENTIFIER]);
 end;
 
 procedure TTestCSSScanner.TestPSEUDOMinus;
@@ -529,7 +529,7 @@ end;
 
 procedure TTestCSSScanner.TestPSEUDO2Minus;
 begin
-  CheckToken(ctkPSEUDO,'::-name');
+  CheckTokens('::-name',[ctkDOUBLECOLON,ctkIDENTIFIER]);
 end;
 
 procedure TTestCSSScanner.TestPSEUDODisabled;
@@ -561,11 +561,9 @@ end;
 
 procedure TTestCSSScanner.TestPSEUDOFUNCTION2;
 begin
-  CheckToken(ctkPSEUDOFUNCTION,'::name(');
+  CheckTokens('::name(',[ctkDOUBLECOLON,ctkFUNCTION]);
 end;
 
-
-
 procedure TTestCSSScanner.TestJUNK;
 var
   HasUnknown: Boolean;