util-jsn.lua /size: 15 Kb    last modification: 2020-07-01 14:35
1if not modules then modules = { } end modules ['util-jsn'] = {
2    version   = 1.001,
3    comment   = "companion to m-json.mkiv",
4    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
5    copyright = "PRAGMA ADE / ConTeXt Development Team",
6    license   = "see context related readme files"
7}
8
9-- Of course we could make a nice complete parser with proper error messages but
10-- as json is generated programmatically errors are systematic and we can assume
11-- a correct stream. If not, we have some fatal error anyway. So, we can just rely
12-- on strings being strings (apart from the unicode escape which is not in 5.1) and
13-- as we first catch known types we just assume that anything else is a number.
14--
15-- Reminder for me: check usage in framework and extend when needed. Also document
16-- it in the cld lib documentation.
17--
18-- Upgraded for handling the somewhat more fax server templates.
19
20local 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
21local lpegmatch = lpeg.match
22local format, gsub = string.format, string.gsub
23local formatters = string.formatters
24local utfchar = utf.char
25local concat, sortedkeys = table.concat, table.sortedkeys
26
27local tonumber, tostring, rawset, type, next = tonumber, tostring, rawset, type, next
28
29local json      = utilities.json or { }
30utilities.json  = json
31
32do
33
34    -- \\ \/ \b \f \n \r \t \uHHHH
35
36    local lbrace     = P("{")
37    local rbrace     = P("}")
38    local lparent    = P("[")
39    local rparent    = P("]")
40    local comma      = P(",")
41    local colon      = P(":")
42    local dquote     = P('"')
43
44    local whitespace = lpeg.patterns.whitespace
45    local optionalws = whitespace^0
46
47    local escapes    = {
48        ["b"] = "\010",
49        ["f"] = "\014",
50        ["n"] = "\n",
51        ["r"] = "\r",
52        ["t"] = "\t",
53    }
54
55    -- todo: also handle larger utf16
56
57    local escape_un  = P("\\u")/"" * (C(R("09","AF","af")^-4) / function(s)
58        return utfchar(tonumber(s,16))
59    end)
60
61    local escape_bs  = P([[\]]) / "" * (P(1) / escapes) -- if not found then P(1) is returned i.e. the to be escaped char
62
63    local jstring    = dquote * Cs((escape_un + escape_bs + (1-dquote))^0) * dquote
64    local jtrue      = P("true")  * Cc(true)
65    local jfalse     = P("false") * Cc(false)
66    local jnull      = P("null")  * Cc(nil)
67    local jnumber    = (1-whitespace-rparent-rbrace-comma)^1 / tonumber
68
69    local key        = jstring
70
71    local jsonconverter = { "value",
72        hash  = lbrace * Cf(Ct("") * (V("pair") * (comma * V("pair"))^0 + optionalws),rawset) * rbrace,
73        pair  = Cg(optionalws * key * optionalws * colon * V("value")),
74        array = Ct(lparent * (V("value") * (comma * V("value"))^0 + optionalws) * rparent),
75    --  value = optionalws * (jstring + V("hash") + V("array") + jtrue + jfalse + jnull + jnumber + #rparent) * optionalws,
76        value = optionalws * (jstring + V("hash") + V("array") + jtrue + jfalse + jnull + jnumber) * optionalws,
77    }
78
79    -- local jsonconverter = { "value",
80    --     hash   = lbrace * Cf(Ct("") * (V("pair") * (comma * V("pair"))^0 + optionalws),rawset) * rbrace,
81    --     pair   = Cg(optionalws * V("string") * optionalws * colon * V("value")),
82    --     array  = Ct(lparent * (V("value") * (comma * V("value"))^0 + optionalws) * rparent),
83    --     string = jstring,
84    --     value  = optionalws * (V("string") + V("hash") + V("array") + jtrue + jfalse + jnull + jnumber) * optionalws,
85    -- }
86
87    -- lpeg.print(jsonconverter) -- size 181
88
89    function json.tolua(str)
90        return lpegmatch(jsonconverter,str)
91    end
92
93    function json.load(filename)
94        local data = io.loaddata(filename)
95        if data then
96            return lpegmatch(jsonconverter,data)
97        end
98    end
99
100end
101
102do
103
104    -- It's pretty bad that JSON doesn't allow the trailing comma ... it's a
105    -- typical example of a spec that then forces all generators to check for
106    -- this. It's a way to make sure programmers keep jobs.
107
108    local escaper
109
110    local f_start_hash      = formatters[         '%w{' ]
111    local f_start_array     = formatters[         '%w[' ]
112    local f_start_hash_new  = formatters[ "\n" .. '%w{' ]
113    local f_start_array_new = formatters[ "\n" .. '%w[' ]
114    local f_start_hash_key  = formatters[ "\n" .. '%w"%s" : {' ]
115    local f_start_array_key = formatters[ "\n" .. '%w"%s" : [' ]
116
117    local f_stop_hash       = formatters[ "\n" .. '%w}' ]
118    local f_stop_array      = formatters[ "\n" .. '%w]' ]
119
120    local f_key_val_seq     = formatters[ "\n" .. '%w"%s" : %s'    ]
121    local f_key_val_str     = formatters[ "\n" .. '%w"%s" : "%s"'  ]
122    local f_key_val_num     = f_key_val_seq
123    local f_key_val_yes     = formatters[ "\n" .. '%w"%s" : true'  ]
124    local f_key_val_nop     = formatters[ "\n" .. '%w"%s" : false' ]
125    local f_key_val_null    = formatters[ "\n" .. '%w"%s" : null'  ]
126
127    local f_val_num         = formatters[ "\n" .. '%w%s'    ]
128    local f_val_str         = formatters[ "\n" .. '%w"%s"'  ]
129    local f_val_yes         = formatters[ "\n" .. '%wtrue'  ]
130    local f_val_nop         = formatters[ "\n" .. '%wfalse' ]
131    local f_val_null        = formatters[ "\n" .. '%wnull'  ]
132    local f_val_empty       = formatters[ "\n" .. '%w{ }'  ]
133    local f_val_seq         = f_val_num
134
135    -- no empty tables because unknown if table or hash
136
137    local t = { }
138    local n = 0
139
140    local function is_simple_table(tt) -- also used in util-tab so maybe public
141        local l = #tt
142        if l > 0 then
143            for i=1,l do
144                if type(tt[i]) == "table" then
145                    return false
146                end
147            end
148            local nn = n
149            n = n + 1 t[n] = "[ "
150            for i=1,l do
151                if i > 1 then
152                    n = n + 1 t[n] = ", "
153                end
154                local v = tt[i]
155                local tv = type(v)
156                if tv == "number" then
157                    n = n + 1 t[n] = v
158                elseif tv == "string" then
159                    n = n + 1 t[n] = '"'
160                    n = n + 1 t[n] = lpegmatch(escaper,v) or v
161                    n = n + 1 t[n] = '"'
162                elseif tv == "boolean" then
163                    n = n + 1 t[n] = v and "true" or "false"
164                elseif v then
165                    n = n + 1 t[n] = tostring(v)
166                else
167                    n = n + 1 t[n] = "null"
168                end
169            end
170            n = n + 1 t[n] = " ]"
171            local s = concat(t,"",nn+1,n)
172            n = nn
173            return s
174        end
175        return false
176    end
177
178    local function tojsonpp(root,name,depth,level,size)
179        if root then
180            local indexed = size > 0
181            n = n + 1
182            if level == 0 then
183                if indexed then
184                    t[n] = f_start_array(depth)
185                else
186                    t[n] = f_start_hash(depth)
187                end
188            elseif name then
189                if tn == "string" then
190                    name = lpegmatch(escaper,name) or name
191                elseif tn ~= "number" then
192                    name = tostring(name)
193                end
194                if indexed then
195                    t[n] = f_start_array_key(depth,name)
196                else
197                    t[n] = f_start_hash_key(depth,name)
198                end
199            else
200                if indexed then
201                    t[n] = f_start_array_new(depth)
202                else
203                    t[n] = f_start_hash_new(depth)
204                end
205            end
206            depth = depth + 1
207            if indexed then -- indexed
208                for i=1,size do
209                    if i > 1 then
210                        n = n + 1 t[n] = ","
211                    end
212                    local v  = root[i]
213                    local tv = type(v)
214                    if tv == "number" then
215                        n = n + 1 t[n] = f_val_num(depth,v)
216                    elseif tv == "string" then
217                        v = lpegmatch(escaper,v) or v
218                        n = n + 1 t[n] = f_val_str(depth,v)
219                    elseif tv == "table" then
220                        if next(v) then
221                            local st = is_simple_table(v)
222                            if st then
223                                n = n + 1 t[n] = f_val_seq(depth,st)
224                            else
225                                tojsonpp(v,nil,depth,level+1,#v)
226                            end
227                        else
228                            n = n + 1
229                            t[n] = f_val_empty(depth)
230                        end
231                    elseif tv == "boolean" then
232                        n = n + 1
233                        if v then
234                            t[n] = f_val_yes(depth,v)
235                        else
236                            t[n] = f_val_nop(depth,v)
237                        end
238                    else
239                        n = n + 1
240                        t[n] = f_val_null(depth)
241                    end
242                end
243            elseif next(root) then
244                local sk = sortedkeys(root)
245                for i=1,#sk do
246                    if i > 1 then
247                        n = n + 1 t[n] = ","
248                    end
249                    local k  = sk[i]
250                    local v  = root[k]
251                    local tv = type(v)
252                    local tk = type(k)
253                    if tv == "number" then
254                        if tk == "number" then
255                            n = n + 1 t[n] = f_key_val_num(depth,k,v)
256                        elseif tk == "string" then
257                            k = lpegmatch(escaper,k) or k
258                            n = n + 1 t[n] = f_key_val_num(depth,k,v)
259                        end
260                    elseif tv == "string" then
261                        if tk == "number" then
262                            v = lpegmatch(escaper,v) or v
263                            n = n + 1 t[n] = f_key_val_str(depth,k,v)
264                        elseif tk == "string" then
265                            k = lpegmatch(escaper,k) or k
266                            v = lpegmatch(escaper,v) or v
267                            n = n + 1 t[n] = f_key_val_str(depth,k,v)
268                        end
269                    elseif tv == "table" then
270                        local l = #v
271                        if l > 0 then
272                            local st = is_simple_table(v)
273                            if not st then
274                                tojsonpp(v,k,depth,level+1,l)
275                            elseif tk == "number" then
276                                n = n + 1 t[n] = f_key_val_seq(depth,k,st)
277                            elseif tk == "string" then
278                                k = lpegmatch(escaper,k) or k
279                                n = n + 1 t[n] = f_key_val_seq(depth,k,st)
280                            end
281                        elseif next(v) then
282                            tojsonpp(v,k,depth,level+1,0)
283                        end
284                    elseif tv == "boolean" then
285                        if tk == "number" then
286                            n = n + 1
287                            if v then
288                                t[n] = f_key_val_yes(depth,k)
289                            else
290                                t[n] = f_key_val_nop(depth,k)
291                            end
292                        elseif tk == "string" then
293                            k = lpegmatch(escaper,k) or k
294                            n = n + 1
295                            if v then
296                                t[n] = f_key_val_yes(depth,k)
297                            else
298                                t[n] = f_key_val_nop(depth,k)
299                            end
300                        end
301                    else
302                        if tk == "number" then
303                            n = n + 1
304                            t[n] = f_key_val_null(depth,k)
305                        elseif tk == "string" then
306                            k = lpegmatch(escaper,k) or k
307                            n = n + 1
308                            t[n] = f_key_val_null(depth,k)
309                        end
310                    end
311                end
312            end
313            n = n + 1
314            if indexed then
315                t[n] = f_stop_array(depth-1)
316            else
317                t[n] = f_stop_hash(depth-1)
318            end
319        end
320    end
321
322    local function tojson(value,n)
323        local kind = type(value)
324        if kind == "table" then
325            local done = false
326            local size = #value
327            if size == 0 then
328                for k, v in next, value do
329                    if done then
330                     -- n = n + 1 ; t[n] = ","
331                        n = n + 1 ; t[n] = ',"'
332                    else
333                     -- n = n + 1 ; t[n] = "{"
334                        n = n + 1 ; t[n] = '{"'
335                        done = true
336                    end
337                    n = n + 1 ; t[n] = lpegmatch(escaper,k) or k
338                    n = n + 1 ; t[n] = '":'
339                    t, n = tojson(v,n)
340                end
341                if done then
342                    n = n + 1 ; t[n] = "}"
343                else
344                    n = n + 1 ; t[n] = "{}"
345                end
346            elseif size == 1 then
347                -- we can optimize for non tables
348                n = n + 1 ; t[n] = "["
349                t, n = tojson(value[1],n)
350                n = n + 1 ; t[n] = "]"
351            else
352                for i=1,size do
353                    if done then
354                        n = n + 1 ; t[n] = ","
355                    else
356                        n = n + 1 ; t[n] = "["
357                        done = true
358                    end
359                    t, n = tojson(value[i],n)
360                end
361                n = n + 1 ; t[n] = "]"
362            end
363        elseif kind == "string"  then
364            n = n + 1 ; t[n] = '"'
365            n = n + 1 ; t[n] = lpegmatch(escaper,value) or value
366            n = n + 1 ; t[n] = '"'
367        elseif kind == "number" then
368            n = n + 1 ; t[n] = value
369        elseif kind == "boolean" then
370            n = n + 1 ; t[n] = tostring(value)
371        else
372            n = n + 1 ; t[n] = "null"
373        end
374        return t, n
375    end
376
377    -- escaping keys can become an option
378
379    local function jsontostring(value,pretty)
380        -- todo optimize for non table
381        local kind = type(value)
382        if kind == "table" then
383            if not escaper then
384                local escapes = {
385                    ["\\"] = "\\u005C",
386                    ["\""] = "\\u0022",
387                }
388                for i=0,0x1F do
389                    escapes[utfchar(i)] = format("\\u%04X",i)
390                end
391                escaper = Cs( (
392                    (R('\0\x20') + S('\"\\')) / escapes
393                  + P(1)
394                )^1 )
395
396            end
397            -- local to the closure (saves wrapping and local functions)
398            t = { }
399            n = 0
400            if pretty then
401                tojsonpp(value,name,0,0,#value)
402                value = concat(t,"",1,n)
403            else
404                t, n = tojson(value,0)
405                value = concat(t,"",1,n)
406            end
407            t = nil
408            n = 0
409            return value
410        elseif kind == "string" or kind == "number" then
411            return lpegmatch(escaper,value) or value
412        else
413            return tostring(value)
414        end
415    end
416
417    json.tostring = jsontostring
418
419    function json.tojson(value)
420        return jsontostring(value,true)
421    end
422
423end
424
425-- local tmp = [[ { "t\nt t" : "foo bar", "a" : true, "b" : [ 123 , 456E-10, { "a" : true, "b" : [ 123 , 456 ] } ] } ]]
426-- tmp = json.tolua(tmp)
427-- inspect(tmp)
428-- tmp = json.tostring(tmp,true)
429-- inspect(tmp)
430-- tmp = json.tolua(tmp)
431-- inspect(tmp)
432-- tmp = json.tostring(tmp)
433-- inspect(tmp)
434-- inspect(json.tostring(true))
435
436-- local s = [[\foo"bar"]]
437-- local j = json.tostring { s = s }
438-- local l = json.tolua(j)
439-- inspect(j)
440-- inspect(l)
441-- print(s==l.s)
442
443return json
444