diff --git a/Example_JSON.ahk b/Example_JSON.ahk new file mode 100644 index 0000000..995f893 --- /dev/null +++ b/Example_JSON.ahk @@ -0,0 +1,49 @@ +#Include %A_LineFile%\..\JSON.ahk + +json_str = +( +{ + "str": "Hello World", + "num": 12345, + "float": 123.5, + "true": true, + "false": false, + "null": null, + "array": [ + "Auto", + "Hot", + "key" + ], + "object": { + "A": "Auto", + "H": "Hot", + "K": "key" + } +} +) + +parsed := JSON.Load(json_str) + +parsed_out := Format(" +(Join`r`n +String: {} +Number: {} +Float: {} +true: {} +false: {} +null: {} +array: [{}, {}, {}] +object: {{}A:""{}"", H:""{}"", K:""{}""{}} +)" +, parsed.str, parsed.num, parsed.float, parsed.true, parsed.false, parsed.null +, parsed.array[1], parsed.array[2], parsed.array[3] +, parsed.object.A, parsed.object.H, parsed.object.K) + +stringified := JSON.Dump(parsed,, 4) +stringified := StrReplace(stringified, "`n", "`r`n") ; for display purposes only + +ListVars +WinWaitActive ahk_class AutoHotkey +ControlSetText Edit1, [PARSED]`r`n%parsed_out%`r`n`r`n[STRINGIFIED]`r`n%stringified% +WinWaitClose +return \ No newline at end of file diff --git a/JSON.ahk b/JSON.ahk index 55297f6..975c528 100644 --- a/JSON.ahk +++ b/JSON.ahk @@ -1,326 +1,374 @@ -/* -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 -*/ +/** + * Lib: JSON.ahk + * JSON lib for AutoHotkey. + * Version: + * v2.1.3 [updated 04/18/2016 (MM/DD/YYYY)] + * License: + * WTFPL [http://wtfpl.net/] + * Requirements: + * Latest version of AutoHotkey (v1.1+ or v2.0-a+) + * Installation: + * Use #Include JSON.ahk or copy into a function library folder and then + * use #Include + * Links: + * GitHub: - https://github.com/cocobelgica/AutoHotkey-JSON + * Forum Topic - http://goo.gl/r0zI8t + * Email: - cocobelgica gmail com + */ + + +/** + * Class: JSON + * The JSON object contains methods for parsing JSON and converting values + * to JSON. Callable - NO; Instantiable - YES; Subclassable - YES; + * Nestable(via #Include) - NO. + * Methods: + * Load() - see relevant documentation before method definition header + * Dump() - see relevant documentation before method definition header + */ 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" - )} - 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. - */ - i := 0, strings := [] - while (i:=InStr(src, """",, i+1)) { - j := i - while (j:=InStr(src, """",, j+1)) { - str := SubStr(src, i+1, j-i-1) - StringReplace, str, str, \\, \u005C, A - if (SubStr(str, 0) != "\") - break - } + /** + * Method: Load + * Parses a JSON string into an AHK value + * Syntax: + * value := JSON.Load( text [, reviver ] ) + * Parameter(s): + * value [retval] - parsed value + * text [in, ByRef] - JSON formatted string + * reviver [in, opt] - function object, similar to JavaScript's + * JSON.parse() 'reviver' parameter + */ + class Load extends JSON.Functor + { + Call(self, ByRef text, reviver:="") + { + this.rev := IsObject(reviver) ? reviver : false + ; Object keys(and array indices) are temporarily stored in arrays so that + ; we can enumerate them in the order they appear in the document/text instead + ; of alphabetically. Skip if no reviver function is specified. + this.keys := this.rev ? {} : false - 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) - - else if (ch = "u") { - hex := "0x" . SubStr(str, z+2, 4) - if !(A_IsUnicode || (Abs(hex) < 0x100)) - continue ; throw Exception() ??? - str := SubStr(str, 1, z-1) . Chr(hex) . SubStr(str, z+6) - - } else throw Exception("Bad string") - } - strings.Insert(str) - } - - pos := 1, ch := " " - key := dummy := [] - stack := [result:=new JSON.array], assert := "{[""tfn0123456789-" - while (ch != "", ch:=SubStr(src, pos, 1), pos+=1) { - - 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 := "" - } - - if InStr(":,", ch) { - assert := "{[""tfn0123456789-" - continue - } + static quot := Chr(34), bashq := "\" . quot + , json_value := quot . "{[01234567890-tfn" + , json_value_or_array_closing := quot . "{[]01234567890-tfn" + , object_key_or_object_closing := quot . "}" - 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 + key := "" + is_key := false + root := {} + stack := [root] + next := json_value + pos := 0 + + while ((ch := SubStr(text, ++pos, 1)) != "") { + if InStr(" `t`r`n", ch) + continue + if !InStr(next, ch, 1) + this.ParseError(next, text, pos) + + holder := stack[1] + is_array := holder.IsArray + + if InStr(",:", ch) { + next := (is_key := !is_array && ch == ",") ? quot : json_value + + } else if InStr("}]", ch) { + ObjRemoveAt(stack, 1) + next := stack[1]==root ? "" : stack[1].IsArray ? ",]" : ",}" + + } else { + if InStr("{[", ch) { + ; Check if Array() is overridden and if its return value has + ; the 'IsArray' property. If so, Array() will be called normally, + ; otherwise, use a custom base object for arrays + static json_array := Func("Array").IsBuiltIn || ![].IsArray ? {IsArray: true} : 0 + + ; sacrifice readability for minor(actually negligible) performance gain + (ch == "{") + ? ( is_key := true + , value := {} + , next := object_key_or_object_closing ) + ; ch == "[" + : ( value := json_array ? new json_array : [] + , next := json_value_or_array_closing ) + + ObjInsertAt(stack, 1, value) + + if (this.keys) + this.keys[value] := [] + } else { - key := str, assert := ":" - continue - } + if (ch == quot) { + i := pos + while (i := InStr(text, quot,, i+1)) { + value := StrReplace(SubStr(text, pos+1, i-pos-1), "\\", "\u005c") + + static tail := A_AhkVersion<"2" ? 0 : -1 + if (SubStr(value, tail) != "\") + break + } + + if (!i) + this.ParseError("'", text, pos) + + value := StrReplace(value, "\/", "/") + , value := StrReplace(value, bashq, quot) + , value := StrReplace(value, "\b", "`b") + , value := StrReplace(value, "\f", "`f") + , value := StrReplace(value, "\n", "`n") + , value := StrReplace(value, "\r", "`r") + , value := StrReplace(value, "\t", "`t") + + pos := i ; update pos + + i := 0 + while (i := InStr(value, "\",, i+1)) { + if !(SubStr(value, i+1, 1) == "u") + this.ParseError("\", text, pos - StrLen(SubStr(value, i+1))) + + uffff := Abs("0x" . SubStr(value, i+2, 4)) + if (A_IsUnicode || uffff < 0x100) + value := SubStr(value, 1, i-1) . Chr(uffff) . SubStr(value, i+6) + } + + if (is_key) { + key := value, next := ":" + continue + } + + } else { + value := SubStr(text, pos, i := RegExMatch(text, "[\]\},\s]|$",, pos)-pos) + + static number := "number", integer :="integer" + if value is %number% + { + if value is %integer% + value += 0 + } + else if (value == "true" || value == "false") + value := %value% + 0 + else if (value == "null") + value := "" + else + ; we can do more here to pinpoint the actual culprit + ; but that's just too much extra work. + this.ParseError(next, text, pos, i) + + pos += i-1 + } + + next := holder==root ? "" : is_array ? ",]" : ",}" + } ; If InStr("{[", ch) { ... } else + + is_array? key := ObjPush(holder, value) : holder[key] := value + + if (this.keys && this.keys.HasKey(holder)) + this.keys[holder].Push(key) } - 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) - } + } ; while ( ... ) - cont := stack[1], len := Round(ObjMaxIndex(cont)) - cont[key == dummy ? len+1 : key] := %val% - key := dummy - assert := "," . (cont.__Class == "JSON.object" ? "}" : "]") - continue + return this.rev ? this.Walk(root, "") : root[""] + } + + ParseError(expect, ByRef text, pos, len:=1) + { + static quot := Chr(34), qurly := quot . "}" - } else { - if (ch != "") - throw Exception("Unexpected '" . ch . "'", -1) - else break - } + line := StrSplit(SubStr(text, 1, pos), "`n", "`r").Length() + col := pos - InStr(text, "`n",, -(StrLen(text)-pos+1)) + msg := Format("{1}`n`nLine:`t{2}`nCol:`t{3}`nChar:`t{4}" + , (expect == "") ? "Extra data" + : (expect == "'") ? "Unterminated string starting at" + : (expect == "\") ? "Invalid \escape" + : (expect == ":") ? "Expecting ':' delimiter" + : (expect == quot) ? "Expecting object key enclosed in double quotes" + : (expect == qurly) ? "Expecting object key enclosed in double quotes or object closing '}'" + : (expect == ",}") ? "Expecting ',' delimiter or object closing '}'" + : (expect == ",]") ? "Expecting ',' delimiter or array closing ']'" + : InStr(expect, "]") ? "Expecting JSON value or array closing ']'" + : "Expecting JSON value(string, number, true, false, null, object or array)" + , line, col, pos) + + static offset := A_AhkVersion<"2" ? -3 : -4 + throw Exception(msg, offset, SubStr(text, pos, len)) } - return result[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 - - 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 + + Walk(holder, key) + { + value := holder[key] + if IsObject(value) { + for i, k in this.keys[value] { + ; check if ObjHasKey(value, k) ?? + v := this.Walk(value, k) + if (v != JSON.Undefined) + value[k] := v + else + ObjDelete(value, k) + } } - str := n . t . Trim(str, ",`n`t ") . n . SubStr(t, StrLen(i)+1) - return arr ? "[" str "]" : "{" str "}" - } - ; 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 - - 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 - } - return """" . obj . """" + return this.rev.Call(holder, key, value) } - ; Number - if obj is xdigit - if obj is not digit - obj := """" . obj . """" - - return obj } - /* - 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 + + /** + * Method: Dump + * Converts an AHK value into a JSON string + * Syntax: + * str := JSON.Dump( value [, replacer, space ] ) + * Parameter(s): + * str [retval] - JSON representation of an AHK value + * value [in] - any value(object, string, number) + * replacer [in, opt] - function object, similar to JavaScript's + * JSON.stringify() 'replacer' parameter + * space [in, opt] - similar to JavaScript's JSON.stringify() + * 'space' parameter + */ + class Dump extends JSON.Functor { - - __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] - } + Call(self, value, replacer:="", space:="") + { + this.rep := IsObject(replacer) ? replacer : "" - __Set(k, v, p*) { - this._.Insert(k) - } + this.gap := "" + if (space) { + static integer := "integer" + if space is %integer% + Loop, % ((n := Abs(space))>10 ? 10 : n) + this.gap .= " " + else + this.gap := SubStr(space, 1, 10) - _NewEnum() { - return new JSON.object.Enum(this) - } + this.indent := "`n" + } - Insert(k, v) { - return this[k] := v + return this.Str({"": value}, "") } - 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) - } + Str(holder, key) + { + value := holder[key] - len() { - return this._.MaxIndex() - } + if (this.rep) + value := this.rep.Call(holder, key, ObjHasKey(holder, key) ? value : JSON.Undefined) + + if IsObject(value) { + ; Check object type, skip serialization for other object types such as + ; ComObject, Func, BoundFunc, FileObject, RegExMatchObject, Property, etc. + static type := A_AhkVersion<"2" ? "" : Func("Type") + if (type ? type.Call(value) == "Object" : ObjGetCapacity(value) != "") { + if (this.gap) { + stepback := this.indent + this.indent .= this.gap + } - stringify(i:="") { - return JSON.stringify(this, i) + is_array := value.IsArray + ; Array() is not overridden, rollback to old method of + ; identifying array-like objects. Due to the use of a for-loop + ; sparse arrays such as '[1,,3]' are detected as objects({}). + if (!is_array) { + for i in value + is_array := i == A_Index + until !is_array + } + + str := "" + if (is_array) { + Loop, % value.Length() { + if (this.gap) + str .= this.indent + + v := this.Str(value, A_Index) + str .= (v != "") ? v . "," : "null," + } + } else { + colon := this.gap ? ": " : ":" + for k in value { + v := this.Str(value, k) + if (v != "") { + if (this.gap) + str .= this.indent + + str .= this.Quote(k) . colon . v . "," + } + } + } + + if (str != "") { + str := RTrim(str, ",") + if (this.gap) + str .= stepback + } + + if (this.gap) + this.indent := stepback + + return is_array ? "[" . str . "]" : "{" . str . "}" + } + + } else ; is_number ? value : "value" + return ObjGetCapacity([value], 1)=="" ? value : this.Quote(value) } - class Enum + Quote(string) { + static quot := Chr(34), bashq := "\" . quot - __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 + if (string != "") { + string := StrReplace(string, "\", "\\") + ; , string := StrReplace(string, "/", "\/") ; optional in ECMAScript + , string := StrReplace(string, quot, bashq) + , string := StrReplace(string, "`b", "\b") + , string := StrReplace(string, "`f", "\f") + , string := StrReplace(string, "`n", "\n") + , string := StrReplace(string, "`r", "\r") + , string := StrReplace(string, "`t", "\t") + + static rx_escapable := A_AhkVersion<"2" ? "O)[^\x20-\x7e]" : "[^\x20-\x7e]" + while RegExMatch(string, rx_escapable, m) + string := StrReplace(string, m.Value, Format("\u{1:04x}", Ord(m.Value))) } + + return quot . string . quot } } - /* - Base object for arrays [] created during parsing. Same as JSON.object above. - */ - class array + + /** + * Property: Undefined + * Proxy for 'undefined' type + * Syntax: + * undefined := JSON.Undefined + * Remarks: + * For use with reviver and replacer functions since AutoHotkey does not + * have an 'undefined' type. Returning blank("") or 0 won't work since these + * can't be distnguished from actual JSON values. This leaves us with objects. + * Replacer() - the caller may return a non-serializable AHK objects such as + * ComObject, Func, BoundFunc, FileObject, RegExMatchObject, and Property to + * mimic the behavior of returning 'undefined' in JavaScript but for the sake + * of code readability and convenience, it's better to do 'return JSON.Undefined'. + * Internally, the property returns a ComObject with the variant type of VT_EMPTY. + */ + Undefined[] { - - __New(p*) { - for k, v in p - this.Insert(v) + get { + static empty := {}, vt_empty := ComObject(0, &empty, 1) + return vt_empty } + } - stringify(i:="") { - return JSON.stringify(this, i) + class Functor + { + __Call(method, ByRef arg, args*) + { + ; When casting to Call(), use a new instance of the "function object" + ; so as to avoid directly storing the properties(used across sub-methods) + ; into the "function object" itself. + if IsObject(method) + return (new this).Call(method, arg, args*) + else if (method == "") + return (new this).Call(arg, args*) } } } \ No newline at end of file diff --git a/Jxon.ahk b/Jxon.ahk new file mode 100644 index 0000000..5126a39 --- /dev/null +++ b/Jxon.ahk @@ -0,0 +1,212 @@ +Jxon_Load(ByRef src, args*) +{ + static q := Chr(34) + + key := "", is_key := false + stack := [ tree := [] ] + is_arr := { (tree): 1 } + next := q . "{[01234567890-tfn" + pos := 0 + while ( (ch := SubStr(src, ++pos, 1)) != "" ) + { + if InStr(" `t`n`r", ch) + continue + if !InStr(next, ch, true) + { + ln := ObjLength(StrSplit(SubStr(src, 1, pos), "`n")) + col := pos - InStr(src, "`n",, -(StrLen(src)-pos+1)) + + msg := Format("{}: line {} col {} (char {})" + , (next == "") ? ["Extra data", ch := SubStr(src, pos)][1] + : (next == "'") ? "Unterminated string starting at" + : (next == "\") ? "Invalid \escape" + : (next == ":") ? "Expecting ':' delimiter" + : (next == q) ? "Expecting object key enclosed in double quotes" + : (next == q . "}") ? "Expecting object key enclosed in double quotes or object closing '}'" + : (next == ",}") ? "Expecting ',' delimiter or object closing '}'" + : (next == ",]") ? "Expecting ',' delimiter or array closing ']'" + : [ "Expecting JSON value(string, number, [true, false, null], object or array)" + , ch := SubStr(src, pos, (SubStr(src, pos)~="[\]\},\s]|$")-1) ][1] + , ln, col, pos) + + throw Exception(msg, -1, ch) + } + + is_array := is_arr[obj := stack[1]] + + if i := InStr("{[", ch) + { + val := (proto := args[i]) ? new proto : {} + is_array? ObjPush(obj, val) : obj[key] := val + ObjInsertAt(stack, 1, val) + + is_arr[val] := !(is_key := ch == "{") + next := q . (is_key ? "}" : "{[]0123456789-tfn") + } + + else if InStr("}]", ch) + { + ObjRemoveAt(stack, 1) + next := stack[1]==tree ? "" : is_arr[stack[1]] ? ",]" : ",}" + } + + else if InStr(",:", ch) + { + is_key := (!is_array && ch == ",") + next := is_key ? q : q . "{[0123456789-tfn" + } + + else ; string | number | true | false | null + { + if (ch == q) ; string + { + i := pos + while i := InStr(src, q,, i+1) + { + val := StrReplace(SubStr(src, pos+1, i-pos-1), "\\", "\u005C") + static end := A_AhkVersion<"2" ? 0 : -1 + if (SubStr(val, end) != "\") + break + } + if !i ? (pos--, next := "'") : 0 + continue + + pos := i ; update pos + + val := StrReplace(val, "\/", "/") + , val := StrReplace(val, "\" . q, q) + , val := StrReplace(val, "\b", "`b") + , val := StrReplace(val, "\f", "`f") + , val := StrReplace(val, "\n", "`n") + , val := StrReplace(val, "\r", "`r") + , val := StrReplace(val, "\t", "`t") + + i := 0 + while i := InStr(val, "\",, i+1) + { + if (SubStr(val, i+1, 1) != "u") ? (pos -= StrLen(SubStr(val, i)), next := "\") : 0 + continue 2 + + ; \uXXXX - JSON unicode escape sequence + xxxx := Abs("0x" . SubStr(val, i+2, 4)) + if (A_IsUnicode || xxxx < 0x100) + val := SubStr(val, 1, i-1) . Chr(xxxx) . SubStr(val, i+6) + } + + if is_key + { + key := val, next := ":" + continue + } + } + + else ; number | true | false | null + { + val := SubStr(src, pos, i := RegExMatch(src, "[\]\},\s]|$",, pos)-pos) + + ; For numerical values, numerify integers and keep floats as is. + ; I'm not yet sure if I should numerify floats in v2.0-a ... + static number := "number", integer := "integer" + if val is %number% + { + if val is %integer% + val += 0 + } + ; in v1.1, true,false,A_PtrSize,A_IsUnicode,A_Index,A_EventInfo, + ; SOMETIMES return strings due to certain optimizations. Since it + ; is just 'SOMETIMES', numerify to be consistent w/ v2.0-a + else if (val == "true" || val == "false") + val := %value% + 0 + ; AHK_H has built-in null, can't do 'val := %value%' where value == "null" + ; as it would raise an exception in AHK_H(overriding built-in var) + else if (val == "null") + val := "" + ; any other values are invalid, continue to trigger error + else if (pos--, next := "#") + continue + + pos += i-1 + } + + is_array? ObjPush(obj, val) : obj[key] := val + next := obj==tree ? "" : is_array ? ",]" : ",}" + } + } + + return tree[1] +} + +Jxon_Dump(obj, indent:="", lvl:=1) +{ + static q := Chr(34) + + if IsObject(obj) + { + static Type := Func("Type") + if Type ? (Type.Call(obj) != "Object") : (ObjGetCapacity(obj) == "") + throw Exception("Object type not supported.", -1, Format("", &obj)) + + is_array := 0 + for k in obj + is_array := k == A_Index + until !is_array + + static integer := "integer" + if indent is %integer% + { + if (indent < 0) + throw Exception("Indent parameter must be a postive integer.", -1, indent) + 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 object key.", -1, k ? Format("", &obj) : "") + + if !is_array + out .= ( ObjGetCapacity([k], 1) ? Jxon_Dump(k) : q . k . q ) ;// key + . ( indent ? ": " : ":" ) ; token + padding + out .= Jxon_Dump(v, indent, lvl) ; value + . ( indent ? ",`n" . indt : "," ) ; token + indent + } + + if (out != "") + { + out := Trim(out, ",`n" . indent) + if (indent != "") + out := "`n" . indt . out . "`n" . SubStr(indt, StrLen(indent)+1) + } + + return is_array ? "[" . out . "]" : "{" . out . "}" + } + + ; Number + else if (ObjGetCapacity([obj], 1) == "") + return obj + + ; String (null -> not supported by AHK) + if (obj != "") + { + obj := StrReplace(obj, "\", "\\") + , obj := StrReplace(obj, "/", "\/") + , obj := StrReplace(obj, q, "\" . q) + , obj := StrReplace(obj, "`b", "\b") + , obj := StrReplace(obj, "`f", "\f") + , obj := StrReplace(obj, "`n", "\n") + , obj := StrReplace(obj, "`r", "\r") + , obj := StrReplace(obj, "`t", "\t") + + static needle := (A_AhkVersion<"2" ? "O)" : "") . "[^\x20-\x7e]" + while RegExMatch(obj, needle, m) + obj := StrReplace(obj, m[0], Format("\u{:04X}", Ord(m[0]))) + } + + return q . obj . q +} \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..fd3d535 --- /dev/null +++ b/README.md @@ -0,0 +1,93 @@ +# JSON and Jxon + +#### [JSON](http://json.org/) lib for [AutoHotkey](http://ahkscript.org/) + +Requirements: Latest version of AutoHotkey _(v1.1+ or v2.0-a+)_ + +Version: v2.1.1 _(updated 01/30/2016)_ + +License: [WTFPL](http://wtfpl.net/) + + +- - - + +## JSON.ahk (class) +Works on both AutoHotkey _v1.1_ and _v2.0a_ + +### Installation +Use `#Include JSON.ahk` or copy into a [function library folder](http://ahkscript.org/docs/Functions.htm#lib) and use `#Include `. + +- - - + +#### .Load() +Parses a JSON string into an AHK value + +#### Syntax: + + value := JSON.Load( text [, reviver ] ) + + +#### Return Value: +An AutoHotkey value _(object, string, number)_ + +#### Parameter(s): + * **text** [in] - JSON formatted string + * **reviver** [in, opt] - function object, prescribes how the value originally produced by parsing is transformed, before being returned. Similar to JavaScript's `JSON.parse()` _reviver_ parameter. + +- - - + +#### .Dump() +Converts an AHK value into a JSON string + +#### Syntax: + + str := JSON.Dump( value, [, replacer, space ] ) + + +#### Return Value: +A JSON formatted string + +#### Parameter(s): + * **value** [in] - AutoHotkey value _(object, string, number)_ + * **replacer** [in, opt] - function object, alters the behavior of the stringification process. Similar to JavaScript's `JSON.stringify()` _replacer_ parameter. + * **space** [in, opt] -if _space_ is a non-negative integer or string, then JSON array elements and object members will be pretty-printed with that indent level. Blank( ``""`` ) _(the default)_ or ``0`` selects the most compact representation. Using a positive integer _space_ indents that many spaces per level, this number is capped at 10 if it's larger than that. If _space_ is a string (such as ``"`t"``), the string _(or the first 10 characters of the string, if it's longer than that)_ is used to indent each level. + +- - - + +## Jxon.ahk (function) +Similar to the JSON class above just implemented as a function. ~~Unlike JSON (class) above, this implementation provides _reading from_ and _writing to_ file~~(Removed `Jxon_Read` and `Jxon_Write`). Works on both AutoHotkey _v1.1_ and _v2.0a_ + +### Installation +Use `#Include Jxon.ahk` or `#Include `. Must be copied into a [function library folder](http://ahkscript.org/docs/Functions.htm#lib) for the latter. + +- - - + +### Jxon_Load() +Deserialize _src_ (a JSON formatted string) to an AutoHotkey object + +#### Syntax: + + obj := Jxon_Load( ByRef src [ , object_base := "", array_base := "" ] ) + + +#### Parameter(s): + * **src** [in, ByRef] - JSON formatted string or path to the file containing JSON formatted string. + * **object_base** [in, opt] - an object to use as prototype for objects( ``{}`` ) created during parsing. + * **array_base** [in, opt] - an object to use as prototype for arrays( ``[]`` ) created during parsing. + +- - - + +### Jxon_Dump() +Serialize _obj_ to a JSON formatted string + +#### Syntax: + + str := Jxon_Dump( obj [ , indent := "" ] ) + + +#### Return Value: +A JSON formatted string. + +#### Parameter(s): + * **obj** [in] - this argument has the same meaning as in _JSON.Dump()_ + * **indent** [in, opt] - this argument has the same meaning as in _JSON.Dump()_ \ No newline at end of file