1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
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
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
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
|
--[[
Small implementation of a JSON file reader.
]]
_libs = _libs or {}
require('tables')
require('lists')
require('sets')
require('strings')
local table, list, set, string = _libs.tables, _libs.lists, _libs.sets, _libs.strings
local files = require('files')
local json = {}
_libs.json = json
-- Define singleton JSON characters that can delimit strings.
local singletons = '{}[],:'
local key_types = S{'string', 'number'}
local value_types = S{'boolean', 'number', 'string', 'nil'}
-- Takes a filename and tries to parse the JSON in it, after a validity check.
function json.read(file)
if type(file) == 'string' then
file = files.new(file)
end
if not file:exists() then
return json.error('File not found: \''..file.path..'\'')
end
return json.parse(file:read())
end
-- Returns nil as the parsed table and an additional error message with an optional line number.
function json.error(message, line)
if line == nil then
return nil, 'JSON error: '..message
end
return nil, 'JSON error, line '..line..': '..message
end
-- Returns a Lua value based on a string.
-- Recognizes all valid atomic JSON values: booleans, numbers, strings and null.
-- Object and error groupings will be eliminated during the classifying process.
-- If stripquotes is set to true, quote characters delimiting strings will be stripped.
function json.make_val(str, stripquotes)
stripquotes = true and (stripquotes ~= false)
str = str:trim()
if str == '' then
return nil
elseif str == 'true' then
return true
elseif str == 'false' then
return false
elseif str == 'null' then
return nil
elseif stripquotes and (str:enclosed('\'') or str:enclosed('"')) then
return str:slice(2, -2)
end
str = str:gsub('\\x([%w%d][%w%d])', string.char..tonumber-{16})
return tonumber(str) or str
end
-- Parsing function. Gets a string representation of a JSON object and outputs a Lua table or an error message.
function json.parse(content)
return json.classify(json.tokenize(content))
end
-- Tokenizer. Reads a string and returns an array of lines, each line with a number of valid JSON tokens. Valid tokens include:
-- * \w+ Keys or values
-- * : Key indexer
-- * , Value separator
-- * \{\} Dictionary start/end
-- * \[\] List start/end
function json.tokenize(content)
-- Tokenizer. Reads the string by characters and finds word boundaries, returning an array of tokens to be interpreted.
local current = nil
local tokens = L{L{}}
local quote = nil
local comment = false
local line = 1
content = content:trim()
local length = #content
if content:sub(length, length) == ',' then
content = content:sub(1, length - 1)
length = length - 1
end
local first = content:sub(1, 1)
local last = content:sub(length, length)
if first ~= '[' and first ~= '{' then
return json.error('Invalid JSON format. Document needs to start with \'{\' (object) or \'[\' (array).')
end
if not (first == '[' and last == ']' or first == '{' and last == '}') then
return json.error('Invalid JSON format. Document starts with \''..first..'\' but ends with \''..last..'\'.')
end
local root
if first == '[' then
root = 'array'
else
root = 'object'
end
content = content:sub(2, length - 1)
for c in content:it() do
-- Only useful for a line count, to produce more accurate debug messages.
if c == '\n' then
line = line + 1
comment = false
tokens:append(L{})
end
-- If the quote character is set, don't parse but syntax, but instead just append to the string until the same quote character is encountered.
if quote ~= nil then
current = current..c
-- If the quote character is found, append the parsed string and reset the parsing values.
if quote == c then
tokens[line]:append(json.make_val(current))
current = nil
quote = nil
end
elseif not comment then
-- If the character is a singleton character, append the previous token and this one, reset the parsing values.
if singletons:contains(c) then
if current ~= nil then
tokens[line]:append(json.make_val(current))
current = nil
end
tokens[line]:append(c)
-- If a quote character is found, start a quoting session, see alternative condition.
elseif c == '"' or c == '\'' and current == nil then
quote = c
current = c
-- Otherwise, just append
elseif not c:match('%s') or current ~= nil then
-- Ignore comments. Not JSON conformant.
if c == '/' and current ~= nil and current:last() == '/' then
current = current:slice(1, -2)
if current == '' then
current = nil
end
comment = true
else
current = current or ''
current = current..c
end
end
end
end
return tokens, root
end
-- Takes a list of tokens and analyzes it to construct a valid Lua object from it.
function json.classify(tokens, root)
if tokens == nil then
return tokens, root
end
local scopes = L{root}
-- Scopes and their domains:
-- * 'object': Object scope, delimited by '{' and '}' as well as global scope
-- * 'array': Array scope, delimited by '[' and ']'
-- Possible modes and triggers:
-- * 'new': After an opening brace, bracket, comma or at the start, expecting a new element
-- * 'key': After reading a key
-- * 'colon': After reading a colon
-- * 'value': After reading or having scoped a value (either an object, or an array for the latter)
local modes = L{'new'}
local parsed
if root == 'object' then
parsed = L{T{}}
else
parsed = L{L{}}
end
local keys = L{}
-- Classifier. Iterates through the tokens and assigns meaning to them. Determines scoping and creates objects and arrays.
for array, line in tokens:it() do
for token, pos in array:it() do
if token == '{' then
if modes:last() == 'colon' or modes:last() == 'new' and scopes:last() == 'array' then
parsed:append(T{})
scopes:append('object')
modes:append('new')
else
return json.error('Unexpected token \'{\'.', line)
end
elseif token == '}' then
if modes:last() == 'value' or modes:last() == 'new' then
modes:remove()
scopes:remove()
if modes:last() == 'colon' then
parsed:last(2)[keys:remove()] = parsed:remove()
elseif modes:last() == 'new' and scopes:last() == 'array' then
parsed:last():append(parsed:remove())
else
return json.error('Unexpected token \'}\'.', line)
end
modes[#modes] = 'value'
else
return json.error('Unexpected token \'}\'.', line)
end
elseif token == '[' then
if modes:last() == 'colon' or modes:last() == 'new' and scopes:last() == 'array' then
parsed:append(T{})
scopes:append('array')
modes:append('new')
else
return json.error('Unexpected token \'{\'.', line)
end
elseif token == ']' then
if modes:last() == 'value' or modes:last() == 'new' then
modes:remove()
scopes:remove()
if modes:last() == 'colon' then
parsed[#parsed-1][keys:remove()] = parsed:remove()
elseif modes:last() == 'new' and scopes:last() == 'array' then
parsed:last():append(parsed:remove())
else
return json.error('Unexpected token \'}\'.', line)
end
modes[#modes] = 'value'
else
return json.error('Unexpected token \'}\'.', line)
end
elseif token == ':' then
if modes:last() == 'key' then
modes[#modes] = 'colon'
else
return json.error('Unexpected token \':\'.', line)
end
elseif token == ',' then
if modes:last() == 'value' then
modes[#modes] = 'new'
else
return json.error('Unexpected token \',\'.', line)
end
elseif key_types:contains(type(token)) and modes:last() == 'new' and scopes:last() == 'object' then
keys:append(token)
modes[#modes] = 'key'
elseif value_types:contains(type(token)) then
if modes:last() == 'colon' then
parsed:last()[keys:remove()] = token
modes[#modes] = 'value'
elseif modes:last() == 'new' then
if scopes:last() == 'array' then
parsed:last():append(token)
modes[#modes] = 'value'
else
return json.error('Unexpected token \''..token..'\'.', line)
end
else
return json.error('Unexpected token \''..token..'\'.', line)
end
else
return json.error('Unkown token parsed. You should never see this. Token type: '..type(token), line)
end
end
end
if parsed:empty() then
return json.error('No JSON found.')
end
if #parsed > 1 then
return json.error('Invalid nesting, missing closing tags.')
end
return parsed:remove()
end
return json
--[[
Copyright (c) 2013, Windower
All rights reserved.
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
* Neither the name of Windower nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL Windower BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
]]
|