1 /+ 2 Copyright Elias Batek 2017 - 2018. 3 Distributed under the Boost Software License, Version 1.0. 4 (See accompanying file LICENSE_1_0.txt or copy at 5 https://www.boost.org/LICENSE_1_0.txt) 6 +/ 7 module midigamepad.lib.translation.mappingsfile; 8 9 import std.ascii : isAlpha; 10 import std.conv : ConvException, to; 11 import std.exception; 12 import std.file : readText; 13 import std.json; 14 import std.string; 15 import std.stdio : write, writeln; 16 import std.utf : UTFException; 17 18 import midigamepad.lib.keyboard; 19 import midigamepad.lib.util; 20 21 public import midigamepad.lib.translation.mapping; 22 23 /++ 24 Parses the specified mappings file 25 +/ 26 MappingsCollection parseFile(string filePath) 27 { 28 string json = void; 29 30 try 31 { 32 json = readText(filePath); 33 } 34 catch (UTFException ex) 35 { 36 throw new MappingsParserException("Not a text file", ex); 37 } 38 39 return parse(json); 40 } 41 42 /++ 43 Parses the specified mappings json 44 +/ 45 MappingsCollection parse(string json) 46 { 47 JSONValue root = void; 48 49 try 50 { 51 root = parseJSON(json, JSONOptions.escapeNonAsciiChars); 52 } 53 catch (JSONException ex) 54 { 55 throw new MappingsParserException("Bad JSON", ex); 56 } 57 58 // Make sure there is a 'map' field 59 enforce(("map" in root.object), new MappingsParserException("Missing 'map' field", root)); 60 immutable JSONValue map = root.object["map"]; 61 62 MappingsCollection output = MappingsCollection(); 63 64 // onNote mappings 65 if ("onNote" in map.object) 66 { 67 immutable JSONValue onNote = map.object["onNote"]; 68 output.noteOnOff = parseNoteOnOffMappings(onNote); 69 } 70 71 return output; 72 } 73 74 /++ 75 Parses the specified NoteOnOffMappings 76 +/ 77 NoteOnOffMapping[] parseNoteOnOffMappings(JSONValue noteOnOffRoot) 78 { 79 auto ar = noteOnOffRoot.array; 80 NoteOnOffMapping[] output = new NoteOnOffMapping[ar.length]; 81 82 foreach (size_t i, JSONValue m; ar) 83 { 84 uint vKey = void; 85 86 // Ensure that the required elements exist 87 enforce(("vkey" in m), new MappingsParserException("Missing 'vkey' field", m)); 88 enforce(("note" in m), new MappingsParserException("Missing 'note' field", m)); 89 90 immutable string vKeyStr = m["vkey"].str; 91 immutable string noteStr = m["note"].str; 92 93 // Try to parse the vKey 94 if (!vKeyStr.tryParseVKEY(vKey)) 95 throw new MappingsParserException("Cannot parse invalid vKey: `" ~ vKeyStr ~ "`", m); 96 97 immutable ushort scancode = vKey.toScancode.to!ushort; 98 immutable bool isExtended = vKey.hasExtendedScancode; 99 100 try 101 { 102 immutable byte note = noteStr.hexOrDecToNumber!byte; 103 output[i] = NoteOnOffMapping(scancode, isExtended, note); 104 } 105 catch (ConvException ex) 106 { 107 throw new MappingsParserException( 108 "Cannot parse invalid MIDI note number: `" ~ noteStr ~ "`", m, ex); 109 } 110 } 111 112 return output; 113 } 114 115 /++ 116 Tries to parse the passed vKey string and converts it into a scan 117 118 Params: 119 input = input vKey string 120 vKey = contains the vKey 121 122 Returns: 123 true = success 124 +/ 125 bool tryParseVKEY(string input, out uint vKey) nothrow pure @safe 126 { 127 // Check whether vKey is a number (hex or dec) 128 if (input.startsWith("0x") || input.isNumeric) 129 { 130 try 131 { 132 vKey = input.hexOrDecToNumber!uint; 133 return true; 134 } 135 catch (Exception) 136 { 137 // Conversion failure 138 return false; 139 } 140 } 141 142 // Check whether vKey is a single char 143 else if ((input.length == 1) && input[0].isAlpha) 144 { 145 vKey = uint(input[0].toUpper); 146 return true; 147 } 148 149 // Not parseable 150 return false; 151 } 152 153 @safe unittest 154 { 155 uint output; 156 157 assert("0x12".tryParseVKEY(output)); 158 assert(output == 0x12); 159 160 assert("0x1B".tryParseVKEY(output)); 161 assert(output == 0x1B); 162 163 assert("31".tryParseVKEY(output)); 164 assert(output == 31); 165 166 assert("A".tryParseVKEY(output)); 167 assert(output == 'A'); 168 169 assert("b".tryParseVKEY(output)); 170 assert(output == 'B'); 171 172 assert(!"".tryParseVKEY(output)); 173 assert(!"yy".tryParseVKEY(output)); 174 assert(!"DD".tryParseVKEY(output)); 175 assert(!"→".tryParseVKEY(output)); 176 assert(!"♠".tryParseVKEY(output)); 177 assert(!".".tryParseVKEY(output)); 178 } 179 180 /++ 181 This is exception is thrown when the parser fails to parse a mapping. 182 183 Check the .faulyMapping property to see which one caused the problem. 184 +/ 185 class MappingsParserException : Exception 186 { 187 @nogc nothrow pure @safe: 188 189 private 190 { 191 JSONValue _faultyMapping; 192 } 193 194 /++ 195 The faulty mapping that could not be parsed 196 +/ 197 public @property JSONValue faultyMapping() const 198 { 199 return this._faultyMapping; 200 } 201 202 /++ 203 ctor 204 +/ 205 public this(string msg, Throwable next = null) 206 { 207 super(msg, next); 208 } 209 210 /++ ditto +/ 211 public this(string msg, JSONValue faultyMapping, Throwable next = null) 212 { 213 this(msg, next); 214 this._faultyMapping = faultyMapping; 215 } 216 }