Newer
Older
// JSON (JavaScript Object Notation) is a common format for exchanging data
// between different computers or programs. See: https://json.org/
// The data types in JSON are number, string, object, and array, which
// correspond very neatly to MiniScript's number, string, map, and list.
//
// This module provides code to read and write data in JSON format, as well
// as a couple of utility functions for escaping/unescaping strings and
// converting numbers to/from hexadecimal.
// Changes by Tom de QTG to adapt the parser to GreyScript
// parse: convert a JSON string into a MiniScript value (which could
// include a list or map of other values). This is the main entry point
// for reading JSON data and converting it to native form.
// Example: parse("42") // returns 42
parse = function(jsonString)
p = new Parser
return p.parse(jsonString)
end function
// toJSON: convert a MiniScript value to its JSON representation.
// Example: toJSON(42) // returns "42"
//
// Parameters:
// value: MiniScript value to convert
// compact: if true, omit all unnecessary whitespace
// indent: amount to indent if multiple lines are needed
toJSON = function(value, compact=false, indent=0)
if typeof(@value) == "function" then return """<function>"""
if value == null then return "null"
if typeof(value) == "number" then return str(value)
if typeof(value) == "string" then return """" + escape(value) + """"
if typeof(value) == "list" then return _listToJSON(value, compact, indent)
if typeof(value) == "map" then return _mapToJSON(value, compact, indent)
//To Implem
if typeof(value) == "router" then return _routerToJSON(value, compact, indent)
if typeof(value) == "computer" then return _computerToJSON(value, compact, indent)
if typeof(value) == "port" then return _portToJSON(value, compact, indent)
if typeof(value) == "file" then return _fileToJSON(value, compact, indent)
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
end function
// hexToInt: convert a string containing a hexadecimal number.
// Supports both uppercase and lowercase for digits A-F.
// Example: hexToInt("002A") // returns 42
hexToInt = function(s)
result = 0
for c in s
result = result * 16 + _hexDigitMap[c]
end for
return result
end function
// escape: find certain characters (tab, newline, etc.) in the given string,
// and represent them with backslash sequences.
// Example: escape(char(c)) // returns "\t"
escape = function(s)
for i in _escapeIndexes
s = s.replace(_escapeFrom[i], _escapeTo[i])
end for
return s
end function
// unescape: replace backslash sequences in the given string.
// Example: unescape("\t") // returns char(9)
unescape = function(s)
result = []
i = 0
maxi = s.len
while i < maxi
di = 1
if s[i] == "\" then
di = 2
c = s[i+1]
if c == "b" then
result.push(char(8))
else if c == "t" then
result.push(char(9))
else if c == "n" then
result.push(char(10))
else if c == "f" then
result.push(char(12))
else if c == "r" then
result.push(char(13))
else if c == "u" then
// Unicode code point (must always be 4 digits)
hex = s[i+2:i+6]
result.push(char(hexToInt(hex)))
di = 6
else
result.push(c)
end if
else
result.push(s[i])
end if
i = i + di
end while
return result.join("")
end function
//----------------------------------------------------------------------
// Stuff below is internal implementation;
// most users don't need to poke around here.
// Parsing JSON
Parser = {}
Parser.source = ""
Parser._sourceLen = 0
Parser._p = 0 // index of next character to consume in source
Parser.init = function(source)
self.source = source
self._sourceLen = source.len
end function
Parser.parse = function(source=null)
if source != null then
self.init(source)
end if
self._p = 0
return self._parseElement
end function
whitespace = " " + char(9) + char(10) + char(13)
Parser._skipWhitespace = function()
while self._p < self._sourceLen
c = self.source[self._p]
if whitespace.indexOf(c) == null then break
self._p = self._p + 1
end while
end function
Parser._parseElement = function()
return self._parseValue // for now!
end function
Parser._parseValue = function()
self._skipWhitespace
c = self.source[self._p]
if c == """" then return self._parseString
if "0123456789-.".indexOf(c) != null then return self._parseNumber
if c == "[" then return self._parseList
if c == "{" then return self._parseMap
if c == "t" and self.source[self._p:self._p+4] == "true" then
self._p = self._p + 4
return true
end if
if c == "f" and self.source[self._p:self._p+5] == "false" then
self._p = self._p + 5
return false
end if
if c == "n" and self.source[self._p:self._p+4] == "null" then
self._p = self._p + 4
return null
end if
end function
Parser._parseList = function()
self._p = self._p + 1 // skip "["
self._skipWhitespace
result = []
while self._p < self._sourceLen
c = self.source[self._p]
if c == "]" then break
result.push(self._parseElement)
self._skipWhitespace
// after an element, we should have either a comma or a ']'
c = self.source[self._p]
if c == "," then
self._p = self._p + 1
self._skipWhitespace
end if
end while
self._p = self._p + 1
return result
end function
Parser._parseMap = function()
self._p = self._p + 1 // skip "{"
self._skipWhitespace
result = {}
while self._p < self._sourceLen
// grab the key (must be a string)
c = self.source[self._p]
if c == "}" then
break
end if
if c != """" then
print("JSON error: object member key must be a string literal") // ToDo: better error handling!
print("Error at position " + self._p + ": " + self.source[self._p-60 : self._p+60])
return null
end if
key = self._parseString
self._skipWhitespace
// next token must be a colon
if self.source[self._p] != ":" then
print("JSON error: colon expected")
print("Error at position " + self._p + ": " + self.source[self._p-60 : self._p+60])
return null
end if
self._p = self._p + 1
self._skipWhitespace
// grab the value (could be anything)
value = self._parseElement
result[key] = value
self._skipWhitespace
// after a a key/value pair, we should have either a comma or a '}'
c = self.source[self._p]
if c == "," then
self._p = self._p + 1
self._skipWhitespace
end if
end while
self._p = self._p + 1
return result
end function
// Get a string literal from the source. Advance to the next
// character after the closing quote.
Parser._parseString = function()
self._p = self._p + 1
startPos = self._p
anyEscape = false
while self._p < self._sourceLen
c = self.source[self._p]
self._p = self._p + 1
if c == """" then break
if c == "\" then
anyEscape = true
self._p = self._p + 1
end if
end while
result = self.source[startPos : self._p-1]
if result == null then result = ""
if anyEscape then result = unescape(result)
return result
end function
// Get a numeric literal from the source. Advance to the next
// character after the number.
Parser._parseNumber = function()
startPos = self._p
while self._p < self._sourceLen
c = self.source[self._p]
// Note that we are rather permissive here, consuming things like
// 1-2e5+4E7, which is not valid JSON. But we're not trying to be
// a JSON validator; we're just trying to grok valid JSON as quickly
// as we can.
if "0123456789+-.eE".indexOf(c) == null then break
self._p = self._p + 1
end while
LaplongeJunior
a validé
// In original source, operation was
//result = val(self.source[startPos : self._p])
// However, it had the side-effect of using the "val" variable from the calling script!
result = self.source[startPos : self._p].val
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
return result
end function
// Generating JSON
_listToJSON = function(lst, compact, indent)
ws = (_eol + " "*(indent+1)) * (not compact)
parts = ["[", ws]
first = true
for item in lst
if not first then
parts.push(",")
parts.push(ws)
end if
parts.push(toJSON(item, compact, indent+1))
first = false
end for
if not compact then parts.push(_eol + " " * indent)
parts.push("]")
return join(parts, "")
end function
_mapToJSON = function(lst, compact, indent)
ws = (_eol + " "*(indent+1)) * (not compact)
parts = ["{", ws]
first = true
for kv in lst
if not first then
parts.push(",")
parts.push(ws)
end if
parts.push(toJSON(str(kv.key)))
parts.push(":")
if not compact then parts.push(" ")
parts.push(toJSON(@kv.value, compact, indent+1))
first = false
end for
if not compact then parts.push(_eol + " " * indent)
parts.push("}")
return join(parts, "")
end function
_routerToJSON = function(router, compact, indent)
end function
_computerToJSON = function(computer, compact, indent)
end function
_portToJSON = function(port, compact, indent)
end function
_fileToJSON = function(file, compact, indent)
// General utility data structures
_hexDigitMap = {}
for i in range(0,15)
if i < 10 then
_hexDigitMap[str(i)] = i
else
_hexDigitMap[char(55+i)] = i // (lowercase hex digit)
_hexDigitMap[char(87+i)] = i // (uppercase hex digit)
end if
end for
_escapeFrom = ["\", """", char(8), char(9), char(10), char(12), char(13)]
_escapeTo = ["\\", "\""", "\b","\t","\"+"n","\f","\r"]
_escapeIndexes = _escapeFrom.indexes
//#UnitTests (run when you load & run this script directly).
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
if locals == globals then
errorCount = 0
assertEqual = function(actual, expected)
if actual != expected then
print("Unit test failure: expected " + expected + ", got " + actual)
outer.errorCount = errorCount + 1
end if
end function
assertEqualEither = function(actual, expected, expected2)
if actual != expected and actual != expected2 then
print("Unit test failure: expected " + expected + ", got " + actual)
outer.errorCount = errorCount + 1
end if
end function
p = new Parser
p.init(" true ")
assertEqual(p.parse, "true")
assertEqual(parse("-123.45"), -123.45)
assertEqual(parse(char(13) + "42"), 42)
assertEqual(parse(".5"), 0.5)
assertEqual(parse("""\tHello, \""Bob\""."""), char(9) + "Hello, ""Bob"".")
assertEqual(parse("""\u002F"""), "/")
assertEqual(parse("[1, 2 , 3]"), [1, 2, 3])
assertEqual(parse("[ ""hey"", true, [0]]"), ["hey", true, [0]])
assertEqual(parse("{""hey"": ""ho"", ""9"" : 81}"), {"hey":"ho", "9":81})
// And here's a longer example... remember, quotes doubled only
// to make them valid MiniScript string literals...
data = parse("{""widget"": {" + " ""debug"": ""on""," + " ""window"": {" + " ""title"": ""Sample Konfabulator Widget""," + " ""name"": ""main_window""," + " ""visible"": false," + " ""width"": 500," + " ""height"": 300," + " ""left"": -123.456," + " ""top"": 234.567," + " }," + " ""image"": { " + " ""src"": ""Images\\Sun.png""," + " ""name"": ""sun1""," + " ""hOffset"": 250," + " ""vOffset"": 250," + " ""alignment"": ""center""" + " }," + " ""text"": {" + " ""data"": ""Click Here""," + " ""size"": 36," + " ""style"": ""bold""," + " ""name"": ""text1""," + " ""hOffset"": 250," + " ""vOffset"": 100," + " ""alignment"": ""center""," + " ""onMouseUp"": ""sun1.opacity = (sun1.opacity / 100) * 90;""" + " }}}")
assertEqual(data.widget.debug, "on")
assertEqual(data.widget.window.width, 500)
assertEqual(data.widget.image.src, "Images\Sun.png")
assertEqual(data.widget.text.size, 36)
ObjectTest = {}
ObjectTest.paramNumber = 42
ObjectTest.paramString = "Potatoe"
ObjectTest.paramList = [1, 2, 3]
ObjectTest.paramMap = {"one":1, "two":2}
ObjectTest.function1 = function ()
return "I am a function"
end function
assertEqual(toJSON(42), "42")
assertEqual(toJSON(char(9)), """\t""")
assertEqual(toJSON([1, 2, 3], true), "[1,2,3]")
assertEqual(toJSON(ObjectTest, true), "{""paramNumber"":42,""paramString"":""Potatoe"",""paramList"":[1,2,3],""paramMap"":{""one"":1,""two"":2},""function1"":""<function>""}")
// Maps are a bit tricky to unit-test, since the order in which the keys appear
// is undefined. But here we go:
assertEqualEither(toJSON({"one":1, "two":2}, true), "{""one"":1,""two"":2}", "{""two"":2,""one"":1}")
if errorCount == 0 then
print("All tests passed. Huzzah!")
else
print(errorCount + " error" + "s" * (errorCount!=1) + " found.")
end if
end if