Browse Source

fcl-css: resolver: has attribute check

mattias 2 years ago
parent
commit
a23a4805f4
2 changed files with 164 additions and 12 deletions
  1. 75 6
      packages/fcl-css/src/fpcssresolver.pas
  2. 89 6
      packages/fcl-css/tests/tccssresolver.pp

+ 75 - 6
packages/fcl-css/src/fpcssresolver.pas

@@ -30,14 +30,15 @@ uses
 
 const
   CSSSpecifityType = 1;
-  CSSSpecifityClass = 10; // includes attribute selectors [href]
+  CSSSpecifityClass = 10; // includes attribute selectors e.g. [href]
   CSSSpecifityIdentifier = 100;
   CSSSpecifityInline = 1000;
   CSSSpecifityImportant = 10000;
 
   CSSIDNone = 0;
-  CSSTypeIDUniversal = 1; // id of type '*'
-  CSSAttributeIDAll = 1; // id of attribute key 'all'
+  CSSTypeID_Universal = 1; // id of type '*'
+  CSSAttributeID_ID = 1; // id of attribute key 'id'
+  CSSAttributeID_All = 2; // id of attribute key 'all'
 
 type
   TCSSMsgID = int64;
@@ -47,6 +48,15 @@ type
   ECSSResolver = class(Exception)
   end;
 
+  TCSSAttributeMatchKind = (
+    camkEqual,
+    camkContains,
+    camkContainsWord,
+    camkBegins,
+    camkEnds
+    );
+  TCSSAttributeMatchKinds = set of TCSSAttributeMatchKind;
+
   { TCSSNode }
 
   TCSSNode = interface
@@ -57,6 +67,8 @@ type
     function GetCSSParent: TCSSNode;
     function GetCSSIndex: integer; // node index in parent's children
     function GetCSSPreviousSibling: TCSSNode;
+    function HasCSSAttribute(const AttrID: TCSSNumericalID): boolean;
+    function GetCSSAttribute(const AttrID: TCSSNumericalID): TCSSString;
     procedure SetCSSValue(AttrID: TCSSNumericalID; Value: TCSSElement);
   end;
 
@@ -152,6 +164,7 @@ type
     function SelectorStringMatches(aString: TCSSStringElement; const 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;
     procedure MergeProperty(El: TCSSElement; Specifity: TCSSSpecifity); virtual;
     function ResolveIdentifier(El: TCSSIdentifierElement; Kind: TCSSNumericalIDKind): TCSSNumericalID; virtual;
     function FindComputedAttribute(AttrID: TCSSNumericalID): PCSSComputedAttribute;
@@ -304,6 +317,8 @@ begin
     Result:=SelectorStringMatches(TCSSStringElement(aSelector),TestNode)
   else if C=TCSSBinaryElement then
     Result:=SelectorBinaryMatches(TCSSBinaryElement(aSelector),TestNode)
+  else if C=TCSSArrayElement then
+    Result:=SelectorArrayMatches(TCSSArrayElement(aSelector),TestNode)
   else if C=TCSSListElement then
     Result:=SelectorListMatches(TCSSListElement(aSelector),TestNode)
   else
@@ -317,7 +332,7 @@ var
 begin
   Result:=-1;
   TypeID:=ResolveIdentifier(Identifier,nikType);
-  if TypeID=CSSTypeIDUniversal then
+  if TypeID=CSSTypeID_Universal then
   begin
     // universal selector
     Result:=0;
@@ -428,6 +443,40 @@ begin
   end;
 end;
 
+function TCSSResolver.SelectorArrayMatches(anArray: TCSSArrayElement;
+  const TestNode: TCSSNode): TCSSSpecifity;
+var
+  El: TCSSElement;
+  C: TClass;
+  AttrID: TCSSNumericalID;
+begin
+  Result:=-1;
+  if anArray.Prefix<>nil then
+    DoError(20220910154004,'Invalid CSS array selector prefix',anArray.Prefix);
+  if anArray.ChildCount<>1 then
+    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];
+  C:=El.ClassType;
+  if C=TCSSIdentifierElement then
+  begin
+    // [name]  ->  has attribute name
+    AttrID:=ResolveIdentifier(TCSSIdentifierElement(El),nikAttribute);
+    case AttrID of
+    CSSIDNone,
+    CSSAttributeID_All: ;
+    CSSAttributeID_ID:
+      Result:=CSSSpecifityClass;
+    else
+      if TestNode.HasCSSAttribute(AttrID) then
+        Result:=CSSSpecifityClass;
+    end;
+  end else
+    DoError(20220910153725,'Invalid CSS array selector',El);
+end;
+
 procedure TCSSResolver.MergeProperty(El: TCSSElement; Specifity: TCSSSpecifity);
 var
   C: TClass;
@@ -456,7 +505,7 @@ begin
       AttrID:=ResolveIdentifier(TCSSIdentifierElement(aKey),nikAttribute);
       if AttrID=CSSIDNone then
         DoError(20220909000932,'Unknown CSS property "'+TCSSIdentifierElement(aKey).Name+'"',aKey)
-      else if AttrID=CSSAttributeIDAll then
+      else if AttrID=CSSAttributeID_All then
         // 'all'
         DoError(20220909001019,'Not yet implemented CSS property "'+TCSSIdentifierElement(aKey).Name+'"',aKey)
       else begin
@@ -483,6 +532,7 @@ function TCSSResolver.ResolveIdentifier(El: TCSSIdentifierElement;
 var
   Data: TObject;
   IdentData: TCSSIdentifierData;
+  aName: TCSSString;
 begin
   Data:=El.CustomData;
   if Data<>nil then
@@ -495,7 +545,26 @@ begin
     {$ENDIF}
   end else
   begin
-    Result:=FNumericalIDs[Kind][El.Name];
+    aName:=El.Name;
+    Result:=CSSIDNone;
+
+    // check built-in names
+    case Kind of
+    nikType:
+      case aName of
+      '*': Result:=CSSTypeID_Universal;
+      end;
+    nikAttribute:
+      case aName of
+      'id': Result:=CSSAttributeID_ID;
+      'all': Result:=CSSAttributeID_All;
+      end;
+    end;
+
+    // resolve user defined names
+    if Result=0 then
+      Result:=FNumericalIDs[Kind][aName];
+
     if Result=CSSIDNone then
     begin
       if roErrorOnUnknownName in FOptions then

+ 89 - 6
packages/fcl-css/tests/tccssresolver.pp

@@ -94,6 +94,8 @@ type
     function GetCSSParent: TCSSNode; virtual;
     function GetCSSIndex: integer; virtual;
     function GetCSSPreviousSibling: TCSSNode; virtual;
+    function HasCSSAttribute(const AttrID: TCSSNumericalID): boolean; virtual;
+    function GetCSSAttribute(const AttrID: TCSSNumericalID): TCSSString; virtual;
     property Parent: TDemoNode read FParent write SetParent;
     property NodeCount: integer read GetNodeCount;
     property Nodes[Index: integer]: TDemoNode read GetNodes; default;
@@ -131,9 +133,18 @@ type
   { TDemoButton }
 
   TDemoButton = class(TDemoNode)
+  private
+    FCaption: string;
+    procedure SetCaption(const AValue: string);
   public
+    class var CSSCaptionID: TCSSNumericalID;
     class function CSSTypeName: TCSSString; override;
     class function CSSTypeID: TCSSNumericalID; override;
+    function HasCSSAttribute(const AttrID: TCSSNumericalID): boolean; override;
+    function GetCSSAttribute(const AttrID: TCSSNumericalID): TCSSString;
+      override;
+    procedure SetCSSValue(AttrID: TCSSNumericalID; Value: TCSSElement); override;
+    property Caption: string read FCaption write SetCaption;
   end;
 
   { TDemoDocument }
@@ -184,12 +195,13 @@ type
     procedure Test_Selector_Type;
     procedure Test_Selector_Id;
     procedure Test_Selector_Class;
-    procedure Test_Selector_ClassClass; // and combinator
-    procedure Test_Selector_ClassSpaceClass; // descendant combinator
+    procedure Test_Selector_ClassClass; // ToDo and combinator
+    procedure Test_Selector_ClassSpaceClass; // ToDo descendant combinator
     procedure Test_Selector_TypeCommaType; // or combinator
     procedure Test_Selector_ClassGTClass; // child combinator
     procedure Test_Selector_TypePlusType; // adjacent sibling combinator
     procedure Test_Selector_TypeTildeType; // general sibling combinator
+    procedure Test_Selector_HasAttribute;
   end;
 
 function LinesToStr(const Args: array of const): string;
@@ -427,6 +439,27 @@ begin
   AssertEquals('Button3.left','10px',Button3.Left);
 end;
 
+procedure TTestCSSResolver.Test_Selector_HasAttribute;
+var
+  Button1: TDemoButton;
+begin
+  Doc.Root:=TDemoNode.Create(nil);
+
+  Button1:=TDemoButton.Create(Doc);
+  Button1.Parent:=Doc.Root;
+  Button1.Left:='2px';
+
+  Doc.Style:=LinesToStr([
+  '[left] { top: 3px; }',
+  '[caption] { width: 4px; }',
+  '']);
+  Doc.ApplyStyle;
+  AssertEquals('Root.Top','3px',Doc.Root.Top);
+  AssertEquals('Root.Width','',Doc.Root.Width);
+  AssertEquals('Button1.Top','3px',Button1.Top);
+  AssertEquals('Button1.Width','4px',Button1.Width);
+end;
+
 { TDemoDiv }
 
 class function TDemoDiv.CSSTypeName: TCSSString;
@@ -453,6 +486,12 @@ end;
 
 { TDemoButton }
 
+procedure TDemoButton.SetCaption(const AValue: string);
+begin
+  if FCaption=AValue then Exit;
+  FCaption:=AValue;
+end;
+
 class function TDemoButton.CSSTypeName: TCSSString;
 begin
   Result:='button';
@@ -463,6 +502,27 @@ begin
   Result:=103;
 end;
 
+function TDemoButton.HasCSSAttribute(const AttrID: TCSSNumericalID): boolean;
+begin
+  Result:=(AttrID=CSSCaptionID) or inherited HasCSSAttribute(AttrID);
+end;
+
+function TDemoButton.GetCSSAttribute(const AttrID: TCSSNumericalID): TCSSString;
+begin
+  if AttrID=CSSCaptionID then
+    Result:=Caption
+  else
+    Result:=inherited GetCSSAttribute(AttrID);
+end;
+
+procedure TDemoButton.SetCSSValue(AttrID: TCSSNumericalID; Value: TCSSElement);
+begin
+  if AttrID=CSSCaptionID then
+    SetCaption(Value.AsString)
+  else
+    inherited SetCSSValue(AttrID, Value);
+end;
+
 { TDemoDocument }
 
 procedure TDemoDocument.SetStyle(const AValue: string);
@@ -507,14 +567,15 @@ var
   Attr: TDemoNodeAttribute;
   TypeIDs, AttributeIDs: TCSSNumericalIDs;
   NumKind: TCSSNumericalIDKind;
+  AttrID: TCSSNumericalID;
 begin
   inherited Create(AOwner);
 
   for NumKind in TCSSNumericalIDKind do
     FNumericalIDs[NumKind]:=TCSSNumericalIDs.Create(NumKind);
   TypeIDs:=FNumericalIDs[nikType];
-  TypeIDs['*']:=CSSTypeIDUniversal;
-  if TypeIDs['*']<>CSSTypeIDUniversal then
+  TypeIDs['*']:=CSSTypeID_Universal;
+  if TypeIDs['*']<>CSSTypeID_Universal then
     raise Exception.Create('20220909004740');
 
   TypeIDs[TDemoNode.CSSTypeName]:=TDemoNode.CSSTypeID;
@@ -522,9 +583,16 @@ begin
   TypeIDs[TDemoButton.CSSTypeName]:=TDemoButton.CSSTypeID;
 
   AttributeIDs:=FNumericalIDs[nikAttribute];
-  AttributeIDs['all']:=CSSAttributeIDAll;
+  AttributeIDs['all']:=CSSAttributeID_All;
+  AttrID:=DemoAttrIDBase;
   for Attr in TDemoNodeAttribute do
-    AttributeIDs[DemoAttributeNames[Attr]]:=ord(Attr)+DemoAttrIDBase;
+  begin
+    AttributeIDs[DemoAttributeNames[Attr]]:=AttrID;
+    inc(AttrID);
+  end;
+  TDemoButton.CSSCaptionID:=AttrID;
+  AttributeIDs['caption']:=AttrID;
+  inc(AttrID);
 
   FCSSResolver:=TCSSResolver.Create;
   for NumKind in TCSSNumericalIDKind do
@@ -747,6 +815,21 @@ begin
     Result:=Parent.Nodes[i-1];
 end;
 
+function TDemoNode.HasCSSAttribute(const AttrID: TCSSNumericalID): boolean;
+begin
+  Result:=(AttrID>=DemoAttrIDBase) and (AttrID<=DemoAttrIDBase+ord(High(TDemoNodeAttribute)));
+end;
+
+function TDemoNode.GetCSSAttribute(const AttrID: TCSSNumericalID): TCSSString;
+var
+  Attr: TDemoNodeAttribute;
+begin
+  if (AttrID<DemoAttrIDBase) or (AttrID>ord(High(TDemoNodeAttribute))) then
+    exit('');
+  Attr:=TDemoNodeAttribute(AttrID-DemoAttrIDBase);
+  Result:=Attribute[Attr];
+end;
+
 function TDemoNode.GetCSSTypeName: TCSSString;
 begin
   Result:=CSSTypeName;