Răsfoiți Sursa

fcl-css:
- renamed TCSSUnits to TCSSUnit
- added a css registry for attributes, types, keywords, and function names
- resolver merges shorthand properties
- resolver distinguishes stylesheets from user-agent, user and author
- resolver now parses css itself, it adds its own types and customdata
- attributes are now checked at parse time and marked invalid
- started skipping invalid instead of raise

mattias 1 an în urmă
părinte
comite
43d84beb01

+ 97 - 56
packages/fcl-css/src/fpcssparser.pp

@@ -17,7 +17,10 @@ unit fpCSSParser;
 {$ENDIF FPC_DOTTEDUNITS}
 {$ENDIF FPC_DOTTEDUNITS}
 
 
 {$mode ObjFPC}{$H+}
 {$mode ObjFPC}{$H+}
+{$IF FPC_FULLVERSION>30300}
 {$WARN 6060 off} // Case statement does not handle all possible cases
 {$WARN 6060 off} // Case statement does not handle all possible cases
+{$WARN 6058 off} // Call to subroutine "$1" marked as inline is not inlined
+{$ENDIF}
 
 
 interface
 interface
 
 
@@ -73,12 +76,12 @@ Type
     function ParseAttributeSelector: TCSSElement; virtual;
     function ParseAttributeSelector: TCSSElement; virtual;
     function ParseWQName: TCSSElement;
     function ParseWQName: TCSSElement;
     function ParseDeclaration(aIsAt : Boolean = false): TCSSDeclarationElement; virtual;
     function ParseDeclaration(aIsAt : Boolean = false): TCSSDeclarationElement; virtual;
-    function ParseCall(aName: TCSSString): TCSSElement; virtual;
+    function ParseCall(aName: TCSSString; IsSelector: boolean): TCSSCallElement; virtual;
     procedure ParseSelectorCommaList(aCall: TCSSCallElement); virtual;
     procedure ParseSelectorCommaList(aCall: TCSSCallElement); virtual;
     procedure ParseRelationalSelectorCommaList(aCall: TCSSCallElement); virtual;
     procedure ParseRelationalSelectorCommaList(aCall: TCSSCallElement); virtual;
     procedure ParseNthChildParams(aCall: TCSSCallElement); virtual;
     procedure ParseNthChildParams(aCall: TCSSCallElement); virtual;
     function ParseUnary: TCSSElement; virtual;
     function ParseUnary: TCSSElement; virtual;
-    function ParseUnit: TCSSUnits; virtual;
+    function ParseUnit: TCSSUnit; virtual;
     function ParseIdentifier : TCSSIdentifierElement; virtual;
     function ParseIdentifier : TCSSIdentifierElement; virtual;
     function ParseHashIdentifier : TCSSHashIdentifierElement; virtual;
     function ParseHashIdentifier : TCSSHashIdentifierElement; virtual;
     function ParseClassName : TCSSClassNameElement; virtual;
     function ParseClassName : TCSSClassNameElement; virtual;
@@ -114,7 +117,7 @@ Type
     CSSUnaryElementClass: TCSSUnaryElementClass;
     CSSUnaryElementClass: TCSSUnaryElementClass;
     CSSUnicodeRangeElementClass: TCSSUnicodeRangeElementClass;
     CSSUnicodeRangeElementClass: TCSSUnicodeRangeElementClass;
     CSSURLElementClass: TCSSURLElementClass;
     CSSURLElementClass: TCSSURLElementClass;
-    Constructor Create(AInput: TStream; ExtraScannerOptions : TCSSScannerOptions = []); overload;
+    Constructor Create(AInput: TStream; ExtraScannerOptions : TCSSScannerOptions = []); overload; // AInput is not freed
     Constructor Create(AScanner : TCSSScanner); virtual; overload;
     Constructor Create(AScanner : TCSSScanner); virtual; overload;
     Destructor Destroy; override;
     Destructor Destroy; override;
     Function Parse : TCSSElement;
     Function Parse : TCSSElement;
@@ -724,28 +727,33 @@ begin
   Result:=FPeekToken;
   Result:=FPeekToken;
 end;
 end;
 
 
-function TCSSParser.ParseUnit : TCSSUnits;
+function TCSSParser.ParseUnit : TCSSUnit;
 
 
+var
+  p: PCSSChar;
+  U: TCSSUnit;
 begin
 begin
   Result:=cuNone;
   Result:=cuNone;
-  if (CurrentToken in [ctkIDENTIFIER,ctkPERCENTAGE]) then
+  case CurrentToken of
+  ctkPERCENTAGE:
     begin
     begin
-    Case currentTokenString of
-    '%'   : Result:=cuPERCENT;
-    'px'  : Result:=cuPX;
-    'rem' : Result:=cuREM;
-    'em'  : Result:=cuEM;
-    'fr'  : Result:=cuFR;
-    'vw'  : Result:=cuVW;
-    'vh'  : Result:=cuVH;
-    'pt'  : Result:=cuPT;
-    'deg' : Result:=cuDEG;
-    else
-      // Ignore. For instance margin: 0 auto
+    Result:=cuPERCENT;
+    Consume(CurrentToken);
     end;
     end;
-    if Result<>cuNone then
-      Consume(CurrentToken);
+  ctkIDENTIFIER:
+    begin
+    p:=PCSSChar(CurrentTokenString);
+    for U:=Succ(cuNONE) to High(TCSSUnit) do
+      if CompareMem(p,PCSSChar(CSSUnitNames[U]),SizeOf(TCSSChar)*length(CSSUnitNames[U])) then
+        begin
+        Result:=U;
+        Consume(CurrentToken);
+        break;
+        end;
     end;
     end;
+  ctkWHITESPACE:
+    Consume(CurrentToken);
+  end;
 end;
 end;
 
 
 function TCSSParser.CreateElement(aClass : TCSSElementClass): TCSSElement;
 function TCSSParser.CreateElement(aClass : TCSSElementClass): TCSSElement;
@@ -795,20 +803,31 @@ end;
 function TCSSParser.ParseInteger: TCSSElement;
 function TCSSParser.ParseInteger: TCSSElement;
 
 
 Var
 Var
-  aValue : Integer;
+  aCode, aValue : Integer;
   aInt : TCSSIntegerElement;
   aInt : TCSSIntegerElement;
+  OldReturnWhiteSpace: Boolean;
 
 
 begin
 begin
-  aValue:=StrToInt(CurrentTokenString);
+  Val(CurrentTokenString,aValue,aCode);
+  if aCode<>0 then
+    begin
+    DoError(SErrInvalidFloat,[CurrentTokenString]);
+    GetNextToken;
+    exit(nil);
+    end;
   aInt:=TCSSIntegerElement(CreateElement(CSSIntegerElementClass));
   aInt:=TCSSIntegerElement(CreateElement(CSSIntegerElementClass));
+  OldReturnWhiteSpace:=Scanner.ReturnWhiteSpace;
   try
   try
     aInt.Value:=aValue;
     aInt.Value:=aValue;
+    Scanner.ReturnWhiteSpace:=true;
     Consume(ctkINTEGER);
     Consume(ctkINTEGER);
     aInt.Units:=ParseUnit;
     aInt.Units:=ParseUnit;
     Result:=aInt;
     Result:=aInt;
     aInt:=nil;
     aInt:=nil;
   finally
   finally
     aInt.Free;
     aInt.Free;
+    Scanner.ReturnWhiteSpace:=OldReturnWhiteSpace;
+    SkipWhiteSpace;
   end;
   end;
 end;
 end;
 
 
@@ -817,19 +836,29 @@ Var
   aCode : Integer;
   aCode : Integer;
   aValue : Double;
   aValue : Double;
   aFloat : TCSSFloatElement;
   aFloat : TCSSFloatElement;
+  OldReturnWhiteSpace: Boolean;
 
 
 begin
 begin
   Val(CurrentTokenString,aValue,aCode);
   Val(CurrentTokenString,aValue,aCode);
   if aCode<>0 then
   if aCode<>0 then
+    begin
     DoError(SErrInvalidFloat,[CurrentTokenString]);
     DoError(SErrInvalidFloat,[CurrentTokenString]);
+    GetNextToken;
+    exit(nil);
+    end;
   aFloat:=TCSSFloatElement(CreateElement(CSSFloatElementClass));
   aFloat:=TCSSFloatElement(CreateElement(CSSFloatElementClass));
+  OldReturnWhiteSpace:=Scanner.ReturnWhiteSpace;
   try
   try
-    Consume(ctkFloat);
     aFloat.Value:=aValue;
     aFloat.Value:=aValue;
+    Scanner.ReturnWhiteSpace:=true;
+    Consume(ctkFloat);
     aFloat.Units:=ParseUnit;
     aFloat.Units:=ParseUnit;
+    if CurrentToken=ctkWHITESPACE then
+      GetNextToken;
     Result:=aFloat;
     Result:=aFloat;
     aFloat:=nil;
     aFloat:=nil;
   finally
   finally
+    Scanner.ReturnWhiteSpace:=OldReturnWhiteSpace;
     aFloat.Free;
     aFloat.Free;
   end;
   end;
 end;
 end;
@@ -903,6 +932,8 @@ Var
 
 
 begin
 begin
   aDecl:=nil;
   aDecl:=nil;
+  while CurrentToken=ctkUNKNOWN do
+    GetNextToken;
   if not (CurrentToken in [ctkRBRACE,ctkSEMICOLON]) then
   if not (CurrentToken in [ctkRBRACE,ctkSEMICOLON]) then
     begin
     begin
     aDecl:=ParseDeclaration(aIsAt);
     aDecl:=ParseDeclaration(aIsAt);
@@ -994,22 +1025,22 @@ function TCSSParser.ParseUnary: TCSSElement;
 var
 var
   Un : TCSSUnaryElement;
   Un : TCSSUnaryElement;
   Op : TCSSUnaryOperation;
   Op : TCSSUnaryOperation;
+  El: TCSSElement;
 
 
 begin
 begin
   Result:=nil;
   Result:=nil;
   if not (CurrentToken in [ctkDOUBLECOLON, ctkMinus, ctkPlus, ctkDiv, ctkGT, ctkTILDE]) then
   if not (CurrentToken in [ctkDOUBLECOLON, ctkMinus, ctkPlus, ctkDiv, ctkGT, ctkTILDE]) then
     Raise ECSSParser.CreateFmt(SUnaryInvalidToken,[CurrentTokenString]);
     Raise ECSSParser.CreateFmt(SUnaryInvalidToken,[CurrentTokenString]);
+  op:=TokenToUnaryOperation(CurrentToken);
+  Consume(CurrentToken);
+  if CurrentToken=ctkWHITESPACE then
+    Raise ECSSParser.CreateFmt(SUnaryInvalidToken,['white space']);
+  El:=ParseComponentValue;
+
   Un:=TCSSUnaryElement(CreateElement(CSSUnaryElementClass));
   Un:=TCSSUnaryElement(CreateElement(CSSUnaryElementClass));
-  try
-    op:=TokenToUnaryOperation(CurrentToken);
-    Un.Operation:=op;
-    Consume(CurrentToken);
-    Un.Right:=ParseComponentValue;
-    Result:=Un;
-    Un:=nil;
-  finally
-    Un.Free;
-  end;
+  Un.Operation:=op;
+  Un.Right:=El;
+  Result:=Un;
 end;
 end;
 
 
 function TCSSParser.ParseComponentValueList(AllowRules : Boolean = True): TCSSElement;
 function TCSSParser.ParseComponentValueList(AllowRules : Boolean = True): TCSSElement;
@@ -1097,7 +1128,16 @@ var
 
 
 begin
 begin
   aToken:=CurrentToken;
   aToken:=CurrentToken;
+  if aToken=ctkUNKNOWN then
+    begin
+    DoError('invalid');
+    repeat
+      GetNextToken;
+    until CurrentToken<>ctkUNKNOWN;
+    aToken:=CurrentToken;
+    end;
   Case aToken of
   Case aToken of
+    ctkEOF: exit(nil);
     ctkLPARENTHESIS: Result:=ParseParenthesis;
     ctkLPARENTHESIS: Result:=ParseParenthesis;
     ctkURL: Result:=ParseURL;
     ctkURL: Result:=ParseURL;
     ctkPSEUDO: Result:=ParsePseudo;
     ctkPSEUDO: Result:=ParsePseudo;
@@ -1114,13 +1154,12 @@ begin
     ctkINTEGER: Result:=ParseInteger;
     ctkINTEGER: Result:=ParseInteger;
     ctkFloat : Result:=ParseFloat;
     ctkFloat : Result:=ParseFloat;
     ctkPSEUDOFUNCTION,
     ctkPSEUDOFUNCTION,
-    ctkFUNCTION : Result:=ParseCall('');
+    ctkFUNCTION : Result:=ParseCall('',false);
     ctkSTAR: Result:=ParseInvalidToken;
     ctkSTAR: Result:=ParseInvalidToken;
-    ctkIDENTIFIER: Result:=ParseIdentifier;
+    ctkIDENTIFIER,ctkPERCENTAGE: Result:=ParseIdentifier;
     ctkCLASSNAME : Result:=ParseClassName;
     ctkCLASSNAME : Result:=ParseClassName;
   else
   else
     Result:=nil;
     Result:=nil;
-//    Consume(aToken);// continue
   end;
   end;
   if aToken in FinalTokens then
   if aToken in FinalTokens then
     exit;
     exit;
@@ -1140,7 +1179,7 @@ function TCSSParser.ParseSelector: TCSSElement;
       ctkCLASSNAME : Result:=ParseClassName;
       ctkCLASSNAME : Result:=ParseClassName;
       ctkLBRACKET: Result:=ParseAttributeSelector;
       ctkLBRACKET: Result:=ParseAttributeSelector;
       ctkPSEUDO: Result:=ParsePseudo;
       ctkPSEUDO: Result:=ParsePseudo;
-      ctkPSEUDOFUNCTION: Result:=ParseCall('');
+      ctkPSEUDOFUNCTION: Result:=ParseCall('',true);
     else
     else
       DoWarn(SErrUnexpectedToken ,[
       DoWarn(SErrUnexpectedToken ,[
                GetEnumName(TypeInfo(TCSSToken),Ord(CurrentToken)),
                GetEnumName(TypeInfo(TCSSToken),Ord(CurrentToken)),
@@ -1242,6 +1281,7 @@ begin
       Bin.Free;
       Bin.Free;
       end;
       end;
   end;
   end;
+  SkipWhiteSpace;
 end;
 end;
 
 
 function TCSSParser.ParseAttributeSelector: TCSSElement;
 function TCSSParser.ParseAttributeSelector: TCSSElement;
@@ -1332,14 +1372,15 @@ function TCSSParser.ParseDeclaration(aIsAt: Boolean = false): TCSSDeclarationEle
 Var
 Var
   aDecl : TCSSDeclarationElement;
   aDecl : TCSSDeclarationElement;
   aKey,aValue : TCSSElement;
   aKey,aValue : TCSSElement;
-  aPrevDisablePseudo : Boolean;
   aList : TCSSListElement;
   aList : TCSSListElement;
+  OldOptions: TCSSScannerOptions;
 
 
 begin
 begin
   aList:=nil;
   aList:=nil;
-  aDecl:= TCSSDeclarationElement(CreateElement(CSSDeclarationElementClass));
+  OldOptions:=Scanner.Options;
+  aDecl:=TCSSDeclarationElement(CreateElement(CSSDeclarationElementClass));
   try
   try
-    aPrevDisablePseudo:= Scanner.DisablePseudo;
+    // read attribute names
     Scanner.DisablePseudo:=True;
     Scanner.DisablePseudo:=True;
     aKey:=ParseComponentValue;
     aKey:=ParseComponentValue;
     aDecl.AddKey(aKey);
     aDecl.AddKey(aKey);
@@ -1348,7 +1389,7 @@ begin
       While (CurrentToken=ctkCOMMA) do
       While (CurrentToken=ctkCOMMA) do
         begin
         begin
         while (CurrentToken=ctkCOMMA) do
         while (CurrentToken=ctkCOMMA) do
-          Consume(ctkCOMMA);
+          GetNextToken;
         aKey:=ParseComponentValue;
         aKey:=ParseComponentValue;
         aDecl.AddKey(aKey);
         aDecl.AddKey(aKey);
         end;
         end;
@@ -1364,12 +1405,13 @@ begin
       if aDecl.Colon then
       if aDecl.Colon then
         Consume(ctkColon)
         Consume(ctkColon)
       end;
       end;
-    Scanner.DisablePseudo:=aPrevDisablePseudo;
     aValue:=ParseComponentValue;
     aValue:=ParseComponentValue;
     aList:=TCSSListElement(CreateElement(CSSListElementClass));
     aList:=TCSSListElement(CreateElement(CSSListElementClass));
     aList.AddChild(aValue);
     aList.AddChild(aValue);
     if aDecl.Colon then
     if aDecl.Colon then
       begin
       begin
+      // read attribute value
+      // + and - must be enclosed in whitespace, +3 and -4 are values
       While not (CurrentToken in [ctkEOF,ctkSemicolon,ctkRBRACE,ctkImportant]) do
       While not (CurrentToken in [ctkEOF,ctkSemicolon,ctkRBRACE,ctkImportant]) do
         begin
         begin
         While CurrentToken=ctkCOMMA do
         While CurrentToken=ctkCOMMA do
@@ -1393,23 +1435,21 @@ begin
     Result:=aDecl;
     Result:=aDecl;
     aDecl:=nil;
     aDecl:=nil;
   finally
   finally
-    Scanner.DisablePseudo:=False;
+    Scanner.Options:=OldOptions;
     aDecl.Free;
     aDecl.Free;
     aList.Free;
     aList.Free;
   end;
   end;
 end;
 end;
 
 
-function TCSSParser.ParseCall(aName : TCSSString): TCSSElement;
-
+function TCSSParser.ParseCall(aName: TCSSString; IsSelector: boolean
+  ): TCSSCallElement;
 var
 var
   aCall : TCSSCallElement;
   aCall : TCSSCallElement;
   l : Integer;
   l : Integer;
-  OldReturnWhiteSpace: Boolean;
   aValue: TCSSElement;
   aValue: TCSSElement;
 begin
 begin
-  OldReturnWhiteSpace:=Scanner.ReturnWhiteSpace;
-  Scanner.ReturnWhiteSpace:=false;
-  aCall:=TCSSCallElement(CreateELement(CSSCallElementClass));
+  if IsSelector then ;
+  aCall:=TCSSCallElement(CreateElement(CSSCallElementClass));
   try
   try
     if (aName='') then
     if (aName='') then
       aName:=CurrentTokenString;
       aName:=CurrentTokenString;
@@ -1417,9 +1457,10 @@ begin
     if (L>0) and (aName[L]='(') then
     if (L>0) and (aName[L]='(') then
       aName:=Copy(aName,1,L-1);
       aName:=Copy(aName,1,L-1);
     aCall.Name:=aName;
     aCall.Name:=aName;
-    if CurrentToken=ctkPSEUDOFUNCTION then
+    if IsSelector and (CurrentToken=ctkPSEUDOFUNCTION) then
       begin
       begin
       Consume(ctkPSEUDOFUNCTION);
       Consume(ctkPSEUDOFUNCTION);
+      SkipWhiteSpace;
       case aName of
       case aName of
       ':not',':is',':where':
       ':not',':is',':where':
         ParseSelectorCommaList(aCall);
         ParseSelectorCommaList(aCall);
@@ -1429,8 +1470,9 @@ begin
         ParseNthChildParams(aCall);
         ParseNthChildParams(aCall);
       end;
       end;
       end
       end
-    else
+    else begin
       Consume(ctkFUNCTION);
       Consume(ctkFUNCTION);
+    end;
     // Call argument list can be empty: mask()
     // Call argument list can be empty: mask()
     While not (CurrentToken in [ctkRPARENTHESIS,ctkEOF]) do
     While not (CurrentToken in [ctkRPARENTHESIS,ctkEOF]) do
       begin
       begin
@@ -1442,7 +1484,7 @@ begin
       end;
       end;
       aCall.AddArg(aValue);
       aCall.AddArg(aValue);
       if (CurrentToken=ctkCOMMA) then
       if (CurrentToken=ctkCOMMA) then
-        Consume(ctkCOMMA);
+        GetNextToken;
       end;
       end;
     if CurrentToken=ctkEOF then
     if CurrentToken=ctkEOF then
       DoError(SErrUnexpectedEndOfFile,[aName]);
       DoError(SErrUnexpectedEndOfFile,[aName]);
@@ -1450,7 +1492,6 @@ begin
     Result:=aCall;
     Result:=aCall;
     aCall:=nil;
     aCall:=nil;
   finally
   finally
-    Scanner.ReturnWhiteSpace:=OldReturnWhiteSpace;
     aCall.Free;
     aCall.Free;
   end;
   end;
 end;
 end;
@@ -1462,12 +1503,12 @@ begin
   while not (CurrentToken in [ctkEOF,ctkRBRACKET,ctkRBRACE,ctkRPARENTHESIS]) do
   while not (CurrentToken in [ctkEOF,ctkRBRACKET,ctkRBRACE,ctkRPARENTHESIS]) do
     begin
     begin
     El:=ParseSelector;
     El:=ParseSelector;
-    if EL=nil then exit;
+    if El=nil then exit;
     aCall.AddArg(El);
     aCall.AddArg(El);
-    SkipWhiteSpace;
     if CurrentToken<>ctkCOMMA then
     if CurrentToken<>ctkCOMMA then
       exit;
       exit;
     GetNextToken;
     GetNextToken;
+    SkipWhiteSpace;
   end;
   end;
 end;
 end;
 
 
@@ -1560,12 +1601,12 @@ begin
 
 
   if CurrentToken in [ctkMINUS,ctkPLUS] then
   if CurrentToken in [ctkMINUS,ctkPLUS] then
     aCall.AddArg(ParseUnary);
     aCall.AddArg(ParseUnary);
-  if CurrentToken=ctkINTEGER then
-    aCall.AddArg(ParseInteger);
   if (CurrentToken=ctkIDENTIFIER) and SameText(CurrentTokenString,'of') then
   if (CurrentToken=ctkIDENTIFIER) and SameText(CurrentTokenString,'of') then
     begin
     begin
     aCall.AddArg(ParseIdentifier);
     aCall.AddArg(ParseIdentifier);
+    SkipWhiteSpace;
     aCall.AddArg(ParseSelector);
     aCall.AddArg(ParseSelector);
+    SkipWhiteSpace;
     end;
     end;
 end;
 end;
 
 

Fișier diff suprimat deoarece este prea mare
+ 611 - 278
packages/fcl-css/src/fpcssresolver.pas


+ 1964 - 0
packages/fcl-css/src/fpcssresparser.pas

@@ -0,0 +1,1964 @@
+{
+    This file is part of the Free Pascal Run time library.
+    Copyright (c) 2023 by Michael Van Canneyt ([email protected])
+
+    This file contains CSS utility class
+
+    See the File COPYING.FPC, included in this distribution,
+    for details about the copyright.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+
+ **********************************************************************
+}
+{$IFNDEF FPC_DOTTEDUNITS}
+unit fpCSSResParser;
+{$ENDIF FPC_DOTTEDUNITS}
+
+{$mode ObjFPC}{$H+}
+{$Interfaces CORBA}
+{$ModeSwitch AdvancedRecords}
+
+{$IF FPC_FULLVERSION>30300}
+  {$WARN 6060 off} // Case statement does not handle all possible cases
+{$ENDIF}
+{$WARN 6058 off : Call to subroutine "$1" marked as inline is not inlined}
+interface
+
+{$IFDEF FPC_DOTTEDUNITS}
+uses
+  System.Classes, System.SysUtils, System.Types, System.Contnrs, System.StrUtils,
+  FPCSS.Tree, FPCSS.Scanner, FPCSS.Parser;
+{$ELSE FPC_DOTTEDUNITS}
+uses
+  Classes, SysUtils, math, Contnrs, AVL_Tree, fpCSSTree, fpCSSScanner,
+  fpCSSParser;
+{$ENDIF FPC_DOTTEDUNITS}
+
+const
+  CSSIDNone = 0;
+  // built-in attribute IDs
+  CSSAttributeID_ID = 1; // id of attribute key 'id'
+  CSSAttributeID_Class = 2; // id of attribute key 'class'
+  CSSAttributeID_All = 3; // id of attribute key 'all'
+  CSSAttributeID_LastResolver = CSSAttributeID_All;
+
+  // built-in type IDs
+  CSSTypeID_Universal = 1; // id of type '*'
+  CSSTypeID_LastResolver = CSSTypeID_Universal;
+
+  // built-in pseudo class IDs
+  CSSPseudoID_Root = 1; // :root
+  CSSPseudoID_Empty = CSSPseudoID_Root+1; // :empty
+  CSSPseudoID_FirstChild = CSSPseudoID_Empty+1; // :first-child
+  CSSPseudoID_LastChild = CSSPseudoID_FirstChild+1; // :last-child
+  CSSPseudoID_OnlyChild = CSSPseudoID_LastChild+1; // :only-child
+  CSSPseudoID_FirstOfType = CSSPseudoID_OnlyChild+1; // :first-of-type
+  CSSPseudoID_LastOfType = CSSPseudoID_FirstOfType+1; // :last-of-type
+  CSSPseudoID_OnlyOfType = CSSPseudoID_LastOfType+1; // :only-of-type
+  CSSPseudoID_LastResolver = CSSPseudoID_OnlyOfType;
+
+  CSSPseudoClassNames: array[0..CSSPseudoID_LastResolver] of TCSSString = (
+    '?',
+    ':root',
+    ':empty',
+    ':first-child',
+    ':last-child',
+    ':only-child',
+    ':first-of-type',
+    ':last-of-type',
+    ':only-of-type'
+    );
+
+  // built-in pseudo function IDs
+  CSSCallID_Not = 1; // :not()
+  CSSCallID_Is = CSSCallID_Not+1; // :is()
+  CSSCallID_Where = CSSCallID_Is+1; // :where()
+  CSSCallID_Has = CSSCallID_Where+1; // :has()
+  CSSCallID_NthChild = CSSCallID_Has+1; // :nth-child(n)
+  CSSCallID_NthLastChild = CSSCallID_NthChild+1; // :nth-last-child(n)
+  CSSCallID_NthOfType = CSSCallID_NthLastChild+1; // :nth-of-type(n)
+  CSSCallID_NthLastOfType = CSSCallID_NthOfType+1; // :nth-last-of-type(n)
+  CSSCallID_LastResolver = CSSCallID_NthLastOfType;
+
+  CSSSelectorCallNames: array[0..CSSCallID_LastResolver] of TCSSString = (
+    '?',
+    ':not()',
+    ':is()',
+    ':where()',
+    ':has()',
+    ':nth-child(n)',
+    ':nth-last-child(n)',
+    ':nth-of-type(n)',
+    ':nth-last-of-type(n)'
+    );
+
+  // keywords
+  CSSKeywordNone = 1;
+  CSSKeywordInitial = CSSKeywordNone+1;
+  CSSKeywordInherit = CSSKeywordInitial+1;
+  CSSKeywordUnset = CSSKeywordInherit+1;
+  CSSKeywordRevert = CSSKeywordUnset+1;
+  CSSKeywordRevertLayer = CSSKeywordRevert+1;
+  CSSKeywordAuto = CSSKeywordRevertLayer+1;
+  CSSKeyword_LastResolver = CSSKeywordAuto;
+
+  CSSMinSafeIntDouble = -$1fffffffffffff; // -9007199254740991 54 bits (52 plus signed bit plus implicit highest bit)
+  CSSMaxSafeIntDouble =  $1fffffffffffff; //  9007199254740991
+
+type
+  TCSSMsgID = int64; // used for message numbers, e.g. error codes
+  TCSSNumericalID = integer; // used for IDs of each type and attribute
+  TCSSNumericalIDArray = array of TCSSNumericalID;
+
+  TCSSNumericalIDKind = (
+    nikAttribute,
+    nikPseudoClass, // e.g. "hover" of ":hover"
+    nikPseudoFunction, // e.g. "is" of ":is()"
+    nikType,
+    nikKeyword,
+    nikAttrFunction // e.g. "calc" of "calc()"
+    );
+  TCSSNumericalIDs = set of TCSSNumericalIDKind;
+
+const
+  nikAllDescriptors = [nikAttribute,nikPseudoClass,nikType]; // all items having a descriptor
+
+  CSSNumericalIDKindNames: array[TCSSNumericalIDKind] of TCSSString = (
+    'Type',
+    'PseudoClass',
+    'PseudoFunction',
+    'Attribute',
+    'Keyword',
+    'AttributeFunction'
+    );
+
+type
+  TCSSRegistryNamedItem = class
+  public
+    Name: TCSSString; // case sensitive
+    Index: TCSSNumericalID;
+  end;
+
+  { TCSSPseudoClassDesc }
+
+  TCSSPseudoClassDesc = class(TCSSRegistryNamedItem)
+  public
+  end;
+  TCSSPseudoClassDescClass = class of TCSSPseudoClassDesc;
+  TCSSPseudoClassDescArray = array of TCSSPseudoClassDesc;
+
+  { TCSSTypeDesc }
+
+  TCSSTypeDesc = class(TCSSRegistryNamedItem)
+  public
+  end;
+  TCSSTypeDescClass = class of TCSSTypeDesc;
+  TCSSTypeDescArray = array of TCSSTypeDesc;
+
+  { TCSSAttributeKeyData }
+
+  TCSSAttributeKeyData = class(TCSSElementOwnedData)
+  public
+    Invalid: boolean;
+    Complete: boolean;
+    Value: TCSSString;
+  end;
+  TCSSAttributeKeyDataClass = class of TCSSAttributeKeyData;
+
+  TCSSBaseResolver = class;
+  TCSSResolverParser = class;
+
+  { TCSSAttributeDesc - general properties of a CSS attribute }
+
+  TCSSAttributeDesc = class(TCSSRegistryNamedItem)
+  public
+    type
+      TCheckEvent = procedure(Resolver: TCSSBaseResolver; Data: TCSSAttributeKeyData) of object;
+      TCheckExEvent = procedure(Resolver: TCSSBaseResolver; Desc: TCSSAttributeDesc;
+                      DeclEl: TCSSElement; Data: TCSSAttributeKeyData) of object;
+      TSplitShorthandEvent = procedure(Resolver: TCSSBaseResolver; Desc: TCSSAttributeDesc;
+           const aValue: TCSSString; var AttrIDs: TCSSNumericalIDArray; var Values: TCSSStringArray) of object;
+  public
+    Syntax: TCSSString; // an optional description
+    Inherits: boolean; // true = the default value is the parent's value
+    All: boolean; // true = can be changed by the 'all' attribute
+    InitialValue: TCSSString;
+    CompProps: array of TCSSAttributeDesc; // if this attribute is a shorthand,
+      // these are the component properties (longhands + sub-shorthands like border-width)
+      // used by the cascade algorithm to delete all overwritten properties
+    OnCheck: TCheckEvent; // called by the parser after reading a declaration
+      // can set Data.Invalid, so the resolver skips this declaration
+    OnCheckEx: TCheckExEvent; // same as OnCheck, except more context
+    OnSplitShorthand: TSplitShorthandEvent; // called by resolver after resolving var()
+  end;
+  TCSSAttributeDescClass = class of TCSSAttributeDesc;
+  TCSSAttributeDescArray = array of TCSSAttributeDesc;
+
+  { TCSSRegistry }
+
+  TCSSRegistry = class
+  private
+    FAttrFunctionCount: TCSSNumericalID;
+    FAttributeCount: TCSSNumericalID;
+    FHashLists: array[TCSSNumericalIDKind] of TFPHashList; // name to TCSSRegistryNamedItem
+    FKeywordCount: TCSSNumericalID;
+    FPseudoClassCount: TCSSNumericalID;
+    FPseudoFunctionCount: TCSSNumericalID;
+    FTypeCount: TCSSNumericalID;
+  public
+    constructor Create;
+    procedure Init; virtual; // add basic items
+    destructor Destroy; override;
+    function FindNamedItem(Kind: TCSSNumericalIDKind; const aName: TCSSString): TCSSRegistryNamedItem; overload;
+    function IndexOfNamedItem(Kind: TCSSNumericalIDKind; const aName: TCSSString): TCSSNumericalID; overload;
+    procedure ConsistencyCheck; virtual;
+  public
+    // attributes
+    Attributes: TCSSAttributeDescArray;
+    Attribute_ClassOf: TCSSAttributeDescClass;
+    NotAllAttributes: TCSSAttributeDescArray;
+    function AddAttribute(Attr: TCSSAttributeDesc): TCSSAttributeDesc; overload;
+    function AddAttribute(const aName: TCSSString; const aInitialValue: TCSSString = '';
+      aInherits: boolean = false; aAll: boolean = true; aClass: TCSSAttributeDescClass = nil): TCSSAttributeDesc; overload;
+    function FindAttribute(const aName: TCSSString): TCSSAttributeDesc; overload;
+    function IndexOfAttributeName(const aName: TCSSString): TCSSNumericalID; overload;
+    property AttributeCount: TCSSNumericalID read FAttributeCount;
+  public
+    // pseudo classes
+    PseudoClasses: TCSSPseudoClassDescArray;
+    PseudoClass_ClassOf: TCSSPseudoClassDescClass;
+    function AddPseudoClass(aPseudo: TCSSPseudoClassDesc): TCSSPseudoClassDesc; overload;
+    function AddPseudoClass(const aName: TCSSString; aClass: TCSSPseudoClassDescClass = nil): TCSSPseudoClassDesc; overload;
+    function FindPseudoClass(const aName: TCSSString): TCSSPseudoClassDesc; overload;
+    function IndexOfPseudoClassName(const aName: TCSSString): TCSSNumericalID; overload;
+    property PseudoClassCount: TCSSNumericalID read FPseudoClassCount;
+  public
+    // pseudo functions
+    PseudoFunctions: TCSSStringArray;
+    function AddPseudoFunction(const aName: TCSSString): TCSSNumericalID; overload;
+    function IndexOfPseudoFunction(const aName: TCSSString): TCSSNumericalID; overload;
+    property PseudoFunctionCount: TCSSNumericalID read FPseudoFunctionCount;
+  public
+    // types
+    Types: TCSSTypeDescArray;
+    Type_ClassOf: TCSSTypeDescClass;
+    function AddType(aType: TCSSTypeDesc): TCSSTypeDesc; overload;
+    function AddType(const aName: TCSSString; aClass: TCSSTypeDescClass = nil): TCSSTypeDesc; overload;
+    function FindType(const aName: TCSSString): TCSSTypeDesc; overload;
+    function IndexOfTypeName(const aName: TCSSString): TCSSNumericalID; overload;
+    property TypeCount: TCSSNumericalID read FTypeCount;
+  public
+    // keywords
+    Keywords: TCSSStringArray;
+    function AddKeyword(const aName: TCSSString): TCSSNumericalID; overload;
+    function IndexOfKeyword(const aName: TCSSString): TCSSNumericalID; overload;
+    property KeywordCount: TCSSNumericalID read FKeywordCount;
+  public
+    // attribute functions
+    AttrFunctions: TCSSStringArray;
+    function AddAttrFunction(const aName: TCSSString): TCSSNumericalID; overload;
+    function IndexOfAttrFunction(const aName: TCSSString): TCSSNumericalID; overload;
+    property AttrFunctionCount: TCSSNumericalID read FAttrFunctionCount;
+  end;
+
+  TCSSValueParserLogEvent = procedure(MsgType: TEventType; const ID: TCSSMsgID;
+                               const Msg: TCSSString; PosEl: TCSSElement) of object;
+
+  { TCSSResolvedIdentifierElement }
+
+  TCSSResolvedIdentifierElement = class(TCSSIdentifierElement)
+  public
+    NumericalID: TCSSNumericalID;
+    Kind: TCSSNumericalIDKind;
+  end;
+
+  { TCSSResolvedPseudoClassElement }
+
+  TCSSResolvedPseudoClassElement = class(TCSSPseudoClassElement)
+  public
+    NumericalID: TCSSNumericalID;
+    Kind: TCSSNumericalIDKind;
+  end;
+
+  { TCSSNthChildParams }
+
+  TCSSNthChildParams = class
+  public
+    Modulo: integer;
+    Start: integer;
+    HasOf: boolean; // for nth-of-type() HasOf=true and OfSelector=nil
+    OfSelector: TCSSElement;
+  end;
+  TCSSNthChildParamsClass = class of TCSSNthChildParams;
+
+  { TCSSResolvedCallElement }
+
+  TCSSResolvedCallElement = class(TCSSCallElement)
+  public
+    NameNumericalID: TCSSNumericalID;
+    Kind: TCSSNumericalIDKind;
+    Params: TObject; // e.g. TCSSNthChildParams
+    destructor Destroy; override;
+  end;
+
+  { TCSSValueData }
+
+  TCSSValueData = class(TCSSElementOwnedData)
+  public
+    NormValue: TCSSString; // normalized value, stripped of comments and e.g. '0.1' instead of '000.100' or '.1'
+  end;
+
+  TCSSResValueKind = (
+    rvkNone,
+    rvkInvalid,
+    rvkSymbol,
+    rvkFloat,
+    rvkCalcFloat,
+    rvkKeyword,
+    rvkKeywordUnknown,
+    rvkFunction,
+    rvkFunctionUnknown,
+    rvkString
+    );
+
+  { TCSSResValue }
+
+  TCSSResValue = record
+    Kind: TCSSResValueKind;
+    Start: PCSSChar;
+    function FloatAsString: TCSSString;
+    case longint of
+    1: (Float: Double; FloatUnit: TCSSUnit);
+    3: (KeywordID: TCSSNumericalID);
+    4: (FunctionID: TCSSNumericalID; BracketOpen: PCSSChar);
+  end;
+
+  { TCSSCheckAttrParams_Float }
+
+  TCSSCheckAttrParams_Float = record
+  public
+    AllowedUnits: TCSSUnits;
+    AllowNegative, AllowFrac: boolean;
+    AllowedKeywordIDs: TCSSNumericalIDArray;
+  end;
+
+  { TCSSBaseResolver }
+
+  TCSSBaseResolver = class(TComponent)
+  private
+    FCSSRegistry: TCSSRegistry;
+  protected
+    procedure SetCSSRegistry(const AValue: TCSSRegistry); virtual;
+  public
+    // parse whole attribute, skipping invalid values:
+    // Result=true, Invalid=false: some value was valid
+    // Result=false, Invalid=false: due to a function call it might be valid
+    // Result=false, Invalid=true: always invalid
+    function CheckAttribute_Keyword(Data: TCSSAttributeKeyData; const AllowedKeywordIDs: TCSSNumericalIDArray; out ResValue: TCSSResValue): boolean; virtual;
+    function CheckAttribute_Dimension(Data: TCSSAttributeKeyData; const Params: TCSSCheckAttrParams_Float; out ResValue: TCSSResValue): boolean; virtual;
+    function ReadAttribute_Keyword(const Value: TCSSString; out Invalid: boolean; const AllowedKeywordIDs: TCSSNumericalIDArray; out ResValue: TCSSResValue): boolean; virtual;
+    function ReadAttribute_Dimension(const Value: TCSSString; out Invalid: boolean; const Params: TCSSCheckAttrParams_Float; out ResValue: TCSSResValue): boolean; virtual;
+    // check values of an attribute
+    function ReadValue(var p: PCSSChar; out Value: TCSSResValue): boolean; virtual; // true if parsing attribute can continue
+    function ReadNumber(var p: PCSSChar; var Value: TCSSResValue): boolean; virtual;
+    function ReadIdentifier(var p: PCSSChar; var Value: TCSSResValue): boolean; virtual;
+    procedure SkipToEndOfAttribute(var p: PCSSChar);
+    function SkipString(var p: PCSSChar): boolean;
+    function SkipBrackets(var p: PCSSChar; Lvl: integer = 1): boolean;
+    property CSSRegistry: TCSSRegistry read FCSSRegistry write SetCSSRegistry;
+  end;
+
+  { TCSSResolverParser
+    - resolves identifiers to IDs
+    - warns about constructs unsupported by resolver }
+
+  TCSSResolverParser = class(TCSSParser)
+  private
+    FOnLog: TCSSValueParserLogEvent;
+    FRegistry: TCSSRegistry;
+    FResolver: TCSSBaseResolver;
+  protected
+    function ResolveIdentifier(El: TCSSResolvedIdentifierElement; Kind: TCSSNumericalIDKind): TCSSNumericalID; virtual;
+    function ResolvePseudoClass(El: TCSSResolvedPseudoClassElement): TCSSNumericalID; virtual;
+    function ResolvePseudoFunction(El: TCSSResolvedCallElement): TCSSNumericalID; virtual;
+    function ParseCall(aName: TCSSString; IsSelector: boolean): TCSSCallElement; override;
+    function ParseDeclaration(aIsAt: Boolean): TCSSDeclarationElement; override;
+    function ParseSelector: TCSSElement; override;
+    procedure CheckSelector(El: TCSSElement); virtual;
+    procedure CheckSelectorArray(anArray: TCSSArrayElement); virtual;
+    procedure CheckSelectorArrayBinary(aBinary: TCSSBinaryElement); virtual;
+    procedure CheckSelectorBinary(aBinary: TCSSBinaryElement); virtual;
+    procedure CheckSelectorList(aList: TCSSListElement); virtual;
+    procedure CheckNthChildParams(aCall: TCSSResolvedCallElement); virtual;
+    function ComputeValue(El: TCSSElement): TCSSString; virtual;
+  public
+    CSSNthChildParamsClass: TCSSNthChildParamsClass;
+    CSSAttributeKeyDataClass: TCSSAttributeKeyDataClass;
+    constructor Create(AScanner: TCSSScanner); override; overload;
+    destructor Destroy; override;
+    procedure Log(MsgType: TEventType; const ID: TCSSMsgID; const Msg: TCSSString; PosEl: TCSSElement); virtual;
+    class function IsWhiteSpace(const s: TCSSString): boolean; virtual; overload;
+    property Registry: TCSSRegistry read FRegistry write FRegistry;
+    property Resolver: TCSSBaseResolver read FResolver write FResolver;
+    property OnLog: TCSSValueParserLogEvent read FOnLog write FOnLog;
+  end;
+
+implementation
+
+Const
+  Alpha = ['A'..'Z','a'..'z'];
+  Num   = ['0'..'9'];
+  AlNum = Alpha+Num;
+  AlIden = Alpha+['-'];
+  AlNumIden = AlNum+['-'];
+  Whitespace = [#9,#10,#13,' '];
+  WhitespaceZ = Whitespace+[#0];
+  ValEnd = [#0,#10,#13,#9,' ',';','}',')',']'];
+
+{ TCSSRegistry }
+
+constructor TCSSRegistry.Create;
+var
+  Kind: TCSSNumericalIDKind;
+  i: Integer;
+begin
+  for Kind in TCSSNumericalIDKind do
+    FHashLists[Kind]:=TFPHashList.Create;
+  if Attribute_ClassOf=nil then
+    Attribute_ClassOf:=TCSSAttributeDesc;
+  if PseudoClass_ClassOf=nil then
+    PseudoClass_ClassOf:=TCSSPseudoClassDesc;
+  if Type_ClassOf=nil then
+    Type_ClassOf:=TCSSTypeDesc;
+
+  // init attributes
+  SetLength(Attributes,32);
+  for i:=0 to length(Attributes)-1 do Attributes[i]:=nil;
+  FAttributeCount:=1; // index 0 is CSSIDNone
+
+  // init pseudo classes
+  SetLength(PseudoClasses,32);
+  for i:=0 to length(PseudoClasses)-1 do PseudoClasses[i]:=nil;
+  FPseudoClassCount:=1; // index 0 is CSSIDNone
+
+  // init pseudo functions
+  SetLength(PseudoFunctions,16);
+  FPseudoFunctionCount:=1; // index 0 is CSSIDNone
+
+  // init types
+  SetLength(Types,32);
+  for i:=0 to length(Types)-1 do Types[i]:=nil;
+  FTypeCount:=1; // index 0 is CSSIDNone
+
+  // init keywords
+  SetLength(Keywords,32);
+  FKeywordCount:=1; // index 0 is CSSIDNone
+
+  // init keywords
+  SetLength(AttrFunctions,32);
+  FAttrFunctionCount:=1; // index 0 is CSSIDNone
+end;
+
+procedure TCSSRegistry.Init;
+begin
+  // init attributes
+  if AddAttribute('id').Index<>CSSAttributeID_ID then
+    raise Exception.Create('20240617191749');
+  if AddAttribute('class').Index<>CSSAttributeID_Class then
+    raise Exception.Create('20240617191801');
+  if AddAttribute('all').Index<>CSSAttributeID_All then
+    raise Exception.Create('20240617191816');
+
+  // init pseudo classes
+  if AddPseudoClass('root').Index<>CSSPseudoID_Root then
+    raise Exception.Create('20240623165848');
+  if AddPseudoClass('empty').Index<>CSSPseudoID_Empty then
+    raise Exception.Create('20240623170450');
+  if AddPseudoClass('first-child').Index<>CSSPseudoID_FirstChild then
+    raise Exception.Create('20240623170508');
+  if AddPseudoClass('last-child').Index<>CSSPseudoID_LastChild then
+    raise Exception.Create('20240623170521');
+  if AddPseudoClass('only-child').Index<>CSSPseudoID_OnlyChild then
+    raise Exception.Create('20240623170534');
+  if AddPseudoClass('first-of-type').Index<>CSSPseudoID_FirstOfType then
+    raise Exception.Create('20240623170547');
+  if AddPseudoClass('last-of-type').Index<>CSSPseudoID_LastOfType then
+    raise Exception.Create('20240623170558');
+  if AddPseudoClass('only-of-type').Index<>CSSPseudoID_OnlyOfType then
+    raise Exception.Create('20240623170609');
+
+  // init pseudo functions
+  if AddPseudoFunction('not')<>CSSCallID_Not then
+    raise Exception.Create('20240625183757');
+  if AddPseudoFunction('is')<>CSSCallID_Is then
+    raise Exception.Create('20240625142038');
+  if AddPseudoFunction('where')<>CSSCallID_Where then
+    raise Exception.Create('20240625142049');
+  if AddPseudoFunction('has')<>CSSCallID_Has then
+    raise Exception.Create('20240625142104');
+  if AddPseudoFunction('nth-child')<>CSSCallID_NthChild then
+    raise Exception.Create('20240625142124');
+  if AddPseudoFunction('nth-last-child')<>CSSCallID_NthLastChild then
+    raise Exception.Create('20240625142136');
+  if AddPseudoFunction('nth-of-type')<>CSSCallID_NthOfType then
+    raise Exception.Create('20240625142156');
+  if AddPseudoFunction('nth-last-of-type')<>CSSCallID_NthLastOfType then
+    raise Exception.Create('20240625142212');
+
+  // init types
+  if AddType('*').Index<>CSSTypeID_Universal then
+    raise Exception.Create('20240617190914');
+
+  // init keywords
+  if AddKeyword('none')<>CSSKeywordNone then
+    raise Exception.Create('20240623184021');
+  if AddKeyword('initial')<>CSSKeywordInitial then
+    raise Exception.Create('20240623184030');
+  if AddKeyword('inherit')<>CSSKeywordInherit then
+    raise Exception.Create('20240623184042');
+  if AddKeyword('unset')<>CSSKeywordUnset then
+    raise Exception.Create('20240623184053');
+  if AddKeyword('revert')<>CSSKeywordRevert then
+    raise Exception.Create('20240623184104');
+  if AddKeyword('revert-layer')<>CSSKeywordRevertLayer then
+    raise Exception.Create('20240623184114');
+  if AddKeyword('auto')<>CSSKeywordAuto then
+    raise Exception.Create('20240625182731');
+end;
+
+destructor TCSSRegistry.Destroy;
+var
+  i: Integer;
+  Kind: TCSSNumericalIDKind;
+begin
+  for Kind in TCSSNumericalIDKind do
+    FreeAndNil(FHashLists[Kind]);
+
+  // attributes
+  NotAllAttributes:=nil;
+  for i:=1 to AttributeCount-1 do
+    FreeAndNil(Attributes[i]);
+  Attributes:=nil;
+  FAttributeCount:=0;
+
+  // pseudo classes
+  for i:=1 to PseudoClassCount-1 do
+    FreeAndNil(PseudoClasses[i]);
+  PseudoClasses:=nil;
+  FPseudoClassCount:=0;
+
+  // types
+  for i:=1 to TypeCount-1 do
+    FreeAndNil(Types[i]);
+  Types:=nil;
+  FTypeCount:=0;
+
+  // keywords
+  FKeywordCount:=0;
+
+  inherited Destroy;
+end;
+
+function TCSSRegistry.FindNamedItem(Kind: TCSSNumericalIDKind;
+  const aName: TCSSString): TCSSRegistryNamedItem;
+begin
+  if Kind in nikAllDescriptors then
+    Result:=TCSSRegistryNamedItem(FHashLists[Kind].Find(aName))
+  else
+    raise Exception.Create('20240625141820');
+end;
+
+function TCSSRegistry.IndexOfNamedItem(Kind: TCSSNumericalIDKind;
+  const aName: TCSSString): TCSSNumericalID;
+var
+  Item: TCSSRegistryNamedItem;
+  p: Pointer;
+begin
+  if Kind in nikAllDescriptors then
+  begin
+    Item:=TCSSRegistryNamedItem(FHashLists[Kind].Find(aName));
+    if Item<>nil then
+      Result:=Item.Index
+    else
+      Result:=-1;
+  end else begin
+    p:=FHashLists[Kind].Find(aName);
+    if p=nil then
+      exit(CSSIDNone)
+    else
+      Result:={%H-}TCSSNumericalID(p);
+  end;
+end;
+
+procedure TCSSRegistry.ConsistencyCheck;
+var
+  ID, ID2: TCSSNumericalID;
+  AttrDesc, SubAttrDesc: TCSSAttributeDesc;
+  PseudoClassDesc: TCSSPseudoClassDesc;
+  TypeDesc: TCSSTypeDesc;
+  i: Integer;
+  aName: TCSSString;
+begin
+  if AttributeCount>length(Attributes) then
+    raise Exception.Create('20240629102438');
+  for ID:=1 to AttributeCount-1 do
+  begin
+    AttrDesc:=Attributes[ID];
+    if AttrDesc=nil then
+      raise Exception.Create('20240629102530 attr ID='+IntToStr(ID)+' Desc=nil');
+    aName:=AttrDesc.Name;
+    if aName='' then
+      raise Exception.Create('20240629100056 attr ID='+IntToStr(ID)+' missing name');
+    if length(aName)>255 then
+      raise Exception.Create('20240629100143 attr ID='+IntToStr(ID)+' name too long "'+aName+'"');
+    if aName[1]=':' then
+      raise Exception.Create('20240701231211 attr ID='+IntToStr(ID)+' invalid name "'+aName+'"');
+    if AttrDesc.Index<>ID then
+      raise Exception.Create('20240629095849 attr ID='+IntToStr(ID)+' Desc.Index='+IntToStr(AttrDesc.Index)+' "'+aName+'"');
+    ID2:=IndexOfAttributeName(aName);
+    if ID2<>ID then
+      raise Exception.Create('20240629101227 attr ID='+IntToStr(ID)+' "'+aName+'" IndexOf failed: '+IntToStr(ID2));
+
+    if length(AttrDesc.CompProps)>0 then
+    begin
+      // check shorthand
+      for i:=0 to length(AttrDesc.CompProps)-1 do
+      begin
+        SubAttrDesc:=AttrDesc.CompProps[i];
+        if SubAttrDesc=nil then
+          raise Exception.Create('20240629102701 attr ID='+IntToStr(ID)+' Shorthand="'+aName+'" CompDesc=nil '+IntToStr(i));
+        if (SubAttrDesc.Index<=0) then
+          raise Exception.Create('20240629100345 attr ID='+IntToStr(ID)+' Shorthand="'+aName+'" invalid CompAttr '+IntToStr(SubAttrDesc.Index));
+        if (SubAttrDesc.Index>=ID) then
+          raise Exception.Create('20240629100345 attr ID='+IntToStr(ID)+' Shorthand="'+aName+'" CompAttr after Shorthand: SubID='+IntToStr(SubAttrDesc.Index)+' SubName='+SubAttrDesc.Name);
+      end;
+    end;
+  end;
+
+  if PseudoClassCount>length(PseudoClasses) then
+    raise Exception.Create('20240629102438');
+  for ID:=1 to PseudoClassCount-1 do
+  begin
+    PseudoClassDesc:=PseudoClasses[ID];
+    if PseudoClassDesc=nil then
+      raise Exception.Create('20240629102605 pseudo class ID='+IntToStr(ID)+' Desc=nil');
+    aName:=PseudoClassDesc.Name;
+    if aName='' then
+      raise Exception.Create('20240629100652 pseudo class ID='+IntToStr(ID)+' missing name');
+    if length(aName)>255 then
+      raise Exception.Create('20240629100657 pseudo class ID='+IntToStr(ID)+' name too long "'+aName+'"');
+    if aName[1]=':' then
+      raise Exception.Create('20240701231235 pseudo class ID='+IntToStr(ID)+' invalid name "'+aName+'"');
+    if PseudoClassDesc.Index<>ID then
+      raise Exception.Create('20240629100659 pseudo class ID='+IntToStr(ID)+' Desc.Index='+IntToStr(PseudoClassDesc.Index)+' "'+aName+'"');
+    ID2:=IndexOfPseudoClassName(PseudoClassDesc.Name);
+    if ID2<>ID then
+      raise Exception.Create('20240629101227 pseudo class ID='+IntToStr(ID)+' "'+aName+'" IndexOf failed: '+IntToStr(ID2));
+  end;
+
+  if PseudoFunctionCount>length(PseudoFunctions) then
+    raise Exception.Create('20240629103430');
+  for ID:=1 to PseudoFunctionCount-1 do
+  begin
+    aName:=PseudoFunctions[ID];
+    if aName='' then
+      raise Exception.Create('20240629103431 pseudo function ID='+IntToStr(ID)+' missing name');
+    if length(aName)>255 then
+      raise Exception.Create('20240629103433 pseudo function ID='+IntToStr(ID)+' name too long "'+aName+'"');
+    if aName[1]=':' then
+      raise Exception.Create('20240701231235 pseudo function ID='+IntToStr(ID)+' invalid name "'+aName+'"');
+    ID2:=IndexOfPseudoFunction(aName);
+    if ID2<>ID then
+      raise Exception.Create('20240629103434 pseudo function ID='+IntToStr(ID)+' "'+aName+'" IndexOf failed: '+IntToStr(ID2));
+  end;
+
+  if TypeCount>length(Types) then
+    raise Exception.Create('20240629102438');
+  for ID:=1 to TypeCount-1 do
+  begin
+    TypeDesc:=Types[ID];
+    if TypeDesc=nil then
+      raise Exception.Create('20240629102620 type ID='+IntToStr(ID)+' Desc=nil');
+    aName:=TypeDesc.Name;
+    if aName='' then
+      raise Exception.Create('20240629100812 type ID='+IntToStr(ID)+' missing name');
+    if length(aName)>255 then
+      raise Exception.Create('20240629100825 type ID='+IntToStr(ID)+' name too long "'+aName+'"');
+    if aName[1]=':' then
+      raise Exception.Create('20240701231645 type ID='+IntToStr(ID)+' invalid name "'+aName+'"');
+    if TypeDesc.Index<>ID then
+      raise Exception.Create('20240629101013 type ID='+IntToStr(ID)+' Desc.Index='+IntToStr(TypeDesc.Index)+' "'+aName+'"');
+    ID2:=IndexOfTypeName(aName);
+    if ID2<>ID then
+      raise Exception.Create('20240629101529 type ID='+IntToStr(ID)+' "'+aName+'" IndexOf failed: '+IntToStr(ID2));
+  end;
+
+  if KeywordCount>length(Keywords) then
+    raise Exception.Create('20240629103200');
+  for ID:=1 to KeywordCount-1 do
+  begin
+    aName:=Keywords[ID];
+    if aName='' then
+      raise Exception.Create('20240629103223 keyword ID='+IntToStr(ID)+' missing name');
+    if length(aName)>255 then
+      raise Exception.Create('20240629103242 keyword ID='+IntToStr(ID)+' name too long "'+aName+'"');
+    if aName[1]=':' then
+      raise Exception.Create('20240701231656 keyword ID='+IntToStr(ID)+' invalid name "'+aName+'"');
+    ID2:=IndexOfKeyword(aName);
+    if ID2<>ID then
+      raise Exception.Create('20240629103303 keyword ID='+IntToStr(ID)+' "'+aName+'" IndexOf failed: '+IntToStr(ID2));
+  end;
+end;
+
+function TCSSRegistry.AddAttribute(Attr: TCSSAttributeDesc
+  ): TCSSAttributeDesc;
+begin
+  Result:=Attr;
+  if Attr.Name='' then
+    raise ECSSParser.Create('missing name');
+  if FindAttribute(Attr.Name)<>nil then
+    raise ECSSParser.Create('duplicate attribute "'+Attr.Name+'"');
+
+  if AttributeCount=length(Attributes) then
+  begin
+    if AttributeCount<32 then
+      SetLength(Attributes,32)
+    else
+      SetLength(Attributes,2*AttributeCount);
+    FillByte(Attributes[AttributeCount],SizeOf(Pointer)*(length(Attributes)-AttributeCount),0);
+  end;
+  Attributes[AttributeCount]:=Attr;
+  Attr.Index:=AttributeCount;
+  FHashLists[nikAttribute].Add(Attr.Name,Attr);
+  inc(FAttributeCount);
+end;
+
+function TCSSRegistry.AddAttribute(const aName: TCSSString;
+  const aInitialValue: TCSSString; aInherits: boolean; aAll: boolean;
+  aClass: TCSSAttributeDescClass): TCSSAttributeDesc;
+begin
+  if aName='' then
+    raise ECSSParser.Create('missing name');
+  if FindAttribute(aName)<>nil then
+    raise ECSSParser.Create('duplicate attribute "'+aName+'"');
+  if aClass=nil then
+    aClass:=Attribute_ClassOf;
+
+  Result:=aClass.Create;
+  Result.Name:=aName;
+  Result.InitialValue:=aInitialValue;
+  Result.Inherits:=aInherits;
+  Result.All:=aAll;
+  AddAttribute(Result);
+
+  if not aAll then
+    Insert(Result,NotAllAttributes,length(NotAllAttributes));
+end;
+
+function TCSSRegistry.FindAttribute(const aName: TCSSString
+  ): TCSSAttributeDesc;
+begin
+  Result:=TCSSAttributeDesc(FHashLists[nikAttribute].Find(aName));
+end;
+
+function TCSSRegistry.IndexOfAttributeName(const aName: TCSSString
+  ): TCSSNumericalID;
+var
+  Attr: TCSSAttributeDesc;
+begin
+  Attr:=TCSSAttributeDesc(FHashLists[nikAttribute].Find(aName));
+  if Attr<>nil then
+    Result:=Attr.Index
+  else
+    Result:=-1;
+end;
+
+function TCSSRegistry.AddPseudoClass(aPseudo: TCSSPseudoClassDesc
+  ): TCSSPseudoClassDesc;
+begin
+  Result:=aPseudo;
+  if aPseudo.Name='' then
+    raise ECSSParser.Create('missing name');
+  if FindPseudoClass(aPseudo.Name)<>nil then
+    raise ECSSParser.Create('duplicate pseudo class "'+aPseudo.Name+'"');
+
+  if PseudoClassCount=length(PseudoClasses) then
+  begin
+    if PseudoClassCount<32 then
+      SetLength(PseudoClasses,32)
+    else
+      SetLength(PseudoClasses,2*PseudoClassCount);
+    FillByte(PseudoClasses[PseudoClassCount],SizeOf(Pointer)*(length(PseudoClasses)-PseudoClassCount),0);
+  end;
+  PseudoClasses[PseudoClassCount]:=aPseudo;
+  aPseudo.Index:=PseudoClassCount;
+  FHashLists[nikPseudoClass].Add(aPseudo.Name,aPseudo);
+  inc(FPseudoClassCount);
+end;
+
+function TCSSRegistry.AddPseudoClass(const aName: TCSSString;
+  aClass: TCSSPseudoClassDescClass): TCSSPseudoClassDesc;
+begin
+  if aName='' then
+    raise ECSSParser.Create('missing name');
+  if FindPseudoClass(aName)<>nil then
+    raise ECSSParser.Create('duplicate pseudo class "'+aName+'"');
+  if aClass=nil then
+    aClass:=PseudoClass_ClassOf;
+
+  Result:=aClass.Create;
+  Result.Name:=aName;
+  AddPseudoClass(Result);
+end;
+
+function TCSSRegistry.FindPseudoClass(const aName: TCSSString
+  ): TCSSPseudoClassDesc;
+begin
+  Result:=TCSSPseudoClassDesc(FHashLists[nikPseudoClass].Find(aName));
+end;
+
+function TCSSRegistry.IndexOfPseudoClassName(const aName: TCSSString
+  ): TCSSNumericalID;
+var
+  aPseudo: TCSSPseudoClassDesc;
+begin
+  aPseudo:=TCSSPseudoClassDesc(FHashLists[nikPseudoClass].Find(aName));
+  if aPseudo<>nil then
+    Result:=aPseudo.Index
+  else
+    Result:=-1;
+end;
+
+function TCSSRegistry.AddPseudoFunction(const aName: TCSSString
+  ): TCSSNumericalID;
+begin
+  if aName='' then
+    raise ECSSParser.Create('missing name');
+  if length(aName)>255 then
+    raise ECSSParser.Create('pseudo function name too long');
+  Result:=IndexOfKeyword(aName);
+  if Result>0 then
+    raise ECSSParser.Create('duplicate pseudo function "'+aName+'"');
+
+  if PseudoFunctionCount=length(PseudoFunctions) then
+  begin
+    if PseudoFunctionCount<32 then
+      SetLength(PseudoFunctions,32)
+    else
+      SetLength(PseudoFunctions,2*PseudoFunctionCount);
+  end;
+  Result:=PseudoFunctionCount;
+  PseudoFunctions[Result]:=aName;
+  FHashLists[nikPseudoFunction].Add(aName,{%H-}Pointer(Result));
+  inc(FPseudoFunctionCount);
+end;
+
+function TCSSRegistry.IndexOfPseudoFunction(const aName: TCSSString
+  ): TCSSNumericalID;
+var
+  p: Pointer;
+begin
+  p:=FHashLists[nikPseudoFunction].Find(aName);
+  if p=nil then
+    exit(CSSIDNone)
+  else
+    Result:={%H-}TCSSNumericalID(p);
+end;
+
+function TCSSRegistry.AddType(aType: TCSSTypeDesc): TCSSTypeDesc;
+begin
+  Result:=aType;
+  if aType.Name='' then
+    raise ECSSParser.Create('missing name');
+  if FindType(aType.Name)<>nil then
+    raise ECSSParser.Create('duplicate type "'+aType.Name+'"');
+
+  if TypeCount=length(Types) then
+  begin
+    if TypeCount<32 then
+      SetLength(Types,32)
+    else
+      SetLength(Types,2*TypeCount);
+    FillByte(Types[TypeCount],SizeOf(Pointer)*(length(Types)-TypeCount),0);
+  end;
+  Types[TypeCount]:=aType;
+  aType.Index:=TypeCount;
+  FHashLists[nikType].Add(aType.Name,aType);
+  inc(FTypeCount);
+end;
+
+function TCSSRegistry.AddType(const aName: TCSSString; aClass: TCSSTypeDescClass
+  ): TCSSTypeDesc;
+begin
+  if aName='' then
+    raise ECSSParser.Create('missing name');
+  if FindType(aName)<>nil then
+    raise ECSSParser.Create('duplicate type "'+aName+'"');
+  if aClass=nil then
+    aClass:=Type_ClassOf;
+
+  Result:=aClass.Create;
+  Result.Name:=aName;
+  AddType(Result);
+end;
+
+function TCSSRegistry.FindType(const aName: TCSSString): TCSSTypeDesc;
+begin
+  Result:=TCSSTypeDesc(FHashLists[nikType].Find(aName));
+end;
+
+function TCSSRegistry.IndexOfTypeName(const aName: TCSSString): TCSSNumericalID;
+var
+  aType: TCSSTypeDesc;
+begin
+  aType:=TCSSTypeDesc(FHashLists[nikType].Find(aName));
+  if aType<>nil then
+    Result:=aType.Index
+  else
+    Result:=-1;
+end;
+
+function TCSSRegistry.AddKeyword(const aName: TCSSString): TCSSNumericalID;
+begin
+  if aName='' then
+    raise ECSSParser.Create('missing name');
+  if length(aName)>255 then
+    raise ECSSParser.Create('keyword too long');
+  Result:=IndexOfKeyword(aName);
+  if Result>0 then
+    raise ECSSParser.Create('duplicate keyword "'+aName+'"');
+
+  if KeywordCount=length(Keywords) then
+  begin
+    if KeywordCount<32 then
+      SetLength(Keywords,32)
+    else
+      SetLength(Keywords,2*KeywordCount);
+  end;
+  Result:=KeywordCount;
+  Keywords[Result]:=aName;
+  FHashLists[nikKeyword].Add(aName,{%H-}Pointer(Result));
+  inc(FKeywordCount);
+end;
+
+function TCSSRegistry.IndexOfKeyword(const aName: TCSSString): TCSSNumericalID;
+var
+  p: Pointer;
+begin
+  p:=FHashLists[nikKeyword].Find(aName);
+  if p=nil then
+    exit(CSSIDNone)
+  else
+    Result:={%H-}TCSSNumericalID(p);
+end;
+
+function TCSSRegistry.AddAttrFunction(const aName: TCSSString): TCSSNumericalID;
+begin
+  if aName='' then
+    raise ECSSParser.Create('missing name');
+  if length(aName)>255 then
+    raise ECSSParser.Create('function name too long');
+  Result:=IndexOfAttrFunction(aName);
+  if Result>0 then
+    raise ECSSParser.Create('duplicate attribute function "'+aName+'"');
+
+  if AttrFunctionCount=length(AttrFunctions) then
+  begin
+    if AttrFunctionCount<32 then
+      SetLength(AttrFunctions,32)
+    else
+      SetLength(AttrFunctions,2*AttrFunctionCount);
+  end;
+  Result:=AttrFunctionCount;
+  AttrFunctions[Result]:=aName;
+  FHashLists[nikAttrFunction].Add(aName,{%H-}Pointer(Result));
+  inc(FAttrFunctionCount);
+end;
+
+function TCSSRegistry.IndexOfAttrFunction(const aName: TCSSString
+  ): TCSSNumericalID;
+var
+  p: Pointer;
+begin
+  p:=FHashLists[nikAttrFunction].Find(aName);
+  if p=nil then
+    exit(CSSIDNone)
+  else
+    Result:={%H-}TCSSNumericalID(p);
+end;
+
+{ TCSSResolvedCallElement }
+
+destructor TCSSResolvedCallElement.Destroy;
+begin
+  FreeAndNil(Params);
+  inherited Destroy;
+end;
+
+{ TCSSResValue }
+
+function TCSSResValue.FloatAsString: TCSSString;
+begin
+  Result:=FloatToCSSStr(Float)+CSSUnitNames[FloatUnit];
+end;
+
+{ TCSSBaseResolver }
+
+procedure TCSSBaseResolver.SetCSSRegistry(const AValue: TCSSRegistry);
+begin
+  if FCSSRegistry=AValue then Exit;
+  FCSSRegistry:=AValue;
+end;
+
+function TCSSBaseResolver.CheckAttribute_Keyword(Data: TCSSAttributeKeyData;
+  const AllowedKeywordIDs: TCSSNumericalIDArray; out ResValue: TCSSResValue
+  ): boolean;
+begin
+  Result:=ReadAttribute_Keyword(Data.Value,Data.Invalid,AllowedKeywordIDs,ResValue);
+end;
+
+function TCSSBaseResolver.CheckAttribute_Dimension(Data: TCSSAttributeKeyData;
+  const Params: TCSSCheckAttrParams_Float; out ResValue: TCSSResValue
+  ): boolean;
+begin
+  Result:=ReadAttribute_Dimension(Data.Value,Data.Invalid,Params,ResValue);
+end;
+
+function TCSSBaseResolver.ReadAttribute_Keyword(const Value: TCSSString; out
+  Invalid: boolean; const AllowedKeywordIDs: TCSSNumericalIDArray; out
+  ResValue: TCSSResValue): boolean;
+var
+  p: PCSSChar;
+  i: Integer;
+begin
+  Invalid:=true;
+  p:=PCSSChar(Value);
+  while ReadValue(p,ResValue) do
+  begin
+    case ResValue.Kind of
+    rvkKeyword:
+      case ResValue.KeywordID of
+      CSSKeywordNone,CSSKeywordInitial,CSSKeywordInherit,CSSKeywordUnset,CSSKeywordRevert,CSSKeywordRevertLayer:
+        begin
+          Invalid:=false;
+          exit(true);
+        end
+      else
+        for i:=0 to length(AllowedKeywordIDs)-1 do
+          if ResValue.KeywordID=AllowedKeywordIDs[i] then
+          begin
+            Invalid:=false;
+            exit(true);
+          end;
+      end;
+    rvkFunction:
+      begin
+        // could be valid
+        Invalid:=false;
+      end;
+    end;
+  end;
+  Result:=false;
+end;
+
+function TCSSBaseResolver.ReadAttribute_Dimension(const Value: TCSSString; out
+  Invalid: boolean; const Params: TCSSCheckAttrParams_Float; out
+  ResValue: TCSSResValue): boolean;
+var
+  p: PCSSChar;
+  i: Integer;
+begin
+  Invalid:=true;
+  p:=PCSSChar(Value);
+  while ReadValue(p,ResValue) do
+  begin
+    case ResValue.Kind of
+    rvkFloat:
+      if ResValue.FloatUnit in Params.AllowedUnits then
+      begin
+        if (not Params.AllowNegative) and (ResValue.Float<0) then continue;
+        if (not Params.AllowFrac) and (Frac(ResValue.Float)>0) then continue;
+        Invalid:=false;
+        exit(true);
+      end;
+    rvkKeyword:
+      case ResValue.KeywordID of
+      CSSKeywordNone,CSSKeywordInitial,CSSKeywordInherit,CSSKeywordUnset,CSSKeywordRevert,CSSKeywordRevertLayer:
+        begin
+          Invalid:=false;
+          exit(true);
+        end
+      else
+        for i:=0 to length(Params.AllowedKeywordIDs)-1 do
+          if ResValue.KeywordID=Params.AllowedKeywordIDs[i] then
+          begin
+            Invalid:=false;
+            exit(true);
+          end;
+      end;
+    rvkFunction:
+      begin
+        // could be valid
+        Invalid:=false;
+      end;
+    end;
+  end;
+  Result:=false;
+end;
+
+function TCSSBaseResolver.ReadValue(var p: PCSSChar; out Value: TCSSResValue
+  ): boolean;
+var
+  c: TCSSChar;
+begin
+  Result:=true;
+  Value.Kind:=rvkNone;
+
+  // skip whitespace
+  while (p^ in Whitespace) do inc(p);
+  Value.Start:=p;
+
+  c:=p^;
+  case c of
+  #0: exit(false);
+  '0'..'9':
+    if ReadNumber(p,Value) then exit;
+  '+':
+    case p[1] of
+    '0'..'9','.':
+      if ReadNumber(p,Value) then exit;
+    #0,#9,#10,#13,' ':
+      begin
+        inc(p);
+        Value.Kind:=rvkSymbol;
+        exit;
+      end;
+    end;
+  '-':
+    case p[1] of
+    '0'..'9','.':
+      if ReadNumber(p,Value) then exit;
+    'a'..'z','A'..'Z','-':
+      if ReadIdentifier(p,Value) then exit;
+    #0,#9,#10,#13,' ':
+      begin
+        inc(p);
+        Value.Kind:=rvkSymbol;
+        exit;
+      end;
+    end;
+  '.':
+    case p[1] of
+    '0'..'9':
+      if ReadNumber(p,Value) then exit;
+    else
+      inc(p);
+      Value.Kind:=rvkSymbol;
+      exit;
+    end;
+  '*','/':
+    if p[1] in WhitespaceZ then
+    begin
+      inc(p);
+      Value.Kind:=rvkSymbol;
+      exit;
+    end;
+  'a'..'z','A'..'Z':
+    if ReadIdentifier(p,Value) then exit;
+  end;
+
+  // skip unknown value
+  Value.Kind:=rvkInvalid;
+  repeat
+    if p^ in ValEnd then exit;
+    case p^ of
+    '(','[': SkipBrackets(p);
+    '''','"': SkipString(p);
+    else inc(p);
+    end;
+  until false;
+end;
+
+function TCSSBaseResolver.ReadNumber(var p: PCSSChar; var Value: TCSSResValue
+  ): boolean;
+var
+  Negative, HasNumber: Boolean;
+  Divisor: double;
+  Exponent: Integer;
+  d: Float;
+  U: TCSSUnit;
+  StartP: PCSSChar;
+begin
+  Result:=false;
+  Value.Kind:=rvkInvalid;
+
+  // number: 1, 0.2, .3, 4.01, 0.0, +0.0, -0.0, .50, 2e3, -6.7E-2
+  if p^='-' then
+  begin
+    Negative:=true;
+    inc(p);
+  end else begin
+    Negative:=false;
+    if p^='+' then
+      inc(p);
+  end;
+  HasNumber:=false;
+  Value.Float:=0;
+  if p^ in Num then
+  begin
+    // read significand
+    HasNumber:=true;
+    repeat
+      Value.Float:=Value.Float*10+ord(p^)-ord('0');
+      if Value.Float>CSSMaxSafeIntDouble then
+        exit; // loosing precision
+      inc(p);
+    until not (p^ in Num);
+  end;
+  if p^='.' then
+  begin
+    // read fraction
+    inc(p);
+    if not (p^ in Num) then exit;
+    Divisor:=1;
+    repeat
+      Divisor:=Divisor*10;
+      Value.Float:=Value.Float*10+ord(p^)-ord('0');
+      if (Divisor>CSSMaxSafeIntDouble)
+          or (Value.Float>CSSMaxSafeIntDouble) then
+        exit; // loosing precision
+      inc(p);
+    until not (p^ in Num);
+    Value.Float:=Value.Float/Divisor;
+  end else if not HasNumber then
+    exit;
+  if Negative then
+    Value.Float:=-Value.Float;
+
+  if (p^ in ['e','E']) and not (p[1] in ['a'..'z']) then
+  begin
+    inc(p);
+    if p^='-' then
+    begin
+      Negative:=true;
+      inc(p);
+    end else begin
+      Negative:=false;
+      if p^='+' then
+        inc(p);
+    end;
+    if not (p^ in Num) then exit;
+    Exponent:=0;
+    repeat
+      inc(p);
+      Exponent:=Exponent*10+ord(p^)-ord('0');
+      if Exponent>2047 then
+        exit; // out of bounds
+    until not (p^ in Num);
+    if Exponent>0 then
+    begin
+      if Negative then
+        Exponent:=-Exponent;
+      try
+        d:=Power(10,Exponent);
+        Value.Float:=Value.Float*d;
+      except
+        exit;
+      end;
+    end;
+  end;
+  Value.Kind:=rvkFloat;
+
+  // parse unit
+  U:=cuNONE;
+  case p^ of
+  '%':
+    begin
+      inc(p);
+      U:=cuPERCENT;
+    end;
+  'a'..'z','A'..'Z':
+    begin
+      StartP:=p;
+      while p^ in Alpha do inc(p);
+      U:=high(TCSSUnit);
+      while (U>cuNONE) and not CompareMem(StartP,PChar(CSSUnitNames[U]),length(CSSUnitNames[U])) do
+        U:=pred(U);
+      if U=cuNONE then
+        exit; // unknown unit
+    end;
+  end;
+  Value.FloatUnit:=U;
+
+  Result:=p^ in ValEnd;
+  //writeln('TCSSBaseResolver.ReadNumber "',p,'" Value=',FloatToCSSStr(Value.Float),' U=',U,' Kind=',Value.Kind,' Result=',Result);
+end;
+
+function TCSSBaseResolver.ReadIdentifier(var p: PCSSChar; var Value: TCSSResValue
+  ): boolean;
+var
+  Identifier: TCSSString;
+  IsFunc: Boolean;
+begin
+  Result:=false;
+  Value.Kind:=rvkInvalid;
+  if not (p^ in AlIden) then exit;
+  Value.Start:=p;
+  repeat
+    inc(p);
+  until not (p^ in AlNumIden);
+  SetString(Identifier,Value.Start,p-Value.Start);
+  IsFunc:=p^='(';
+  if IsFunc then
+  begin
+    // function call
+    Value.BracketOpen:=p;
+    if not SkipBrackets(p) then exit;
+  end;
+  Result:=p^ in ValEnd;
+  if not Result then exit;
+
+  if IsFunc then
+  begin
+    Value.FunctionID:=CSSRegistry.IndexOfAttrFunction(Identifier);
+    if Value.FunctionID>CSSIDNone then
+      Value.Kind:=rvkFunction
+    else
+      Value.Kind:=rvkFunctionUnknown;
+  end else begin
+    Value.KeywordID:=CSSRegistry.IndexOfKeyword(Identifier);
+    if Value.KeywordID>CSSIDNone then
+      Value.Kind:=rvkKeyword
+    else
+      Value.Kind:=rvkKeywordUnknown;
+  end;
+end;
+
+procedure TCSSBaseResolver.SkipToEndOfAttribute(var p: PCSSChar);
+begin
+  repeat
+    case p^ of
+    #0,'{','}',';': exit;
+    '''','"': SkipString(p);
+    else inc(p);
+    end;
+  until false;
+end;
+
+function TCSSBaseResolver.SkipString(var p: PCSSChar): boolean;
+var
+  Delim, c: TCSSChar;
+begin
+  Result:=false;
+  Delim:=p^;
+  repeat
+    inc(p);
+    c:=p^;
+    if c=Delim then
+    begin
+      inc(p);
+      exit(true);
+    end else if c=#0 then
+      exit
+    else
+      inc(p);
+  until false;
+end;
+
+function TCSSBaseResolver.SkipBrackets(var p: PCSSChar; Lvl: integer): boolean;
+const
+  CSSMaxBracketLvl = 10;
+var
+  CloseBracket: TCSSChar;
+begin
+  Result:=false;
+  if Lvl>CSSMaxBracketLvl then
+  begin
+    SkipToEndOfAttribute(p);
+    exit;
+  end;
+
+  if p^='[' then
+    CloseBracket:=']'
+  else
+    CloseBracket:=')';
+  repeat
+    inc(p);
+    case p^ of
+    #0,'{','}',';': exit;
+    '''','"': SkipString(p);
+    '(','[': SkipBrackets(p,Lvl+1);
+    ')',']':
+      if p^=CloseBracket then
+      begin
+        inc(p);
+        exit(true);
+      end else begin
+        SkipToEndOfAttribute(p);
+        exit;
+      end;
+    end;
+  until false;
+end;
+
+{ TCSSResolverParser }
+
+function TCSSResolverParser.ResolveIdentifier(El: TCSSResolvedIdentifierElement;
+  Kind: TCSSNumericalIDKind): TCSSNumericalID;
+var
+  aName: TCSSString;
+begin
+  if El.NumericalID<>CSSIDNone then
+    raise Exception.Create('20240701143234');
+  aName:=El.Name;
+  if Kind=nikPseudoClass then
+  begin
+    // pseudo classes are ASCII case insensitive
+    System.Delete(aName,1,1);
+    aName:=lowercase(aName);
+  end;
+
+  El.Kind:=Kind;
+  Result:=Registry.IndexOfNamedItem(Kind,aName);
+  //writeln('TCSSResolverParser.ResolveIdentifier ',aName,' ID=',Result);
+  if Result=CSSIDNone then
+  begin
+    El.NumericalID:=-1;
+    Log(etWarning,20240625130648,'unknown '+CSSNumericalIDKindNames[Kind]+' "'+aName+'"',El);
+  end else
+    El.NumericalID:=Result;
+end;
+
+function TCSSResolverParser.ResolvePseudoClass(
+  El: TCSSResolvedPseudoClassElement): TCSSNumericalID;
+var
+  aName: TCSSString;
+begin
+  aName:=El.Name;
+  // pseudo classes are ASCII case insensitive
+  System.Delete(aName,1,1);
+  aName:=lowercase(aName);
+
+  if El.NumericalID<>CSSIDNone then
+    raise Exception.Create('20240701143234');
+
+  El.Kind:=nikPseudoClass;
+  Result:=Registry.IndexOfNamedItem(nikPseudoClass,aName);
+  //writeln('TCSSResolverParser.ResolvePseudoClass ',aName,' ID=',Result);
+  if Result=CSSIDNone then
+  begin
+    El.NumericalID:=-1;
+    Log(etWarning,20240625130648,'unknown pseudo class "'+aName+'"',El);
+  end else
+    El.NumericalID:=Result;
+end;
+
+function TCSSResolverParser.ResolvePseudoFunction(El: TCSSResolvedCallElement
+  ): TCSSNumericalID;
+var
+  aName: TCSSString;
+begin
+  if El.NameNumericalID<>CSSIDNone then
+    raise Exception.Create('20240701143035');
+  aName:=El.Name;
+  if aName[1]<>':' then
+    raise Exception.Create('20240701143650');
+
+  // pseudo functions are ASCII case insensitive
+  System.Delete(aName,1,1);
+  aName:=lowercase(aName);
+
+  El.Kind:=nikPseudoFunction;
+  Result:=Registry.IndexOfNamedItem(nikPseudoFunction,aName);
+  //writeln('TCSSResolverParser.ResolvePseudoFunction ',aName,' ID=',Result);
+  if Result=CSSIDNone then
+  begin
+    El.NameNumericalID:=-1;
+    Log(etWarning,20240625130648,'unknown pseudo class "'+aName+'"',El);
+  end else
+    El.NameNumericalID:=Result;
+end;
+
+function TCSSResolverParser.ParseCall(aName: TCSSString; IsSelector: boolean
+  ): TCSSCallElement;
+var
+  CallID: TCSSNumericalID;
+begin
+  Result:=inherited ParseCall(aName, IsSelector);
+  if IsSelector then
+  begin
+    if Result.Name[1]=':' then
+    begin
+      CallID:=ResolvePseudoFunction(TCSSResolvedCallElement(Result));
+      case CallID of
+      CSSCallID_NthChild,CSSCallID_NthLastChild,
+      CSSCallID_NthOfType,CSSCallID_NthLastOfType: CheckNthChildParams(TCSSResolvedCallElement(Result));
+      end;
+    end
+    else
+      Log(etWarning,20240701222744,'invalid selector function',Result);
+  end;
+end;
+
+function TCSSResolverParser.ParseDeclaration(aIsAt: Boolean
+  ): TCSSDeclarationElement;
+var
+  aKey: TCSSElement;
+  AttrId: TCSSNumericalID;
+  Desc: TCSSAttributeDesc;
+  AttrData: TCSSAttributeKeyData;
+  i: Integer;
+begin
+  Result:=inherited ParseDeclaration(aIsAt);
+  if Result.KeyCount<>1 then
+  begin
+    if Result.KeyCount<1 then
+      Log(etWarning,20231112135955,'missing keys in declaration',Result);
+    if Result.KeyCount>1 then
+      Log(etWarning,20231112140722,'too many keys in declaration',Result);
+    exit;
+  end;
+
+  aKey:=Result.Keys[0];
+  if aKey is TCSSResolvedIdentifierElement then
+  begin
+    // todo: custom attributes
+    AttrId:=ResolveIdentifier(TCSSResolvedIdentifierElement(aKey),nikAttribute);
+
+    if aKey.CustomData<>nil then
+      raise Exception.Create('20240626113536');
+    AttrData:=CSSAttributeKeyDataClass.Create;
+    aKey.CustomData:=AttrData;
+
+    i:=Result.ChildCount;
+    if i=0 then
+    begin
+      AttrData.Invalid:=true;
+      exit;
+    end;
+    for i:=0 to Result.ChildCount-1 do
+    begin
+      if (i>0) then
+        AttrData.Value+=', ';
+      AttrData.Value+=Result.Children[i].AsString;
+    end;
+
+    if AttrId>CSSIDNone then
+    begin
+      Desc:=Registry.Attributes[AttrId];
+      if Assigned(Desc.OnCheck) then
+        Desc.OnCheck(Resolver,AttrData);
+      if Assigned(Desc.OnCheckEx) and (not AttrData.Invalid) then
+        Desc.OnCheckEx(Resolver,Desc,Result,AttrData);
+      {$IFDEF VerboseCSSResolver}
+      if AttrData.Invalid then
+        Log(etWarning,20240710162400,'Invalid CSS attribute value: '+Result.AsString,aKey);
+      {$ENDIF}
+    end;
+  end else begin
+    Log(etWarning,20220908230855,'Expected property name, but found '+aKey.ClassName,aKey);
+  end;
+end;
+
+function TCSSResolverParser.ParseSelector: TCSSElement;
+begin
+  Result:=inherited ParseSelector;
+  CheckSelector(Result);
+end;
+
+procedure TCSSResolverParser.CheckSelector(El: TCSSElement);
+var
+  C: TClass;
+begin
+  //writeln('TCSSResolverParser.CheckSelector ',GetCSSObj(El));
+  C:=El.ClassType;
+  if C=TCSSResolvedIdentifierElement then
+    // e.g. div {}
+    ResolveIdentifier(TCSSResolvedIdentifierElement(El),nikType)
+  else if C=TCSSHashIdentifierElement then
+    // e.g. #id {}
+  else if C=TCSSClassNameElement then
+    // e.g. .classname {}
+  else if C=TCSSResolvedPseudoClassElement then
+    // e.g. :pseudoclass {}
+    ResolvePseudoClass(TCSSResolvedPseudoClassElement(El))
+  else if C=TCSSBinaryElement then
+    CheckSelectorBinary(TCSSBinaryElement(El))
+  else if C=TCSSArrayElement then
+    CheckSelectorArray(TCSSArrayElement(El))
+  else if C=TCSSListElement then
+    CheckSelectorList(TCSSListElement(El))
+  else if C=TCSSResolvedCallElement then
+    // checked via ParseCall
+  else
+    Log(etWarning,20240625131810,'Unknown CSS selector element',El);
+end;
+
+procedure TCSSResolverParser.CheckSelectorArray(anArray: TCSSArrayElement);
+var
+  {$IFDEF VerboseCSSResolver}
+  i: integer;
+  {$ENDIF}
+  El: TCSSElement;
+  C: TClass;
+  aValue: TCSSString;
+begin
+  if anArray.Prefix<>nil then
+  begin
+    Log(etWarning,20240625134737,'Invalid CSS attribute selector prefix',anArray.Prefix);
+    exit;
+  end;
+
+  {$IFDEF VerboseCSSResolver}
+  writeln('TCSSResolverParser.CheckSelectorArray Prefix=',GetCSSObj(anArray.Prefix),' ChildCount=',anArray.ChildCount);
+  for i:=0 to anArray.ChildCount-1 do
+    writeln('TCSSResolverParser.CheckSelectorArray ',i,' ',GetCSSObj(anArray.Children[i]));
+  {$ENDIF}
+  if anArray.ChildCount<1 then
+  begin
+    Log(etWarning,20220910154033,'Invalid CSS attribute selector',anArray);
+    exit;
+  end;
+
+  if anArray.ChildCount>1 then
+  begin
+    El:=anArray.Children[1];
+    C:=El.ClassType;
+    if C=TCSSResolvedIdentifierElement then
+    begin
+      aValue:=TCSSResolvedIdentifierElement(El).Value;
+      case aValue of
+      'i','s': ;
+      else
+        Log(etWarning,20240625134931,'Invalid attribute modifier "'+aValue+'"',El);
+        exit;
+      end;
+    end else begin
+      Log(etWarning,20240625134940,'Invalid CSS attribute modifier',El);
+      exit;
+    end;
+  end;
+  if (anArray.ChildCount>2) then
+    Log(etWarning,20240625134951,'Invalid CSS attribute modifier',anArray.Children[2]);
+
+  El:=anArray.Children[0];
+  C:=El.ClassType;
+  if C=TCSSResolvedIdentifierElement then
+  begin
+    // [name]  ->  has explicit attribute
+    ResolveIdentifier(TCSSResolvedIdentifierElement(El),nikAttribute);
+  end else if C=TCSSBinaryElement then
+    CheckSelectorArrayBinary(TCSSBinaryElement(El))
+  else begin
+    Log(etWarning,20240625135119,'Invalid CSS array selector',El);
+  end;
+end;
+
+procedure TCSSResolverParser.CheckSelectorArrayBinary(aBinary: TCSSBinaryElement
+  );
+var
+  Left, Right: TCSSElement;
+  C: TClass;
+begin
+  Left:=aBinary.Left;
+  if Left.ClassType<>TCSSResolvedIdentifierElement then
+  begin
+    Log(etWarning,20240625154314,'Invalid CSS array selector, expected attribute',Left);
+    exit;
+  end;
+  ResolveIdentifier(TCSSResolvedIdentifierElement(Left),nikAttribute);
+
+  Right:=aBinary.Right;
+  C:=Right.ClassType;
+  if (C=TCSSStringElement) or (C=TCSSIntegerElement) or (C=TCSSFloatElement)
+      or (C=TCSSResolvedIdentifierElement) then
+    // ok
+  else begin
+    Log(etWarning,20240625154455,'Invalid CSS array selector, expected string',Right);
+    exit;
+  end;
+  ComputeValue(Right);
+
+  case aBinary.Operation of
+  boEquals,
+  boSquaredEqual,
+  boDollarEqual,
+  boPipeEqual,
+  boStarEqual,
+  boTildeEqual: ;
+  else
+    Log(etWarning,20240625154617,'Invalid CSS array selector operator',aBinary);
+  end;
+end;
+
+procedure TCSSResolverParser.CheckSelectorBinary(aBinary: TCSSBinaryElement);
+begin
+  case aBinary.Operation of
+  boGT,
+  boPlus,
+  boTilde,
+  boWhiteSpace: ;
+  else
+    Log(etWarning,20240625153307,'Invalid CSS binary selector '+BinaryOperators[aBinary.Operation],aBinary);
+  end;
+
+  CheckSelector(aBinary.Left);
+  CheckSelector(aBinary.Right);
+end;
+
+procedure TCSSResolverParser.CheckSelectorList(aList: TCSSListElement);
+var
+  i: Integer;
+  El: TCSSElement;
+begin
+  for i:=0 to aList.ChildCount-1 do
+  begin
+    El:=aList.Children[i];
+    {$IFDEF VerboseCSSResolver}
+    writeln('TCSSResolverParser.CheckSelectorList ',i,' ',GetCSSObj(El),' AsString=',El.AsString);
+    {$ENDIF}
+    CheckSelector(El);
+  end;
+end;
+
+procedure TCSSResolverParser.CheckNthChildParams(aCall: TCSSResolvedCallElement);
+
+  procedure NthWarn(const ID: TCSSMsgID; const Msg: string; PosEl: TCSSElement);
+  begin
+    Log(etWarning,ID,CSSSelectorCallNames[aCall.NameNumericalID]+' '+Msg,PosEl);
+  end;
+
+var
+  i, ArgCount, aModulo, aStart: Integer;
+  Arg, OffsetEl: TCSSElement;
+  Str: TCSSString;
+  UnaryEl, anUnary: TCSSUnaryElement;
+  Params: TCSSNthChildParams;
+begin
+  if aCall.Params<>nil then
+    raise Exception.Create('20240625150639');
+  ArgCount:=aCall.ArgCount;
+  {$IFDEF VerboseCSSResolver}
+  writeln('TCSSResolverParser.CheckSelectorCall_NthChild ArgCount=',aCall.ArgCount);
+  for i:=0 to aCall.ArgCount-1 do
+    writeln('TCSSResolverParser.CheckSelectorCall_NthChild Arg[',i,'] ',GetCSSObj(aCall.Args[i]),' AsString=',aCall.Args[i].AsString);
+  {$ENDIF}
+
+  // An, An+B, An+B of S, odd, even
+  i:=0;
+  aModulo:=0;
+  aStart:=1;
+  // check step
+  if ArgCount<=i then
+  begin
+    NthWarn(20220915143843,'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
+      NthWarn(20220915143843,'missing arguments',aCall);
+      exit;
+    end;
+    Arg:=aCall.Args[i];
+    if Arg.ClassType<>TCSSResolvedIdentifierElement then
+    begin
+      NthWarn(20220915144312,'expected n',Arg);
+      exit;
+    end;
+    if TCSSResolvedIdentifierElement(Arg).Value<>'n' then
+    begin
+      NthWarn(20220915144359,'expected n',Arg);
+      exit;
+    end;
+
+  end
+  else if Arg.ClassType=TCSSResolvedIdentifierElement then
+  begin
+    Str:=TCSSResolvedIdentifierElement(Arg).Value;
+    case lowercase(Str) of
+    'even':
+      begin
+      aModulo:=2;
+      aStart:=2;
+      end;
+    'odd':
+      begin
+      aModulo:=2;
+      end;
+    'n':
+      begin
+      aModulo:=1;
+      end;
+    else
+      NthWarn(20220915150332,'expected multiplier',Arg);
+      exit;
+    end
+  end else if Arg.ClassType=TCSSUnaryElement then
+  begin
+    anUnary:=TCSSUnaryElement(Arg);
+    case anUnary.Operation of
+    uoMinus: aModulo:=-1;
+    uoPlus: aModulo:=1;
+    else
+      NthWarn(20220917080309,'expected multiplier',Arg);
+      exit;
+    end;
+    if (anUnary.Right.ClassType=TCSSResolvedIdentifierElement)
+        and (SameText(TCSSResolvedIdentifierElement(anUnary.Right).Value,'n')) then
+    begin
+      // ok
+    end else begin
+      NthWarn(20220917080154,'expected multiplier',Arg);
+      exit;
+    end;
+  end else
+  begin
+    NthWarn(20220915144056,'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);
+      if not (UnaryEl.Operation in [uoMinus,uoPlus]) then
+      begin
+        NthWarn(20220915151422,'unexpected offset',UnaryEl);
+        exit;
+      end;
+      OffsetEl:=UnaryEl.Right;
+      if OffsetEl=nil then
+      begin
+        NthWarn(20220915151511,'unexpected offset',UnaryEl);
+        exit;
+      end;
+      if OffsetEl.ClassType<>TCSSIntegerElement then
+      begin
+        NthWarn(20220915151718,'unexpected offset',OffsetEl);
+        exit;
+      end;
+      aStart:=TCSSIntegerElement(OffsetEl).Value;
+      if UnaryEl.Operation=uoMinus then
+        aStart:=-aStart;
+    end else
+    begin
+      NthWarn(20220915150851,'expected offset',Arg);
+      exit;
+    end;
+  end;
+
+  Params:=CSSNthChildParamsClass.Create;
+  aCall.Params:=Params;
+  Params.Modulo:=aModulo;
+  Params.Start:=aStart;
+
+  inc(i);
+  if (i<ArgCount) then
+  begin
+    Arg:=aCall.Args[i];
+    if (Arg.ClassType=TCSSResolvedIdentifierElement)
+        and (SameText(TCSSResolvedIdentifierElement(Arg).Value,'of')) then
+    begin
+      // An+B of Selector
+      inc(i);
+      if i=ArgCount then
+      begin
+        NthWarn(20240711154813,'expected selector',Arg);
+        exit;
+      end;
+      Arg:=aCall.Args[i];
+      Params.HasOf:=true;
+      Params.OfSelector:=Arg;
+    end;
+  end;
+
+  if (aCall.NameNumericalID in [CSSCallID_NthOfType,CSSCallID_NthLastOfType]) then
+    Params.HasOf:=true;
+end;
+
+function TCSSResolverParser.ComputeValue(El: TCSSElement): TCSSString;
+var
+  ElData: TObject;
+  C: TClass;
+  StrEl: TCSSStringElement;
+  IntEl: TCSSIntegerElement;
+  FloatEl: TCSSFloatElement;
+begin
+  C:=El.ClassType;
+  if C=TCSSResolvedIdentifierElement then
+    Result:=TCSSResolvedIdentifierElement(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;
+    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;
+    ElData:=TCSSValueData.Create;
+    TCSSValueData(ElData).NormValue:=Result;
+    El.CustomData:=ElData;
+  end else begin
+    Log(etWarning,20240625162632,'TCSSResolverParser.ComputeValue not supported',El);
+  end;
+end;
+
+constructor TCSSResolverParser.Create(AScanner: TCSSScanner);
+begin
+  inherited Create(AScanner);
+  CSSIdentifierElementClass:=TCSSResolvedIdentifierElement;
+  CSSPseudoClassElementClass:=TCSSResolvedPseudoClassElement;
+  CSSCallElementClass:=TCSSResolvedCallElement;
+  CSSNthChildParamsClass:=TCSSNthChildParams;
+  CSSAttributeKeyDataClass:=TCSSAttributeKeyData;
+end;
+
+destructor TCSSResolverParser.Destroy;
+begin
+  inherited Destroy;
+end;
+
+procedure TCSSResolverParser.Log(MsgType: TEventType; const ID: TCSSMsgID;
+  const Msg: TCSSString; PosEl: TCSSElement);
+begin
+  if Assigned(OnLog) then
+    OnLog(MsgType,ID,Msg,PosEl);
+end;
+
+class function TCSSResolverParser.IsWhiteSpace(const s: TCSSString): boolean;
+var
+  i: Integer;
+begin
+  for i:=1 to length(s) do
+    if not (s[i] in [' ',#10,#13]) then
+      exit(false);
+  Result:=true;
+end;
+
+end.
+

+ 163 - 124
packages/fcl-css/src/fpcssscanner.pp

@@ -79,13 +79,10 @@ Type
     ctkPIPE,
     ctkPIPE,
     ctkPIPEEQUAL,
     ctkPIPEEQUAL,
     ctkDOLLAR,
     ctkDOLLAR,
-    ctkDOLLAREQUAL,
-    ctkINVALID
+    ctkDOLLAREQUAL
    );
    );
   TCSSTokens = Set of TCSSToken;
   TCSSTokens = Set of TCSSToken;
 
 
-  TCSSString = UTF8String;
-
 resourcestring
 resourcestring
   SErrInvalidCharacter = 'Invalid character ''%s''';
   SErrInvalidCharacter = 'Invalid character ''%s''';
   SErrOpenString = 'String exceeds end of line';
   SErrOpenString = 'String exceeds end of line';
@@ -130,13 +127,12 @@ Type
 
 
   { TCSSScanner }
   { TCSSScanner }
 
 
-  TCSSScannerOption = (csoExtendedIdentifiers,csoReturnComments,csoReturnWhiteSpace);
+  TCSSScannerOption = (csoExtendedIdentifiers,csoReturnComments,csoReturnWhiteSpace,csoDisablePseudo);
   TCSSScannerOptions = set of TCSSScannerOption;
   TCSSScannerOptions = set of TCSSScannerOption;
-  TCSSScannerWarnEvent = procedure(Sender: TObject; Msg: string) of object;
+  TCSSScannerWarnEvent = procedure(Sender: TObject; Msg: TCSSString) of object;
 
 
   TCSSScanner = class
   TCSSScanner = class
   private
   private
-    FDisablePseudo: Boolean;
     FOnWarn: TCSSScannerWarnEvent;
     FOnWarn: TCSSScannerWarnEvent;
     FOptions: TCSSScannerOptions;
     FOptions: TCSSScannerOptions;
     FSourceFile: TLineReader;
     FSourceFile: TLineReader;
@@ -145,7 +141,7 @@ Type
     FCurToken: TCSSToken;
     FCurToken: TCSSToken;
     FCurTokenString: TCSSString;
     FCurTokenString: TCSSString;
     FCurLine: TCSSString;
     FCurLine: TCSSString;
-    TokenStr: PAnsiChar;
+    TokenStr: PCSSChar;
     FSourceStream : TStream;
     FSourceStream : TStream;
     FOwnSourceFile : Boolean;
     FOwnSourceFile : Boolean;
     function DoHash: TCSSToken;
     function DoHash: TCSSToken;
@@ -156,17 +152,16 @@ Type
     function DoNumericLiteral: TCSSToken;
     function DoNumericLiteral: TCSSToken;
     function DoSingleLineComment: TCSSToken;
     function DoSingleLineComment: TCSSToken;
     function DoStringLiteral: TCSSToken;
     function DoStringLiteral: TCSSToken;
+    function DoStringEscape: TCSSToken;
     function DoWhiteSpace: TCSSToken;
     function DoWhiteSpace: TCSSToken;
     function EatBadURL: TCSSToken;
     function EatBadURL: TCSSToken;
     Function DoUnicodeRange : TCSSTOKEN;
     Function DoUnicodeRange : TCSSTOKEN;
     function FetchLine: Boolean;
     function FetchLine: Boolean;
     function GetCurColumn: Integer;
     function GetCurColumn: Integer;
-    function GetReturnComments: Boolean;
-    function GetReturnWhiteSpace: Boolean;
+    function GetOption(anOption: TCSSScannerOption): Boolean;
     function ReadUnicodeEscape: WideChar;
     function ReadUnicodeEscape: WideChar;
-    procedure SetReturnComments(AValue: Boolean);
-    procedure SetReturnWhiteSpace(AValue: Boolean);
-    class function UnknownCharToStr(C: AnsiChar): TCSSString;
+    procedure SetOption(anOption: TCSSScannerOption; const AValue: Boolean);
+    class function UnknownCharToStr(C: TCSSChar): TCSSString;
   protected
   protected
     procedure DoError(const Msg: TCSSString; Args: array of const); overload;
     procedure DoError(const Msg: TCSSString; Args: array of const); overload;
     procedure DoError(const Msg: TCSSString); overload;
     procedure DoError(const Msg: TCSSString); overload;
@@ -178,8 +173,8 @@ Type
     procedure OpenFile(const AFilename: TCSSString);
     procedure OpenFile(const AFilename: TCSSString);
     Function FetchToken: TCSSToken;
     Function FetchToken: TCSSToken;
     function IsUTF8BOM: boolean;
     function IsUTF8BOM: boolean;
-    Property ReturnComments : Boolean Read GetReturnComments Write SetReturnComments;
-    Property ReturnWhiteSpace : Boolean Read GetReturnWhiteSpace Write SetReturnWhiteSpace;
+    Property ReturnComments : Boolean Index csoReturnComments Read GetOption Write SetOption;
+    Property ReturnWhiteSpace : Boolean Index csoReturnWhiteSpace Read GetOption Write SetOption;
     Property Options : TCSSScannerOptions Read FOptions Write FOptions;
     Property Options : TCSSScannerOptions Read FOptions Write FOptions;
     property SourceFile: TLineReader read FSourceFile;
     property SourceFile: TLineReader read FSourceFile;
     property CurFilename: TCSSString read FSourceFilename;
     property CurFilename: TCSSString read FSourceFilename;
@@ -188,11 +183,11 @@ Type
     property CurColumn: Integer read GetCurColumn;
     property CurColumn: Integer read GetCurColumn;
     property CurToken: TCSSToken read FCurToken;
     property CurToken: TCSSToken read FCurToken;
     property CurTokenString: TCSSString read FCurTokenString;
     property CurTokenString: TCSSString read FCurTokenString;
-    property DisablePseudo : Boolean Read FDisablePseudo Write FDisablePseudo;
+    property DisablePseudo : Boolean Index csoDisablePseudo Read GetOption Write SetOption;
     property OnWarn: TCSSScannerWarnEvent read FOnWarn write FOnWarn;
     property OnWarn: TCSSScannerWarnEvent read FOnWarn write FOnWarn;
   end;
   end;
 
 
-function SafeFormat(const Fmt: string; const Args: array of const): string;
+function SafeFormat(const Fmt: TCSSString; const Args: array of const): TCSSString;
 
 
 implementation
 implementation
 
 
@@ -200,11 +195,11 @@ Const
   Alpha = ['A'..'Z','a'..'z'];
   Alpha = ['A'..'Z','a'..'z'];
   Num   = ['0'..'9'];
   Num   = ['0'..'9'];
   AlNum = Alpha+Num;
   AlNum = Alpha+Num;
-  AlNumIden = Alpha+Num+['-'];
+  AlNumIden = AlNum+['-'];
   WhiteSpace = [' ',#9];
   WhiteSpace = [' ',#9];
 
 
 type
 type
-  TMessageArgs = array of string;
+  TMessageArgs = array of TCSSString;
 
 
 procedure CreateMsgArgs(var MsgArgs: TMessageArgs; const Args: array of const);
 procedure CreateMsgArgs(var MsgArgs: TMessageArgs; const Args: array of const);
 var
 var
@@ -281,7 +276,8 @@ begin
     {$endif}
     {$endif}
 end;
 end;
 
 
-function SafeFormat(const Fmt: string; const Args: array of const): string;
+function SafeFormat(const Fmt: TCSSString;
+  const Args: array of const): TCSSString;
 var
 var
   MsgArgs: TMessageArgs;
   MsgArgs: TMessageArgs;
   i: Integer;
   i: Integer;
@@ -335,7 +331,7 @@ end;
 
 
 constructor TCSSScanner.Create(AStream: TStream);
 constructor TCSSScanner.Create(AStream: TStream);
 begin
 begin
-  FSourceStream:=ASTream;
+  FSourceStream:=AStream;
   FOwnSourceFile:=True;
   FOwnSourceFile:=True;
   Create(TStreamLineReader.Create(AStream));
   Create(TStreamLineReader.Create(AStream));
 end;
 end;
@@ -343,7 +339,7 @@ end;
 destructor TCSSScanner.Destroy;
 destructor TCSSScanner.Destroy;
 begin
 begin
   If FOwnSourceFile then
   If FOwnSourceFile then
-    FSourceFile.Free;
+    FreeAndNil(FSourceFile);
   inherited Destroy;
   inherited Destroy;
 end;
 end;
 
 
@@ -363,7 +359,7 @@ begin
   end else
   end else
   begin
   begin
     FCurLine := FSourceFile.ReadLine;
     FCurLine := FSourceFile.ReadLine;
-    TokenStr := PAnsiChar(CurLine);
+    TokenStr := PCSSChar(CurLine);
     Result := true;
     Result := true;
     Inc(FCurRow);
     Inc(FCurRow);
   end;
   end;
@@ -387,7 +383,7 @@ end;
 function TCSSScanner.DoSingleLineComment : TCSSToken;
 function TCSSScanner.DoSingleLineComment : TCSSToken;
 
 
 Var
 Var
-  TokenStart : PAnsiChar;
+  TokenStart : PCSSChar;
   Len : Integer;
   Len : Integer;
 
 
 begin
 begin
@@ -405,9 +401,9 @@ end;
 function TCSSScanner.DoMultiLineComment : TCSSToken;
 function TCSSScanner.DoMultiLineComment : TCSSToken;
 
 
 Var
 Var
-  TokenStart : PAnsiChar;
+  TokenStart : PCSSChar;
   Len,OLen : Integer;
   Len,OLen : Integer;
-  PrevToken : AnsiChar;
+  PrevToken : TCSSChar;
 
 
 begin
 begin
   Inc(TokenStr);
   Inc(TokenStr);
@@ -485,28 +481,20 @@ begin
   Result:=WideChar(StrToInt('$'+S));
   Result:=WideChar(StrToInt('$'+S));
 end;
 end;
 
 
-procedure TCSSScanner.SetReturnComments(AValue: Boolean);
+procedure TCSSScanner.SetOption(anOption: TCSSScannerOption; const AValue: Boolean
+  );
 begin
 begin
   if AValue then
   if AValue then
-    Include(FOptions,csoReturnComments)
+    Include(FOptions,anOption)
   else
   else
-    Exclude(FOptions,csoReturnComments)
+    Exclude(FOptions,anOption);
 end;
 end;
 
 
-procedure TCSSScanner.SetReturnWhiteSpace(AValue: Boolean);
-begin
-  if AValue then
-    Include(FOptions,csoReturnWhiteSpace)
-  else
-    Exclude(FOptions,csoReturnWhiteSpace)
-end;
-
-
 function TCSSScanner.DoStringLiteral: TCSSToken;
 function TCSSScanner.DoStringLiteral: TCSSToken;
 
 
 Var
 Var
-  Delim : AnsiChar;
-  TokenStart : PAnsiChar;
+  Delim : TCSSChar;
+  TokenStart : PCSSChar;
   Len,OLen: Integer;
   Len,OLen: Integer;
   S : TCSSString;
   S : TCSSString;
 
 
@@ -541,7 +529,7 @@ begin
         Move(TokenStart^, FCurTokenString[OLen + 1], Len);
         Move(TokenStart^, FCurTokenString[OLen + 1], Len);
       Move(S[1],FCurTokenString[OLen + Len+1],Length(S));
       Move(S[1],FCurTokenString[OLen + Len+1],Length(S));
       Inc(OLen, Len+Length(S));
       Inc(OLen, Len+Length(S));
-      // Next AnsiChar
+      // Next char
       // Inc(TokenStr);
       // Inc(TokenStr);
       TokenStart := TokenStr+1;
       TokenStart := TokenStr+1;
       end;
       end;
@@ -559,58 +547,105 @@ begin
   Result := ctkSTRING;
   Result := ctkSTRING;
 end;
 end;
 
 
-function TCSSScanner.DoNumericLiteral :TCSSToken;
+function TCSSScanner.DoStringEscape: TCSSToken;
+var
+  TokenStart: PCSSChar;
+  Len: Integer;
+begin
+  Inc(TokenStr); // skip \
+  TokenStart := TokenStr;
+  while TokenStr[0] in Num do
+    inc(TokenStr);
+  Len:=TokenStr-TokenStart;
+  Setlength(FCurTokenString, Len);
+  if (Len>0) then
+    Move(TokenStart^,FCurTokenString[1],Len);
+  Result:=ctkString;
+  FCurTokenString:=TCSSChar(StrToInt(FCurTokenString));
+end;
+
+function TCSSScanner.DoNumericLiteral: TCSSToken;
+// number: 1, 0.2, .3, 4.01, 0.0, +0.0, -0.0, .50, 2e3, -6.7E-2
+const
+  NumEnd = [#0..#31,' ',';','{','}',
+    ',',
+    ')', // e.g. calc(3*4)
+    '*','/', // e.g. 3*4, note that + and - require whitespace
+    'a'..'z','A'..'Z' // e.g. 3px
+    ];
+
+  procedure Skip;
+  begin
+    while not (TokenStr^ in NumEnd) do inc(TokenStr);
+    Result:=ctkUNKNOWN;
+  end;
 
 
 Var
 Var
-  TokenStart : PAnsiChar;
+  TokenStart : PCSSChar;
   Len : Integer;
   Len : Integer;
-  isEscape : Boolean;
+  HasNumber: Boolean;
 
 
 begin
 begin
   Result := ctkINTEGER;
   Result := ctkINTEGER;
-  isEscape:=TokenStr[0]='\';
-  if IsEscape then
-    Inc(TokenStr);
   TokenStart := TokenStr;
   TokenStart := TokenStr;
-  while true do
+  case TokenStr^ of
+  '-': inc(TokenStr);
+  '+': if csoReturnWhiteSpace in Options then inc(TokenStr);
+  end;
+  HasNumber:=false;
+  if TokenStr^ in Num then
     begin
     begin
-    Inc(TokenStr);
-    case TokenStr[0] of
-      '.':
-        if IsEscape then
-          Break
-        else
-          begin
-            Result := ctkFLOAT;
-            if TokenStr[1] in ['0'..'9'] then
-            begin
-              Inc(TokenStr);
-              repeat
-                Inc(TokenStr);
-              until not (TokenStr[0] in ['0'..'9']);
-            end;
-            break;
-          end;
-      '0'..'9': ;
-      else
-        break;
+    // read significand
+    HasNumber:=true;
+    repeat
+      inc(TokenStr);
+    until not (TokenStr^ in Num);
     end;
     end;
-  end;
+  if TokenStr^='.' then
+    begin
+    // read fraction
+    inc(TokenStr);
+    if TokenStr^ in Num then
+      begin
+      Result := ctkFLOAT;
+      HasNumber:=true;
+      repeat
+        inc(TokenStr);
+      until not (TokenStr^ in Num)
+      end;
+    end;
+  if not HasNumber then
+    begin
+    Skip;
+    exit;
+    end;
+  if (TokenStr^ in ['e','E']) and not (TokenStr[1] in Alpha) then
+    begin
+    // read exponent
+    Result := ctkFLOAT;
+    inc(TokenStr);
+    if TokenStr^ in ['-','+'] then
+      inc(TokenStr);
+    if not (TokenStr^ in Num) then
+      begin
+      Skip;
+      exit;
+      end;
+    repeat
+      inc(TokenStr);
+    until not (TokenStr^ in Num)
+    end;
+
   Len:=TokenStr-TokenStart;
   Len:=TokenStr-TokenStart;
   Setlength(FCurTokenString, Len);
   Setlength(FCurTokenString, Len);
   if (Len>0) then
   if (Len>0) then
-  Move(TokenStart^,FCurTokenString[1],Len);
-  if IsEscape then
-    begin
-    Result:=ctkString;
-    FCurTokenString:=AnsiChar(StrToInt(FCurTokenString));
-    end;
+    Move(TokenStart^,FCurTokenString[1],Len);
 end;
 end;
 
 
 function TCSSScanner.DoHash :TCSSToken;
 function TCSSScanner.DoHash :TCSSToken;
 
 
 Var
 Var
-  TokenStart : PAnsiChar;
+  TokenStart : PCSSChar;
   Len : Integer;
   Len : Integer;
 
 
 begin
 begin
@@ -629,7 +664,7 @@ end;
 function TCSSScanner.EatBadURL: TCSSToken;
 function TCSSScanner.EatBadURL: TCSSToken;
 
 
 var
 var
-  TokenStart : PAnsiChar;
+  TokenStart : PCSSChar;
   C : AnsiChar;
   C : AnsiChar;
   len,oldlen : integer;
   len,oldlen : integer;
 
 
@@ -658,9 +693,9 @@ end;
 
 
 function TCSSScanner.DoUnicodeRange: TCSSTOKEN;
 function TCSSScanner.DoUnicodeRange: TCSSTOKEN;
 Var
 Var
-  TokenStart:PAnsiChar;
+  TokenStart: PCSSChar;
   Len : Integer;
   Len : Integer;
-  Tokens : Set of AnsiChar;
+  Tokens : Set of TCSSChar;
 
 
 begin
 begin
   Tokens:= ['A'..'F', 'a'..'f', '0'..'9', '-'];
   Tokens:= ['A'..'F', 'a'..'f', '0'..'9', '-'];
@@ -680,7 +715,7 @@ begin
 
 
 end;
 end;
 
 
-class function TCSSScanner.UnknownCharToStr(C: AnsiChar): TCSSString;
+class function TCSSScanner.UnknownCharToStr(C: TCSSChar): TCSSString;
 
 
 begin
 begin
   if C=#0 then
   if C=#0 then
@@ -694,7 +729,7 @@ end;
 function TCSSScanner.DoIdentifierLike : TCSSToken;
 function TCSSScanner.DoIdentifierLike : TCSSToken;
 
 
 Var
 Var
-  TokenStart:PAnsiChar;
+  TokenStart: PCSSChar;
   Len,oLen : Integer;
   Len,oLen : Integer;
   IsEscape,IsAt, IsPseudo, IsFunc : Boolean;
   IsEscape,IsAt, IsPseudo, IsFunc : Boolean;
 
 
@@ -744,7 +779,17 @@ begin
     Result:=ctkATKEYWORD
     Result:=ctkATKEYWORD
   else if CurTokenString='!important' then
   else if CurTokenString='!important' then
     Result:=ctkIMPORTANT
     Result:=ctkIMPORTANT
-  else if (CurtokenString='url(') then
+  else if CurTokenString='!' then
+    begin
+    if (TokenStr^=' ') and CompareMem(TokenStr+1,PChar('important'),9)
+    and (TokenStr[10] in [' ',';']) then
+      begin
+      inc(TokenStr,10);
+      FCurTokenString:='!important';
+      Result:=ctkIMPORTANT;
+      end;
+    end
+  else if (CurTokenString='url(') then
     begin
     begin
     Result:=ctkURL;
     Result:=ctkURL;
     If TokenStr[0] in ['"',''''] then
     If TokenStr[0] in ['"',''''] then
@@ -769,13 +814,13 @@ end;
 
 
 function TCSSScanner.DoInvalidChars: TCSSToken;
 function TCSSScanner.DoInvalidChars: TCSSToken;
 var
 var
-  TokenStart: PAnsiChar;
+  TokenStart: PCSSChar;
   Len: SizeUInt;
   Len: SizeUInt;
 begin
 begin
-  Result:=ctkINVALID;
+  Result:=ctkUNKNOWN;
   TokenStart := TokenStr;
   TokenStart := TokenStr;
   repeat
   repeat
-    writeln('TCSSScanner.DoInvalidChars ',hexstr(ord(TokenStr^),2));
+    //writeln('TCSSScanner.DoInvalidChars ',hexstr(ord(TokenStr^),2));
     Inc(TokenStr);
     Inc(TokenStr);
   until (TokenStr[0] in [#0,#9,#10,#13,#32..#127]);
   until (TokenStr[0] in [#0,#9,#10,#13,#32..#127]);
   Len:=TokenStr-TokenStart;
   Len:=TokenStr-TokenStart;
@@ -792,7 +837,7 @@ var
 begin
 begin
   Repeat
   Repeat
     Result:=DoFetchToken;
     Result:=DoFetchToken;
-    if (Result=ctkINVALID) and IsUTF8BOM then
+    if (Result=ctkUNKNOWN) and IsUTF8BOM then
       CanStop:=false
       CanStop:=false
     else
     else
       CanStop:=(Not (Result in [ctkComment,ctkWhiteSpace]))
       CanStop:=(Not (Result in [ctkComment,ctkWhiteSpace]))
@@ -843,35 +888,35 @@ begin
   //CurPos:=TokenStr;
   //CurPos:=TokenStr;
   FCurTokenString := '';
   FCurTokenString := '';
   case TokenStr[0] of
   case TokenStr[0] of
-    #0:         // Empty line
+    #0:         // EOL
       begin
       begin
       FetchLine;
       FetchLine;
       Result := ctkWhitespace;
       Result := ctkWhitespace;
       end;
       end;
     '''','"':
     '''','"':
-       Result:=DoStringLiteral;
+      Result:=DoStringLiteral;
     '/' :
     '/' :
-       Result:=CommentDiv;
+      Result:=CommentDiv;
     #9, ' ':
     #9, ' ':
-       Result := DoWhiteSpace;
+      Result := DoWhiteSpace;
     '#':
     '#':
-       Result:=DoHash;
+      Result:=DoHash;
     '\':
     '\':
-       begin
-       if TokenStr[1] in ['0'..'9'] then
-         Result:=DoNumericLiteral
-       else
-         begin
-         if (TokenStr[1] in WhiteSpace) or (TokenStr[1]=#0) then
-           DoError(SErrUnknownCharacter ,[UnknownCharToStr(TokenStr[1])])
-         else
-           Result:=DoIdentifierLike
-         end;
-       end;
+      begin
+      if TokenStr[1] in ['0'..'9'] then
+        Result:=DoStringEscape
+      else
+        begin
+        if (TokenStr[1] in WhiteSpace) or (TokenStr[1]=#0) then
+          DoError(SErrUnknownCharacter ,[UnknownCharToStr(TokenStr[1])])
+        else
+          Result:=DoIdentifierLike
+        end;
+      end;
     '0'..'9':
     '0'..'9':
-       Result:=DoNumericLiteral;
+      Result:=DoNumericLiteral;
     '&': CharToken(ctkAnd);
     '&': CharToken(ctkAnd);
-    '{': CharToken( ctkLBRACE);
+    '{': CharToken(ctkLBRACE);
     '}': CharToken(ctkRBRACE);
     '}': CharToken(ctkRBRACE);
     '*': if TokenStr[1]='=' then
     '*': if TokenStr[1]='=' then
            TwoCharsToken(ctkSTAREQUAL)
            TwoCharsToken(ctkSTAREQUAL)
@@ -904,7 +949,7 @@ begin
     '@': Result:=DoIdentifierLike;
     '@': Result:=DoIdentifierLike;
     ':':
     ':':
       begin
       begin
-      if DisablePseudo then
+      if csoDisablePseudo in Options then
         CharToken(ctkCOLON)
         CharToken(ctkCOLON)
       else if (TokenStr[1]=':') then
       else if (TokenStr[1]=':') then
         begin
         begin
@@ -920,8 +965,10 @@ begin
       end;
       end;
     '.':
     '.':
       begin
       begin
-      if (TokenStr[1] in AlNum) then
-        Result:=Self.DoIdentifierLike
+      if TokenStr[1] in Num then
+        Result:=DoNumericLiteral  // e.g. .1 = 0.1
+      else if TokenStr[1] in Alpha then
+        Result:=DoIdentifierLike
       else
       else
         CharToken(ctkDOT);
         CharToken(ctkDOT);
       end;
       end;
@@ -945,16 +992,17 @@ begin
       '0'..'9':
       '0'..'9':
         Result:=DoNumericLiteral;
         Result:=DoNumericLiteral;
       '.':
       '.':
-        if TokenStr[2] in ['0'..'9'] then
+        if TokenStr[2] in Num then
           Result:=DoNumericLiteral
           Result:=DoNumericLiteral
         else
         else
           CharToken(ctkMINUS);
           CharToken(ctkMINUS);
-      #9,' ',#0:
+      #9,#10,#13,' ',#0:
         CharToken(ctkMINUS);
         CharToken(ctkMINUS);
       else
       else
         Result:=DoIdentifierLike;
         Result:=DoIdentifierLike;
       end;
       end;
-    '+': CharToken(ctkPLUS);
+    '+':
+      CharToken(ctkPLUS);
     '%': CharToken(ctkPERCENTAGE);
     '%': CharToken(ctkPERCENTAGE);
     '_','!',
     '_','!',
     'a'..'z',
     'a'..'z',
@@ -966,12 +1014,8 @@ begin
          Result:=DoIdentifierLike;
          Result:=DoIdentifierLike;
        end;
        end;
   else
   else
-    writeln('TCSSScanner.DoFetchToken ',Ord(TokenStr[0]));
-    If Ord(TokenStr[0])>127 then
-      Result:=DoInvalidChars
-    else
-      DoError(SErrUnknownCharacter ,['"'+TokenStr[0]+'"']);
-
+    //writeln('TCSSScanner.DoFetchToken ',Ord(TokenStr[0]));
+    Result:=DoInvalidChars;
   end; // Case
   end; // Case
 end;
 end;
 
 
@@ -995,17 +1039,12 @@ begin
   if (TokenStr=Nil) or (Length(CurLine)=0) then
   if (TokenStr=Nil) or (Length(CurLine)=0) then
     Result:=0
     Result:=0
   else
   else
-    Result := TokenStr - PAnsiChar(CurLine);
-end;
-
-function TCSSScanner.GetReturnComments: Boolean;
-begin
-  Result:=(csoReturnComments in FOptions);
+    Result := TokenStr - PCSSChar(CurLine);
 end;
 end;
 
 
-function TCSSScanner.GetReturnWhiteSpace: Boolean;
+function TCSSScanner.GetOption(anOption: TCSSScannerOption): Boolean;
 begin
 begin
-  Result:=(csoReturnWhiteSpace in FOptions);
+  Result:=anOption in Options;
 end;
 end;
 
 
 { TStreamLineReader }
 { TStreamLineReader }

+ 97 - 13
packages/fcl-css/src/fpcsstree.pp

@@ -18,7 +18,6 @@ unit fpCSSTree;
 {$ENDIF FPC_DOTTEDUNITS}
 {$ENDIF FPC_DOTTEDUNITS}
 
 
 {$mode ObjFPC}{$H+}
 {$mode ObjFPC}{$H+}
-{$codepage utf8}
 
 
 interface
 interface
 
 
@@ -29,12 +28,94 @@ uses Contnrs, RtlConsts, SysUtils, Classes, Math;
 {$ENDIF FPC_DOTTEDUNITS}
 {$ENDIF FPC_DOTTEDUNITS}
 
 
 
 
+const
+  CSSFormatSettings: TFormatSettings = (
+    CurrencyFormat: 1;
+    NegCurrFormat: 5;
+    ThousandSeparator: ',';
+    DecimalSeparator: '.';
+    CurrencyDecimals: 2;
+    DateSeparator: '-';
+    TimeSeparator: ':';
+    ListSeparator: ',';
+    CurrencyString: '$';
+    ShortDateFormat: 'd/m/y';
+    LongDateFormat: 'dd" "mmmm" "yyyy';
+    TimeAMString: 'AM';
+    TimePMString: 'PM';
+    ShortTimeFormat: 'hh:nn';
+    LongTimeFormat: 'hh:nn:ss';
+    ShortMonthNames: ('Jan','Feb','Mar','Apr','May','Jun',
+                      'Jul','Aug','Sep','Oct','Nov','Dec');
+    LongMonthNames: ('January','February','March','April','May','June',
+                     'July','August','September','October','November','December');
+    ShortDayNames: ('Sun','Mon','Tue','Wed','Thu','Fri','Sat');
+    LongDayNames:  ('Sunday','Monday','Tuesday','Wednesday','Thursday','Friday','Saturday');
+    TwoDigitYearCenturyWindow: 50;
+  );
+
 Type
 Type
   ECSSException = class(Exception);
   ECSSException = class(Exception);
 
 
-  TCSSString = UTF8String;
-  TCSSStringDynArray = array of TCSSString;
-  TCSSUnits = (cuNONE, cuPX,cuPERCENT,cuREM,cuEM,cuPT,cuFR,cuVW,cuVH,cuDEG);
+  {$IF FPC_FULLVERSION>30300}
+  TCSSChar = Char;
+  TCSSString = String; // can be AnsiString or UnicodeString
+  {$ELSE}
+  TCSSChar = Char;
+  TCSSString = String;
+  {$ENDIF}
+  PCSSChar = ^TCSSChar;
+  TCSSStringArray = array of TCSSString;
+
+  TCSSUnit = (
+    cuNONE,// no unit, only allowed for 0
+    cuPX,  // pixels
+    cuCM,  // centimeters
+    cuMM,  // milimeters
+    cuQ,   // quarter-milimeters
+    cuIN,  // inches
+    cuPC,  // picas
+    cuPT,  // points
+    cuPERCENT, // percentage %
+    cuEM,  // relative to element's font-size
+    cuREM, // relative to parent's font-size
+    cuVW,  // relative to viewport's width
+    cuVH,  // relative to viewport's height
+    cuFR,  // fraction of flex space
+    cuDEG, // degrees, full circle is 360deg
+    cuGRAD,// gradians, full circle is 400grad
+    cuRAD, // radians, full circle is (2*pi)rad
+    cuTURN // turns, full circle is 1turn
+    );
+  TCSSUnits = set of TCSSUnit;
+const
+  cuAllAbsoluteLengths = [cuPX,cuCM,cuMM,cuQ,cuIN,cuPC,cuPT];
+  cuAllRelativeFontSize = [cuEM,cuREM];
+  cuAllLengths = cuAllAbsoluteLengths+cuAllRelativeFontSize;
+  cuAllAngles = [cuDEG,cuGRAD,cuRAD,cuTURN];
+
+  CSSUnitNames: array[TCSSUnit] of TCSSString = (
+    '',    // no unit
+    'px',  // pixels
+    'cm',  // centimeters
+    'mm',  // milimeters
+    'Q',   // quarter-milimeters
+    'in',  // inches
+    'pc',  // picas
+    'pt',  // points
+    '%',   // percentage
+    'em',  // elements font-size
+    'rem', // relative-em
+    'vw',  // viewport-width
+    'vh',  // viewport-height
+    'fr',  // fraction
+    'deg', // degrees
+    'grad',// gradians
+    'rad', // radians
+    'turn' // turns
+    );
+
+type
   TCSSType = (
   TCSSType = (
     csstUnknown,
     csstUnknown,
     csstInteger, csstString, csstFloat,
     csstInteger, csstString, csstFloat,
@@ -145,7 +226,7 @@ Type
   TCSSIntegerElement = class(TCSSElement)
   TCSSIntegerElement = class(TCSSElement)
   private
   private
     FIsEscaped: Boolean;
     FIsEscaped: Boolean;
-    FUnits: TCSSUnits;
+    FUnits: TCSSUnit;
     FValue: Integer;
     FValue: Integer;
   protected
   protected
     function GetAsString(aFormat : Boolean; const aIndent : TCSSString): TCSSString; override;
     function GetAsString(aFormat : Boolean; const aIndent : TCSSString): TCSSString; override;
@@ -154,7 +235,7 @@ Type
     function Equals(Obj: TObject): boolean; override;
     function Equals(Obj: TObject): boolean; override;
     Property Value : Integer Read FValue Write FValue;
     Property Value : Integer Read FValue Write FValue;
     Property IsEscaped : Boolean Read FIsEscaped Write FIsEscaped;
     Property IsEscaped : Boolean Read FIsEscaped Write FIsEscaped;
-    Property Units : TCSSUnits Read FUnits Write FUnits;
+    Property Units : TCSSUnit Read FUnits Write FUnits;
   end;
   end;
   TCSSIntegerElementClass = class of TCSSIntegerElement;
   TCSSIntegerElementClass = class of TCSSIntegerElement;
 
 
@@ -162,7 +243,7 @@ Type
 
 
   TCSSFloatElement = class(TCSSElement)
   TCSSFloatElement = class(TCSSElement)
   private
   private
-    FUnits: TCSSUnits;
+    FUnits: TCSSUnit;
     FValue: Double;
     FValue: Double;
   protected
   protected
     function GetAsString(aFormat : Boolean; const aIndent : TCSSString): TCSSString;override;
     function GetAsString(aFormat : Boolean; const aIndent : TCSSString): TCSSString;override;
@@ -170,7 +251,7 @@ Type
     Class function CSSType : TCSSType; override;
     Class function CSSType : TCSSType; override;
     function Equals(Obj: TObject): boolean; override;
     function Equals(Obj: TObject): boolean; override;
     Property Value : Double Read FValue Write FValue;
     Property Value : Double Read FValue Write FValue;
-    Property Units : TCSSUnits Read FUnits Write FUnits;
+    Property Units : TCSSUnit Read FUnits Write FUnits;
   end;
   end;
   TCSSFloatElementClass = class of TCSSFloatElement;
   TCSSFloatElementClass = class of TCSSFloatElement;
 
 
@@ -448,14 +529,14 @@ Function StringToCSSString(const S : TCSSString) : TCSSString;
 // Escapes non-identifier characters C to \C
 // Escapes non-identifier characters C to \C
 Function StringToIdentifier(const S : TCSSString) : TCSSString;
 Function StringToIdentifier(const S : TCSSString) : TCSSString;
 
 
+function FloatToCSSStr(const f: double): string;
+
 Function GetCSSObj(El: TCSSElement): TCSSString;
 Function GetCSSObj(El: TCSSElement): TCSSString;
 Function GetCSSPath(El: TCSSElement): TCSSString;
 Function GetCSSPath(El: TCSSElement): TCSSString;
 
 
 Function CSSElementListEquals(ListA, ListB: TCSSElementList): boolean;
 Function CSSElementListEquals(ListA, ListB: TCSSElementList): boolean;
 
 
 Const
 Const
-  CSSUnitNames : Array[TCSSUnits] of TCSSString =
-        ('','px','%','rem','em','pt','fr','vw','vh','deg');
   UnaryOperators : Array[TCSSUnaryOperation] of TCSSString =
   UnaryOperators : Array[TCSSUnaryOperation] of TCSSString =
         ('::','-','+','/','>','~');
         ('::','-','+','/','>','~');
   BinaryOperators : Array[TCSSBinaryOperation] of TCSSString =
   BinaryOperators : Array[TCSSBinaryOperation] of TCSSString =
@@ -566,6 +647,11 @@ begin
   SetLength(Result,iOut);
   SetLength(Result,iOut);
 end;
 end;
 
 
+function FloatToCSSStr(const f: double): string;
+begin
+  Result:=FloatToStr(f,CSSFormatSettings);
+end;
+
 function GetCSSObj(El: TCSSElement): TCSSString;
 function GetCSSObj(El: TCSSElement): TCSSString;
 begin
 begin
   if El=nil then
   if El=nil then
@@ -1075,9 +1161,7 @@ end;
 function TCSSFloatElement.GetAsString(aFormat: Boolean;
 function TCSSFloatElement.GetAsString(aFormat: Boolean;
   const aIndent: TCSSString): TCSSString;
   const aIndent: TCSSString): TCSSString;
 begin
 begin
-  Str(Value:5:2,Result);
-  Result:=TrimLeft(Result); // Space for positive numbers
-  Result:=Result+CSSUnitNames[Units];
+  Result:=FloatToCSSStr(Value)+CSSUnitNames[Units];
   if aFormat then
   if aFormat then
     Result:=aIndent+Result;
     Result:=aIndent+Result;
 end;
 end;

+ 9 - 9
packages/fcl-css/tests/tccssparser.pp

@@ -43,7 +43,7 @@ type
     procedure Parse;
     procedure Parse;
     procedure Parse(Const aSource : String);
     procedure Parse(Const aSource : String);
     function ParseRule(Const aSource : String) : TCSSRuleElement;
     function ParseRule(Const aSource : String) : TCSSRuleElement;
-    procedure AssertEquals(AMessage: String; AExpected, AActual: TCSSUnits);   overload;
+    procedure AssertEquals(AMessage: String; AExpected, AActual: TCSSUnit);   overload;
     procedure AssertEquals(AMessage: String; AExpected, AActual: TCSSBinaryOperation);   overload;
     procedure AssertEquals(AMessage: String; AExpected, AActual: TCSSBinaryOperation);   overload;
     Function CheckClass(Const aMsg : String; aExpectedClass : TCSSElementClass; aActual : TCSSElement) : TCSSElement;
     Function CheckClass(Const aMsg : String; aExpectedClass : TCSSElementClass; aActual : TCSSElement) : TCSSElement;
     Function CheckDeclaration(aRule : TCSSRuleElement; aIndex : Integer) : TCSSDeclarationElement;
     Function CheckDeclaration(aRule : TCSSRuleElement; aIndex : Integer) : TCSSDeclarationElement;
@@ -54,7 +54,7 @@ type
     function CheckList(aList: TCSSListElement; aIndex: Integer; const aName: String): TCSSElement;
     function CheckList(aList: TCSSListElement; aIndex: Integer; const aName: String): TCSSElement;
     function CheckLiteral(Msg: String; aEl: TCSSelement; aValue: String) : TCSSStringElement; overload;
     function CheckLiteral(Msg: String; aEl: TCSSelement; aValue: String) : TCSSStringElement; overload;
     function CheckLiteral(Msg: String; aEl: TCSSelement; aValue: Integer) : TCSSIntegerElement;  overload;
     function CheckLiteral(Msg: String; aEl: TCSSelement; aValue: Integer) : TCSSIntegerElement;  overload;
-    function CheckLiteral(Msg: String; aEl: TCSSelement; aValue: Integer; AUnits : TCSSUnits) : TCSSIntegerElement;  overload;
+    function CheckLiteral(Msg: String; aEl: TCSSelement; aValue: Integer; AUnits : TCSSUnit) : TCSSIntegerElement;  overload;
     Function GetCalArg(aCall : TCSSCallElement; aIndex : Integer) : TCSSElement;
     Function GetCalArg(aCall : TCSSCallElement; aIndex : Integer) : TCSSElement;
   Public
   Public
     Property ParseResult : TCSSElement read FParseResult;
     Property ParseResult : TCSSElement read FParseResult;
@@ -156,7 +156,7 @@ end;
 procedure TTestCSSFilesParser.SetUp;
 procedure TTestCSSFilesParser.SetUp;
 begin
 begin
   inherited SetUp;
   inherited SetUp;
-  With TMemIniFile.Create(ChangeFileExt(Paramstr(0),RTLString('.ini'))) do
+  With TMemIniFile.Create(ChangeFileExt(Paramstr(0),TCSSString('.ini'))) do
     try
     try
       TestDir:=ReadString('CSS','SourceDir','css');
       TestDir:=ReadString('CSS','SourceDir','css');
     finally
     finally
@@ -635,10 +635,10 @@ procedure TTestCSSParser.TestOneDeclarationIntValue;
 var
 var
   R : TCSSRuleElement;
   R : TCSSRuleElement;
   D : TCSSDeclarationElement;
   D : TCSSDeclarationElement;
-  U : TCSSUnits;
+  U : TCSSUnit;
 
 
 begin
 begin
-  For U in TCSSUnits do
+  For U in TCSSUnit do
     begin
     begin
     R:=ParseRule('{ a : 1'+CSSUnitNames[U]+'; }');
     R:=ParseRule('{ a : 1'+CSSUnitNames[U]+'; }');
     AssertEquals('selector count',0,R.SelectorCount);
     AssertEquals('selector count',0,R.SelectorCount);
@@ -902,7 +902,7 @@ begin
     Result:=FirstRule;
     Result:=FirstRule;
 end;
 end;
 
 
-procedure TTestBaseCSSParser.AssertEquals(AMessage : String; AExpected, AActual: TCSSUnits);
+procedure TTestBaseCSSParser.AssertEquals(AMessage : String; AExpected, AActual: TCSSUnit);
 
 
 Var
 Var
   S,EN1,EN2 : String;
   S,EN1,EN2 : String;
@@ -910,8 +910,8 @@ Var
 begin
 begin
   If (AActual<>AExpected) then
   If (AActual<>AExpected) then
     begin
     begin
-    EN1:=GetEnumName(TypeINfo(TCSSUnits),Ord(AExpected));
-    EN2:=GetEnumName(TypeINfo(TCSSUnits),Ord(AActual));
+    EN1:=GetEnumName(TypeINfo(TCSSUnit),Ord(AExpected));
+    EN2:=GetEnumName(TypeINfo(TCSSUnit),Ord(AActual));
     S:=Format('%s : %s <> %s',[AMessage,EN1,EN2]);
     S:=Format('%s : %s <> %s',[AMessage,EN1,EN2]);
     Fail(S);
     Fail(S);
     end;
     end;
@@ -1007,7 +1007,7 @@ begin
   AssertEquals(Msg+': Value ',aValue,Result.Value);
   AssertEquals(Msg+': Value ',aValue,Result.Value);
 end;
 end;
 
 
-function TTestBaseCSSParser.CheckLiteral(Msg: String; aEl: TCSSelement; aValue: Integer; AUnits: TCSSUnits): TCSSIntegerElement;
+function TTestBaseCSSParser.CheckLiteral(Msg: String; aEl: TCSSelement; aValue: Integer; AUnits: TCSSUnit): TCSSIntegerElement;
 begin
 begin
   Result:=CheckLiteral(Msg,aEl,aValue);
   Result:=CheckLiteral(Msg,aEl,aValue);
   AssertEquals('Units',aUnits,Result.Units);
   AssertEquals('Units',aUnits,Result.Units);

Fișier diff suprimat deoarece este prea mare
+ 433 - 154
packages/fcl-css/tests/tccssresolver.pp


+ 12 - 13
packages/fcl-css/tests/tccssscanner.pp

@@ -58,7 +58,6 @@ type
     procedure DoTestFloat(F: Double);
     procedure DoTestFloat(F: Double);
     procedure DoTestFloat(F: Double; S: String);
     procedure DoTestFloat(F: Double; S: String);
     procedure DoTestString(S: String);
     procedure DoTestString(S: String);
-    procedure TestErrorSource;
   protected
   protected
     Function CreateScanner(AInput : String) : TCSSScanner;
     Function CreateScanner(AInput : String) : TCSSScanner;
     procedure FreeScanner;
     procedure FreeScanner;
@@ -224,17 +223,6 @@ begin
   end;
   end;
 end;
 end;
 
 
-procedure TTestCSSScanner.TestErrorSource;
-
-begin
-  CreateScanner(FErrorSource);
-  try
-    While (FScanner.FetchToken<>ctkEOF) do ;
-  finally
-    FreeScanner;
-  end;
-end;
-
 procedure TTestCSSScanner.TestEmpty;
 procedure TTestCSSScanner.TestEmpty;
 
 
 Var
 Var
@@ -573,9 +561,20 @@ end;
 
 
 
 
 procedure TTestCSSScanner.TestJUNK;
 procedure TTestCSSScanner.TestJUNK;
+var
+  HasUnknown: Boolean;
 begin
 begin
   FErrorSource:='?';
   FErrorSource:='?';
-  AssertException('Exception',ECSSScanner, @TestErrorSource);
+  HasUnknown:=false;
+  CreateScanner(FErrorSource);
+  try
+    While (FScanner.FetchToken<>ctkEOF) do
+      if FScanner.CurToken=ctkUNKNOWN then HasUnknown:=true;
+  finally
+    FreeScanner;
+  end;
+  if not HasUnknown then
+    Fail('missing unknown');
 end;
 end;
 
 
 procedure TTestCSSScanner.TestSQUARED;
 procedure TTestCSSScanner.TestSQUARED;

Unele fișiere nu au fost afișate deoarece prea multe fișiere au fost modificate în acest diff