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