/// <summary> /// Extract an integral number from 'src' (basically 'strtol') /// Expects 'src' to point to a string of the following form: /// [delim] [{+|–}][0[{x|X|b|B}]][digits] /// The first character that does not fit this form stops the scan. /// If 'radix' is between 2 and 36, then it is used as the base of the number. /// If 'radix' is 0, the initial characters of the string are used to determine the base. /// If the first character is 0 and the second character is not 'x' or 'X', the string is interpreted as an octal integer; /// otherwise, it is interpreted as a decimal number. If the first character is '0' and the second character is 'x' or 'X', /// the string is interpreted as a hexadecimal integer. If the first character is '1' through '9', the string is interpreted /// as a decimal integer. The letters 'a' through 'z' (or 'A' through 'Z') are assigned the values 10 through 35; only letters /// whose assigned values are less than 'radix' are permitted.</summary> public static bool Int(out long intg, int radix, Src src, string?delim = null) { intg = 0; if (!BufferNumber(src, ref radix, out var len, ENumType.Int, delim)) { return(false); } try { var str = src.Buffer.ToString(0, len).ToLower(); str = str.TrimEnd('u', 'l'); if (radix == 2 && str.StartsWith("0b")) { str = str.Remove(0, 2); } if (radix == 8 && str.StartsWith("0o")) { str = str.Remove(0, 2); } if (radix == 16 && str.StartsWith("0x")) { str = str.Remove(0, 2); } intg = Convert.ToInt64(str, radix); src += len; return(true); } catch { return(false); } }
/// <summary> /// Construct a preprocessor macro of the form: 'TAG(p0,p1,..,pn)' expansion /// from a stream of characters. Stops at the first non-escaped new line</summary> public Macro(Src src, Loc loc) : this(null, null, null, loc) { // Extract the tag and find it's hash code m_tag = ReadTag(src, loc); // Hash the tag // m_hash = Hash(m_tag); // Extract the optional parameters if (src == '(') { ReadParams(src, m_params, loc, true); } // Trim whitespace from before the expansion text for (; src != 0 && Str_.IsLineSpace(src); ++src) { } // Extract the expansion and trim all leading and following whitespace if (!Extract.Line(out var expansion, src, true)) { throw new ScriptException(EResult.InvalidMacroDefinition, loc, "invalid macro expansion"); } m_expansion = expansion.Trim(); }
/// <summary>Extract multi-part identifiers from 'src' incrementing 'src'. E.g. House.Room.Item</summary> public static bool Identifiers(out string[] ids, Src src, char sep = '.', string?delim = null) { ids = Array.Empty <string>(); var idents = new List <string>(); for (; ;) { if (idents.Count != 0) { if (src == sep) { ++src; } else { break; } } if (!Identifier(out var id, src, delim)) { return(false); } idents.Add(id); } ids = idents.ToArray(); return(true); }
public Reader(Src src, bool case_sensitive = false, IIncludeHandler?inc = null, IMacroHandler?mac = null, EmbeddedCodeFactory?emb = null) { Src = new Preprocessor(src, inc, mac, emb); Delimiters = " \t\r\n\v,;"; LastKeyword = string.Empty; CaseSensitive = case_sensitive; }
protected Src(Src wrapped) { m_src = wrapped; m_loc = new Loc(); Buffer = new StringBuilder(256); Limit = long.MaxValue; }
/// <summary> /// Extract a boolean from 'src' /// Expects 'src' to point to a string of the following form: /// [delim]{0|1|true|false} /// The first character that does not fit this form stops the scan. /// '0','1' must be followed by a non-identifier character /// 'true', 'false' can have any case</summary> public static bool Bool(out bool bool_, Src src, string?delim = null) { bool_ = false; delim ??= DefaultDelimiters; // Find the first non-delimiter if (!AdvanceToNonDelim(src, delim)) { return(false); } // Extract the boolean switch (char.ToLower(src)) { default: return(false); case '0': bool_ = false; return(!Str_.IsIdentifier(++src, false)); case '1': bool_ = true; return(!Str_.IsIdentifier(++src, false)); case 't': bool_ = true; return(char.ToLower(++src) == 'r' && char.ToLower(++src) == 'u' && char.ToLower(++src) == 'e' && !Str_.IsIdentifier(++src, false)); case 'f': bool_ = false; return(char.ToLower(++src) == 'a' && char.ToLower(++src) == 'l' && char.ToLower(++src) == 's' && char.ToLower(++src) == 'e' && !Str_.IsIdentifier(++src, false)); } }
/// <summary>Return a macro tag from 'src' (or fail)</summary> private string ReadTag(Src src, Loc loc) { if (Extract.Identifier(out var tag, src)) { return(tag); } throw new ScriptException(EResult.InvalidIdentifier, loc, "invalid macro name"); }
public TemplateReplacer(Src src, string pattern, SubstituteFunction subst, int chunk_size = 4096) { Src = new SrcStack(src); Substitute = subst; Pattern = new Regex(pattern); ChunkSize = chunk_size; OutputLocation = new Loc(); m_match_ofs = 0; }
/// <summary>Push a script source onto the stack</summary> public void Push(Src src) { // The stack takes ownership of 'src' and calls dispose on it when popped. if (src == null) { throw new ArgumentNullException("src", "Null character source provided"); } Stack.Push(src); }
public static bool IntArray(out long[] intg, int count, int radix, Src src, string?delim = null) { intg = new long[count]; for (int i = 0; i != count; ++i) { if (!Int(out intg[i], radix, src, delim)) { return(false); } } return(true); }
public static bool BoolArray(out bool[] bool_, int count, Src src, string?delim = null) { bool_ = new bool[count]; for (int i = 0; i != count; ++i) { if (!Bool(out bool_[i], src, delim)) { return(false); } } return(true); }
/// <summary>Extract a contiguous block of characters up to (and possibly including) a new line character</summary> public static bool Line(out string line, Src src, bool inc_cr, string?newline = null) { newline ??= "\n"; BufferWhile(src, (s, i) => !newline.Contains(s[i]) ? 1 : 0, 0, out var len); if (inc_cr && src[len] != 0) { len += newline.Length; src.ReadAhead(len); } line = src.Buffer.ToString(0, len); src += len; return(true); }
public static bool RealArray(out double[] real, int count, Src src, string?delim = null) { real = new double[count]; for (int i = 0; i != count; ++i) { if (!Real(out real[i], src, delim)) { return(false); } } return(true); }
/// <summary> /// Reads the next character from the input stream. The returned value is /// -1 if no further characters are available.</summary> public override int Read() { var ch = Peek(); if (ch == -1) { return(-1); } Src.Next(); --m_match_ofs; OutputLocation.inc((char)ch); return(ch); }
/// <summary>Extract a contiguous block of non-delimiter characters from 'src'</summary> public static bool Token(out string token, Src src, string?delim = null) { token = string.Empty; delim ??= DefaultDelimiters; // Find the first non-delimiter if (!AdvanceToNonDelim(src, delim)) { return(false); } // Copy up to the next delimiter BufferWhile(src, (s, i) => !delim.Contains(s[i]) ? 1 : 0, 0, out var len); token = src.Buffer.ToString(0, len); src += len; return(true); }
/// <summary> /// Extract a floating point number from 'src' /// Expects 'src' to point to a string of the following form: /// [delim] [{+|-}][digits][.digits][{d|D|e|E}[{+|-}]digits] /// The first character that does not fit this form stops the scan. /// If no digits appear before the '.' character, at least one must appear after the '.' character. /// The decimal digits can be followed by an exponent, which consists of an introductory letter (d, D, e, or E) and an optionally signed integer. /// If neither an exponent part nor a '.' character appears, a '.' character is assumed to follow the last digit in the string.</summary> public static bool Real(out double real, Src src, string?delim = null) { real = 0; int radix = 10; if (!BufferNumber(src, ref radix, out var len, ENumType.FP, delim)) { return(false); } try { var str = src.Buffer.ToString(0, len).ToLower(); str = str.TrimEnd('f'); real = Convert.ToDouble(str); src += len; return(true); } catch { return(false); } }
/// <summary>Extract a contiguous block of identifier characters from 'src' incrementing 'src'</summary> public static bool Identifier(out string id, Src src, string?delim = null) { id = string.Empty; delim ??= DefaultDelimiters; // Find the first non-delimiter if (!AdvanceToNonDelim(src, delim)) { return(false); } // If the first non-delimiter is not a valid identifier character, then we can't extract an identifier if (!Str_.IsIdentifier(src, true)) { return(false); } // Copy up to the first non-identifier character BufferWhile(src, (s, i) => Str_.IsIdentifier(s[i], false) ? 1 : 0, 1, out var len); id = src.Buffer.ToString(0, len); src += len; return(true); }
/// <summary>Expands template mark-up files</summary> public static string Markup(Src src, string current_dir) { // Supported substitutions: // <!--#include file="filepath"--> // Substitutes the content of the file 'filepath' at the declared location. // 'filepath' can be relative to directory of the template file, or a full path // <!--#var name="count" file="filepath" value="const int Count = (?<value>\d+);"--> // Declares a variable for later use. Applies a regex match to the contents of 'filepath' to get a definition for 'value' // 'filepath' can be relative to directory of the template file, or a full path // <!--#value name="count"--> // Substitute a previously defined variable called 'count' // <!--#image file="filepath"--> // Substitute a base64 image file 'filepath' var variables = new Dictionary <string, string>(); // General form: [optional leading whitespace]<!--#command key="value" key="value"... --> // The pattern matches leading white space which is then inserted before every line in the substituted result. // This means includes on lines of the own are correctly tabbed, and <!--inline--> substitutions are also correct var result = TemplateReplacer.Process(src, @"(?<indent>[ \t]*)<!--#(?<cmd>\w+)\s+(?<kv>.*?)-->", (tr, match) => { var indent = match.Result("${indent}"); var cmd = match.Result("${cmd}"); var kv = match.Result("${kv}"); // list of key="value" pairs Match m; switch (cmd) { default: { throw new Exception(string.Format("Unknown template command: '{0}' ({1})", cmd, match.ToString())); } case "include": #region include { const string expected_form = "<!--#include file=\"filepath\"-->"; // Read the include filepath and check it exists m = Regex.Match(kv, @".*file=""(?<file>.*?)"".*"); if (!m.Success) { throw new Exception(string.Format("Invalid include: '{0}'.\nCould not match 'file' field.\nExpected form: {1}", match.ToString(), expected_form)); } var fpath = m.Result("${file}"); fpath = Path.IsPathRooted(fpath) ? fpath : Path.Combine(current_dir, fpath); if (!File.Exists(fpath)) { throw new FileNotFoundException(string.Format("File reference not found: {0}", fpath), fpath); } tr.PushSource(new AddIndents(new FileSrc(fpath), indent, true)); return(string.Empty); } #endregion case "var": #region var { const string expected_form = "<!--#var name=\"variable_name\" file=\"filepath\" value=\"regex_pattern_defining_value\"-->"; // Read the name of the variable m = Regex.Match(kv, @".*name=""(?<name>\w+)"".*"); if (!m.Success) { throw new Exception(string.Format("Invalid variable declaration: '{0}'.\nCould not match 'name' field.\nExpected form: {1}", match.ToString(), expected_form)); } var name = m.Result("${name}"); // Read the source filepath m = Regex.Match(kv, @".*file=""(?<file>.*?)"".*"); if (!m.Success) { throw new Exception(string.Format("Invalid variable declaration: '{0}'.\nCould not match 'file' field.\nExpected form: {1}", match.ToString(), expected_form)); } var filepath = m.Result("${file}"); filepath = Path.IsPathRooted(filepath) ? filepath : Path.Combine(current_dir, filepath); if (!File.Exists(filepath)) { throw new FileNotFoundException(string.Format("File reference not found: {0}", filepath), filepath); } // Read the regex pattern that defines 'value' m = Regex.Match(kv, @".*value=""(?<pattern>.*?)"".*"); if (!m.Success) { throw new Exception(string.Format("Invalid variable declaration: '{0}'.\nCould not match 'value' field.\nExpected form: {1}", match.ToString(), expected_form)); } var pat = m.Result("${pattern}"); Regex pattern; try { pattern = new Regex(pat, RegexOptions.Compiled); } catch (Exception ex) { throw new Exception(string.Format("Invalid variable declaration: '{0}'.\n'value' field is not a valid Regex expression.\nExpected form: {1}", match.ToString(), expected_form), ex); } // Use the pattern to get the value for the variable using (var sr = new StreamReader(filepath, Encoding.UTF8, true, 65536)) for (string?line; (line = sr.ReadLine()) != null && !(m = pattern.Match(line)).Success;) { } if (!m.Success) { throw new Exception(string.Format("Invalid variable declaration: '{0}'.\n'value' regex expression did not find a match in {2}.\nExpected form: {1}", match.ToString(), expected_form, filepath)); } var value = m.Result("${value}"); // Save the name/value pair variables[name] = value; return(string.Empty); } #endregion case "value": #region value { const string expected_form = "<!--#value name=\"variable_name\"-->"; // Read the name of the variable m = Regex.Match(kv, @".*name=""(?<name>\w+)"".*"); if (!m.Success) { throw new Exception(string.Format("Invalid value declaration: '{0}'.\nCould not match 'name' field.\nExpected form: {1}", match.ToString(), expected_form)); } var name = m.Result("${name}"); // Lookup the value for the variable if (!variables.TryGetValue(name, out var value)) { throw new Exception(string.Format("Invalid value declaration: '{0}'.\nVariable with 'name' {2} is not defined.\nExpected form: {1}", match.ToString(), expected_form, name)); } return(indent + value); } #endregion case "image": #region image { const string expected_form = "<!--#image file=\"filepath\"-->"; // Read the filename m = Regex.Match(kv, @".*file=""(?<file>.*?)"".*"); if (!m.Success) { throw new Exception(string.Format("Invalid image declaration: '{0}'.\nCould not match 'file' field.\nExpected form: {1}", match.ToString(), expected_form)); } var fpath = m.Result("${file}"); fpath = Path.IsPathRooted(fpath) ? fpath : Path.Combine(current_dir, fpath); if (!File.Exists(fpath)) { throw new FileNotFoundException(string.Format("File reference not found: {0}", fpath), fpath); } // Determine the image type var extn = (Path.GetExtension(fpath) ?? string.Empty).ToLowerInvariant(); if (string.IsNullOrEmpty(extn)) { throw new Exception(string.Format("Could not determine image file format from path '{0}'", fpath)); } // Read the image file var img = File.ReadAllBytes(fpath); switch (extn) { default: throw new Exception(string.Format("Unsupported image format: {0}", extn)); case ".png": return(string.Format("data:image/png;base64,{0}", Convert.ToBase64String(img))); case ".jpg": return(string.Format("data:image/jpg;base64,{0}", Convert.ToBase64String(img))); } } #endregion } }); return(result); }
/// <summary>Process a string template</summary> public static string Process(Src template, string pattern, SubstituteFunction subst, int chunk_size = 4096) { using var tr = new TemplateReplacer(template, pattern, subst, chunk_size); return(tr.ReadToEnd()); }
/// <summary> /// Extract a quoted (") string. /// if 'escape' is not 0, it is treated as the escape character /// if 'quote' is not null, it is treated as the accepted quote characters</summary> public static bool String(out string str, Src src, string?delim = null) { return(String(out str, src, '\0', null, delim)); }
protected virtual void Dispose(bool _) { Src.Dispose(); }
/// <summary> /// Extract a number of unknown format from 'src' /// Expects 'src' to point to a string containing any allowable number format (int or real).</summary> public static bool Number <Num>(out Num num, Src src, int radix = 0, string?delim = null) where Num : struct { num = default !;
public Preprocessor(Src src, IIncludeHandler?inc = null, IMacroHandler?mac = null, EmbeddedCodeFactory?emb = null) : this(inc, mac, emb) { Stack.Push(src); }
public StripComments(Src src, InLiteral.EFlags literal_flags = InLiteral.EFlags.Escaped | InLiteral.EFlags.SingleLineStrings, InComment.Patterns?comment_patterns = null) : base(src) { m_com = new InComment(comment_patterns ?? new InComment.Patterns(), literal_flags); }
/// <summary>Expands template html files</summary> public static string Html(Src src, string current_dir) => Markup(src, current_dir);
protected override void Dispose(bool disposing) { Src.Dispose(); base.Dispose(disposing); }
public static bool String(out string str, Src src, char escape, string?quotes = null, string?delim = null) { str = string.Empty; delim ??= DefaultDelimiters; // Set the accepted quote characters quotes ??= "\"\'"; // Find the first non-delimiter if (!AdvanceToNonDelim(src, delim)) { return(false); } // If the next character is not an acceptable quote, then this isn't a string char quote = src; if (quotes.Contains(quote)) { ++src; } else { return(false); } // Copy the string var sb = new StringBuilder(); if (escape != 0) { // Copy to the closing quote, allowing for the escape character for (; src != 0 && src != quote; ++src) { if (src == escape) { switch (++src) { default: break; case 'a': sb.Append('\a'); break; case 'b': sb.Append('\b'); break; case 'f': sb.Append('\f'); break; case 'n': sb.Append('\n'); break; case 'r': sb.Append('\r'); break; case 't': sb.Append('\t'); break; case 'v': sb.Append('\v'); break; case '\'': sb.Append('\''); break; case '\"': sb.Append('\"'); break; case '\\': sb.Append('\\'); break; case '0': case '1': case '2': case '3': { // ASCII character in octal var oct = new char[8]; var i = 0; for (; i != oct.Length && Str_.IsOctDigit(src); ++i, ++src) { oct[i] = src; } sb.Append((char)Convert.ToInt32(new string(oct, 0, i), 8)); break; } case 'x': { // ASCII or UNICODE character in hex var hex = new char[8]; var i = 0; for (; i != hex.Length && Str_.IsHexDigit(src); ++i, ++src) { hex[i] = src; } sb.Append((char)Convert.ToInt32(new string(hex, 0, i), 16)); break; } } } else { sb.Append(src); } } } else { // Copy to the next quote for (; src != 0 && src != quote; ++src) { sb.Append(src); } } // If the string doesn't end with a quote, then it's not a valid string if (src == quote) { ++src; } else { return(false); } str = sb.ToString(); return(true); }
/// <summary>Push a new stream onto the source stack</summary> public void PushSource(Src src) { Src.Push(src); }
/// <summary> /// Extract a comma separated parameter list of the form '(p0,p1,..,pn)' /// If 'identifiers' is true then the parameters are expected to be identifiers. /// If not, then anything delimited by commas is accepted. 'identifiers' == true is used when /// reading the definition of the macro, 'identifiers' == false is used when expanding an instance. /// If an empty parameter list is given, i.e. "()" then 'args' is returned containing one blank parameter. /// Returns true if the macro does not take parameters or the correct number of parameters where given, /// false if the macro takes parameters but none were given. Basically, 'false' means, don't treat /// this macro as matching because no params were given. If false is returned the buffer will /// contain anything read during this method.</summary> private bool ReadParams(Src buf, List <string> args, Loc location, bool identifiers) { // Buffer up to the first non-whitespace character // If no parameters are given, then the macro doesn't match if (!identifiers && args.Count != 0) { int i = 0; for (; Str_.IsWhiteSpace(buf[i]); ++i) { } if (buf[i] != '(') { return(false); } buf += i; } // If we're not reading the parameter names for a macro definition // and the macro takes no parameters, then ReadParams is a no-op if (!identifiers && m_params.Count == 0) { return(true); } // Capture the strings between commas as the parameters for (++buf; buf != ')'; buf += buf != ')' ? 1 : 0) { // Read parameter names for a macro definition if (identifiers) { if (!Extract.Identifier(out var arg, buf)) { throw new ScriptException(EResult.InvalidIdentifier, location, "invalid macro identifier"); } args.Add(arg); } // Read parameters being passed to the macro else { var arg = new StringBuilder(); for (int nest = 0; (buf != ',' && buf != ')') || nest != 0; ++buf) { if (buf == 0) { throw new ScriptException(EResult.UnexpectedEndOfFile, location, "macro parameter list incomplete"); } arg.Append(buf); nest += buf == '(' ? 1 : 0; nest -= buf == ')' ? 1 : 0; } args.Add(arg.ToString()); } } ++buf; // Skip over the ')' // Add a blank argument to distinguish between "TAG()" and "TAG" if (args.Count == 0) { args.Add(string.Empty); } // Check enough parameters have been given if (!identifiers && m_params.Count != args.Count) { throw new ScriptException(EResult.ParameterCountMismatch, location, "incorrect number of macro parameters"); } return(true); }