Browse Source

fcl-css: fixed skipping invalid, skip utf8bom, nth-child

mattias 2 years ago
parent
commit
abed465c08

+ 127 - 27
packages/fcl-css/src/fpcssparser.pp

@@ -45,16 +45,19 @@ Type
   protected
     Procedure DoWarn(const Msg : TCSSString);
     Procedure DoWarn(const Fmt : TCSSString; const Args : Array of const);
+    Procedure DoWarnExpectedButGot(const Expected: string);
     Procedure DoError(const Msg : TCSSString);
     Procedure DoError(const Fmt : TCSSString; const Args : Array of const);
+    Procedure DoErrorExpectedButGot(const Expected: string);
     Procedure Consume(aToken : TCSSToken);
     Procedure SkipWhiteSpace;
     function ParseComponentValueList(AllowRules: Boolean=True): TCSSElement;
     function ParseComponentValue: TCSSElement;
     function ParseExpression: TCSSElement;
     function ParseRule: TCSSElement;
-    function ParseAtRule: TCSSElement;
+    function ParseAtUnknownRule: TCSSElement;
     function ParseAtMediaRule: TCSSAtRuleElement;
+    function ParseAtSimpleRule: TCSSAtRuleElement;
     function ParseMediaCondition: TCSSElement;
     function ParseRuleList(aStopOn : TCSStoken = ctkEOF): TCSSElement;
     function ParseSelector: TCSSElement;
@@ -85,6 +88,7 @@ Type
     Constructor Create(AScanner : TCSSScanner); virtual;
     Destructor Destroy; override;
     Function Parse : TCSSElement;
+    Function ParseInline : TCSSElement;
     Property CurrentToken : TCSSToken Read FCurrent;
     Property CurrentTokenString : TCSSString Read FCurrentTokenString;
     Function GetNextToken : TCSSToken;
@@ -176,6 +180,15 @@ begin
   DoError(SafeFormat(Fmt,Args));
 end;
 
+procedure TCSSParser.DoErrorExpectedButGot(const Expected: string);
+begin
+  DoError(SErrUnexpectedToken ,[
+           GetEnumName(TypeInfo(TCSSToken),Ord(CurrentToken)),
+           CurrentTokenString,
+           Expected
+           ]);
+end;
+
 procedure TCSSParser.Consume(aToken: TCSSToken);
 begin
   if CurrentToken<>aToken then
@@ -230,6 +243,15 @@ begin
   DoWarn(SafeFormat(Fmt,Args));
 end;
 
+procedure TCSSParser.DoWarnExpectedButGot(const Expected: string);
+begin
+  DoWarn(SErrUnexpectedToken ,[
+           GetEnumName(TypeInfo(TCSSToken),Ord(CurrentToken)),
+           CurrentTokenString,
+           Expected
+           ]);
+end;
+
 constructor TCSSParser.Create(AInput: TStream; ExtraScannerOptions : TCSSScannerOptions = []);
 begin
   FInput:=AInput;
@@ -266,7 +288,7 @@ begin
     aList.Free;
 end;
 
-function TCSSParser.ParseAtRule: TCSSElement;
+function TCSSParser.ParseAtUnknownRule: TCSSElement;
 // read unknown at-rule
 
 Var
@@ -324,13 +346,13 @@ end;
 function TCSSParser.ParseAtMediaRule: TCSSAtRuleElement;
 
 Var
+  {$ifdef VerboseCSSParser}
+  aAt : TCSSString;
+  {$endif}
   aRule : TCSSAtRuleElement;
   Term : TCSSTokens;
   aLast , aToken: TCSSToken;
   aList : TCSSListElement;
-  {$ifdef VerboseCSSParser}
-  aAt : TCSSString;
-  {$endif}
 
 begin
   Inc(FRuleLevel);
@@ -340,7 +362,7 @@ begin
 {$endif}
   Term:=[ctkLBRACE,ctkEOF,ctkSEMICOLON];
   aRule:=TCSSAtRuleElement(CreateElement(TCSSAtRuleElement));
-  TCSSAtRuleElement(aRule).AtKeyWord:=CurrentTokenString;
+  aRule.AtKeyWord:=CurrentTokenString;
   GetNextToken;
   aList:=nil;
   try
@@ -348,7 +370,7 @@ begin
     While Not (CurrentToken in Term) do
       begin
       aToken:=CurrentToken;
-        writeln('TCSSParser.ParseAtMediaRule Token=',CurrentToken);
+      //  writeln('TCSSParser.ParseAtMediaRule Token=',CurrentToken);
       case aToken of
       ctkIDENTIFIER:
         aList.AddChild(ParseIdentifier);
@@ -382,6 +404,56 @@ begin
   end;
 end;
 
+function TCSSParser.ParseAtSimpleRule: TCSSAtRuleElement;
+var
+  {$ifdef VerboseCSSParser}
+  aAt : TCSSString;
+  {$endif}
+  aRule: TCSSAtRuleElement;
+begin
+  Result:=nil;
+  Inc(FRuleLevel);
+{$ifdef VerboseCSSParser}
+  aAt:=Format(' Level %d at (%d:%d)',[FRuleLevel,CurrentLine,CurrentPos]);
+  Writeln('Parse @font-face rule');
+{$endif}
+  aRule:=TCSSAtRuleElement(CreateElement(TCSSAtRuleElement));
+  try
+    aRule.AtKeyWord:=CurrentTokenString;
+    GetNextToken;
+
+    // read {
+    repeat
+      case CurrentToken of
+      ctkEOF:
+        DoErrorExpectedButGot('{');
+      ctkRBRACE, ctkRPARENTHESIS, ctkSEMICOLON:
+        begin
+        DoWarnExpectedButGot('{');
+        Result:=aRule;
+        aRule:=nil;
+        exit;
+        end;
+      ctkLBRACE:
+        break;
+      end;
+    until false;
+    GetNextToken;
+
+    // read declarations
+    ParseRuleBody(aRule);
+    if CurrentToken=ctkRBRACE then
+      GetNextToken;
+
+    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)
@@ -497,12 +569,6 @@ end;
 
 function TCSSParser.ParseExpression: TCSSElement;
 
-  Function AllowRules(const aName : TCSSString) : Boolean;
-
-  begin
-    Result:=sameText(aName,'@print');
-  end;
-
 Const
   RuleTokens =
        [ctkIDENTIFIER,ctkCLASSNAME,ctkHASH,ctkINTEGER,
@@ -513,11 +579,12 @@ begin
   if CurrentToken in RuleTokens then
     Result:=ParseRule
   else if CurrentToken=ctkATKEYWORD then
-    begin
-    if SameText(CurrentTokenString,'@media') then
-      Result:=ParseAtMediaRule
+    case lowercase(CurrentTokenString) of
+    '@media': Result:=ParseAtMediaRule;
+    '@font-face',
+    '@page': Result:=ParseAtSimpleRule;
     else
-      Result:=ParseAtRule;
+      Result:=ParseAtUnknownRule;
     end
   else
     Result:=ParseComponentValueList;
@@ -556,6 +623,21 @@ begin
     Result:=ParseRuleList;
 end;
 
+function TCSSParser.ParseInline: TCSSElement;
+var
+  aRule: TCSSRuleElement;
+begin
+  GetNextToken;
+  aRule:=TCSSRuleElement(CreateElement(TCSSRuleElement));
+  try
+    ParseRuleBody(aRule);
+    Result:=aRule;
+    aRule:=nil;
+  finally
+    aRule.Free;
+  end;
+end;
+
 function TCSSParser.GetNextToken: TCSSToken;
 begin
   FPrevious:=FCurrent;
@@ -783,7 +865,7 @@ begin
     if Not (CurrentToken in [ctkEOF,ctkRBRACE]) then
       begin
       if CurrentToken=ctkATKEYWORD then
-        aDecl:=ParseAtRule
+        aDecl:=ParseAtUnknownRule
       else
         aDecl:=ParseDeclaration(aIsAt);
       aRule.AddChild(aDecl);
@@ -923,7 +1005,7 @@ begin
     if AllowRules and (CurrentToken in [ctkLBRACE,ctkATKEYWORD]) then
       begin
       if CurrentToken=ctkATKEYWORD then
-        aFactor:=ParseAtRule
+        aFactor:=ParseAtUnknownRule
       else
         aFactor:=ParseRule;
       end
@@ -1038,12 +1120,16 @@ begin
   Scanner.ReturnWhiteSpace:=true;
   try
     repeat
-      //writeln('TCSSParser.ParseSelector LIST START ',CurrentToken,' ',CurrentTokenString);
+      {$IFDEF VerbosecSSParser}
+      writeln('TCSSParser.ParseSelector LIST START ',CurrentToken,' ',CurrentTokenString);
+      {$ENDIF}
       // read list
       List:=nil;
       El:=ParseSub;
-      //writeln('TCSSParser.ParseSelector LIST NEXT ',CurrentToken,' ',CurrentTokenString);
-      while CurrentToken in [ctkSTAR,ctkIDENTIFIER,ctkCLASSNAME,ctkLBRACKET,ctkPSEUDO,ctkPSEUDOFUNCTION] do
+      {$IFDEF VerbosecSSParser}
+      writeln('TCSSParser.ParseSelector LIST NEXT ',CurrentToken,' ',CurrentTokenString);
+      {$ENDIF}
+      while CurrentToken in [ctkSTAR,ctkHASH,ctkIDENTIFIER,ctkCLASSNAME,ctkLBRACKET,ctkPSEUDO,ctkPSEUDOFUNCTION] do
         begin
         if List=nil then
           begin
@@ -1062,8 +1148,10 @@ begin
         Result:=El;
       El:=nil;
 
-      //writeln('TCSSParser.ParseSelector LIST END ',CurrentToken,' ',CurrentTokenString);
       SkipWhiteSpace;
+      {$IFDEF VerbosecSSParser}
+      writeln('TCSSParser.ParseSelector LIST END ',CurrentToken,' ',CurrentTokenString);
+      {$ENDIF}
 
       case CurrentToken of
       ctkLBRACE,ctkEOF,ctkSEMICOLON,ctkCOMMA:
@@ -1078,7 +1166,7 @@ begin
         GetNextToken;
         SkipWhiteSpace;
         end;
-      ctkIDENTIFIER,ctkCLASSNAME,ctkLBRACKET,ctkPSEUDO,ctkPSEUDOFUNCTION:
+      ctkSTAR,ctkHASH,ctkIDENTIFIER,ctkCLASSNAME,ctkLBRACKET,ctkPSEUDO,ctkPSEUDOFUNCTION:
         begin
         // decendant combinator
         Bin:=TCSSBinaryElement(CreateElement(TCSSBinaryElement));
@@ -1229,7 +1317,7 @@ begin
     aList.AddChild(aValue);
     if aDecl.Colon then
       begin
-      While not (CurrentToken in [ctkSemicolon,ctkRBRACE,ctkImportant]) do
+      While not (CurrentToken in [ctkEOF,ctkSemicolon,ctkRBRACE,ctkImportant]) do
         begin
         While CurrentToken=ctkCOMMA do
           begin
@@ -1238,6 +1326,7 @@ begin
           aList:=TCSSListElement(CreateElement(TCSSListElement));
           end;
         aValue:=ParseComponentValue;
+        if aValue=nil then break;
         aList.AddChild(aValue);
         end;
       if CurrentToken=ctkImportant then
@@ -1262,7 +1351,11 @@ function TCSSParser.ParseCall(aName : TCSSString): TCSSElement;
 var
   aCall : TCSSCallElement;
   l : Integer;
+  OldReturnWhiteSpace: Boolean;
+  aValue: TCSSElement;
 begin
+  OldReturnWhiteSpace:=Scanner.ReturnWhiteSpace;
+  Scanner.ReturnWhiteSpace:=false;
   aCall:=TCSSCallElement(CreateELement(TCSSCallElement));
   try
     if (aName='') then
@@ -1275,19 +1368,26 @@ begin
       Consume(ctkPSEUDOFUNCTION)
     else
       Consume(ctkFUNCTION);
+    // Call argument list can be empty: mask()
     While not (CurrentToken in [ctkRPARENTHESIS,ctkEOF]) do
       begin
-      aCall.AddArg(ParseComponentValueList);
+      aValue:=ParseComponentValue;
+      if aValue=nil then
+      begin
+        aValue:=TCSSElement(CreateElement(TCSSElement));
+        GetNextToken;
+      end;
+      aCall.AddArg(aValue);
       if (CurrentToken=ctkCOMMA) then
         Consume(ctkCOMMA);
       end;
     if CurrentToken=ctkEOF then
       DoError(SErrUnexpectedEndOfFile,[aName]);
     Consume(ctkRPARENTHESIS);
-    // Call argument list can be empty: mask()
     Result:=aCall;
     aCall:=nil;
   finally
+    Scanner.ReturnWhiteSpace:=OldReturnWhiteSpace;
     aCall.Free;
   end;
 end;

+ 244 - 46
packages/fcl-css/src/fpcssresolver.pas

@@ -159,9 +159,18 @@ type
     NormValue: string;
   end;
 
+  { TCSSCallData }
+
   TCSSCallData = class(TCSSElResolverData)
   public
     NumericalID: TCSSNumericalID;
+    Params: TObject;
+    destructor Destroy; override;
+  end;
+
+  TCSSCallNthChildParams = class
+    Modulo: integer;
+    Start: integer;
   end;
 
   TCSSResolverOption = (
@@ -185,11 +194,15 @@ type
     );
   TCSSResStringComparisons = set of TCSSResStringComparison;
 
+  TCSSResolverLogEvent = procedure(Sender: TObject; aType: TEventType;
+    const ID: TCSSMsgID; const Msg: string; PosEl: TCSSElement) of object;
+
   { TCSSResolver }
 
   TCSSResolver = class
   private
     FNumericalIDs: array[TCSSNumericalIDKind] of TCSSNumericalIDs;
+    FOnLog: TCSSResolverLogEvent;
     FOptions: TCSSResolverOptions;
     FStringComparison: TCSSResStringComparison;
     FStyle: TCSSElement;
@@ -208,6 +221,8 @@ type
     procedure SetStyle(const AValue: TCSSElement); virtual;
     procedure ComputeElement(El: TCSSElement); virtual;
     procedure ComputeRule(aRule: TCSSRuleElement); virtual;
+    procedure ComputeInline(El: TCSSElement); virtual;
+    procedure ComputeInlineRule(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;
@@ -232,7 +247,7 @@ type
     function FindComputedAttribute(AttrID: TCSSNumericalID): PCSSComputedAttribute;
     function AddComputedAttribute(TheAttrID: TCSSNumericalID; aSpecifity: TCSSSpecifity;
                           aValue: TCSSElement): PCSSComputedAttribute;
-    procedure DoError(const ID: TCSSMsgID; Msg: string; PosEl: TCSSElement); virtual;
+    procedure Log(MsgType: TEventType; const ID: TCSSMsgID; Msg: string; PosEl: TCSSElement); virtual;
     function GetElPos(El: TCSSElement): string; virtual;
     function GetElPath(El: TCSSElement): string; virtual;
   public
@@ -249,10 +264,19 @@ type
     property Attributes[Index: integer]: PCSSComputedAttribute read GetAttributes;
     property AttributeCount: integer read FAttributeCount;
     property StringComparison: TCSSResStringComparison read FStringComparison;
+    property OnLog: TCSSResolverLogEvent read FOnLog write FOnLog;
   end;
 
 implementation
 
+{ TCSSCallData }
+
+destructor TCSSCallData.Destroy;
+begin
+  FreeAndNil(Params);
+  inherited Destroy;
+end;
+
 { TCSSNumericalIDs }
 
 function TCSSNumericalIDs.GetIDs(const aName: TCSSString): TCSSNumericalID;
@@ -348,26 +372,53 @@ begin
   end else if C=TCSSRuleElement then
     ComputeRule(TCSSRuleElement(El))
   else
-    DoError(20220908150252,'Unknown CSS element',El);
+    Log(etError,20220908150252,'Unknown CSS element',El);
 end;
 
 procedure TCSSResolver.ComputeRule(aRule: TCSSRuleElement);
 var
-  i, j: Integer;
-  Specifity: TCSSSpecifity;
+  i: Integer;
+  BestSpecifity, Specifity: TCSSSpecifity;
   aSelector: TCSSElement;
 begin
+  BestSpecifity:=CSSSpecifityNoMatch;
   for i:=0 to aRule.SelectorCount-1 do
   begin
     aSelector:=aRule.Selectors[i];
     Specifity:=SelectorMatches(aSelector,FNode);
-    if Specifity<0 then continue;
+    if Specifity>BestSpecifity then
+      BestSpecifity:=Specifity;
+  end;
+  if BestSpecifity>=0 then
+  begin
     // match -> apply properties
-    for j:=0 to aRule.ChildCount-1 do
-      MergeProperty(aRule.Children[j],Specifity);
+    for i:=0 to aRule.ChildCount-1 do
+      MergeProperty(aRule.Children[i],BestSpecifity);
   end;
 end;
 
+procedure TCSSResolver.ComputeInline(El: TCSSElement);
+var
+  C: TClass;
+begin
+  if El=nil then exit;
+  C:=El.ClassType;
+  if C=TCSSRuleElement then
+    ComputeInlineRule(TCSSRuleElement(El))
+  else
+    Log(etError,20220915140402,'TCSSResolver.ComputeInline Not yet supported inline element',El);
+end;
+
+procedure TCSSResolver.ComputeInlineRule(aRule: TCSSRuleElement);
+var
+  i: Integer;
+begin
+  if aRule.SelectorCount>0 then
+    exit;
+  for i:=0 to aRule.ChildCount-1 do
+    MergeProperty(aRule.Children[i],CSSSpecifityInline);
+end;
+
 function TCSSResolver.SelectorMatches(aSelector: TCSSElement;
   const TestNode: TCSSNode): TCSSSpecifity;
 
@@ -401,7 +452,7 @@ begin
   else if C=TCSSCallElement then
     Result:=SelectorCallMatches(TCSSCallElement(aSelector),TestNode)
   else
-    DoError(20220908230152,'Unknown CSS selector element',aSelector);
+    Log(etError,20220908230152,'Unknown CSS selector element',aSelector);
 end;
 
 function TCSSResolver.SelectorIdentifierMatches(
@@ -411,6 +462,9 @@ var
 begin
   Result:=CSSSpecifityNoMatch;
   TypeID:=ResolveIdentifier(Identifier,nikType);
+  {$IFDEF VerboseCSSResolver}
+  writeln('TCSSResolver.SelectorIdentifierMatches ',Identifier.Value,' TypeId=',TypeID);
+  {$ENDIF}
   if TypeID=CSSTypeID_Universal then
   begin
     // universal selector
@@ -418,7 +472,7 @@ begin
   end else if TypeID=CSSIDNone then
   begin
     if croErrorOnUnknownName in Options then
-      DoError(20220911230224,'Unknown CSS selector type name "'+Identifier.Name+'"',Identifier);
+      Log(etError,20220911230224,'Unknown CSS selector type name "'+Identifier.Name+'"',Identifier);
     Result:=CSSSpecifityInvalid;
   end else if TypeID=TestNode.GetCSSTypeID then
     Result:=CSSSpecifityType;
@@ -459,7 +513,7 @@ begin
   case PseudoID of
   CSSIDNone:
     if croErrorOnUnknownName in Options then
-      DoError(20220911205605,'Unknown CSS selector pseudo attribute name "'+aPseudoClass.Name+'"',aPseudoClass);
+      Log(etError,20220911205605,'Unknown CSS selector pseudo attribute name "'+aPseudoClass.Name+'"',aPseudoClass);
   CSSPseudoID_Root:
     if TestNode.GetCSSParent=nil then
       Result:=CSSSpecifityClass;
@@ -514,7 +568,7 @@ begin
     {$ENDIF}
     C:=El.ClassType;
     if (C=TCSSIdentifierElement) and (i>0) then
-      DoError(20220914163218,'Type selector must be first',aList)
+      Log(etError,20220914163218,'Type selector must be first',aList)
     else if C=TCSSPseudoClassElement then
     begin
       Specifity:=SelectorPseudoClassMatches(TCSSPseudoClassElement(El),aNode);
@@ -602,7 +656,7 @@ begin
     end
   else
     if croErrorOnUnknownName in Options then
-      DoError(20220910123724,'Invalid CSS binary selector '+BinaryOperators[aBinary.Operation],aBinary);
+      Log(etError,20220910123724,'Invalid CSS binary selector '+BinaryOperators[aBinary.Operation],aBinary);
   end;
 end;
 
@@ -620,14 +674,14 @@ var
 begin
   Result:=CSSSpecifityInvalid;
   if anArray.Prefix<>nil then
-    DoError(20220910154004,'Invalid CSS attribute selector prefix',anArray.Prefix);
+    Log(etError,20220910154004,'Invalid CSS attribute selector prefix',anArray.Prefix);
   {$IFDEF VerboseCSSResolver}
   writeln('TCSSResolver.SelectorArrayMatches Prefix=',GetCSSObj(anArray.Prefix),' ChildCount=',anArray.ChildCount);
   for i:=0 to anArray.ChildCount-1 do
     writeln('TCSSResolver.SelectorArrayMatches ',i,' ',GetCSSObj(anArray.Children[i]));
   {$ENDIF}
   if anArray.ChildCount<1 then
-    DoError(20220910154033,'Invalid CSS attribute selector',anArray);
+    Log(etError,20220910154033,'Invalid CSS attribute selector',anArray);
   OldStringComparison:=StringComparison;
   try
     if anArray.ChildCount>1 then
@@ -642,13 +696,13 @@ begin
         's': FStringComparison:=crscCaseSensitive;
         else
           if croErrorOnUnknownName in Options then
-            DoError(20220914174409,'Invalid attribute modifier "'+aValue+'"',El);
+            Log(etError,20220914174409,'Invalid attribute modifier "'+aValue+'"',El);
         end;
       end else
-        DoError(20220914173643,'Invalid CSS attribute modifier',El);
+        Log(etError,20220914173643,'Invalid CSS attribute modifier',El);
       end;
     if (anArray.ChildCount>2) and (croErrorOnUnknownName in Options) then
-      DoError(20220914174550,'Invalid CSS attribute modifier',anArray.Children[2]);
+      Log(etError,20220914174550,'Invalid CSS attribute modifier',anArray.Children[2]);
 
     El:=anArray.Children[0];
     C:=El.ClassType;
@@ -675,7 +729,7 @@ begin
     end else if C=TCSSBinaryElement then
       Result:=SelectorArrayBinaryMatches(TCSSBinaryElement(El),TestNode)
     else
-      DoError(20220910153725,'Invalid CSS array selector',El);
+      Log(etError,20220910153725,'Invalid CSS array selector',El);
   finally
     FStringComparison:=OldStringComparison;
   end;
@@ -692,7 +746,7 @@ begin
   Result:=CSSSpecifityNoMatch;
   Left:=aBinary.Left;
   if Left.ClassType<>TCSSIdentifierElement then
-    DoError(20220910164353,'Invalid CSS array selector, expected attribute',Left);
+    Log(etError,20220910164353,'Invalid CSS array selector, expected attribute',Left);
   AttrID:=ResolveIdentifier(TCSSIdentifierElement(Left),nikAttribute);
   {$IFDEF VerboseCSSResolver}
   writeln('TCSSResolver.SelectorArrayBinaryMatches AttrID=',AttrID,' Value=',TCSSIdentifierElement(Left).Value);
@@ -714,7 +768,7 @@ begin
       or (C=TCSSIdentifierElement) then
     // ok
   else
-    DoError(20220910164921,'Invalid CSS array selector, expected string',Right);
+    Log(etError,20220910164921,'Invalid CSS array selector, expected string',Right);
   RightValue:=ComputeValue(Right);
 
   {$IFDEF VerboseCSSResolver}
@@ -748,7 +802,7 @@ begin
       Result:=CSSSpecifityClass;
   else
     if croErrorOnUnknownName in Options then
-      DoError(20220910164356,'Invalid CSS array selector operator',aBinary);
+      Log(etError,20220910164356,'Invalid CSS array selector operator',aBinary);
     Result:=CSSSpecifityInvalid;
   end;
   {$IFDEF VerboseCSSResolver}
@@ -774,15 +828,150 @@ end;
 function TCSSResolver.Call_NthChild(aCall: TCSSCallElement;
   const TestNode: TCSSNode): TCSSSpecifity;
 var
-  i: Integer;
+  i, ArgCount, aModulo, aStart: Integer;
+  Arg, OffsetEl: TCSSElement;
+  Str: TCSSString;
+  UnaryEl: TCSSUnaryElement;
+  Params: TCSSCallNthChildParams;
+  CallData: TCSSCallData;
 begin
   Result:=CSSSpecifityInvalid;
+  CallData:=TCSSCallData(aCall.CustomData);
+  Params:=TCSSCallNthChildParams(CallData.Params);
+  if Params=nil then
+  begin
+    ArgCount:=aCall.ArgCount;
+    {$IFDEF VerboseCSSResolver}
+    writeln('TCSSResolver.Call_NthChild ',aCall.ArgCount);
+    for i:=0 to aCall.ArgCount-1 do
+      writeln('TCSSResolver.Call_NthChild ',i,' ',GetCSSObj(aCall.Args[i]),' AsString=',aCall.Args[i].AsString);
+    {$ENDIF}
+    // An+B[of S], odd, even, An
+
+    i:=0;
+    aModulo:=0;
+    aStart:=0;
+    // check step
+    if ArgCount<=i then
+    begin
+      Log(etWarning,20220915143843,':nth-child missing arguments',aCall);
+      exit;
+    end;
+    Arg:=aCall.Args[i];
+    if Arg.ClassType=TCSSIntegerElement then
+    begin
+      aModulo:=TCSSIntegerElement(Arg).Value;
+      inc(i);
+      // check n
+      if ArgCount<=i then
+      begin
+        Log(etWarning,20220915143843,':nth-child missing arguments',aCall);
+        exit;
+      end;
+      Arg:=aCall.Args[i];
+      if Arg.ClassType<>TCSSIdentifierElement then
+      begin
+        Log(etWarning,20220915144312,':nth-child expected n',Arg);
+        exit;
+      end;
+      if TCSSIdentifierElement(Arg).Value<>'n' then
+      begin
+        Log(etWarning,20220915144359,':nth-child expected n',Arg);
+        exit;
+      end;
+
+    end
+    else if Arg.ClassType=TCSSIdentifierElement then
+    begin
+      Str:=TCSSIdentifierElement(Arg).Value;
+      case lowercase(Str) of
+      'even':
+        begin
+        writeln('TCSSResolver.Call_NthChild EVEN');
+        aModulo:=2;
+        aStart:=2;
+        end;
+      'odd':
+        begin
+        writeln('TCSSResolver.Call_NthChild ODD');
+        aModulo:=2;
+        aStart:=1;
+        end;
+      'n':
+        begin
+        writeln('TCSSResolver.Call_NthChild N');
+        aModulo:=1;
+        aStart:=1;
+        end;
+      else
+        Log(etWarning,20220915150332,':nth-child expected multiplier',Arg);
+        exit;
+      end;
+    end else
+    begin
+      Log(etWarning,20220915144056,':nth-child expected multiplier',Arg);
+      exit;
+    end;
+
+    inc(i);
+    if ArgCount>i then
+    begin
+      Arg:=aCall.Args[i];
+      if Arg.ClassType=TCSSUnaryElement then
+      begin
+        UnaryEl:=TCSSUnaryElement(Arg);
+        //writeln('TCSSResolver.Call_NthChild UNARY ',UnaryEl.AsString);
+        if not (UnaryEl.Operation in [uoMinus,uoPlus]) then
+        begin
+          Log(etWarning,20220915151422,':nth-child unexpected offset',UnaryEl);
+          exit;
+        end;
+        OffsetEl:=UnaryEl.Right;
+        if OffsetEl=nil then
+        begin
+          Log(etWarning,20220915151511,':nth-child unexpected offset',UnaryEl);
+          exit;
+        end;
+        if OffsetEl.ClassType<>TCSSIntegerElement then
+        begin
+          Log(etWarning,20220915151718,':nth-child unexpected offset',OffsetEl);
+          exit;
+        end;
+        aStart:=TCSSIntegerElement(OffsetEl).Value;
+        if UnaryEl.Operation=uoMinus then
+          aStart:=-aStart;
+      end else
+      begin
+        Log(etWarning,20220915150851,':nth-child expected offset',Arg);
+        exit;
+      end;
+    end;
+
+    Params:=TCSSCallNthChildParams.Create;
+    CallData.Params:=Params;
+    Params.Modulo:=aModulo;
+    Params.Start:=aStart;
+  end else begin
+    aModulo:=Params.Modulo;
+    aStart:=Params.Start;
+  end;
+
+  Result:=CSSSpecifityNoMatch;
+  if aModulo<1 then
+    exit;
+  i:=TestNode.GetCSSIndex;
+  if i<0 then
+    exit;
+  i:=i+1-aStart;
+  if i mod aModulo = 0 then
+  begin
+    i:=i div aModulo;
+    if i>=0 then
+      Result:=CSSSpecifityClass
+  end;
   {$IFDEF VerboseCSSResolver}
-  writeln('TCSSResolver.Call_NthChild ',aCall.ArgCount);
-  for i:=0 to aCall.ArgCount-1 do
-    writeln('TCSSResolver.Call_NthChild ',i,' ',GetCSSObj(aCall.Args[i]),' AsString=',aCall.Args[i].AsString);
+  writeln('TCSSResolver.Call_NthChild ',aModulo,' * N + ',aStart,' Index=',TestNode.GetCSSIndex+1,' Result=',Result);
   {$ENDIF}
-  DoError(20220914194913,'TCSSResolver.Call_NthChild',aCall);
 end;
 
 function TCSSResolver.ComputeValue(El: TCSSElement): TCSSString;
@@ -828,7 +1017,7 @@ begin
     end;
     AddElValueData(El,Result);
   end else
-    DoError(20220910235106,'TCSSResolver.ComputeValue not supported',El);
+    Log(etError,20220910235106,'TCSSResolver.ComputeValue not supported',El);
 end;
 
 function TCSSResolver.SameValueText(const A, B: TCSSString): boolean;
@@ -870,8 +1059,9 @@ var
   SearchP, StrP: PChar;
   AC, BC: Char;
 begin
-  if SearchStr='' then exit(0);
-  if Str='' then exit(0);
+  Result:=0;
+  if SearchStr='' then exit;
+  if Str='' then exit;
   if StringComparison=crscCaseInsensitive then
   begin
     SearchP:=PChar(SearchStr);
@@ -898,10 +1088,12 @@ var
   WordsLen, SearchLen: SizeInt;
   p, WordStart: Integer;
 begin
-  if SearchWord='' then exit(0);
-  if Words='' then exit(0);
+  Result:=0;
+  if SearchWord='' then exit;
+  if Words='' then exit;
   WordsLen:=length(Words);
   SearchLen:=length(SearchWord);
+  //writeln('TCSSResolver.PosWord "',SearchWord,'" Words="',words,'"');
   p:=1;
   repeat
     repeat
@@ -914,6 +1106,7 @@ begin
     WordStart:=p;
     while (p<=WordsLen) and not (Words[p] in Whitespace) do
       inc(p);
+    //writeln('TCSSResolver.PosWord start=',WordStart,' p=',p);
     if SameValueText(@SearchWord[1],SearchLen,@Words[WordStart],p-WordStart) then
       exit(WordStart);
   until p>WordsLen;
@@ -932,9 +1125,9 @@ begin
   begin
     Decl:=TCSSDeclarationElement(El);
     if Decl.KeyCount<>1 then
-      DoError(20220908232213,'Not yet implemented CSS declaration with KeyCount='+IntToStr(Decl.KeyCount),El);
+      Log(etError,20220908232213,'Not yet implemented CSS declaration with KeyCount='+IntToStr(Decl.KeyCount),El);
     if Decl.ChildCount<>1 then
-      DoError(20220908232324,'Not yet implemented CSS declaration with ChildCount='+IntToStr(Decl.ChildCount),El);
+      Log(etError,20220908232324,'Not yet implemented CSS declaration with ChildCount='+IntToStr(Decl.ChildCount),El);
 
     aKey:=Decl.Keys[0];
     aValue:=Decl.Children[0];
@@ -946,10 +1139,10 @@ begin
     begin
       AttrID:=ResolveIdentifier(TCSSIdentifierElement(aKey),nikAttribute);
       if AttrID=CSSIDNone then
-        DoError(20220909000932,'Unknown CSS property "'+TCSSIdentifierElement(aKey).Name+'"',aKey)
+        Log(etError,20220909000932,'Unknown CSS property "'+TCSSIdentifierElement(aKey).Name+'"',aKey)
       else if AttrID=CSSAttributeID_All then
         // 'all'
-        DoError(20220909001019,'Not yet implemented CSS property "'+TCSSIdentifierElement(aKey).Name+'"',aKey)
+        Log(etError,20220909001019,'Not yet implemented CSS property "'+TCSSIdentifierElement(aKey).Name+'"',aKey)
       else begin
         // set property
         CompAttr:=FindComputedAttribute(AttrID);
@@ -964,9 +1157,9 @@ begin
         end;
       end;
     end else
-      DoError(20220908232359,'Unknown CSS key',aKey);
+      Log(etError,20220908232359,'Unknown CSS key',aKey);
   end else
-    DoError(20220908230855,'Unknown CSS property',El);
+    Log(etError,20220908230855,'Unknown CSS property',El);
 end;
 
 function TCSSResolver.ResolveIdentifier(El: TCSSIdentifierElement;
@@ -983,7 +1176,7 @@ begin
     Result:=IdentData.NumericalID;
     {$IFDEF VerboseCSSResolver}
     if IdentData.Kind<>Kind then
-      DoError(20220908235300,'TCSSResolver.ResolveIdentifier',El);
+      Log(etError,20220908235300,'TCSSResolver.ResolveIdentifier',El);
     {$ENDIF}
   end else
   begin
@@ -1025,7 +1218,7 @@ begin
     if Result=CSSIDNone then
     begin
       if croErrorOnUnknownName in FOptions then
-        DoError(20220908235919,'TCSSResolver.ResolveIdentifier unknown '+CSSNumericalIDKindNames[Kind]+' "'+El.Name+'"',El);
+        Log(etError,20220908235919,'TCSSResolver.ResolveIdentifier unknown '+CSSNumericalIDKindNames[Kind]+' "'+El.Name+'"',El);
     end;
     IdentData:=TCSSIdentifierData.Create;
     IdentData.Kind:=Kind;
@@ -1054,7 +1247,7 @@ begin
     ':nth-child': Result:=CSSCallID_NthChild;
     else
       if croErrorOnUnknownName in FOptions then
-        DoError(20220914193946,'TCSSResolver.ResolveCall unknown "'+El.Name+'"',El);
+        Log(etError,20220914193946,'TCSSResolver.ResolveCall unknown "'+El.Name+'"',El);
     end;
     CallData:=TCSSCallData.Create;
     CallData.NumericalID:=Result;
@@ -1117,11 +1310,16 @@ begin
   inc(FAttributeCount);
 end;
 
-procedure TCSSResolver.DoError(const ID: TCSSMsgID; Msg: string;
-  PosEl: TCSSElement);
+procedure TCSSResolver.Log(MsgType: TEventType; const ID: TCSSMsgID;
+  Msg: string; PosEl: TCSSElement);
 begin
-  Msg:='['+IntToStr(ID)+'] '+Msg+' at '+GetElPos(PosEl);
-  raise ECSSResolver.Create(Msg);
+  if assigned(OnLog) then
+    OnLog(Self,MsgType,ID,Msg,PosEl);
+  if (MsgType=etError) or (FOnLog=nil) then
+  begin
+    Msg:='['+IntToStr(ID)+'] '+Msg+' at '+GetElPos(PosEl);
+    raise ECSSResolver.Create(Msg);
+  end;
 end;
 
 function TCSSResolver.GetElPos(El: TCSSElement): string;
@@ -1167,7 +1365,7 @@ begin
     else
       FFirstElData:=nil;
     if Data.Element.CustomData<>Data then
-      DoError(20220908234726,'TCSSResolver.ClearStyleCustomData',Data.Element);
+      Log(etError,20220908234726,'TCSSResolver.ClearStyleCustomData',Data.Element);
     Data.Element.CustomData:=nil;
     Data.Free;
   end;
@@ -1180,7 +1378,7 @@ begin
   try
     FAttributeCount:=0;
     ComputeElement(Style);
-    ComputeElement(NodeStyle);
+    ComputeInline(NodeStyle);
     if ccoCommit in CompOptions then
       Commit;
   finally

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

@@ -72,7 +72,8 @@ Type
     ctkPIPE,
     ctkPIPEEQUAL,
     ctkDOLLAR,
-    ctkDOLLAREQUAL
+    ctkDOLLAREQUAL,
+    ctkINVALID
    );
   TCSSTokens = Set of TCSSToken;
 
@@ -142,6 +143,7 @@ Type
     FOwnSourceFile : Boolean;
     function DoHash: TCSSToken;
     function DoIdentifierLike : TCSSToken;
+    function DoInvalidChars : TCSSToken;
     function DoMultiLineComment: TCSSToken;
     function CommentDiv: TCSSToken;
     function DoNumericLiteral: TCSSToken;
@@ -168,6 +170,7 @@ Type
     destructor Destroy; override;
     procedure OpenFile(const AFilename: TCSSString);
     Function FetchToken: TCSSToken;
+    function IsUTF8BOM: boolean;
     Property ReturnComments : Boolean Read GetReturnComments Write SetReturnComments;
     Property ReturnWhiteSpace : Boolean Read GetReturnWhiteSpace Write SetReturnWhiteSpace;
     Property Options : TCSSScannerOptions Read FOptions Write FOptions;
@@ -322,8 +325,6 @@ begin
   FSourceFilename := AFilename;
 end;
 
-
-
 function TCSSScanner.FetchLine: Boolean;
 begin
   if FSourceFile.IsEOF then
@@ -669,7 +670,6 @@ Var
   Len,oLen : Integer;
   IsEscape,IsAt, IsPseudo, IsFunc : Boolean;
 
-
 begin
   Result:=ctkIDENTIFIER;
   TokenStart := TokenStr;
@@ -739,6 +739,23 @@ begin
     Result:=ctkFUNCTION;
 end;
 
+function TCSSScanner.DoInvalidChars: TCSSToken;
+var
+  TokenStart: PChar;
+  Len: SizeUInt;
+begin
+  Result:=ctkINVALID;
+  TokenStart := TokenStr;
+  repeat
+    writeln('TCSSScanner.DoInvalidChars ',hexstr(ord(TokenStr^),2));
+    Inc(TokenStr);
+  until (TokenStr[0] in [#0,#9,#10,#13,#32..#127]);
+  Len:=TokenStr-TokenStart;
+  SetLength(FCurTokenString,Len);
+  if Len > 0 then
+    Move(TokenStart^,FCurTokenString[1],Len);
+end;
+
 function TCSSScanner.FetchToken: TCSSToken;
 
 var
@@ -747,7 +764,10 @@ var
 begin
   Repeat
     Result:=DoFetchToken;
-    CanStop:=(Not (Result in [ctkComment,ctkWhiteSpace]))
+    if (Result=ctkINVALID) and IsUTF8BOM then
+      CanStop:=false
+    else
+      CanStop:=(Not (Result in [ctkComment,ctkWhiteSpace]))
              or ((ReturnComments and (Result=ctkComment))
                   or
                  (ReturnWhiteSpace and (Result=ctkWhiteSpace))
@@ -755,6 +775,14 @@ begin
   Until CanStop;
 end;
 
+function TCSSScanner.IsUTF8BOM: boolean;
+begin
+  Result:=(length(FCurTokenString)=3)
+      and (FCurTokenString[1]=#$EF)
+      and (FCurTokenString[2]=#$BB)
+      and (FCurTokenString[3]=#$BF);
+end;
+
 function TCSSScanner.DoFetchToken: TCSSToken;
 
 
@@ -905,8 +933,9 @@ begin
          Result:=DoIdentifierLike;
        end;
   else
+    writeln('TCSSScanner.DoFetchToken ',Ord(TokenStr[0]));
     If Ord(TokenStr[0])>127 then
-      Result:=DoIdentifierLike
+      Result:=DoInvalidChars
     else
       DoError(SErrUnknownCharacter ,['"'+TokenStr[0]+'"']);
 

+ 10 - 0
packages/fcl-css/tests/tccssparser.pp

@@ -102,6 +102,7 @@ type
     Procedure TestImportAtKeyWord;
     Procedure TestMediaPrint;
     Procedure TestSupportsFunction;
+    Procedure TestSkipUnknownFunction;
   end;
 
   { TTestCSSFilesParser }
@@ -201,6 +202,7 @@ end;
 
 procedure TTestCSSFilesParser.Testanimation;
 begin
+  SkipInvalid:=true;
   RunFileTest;
 end;
 
@@ -798,6 +800,14 @@ begin
   );
 end;
 
+procedure TTestCSSParser.TestSkipUnknownFunction;
+begin
+  SkipInvalid:=true;
+  ParseRule(':-webkit-any(table, thead, tbody, tfoot, tr) > form:-internal-is-html {'+sLineBreak
+    +'  display: none !important;'+sLineBreak
+    +'}');
+end;
+
 
 { TTestBaseCSSParser }
 

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

@@ -227,6 +227,7 @@ type
     procedure Test_Selector_FirstChild;
     procedure Test_Selector_LastChild;
     procedure Test_Selector_OnlyChild;
+    //procedure Test_Selector_Not;
     procedure Test_Selector_NthChild;
     // ToDo: :nth-last-child(n)
     procedure Test_Selector_FirstOfType;
@@ -234,14 +235,13 @@ type
     procedure Test_Selector_OnlyOfType;
     // ToDo: :nth-of-type(n)
     // ToDo: :nth-last-of-type(n)
-    // ToDo: :not(selector)
     // ToDo: div:has(>img)
     // ToDo: div:has(+img)
     // ToDo: :is()
     // ToDo: :where()
     // ToDo: :lang()
 
-    // ToDo: inline style
+    // inline style
     procedure Test_InlineStyle;
 
     // ToDo: specifity
@@ -635,7 +635,7 @@ begin
   '[left~=One] { top: 4px; }',
   '[left~=Two] { width: 5px; }',
   '[left~=Three] { height: 6px; }',
-  '[left~="Four Five"] { color: #123; }',
+  '[left~="Four Five"] { color: #123; }',  // not one word, so does not match!
   '[left~=our] { display: none; }',
   '']);
   Doc.ApplyStyle;
@@ -848,22 +848,33 @@ begin
 
   Doc.Style:=LinesToStr([
   ':nth-child(2n+1) { left: 8px; }',
-  //':nth-child(even) { top: 3px; }',
-  //':nth-child(odd) { width: 4px; }',
+  ':nth-child(n+3) { border: 6px; }',
+  ':nth-child(even) { top: 3px; }',
+  ':nth-child(odd) { width: 4px; }',
   //':nth-child(odd of .red) { height: 4px; }',
   //':nth-child(even of :not([display=none])) { color: blue; }',
   '']);
   Doc.ApplyStyle;
-  AssertEquals('Root.Left','8px',Doc.Root.Left);
+  AssertEquals('Root.Left','',Doc.Root.Left);
+  AssertEquals('Root.Border','',Doc.Root.Border);
   AssertEquals('Root.Top','',Doc.Root.Top);
+  AssertEquals('Root.Width','',Doc.Root.Width);
   AssertEquals('Div1.Left','8px',Div1.Left);
+  AssertEquals('Div1.Border','',Div1.Border);
   AssertEquals('Div1.Top','',Div1.Top);
+  AssertEquals('Div1.Width','4px',Div1.Width);
   AssertEquals('Div2.Left','',Div2.Left);
-  AssertEquals('Div2.Top','',Div2.Top);
+  AssertEquals('Div2.Border','',Div2.Border);
+  AssertEquals('Div2.Top','3px',Div2.Top);
+  AssertEquals('Div2.Width','',Div2.Width);
   AssertEquals('Div3.Left','8px',Div3.Left);
+  AssertEquals('Div3.Border','6px',Div3.Border);
   AssertEquals('Div3.Top','',Div3.Top);
-  AssertEquals('Div4.Left','8px',Div4.Left);
-  AssertEquals('Div4.Top','',Div4.Top);
+  AssertEquals('Div3.Width','4px',Div3.Width);
+  AssertEquals('Div4.Left','',Div4.Left);
+  AssertEquals('Div4.Border','6px',Div4.Border);
+  AssertEquals('Div4.Top','3px',Div4.Top);
+  AssertEquals('Div4.Width','',Div4.Width);
 end;
 
 procedure TTestCSSResolver.Test_Selector_FirstOfType;
@@ -1239,7 +1250,7 @@ begin
   ss:=TStringStream.Create(Style);
   try
     aParser:=TCSSParser.Create(ss);
-    FStyleElements:=aParser.Parse;
+    FStyleElements:=aParser.ParseInline;
   finally
     aParser.Free;
   end;

+ 0 - 6
packages/fcl-css/tests/testcss.lpi

@@ -45,12 +45,6 @@
       <OtherUnitFiles Value="../src"/>
       <UnitOutputDirectory Value="lib/$(TargetCPU)-$(TargetOS)"/>
     </SearchPaths>
-    <Other>
-      <OtherDefines Count="2">
-        <Define0 Value="VerboseCSSParser"/>
-        <Define1 Value="VerboseCSSResolver"/>
-      </OtherDefines>
-    </Other>
   </CompilerOptions>
   <Debugging>
     <Exceptions>