using System; using System.Collections.Generic; using System.IO; using System.Linq; namespace QFSW.QC { public static class QuantumMacros { private class MacroPreprocessor : IQcPreprocessor { public int Priority => 1000; public string Process(string text) { if (!text.StartsWith("#define", StringComparison.CurrentCulture)) { text = ExpandMacros(text); } return text; } } private static readonly Dictionary _macroTable = new Dictionary(); /// /// Expands all the macros in the given text. /// /// The macro expanded text. /// The text to expand the macros in. /// The maximum number of macro expansions that can be performed before an exception is thrown. public static string ExpandMacros(string text, int maximumExpansions = 1000) { if (_macroTable.Count == 0) { return text; } int expansionCount = 0; for (int i = 0; i < text.Length; i++) { if (text[i] == '#') { foreach (KeyValuePair macro in _macroTable) { int keyLength = macro.Key.Length; if (i + keyLength < text.Length) { if (text.Substring(i + 1, keyLength) == macro.Key) { if (expansionCount >= maximumExpansions) { throw new ArgumentException($"Maximum macro expansions of {maximumExpansions} was exhausted: infinitely recursive macro is likely."); } else { string start = text.Substring(0, i); string end = text.Substring(i + 1 + keyLength); text = $"{start}{macro.Value}{end}"; expansionCount++; i--; } } } } } } return text; } [Command("#define")] [CommandDescription("Adds a macro to the macro table which can then be used in the Quantum Console. If the macro 'name' is added, " + "then all instances of '#name' will be expanded into the full macro expansion. This allows you to define " + "shortcuts for various things such as long type names or commonly used command strings.\n\n" + "Macros may not contain hashtags or whitespace in their name.\n\n" + "Note: macros will not be expanded when using #define, this is so that defining nested macros is possible.")] public static void DefineMacro(string macroName, string macroExpansion) { macroName = macroName.Trim(); if (macroName.Contains(' ')) { throw new ArgumentException("Macro names cannot contain whitespace."); } if (macroName.Contains('\n')) { throw new ArgumentException("Macro names cannot contain newlines."); } if (macroName.Contains('#')) { throw new ArgumentException("Macro names cannot contain hashtags."); } if (macroName == "define") { throw new ArgumentException("Macros cannot be named define."); } if (macroExpansion.Contains('\n')) { throw new ArgumentException("Macro names cannot contain newlines."); } if (macroExpansion.Contains($"#{macroName}")) { throw new ArgumentException("Macros cannot contain themselves within the expansion."); } if (_macroTable.ContainsKey(macroName)) { _macroTable[macroName] = macroExpansion; } else { _macroTable.Add(macroName, macroExpansion); } } [Command("remove-macro")] [CommandDescription("Removes the specified macro from the macro table")] public static void RemoveMacro(string macroName) { if (_macroTable.ContainsKey(macroName)) { _macroTable.Remove(macroName); } else { throw new Exception($"Specified macro #{macroName} as it was not defined."); } } [Command("clear-macros")] [CommandDescription("Clears the macro table")] public static void ClearMacros() { _macroTable.Clear(); } [Command("all-macros", "Displays all of the macros currently stored in the macro table")] private static string GetAllMacros() { if (_macroTable.Count == 0) { return "Macro table is empty"; } else { return $"Macro table:\n{string.Join("\n", _macroTable.Select((x) => $"#{x.Key} = {x.Value}"))}"; } } [Command("dump-macros", "Creates a file dump of macro table which can the be loaded to repopulate the table using load-macros")] [CommandPlatform(Platform.AllPlatforms ^ (Platform.WebGLPlayer))] public static void DumpMacrosToFile(string filePath) { using (StreamWriter dumpFile = new StreamWriter(filePath)) { foreach (KeyValuePair macro in _macroTable) { dumpFile.WriteLine($"{macro.Key} {macro.Value}"); } dumpFile.Flush(); dumpFile.Close(); } } [Command("load-macros", "Loads macros from an external file into the macro table")] [CommandPlatform(Platform.AllPlatforms ^ (Platform.WebGLPlayer))] public static string LoadMacrosFromFile(string filePath) { if (!File.Exists(filePath)) { throw new ArgumentException($"file at the specified path '{filePath}' did not exist."); } using (StreamReader macroFile = new StreamReader(filePath)) { List messages = new List(); while (!macroFile.EndOfStream) { string line = macroFile.ReadLine(); string[] parts = line.Split(" ".ToCharArray(), 2); if (parts.Length != 2) { messages.Add($"'{line}' is not a valid macro definition"); } try { DefineMacro(parts[0], parts[1]); messages.Add($"#{parts[0]} was successfully defined"); } catch (Exception e) { messages.Add($"#{parts[0]} could not be defined: {e.Message}"); } } macroFile.Close(); return string.Join("\n", messages); } } } }