using System; using System.Collections.Generic; using System.IO; using System.Text; namespace Clave.Vellum { /// /// Spells a term of a as one line of text and reads it back, eg: /// Pressed(Z) & Held(Layout:ControlLeft) & ~Held(ShiftLeft). /// /// /// The space is left off for the physical keys. Reading takes any case and spacing, a ! in place of the /// ~, and a comma in place of the &, so a person can edit a save by hand. A term without /// literals is the empty text and always matches. /// public static class KeyTermText { public const char Negation = '~'; public const string Separator = " & "; private static Dictionary NamesOf() where EnumType : struct { var returnMe = new Dictionary(StringComparer.OrdinalIgnoreCase); foreach (EnumType value in Enum.GetValues(typeof(EnumType))) { string name = value.ToString(); if (returnMe.ContainsKey(name)) { // An alias of the same value is fine, and a different value under one name is a mistake. if (!EqualityComparer.Default.Equals(returnMe[name], value)) { throw new InvalidOperationException(typeof(EnumType).Name + " has two values named " + name + " apart from case."); } continue; } returnMe.Add(name, value); } return returnMe; } private static readonly Dictionary Keys = NamesOf(); private static readonly Dictionary Kinds = NamesOf(); private static readonly Dictionary Spaces = NamesOf(); private static void AppendLiteral(StringBuilder builder, KeyLiteral literal) { if (literal.Negated) { builder.Append(Negation); } builder.Append(literal.Kind).Append('('); if (literal.Space != KeySpace.Physical) { builder.Append(literal.Space).Append(':'); } builder.Append(literal.Key).Append(')'); } /// /// Returns the text of the term made of . /// public static string Format(IReadOnlyList literals) { if (literals == null) { throw new ArgumentNullException(nameof(literals)); } var builder = new StringBuilder(); for (int i = 0; i < literals.Count; ++i) { if (i > 0) { builder.Append(Separator); } AppendLiteral(builder, literals[i]); } return builder.ToString(); } private static InvalidDataException Bad(string text, int position, string problem) { return new InvalidDataException("The key term \"" + text + "\" has " + problem + " at character " + position + "."); } private static int SkipSpaces(string text, int position) { while (position < text.Length && char.IsWhiteSpace(text[position])) { ++position; } return position; } // The text from `position` up to the stop character, trimmed. The position is left on the stop. private static string ReadUntil(string text, ref int position, char stop) { int start = position; while (position < text.Length && text[position] != stop) { ++position; } return text.Substring(start, position - start).Trim(); } private static KeyLiteral ReadLiteral(string text, ref int position) { bool negated = false; if (text[position] == Negation || text[position] == '!') { negated = true; position = SkipSpaces(text, position + 1); } int kindStart = position; string kindName = ReadUntil(text, ref position, '('); KeyStateKind kind; if (position == text.Length || !Kinds.TryGetValue(kindName, out kind)) { throw Bad(text, kindStart, "an unknown kind of key state \"" + kindName + "\""); } int insideStart = ++position; string inside = ReadUntil(text, ref position, ')'); if (position == text.Length) { throw Bad(text, insideStart, "no closing bracket"); } ++position; KeySpace space = KeySpace.Physical; int colon = inside.IndexOf(':'); if (colon >= 0) { string spaceName = inside.Substring(0, colon).Trim(); if (!Spaces.TryGetValue(spaceName, out space)) { throw Bad(text, insideStart, "an unknown key space \"" + spaceName + "\""); } inside = inside.Substring(colon + 1).Trim(); } KeyCode key; if (!Keys.TryGetValue(inside, out key)) { throw Bad(text, insideStart, "an unknown key \"" + inside + "\""); } return new KeyLiteral(key, kind, space, negated); } /// /// Reads the literals of a term back. /// /// The text isn't a term. public static List Parse(string text) { if (text == null) { throw new ArgumentNullException(nameof(text)); } var returnMe = new List(); int position = SkipSpaces(text, 0); while (position < text.Length) { returnMe.Add(ReadLiteral(text, ref position)); position = SkipSpaces(text, position); if (position == text.Length) { break; } if (text[position] != '&' && text[position] != ',') { throw Bad(text, position, "something other than & between its parts"); } position = SkipSpaces(text, position + 1); if (position == text.Length) { throw Bad(text, position, "nothing after the last &"); } } return returnMe; } } }