diff --git a/ASUnit/ASUnit.applescript b/ASUnit/ASUnit.applescript new file mode 100644 index 0000000..b03b1c8 Binary files /dev/null and b/ASUnit/ASUnit.applescript differ diff --git a/Makefile b/Makefile index 5bf1d64..fc221ac 100644 --- a/Makefile +++ b/Makefile @@ -2,4 +2,5 @@ json.scpt: json.applescript osacompile -o json.scpt json.applescript test: json.scpt + osacompile -o ASUnit/ASUnit.scpt -x ASUnit/ASUnit.applescript osascript tests.applescript diff --git a/README.md b/README.md index afe454f..7ccdd15 100644 --- a/README.md +++ b/README.md @@ -47,3 +47,25 @@ set my_dict_2 to json's createDictWith({ {"foo", "bar"}, {"baz", 22} }) json's encode(my_dict_2) -- {"foo": "bar", "baz": 22} ``` + +And also natively (which gives a small performance penalty): +```applescript +json's encode({glossary:¬ + {GlossDiv:¬ + {GlossList:¬ + {GlossEntry:¬ + {GlossDef:¬ + {GlossSeeAlso:¬ + ["GML", "XML"], para:"A meta-markup language, used to create markup languages such as DocBook."} ¬ + , GlossSee:"markup", Acronym:"SGML", GlossTerm:"Standard Generalized Markup Language", Abbrev:"ISO 8879:1986", SortAs:"SGML", id:¬ + "SGML"} ¬ + }, title:"S"} ¬ + , title:"example glossary"} ¬ + }) +``` + +Decoding is also available (dictionaries with keys which contain spaces are not supported): +```applescript +set dict to {foo:"bar"} +json's decode(json's encode(dict)) -- {foo: "bar"} +``` diff --git a/json.applescript b/json.applescript index 3249de0..92c33b8 100644 --- a/json.applescript +++ b/json.applescript @@ -1,41 +1,191 @@ +on decodeWithDicts(value) + set value to replaceString(value, "\\", "\\\\") + set s to "import json, sys, codecs" & return + set s to s & "sys.stdin = codecs.getreader('utf8')(sys.stdin)" & return + set s to s & "def toAppleScript(pythonValue):" & return + set s to s & " output = ''" & return + set s to s & " if(pythonValue == None):" & return + set s to s & " output += 'null'" & return + set s to s & " elif (isinstance(pythonValue, dict)):" & return + set s to s & " output += 'json\\'s createDictWith({'" & return + set s to s & " first = True" & return + set s to s & " for (key, value) in pythonValue.iteritems():" & return + set s to s & " if first:" & return + set s to s & " first = False" & return + set s to s & " else:" & return + set s to s & " output += ','" & return + set s to s & " output += '{' + toAppleScript(key) + ',' " & return + set s to s & " output += toAppleScript(value) + '}'" & return + set s to s & " output += '})'" & return + set s to s & " elif (isinstance(pythonValue, list)):" & return + set s to s & " output += '{'" & return + set s to s & " first = True" & return + set s to s & " for value in pythonValue:" & return + set s to s & " if first:" & return + set s to s & " first = False" & return + set s to s & " else:" & return + set s to s & " output += ','" & return + set s to s & " output += toAppleScript(value)" & return + set s to s & " output += '}'" & return + set s to s & " elif(isinstance(pythonValue, basestring)):" & return + set s to s & " output += '\"' + pythonValue.replace('\"', '\\\\\"') + '\"'" & return + set s to s & " else:" & return + set s to s & " output += json.dumps(pythonValue)" & return + set s to s & " return output" & return + -- sys.stdout to be able to write utf8 to our buffer + -- We can ignore newlines in JSON format freely, as our \n will convert into new lines in bash we will use the actual new lines as the string \n + set s to s & "sys.stdout.write(toAppleScript(json.loads(sys.stdin.read())).encode('utf8'))" + set value to replaceString(value, {return & linefeed, return, linefeed, character id 8233, character id 8232, character id 12, character id 9, character id 8}, "") + -- AppleScript translates new lines in old mac returns so we need to turn that off + set appleCode to do shell script "echo " & quoted form of value & " \"\\c\" |python2.7 -c " & quoted form of s without altering line endings + set appleCode to replaceString(replaceString(appleCode, "\\", "\\\\"), "\\\"", "\"") + set s to "on run {json}" & return + set s to s & appleCode & return + set s to s & "end" + return (run script s with parameters {me}) +end decodeWithDicts + +on decode(value) + set value to replaceString(value, "\\", "\\\\") + set s to "import json, sys, codecs" & return + set s to s & "sys.stdin = codecs.getreader('utf8')(sys.stdin)" & return + set s to s & "def toAppleScript(pythonValue):" & return + set s to s & " output = ''" & return + set s to s & " if(pythonValue == None):" & return + set s to s & " output += 'null'" & return + set s to s & " elif (isinstance(pythonValue, dict)):" & return + set s to s & " output += '{'" & return + set s to s & " first = True" & return + set s to s & " for (key, value) in pythonValue.iteritems():" & return + set s to s & " if first:" & return + set s to s & " first = False" & return + set s to s & " else:" & return + set s to s & " output += ','" & return + set s to s & " output += key + ':' " & return + set s to s & " output += toAppleScript(value)" & return + set s to s & " output += '}'" & return + set s to s & " elif (isinstance(pythonValue, list)):" & return + set s to s & " output += '{'" & return + set s to s & " first = True" & return + set s to s & " for value in pythonValue:" & return + set s to s & " if first:" & return + set s to s & " first = False" & return + set s to s & " else:" & return + set s to s & " output += ','" & return + set s to s & " output += toAppleScript(value)" & return + set s to s & " output += '}'" & return + set s to s & " elif(isinstance(pythonValue, basestring)):" & return + set s to s & " output += '\"' + pythonValue.replace('\"', '\\\\\"') + '\"'" & return + set s to s & " else:" & return + set s to s & " output += json.dumps(pythonValue)" & return + set s to s & " return output" & return + -- sys.stdout to be able to write utf8 to our buffer + -- We can ignore newlines in JSON format freely, as our \n will convert into new lines in bash we will use the actual new lines as the string \n + set s to s & "sys.stdout.write(toAppleScript(json.loads(sys.stdin.read())).encode('utf8'))" + set value to replaceString(value, {return & linefeed, return, linefeed, character id 8233, character id 8232, character id 12, character id 9, character id 8}, "") + -- AppleScript translates new lines in old mac returns so we need to turn that off + set appleCode to do shell script "echo " & quoted form of value & " \"\\c\" |python2.7 -c " & quoted form of s without altering line endings + set appleCode to replaceString(replaceString(appleCode, "\\", "\\\\"), "\\\"", "\"") + set s to "on run " & return + set s to s & appleCode & return + set s to s & "end" + return (run script s) +end decode + on encode(value) set type to class of value - if type = integer - return value as text - else if type = text + if type = integer or type = real then + return replaceString(value as text, ",", ".") + else if type = text then return encodeString(value) - else if type = list + else if type = list then + if isBigList(value) then + return encodeRecord(value) + else + return encodeList(value) + end if + else if type = script then + return value's toJson() + else if type = record then + return encodeRecord(value) + else if type = class and (value as text) = "null" then + return "null" + else + error "Unknown type " & type + end if +end encode + +-- skips BigList check +on _encode(value) + set type to class of value + if type = integer or type = real then + return replaceString(value as text, ",", ".") + else if type = text then + return encodeString(value) + else if type = list then return encodeList(value) - else if type = script + else if type = script then return value's toJson() + else if type = record then + return encodeRecord(value) + else if type = class and (value as text) = "null" then + return "null" else error "Unknown type " & type - end -end + end if +end _encode +on isBigList(value) + repeat with element in value + set type to class of element + if type = list then + if isBigList(element) then + return true + end if + else if type = record then + return true + end if + end repeat + return false +end isBigList on encodeList(value_list) set out_list to {} repeat with value in value_list - copy encode(value) to end of out_list - end + copy _encode(value) to end of out_list + end repeat return "[" & join(out_list, ", ") & "]" -end +end encodeList on encodeString(value) + if (count of value) ³ 256 then -- Large string manipulations are slow in AppleScript + set s to "import json, sys, codecs" & return + set s to s & "sys.stdin = codecs.getreader('utf8')(sys.stdin)" & return + set s to s & "sys.stdout.write(json.dumps(sys.stdin.read()))" + return do shell script "echo " & quoted form of value & " \"\\c\" | python2.7 -c " & quoted form of s + end if set rv to "" repeat with ch in value - if id of ch >= 32 and id of ch < 127 + if id of ch = 34 or id of ch = 92 then + set quoted_ch to "\\" & ch + else if id of ch ³ 32 and id of ch < 127 then set quoted_ch to ch - else + else if id of ch < 65536 then set quoted_ch to "\\u" & hex4(id of ch) - end + else + set v to id of ch + set v_ to v - 65536 + set vh to v_ / 1024 + set vl to v_ mod 1024 + set w1 to 55296 + vh + set w2 to 56320 + vl + set quoted_ch to "\\u" & hex4(w1) & "\\u" & hex4(w2) + end if set rv to rv & quoted_ch - end + end repeat return "\"" & rv & "\"" -end - +end encodeString on join(value_list, delimiter) set original_delimiter to AppleScript's text item delimiters @@ -43,7 +193,16 @@ on join(value_list, delimiter) set rv to value_list as text set AppleScript's text item delimiters to original_delimiter return rv -end +end join + +on replaceString(theText, oldString, newString) + set AppleScript's text item delimiters to oldString + set tempList to every text item of theText + set AppleScript's text item delimiters to newString + set theText to the tempList as string + set AppleScript's text item delimiters to "" + return theText +end replaceString on hex4(n) @@ -52,39 +211,140 @@ on hex4(n) repeat until length of rv = 4 set digit to (n mod 16) set n to (n - digit) / 16 as integer - set rv to (character (1+digit) of digit_list) & rv - end + set rv to (character (1 + digit) of digit_list) & rv + end repeat return rv -end +end hex4 on createDictWith(item_pairs) set item_list to {} - - script Dict - on setkv(key, value) + + script dict + on setValue(key, value) + set i to 1 + set C to count item_list + repeat until i > C + set kv to item i of item_list + if item 1 of kv = key then + set item 2 of kv to value + set item i of item_list to kv + return + end if + set i to i + 1 + end repeat copy {key, value} to end of item_list - end - + end setValue + on toJson() set item_strings to {} repeat with kv in item_list set key_str to encodeString(item 1 of kv) set value_str to encode(item 2 of kv) copy key_str & ": " & value_str to end of item_strings - end + end repeat return "{" & join(item_strings, ", ") & "}" - end - end - + end toJson + + on getValue(key) + repeat with kv in item_list + if item 1 of kv = key then + return item 2 of kv + end if + end repeat + error "No such key " & key & " found." + end getValue + + on toRecord() + return decode(toJson()) + end toRecord + end script + repeat with pair in item_pairs - Dict's setkv(item 1 of pair, item 2 of pair) - end - - return Dict -end - + dict's setValue(item 1 of pair, item 2 of pair) + end repeat + + return dict +end createDictWith on createDict() return createDictWith({}) -end +end createDict + +on recordToString(aRecord) + try + set type to class of aRecord + --This ensures applescript knows about the type + if class of aRecord = list then + set aRecord to aRecord as list + else + set aRecord to aRecord as record + end if + set aRecord to aRecord + set str to aRecord as text + on error errorMsg + set startindex to 1 + set eos to length of errorMsg + repeat until startindex is eos + if character startindex of errorMsg = "{" then + exit repeat + end if + set startindex to startindex + 1 + end repeat + set endindex to eos + repeat until endindex is 1 + if character endindex of errorMsg = "}" then + exit repeat + end if + set endindex to endindex - 1 + end repeat + set str to ((characters startindex thru endindex of errorMsg) as string) + if startindex < endindex then + return str + end if + end try + set oldClipboard to the clipboard + set the clipboard to {aRecord} + set str to (do shell script "osascript -s s -e 'the clipboard as record'") + set the clipboard to oldClipboard + set str to ((characters 8 thru -1 of str) as string) + set str to ((characters 1 thru -3 of str as string)) + return str +end recordToString + +on encodeRecord(value_record) + -- json can be used to escape a string for python + set strRepr to recordToString(value_record) + set strRepr to replaceString(strRepr, "\\\\", "\\\\\\\\") + set s to "import json, token, tokenize, sys, codecs" & return + set s to s & "from StringIO import StringIO" & return + set s to s & "sys.stdin = codecs.getreader('utf8')(sys.stdin)" & return + set s to s & "def appleScriptNotationToJSON (in_text):" & return + + set s to s & " tokengen = tokenize.generate_tokens(StringIO(in_text).readline)" & return + set s to s & " depth = 0" & return + set s to s & " opstack = []" & return + set s to s & " result = []" & return + set s to s & " for tokid, tokval, _, _, _ in tokengen:" & return + set s to s & " if (tokid == token.NAME):" & return + set s to s & " if tokval not in ['true', 'false', 'null', '-Infinity', 'Infinity', 'NaN']:" & return + set s to s & " tokid = token.STRING" & return + set s to s & " tokval = u'\"%s\"' % tokval" & return + set s to s & " elif (tokid == token.STRING):" & return + set s to s & " if tokval.startswith (\"'\"):" & return + set s to s & " tokval = u'\"%s\"' % tokval[1:-1]" & return + set s to s & " elif (tokid == token.OP) and ((tokval == '}') or (tokval == ']')):" & return + set s to s & " if (len(result) > 0) and (result[-1][1] == ','):" & return + set s to s & " result.pop()" & return + set s to s & " tokval = '}' if result[opstack[-1]][1] == '{' else ']'" & return + set s to s & " opstack.pop()" & return + set s to s & " elif (tokid == token.OP) and (tokval == '{' or tokval == ']'):" & return + set s to s & " tokval = '['" & return + set s to s & " opstack.append(len(result))" & return + set s to s & " elif (tokid == token.OP) and (tokval == ':') and result[opstack[-1]][1] != '}':" & return + set s to s & " result[opstack[-1]] = (result[opstack[-1]][0], '{')" & return + set s to s & " result.append((tokid, tokval))" & return + set s to s & " return tokenize.untokenize(result)" & return + set s to s & "print json.dumps(json.loads(appleScriptNotationToJSON(sys.stdin.read().replace(\"\\n\", \"\\\\n\").replace(\"\\b\", \"\\\\b\").replace(\"\\f\", \"\\\\f\").replace(\"\\t\", \"\\\\t\").replace(\"\\r\", \"\\\\r\"))))" & return + return (do shell script "echo " & quoted form of strRepr & "\"\\c\" | python2.7 -c " & quoted form of s) +end encodeRecord diff --git a/tests.applescript b/tests.applescript index c3684e4..66b37e5 100644 Binary files a/tests.applescript and b/tests.applescript differ