DEADSOFTWARE

FlexUI: simple styling system (yay, no more hardcoded colors!)
[d2df-sdl.git] / src / shared / xparser.pas
index 7d7a9ed5d3bbdeb237376833203c68374a90986c..5332f0afe7c4af022019799a6d2eb4ce796f40c0 100644 (file)
@@ -1,4 +1,5 @@
-(* Copyright (C)  DooM 2D:Forever Developers
+(* coded by Ketmar // Invisible Vector <ketmar@ketmar.no-ip.org>
+ * Understanding is not required. Only obedience.
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -11,7 +12,7 @@
  * GNU General Public License for more details.
  *
  * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
  *)
 {$INCLUDE a_modes.inc}
 {.$DEFINE XPARSER_DEBUG}
@@ -20,11 +21,22 @@ unit xparser;
 interface
 
 uses
-  Classes{$IFDEF USE_MEMPOOL}, mempool{$ENDIF};
+  SysUtils, Classes{$IFDEF USE_MEMPOOL}, mempool{$ENDIF};
 
 
 // ////////////////////////////////////////////////////////////////////////// //
 type
+  TTextParser = class;
+
+  TParserException = class(Exception)
+  public
+    tokLine, tokCol: Integer;
+
+  public
+    constructor Create (pr: TTextParser; const amsg: AnsiString);
+    constructor CreateFmt (pr: TTextParser; const afmt: AnsiString; const args: array of const);
+  end;
+
   TTextParser = class{$IFDEF USE_MEMPOOL}(TPoolObject){$ENDIF}
   public
     const
@@ -53,6 +65,7 @@ type
         SignedNumbers, // allow signed numbers; otherwise sign will be TTDelim
         DollarIsId, // allow dollar in identifiers; otherwise dollar will be TTDelim
         DotIsId, // allow dot in identifiers; otherwise dot will be TTDelim
+        DashIsId, // '-' can be part of identifier (but identifier cannot start with '-')
         PascalComments // allow `{}` pascal comments
       );
       TOptions = set of TOption;
@@ -81,6 +94,9 @@ type
     constructor Create (aopts: TOptions=[TOption.SignedNumbers]);
     destructor Destroy (); override;
 
+    procedure error (const amsg: AnsiString); noreturn;
+    procedure errorfmt (const afmt: AnsiString; const args: array of const); noreturn;
+
     function isEOF (): Boolean; inline;
 
     function skipChar (): Boolean; // returns `false` on eof
@@ -92,14 +108,18 @@ type
     function skipToken1 (): Boolean;
     {$ENDIF}
 
+    function isIdOrStr (): Boolean; inline;
+
     function expectId (): AnsiString;
-    procedure expectId (const aid: AnsiString);
-    function eatId (const aid: AnsiString): Boolean;
+    procedure expectId (const aid: AnsiString; caseSens: Boolean=true);
+    function eatId (const aid: AnsiString; caseSens: Boolean=true): Boolean;
+    function eatIdOrStr (const aid: AnsiString; caseSens: Boolean=true): Boolean;
+    function eatIdOrStrCI (const aid: AnsiString): Boolean; inline;
 
     function expectStr (allowEmpty: Boolean=false): AnsiString;
     function expectInt (): Integer;
 
-    function expectStrOrId (allowEmpty: Boolean=false): AnsiString;
+    function expectIdOrStr (allowEmpty: Boolean=false): AnsiString;
 
     procedure expectTT (ttype: Integer);
     function eatTT (ttype: Integer): Boolean;
@@ -235,7 +255,21 @@ type
 implementation
 
 uses
-  SysUtils, utils;
+  utils;
+
+
+// ////////////////////////////////////////////////////////////////////////// //
+constructor TParserException.Create (pr: TTextParser; const amsg: AnsiString);
+begin
+  if (pr <> nil) then begin tokLine := pr.tokLine; tokCol := pr.tokCol; end;
+  inherited Create(amsg);
+end;
+
+constructor TParserException.CreateFmt (pr: TTextParser; const afmt: AnsiString; const args: array of const);
+begin
+  if (pr <> nil) then begin tokLine := pr.tokLine; tokCol := pr.tokCol; end;
+  inherited Create(formatstrf(afmt, args));
+end;
 
 
 // ////////////////////////////////////////////////////////////////////////// //
@@ -261,7 +295,19 @@ begin
 end;
 
 
-function TTextParser.isEOF (): Boolean; inline; begin result := (mCurChar = #0); end;
+procedure TTextParser.error (const amsg: AnsiString); noreturn;
+begin
+  raise TParserException.Create(self, amsg);
+end;
+
+
+procedure TTextParser.errorfmt (const afmt: AnsiString; const args: array of const); noreturn;
+begin
+  raise TParserException.CreateFmt(self, afmt, args);
+end;
+
+
+function TTextParser.isEOF (): Boolean; inline; begin {result := (mCurChar = #0);} result := (mTokType = TTEOF); end;
 
 
 procedure TTextParser.warmup ();
@@ -294,26 +340,26 @@ function TTextParser.skipBlanks (): Boolean;
 var
   level: Integer;
 begin
-  while not isEOF do
+  while (mCurChar <> #0) do
   begin
-    if (curChar = '/') then
+    if (mCurChar = '/') then
     begin
       // single-line comment
-      if (nextChar = '/') then
+      if (mNextChar = '/') then
       begin
-        while not isEOF and (curChar <> #10) do skipChar();
+        while (mCurChar <> #0) and (mCurChar <> #10) do skipChar();
         skipChar(); // skip EOL
         continue;
       end;
       // multline comment
-      if (nextChar = '*') then
+      if (mNextChar = '*') then
       begin
         // skip comment start
         skipChar();
         skipChar();
-        while not isEOF do
+        while (mCurChar <> #0) do
         begin
-          if (curChar = '*') and (nextChar = '/') then
+          if (mCurChar = '*') and (mNextChar = '/') then
           begin
             // skip comment end
             skipChar();
@@ -325,15 +371,15 @@ begin
         continue;
       end;
       // nesting multline comment
-      if (nextChar = '+') then
+      if (mNextChar = '+') then
       begin
         // skip comment start
         skipChar();
         skipChar();
         level := 1;
-        while not isEOF do
+        while (mCurChar <> #0) do
         begin
-          if (curChar = '+') and (nextChar = '/') then
+          if (mCurChar = '+') and (mNextChar = '/') then
           begin
             // skip comment end
             skipChar();
@@ -342,7 +388,7 @@ begin
             if (level = 0) then break;
             continue;
           end;
-          if (curChar = '/') and (nextChar = '+') then
+          if (mCurChar = '/') and (mNextChar = '+') then
           begin
             // skip comment start
             skipChar();
@@ -355,14 +401,14 @@ begin
         continue;
       end;
     end
-    else if (curChar = '(') and (nextChar = '*') then
+    else if (mCurChar = '(') and (mNextChar = '*') then
     begin
       // pascal comment; skip comment start
       skipChar();
       skipChar();
-      while not isEOF do
+      while (mCurChar <> #0) do
       begin
-        if (curChar = '*') and (nextChar = ')') then
+        if (mCurChar = '*') and (mNextChar = ')') then
         begin
           // skip comment end
           skipChar();
@@ -373,13 +419,13 @@ begin
       end;
       continue;
     end
-    else if (curChar = '{') and (TOption.PascalComments in mOptions) then
+    else if (mCurChar = '{') and (TOption.PascalComments in mOptions) then
     begin
       // pascal comment; skip comment start
       skipChar();
-      while not isEOF do
+      while (mCurChar <> #0) do
       begin
-        if (curChar = '}') then
+        if (mCurChar = '}') then
         begin
           // skip comment end
           skipChar();
@@ -389,10 +435,10 @@ begin
       end;
       continue;
     end;
-    if (curChar > ' ') then break;
+    if (mCurChar > ' ') then break;
     skipChar(); // skip blank
   end;
-  result := not isEOF;
+  result := (mCurChar <> #0);
 end;
 
 
@@ -416,11 +462,11 @@ function TTextParser.skipToken (): Boolean;
   begin
     if (TOption.SignedNumbers in mOptions) then
     begin
-      if (curChar = '+') or (curChar = '-') then
+      if (mCurChar = '+') or (mCurChar = '-') then
       begin
-        neg := (curChar = '-');
+        neg := (mCurChar = '-');
         skipChar();
-        if (curChar < '0') or (curChar > '9') then
+        if (mCurChar < '0') or (mCurChar > '9') then
         begin
           mTokType := TTDelim;
           if (neg) then mTokChar := '-' else mTokChar := '+';
@@ -428,9 +474,9 @@ function TTextParser.skipToken (): Boolean;
         end;
       end;
     end;
-    if (curChar = '0') then
+    if (mCurChar = '0') then
     begin
-      case nextChar of
+      case mNextChar of
         'b','B': base := 2;
         'o','O': base := 8;
         'd','D': base := 10;
@@ -445,12 +491,12 @@ function TTextParser.skipToken (): Boolean;
     end;
     // default base
     if (base < 0) then base := 10;
-    if (digitInBase(curChar, base) < 0) then raise Exception.Create('invalid number');
+    if (digitInBase(mCurChar, base) < 0) then raise Exception.Create('invalid number');
     mTokType := TTInt;
     mTokInt := 0; // just in case
-    while not isEOF do
+    while (mCurChar <> #0) do
     begin
-      n := digitInBase(curChar, base);
+      n := digitInBase(mCurChar, base);
       if (n < 0) then break;
       n := mTokInt*10+n;
       if (n < 0) or (n < mTokInt) then raise Exception.Create('integer overflow');
@@ -458,10 +504,10 @@ function TTextParser.skipToken (): Boolean;
       skipChar();
     end;
     // check for valid number end
-    if not isEOF then
+    if (mCurChar <> #0) then
     begin
-      if (curChar = '.') then raise Exception.Create('floating numbers aren''t supported yet');
-      if (curChar = '_') or ((curChar >= 'A') and (curChar <= 'Z')) or ((curChar >= 'a') and (curChar <= 'z')) or (curChar >= #128) then
+      if (mCurChar = '.') then raise Exception.Create('floating numbers aren''t supported yet');
+      if (mCurChar = '_') or ((mCurChar >= 'A') and (mCurChar <= 'Z')) or ((mCurChar >= 'a') and (mCurChar <= 'z')) or (mCurChar >= #128) then
       begin
         raise Exception.Create('invalid number');
       end;
@@ -476,15 +522,15 @@ function TTextParser.skipToken (): Boolean;
   begin
     mTokType := TTStr;
     mTokStr := ''; // just in case
-    qch := curChar;
+    qch := mCurChar;
     skipChar(); // skip starting quote
-    while not isEOF do
+    while (mCurChar <> #0) do
     begin
       // escape
-      if (qch = '"') and (curChar = '\') then
+      if (qch = '"') and (mCurChar = '\') then
       begin
-        if (nextChar = #0) then raise Exception.Create('unterminated string escape');
-        ch := nextChar;
+        if (mNextChar = #0) then raise Exception.Create('unterminated string escape');
+        ch := mNextChar;
         // skip backslash and escape type
         skipChar();
         skipChar();
@@ -496,12 +542,12 @@ function TTextParser.skipToken (): Boolean;
           'e': mTokStr += #27;
           'x', 'X': // hex escape
             begin
-              n := digitInBase(curChar, 16);
+              n := digitInBase(mCurChar, 16);
               if (n < 0) then raise Exception.Create('invalid hexstr escape');
               skipChar();
-              if (digitInBase(curChar, 16) > 0) then
+              if (digitInBase(mCurChar, 16) > 0) then
               begin
-                n := n*16+digitInBase(curChar, 16);
+                n := n*16+digitInBase(mCurChar, 16);
                 skipChar();
               end;
               mTokStr += AnsiChar(n);
@@ -511,7 +557,7 @@ function TTextParser.skipToken (): Boolean;
         continue;
       end;
       // duplicate single quote (pascal style)
-      if (qch = '''') and (curChar = '''') and (nextChar = '''') then
+      if (qch = '''') and (mCurChar = '''') and (mNextChar = '''') then
       begin
         // skip both quotes
         skipChar();
@@ -519,12 +565,12 @@ function TTextParser.skipToken (): Boolean;
         mTokStr += '''';
         continue;
       end;
-      if (curChar = qch) then
+      if (mCurChar = qch) then
       begin
         skipChar(); // skip ending quote
         break;
       end;
-      mTokStr += curChar;
+      mTokStr += mCurChar;
       skipChar();
     end;
   end;
@@ -533,20 +579,21 @@ function TTextParser.skipToken (): Boolean;
   begin
     mTokType := TTId;
     mTokStr := ''; // just in case
-    while (curChar = '_') or ((curChar >= '0') and (curChar <= '9')) or
-          ((curChar >= 'A') and (curChar <= 'Z')) or
-          ((curChar >= 'a') and (curChar <= 'z')) or
-          (curChar >= #128) or
-          ((TOption.DollarIsId in mOptions) and (curChar = '$')) or
-          ((TOption.DotIsId in mOptions) and (curChar = '.') and (nextChar <> '.')) do
+    while (mCurChar = '_') or ((mCurChar >= '0') and (mCurChar <= '9')) or
+          ((mCurChar >= 'A') and (mCurChar <= 'Z')) or
+          ((mCurChar >= 'a') and (mCurChar <= 'z')) or
+          (mCurChar >= #128) or
+          ((TOption.DollarIsId in mOptions) and (mCurChar = '$')) or
+          ((TOption.DotIsId in mOptions) and (mCurChar = '.') and (mNextChar <> '.')) or
+          ((TOption.DashIsId in mOptions) and (mCurChar = '-')) do
     begin
-      mTokStr += curChar;
+      mTokStr += mCurChar;
       skipChar();
     end;
   end;
 
 begin
-  mTokType := TTEOF;
+  mTokType := TTNone;
   mTokStr := '';
   mTokChar := #0;
   mTokInt := 0;
@@ -554,6 +601,7 @@ begin
   if not skipBlanks() then
   begin
     result := false;
+    mTokType := TTEOF;
     mTokLine := mLine;
     mTokCol := mCol;
     exit;
@@ -565,22 +613,22 @@ begin
   result := true;
 
   // number?
-  if (TOption.SignedNumbers in mOptions) and ((curChar = '+') or (curChar = '-')) then begin parseInt(); exit; end;
-  if (curChar >= '0') and (curChar <= '9') then begin parseInt(); exit; end;
+  if (TOption.SignedNumbers in mOptions) and ((mCurChar = '+') or (mCurChar = '-')) then begin parseInt(); exit; end;
+  if (mCurChar >= '0') and (mCurChar <= '9') then begin parseInt(); exit; end;
 
   // string?
-  if (curChar = '"') or (curChar = '''') then begin parseString(); exit; end;
+  if (mCurChar = '"') or (mCurChar = '''') then begin parseString(); exit; end;
 
   // identifier?
-  if (curChar = '_') or ((curChar >= 'A') and (curChar <= 'Z')) or ((curChar >= 'a') and (curChar <= 'z')) or (curChar >= #128) then begin parseId(); exit; end;
-  if (TOption.DollarIsId in mOptions) and (curChar = '$') then begin parseId(); exit; end;
-  if (TOption.DotIsId in mOptions) and (curChar = '.') and (nextChar <> '.') then begin parseId(); exit; end;
+  if (mCurChar = '_') or ((mCurChar >= 'A') and (mCurChar <= 'Z')) or ((mCurChar >= 'a') and (mCurChar <= 'z')) or (mCurChar >= #128) then begin parseId(); exit; end;
+  if (TOption.DollarIsId in mOptions) and (mCurChar = '$') then begin parseId(); exit; end;
+  if (TOption.DotIsId in mOptions) and (mCurChar = '.') and (mNextChar <> '.') then begin parseId(); exit; end;
 
   // known delimiters?
-  mTokChar := curChar;
+  mTokChar := mCurChar;
   mTokType := TTDelim;
   skipChar();
-  if (curChar = '=') then
+  if (mCurChar = '=') then
   begin
     case mTokChar of
       '<': begin mTokType := TTLessEqu; mTokStr := '<='; skipChar(); exit; end;
@@ -590,7 +638,7 @@ begin
       ':': begin mTokType := TTAss; mTokStr := ':='; skipChar(); exit; end;
     end;
   end
-  else if (mTokChar = curChar) then
+  else if (mTokChar = mCurChar) then
   begin
     case mTokChar of
       '<': begin mTokType := TTShl; mTokStr := '<<'; skipChar(); exit; end;
@@ -602,13 +650,19 @@ begin
   else
   begin
     case mTokChar of
-      '<': if (curChar = '>') then begin mTokType := TTNotEqu; mTokStr := '<>'; skipChar(); exit; end;
-      '.': if (curChar = '.') then begin mTokType := TTDotDot; mTokStr := '..'; skipChar(); exit; end;
+      '<': if (mCurChar = '>') then begin mTokType := TTNotEqu; mTokStr := '<>'; skipChar(); exit; end;
+      '.': if (mCurChar = '.') then begin mTokType := TTDotDot; mTokStr := '..'; skipChar(); exit; end;
     end;
   end;
 end;
 
 
+function TTextParser.isIdOrStr (): Boolean; inline;
+begin
+  result := (mTokType = TTId) or (mTokType = TTStr);
+end;
+
+
 function TTextParser.expectId (): AnsiString;
 begin
   if (mTokType <> TTId) then raise Exception.Create('identifier expected');
@@ -617,20 +671,56 @@ begin
 end;
 
 
-procedure TTextParser.expectId (const aid: AnsiString);
+procedure TTextParser.expectId (const aid: AnsiString; caseSens: Boolean=true);
 begin
-  if (mTokType <> TTId) or (mTokStr <> aid) then raise Exception.Create('identifier '''+aid+''' expected');
+  if caseSens then
+  begin
+    if (mTokType <> TTId) or (mTokStr <> aid) then raise Exception.Create('identifier '''+aid+''' expected');
+  end
+  else
+  begin
+    if (mTokType <> TTId) or (not strEquCI1251(mTokStr, aid)) then raise Exception.Create('identifier '''+aid+''' expected');
+  end;
   skipToken();
 end;
 
 
-function TTextParser.eatId (const aid: AnsiString): Boolean;
+function TTextParser.eatId (const aid: AnsiString; caseSens: Boolean=true): Boolean;
+begin
+  if caseSens then
+  begin
+    result := (mTokType = TTId) and (mTokStr = aid);
+  end
+  else
+  begin
+    result := (mTokType = TTId) and strEquCI1251(mTokStr, aid);
+  end;
+  if result then skipToken();
+end;
+
+
+function TTextParser.eatIdOrStr (const aid: AnsiString; caseSens: Boolean=true): Boolean;
 begin
-  result := (mTokType = TTId) and (mTokStr = aid);
+  if caseSens then
+  begin
+    result := (mTokType = TTId) and (mTokStr = aid);
+    if not result then result := (mTokType = TTStr) and (mTokStr = aid);
+  end
+  else
+  begin
+    result := (mTokType = TTId) and strEquCI1251(mTokStr, aid);
+    if not result then result := (mTokType = TTStr) and strEquCI1251(mTokStr, aid);
+  end;
   if result then skipToken();
 end;
 
 
+function TTextParser.eatIdOrStrCI (const aid: AnsiString): Boolean; inline;
+begin
+  result := eatIdOrStr(aid, false);
+end;
+
+
 function TTextParser.expectStr (allowEmpty: Boolean=false): AnsiString;
 begin
   if (mTokType <> TTStr) then raise Exception.Create('string expected');
@@ -640,7 +730,7 @@ begin
 end;
 
 
-function TTextParser.expectStrOrId (allowEmpty: Boolean=false): AnsiString;
+function TTextParser.expectIdOrStr (allowEmpty: Boolean=false): AnsiString;
 begin
   case mTokType of
     TTStr: