UltimateZoneBuilder/Source/Core/IO/UniversalParser.cs
MaxED 0369c969d1 According to dotnetperls.com, "new Dictionary<string, [anything]>(StringComparer.Ordinal)" works 17% faster than "new Dictionary<string, [anything]>()", so let's stick that everywhere and see what happens :)
Draw Curve Mode: added settings panel.
Sectors mode: added "Make Door" button to the toolbar.
Swapped Side panel and Info panel z-order. 
Interface: split toolbar into 3 separate toolbars. All toolbar buttons are now viewable at 1024x768.
Interface: grouped stuff in "Modes" menu a bit better.
Interface: added "Draw [stuff]" buttons to modes toolbar.
Interface: reorganized main menu. Hope it makes more sense now.
API: added General.Interface.AddModesButton() and General.Interface.AddModesMenu(), which can be used to add buttons to specific group in "Modes" toolbar and menu items to specific group in "Modes" menu, so actions, which behave like an editing mode, but are not part of one can be added there.
2014-02-26 14:11:06 +00:00

847 lines
23 KiB
C#

#region ================== Copyright (c) 2007 Pascal vd Heiden
/*
* Copyright (c) 2007 Pascal vd Heiden, www.codeimp.com
* This program is released under GNU General Public License
*
* 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. See the
* GNU General Public License for more details.
*
*/
#endregion
#region ================== Namespaces
using System;
using System.IO;
using System.Text;
using System.Globalization;
using System.Collections.Generic;
#endregion
namespace CodeImp.DoomBuilder.IO
{
public sealed class UniversalParser
{
#region ================== Constants
// Path seperator
public const string DEFAULT_SEPERATOR = ".";
// Allowed characters in a key
public const string KEY_CHARACTERS = "abcdefghijklmnopqrstuvwxyz0123456789_";
// Parse mode constants
private const int PM_NOTHING = 0;
private const int PM_ASSIGNMENT = 1;
private const int PM_NUMBER = 2;
private const int PM_STRING = 3;
private const int PM_KEYWORD = 4;
// Error strings
private const string ERROR_KEYMISSING = "Missing key name in assignment or scope.";
private const string ERROR_KEYCHARACTERS = "Invalid characters in key name.";
//private const string ERROR_ASSIGNINVALID = "Invalid assignment. Missing a previous terminator symbol?";
private const string ERROR_VALUEINVALID = "Invalid value in assignment. Missing a previous terminator symbol?";
private const string ERROR_VALUETOOBIG = "Value too big.";
private const string ERROR_KEYWITHOUTVALUE = "Key has no value assigned.";
private const string ERROR_KEYWORDUNKNOWN = "Unknown keyword in assignment. Missing a previous terminator symbol?";
#endregion
#region ================== Variables
// Error result
private int cpErrorResult;
private string cpErrorDescription = "";
private int cpErrorLine;
// Configuration root
private UniversalCollection root;
private const string newline = "\n";
private char[] newlineChar = new[] {'\n'};
private StringBuilder key; //mxd
private StringBuilder val; //mxd
private Dictionary<string, UniversalEntry> matches; //mxd
// Settings
private bool strictchecking = true;
#endregion
#region ================== Properties
// Properties
public int ErrorResult { get { return cpErrorResult; } }
public string ErrorDescription { get { return cpErrorDescription; } }
public int ErrorLine { get { return cpErrorLine; } }
public UniversalCollection Root { get { return root; } }
public bool StrictChecking { get { return strictchecking; } set { strictchecking = value; } }
#endregion
#region ================== Constructor / Destructor
// Constructor
public UniversalParser()
{
// Standard new configuration
NewConfiguration();
// We have no destructor
GC.SuppressFinalize(this);
}
// Constructor to load a file immediately
public UniversalParser(string filename)
{
// Load configuration from file
LoadConfiguration(filename);
// We have no destructor
GC.SuppressFinalize(this);
}
#endregion
#region ================== Private Methods
// This returns a string added with escape characters
private string EscapedString(string str)
{
// Replace the \ with \\ first!
str = str.Replace("\\", "\\\\");
str = str.Replace(newline, "\\n");
str = str.Replace("\r", "\\r");
str = str.Replace("\t", "\\t");
str = str.Replace("\"", "\\\"");
// Return result
return str;
}
// This raises an error
private void RaiseError(int line, string description)
{
// Raise error
cpErrorResult = 1;
cpErrorDescription = description;
cpErrorLine = line;
}
// This validates a given key and sets
// error properties if key is invalid and errorline > -1
private bool ValidateKey(string key, int errorline)
{
bool validateresult = true;
// Check if key is an empty string
if(key.Length == 0)
{
// ERROR: Missing key name in statement
if(errorline > -1) RaiseError(errorline, ERROR_KEYMISSING);
validateresult = false;
}
else if(strictchecking) //Only when strict checking
{
// Check if all characters are valid
foreach(char c in key)
{
if(KEY_CHARACTERS.IndexOf(c) == -1)
{
// ERROR: Invalid characters in key name
if(errorline > -1) RaiseError(errorline, ERROR_KEYCHARACTERS);
validateresult = false;
break;
}
}
}
// Return result
return validateresult;
}
// This parses a structure in the given data starting
// from the given pos and line and updates pos and line.
private UniversalCollection InputStructure(ref string[] data, ref int pos, ref int line, bool topLevel)
{
char c = '\0'; // current data character
int pm = PM_NOTHING; // current parse mode
key.Remove(0, key.Length);
val.Remove(0, val.Length);
string s;
bool escape = false; // escape sequence?
bool endofstruct = false; // true as soon as this level struct ends
UniversalCollection cs = new UniversalCollection();
// Go through all of the data until
// the end or until the struct closes
// or when an arror occurred
while ((cpErrorResult == 0) && (endofstruct == false))
{
// Get current character
if(line == data.Length - 1) break;
if(pos > data[line].Length - 1) {
pos = 0;
line++;
}
c = data[line][pos];
// ================ What parse mode are we at?
if(pm == PM_NOTHING)
{
// Now check what character this is
switch(c)
{
case '{': // Begin of new struct
// Validate key
s = key.ToString().Trim();
if(ValidateKey(s, line))
{
// Next character
pos++;
// Parse this struct and add it
cs.Add(new UniversalEntry(s.ToLowerInvariant(), InputStructure(ref data, ref pos, ref line, false)));
// Check the last character
pos--;
// Reset the key
key.Remove(0, key.Length);
}
// Leave switch
break;
case '}': // End of this struct
// Stop parsing in this struct
endofstruct = true;
// Leave the loop
break;
case '=': // Assignment
// Validate key
if(ValidateKey(key.ToString().Trim(), line))
{
// Now parsing assignment
pm = PM_ASSIGNMENT;
}
// Leave switch
break;
case ';': // Terminator
// Validate key
if(ValidateKey(key.ToString().Trim(), line))
{
// Error: No value
RaiseError(line, ERROR_KEYWITHOUTVALUE);
}
// Leave switch
break;
case '\n': // New line
// Count the line
line++;
pos = -1;
// Add this to the key as a space.
// Spaces are not allowed, but it will be trimmed
// when its the first or last character.
key.Append(" ");
// Leave switch
break;
case '\\': // Possible comment
case '/':
// Check for the line comment //
if(data[line].Substring(pos, 2) == "//")
{
// Have next line?
if(line < data.Length)
{
// Count the line
line++;
// Skip everything on this line
pos = -1;
}
else
{
// No end of line
// Skip everything else
pos = data.Length;
}
}
// Check for the block comment /* */
else if(data[line].Substring(pos, 2) == "/*")
{
// Find the next closing block comment
int np = data[line].IndexOf("*/", pos);
// Closing block comment found?
if(np > -1)
{
// Count the lines in the block comment
string blockdata = data[line].Substring(pos, np - pos + 2);
line += (blockdata.Split(newlineChar).Length - 1);
// Skip everything in this block
pos = np + 1;
}
else
{
// No end of line
// Skip everything else
pos = data.Length;
}
}
// Leave switch
break;
default: // Everything else
if (!topLevel && pos == 0) {
while(matches.ContainsKey(data[line])) {
cs.Add(matches[data[line]].Key, matches[data[line]].Value);
line++;
pos = -1;
}
}
// Add character to key
if(pos != -1) key.Append(c);
// Leave switch
break;
}
}
// ================ Parsing an assignment
else if(pm == PM_ASSIGNMENT)
{
// Check for string opening
if(c == '\"')
{
// Now parsing string
pm = PM_STRING; //numbers
}
// Check for numeric character numbers
else if(Configuration.NUMBERS2.IndexOf(c) > -1)
{
// Now parsing number
pm = PM_NUMBER;
// Go one byte back, because this
// byte is part of the number!
pos--;
}
// Check for new line
else if(c == '\n')
{
// Count the new line
line++;
}
// Check if assignment ends
else if(c == ';')
{
// End of assignment
pm = PM_NOTHING;
// Remove this if it causes problems
key.Remove(0, key.Length);
val.Remove(0, val.Length);
}
// Otherwise (if not whitespace) it will be a keyword
else if((c != ' ') && (c != '\t'))
{
// Now parsing a keyword
pm = PM_KEYWORD;
// Go one byte back, because this
// byte is part of the keyword!
pos--;
}
}
// ================ Parsing a number
else if(pm == PM_NUMBER)
{
// Check if number ends here
if(c == ';')
{
// Hexadecimal?
s = val.ToString();
if((s.Length > 2) && s.StartsWith("0x", StringComparison.InvariantCultureIgnoreCase))
{
// Convert to int
try
{
// Convert to value
int ival = Convert.ToInt32(s.Substring(2).Trim(), 16);
// Add it to struct
UniversalEntry entry = new UniversalEntry(key.ToString().Trim().ToLowerInvariant(), ival);
cs.Add(entry);
matches.Add(data[line], entry);
}
catch(OverflowException)
{
// Too large for Int32, try Int64
try
{
// Convert to value
long lval = Convert.ToInt64(s.Substring(2).Trim(), 16);
// Add it to struct
UniversalEntry entry = new UniversalEntry(key.ToString().Trim().ToLowerInvariant(), lval);
cs.Add(entry);
matches.Add(data[line], entry);
}
catch(OverflowException)
{
// Too large for Int64, return error
RaiseError(line, ERROR_VALUETOOBIG);
}
catch(FormatException)
{
// ERROR: Invalid value in assignment
RaiseError(line, ERROR_VALUEINVALID);
}
}
catch(FormatException)
{
// ERROR: Invalid value in assignment
RaiseError(line, ERROR_VALUEINVALID);
}
}
// Floating point?
else if(s.IndexOf('.') > -1)
{
float fval = 0;
// Convert to float (remove the f first)
try { fval = Convert.ToSingle(s.Trim(), CultureInfo.InvariantCulture); }
catch(FormatException)
{
// ERROR: Invalid value in assignment
RaiseError(line, ERROR_VALUEINVALID);
}
// Add it to struct
UniversalEntry entry = new UniversalEntry(key.ToString().Trim().ToLowerInvariant(), fval);
cs.Add(entry);
matches.Add(data[line], entry);
}
else
{
// Convert to int
try
{
// Convert to value
int ival = Convert.ToInt32(s.Trim(), CultureInfo.InvariantCulture);
// Add it to struct
UniversalEntry entry = new UniversalEntry(key.ToString().Trim().ToLowerInvariant(), ival);
cs.Add(entry);
matches.Add(data[line], entry);
}
catch(OverflowException)
{
// Too large for Int32, try Int64
try
{
// Convert to value
long lval = System.Convert.ToInt64(s.Trim(), CultureInfo.InvariantCulture);
// Add it to struct
UniversalEntry entry = new UniversalEntry(key.ToString().Trim().ToLowerInvariant(), lval);
cs.Add(entry);
matches.Add(data[line], entry);
}
catch(OverflowException)
{
// Too large for Int64, return error
RaiseError(line, ERROR_VALUETOOBIG);
}
catch(System.FormatException)
{
// ERROR: Invalid value in assignment
RaiseError(line, ERROR_VALUEINVALID);
}
}
catch(FormatException)
{
// ERROR: Invalid value in assignment
RaiseError(line, ERROR_VALUEINVALID);
}
}
// Reset key and value
key.Remove(0, key.Length);
val.Remove(0, val.Length);
// End of assignment
pm = PM_NOTHING;
}
// Check for new line
else if(c == '\n')
{
// Count the new line
line++;
pos = -1;
}
// Everything else is part of the value
else
{
val.Append(c);
}
}
// ================ Parsing a string
else if(pm == PM_STRING)
{
// Check if in an escape sequence
if(escape)
{
// What character?
switch(c)
{
case '\\': val.Append('\\'); break;
case 'n': val.Append(newline); break;
case '\"': val.Append('\"'); break;
case 'r': val.Append('\r'); break;
case 't': val.Append('\t'); break;
default:
// Is it a number?
if(Configuration.NUMBERS.IndexOf(c) > -1)
{
int vv = 0;
char vc = '0';
// Convert the next 3 characters to a number
string v = data[line].Substring(pos, 3);
try { vv = Convert.ToInt32(v.Trim(), CultureInfo.InvariantCulture); }
catch(FormatException)
{
// ERROR: Invalid value in assignment
RaiseError(line, ERROR_VALUEINVALID);
}
// Convert the number to a char
try { vc = Convert.ToChar(vv, CultureInfo.InvariantCulture); }
catch(FormatException)
{
// ERROR: Invalid value in assignment
RaiseError(line, ERROR_VALUEINVALID);
}
// Add the char
val.Append(vc);
}
else
{
// Add the character as it is
val.Append(c);
}
// Leave switch
break;
}
// End of escape sequence
escape = false;
}
else
{
// Check for sequence start
if(c == '\\')
{
// Next character is of escape sequence
escape = true;
}
// Check if string ends
else if(c == '\"')
{
// Add string to struct
UniversalEntry entry = new UniversalEntry(key.ToString().Trim().ToLowerInvariant(), val.ToString());
cs.Add(entry);
matches.Add(data[line], entry);
// End of assignment
pm = PM_ASSIGNMENT;
// Reset key and value
key.Remove(0, key.Length);
val.Remove(0, val.Length);
}
// Check for new line
else if(c == '\n')
{
// Count the new line
line++;
pos = -1;
}
// Everything else is just part of string
else
{
// Add to value
val.Append(c);
}
}
}
// ================ Parsing a keyword
else if(pm == PM_KEYWORD)
{
// Check if keyword ends
if(c == ';')
{
// Add to the struct depending on the keyword
switch(val.ToString().Trim().ToLowerInvariant())
{
case "true":
// Add boolean true
UniversalEntry t = new UniversalEntry(key.ToString().Trim().ToLowerInvariant(), true);
cs.Add(t);
matches.Add(data[line], t);
break;
case "false":
// Add boolean false
UniversalEntry f = new UniversalEntry(key.ToString().Trim().ToLowerInvariant(), false);
cs.Add(f);
matches.Add(data[line], f);
break;
default:
// Unknown keyword
RaiseError(line, ERROR_KEYWORDUNKNOWN + "\nUnrecognized token: '" + val.ToString().Trim().ToLowerInvariant() + "'");
break;
}
// End of assignment
pm = PM_NOTHING;
// Reset key and value
key.Remove(0, key.Length);
val.Remove(0, val.Length);
}
// Check for new line
else if(c == '\n')
{
// Count the new line
line++;
pos = -1;
}
// Everything else is just part of keyword
else
{
// Add to value
val.Append(c);
}
}
// Next character
pos++;
}
// Return the parsed result
return cs;
}
// This will create a data structure from the given object
private string OutputStructure(UniversalCollection cs, int level, string newline, bool whitespace)
{
string leveltabs = "";
string spacing = "";
StringBuilder db = new StringBuilder("");
// Check if this ConfigStruct is not empty
if(cs.Count > 0)
{
// Create whitespace
if(whitespace)
{
for(int i = 0; i < level; i++) leveltabs += "\t";
spacing = " ";
}
// Go for each item
for(int i = 0; i < cs.Count; i++)
{
// Check if the value if of collection type
if(cs[i].Value is UniversalCollection)
{
UniversalCollection c = (UniversalCollection)cs[i].Value;
// Output recursive structure
if(whitespace) { db.Append(leveltabs); db.Append(newline); }
db.Append(leveltabs); db.Append(cs[i].Key);
if(!string.IsNullOrEmpty(c.Comment))
{
if(whitespace) db.Append("\t");
db.Append("// " + c.Comment);
}
db.Append(newline);
db.Append(leveltabs); db.Append("{"); db.Append(newline);
db.Append(OutputStructure(c, level + 1, newline, whitespace));
db.Append(leveltabs); db.Append("}"); db.Append(newline);
if(whitespace) { db.Append(leveltabs); db.Append(newline); }
}
// Check if the value is of boolean type
else if(cs[i].Value is bool)
{
db.Append(leveltabs); db.Append(cs[i].Key); db.Append(spacing); db.Append("="); db.Append(spacing);
db.Append((bool)cs[i].Value ? "true;" : "false;"); db.Append(newline);
}
// Check if value is of float type
else if(cs[i].Value is float)
{
// Output the value as float (3 decimals)
float f = (float)cs[i].Value;
db.Append(leveltabs); db.Append(cs[i].Key); db.Append(spacing); db.Append("=");
db.Append(spacing); db.Append(f.ToString("0.000", CultureInfo.InvariantCulture)); db.Append(";"); db.Append(newline);
}
// Check if value is of other numeric type
else if(cs[i].Value.GetType().IsPrimitive)
{
// Output the value unquoted
db.Append(leveltabs); db.Append(cs[i].Key); db.Append(spacing); db.Append("=");
db.Append(spacing); db.Append(String.Format(CultureInfo.InvariantCulture, "{0}", cs[i].Value)); db.Append(";"); db.Append(newline);
}
else
{
// Output the value with quotes and escape characters
db.Append(leveltabs); db.Append(cs[i].Key); db.Append(spacing); db.Append("=");
db.Append(spacing); db.Append("\""); db.Append(EscapedString(cs[i].Value.ToString())); db.Append("\";"); db.Append(newline);
}
}
}
// Return the structure
return db.ToString();
}
#endregion
#region ================== Public Methods
// This clears the last error
public void ClearError()
{
// Clear error
cpErrorResult = 0;
cpErrorDescription = "";
cpErrorLine = 0;
}
// This creates a new configuration
public void NewConfiguration()
{
// Create new configuration
root = new UniversalCollection();
}
// This will save the current configuration to the specified file
public bool SaveConfiguration(string filename) { return SaveConfiguration(filename, "\r\n", true); }
public bool SaveConfiguration(string filename, string newline) { return SaveConfiguration(filename, newline, true); }
public bool SaveConfiguration(string filename, string newline, bool whitespace)
{
// Kill the file if it exists
if(File.Exists(filename)) File.Delete(filename);
// Open file stream for writing
FileStream fstream = File.OpenWrite(filename);
// Create output structure and write to file
string data = OutputConfiguration(newline, whitespace);
byte[] baData= Encoding.ASCII.GetBytes(data);
fstream.Write(baData, 0, baData.Length);
fstream.Flush();
fstream.Close();
// Return true when done, false when errors occurred
if(cpErrorResult == 0) return true; else return false;
}
// This will output the current configuration as a string
public string OutputConfiguration() { return OutputConfiguration("\r\n", true); }
public string OutputConfiguration(string newline) { return OutputConfiguration(newline, true); }
public string OutputConfiguration(string newline, bool whitespace)
{
// Simply return the configuration structure as string
return OutputStructure(root, 0, newline, whitespace);
}
// This will load a configuration from file
public bool LoadConfiguration(string filename)
{
// Check if the file is missing
if(!File.Exists(filename))
{
throw(new FileNotFoundException("File not found \"" + filename + "\"", filename));
}
else
{
// Load the file contents
List<string> data = new List<string>(100);
using(FileStream stream = File.OpenRead(filename)) {
StreamReader reader = new StreamReader(stream, Encoding.ASCII);
while(!reader.EndOfStream) {
string line = reader.ReadLine();
if(string.IsNullOrEmpty(line)) continue;
// Remove returns and tabs because the
// parser only uses newline for new lines.
line = line.Replace("\r", "");
line = line.Replace("\t", "");
data.Add(line);
}
}
// Load the configuration from this data
return InputConfiguration(data.ToArray());
}
}
// This will load a configuration from string
public bool InputConfiguration(string[] data)
{
// Clear errors
ClearError();
// Parse the data to the root structure
int pos = 0;
int line = 1;
matches = new Dictionary<string, UniversalEntry>(StringComparer.Ordinal); //mxd
key = new StringBuilder(16); //mxd
val = new StringBuilder(16); //mxd
root = InputStructure(ref data, ref pos, ref line, true);
// Return true when done, false when errors occurred
return (cpErrorResult == 0);
}
#endregion
}
}