Browse Source

fcl-css: resolver: invalid selectors

mattias 2 years ago
parent
commit
3e9fd9073f
2 changed files with 80 additions and 39 deletions
  1. 66 36
      packages/fcl-css/src/fpcssresolver.pas
  2. 14 3
      packages/fcl-css/tests/tccssresolver.pp

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

@@ -35,6 +35,9 @@ uses
   Classes, SysUtils, Contnrs, StrUtils, fpCSSTree;
   Classes, SysUtils, Contnrs, StrUtils, fpCSSTree;
 
 
 const
 const
+  CSSSpecifityInvalid = -2;
+  CSSSpecifityNoMatch = -1;
+  CSSSpecifityUniversal = 0;
   CSSSpecifityType = 1;
   CSSSpecifityType = 1;
   CSSSpecifityClass = 10; // includes attribute selectors e.g. [href]
   CSSSpecifityClass = 10; // includes attribute selectors e.g. [href]
   CSSSpecifityIdentifier = 100;
   CSSSpecifityIdentifier = 100;
@@ -153,7 +156,7 @@ type
   end;
   end;
 
 
   TCSSResolverOption = (
   TCSSResolverOption = (
-    roErrorOnUnknownName
+    croErrorOnUnknownName
     );
     );
   TCSSResolverOptions = set of TCSSResolverOption;
   TCSSResolverOptions = set of TCSSResolverOption;
 
 
@@ -346,7 +349,7 @@ function TCSSResolver.SelectorMatches(aSelector: TCSSElement;
 var
 var
   C: TClass;
   C: TClass;
 begin
 begin
-  Result:=-1;
+  Result:=CSSSpecifityInvalid;
   C:=aSelector.ClassType;
   C:=aSelector.ClassType;
   if C=TCSSIdentifierElement then
   if C=TCSSIdentifierElement then
     Result:=SelectorIdentifierMatches(TCSSIdentifierElement(aSelector),TestNode)
     Result:=SelectorIdentifierMatches(TCSSIdentifierElement(aSelector),TestNode)
@@ -371,18 +374,22 @@ function TCSSResolver.SelectorIdentifierMatches(
 var
 var
   TypeID: TCSSNumericalID;
   TypeID: TCSSNumericalID;
 begin
 begin
-  Result:=-1;
+  Result:=CSSSpecifityNoMatch;
   TypeID:=ResolveIdentifier(Identifier,nikType);
   TypeID:=ResolveIdentifier(Identifier,nikType);
   if TypeID=CSSTypeID_Universal then
   if TypeID=CSSTypeID_Universal then
   begin
   begin
     // universal selector
     // universal selector
-    Result:=0;
-  end else if TypeID<>CSSIDNone then
+    Result:=CSSSpecifityUniversal;
+  end else if TypeID=CSSIDNone then
+  begin
+    if croErrorOnUnknownName in Options then
+      DoError(20220911230224,'Unknown CSS selector type name "'+Identifier.Name+'"',Identifier);
+    Result:=CSSSpecifityInvalid;
+  end else
   begin
   begin
     if TypeID=TestNode.GetCSSTypeID then
     if TypeID=TestNode.GetCSSTypeID then
       Result:=CSSSpecifityType;
       Result:=CSSSpecifityType;
-  end else
-    DoError(20220908230426,'Unknown CSS selector type name "'+Identifier.Name+'"',Identifier);
+  end;
 end;
 end;
 
 
 function TCSSResolver.SelectorClassNameMatches(
 function TCSSResolver.SelectorClassNameMatches(
@@ -394,7 +401,7 @@ begin
   if TestNode.HasCSSClass(aValue) then
   if TestNode.HasCSSClass(aValue) then
     Result:=CSSSpecifityClass
     Result:=CSSSpecifityClass
   else
   else
-    Result:=-1;
+    Result:=CSSSpecifityNoMatch;
 end;
 end;
 
 
 function TCSSResolver.SelectorPseudoClassMatches(
 function TCSSResolver.SelectorPseudoClassMatches(
@@ -403,11 +410,11 @@ function TCSSResolver.SelectorPseudoClassMatches(
 var
 var
   PseudoID: TCSSNumericalID;
   PseudoID: TCSSNumericalID;
 begin
 begin
-  Result:=-1;
+  Result:=CSSSpecifityNoMatch;
   PseudoID:=ResolveIdentifier(aPseudoClass,nikPseudoAttribute);
   PseudoID:=ResolveIdentifier(aPseudoClass,nikPseudoAttribute);
   case PseudoID of
   case PseudoID of
   CSSIDNone:
   CSSIDNone:
-    if roErrorOnUnknownName in Options then
+    if croErrorOnUnknownName in Options then
       DoError(20220911205605,'Unknown CSS selector pseudo attribute name "'+aPseudoClass.Name+'"',aPseudoClass);
       DoError(20220911205605,'Unknown CSS selector pseudo attribute name "'+aPseudoClass.Name+'"',aPseudoClass);
   CSSPseudoID_Root:
   CSSPseudoID_Root:
     if TestNode.GetCSSParent=nil then
     if TestNode.GetCSSParent=nil then
@@ -436,16 +443,18 @@ begin
         and (TestNode.GetCSSPreviousOfType=nil) then
         and (TestNode.GetCSSPreviousOfType=nil) then
       Result:=CSSSpecifityClass;
       Result:=CSSSpecifityClass;
   else
   else
-    TestNode.GetCSSPseudoAttribute(PseudoID);
+    if TestNode.GetCSSPseudoAttribute(PseudoID)<>'' then
+      Result:=CSSSpecifityClass;
   end;
   end;
 end;
 end;
 
 
 function TCSSResolver.SelectorStringMatches(aString: TCSSStringElement;
 function TCSSResolver.SelectorStringMatches(aString: TCSSStringElement;
   const TestNode: TCSSNode): TCSSSpecifity;
   const TestNode: TCSSNode): TCSSSpecifity;
+// id selector #name
 var
 var
   aValue: TCSSString;
   aValue: TCSSString;
 begin
 begin
-  Result:=-1;
+  Result:=CSSSpecifityNoMatch;
   if aString.Children.Count>0 then
   if aString.Children.Count>0 then
     DoError(20220910113909,'Invalid CSS string selector',aString.Children[0]);
     DoError(20220910113909,'Invalid CSS string selector',aString.Children[0]);
   aValue:=aString.Value;
   aValue:=aString.Value;
@@ -463,7 +472,7 @@ function TCSSResolver.SelectorListMatches(aList: TCSSListElement;
 var
 var
   i: Integer;
   i: Integer;
 begin
 begin
-  Result:=-1;
+  Result:=CSSSpecifityInvalid;
   writeln('TCSSResolver.SelectorListMatches ChildCount=',aList.ChildCount);
   writeln('TCSSResolver.SelectorListMatches ChildCount=',aList.ChildCount);
   for i:=0 to aList.ChildCount-1 do
   for i:=0 to aList.ChildCount-1 do
     writeln('TCSSResolver.SelectorListMatches ',i,' ',GetCSSObj(aList.Children[i]),' AsString=',aList.Children[i].AsString);
     writeln('TCSSResolver.SelectorListMatches ',i,' ',GetCSSObj(aList.Children[i]),' AsString=',aList.Children[i].AsString);
@@ -476,7 +485,7 @@ var
   aParent, Sibling: TCSSNode;
   aParent, Sibling: TCSSNode;
   aSpecifity: TCSSSpecifity;
   aSpecifity: TCSSSpecifity;
 begin
 begin
-  Result:=-1;
+  Result:=CSSSpecifityInvalid;
   case aBinary.Operation of
   case aBinary.Operation of
   boGT:
   boGT:
     begin
     begin
@@ -485,10 +494,10 @@ begin
       if Result<0 then exit;
       if Result<0 then exit;
       aParent:=TestNode.GetCSSParent;
       aParent:=TestNode.GetCSSParent;
       if aParent=nil then
       if aParent=nil then
-        exit(-1);
+        exit(CSSSpecifityNoMatch);
       aSpecifity:=SelectorMatches(aBinary.Left,aParent);
       aSpecifity:=SelectorMatches(aBinary.Left,aParent);
       if aSpecifity<0 then
       if aSpecifity<0 then
-        exit(-1);
+        exit(aSpecifity);
       inc(Result,aSpecifity);
       inc(Result,aSpecifity);
     end;
     end;
   boPlus:
   boPlus:
@@ -498,10 +507,10 @@ begin
       if Result<0 then exit;
       if Result<0 then exit;
       Sibling:=TestNode.GetCSSPreviousSibling;
       Sibling:=TestNode.GetCSSPreviousSibling;
       if Sibling=nil then
       if Sibling=nil then
-        exit(-1);
+        exit(CSSSpecifityNoMatch);
       aSpecifity:=SelectorMatches(aBinary.Left,Sibling);
       aSpecifity:=SelectorMatches(aBinary.Left,Sibling);
       if aSpecifity<0 then
       if aSpecifity<0 then
-        exit(-1);
+        exit(aSpecifity);
       inc(Result,aSpecifity);
       inc(Result,aSpecifity);
     end;
     end;
   boTilde:
   boTilde:
@@ -513,17 +522,20 @@ begin
       while Sibling<>nil do
       while Sibling<>nil do
       begin
       begin
         aSpecifity:=SelectorMatches(aBinary.Left,Sibling);
         aSpecifity:=SelectorMatches(aBinary.Left,Sibling);
-        if aSpecifity>=0 then
+        if aSpecifity=CSSSpecifityInvalid then
+          exit(aSpecifity)
+        else if aSpecifity>=0 then
         begin
         begin
           inc(Result,aSpecifity);
           inc(Result,aSpecifity);
           exit;
           exit;
         end;
         end;
         Sibling:=Sibling.GetCSSPreviousSibling;
         Sibling:=Sibling.GetCSSPreviousSibling;
       end;
       end;
-      Result:=-1;
+      Result:=CSSSpecifityNoMatch;
     end;
     end;
   else
   else
-    DoError(20220910123724,'Invalid CSS binary selector '+BinaryOperators[aBinary.Operation],aBinary);
+    if croErrorOnUnknownName in Options then
+      DoError(20220910123724,'Invalid CSS binary selector '+BinaryOperators[aBinary.Operation],aBinary);
   end;
   end;
 end;
 end;
 
 
@@ -533,15 +545,20 @@ var
   El: TCSSElement;
   El: TCSSElement;
   C: TClass;
   C: TClass;
   AttrID: TCSSNumericalID;
   AttrID: TCSSNumericalID;
+  {$IFDEF VerboseCSSResolver}
+  i: integer;
+  {$ENDIF}
 begin
 begin
-  Result:=-1;
+  Result:=CSSSpecifityInvalid;
   if anArray.Prefix<>nil then
   if anArray.Prefix<>nil then
     DoError(20220910154004,'Invalid CSS array selector prefix',anArray.Prefix);
     DoError(20220910154004,'Invalid CSS array 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
   if anArray.ChildCount<>1 then
     DoError(20220910154033,'Invalid CSS array selector',anArray);
     DoError(20220910154033,'Invalid CSS array selector',anArray);
-  //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]));
   El:=anArray.Children[0];
   El:=anArray.Children[0];
   C:=El.ClassType;
   C:=El.ClassType;
   if C=TCSSIdentifierElement then
   if C=TCSSIdentifierElement then
@@ -550,12 +567,14 @@ begin
     AttrID:=ResolveIdentifier(TCSSIdentifierElement(El),nikAttribute);
     AttrID:=ResolveIdentifier(TCSSIdentifierElement(El),nikAttribute);
     case AttrID of
     case AttrID of
     CSSIDNone,
     CSSIDNone,
-    CSSAttributeID_All: ;
+    CSSAttributeID_All: Result:=CSSSpecifityNoMatch;
     CSSAttributeID_ID:
     CSSAttributeID_ID:
       Result:=CSSSpecifityClass;
       Result:=CSSSpecifityClass;
     else
     else
       if TestNode.HasCSSAttribute(AttrID) then
       if TestNode.HasCSSAttribute(AttrID) then
-        Result:=CSSSpecifityClass;
+        Result:=CSSSpecifityClass
+      else
+        Result:=CSSSpecifityNoMatch;
     end;
     end;
   end else if C=TCSSBinaryElement then
   end else if C=TCSSBinaryElement then
     Result:=SelectorArrayBinaryMatches(TCSSBinaryElement(El),TestNode)
     Result:=SelectorArrayBinaryMatches(TCSSBinaryElement(El),TestNode)
@@ -571,15 +590,17 @@ var
   LeftValue, RightValue: TCSSString;
   LeftValue, RightValue: TCSSString;
   C: TClass;
   C: TClass;
 begin
 begin
-  Result:=-1;
+  Result:=CSSSpecifityNoMatch;
   Left:=aBinary.Left;
   Left:=aBinary.Left;
   if Left.ClassType<>TCSSIdentifierElement then
   if Left.ClassType<>TCSSIdentifierElement then
     DoError(20220910164353,'Invalid CSS array selector, expected attribute',Left);
     DoError(20220910164353,'Invalid CSS array selector, expected attribute',Left);
   AttrID:=ResolveIdentifier(TCSSIdentifierElement(Left),nikAttribute);
   AttrID:=ResolveIdentifier(TCSSIdentifierElement(Left),nikAttribute);
+  {$IFDEF VerboseCSSResolver}
   writeln('TCSSResolver.SelectorArrayBinaryMatches AttrID=',AttrID,' Value=',TCSSIdentifierElement(Left).Value);
   writeln('TCSSResolver.SelectorArrayBinaryMatches AttrID=',AttrID,' Value=',TCSSIdentifierElement(Left).Value);
+  {$ENDIF}
   case AttrID of
   case AttrID of
   CSSIDNone,
   CSSIDNone,
-  CSSAttributeID_All: exit;
+  CSSAttributeID_All: exit(CSSSpecifityNoMatch);
   CSSAttributeID_ID:
   CSSAttributeID_ID:
     LeftValue:=TestNode.GetCSSID;
     LeftValue:=TestNode.GetCSSID;
   else
   else
@@ -595,23 +616,25 @@ begin
     DoError(20220910164921,'Invalid CSS array selector, expected string',Right);
     DoError(20220910164921,'Invalid CSS array selector, expected string',Right);
   RightValue:=ComputeValue(Right);
   RightValue:=ComputeValue(Right);
 
 
+  {$IFDEF VerboseCSSResolver}
   writeln('TCSSResolver.SelectorArrayBinaryMatches Left="',LeftValue,'" Right="',RightValue,'" Op=',aBinary.Operation);
   writeln('TCSSResolver.SelectorArrayBinaryMatches Left="',LeftValue,'" Right="',RightValue,'" Op=',aBinary.Operation);
+  {$ENDIF}
   case aBinary.Operation of
   case aBinary.Operation of
   boEquals:
   boEquals:
-    if AnsiCompareStr(LeftValue,RightValue)=0 then
+    if LeftValue=RightValue then
       Result:=CSSSpecifityClass;
       Result:=CSSSpecifityClass;
   boSquaredEqual:
   boSquaredEqual:
     // begins with
     // begins with
-    if AnsiCompareStr(LeftStr(LeftValue,length(RightValue)),RightValue)=0 then
+    if LeftStr(LeftValue,length(RightValue))=RightValue then
       Result:=CSSSpecifityClass;
       Result:=CSSSpecifityClass;
   boDollarEqual:
   boDollarEqual:
     // ends with
     // ends with
-    if AnsiCompareStr(RightStr(LeftValue,length(RightValue)),RightValue)=0 then
+    if RightStr(LeftValue,length(RightValue))=RightValue then
       Result:=CSSSpecifityClass;
       Result:=CSSSpecifityClass;
   boPipeEqual:
   boPipeEqual:
     // equal to or starts with name-hyphen
     // equal to or starts with name-hyphen
-    if (AnsiCompareStr(LeftValue,RightValue)=0)
-        or (AnsiCompareStr(LeftStr(LeftValue,length(RightValue)+1),RightValue+'-')=0) then
+    if (LeftValue=RightValue)
+        or (LeftStr(LeftValue,length(RightValue)+1)=RightValue+'-') then
       Result:=CSSSpecifityClass;
       Result:=CSSSpecifityClass;
   boStarEqual:
   boStarEqual:
     // contains substring
     // contains substring
@@ -622,9 +645,13 @@ begin
     if PosWord(RightValue,LeftValue)>0 then
     if PosWord(RightValue,LeftValue)>0 then
       Result:=CSSSpecifityClass;
       Result:=CSSSpecifityClass;
   else
   else
-    DoError(20220910164356,'Invalid CSS array selector operator',aBinary);
+    if croErrorOnUnknownName in Options then
+      DoError(20220910164356,'Invalid CSS array selector operator',aBinary);
+    Result:=CSSSpecifityInvalid;
   end;
   end;
+  {$IFDEF VerboseCSSResolver}
   writeln('TCSSResolver.SelectorArrayBinaryMatches Result=',Result);
   writeln('TCSSResolver.SelectorArrayBinaryMatches Result=',Result);
+  {$ENDIF}
 end;
 end;
 
 
 function TCSSResolver.ComputeValue(El: TCSSElement): TCSSString;
 function TCSSResolver.ComputeValue(El: TCSSElement): TCSSString;
@@ -787,6 +814,8 @@ begin
       'all': Result:=CSSAttributeID_All;
       'all': Result:=CSSAttributeID_All;
       end;
       end;
     nikPseudoAttribute:
     nikPseudoAttribute:
+      begin
+      aName:=lowercase(aName); // pseudo attributes are ASCII case insensitive
       case aName of
       case aName of
       ':root': Result:=CSSPseudoID_Root;
       ':root': Result:=CSSPseudoID_Root;
       ':empty': Result:=CSSPseudoID_Empty;
       ':empty': Result:=CSSPseudoID_Empty;
@@ -797,6 +826,7 @@ begin
       ':last-of-type': Result:=CSSPseudoID_LastOfType;
       ':last-of-type': Result:=CSSPseudoID_LastOfType;
       ':only-of-type': Result:=CSSPseudoID_OnlyOfType;
       ':only-of-type': Result:=CSSPseudoID_OnlyOfType;
       end;
       end;
+      end;
     end;
     end;
 
 
     // resolve user defined names
     // resolve user defined names
@@ -805,7 +835,7 @@ begin
 
 
     if Result=CSSIDNone then
     if Result=CSSIDNone then
     begin
     begin
-      if roErrorOnUnknownName in FOptions then
+      if croErrorOnUnknownName in FOptions then
         DoError(20220908235919,'TCSSResolver.ResolveTypeIdentifier unknown '+CSSNumericalIDKindNames[Kind]+' "'+El.Name+'"',El);
         DoError(20220908235919,'TCSSResolver.ResolveTypeIdentifier unknown '+CSSNumericalIDKindNames[Kind]+' "'+El.Name+'"',El);
     end;
     end;
     IdentData:=TCSSIdentifierData.Create;
     IdentData:=TCSSIdentifierData.Create;

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

@@ -200,6 +200,7 @@ type
   published
   published
     procedure Test_Selector_Universal;
     procedure Test_Selector_Universal;
     procedure Test_Selector_Type;
     procedure Test_Selector_Type;
+    // Test list spaces "div, button ,span {}"
     procedure Test_Selector_Id;
     procedure Test_Selector_Id;
     procedure Test_Selector_Class;
     procedure Test_Selector_Class;
     procedure Test_Selector_ClassClass; // ToDo and combinator
     procedure Test_Selector_ClassClass; // ToDo and combinator
@@ -210,6 +211,8 @@ type
     procedure Test_Selector_TypeTildeType; // general sibling combinator
     procedure Test_Selector_TypeTildeType; // general sibling combinator
     procedure Test_Selector_HasAttribute;
     procedure Test_Selector_HasAttribute;
     procedure Test_Selector_AttributeEquals;
     procedure Test_Selector_AttributeEquals;
+    // ToDo: procedure Test_Selector_AttributeEqualsI;
+    // ToDo: procedure Test_Selector_AttributeEqualsS;
     procedure Test_Selector_AttributeBeginsWith;
     procedure Test_Selector_AttributeBeginsWith;
     procedure Test_Selector_AttributeEndsWith;
     procedure Test_Selector_AttributeEndsWith;
     procedure Test_Selector_AttributeBeginsWithHyphen;
     procedure Test_Selector_AttributeBeginsWithHyphen;
@@ -240,6 +243,11 @@ type
     // ToDo: inline style
     // ToDo: inline style
 
 
     // ToDo: specifity
     // ToDo: specifity
+
+    // pseudo elements
+
+    // ToDo: invalid token in selector makes selector invalid
+
   end;
   end;
 
 
 function LinesToStr(const Args: array of const): string;
 function LinesToStr(const Args: array of const): string;
@@ -599,16 +607,19 @@ begin
   '[left~=Two] { width: 5px; }',
   '[left~=Two] { width: 5px; }',
   '[left~=Three] { height: 6px; }',
   '[left~=Three] { height: 6px; }',
   '[left~="Four Five"] { color: #123; }',
   '[left~="Four Five"] { color: #123; }',
+  '[left~=our] { display: none; }',
   '']);
   '']);
   Doc.ApplyStyle;
   Doc.ApplyStyle;
   AssertEquals('Root.Top','4px',Doc.Root.Top);
   AssertEquals('Root.Top','4px',Doc.Root.Top);
   AssertEquals('Root.Width','5px',Doc.Root.Width);
   AssertEquals('Root.Width','5px',Doc.Root.Width);
   AssertEquals('Root.Height','6px',Doc.Root.Height);
   AssertEquals('Root.Height','6px',Doc.Root.Height);
   AssertEquals('Root.Color','',Doc.Root.Color);
   AssertEquals('Root.Color','',Doc.Root.Color);
+  AssertEquals('Root.Display','',Doc.Root.Display);
   AssertEquals('Button1.Top','',Button1.Top);
   AssertEquals('Button1.Top','',Button1.Top);
   AssertEquals('Button1.Width','',Button1.Width);
   AssertEquals('Button1.Width','',Button1.Width);
   AssertEquals('Button1.Height','',Button1.Height);
   AssertEquals('Button1.Height','',Button1.Height);
   AssertEquals('Button1.Color','#123',Button1.Color);
   AssertEquals('Button1.Color','#123',Button1.Color);
+  AssertEquals('Button1.Display','',Button1.Display);
 end;
 end;
 
 
 procedure TTestCSSResolver.Test_Selector_AttributeContainsSubstring;
 procedure TTestCSSResolver.Test_Selector_AttributeContainsSubstring;
@@ -644,7 +655,7 @@ begin
   Button1.Parent:=Doc.Root;
   Button1.Parent:=Doc.Root;
 
 
   Doc.Style:=LinesToStr([
   Doc.Style:=LinesToStr([
-  ':root { top: 4px; }',
+  ':roOt { top: 4px; }',
   '']);
   '']);
   Doc.ApplyStyle;
   Doc.ApplyStyle;
   AssertEquals('Root.Top','4px',Doc.Root.Top);
   AssertEquals('Root.Top','4px',Doc.Root.Top);
@@ -667,8 +678,8 @@ begin
   Div2.Parent:=Doc.Root;
   Div2.Parent:=Doc.Root;
 
 
   Doc.Style:=LinesToStr([
   Doc.Style:=LinesToStr([
-  ':empty { left: 1px; }',
-  'div:empty { top: 2px; }',
+  ':eMpty { left: 1px; }',
+  'div:emPty { top: 2px; }',
   '']);
   '']);
   Doc.ApplyStyle;
   Doc.ApplyStyle;
   AssertEquals('Root.Left','',Doc.Root.Left);
   AssertEquals('Root.Left','',Doc.Root.Left);