Browse Source

fcl-css: attribute selector operations

mattias 2 years ago
parent
commit
b33ae75c59

+ 33 - 7
packages/fcl-css/src/fpcssparser.pp

@@ -44,7 +44,7 @@ Type
     Function GetCurPos : Integer;
   protected
     Procedure DoError(Msg : TCSSString);
-    Procedure DoError(Fmt : TCSSString; Args : Array of const);
+    Procedure DoError(Fmt : TCSSString; const Args : Array of const);
     Procedure Consume(aToken : TCSSToken);
     function ParseComponentValueList(allowRules: Boolean=True): TCSSElement;
     function ParseComponentValue: TCSSElement;
@@ -93,7 +93,7 @@ Resourcestring
   SUnaryInvalidToken = 'Invalid token for unary operation: %s';
   SErrFileSource = 'Error: file "%s" line %d, pos %d: ';
   SErrSource = 'Error: line %d, pos %d: ';
-  SErrUnexpectedToken = 'Unexpected token: Got %s (as TCSSString: "%s"), expected: %s ';
+  SErrUnexpectedToken = 'Unexpected token: Got %s (as string: "%s"), expected: %s ';
   SErrInvalidFloat = 'Invalid float: %s';
   SErrUnexpectedEndOfFile = 'Unexpected EOF while scanning function args: %s';
 
@@ -105,14 +105,16 @@ begin
     ctkPlus : Result:=boPlus;
     ctkMinus:  Result:=boMinus;
     ctkAnd : result:=boAnd;
+    ctkGT : Result:=boGT;
     ctkLT : Result:=boLT;
     ctkDIV : Result:=boDIV;
     ctkStar : Result:=boSTAR;
     ctkTilde : Result:=boTilde;
+    ctkSquared : Result:=boSquared;
+    ctkPIPE : Result:=boPipe;
+    ctkDOLLAR : Result:=boDollar;
     ctkColon : Result:=boCOLON;
     ctkDoubleColon : Result:=boDoubleColon;
-    ctkSquared : Result:=boSquared;
-    ctkGT : Result:=boGT;
   else
     Raise ECSSParser.CreateFmt(SBinaryInvalidToken,[GetEnumName(TypeInfo(aToken),Ord(aToken))]);
     // Result:=boEquals;
@@ -152,7 +154,7 @@ begin
   Raise ECSSParser.Create(ErrAt+Msg)
 end;
 
-procedure TCSSParser.DoError(Fmt: TCSSString; Args: array of const);
+procedure TCSSParser.DoError(Fmt: TCSSString; const Args: array of const);
 begin
   DoError(Format(Fmt,Args));
 end;
@@ -216,7 +218,7 @@ begin
   inherited Destroy;
 end;
 
-Class Function TCSSParser.GetAppendElement(aList : TCSSListElement) : TCSSElement;
+class function TCSSParser.GetAppendElement(aList: TCSSListElement): TCSSElement;
 
 begin
   Case aList.ChildCount of
@@ -659,7 +661,7 @@ function TCSSParser.ParseComponentValueList(allowRules : Boolean = True): TCSSEl
 Const
   TermSeps = [ctkEquals,ctkPlus,ctkMinus,ctkAnd,ctkLT,ctkDIV,
               ctkStar,ctkTilde,ctkColon, ctkDoubleColon,
-              ctkSquared,ctkGT];
+              ctkSquared,ctkGT, ctkPIPE, ctkDOLLAR];
   ListTerms = [ctkEOF,ctkLBRACE,ctkATKEYWORD,ctkComma];
 
   function DoBinary(var aLeft : TCSSElement) : TCSSElement;
@@ -672,7 +674,25 @@ 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 ,[
+               GetEnumName(TypeInfo(TCSSToken),Ord(CurrentToken)),
+               CurrentTokenString,
+               'value'
+               ]);
       Result:=Bin;
       Bin:=nil;
     finally
@@ -692,6 +712,12 @@ begin
       aFactor:=ParseRule(CurrentToken=ctkATKEYWORD)
     else
       aFactor:=ParseComponentValue;
+    if aFactor=nil then
+      DoError(SErrUnexpectedToken ,[
+             GetEnumName(TypeInfo(TCSSToken),Ord(CurrentToken)),
+             CurrentTokenString,
+             'value'
+             ]);
     While Assigned(aFactor) do
       begin
       While CurrentToken in TermSeps do

+ 205 - 27
packages/fcl-css/src/fpcssresolver.pas

@@ -14,7 +14,13 @@
  **********************************************************************
 
 ToDo:
+- descendant combinator
+- and combinator
+- 'all' attribute
 - TCSSResolver.FindComputedAttribute  use binary search for >8 elements
+- CSSSpecifityInline
+- namespaces
+- layers
 
 }
 
@@ -26,7 +32,7 @@ unit fpCSSResolver;
 interface
 
 uses
-  Classes, SysUtils, Contnrs, fpCSSTree;
+  Classes, SysUtils, Contnrs, StrUtils, fpCSSTree;
 
 const
   CSSSpecifityType = 1;
@@ -113,12 +119,21 @@ type
   TCSSComputedAttributeArray = array of TCSSComputedAttribute;
   PCSSComputedAttribute = ^TCSSComputedAttribute;
 
-  TCSSIdentifierData = class
+  TCSSElResolverData = class
+  public
+    Element: TCSSElement;
+    Next, Prev: TCSSElResolverData;
+  end;
+
+  TCSSIdentifierData = class(TCSSElResolverData)
   public
-    Identifier: TCSSIdentifierElement;
     NumericalID: TCSSNumericalID;
     Kind: TCSSNumericalIDKind;
-    Next, Prev: TCSSIdentifierData;
+  end;
+
+  TCSSValueData = class(TCSSElResolverData)
+  public
+    NormValue: string;
   end;
 
   TCSSResolverOption = (
@@ -144,8 +159,8 @@ type
     FOptions: TCSSResolverOptions;
     FStyle: TCSSElement;
     FOwnsStyle: boolean;
-    FFirstIdentifierData: TCSSIdentifierData;
-    FLastIdentifierData: TCSSIdentifierData;
+    FFirstElData: TCSSElResolverData;
+    FLastElData: TCSSElResolverData;
     function GetAttributes(Index: integer): PCSSComputedAttribute;
     function GetNumericalIDs(Kind: TCSSNumericalIDKind): TCSSNumericalIDs;
     procedure SetNumericalIDs(Kind: TCSSNumericalIDKind;
@@ -165,8 +180,15 @@ type
     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;
+    function SelectorArrayBinaryMatches(aBinary: TCSSBinaryElement; const TestNode: TCSSNode): TCSSSpecifity; virtual;
+    function ComputeValue(El: TCSSElement): TCSSString; virtual;
+    function IsWordBegin(const s: TCSSString; p: integer): boolean; virtual;
+    function IsWordEnd(const s: TCSSString; p: integer): boolean; virtual;
+    function PosWord(const aSearch, aText: TCSSString): integer; virtual;
     procedure MergeProperty(El: TCSSElement; Specifity: TCSSSpecifity); virtual;
     function ResolveIdentifier(El: TCSSIdentifierElement; Kind: TCSSNumericalIDKind): TCSSNumericalID; virtual;
+    procedure AddElData(El: TCSSElement; ElData: TCSSElResolverData); virtual;
+    function AddElValueData(El: TCSSElement; const aValue: TCSSString): TCSSValueData; virtual;
     function FindComputedAttribute(AttrID: TCSSNumericalID): PCSSComputedAttribute;
     function AddComputedAttribute(TheAttrID: TCSSNumericalID; aSpecifity: TCSSSpecifity;
                           aValue: TCSSElement): PCSSComputedAttribute;
@@ -473,10 +495,153 @@ begin
       if TestNode.HasCSSAttribute(AttrID) then
         Result:=CSSSpecifityClass;
     end;
-  end else
+  end else if C=TCSSBinaryElement then
+    Result:=SelectorArrayBinaryMatches(TCSSBinaryElement(El),TestNode)
+  else
     DoError(20220910153725,'Invalid CSS array selector',El);
 end;
 
+function TCSSResolver.SelectorArrayBinaryMatches(aBinary: TCSSBinaryElement;
+  const TestNode: TCSSNode): TCSSSpecifity;
+var
+  Left, Right: TCSSElement;
+  AttrID: TCSSNumericalID;
+  LeftValue, RightValue: TCSSString;
+  C: TClass;
+begin
+  Result:=-1;
+  Left:=aBinary.Left;
+  if Left.ClassType<>TCSSIdentifierElement then
+    DoError(20220910164353,'Invalid CSS array selector, expected attribute',Left);
+  AttrID:=ResolveIdentifier(TCSSIdentifierElement(Left),nikAttribute);
+  writeln('TCSSResolver.SelectorArrayBinaryMatches AttrID=',AttrID,' Value=',TCSSIdentifierElement(Left).Value);
+  case AttrID of
+  CSSIDNone,
+  CSSAttributeID_All: exit;
+  CSSAttributeID_ID:
+    LeftValue:=TestNode.GetCSSID;
+  else
+    LeftValue:=TestNode.GetCSSAttribute(AttrID);
+  end;
+
+  Right:=aBinary.Right;
+  C:=Right.ClassType;
+  if (C=TCSSStringElement) or (C=TCSSIntegerElement) or (C=TCSSFloatElement)
+      or (C=TCSSIdentifierElement) then
+    // ok
+  else
+    DoError(20220910164921,'Invalid CSS array selector, expected string',Right);
+  RightValue:=ComputeValue(Right);
+
+  writeln('TCSSResolver.SelectorArrayBinaryMatches Left="',LeftValue,'" Right="',RightValue,'" Op=',aBinary.Operation);
+  case aBinary.Operation of
+  boEquals:
+    if AnsiCompareStr(LeftValue,RightValue)=0 then
+      Result:=CSSSpecifityClass;
+  boSquaredEqual:
+    // begins with
+    if AnsiCompareStr(LeftStr(LeftValue,length(RightValue)),RightValue)=0 then
+      Result:=CSSSpecifityClass;
+  boDollarEqual:
+    // ends with
+    if AnsiCompareStr(RightStr(LeftValue,length(RightValue)),RightValue)=0 then
+      Result:=CSSSpecifityClass;
+  boPipeEqual:
+    // equal to or starts with name-hyphen
+    if (AnsiCompareStr(LeftValue,RightValue)=0)
+        or (AnsiCompareStr(LeftStr(LeftValue,length(RightValue)+1),RightValue+'-')=0) then
+      Result:=CSSSpecifityClass;
+  boStarEqual:
+    // contains substring
+    if Pos(RightValue,LeftValue)>0 then
+      Result:=CSSSpecifityClass;
+  boTileEqual:
+    // contains word
+    if PosWord(RightValue,LeftValue)>0 then
+      Result:=CSSSpecifityClass;
+  else
+    DoError(20220910164356,'Invalid CSS array selector operator',aBinary);
+  end;
+  writeln('TCSSResolver.SelectorArrayBinaryMatches Result=',Result);
+end;
+
+function TCSSResolver.ComputeValue(El: TCSSElement): TCSSString;
+var
+  ElData: TObject;
+  C: TClass;
+  StrEl: TCSSStringElement;
+  IntEl: TCSSIntegerElement;
+  FloatEl: TCSSFloatElement;
+begin
+  C:=El.ClassType;
+  if C=TCSSIdentifierElement then
+    Result:=TCSSIdentifierElement(El).Value
+  else if (C=TCSSStringElement)
+      or (C=TCSSIntegerElement)
+      or (C=TCSSFloatElement) then
+  begin
+    ElData:=El.CustomData;
+    if ElData is TCSSValueData then
+      exit(TCSSValueData(ElData).NormValue);
+    if C=TCSSStringElement then
+    begin
+      StrEl:=TCSSStringElement(El);
+      Result:=StrEl.Value;
+      writeln('TCSSResolver.ComputeValue String=[',Result,']');
+    end
+    else if C=TCSSIntegerElement then
+    begin
+      IntEl:=TCSSIntegerElement(El);
+      Result:=IntEl.AsString;
+    end else if C=TCSSFloatElement then
+    begin
+      FloatEl:=TCSSFloatElement(El);
+      Result:=FloatEl.AsString;
+    end;
+    writeln('TCSSResolver.ComputeValue Value="',Result,'"');
+    AddElValueData(El,Result);
+  end else
+    DoError(20220910235106,'TCSSResolver.ComputeValue not supported',El);
+end;
+
+const
+  WordChar = ['a'..'z','A'..'Z','0'..'9',#192..#255];
+
+function TCSSResolver.IsWordBegin(const s: TCSSString; p: integer): boolean;
+begin
+  Result:=false;
+  if p<1 then exit;
+  if p>length(s) then exit;
+  // simple check. ToDo: check unicode
+  if (p>1) and (s[p-1] in WordChar) then exit;
+  if not (s[p] in WordChar) then exit;
+  Result:=true;
+end;
+
+function TCSSResolver.IsWordEnd(const s: TCSSString; p: integer): boolean;
+begin
+  Result:=false;
+  if p<=1 then exit;
+  if p>length(s)+1 then exit;
+  // simple check. ToDo: check unicode
+  if (p>1) and not (s[p-1] in WordChar) then exit;
+  if (s[p] in WordChar) then exit;
+  Result:=true;
+end;
+
+function TCSSResolver.PosWord(const aSearch, aText: TCSSString): integer;
+begin
+  if aSearch='' then exit(0);
+  if aText='' then exit(0);
+  Result:=Pos(aSearch,aText);
+  while Result>0 do
+  begin
+    if IsWordBegin(aText,Result) and IsWordEnd(aText,Result+length(aSearch)) then
+      exit;
+    Result:=PosEx(aSearch,aText,Result+1);
+  end;
+end;
+
 procedure TCSSResolver.MergeProperty(El: TCSSElement; Specifity: TCSSSpecifity);
 var
   C: TClass;
@@ -571,21 +736,34 @@ begin
         DoError(20220908235919,'TCSSResolver.ResolveTypeIdentifier unknown '+CSSNumericalIDKindNames[Kind]+' "'+El.Name+'"',El);
     end;
     IdentData:=TCSSIdentifierData.Create;
-    El.CustomData:=IdentData;
-    IdentData.Identifier:=El;
     IdentData.Kind:=Kind;
     IdentData.NumericalID:=Result;
-    if FFirstIdentifierData=nil then
-    begin
-      FFirstIdentifierData:=IdentData;
-    end else begin
-      FLastIdentifierData.Next:=IdentData;
-      IdentData:=FLastIdentifierData;
-    end;
-    FLastIdentifierData:=IdentData;
+    AddElData(El,IdentData);
   end;
 end;
 
+procedure TCSSResolver.AddElData(El: TCSSElement; ElData: TCSSElResolverData);
+begin
+  El.CustomData:=ElData;
+  ElData.Element:=El;
+  if FFirstElData=nil then
+  begin
+    FFirstElData:=ElData;
+  end else begin
+    FLastElData.Next:=ElData;
+    ElData.Prev:=FLastElData;
+  end;
+  FLastElData:=ElData;
+end;
+
+function TCSSResolver.AddElValueData(El: TCSSElement; const aValue: TCSSString
+  ): TCSSValueData;
+begin
+  Result:=TCSSValueData.Create;
+  Result.NormValue:=aValue;
+  AddElData(El,Result);
+end;
+
 function TCSSResolver.FindComputedAttribute(AttrID: TCSSNumericalID
   ): PCSSComputedAttribute;
 var
@@ -658,19 +836,19 @@ end;
 
 procedure TCSSResolver.ClearStyleCustomData;
 var
-  Data: TCSSIdentifierData;
+  Data: TCSSElResolverData;
 begin
-  while FLastIdentifierData<>nil do
+  while FLastElData<>nil do
   begin
-    Data:=FLastIdentifierData;
-    FLastIdentifierData:=Data.Prev;
-    if FLastIdentifierData<>nil then
-      FLastIdentifierData.Next:=nil
+    Data:=FLastElData;
+    FLastElData:=Data.Prev;
+    if FLastElData<>nil then
+      FLastElData.Next:=nil
     else
-      FFirstIdentifierData:=nil;
-    if Data.Identifier.CustomData<>Data then
-      DoError(20220908234726,'TCSSResolver.ClearStyleCustomData',Data.Identifier);
-    Data.Identifier.CustomData:=nil;
+      FFirstElData:=nil;
+    if Data.Element.CustomData<>Data then
+      DoError(20220908234726,'TCSSResolver.ClearStyleCustomData',Data.Element);
+    Data.Element.CustomData:=nil;
     Data.Free;
   end;
 end;

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

@@ -63,7 +63,9 @@ Type
     ctkPSEUDO,
     ctkPSEUDOFUNCTION,
     ctkSQUARED,
-    ctkUNICODERANGE
+    ctkUNICODERANGE,
+    ctkPIPE,
+    ctkDOLLAR
    );
   TCSSTokens = Set of TCSSToken;
 
@@ -597,7 +599,7 @@ begin
       repeat
         Inc(TokenStr);
         //If (TokenStr[0]='\') and (TokenStr[1]='u') then
-      until not (TokenStr[0] in ['A'..'Z', 'a'..'z', '0'..'9', '_','-','$']);
+      until not (TokenStr[0] in ['A'..'Z', 'a'..'z', '0'..'9', '_','-']);
     IsEscape:=TokenStr[0]='\';
     if IsEscape then
       begin
@@ -728,6 +730,8 @@ begin
     '^': CharToken(ctkSQUARED);
     ',': CharToken(ctkCOMMA);
     '~': CharToken(ctkTILDE);
+    '|': CharToken(ctkPIPE);
+    '$': CharToken(ctkDOLLAR);
     ';': CharToken(ctkSEMICOLON);
     '@': Result:=DoIdentifierLike;
     ':':

+ 5 - 3
packages/fcl-css/src/fpcsstree.pp

@@ -158,7 +158,9 @@ Type
 
   { TCSSBinaryElement }
   TCSSBinaryOperation = (boEquals,boPlus,boMinus,boAnd,boLT,boGT,boDIV,
-                         boStar,boTilde,boColon, boDoubleColon,boSquared);
+                         boStar,boTilde,boColon, boDoubleColon,boSquared,
+                         boPipe, boDollar,
+                         boStarEqual,boTileEqual,boSquaredEqual,boPipeEqual,boDollarEqual);
   TCSSBinaryElement = Class(TCSSBaseUnaryElement)
   private
     FLeft: TCSSElement;
@@ -375,8 +377,8 @@ Const
   UnaryOperators : Array[TCSSUnaryOperation] of TCSSString =
         ('::','-','+','/');
   BinaryOperators : Array[TCSSBinaryOperation] of TCSSString =
-        ('=','+','-','and','<','>','/','*','~',':','::','^');
-
+        ('=','+','-','and','<','>','/','*','~',':','::','^','|','$',
+         '*=','~=','^=','|=','$=');
 
 implementation
 

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

@@ -72,6 +72,11 @@ type
     procedure TestDoublePrefixedEmptyRule;
     procedure TestDoubleMixedPrefixedEmptyRule;
     procedure TestAttributePrefixedEmptyRule;
+    procedure TestAttributeSquaredEqualRule;
+    procedure TestAttributePipeEqualRule;
+    procedure TestAttributeStarEqualRule;
+    procedure TestAttributeDollarEqualRule;
+    procedure TestAttributeTildeEqualRule;
     procedure TestPseudoPrefixedEmptyRule;
     procedure TestPseudoFunctionEmptyRule;
     procedure TestFuncPrefixedEmptyRule;
@@ -415,6 +420,121 @@ begin
   AssertEquals('Binary op',boEquals,Bin.Operation);
 end;
 
+procedure TTestCSSParser.TestAttributeSquaredEqualRule;
+var
+  R : TCSSRuleElement;
+  sel: TCSSArrayElement;
+  bin : TCSSBinaryElement;
+  Left: TCSSIdentifierElement;
+
+begin
+  ParseRule('[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]));
+  if Sel.Prefix<>nil then
+    Fail('no prefix');
+  AssertEquals('Array count',1,Sel.ChildCount);
+  Bin:=TCSSBinaryElement(CheckClass('Bin',TCSSBinaryElement,sel.children[0]));
+  AssertEquals('Binary op',boSquaredEqual,Bin.Operation);
+  Left:=TCSSIdentifierElement(CheckClass('Bin.Left',TCSSIdentifierElement,Bin.Left));
+  AssertEquals('left=b','b',Left.Value);
+  CheckClass('Bin.Right',TCSSStringElement,Bin.Right);
+end;
+
+procedure TTestCSSParser.TestAttributePipeEqualRule;
+var
+  R : TCSSRuleElement;
+  sel: TCSSArrayElement;
+  bin : TCSSBinaryElement;
+  Left: TCSSIdentifierElement;
+
+begin
+  ParseRule('[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]));
+  if Sel.Prefix<>nil then
+    Fail('no prefix');
+  AssertEquals('Array count',1,Sel.ChildCount);
+  Bin:=TCSSBinaryElement(CheckClass('Bin',TCSSBinaryElement,sel.children[0]));
+  AssertEquals('Binary op',boPipeEqual,Bin.Operation);
+  Left:=TCSSIdentifierElement(CheckClass('Bin.Left',TCSSIdentifierElement,Bin.Left));
+  AssertEquals('left=b','b',Left.Value);
+  CheckClass('Bin.Right',TCSSStringElement,Bin.Right);
+end;
+
+procedure TTestCSSParser.TestAttributeStarEqualRule;
+var
+  R : TCSSRuleElement;
+  sel: TCSSArrayElement;
+  bin : TCSSBinaryElement;
+  Left: TCSSIdentifierElement;
+
+begin
+  ParseRule('[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]));
+  if Sel.Prefix<>nil then
+    Fail('no prefix');
+  AssertEquals('Array count',1,Sel.ChildCount);
+  Bin:=TCSSBinaryElement(CheckClass('Bin',TCSSBinaryElement,sel.children[0]));
+  AssertEquals('Binary op',boStarEqual,Bin.Operation);
+  Left:=TCSSIdentifierElement(CheckClass('Bin.Left',TCSSIdentifierElement,Bin.Left));
+  AssertEquals('left=b','b',Left.Value);
+  CheckClass('Bin.Right',TCSSStringElement,Bin.Right);
+end;
+
+procedure TTestCSSParser.TestAttributeDollarEqualRule;
+var
+  R : TCSSRuleElement;
+  sel: TCSSArrayElement;
+  bin : TCSSBinaryElement;
+  Left: TCSSIdentifierElement;
+
+begin
+  ParseRule('[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]));
+  if Sel.Prefix<>nil then
+    Fail('no prefix');
+  AssertEquals('Array count',1,Sel.ChildCount);
+  Bin:=TCSSBinaryElement(CheckClass('Bin',TCSSBinaryElement,sel.children[0]));
+  AssertEquals('Binary op',boDollarEqual,Bin.Operation);
+  Left:=TCSSIdentifierElement(CheckClass('Bin.Left',TCSSIdentifierElement,Bin.Left));
+  AssertEquals('left=b','b',Left.Value);
+  CheckClass('Bin.Right',TCSSStringElement,Bin.Right);
+end;
+
+procedure TTestCSSParser.TestAttributeTildeEqualRule;
+var
+  R : TCSSRuleElement;
+  sel: TCSSArrayElement;
+  bin : TCSSBinaryElement;
+  Left: TCSSIdentifierElement;
+
+begin
+  ParseRule('[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]));
+  if Sel.Prefix<>nil then
+    Fail('no prefix');
+  AssertEquals('Array count',1,Sel.ChildCount);
+  Bin:=TCSSBinaryElement(CheckClass('Bin',TCSSBinaryElement,sel.children[0]));
+  AssertEquals('Binary op',boTileEqual,Bin.Operation);
+  Left:=TCSSIdentifierElement(CheckClass('Bin.Left',TCSSIdentifierElement,Bin.Left));
+  AssertEquals('left=b','b',Left.Value);
+  CheckClass('Bin.Right',TCSSStringElement,Bin.Right);
+end;
+
 procedure TTestCSSParser.TestPseudoPrefixedEmptyRule;
 var
   R : TCSSRuleElement;

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

@@ -202,6 +202,12 @@ type
     procedure Test_Selector_TypePlusType; // adjacent sibling combinator
     procedure Test_Selector_TypeTildeType; // general sibling combinator
     procedure Test_Selector_HasAttribute;
+    procedure Test_Selector_AttributeEquals;
+    procedure Test_Selector_AttributeBeginsWith;
+    procedure Test_Selector_AttributeEndsWith;
+    procedure Test_Selector_AttributeBeginsWithHyphen;
+    procedure Test_Selector_AttributeContainsWord;
+    procedure Test_Selector_AttributeContainsSubstring;
   end;
 
 function LinesToStr(const Args: array of const): string;
@@ -460,6 +466,141 @@ begin
   AssertEquals('Button1.Width','4px',Button1.Width);
 end;
 
+procedure TTestCSSResolver.Test_Selector_AttributeEquals;
+var
+  Button1: TDemoButton;
+begin
+  Doc.Root:=TDemoNode.Create(nil);
+  Doc.Root.Left:='2px';
+
+  Button1:=TDemoButton.Create(Doc);
+  Button1.Parent:=Doc.Root;
+  Button1.Left:='3px';
+
+  Doc.Style:=LinesToStr([
+  '[left=2px] { top: 4px; }',
+  '']);
+  Doc.ApplyStyle;
+  AssertEquals('Root.Top','4px',Doc.Root.Top);
+  AssertEquals('Button1.Top','',Button1.Top);
+end;
+
+procedure TTestCSSResolver.Test_Selector_AttributeBeginsWith;
+var
+  Button1: TDemoButton;
+begin
+  Doc.Root:=TDemoNode.Create(nil);
+  Doc.Root.Left:='Foo';
+
+  Button1:=TDemoButton.Create(Doc);
+  Button1.Parent:=Doc.Root;
+  Button1.Left:='Foo Bar';
+
+  Doc.Style:=LinesToStr([
+  '[left^=Fo] { top: 4px; }',
+  '[left^="Foo B"] { width: 5px; }',
+  '']);
+  Doc.ApplyStyle;
+  AssertEquals('Root.Top','4px',Doc.Root.Top);
+  AssertEquals('Root.Width','',Doc.Root.Width);
+  AssertEquals('Button1.Top','4px',Button1.Top);
+  AssertEquals('Button1.Width','5px',Button1.Width);
+end;
+
+procedure TTestCSSResolver.Test_Selector_AttributeEndsWith;
+var
+  Button1: TDemoButton;
+begin
+  Doc.Root:=TDemoNode.Create(nil);
+  Doc.Root.Left:='Foo';
+
+  Button1:=TDemoButton.Create(Doc);
+  Button1.Parent:=Doc.Root;
+  Button1.Left:='Foo Bar';
+
+  Doc.Style:=LinesToStr([
+  '[left$=o] { top: 4px; }',
+  '[left$="o Bar"] { width: 5px; }',
+  '']);
+  Doc.ApplyStyle;
+  AssertEquals('Root.Top','4px',Doc.Root.Top);
+  AssertEquals('Root.Width','',Doc.Root.Width);
+  AssertEquals('Button1.Top','',Button1.Top);
+  AssertEquals('Button1.Width','5px',Button1.Width);
+end;
+
+procedure TTestCSSResolver.Test_Selector_AttributeBeginsWithHyphen;
+var
+  Button1: TDemoButton;
+begin
+  Doc.Root:=TDemoNode.Create(nil);
+  Doc.Root.Left:='Foo';
+
+  Button1:=TDemoButton.Create(Doc);
+  Button1.Parent:=Doc.Root;
+  Button1.Left:='Foo-Bar';
+
+  Doc.Style:=LinesToStr([
+  '[left|=Foo] { top: 4px; }',
+  '[left|="Fo"] { width: 5px; }',
+  '']);
+  Doc.ApplyStyle;
+  AssertEquals('Root.Top','4px',Doc.Root.Top);
+  AssertEquals('Root.Width','',Doc.Root.Width);
+  AssertEquals('Button1.Top','4px',Button1.Top);
+  AssertEquals('Button1.Width','',Button1.Width);
+end;
+
+procedure TTestCSSResolver.Test_Selector_AttributeContainsWord;
+var
+  Button1: TDemoButton;
+begin
+  Doc.Root:=TDemoNode.Create(nil);
+  Doc.Root.Left:='One Two Three';
+
+  Button1:=TDemoButton.Create(Doc);
+  Button1.Parent:=Doc.Root;
+  Button1.Left:='Four Five';
+
+  Doc.Style:=LinesToStr([
+  '[left~=One] { top: 4px; }',
+  '[left~=Two] { width: 5px; }',
+  '[left~=Three] { height: 6px; }',
+  '[left~="Four Five"] { color: #123; }',
+  '']);
+  Doc.ApplyStyle;
+  AssertEquals('Root.Top','4px',Doc.Root.Top);
+  AssertEquals('Root.Width','5px',Doc.Root.Width);
+  AssertEquals('Root.Height','6px',Doc.Root.Height);
+  AssertEquals('Root.Color','',Doc.Root.Color);
+  AssertEquals('Button1.Top','',Button1.Top);
+  AssertEquals('Button1.Width','',Button1.Width);
+  AssertEquals('Button1.Height','',Button1.Height);
+  AssertEquals('Button1.Color','#123',Button1.Color);
+end;
+
+procedure TTestCSSResolver.Test_Selector_AttributeContainsSubstring;
+var
+  Button1: TDemoButton;
+begin
+  Doc.Root:=TDemoNode.Create(nil);
+  Doc.Root.Left:='Foo';
+
+  Button1:=TDemoButton.Create(Doc);
+  Button1.Parent:=Doc.Root;
+  Button1.Left:='Foo Bar';
+
+  Doc.Style:=LinesToStr([
+  '[left*=oo] { top: 4px; }',
+  '[left*="o B"] { width: 5px; }',
+  '']);
+  Doc.ApplyStyle;
+  AssertEquals('Root.Top','4px',Doc.Root.Top);
+  AssertEquals('Root.Width','',Doc.Root.Width);
+  AssertEquals('Button1.Top','4px',Button1.Top);
+  AssertEquals('Button1.Width','5px',Button1.Width);
+end;
+
 { TDemoDiv }
 
 class function TDemoDiv.CSSTypeName: TCSSString;
@@ -824,7 +965,7 @@ function TDemoNode.GetCSSAttribute(const AttrID: TCSSNumericalID): TCSSString;
 var
   Attr: TDemoNodeAttribute;
 begin
-  if (AttrID<DemoAttrIDBase) or (AttrID>ord(High(TDemoNodeAttribute))) then
+  if (AttrID<DemoAttrIDBase) or (AttrID>DemoAttrIDBase+ord(High(TDemoNodeAttribute))) then
     exit('');
   Attr:=TDemoNodeAttribute(AttrID-DemoAttrIDBase);
   Result:=Attribute[Attr];

+ 1 - 5
packages/fcl-css/tests/tccsstree.pp

@@ -535,10 +535,6 @@ end;
 
 procedure TCSSTreeAsStringTest.TestBINARYOP;
 
-Const
-  MyBinaryOperators : Array[TCSSBinaryOperation] of string =
-        ('=','+','-','and','<','>','/','*','~',':','::','^');
-
 Var
   Op : TCSSBinaryOperation;
   Sop : String;
@@ -547,7 +543,7 @@ begin
   For Op in TCSSBinaryOperation do
     begin
     CreateBinaryOperation(Op,'a','b',amReplace);
-    Sop:=MyBinaryOperators[Op];
+    Sop:=BinaryOperators[Op];
     if Not (Op in [boColon,boDoubleColon]) then
       Sop:=' '+Sop+' ';
     AssertEquals('Value '+Sop,'a'+sop+'b',Element.AsString)