From 65caaa1f930b81607619425f7e7cfb53f4623f4d Mon Sep 17 00:00:00 2001 From: Coco Belgica Date: Tue, 15 Apr 2014 22:16:33 +0800 Subject: [PATCH 01/56] Fix bug: - JSON.object.len() returning blank("") instead of 0. --- JSON.ahk | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/JSON.ahk b/JSON.ahk index 55297f6..737676d 100644 --- a/JSON.ahk +++ b/JSON.ahk @@ -286,7 +286,7 @@ class JSON } len() { - return this._.MaxIndex() + return Round(this._.MaxIndex()) } stringify(i:="") { From dd8bb645896eb899306463f456e9eaafbd34ff04 Mon Sep 17 00:00:00 2001 From: Coco Belgica Date: Wed, 23 Apr 2014 23:22:14 +0800 Subject: [PATCH 02/56] Fixed: - JSON.object.Remove() now behaves like AHK's obj.Remove() except existing integer key(s) are not adjusted when an integer key(or a range of integer keys) is removed. Return value is the same with AHK's obj.Remove(). --- JSON.ahk | 37 +++++++++++++++++++++++++++---------- 1 file changed, 27 insertions(+), 10 deletions(-) diff --git a/JSON.ahk b/JSON.ahk index 737676d..39a55fd 100644 --- a/JSON.ahk +++ b/JSON.ahk @@ -273,16 +273,33 @@ class JSON return this[k] := v } - 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(k*) { + ascs := A_StringCaseSense + StringCaseSense, Off + if (k.MaxIndex() > 1) { + k1 := k[1], k2 := k[2], is_int := false + if (Abs(k1) != "" && Abs(k2) != "") + k1 := Round(k1), k2 := Round(k2), is_int := true + while true { + for each, key in this._ + i := each + until found:=(key >= k1 && key <= k2) + if !found + break + key := this._.Remove(i) + ObjRemove(this, (is_int ? [key, ""] : [key])*) + res := A_Index + } + + } else for each, key in this._ { + if (key = (k.MaxIndex() ? k[1] : ObjMaxIndex(this))) { + key := this._.Remove(each) + res := ObjRemove(this, (Abs(key) != "" ? [key, ""] : [key])*) + break + } + } + StringCaseSense, % ascs + return res } len() { From 6004f64f13efab8dbf7ac19b9da25a8b4dfb1082 Mon Sep 17 00:00:00 2001 From: Coco Belgica Date: Thu, 24 Apr 2014 22:50:12 +0800 Subject: [PATCH 03/56] Minor adjustments. Nothing significant. --- JSON.ahk | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/JSON.ahk b/JSON.ahk index 39a55fd..d1a46d9 100644 --- a/JSON.ahk +++ b/JSON.ahk @@ -27,8 +27,8 @@ class JSON (Join """": """", "/": "/", - "b": Chr(08), - "f": Chr(12), + "b": "`b", + "f": "`f", "n": "`n", "r": "`r", "t": "`t" @@ -74,7 +74,7 @@ class JSON 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 + while (ch != "" && InStr(" `t`n`r", ch)) ; skip whitespace ch := SubStr(src, pos, 1), pos += 1 ;pos := RegExMatch(src, "\S", ch, pos)+1 /* @@ -208,8 +208,8 @@ class JSON (Join """": "\""", "/": "\/", - Chr(08): "\b", - Chr(12): "\f", + "`b": "\b", + "`f": "\f", "`n": "\n", "`r": "\r", "`t": "\t" From b6d7d59e5b9ff2acc48dd193a9f8b4f84f25018a Mon Sep 17 00:00:00 2001 From: Coco Belgica Date: Mon, 16 Jun 2014 04:38:31 +0800 Subject: [PATCH 04/56] Modification(s): - parse(): Improved validation of JSON source. Most (if all) common format errors are detected. As before, an exception is thrown. Code refactored. Added 'OutputNormal' class property to allow users to set whether returned object(s)/array(s) are sublclassed as JSON.object/JSON.array instance(s). Default is 'true' which returns instance(s) of JSON._object/JSON._array(notice the underscores) which are actually just normal AHK object(s) with no special behavior. - stringify(): A space is no longer added after a comma or colon if indent is not specified. Output is truly compact. --- JSON.ahk | 279 ++++++++++++++++++++++++++++++------------------------- 1 file changed, 155 insertions(+), 124 deletions(-) diff --git a/JSON.ahk b/JSON.ahk index d1a46d9..7e9f75d 100644 --- a/JSON.ahk +++ b/JSON.ahk @@ -1,28 +1,35 @@ -/* -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 -*/ +/* 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-- - */ + static _object := {} ;// object(s)/'{}' are derived from this -> no special behavior + static _array := [] ;// array(s)/'[]' are derived from this -> no special behavior + /* If 'OutputNormal' class property is set to true, object(s)/array(s) and + * their descendants are returned as normal AHK object(s). Otherwise, + * they're wrapped as instance(s) of JSON.object and JSON.array. + */ + static OutputNormal := true + parse(src) { + ;// 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, 0) + 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) && !InStr("0123456789", last)) ;// number + throw Exception("Invalid JSON format.", -1) + esc_char := { (Join """": """", @@ -33,13 +40,10 @@ class JSON "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. - */ + /* 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 @@ -49,124 +53,147 @@ class JSON if (SubStr(str, 0) != "\") break } - + if !j + throw Exception("Missing close quote(s).", -1) 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) + 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() ??? + 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`n`r", 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 - */ + ;// Check for missing opening/closing brace(s) + if InStr(src, "{") || InStr(src, "}") { + StringReplace, dummy, src, {, {, UseErrorLevel + c1 := ErrorLevel + StringReplace, dummy, src, }, }, UseErrorLevel + c2 := ErrorLevel + if (c1 != c2) + throw Exception("Missing " . Abs(c1-c2) + . (c1 > c2 ? "clos" : "open") . "ing brace(s)", -1) + } + ;// Check for missing opening/closing bracket(s) + if InStr(src, "[") || InStr(src, "]") { + StringReplace, dummy, src, [, [, UseErrorLevel + c1 := ErrorLevel + StringReplace, dummy, src, ], ], UseErrorLevel + c2 := ErrorLevel + if (c1 != c2) + throw Exception("Missing " . Abs(c1-c2) + . (c1 > c2 ? "clos" : "open") . "ing bracket(s)", -1) + } + /* Determine whether to subclass objects/arrays as JSON.object and + * JSON.array. The user can set this setting via the JSON.OutputNormal + * class property. + */ + if this.OutputNormal + _object := this._object, _array := this._array + else (_object := this.object, _array := this.array) + pos := 0 + , key := dummy := [] + , stack := [result := []] + , assert := "" ;// "{[""tfn0123456789-" + , null := "" + ;// Begin recursive descent + while ((ch := SubStr(src, ++pos, 1)) != "") { + ;// skip whitespace + while (ch != "" && InStr(" `t`n`r", ch)) + ch := SubStr(src, ++pos, 1) + ;// check if current char is expected or not if (assert != "") { if !InStr(assert, ch) throw Exception("Unexpected '" . ch . "'", -1) assert := "" } - if InStr(":,", ch) { - assert := "{[""tfn0123456789-" - continue - } - - 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 + if InStr(":,", ch) { ;// colon(s) and comma(s) + if (cont == result) + throw Exception("Unexpected '" . ch . "' -> there is no " + . "container object/array.") + assert := """" + if (ch == ":" || cont.base != _object) + assert .= "{[tfn0123456789-" + + } else if InStr("{[", ch) { ; object|array - opening + cont := stack[1] + , sub := ch == "{" ? new _object : new _array + , stack.Insert(1, cont[key == dummy ? Round(ObjMaxIndex(cont))+1 : key] := sub) + , assert := (ch == "{" ? """}" : "]{[""tfn0123456789-") + if (key != dummy) + key := dummy - } else if InStr("}]", ch) { ; object|array - closing - stack.Remove(1), assert := "]}," - continue + } else if InStr("}]", ch) { ;// object|array - closing + stack.Remove(1), cont := stack[1] + assert := (cont.base == _object) ? "}," : "]," - } else if (ch == """") { ; string + } else if (ch == """") { ;// string str := strings.Remove(1), cont := stack[1] if (key == dummy) { - if (cont.__Class == "JSON.array") { + if (cont.base == _array || cont == result) { key := Round(ObjMaxIndex(cont))+1 } else { key := str, assert := ":" continue } } - cont[key] := str, key := dummy - assert := "," . (cont.__Class == "JSON.object" ? "}" : "]") - continue + cont[key] := str + , assert := (cont.base == _object ? "}," : "],") + , key := dummy - } else if (ch >= 0 && ch <= 9) || (ch == "-") { ; number - if !RegExMatch(src, "-?\d+(\.\d+)?((?i)E[-+]?\d+)?", num, pos-1) + } else if (ch >= 0 && ch <= 9) || (ch == "-") { ;// number + if !RegExMatch(src, "-?\d+(\.\d+)?((?i)E[-+]?\d+)?", num, pos) 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 + , cont := stack[1] + , cont[key == dummy ? Round(ObjMaxIndex(cont))+1 : key] := num+0 + , assert := (cont.base == _object ? "}," : "],") + if (key != dummy) + key := dummy - } else if InStr("tfn", ch, true) { ; true|false|null + } 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 + ;// 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 + ch := SubStr(src, ++pos, 1) + if !(ch == c) ;// case-sensitive comparison throw Exception("Expected '" c "' instead of " ch) } - cont := stack[1], len := Round(ObjMaxIndex(cont)) - cont[key == dummy ? len+1 : key] := %val% - key := dummy - assert := "," . (cont.__Class == "JSON.object" ? "}" : "]") - continue + cont := stack[1] + , cont[key == dummy ? Round(ObjMaxIndex(cont))+1 : key] := %val% + , assert := (cont.base == _object ? "}," : "],") + if (key != dummy) + key := dummy - } else { - if (ch != "") - throw Exception("Unexpected '" . ch . "'", -1) - else break } } 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. - */ + /* 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 @@ -189,9 +216,14 @@ class JSON ; 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 + ;// format output + str .= (arr + ? "" + : key . ":" + . ((IsObject(v) && InStr(val, "{") == 1) ;// if value is {} + ? (n . t) ;// put opening '{' to next line, else OTB if '[' + : (i ? " " : ""))) ;// put space after ':' if indented + . (val . "," . (n ? n : "") . t) ;// value+comma+[newline+indent] } str := n . t . Trim(str, ",`n`t ") . n . SubStr(t, StrLen(i)+1) return arr ? "[" str "]" : "{" str "}" @@ -228,28 +260,27 @@ class JSON } return """" . obj . """" } - ; Number + ;// 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-- - */ + /* 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 { @@ -325,9 +356,9 @@ class JSON } } } - /* - Base object for arrays [] created during parsing. Same as JSON.object above. - */ + /* Base object for arrays [] created during parsing. + * Same as JSON.object above. + */ class array { From fc657e9fd68c528e908858825e2c42166f0152c6 Mon Sep 17 00:00:00 2001 From: Coco Belgica Date: Wed, 18 Jun 2014 22:16:07 +0800 Subject: [PATCH 05/56] Modification(s): - parse(): Removed 'OutputNormal', '_object' and '_array' class properties. 'OutputNormal' has been replaced with 'jsonize' parameter. Defaults to 'false' which returns object(s) as normal/ordinary AHK object(s). Changed parsing of true,false,null values -> no longer loops through each character to validate. Minor optimizations + code refactoring. - stringify(): Fixed output for empty object(s) when indentation is specified. No longer checks if an object is an instance of JSON.object or JSON.array as this will cause erroneous output if the user modifies the object's contents prior stringification. Minor optimization + changed some variable names. --- JSON.ahk | 101 +++++++++++++++++++++++++++++-------------------------- 1 file changed, 54 insertions(+), 47 deletions(-) diff --git a/JSON.ahk b/JSON.ahk index 7e9f75d..f423e8b 100644 --- a/JSON.ahk +++ b/JSON.ahk @@ -7,15 +7,7 @@ */ class JSON { - static _object := {} ;// object(s)/'{}' are derived from this -> no special behavior - static _array := [] ;// array(s)/'[]' are derived from this -> no special behavior - /* If 'OutputNormal' class property is set to true, object(s)/array(s) and - * their descendants are returned as normal AHK object(s). Otherwise, - * they're wrapped as instance(s) of JSON.object and JSON.array. - */ - static OutputNormal := true - - parse(src) { + 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.") @@ -55,7 +47,7 @@ class JSON } if !j throw Exception("Missing close quote(s).", -1) - src := SubStr(src, 1, i-1) . SubStr(src, j) + src := SubStr(src, 1, i) . SubStr(src, j+1) z := 0 while (z:=InStr(str, "\",, z+1)) { ch := SubStr(str, z+1, 1) @@ -93,12 +85,11 @@ class JSON . (c1 > c2 ? "clos" : "open") . "ing bracket(s)", -1) } /* Determine whether to subclass objects/arrays as JSON.object and - * JSON.array. The user can set this setting via the JSON.OutputNormal - * class property. + * JSON.array. User(s) can set this setting via the 'jsonize' parameter. */ - if this.OutputNormal - _object := this._object, _array := this._array - else (_object := this.object, _array := this.array) + if jsonize + _object := this.object, _array := this.array + else (_object := Object(), _array := Array()) pos := 0 , key := dummy := [] , stack := [result := []] @@ -133,18 +124,21 @@ class JSON key := dummy } else if InStr("}]", ch) { ;// object|array - closing - stack.Remove(1), cont := stack[1] - assert := (cont.base == _object) ? "}," : "]," + cont := stack.Remove(1) + if !jsonize + cont.base := "" + cont := stack[1] + , assert := (cont.base == _object) ? "}," : "]," } else if (ch == """") { ;// string str := strings.Remove(1), cont := stack[1] if (key == dummy) { - if (cont.base == _array || cont == result) { - key := Round(ObjMaxIndex(cont))+1 - } else { + if (cont.base == _object) { key := str, assert := ":" continue } + ;// _array or result | using 'else' seems faster, sometimes + else key := Round(ObjMaxIndex(cont))+1 } cont[key] := str , assert := (cont.base == _object ? "}," : "],") @@ -161,14 +155,22 @@ class JSON key := dummy } else if InStr("tfn", ch, true) { ;// true|false|null - val := {t:"true", f:"false", n:"null"}[ch] + /* ternary seems faster than using object -> + * val := {t:"true", f:"false", n:"null"}[ch] + */ + val := (ch == "t") ? "true" : (ch == "f") ? "false" : "null" + ;// case-sensitive comparison + if !((tfn:=SubStr(src, pos, len:=StrLen(val))) == val) + throw Exception("Expected '" val "' instead of '" tfn "'") + pos += len-1 + /* ;// advance to next char, first char has already been validated while (c:=SubStr(val, A_Index+1, 1)) { ch := SubStr(src, ++pos, 1) if !(ch == c) ;// case-sensitive comparison throw Exception("Expected '" c "' instead of " ch) } - + */ cont := stack[1] , cont[key == dummy ? Round(ObjMaxIndex(cont))+1 : key] := %val% , assert := (cont.base == _object ? "}," : "],") @@ -194,44 +196,49 @@ class JSON * 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) { + stringify(obj:="", indent:="", 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 + if (ComObjValue(x) != "") ;// COM Object + || IsFunc(obj) ;// Func object + throw Exception("Unsupported object type") + for k in obj arr := (k == A_Index) until !arr - n := i ? "`n" : (i:="", t:="") - Loop, % i ? lvl : 0 - t .= i + n := indent ? "`n" : (i := indent := "") + Loop, % indent ? lvl : 0 + i .= indent - lvl += 1 + lvl += 1, str := "" ;// make #Warn happy for k, v in obj { if IsObject(k) || (k == "") throw Exception("Invalid key.", -1) if !arr - ; integer key(s) are automatically wrapped in quotes + ;// integer key(s) are automatically wrapped in quotes key := k+0 == k ? """" . k . """" : JSON.stringify(k) - val := JSON.stringify(v, i, lvl) + val := JSON.stringify(v, indent, lvl) ;// format output - str .= (arr - ? "" - : key . ":" - . ((IsObject(v) && InStr(val, "{") == 1) ;// if value is {} - ? (n . t) ;// put opening '{' to next line, else OTB if '[' - : (i ? " " : ""))) ;// put space after ':' if indented - . (val . "," . (n ? n : "") . t) ;// value+comma+[newline+indent] + str .= (arr ? "" : key . ":" . (indent + ? (IsObject(v) && InStr(val, "{") == 1 && val != "{}") + ? n . i + : " " + : "")) . val . "," . (indent ? n . i : "") + } + ;// trim and pad + if (str != "") { + str := Trim(str, ",`n`t ") + if indent + str := n . i . str . n . SubStr(i, StrLen(indent)+1) } - 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 + ;// null + else if (obj == "") + return "null" + ;// true|false + else if (obj == "0" || obj == "1") ;// compare as string to bypass float + return obj ? "true" : "false" + ;// string else if [obj].GetCapacity(1) { if obj is float return obj @@ -260,7 +267,7 @@ class JSON } return """" . obj . """" } - ;// Number + ;// number if obj is xdigit if obj is not digit obj := """" . obj . """" From 55b60c40b71f1da387cb90d5ec8101368ecc84ce Mon Sep 17 00:00:00 2001 From: Coco Belgica Date: Tue, 22 Jul 2014 22:52:32 +0800 Subject: [PATCH 06/56] Code refactored. Added/fixed inline documentation. --- JSON.ahk | 125 ++++++++++++++++++++++++++----------------------------- 1 file changed, 59 insertions(+), 66 deletions(-) diff --git a/JSON.ahk b/JSON.ahk index f423e8b..6cbbde6 100644 --- a/JSON.ahk +++ b/JSON.ahk @@ -7,6 +7,15 @@ */ class JSON { + /* Function: parse + * Deserialize a string containing a JSON document to an AHK object. + * Syntax: + * json_obj := JSON.parse( src [, jsonize:=false ] ) + * Parameter(s): + * src [in] - String containing a JSON document + * jsonize [in] - If true, objects {} and arrays [] are wrapped as + * JSON.object and JSON.array instances respectively. + */ parse(src, jsonize:=false) { ;// Pre-validate JSON source before parsing if ((src:=Trim(src, " `t`n`r")) == "") ;// trim whitespace(s) @@ -118,7 +127,7 @@ class JSON } else if InStr("{[", ch) { ; object|array - opening cont := stack[1] , sub := ch == "{" ? new _object : new _array - , stack.Insert(1, cont[key == dummy ? Round(ObjMaxIndex(cont))+1 : key] := sub) + , stack.Insert(1, cont[key == dummy ? NumGet(&cont+4*A_PtrSize)+1 : key] := sub) , assert := (ch == "{" ? """}" : "]{[""tfn0123456789-") if (key != dummy) key := dummy @@ -138,7 +147,7 @@ class JSON continue } ;// _array or result | using 'else' seems faster, sometimes - else key := Round(ObjMaxIndex(cont))+1 + else key := NumGet(&cont+4*A_PtrSize)+1 } cont[key] := str , assert := (cont.base == _object ? "}," : "],") @@ -149,7 +158,7 @@ class JSON throw Exception("Bad number", -1) pos += StrLen(num)-1 , cont := stack[1] - , cont[key == dummy ? Round(ObjMaxIndex(cont))+1 : key] := num+0 + , cont[key == dummy ? NumGet(&cont+4*A_PtrSize)+1 : key] := num+0 , assert := (cont.base == _object ? "}," : "],") if (key != dummy) key := dummy @@ -163,16 +172,8 @@ class JSON if !((tfn:=SubStr(src, pos, len:=StrLen(val))) == val) throw Exception("Expected '" val "' instead of '" tfn "'") pos += len-1 - /* - ;// advance to next char, first char has already been validated - while (c:=SubStr(val, A_Index+1, 1)) { - ch := SubStr(src, ++pos, 1) - if !(ch == c) ;// case-sensitive comparison - throw Exception("Expected '" c "' instead of " ch) - } - */ - cont := stack[1] - , cont[key == dummy ? Round(ObjMaxIndex(cont))+1 : key] := %val% + , cont := stack[1] + , cont[key == dummy ? NumGet(&cont+4*A_PtrSize)+1 : key] := %val%+0 , assert := (cont.base == _object ? "}," : "],") if (key != dummy) key := dummy @@ -181,20 +182,21 @@ class JSON } 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 + /* Function: stringify + * Serialize an object to a JSON formatted string. + * Syntax: + * json_str := JSON.stringify( obj [, indent:="" ] ) + * Parameter(s): + * obj [in] - The object to stringify. + * indent [in] - Specify string(s) to use as indentation per level. * 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. + * 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:="", indent:="", lvl:=1) { if IsObject(obj) { @@ -232,47 +234,38 @@ class JSON } return arr ? "[" str "]" : "{" str "}" } - ;// null - else if (obj == "") - return "null" - ;// true|false - else if (obj == "0" || obj == "1") ;// compare as string to bypass float - return obj ? "true" : "false" - ;// string - else if [obj].GetCapacity(1) { - if obj is float - return obj - - esc_char := { - (Join - """": "\""", - "/": "\/", - "`b": "\b", - "`f": "\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 . """" + ;// Not a string - assume number + if ![obj].GetCapacity(1) { ;// returns 0 for blank strings ("") + return (obj == "0" || obj == "1") ;// compare as string to bypass float + ? (obj ? "true" : "false") ;// true/false + : (obj == "" ? "null" : obj) ;// null OR number } - ;// number - if obj is xdigit - if obj is not digit - obj := """" . obj . """" + ;// String + if obj is float + return obj + esc_char := { + (Join + """": "\""", + "/": "\/", + "`b": "\b", + "`f": "\f", + "`n": "\n", + "`r": "\r", + "`t": "\t" + )} - return obj + 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 . """" } /* 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 @@ -341,7 +334,7 @@ class JSON } len() { - return Round(this._.MaxIndex()) + return NumGet(&(this._)+4*A_PtrSize) ;// Round(this._.MaxIndex()) } stringify(i:="") { From 17be321d9ce48b9e7c668bf46a63dfe41a8518ec Mon Sep 17 00:00:00 2001 From: Coco Belgica Date: Sun, 10 Aug 2014 12:09:50 +0800 Subject: [PATCH 07/56] Minor fix -> JSON.stringify() --- JSON.ahk | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/JSON.ahk b/JSON.ahk index 6cbbde6..2122671 100644 --- a/JSON.ahk +++ b/JSON.ahk @@ -234,12 +234,15 @@ class JSON } return arr ? "[" str "]" : "{" str "}" } - ;// Not a string - assume number - if ![obj].GetCapacity(1) { ;// returns 0 for blank strings ("") + ;// Not a string - assume number -> integer or float + if ([obj].GetCapacity(1) == "") { ;// returns an integer if 'obj' is string return (obj == "0" || obj == "1") ;// compare as string to bypass float - ? (obj ? "true" : "false") ;// true/false - : (obj == "" ? "null" : obj) ;// null OR number + ? (obj ? "true" : "false") ;// true/false + : obj ;// number } + ;// null + else if (obj == "") + return "null" ;// String if obj is float return obj From 0025f1387b9d35e9b63a9af9595ecf9681defa17 Mon Sep 17 00:00:00 2001 From: Coco Belgica Date: Fri, 15 Aug 2014 01:29:28 +0800 Subject: [PATCH 08/56] Added Json2.ahk -> first commit. - Alternative to JSON.ahk. - Interface implemented as a function instead of a class. - Will work on both AHK v1.1 and v2.0-a - FOR v2.0-a -> A_AhkVersion >= v2.0-a049. --- Json2.ahk | 198 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 198 insertions(+) create mode 100644 Json2.ahk diff --git a/Json2.ahk b/Json2.ahk new file mode 100644 index 0000000..2b372d8 --- /dev/null +++ b/Json2.ahk @@ -0,0 +1,198 @@ +Json2(src, arg1:="", arg2:="") { + if IsObject(src) { + ret := _Json2(src, arg2) ;// arg2=indent + if (arg1 == "") + return ret + if !(fobj := FileOpen(arg1, "w")) ;// arg1=outfile + throw "Failed to open file: '" arg1 "' for writing." + bytes := fobj.Write(ret), fobj.Close() + return bytes + } + if FileExist(src) { + if !(fobj := FileOpen(src, "r")) + throw "Failed to open file: '" src "' for reading." + src := fobj.Read(), fobj.Close() + } + ;// Begin de-serialization routine + static is_v2 := (A_AhkVersion >= "2"), q := Chr(34) ;// Double quote + , push := Func(is_v2 ? "ObjPush" : "ObjInsert") + , ins := Func(is_v2 ? "ObjInsertAt" : "ObjInsert") + , set := Func(is_v2 ? "ObjRawSet" : "ObjInsert") + , pop := Func(is_v2 ? "ObjPop" : "ObjRemove") + , del := Func(is_v2 ? "ObjRemoveAt" : "ObjRemove") + static esc_seq := { + (Join Q C + (q): q, + "/": "/", + "b": "`b", + "f": "`f", + "n": "`n", + "r": "`r", + "t": "`t" + )} + i := 0, strings := [], end := 0-is_v2 + while (i := InStr(src, q,, i+1)) { + j := i + while (j := InStr(src, q,, j+1)) { + str := SubStr(src, i+1, j-i-1) + ;// 'StringReplace, str, str, \\, \u005C, A' workaround + k := -5 + while (k := InStr(str, "\\",, k+6)) + str := SubStr(str, 1, k-1) "\u005C" SubStr(str, k+2) + if (SubStr(str, end) != "\") + break + } + if !j + throw "Missing close quote(s)" + src := SubStr(src, 1, i) . SubStr(src, j+1) + z := 0 + while (z := InStr(str, "\",, z+1)) { + ch := SubStr(str, z+1, 1) + if InStr(q "btnfr/", ch, 1) { + str := SubStr(str, 1, z-1) esc_seq[ch] SubStr(str, z+2) + + } else if (ch = "u") { + hex := "0x" SubStr(str, z+2, 4) + if !(A_IsUnicode || (Abs(hex) < 0x100)) + continue + str := SubStr(str, 1, z-1) Chr(hex) SubStr(str, z+6) + + } else throw "Invalid escape sequence" + } + %push%(strings, str) + } + static t := "true", f := "false", n := "null", null := "" + jbase := Object("[", arg1, "{", arg2) ;// { "[":arg1, "{":arg2 } + , pos := 0 + , key := "", is_key := false + , stack := [tree := []] + , is_arr := Object(tree, 1) + , next := q "{[01234567890-tfn" + while ((ch := SubStr(src, ++pos, 1)) != "") { + if InStr(" `t`n`r", ch) + continue + if !InStr(next, ch) + throw "Unexpected char: '" ch "'" + + is_array := is_arr[obj := stack[1]] + + if InStr("{[", ch) { + val := (proto := jbase[ch]) ? new proto : {} + ;// is_array? %push%(obj, val) : %set%(obj, key, val) + , obj[is_array? NumGet(&obj+4*A_PtrSize)+1 : key] := val + , %ins%(stack, 1, val) + , is_arr[val] := !(is_key := ch == "{") + , next := q (is_key ? "}" : "{[]0123456789-tfn") + } + + else if InStr("}]", ch) { + %del%(stack, 1) + , next := is_arr[stack[1]] ? "]," : "}," + } + + else if InStr(",:", ch) { + if (obj == tree) + throw "Unexpected char: '" ch "' -> there is no container object." + next := q "{[0123456789-tfn", is_key := (!is_array && ch == ",") + } + + else { + if (ch == q) { + val := %del%(strings, 1) + if is_key { + key := val, next := ":" + continue + } + + } else { + val := SubStr(src, pos, (SubStr(src, pos) ~= "[\]\},\s]|$")-1) + , pos += StrLen(val)-1 + if InStr("tfn", ch, 1) { + if !(val == %ch%) + throw "Expected '" %ch% "' instead of '" val "'" + val := %val% + + } else if (Abs(val) == "") { + throw "Invalid number: " val + } + val += 0 + } + ;// is_array? %push%(obj, val) : %set%(obj, key, val) + obj[is_array? NumGet(&obj+4*A_PtrSize)+1 : key] := val + , next := is_array ? "]," : "}," + } + } + return tree[1] +} + +_Json2(obj, indent:="", lvl:=1) { + static is_v2 := (A_AhkVersion >= "2"), q := Chr(34) + static len := Func(is_v2 ? "ObjLength" : "ObjMaxIndex") + + if IsObject(obj) { + count := NumGet(&obj+4*A_PtrSize), is_array := 0 + for k in count == %len%(obj) ? obj : 0 + is_array := (k == A_Index) + until !is_array + + 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 "Invalid JSON key" + if !is_array + out .= ( ObjGetCapacity([k], 1) ? _Json2(k) : q . k . q ) ;// key + . ( indent ? ": " : ":" ) ;// token + padding + out .= _Json2(v, indent, lvl) ;// value + . ( A_Index < count ? (indent ? ",`n" . indt : ",") : "" ) + } + if (out != "" && indent != "") + out := "`n" . indt . out . "`n" . SubStr(indt, StrLen(indent)+1) + + return is_array ? "[" out "]" : "{" out "}" + } + + else if (ObjGetCapacity([obj], 1) == "") + return (obj == "0" || obj == "1") ? (obj ? "true" : "false") : obj + + else if (obj == "") + return "null" + + static ord := Func(is_v2 ? "Ord" : "Asc") + static esc_seq := { ;// JSON escape sequences + (Join Q C + (q): "\" q, + "/": "\/", + "`b": "\b", + "`f": "\f", + "`n": "\n", + "`r": "\r", + "`t": "\t" + )} + i := -1 + while (i := InStr(obj, "\",, i+2)) ;// Replacement is 2 chars long + obj := SubStr(obj, 1, i-1) "\\" SubStr(obj, i+1) + for k, v in esc_seq { + i := -1 + while (i := InStr(obj, k,, i+2)) + obj := SubStr(obj, 1, i-1) . v . SubStr(obj, i+1) + } + i := -5 ;// i+6 -> Unicode escape sequence is 6 chars long + while (i := RegExMatch(obj, "[^\x20-\x7e]", wstr, i+6)) { + ucp := %ord%(is_v2 ? wstr.Value : wstr), hex := "\u", n := 16 + while ((n-=4) >= 0) + hex .= Chr( (x := (ucp >> n) & 15) + (x < 10 ? 48 : 55) ) + obj := SubStr(obj, 1, i-1) . hex . SubStr(obj, i+1) + } + return q . obj . q +} \ No newline at end of file From 9d3563de8f19a0473759f54fbcb13fa5e1ae9938 Mon Sep 17 00:00:00 2001 From: Coco Belgica Date: Fri, 15 Aug 2014 01:37:20 +0800 Subject: [PATCH 09/56] Minor fix on formatting --- Json2.ahk | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Json2.ahk b/Json2.ahk index 2b372d8..350f7f7 100644 --- a/Json2.ahk +++ b/Json2.ahk @@ -172,12 +172,12 @@ _Json2(obj, indent:="", lvl:=1) { static esc_seq := { ;// JSON escape sequences (Join Q C (q): "\" q, - "/": "\/", - "`b": "\b", - "`f": "\f", - "`n": "\n", - "`r": "\r", - "`t": "\t" + "/": "\/", + "`b": "\b", + "`f": "\f", + "`n": "\n", + "`r": "\r", + "`t": "\t" )} i := -1 while (i := InStr(obj, "\",, i+2)) ;// Replacement is 2 chars long From 7586b375e631f2b55442c8e79cd071e20e6ef045 Mon Sep 17 00:00:00 2001 From: Coco Belgica Date: Mon, 18 Aug 2014 02:16:29 +0800 Subject: [PATCH 10/56] Updated JSON.ahk to mirror v1.1 branch --- JSON.ahk | 381 +++++++++++++++++++++---------------------------------- 1 file changed, 147 insertions(+), 234 deletions(-) diff --git a/JSON.ahk b/JSON.ahk index 2122671..f5a419f 100644 --- a/JSON.ahk +++ b/JSON.ahk @@ -1,10 +1,3 @@ -/* 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 { /* Function: parse @@ -18,8 +11,8 @@ class JSON */ 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.") + if ((src := Trim(src, " `t`n`r")) == "") ;// trim whitespace(s) + throw "Empty JSON source" first := SubStr(src, 1, 1), last := SubStr(src, 0) if !InStr("{[""tfn0123456789-", first) ;// valid beginning chars || !InStr("}]""el0123456789", last) ;// valid ending chars @@ -29,9 +22,9 @@ class JSON || (first == "n" && last != "l") ;// assume 'null' || (InStr("tf", first) && last != "e") ;// assume 'true' OR 'false' || (InStr("-0123456789", first) && !InStr("0123456789", last)) ;// number - throw Exception("Invalid JSON format.", -1) + throw "Invalid JSON format" - esc_char := { + esc_seq := { (Join """": """", "/": "/", @@ -41,35 +34,31 @@ class JSON "r": "`r", "t": "`t" )} - /* 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)) { + 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) != "\") break } if !j - throw Exception("Missing close quote(s).", -1) + throw "Missing close quote(s)" src := SubStr(src, 1, i) . SubStr(src, j+1) - 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) + k := 0 + while (k := InStr(str, "\",, k+1)) { + ch := SubStr(str, k+1, 1) + if InStr("""btnfr/", ch, 1) + 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) + str := SubStr(str, 1, k-1) . Chr(hex) . SubStr(str, k+6) - } else throw Exception("Bad string") + } else throw "Invalid escape sequence: '\" ch "'" } strings.Insert(str) } @@ -80,8 +69,7 @@ class JSON StringReplace, dummy, src, }, }, UseErrorLevel c2 := ErrorLevel if (c1 != c2) - throw Exception("Missing " . Abs(c1-c2) - . (c1 > c2 ? "clos" : "open") . "ing brace(s)", -1) + throw "Missing " . Abs(c1-c2) . (c1 > c2 ? "clos" : "open") . "ing brace(s)" } ;// Check for missing opening/closing bracket(s) if InStr(src, "[") || InStr(src, "]") { @@ -90,97 +78,68 @@ class JSON StringReplace, dummy, src, ], ], UseErrorLevel c2 := ErrorLevel if (c1 != c2) - throw Exception("Missing " . Abs(c1-c2) - . (c1 > c2 ? "clos" : "open") . "ing bracket(s)", -1) + throw "Missing " . Abs(c1-c2) . (c1 > c2 ? "clos" : "open") . "ing bracket(s)" } - /* Determine whether to subclass objects/arrays as JSON.object and - * JSON.array. User(s) can set this setting via the 'jsonize' parameter. - */ - if jsonize - _object := this.object, _array := this.array - else (_object := Object(), _array := Array()) - pos := 0 - , key := dummy := [] - , stack := [result := []] - , assert := "" ;// "{[""tfn0123456789-" - , null := "" - ;// Begin recursive descent + 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 := Object(tree, 1) + , next := first ;// """{[01234567890-tfn" while ((ch := SubStr(src, ++pos, 1)) != "") { - ;// skip whitespace - while (ch != "" && InStr(" `t`n`r", ch)) - ch := SubStr(src, ++pos, 1) - ;// check if current char is expected or not - if (assert != "") { - if !InStr(assert, ch) - throw Exception("Unexpected '" . ch . "'", -1) - assert := "" - } - - if InStr(":,", ch) { ;// colon(s) and comma(s) - if (cont == result) - throw Exception("Unexpected '" . ch . "' -> there is no " - . "container object/array.") - assert := """" - if (ch == ":" || cont.base != _object) - assert .= "{[tfn0123456789-" - - } else if InStr("{[", ch) { ; object|array - opening - cont := stack[1] - , sub := ch == "{" ? new _object : new _array - , stack.Insert(1, cont[key == dummy ? NumGet(&cont+4*A_PtrSize)+1 : key] := sub) - , assert := (ch == "{" ? """}" : "]{[""tfn0123456789-") - if (key != dummy) - key := dummy + if InStr(" `t`n`r", ch) + continue + if !InStr(next, ch) + throw "Unexpected char: '" ch "'" - } else if InStr("}]", ch) { ;// object|array - closing - cont := stack.Remove(1) - if !jsonize - cont.base := "" - cont := stack[1] - , assert := (cont.base == _object) ? "}," : "]," + is_array := is_arr[obj := stack[1]] - } else if (ch == """") { ;// string - str := strings.Remove(1), cont := stack[1] - if (key == dummy) { - if (cont.base == _object) { - key := str, assert := ":" + if InStr("{[", ch) { + val := (proto := jbase[ch]) ? new proto : {} + , obj[is_array? NumGet(&obj+4*A_PtrSize)+1 : key] := val + , ObjInsert(stack, 1, val) + , is_arr[val] := !(is_key := ch == "{") + , next := is_key ? """}" : """{[]0123456789-tfn" + } + + else if InStr("}]", ch) { + ObjRemove(stack, 1) + , next := is_arr[stack[1]] ? "]," : "}," + } + + else if InStr(",:", ch) { + if (obj == tree) + throw "Unexpected char: '" ch "' -> there is no container object." + next := """{[0123456789-tfn", is_key := (!is_array && ch == ",") + } + + else { + if (ch == """") { + val := ObjRemove(strings, 1) + if is_key { + key := val, next := ":" continue } - ;// _array or result | using 'else' seems faster, sometimes - else key := NumGet(&cont+4*A_PtrSize)+1 + + } else { + val := SubStr(src, pos, (SubStr(src, pos) ~= "[\]\},\s]|$")-1) + , pos += StrLen(val)-1 + if InStr("tfn", ch, 1) { + if !(val == %ch%) + throw "Expected '" %ch% "' instead of '" val "'" + val := %val% + + } else if (Abs(val) == "") { + throw "Invalid number: " val + } + val += 0 } - cont[key] := str - , assert := (cont.base == _object ? "}," : "],") - , key := dummy - - } else if (ch >= 0 && ch <= 9) || (ch == "-") { ;// number - if !RegExMatch(src, "-?\d+(\.\d+)?((?i)E[-+]?\d+)?", num, pos) - throw Exception("Bad number", -1) - pos += StrLen(num)-1 - , cont := stack[1] - , cont[key == dummy ? NumGet(&cont+4*A_PtrSize)+1 : key] := num+0 - , assert := (cont.base == _object ? "}," : "],") - if (key != dummy) - key := dummy - - } else if InStr("tfn", ch, true) { ;// true|false|null - /* ternary seems faster than using object -> - * val := {t:"true", f:"false", n:"null"}[ch] - */ - val := (ch == "t") ? "true" : (ch == "f") ? "false" : "null" - ;// case-sensitive comparison - if !((tfn:=SubStr(src, pos, len:=StrLen(val))) == val) - throw Exception("Expected '" val "' instead of '" tfn "'") - pos += len-1 - , cont := stack[1] - , cont[key == dummy ? NumGet(&cont+4*A_PtrSize)+1 : key] := %val%+0 - , assert := (cont.base == _object ? "}," : "],") - if (key != dummy) - key := dummy - + obj[is_array? NumGet(&obj+4*A_PtrSize)+1 : key] := val + , next := is_array ? "]," : "}," } } - return result[1] + return tree[1] } /* Function: stringify * Serialize an object to a JSON formatted string. @@ -189,64 +148,56 @@ class JSON * Parameter(s): * obj [in] - The object to stringify. * indent [in] - Specify string(s) to use as indentation per level. - * 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:="", indent:="", lvl:=1) { if IsObject(obj) { - if (ComObjValue(x) != "") ;// COM Object - || IsFunc(obj) ;// Func object - throw Exception("Unsupported object type") + if (ComObjValue(obj) != "") || IsFunc(obj) ;// COM/Func object + throw "Unsupported object type" + is_array := 0 for k in obj - arr := (k == A_Index) - until !arr + is_array := (k == A_Index) + until !is_array - n := indent ? "`n" : (i := indent := "") + 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 - i .= indent + indt .= indent lvl += 1, str := "" ;// make #Warn happy 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, indent, lvl) - ;// format output - str .= (arr ? "" : key . ":" . (indent - ? (IsObject(v) && InStr(val, "{") == 1 && val != "{}") - ? n . i - : " " - : "")) . val . "," . (indent ? n . i : "") + throw "Invalid JSON key" + if !is_array + out .= ( ObjGetCapacity([k], 1) ? JSON.stringify(k) : q . k . q ) ;// key + . ( indent ? ": " : ":" ) ;// token + padding + out .= JSON.stringify(v, indent, lvl) ;// value + . ( indent ? ",`n" . indt : "," ) ;// token + indent } - ;// trim and pad - if (str != "") { - str := Trim(str, ",`n`t ") - if indent - str := n . i . str . n . SubStr(i, StrLen(indent)+1) + + if (out != "") { + out := Trim(out, ",`n" indent) + if (indent != "") + out := "`n" . indt . out . "`n" . SubStr(indt, StrLen(indent)+1) } - return arr ? "[" str "]" : "{" str "}" + + return is_array ? "[" out "]" : "{" out "}" } ;// Not a string - assume number -> integer or float - if ([obj].GetCapacity(1) == "") { ;// returns an integer if 'obj' is string - return (obj == "0" || obj == "1") ;// compare as string to bypass float - ? (obj ? "true" : "false") ;// true/false - : obj ;// number - } + if (ObjGetCapacity([obj], 1) == "") ;// returns an integer if 'obj' is string + return InStr("01", obj) ? (obj ? "true" : "false") : obj ;// null else if (obj == "") return "null" ;// String if obj is float return obj - esc_char := { + esc_seq := { (Join """": "\""", "/": "\/", @@ -258,120 +209,82 @@ class JSON )} StringReplace, obj, obj, \, \\, A - for k, v in esc_char - StringReplace, obj, obj, % k, % v, A + for k, v in esc_seq + 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 + while RegExMatch(obj, "[^\x20-\x7e]", wstr) { + ucp := Asc(wstr), hex := "\u", n := 16 + while ((n-=4) >= 0) + hex .= Chr( (x := (ucp >> n) & 15) + (x < 10 ? 48 : 55) ) + StringReplace, obj, obj, %wstr%, %hex%, A } 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 { - __New(p*) { + __New(args*) { 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) + if ((count := NumGet(&args+4*A_PtrSize)) & 1) + throw "Invalid number of parameters" + Loop, % count//2 + this[args[A_Index*2-1]] := args[A_Index*2] } - _NewEnum() { - return new JSON.object.Enum(this) + __Set(key, val, args*) { + ObjInsert(this._, key) } - Insert(k, v) { - return this[k] := v + Insert(key, val) { + return this[key] := val } - Remove(k*) { - ascs := A_StringCaseSense - StringCaseSense, Off - if (k.MaxIndex() > 1) { - k1 := k[1], k2 := k[2], is_int := false - if (Abs(k1) != "" && Abs(k2) != "") - k1 := Round(k1), k2 := Round(k2), is_int := true - while true { - for each, key in this._ - i := each - until found:=(key >= k1 && key <= k2) - if !found - break - key := this._.Remove(i) - ObjRemove(this, (is_int ? [key, ""] : [key])*) - res := A_Index - } - - } else for each, key in this._ { - if (key = (k.MaxIndex() ? k[1] : ObjMaxIndex(this))) { - key := this._.Remove(each) - res := ObjRemove(this, (Abs(key) != "" ? [key, ""] : [key])*) - break - } + Remove(args*) { + ret := ObjRemove(this, args*), i := -1 + for index, key in ObjClone(this._) { + if ObjHasKey(this, key) + continue + ObjRemove(this._, index-(i+=1)) } - StringCaseSense, % ascs - return res + return ret } - - len() { + + Count() { return NumGet(&(this._)+4*A_PtrSize) ;// Round(this._.MaxIndex()) } - stringify(i:="") { - return JSON.stringify(this, i) + stringify(indent:="") { + return JSON.stringify(this, indent) } - class Enum - { + _NewEnum() { + static proto := {"Next":JSON.object.Next} + return { + (LTrim Join + "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 From d96164f2345351009626bd2831045a2c4ac3dc4d Mon Sep 17 00:00:00 2001 From: Coco Belgica Date: Sat, 23 Aug 2014 00:11:44 +0800 Subject: [PATCH 11/56] Added exception handling when dumping non-standard AHK objects such as COM, Func, RegExMatch, File object(s). Fixed dumping bug when object has a custom enumerator. --- Json2.ahk | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/Json2.ahk b/Json2.ahk index 350f7f7..c4e5d9c 100644 --- a/Json2.ahk +++ b/Json2.ahk @@ -127,12 +127,13 @@ Json2(src, arg1:="", arg2:="") { _Json2(obj, indent:="", lvl:=1) { static is_v2 := (A_AhkVersion >= "2"), q := Chr(34) - static len := Func(is_v2 ? "ObjLength" : "ObjMaxIndex") if IsObject(obj) { - count := NumGet(&obj+4*A_PtrSize), is_array := 0 - for k in count == %len%(obj) ? obj : 0 - is_array := (k == A_Index) + if (ObjGetCapacity(obj) == "") + throw "Only standard AHK objects are supported" + is_array := 0 + for k in obj + is_array := k == A_Index until !is_array if (Abs(indent) != "") { @@ -154,19 +155,22 @@ _Json2(obj, indent:="", lvl:=1) { out .= ( ObjGetCapacity([k], 1) ? _Json2(k) : q . k . q ) ;// key . ( indent ? ": " : ":" ) ;// token + padding out .= _Json2(v, indent, lvl) ;// value - . ( A_Index < count ? (indent ? ",`n" . indt : ",") : "" ) + . ( indent ? ",`n" . indt : "," ) ;// token + indent + } + if (out != "") { + out := Trim(out, ",`n" . indent) + if (indent != "") + out := "`n" . indt . out . "`n" . SubStr(indt, StrLen(indent)+1) } - if (out != "" && indent != "") - out := "`n" . indt . out . "`n" . SubStr(indt, StrLen(indent)+1) return is_array ? "[" out "]" : "{" out "}" } else if (ObjGetCapacity([obj], 1) == "") - return (obj == "0" || obj == "1") ? (obj ? "true" : "false") : obj + return InStr("01", obj) ? (obj ? "true" : "false") : obj else if (obj == "") - return "null" + return n := "null" ;// compensate for v2.0-a049 bug/behavior static ord := Func(is_v2 ? "Ord" : "Asc") static esc_seq := { ;// JSON escape sequences From a013ec1b3b2043e443cfda74bcd92fe38d07dcc5 Mon Sep 17 00:00:00 2001 From: Coco Belgica Date: Sat, 23 Aug 2014 00:14:29 +0800 Subject: [PATCH 12/56] Commented out JSON.object.Remove() -> Bug: when removing a range of integer keys, remaining integer keys are not adjusted. Will fix in next commit. --- JSON.ahk | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/JSON.ahk b/JSON.ahk index f5a419f..0d6381d 100644 --- a/JSON.ahk +++ b/JSON.ahk @@ -151,7 +151,7 @@ class JSON */ stringify(obj:="", indent:="", lvl:=1) { if IsObject(obj) { - if (ComObjValue(obj) != "") || IsFunc(obj) ;// COM/Func object + if (ObjGetCapacity(obj) == "") ;// COM,Func,RegExMatch,File object throw "Unsupported object type" is_array := 0 for k in obj @@ -169,7 +169,7 @@ class JSON Loop, % indent ? lvl : 0 indt .= indent - lvl += 1, str := "" ;// make #Warn happy + lvl += 1, out := "" ;// make #Warn happy for k, v in obj { if IsObject(k) || (k == "") throw "Invalid JSON key" @@ -195,8 +195,8 @@ class JSON else if (obj == "") return "null" ;// String - if obj is float - return obj + ; if obj is float + ; return obj esc_seq := { (Join """": "\""", @@ -239,8 +239,8 @@ class JSON Insert(key, val) { return this[key] := val } - - Remove(args*) { + /* Buggy - remaining integer keys are not adjusted + Remove(args*) { ret := ObjRemove(this, args*), i := -1 for index, key in ObjClone(this._) { if ObjHasKey(this, key) @@ -249,7 +249,7 @@ class JSON } return ret } - + */ Count() { return NumGet(&(this._)+4*A_PtrSize) ;// Round(this._.MaxIndex()) } From bf80e164c5e7f038a10fdb790517af79040ae8ee Mon Sep 17 00:00:00 2001 From: Coco Belgica Date: Sat, 6 Sep 2014 12:37:32 +0800 Subject: [PATCH 13/56] Add README --- README.md | 47 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..e043ae2 --- /dev/null +++ b/README.md @@ -0,0 +1,47 @@ +# JSON and JSON2 +### [JSON](http://json.org/) module for [AutoHotkey](http://ahkscript.org/) +_ Tested on AutoHotkey **v1.1.15.04** and **v2.0-a049** _ + +License: [WTFPL](www.wtfpl.net) +- - - +# JSON (class) +### There are multiple version(s) available (as branches) in this repo to provide support for different AutoHotkey builds. The _master_ branch is meant to be compatible with both AHK _v1.1_ and _v2.0-a_ (haven't updated yet), while branches _v1.1_ and _v2_ support the specific AutoHotkey version as indicated by their branch name. +- - - +### **.parse()** - deserialize _src_ (a JSON formatted string) to an AutoHotkey object +### Synax: `` obj := JSON.parse( src [, jsonize := false ] ) `` +### Return Value: +An AutoHotkey object + +### Parameter(s): + * **src** [in] - JSON formatted string + * **jsonize** [in, opt] - if _true_, **_objects_( ``{}`` )** and **_arrays_( ``[]`` )** are wrapped as **JSON.object** and **JSON.array** instances respectively. This is to compensate for AutoHotkey's non-distinction between these types and other AHK object type quirks. _e.g.: In AutoHotkey, object keys are enumerated in alphabetical order not in the sequence in which they are created_ +- - - +### **.stringify()** - serialize _obj_ to a JSON formatted string +### Syntax: `` str := JSON.stringify( obj, [, indent := "" ] ) `` +### Return Value: +A JSON formatted string + +### Parameter(s): + * **obj** [in] - AutoHotkey object. Non-standard AHK objects like _COM_, _Func_, _FileObject_, _RegExMatchObject_ are not supported. + * **indent** [in, opt] -if indent 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 indent indents that many spaces per level. If indent is a string (such as ``"`t"``), that string is used to indent each level. _(I'm lazy, wording taken from Python docs)_ +- - - + +# JSON2 (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. Works on both AutoHotkey _v1.1_ and _v2.0_ + - - - +### **Deserialize** - deserialize _src_ (a JSON formatted string) to an AutoHotkey object +### Syntax: `` obj := Json2( src [, object_base := "", array_base := "" ]) `` +### Parameter(s): + * **src** [in] - 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. +- - - +### **Serialize** - Serialize _obj_ to a JSON formatted string OR dumps _obj_ to the file specified in _out_ +### Syntax: `` str := Json2( obj [, out := "", indent := "" ]) `` +### Return Value: +A JSON formatted string. If _out_ is specified, the number of bytes written is returned. + +### Parameter(s): + * **obj** [in] - this argument has the same meaning as in _JSON.stringify()_ + * **out** [in, opt] - path to the file to write to. If specified, the function returns the number of bytes written. + * **indent** [in, opt] - this argument has the same meaning as in _JSON.stringify()_ \ No newline at end of file From e6b1a71821d9bf90a7561b65c56a307cd508b5bf Mon Sep 17 00:00:00 2001 From: Coco Belgica Date: Sat, 6 Sep 2014 12:54:25 +0800 Subject: [PATCH 14/56] Fixed fcked up formatting --- README.md | 62 +++++++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 49 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index e043ae2..6ff9a1a 100644 --- a/README.md +++ b/README.md @@ -1,43 +1,79 @@ # JSON and JSON2 -### [JSON](http://json.org/) module for [AutoHotkey](http://ahkscript.org/) -_ Tested on AutoHotkey **v1.1.15.04** and **v2.0-a049** _ + +#### [JSON](http://json.org/) module for [AutoHotkey](http://ahkscript.org/) + +_Tested on AutoHotkey **v1.1.15.04** and **v2.0-a049**_ License: [WTFPL](www.wtfpl.net) + - - - + # JSON (class) -### There are multiple version(s) available (as branches) in this repo to provide support for different AutoHotkey builds. The _master_ branch is meant to be compatible with both AHK _v1.1_ and _v2.0-a_ (haven't updated yet), while branches _v1.1_ and _v2_ support the specific AutoHotkey version as indicated by their branch name. +There are multiple version(s) available (as branches) in this repo to provide support for different AutoHotkey builds. The _master_ branch is meant to be compatible with both AHK _v1.1_ and _v2.0-a_ (haven't updated yet), while branches _v1.1_ and _v2_ support the specific AutoHotkey version as indicated by their branch name. - - - -### **.parse()** - deserialize _src_ (a JSON formatted string) to an AutoHotkey object -### Synax: `` obj := JSON.parse( src [, jsonize := false ] ) `` +### **.parse()** +Deserialize _src_ (a JSON formatted string) to an AutoHotkey object + +### Synax: +``` +obj := JSON.parse( src [, jsonize := false ] ) +``` + ### Return Value: An AutoHotkey object ### Parameter(s): * **src** [in] - JSON formatted string * **jsonize** [in, opt] - if _true_, **_objects_( ``{}`` )** and **_arrays_( ``[]`` )** are wrapped as **JSON.object** and **JSON.array** instances respectively. This is to compensate for AutoHotkey's non-distinction between these types and other AHK object type quirks. _e.g.: In AutoHotkey, object keys are enumerated in alphabetical order not in the sequence in which they are created_ + - - - -### **.stringify()** - serialize _obj_ to a JSON formatted string -### Syntax: `` str := JSON.stringify( obj, [, indent := "" ] ) `` + +### **.stringify()** +serialize _obj_ to a JSON formatted string + +### Syntax: +``` +str := JSON.stringify( obj, [, indent := "" ] ) +``` + ### Return Value: A JSON formatted string ### Parameter(s): * **obj** [in] - AutoHotkey object. Non-standard AHK objects like _COM_, _Func_, _FileObject_, _RegExMatchObject_ are not supported. * **indent** [in, opt] -if indent 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 indent indents that many spaces per level. If indent is a string (such as ``"`t"``), that string is used to indent each level. _(I'm lazy, wording taken from Python docs)_ + - - - # JSON2 (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. Works on both AutoHotkey _v1.1_ and _v2.0_ - - - - -### **Deserialize** - deserialize _src_ (a JSON formatted string) to an AutoHotkey object -### Syntax: `` obj := Json2( src [, object_base := "", array_base := "" ]) `` +Similar to the JSON class above just implemented as a function. Unlike JSON (class) above, this implementation provides _reading from_ and _writing to_ file. Works on both AutoHotkey _v1.1_ and _v2.0_ + +- - - + +### **Deserialize** +Deserialize _src_ (a JSON formatted string) to an AutoHotkey object + +### Syntax: +``` +obj := Json2( src [, object_base := "", array_base := "" ]) +``` + ### Parameter(s): * **src** [in] - 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. + - - - -### **Serialize** - Serialize _obj_ to a JSON formatted string OR dumps _obj_ to the file specified in _out_ -### Syntax: `` str := Json2( obj [, out := "", indent := "" ]) `` + +### **Serialize** +Serialize _obj_ to a JSON formatted string OR dumps _obj_ to the file specified in _out_ + +### Syntax: +``` +str := Json2( obj [, out := "", indent := "" ]) +bytes_written := Json2( obj, out [, indent := "" ]) +``` + ### Return Value: A JSON formatted string. If _out_ is specified, the number of bytes written is returned. From 3e20e57a4f31a347679ba0307e6014b01c63a3e0 Mon Sep 17 00:00:00 2001 From: Coco Belgica Date: Sat, 6 Sep 2014 13:03:07 +0800 Subject: [PATCH 15/56] Fixed link to WTFPL. Minor formatting changes. --- README.md | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index 6ff9a1a..65f32c5 100644 --- a/README.md +++ b/README.md @@ -4,80 +4,80 @@ _Tested on AutoHotkey **v1.1.15.04** and **v2.0-a049**_ -License: [WTFPL](www.wtfpl.net) +License: [WTFPL](http://wtfpl.net/) - - - -# JSON (class) +## JSON (class) There are multiple version(s) available (as branches) in this repo to provide support for different AutoHotkey builds. The _master_ branch is meant to be compatible with both AHK _v1.1_ and _v2.0-a_ (haven't updated yet), while branches _v1.1_ and _v2_ support the specific AutoHotkey version as indicated by their branch name. - - - -### **.parse()** +#### .parse() Deserialize _src_ (a JSON formatted string) to an AutoHotkey object -### Synax: +#### Syntax: ``` obj := JSON.parse( src [, jsonize := false ] ) ``` -### Return Value: +#### Return Value: An AutoHotkey object -### Parameter(s): +#### Parameter(s): * **src** [in] - JSON formatted string * **jsonize** [in, opt] - if _true_, **_objects_( ``{}`` )** and **_arrays_( ``[]`` )** are wrapped as **JSON.object** and **JSON.array** instances respectively. This is to compensate for AutoHotkey's non-distinction between these types and other AHK object type quirks. _e.g.: In AutoHotkey, object keys are enumerated in alphabetical order not in the sequence in which they are created_ - - - -### **.stringify()** +#### .stringify() serialize _obj_ to a JSON formatted string -### Syntax: +#### Syntax: ``` str := JSON.stringify( obj, [, indent := "" ] ) ``` -### Return Value: +#### Return Value: A JSON formatted string -### Parameter(s): +#### Parameter(s): * **obj** [in] - AutoHotkey object. Non-standard AHK objects like _COM_, _Func_, _FileObject_, _RegExMatchObject_ are not supported. * **indent** [in, opt] -if indent 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 indent indents that many spaces per level. If indent is a string (such as ``"`t"``), that string is used to indent each level. _(I'm lazy, wording taken from Python docs)_ - - - -# JSON2 (function) +## JSON2 (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. Works on both AutoHotkey _v1.1_ and _v2.0_ - - - -### **Deserialize** +#### Deserialize Deserialize _src_ (a JSON formatted string) to an AutoHotkey object -### Syntax: +#### Syntax: ``` obj := Json2( src [, object_base := "", array_base := "" ]) ``` -### Parameter(s): +#### Parameter(s): * **src** [in] - 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. - - - -### **Serialize** +#### Serialize Serialize _obj_ to a JSON formatted string OR dumps _obj_ to the file specified in _out_ -### Syntax: +#### Syntax: ``` str := Json2( obj [, out := "", indent := "" ]) bytes_written := Json2( obj, out [, indent := "" ]) ``` -### Return Value: +#### Return Value: A JSON formatted string. If _out_ is specified, the number of bytes written is returned. -### Parameter(s): +#### Parameter(s): * **obj** [in] - this argument has the same meaning as in _JSON.stringify()_ * **out** [in, opt] - path to the file to write to. If specified, the function returns the number of bytes written. * **indent** [in, opt] - this argument has the same meaning as in _JSON.stringify()_ \ No newline at end of file From a72437204ec8cad8b669065197e7f5e2c7cfa7a2 Mon Sep 17 00:00:00 2001 From: Coco Belgica Date: Wed, 7 Jan 2015 17:43:20 +0800 Subject: [PATCH 16/56] Code refactored --- JSON.ahk | 159 +++++++++++++++++++++++++++++++++---------------------- 1 file changed, 97 insertions(+), 62 deletions(-) diff --git a/JSON.ahk b/JSON.ahk index 0d6381d..12f78a4 100644 --- a/JSON.ahk +++ b/JSON.ahk @@ -9,7 +9,8 @@ class JSON * jsonize [in] - If true, objects {} and arrays [] are wrapped as * JSON.object and JSON.array instances respectively. */ - parse(src, jsonize:=false) { + parse(src, jsonize:=false) + { ;// Pre-validate JSON source before parsing if ((src := Trim(src, " `t`n`r")) == "") ;// trim whitespace(s) throw "Empty JSON source" @@ -22,7 +23,7 @@ class JSON || (first == "n" && last != "l") ;// assume 'null' || (InStr("tf", first) && last != "e") ;// assume 'true' OR 'false' || (InStr("-0123456789", first) && !InStr("0123456789", last)) ;// number - throw "Invalid JSON format" + throw Exception("Invalid JSON format") esc_seq := { (Join @@ -35,108 +36,127 @@ class JSON "t": "`t" )} 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) != "\") break } if !j - throw "Missing close quote(s)" + throw Exception("Missing close quote(s)") src := SubStr(src, 1, i) . SubStr(src, j+1) k := 0 - while (k := InStr(str, "\",, k+1)) { + while (k := InStr(str, "\",, k+1)) + { ch := SubStr(str, k+1, 1) if InStr("""btnfr/", ch, 1) str := SubStr(str, 1, k-1) . esc_seq[ch] . SubStr(str, k+2) - else if (ch == "u") { + else if (ch == "u") + { hex := "0x" . SubStr(str, k+2, 4) if !(A_IsUnicode || (Abs(hex) < 0x100)) continue ;// throw Exception() ??? str := SubStr(str, 1, k-1) . Chr(hex) . SubStr(str, k+6) - } else throw "Invalid escape sequence: '\" ch "'" + } + else + throw Exception(Format("Invalid escape sequence: '\{}'", ch)) } strings.Insert(str) } + ;// Check for missing opening/closing brace(s) - if InStr(src, "{") || InStr(src, "}") { + if InStr(src, "{") || InStr(src, "}") + { StringReplace, dummy, src, {, {, UseErrorLevel c1 := ErrorLevel StringReplace, dummy, src, }, }, UseErrorLevel c2 := ErrorLevel if (c1 != c2) - throw "Missing " . Abs(c1-c2) . (c1 > c2 ? "clos" : "open") . "ing brace(s)" + throw Exception(Format("Missing {} {}ing brace(s)", Abs(c1-c2), c1 > c2 ? "clos" : "open")) } ;// Check for missing opening/closing bracket(s) - if InStr(src, "[") || InStr(src, "]") { + if InStr(src, "[") || InStr(src, "]") + { StringReplace, dummy, src, [, [, UseErrorLevel c1 := ErrorLevel StringReplace, dummy, src, ], ], UseErrorLevel c2 := ErrorLevel if (c1 != c2) - throw "Missing " . Abs(c1-c2) . (c1 > c2 ? "clos" : "open") . "ing bracket(s)" + throw Exception(Format("Missing {} {}ing bracket(s)", Abs(c1-c2), c1 > c2 ? "clos" : "open")) } + t := "true", f := "false", n := "null", null := "" - jbase := jsonize ? {"{":JSON.object, "[":JSON.array} : {"{":0, "[":0} + jbase := jsonize ? { "{":JSON.object, "[":JSON.array } : { "{":0, "[":0 } , pos := 0 , key := "", is_key := false , stack := [tree := []] - , is_arr := Object(tree, 1) + , is_arr := {(tree): 1} , next := first ;// """{[01234567890-tfn" - while ((ch := SubStr(src, ++pos, 1)) != "") { + while ((ch := SubStr(src, ++pos, 1)) != "") + { if InStr(" `t`n`r", ch) continue if !InStr(next, ch) - throw "Unexpected char: '" ch "'" + throw Exception(Format("Unexpected char: '{}'", ch)) is_array := is_arr[obj := stack[1]] - if InStr("{[", ch) { + if InStr("{[", ch) + { val := (proto := jbase[ch]) ? new proto : {} - , obj[is_array? NumGet(&obj+4*A_PtrSize)+1 : key] := val + , obj[is_array? NumGet(&obj + 4*A_PtrSize)+1 : key] := val , ObjInsert(stack, 1, val) , is_arr[val] := !(is_key := ch == "{") , next := is_key ? """}" : """{[]0123456789-tfn" } - else if InStr("}]", ch) { + else if InStr("}]", ch) + { ObjRemove(stack, 1) , next := is_arr[stack[1]] ? "]," : "}," } - else if InStr(",:", ch) { + else if InStr(",:", ch) + { if (obj == tree) - throw "Unexpected char: '" ch "' -> there is no container object." + throw Exception(Format("Unexpected char: '{}' -> there is no container object.", ch)) next := """{[0123456789-tfn", is_key := (!is_array && ch == ",") } - else { - if (ch == """") { + else + { + if (ch == """") + { val := ObjRemove(strings, 1) - if is_key { + if is_key + { key := val, next := ":" continue } - } else { + } + else + { val := SubStr(src, pos, (SubStr(src, pos) ~= "[\]\},\s]|$")-1) , pos += StrLen(val)-1 - if InStr("tfn", ch, 1) { + if InStr("tfn", ch, 1) + { if !(val == %ch%) - throw "Expected '" %ch% "' instead of '" val "'" + throw Exception(Format("Expected '{}' instead of '{}'", %ch%, val)) val := %val% - - } else if (Abs(val) == "") { - throw "Invalid number: " val } + else if (Abs(val) == "") + throw Exception("Invalid number: " . val) val += 0 } - obj[is_array? NumGet(&obj+4*A_PtrSize)+1 : key] := val - , next := is_array ? "]," : "}," + obj[is_array? NumGet(&obj + 4*A_PtrSize)+1 : key] := val + next := is_array ? "]," : "}," } } return tree[1] @@ -149,18 +169,22 @@ class JSON * obj [in] - The object to stringify. * indent [in] - Specify string(s) to use as indentation per level. */ - stringify(obj:="", indent:="", lvl:=1) { - if IsObject(obj) { + stringify(obj:="", indent:="", lvl:=1) + { + if IsObject(obj) + { if (ObjGetCapacity(obj) == "") ;// COM,Func,RegExMatch,File object - throw "Unsupported object type" + throw Exception("Unsupported object type") + is_array := 0 for k in obj is_array := (k == A_Index) until !is_array - if (Abs(indent) != "") { + if (Abs(indent) != "") + { if (indent < 0) - throw "Indent parameter must be a postive integer" + throw Exception("Indent parameter must be a postive integer") spaces := indent, indent := "" Loop % spaces indent .= " " @@ -170,9 +194,11 @@ class JSON indt .= indent lvl += 1, out := "" ;// make #Warn happy - for k, v in obj { + for k, v in obj + { if IsObject(k) || (k == "") - throw "Invalid JSON key" + throw Exception("Invalid JSON key") + if !is_array out .= ( ObjGetCapacity([k], 1) ? JSON.stringify(k) : q . k . q ) ;// key . ( indent ? ": " : ":" ) ;// token + padding @@ -180,20 +206,24 @@ class JSON . ( indent ? ",`n" . indt : "," ) ;// token + indent } - if (out != "") { + if (out != "") + { out := Trim(out, ",`n" indent) if (indent != "") - out := "`n" . indt . out . "`n" . SubStr(indt, StrLen(indent)+1) + out := Format("`n{}{}`n{}", indt, out, SubStr(indt, StrLen(indent)+1)) } return is_array ? "[" out "]" : "{" out "}" } + ;// Not a string - assume number -> integer or float if (ObjGetCapacity([obj], 1) == "") ;// returns an integer if 'obj' is string return InStr("01", obj) ? (obj ? "true" : "false") : obj + ;// null - else if (obj == "") + if (obj == "") return "null" + ;// String ; if obj is float ; return obj @@ -212,9 +242,10 @@ class JSON for k, v in esc_seq StringReplace, obj, obj, %k%, %v%, A - while RegExMatch(obj, "[^\x20-\x7e]", wstr) { + while RegExMatch(obj, "[^\x20-\x7e]", wstr) + { ucp := Asc(wstr), hex := "\u", n := 16 - while ((n-=4) >= 0) + while ((n -= 4) >= 0) hex .= Chr( (x := (ucp >> n) & 15) + (x < 10 ? 48 : 55) ) StringReplace, obj, obj, %wstr%, %hex%, A } @@ -224,19 +255,22 @@ class JSON class object { - __New(args*) { + __New(args*) + { ObjInsert(this, "_", []) if ((count := NumGet(&args+4*A_PtrSize)) & 1) throw "Invalid number of parameters" - Loop, % count//2 + Loop % count//2 this[args[A_Index*2-1]] := args[A_Index*2] } - __Set(key, val, args*) { + __Set(key, val, args*) + { ObjInsert(this._, key) } - Insert(key, val) { + Insert(key, val) + { return this[key] := val } /* Buggy - remaining integer keys are not adjusted @@ -250,25 +284,24 @@ class JSON return ret } */ - Count() { - return NumGet(&(this._)+4*A_PtrSize) ;// Round(this._.MaxIndex()) + Count() + { + return NumGet(&(this._) + 4*A_PtrSize) ;// Round(this._.MaxIndex()) } - stringify(indent:="") { + stringify(indent:="") + { return JSON.stringify(this, indent) } - _NewEnum() { - static proto := {"Next":JSON.object.Next} - return { - (LTrim Join - "base": proto, - "enum": this._._NewEnum(), - "obj": this - )} + _NewEnum() + { + static proto := { "Next": JSON.object.Next } + return { base: proto, enum: this._._NewEnum(), obj: this } } - Next(ByRef key, ByRef val:="") { + Next(ByRef key, ByRef val:="") + { if (ret := this.enum.Next(i, key)) val := this.obj[key] return ret @@ -278,12 +311,14 @@ class JSON class array { - __New(args*) { + __New(args*) + { args.base := this.base return args } - stringify(indent:="") { + stringify(indent:="") + { return JSON.stringify(this, indent) } } From 71ffa674168ea629e26244e5871b86b5e20759ee Mon Sep 17 00:00:00 2001 From: Coco Belgica Date: Wed, 7 Jan 2015 17:46:07 +0800 Subject: [PATCH 17/56] .stringify() - Fixed integer keys are not getting quoted. --- JSON.ahk | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/JSON.ahk b/JSON.ahk index 12f78a4..6159be8 100644 --- a/JSON.ahk +++ b/JSON.ahk @@ -200,7 +200,7 @@ class JSON throw Exception("Invalid JSON key") if !is_array - out .= ( ObjGetCapacity([k], 1) ? JSON.stringify(k) : q . k . q ) ;// key + out .= ( ObjGetCapacity([k], 1) ? JSON.stringify(k) : """" . k . """" ) ;// key . ( indent ? ": " : ":" ) ;// token + padding out .= JSON.stringify(v, indent, lvl) ;// value . ( indent ? ",`n" . indt : "," ) ;// token + indent From a7be08ffdf036e438db9af34f8ff838a95884f14 Mon Sep 17 00:00:00 2001 From: Coco Belgica Date: Wed, 7 Jan 2015 17:49:03 +0800 Subject: [PATCH 18/56] .stringify() - 0, 1 and "" are no longer returned as true, false and null. --- JSON.ahk | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/JSON.ahk b/JSON.ahk index 6159be8..a97d093 100644 --- a/JSON.ahk +++ b/JSON.ahk @@ -218,11 +218,9 @@ class JSON ;// Not a string - assume number -> integer or float if (ObjGetCapacity([obj], 1) == "") ;// returns an integer if 'obj' is string - return InStr("01", obj) ? (obj ? "true" : "false") : obj + return obj - ;// null - if (obj == "") - return "null" + ;// null - not supported in AHK ;// String ; if obj is float From 9f6da79bb98efafb2c7e14cd8330de9739fb6c31 Mon Sep 17 00:00:00 2001 From: Coco Belgica Date: Thu, 8 Jan 2015 05:07:37 +0800 Subject: [PATCH 19/56] Updated required AHK version(s) --- README.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 65f32c5..7f94803 100644 --- a/README.md +++ b/README.md @@ -2,15 +2,17 @@ #### [JSON](http://json.org/) module for [AutoHotkey](http://ahkscript.org/) -_Tested on AutoHotkey **v1.1.15.04** and **v2.0-a049**_ +_Requires AutoHotkey **v1.1.17.00+** OR **2.0-a057+**_ License: [WTFPL](http://wtfpl.net/) - - - ## JSON (class) -There are multiple version(s) available (as branches) in this repo to provide support for different AutoHotkey builds. The _master_ branch is meant to be compatible with both AHK _v1.1_ and _v2.0-a_ (haven't updated yet), while branches _v1.1_ and _v2_ support the specific AutoHotkey version as indicated by their branch name. +There are multiple version(s) available (as branches) in this repo to provide support for different AutoHotkey builds. The _master_ branch is for `v1.1` _(except for Json2.ahk which is version independent)_ while the _v2_ branch is for `AHK v2.0-a`. + - - - + #### .parse() Deserialize _src_ (a JSON formatted string) to an AutoHotkey object From 58637b6445e664f60fe680f968ed430792bb61b8 Mon Sep 17 00:00:00 2001 From: Coco Belgica Date: Thu, 8 Jan 2015 05:52:47 +0800 Subject: [PATCH 20/56] Fixed: .parse() - 'null' converted to 0 instead of "" --- JSON.ahk | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/JSON.ahk b/JSON.ahk index a97d093..8913fd8 100644 --- a/JSON.ahk +++ b/JSON.ahk @@ -153,7 +153,7 @@ class JSON } else if (Abs(val) == "") throw Exception("Invalid number: " . val) - val += 0 + val := val + 0 } obj[is_array? NumGet(&obj + 4*A_PtrSize)+1 : key] := val next := is_array ? "]," : "}," From 00f3c74c9629c5995a4207788bfbe2968544ba72 Mon Sep 17 00:00:00 2001 From: Coco Belgica Date: Thu, 8 Jan 2015 05:58:10 +0800 Subject: [PATCH 21/56] Added example --- Example_JSON.ahk | 49 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 Example_JSON.ahk diff --git a/Example_JSON.ahk b/Example_JSON.ahk new file mode 100644 index 0000000..f28edc6 --- /dev/null +++ b/Example_JSON.ahk @@ -0,0 +1,49 @@ +#Include + +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.parse(json_str, true) + +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.stringify(parsed, 4) +StringReplace stringified, stringified, `n, `r`n, All ; 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 From 23f69115a438f8ade3dc7f663e6ab2a1df1e4df3 Mon Sep 17 00:00:00 2001 From: Coco Belgica Date: Thu, 8 Jan 2015 06:02:15 +0800 Subject: [PATCH 22/56] Fixed #Include line --- Example_JSON.ahk | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Example_JSON.ahk b/Example_JSON.ahk index f28edc6..7cf82c1 100644 --- a/Example_JSON.ahk +++ b/Example_JSON.ahk @@ -1,4 +1,4 @@ -#Include +#Include %A_LineFile%\..\JSON.ahk json_str = ( From 3ddab43e2e07245c0bdb091ad825280e03377d39 Mon Sep 17 00:00:00 2001 From: Coco Belgica Date: Fri, 9 Jan 2015 04:47:14 +0800 Subject: [PATCH 23/56] Fixed: [parsing] - 'null' converted to 0 instead of "" --- Json2.ahk | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Json2.ahk b/Json2.ahk index c4e5d9c..d21e176 100644 --- a/Json2.ahk +++ b/Json2.ahk @@ -115,7 +115,7 @@ Json2(src, arg1:="", arg2:="") { } else if (Abs(val) == "") { throw "Invalid number: " val } - val += 0 + val := val + 0 ;// val += 0 on v1.1+ converts "" to 0 } ;// is_array? %push%(obj, val) : %set%(obj, key, val) obj[is_array? NumGet(&obj+4*A_PtrSize)+1 : key] := val From b66dd5e23927934ff5545214c2390a18fa21b753 Mon Sep 17 00:00:00 2001 From: Coco Belgica Date: Fri, 9 Jan 2015 04:56:57 +0800 Subject: [PATCH 24/56] Changed: [dumping] - 1, 0 and "" are no longer returned as true, false and null --- Json2.ahk | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/Json2.ahk b/Json2.ahk index d21e176..cfc35b2 100644 --- a/Json2.ahk +++ b/Json2.ahk @@ -166,11 +166,10 @@ _Json2(obj, indent:="", lvl:=1) { return is_array ? "[" out "]" : "{" out "}" } - else if (ObjGetCapacity([obj], 1) == "") - return InStr("01", obj) ? (obj ? "true" : "false") : obj + else if (ObjGetCapacity([obj], 1) == "") ;// number + return obj - else if (obj == "") - return n := "null" ;// compensate for v2.0-a049 bug/behavior + ;// null - not supported by AHK static ord := Func(is_v2 ? "Ord" : "Asc") static esc_seq := { ;// JSON escape sequences From 3267547c88a91387c061559bf0d48fcc89002400 Mon Sep 17 00:00:00 2001 From: Coco Belgica Date: Fri, 9 Jan 2015 06:01:35 +0800 Subject: [PATCH 25/56] Code refactored --- Json2.ahk | 176 ++++++++++++++++++++++++++++++++---------------------- 1 file changed, 104 insertions(+), 72 deletions(-) diff --git a/Json2.ahk b/Json2.ahk index cfc35b2..77c98a4 100644 --- a/Json2.ahk +++ b/Json2.ahk @@ -1,25 +1,29 @@ -Json2(src, arg1:="", arg2:="") { - if IsObject(src) { +Json2(src, arg1:="", arg2:="") +{ + if IsObject(src) + { ret := _Json2(src, arg2) ;// arg2=indent if (arg1 == "") return ret if !(fobj := FileOpen(arg1, "w")) ;// arg1=outfile - throw "Failed to open file: '" arg1 "' for writing." + throw Exception(Format("Failed to open file: '{}' for writing", arg1)) bytes := fobj.Write(ret), fobj.Close() return bytes } - if FileExist(src) { + if FileExist(src) + { if !(fobj := FileOpen(src, "r")) - throw "Failed to open file: '" src "' for reading." + throw Exception(Format("Failed to open file: '{}' for reading.", src)) src := fobj.Read(), fobj.Close() } + ;// Begin de-serialization routine static is_v2 := (A_AhkVersion >= "2"), q := Chr(34) ;// Double quote - , push := Func(is_v2 ? "ObjPush" : "ObjInsert") - , ins := Func(is_v2 ? "ObjInsertAt" : "ObjInsert") - , set := Func(is_v2 ? "ObjRawSet" : "ObjInsert") - , pop := Func(is_v2 ? "ObjPop" : "ObjRemove") - , del := Func(is_v2 ? "ObjRemoveAt" : "ObjRemove") + , ObjPush := Func(is_v2 ? "ObjPush" : "ObjInsert") + , ObjInsertAt := Func(is_v2 ? "ObjInsertAt" : "ObjInsert") + , ObjRemoveAt := Func(is_v2 ? "ObjRemoveAt" : "ObjRemove") + ; , ObjRawSet := Func(is_v2 ? "ObjRawSet" : "ObjInsert") + ; , ObjPop := Func(is_v2 ? "ObjPop" : "ObjRemove") static esc_seq := { (Join Q C (q): q, @@ -30,115 +34,136 @@ Json2(src, arg1:="", arg2:="") { "r": "`r", "t": "`t" )} + i := 0, strings := [], end := 0-is_v2 - while (i := InStr(src, q,, i+1)) { + while (i := InStr(src, q,, i+1)) + { j := i - while (j := InStr(src, q,, j+1)) { + while (j := InStr(src, q,, j+1)) + { str := SubStr(src, i+1, j-i-1) ;// 'StringReplace, str, str, \\, \u005C, A' workaround k := -5 while (k := InStr(str, "\\",, k+6)) - str := SubStr(str, 1, k-1) "\u005C" SubStr(str, k+2) + str := SubStr(str, 1, k-1) . "\u005C" . SubStr(str, k+2) if (SubStr(str, end) != "\") break } if !j - throw "Missing close quote(s)" + throw Exception("Missing close quote(s)") + src := SubStr(src, 1, i) . SubStr(src, j+1) + z := 0 - while (z := InStr(str, "\",, z+1)) { + while (z := InStr(str, "\",, z+1)) + { ch := SubStr(str, z+1, 1) - if InStr(q "btnfr/", ch, 1) { - str := SubStr(str, 1, z-1) esc_seq[ch] SubStr(str, z+2) + if InStr(q . "btnfr/", ch, 1) + str := SubStr(str, 1, z-1) . esc_seq[ch] . SubStr(str, z+2) - } else if (ch = "u") { - hex := "0x" SubStr(str, z+2, 4) + else if (ch = "u") + { + hex := "0x" . SubStr(str, z+2, 4) if !(A_IsUnicode || (Abs(hex) < 0x100)) continue - str := SubStr(str, 1, z-1) Chr(hex) SubStr(str, z+6) - - } else throw "Invalid escape sequence" + str := SubStr(str, 1, z-1) . Chr(hex) . SubStr(str, z+6) + } + else + throw Exception("Invalid escape sequence") } - %push%(strings, str) + %ObjPush%(strings, str) } + static t := "true", f := "false", n := "null", null := "" jbase := Object("[", arg1, "{", arg2) ;// { "[":arg1, "{":arg2 } , pos := 0 , key := "", is_key := false , stack := [tree := []] - , is_arr := Object(tree, 1) - , next := q "{[01234567890-tfn" - while ((ch := SubStr(src, ++pos, 1)) != "") { + , is_arr := {(tree): 1} + , next := q . "{[01234567890-tfn" + while ((ch := SubStr(src, ++pos, 1)) != "") + { if InStr(" `t`n`r", ch) continue if !InStr(next, ch) - throw "Unexpected char: '" ch "'" + throw Exception(Format("Unexpected char: '{}'", ch)) is_array := is_arr[obj := stack[1]] - if InStr("{[", ch) { + if InStr("{[", ch) + { val := (proto := jbase[ch]) ? new proto : {} - ;// is_array? %push%(obj, val) : %set%(obj, key, val) - , obj[is_array? NumGet(&obj+4*A_PtrSize)+1 : key] := val - , %ins%(stack, 1, val) + ;// is_array? %ObjPush%(obj, val) : %ObjRawSet%(obj, key, val) + , obj[is_array? NumGet(&obj + 4*A_PtrSize)+1 : key] := val + , %ObjInsertAt%(stack, 1, val) , is_arr[val] := !(is_key := ch == "{") - , next := q (is_key ? "}" : "{[]0123456789-tfn") + , next := q . (is_key ? "}" : "{[]0123456789-tfn") } - else if InStr("}]", ch) { - %del%(stack, 1) + else if InStr("}]", ch) + { + %ObjRemoveAt%(stack, 1) , next := is_arr[stack[1]] ? "]," : "}," } - else if InStr(",:", ch) { + else if InStr(",:", ch) + { if (obj == tree) - throw "Unexpected char: '" ch "' -> there is no container object." - next := q "{[0123456789-tfn", is_key := (!is_array && ch == ",") + throw Exception(Format("Unexpected char: '{}' -> there is no container object.", ch)) + next := q . "{[0123456789-tfn", is_key := (!is_array && ch == ",") } - else { - if (ch == q) { - val := %del%(strings, 1) - if is_key { + else + { + if (ch == q) + { + val := %ObjRemoveAt%(strings, 1) + if is_key + { key := val, next := ":" continue } - - } else { + } + else + { val := SubStr(src, pos, (SubStr(src, pos) ~= "[\]\},\s]|$")-1) , pos += StrLen(val)-1 - if InStr("tfn", ch, 1) { + if InStr("tfn", ch, 1) + { if !(val == %ch%) - throw "Expected '" %ch% "' instead of '" val "'" + throw Exception(Format("Expected '{}' instead of '{}'", %ch%, val)) val := %val% - - } else if (Abs(val) == "") { - throw "Invalid number: " val } + else if (Abs(val) == "") + throw Exception("Invalid number: " . val) val := val + 0 ;// val += 0 on v1.1+ converts "" to 0 } - ;// is_array? %push%(obj, val) : %set%(obj, key, val) - obj[is_array? NumGet(&obj+4*A_PtrSize)+1 : key] := val + ;// is_array? %ObjPush%(obj, val) : %ObjRawSet%(obj, key, val) + obj[is_array? NumGet(&obj + 4*A_PtrSize)+1 : key] := val , next := is_array ? "]," : "}," } } return tree[1] } -_Json2(obj, indent:="", lvl:=1) { +_Json2(obj, indent:="", lvl:=1) +{ static is_v2 := (A_AhkVersion >= "2"), q := Chr(34) - if IsObject(obj) { + if IsObject(obj) + { if (ObjGetCapacity(obj) == "") - throw "Only standard AHK objects are supported" + throw Exception("Only standard AHK objects are supported") + is_array := 0 for k in obj is_array := k == A_Index until !is_array - if (Abs(indent) != "") { + if (Abs(indent) != "") + { if (indent < 0) - throw "Indent parameter must be a postive integer" + throw Exception("Indent parameter must be a postive integer") spaces := indent, indent := "" Loop % spaces indent .= " " @@ -148,16 +173,19 @@ _Json2(obj, indent:="", lvl:=1) { indt .= indent lvl += 1, out := "" ;// Make #Warn happy - for k, v in obj { + for k, v in obj + { if IsObject(k) || (k == "") - throw "Invalid JSON key" + throw Exception("Invalid JSON key") + if !is_array out .= ( ObjGetCapacity([k], 1) ? _Json2(k) : q . k . q ) ;// key . ( indent ? ": " : ":" ) ;// token + padding out .= _Json2(v, indent, lvl) ;// value . ( indent ? ",`n" . indt : "," ) ;// token + indent } - if (out != "") { + if (out != "") + { out := Trim(out, ",`n" . indent) if (indent != "") out := "`n" . indt . out . "`n" . SubStr(indt, StrLen(indent)+1) @@ -171,7 +199,7 @@ _Json2(obj, indent:="", lvl:=1) { ;// null - not supported by AHK - static ord := Func(is_v2 ? "Ord" : "Asc") + static Ord := Func(is_v2 ? "Ord" : "Asc") static esc_seq := { ;// JSON escape sequences (Join Q C (q): "\" q, @@ -182,20 +210,24 @@ _Json2(obj, indent:="", lvl:=1) { "`r": "\r", "`t": "\t" )} - i := -1 - while (i := InStr(obj, "\",, i+2)) ;// Replacement is 2 chars long - obj := SubStr(obj, 1, i-1) "\\" SubStr(obj, i+1) - for k, v in esc_seq { + if (obj != "") + { i := -1 - while (i := InStr(obj, k,, i+2)) - obj := SubStr(obj, 1, i-1) . v . SubStr(obj, i+1) - } - i := -5 ;// i+6 -> Unicode escape sequence is 6 chars long - while (i := RegExMatch(obj, "[^\x20-\x7e]", wstr, i+6)) { - ucp := %ord%(is_v2 ? wstr.Value : wstr), hex := "\u", n := 16 - while ((n-=4) >= 0) - hex .= Chr( (x := (ucp >> n) & 15) + (x < 10 ? 48 : 55) ) - obj := SubStr(obj, 1, i-1) . hex . SubStr(obj, i+1) + while (i := InStr(obj, "\",, i+2)) ;// Replacement is 2 chars long + obj := SubStr(obj, 1, i-1) . "\\" . SubStr(obj, i+1) + for k, v in esc_seq + { + i := -1 + while (i := InStr(obj, k,, i+2)) + obj := SubStr(obj, 1, i-1) . v . SubStr(obj, i+1) + } + i := -5 ;// i+6 -> Unicode escape sequence is 6 chars long + while (i := RegExMatch(obj, "[^\x20-\x7e]", wstr, i+6)) + obj := Format("{1}\u{2:04X}{3}" + , SubStr(obj, 1, i-1) + , %Ord%(is_v2 ? wstr.Value : wstr) ;// char code point + , SubStr(obj, i+1)) } + return q . obj . q } \ No newline at end of file From 543ec4bf1c8c7b0bea4acfaa43eed2612f89e5ab Mon Sep 17 00:00:00 2001 From: Coco Belgica Date: Fri, 9 Jan 2015 06:12:44 +0800 Subject: [PATCH 26/56] Minor refactoring --- JSON.ahk | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/JSON.ahk b/JSON.ahk index 8913fd8..ef3cfb6 100644 --- a/JSON.ahk +++ b/JSON.ahk @@ -25,7 +25,7 @@ class JSON || (InStr("-0123456789", first) && !InStr("0123456789", last)) ;// number throw Exception("Invalid JSON format") - esc_seq := { + static esc_seq := { (Join """": """", "/": "/", @@ -91,7 +91,7 @@ class JSON throw Exception(Format("Missing {} {}ing bracket(s)", Abs(c1-c2), c1 > c2 ? "clos" : "open")) } - t := "true", f := "false", n := "null", null := "" + static t := "true", f := "false", n := "null", null := "" jbase := jsonize ? { "{":JSON.object, "[":JSON.array } : { "{":0, "[":0 } , pos := 0 , key := "", is_key := false @@ -225,7 +225,7 @@ class JSON ;// String ; if obj is float ; return obj - esc_seq := { + static esc_seq := { (Join """": "\""", "/": "\/", @@ -236,17 +236,19 @@ class JSON "`t": "\t" )} - StringReplace, obj, obj, \, \\, A - for k, v in esc_seq - StringReplace, obj, obj, %k%, %v%, A - - while RegExMatch(obj, "[^\x20-\x7e]", wstr) + if (obj != "") { - ucp := Asc(wstr), hex := "\u", n := 16 - while ((n -= 4) >= 0) - hex .= Chr( (x := (ucp >> n) & 15) + (x < 10 ? 48 : 55) ) - StringReplace, obj, obj, %wstr%, %hex%, A + StringReplace, obj, obj, \, \\, A + for k, v in esc_seq + StringReplace, obj, obj, %k%, %v%, A + + while RegExMatch(obj, "[^\x20-\x7e]", wstr) + { + esc_hex := Format("\u{:04X}", Asc(wstr)) + StringReplace, obj, obj, %wstr%, %esc_hex%, A + } } + return """" . obj . """" } From 64745bb45416eedcad8012b15b224e5127829f2c Mon Sep 17 00:00:00 2001 From: Coco Belgica Date: Wed, 4 Feb 2015 22:59:21 +0800 Subject: [PATCH 27/56] Renamed Json2 to Jxon(function name prefix) - New function names: Jxon_Load(), Jxon_Dump() - Added Jxon_Read() and Jxon_Write() -> for file read/write --- Json2.ahk | 211 ++++++++++++++++++++++++++++-------------------------- 1 file changed, 109 insertions(+), 102 deletions(-) diff --git a/Json2.ahk b/Json2.ahk index 77c98a4..3fa2f44 100644 --- a/Json2.ahk +++ b/Json2.ahk @@ -1,59 +1,36 @@ -Json2(src, arg1:="", arg2:="") +Jxon_Load(src, args*) { - if IsObject(src) - { - ret := _Json2(src, arg2) ;// arg2=indent - if (arg1 == "") - return ret - if !(fobj := FileOpen(arg1, "w")) ;// arg1=outfile - throw Exception(Format("Failed to open file: '{}' for writing", arg1)) - bytes := fobj.Write(ret), fobj.Close() - return bytes - } - if FileExist(src) - { - if !(fobj := FileOpen(src, "r")) - throw Exception(Format("Failed to open file: '{}' for reading.", src)) - src := fobj.Read(), fobj.Close() - } - - ;// Begin de-serialization routine - static is_v2 := (A_AhkVersion >= "2"), q := Chr(34) ;// Double quote - , ObjPush := Func(is_v2 ? "ObjPush" : "ObjInsert") - , ObjInsertAt := Func(is_v2 ? "ObjInsertAt" : "ObjInsert") - , ObjRemoveAt := Func(is_v2 ? "ObjRemoveAt" : "ObjRemove") - ; , ObjRawSet := Func(is_v2 ? "ObjRawSet" : "ObjInsert") - ; , ObjPop := Func(is_v2 ? "ObjPop" : "ObjRemove") - static esc_seq := { - (Join Q C - (q): q, - "/": "/", - "b": "`b", - "f": "`f", - "n": "`n", - "r": "`r", - "t": "`t" - )} - - i := 0, strings := [], end := 0-is_v2 - while (i := InStr(src, q,, i+1)) + static is_v2 := A_AhkVersion >= "2", q := Chr(34) + + i := 0, strings := [], end := is_v2 ? -1 : 0 + while i := InStr(src, q,, i+1) { j := i - while (j := InStr(src, q,, j+1)) + while j := InStr(src, q,, j+1) { str := SubStr(src, i+1, j-i-1) - ;// 'StringReplace, str, str, \\, \u005C, A' workaround k := -5 - while (k := InStr(str, "\\",, k+6)) + while k := InStr(str, "\\",, k+6) str := SubStr(str, 1, k-1) . "\u005C" . SubStr(str, k+2) if (SubStr(str, end) != "\") break } if !j - throw Exception("Missing close quote(s)") - + throw Exception("Missing close quote(s)", -1) + src := SubStr(src, 1, i) . SubStr(src, j+1) - + + static esc_seq := { + (Join Q C + (q): q, + "/": "/", + "b": "`b", + "f": "`f", + "n": "`n", + "r": "`r", + "t": "`t" + )} + z := 0 while (z := InStr(str, "\",, z+1)) { @@ -69,48 +46,54 @@ Json2(src, arg1:="", arg2:="") str := SubStr(str, 1, z-1) . Chr(hex) . SubStr(str, z+6) } else - throw Exception("Invalid escape sequence") + throw Exception("Invalid escape sequence", -1, "\" . ch) } + + static ObjPush := Func(is_v2 ? "ObjPush" : "ObjInsert") %ObjPush%(strings, str) } - static t := "true", f := "false", n := "null", null := "" - jbase := Object("[", arg1, "{", arg2) ;// { "[":arg1, "{":arg2 } - , pos := 0 - , key := "", is_key := false - , stack := [tree := []] - , is_arr := {(tree): 1} - , next := q . "{[01234567890-tfn" - while ((ch := SubStr(src, ++pos, 1)) != "") + 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) - throw Exception(Format("Unexpected char: '{}'", ch)) - + throw Exception("Unexpected char", -1, ch) + is_array := is_arr[obj := stack[1]] - - if InStr("{[", ch) + + if i := InStr("{[", ch) { - val := (proto := jbase[ch]) ? new proto : {} - ;// is_array? %ObjPush%(obj, val) : %ObjRawSet%(obj, key, val) - , obj[is_array? NumGet(&obj + 4*A_PtrSize)+1 : key] := val - , %ObjInsertAt%(stack, 1, val) - , is_arr[val] := !(is_key := ch == "{") - , next := q . (is_key ? "}" : "{[]0123456789-tfn") + val := (proto := args[i]) ? new proto : {} + is_array? %ObjPush%(obj, val) : obj[key] := val + + static ObjInsertAt := Func(is_v2 ? "ObjInsertAt" : "ObjInsert") + %ObjInsertAt%(stack, 1, val) + + is_arr[val] := !(is_key := ch == "{") + next := q . (is_key ? "}" : "{[]0123456789-tfn") } else if InStr("}]", ch) { + static ObjRemoveAt := Func(is_v2 ? "ObjRemoveAt" : "ObjRemove") %ObjRemoveAt%(stack, 1) - , next := is_arr[stack[1]] ? "]," : "}," + + next := is_arr[stack[1]] ? "]," : "}," } else if InStr(",:", ch) { if (obj == tree) - throw Exception(Format("Unexpected char: '{}' -> there is no container object.", ch)) - next := q . "{[0123456789-tfn", is_key := (!is_array && ch == ",") + throw Exception("Unexpected char -> there is no container object", -1, ch) + + is_key := (!is_array && ch == ",") + next := q . "{[0123456789-tfn" } else @@ -124,37 +107,40 @@ Json2(src, arg1:="", arg2:="") continue } } + else { val := SubStr(src, pos, (SubStr(src, pos) ~= "[\]\},\s]|$")-1) - , pos += StrLen(val)-1 - if InStr("tfn", ch, 1) + pos += StrLen(val)-1 + if InStr("tfn", ch) ; case-insensitive to avoid casting it to 'else if' { - if !(val == %ch%) - throw Exception(Format("Expected '{}' instead of '{}'", %ch%, val)) + static t := "true", f := "false", n := "null", null := "" + if !(val == %ch%) ; case-sensitive comparison + throw Exception(Format("Expected '{}' instead of '{}'", %ch%, val), -1) val := %val% } else if (Abs(val) == "") - throw Exception("Invalid number: " . val) - val := val + 0 ;// val += 0 on v1.1+ converts "" to 0 + throw Exception("Invalid number", -1, val) + val := val + 0 ; val += 0 on v1.1+ converts "" to 0 } - ;// is_array? %ObjPush%(obj, val) : %ObjRawSet%(obj, key, val) - obj[is_array? NumGet(&obj + 4*A_PtrSize)+1 : key] := val - , next := is_array ? "]," : "}," + + is_array? %ObjPush%(obj, val) : obj[key] := val + next := is_array ? "]," : "}," } } + return tree[1] } -_Json2(obj, indent:="", lvl:=1) +Jxon_Dump(obj, indent:="", lvl:=1) { - static is_v2 := (A_AhkVersion >= "2"), q := Chr(34) + static q := Chr(34) if IsObject(obj) { if (ObjGetCapacity(obj) == "") - throw Exception("Only standard AHK objects are supported") - + throw Exception("Only standard AHK objects are supported.", -1, Format("0x{:x}", &obj)) + is_array := 0 for k in obj is_array := k == A_Index @@ -163,7 +149,7 @@ _Json2(obj, indent:="", lvl:=1) if (Abs(indent) != "") { if (indent < 0) - throw Exception("Indent parameter must be a postive integer") + throw Exception("Indent parameter must be a postive integer.", -1, indent) spaces := indent, indent := "" Loop % spaces indent .= " " @@ -176,14 +162,15 @@ _Json2(obj, indent:="", lvl:=1) for k, v in obj { if IsObject(k) || (k == "") - throw Exception("Invalid JSON key") + throw Exception("Invalid JSON key", -1) if !is_array - out .= ( ObjGetCapacity([k], 1) ? _Json2(k) : q . k . q ) ;// key + out .= ( ObjGetCapacity([k], 1) ? Jxon_Dump(k) : q . k . q ) ;// key . ( indent ? ": " : ":" ) ;// token + padding - out .= _Json2(v, indent, lvl) ;// value + out .= Jxon_Dump(v, indent, lvl) ;// value . ( indent ? ",`n" . indt : "," ) ;// token + indent } + if (out != "") { out := Trim(out, ",`n" . indent) @@ -194,40 +181,60 @@ _Json2(obj, indent:="", lvl:=1) return is_array ? "[" out "]" : "{" out "}" } - else if (ObjGetCapacity([obj], 1) == "") ;// number + ; Number + else if (ObjGetCapacity([obj], 1) == "") return obj - ;// null - not supported by AHK - - static Ord := Func(is_v2 ? "Ord" : "Asc") - static esc_seq := { ;// JSON escape sequences - (Join Q C - (q): "\" q, - "/": "\/", - "`b": "\b", - "`f": "\f", - "`n": "\n", - "`r": "\r", - "`t": "\t" - )} + ; String (null -> not supported by AHK) if (obj != "") { + static esc_seq := { ; JSON escape sequences + (Join Q C + (q): "\" q, + "/": "\/", + "`b": "\b", + "`f": "\f", + "`n": "\n", + "`r": "\r", + "`t": "\t" + )} + i := -1 - while (i := InStr(obj, "\",, i+2)) ;// Replacement is 2 chars long + while i := InStr(obj, "\",, i+2) ; Replacement is 2 chars long obj := SubStr(obj, 1, i-1) . "\\" . SubStr(obj, i+1) for k, v in esc_seq { i := -1 - while (i := InStr(obj, k,, i+2)) + while i := InStr(obj, k,, i+2) obj := SubStr(obj, 1, i-1) . v . SubStr(obj, i+1) } - i := -5 ;// i+6 -> Unicode escape sequence is 6 chars long - while (i := RegExMatch(obj, "[^\x20-\x7e]", wstr, i+6)) + + static Ord := Func(A_AhkVersion<"2" ? "Asc" : "Ord") + i := -5 ; i+6 -> Unicode escape sequence is 6 chars long + while i := RegExMatch(obj, "[^\x20-\x7e]", wstr, i+6) obj := Format("{1}\u{2:04X}{3}" , SubStr(obj, 1, i-1) - , %Ord%(is_v2 ? wstr.Value : wstr) ;// char code point + , %Ord%(IsObject(wstr) ? wstr.Value : wstr) ; char code point , SubStr(obj, i+1)) } return q . obj . q +} + +Jxon_Read(src, prototype*) +{ + if f := FileOpen(src, "r", "UTF-8") + { + jstr := f.Read(), f.Close() + return Jxon_Load(jstr, prototype*) + } +} + +Jxon_Write(obj, dest, indent:="") +{ + if f := FileOpen(dest, "w", "UTF-8") + { + bytes := f.Write(Jxon_Dump(obj, indent)), f.Close() + return bytes + } } \ No newline at end of file From 3435fce5bb8debe5235571fc43b31efd1087f278 Mon Sep 17 00:00:00 2001 From: Coco Belgica Date: Wed, 4 Feb 2015 23:07:15 +0800 Subject: [PATCH 28/56] File rename to reflect recent changes. --- Json2.ahk => Jxon.ahk | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename Json2.ahk => Jxon.ahk (100%) diff --git a/Json2.ahk b/Jxon.ahk similarity index 100% rename from Json2.ahk rename to Jxon.ahk From d24c6a76ea83151920e5c7de4a0308a6f9b4c785 Mon Sep 17 00:00:00 2001 From: Coco Belgica Date: Wed, 4 Feb 2015 23:27:38 +0800 Subject: [PATCH 29/56] Updated README to reflect changes to Jxon.ahk(previously Json2.ahk) --- README.md | 61 +++++++++++++++++++++++++++++++++++-------------------- 1 file changed, 39 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index 7f94803..87f6f87 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# JSON and JSON2 +# JSON and Jxon #### [JSON](http://json.org/) module for [AutoHotkey](http://ahkscript.org/) @@ -8,7 +8,7 @@ License: [WTFPL](http://wtfpl.net/) - - - -## JSON (class) +## JSON.ahk (class) There are multiple version(s) available (as branches) in this repo to provide support for different AutoHotkey builds. The _master_ branch is for `v1.1` _(except for Json2.ahk which is version independent)_ while the _v2_ branch is for `AHK v2.0-a`. - - - @@ -17,9 +17,9 @@ There are multiple version(s) available (as branches) in this repo to provide su Deserialize _src_ (a JSON formatted string) to an AutoHotkey object #### Syntax: -``` -obj := JSON.parse( src [, jsonize := false ] ) -``` + + obj := JSON.parse( src [, jsonize := false ] ) + #### Return Value: An AutoHotkey object @@ -34,9 +34,9 @@ An AutoHotkey object serialize _obj_ to a JSON formatted string #### Syntax: -``` -str := JSON.stringify( obj, [, indent := "" ] ) -``` + + str := JSON.stringify( obj, [, indent := "" ] ) + #### Return Value: A JSON formatted string @@ -47,18 +47,18 @@ A JSON formatted string - - - -## JSON2 (function) +## 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. Works on both AutoHotkey _v1.1_ and _v2.0_ - - - -#### Deserialize +### Jxon_Load() Deserialize _src_ (a JSON formatted string) to an AutoHotkey object #### Syntax: -``` -obj := Json2( src [, object_base := "", array_base := "" ]) -``` + + obj := Jxon_Load( src [ , object_base := "", array_base := "" ] ) + #### Parameter(s): * **src** [in] - JSON formatted string or path to the file containing JSON formatted string. @@ -67,19 +67,36 @@ obj := Json2( src [, object_base := "", array_base := "" ]) - - - -#### Serialize -Serialize _obj_ to a JSON formatted string OR dumps _obj_ to the file specified in _out_ +### Jxon_Dump() +Serialize _obj_ to a JSON formatted string #### Syntax: -``` -str := Json2( obj [, out := "", indent := "" ]) -bytes_written := Json2( obj, out [, indent := "" ]) -``` + + str := Jxon_Dump( obj [ , indent := "" ] ) + #### Return Value: -A JSON formatted string. If _out_ is specified, the number of bytes written is returned. +A JSON formatted string. #### Parameter(s): * **obj** [in] - this argument has the same meaning as in _JSON.stringify()_ - * **out** [in, opt] - path to the file to write to. If specified, the function returns the number of bytes written. - * **indent** [in, opt] - this argument has the same meaning as in _JSON.stringify()_ \ No newline at end of file + * **indent** [in, opt] - this argument has the same meaning as in _JSON.stringify()_ + +- - - + +### Jxon_Read() +Similar to `Jxon_Load()` except _src_ is a path to the file containing a JSON document. + +#### Syntax: + + Jxon_Read( src [ , object_base := "", array_base := "" ] ) + +
+- - - + +### Jxon_Write() +Similar to `Jxon_Dump()` except output is written to _dest_. The number of bytes written is returned if successful. + +#### Syntax: + + bytes_written := Jxon_Write( obj , dest [ , indent := "" ] ) \ No newline at end of file From cc42d90bb5b67cf6c0411fb3c7c1522390e08fb9 Mon Sep 17 00:00:00 2001 From: Coco Belgica Date: Wed, 11 Feb 2015 21:46:21 +0800 Subject: [PATCH 30/56] Improved string replacement routines, ditched previous workaround. --- Jxon.ahk | 121 +++++++++++++++++++++++-------------------------------- 1 file changed, 50 insertions(+), 71 deletions(-) diff --git a/Jxon.ahk b/Jxon.ahk index 3fa2f44..b955d54 100644 --- a/Jxon.ahk +++ b/Jxon.ahk @@ -2,51 +2,43 @@ Jxon_Load(src, args*) { static is_v2 := A_AhkVersion >= "2", q := Chr(34) - i := 0, strings := [], end := is_v2 ? -1 : 0 + i := 0, strings := [] while i := InStr(src, q,, i+1) { + static Replace := Func(is_v2 ? "StrReplace" : "RegExReplace") + static bash := is_v2 ? "\" : "\\" + j := i while j := InStr(src, q,, j+1) { - str := SubStr(src, i+1, j-i-1) - k := -5 - while k := InStr(str, "\\",, k+6) - str := SubStr(str, 1, k-1) . "\u005C" . SubStr(str, k+2) + str := %Replace%(SubStr(src, i+1, j-i-1), bash . bash, "\u005C") + static end := is_v2 ? -1 : 0 if (SubStr(str, end) != "\") break } if !j - throw Exception("Missing close quote(s)", -1) + throw Exception("Missing close quote(s).", -1) src := SubStr(src, 1, i) . SubStr(src, j+1) - static esc_seq := { - (Join Q C - (q): q, - "/": "/", - "b": "`b", - "f": "`f", - "n": "`n", - "r": "`r", - "t": "`t" - )} - - z := 0 - while (z := InStr(str, "\",, z+1)) + str := %Replace%(str, bash . "/", "/") + , str := %Replace%(str, bash . q, q) + , str := %Replace%(str, bash . "b", "`b") + , str := %Replace%(str, bash . "f", "`f") + , str := %Replace%(str, bash . "n", "`n") + , str := %Replace%(str, bash . "r", "`r") + , str := %Replace%(str, bash . "t", "`t") + + j := 0 + while j := InStr(str, "\",, j+1) ; parse remaining chars with preceding "\" { - ch := SubStr(str, z+1, 1) - if InStr(q . "btnfr/", ch, 1) - str := SubStr(str, 1, z-1) . esc_seq[ch] . SubStr(str, z+2) - - else if (ch = "u") - { - hex := "0x" . SubStr(str, z+2, 4) - if !(A_IsUnicode || (Abs(hex) < 0x100)) - continue - str := SubStr(str, 1, z-1) . Chr(hex) . SubStr(str, z+6) - } - else - throw Exception("Invalid escape sequence", -1, "\" . ch) + if (SubStr(str, j+1, 1) != "u") + throw Exception("Invalid escape sequence.", -1, SubStr(str, j, 2)) + + ; \uXXXX - JSON unicode escape sequence + ord := Abs("0x" . SubStr(str, j+2, 4)) ; XXXX + if (A_IsUnicode || ord < 0x100) + str := SubStr(str, 1, j-1) . Chr(ord) . SubStr(str, j+6) } static ObjPush := Func(is_v2 ? "ObjPush" : "ObjInsert") @@ -63,7 +55,7 @@ Jxon_Load(src, args*) if InStr(" `t`n`r", ch) continue if !InStr(next, ch) - throw Exception("Unexpected char", -1, ch) + throw Exception("Unexpected char.", -1, ch) is_array := is_arr[obj := stack[1]] @@ -90,7 +82,7 @@ Jxon_Load(src, args*) else if InStr(",:", ch) { if (obj == tree) - throw Exception("Unexpected char -> there is no container object", -1, ch) + throw Exception("Unexpected char -> there is no container object.", -1, ch) is_key := (!is_array && ch == ",") next := q . "{[0123456789-tfn" @@ -98,7 +90,7 @@ Jxon_Load(src, args*) else { - if (ch == q) + if (ch == q) ; string { val := %ObjRemoveAt%(strings, 1) if is_key @@ -108,7 +100,7 @@ Jxon_Load(src, args*) } } - else + else ; number, true|false|null { val := SubStr(src, pos, (SubStr(src, pos) ~= "[\]\},\s]|$")-1) pos += StrLen(val)-1 @@ -116,11 +108,11 @@ Jxon_Load(src, args*) { static t := "true", f := "false", n := "null", null := "" if !(val == %ch%) ; case-sensitive comparison - throw Exception(Format("Expected '{}' instead of '{}'", %ch%, val), -1) + throw Exception(Format("Expected '{}' instead of '{}'.", %ch%, val), -1) val := %val% } else if (Abs(val) == "") - throw Exception("Invalid number", -1, val) + throw Exception("Invalid number.", -1, val) val := val + 0 ; val += 0 on v1.1+ converts "" to 0 } @@ -139,14 +131,15 @@ Jxon_Dump(obj, indent:="", lvl:=1) if IsObject(obj) { if (ObjGetCapacity(obj) == "") - throw Exception("Only standard AHK objects are supported.", -1, Format("0x{:x}", &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 - if (Abs(indent) != "") + static integer := "integer" + if indent is %integer% { if (indent < 0) throw Exception("Indent parameter must be a postive integer.", -1, indent) @@ -158,7 +151,7 @@ Jxon_Dump(obj, indent:="", lvl:=1) Loop, % indent ? lvl : 0 indt .= indent - lvl += 1, out := "" ;// Make #Warn happy + lvl += 1, out := "" ; Make #Warn happy for k, v in obj { if IsObject(k) || (k == "") @@ -166,9 +159,9 @@ Jxon_Dump(obj, indent:="", lvl:=1) 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 + . ( indent ? ": " : ":" ) ; token + padding + out .= Jxon_Dump(v, indent, lvl) ; value + . ( indent ? ",`n" . indt : "," ) ; token + indent } if (out != "") @@ -178,7 +171,7 @@ Jxon_Dump(obj, indent:="", lvl:=1) out := "`n" . indt . out . "`n" . SubStr(indt, StrLen(indent)+1) } - return is_array ? "[" out "]" : "{" out "}" + return is_array ? "[" . out . "]" : "{" . out . "}" } ; Number @@ -188,34 +181,20 @@ Jxon_Dump(obj, indent:="", lvl:=1) ; String (null -> not supported by AHK) if (obj != "") { - static esc_seq := { ; JSON escape sequences - (Join Q C - (q): "\" q, - "/": "\/", - "`b": "\b", - "`f": "\f", - "`n": "\n", - "`r": "\r", - "`t": "\t" - )} - - i := -1 - while i := InStr(obj, "\",, i+2) ; Replacement is 2 chars long - obj := SubStr(obj, 1, i-1) . "\\" . SubStr(obj, i+1) - for k, v in esc_seq - { - i := -1 - while i := InStr(obj, k,, i+2) - obj := SubStr(obj, 1, i-1) . v . SubStr(obj, i+1) - } + static Replace := Func(A_AhkVersion<"2" ? "RegExReplace" : "StrReplace") + static bash := A_AhkVersion<"2" ? "\\" : "\" + obj := %Replace%(obj, bash, "\\") + , obj := %Replace%(obj, "/", "\/") + , obj := %Replace%(obj, q, "\" . q) + , obj := %Replace%(obj, "`b", "\b") + , obj := %Replace%(obj, "`f", "\f") + , obj := %Replace%(obj, "`n", "\n") + , obj := %Replace%(obj, "`r", "\r") + , obj := %Replace%(obj, "`t", "\t") static Ord := Func(A_AhkVersion<"2" ? "Asc" : "Ord") - i := -5 ; i+6 -> Unicode escape sequence is 6 chars long - while i := RegExMatch(obj, "[^\x20-\x7e]", wstr, i+6) - obj := Format("{1}\u{2:04X}{3}" - , SubStr(obj, 1, i-1) - , %Ord%(IsObject(wstr) ? wstr.Value : wstr) ; char code point - , SubStr(obj, i+1)) + while RegExMatch(obj, "[^\x20-\x7e]", m) + obj := %Replace%(obj, ch := IsObject(m) ? m[0] : m, Format("\u{:04X}", %Ord%(ch))) } return q . obj . q From b33db323c3650327b8c7a9f545a0a464516ea4f3 Mon Sep 17 00:00:00 2001 From: Coco Belgica Date: Sat, 14 Feb 2015 15:58:35 +0800 Subject: [PATCH 31/56] Jxon_Load(): Changed from two-pass to single-pass method - quoted literal strings are no longer extracted prior markup parsing. This allows passing of variable(if any) containing the JSON document as ByRef. --- Jxon.ahk | 95 ++++++++++++++++++++++++++------------------------------ 1 file changed, 44 insertions(+), 51 deletions(-) diff --git a/Jxon.ahk b/Jxon.ahk index b955d54..7bf48ef 100644 --- a/Jxon.ahk +++ b/Jxon.ahk @@ -1,49 +1,6 @@ -Jxon_Load(src, args*) +Jxon_Load(ByRef src, args*) { - static is_v2 := A_AhkVersion >= "2", q := Chr(34) - - i := 0, strings := [] - while i := InStr(src, q,, i+1) - { - static Replace := Func(is_v2 ? "StrReplace" : "RegExReplace") - static bash := is_v2 ? "\" : "\\" - - j := i - while j := InStr(src, q,, j+1) - { - str := %Replace%(SubStr(src, i+1, j-i-1), bash . bash, "\u005C") - static end := is_v2 ? -1 : 0 - if (SubStr(str, end) != "\") - break - } - if !j - throw Exception("Missing close quote(s).", -1) - - src := SubStr(src, 1, i) . SubStr(src, j+1) - - str := %Replace%(str, bash . "/", "/") - , str := %Replace%(str, bash . q, q) - , str := %Replace%(str, bash . "b", "`b") - , str := %Replace%(str, bash . "f", "`f") - , str := %Replace%(str, bash . "n", "`n") - , str := %Replace%(str, bash . "r", "`r") - , str := %Replace%(str, bash . "t", "`t") - - j := 0 - while j := InStr(str, "\",, j+1) ; parse remaining chars with preceding "\" - { - if (SubStr(str, j+1, 1) != "u") - throw Exception("Invalid escape sequence.", -1, SubStr(str, j, 2)) - - ; \uXXXX - JSON unicode escape sequence - ord := Abs("0x" . SubStr(str, j+2, 4)) ; XXXX - if (A_IsUnicode || ord < 0x100) - str := SubStr(str, 1, j-1) . Chr(ord) . SubStr(str, j+6) - } - - static ObjPush := Func(is_v2 ? "ObjPush" : "ObjInsert") - %ObjPush%(strings, str) - } + static q := Chr(34) key := "", is_key := false stack := [ tree := [] ] @@ -52,6 +9,11 @@ Jxon_Load(src, args*) pos := 0 while ( (ch := SubStr(src, ++pos, 1)) != "" ) { + static is_v2 := A_AhkVersion >= "2" + static ObjPush := Func(is_v2 ? "ObjPush" : "ObjInsert") + static ObjInsertAt := Func(is_v2 ? "ObjInsertAt" : "ObjInsert") + static ObjRemoveAt := Func(is_v2 ? "ObjRemoveAt" : "ObjRemove") + if InStr(" `t`n`r", ch) continue if !InStr(next, ch) @@ -63,8 +25,6 @@ Jxon_Load(src, args*) { val := (proto := args[i]) ? new proto : {} is_array? %ObjPush%(obj, val) : obj[key] := val - - static ObjInsertAt := Func(is_v2 ? "ObjInsertAt" : "ObjInsert") %ObjInsertAt%(stack, 1, val) is_arr[val] := !(is_key := ch == "{") @@ -73,9 +33,7 @@ Jxon_Load(src, args*) else if InStr("}]", ch) { - static ObjRemoveAt := Func(is_v2 ? "ObjRemoveAt" : "ObjRemove") %ObjRemoveAt%(stack, 1) - next := is_arr[stack[1]] ? "]," : "}," } @@ -88,11 +46,46 @@ Jxon_Load(src, args*) next := q . "{[0123456789-tfn" } - else + else ; string | number | true | false | null { if (ch == q) ; string { - val := %ObjRemoveAt%(strings, 1) + static Replace := Func(is_v2 ? "StrReplace" : "RegExReplace") + static bash := is_v2 ? "\" : "\\" + + i := pos + while i := InStr(src, q,, i+1) + { + val := %Replace%(SubStr(src, pos+1, i-pos-1), bash . bash, "\u005C") + static end := is_v2 ? -1 : 0 + if (SubStr(val, end) != "\") + break + } + if !i + throw Exception("Missing close quote(s).", -1) + + pos := i ; update pos + + val := %Replace%(val, bash . "/", "/") + , val := %Replace%(val, bash . q, q) + , val := %Replace%(val, bash . "b", "`b") + , val := %Replace%(val, bash . "f", "`f") + , val := %Replace%(val, bash . "n", "`n") + , val := %Replace%(val, bash . "r", "`r") + , val := %Replace%(val, bash . "t", "`t") + + i := 0 + while i := InStr(val, "\",, i+1) + { + if (SubStr(val, i+1, 1) != "u") + throw Exception("Invalid escape sequence.", -1, SubStr(val, i, 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 := ":" From c664a371f768fb10bf2f26177a064aef765f0b64 Mon Sep 17 00:00:00 2001 From: Coco Belgica Date: Sat, 14 Feb 2015 16:01:04 +0800 Subject: [PATCH 32/56] Jxon_Load() - Simplified parsing of number(s), true, false and null. --- Jxon.ahk | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/Jxon.ahk b/Jxon.ahk index 7bf48ef..9bfbcef 100644 --- a/Jxon.ahk +++ b/Jxon.ahk @@ -93,20 +93,17 @@ Jxon_Load(ByRef src, args*) } } - else ; number, true|false|null + else ; number | true,false,null { - val := SubStr(src, pos, (SubStr(src, pos) ~= "[\]\},\s]|$")-1) - pos += StrLen(val)-1 - if InStr("tfn", ch) ; case-insensitive to avoid casting it to 'else if' - { - static t := "true", f := "false", n := "null", null := "" - if !(val == %ch%) ; case-sensitive comparison - throw Exception(Format("Expected '{}' instead of '{}'.", %ch%, val), -1) + val := SubStr(src, pos, i := RegExMatch(src, "[\]\},\s]|$",, pos)-pos) + + static null := "" + if InStr(",true,false,null,", "," . val . ",", true) ; if var in val := %val% - } else if (Abs(val) == "") - throw Exception("Invalid number.", -1, val) - val := val + 0 ; val += 0 on v1.1+ converts "" to 0 + throw Exception("Invalid JSON value.", -1, val) + + val := val + 0, pos += i-1 } is_array? %ObjPush%(obj, val) : obj[key] := val From 315d6fe71eab9f0e9df86ee219a524ad91e6d6ca Mon Sep 17 00:00:00 2001 From: Coco Belgica Date: Sat, 14 Feb 2015 16:03:35 +0800 Subject: [PATCH 33/56] Jxon_Load() - Fixed a bug when validating next char after comma(,) in object. --- Jxon.ahk | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Jxon.ahk b/Jxon.ahk index 9bfbcef..6f605e5 100644 --- a/Jxon.ahk +++ b/Jxon.ahk @@ -43,7 +43,7 @@ Jxon_Load(ByRef src, args*) throw Exception("Unexpected char -> there is no container object.", -1, ch) is_key := (!is_array && ch == ",") - next := q . "{[0123456789-tfn" + next := is_key ? q : q . "{[0123456789-tfn" } else ; string | number | true | false | null From 46c262ba148aa94821e72aa8d3e5fe4a26e626d1 Mon Sep 17 00:00:00 2001 From: Coco Belgica Date: Sat, 14 Feb 2015 16:17:08 +0800 Subject: [PATCH 34/56] Jxon_Load() - Improved exception handling - error messages are more descriptive and provide information on the line number, column number and character position of the invalid data/token. --- Jxon.ahk | 42 ++++++++++++++++++++++++++++++------------ 1 file changed, 30 insertions(+), 12 deletions(-) diff --git a/Jxon.ahk b/Jxon.ahk index 6f605e5..1649dce 100644 --- a/Jxon.ahk +++ b/Jxon.ahk @@ -16,8 +16,26 @@ Jxon_Load(ByRef src, args*) if InStr(" `t`n`r", ch) continue - if !InStr(next, ch) - throw Exception("Unexpected char.", -1, ch) + if !InStr(next, ch, true) + { + ln := StrSplit(SubStr(src, 1, pos), "`n")[is_v2 ? "Length" : "MaxIndex"]() + 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]] @@ -34,7 +52,7 @@ Jxon_Load(ByRef src, args*) else if InStr("}]", ch) { %ObjRemoveAt%(stack, 1) - next := is_arr[stack[1]] ? "]," : "}," + next := stack[1]==tree ? "" : is_arr[stack[1]] ? ",]" : ",}" } else if InStr(",:", ch) @@ -61,8 +79,8 @@ Jxon_Load(ByRef src, args*) if (SubStr(val, end) != "\") break } - if !i - throw Exception("Missing close quote(s).", -1) + if !i ? (pos--, next := "'") : 0 + continue pos := i ; update pos @@ -77,8 +95,8 @@ Jxon_Load(ByRef src, args*) i := 0 while i := InStr(val, "\",, i+1) { - if (SubStr(val, i+1, 1) != "u") - throw Exception("Invalid escape sequence.", -1, SubStr(val, i, 2)) + 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)) @@ -93,21 +111,21 @@ Jxon_Load(ByRef src, args*) } } - else ; number | true,false,null + else ; number | true | false | null { val := SubStr(src, pos, i := RegExMatch(src, "[\]\},\s]|$",, pos)-pos) - static null := "" + static null := "", Ord := Func(is_v2 ? "Ord" : "Asc") if InStr(",true,false,null,", "," . val . ",", true) ; if var in val := %val% - else if (Abs(val) == "") - throw Exception("Invalid JSON value.", -1, val) + else if (Abs(val) == "") ? (pos--, next := Chr(%Ord%(ch)+1)) : 0 + continue val := val + 0, pos += i-1 } is_array? %ObjPush%(obj, val) : obj[key] := val - next := is_array ? "]," : "}," + next := obj==tree ? "" : is_array ? ",]" : ",}" } } From a4824b728218a81bcd66d383184f66eeeec94c3d Mon Sep 17 00:00:00 2001 From: Coco Belgica Date: Sat, 14 Feb 2015 16:30:08 +0800 Subject: [PATCH 35/56] Removed Jxon_Read() and Jxon_Write(). It was a mistake including them in the first place. --- Jxon.ahk | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/Jxon.ahk b/Jxon.ahk index 1649dce..8ba6c49 100644 --- a/Jxon.ahk +++ b/Jxon.ahk @@ -206,22 +206,4 @@ Jxon_Dump(obj, indent:="", lvl:=1) } return q . obj . q -} - -Jxon_Read(src, prototype*) -{ - if f := FileOpen(src, "r", "UTF-8") - { - jstr := f.Read(), f.Close() - return Jxon_Load(jstr, prototype*) - } -} - -Jxon_Write(obj, dest, indent:="") -{ - if f := FileOpen(dest, "w", "UTF-8") - { - bytes := f.Write(Jxon_Dump(obj, indent)), f.Close() - return bytes - } } \ No newline at end of file From 5f8effb9934d929b883ec633b387166d50b1f8b9 Mon Sep 17 00:00:00 2001 From: Coco Belgica Date: Sun, 15 Feb 2015 00:16:29 +0800 Subject: [PATCH 36/56] Jxon_Load(): Removed unneeded validation(forgot to remove it). Jxon_Dump(): Improved error info if object key is invalid. --- Jxon.ahk | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/Jxon.ahk b/Jxon.ahk index 8ba6c49..45915da 100644 --- a/Jxon.ahk +++ b/Jxon.ahk @@ -57,9 +57,6 @@ Jxon_Load(ByRef src, args*) else if InStr(",:", ch) { - if (obj == tree) - throw Exception("Unexpected char -> there is no container object.", -1, ch) - is_key := (!is_array && ch == ",") next := is_key ? q : q . "{[0123456789-tfn" } @@ -163,7 +160,7 @@ Jxon_Dump(obj, indent:="", lvl:=1) for k, v in obj { if IsObject(k) || (k == "") - throw Exception("Invalid JSON key", -1) + throw Exception("Invalid object key.", -1, k ? Format("", &obj) : "") if !is_array out .= ( ObjGetCapacity([k], 1) ? Jxon_Dump(k) : q . k . q ) ;// key From f56200ab1f1e03b1ff40be77e23e83a6ab956654 Mon Sep 17 00:00:00 2001 From: Coco Belgica Date: Sun, 15 Feb 2015 09:49:23 +0800 Subject: [PATCH 37/56] Jxon_Load(): Simplified dummy 'next' variable(for invalid values). The fact that the block is reached means that the currently evaluated char/token must be either of these: '01234567890-tfn'. So to signal an error, 'next' must simply be neither of those. --- Jxon.ahk | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Jxon.ahk b/Jxon.ahk index 45915da..5b34c5b 100644 --- a/Jxon.ahk +++ b/Jxon.ahk @@ -112,10 +112,10 @@ Jxon_Load(ByRef src, args*) { val := SubStr(src, pos, i := RegExMatch(src, "[\]\},\s]|$",, pos)-pos) - static null := "", Ord := Func(is_v2 ? "Ord" : "Asc") + static null := "" ; for #Warn if InStr(",true,false,null,", "," . val . ",", true) ; if var in val := %val% - else if (Abs(val) == "") ? (pos--, next := Chr(%Ord%(ch)+1)) : 0 + else if (Abs(val) == "") ? (pos--, next := "#") : 0 continue val := val + 0, pos += i-1 From 834f5f6015529b2bb38bf8fea752bafe108ead05 Mon Sep 17 00:00:00 2001 From: Coco Belgica Date: Sun, 15 Feb 2015 11:36:03 +0800 Subject: [PATCH 38/56] Major update based on some commits to Jxon.ahk. General - [commit cc42d90] Improved string replacement routines. JSON.parse() - [commit b33db32] Changed from two-pass to single-pass method. - [commit c664a37] Simplified parsing of number(s), true, false and null. - [commit 315d6fe] Fixed a bug when validating next char after comma(,) in object. - [commit 46c262b] Improved exception handling w/ descriptive error messages. --- JSON.ahk | 260 +++++++++++++++++++++++-------------------------------- 1 file changed, 108 insertions(+), 152 deletions(-) diff --git a/JSON.ahk b/JSON.ahk index ef3cfb6..dd6993f 100644 --- a/JSON.ahk +++ b/JSON.ahk @@ -9,156 +9,125 @@ class JSON * jsonize [in] - If true, objects {} and arrays [] are wrapped as * JSON.object and JSON.array instances respectively. */ - parse(src, jsonize:=false) + parse(ByRef src, jsonize:=false) { - ;// Pre-validate JSON source before parsing - if ((src := Trim(src, " `t`n`r")) == "") ;// trim whitespace(s) - throw "Empty JSON source" - first := SubStr(src, 1, 1), last := SubStr(src, 0) - 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) && !InStr("0123456789", last)) ;// number - throw Exception("Invalid JSON format") - - static esc_seq := { - (Join - """": """", - "/": "/", - "b": "`b", - "f": "`f", - "n": "`n", - "r": "`r", - "t": "`t" - )} - 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 - } - 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, 1) - str := SubStr(str, 1, k-1) . esc_seq[ch] . SubStr(str, k+2) - - else if (ch == "u") - { - hex := "0x" . SubStr(str, k+2, 4) - if !(A_IsUnicode || (Abs(hex) < 0x100)) - continue ;// throw Exception() ??? - str := SubStr(str, 1, k-1) . Chr(hex) . SubStr(str, k+6) - - } - else - throw Exception(Format("Invalid escape sequence: '\{}'", ch)) - } - strings.Insert(str) - } - - ;// Check for missing opening/closing brace(s) - if InStr(src, "{") || InStr(src, "}") - { - StringReplace, dummy, src, {, {, UseErrorLevel - c1 := ErrorLevel - StringReplace, dummy, src, }, }, UseErrorLevel - c2 := ErrorLevel - if (c1 != c2) - throw Exception(Format("Missing {} {}ing brace(s)", Abs(c1-c2), c1 > c2 ? "clos" : "open")) - } - ;// Check for missing opening/closing bracket(s) - if InStr(src, "[") || InStr(src, "]") - { - StringReplace, dummy, src, [, [, UseErrorLevel - c1 := ErrorLevel - StringReplace, dummy, src, ], ], UseErrorLevel - c2 := ErrorLevel - if (c1 != c2) - throw Exception(Format("Missing {} {}ing bracket(s)", Abs(c1-c2), c1 > c2 ? "clos" : "open")) - } - - 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)) != "") + args := jsonize ? [ JSON.object, JSON.array ] : [] + key := "", is_key := false + stack := [ tree := [] ] + is_arr := { (tree): 1 } + next := """{[01234567890-tfn" + pos := 0 + while ( (ch := SubStr(src, ++pos, 1)) != "" ) { if InStr(" `t`n`r", ch) continue if !InStr(next, ch) - throw Exception(Format("Unexpected char: '{}'", ch)) + { + ln := ObjMaxIndex(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 == """") ? "Expecting object key enclosed in double quotes" + : (next == """}") ? "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 InStr("{[", ch) + if i := InStr("{[", ch) { - val := (proto := jbase[ch]) ? new proto : {} - , obj[is_array? NumGet(&obj + 4*A_PtrSize)+1 : key] := val - , ObjInsert(stack, 1, val) - , is_arr[val] := !(is_key := ch == "{") - , next := is_key ? """}" : """{[]0123456789-tfn" + val := (proto := args[i]) ? new proto : {} + is_array? ObjInsert(obj, val) : obj[key] := val + ObjInsert(stack, 1, val) + + is_arr[val] := !(is_key := ch == "{") + next := is_key ? """}" : """{[]0123456789-tfn" } else if InStr("}]", ch) { ObjRemove(stack, 1) - , next := is_arr[stack[1]] ? "]," : "}," + next := stack[1]==tree ? "" : is_arr[stack[1]] ? ",]" : ",}" } else if InStr(",:", ch) { - if (obj == tree) - throw Exception(Format("Unexpected char: '{}' -> there is no container object.", ch)) - next := """{[0123456789-tfn", is_key := (!is_array && ch == ",") + is_key := (!is_array && ch == ",") + next := is_key ? """" : """{[0123456789-tfn" } else { if (ch == """") { - val := ObjRemove(strings, 1) + i := pos + while (i := InStr(src, """",, i+1)) + { + val := SubStr(src, pos+1, i-pos-1) + StringReplace, val, val, \\, \u005C, A + if (SubStr(val, 0) != "\") + break + } + if !i ? (pos--, next := "'") : 0 + continue + + pos := i + + StringReplace, val, val, \/, /, A + StringReplace, val, val, \", ", A + StringReplace, val, val, \b, `b, A + StringReplace, val, val, \f, `f, A + StringReplace, val, val, \n, `n, A + StringReplace, val, val, \r, `r, A + StringReplace, val, val, \t, `t, A + + 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 { - val := SubStr(src, pos, (SubStr(src, pos) ~= "[\]\},\s]|$")-1) - , pos += StrLen(val)-1 - if InStr("tfn", ch, 1) - { - if !(val == %ch%) - throw Exception(Format("Expected '{}' instead of '{}'", %ch%, val)) + val := SubStr(src, pos, i := RegExMatch(src, "[\]\},\s]|$",, pos)-pos) + + static null := "" ; for #Warn + if InStr(",true,false,null,", "," . val . ",", true) ; if var in val := %val% - } - else if (Abs(val) == "") - throw Exception("Invalid number: " . val) - val := val + 0 + else if (Abs(val) == "") ? (pos--, next := "#") : 0 + continue + + val := val + 0, pos += i-1 } - obj[is_array? NumGet(&obj + 4*A_PtrSize)+1 : key] := val - next := is_array ? "]," : "}," + + is_array? ObjInsert(obj, val) : obj[key] := val + next := obj==tree ? "" : is_array ? ",]" : ",}" } } + return tree[1] } /* Function: stringify @@ -173,18 +142,18 @@ class JSON { if IsObject(obj) { - if (ObjGetCapacity(obj) == "") ;// COM,Func,RegExMatch,File object - throw Exception("Unsupported object type") + if (ObjGetCapacity(obj) == "") ; COM,Func,RegExMatch,File,Property object + throw Exception("Object type not supported.", -1, Format("", &obj)) is_array := 0 for k in obj is_array := (k == A_Index) until !is_array - if (Abs(indent) != "") + if indent is integer { if (indent < 0) - throw Exception("Indent parameter must be a postive integer") + throw Exception("Indent parameter must be a postive integer.", -1, indent) spaces := indent, indent := "" Loop % spaces indent .= " " @@ -193,17 +162,17 @@ class JSON Loop, % indent ? lvl : 0 indt .= indent - lvl += 1, out := "" ;// make #Warn happy + lvl += 1, out := "" ; make #Warn happy for k, v in obj { if IsObject(k) || (k == "") - throw Exception("Invalid JSON key") + throw Exception("Invalid object key.", -1, k ? Format("", &obj) : "") if !is_array - out .= ( ObjGetCapacity([k], 1) ? JSON.stringify(k) : """" . k . """" ) ;// key - . ( indent ? ": " : ":" ) ;// token + padding - out .= JSON.stringify(v, indent, lvl) ;// value - . ( indent ? ",`n" . indt : "," ) ;// token + indent + out .= ( ObjGetCapacity([k], 1) ? JSON.stringify(k) : """" . k . """" ) ; key + . ( indent ? ": " : ":" ) ; token + padding + out .= JSON.stringify(v, indent, lvl) ; value + . ( indent ? ",`n" . indt : "," ) ; token + indent } if (out != "") @@ -213,40 +182,27 @@ class JSON out := Format("`n{}{}`n{}", indt, out, SubStr(indt, StrLen(indent)+1)) } - return is_array ? "[" out "]" : "{" out "}" + return is_array ? "[" . out . "]" : "{" . out . "}" } - ;// Not a string - assume number -> integer or float - if (ObjGetCapacity([obj], 1) == "") ;// returns an integer if 'obj' is string + ; Number + if (ObjGetCapacity([obj], 1) == "") ; returns an integer if 'obj' is string return obj - ;// null - not supported in AHK - - ;// String - ; if obj is float - ; return obj - static esc_seq := { - (Join - """": "\""", - "/": "\/", - "`b": "\b", - "`f": "\f", - "`n": "\n", - "`r": "\r", - "`t": "\t" - )} - + ; String (null -> not supported by AHK) if (obj != "") { - StringReplace, obj, obj, \, \\, A - for k, v in esc_seq - StringReplace, obj, obj, %k%, %v%, A + StringReplace, obj, obj, \, \\, A + StringReplace, obj, obj, /, \/, A + StringReplace, obj, obj, ", \", A + StringReplace, obj, obj, `b, \b, A + StringReplace, obj, obj, `f, \f, A + StringReplace, obj, obj, `n, \n, A + StringReplace, obj, obj, `r, \r, A + StringReplace, obj, obj, `t, \t, A - while RegExMatch(obj, "[^\x20-\x7e]", wstr) - { - esc_hex := Format("\u{:04X}", Asc(wstr)) - StringReplace, obj, obj, %wstr%, %esc_hex%, A - } + while RegExMatch(obj, "[^\x20-\x7e]", m) + StringReplace, obj, obj, %m%, % Format("\u{:04X}", Asc(m)), A } return """" . obj . """" @@ -286,7 +242,7 @@ class JSON */ Count() { - return NumGet(&(this._) + 4*A_PtrSize) ;// Round(this._.MaxIndex()) + return NumGet(&(this._) + 4*A_PtrSize) ; Round(this._.MaxIndex()) } stringify(indent:="") From f6b7ccd8735aa0b3d360d4fe297b114bd1f20572 Mon Sep 17 00:00:00 2001 From: Coco Belgica Date: Sun, 15 Feb 2015 11:50:47 +0800 Subject: [PATCH 39/56] Added some meta info (GH url, email, date of last update). --- JSON.ahk | 29 ++++++++++++++++++++--------- 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/JSON.ahk b/JSON.ahk index dd6993f..1ac5f38 100644 --- a/JSON.ahk +++ b/JSON.ahk @@ -1,13 +1,24 @@ +/* Class: JSON + * JSON lib for AutoHotkey + * License: + * WTFPL [http://wtfpl.net/] + * Requirements: + * AutoHotkey v1.1.17+ + * Others: + * Github URL: https://github.com/cocobelgica/AutoHotkey-JSON + * Email: cocobelgica@gmail.com + * Last Update: 02/15/2015 (MM/DD/YYYY) + */ class JSON { - /* Function: parse - * Deserialize a string containing a JSON document to an AHK object. + /* Method: parse + * Deserialize a string containing a JSON document to an AHK object. * Syntax: - * json_obj := JSON.parse( src [, jsonize:=false ] ) + * json_obj := JSON.parse( ByRef src [ , jsonize := false ] ) * Parameter(s): - * src [in] - String containing a JSON document - * jsonize [in] - If true, objects {} and arrays [] are wrapped as - * JSON.object and JSON.array instances respectively. + * src [in, ByRef] - String containing a JSON document + * jsonize [in] - If true, objects {} and arrays [] are wrapped as + * JSON.object and JSON.array instances respectively. */ parse(ByRef src, jsonize:=false) { @@ -130,10 +141,10 @@ class JSON return tree[1] } - /* Function: stringify - * Serialize an object to a JSON formatted string. + /* Method: stringify + * Serialize an object to a JSON formatted string. * Syntax: - * json_str := JSON.stringify( obj [, indent:="" ] ) + * json_str := JSON.stringify( obj [ , indent := "" ] ) * Parameter(s): * obj [in] - The object to stringify. * indent [in] - Specify string(s) to use as indentation per level. From d7fe62cb4fc6437f66e9eb3afebf97b4f328e628 Mon Sep 17 00:00:00 2001 From: Coco Belgica Date: Sun, 15 Feb 2015 11:59:26 +0800 Subject: [PATCH 40/56] Removed entry on Jxon_Read() and Jxon_Write(). --- README.md | 31 ++++++------------------------- 1 file changed, 6 insertions(+), 25 deletions(-) diff --git a/README.md b/README.md index 87f6f87..f1e3788 100644 --- a/README.md +++ b/README.md @@ -18,14 +18,14 @@ Deserialize _src_ (a JSON formatted string) to an AutoHotkey object #### Syntax: - obj := JSON.parse( src [, jsonize := false ] ) + obj := JSON.parse( ByRef src [, jsonize := false ] ) #### Return Value: An AutoHotkey object #### Parameter(s): - * **src** [in] - JSON formatted string + * **src** [in, ByRef] - JSON formatted string * **jsonize** [in, opt] - if _true_, **_objects_( ``{}`` )** and **_arrays_( ``[]`` )** are wrapped as **JSON.object** and **JSON.array** instances respectively. This is to compensate for AutoHotkey's non-distinction between these types and other AHK object type quirks. _e.g.: In AutoHotkey, object keys are enumerated in alphabetical order not in the sequence in which they are created_ - - - @@ -48,7 +48,7 @@ A JSON formatted string - - - ## 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. Works on both AutoHotkey _v1.1_ and _v2.0_ +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.0_ - - - @@ -57,11 +57,11 @@ Deserialize _src_ (a JSON formatted string) to an AutoHotkey object #### Syntax: - obj := Jxon_Load( src [ , object_base := "", array_base := "" ] ) + obj := Jxon_Load( ByRef src [ , object_base := "", array_base := "" ] ) #### Parameter(s): - * **src** [in] - JSON formatted string or path to the file containing JSON formatted string. + * **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. @@ -80,23 +80,4 @@ A JSON formatted string. #### Parameter(s): * **obj** [in] - this argument has the same meaning as in _JSON.stringify()_ - * **indent** [in, opt] - this argument has the same meaning as in _JSON.stringify()_ - -- - - - -### Jxon_Read() -Similar to `Jxon_Load()` except _src_ is a path to the file containing a JSON document. - -#### Syntax: - - Jxon_Read( src [ , object_base := "", array_base := "" ] ) - -
-- - - - -### Jxon_Write() -Similar to `Jxon_Dump()` except output is written to _dest_. The number of bytes written is returned if successful. - -#### Syntax: - - bytes_written := Jxon_Write( obj , dest [ , indent := "" ] ) \ No newline at end of file + * **indent** [in, opt] - this argument has the same meaning as in _JSON.stringify()_ \ No newline at end of file From 29be2bf6ba692d2d2cdd1d7bc8d9184ce01f50ce Mon Sep 17 00:00:00 2001 From: Coco Belgica Date: Sun, 15 Mar 2015 21:20:11 +0800 Subject: [PATCH 41/56] Updated (relevant)parts to use backported v2.0-a features. --- Jxon.ahk | 62 ++++++++++++++++++++++++-------------------------------- 1 file changed, 26 insertions(+), 36 deletions(-) diff --git a/Jxon.ahk b/Jxon.ahk index 5b34c5b..6210a99 100644 --- a/Jxon.ahk +++ b/Jxon.ahk @@ -9,16 +9,11 @@ Jxon_Load(ByRef src, args*) pos := 0 while ( (ch := SubStr(src, ++pos, 1)) != "" ) { - static is_v2 := A_AhkVersion >= "2" - static ObjPush := Func(is_v2 ? "ObjPush" : "ObjInsert") - static ObjInsertAt := Func(is_v2 ? "ObjInsertAt" : "ObjInsert") - static ObjRemoveAt := Func(is_v2 ? "ObjRemoveAt" : "ObjRemove") - if InStr(" `t`n`r", ch) continue if !InStr(next, ch, true) { - ln := StrSplit(SubStr(src, 1, pos), "`n")[is_v2 ? "Length" : "MaxIndex"]() + ln := ObjLength(StrSplit(SubStr(src, 1, pos), "`n")) col := pos - InStr(src, "`n",, -(StrLen(src)-pos+1)) msg := Format("{}: line {} col {} (char {})" @@ -42,8 +37,8 @@ Jxon_Load(ByRef src, args*) if i := InStr("{[", ch) { val := (proto := args[i]) ? new proto : {} - is_array? %ObjPush%(obj, val) : obj[key] := val - %ObjInsertAt%(stack, 1, val) + is_array? ObjPush(obj, val) : obj[key] := val + ObjInsertAt(stack, 1, val) is_arr[val] := !(is_key := ch == "{") next := q . (is_key ? "}" : "{[]0123456789-tfn") @@ -51,7 +46,7 @@ Jxon_Load(ByRef src, args*) else if InStr("}]", ch) { - %ObjRemoveAt%(stack, 1) + ObjRemoveAt(stack, 1) next := stack[1]==tree ? "" : is_arr[stack[1]] ? ",]" : ",}" } @@ -65,14 +60,11 @@ Jxon_Load(ByRef src, args*) { if (ch == q) ; string { - static Replace := Func(is_v2 ? "StrReplace" : "RegExReplace") - static bash := is_v2 ? "\" : "\\" - i := pos while i := InStr(src, q,, i+1) { - val := %Replace%(SubStr(src, pos+1, i-pos-1), bash . bash, "\u005C") - static end := is_v2 ? -1 : 0 + val := StrReplace(SubStr(src, pos+1, i-pos-1), "\\", "\u005C") + static end := A_AhkVersion<"2" ? 0 : -1 if (SubStr(val, end) != "\") break } @@ -81,13 +73,13 @@ Jxon_Load(ByRef src, args*) pos := i ; update pos - val := %Replace%(val, bash . "/", "/") - , val := %Replace%(val, bash . q, q) - , val := %Replace%(val, bash . "b", "`b") - , val := %Replace%(val, bash . "f", "`f") - , val := %Replace%(val, bash . "n", "`n") - , val := %Replace%(val, bash . "r", "`r") - , val := %Replace%(val, bash . "t", "`t") + 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) @@ -121,7 +113,7 @@ Jxon_Load(ByRef src, args*) val := val + 0, pos += i-1 } - is_array? %ObjPush%(obj, val) : obj[key] := val + is_array? ObjPush(obj, val) : obj[key] := val next := obj==tree ? "" : is_array ? ",]" : ",}" } } @@ -186,20 +178,18 @@ Jxon_Dump(obj, indent:="", lvl:=1) ; String (null -> not supported by AHK) if (obj != "") { - static Replace := Func(A_AhkVersion<"2" ? "RegExReplace" : "StrReplace") - static bash := A_AhkVersion<"2" ? "\\" : "\" - obj := %Replace%(obj, bash, "\\") - , obj := %Replace%(obj, "/", "\/") - , obj := %Replace%(obj, q, "\" . q) - , obj := %Replace%(obj, "`b", "\b") - , obj := %Replace%(obj, "`f", "\f") - , obj := %Replace%(obj, "`n", "\n") - , obj := %Replace%(obj, "`r", "\r") - , obj := %Replace%(obj, "`t", "\t") - - static Ord := Func(A_AhkVersion<"2" ? "Asc" : "Ord") - while RegExMatch(obj, "[^\x20-\x7e]", m) - obj := %Replace%(obj, ch := IsObject(m) ? m[0] : m, Format("\u{:04X}", %Ord%(ch))) + 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 From 61c6a8e0ff276f248b7d5aca42b858ff5a66ca97 Mon Sep 17 00:00:00 2001 From: Coco Belgica Date: Sun, 15 Mar 2015 21:38:45 +0800 Subject: [PATCH 42/56] Jxon_Dump() - Fixed wrong error being thrown in v2.0-a. --- Jxon.ahk | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Jxon.ahk b/Jxon.ahk index 6210a99..7037b7c 100644 --- a/Jxon.ahk +++ b/Jxon.ahk @@ -127,7 +127,8 @@ Jxon_Dump(obj, indent:="", lvl:=1) if IsObject(obj) { - if (ObjGetCapacity(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 From 0f527d673d0d010b1d1fec3569d0f033f0cf6c28 Mon Sep 17 00:00:00 2001 From: Coco Belgica Date: Sat, 28 Mar 2015 19:29:34 +0800 Subject: [PATCH 43/56] Updated to use backported v2.0-a features. - Lib is now AutoHotkey v1.1 and v2.0-a compatible - Added JSON.object.Delete() - similar to ObjDelete() - Removed JSON.object.Count() and JSON.object.Insert() - Array methods such as InsertAt/RemoveAt and Push/Pop are not implemented for JSON.object object(s). - Some changes here and there. --- JSON.ahk | 164 +++++++++++++++++++++++++++++-------------------------- 1 file changed, 87 insertions(+), 77 deletions(-) diff --git a/JSON.ahk b/JSON.ahk index 1ac5f38..6997e29 100644 --- a/JSON.ahk +++ b/JSON.ahk @@ -1,13 +1,18 @@ /* Class: JSON * JSON lib for AutoHotkey + * Version: + * -- * License: * WTFPL [http://wtfpl.net/] * Requirements: - * AutoHotkey v1.1.17+ + * AutoHotkey v1.1.21.00+ OR v2.0-a+ + * Installation: + * Use #Include JSON.ahk or #Include . Must be copied into a function + * library folder for the latter. * Others: - * Github URL: https://github.com/cocobelgica/AutoHotkey-JSON - * Email: cocobelgica@gmail.com - * Last Update: 02/15/2015 (MM/DD/YYYY) + * Github: - https://github.com/cocobelgica/AutoHotkey-JSON + * Forum Topic - http://goo.gl/r0zI8t + * Email: - cocobelgica@gmail.com */ class JSON { @@ -22,11 +27,13 @@ class JSON */ parse(ByRef src, jsonize:=false) { + static q := Chr(34) + args := jsonize ? [ JSON.object, JSON.array ] : [] key := "", is_key := false stack := [ tree := [] ] is_arr := { (tree): 1 } - next := """{[01234567890-tfn" + next := q . "{[01234567890-tfn" pos := 0 while ( (ch := SubStr(src, ++pos, 1)) != "" ) { @@ -34,18 +41,18 @@ class JSON continue if !InStr(next, ch) { - ln := ObjMaxIndex(StrSplit(SubStr(src, 1, pos), "`n")) + 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 == """") ? "Expecting object key enclosed in double quotes" - : (next == """}") ? "Expecting object key enclosed in double quotes or object closing '}'" - : (next == ",}") ? "Expecting ',' delimiter or object closing '}'" - : (next == ",]") ? "Expecting ',' delimiter or array closing ']'" + , (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) @@ -58,49 +65,49 @@ class JSON if i := InStr("{[", ch) { val := (proto := args[i]) ? new proto : {} - is_array? ObjInsert(obj, val) : obj[key] := val - ObjInsert(stack, 1, val) + is_array? ObjPush(obj, val) : obj[key] := val + ObjInsertAt(stack, 1, val) is_arr[val] := !(is_key := ch == "{") - next := is_key ? """}" : """{[]0123456789-tfn" + next := q . (is_key ? "}" : "{[]0123456789-tfn") } else if InStr("}]", ch) { - ObjRemove(stack, 1) + ObjRemoveAt(stack, 1) next := stack[1]==tree ? "" : is_arr[stack[1]] ? ",]" : ",}" } else if InStr(",:", ch) { is_key := (!is_array && ch == ",") - next := is_key ? """" : """{[0123456789-tfn" + next := is_key ? q : q . "{[0123456789-tfn" } else { - if (ch == """") + if (ch == q) { i := pos - while (i := InStr(src, """",, i+1)) + while i := InStr(src, q,, i+1) { - val := SubStr(src, pos+1, i-pos-1) - StringReplace, val, val, \\, \u005C, A - if (SubStr(val, 0) != "\") + 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 + pos := i ; update pos - StringReplace, val, val, \/, /, A - StringReplace, val, val, \", ", A - StringReplace, val, val, \b, `b, A - StringReplace, val, val, \f, `f, A - StringReplace, val, val, \n, `n, A - StringReplace, val, val, \r, `r, A - StringReplace, val, val, \t, `t, A + 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)) @@ -134,7 +141,7 @@ class JSON val := val + 0, pos += i-1 } - is_array? ObjInsert(obj, val) : obj[key] := val + is_array? ObjPush(obj, val) : obj[key] := val next := obj==tree ? "" : is_array ? ",]" : ",}" } } @@ -151,9 +158,12 @@ class JSON */ stringify(obj:="", indent:="", lvl:=1) { + static q := Chr(34) + if IsObject(obj) { - if (ObjGetCapacity(obj) == "") ; COM,Func,RegExMatch,File,Property object + static Type := Func("Type") + if Type ? (Type.Call(obj) != "Object") : (ObjGetCapacity(obj) == "") ; COM,Func,RegExMatch,File,Property object throw Exception("Object type not supported.", -1, Format("", &obj)) is_array := 0 @@ -161,7 +171,8 @@ class JSON is_array := (k == A_Index) until !is_array - if indent is integer + static integer := "integer" + if indent is %integer% { if (indent < 0) throw Exception("Indent parameter must be a postive integer.", -1, indent) @@ -180,7 +191,7 @@ class JSON throw Exception("Invalid object key.", -1, k ? Format("", &obj) : "") if !is_array - out .= ( ObjGetCapacity([k], 1) ? JSON.stringify(k) : """" . k . """" ) ; key + out .= ( ObjGetCapacity([k], 1) ? JSON.stringify(k) : q . k . q ) ; key . ( indent ? ": " : ":" ) ; token + padding out .= JSON.stringify(v, indent, lvl) ; value . ( indent ? ",`n" . indt : "," ) ; token + indent @@ -190,7 +201,7 @@ class JSON { out := Trim(out, ",`n" indent) if (indent != "") - out := Format("`n{}{}`n{}", indt, out, SubStr(indt, StrLen(indent)+1)) + out := "`n" . indt . out . "`n" . SubStr(indt, StrLen(indent)+1) } return is_array ? "[" . out . "]" : "{" . out . "}" @@ -203,20 +214,21 @@ class JSON ; String (null -> not supported by AHK) if (obj != "") { - StringReplace, obj, obj, \, \\, A - StringReplace, obj, obj, /, \/, A - StringReplace, obj, obj, ", \", A - StringReplace, obj, obj, `b, \b, A - StringReplace, obj, obj, `f, \f, A - StringReplace, obj, obj, `n, \n, A - StringReplace, obj, obj, `r, \r, A - StringReplace, obj, obj, `t, \t, A + 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") - while RegExMatch(obj, "[^\x20-\x7e]", m) - StringReplace, obj, obj, %m%, % Format("\u{:04X}", Asc(m)), A + 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 """" . obj . """" + return q . obj . q } class object @@ -224,36 +236,32 @@ class JSON __New(args*) { - ObjInsert(this, "_", []) - if ((count := NumGet(&args+4*A_PtrSize)) & 1) - throw "Invalid number of parameters" - Loop % count//2 - this[args[A_Index*2-1]] := args[A_Index*2] - } + if ((len := ObjLength(args)) & 1) + throw Exception("Too few parameters passed to function.", -1, len) - __Set(key, val, args*) - { - ObjInsert(this._, key) + ObjRawSet(this, "_", []) ; bypass __Set + Loop % len//2 + this[args[A_Index*2-1]] := args[A_Index*2] ; invoke __Set } - Insert(key, val) + __Set(key, args*) { - return this[key] := val - } - /* Buggy - remaining integer keys are not adjusted - Remove(args*) { - ret := ObjRemove(this, args*), i := -1 - for index, key in ObjClone(this._) { - if ObjHasKey(this, key) - continue - ObjRemove(this._, index-(i+=1)) - } - return ret + ObjPush(this._, key) ; add key to key list and allow __Set to continue normally } - */ - Count() + + Delete(FirstKey, LastKey*) { - return NumGet(&(this._) + 4*A_PtrSize) ; Round(this._.MaxIndex()) + IsRange := ObjLength(LastKey) + i := 0 + for index, key in ObjClone(this._) + if IsRange ? (key >= FirstKey && key <= LastKey[1]) : (key = FirstKey) + { + ObjRemoveAt(this._, index - (i++)) + if !IsRange ; single key only + break + } + + return ObjDelete(this, FirstKey, LastKey*) } stringify(indent:="") @@ -263,16 +271,18 @@ class JSON _NewEnum() { - static proto := { "Next": JSON.object.Next } - return { base: proto, enum: this._._NewEnum(), obj: this } + static enum := { "Next": JSON.object._EnumNext } + return { base: enum, enum: ObjNewEnum(this._), obj: this } } - Next(ByRef key, ByRef val:="") + _EnumNext(ByRef key, ByRef val:="") { - if (ret := this.enum.Next(i, key)) + if r := this.enum.Next(, key) val := this.obj[key] - return ret + return r } + ; Do not implement array methods?? + static InsertAt := "", RemoveAt := "", Push := "", Pop := "" } class array From cdac8d40c847a826dea644966db4c53fcddfff3c Mon Sep 17 00:00:00 2001 From: Coco Belgica Date: Sat, 28 Mar 2015 20:00:38 +0800 Subject: [PATCH 44/56] Method names changed. - Changed .parse() to .Load() - Changed .stringify() to .Dump() - For compatibility, call(s) to .parse()/.stringify() are casted to .Load()/.Dump() respectively. --- JSON.ahk | 39 ++++++++++++++++++++++----------------- 1 file changed, 22 insertions(+), 17 deletions(-) diff --git a/JSON.ahk b/JSON.ahk index 6997e29..d41b03e 100644 --- a/JSON.ahk +++ b/JSON.ahk @@ -16,20 +16,20 @@ */ class JSON { - /* Method: parse + /* Method: Load * Deserialize a string containing a JSON document to an AHK object. * Syntax: - * json_obj := JSON.parse( ByRef src [ , jsonize := false ] ) + * json_obj := JSON.Load( ByRef src [ , jsonize := false ] ) * Parameter(s): * src [in, ByRef] - String containing a JSON document * jsonize [in] - If true, objects {} and arrays [] are wrapped as - * JSON.object and JSON.array instances respectively. + * JSON.Object and JSON.Array instances respectively. */ - parse(ByRef src, jsonize:=false) + Load(ByRef src, jsonize:=false) { static q := Chr(34) - args := jsonize ? [ JSON.object, JSON.array ] : [] + args := jsonize ? [ JSON.Object, JSON.Array ] : [] key := "", is_key := false stack := [ tree := [] ] is_arr := { (tree): 1 } @@ -148,15 +148,15 @@ class JSON return tree[1] } - /* Method: stringify + /* Method: Dump * Serialize an object to a JSON formatted string. * Syntax: - * json_str := JSON.stringify( obj [ , indent := "" ] ) + * json_str := JSON.Dump( obj [ , indent := "" ] ) * Parameter(s): * obj [in] - The object to stringify. * indent [in] - Specify string(s) to use as indentation per level. */ - stringify(obj:="", indent:="", lvl:=1) + Dump(obj:="", indent:="", lvl:=1) { static q := Chr(34) @@ -191,9 +191,9 @@ class JSON throw Exception("Invalid object key.", -1, k ? Format("", &obj) : "") if !is_array - out .= ( ObjGetCapacity([k], 1) ? JSON.stringify(k) : q . k . q ) ; key + out .= ( ObjGetCapacity([k], 1) ? JSON.Dump(k) : q . k . q ) ; key . ( indent ? ": " : ":" ) ; token + padding - out .= JSON.stringify(v, indent, lvl) ; value + out .= JSON.Dump(v, indent, lvl) ; value . ( indent ? ",`n" . indt : "," ) ; token + indent } @@ -231,7 +231,7 @@ class JSON return q . obj . q } - class object + class Object { __New(args*) @@ -264,14 +264,15 @@ class JSON return ObjDelete(this, FirstKey, LastKey*) } - stringify(indent:="") + Dump(indent:="") { - return JSON.stringify(this, indent) + return JSON.Dump(this, indent) } + static Stringify := JSON.Object.Dump _NewEnum() { - static enum := { "Next": JSON.object._EnumNext } + static enum := { "Next": JSON.Object._EnumNext } return { base: enum, enum: ObjNewEnum(this._), obj: this } } @@ -285,7 +286,7 @@ class JSON static InsertAt := "", RemoveAt := "", Push := "", Pop := "" } - class array + class Array { __New(args*) @@ -294,9 +295,13 @@ class JSON return args } - stringify(indent:="") + Dump(indent:="") { - return JSON.stringify(this, indent) + return JSON.Dump(this, indent) } + static Stringify := JSON.Array.Dump } + ; Deprecated but maintained for existing scripts using the lib + static Parse := JSON.Load ; cast to .Load + static Stringify := JSON.Dump ; cast to .Dump } \ No newline at end of file From f8b780c11a622051001e784494f9b98fc51853a9 Mon Sep 17 00:00:00 2001 From: Coco Belgica Date: Sat, 28 Mar 2015 20:18:00 +0800 Subject: [PATCH 45/56] v1.2.00.00 - [cdac8d4] Method names changed. - [0f527d6] Updated to use backported v2.0-a features. --- JSON.ahk | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/JSON.ahk b/JSON.ahk index d41b03e..1dfd199 100644 --- a/JSON.ahk +++ b/JSON.ahk @@ -1,7 +1,7 @@ /* Class: JSON * JSON lib for AutoHotkey * Version: - * -- + * v1.2.00.00 [updated 03/28/2015 (MM/DD/YYYY)] * License: * WTFPL [http://wtfpl.net/] * Requirements: @@ -10,7 +10,7 @@ * Use #Include JSON.ahk or #Include . Must be copied into a function * library folder for the latter. * Others: - * Github: - https://github.com/cocobelgica/AutoHotkey-JSON + * GitHub: - https://github.com/cocobelgica/AutoHotkey-JSON * Forum Topic - http://goo.gl/r0zI8t * Email: - cocobelgica@gmail.com */ From 5795836da2efa634aa4b5881f13b04c14491d6ae Mon Sep 17 00:00:00 2001 From: Coco Belgica Date: Sat, 28 Mar 2015 20:41:12 +0800 Subject: [PATCH 46/56] Updated README to reflect v1.2.00.00 changes --- README.md | 36 ++++++++++++++++++++++++------------ 1 file changed, 24 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index f1e3788..c364f63 100644 --- a/README.md +++ b/README.md @@ -1,24 +1,30 @@ # JSON and Jxon -#### [JSON](http://json.org/) module for [AutoHotkey](http://ahkscript.org/) +#### [JSON](http://json.org/) lib for [AutoHotkey](http://ahkscript.org/) -_Requires AutoHotkey **v1.1.17.00+** OR **2.0-a057+**_ +Requirements: AutoHotkey _v1.1.21.00+_ or _v2.0-a_ + +Version: v1.2.00.00 _(updated 03/28/2015)_ License: [WTFPL](http://wtfpl.net/) + - - - ## JSON.ahk (class) -There are multiple version(s) available (as branches) in this repo to provide support for different AutoHotkey builds. The _master_ branch is for `v1.1` _(except for Json2.ahk which is version independent)_ while the _v2_ branch is for `AHK v2.0-a`. +Works on both AutoHotkey _v1.1_ and _v2.0a_ + +### Installation +Use `#Include JSON.ahk` or `#Include `. Must be copied into a [function library folder](http://ahkscript.org/docs/Functions.htm#lib) for the latter. - - - -#### .parse() +#### .Load() _(previously `.parse()`)_ Deserialize _src_ (a JSON formatted string) to an AutoHotkey object #### Syntax: - obj := JSON.parse( ByRef src [, jsonize := false ] ) + obj := JSON.Load( ByRef src [, jsonize := false ] ) #### Return Value: @@ -26,16 +32,16 @@ An AutoHotkey object #### Parameter(s): * **src** [in, ByRef] - JSON formatted string - * **jsonize** [in, opt] - if _true_, **_objects_( ``{}`` )** and **_arrays_( ``[]`` )** are wrapped as **JSON.object** and **JSON.array** instances respectively. This is to compensate for AutoHotkey's non-distinction between these types and other AHK object type quirks. _e.g.: In AutoHotkey, object keys are enumerated in alphabetical order not in the sequence in which they are created_ + * **jsonize** [in, opt] - if _true_, **_objects_( ``{}`` )** and **_arrays_( ``[]`` )** are wrapped as **JSON.Object** and **JSON.Array** instances respectively. This is to compensate for AutoHotkey's non-distinction between these types and other AHK object type quirks. _e.g.: In AutoHotkey, object keys are enumerated in alphabetical order not in the sequence in which they are created_ - - - -#### .stringify() -serialize _obj_ to a JSON formatted string +#### .Dump() _(previously `.stringify()`)_ +Serialize _obj_ to a JSON formatted string #### Syntax: - str := JSON.stringify( obj, [, indent := "" ] ) + str := JSON.Dump( obj, [, indent := "" ] ) #### Return Value: @@ -45,10 +51,16 @@ A JSON formatted string * **obj** [in] - AutoHotkey object. Non-standard AHK objects like _COM_, _Func_, _FileObject_, _RegExMatchObject_ are not supported. * **indent** [in, opt] -if indent 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 indent indents that many spaces per level. If indent is a string (such as ``"`t"``), that string is used to indent each level. _(I'm lazy, wording taken from Python docs)_ +### Remarks: +For compatibilty with existing scripts using this lib. Calls to `.parse()` and `.stringify()` are cast to `.Load()` and `.Dump()` respectively. + - - - ## 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.0_ +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. - - - @@ -79,5 +91,5 @@ Serialize _obj_ to a JSON formatted string A JSON formatted string. #### Parameter(s): - * **obj** [in] - this argument has the same meaning as in _JSON.stringify()_ - * **indent** [in, opt] - this argument has the same meaning as in _JSON.stringify()_ \ No newline at end of file + * **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 From cffa38495ab5b1655403b3894e02cb41f28301d2 Mon Sep 17 00:00:00 2001 From: Coco Belgica Date: Sat, 28 Mar 2015 20:45:53 +0800 Subject: [PATCH 47/56] Updated example to reflect v1.2.00.00 changes. --- Example_JSON.ahk | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Example_JSON.ahk b/Example_JSON.ahk index 7cf82c1..4d79381 100644 --- a/Example_JSON.ahk +++ b/Example_JSON.ahk @@ -22,7 +22,7 @@ json_str = } ) -parsed := JSON.parse(json_str, true) +parsed := JSON.Load(json_str, true) parsed_out := Format(" (Join`r`n @@ -39,8 +39,8 @@ object: {{}A:""{}"", H:""{}"", K:""{}""{}} , parsed.array[1], parsed.array[2], parsed.array[3] , parsed.object.A, parsed.object.H, parsed.object.K) -stringified := JSON.stringify(parsed, 4) -StringReplace stringified, stringified, `n, `r`n, All ; for display purposes only +stringified := JSON.Dump(parsed, 4) +stringified := StrReplace(stringified, "`n", "`r`n") ; for display purposes only ListVars WinWaitActive ahk_class AutoHotkey From af52ad073daaf677bfdb3280574d972d2643a87b Mon Sep 17 00:00:00 2001 From: Coco Belgica Date: Sat, 7 Nov 2015 18:32:36 +0800 Subject: [PATCH 48/56] v2.0.00.00 Conform with ECMAScript's JSON.parse() and JSON.stringfy() specifications - Remove backward support for old Parse() and Stringify() methods - Remove JSON.Object and JSON.Array objects - Remove JSON.Load()'s 'jsonize' paramter - Add 'reviver' parameter to JSON.Load() - Add 'replacer' parameter to JSON.Dump() - Improve handling of array(s) during stringification process - Limit indentation(pretty-printing) to 10 characters - Add missing error message - Disable '/' escaping when stringifying --- JSON.ahk | 533 ++++++++++++++++++++++++++++--------------------------- 1 file changed, 276 insertions(+), 257 deletions(-) diff --git a/JSON.ahk b/JSON.ahk index 1dfd199..cd5892f 100644 --- a/JSON.ahk +++ b/JSON.ahk @@ -1,307 +1,326 @@ -/* Class: JSON - * JSON lib for AutoHotkey +/** + * Lib: JSON.ahk + * JSON lib for AutoHotkey. * Version: - * v1.2.00.00 [updated 03/28/2015 (MM/DD/YYYY)] + * v2.0.00.00 [updated 11/07/2015 (MM/DD/YYYY)] * License: * WTFPL [http://wtfpl.net/] * Requirements: - * AutoHotkey v1.1.21.00+ OR v2.0-a+ + * Latest version of AutoHotkey (v1.1+ or v2.0-a+) * Installation: - * Use #Include JSON.ahk or #Include . Must be copied into a function - * library folder for the latter. - * Others: + * 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 + * 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 { - /* Method: Load - * Deserialize a string containing a JSON document to an AHK object. + /** + * Method: Load + * Parses a JSON string into an AHK value * Syntax: - * json_obj := JSON.Load( ByRef src [ , jsonize := false ] ) + * value := JSON.Load( text [, reviver ] ) * Parameter(s): - * src [in, ByRef] - String containing a JSON document - * jsonize [in] - If true, objects {} and arrays [] are wrapped as - * JSON.Object and JSON.Array instances respectively. + * value [retval] - parsed value + * text [in, opt] - JSON formatted string + * reviver [in, opt] - function object, similar to JavaScript's + * JSON.parse() 'reviver' parameter */ - Load(ByRef src, jsonize:=false) + class Load extends JSON.Functor { - static q := Chr(34) - - args := jsonize ? [ JSON.Object, JSON.Array ] : [] - key := "", is_key := false - stack := [ tree := [] ] - is_arr := { (tree): 1 } - next := q . "{[01234567890-tfn" - pos := 0 - while ( (ch := SubStr(src, ++pos, 1)) != "" ) + Call(self, text, reviver:="") { - if InStr(" `t`n`r", ch) - continue - if !InStr(next, ch) - { - 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") - } + this.rev := IsObject(reviver) ? reviver : false + this.keys := this.rev ? {} : false - else if InStr("}]", ch) - { - ObjRemoveAt(stack, 1) - next := stack[1]==tree ? "" : is_arr[stack[1]] ? ",]" : ",}" - } + static q := Chr(34) + , json_value := q . "{[01234567890-tfn" + , json_value_or_array_closing := q . "{[]01234567890-tfn" + , object_key_or_object_closing := q . "}" - else if InStr(",:", ch) - { - is_key := (!is_array && ch == ",") - next := is_key ? q : q . "{[0123456789-tfn" - } + key := "" + is_key := false + root := {} + stack := [root] + next := json_value + pos := 0 - else - { - if (ch == q) - { - 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) - } + while ((ch := SubStr(text, ++pos, 1)) != "") { + if InStr(" `t`r`n", ch) + continue + if !InStr(next, ch, 1) + this.ParseError(next, text, pos) - if is_key - { - key := val, next := ":" - continue - } - } - - else - { - val := SubStr(src, pos, i := RegExMatch(src, "[\]\},\s]|$",, pos)-pos) + holder := stack[1] + is_array := holder.IsArray + + if InStr(",:", ch) { + next := (is_key := !is_array && ch == ",") ? q : 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 - static null := "" ; for #Warn - if InStr(",true,false,null,", "," . val . ",", true) ; if var in - val := %val% - else if (Abs(val) == "") ? (pos--, next := "#") : 0 - continue + ; 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] := [] - val := val + 0, pos += i-1 + } else { + if (ch == q) { + i := pos + while (i := InStr(text, q,, i+1)) { + value := StrReplace(SubStr(text, pos+1, i-pos-1), "\\", "\u005c") + + static ss_end := A_AhkVersion<"2" ? 0 : -1 + if (SubStr(value, ss_end) != "\") + break + } + + if (!i) + this.ParseError("'", text, pos) + + value := StrReplace(value, "\/", "/") + , value := StrReplace(value, "\" . q, q) + , 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", null := "" + if value is %number% + value += 0 + else if (value == "true" || value == "false" || value == "null") + value := %value% + 0 + 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) } - - is_array? ObjPush(obj, val) : obj[key] := val - next := obj==tree ? "" : is_array ? ",]" : ",}" - } + + } ; while ( ... ) + + return this.rev ? this.Walk(root, "") : root[""] } - - return tree[1] - } - /* Method: Dump - * Serialize an object to a JSON formatted string. - * Syntax: - * json_str := JSON.Dump( obj [ , indent := "" ] ) - * Parameter(s): - * obj [in] - The object to stringify. - * indent [in] - Specify string(s) to use as indentation per level. - */ - Dump(obj:="", indent:="", lvl:=1) - { - static q := Chr(34) - if IsObject(obj) + ParseError(expect, text, pos, len:=1) { - static Type := Func("Type") - if Type ? (Type.Call(obj) != "Object") : (ObjGetCapacity(obj) == "") ; COM,Func,RegExMatch,File,Property object - 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) ? JSON.Dump(k) : q . k . q ) ; key - . ( indent ? ": " : ":" ) ; token + padding - out .= JSON.Dump(v, indent, lvl) ; value - . ( indent ? ",`n" . indt : "," ) ; token + indent - } + static q := Chr(34) - if (out != "") - { - out := Trim(out, ",`n" indent) - if (indent != "") - out := "`n" . indt . out . "`n" . SubStr(indt, StrLen(indent)+1) - } - - return is_array ? "[" . out . "]" : "{" . out . "}" + 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 == q) ? "Expecting object key enclosed in double quotes" + : (expect == q . "}") ? "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)) } - - ; Number - if (ObjGetCapacity([obj], 1) == "") ; returns an integer if 'obj' is string - return obj - - ; String (null -> not supported by AHK) - if (obj != "") + + Walk(holder, key) { - 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]))) + value := holder[key] + if IsObject(value) + for i, k in this.keys[value] + value[k] := this.Walk.Call(this, value, k) ; bypass __Call + + return this.rev.Call(holder, key, value) } - - return q . obj . q } - - 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(args*) + Call(self, value, replacer:="", space:="") { - if ((len := ObjLength(args)) & 1) - throw Exception("Too few parameters passed to function.", -1, len) + this.rep := IsObject(replacer) ? replacer : "" - ObjRawSet(this, "_", []) ; bypass __Set - Loop % len//2 - this[args[A_Index*2-1]] := args[A_Index*2] ; invoke __Set - } + 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) - __Set(key, args*) - { - ObjPush(this._, key) ; add key to key list and allow __Set to continue normally + this.indent := "`n" + } + + return this.Str({"": value}, "") } - Delete(FirstKey, LastKey*) + Str(holder, key) { - IsRange := ObjLength(LastKey) - i := 0 - for index, key in ObjClone(this._) - if IsRange ? (key >= FirstKey && key <= LastKey[1]) : (key = FirstKey) - { - ObjRemoveAt(this._, index - (i++)) - if !IsRange ; single key only - break + value := holder[key] + + if (this.rep) + value := this.rep.Call(holder, key, value) + + if IsObject(value) { + if (this.gap) { + stepback := this.indent + this.indent .= this.gap } - - return ObjDelete(this, FirstKey, LastKey*) - } - Dump(indent:="") - { - return JSON.Dump(this, indent) - } - static Stringify := JSON.Object.Dump + is_array := value.IsArray + ; Array() is not overridden, rollback to old method of + ; identifying array-like objects + if (!is_array) { + for i in value + is_array := i == A_Index + until !is_array + } - _NewEnum() - { - static enum := { "Next": JSON.Object._EnumNext } - return { base: enum, enum: ObjNewEnum(this._), obj: this } + str := "" + if (is_array) { + Loop, % value.Length() { + if (this.gap) + str .= this.indent + + str .= value.HasKey(A_Index) ? this.Str(value, A_Index) . "," : "null," + } + } else { + colon := this.gap ? ": " : ":" + for k in value { + if (this.gap) + str .= this.indent + + str .= this.Quote(k) . colon . this.Str(value, k) . "," + } + } + + if (str != "") { + str := RTrim(str, ",") + if (this.gap) + str .= stepback + } + + if (this.gap) + this.indent := stepback + + return is_array ? "[" . str . "]" : "{" . str . "}" + } + ; is_number ? value : "value" + return ObjGetCapacity([value], 1)=="" ? value : this.Quote(value) } - _EnumNext(ByRef key, ByRef val:="") + Quote(string) { - if r := this.enum.Next(, key) - val := this.obj[key] - return r + static q := Chr(34) + + if (string != "") { + string := StrReplace(string, "\", "\\") + ; , string := StrReplace(string, "/", "\/") ; optional in ECMAScript + , string := StrReplace(string, q, "\" . q) + , 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 q . string . q } - ; Do not implement array methods?? - static InsertAt := "", RemoveAt := "", Push := "", Pop := "" } - - class Array - { - - __New(args*) - { - args.base := this.base - return args - } - Dump(indent:="") + class Functor + { + __Call(method, args*) { - return JSON.Dump(this, indent) + ; 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, args*) + else if (method == "") + return (new this).Call(args*) } - static Stringify := JSON.Array.Dump } - ; Deprecated but maintained for existing scripts using the lib - static Parse := JSON.Load ; cast to .Load - static Stringify := JSON.Dump ; cast to .Dump } \ No newline at end of file From 7d6d12d0f8b4ca701823bc19d9b3ac97fe6ed268 Mon Sep 17 00:00:00 2001 From: Coco Belgica Date: Sat, 7 Nov 2015 18:59:02 +0800 Subject: [PATCH 49/56] Update README to reflect v2.0 changes --- README.md | 32 +++++++++++++++----------------- 1 file changed, 15 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index c364f63..0da5d52 100644 --- a/README.md +++ b/README.md @@ -2,9 +2,9 @@ #### [JSON](http://json.org/) lib for [AutoHotkey](http://ahkscript.org/) -Requirements: AutoHotkey _v1.1.21.00+_ or _v2.0-a_ +Requirements: Latest version of AutoHotkey _(v1.1+ or v2.0-a+)_ -Version: v1.2.00.00 _(updated 03/28/2015)_ +Version: v2.0.00.00 _(updated 11/07/2015)_ License: [WTFPL](http://wtfpl.net/) @@ -15,44 +15,42 @@ License: [WTFPL](http://wtfpl.net/) Works on both AutoHotkey _v1.1_ and _v2.0a_ ### Installation -Use `#Include JSON.ahk` or `#Include `. Must be copied into a [function library folder](http://ahkscript.org/docs/Functions.htm#lib) for the latter. +Use `#Include JSON.ahk` or copy into a [function library folder](http://ahkscript.org/docs/Functions.htm#lib) and use `#Include `. - - - -#### .Load() _(previously `.parse()`)_ -Deserialize _src_ (a JSON formatted string) to an AutoHotkey object +#### .Load() +Parses a JSON string into an AHK value #### Syntax: - obj := JSON.Load( ByRef src [, jsonize := false ] ) + value := JSON.Load( text [, reviver ] ) #### Return Value: -An AutoHotkey object +An AutoHotkey value _(object, string, number)_ #### Parameter(s): - * **src** [in, ByRef] - JSON formatted string - * **jsonize** [in, opt] - if _true_, **_objects_( ``{}`` )** and **_arrays_( ``[]`` )** are wrapped as **JSON.Object** and **JSON.Array** instances respectively. This is to compensate for AutoHotkey's non-distinction between these types and other AHK object type quirks. _e.g.: In AutoHotkey, object keys are enumerated in alphabetical order not in the sequence in which they are created_ + * **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() _(previously `.stringify()`)_ -Serialize _obj_ to a JSON formatted string +#### .Dump() +Converts an AHK value into a JSON string #### Syntax: - str := JSON.Dump( obj, [, indent := "" ] ) + str := JSON.Dump( value, [, replacer, space ] ) #### Return Value: A JSON formatted string #### Parameter(s): - * **obj** [in] - AutoHotkey object. Non-standard AHK objects like _COM_, _Func_, _FileObject_, _RegExMatchObject_ are not supported. - * **indent** [in, opt] -if indent 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 indent indents that many spaces per level. If indent is a string (such as ``"`t"``), that string is used to indent each level. _(I'm lazy, wording taken from Python docs)_ - -### Remarks: -For compatibilty with existing scripts using this lib. Calls to `.parse()` and `.stringify()` are cast to `.Load()` and `.Dump()` respectively. + * **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. - - - From ec2d68f9001d1f07d407af2b2097b2e9d67a9a48 Mon Sep 17 00:00:00 2001 From: Coco Belgica Date: Sat, 7 Nov 2015 19:11:29 +0800 Subject: [PATCH 50/56] Update example script --- Example_JSON.ahk | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Example_JSON.ahk b/Example_JSON.ahk index 4d79381..995f893 100644 --- a/Example_JSON.ahk +++ b/Example_JSON.ahk @@ -22,7 +22,7 @@ json_str = } ) -parsed := JSON.Load(json_str, true) +parsed := JSON.Load(json_str) parsed_out := Format(" (Join`r`n @@ -39,7 +39,7 @@ object: {{}A:""{}"", H:""{}"", K:""{}""{}} , parsed.array[1], parsed.array[2], parsed.array[3] , parsed.object.A, parsed.object.H, parsed.object.K) -stringified := JSON.Dump(parsed, 4) +stringified := JSON.Dump(parsed,, 4) stringified := StrReplace(stringified, "`n", "`r`n") ; for display purposes only ListVars From 6aaeb3d3285541d6f70c34a8724dee88dc12d459 Mon Sep 17 00:00:00 2001 From: Coco Belgica Date: Thu, 28 Jan 2016 00:59:23 +0800 Subject: [PATCH 51/56] v2.1.0 - Small fixes/changes - Load(), remove static variable 'null'. This should make it AutoHotkey_H-compatible(untested). - Dump(), improve skipping of non-serializable objects such as ComObject, Func, BoundFunc, FileObject, RegExMatchObject, Property, etc. - Add JSON.Undefined for use within reviver/replacer functions --- JSON.ahk | 113 ++++++++++++++++++++++++++++++++++++------------------- 1 file changed, 74 insertions(+), 39 deletions(-) diff --git a/JSON.ahk b/JSON.ahk index cd5892f..c43980f 100644 --- a/JSON.ahk +++ b/JSON.ahk @@ -2,7 +2,7 @@ * Lib: JSON.ahk * JSON lib for AutoHotkey. * Version: - * v2.0.00.00 [updated 11/07/2015 (MM/DD/YYYY)] + * v2.1.0 [updated 01/28/2016 (MM/DD/YYYY)] * License: * WTFPL [http://wtfpl.net/] * Requirements: @@ -137,11 +137,13 @@ class JSON } else { value := SubStr(text, pos, i := RegExMatch(text, "[\]\},\s]|$",, pos)-pos) - static number := "number", null := "" + static number := "number" if value is %number% value += 0 - else if (value == "true" || value == "false" || value == "null") + 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. @@ -240,51 +242,61 @@ class JSON value := this.rep.Call(holder, key, value) if IsObject(value) { - if (this.gap) { - stepback := this.indent - this.indent .= this.gap - } + ; 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 + } - is_array := value.IsArray + is_array := value.IsArray ; Array() is not overridden, rollback to old method of - ; identifying array-like objects - if (!is_array) { - for i in value - is_array := i == A_Index - until !is_array - } + ; 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 - - str .= value.HasKey(A_Index) ? this.Str(value, A_Index) . "," : "null," + str := "" + if (is_array) { + Loop, % value.Length() { + if (this.gap) + str .= this.indent + + v := this.Str(value, A_Index) + str .= (v != "") && value.HasKey(A_Index) ? 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 . "," + } + } } - } else { - colon := this.gap ? ": " : ":" - for k in value { - if (this.gap) - str .= this.indent - str .= this.Quote(k) . colon . this.Str(value, k) . "," + if (str != "") { + str := RTrim(str, ",") + if (this.gap) + str .= stepback } - } - if (str != "") { - str := RTrim(str, ",") if (this.gap) - str .= stepback - } + this.indent := stepback - if (this.gap) - this.indent := stepback - - return is_array ? "[" . str . "]" : "{" . str . "}" - } - ; is_number ? value : "value" - return ObjGetCapacity([value], 1)=="" ? value : this.Quote(value) + return is_array ? "[" . str . "]" : "{" . str . "}" + } + + } else ; is_number ? value : "value" + return ObjGetCapacity([value], 1)=="" ? value : this.Quote(value) } Quote(string) @@ -310,6 +322,29 @@ class JSON } } + /** + * 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. + * 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[] + { + get { + static empty := {}, vt_empty := ComObject(0, &empty, 1) + return vt_empty + } + } + class Functor { __Call(method, args*) From 43cb649be122afa704816bf5014c6026df7de4fc Mon Sep 17 00:00:00 2001 From: Coco Belgica Date: Sat, 30 Jan 2016 15:23:14 +0800 Subject: [PATCH 52/56] 2.1.1 - Fix issues w/ reviver and replacer functions - Properties are not removed if the reviver function returns JSON.Undefined - When stringifying sparse arrays, Dump() overrides the returned value of the replacer function(when an empty element is passed) with 'null'. - Pass JSON.Undefined as value parameter to the replacer function if the current field is an empty field in a sparse array. --- JSON.ahk | 30 ++++++++++++++++++++---------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/JSON.ahk b/JSON.ahk index c43980f..6205351 100644 --- a/JSON.ahk +++ b/JSON.ahk @@ -2,7 +2,7 @@ * Lib: JSON.ahk * JSON lib for AutoHotkey. * Version: - * v2.1.0 [updated 01/28/2016 (MM/DD/YYYY)] + * v2.1.1 [updated 01/30/2016 (MM/DD/YYYY)] * License: * WTFPL [http://wtfpl.net/] * Requirements: @@ -44,6 +44,9 @@ class JSON Call(self, 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 static q := Chr(34) @@ -192,9 +195,16 @@ class JSON Walk(holder, key) { value := holder[key] - if IsObject(value) - for i, k in this.keys[value] - value[k] := this.Walk.Call(this, value, k) ; bypass __Call + if IsObject(value) { + for i, k in this.keys[value] { + ; check if ObjHasKey(value, k) ?? + v := this.Walk.Call(this, value, k) ; bypass __Call + if (v != JSON.Undefined) + value[k] := v + else + ObjDelete(value, k) + } + } return this.rev.Call(holder, key, value) } @@ -239,7 +249,7 @@ class JSON value := holder[key] if (this.rep) - value := this.rep.Call(holder, key, value) + 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 @@ -268,7 +278,7 @@ class JSON str .= this.indent v := this.Str(value, A_Index) - str .= (v != "") && value.HasKey(A_Index) ? v . "," : "null," + str .= (v != "") ? v . "," : "null," } } else { colon := this.gap ? ": " : ":" @@ -331,10 +341,10 @@ class JSON * 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. - * 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'. + * 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[] From 0b5485e5aee3f94e4d96dd696a69274ad0d565a9 Mon Sep 17 00:00:00 2001 From: Coco Belgica Date: Sat, 30 Jan 2016 15:37:04 +0800 Subject: [PATCH 53/56] Update version no. and date of last update --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 0da5d52..fd3d535 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ Requirements: Latest version of AutoHotkey _(v1.1+ or v2.0-a+)_ -Version: v2.0.00.00 _(updated 11/07/2015)_ +Version: v2.1.1 _(updated 01/30/2016)_ License: [WTFPL](http://wtfpl.net/) From 1560aaa54920c0e627a09fa6ec950dc11ddaa803 Mon Sep 17 00:00:00 2001 From: Coco Belgica Date: Thu, 7 Apr 2016 02:37:24 +0800 Subject: [PATCH 54/56] Fix parser to return unquoted floating point numbers as is. Previously math (value += 0) is applied on unquoted numerical values to "numerify" the numbers. As a side-effect, floating point values are sometimes altered based on A_FormatFloat. Returning the value as it appears in the document/text should be the behavior. --- JSON.ahk | 9 ++++++--- Jxon.ahk | 27 +++++++++++++++++++++------ 2 files changed, 27 insertions(+), 9 deletions(-) diff --git a/JSON.ahk b/JSON.ahk index 6205351..2bd7280 100644 --- a/JSON.ahk +++ b/JSON.ahk @@ -2,7 +2,7 @@ * Lib: JSON.ahk * JSON lib for AutoHotkey. * Version: - * v2.1.1 [updated 01/30/2016 (MM/DD/YYYY)] + * v2.1.2 [updated 04/07/2016 (MM/DD/YYYY)] * License: * WTFPL [http://wtfpl.net/] * Requirements: @@ -140,9 +140,12 @@ class JSON } else { value := SubStr(text, pos, i := RegExMatch(text, "[\]\},\s]|$",, pos)-pos) - static number := "number" + static number := "number", integer :="integer" if value is %number% - value += 0 + { + if value is %integer% + value += 0 + } else if (value == "true" || value == "false") value := %value% + 0 else if (value == "null") diff --git a/Jxon.ahk b/Jxon.ahk index 7037b7c..5126a39 100644 --- a/Jxon.ahk +++ b/Jxon.ahk @@ -103,14 +103,29 @@ Jxon_Load(ByRef src, args*) else ; number | true | false | null { val := SubStr(src, pos, i := RegExMatch(src, "[\]\},\s]|$",, pos)-pos) - - static null := "" ; for #Warn - if InStr(",true,false,null,", "," . val . ",", true) ; if var in - val := %val% - else if (Abs(val) == "") ? (pos--, next := "#") : 0 + + ; 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 - val := val + 0, pos += i-1 + pos += i-1 } is_array? ObjPush(obj, val) : obj[key] := val From 4520554d318ea2a4580c5ed3c20278c4242f7fd7 Mon Sep 17 00:00:00 2001 From: Coco Belgica Date: Mon, 18 Apr 2016 23:44:44 +0800 Subject: [PATCH 55/56] Fix [af52ad0] regression where input text is no longer passed as ByRef. --- JSON.ahk | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/JSON.ahk b/JSON.ahk index 2bd7280..5ad70f9 100644 --- a/JSON.ahk +++ b/JSON.ahk @@ -2,7 +2,7 @@ * Lib: JSON.ahk * JSON lib for AutoHotkey. * Version: - * v2.1.2 [updated 04/07/2016 (MM/DD/YYYY)] + * v2.1.3 [updated 04/18/2016 (MM/DD/YYYY)] * License: * WTFPL [http://wtfpl.net/] * Requirements: @@ -35,13 +35,13 @@ class JSON * value := JSON.Load( text [, reviver ] ) * Parameter(s): * value [retval] - parsed value - * text [in, opt] - JSON formatted string + * 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, text, reviver:="") + Call(self, ByRef text, reviver:="") { this.rev := IsObject(reviver) ? reviver : false ; Object keys(and array indices) are temporarily stored in arrays so that @@ -172,7 +172,7 @@ class JSON return this.rev ? this.Walk(root, "") : root[""] } - ParseError(expect, text, pos, len:=1) + ParseError(expect, ByRef text, pos, len:=1) { static q := Chr(34) @@ -360,15 +360,15 @@ class JSON class Functor { - __Call(method, args*) + __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, args*) + return (new this).Call(method, arg, args*) else if (method == "") - return (new this).Call(args*) + return (new this).Call(arg, args*) } } } \ No newline at end of file From 5d0ca0557b1ee1d167e0972896757d15bc582d92 Mon Sep 17 00:00:00 2001 From: Coco Belgica Date: Tue, 19 Apr 2016 01:20:53 +0800 Subject: [PATCH 56/56] Minor refactoring. --- JSON.ahk | 76 ++++++++++++++++++++++++++++---------------------------- 1 file changed, 38 insertions(+), 38 deletions(-) diff --git a/JSON.ahk b/JSON.ahk index 5ad70f9..975c528 100644 --- a/JSON.ahk +++ b/JSON.ahk @@ -49,10 +49,10 @@ class JSON ; of alphabetically. Skip if no reviver function is specified. this.keys := this.rev ? {} : false - static q := Chr(34) - , json_value := q . "{[01234567890-tfn" - , json_value_or_array_closing := q . "{[]01234567890-tfn" - , object_key_or_object_closing := q . "}" + 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 . "}" key := "" is_key := false @@ -71,7 +71,7 @@ class JSON is_array := holder.IsArray if InStr(",:", ch) { - next := (is_key := !is_array && ch == ",") ? q : json_value + next := (is_key := !is_array && ch == ",") ? quot : json_value } else if InStr("}]", ch) { ObjRemoveAt(stack, 1) @@ -99,26 +99,26 @@ class JSON this.keys[value] := [] } else { - if (ch == q) { + if (ch == quot) { i := pos - while (i := InStr(text, q,, i+1)) { + while (i := InStr(text, quot,, i+1)) { value := StrReplace(SubStr(text, pos+1, i-pos-1), "\\", "\u005c") - static ss_end := A_AhkVersion<"2" ? 0 : -1 - if (SubStr(value, ss_end) != "\") + static tail := A_AhkVersion<"2" ? 0 : -1 + if (SubStr(value, tail) != "\") break } if (!i) this.ParseError("'", text, pos) - value := StrReplace(value, "\/", "/") - , value := StrReplace(value, "\" . q, q) - , 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") + 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 @@ -174,21 +174,21 @@ class JSON ParseError(expect, ByRef text, pos, len:=1) { - static q := Chr(34) + static quot := Chr(34), qurly := quot . "}" 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 == q) ? "Expecting object key enclosed in double quotes" - : (expect == q . "}") ? "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)" + , (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 @@ -201,7 +201,7 @@ class JSON if IsObject(value) { for i, k in this.keys[value] { ; check if ObjHasKey(value, k) ?? - v := this.Walk.Call(this, value, k) ; bypass __Call + v := this.Walk(value, k) if (v != JSON.Undefined) value[k] := v else @@ -314,24 +314,24 @@ class JSON Quote(string) { - static q := Chr(34) + static quot := Chr(34), bashq := "\" . quot if (string != "") { - string := StrReplace(string, "\", "\\") - ; , string := StrReplace(string, "/", "\/") ; optional in ECMAScript - , string := StrReplace(string, q, "\" . q) - , 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") + 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 q . string . q + return quot . string . quot } }