diff --git a/JSON.ahk b/JSON.ahk index 55297f6..268d6ac 100644 --- a/JSON.ahk +++ b/JSON.ahk @@ -1,326 +1,305 @@ -/* -JSON module for AutoHotkey [requires v.1.1+, tested on v.1.1.13.01] - -The parser is inspired by Douglas Crockford's(json.org) json_parse.js and -and Mike Samuel's json_sans_eval.js (https://code.google.com/p/json-sans-eval/). -I've combined the two implementation to create a fast(somehow:P) and validating -JSON parser. Some section(s) are based on VxE's JSON function(s) - -[http://www.ahkscript.org/boards/viewtopic.php?f=6&t=30] - Thank you VxE -*/ class JSON { - /* - Parses a string containing JSON string and returns it as an AHK object. - Objects {} and arrays [] are wrapped as JSON.object and JSON.array instances. - Objects {} key-value pairs are enumerated in the order they are created - instead of the default behavior -- alpahabetically. An exception is thrown - if the JSON string is badly formatted, e.g. illegal chars, invalid escaping - Usage: - --start-of-code-- - j := JSON.parse("[{""foo"": ""Hello World"", ""bar"":""AutoHotkey""}]") - MsgBox, % j[1].foo ; displays 'Hello World' - MsgBox, % j[1].bar ; displays 'AutoHotkey' - --end-of-code-- - */ - parse(src) { - esc_char := { - (Join - """": """", - "/": "/", - "b": Chr(08), - "f": Chr(12), - "n": "`n", - "r": "`r", - "t": "`t" + parse(src, jsonize:=false) + { + ;// Pre-validate JSON source before parsing + if ((src:=Trim(src, " `t`n`r")) == "") ;// trim whitespace(s) + throw Exception("Empty JSON source") + first := SubStr(src, 1, 1), last := SubStr(src, -1) + if !InStr('{["tfn0123456789-', first) ;// valid beginning chars + || !InStr('}]el0123456789"', last) ;// valid ending chars + || (first == '{' && last != '}') ;// if starts w/ '{' must end w/ '}' + || (first == '[' && last != ']') ;// if starts w/ '[' must end w/ ']' + || (first == '"' && last != '"') ;// if starts w/ '"' must end w/ '"' + || (first == 'n' && last != 'l') ;// assume 'null' + || (InStr('tf', first) && last != 'e') ;// assume 'true' OR 'false' + || (InStr('-0123456789', first) && !(last is 'number')) ;// number + throw Exception("Invalid JSON format") + + static esc_seq := { + (Q + '"': '"', + '/': '/', + 'b': '`b', + 'f': '`f', + 'n': '`n', + 'r': '`r', + 't': '`t' )} - null := "" ; needed?? - - /* - This loop is based on VxE's JSON_ToObj.ahk - thank you VxE - Quoted strings are extracted and temporarily stored in an object and - later on re-inserted while the result object is being created. - */ + ;// Extract string literals i := 0, strings := [] - while (i:=InStr(src, """",, i+1)) { + while (i:=InStr(src, '"',, i+1)) + { j := i - while (j:=InStr(src, """",, j+1)) { + while (j:=InStr(src, '"',, j+1)) + { str := SubStr(src, i+1, j-i-1) - StringReplace, str, str, \\, \u005C, A - if (SubStr(str, 0) != "\") + str := StrReplace(str, "\\", "\u005C") + if (SubStr(str, -1) != "\") break } - - src := SubStr(src, 1, i-1) . SubStr(src, j) - - z := 0 - while (z:=InStr(str, "\",, z+1)) { - ch := SubStr(str, z+1, 1) - if InStr("""btnfr/", ch) ; esc_char.HasKey(ch) - str := SubStr(str, 1, z-1) . esc_char[ch] . SubStr(str, z+2) + + if !j, throw Exception("Missing close quote(s)") + src := SubStr(src, 1, i) . SubStr(src, j+1) + + k := 0 + while (k := InStr(str, "\",, k+1)) + { + ch := SubStr(str, k+1, 1) + if InStr('"btnfr/', ch) + str := SubStr(str, 1, k-1) . esc_seq[ch] . SubStr(str, k+2) - else if (ch = "u") { - hex := "0x" . SubStr(str, z+2, 4) + else if (ch = "u") + { + hex := "0x" . SubStr(str, k+2, 4) if !(A_IsUnicode || (Abs(hex) < 0x100)) - continue ; throw Exception() ??? - str := SubStr(str, 1, z-1) . Chr(hex) . SubStr(str, z+6) + continue + str := SubStr(str, 1, k-1) . Chr(hex) . SubStr(str, k+6) - } else throw Exception("Bad string") + } + else + throw Exception("Invalid escape sequence: '\%ch%'") } - strings.Insert(str) + ObjPush(strings, str) } - pos := 1, ch := " " - key := dummy := [] - stack := [result:=new JSON.array], assert := "{[""tfn0123456789-" - while (ch != "", ch:=SubStr(src, pos, 1), pos+=1) { + ;// Check for missing opening/closing brace(s) + if InStr(src, '{') || InStr(src, '}') + { + StrReplace(src, '{', '{', c1), StrReplace(src, '}', '}', c2) + if (c1 != c2) + throw Exception("Missing %Abs(c1-c2)% %(c1 > c2 ? 'clos' : 'open')%ing brace(s)") + } + ;// Check for missing opening/closing bracket(s) + if InStr(src, '[') || InStr(src, ']') + { + StrReplace(src, '[', '[', c1), StrReplace(src, ']', ']', c2) + if (c1 != c2) + throw Exception("Missing %Abs(c1-c2)% %(c1 > c2 ? 'clos' : 'open')%ing bracket(s)") + } + + static t := "true", f := "false", n := "null", null := "" + jbase := jsonize ? { "{":JSON.object, "[":JSON.array } : { "{":0, "[":0 } + , pos := 0 + , key := "", is_key := false + , stack := [tree := []] + , is_arr := {(tree): 1} + , next := first ;// '"{[01234567890-tfn' + while ((ch := SubStr(src, ++pos, 1)) != "") + { + if InStr(" `t`n`r", ch) + continue + if !InStr(next, ch) + throw Exception("Unexpected char: '%ch%'") - while (ch != "" && InStr(" `t`r`n", ch)) ; skip whitespace - ch := SubStr(src, pos, 1), pos += 1 - ;pos := RegExMatch(src, "\S", ch, pos)+1 - /* - Check if the current character is expected or not - Acts as a simple validator for badly formatted JSON string - */ - if (assert != "") { - if !InStr(assert, ch) - throw Exception("Unexpected '" . ch . "'", -1) - assert := "" - } + is_array := is_arr[obj := stack[1]] - if InStr(":,", ch) { - assert := "{[""tfn0123456789-" - continue + if InStr("{[", ch) + { + val := (proto := jbase[ch]) ? new proto : {} + , obj[is_array? ObjLength(obj)+1 : key] := val + , ObjInsertAt(stack, 1, val) + , is_arr[val] := !(is_key := ch == "{") + , next := is_key ? '"}' : '"{[]0123456789-tfn' } - if InStr("{[", ch) { ; object|array - opening - cont := stack[1], base := (ch == "{" ? "object" : "array") - len := Round(ObjMaxIndex(cont)) - stack.Insert(1, cont[key == dummy ? len+1 : key] := new JSON[base]) - key := dummy - assert := (ch == "{" ? """}" : "]{[""tfn0123456789-") - continue - - } else if InStr("}]", ch) { ; object|array - closing - stack.Remove(1), assert := "]}," - continue - - } else if (ch == """") { ; string - str := strings.Remove(1), cont := stack[1] - if (key == dummy) { - if (cont.__Class == "JSON.array") { - key := Round(ObjMaxIndex(cont))+1 - } else { - key := str, assert := ":" + else if InStr("}]", ch) + { + ObjRemoveAt(stack, 1) + , next := is_arr[stack[1]] ? "]," : "}," + } + + else if InStr(",:", ch) + { + if (obj == tree) + throw Exception("Unexpected char: '%ch%' -> there is no container object.") + next := '"{[0123456789-tfn', is_key := (!is_array && ch == ",") + } + + else + { + if (ch == '"') + { + val := ObjRemoveAt(strings, 1) + if is_key + { + key := val, next := ":" continue } } - cont[key] := str, key := dummy - assert := "," . (cont.__Class == "JSON.object" ? "}" : "]") - continue - - } else if (ch >= 0 && ch <= 9) || (ch == "-") { ; number - if !RegExMatch(src, "-?\d+(\.\d+)?((?i)E[-+]?\d+)?", num, pos-1) - throw Exception("Bad number", -1) - pos += StrLen(num)-1 - cont := stack[1], len := Round(ObjMaxIndex(cont)) - cont[key == dummy ? len+1 : key] := num - key := dummy - assert := "," . (cont.__Class == "JSON.object" ? "}" : "]") - continue - - } else if InStr("tfn", ch, true) { ; true|false|null - val := {t:"true", f:"false", n:"null"}[ch] - ; advance to next char, first char has already been validated - while (c:=SubStr(val, A_Index+1, 1)) { - ch := SubStr(src, pos, 1), pos += 1 - if !(ch == c) ; case-sensitive comparison - throw Exception("Expected '" c "' instead of " ch) + else + { + val := SubStr(src, pos, (SubStr(src, pos) ~= "[\]\},\s]|$")-1) + , pos += StrLen(val)-1 + if InStr("tfn", ch, 1) + { + if !(val == %ch%) + throw Exception("Expected '%(%ch%)%' instead of '%val%'") + val := %val% + } + else if (Abs(val) == "") + throw Exception("Invalid number: %val%") + val += 0 } - - cont := stack[1], len := Round(ObjMaxIndex(cont)) - cont[key == dummy ? len+1 : key] := %val% - key := dummy - assert := "," . (cont.__Class == "JSON.object" ? "}" : "]") - continue - - } else { - if (ch != "") - throw Exception("Unexpected '" . ch . "'", -1) - else break + obj[is_array? ObjLength(obj)+1 : key] := val + , next := is_array ? "]," : "}," } } - return result[1] + return tree[1] } - /* - Returns a string representation of an AHK object. - The 'i' (indent) parameter allows 'pretty printing'. Specify any char(s) - to use as indentation. - Usage: JSON.stringify(object, "`t") ; use tab as indentation - JSON.stringify(object, " ") ; 4-spaces indentation - JSON.stringify(object) ; no indentation - Remarks: - JSON.object and JSON.array instance(s) may call this method, automatically - passing itself as the first parameter. If indententation is specified, - nested arrays [] are in OTB-style. - As per JSON spec, hex numbers are treated as strings - doing something - like: 'JSON.stringify([0xffff])' will output '0xffff' as decimal. To - output as string, wrap it in quotes: 'JSON.stringify(["0xffff"])' - 0, 1 and ""(blank) are output as false, true and null respectively. - */ - stringify(obj:="", i:="", lvl:=1) { - if IsObject(obj) { - if (ComObjValue(x) != "") ; COM Object - throw Exception("COM Object(s) are not supported.") - if (obj.base == JSON.object || obj.base == JSON.array) - arr := (obj.base == JSON.array ? true : false) - else for k in obj - arr := (k == A_Index) - until !arr - n := i ? "`n" : (i:="", t:="") - Loop, % i ? lvl : 0 - t .= i + stringify(obj:="", indent:="", lvl:=1) + { + type := Type(obj) + if (type == "Object") + { + is_array := 0 + for k in obj + if !(is_array := (k == A_Index)), break + + if (Abs(indent) != "") + { + if (indent < 0) + throw "Indent parameter must be a postive integer" + spaces := indent, indent := "" + Loop % spaces + indent .= " " + } + indt := "" + Loop, % indent ? lvl : 0 + indt .= indent + + lvl += 1, out := "" ;// make #Warn happy + for k, v in obj + { + if IsObject(k) || (k == ""), throw Exception("Invalid JSON key") + + if !is_array + out .= ( Type(k) == "String" ? JSON.stringify(k) : '"%k%"' ) ;// key + . ( indent ? ": " : ":" ) ;// token + out .= JSON.stringify(v, indent, lvl) ;// value + . ( indent ? ",`n%indt%" : "," ) ;// token + indent + } - lvl += 1 - for k, v in obj { - if IsObject(k) || (k == "") - throw Exception("Invalid key.", -1) - if !arr - ; integer key(s) are automatically wrapped in quotes - key := k+0 == k ? """" . k . """" : JSON.stringify(k) - val := JSON.stringify(v, i, lvl) - s := "," . (n ? n : " ") . t - str .= arr ? (val . s) - : key . ":" . ((IsObject(v) && InStr(val, "{") == 1) ? n . t : " ") . val . s + if (out != "") + { + out := Trim(out, ",`n%indent%") + if (indent != "") + out := "`n%indt%%out%`n" . SubStr(indt, StrLen(indent)+1) } - str := n . t . Trim(str, ",`n`t ") . n . SubStr(t, StrLen(i)+1) - return arr ? "[" str "]" : "{" str "}" + + return is_array ? "[%out%]" : "{%out%}" } - ; true|false|null - else if InStr(01, obj) || (obj == "") - return {"": "null", 0:"false", 1:"true"}[obj] - ; String - else if [obj].GetCapacity(1) { - if obj is float - return obj - esc_char := { - (Join - """": "\""", - "/": "\/", - Chr(08): "\b", - Chr(12): "\f", - "`n": "\n", - "`r": "\r", - "`t": "\t" - )} - - StringReplace, obj, obj, \, \\, A - for k, v in esc_char - StringReplace, obj, obj, % k, % v, A + else if (type == "Integer" || type == "Float") + return obj - while RegExMatch(obj, "[^\x20-\x7e]", ch) { - ustr := Asc(ch), esc_ch := "\u", n := 12 - while (n >= 0) - esc_ch .= Chr((x:=(ustr>>n) & 15) + (x<10 ? 48 : 55)) - , n -= 4 - StringReplace, obj, obj, % ch, % esc_ch, A + else if (type == "String") + { + static esc_seq := { + (Q + '"': '\"', + '/': '\/', + '`b': '\b', + '`f': '\f', + '`n': '\n', + '`r': '\r', + '`t': '\t' + )} + if (obj != "") + { + obj := StrReplace(obj, "\", "\\") + for k, v in esc_seq + obj := StrReplace(obj, k, v) + while RegExMatch(obj, "[^\x20-\x7e]", wstr) + { + ucp := Ord(wstr.Value), hex := "\u", n := 16 + while ((n -= 4) >= 0) + hex .= Chr( (x := (ucp >> n) & 15) + (x < 10 ? 48 : 55) ) + obj := StrReplace(obj, wstr.Value, hex) + } } - return """" . obj . """" + return '"%obj%"' } - ; Number - if obj is xdigit - if obj is not digit - obj := """" . obj . """" - - return obj + throw Exception("Unsupported type: '%type%'") } - /* - Base object for objects {} created during parsing. The user may also manually - create an insatnce of this class. The sole purpose of wrapping objects {} as - JSON.object instance is to allow enumeration of key-value pairs in the order - they were created. The len() method may be used to get the total count of - key-value pairs. - Usage: Instances are automatically created during parsing. The user may - use the 'new' operator to create a JSON.object object manually. - --start-of-code-- - obj := new JSON.object("key1", "value1", "key2", "value2") - obj["key3"] := "Add a new key-value pair" - MsgBox, % obj.stringify() ; display as string - ; '{"key1": "value1", "key2": "value2", "key3": "Add a new key-value pair"}' - --end-of-code-- - */ + class object { - - __New(p*) { - ObjInsert(this, "_", []) - if Mod(p.MaxIndex(), 2) - p.Insert("") - Loop, % p.MaxIndex()//2 - this[p[A_Index*2-1]] := p[A_Index*2] - } - - __Set(k, v, p*) { - this._.Insert(k) - } - - _NewEnum() { - return new JSON.object.Enum(this) + __New(args*) + { + ObjRawSet(this, "_", []) + if ((len := ObjLength(args)) & 1) + throw Exception("Invalid number of parameters") + Loop len//2 + this[args[A_Index*2-1]] := args[A_Index*2] } - Insert(k, v) { - return this[k] := v + __Set(key, val, args*) + { + ObjPush(this._, key) } - Remove(k) { ; restrict to single key - if !ObjHasKey(this, k) - return - for i, v in this._ - continue - until (v = k) - this._.Remove(i) - if k is integer - return ObjRemove(this, k, "") - return ObjRemove(this, k) + Remove(args*) + { + is_range := ObjLength(args) > 1 + scs := A_StringCaseSense, A_StringCaseSense := "Off" + i := -1 + for index, key in ObjClone(this._) { + if is_range? (key >= args[1] && key <= args[2]) : (key = args[1]) + { + ObjRemoveAt(this._, index-(i+=1)) + if !is_range, break ;// single key only + } + } + /* Alternative way + keys := [] + for index, key in this._ { + if is_range? (key >= args[1] && key <= args[2]) : (key = args[1]) + continue + ObjPush(keys, key) + } + ObjRawSet(this, "_", keys) + */ + A_StringCaseSense := scs + return ObjRemove(this, args*) } - len() { - return this._.MaxIndex() + Count() + { + return ObjLength(this._) } - stringify(i:="") { + stringify(i:="") + { return JSON.stringify(this, i) } - class Enum + _NewEnum() { + static proto := { "Next": JSON.object.Next } + return { base: proto, enum: this._._NewEnum(), obj: this } + } - __New(obj) { - this.obj := obj - this.enum := obj._._NewEnum() - } - ; Lexikos' ordered array workaround - Next(ByRef k, ByRef v:="") { - if (r:=this.enum.Next(i, k)) - v := this.obj[k] - return r - } + Next(ByRef key, ByRef val:="") + { + if (ret := this.enum.Next(i, key)) + val := this.obj[key] + return ret } } - /* - Base object for arrays [] created during parsing. Same as JSON.object above. - */ + class array { - - __New(p*) { - for k, v in p - this.Insert(v) + __New(args*) + { + args.base := this.base + return args } - stringify(i:="") { - return JSON.stringify(this, i) + stringify(indent:="") + { + return JSON.stringify(this, indent) } } } \ No newline at end of file