From 271a4e0067c1ee1192ed835dee1ef908e9a5fedc Mon Sep 17 00:00:00 2001 From: Coco Belgica Date: Mon, 14 Apr 2014 23:43:12 +0800 Subject: [PATCH 01/14] Initial commit. For AHK v2 | Version tested: 2.0-a046-692ef59 --- JSON.ahk | 264 ++++++++++++++++++------------------------------------- 1 file changed, 85 insertions(+), 179 deletions(-) diff --git a/JSON.ahk b/JSON.ahk index 55297f6..3d9d0a3 100644 --- a/JSON.ahk +++ b/JSON.ahk @@ -1,131 +1,92 @@ -/* -JSON module for AutoHotkey [requires v.1.1+, tested on v.1.1.13.01] - -The parser is inspired by Douglas Crockford's(json.org) json_parse.js and -and Mike Samuel's json_sans_eval.js (https://code.google.com/p/json-sans-eval/). -I've combined the two implementation to create a fast(somehow:P) and validating -JSON parser. Some section(s) are based on VxE's JSON function(s) - -[http://www.ahkscript.org/boards/viewtopic.php?f=6&t=30] - Thank you VxE -*/ class JSON { - /* - Parses a string containing JSON string and returns it as an AHK object. - Objects {} and arrays [] are wrapped as JSON.object and JSON.array instances. - Objects {} key-value pairs are enumerated in the order they are created - instead of the default behavior -- alpahabetically. An exception is thrown - if the JSON string is badly formatted, e.g. illegal chars, invalid escaping - Usage: - --start-of-code-- - j := JSON.parse("[{""foo"": ""Hello World"", ""bar"":""AutoHotkey""}]") - MsgBox, % j[1].foo ; displays 'Hello World' - MsgBox, % j[1].bar ; displays 'AutoHotkey' - --end-of-code-- - */ parse(src) { esc_char := { - (Join - """": """", - "/": "/", - "b": Chr(08), - "f": Chr(12), - "n": "`n", - "r": "`r", - "t": "`t" + (Q + '"': '"', + '/': '/', + 'b': Chr(08), + 'f': Chr(12), + 'n': '`n', + 'r': '`r', + 't': '`t' )} - null := "" ; needed?? - - /* - This loop is based on VxE's JSON_ToObj.ahk - thank you VxE - Quoted strings are extracted and temporarily stored in an object and - later on re-inserted while the result object is being created. - */ + null := "" 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 + str := StrReplace(str, "\\", "\u005C") if (SubStr(str, 0) != "\") break } - src := SubStr(src, 1, i-1) . SubStr(src, j) - z := 0 while (z:=InStr(str, "\",, z+1)) { ch := SubStr(str, z+1, 1) - if InStr("""btnfr/", ch) ; esc_char.HasKey(ch) + if InStr('"btnfr/', 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 str := SubStr(str, 1, z-1) . Chr(hex) . SubStr(str, z+6) - } else throw Exception("Bad string") } strings.Insert(str) } - pos := 1, ch := " " key := dummy := [] - stack := [result:=new JSON.array], assert := "{[""tfn0123456789-" - while (ch != "", ch:=SubStr(src, pos, 1), pos+=1) { - - while (ch != "" && InStr(" `t`r`n", ch)) ; skip whitespace + stack := [result:=new JSON.array], assert := '{["tfn0123456789-' + while (ch != "") { + ch := SubStr(src, pos, 1), pos += 1 + while (ch != "" && InStr(" `t`r`n", ch)) ch := SubStr(src, pos, 1), pos += 1 - ;pos := RegExMatch(src, "\S", ch, pos)+1 - /* - Check if the current character is expected or not - Acts as a simple validator for badly formatted JSON string - */ if (assert != "") { - if !InStr(assert, ch) - throw Exception("Unexpected '" . ch . "'", -1) + if !InStr(assert, ch), throw Exception("Unexpected '%ch%'", -1) assert := "" } - + if InStr(":,", ch) { - assert := "{[""tfn0123456789-" + 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]) + cnt := stack[1], base := (ch == '{' ? 'object' : 'array') + len := (ObjMaxIndex(cnt) || 0) + stack.Insert(1, cnt[key == dummy ? len+1 : key] := new JSON[base]) key := dummy - assert := (ch == "{" ? """}" : "]{[""tfn0123456789-") + assert := (ch == '{' ? '"}' : ']{["tfn0123456789-') continue } else if InStr("}]", ch) { ; object|array - closing - stack.Remove(1), assert := "]}," + stack.Remove(1), assert := ']},' continue - } else if (ch == """") { ; string - str := strings.Remove(1), cont := stack[1] + } else if (ch == '"') { ; string + str := strings.Remove(1), cnt := stack[1] if (key == dummy) { - if (cont.__Class == "JSON.array") { - key := Round(ObjMaxIndex(cont))+1 + if cnt is JSON.array { + key := (ObjMaxIndex(cnt) || 0)+1 } else { key := str, assert := ":" continue } } - cont[key] := str, key := dummy - assert := "," . (cont.__Class == "JSON.object" ? "}" : "]") + cnt[key] := str, key := dummy + assert := ",%(cnt is JSON.object ? '}' : ']')%" continue } else if (ch >= 0 && ch <= 9) || (ch == "-") { ; number if !RegExMatch(src, "-?\d+(\.\d+)?((?i)E[-+]?\d+)?", num, pos-1) throw Exception("Bad number", -1) - pos += StrLen(num)-1 - cont := stack[1], len := Round(ObjMaxIndex(cont)) - cont[key == dummy ? len+1 : key] := num + pos += StrLen(num.Value)-1 + cnt := stack[1], len := (ObjMaxIndex(cnt) || 0) + cnt[key == dummy ? len+1 : key] := num.Value+0 ; convert to pure number key := dummy - assert := "," . (cont.__Class == "JSON.object" ? "}" : "]") + assert := ",%(cnt is JSON.object ? '}' : ']')%" continue } else if InStr("tfn", ch, true) { ; true|false|null @@ -134,129 +95,82 @@ class JSON while (c:=SubStr(val, A_Index+1, 1)) { ch := SubStr(src, pos, 1), pos += 1 if !(ch == c) ; case-sensitive comparison - throw Exception("Expected '" c "' instead of " ch) + throw Exception("Expected '%c%' instead of %ch%") } - cont := stack[1], len := Round(ObjMaxIndex(cont)) - cont[key == dummy ? len+1 : key] := %val% + cnt := stack[1], len := (ObjMaxIndex(cnt) || 0) + cnt[key == dummy ? len+1 : key] := Abs(%val%) key := dummy - assert := "," . (cont.__Class == "JSON.object" ? "}" : "]") + assert := ",%(cnt is JSON.object ? '}' : ']')%" continue } else { - if (ch != "") - throw Exception("Unexpected '" . ch . "'", -1) + 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. - */ + stringify(obj:="", i:="", lvl:=1) { - if IsObject(obj) { - if (ComObjValue(x) != "") ; COM Object - throw Exception("COM Object(s) are not supported.") - if (obj.base == JSON.object || obj.base == JSON.array) - arr := (obj.base == JSON.array ? true : false) + type := Type(obj) + if (type == "Object") { + if (obj is JSON.object || obj is JSON.array) + arr := obj is JSON.array else for k in obj - arr := (k == A_Index) - until !arr - + if !(arr := (k == A_Index)), break + n := i ? "`n" : (i:="", t:="") Loop, % i ? lvl : 0 t .= i - + lvl += 1 for k, v in obj { - if IsObject(k) || (k == "") - throw Exception("Invalid key.", -1) - if !arr - ; integer key(s) are automatically wrapped in quotes - key := k+0 == k ? """" . k . """" : JSON.stringify(k) + if IsObject(k) || (k == ""), throw Exception("Invalid key.", -1) + if !arr, key := k is 'number' ? '"%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 + s := ",%(n ? n : ' ') . t%" + str .= arr + ? val . s + : "%key%:%((IsObject(v) && InStr(val, '{') == 1) ? n . t : ' ')%%val%%s%" } str := n . t . Trim(str, ",`n`t ") . n . SubStr(t, StrLen(i)+1) - return arr ? "[" str "]" : "{" str "}" - } - ; true|false|null - else if InStr(01, obj) || (obj == "") - return {"": "null", 0:"false", 1:"true"}[obj] - ; String - else if [obj].GetCapacity(1) { - if obj is float - return obj - + return arr ? "[%str%]" : "{%str%}" + + } else if (type == "Integer" || type == "Float") { + return InStr('01', obj) ? (obj ? 'true' : 'false') : obj + + } else if (type == "String") { + if (obj == ""), return 'null' esc_char := { - (Join - """": "\""", - "/": "\/", - Chr(08): "\b", - Chr(12): "\f", - "`n": "\n", - "`r": "\r", - "`t": "\t" + (Q + '"': '\"', + '/': '\/', + Chr(08): '\b', + Chr(12): '\f', + '`n': '\n', + '`r': '\r', + '`t': '\t' )} - - StringReplace, obj, obj, \, \\, A + obj := StrReplace(obj, "\", "\\") for k, v in esc_char - StringReplace, obj, obj, % k, % v, A - + obj := StrReplace(obj, k, v) while RegExMatch(obj, "[^\x20-\x7e]", ch) { - ustr := Asc(ch), esc_ch := "\u", n := 12 + ustr := Ord(ch.Value), 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 + esc_ch .= Chr((x:=(ustr>>n) & 15) + (x<10 ? 48 : 55)), n -= 4 + obj := StrReplace(obj, ch.Value, esc_ch) } - return """" . obj . """" + return '"%obj%"' } - ; Number - if obj is xdigit - if obj is not digit - obj := """" . obj . """" - - return obj + throw Exception("Unsupported type: '%type%'") } - /* - Base object for objects {} created during parsing. The user may also manually - create an insatnce of this class. The sole purpose of wrapping objects {} as - JSON.object instance is to allow enumeration of key-value pairs in the order - they were created. The len() method may be used to get the total count of - key-value pairs. - Usage: Instances are automatically created during parsing. The user may - use the 'new' operator to create a JSON.object object manually. - --start-of-code-- - obj := new JSON.object("key1", "value1", "key2", "value2") - obj["key3"] := "Add a new key-value pair" - MsgBox, % obj.stringify() ; display as string - ; '{"key1": "value1", "key2": "value2", "key3": "Add a new key-value pair"}' - --end-of-code-- - */ + class object { - __New(p*) { ObjInsert(this, "_", []) - if Mod(p.MaxIndex(), 2) - p.Insert("") + if Mod(p.MaxIndex(), 2), p.Insert("") Loop, % p.MaxIndex()//2 this[p[A_Index*2-1]] := p[A_Index*2] } @@ -274,14 +188,11 @@ class JSON } Remove(k) { ; restrict to single key - if !ObjHasKey(this, k) - return + if !ObjHasKey(this, k), return for i, v in this._ - continue - until (v = k) + if (v = k), break this._.Remove(i) - if k is integer - return ObjRemove(this, k, "") + if k is 'integer', return ObjRemove(this, k, "") return ObjRemove(this, k) } @@ -295,25 +206,20 @@ class JSON class Enum { - __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] + if (r:=this.enum.Next(i, k)), v := this.obj[k] return r } } } - /* - Base object for arrays [] created during parsing. Same as JSON.object above. - */ + class array { - __New(p*) { for k, v in p this.Insert(v) @@ -323,4 +229,4 @@ class JSON return JSON.stringify(this, i) } } -} \ No newline at end of file +} From 617924917d7d18271185511554ac2247f4777308 Mon Sep 17 00:00:00 2001 From: Coco Belgica Date: Tue, 15 Apr 2014 21:38:36 +0800 Subject: [PATCH 02/14] Fix bug: - JSON.object.len() returning blank ("") if object{} is empty. - JSON.object.Remove(): new v2 for-loop resets variable(s) (k, v) to their previous value(s), hence, function not working properly. --- JSON.ahk | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/JSON.ahk b/JSON.ahk index 3d9d0a3..3596f50 100644 --- a/JSON.ahk +++ b/JSON.ahk @@ -171,7 +171,7 @@ class JSON __New(p*) { ObjInsert(this, "_", []) if Mod(p.MaxIndex(), 2), p.Insert("") - Loop, % p.MaxIndex()//2 + Loop p.MaxIndex()//2 this[p[A_Index*2-1]] := p[A_Index*2] } @@ -190,14 +190,15 @@ class JSON Remove(k) { ; restrict to single key if !ObjHasKey(this, k), return for i, v in this._ - if (v = k), break - this._.Remove(i) + idx := i + until (v = k) + this._.Remove(idx) if k is 'integer', return ObjRemove(this, k, "") return ObjRemove(this, k) } len() { - return this._.MaxIndex() + return (this._.MaxIndex() || 0) } stringify(i:="") { From 7add6c92080ce78b99f5fe5c64cbd4dc8796e0a3 Mon Sep 17 00:00:00 2001 From: Coco Belgica Date: Wed, 23 Apr 2014 23:29:38 +0800 Subject: [PATCH 03/14] Fixed: - JSON.object.Remove() now behaves like AHK's obj.Remove() except existing integer key(s) are not adjusted when an integer key(or range of integer keys) is removed. Return value is the same as AHK's obj.Remove(). --- JSON.ahk | 34 ++++++++++++++++++++++++++-------- 1 file changed, 26 insertions(+), 8 deletions(-) diff --git a/JSON.ahk b/JSON.ahk index 3596f50..0232608 100644 --- a/JSON.ahk +++ b/JSON.ahk @@ -187,14 +187,32 @@ class JSON return this[k] := v } - Remove(k) { ; restrict to single key - if !ObjHasKey(this, k), return - for i, v in this._ - idx := i - until (v = k) - this._.Remove(idx) - if k is 'integer', return ObjRemove(this, k, "") - return ObjRemove(this, k) + Remove(k*) { + ascs := A_StringCaseSense + A_StringCaseSense := 'Off' + if (k.MaxIndex() > 1) { + k1 := k[1], k2 := k[2], is_int := false + if k1 is 'integer' && k2 is 'integer' + 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, (key is 'integer' ? [key, ''] : [key])*) + break + } + } + A_StringCaseSense := ascs + return res } len() { From bc63fe031ab82e0dea1d1d0ab551517cfec70b18 Mon Sep 17 00:00:00 2001 From: Coco Belgica Date: Thu, 24 Apr 2014 22:46:14 +0800 Subject: [PATCH 04/14] Minor adjustments. Nothing significant. --- JSON.ahk | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/JSON.ahk b/JSON.ahk index 0232608..d02031b 100644 --- a/JSON.ahk +++ b/JSON.ahk @@ -5,8 +5,8 @@ class JSON (Q '"': '"', '/': '/', - 'b': Chr(08), - 'f': Chr(12), + 'b': '`b', + 'f': '`f', 'n': '`n', 'r': '`r', 't': '`t' @@ -41,7 +41,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)) + while (ch != "" && InStr(" `t`n`r", ch)) ch := SubStr(src, pos, 1), pos += 1 if (assert != "") { if !InStr(assert, ch), throw Exception("Unexpected '%ch%'", -1) @@ -146,8 +146,8 @@ class JSON (Q '"': '\"', '/': '\/', - Chr(08): '\b', - Chr(12): '\f', + '`b': '\b', + '`f': '\f', '`n': '\n', '`r': '\r', '`t': '\t' From 1f547e44b28614452e0cd77e34da30822f755274 Mon Sep 17 00:00:00 2001 From: Coco Belgica Date: Fri, 25 Apr 2014 22:32:35 +0800 Subject: [PATCH 05/14] Minor fix: Variable name changes. --- JSON.ahk | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/JSON.ahk b/JSON.ahk index d02031b..96d851e 100644 --- a/JSON.ahk +++ b/JSON.ahk @@ -54,9 +54,9 @@ class JSON } if InStr("{[", ch) { ; object|array - opening - cnt := stack[1], base := (ch == '{' ? 'object' : 'array') - len := (ObjMaxIndex(cnt) || 0) - stack.Insert(1, cnt[key == dummy ? len+1 : key] := new JSON[base]) + cont := stack[1], base := (ch == '{' ? 'object' : 'array') + len := (ObjMaxIndex(cont) || 0) + stack.Insert(1, cont[key == dummy ? len+1 : key] := new JSON[base]) key := dummy assert := (ch == '{' ? '"}' : ']{["tfn0123456789-') continue @@ -66,27 +66,27 @@ class JSON continue } else if (ch == '"') { ; string - str := strings.Remove(1), cnt := stack[1] + str := strings.Remove(1), cont := stack[1] if (key == dummy) { - if cnt is JSON.array { - key := (ObjMaxIndex(cnt) || 0)+1 + if cont is JSON.array { + key := (ObjMaxIndex(cont) || 0)+1 } else { key := str, assert := ":" continue } } - cnt[key] := str, key := dummy - assert := ",%(cnt is JSON.object ? '}' : ']')%" + cont[key] := str, key := dummy + assert := ",%(cont is JSON.object ? '}' : ']')%" continue } else if (ch >= 0 && ch <= 9) || (ch == "-") { ; number if !RegExMatch(src, "-?\d+(\.\d+)?((?i)E[-+]?\d+)?", num, pos-1) throw Exception("Bad number", -1) pos += StrLen(num.Value)-1 - cnt := stack[1], len := (ObjMaxIndex(cnt) || 0) - cnt[key == dummy ? len+1 : key] := num.Value+0 ; convert to pure number + cont := stack[1], len := (ObjMaxIndex(cont) || 0) + cont[key == dummy ? len+1 : key] := num.Value+0 ; convert to pure number key := dummy - assert := ",%(cnt is JSON.object ? '}' : ']')%" + assert := ",%(cont is JSON.object ? '}' : ']')%" continue } else if InStr("tfn", ch, true) { ; true|false|null @@ -98,10 +98,10 @@ class JSON throw Exception("Expected '%c%' instead of %ch%") } - cnt := stack[1], len := (ObjMaxIndex(cnt) || 0) - cnt[key == dummy ? len+1 : key] := Abs(%val%) + cont := stack[1], len := (ObjMaxIndex(cont) || 0) + cont[key == dummy ? len+1 : key] := Abs(%val%) key := dummy - assert := ",%(cnt is JSON.object ? '}' : ']')%" + assert := ",%(cont is JSON.object ? '}' : ']')%" continue } else { From 605b3d676d105d4bcdc94a0c0fda2df9e118a178 Mon Sep 17 00:00:00 2001 From: Coco Belgica Date: Sat, 14 Jun 2014 09:10:58 +0800 Subject: [PATCH 06/14] Fix bug: Escaped double quotes (\") are not detected due to wrong starting pos in SubStr() during parsing of literal string(s). --- JSON.ahk | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/JSON.ahk b/JSON.ahk index 96d851e..d7a4da6 100644 --- a/JSON.ahk +++ b/JSON.ahk @@ -18,7 +18,7 @@ class JSON while (j:=InStr(src, '"',, j+1)) { str := SubStr(src, i+1, j-i-1) str := StrReplace(str, "\\", "\u005C") - if (SubStr(str, 0) != "\") + if (SubStr(str, -1) != "\") break } src := SubStr(src, 1, i-1) . SubStr(src, j) From ac22c7ca8c68a236e179563d3f2c058f82fe1027 Mon Sep 17 00:00:00 2001 From: Coco Belgica Date: Sun, 15 Jun 2014 14:31:59 +0800 Subject: [PATCH 07/14] 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 | 166 +++++++++++++++++++++++++++++++++++++------------------ 1 file changed, 113 insertions(+), 53 deletions(-) diff --git a/JSON.ahk b/JSON.ahk index d7a4da6..d384b61 100644 --- a/JSON.ahk +++ b/JSON.ahk @@ -1,6 +1,28 @@ 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) { + ;// Pre-validate JSON source before parsing + if ((src:=Trim(src, " `t`n`r")) == "") ;// trim whitespace(s) + throw Exception('Empty JSON source.') + first := SubStr(src, 1, 1), last := SubStr(src, -1) + if !InStr('{["tfn0123456789-', first) ;// valid beginning chars + || !InStr('}]el0123456789"', last) ;// valid ending chars + || (first == '{' && last != '}') ;// if starts w/ '{' must end w/ '}' + || (first == '[' && last != ']') ;// if starts w/ '[' must end w/ ']' + || (first == '"' && last != '"') ;// if starts w/ '"' must end w/ '"' + || (first == 'n' && last != 'l') ;// assume 'null' + || (InStr('tf', first) && last != 'e') ;// assume 'true' OR 'false' + || (InStr('-0123456789', first) && !(last is 'number')) ;// number + throw Exception('Invalid JSON format.', -1) + esc_char := { (Q '"': '"', @@ -11,7 +33,7 @@ class JSON 'r': '`r', 't': '`t' )} - null := "" + ;// Extract string literals i := 0, strings := [] while (i:=InStr(src, '"',, i+1)) { j := i @@ -21,6 +43,7 @@ class JSON if (SubStr(str, -1) != "\") 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)) { @@ -36,77 +59,100 @@ class JSON } strings.Insert(str) } - pos := 1, ch := " " - key := dummy := [] - stack := [result:=new JSON.array], assert := '{["tfn0123456789-' - while (ch != "") { - ch := SubStr(src, pos, 1), pos += 1 + ;// Check for missing opening/closing brace(s) + if InStr(src, '{') || InStr(src, '}') { + StrReplace(src, '{', '{', c1), StrReplace(src, '}', '}', c2) + if (c1 != c2), throw Exception(" + (LTrim Q + Missing %Abs(c1-c2)% %(c1 > c2 ? 'clos' : 'open')%ing brace(s) + )", -1) + } + ;// Check for missing opening/closing bracket(s) + if InStr(src, '[') || InStr(src, ']') { + StrReplace(src, '[', '[', c1), StrReplace(src, ']', ']', c2) + if (c1 != c2), throw Exception(" + (LTrim Q + 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 := "" ;// 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), pos += 1 + 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 := (ObjMaxIndex(cont) || 0) - 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) + ;// no container object + if (cont == result), throw Exception(" + (LTrim + Unexpected '%ch%' -> there is no container object/array. + )") + assert := '"' + if (ch == ':' || !(cont is _object)), assert .= '{[tfn0123456789-' + + } else if InStr("{[", ch) { ;// object|array - opening + cont := stack[1] + , sub := new (ch == '{' ? _object : _array) + , stack.Insert(1, cont[key == dummy ? (ObjMaxIndex(cont) || 0)+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 is _object ? '},' : '],' - } else if (ch == '"') { ; string + } else if (ch == '"') { ;// string str := strings.Remove(1), cont := stack[1] if (key == dummy) { - if cont is JSON.array { + if (cont is _array || cont == result) { key := (ObjMaxIndex(cont) || 0)+1 } else { key := str, assert := ":" continue } } - cont[key] := str, key := dummy - assert := ",%(cont is JSON.object ? '}' : ']')%" - continue + cont[key] := str + , assert := cont is _object ? '},' : '],' + , key := dummy - } else if (ch >= 0 && ch <= 9) || (ch == "-") { ; number + } else if (ch >= 0 && ch <= 9) || (ch == "-") { ;// number if !RegExMatch(src, "-?\d+(\.\d+)?((?i)E[-+]?\d+)?", num, pos-1) throw Exception("Bad number", -1) pos += StrLen(num.Value)-1 - cont := stack[1], len := (ObjMaxIndex(cont) || 0) - cont[key == dummy ? len+1 : key] := num.Value+0 ; convert to pure number - key := dummy - assert := ",%(cont is JSON.object ? '}' : ']')%" - continue + , cont := stack[1] + , cont[key == dummy ? (ObjMaxIndex(cont) || 0)+1 : key] := num.Value+0 + , assert := cont is _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 := (ObjMaxIndex(cont) || 0) - cont[key == dummy ? len+1 : key] := Abs(%val%) - key := dummy - assert := ",%(cont is JSON.object ? '}' : ']')%" - continue - - } else { - if (ch != ""), throw Exception("Unexpected '%ch%'", -1) - else break + cont := stack[1] + , cont[key == dummy ? (ObjMaxIndex(cont) || 0)+1 : key] := Abs(%val%) + , assert := cont is _object ? '},' : '],' + if (key != dummy), key := dummy } } return result[1] @@ -115,8 +161,9 @@ class JSON stringify(obj:="", i:="", lvl:=1) { type := Type(obj) if (type == "Object") { - if (obj is JSON.object || obj is JSON.array) - arr := obj is JSON.array + if (obj is JSON.object || obj is JSON.array + || obj is JSON._object || obj is JSON._array) + arr := (obj is JSON.array || obj is JSON._array) else for k in obj if !(arr := (k == A_Index)), break @@ -129,10 +176,13 @@ class JSON if IsObject(k) || (k == ""), throw Exception("Invalid key.", -1) if !arr, key := k is 'number' ? '"%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 ? "" : " + (LTrim Join Q C + %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%}" @@ -215,6 +265,16 @@ class JSON return res } + __Remove(k) { ; restrict to single key + if !ObjHasKey(this, k), return + for i, v in this._ + idx := i + until (v = k) + this._.Remove(idx) + if k is 'integer', return ObjRemove(this, k, "") + return ObjRemove(this, k) + } + len() { return (this._.MaxIndex() || 0) } @@ -248,4 +308,4 @@ class JSON return JSON.stringify(this, i) } } -} +} \ No newline at end of file From 37cc096df0651a26eb9076cd672c7824501b58e1 Mon Sep 17 00:00:00 2001 From: Coco Belgica Date: Mon, 16 Jun 2014 04:48:11 +0800 Subject: [PATCH 08/14] Minor bug fix --- JSON.ahk | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/JSON.ahk b/JSON.ahk index d384b61..7402922 100644 --- a/JSON.ahk +++ b/JSON.ahk @@ -132,7 +132,7 @@ class JSON , key := dummy } else if (ch >= 0 && ch <= 9) || (ch == "-") { ;// number - if !RegExMatch(src, "-?\d+(\.\d+)?((?i)E[-+]?\d+)?", num, pos-1) + if !RegExMatch(src, "-?\d+(\.\d+)?((?i)E[-+]?\d+)?", num, pos) throw Exception("Bad number", -1) pos += StrLen(num.Value)-1 , cont := stack[1] From 5c79071b158828dbf8794c1fa8bdb8b684b0ef57 Mon Sep 17 00:00:00 2001 From: Coco Belgica Date: Tue, 17 Jun 2014 04:08:16 +0800 Subject: [PATCH 09/14] 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). - 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 | 66 +++++++++++++++++++++++--------------------------------- 1 file changed, 27 insertions(+), 39 deletions(-) diff --git a/JSON.ahk b/JSON.ahk index 7402922..b039e35 100644 --- a/JSON.ahk +++ b/JSON.ahk @@ -1,14 +1,6 @@ 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.') @@ -75,12 +67,8 @@ class JSON 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) + if jsonize, (_object := this.object, _array := this.array) + else (_object := Object(), _array := Array()) pos := 0 , key := dummy := [] , stack := [result := []] @@ -114,7 +102,10 @@ class JSON if (key != dummy), key := dummy } else if InStr("}]", ch) { ;// object|array - closing - stack.Remove(1), cont := stack[1] + cont := stack.Remove(1) + ;// remove base if set to output normal AHK object(s) + if !jsonize, cont.base := "" + cont := stack[1] , assert := cont is _object ? '},' : '],' } else if (ch == '"') { ;// string @@ -158,33 +149,36 @@ class JSON return result[1] } - stringify(obj:="", i:="", lvl:=1) { + stringify(obj:="", indent:="", lvl:=1) { type := Type(obj) if (type == "Object") { - if (obj is JSON.object || obj is JSON.array - || obj is JSON._object || obj is JSON._array) - arr := (obj is JSON.array || obj is JSON._array) - else for k in obj + for k in obj if !(arr := (k == A_Index)), break - 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, key := k is 'number' ? '"%k%"' : JSON.stringify(k) - val := JSON.stringify(v, i, lvl) + val := JSON.stringify(v, indent, lvl) ;// format output str .= (arr ? "" : " (LTrim Join Q C - %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] + %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%}" } else if (type == "Integer" || type == "Float") { @@ -265,14 +259,8 @@ class JSON return res } - __Remove(k) { ; restrict to single key - if !ObjHasKey(this, k), return - for i, v in this._ - idx := i - until (v = k) - this._.Remove(idx) - if k is 'integer', return ObjRemove(this, k, "") - return ObjRemove(this, k) + GetCapacity(k*) { + return ObjGetCapacity((k.MinIndex() ? [this, k[1]] : [this._])*) } len() { From be14a0846a30ac5c3189deec927ed5ab68fedc59 Mon Sep 17 00:00:00 2001 From: Coco Belgica Date: Wed, 18 Jun 2014 02:32:05 +0800 Subject: [PATCH 10/14] Modification(s): - parse(): Changed parsing of true,false,null values -> no longer loops through each character to validate. Minor optimizations + code refactoring. --- JSON.ahk | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/JSON.ahk b/JSON.ahk index b039e35..d585680 100644 --- a/JSON.ahk +++ b/JSON.ahk @@ -36,7 +36,7 @@ class JSON break } 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) @@ -111,12 +111,12 @@ class JSON } else if (ch == '"') { ;// string str := strings.Remove(1), cont := stack[1] if (key == dummy) { - if (cont is _array || cont == result) { - key := (ObjMaxIndex(cont) || 0)+1 - } else { - key := str, assert := ":" + if (cont is _object) { + key := str, assert := ':' continue } + ;// _array or result | using 'else' seems faster, sometimes + else key := (ObjMaxIndex(cont) || 0)+1 } cont[key] := str , assert := cont is _object ? '},' : '],' @@ -132,14 +132,22 @@ class JSON if (key != dummy), key := dummy } else if InStr("tfn", ch, true) { ;// true|false|null - val := {t:"true", f:"false", n:"null"}[ch] + /* Ternary seems faster compared to object -> + * val := {t:"true", f:"false", n:"null"}[ch] + */ + val := (ch == 't') ? 'true' : (ch == 'f') ? 'false' : 'null' + ;// case-sensitve 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 ? (ObjMaxIndex(cont) || 0)+1 : key] := Abs(%val%) , assert := cont is _object ? '},' : '],' From be0c87477c2928e87f020149821704c051a21233 Mon Sep 17 00:00:00 2001 From: Coco Belgica Date: Mon, 7 Jul 2014 22:42:29 +0800 Subject: [PATCH 11/14] Modification(s): - Code refactored. - Removed previously kept commented old code parts. --- JSON.ahk | 17 ++++------------- 1 file changed, 4 insertions(+), 13 deletions(-) diff --git a/JSON.ahk b/JSON.ahk index d585680..18bcdc4 100644 --- a/JSON.ahk +++ b/JSON.ahk @@ -91,8 +91,7 @@ class JSON (LTrim Unexpected '%ch%' -> there is no container object/array. )") - assert := '"' - if (ch == ':' || !(cont is _object)), assert .= '{[tfn0123456789-' + assert := (cont is _object && ch == ',') ? '"' : '{["tfn0123456789-' } else if InStr("{[", ch) { ;// object|array - opening cont := stack[1] @@ -125,7 +124,7 @@ class JSON } 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.Value)-1 + pos += num.Len()-1 , cont := stack[1] , cont[key == dummy ? (ObjMaxIndex(cont) || 0)+1 : key] := num.Value+0 , assert := cont is _object ? '},' : '],' @@ -140,16 +139,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 ? (ObjMaxIndex(cont) || 0)+1 : key] := Abs(%val%) + , cont := stack[1] + , cont[key == dummy ? (ObjMaxIndex(cont) || 0)+1 : key] := %val%+0 , assert := cont is _object ? '},' : '],' if (key != dummy), key := dummy } From 11ee565c4647dc2632d68b4b5ef0c86c10e7bb9b Mon Sep 17 00:00:00 2001 From: Coco Belgica Date: Sat, 16 Aug 2014 02:34:44 +0800 Subject: [PATCH 12/14] Updated to be compatible with the recent AHK v2.0-a049 changes. Rewrote some parts -> changes mostly taken from Json2.ahk(master branch). --- JSON.ahk | 344 ++++++++++++++++++++++++++----------------------------- 1 file changed, 160 insertions(+), 184 deletions(-) diff --git a/JSON.ahk b/JSON.ahk index 18bcdc4..dd2b574 100644 --- a/JSON.ahk +++ b/JSON.ahk @@ -3,7 +3,7 @@ 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.') + throw "Empty JSON source" first := SubStr(src, 1, 1), last := SubStr(src, -1) if !InStr('{["tfn0123456789-', first) ;// valid beginning chars || !InStr('}]el0123456789"', last) ;// valid ending chars @@ -13,9 +13,9 @@ class JSON || (first == 'n' && last != 'l') ;// assume 'null' || (InStr('tf', first) && last != 'e') ;// assume 'true' OR 'false' || (InStr('-0123456789', first) && !(last is 'number')) ;// number - throw Exception('Invalid JSON format.', -1) + throw "Invalid JSON format" - esc_char := { + esc_seq := { (Q '"': '"', '/': '/', @@ -35,157 +35,145 @@ class JSON if (SubStr(str, -1) != "\") break } - if !j, throw Exception("Missing close quote(s).", -1) + 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) + k := 0 + while (k := InStr(str, "\",, k+1)) { + ch := SubStr(str, k+1, 1) if InStr('"btnfr/', ch) - str := SubStr(str, 1, z-1) . esc_char[ch] . SubStr(str, z+2) + str := SubStr(str, 1, k-1) . esc_seq[ch] . SubStr(str, k+2) + else if (ch = "u") { - hex := "0x" . SubStr(str, z+2, 4) + hex := "0x" . SubStr(str, k+2, 4) if !(A_IsUnicode || (Abs(hex) < 0x100)) continue - str := SubStr(str, 1, z-1) . Chr(hex) . SubStr(str, z+6) - } else throw Exception("Bad string") + str := SubStr(str, 1, k-1) . Chr(hex) . SubStr(str, k+6) + + } else throw "Invalid escape sequence: '\%ch%'" } - strings.Insert(str) + ObjPush(strings, str) } ;// Check for missing opening/closing brace(s) if InStr(src, '{') || InStr(src, '}') { StrReplace(src, '{', '{', c1), StrReplace(src, '}', '}', c2) - if (c1 != c2), throw Exception(" + if (c1 != c2), throw " (LTrim Q Missing %Abs(c1-c2)% %(c1 > c2 ? 'clos' : 'open')%ing brace(s) - )", -1) + )" } ;// Check for missing opening/closing bracket(s) if InStr(src, '[') || InStr(src, ']') { StrReplace(src, '[', '[', c1), StrReplace(src, ']', ']', c2) - if (c1 != c2), throw Exception(" + if (c1 != c2), throw " (LTrim Q Missing %Abs(c1-c2)% %(c1 > c2 ? 'clos' : 'open')%ing bracket(s) - )", -1) + )" } - if jsonize, (_object := this.object, _array := this.array) - else (_object := Object(), _array := Array()) - pos := 0 - , key := dummy := [] - , stack := [result := []] - , assert := "" ;// 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) - ;// no container object - if (cont == result), throw Exception(" - (LTrim - Unexpected '%ch%' -> there is no container object/array. - )") - assert := (cont is _object && ch == ',') ? '"' : '{["tfn0123456789-' - - } else if InStr("{[", ch) { ;// object|array - opening - cont := stack[1] - , sub := new (ch == '{' ? _object : _array) - , stack.Insert(1, cont[key == dummy ? (ObjMaxIndex(cont) || 0)+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) - ;// remove base if set to output normal AHK object(s) - if !jsonize, cont.base := "" - cont := stack[1] - , assert := cont is _object ? '},' : '],' + is_array := is_arr[obj := stack[1]] - } else if (ch == '"') { ;// string - str := strings.Remove(1), cont := stack[1] - if (key == dummy) { - if (cont is _object) { - key := str, assert := ':' + if InStr("{[", ch) { + val := (proto := jbase[ch]) ? new proto : {} + , obj[is_array? ObjLength(obj)+1 : key] := val + , ObjInsertAt(stack, 1, val) + , is_arr[val] := !(is_key := ch == "{") + , next := is_key ? '"}' : '"{[]0123456789-tfn' + } + + else if InStr("}]", ch) { + ObjRemoveAt(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 := ObjRemoveAt(strings, 1) + if is_key { + key := val, next := ":" continue } - ;// _array or result | using 'else' seems faster, sometimes - else key := (ObjMaxIndex(cont) || 0)+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 is _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 += num.Len()-1 - , cont := stack[1] - , cont[key == dummy ? (ObjMaxIndex(cont) || 0)+1 : key] := num.Value+0 - , assert := cont is _object ? '},' : '],' - if (key != dummy), key := dummy - - } else if InStr("tfn", ch, true) { ;// true|false|null - /* Ternary seems faster compared to object -> - * val := {t:"true", f:"false", n:"null"}[ch] - */ - val := (ch == 't') ? 'true' : (ch == 'f') ? 'false' : 'null' - ;// case-sensitve 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 ? (ObjMaxIndex(cont) || 0)+1 : key] := %val%+0 - , assert := cont is _object ? '},' : '],' - if (key != dummy), key := dummy + obj[is_array? ObjLength(obj)+1 : key] := val + , next := is_array ? "]," : "}," } } - return result[1] + return tree[1] } stringify(obj:="", indent:="", lvl:=1) { type := Type(obj) if (type == "Object") { + is_array := 0 for k in obj - if !(arr := (k == A_Index)), break - - n := indent ? "`n" : (i := indent := "") + if !(is_array := (k == A_Index)), break + + if (Abs(indent) != "") { + if (indent < 0) + throw "Indent parameter must be a postive integer" + spaces := indent, indent := "" + Loop % spaces + indent .= " " + } + indt := "" Loop, % indent ? lvl : 0 - i .= indent + indt .= indent - lvl += 1, str := "" ;// make #Warn happy + lvl += 1, out := "" ;// make #Warn happy for k, v in obj { - if IsObject(k) || (k == ""), throw Exception("Invalid key.", -1) - if !arr, key := k is 'number' ? '"%k%"' : JSON.stringify(k) - val := JSON.stringify(v, indent, lvl) - ;// format output - str .= (arr ? "" : " - (LTrim Join Q C - %key%:%(indent - ? (IsObject(v) && InStr(val, '{') == 1 && val != '{}') - ? n . i - : ' ' - : '')% - )") . "%val%,%(indent ? n . i : '')%" + if IsObject(k) || (k == ""), throw "Invalid JSON key" + if !is_array + out .= ( Type(k) == "String" ? JSON.stringify(k) : '"%k%"' ) ;// key + . ( indent ? ": " : ":" ) ;// token + out .= JSON.stringify(v, indent, lvl) ;// value + . ( indent ? ",`n%indt%" : "," ) ;// token + indent } - ;// 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%}" - - } else if (type == "Integer" || type == "Float") { - return InStr('01', obj) ? (obj ? 'true' : 'false') : obj - - } else if (type == "String") { - if (obj == ""), return 'null' - esc_char := { + + return is_array ? "[%out%]" : "{%out%}" + } + + else if (type == "Integer" || type == "Float") + return InStr("01", obj) ? (obj ? "true" : "false") : obj + + else if (type == "String") { + if (obj == ""), return null := "null" ;// compensate for v2.0-a049 bug + esc_seq := { (Q '"': '\"', '/': '\/', @@ -196,103 +184,91 @@ class JSON '`t': '\t' )} obj := StrReplace(obj, "\", "\\") - for k, v in esc_char + for k, v in esc_seq obj := StrReplace(obj, k, v) - while RegExMatch(obj, "[^\x20-\x7e]", ch) { - ustr := Ord(ch.Value), esc_ch := "\u", n := 12 - while (n >= 0) - esc_ch .= Chr((x:=(ustr>>n) & 15) + (x<10 ? 48 : 55)), n -= 4 - obj := StrReplace(obj, ch.Value, esc_ch) + while RegExMatch(obj, "[^\x20-\x7e]", wstr) { + ucp := Ord(wstr.Value), hex := "\u", n := 16 + while ((n -= 4) >= 0) + hex .= Chr( (x := (ucp >> n) & 15) + (x < 10 ? 48 : 55) ) + obj := StrReplace(obj, wstr.Value, hex) } return '"%obj%"' } - throw Exception("Unsupported type: '%type%'") + throw "Unsupported type: '%type%'" } class object { - __New(p*) { - ObjInsert(this, "_", []) - if Mod(p.MaxIndex(), 2), p.Insert("") - Loop p.MaxIndex()//2 - this[p[A_Index*2-1]] := p[A_Index*2] - } - - __Set(k, v, p*) { - this._.Insert(k) - } - - _NewEnum() { - return new JSON.object.Enum(this) + __New(args*) { + ObjRawSet(this, "_", []) + if ((len := ObjLength(args)) & 1) + throw "Invalid number of parameters" + Loop len//2 + this[args[A_Index*2-1]] := args[A_Index*2] } - Insert(k, v) { - return this[k] := v + __Set(key, val, args*) { + ObjPush(this._, key) } - Remove(k*) { - ascs := A_StringCaseSense - A_StringCaseSense := 'Off' - if (k.MaxIndex() > 1) { - k1 := k[1], k2 := k[2], is_int := false - if k1 is 'integer' && k2 is 'integer' - 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, (key is 'integer' ? [key, ''] : [key])*) - break + Remove(args*) { + is_range := ObjLength(args) > 1 + scs := A_StringCaseSense, A_StringCaseSense := "Off" + i := -1 + for index, key in ObjClone(this._) { + if is_range? (key >= args[1] && key <= args[2]) : (key = args[1]) + { + ObjRemoveAt(this._, index-(i+=1)) + if !is_range, break ;// single key only } } - A_StringCaseSense := ascs - return res - } - - GetCapacity(k*) { - return ObjGetCapacity((k.MinIndex() ? [this, k[1]] : [this._])*) + /* Alternative way + keys := [] + for index, key in this._ { + if is_range? (key >= args[1] && key <= args[2]) : (key = args[1]) + continue + ObjPush(keys, key) + } + ObjRawSet(this, "_", keys) + */ + A_StringCaseSense := scs + return ObjRemove(this, args*) } - len() { - return (this._.MaxIndex() || 0) + Count() { + return ObjLength(this._) } stringify(i:="") { return JSON.stringify(this, i) } - class Enum - { - __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 - } + _NewEnum() { + static proto := {"Next":JSON.object.Next} + return { + (Q + "base": proto, + "enum": this._._NewEnum(), + "obj": this + )} + } + + Next(ByRef key, ByRef val:="") { + if (ret := this.enum.Next(i, key)) + val := this.obj[key] + return ret } } 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 b85f4064ddea054a83dce5057cac406909cb69a2 Mon Sep 17 00:00:00 2001 From: Coco Belgica Date: Thu, 8 Jan 2015 04:37:58 +0800 Subject: [PATCH 13/14] Code refactored --- JSON.ahk | 161 ++++++++++++++++++++++++++++++++----------------------- 1 file changed, 95 insertions(+), 66 deletions(-) diff --git a/JSON.ahk b/JSON.ahk index dd2b574..6d65904 100644 --- a/JSON.ahk +++ b/JSON.ahk @@ -1,9 +1,10 @@ class JSON { - 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" + throw Exception("Empty JSON source") first := SubStr(src, 1, 1), last := SubStr(src, -1) if !InStr('{["tfn0123456789-', first) ;// valid beginning chars || !InStr('}]el0123456789"', last) ;// valid ending chars @@ -13,9 +14,9 @@ class JSON || (first == 'n' && last != 'l') ;// assume 'null' || (InStr('tf', first) && last != 'e') ;// assume 'true' OR 'false' || (InStr('-0123456789', first) && !(last is 'number')) ;// number - throw "Invalid JSON format" + throw Exception("Invalid JSON format") - esc_seq := { + static esc_seq := { (Q '"': '"', '/': '/', @@ -27,64 +28,74 @@ class JSON )} ;// Extract string literals i := 0, strings := [] - while (i:=InStr(src, '"',, i+1)) { + while (i:=InStr(src, '"',, i+1)) + { j := i - while (j:=InStr(src, '"',, j+1)) { + while (j:=InStr(src, '"',, j+1)) + { str := SubStr(src, i+1, j-i-1) str := StrReplace(str, "\\", "\u005C") if (SubStr(str, -1) != "\") break } - if !j, throw "Missing close quote(s)" + + 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)) { + while (k := InStr(str, "\",, k+1)) + { ch := SubStr(str, k+1, 1) if InStr('"btnfr/', ch) str := SubStr(str, 1, k-1) . esc_seq[ch] . SubStr(str, k+2) - else if (ch = "u") { + else if (ch = "u") + { hex := "0x" . SubStr(str, k+2, 4) if !(A_IsUnicode || (Abs(hex) < 0x100)) continue str := SubStr(str, 1, k-1) . Chr(hex) . SubStr(str, k+6) - } else throw "Invalid escape sequence: '\%ch%'" + } + else + throw Exception("Invalid escape sequence: '\%ch%'") } ObjPush(strings, str) } + ;// Check for missing opening/closing brace(s) - if InStr(src, '{') || InStr(src, '}') { + if InStr(src, '{') || InStr(src, '}') + { StrReplace(src, '{', '{', c1), StrReplace(src, '}', '}', c2) - if (c1 != c2), throw " - (LTrim Q - Missing %Abs(c1-c2)% %(c1 > c2 ? 'clos' : 'open')%ing brace(s) - )" + if (c1 != c2) + throw Exception("Missing %Abs(c1-c2)% %(c1 > c2 ? 'clos' : 'open')%ing brace(s)") } ;// Check for missing opening/closing bracket(s) - if InStr(src, '[') || InStr(src, ']') { + if InStr(src, '[') || InStr(src, ']') + { StrReplace(src, '[', '[', c1), StrReplace(src, ']', ']', c2) - if (c1 != c2), throw " - (LTrim Q - Missing %Abs(c1-c2)% %(c1 > c2 ? 'clos' : 'open')%ing bracket(s) - )" + if (c1 != c2) + throw Exception("Missing %Abs(c1-c2)% %(c1 > c2 ? 'clos' : 'open')%ing bracket(s)") } - t := "true", f := "false", n := "null", null := "" - jbase := jsonize ? {"{":JSON.object, "[":JSON.array} : {"{":0, "[":0} + + 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 := 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("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? ObjLength(obj)+1 : key] := val , ObjInsertAt(stack, 1, val) @@ -92,36 +103,42 @@ class JSON , next := is_key ? '"}' : '"{[]0123456789-tfn' } - else if InStr("}]", ch) { + 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." + throw Exception("Unexpected char: '%ch%' -> there is no container object.") next := '"{[0123456789-tfn', is_key := (!is_array && ch == ",") } - else { - if (ch == '"') { + else + { + if (ch == '"') + { val := ObjRemoveAt(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("Expected '%(%ch%)%' instead of '%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? ObjLength(obj)+1 : key] := val @@ -131,14 +148,17 @@ class JSON return tree[1] } - stringify(obj:="", indent:="", lvl:=1) { + stringify(obj:="", indent:="", lvl:=1) + { type := Type(obj) - if (type == "Object") { + if (type == "Object") + { is_array := 0 for k in obj if !(is_array := (k == A_Index)), break - if (Abs(indent) != "") { + if (Abs(indent) != "") + { if (indent < 0) throw "Indent parameter must be a postive integer" spaces := indent, indent := "" @@ -150,8 +170,10 @@ class JSON indt .= indent lvl += 1, out := "" ;// make #Warn happy - for k, v in obj { - if IsObject(k) || (k == ""), throw "Invalid JSON key" + for k, v in obj + { + if IsObject(k) || (k == ""), throw Exception("Invalid JSON key") + if !is_array out .= ( Type(k) == "String" ? JSON.stringify(k) : '"%k%"' ) ;// key . ( indent ? ": " : ":" ) ;// token @@ -159,7 +181,8 @@ 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) @@ -171,9 +194,10 @@ class JSON else if (type == "Integer" || type == "Float") return InStr("01", obj) ? (obj ? "true" : "false") : obj - else if (type == "String") { + else if (type == "String") + { if (obj == ""), return null := "null" ;// compensate for v2.0-a049 bug - esc_seq := { + static esc_seq := { (Q '"': '\"', '/': '\/', @@ -186,7 +210,8 @@ class JSON obj := StrReplace(obj, "\", "\\") for k, v in esc_seq obj := StrReplace(obj, k, v) - while RegExMatch(obj, "[^\x20-\x7e]", wstr) { + while RegExMatch(obj, "[^\x20-\x7e]", wstr) + { ucp := Ord(wstr.Value), hex := "\u", n := 16 while ((n -= 4) >= 0) hex .= Chr( (x := (ucp >> n) & 15) + (x < 10 ? 48 : 55) ) @@ -194,24 +219,27 @@ class JSON } return '"%obj%"' } - throw "Unsupported type: '%type%'" + throw Exception("Unsupported type: '%type%'") } class object { - __New(args*) { + __New(args*) + { ObjRawSet(this, "_", []) if ((len := ObjLength(args)) & 1) - throw "Invalid number of parameters" + throw Exception("Invalid number of parameters") Loop len//2 this[args[A_Index*2-1]] := args[A_Index*2] } - __Set(key, val, args*) { + __Set(key, val, args*) + { ObjPush(this._, key) } - Remove(args*) { + Remove(args*) + { is_range := ObjLength(args) > 1 scs := A_StringCaseSense, A_StringCaseSense := "Off" i := -1 @@ -235,25 +263,24 @@ class JSON return ObjRemove(this, args*) } - Count() { + Count() + { return ObjLength(this._) } - stringify(i:="") { + stringify(i:="") + { return JSON.stringify(this, i) } - _NewEnum() { - static proto := {"Next":JSON.object.Next} - return { - (Q - "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 @@ -262,12 +289,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 c35c37e020d3d7fcc192577352fc6b7253b3c1bd Mon Sep 17 00:00:00 2001 From: Coco Belgica Date: Thu, 8 Jan 2015 04:46:20 +0800 Subject: [PATCH 14/14] Changed: .stringify() - 1, 0 and "" are no longer returned as true, false and null --- JSON.ahk | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/JSON.ahk b/JSON.ahk index 6d65904..268d6ac 100644 --- a/JSON.ahk +++ b/JSON.ahk @@ -192,11 +192,10 @@ class JSON } else if (type == "Integer" || type == "Float") - return InStr("01", obj) ? (obj ? "true" : "false") : obj + return obj else if (type == "String") { - if (obj == ""), return null := "null" ;// compensate for v2.0-a049 bug static esc_seq := { (Q '"': '\"', @@ -207,15 +206,18 @@ class JSON '`r': '\r', '`t': '\t' )} - obj := StrReplace(obj, "\", "\\") - for k, v in esc_seq - obj := StrReplace(obj, k, v) - while RegExMatch(obj, "[^\x20-\x7e]", wstr) + if (obj != "") { - ucp := Ord(wstr.Value), hex := "\u", n := 16 - while ((n -= 4) >= 0) - hex .= Chr( (x := (ucp >> n) & 15) + (x < 10 ? 48 : 55) ) - obj := StrReplace(obj, wstr.Value, hex) + obj := StrReplace(obj, "\", "\\") + for k, v in esc_seq + obj := StrReplace(obj, k, v) + while RegExMatch(obj, "[^\x20-\x7e]", wstr) + { + ucp := Ord(wstr.Value), hex := "\u", n := 16 + while ((n -= 4) >= 0) + hex .= Chr( (x := (ucp >> n) & 15) + (x < 10 ? 48 : 55) ) + obj := StrReplace(obj, wstr.Value, hex) + } } return '"%obj%"' }