Added JSON Lua parser

Author Mitja Felicijan <mitja.felicijan@gmail.com> 2025-08-06 05:21:54 +0200
Committer Mitja Felicijan <mitja.felicijan@gmail.com> 2025-08-06 05:21:54 +0200
Commit 4c8e0f6f28ce3a5b6729ddccfeeb5445e9ec9605 (patch)
-rw-r--r-- README.md 1
-rw-r--r-- stdlib/json.lua 388
2 files changed, 389 insertions, 0 deletions
diff --git a/README.md b/README.md
...
19
- https://github.com/rxi/microtar
19
- https://github.com/rxi/microtar
20
- https://github.com/lua/lua
20
- https://github.com/lua/lua
21
- https://github.com/raysan5/raylib
21
- https://github.com/raysan5/raylib
  
22
- https://github.com/rxi/json.lua
22
  
23
  
23
## Inspiration & Materials
24
## Inspiration & Materials
24
  
25
  
...
diff --git a/stdlib/json.lua b/stdlib/json.lua
  
1
--
  
2
-- json.lua
  
3
--
  
4
-- Copyright (c) 2020 rxi
  
5
--
  
6
-- Permission is hereby granted, free of charge, to any person obtaining a copy of
  
7
-- this software and associated documentation files (the "Software"), to deal in
  
8
-- the Software without restriction, including without limitation the rights to
  
9
-- use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  
10
-- of the Software, and to permit persons to whom the Software is furnished to do
  
11
-- so, subject to the following conditions:
  
12
--
  
13
-- The above copyright notice and this permission notice shall be included in all
  
14
-- copies or substantial portions of the Software.
  
15
--
  
16
-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  
17
-- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  
18
-- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  
19
-- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  
20
-- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  
21
-- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  
22
-- SOFTWARE.
  
23
--
  
24
  
  
25
local json = { _version = "0.1.2" }
  
26
  
  
27
-------------------------------------------------------------------------------
  
28
-- Encode
  
29
-------------------------------------------------------------------------------
  
30
  
  
31
local encode
  
32
  
  
33
local escape_char_map = {
  
34
  [ "\\" ] = "\\",
  
35
  [ "\"" ] = "\"",
  
36
  [ "\b" ] = "b",
  
37
  [ "\f" ] = "f",
  
38
  [ "\n" ] = "n",
  
39
  [ "\r" ] = "r",
  
40
  [ "\t" ] = "t",
  
41
}
  
42
  
  
43
local escape_char_map_inv = { [ "/" ] = "/" }
  
44
for k, v in pairs(escape_char_map) do
  
45
  escape_char_map_inv[v] = k
  
46
end
  
47
  
  
48
  
  
49
local function escape_char(c)
  
50
  return "\\" .. (escape_char_map[c] or string.format("u%04x", c:byte()))
  
51
end
  
52
  
  
53
  
  
54
local function encode_nil(val)
  
55
  return "null"
  
56
end
  
57
  
  
58
  
  
59
local function encode_table(val, stack)
  
60
  local res = {}
  
61
  stack = stack or {}
  
62
  
  
63
  -- Circular reference?
  
64
  if stack[val] then error("circular reference") end
  
65
  
  
66
  stack[val] = true
  
67
  
  
68
  if rawget(val, 1) ~= nil or next(val) == nil then
  
69
    -- Treat as array -- check keys are valid and it is not sparse
  
70
    local n = 0
  
71
    for k in pairs(val) do
  
72
      if type(k) ~= "number" then
  
73
        error("invalid table: mixed or invalid key types")
  
74
      end
  
75
      n = n + 1
  
76
    end
  
77
    if n ~= #val then
  
78
      error("invalid table: sparse array")
  
79
    end
  
80
    -- Encode
  
81
    for i, v in ipairs(val) do
  
82
      table.insert(res, encode(v, stack))
  
83
    end
  
84
    stack[val] = nil
  
85
    return "[" .. table.concat(res, ",") .. "]"
  
86
  
  
87
  else
  
88
    -- Treat as an object
  
89
    for k, v in pairs(val) do
  
90
      if type(k) ~= "string" then
  
91
        error("invalid table: mixed or invalid key types")
  
92
      end
  
93
      table.insert(res, encode(k, stack) .. ":" .. encode(v, stack))
  
94
    end
  
95
    stack[val] = nil
  
96
    return "{" .. table.concat(res, ",") .. "}"
  
97
  end
  
98
end
  
99
  
  
100
  
  
101
local function encode_string(val)
  
102
  return '"' .. val:gsub('[%z\1-\31\\"]', escape_char) .. '"'
  
103
end
  
104
  
  
105
  
  
106
local function encode_number(val)
  
107
  -- Check for NaN, -inf and inf
  
108
  if val ~= val or val <= -math.huge or val >= math.huge then
  
109
    error("unexpected number value '" .. tostring(val) .. "'")
  
110
  end
  
111
  return string.format("%.14g", val)
  
112
end
  
113
  
  
114
  
  
115
local type_func_map = {
  
116
  [ "nil"     ] = encode_nil,
  
117
  [ "table"   ] = encode_table,
  
118
  [ "string"  ] = encode_string,
  
119
  [ "number"  ] = encode_number,
  
120
  [ "boolean" ] = tostring,
  
121
}
  
122
  
  
123
  
  
124
encode = function(val, stack)
  
125
  local t = type(val)
  
126
  local f = type_func_map[t]
  
127
  if f then
  
128
    return f(val, stack)
  
129
  end
  
130
  error("unexpected type '" .. t .. "'")
  
131
end
  
132
  
  
133
  
  
134
function json.encode(val)
  
135
  return ( encode(val) )
  
136
end
  
137
  
  
138
  
  
139
-------------------------------------------------------------------------------
  
140
-- Decode
  
141
-------------------------------------------------------------------------------
  
142
  
  
143
local parse
  
144
  
  
145
local function create_set(...)
  
146
  local res = {}
  
147
  for i = 1, select("#", ...) do
  
148
    res[ select(i, ...) ] = true
  
149
  end
  
150
  return res
  
151
end
  
152
  
  
153
local space_chars   = create_set(" ", "\t", "\r", "\n")
  
154
local delim_chars   = create_set(" ", "\t", "\r", "\n", "]", "}", ",")
  
155
local escape_chars  = create_set("\\", "/", '"', "b", "f", "n", "r", "t", "u")
  
156
local literals      = create_set("true", "false", "null")
  
157
  
  
158
local literal_map = {
  
159
  [ "true"  ] = true,
  
160
  [ "false" ] = false,
  
161
  [ "null"  ] = nil,
  
162
}
  
163
  
  
164
  
  
165
local function next_char(str, idx, set, negate)
  
166
  for i = idx, #str do
  
167
    if set[str:sub(i, i)] ~= negate then
  
168
      return i
  
169
    end
  
170
  end
  
171
  return #str + 1
  
172
end
  
173
  
  
174
  
  
175
local function decode_error(str, idx, msg)
  
176
  local line_count = 1
  
177
  local col_count = 1
  
178
  for i = 1, idx - 1 do
  
179
    col_count = col_count + 1
  
180
    if str:sub(i, i) == "\n" then
  
181
      line_count = line_count + 1
  
182
      col_count = 1
  
183
    end
  
184
  end
  
185
  error( string.format("%s at line %d col %d", msg, line_count, col_count) )
  
186
end
  
187
  
  
188
  
  
189
local function codepoint_to_utf8(n)
  
190
  -- http://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=iws-appendixa
  
191
  local f = math.floor
  
192
  if n <= 0x7f then
  
193
    return string.char(n)
  
194
  elseif n <= 0x7ff then
  
195
    return string.char(f(n / 64) + 192, n % 64 + 128)
  
196
  elseif n <= 0xffff then
  
197
    return string.char(f(n / 4096) + 224, f(n % 4096 / 64) + 128, n % 64 + 128)
  
198
  elseif n <= 0x10ffff then
  
199
    return string.char(f(n / 262144) + 240, f(n % 262144 / 4096) + 128,
  
200
                       f(n % 4096 / 64) + 128, n % 64 + 128)
  
201
  end
  
202
  error( string.format("invalid unicode codepoint '%x'", n) )
  
203
end
  
204
  
  
205
  
  
206
local function parse_unicode_escape(s)
  
207
  local n1 = tonumber( s:sub(1, 4),  16 )
  
208
  local n2 = tonumber( s:sub(7, 10), 16 )
  
209
   -- Surrogate pair?
  
210
  if n2 then
  
211
    return codepoint_to_utf8((n1 - 0xd800) * 0x400 + (n2 - 0xdc00) + 0x10000)
  
212
  else
  
213
    return codepoint_to_utf8(n1)
  
214
  end
  
215
end
  
216
  
  
217
  
  
218
local function parse_string(str, i)
  
219
  local res = ""
  
220
  local j = i + 1
  
221
  local k = j
  
222
  
  
223
  while j <= #str do
  
224
    local x = str:byte(j)
  
225
  
  
226
    if x < 32 then
  
227
      decode_error(str, j, "control character in string")
  
228
  
  
229
    elseif x == 92 then -- `\`: Escape
  
230
      res = res .. str:sub(k, j - 1)
  
231
      j = j + 1
  
232
      local c = str:sub(j, j)
  
233
      if c == "u" then
  
234
        local hex = str:match("^[dD][89aAbB]%x%x\\u%x%x%x%x", j + 1)
  
235
                 or str:match("^%x%x%x%x", j + 1)
  
236
                 or decode_error(str, j - 1, "invalid unicode escape in string")
  
237
        res = res .. parse_unicode_escape(hex)
  
238
        j = j + #hex
  
239
      else
  
240
        if not escape_chars[c] then
  
241
          decode_error(str, j - 1, "invalid escape char '" .. c .. "' in string")
  
242
        end
  
243
        res = res .. escape_char_map_inv[c]
  
244
      end
  
245
      k = j + 1
  
246
  
  
247
    elseif x == 34 then -- `"`: End of string
  
248
      res = res .. str:sub(k, j - 1)
  
249
      return res, j + 1
  
250
    end
  
251
  
  
252
    j = j + 1
  
253
  end
  
254
  
  
255
  decode_error(str, i, "expected closing quote for string")
  
256
end
  
257
  
  
258
  
  
259
local function parse_number(str, i)
  
260
  local x = next_char(str, i, delim_chars)
  
261
  local s = str:sub(i, x - 1)
  
262
  local n = tonumber(s)
  
263
  if not n then
  
264
    decode_error(str, i, "invalid number '" .. s .. "'")
  
265
  end
  
266
  return n, x
  
267
end
  
268
  
  
269
  
  
270
local function parse_literal(str, i)
  
271
  local x = next_char(str, i, delim_chars)
  
272
  local word = str:sub(i, x - 1)
  
273
  if not literals[word] then
  
274
    decode_error(str, i, "invalid literal '" .. word .. "'")
  
275
  end
  
276
  return literal_map[word], x
  
277
end
  
278
  
  
279
  
  
280
local function parse_array(str, i)
  
281
  local res = {}
  
282
  local n = 1
  
283
  i = i + 1
  
284
  while 1 do
  
285
    local x
  
286
    i = next_char(str, i, space_chars, true)
  
287
    -- Empty / end of array?
  
288
    if str:sub(i, i) == "]" then
  
289
      i = i + 1
  
290
      break
  
291
    end
  
292
    -- Read token
  
293
    x, i = parse(str, i)
  
294
    res[n] = x
  
295
    n = n + 1
  
296
    -- Next token
  
297
    i = next_char(str, i, space_chars, true)
  
298
    local chr = str:sub(i, i)
  
299
    i = i + 1
  
300
    if chr == "]" then break end
  
301
    if chr ~= "," then decode_error(str, i, "expected ']' or ','") end
  
302
  end
  
303
  return res, i
  
304
end
  
305
  
  
306
  
  
307
local function parse_object(str, i)
  
308
  local res = {}
  
309
  i = i + 1
  
310
  while 1 do
  
311
    local key, val
  
312
    i = next_char(str, i, space_chars, true)
  
313
    -- Empty / end of object?
  
314
    if str:sub(i, i) == "}" then
  
315
      i = i + 1
  
316
      break
  
317
    end
  
318
    -- Read key
  
319
    if str:sub(i, i) ~= '"' then
  
320
      decode_error(str, i, "expected string for key")
  
321
    end
  
322
    key, i = parse(str, i)
  
323
    -- Read ':' delimiter
  
324
    i = next_char(str, i, space_chars, true)
  
325
    if str:sub(i, i) ~= ":" then
  
326
      decode_error(str, i, "expected ':' after key")
  
327
    end
  
328
    i = next_char(str, i + 1, space_chars, true)
  
329
    -- Read value
  
330
    val, i = parse(str, i)
  
331
    -- Set
  
332
    res[key] = val
  
333
    -- Next token
  
334
    i = next_char(str, i, space_chars, true)
  
335
    local chr = str:sub(i, i)
  
336
    i = i + 1
  
337
    if chr == "}" then break end
  
338
    if chr ~= "," then decode_error(str, i, "expected '}' or ','") end
  
339
  end
  
340
  return res, i
  
341
end
  
342
  
  
343
  
  
344
local char_func_map = {
  
345
  [ '"' ] = parse_string,
  
346
  [ "0" ] = parse_number,
  
347
  [ "1" ] = parse_number,
  
348
  [ "2" ] = parse_number,
  
349
  [ "3" ] = parse_number,
  
350
  [ "4" ] = parse_number,
  
351
  [ "5" ] = parse_number,
  
352
  [ "6" ] = parse_number,
  
353
  [ "7" ] = parse_number,
  
354
  [ "8" ] = parse_number,
  
355
  [ "9" ] = parse_number,
  
356
  [ "-" ] = parse_number,
  
357
  [ "t" ] = parse_literal,
  
358
  [ "f" ] = parse_literal,
  
359
  [ "n" ] = parse_literal,
  
360
  [ "[" ] = parse_array,
  
361
  [ "{" ] = parse_object,
  
362
}
  
363
  
  
364
  
  
365
parse = function(str, idx)
  
366
  local chr = str:sub(idx, idx)
  
367
  local f = char_func_map[chr]
  
368
  if f then
  
369
    return f(str, idx)
  
370
  end
  
371
  decode_error(str, idx, "unexpected character '" .. chr .. "'")
  
372
end
  
373
  
  
374
  
  
375
function json.decode(str)
  
376
  if type(str) ~= "string" then
  
377
    error("expected argument of type string, got " .. type(str))
  
378
  end
  
379
  local res, idx = parse(str, next_char(str, 1, space_chars, true))
  
380
  idx = next_char(str, idx, space_chars, true)
  
381
  if idx <= #str then
  
382
    decode_error(str, idx, "trailing garbage")
  
383
  end
  
384
  return res
  
385
end
  
386
  
  
387
  
  
388
return json