You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
443 lines
16 KiB
443 lines
16 KiB
if not modules then modules = { } end modules ['util-jsn'] = {
|
|
version = 1.001,
|
|
comment = "companion to m-json.mkiv",
|
|
author = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
|
|
copyright = "PRAGMA ADE / ConTeXt Development Team",
|
|
license = "see context related readme files"
|
|
}
|
|
|
|
-- Of course we could make a nice complete parser with proper error messages but
|
|
-- as json is generated programmatically errors are systematic and we can assume
|
|
-- a correct stream. If not, we have some fatal error anyway. So, we can just rely
|
|
-- on strings being strings (apart from the unicode escape which is not in 5.1) and
|
|
-- as we first catch known types we just assume that anything else is a number.
|
|
--
|
|
-- Reminder for me: check usage in framework and extend when needed. Also document
|
|
-- it in the cld lib documentation.
|
|
--
|
|
-- Upgraded for handling the somewhat more fax server templates.
|
|
|
|
local P, V, R, S, C, Cc, Cs, Ct, Cf, Cg = lpeg.P, lpeg.V, lpeg.R, lpeg.S, lpeg.C, lpeg.Cc, lpeg.Cs, lpeg.Ct, lpeg.Cf, lpeg.Cg
|
|
local lpegmatch = lpeg.match
|
|
local format, gsub = string.format, string.gsub
|
|
local formatters = string.formatters
|
|
local utfchar = utf.char
|
|
local concat, sortedkeys = table.concat, table.sortedkeys
|
|
|
|
local tonumber, tostring, rawset, type, next = tonumber, tostring, rawset, type, next
|
|
|
|
local json = utilities.json or { }
|
|
utilities.json = json
|
|
|
|
do
|
|
|
|
-- \\ \/ \b \f \n \r \t \uHHHH
|
|
|
|
local lbrace = P("{")
|
|
local rbrace = P("}")
|
|
local lparent = P("[")
|
|
local rparent = P("]")
|
|
local comma = P(",")
|
|
local colon = P(":")
|
|
local dquote = P('"')
|
|
|
|
local whitespace = lpeg.patterns.whitespace
|
|
local optionalws = whitespace^0
|
|
|
|
local escapes = {
|
|
["b"] = "\010",
|
|
["f"] = "\014",
|
|
["n"] = "\n",
|
|
["r"] = "\r",
|
|
["t"] = "\t",
|
|
}
|
|
|
|
-- todo: also handle larger utf16
|
|
|
|
local escape_un = P("\\u")/"" * (C(R("09","AF","af")^-4) / function(s)
|
|
return utfchar(tonumber(s,16))
|
|
end)
|
|
|
|
local escape_bs = P([[\]]) / "" * (P(1) / escapes) -- if not found then P(1) is returned i.e. the to be escaped char
|
|
|
|
local jstring = dquote * Cs((escape_un + escape_bs + (1-dquote))^0) * dquote
|
|
local jtrue = P("true") * Cc(true)
|
|
local jfalse = P("false") * Cc(false)
|
|
local jnull = P("null") * Cc(nil)
|
|
local jnumber = (1-whitespace-rparent-rbrace-comma)^1 / tonumber
|
|
|
|
local key = jstring
|
|
|
|
local jsonconverter = { "value",
|
|
hash = lbrace * Cf(Ct("") * (V("pair") * (comma * V("pair"))^0 + optionalws),rawset) * rbrace,
|
|
pair = Cg(optionalws * key * optionalws * colon * V("value")),
|
|
array = Ct(lparent * (V("value") * (comma * V("value"))^0 + optionalws) * rparent),
|
|
-- value = optionalws * (jstring + V("hash") + V("array") + jtrue + jfalse + jnull + jnumber + #rparent) * optionalws,
|
|
value = optionalws * (jstring + V("hash") + V("array") + jtrue + jfalse + jnull + jnumber) * optionalws,
|
|
}
|
|
|
|
-- local jsonconverter = { "value",
|
|
-- hash = lbrace * Cf(Ct("") * (V("pair") * (comma * V("pair"))^0 + optionalws),rawset) * rbrace,
|
|
-- pair = Cg(optionalws * V("string") * optionalws * colon * V("value")),
|
|
-- array = Ct(lparent * (V("value") * (comma * V("value"))^0 + optionalws) * rparent),
|
|
-- string = jstring,
|
|
-- value = optionalws * (V("string") + V("hash") + V("array") + jtrue + jfalse + jnull + jnumber) * optionalws,
|
|
-- }
|
|
|
|
-- lpeg.print(jsonconverter) -- size 181
|
|
|
|
function json.tolua(str)
|
|
return lpegmatch(jsonconverter,str)
|
|
end
|
|
|
|
function json.load(filename)
|
|
local data = io.loaddata(filename)
|
|
if data then
|
|
return lpegmatch(jsonconverter,data)
|
|
end
|
|
end
|
|
|
|
end
|
|
|
|
do
|
|
|
|
-- It's pretty bad that JSON doesn't allow the trailing comma ... it's a
|
|
-- typical example of a spec that then forces all generators to check for
|
|
-- this. It's a way to make sure programmers keep jobs.
|
|
|
|
local escaper
|
|
|
|
local f_start_hash = formatters[ '%w{' ]
|
|
local f_start_array = formatters[ '%w[' ]
|
|
local f_start_hash_new = formatters[ "\n" .. '%w{' ]
|
|
local f_start_array_new = formatters[ "\n" .. '%w[' ]
|
|
local f_start_hash_key = formatters[ "\n" .. '%w"%s" : {' ]
|
|
local f_start_array_key = formatters[ "\n" .. '%w"%s" : [' ]
|
|
|
|
local f_stop_hash = formatters[ "\n" .. '%w}' ]
|
|
local f_stop_array = formatters[ "\n" .. '%w]' ]
|
|
|
|
local f_key_val_seq = formatters[ "\n" .. '%w"%s" : %s' ]
|
|
local f_key_val_str = formatters[ "\n" .. '%w"%s" : "%s"' ]
|
|
local f_key_val_num = f_key_val_seq
|
|
local f_key_val_yes = formatters[ "\n" .. '%w"%s" : true' ]
|
|
local f_key_val_nop = formatters[ "\n" .. '%w"%s" : false' ]
|
|
local f_key_val_null = formatters[ "\n" .. '%w"%s" : null' ]
|
|
|
|
local f_val_num = formatters[ "\n" .. '%w%s' ]
|
|
local f_val_str = formatters[ "\n" .. '%w"%s"' ]
|
|
local f_val_yes = formatters[ "\n" .. '%wtrue' ]
|
|
local f_val_nop = formatters[ "\n" .. '%wfalse' ]
|
|
local f_val_null = formatters[ "\n" .. '%wnull' ]
|
|
local f_val_empty = formatters[ "\n" .. '%w{ }' ]
|
|
local f_val_seq = f_val_num
|
|
|
|
-- no empty tables because unknown if table or hash
|
|
|
|
local t = { }
|
|
local n = 0
|
|
|
|
local function is_simple_table(tt) -- also used in util-tab so maybe public
|
|
local l = #tt
|
|
if l > 0 then
|
|
for i=1,l do
|
|
if type(tt[i]) == "table" then
|
|
return false
|
|
end
|
|
end
|
|
local nn = n
|
|
n = n + 1 t[n] = "[ "
|
|
for i=1,l do
|
|
if i > 1 then
|
|
n = n + 1 t[n] = ", "
|
|
end
|
|
local v = tt[i]
|
|
local tv = type(v)
|
|
if tv == "number" then
|
|
n = n + 1 t[n] = v
|
|
elseif tv == "string" then
|
|
n = n + 1 t[n] = '"'
|
|
n = n + 1 t[n] = lpegmatch(escaper,v) or v
|
|
n = n + 1 t[n] = '"'
|
|
elseif tv == "boolean" then
|
|
n = n + 1 t[n] = v and "true" or "false"
|
|
elseif v then
|
|
n = n + 1 t[n] = tostring(v)
|
|
else
|
|
n = n + 1 t[n] = "null"
|
|
end
|
|
end
|
|
n = n + 1 t[n] = " ]"
|
|
local s = concat(t,"",nn+1,n)
|
|
n = nn
|
|
return s
|
|
end
|
|
return false
|
|
end
|
|
|
|
local function tojsonpp(root,name,depth,level,size)
|
|
if root then
|
|
local indexed = size > 0
|
|
n = n + 1
|
|
if level == 0 then
|
|
if indexed then
|
|
t[n] = f_start_array(depth)
|
|
else
|
|
t[n] = f_start_hash(depth)
|
|
end
|
|
elseif name then
|
|
if tn == "string" then
|
|
name = lpegmatch(escaper,name) or name
|
|
elseif tn ~= "number" then
|
|
name = tostring(name)
|
|
end
|
|
if indexed then
|
|
t[n] = f_start_array_key(depth,name)
|
|
else
|
|
t[n] = f_start_hash_key(depth,name)
|
|
end
|
|
else
|
|
if indexed then
|
|
t[n] = f_start_array_new(depth)
|
|
else
|
|
t[n] = f_start_hash_new(depth)
|
|
end
|
|
end
|
|
depth = depth + 1
|
|
if indexed then -- indexed
|
|
for i=1,size do
|
|
if i > 1 then
|
|
n = n + 1 t[n] = ","
|
|
end
|
|
local v = root[i]
|
|
local tv = type(v)
|
|
if tv == "number" then
|
|
n = n + 1 t[n] = f_val_num(depth,v)
|
|
elseif tv == "string" then
|
|
v = lpegmatch(escaper,v) or v
|
|
n = n + 1 t[n] = f_val_str(depth,v)
|
|
elseif tv == "table" then
|
|
if next(v) then
|
|
local st = is_simple_table(v)
|
|
if st then
|
|
n = n + 1 t[n] = f_val_seq(depth,st)
|
|
else
|
|
tojsonpp(v,nil,depth,level+1,#v)
|
|
end
|
|
else
|
|
n = n + 1
|
|
t[n] = f_val_empty(depth)
|
|
end
|
|
elseif tv == "boolean" then
|
|
n = n + 1
|
|
if v then
|
|
t[n] = f_val_yes(depth,v)
|
|
else
|
|
t[n] = f_val_nop(depth,v)
|
|
end
|
|
else
|
|
n = n + 1
|
|
t[n] = f_val_null(depth)
|
|
end
|
|
end
|
|
elseif next(root) then
|
|
local sk = sortedkeys(root)
|
|
for i=1,#sk do
|
|
if i > 1 then
|
|
n = n + 1 t[n] = ","
|
|
end
|
|
local k = sk[i]
|
|
local v = root[k]
|
|
local tv = type(v)
|
|
local tk = type(k)
|
|
if tv == "number" then
|
|
if tk == "number" then
|
|
n = n + 1 t[n] = f_key_val_num(depth,k,v)
|
|
elseif tk == "string" then
|
|
k = lpegmatch(escaper,k) or k
|
|
n = n + 1 t[n] = f_key_val_num(depth,k,v)
|
|
end
|
|
elseif tv == "string" then
|
|
if tk == "number" then
|
|
v = lpegmatch(escaper,v) or v
|
|
n = n + 1 t[n] = f_key_val_str(depth,k,v)
|
|
elseif tk == "string" then
|
|
k = lpegmatch(escaper,k) or k
|
|
v = lpegmatch(escaper,v) or v
|
|
n = n + 1 t[n] = f_key_val_str(depth,k,v)
|
|
end
|
|
elseif tv == "table" then
|
|
local l = #v
|
|
if l > 0 then
|
|
local st = is_simple_table(v)
|
|
if not st then
|
|
tojsonpp(v,k,depth,level+1,l)
|
|
elseif tk == "number" then
|
|
n = n + 1 t[n] = f_key_val_seq(depth,k,st)
|
|
elseif tk == "string" then
|
|
k = lpegmatch(escaper,k) or k
|
|
n = n + 1 t[n] = f_key_val_seq(depth,k,st)
|
|
end
|
|
elseif next(v) then
|
|
tojsonpp(v,k,depth,level+1,0)
|
|
end
|
|
elseif tv == "boolean" then
|
|
if tk == "number" then
|
|
n = n + 1
|
|
if v then
|
|
t[n] = f_key_val_yes(depth,k)
|
|
else
|
|
t[n] = f_key_val_nop(depth,k)
|
|
end
|
|
elseif tk == "string" then
|
|
k = lpegmatch(escaper,k) or k
|
|
n = n + 1
|
|
if v then
|
|
t[n] = f_key_val_yes(depth,k)
|
|
else
|
|
t[n] = f_key_val_nop(depth,k)
|
|
end
|
|
end
|
|
else
|
|
if tk == "number" then
|
|
n = n + 1
|
|
t[n] = f_key_val_null(depth,k)
|
|
elseif tk == "string" then
|
|
k = lpegmatch(escaper,k) or k
|
|
n = n + 1
|
|
t[n] = f_key_val_null(depth,k)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
n = n + 1
|
|
if indexed then
|
|
t[n] = f_stop_array(depth-1)
|
|
else
|
|
t[n] = f_stop_hash(depth-1)
|
|
end
|
|
end
|
|
end
|
|
|
|
local function tojson(value,n)
|
|
local kind = type(value)
|
|
if kind == "table" then
|
|
local done = false
|
|
local size = #value
|
|
if size == 0 then
|
|
for k, v in next, value do
|
|
if done then
|
|
-- n = n + 1 ; t[n] = ","
|
|
n = n + 1 ; t[n] = ',"'
|
|
else
|
|
-- n = n + 1 ; t[n] = "{"
|
|
n = n + 1 ; t[n] = '{"'
|
|
done = true
|
|
end
|
|
n = n + 1 ; t[n] = lpegmatch(escaper,k) or k
|
|
n = n + 1 ; t[n] = '":'
|
|
t, n = tojson(v,n)
|
|
end
|
|
if done then
|
|
n = n + 1 ; t[n] = "}"
|
|
else
|
|
n = n + 1 ; t[n] = "{}"
|
|
end
|
|
elseif size == 1 then
|
|
-- we can optimize for non tables
|
|
n = n + 1 ; t[n] = "["
|
|
t, n = tojson(value[1],n)
|
|
n = n + 1 ; t[n] = "]"
|
|
else
|
|
for i=1,size do
|
|
if done then
|
|
n = n + 1 ; t[n] = ","
|
|
else
|
|
n = n + 1 ; t[n] = "["
|
|
done = true
|
|
end
|
|
t, n = tojson(value[i],n)
|
|
end
|
|
n = n + 1 ; t[n] = "]"
|
|
end
|
|
elseif kind == "string" then
|
|
n = n + 1 ; t[n] = '"'
|
|
n = n + 1 ; t[n] = lpegmatch(escaper,value) or value
|
|
n = n + 1 ; t[n] = '"'
|
|
elseif kind == "number" then
|
|
n = n + 1 ; t[n] = value
|
|
elseif kind == "boolean" then
|
|
n = n + 1 ; t[n] = tostring(value)
|
|
else
|
|
n = n + 1 ; t[n] = "null"
|
|
end
|
|
return t, n
|
|
end
|
|
|
|
-- escaping keys can become an option
|
|
|
|
local function jsontostring(value,pretty)
|
|
-- todo optimize for non table
|
|
local kind = type(value)
|
|
if kind == "table" then
|
|
if not escaper then
|
|
local escapes = {
|
|
["\\"] = "\\u005C",
|
|
["\""] = "\\u0022",
|
|
}
|
|
for i=0,0x1F do
|
|
escapes[utfchar(i)] = format("\\u%04X",i)
|
|
end
|
|
escaper = Cs( (
|
|
(R('\0\x20') + S('\"\\')) / escapes
|
|
+ P(1)
|
|
)^1 )
|
|
|
|
end
|
|
-- local to the closure (saves wrapping and local functions)
|
|
t = { }
|
|
n = 0
|
|
if pretty then
|
|
tojsonpp(value,name,0,0,#value)
|
|
value = concat(t,"",1,n)
|
|
else
|
|
t, n = tojson(value,0)
|
|
value = concat(t,"",1,n)
|
|
end
|
|
t = nil
|
|
n = 0
|
|
return value
|
|
elseif kind == "string" or kind == "number" then
|
|
return lpegmatch(escaper,value) or value
|
|
else
|
|
return tostring(value)
|
|
end
|
|
end
|
|
|
|
json.tostring = jsontostring
|
|
|
|
function json.tojson(value)
|
|
return jsontostring(value,true)
|
|
end
|
|
|
|
end
|
|
|
|
-- local tmp = [[ { "t\nt t" : "foo bar", "a" : true, "b" : [ 123 , 456E-10, { "a" : true, "b" : [ 123 , 456 ] } ] } ]]
|
|
-- tmp = json.tolua(tmp)
|
|
-- inspect(tmp)
|
|
-- tmp = json.tostring(tmp,true)
|
|
-- inspect(tmp)
|
|
-- tmp = json.tolua(tmp)
|
|
-- inspect(tmp)
|
|
-- tmp = json.tostring(tmp)
|
|
-- inspect(tmp)
|
|
-- inspect(json.tostring(true))
|
|
|
|
-- local s = [[\foo"bar"]]
|
|
-- local j = json.tostring { s = s }
|
|
-- local l = json.tolua(j)
|
|
-- inspect(j)
|
|
-- inspect(l)
|
|
-- print(s==l.s)
|
|
|
|
return json
|
|
|