فهرست منبع

fcl-css: added two char tokens, parse descendant combinator as binary op

mattias 2 سال پیش
والد
کامیت
0c4d61b3de

+ 432 - 59
packages/fcl-css/src/fpcssparser.pp

@@ -46,18 +46,25 @@ Type
     Procedure DoError(Msg : TCSSString);
     Procedure DoError(Fmt : TCSSString; const Args : Array of const);
     Procedure Consume(aToken : TCSSToken);
-    function ParseComponentValueList(allowRules: Boolean=True): TCSSElement;
+    Procedure SkipWhiteSpace;
+    function ParseComponentValueList(AllowRules: Boolean=True): TCSSElement;
     function ParseComponentValue: TCSSElement;
     function ParseExpression: TCSSElement;
-    function ParseRule(IsAt: Boolean=False): TCSSElement;
+    function ParseRule: TCSSElement;
     function ParseAtRule: TCSSElement;
+    function ParseAtMediaRule: TCSSAtRuleElement;
+    function ParseMediaCondition: TCSSElement;
     function ParseRuleList(aStopOn : TCSStoken = ctkEOF): TCSSElement;
     function ParseSelector: TCSSElement;
+    function ParseAttributeSelector: TCSSElement;
+    function ParseWQName: TCSSElement;
     function ParseDeclaration(aIsAt : Boolean = false): TCSSDeclarationElement;
     function ParseCall(aName: TCSSString): TCSSElement;
     function ParseUnary: TCSSElement;
     function ParseUnit: TCSSUnits;
-    function ParseIdentifier : TCSSElement;
+    function ParseIdentifier : TCSSIdentifierElement;
+    function ParseHashIdentifier : TCSSHashIdentifierElement;
+    function ParseClassName : TCSSClassNameElement;
     function ParseParenthesis: TCSSElement;
     function ParsePseudo: TCSSElement;
     Function ParseRuleBody(aRule: TCSSRuleElement; aIsAt : Boolean = False) : integer;
@@ -83,7 +90,7 @@ Type
     Property atEOF : Boolean Read GetAtEOF;
   end;
 
-Function TokenToBinaryOperation(aToken : TCSSToken)  : TCSSBinaryOperation;
+Function TokenToBinaryOperation(aToken : TCSSToken) : TCSSBinaryOperation;
 Function TokenToUnaryOperation(aToken : TCSSToken) : TCSSUnaryOperation;
 
 implementation
@@ -97,7 +104,7 @@ Resourcestring
   SErrInvalidFloat = 'Invalid float: %s';
   SErrUnexpectedEndOfFile = 'Unexpected EOF while scanning function args: %s';
 
-Function TokenToBinaryOperation(aToken : TCSSToken)  : TCSSBinaryOperation;
+Function TokenToBinaryOperation(aToken : TCSSToken) : TCSSBinaryOperation;
 
 begin
   Case aToken of
@@ -105,14 +112,21 @@ begin
     ctkPlus : Result:=boPlus;
     ctkMinus:  Result:=boMinus;
     ctkAnd : result:=boAnd;
+    ctkGE : Result:=boGE;
     ctkGT : Result:=boGT;
+    ctkLE : Result:=boLE;
     ctkLT : Result:=boLT;
     ctkDIV : Result:=boDIV;
-    ctkStar : Result:=boSTAR;
+    ctkStar : Result:=boStar;
+    ctkSTAREQUAL : Result:=boStarEqual;
     ctkTilde : Result:=boTilde;
+    ctkTILDEEQUAL : Result:=boTildeEqual;
     ctkSquared : Result:=boSquared;
+    ctkSQUAREDEQUAL : Result:=boSquaredEqual;
     ctkPIPE : Result:=boPipe;
+    ctkPIPEEQUAL : Result:=boPipeEqual;
     ctkDOLLAR : Result:=boDollar;
+    ctkDOLLAREQUAL : Result:=boDollarEqual;
     ctkColon : Result:=boCOLON;
     ctkDoubleColon : Result:=boDoubleColon;
   else
@@ -170,6 +184,11 @@ begin
   GetNextToken;
 end;
 
+procedure TCSSParser.SkipWhiteSpace;
+begin
+  while CurrentToken=ctkWHITESPACE do
+    GetNextToken;
+end;
 
 function TCSSParser.GetCurSource: TCSSString;
 begin
@@ -232,6 +251,7 @@ begin
 end;
 
 function TCSSParser.ParseAtRule: TCSSElement;
+// read unknown at-rule
 
 Var
   aRule : TCSSRuleElement;
@@ -258,7 +278,7 @@ begin
     aList:=TCSSListElement(CreateElement(TCSSListElement));
     While Not (CurrentToken in Term) do
       begin
-      aSel:=ParseSelector;
+      aSel:=ParseComponentValue;
       aList.AddChild(aSel);
       if CurrentToken=ctkCOMMA then
         begin
@@ -285,28 +305,203 @@ begin
   end;
 end;
 
-function TCSSParser.ParseExpression: TCSSElement;
+function TCSSParser.ParseAtMediaRule: TCSSAtRuleElement;
 
-    Function AllowRules(const aName : TCSSString) : Boolean;
+Var
+  aRule : TCSSAtRuleElement;
+  Term : TCSSTokens;
+  aLast , aToken: TCSSToken;
+  aList : TCSSListElement;
+  {$ifdef VerboseCSSParser}
+  aAt : TCSSString;
+  {$endif}
 
-    begin
-      result:=sameText(aName,'@media') or sameText(aName,'@print');
+begin
+  Inc(FRuleLevel);
+{$ifdef VerboseCSSParser}
+  aAt:=Format(' Level %d at (%d:%d)',[FRuleLevel,CurrentLine,CurrentPos]);
+  Writeln('Parse @media rule');
+{$endif}
+  Term:=[ctkLBRACE,ctkEOF,ctkSEMICOLON];
+  aRule:=TCSSAtRuleElement(CreateElement(TCSSAtRuleElement));
+  TCSSAtRuleElement(aRule).AtKeyWord:=CurrentTokenString;
+  GetNextToken;
+  aList:=nil;
+  try
+    aList:=TCSSListElement(CreateElement(TCSSListElement));
+    While Not (CurrentToken in Term) do
+      begin
+      aToken:=CurrentToken;
+        writeln('TCSSParser.ParseAtMediaRule Token=',CurrentToken);
+      case aToken of
+      ctkIDENTIFIER:
+        aList.AddChild(ParseIdentifier);
+      ctkLPARENTHESIS:
+        aList.AddChild(ParseMediaCondition);
+      else
+        Consume(ctkIDENTIFIER);
+      end;
+      if CurrentToken=ctkCOMMA then
+        begin
+        Consume(ctkCOMMA);
+        aRule.AddSelector(GetAppendElement(aList));
+        aList:=TCSSListElement(CreateElement(TCSSListElement));
+        end;
+      end;
+    aRule.AddSelector(GetAppendElement(aList));
+    aList:=nil;
+    aLast:=CurrentToken;
+    if (aLast<>ctkSEMICOLON) then
+      begin
+      Consume(ctkLBRACE);
+      aRule.AddChild(ParseRuleList(ctkRBRACE));
+      Consume(ctkRBRACE);
+      end;
+    Result:=aRule;
+    aRule:=nil;
+{$ifdef VerboseCSSParser}  Writeln('Done Parse @ rule ',aAt); {$endif}
+    Inc(FRuleLevel);
+  finally
+    aRule.Free;
+  end;
+end;
+
+function TCSSParser.ParseMediaCondition: TCSSElement;
+// for example:
+//   (color)
+//   (color: #fff)
+//   (30em <= width)
+//   (30em >= width > 20em)
+//   (not(MediaCondition))
+var
+  El: TCSSElement;
+  Bin: TCSSBinaryElement;
+  List: TCSSListElement;
+  aToken: TCSSToken;
+begin
+  Consume(ctkLPARENTHESIS);
+  {$IFDEF VerboseCSSParser}
+  writeln('TCSSParser.ParseMediaCondition START ',CurrentToken);
+  {$ENDIF}
+
+  El:=nil;
+  Bin:=nil;
+  List:=nil;
+  try
+    case CurrentToken of
+    ctkIDENTIFIER:
+      begin
+      El:=ParseIdentifier;
+      if TCSSIdentifierElement(El).Value='not' then
+        begin
+        // (not(mediacondition))
+        List:=TCSSListElement(CreateElement(TCSSListElement));
+        List.AddChild(El);
+        El:=nil;
+        List.AddChild(ParseMediaCondition());
+        Result:=List;
+        List:=nil;
+        exit;
+        end
+      else if CurrentToken=ctkCOLON then
+        begin
+        // (mediaproperty: value)
+        Bin:=TCSSBinaryElement(CreateElement(TCSSBinaryElement));
+        Bin.Left:=El;
+        El:=nil;
+        Consume(ctkCOLON);
+        Bin.Right:=ParseComponentValue;
+        Consume(ctkRPARENTHESIS);
+        Result:=Bin;
+        Bin:=nil;
+        exit;
+        end;
+      end;
+    ctkSTRING:
+      El:=ParseString;
+    ctkINTEGER:
+      El:=ParseInteger;
+    ctkFLOAT:
+      El:=ParseFloat;
+    else
+      Consume(ctkIDENTIFIER);
     end;
 
+    // read binaryoperator operand til bracket close
+    repeat
+      aToken:=CurrentToken;
+      {$IFDEF VerboseCSSResolver}
+      writeln('TCSSParser.ParseMediaCondition NEXT ',CurrentToken);
+      {$ENDIF}
+      case aToken of
+      ctkRPARENTHESIS:
+        begin
+        Result:=El;
+        GetNextToken;
+        break;
+        end;
+      ctkEQUALS,
+      ctkGE,ctkGT,ctkLE,ctkLT:
+        begin
+        Bin:=TCSSBinaryElement(CreateElement(TCSSBinaryElement));
+        Bin.Left:=El;
+        Bin.Operation:=TokenToBinaryOperation(aToken);
+        GetNextToken;
+        end;
+      else
+        Consume(ctkRPARENTHESIS);
+      end;
+
+      case CurrentToken of
+      ctkIDENTIFIER:
+        Bin.Right:=ParseIdentifier;
+      ctkSTRING:
+        Bin.Right:=ParseString;
+      ctkINTEGER:
+        Bin.Right:=ParseInteger;
+      ctkFLOAT:
+        Bin.Right:=ParseFloat;
+      else
+        Consume(ctkIDENTIFIER);
+      end;
+      El:=Bin;
+      Bin:=nil;
+    until false;
+
+  finally
+    List.Free;
+    Bin.Free;
+    El.Free;
+  end;
+
+  {$IFDEF VerboseCSSParser}
+  writeln('TCSSParser.ParseMediaCondition END');
+  {$ENDIF}
+end;
+
+function TCSSParser.ParseExpression: TCSSElement;
+
+  Function AllowRules(const aName : TCSSString) : Boolean;
+
+  begin
+    Result:=sameText(aName,'@print');
+  end;
+
 Const
   RuleTokens =
-       [ctkIDENTIFIER,ctkCLASSNAME,ctkHASH,ctkINTEGER, ctkPSEUDO,ctkPSEUDOFUNCTION,
-        ctkDOUBLECOLON,ctkSTAR,ctkTILDE,ctkCOLON,ctkLBRACKET];
+       [ctkIDENTIFIER,ctkCLASSNAME,ctkHASH,ctkINTEGER,
+        ctkPSEUDO,ctkPSEUDOFUNCTION,
+        ctkCOLON,ctkDOUBLECOLON,ctkSTAR,ctkTILDE,ctkLBRACKET];
 
 begin
   if CurrentToken in RuleTokens then
     Result:=ParseRule
   else if CurrentToken=ctkATKEYWORD then
     begin
-    if AllowRules(CurrentTokenString) then
-      Result:=ParseAtRule
+    if SameText(CurrentTokenString,'@media') then
+      Result:=ParseAtMediaRule
     else
-      Result:=ParseRule(True);
+      Result:=ParseAtRule;
     end
   else
     Result:=ParseComponentValueList;
@@ -411,26 +606,42 @@ begin
   Result:=aClass.Create(CurrentSource,CurrentLine,CurrentPos);
 end;
 
-function TCSSParser.ParseIdentifier: TCSSElement;
+function TCSSParser.ParseIdentifier: TCSSIdentifierElement;
 
 Var
   aValue : TCSSString;
-  aId : TCSSIdentifierElement;
 
 begin
   aValue:=CurrentTokenString;
-  if CurrentToken=ctkCLASSNAME then
-    aId:=TCSSClassNameElement(CreateElement(TCSSClassNameElement))
-  else
-    aId:=TCSSIdentifierElement(CreateElement(TCSSIdentifierElement));
-  aId.Value:=aValue;
-  try
-    Consume(CurrentToken);
-    Result:=aId;
-    aId:=nil;
-  finally
-    aId.Free;
-  end;
+  Result:=TCSSIdentifierElement(CreateElement(TCSSIdentifierElement));
+  Result.Value:=aValue;
+  GetNextToken;
+end;
+
+function TCSSParser.ParseHashIdentifier: TCSSHashIdentifierElement;
+
+Var
+  aValue : TCSSString;
+
+begin
+  aValue:=CurrentTokenString;
+  system.delete(aValue,1,1);
+  Result:=TCSSHashIdentifierElement(CreateElement(TCSSHashIdentifierElement));
+  Result.Value:=aValue;
+  GetNextToken;
+end;
+
+function TCSSParser.ParseClassName: TCSSClassNameElement;
+
+Var
+  aValue : TCSSString;
+
+begin
+  aValue:=CurrentTokenString;
+  system.delete(aValue,1,1);
+  Result:=TCSSClassNameElement(CreateElement(TCSSClassNameElement));
+  Result.Value:=aValue;
+  GetNextToken;
 end;
 
 function TCSSParser.ParseInteger: TCSSElement;
@@ -559,7 +770,7 @@ begin
   Result:=aRule.ChildCount;
 end;
 
-function TCSSParser.ParseRule(IsAt : Boolean = False): TCSSElement;
+function TCSSParser.ParseRule: TCSSElement;
 
 Var
   aRule : TCSSRuleElement;
@@ -575,7 +786,7 @@ begin
   Inc(FRuleLevel);
 {$IFDEF VerboseCSSParser}
   aAt:=Format(' Level %d at (%d:%d)',[FRuleLevel,CurrentLine,CurrentPos]);
-  Writeln('Parse rule. IsAt: ',IsAt,aAt);
+  Writeln('Parse rule.: ',aAt);
 {$ENDIF}
   case CurrentToken of
   ctkEOF: exit(nil);
@@ -587,14 +798,7 @@ begin
   end;
 
   Term:=[ctkLBRACE,ctkEOF,ctkSEMICOLON];
-  if IsAt then
-    begin
-    aRule:=TCSSAtRuleElement(CreateElement(TCSSAtRuleElement));
-    TCSSAtRuleElement(aRule).AtKeyWord:=CurrentTokenString;
-    GetNextToken;
-    end
-  else
-    aRule:=TCSSRuleElement(CreateElement(TCSSRuleElement));
+  aRule:=TCSSRuleElement(CreateElement(TCSSRuleElement));
   aList:=nil;
   try
     aList:=TCSSListElement(CreateElement(TCSSListElement));
@@ -616,7 +820,7 @@ begin
     if (aLast<>ctkSEMICOLON) then
       begin
       Consume(ctkLBrace);
-      ParseRuleBody(aRule,IsAt);
+      ParseRuleBody(aRule);
       Consume(ctkRBRACE);
       end;
     Result:=aRule;
@@ -654,7 +858,7 @@ begin
   end;
 end;
 
-function TCSSParser.ParseComponentValueList(allowRules : Boolean = True): TCSSElement;
+function TCSSParser.ParseComponentValueList(AllowRules : Boolean = True): TCSSElement;
 
 Const
   TermSeps = [ctkEquals,ctkPlus,ctkMinus,ctkAnd,ctkLT,ctkDIV,
@@ -672,18 +876,6 @@ Const
       aLeft:=Nil;
       Bin.Operation:=TokenToBinaryOperation(CurrentToken);
       Consume(CurrentToken);
-      if allowRules
-          and (Bin.Operation in [boTilde,boStar,boSquared,boPipe,boDollar]) then
-        begin
-        Consume(ctkEQUALS);
-        case Bin.Operation of
-        boTilde: Bin.Operation:=boTileEqual;
-        boStar: Bin.Operation:=boStarEqual;
-        boSquared: Bin.Operation:=boSquaredEqual;
-        boPipe: Bin.Operation:=boPipeEqual;
-        boDollar: Bin.Operation:=boDollarEqual;
-        end;
-        end;
       Bin.Right:=ParseComponentValue;
       if Bin.Right=nil then
         DoError(SErrUnexpectedToken ,[
@@ -707,7 +899,12 @@ begin
   List:=TCSSListElement(CreateElement(TCSSListElement));
   try
     if AllowRules and (CurrentToken in [ctkLBRACE,ctkATKEYWORD]) then
-      aFactor:=ParseRule(CurrentToken=ctkATKEYWORD)
+      begin
+      if CurrentToken=ctkATKEYWORD then
+        aFactor:=ParseAtRule
+      else
+        aFactor:=ParseRule;
+      end
     else
       aFactor:=ParseComponentValue;
     if aFactor=nil then
@@ -764,8 +961,8 @@ begin
     ctkFUNCTION : Result:=ParseCall('');
     ctkSTAR,
     ctkTILDE,
-    ctkIDENTIFIER,
-    ctkCLASSNAME : Result:=ParseIdentifier;
+    ctkIDENTIFIER: Result:=ParseIdentifier;
+    ctkCLASSNAME : Result:=ParseClassName;
   else
     Result:=nil;
 //    Consume(aToken);// continue
@@ -778,8 +975,184 @@ end;
 
 function TCSSParser.ParseSelector: TCSSElement;
 
+  function ParseSub: TCSSElement;
+  begin
+    Result:=nil;
+    Case CurrentToken of
+      ctkSTAR,
+      ctkIDENTIFIER : Result:=ParseIdentifier;
+      ctkHASH : Result:=ParseHashIdentifier;
+      ctkCLASSNAME : Result:=ParseClassName;
+      ctkLBRACKET: Result:=ParseAttributeSelector;
+      ctkPSEUDO: Result:=ParsePseudo;
+      ctkPSEUDOFUNCTION: Result:=ParseCall('');
+    else
+      DoError(SErrUnexpectedToken ,[
+               GetEnumName(TypeInfo(TCSSToken),Ord(CurrentToken)),
+               CurrentTokenString,
+               'selector'
+               ]);
+    end;
+  end;
+
+var
+  ok, OldReturnWhiteSpace: Boolean;
+  Bin: TCSSBinaryElement;
+  El: TCSSElement;
+  List: TCSSListElement;
+begin
+  Result:=nil;
+  El:=nil;
+  Bin:=nil;
+  List:=nil;
+  ok:=false;
+  //writeln('TCSSParser.ParseSelector START ',CurrentToken);
+  OldReturnWhiteSpace:=Scanner.ReturnWhiteSpace;
+  Scanner.ReturnWhiteSpace:=true;
+  try
+    repeat
+      //writeln('TCSSParser.ParseSelector LIST START ',CurrentToken);
+      // read list
+      List:=nil;
+      El:=ParseSub;
+      //writeln('TCSSParser.ParseSelector LIST NEXT ',CurrentToken);
+      while CurrentToken in [ctkSTAR,ctkIDENTIFIER,ctkCLASSNAME,ctkLBRACKET,ctkPSEUDO,ctkPSEUDOFUNCTION] do
+        begin
+        if List=nil then
+          begin
+          List:=TCSSListElement(CreateElement(TCSSListElement));
+          List.AddChild(El);
+          El:=List;
+          end;
+        List.AddChild(ParseSub);
+        end;
+      List:=nil;
+
+      // use element
+      if Bin<>nil then
+        Bin.Right:=El
+      else
+        Result:=El;
+      El:=nil;
+
+      //writeln('TCSSParser.ParseSelector LIST END ',CurrentToken);
+      SkipWhiteSpace;
+
+      case CurrentToken of
+      ctkLBRACE,ctkEOF,ctkSEMICOLON,ctkCOMMA:
+        break;
+      ctkGT,ctkPLUS,ctkTILDE,ctkPIPE:
+        begin
+        // combinator
+        Bin:=TCSSBinaryElement(CreateElement(TCSSBinaryElement));
+        Bin.Left:=Result;
+        Result:=Bin;
+        Bin.Operation:=TokenToBinaryOperation(CurrentToken);
+        GetNextToken;
+        SkipWhiteSpace;
+        end;
+      ctkIDENTIFIER,ctkCLASSNAME,ctkLBRACKET,ctkPSEUDO,ctkPSEUDOFUNCTION:
+        begin
+        // decendant combinator
+        Bin:=TCSSBinaryElement(CreateElement(TCSSBinaryElement));
+        Bin.Left:=Result;
+        Result:=Bin;
+        Bin.Operation:=boWhiteSpace;
+        end;
+      else
+        Consume(ctkLBRACE);
+      end;
+    until false;
+    ok:=true;
+  finally
+    Scanner.ReturnWhiteSpace:=OldReturnWhiteSpace;
+    if not ok then
+      begin
+      Result.Free;
+      El.Free;
+      List.Free;
+      Bin.Free;
+      end;
+  end;
+end;
+
+function TCSSParser.ParseAttributeSelector: TCSSElement;
+
+Var
+  aEl : TCSSElement;
+  aArray : TCSSArrayElement;
+  Bin: TCSSBinaryElement;
+  StrEl: TCSSStringElement;
+  aToken: TCSSToken;
+
+begin
+  Result:=Nil;
+  aArray:=TCSSArrayElement(CreateElement(TCSSArrayElement));
+  try
+    Consume(ctkLBRACKET);
+    SkipWhiteSpace;
+    aEl:=ParseWQName;
+    SkipWhiteSpace;
+    aToken:=CurrentToken;
+    case aToken of
+    ctkEQUALS,ctkTILDEEQUAL,ctkPIPEEQUAL,ctkSQUAREDEQUAL,ctkDOLLAREQUAL,ctkSTAREQUAL:
+      begin
+      // parse attr-matcher
+      Bin:=TCSSBinaryElement(CreateElement(TCSSBinaryElement));
+      aArray.AddChild(Bin);
+      Bin.Left:=aEl;
+      Bin.Operation:=TokenToBinaryOperation(aToken);
+      GetNextToken;
+      SkipWhiteSpace;
+      // parse value
+      case CurrentToken of
+      ctkSTRING:
+        begin
+        StrEl:=TCSSStringElement(CreateElement(TCSSStringElement));
+        StrEl.Value:=CurrentTokenString;
+        Bin.Right:=StrEl;
+        GetNextToken;
+        end;
+      ctkIDENTIFIER:
+        Bin.Right:=ParseIdentifier;
+      else
+        DoError(SErrUnexpectedToken ,[
+                 GetEnumName(TypeInfo(TCSSToken),Ord(CurrentToken)),
+                 CurrentTokenString,
+                 'attribute value'
+                 ]);
+      end;
+      end;
+    else
+      aArray.AddChild(aEl);
+    end;
+    SkipWhiteSpace;
+    while CurrentToken=ctkIDENTIFIER do
+      begin
+      // attribute modifier
+      // with CSS 5 there is only i and s, but for future compatibility read all
+      aArray.AddChild(ParseIdentifier);
+      SkipWhiteSpace;
+      end;
+    Consume(ctkRBRACKET);
+
+    Result:=aArray;
+    aArray:=nil;
+  finally
+    aArray.Free;
+  end;
+end;
+
+function TCSSParser.ParseWQName: TCSSElement;
 begin
-  Result:=ParseComponentValueList(false);
+  if CurrentToken<>ctkIDENTIFIER then
+    DoError(SErrUnexpectedToken ,[
+             GetEnumName(TypeInfo(TCSSToken),Ord(CurrentToken)),
+             CurrentTokenString,
+             'identifier'
+             ]);
+  Result:=ParseIdentifier;
+  // todo: parse optional ns-prefix
 end;
 
 function TCSSParser.ParseDeclaration(aIsAt: Boolean = false): TCSSDeclarationElement;
@@ -902,7 +1275,7 @@ begin
     if CurrentToken=ctkSTRING then
       Consume(ctkSTRING)
     else
-      Consume(ctkHASH);
+      Consume(ctkHASH); // e.g. #rrggbb
     aStr.Value:=aValue;
     While (CurrentToken in [ctkIDENTIFIER,ctkSTRING,ctkINTEGER,ctkFLOAT,ctkHASH]) do
       begin

+ 57 - 36
packages/fcl-css/src/fpcssresolver.pas

@@ -197,9 +197,9 @@ type
     procedure ComputeRule(aRule: TCSSRuleElement); virtual;
     function SelectorMatches(aSelector: TCSSElement; const TestNode: TCSSNode): TCSSSpecifity; virtual;
     function SelectorIdentifierMatches(Identifier: TCSSIdentifierElement; const TestNode: TCSSNode): TCSSSpecifity; virtual;
+    function SelectorHashIdentifierMatches(Identifier: TCSSHashIdentifierElement; const TestNode: TCSSNode): TCSSSpecifity; virtual;
     function SelectorClassNameMatches(aClassName: TCSSClassNameElement; const TestNode: TCSSNode): TCSSSpecifity; virtual;
-    function SelectorPseudoClassMatches(aPseudoClass: TCSSPseudoClassElement; const TestNode: TCSSNode): TCSSSpecifity; virtual;
-    function SelectorStringMatches(aString: TCSSStringElement; const TestNode: TCSSNode): TCSSSpecifity; virtual;
+    function SelectorPseudoClassMatches(aPseudoClass: TCSSPseudoClassElement; var TestNode: TCSSNode): TCSSSpecifity; virtual;
     function SelectorListMatches(aList: TCSSListElement; const TestNode: TCSSNode): TCSSSpecifity; virtual;
     function SelectorBinaryMatches(aBinary: TCSSBinaryElement; const TestNode: TCSSNode): TCSSSpecifity; virtual;
     function SelectorArrayMatches(anArray: TCSSArrayElement; const TestNode: TCSSNode): TCSSSpecifity; virtual;
@@ -350,6 +350,15 @@ end;
 
 function TCSSResolver.SelectorMatches(aSelector: TCSSElement;
   const TestNode: TCSSNode): TCSSSpecifity;
+
+  procedure MatchPseudo;
+  var
+    aNode: TCSSNode;
+  begin
+    aNode:=TestNode;
+    Result:=SelectorPseudoClassMatches(TCSSPseudoClassElement(aSelector),aNode);
+  end;
+
 var
   C: TClass;
 begin
@@ -357,12 +366,12 @@ begin
   C:=aSelector.ClassType;
   if C=TCSSIdentifierElement then
     Result:=SelectorIdentifierMatches(TCSSIdentifierElement(aSelector),TestNode)
+  else if C=TCSSHashIdentifierElement then
+    Result:=SelectorHashIdentifierMatches(TCSSHashIdentifierElement(aSelector),TestNode)
   else if C=TCSSClassNameElement then
     Result:=SelectorClassNameMatches(TCSSClassNameElement(aSelector),TestNode)
   else if C=TCSSPseudoClassElement then
-    Result:=SelectorPseudoClassMatches(TCSSPseudoClassElement(aSelector),TestNode)
-  else if C=TCSSStringElement then
-    Result:=SelectorStringMatches(TCSSStringElement(aSelector),TestNode)
+    MatchPseudo
   else if C=TCSSBinaryElement then
     Result:=SelectorBinaryMatches(TCSSBinaryElement(aSelector),TestNode)
   else if C=TCSSArrayElement then
@@ -389,11 +398,20 @@ begin
     if croErrorOnUnknownName in Options then
       DoError(20220911230224,'Unknown CSS selector type name "'+Identifier.Name+'"',Identifier);
     Result:=CSSSpecifityInvalid;
-  end else
-  begin
-    if TypeID=TestNode.GetCSSTypeID then
-      Result:=CSSSpecifityType;
-  end;
+  end else if TypeID=TestNode.GetCSSTypeID then
+    Result:=CSSSpecifityType;
+end;
+
+function TCSSResolver.SelectorHashIdentifierMatches(
+  Identifier: TCSSHashIdentifierElement; const TestNode: TCSSNode
+  ): TCSSSpecifity;
+var
+  aValue: TCSSString;
+begin
+  Result:=CSSSpecifityNoMatch;
+  aValue:=Identifier.Value;
+  if TestNode.GetCSSID=aValue then
+    Result:=CSSSpecifityIdentifier;
 end;
 
 function TCSSResolver.SelectorClassNameMatches(
@@ -401,16 +419,16 @@ function TCSSResolver.SelectorClassNameMatches(
 var
   aValue: TCSSString;
 begin
-  aValue:=copy(aClassName.Name,2,255);
+  aValue:=aClassName.Name;
   if TestNode.HasCSSClass(aValue) then
     Result:=CSSSpecifityClass
   else
     Result:=CSSSpecifityNoMatch;
+  //writeln('TCSSResolver.SelectorClassNameMatches ',aValue,' ',Result);
 end;
 
 function TCSSResolver.SelectorPseudoClassMatches(
-  aPseudoClass: TCSSPseudoClassElement; const TestNode: TCSSNode
-  ): TCSSSpecifity;
+  aPseudoClass: TCSSPseudoClassElement; var TestNode: TCSSNode): TCSSSpecifity;
 var
   PseudoID: TCSSNumericalID;
 begin
@@ -452,35 +470,38 @@ begin
   end;
 end;
 
-function TCSSResolver.SelectorStringMatches(aString: TCSSStringElement;
-  const TestNode: TCSSNode): TCSSSpecifity;
-// id selector #name
-var
-  aValue: TCSSString;
-begin
-  Result:=CSSSpecifityNoMatch;
-  if aString.Children.Count>0 then
-    DoError(20220910113909,'Invalid CSS string selector',aString.Children[0]);
-  aValue:=aString.Value;
-  if aValue[1]<>'#' then
-    DoError(20220910114014,'Invalid CSS selector',aString);
-  System.Delete(aValue,1,1);
-  if aValue='' then
-    DoError(20220910114133,'Invalid CSS identifier selector',aString);
-  if aValue=TestNode.GetCSSID then
-    Result:=CSSSpecifityIdentifier;
-end;
-
 function TCSSResolver.SelectorListMatches(aList: TCSSListElement;
   const TestNode: TCSSNode): TCSSSpecifity;
 var
   i: Integer;
+  El: TCSSElement;
+  C: TClass;
+  Specifity: TCSSSpecifity;
+  aNode: TCSSNode;
 begin
-  Result:=CSSSpecifityInvalid;
+  Result:=0;
+  {$IFDEF VerboseCSSResolver}
   writeln('TCSSResolver.SelectorListMatches ChildCount=',aList.ChildCount);
+  {$ENDIF}
+  aNode:=TestNode;
   for i:=0 to aList.ChildCount-1 do
-    writeln('TCSSResolver.SelectorListMatches ',i,' ',GetCSSObj(aList.Children[i]),' AsString=',aList.Children[i].AsString);
-  DoError(20220910115531,'Invalid CSS list selector',aList);
+    begin
+    El:=aList.Children[i];
+    {$IFDEF VerboseCSSResolver}
+    writeln('TCSSResolver.SelectorListMatches ',i,' ',GetCSSObj(El),' AsString=',El.AsString);
+    {$ENDIF}
+    C:=El.ClassType;
+    if (C=TCSSIdentifierElement) and (i>0) then
+      DoError(20220914163218,'Type selector must be first',aList)
+    else if C=TCSSPseudoClassElement then
+    begin
+      Specifity:=SelectorPseudoClassMatches(TCSSPseudoClassElement(El),aNode);
+    end else
+      Specifity:=SelectorMatches(El,aNode);
+    if Specifity<0 then
+      exit(Specifity);
+    inc(Result,Specifity);
+    end;
 end;
 
 function TCSSResolver.SelectorBinaryMatches(aBinary: TCSSBinaryElement;
@@ -652,7 +673,7 @@ begin
     // contains substring
     if (RightValue<>'') and (Pos(RightValue,LeftValue)>0) then
       Result:=CSSSpecifityClass;
-  boTileEqual:
+  boTildeEqual:
     // contains word
     if PosWord(RightValue,LeftValue)>0 then
       Result:=CSSSpecifityClass;

+ 54 - 14
packages/fcl-css/src/fpcssscanner.pp

@@ -39,16 +39,20 @@ Type
     ctkEQUALS,
     ctkAND,
     ctkTILDE,
+    ctkTILDEEQUAL,
     ctkPLUS,
     ctkCOLON,
     ctkDOUBLECOLON,
     ctkDOT,
     ctkDIV,
     ctkGT,
+    ctkGE,
     ctkLT,
+    ctkLE,
     ctkPERCENTAGE,
     ctkMINUS,
     ctkSTAR,
+    ctkSTAREQUAL,
     ctkINTEGER,
     ctkFLOAT,
     ctkHASH,
@@ -63,9 +67,12 @@ Type
     ctkPSEUDO,
     ctkPSEUDOFUNCTION,
     ctkSQUARED,
+    ctkSQUAREDEQUAL,
     ctkUNICODERANGE,
     ctkPIPE,
-    ctkDOLLAR
+    ctkPIPEEQUAL,
+    ctkDOLLAR,
+    ctkDOLLAREQUAL
    );
   TCSSTokens = Set of TCSSToken;
 
@@ -588,6 +595,7 @@ begin
   TokenStart := TokenStr;
   IsPseudo:=False;
   IsAt:=TokenStr[0]='@';
+  IsFunc:=false;
   For Len:=1 to 2 do
     if TokenStr[0]=':' then
       begin
@@ -617,9 +625,9 @@ begin
     SetLength(FCurTokenString,Olen+Len);
     if Len > 0 then
       Move(TokenStart^,FCurTokenString[Olen+1],Len);
-     if IsEscape then
-       Inc(TokenStr);
-     TokenStart := TokenStr;
+    if IsEscape then
+      Inc(TokenStr);
+    TokenStart := TokenStr;
   until Not IsEscape;
   // Some specials
   if (CurTokenString[1]='.') and not IsFunc then
@@ -670,7 +678,7 @@ end;
 function TCSSScanner.DoFetchToken: TCSSToken;
 
 
-  Procedure CharToken(aToken : TCSSToken); inline;
+  Procedure CharToken(aToken : TCSSToken);
 
   begin
     FCurTokenString:=TokenStr[0];
@@ -678,6 +686,14 @@ function TCSSScanner.DoFetchToken: TCSSToken;
     Result:=aToken;
   end;
 
+  Procedure TwoCharsToken(aToken : TCSSToken);
+
+  begin
+    FCurTokenString:=TokenStr[0]+TokenStr[1];
+    Inc(TokenStr,2);
+    Result:=aToken;
+  end;
+
 begin
   if TokenStr = nil then
     begin
@@ -721,17 +737,33 @@ begin
     '&': CharToken(ctkAnd);
     '{': CharToken( ctkLBRACE);
     '}': CharToken(ctkRBRACE);
-    '*': if Not (csoExtendedIdentifiers in Options) then
-           CharToken(ctkSTAR)
-         else if TokenStr[1] in AlNumIden then
+    '*': if TokenStr[1]='=' then
+           TwoCharsToken(ctkSTAREQUAL)
+         else if (csoExtendedIdentifiers in Options) and (TokenStr[1] in AlNumIden) then
            Result:=DoIdentifierLike
          else
            CharToken(ctkSTAR);
-    '^': CharToken(ctkSQUARED);
+    '^':
+      if TokenStr[1]='=' then
+        TwoCharsToken(ctkSQUAREDEQUAL)
+      else
+        CharToken(ctkSQUARED);
     ',': CharToken(ctkCOMMA);
-    '~': CharToken(ctkTILDE);
-    '|': CharToken(ctkPIPE);
-    '$': CharToken(ctkDOLLAR);
+    '~':
+      if TokenStr[1]='=' then
+        TwoCharsToken(ctkTILDEEQUAL)
+      else
+        CharToken(ctkTILDE);
+    '|':
+      if TokenStr[1]='=' then
+        TwoCharsToken(ctkPIPEEQUAL)
+      else
+        CharToken(ctkPIPE);
+    '$':
+      if TokenStr[1]='=' then
+        TwoCharsToken(ctkDOLLAREQUAL)
+      else
+        CharToken(ctkDOLLAR);
     ';': CharToken(ctkSEMICOLON);
     '@': Result:=DoIdentifierLike;
     ':':
@@ -757,8 +789,16 @@ begin
       else
         CharToken(ctkDOT);
       end;
-    '>': CharToken(ctkGT);
-    '<': CharToken(ctkLT);
+    '>':
+      if TokenStr[1]='=' then
+        TwoCharsToken(ctkGE)
+      else
+        CharToken(ctkGT);
+    '<':
+      if TokenStr[1]='=' then
+        TwoCharsToken(ctkLE)
+      else
+        CharToken(ctkLT);
     '(': CharToken(ctkLPARENTHESIS);
     ')': CharToken(ctkRPARENTHESIS);
     '[': CharToken(ctkLBRACKET);

+ 56 - 20
packages/fcl-css/src/fpcsstree.pp

@@ -26,10 +26,23 @@ uses contnrs, Classes;
 Type
   TCSSString = UTF8String;
   TCSSUnits = (cuNONE, cuPX,cuPERCENT,cuREM,cuEM,cuPT,cuFR,cuVW,cuVH,cuDEG);
-  TCSSType = (csstUNKNOWN, csstINTEGER, csstSTRING, csstFLOAT,
-              csstIDENTIFIER, csstCLASSNAME, csstPSEUDOCLASS, csstCOMPOUND, csstRULE,
-              csstDECLARATION, csstBINARYOP, csstCALL, csstUNARYOP, csstARRAY, csstURL,
-              csstUNICODERANGE,csstLIST);
+  TCSSType = (
+    csstUnknown,
+    csstInteger, csstString, csstFloat,
+    csstIdentifier, // name
+    csstHashIdentifier, // #name
+    csstClassname, // .name
+    csstPseudoClass, // :name, ::name
+    csstCompound,
+    csstRule,
+    csstDeclaration,
+    csstBinaryOp,
+    csstCall, // name(, :name(, ::name(
+    csstUnaryOp,
+    csstArray, // []
+    csstURL, // url()
+    csstUnicodeRange,
+    csstList);
 
   TCSSElement = class;
 
@@ -157,10 +170,10 @@ Type
   end;
 
   { TCSSBinaryElement }
-  TCSSBinaryOperation = (boEquals,boPlus,boMinus,boAnd,boLT,boGT,boDIV,
+  TCSSBinaryOperation = (boEquals,boPlus,boMinus,boAnd,boLE,boLT,boGE,boGT,boDIV,
                          boStar,boTilde,boColon, boDoubleColon,boSquared,
-                         boPipe, boDollar,
-                         boStarEqual,boTileEqual,boSquaredEqual,boPipeEqual,boDollarEqual);
+                         boPipe, boDollar, boWhiteSpace,
+                         boStarEqual,boTildeEqual,boSquaredEqual,boPipeEqual,boDollarEqual);
   TCSSBinaryElement = Class(TCSSBaseUnaryElement)
   private
     FLeft: TCSSElement;
@@ -228,6 +241,15 @@ Type
     Property Name : TCSSString Read GetName;
   end;
 
+  { TCSSHashIdentifierElement }
+
+  TCSSHashIdentifierElement = Class(TCSSIdentifierElement)
+  Protected
+    function GetAsString(aFormat : Boolean; const aIndent : TCSSString): TCSSString; override;
+  Public
+    Class function CSSType : TCSSType; override;
+  end;
+
   { TCSSClassNameElement }
 
   TCSSClassNameElement = Class(TCSSIdentifierElement)
@@ -377,7 +399,7 @@ Const
   UnaryOperators : Array[TCSSUnaryOperation] of TCSSString =
         ('::','-','+','/');
   BinaryOperators : Array[TCSSBinaryOperation] of TCSSString =
-        ('=','+','-','and','<','>','/','*','~',':','::','^','|','$',
+        ('=','+','-','and','<=','<','>=','>','/','*','~',':','::','^','|','$',' ',
          '*=','~=','^=','|=','$=');
 
 implementation
@@ -556,7 +578,7 @@ end;
 
 class function TCSSUnicodeRangeElement.CSSType: TCSSType;
 begin
-  Result:=csstUNICODERANGE;
+  Result:=csstUnicodeRange;
 end;
 
 { TCSSURLElement }
@@ -646,7 +668,7 @@ end;
 
 class function TCSSDeclarationElement.CSSType: TCSSType;
 begin
-  Result:=csstDECLARATION;
+  Result:=csstDeclaration;
 end;
 
 destructor TCSSDeclarationElement.Destroy;
@@ -667,7 +689,7 @@ end;
 
 class function TCSSUnaryElement.CSSType: TCSSType;
 begin
-  Result:=csstUNARYOP;
+  Result:=csstUnaryOp;
 end;
 
 function TCSSUnaryElement.GetAsString(aFormat: Boolean;
@@ -778,7 +800,7 @@ end;
 
 class function TCSSRuleElement.CSSType: TCSSType;
 begin
-  Result:=csstRULE;
+  Result:=csstRule;
 end;
 
 destructor TCSSRuleElement.Destroy;
@@ -815,7 +837,7 @@ end;
 
 class function TCSSPseudoClassElement.CSSType: TCSSType;
 begin
-  Result:=csstPSEUDOCLASS;
+  Result:=csstPseudoClass;
 end;
 
 { TCSSChildrenElement }
@@ -942,7 +964,7 @@ end;
 
 class function TCSSStringElement.CSSType: TCSSType;
 begin
-  Result:=csstSTRING;
+  Result:=csstString;
 end;
 
 destructor TCSSStringElement.Destroy;
@@ -958,12 +980,12 @@ function TCSSClassNameElement.GetAsString(aFormat: Boolean;
 begin
   if aFormat then ;
   if aIndent='' then ;
-  Result:=Copy(Value,1,1)+StringToIdentifier(Copy(Value,2,Length(Value)-1));
+  Result:='.'+StringToIdentifier(Value);
 end;
 
 class function TCSSClassNameElement.CSSType: TCSSType;
 begin
-  Result:=csstCLASSNAME;
+  Result:=csstClassname;
 end;
 
 { TCSSIdentifierElement }
@@ -983,11 +1005,25 @@ end;
 
 class function TCSSIdentifierElement.CSSType: TCSSType;
 begin
-  Result:=csstIDENTIFIER;
+  Result:=csstIdentifier;
 end;
 
-{ TCSSArrayElement }
+{ TCSSHashIdentifierElement }
+
+function TCSSHashIdentifierElement.GetAsString(aFormat: Boolean;
+  const aIndent: TCSSString): TCSSString;
+begin
+  if aFormat then ;
+  if aIndent='' then ;
+  Result:='#'+StringToIdentifier(Value);
+end;
 
+class function TCSSHashIdentifierElement.CSSType: TCSSType;
+begin
+  Result:=csstHashIdentifier;
+end;
+
+{ TCSSArrayElement }
 
 procedure TCSSArrayElement.SetPrefix(AValue: TCSSElement);
 begin
@@ -1153,7 +1189,7 @@ end;
 
 class function TCSSIntegerElement.CSSType: TCSSType;
 begin
-  Result:=csstINTEGER;
+  Result:=csstInteger;
 end;
 
 { TCSSBinaryElement }
@@ -1191,7 +1227,7 @@ end;
 
 class function TCSSBinaryElement.CSSType: TCSSType;
 begin
-  Result:=csstBINARYOP;
+  Result:=csstBinaryOp;
 end;
 
 procedure TCSSBinaryElement.IterateChildren(aVisitor: TCSSTreeVisitor);

+ 26 - 23
packages/fcl-css/tests/tccssparser.pp

@@ -69,8 +69,8 @@ type
     Procedure TestPrefixedEmptyRule;
     Procedure TestClassPrefixedEmptyRule;
     Procedure TestHashPrefixedEmptyRule;
-    procedure TestDoublePrefixedEmptyRule;
-    procedure TestDoubleMixedPrefixedEmptyRule;
+    procedure TestDescendantPrefixedEmptyRule;
+    procedure TestDescendantMixedPrefixedEmptyRule;
     procedure TestAttributePrefixedEmptyRule;
     procedure TestAttributeSquaredEqualRule;
     procedure TestAttributePipeEqualRule;
@@ -343,61 +343,59 @@ begin
   AssertEquals('No rule children',0,R.ChildCount);
   AssertEquals('selector count',1,R.SelectorCount);
   sel:=TCSSClassNameElement(CheckClass('Selector', TCSSClassNameElement,R.Selectors[0]));
-  AssertEquals('Sel name','.a',Sel.Value);
+  AssertEquals('Sel name','a',Sel.Value);
 end;
 
 procedure TTestCSSParser.TestHashPrefixedEmptyRule;
 var
   R : TCSSRuleElement;
-  sel: TCSSStringElement;
+  sel: TCSSHashIdentifierElement;
 
 begin
   ParseRule('#a { }');
   R:=TCSSRuleElement(CheckClass('Rule',TCSSRuleElement,FirstRule));
   AssertEquals('No rule children',0,R.ChildCount);
   AssertEquals('selector count',1,R.SelectorCount);
-  sel:=TCSSStringElement(CheckClass('Selector', TCSSStringElement,R.Selectors[0]));
-  AssertEquals('Sel name','#a',Sel.Value);
+  sel:=TCSSHashIdentifierElement(CheckClass('Selector', TCSSHashIdentifierElement,R.Selectors[0]));
+  AssertEquals('Sel name','a',Sel.Value);
 end;
 
-procedure TTestCSSParser.TestDoublePrefixedEmptyRule;
+procedure TTestCSSParser.TestDescendantPrefixedEmptyRule;
 
 var
   R : TCSSRuleElement;
   sel: TCSSIdentifierElement;
-  List : TCSSListElement;
+  Bin: TCSSBinaryElement;
 
 begin
   ParseRule('a b { }');
   R:=TCSSRuleElement(CheckClass('Rule',TCSSRuleElement,FirstRule));
   AssertEquals('No rule children',0,R.ChildCount);
   AssertEquals('selector count',1,R.SelectorCount);
-  List:=TCSSListElement(CheckClass('Selector', TCSSListElement,R.Selectors[0]));
-  AssertEquals('selector list count',2,List.ChildCount);
-  sel:=TCSSIdentifierElement(CheckClass('Selector', TCSSIdentifierElement,List[0]));
+  Bin:=TCSSBinaryElement(CheckClass('Selector', TCSSBinaryElement,R.Selectors[0]));
+  sel:=TCSSIdentifierElement(CheckClass('Selector', TCSSIdentifierElement,Bin.Left));
   AssertEquals('Sel 1 name','a',Sel.Value);
-  sel:=TCSSIdentifierElement(CheckClass('Selector', TCSSIdentifierElement,List[1]));
+  sel:=TCSSIdentifierElement(CheckClass('Selector', TCSSIdentifierElement,Bin.Right));
   AssertEquals('Sel 2 name','b',Sel.Value);
 end;
 
-procedure TTestCSSParser.TestDoubleMixedPrefixedEmptyRule;
+procedure TTestCSSParser.TestDescendantMixedPrefixedEmptyRule;
 
 var
   R : TCSSRuleElement;
   sel: TCSSIdentifierElement;
-  List : TCSSListElement;
+  Bin: TCSSBinaryElement;
 
 begin
   ParseRule('a .b { }');
   R:=TCSSRuleElement(CheckClass('Rule',TCSSRuleElement,FirstRule));
   AssertEquals('No rule children',0,R.ChildCount);
   AssertEquals('selector count',1,R.SelectorCount);
-  List:=TCSSListElement(CheckClass('Selector', TCSSListElement,R.Selectors[0]));
-  AssertEquals('selector list count',2,List.ChildCount);
-  sel:=TCSSIdentifierElement(CheckClass('Selector', TCSSIdentifierElement,List[0]));
+  Bin:=TCSSBinaryElement(CheckClass('Selector', TCSSBinaryElement,R.Selectors[0]));
+  sel:=TCSSIdentifierElement(CheckClass('Selector', TCSSIdentifierElement,Bin.Left));
   AssertEquals('Sel 1 name','a',Sel.Value);
-  sel:=TCSSClassNameElement(CheckClass('Selector', TCSSClassNameElement,List[1]));
-  AssertEquals('Sel 2 name','.b',Sel.Value);
+  sel:=TCSSClassNameElement(CheckClass('Selector', TCSSClassNameElement,Bin.Right));
+  AssertEquals('Sel 2 name','b',Sel.Value);
 end;
 
 procedure TTestCSSParser.TestAttributePrefixedEmptyRule;
@@ -406,14 +404,17 @@ var
   sel: TCSSArrayElement;
   id : TCSSIdentifierElement;
   bin : TCSSBinaryElement;
+  List: TCSSListElement;
 
 begin
   ParseRule('a[b="c"] { }');
   R:=TCSSRuleElement(CheckClass('Rule',TCSSRuleElement,FirstRule));
   AssertEquals('No rule children',0,R.ChildCount);
   AssertEquals('selector count',1,R.SelectorCount);
-  sel:=TCSSArrayElement(CheckClass('Selector', TCSSArrayElement,R.Selectors[0]));
-  Id:=TCSSIdentifierElement(CheckClass('Array prefix',TCSSIdentifierElement,Sel.Prefix));
+  List:=TCSSListElement(CheckClass('Selector', TCSSListElement,R.Selectors[0]));
+  AssertEquals('list selector count',2,List.ChildCount);
+  Id:=TCSSIdentifierElement(CheckClass('prefix',TCSSIdentifierElement,List[0]));
+  sel:=TCSSArrayElement(CheckClass('Attribute Selector', TCSSArrayElement,List[1]));
   AssertEquals('Prefix name','a',Id.Value);
   AssertEquals('Array count',1,Sel.ChildCount);
   Bin:=TCSSBinaryElement(CheckClass('Bin',TCSSBinaryElement,sel.children[0]));
@@ -529,7 +530,7 @@ begin
     Fail('no prefix');
   AssertEquals('Array count',1,Sel.ChildCount);
   Bin:=TCSSBinaryElement(CheckClass('Bin',TCSSBinaryElement,sel.children[0]));
-  AssertEquals('Binary op',boTileEqual,Bin.Operation);
+  AssertEquals('Binary op',boTildeEqual,Bin.Operation);
   Left:=TCSSIdentifierElement(CheckClass('Bin.Left',TCSSIdentifierElement,Bin.Left));
   AssertEquals('left=b','b',Left.Value);
   CheckClass('Bin.Right',TCSSStringElement,Bin.Right);
@@ -765,9 +766,11 @@ end;
 
 procedure TTestCSSParser.TestImportAtKeyWord;
 var
+  Rule: TCSSRuleElement;
   R : TCSSAtRuleElement;
 begin
-  R:=TCSSAtRuleElement(CheckClass('at',TCSSAtRuleElement,ParseRule('@import url("abc.css");')));
+  Rule:=ParseRule('@import url("abc.css");');
+  R:=TCSSAtRuleElement(CheckClass('at',TCSSAtRuleElement,Rule));
   AssertEquals('selector count',1,R.SelectorCount);
   AssertEquals('declaration count',0,R.ChildCount);
 end;

+ 3 - 1
packages/fcl-css/tests/tccssresolver.pp

@@ -354,7 +354,8 @@ begin
   Button1.Parent:=Doc.Root;
 
   Button2:=TDemoButton.Create(Doc);
-  Button2.CSSClasses.Add('west south');
+  Button2.CSSClasses.DelimitedText:='west south';
+  AssertEquals('Button2.CSSClasses.Count',2,Button2.CSSClasses.Count);
   Button2.Parent:=Doc.Root;
 
   Doc.Style:='.west.south { left: 10px; }';
@@ -1201,6 +1202,7 @@ begin
   inherited Create(AOwner);
   FNodes:=TFPObjectList.Create(false);
   FCSSClasses:=TStringList.Create;
+  FCSSClasses.Delimiter:=' ';
   for a in TDemoNodeAttribute do
     FAttributeValues[a]:=FAttributeInitialValues[a];
 end;

+ 1 - 1
packages/fcl-css/tests/tccssscanner.pp

@@ -44,7 +44,7 @@ type
 
   { TTestCSSScanner }
 
-  TTestCSSScanner= class(TTestCase)
+  TTestCSSScanner = class(TTestCase)
   Private
     FPSeudoDisabled,
     FNeedWhiteSpace : Boolean;

+ 26 - 4
packages/fcl-css/tests/tccsstree.pp

@@ -35,6 +35,7 @@ type
     Procedure TestSTRING;
     Procedure TestFLOAT;
     Procedure TestIDENTIFIER;
+    Procedure TestHASHIDENTIFIER;
     Procedure TestCLASSNAME;
     Procedure TestPSEUDOCLASS;
     Procedure TestCOMPOUND;
@@ -56,6 +57,7 @@ type
     Procedure TestSTRING;
     Procedure TestFLOAT;
     Procedure TestIDENTIFIER;
+    Procedure TestHashIDENTIFIER;
     Procedure TestCLASSNAME;
     Procedure TestPSEUDOCLASS;
     Procedure TestCOMPOUND;
@@ -104,6 +106,7 @@ type
     Procedure TestSTRING;
     Procedure TestFLOAT;
     Procedure TestIDENTIFIER;
+    Procedure TestHASHIDENTIFIER;
     Procedure TestCLASSNAME;
     Procedure TestPSEUDOCLASS;
     Procedure TestCOMPOUND;
@@ -213,6 +216,14 @@ begin
   CheckElement(0,Element);
 end;
 
+procedure TCSSTreeVisitorTest.TestHASHIDENTIFIER;
+begin
+  CreateElement(TCSSHashIdentifierElement);
+  Element.Iterate(Visitor);
+  CheckCount(1);
+  CheckElement(0,Element);
+end;
+
 procedure TCSSTreeVisitorTest.TestCLASSNAME;
 begin
   CreateElement(TCSSClassNameElement);
@@ -406,9 +417,15 @@ begin
   AssertEquals('Value','abc',Element.AsString);
 end;
 
+procedure TCSSTreeAsStringTest.TestHashIDENTIFIER;
+begin
+  TCSSHashIdentifierElement(CreateElement(TCSSHashIdentifierElement)).Value:='abc';
+  AssertEquals('Value','#abc',Element.AsString);
+end;
+
 procedure TCSSTreeAsStringTest.TestCLASSNAME;
 begin
-  TCSSClassNameElement(CreateElement(TCSSClassNameElement)).Value:='.abc';
+  TCSSClassNameElement(CreateElement(TCSSClassNameElement)).Value:='abc';
   AssertEquals('Value','.abc',Element.AsString);
 end;
 
@@ -612,7 +629,7 @@ end;
 
 procedure TCSSTreeTypeTest.TestINTEGER;
 begin
-  AssertEquals('Type',csstINTEGER,CreateElement(TCSSIntegerElement).CSSType);
+  AssertEquals('Type',csstInteger,CreateElement(TCSSIntegerElement).CSSType);
 end;
 
 procedure TCSSTreeTypeTest.TestSTRING;
@@ -628,12 +645,17 @@ end;
 
 procedure TCSSTreeTypeTest.TestIDENTIFIER;
 begin
-  AssertEquals('Type',csstINTEGER,CreateElement(TCSSIntegerElement).CSSType);
+  AssertEquals('Type',csstIdentifier,CreateElement(TCSSIdentifierElement).CSSType);
+end;
+
+procedure TCSSTreeTypeTest.TestHASHIDENTIFIER;
+begin
+  AssertEquals('Type',csstHashIdentifier,CreateElement(TCSSHashIdentifierElement).CSSType);
 end;
 
 procedure TCSSTreeTypeTest.TestCLASSNAME;
 begin
-  AssertEquals('Type',csstClassName,CreateElement(TCSSClassNameElement).CSSType);
+  AssertEquals('Type',csstClassname,CreateElement(TCSSClassNameElement).CSSType);
 end;
 
 procedure TCSSTreeTypeTest.TestPSEUDOCLASS;