Documentation for this module may be created at Module:JSONutil/doc
local JSONutil = {
item = 63869449,
serial = "2020-11-08",
suite = "JSONutil"
}
--[=[
Preprocess or generate JSON data.
]=]
local Failsafe = JSONutil
JSONutil.Encoder = {
scream = "@error@JSONencoder@",
sep = string.char(10),
stab = string.char(9)
}
JSONutil.more = 50 -- Length of trailing context.
local Fallback = function()
-- Retrieve current default language code.
-- Returns string.
return mw.language.getContentLanguage():getCode():lower()
end -- Fallback()
local flat = function(adjust)
-- Clean template argument string.
-- Parameter:
-- adjust -- string, or not
-- Returns:
-- string
local r
if adjust then
r = mw.text.trim(mw.text.unstripNoWiki(adjust))
else
r = ""
end
return r
end -- flat()
local flip = function(frame)
-- Retrieve template argument indent.
-- Parameter:
-- frame -- object
-- Returns:
-- Number of indentation level, or not.
local r
if frame.args.indent and frame.args.indent:match("^%d+$") then
r = tonumber(frame.args.indent)
end
return r
end -- flip()
JSONutil.Encoder.Array = function(apply, adapt, alert)
-- Convert table to JSON array.
-- Parameter:
-- apply -- table, with sequence of raw elements, or
-- string, with formatted Array, or empty
-- adapt -- string, with requested type, or not
-- alert -- true, if non-numeric elements shall trigger errors
-- Returns:
-- String, with JSON array.
local r = type(apply)
if r == "string" then
r = mw.text.trim(apply)
if r == "" then
r = "[]"
elseif r:byte(1, 1) ~= 0x5B or r:byte(-1, -1) ~= 0x5D then
r = false
end
elseif r == "table" then
local n = 0
local strange
for k, v in pairs(apply) do
if type(k) == "number" then
if k > n then
n = k
end
elseif alert then
if strange then
strange = strange .. " "
else
strange = ""
end
strange = strange .. tostring(k)
end
end -- for k, v
if strange then
r = string.format('{ "%s": "%s" }', JSONutil.Encoder.scream, JSONutil.Encoder.string(strange))
elseif n > 0 then
local sep = ""
local scope = adapt or "string"
local s
if type(JSONutil.Encoder[scope]) ~= "function" then
scope = "string"
end
r = " ]"
for i = n, 1, -1 do
s = JSONutil.Encoder[scope](apply[i])
r = string.format("%s%s%s", s, sep, r)
sep = ",\n "
end -- for i = n, 1, -1
r = "[ " .. r
else
r = "[]"
end
else
r = false
end
if not r then
r = string.format('[ "%s * %s" ]', JSONutil.Encoder.scream, "Bad Array")
end
return r
end -- JSONutil.Encoder.Array()
JSONutil.Encoder.boolean = function(apply)
-- Convert string to JSON boolean.
-- Parameter:
-- apply -- string, with value
-- Returns:
-- Boolean, as string.
local r = mw.text.trim(apply)
if r == "" or r == "null" or r == "false" or r == "0" or r == "-" then
r = "false"
else
r = "true"
end
return r
end -- JSONutil.Encoder.boolean()
JSONutil.Encoder.Component = function(access, apply, adapt, align, alert)
-- Create single entry for mapping object.
-- Parameter:
-- access -- string, with component name
-- apply -- component value
-- adapt -- string, with value type, or not
-- align -- number, of indentation level, or not
-- alert --
-- Returns:
-- String, with JSON fragment and comma.
local v = apply
local types = adapt
local indent, liner, scope, sep, sign
if type(access) == "string" then
sign = mw.text.trim(access)
if sign == "" then
sign = false
end
end
if type(types) == "string" then
types = mw.text.split(mw.text.trim(types), "%s+")
end
if type(types) ~= "table" then
types = {}
table.insert(types, "string")
end
if #types == 1 then
scope = types[1]
else
for i = 1, #types do
if types[i] == "boolean" then
if v == "1" or v == 1 or v == true then
v = "true"
scope = "boolean"
elseif v == "0" or v == 0 or v == false then
v = "false"
scope = "boolean"
end
if scope then
types = {}
break -- for i
else
table.remove(types, i)
end
end
end -- for i
for i = 1, #types do
if types[i] == "number" then
if tonumber(v) then
v = tostring(v)
scope = "number"
types = {}
break -- for i
else
table.remove(types, i)
end
end
end -- for i
end
scope = scope or "string"
if type(JSONutil.Encoder[scope]) ~= "function" then
scope = "string"
elseif scope == "I18N" then
scope = "Polyglott"
end
if scope == "string" then
v = v or ""
end
if type(align) == "number" and align > 0 then
indent = math.floor(align)
if indent == 0 then
indent = false
end
end
if scope == "object" or not sign then
liner = true
elseif scope == "string" then
local k = mw.ustring.len(sign) + mw.ustring.len(v)
if k > 60 then
liner = true
end
end
if liner then
if indent then
sep = "\n" .. string.rep(" ", indent)
else
sep = "\n"
end
else
sep = " "
end
if indent then
indent = indent + 1
end
return string.format(' "%s":%s%s,\n', sign or "???", sep, JSONutil.Encoder[scope](v, indent))
end -- JSONutil.Encoder.Component()
JSONutil.Encoder.Hash = function(apply, adapt, alert)
-- Create entries for mapping object.
-- Parameter:
-- apply -- table, with element value assignments
-- adapt -- table, with value types assignment, or not
-- Returns:
-- String, with JSON fragment and comma.
local r = ""
local s
for k, v in pairs(apply) do
if type(adapt) == "table" then
s = adapt[k]
end
r = r .. JSONutil.Encoder.Component(tostring(k), v, s)
end -- for k, v
return
end -- JSONutil.Encoder.Hash()
JSONutil.Encoder.I18N = function(apply, align)
-- Convert multilingual string table to JSON.
-- Parameter:
-- apply -- table, with mapping object
-- align -- number, of indentation level, or not
-- Returns:
-- String, with JSON object.
local r = type(apply)
if r == "table" then
local strange
local fault = function(a)
if strange then
strange = strange .. " *\n "
else
strange = ""
end
strange = strange .. a
end
local got, sep, indent
for k, v in pairs(apply) do
if type(k) == "string" then
k = mw.text.trim(k)
if type(v) == "string" then
v = mw.text.trim(v)
if v == "" then
fault(string.format("%s %s=", "Empty text", k))
end
if not (k:match("%l%l%l?") or k:match("%l%l%l?-%u%u") or k:match("%l%l%l?-%u%l%l%l+")) then
fault(string.format("%s %s=", "Strange language code", k))
end
else
v = tostring(v)
fault(string.format("%s %s=%s", "Bad type for text", k, type(v)))
end
got = got or {}
got[k] = v
else
fault(string.format("%s %s: %s", "Bad language code type", type(k), tostring(k)))
end
end -- for k, v
if not got then
fault("No language codes")
got = {}
end
if strange then
got[JSONutil.Encoder.scream] = strange
end
r = false
if type(align) == "number" and align > 0 then
indent = math.floor(align)
else
indent = 0
end
sep = string.rep(" ", indent + 1)
for k, v in pairs(got) do
if r then
r = r .. ",\n"
else
r = ""
end
r = string.format("%s %s%s: %s", r, sep, JSONutil.Encoder.string(k), JSONutil.Encoder.string(v))
end -- for k, v
r = string.format("{\n%s\n%s}", r, sep)
elseif r == "string" then
r = JSONutil.Encoder.string(apply)
else
r = string.format('{ "%s": "%s: %s" }', JSONutil.Encoder.scream, "Bad Lua type", r)
end
return r
end -- JSONutil.Encoder.I18N()
JSONutil.Encoder.number = function(apply)
-- Convert string to JSON number.
-- Parameter:
-- apply -- string, with presumable number
-- Returns:
-- Number or "NaN".
local s = mw.text.trim(apply)
JSONutil.Encoder.minus = JSONutil.Encoder.minus or mw.ustring.char(0x2212)
s = s:gsub(JSONutil.Encoder.minus, "-")
return tonumber(s:lower()) or "NaN"
end -- JSONutil.Encoder.number()
JSONutil.Encoder.object = function(apply, align)
-- Create mapping object.
-- Parameter:
-- apply -- string, with components, may end with comma
-- align -- number, of indentation level, or not
-- Returns:
-- String, with JSON fragment.
local story = mw.text.trim(apply)
local start = ""
if story:sub(-1) == "," then
story = story:sub(1, -2)
end
if type(align) == "number" and align > 0 then
local indent = math.floor(align)
if indent > 0 then
start = string.rep(" ", indent)
end
end
return string.format("%s{ %s\n%s}", start, story, start)
end -- JSONutil.Encoder.object()
JSONutil.Encoder.Polyglott = function(apply, align)
-- Convert string or multilingual string table to JSON.
-- Parameter:
-- apply -- string, with string or object
-- align -- number, of indentation level, or not
-- Returns:
-- string
local r = type(apply)
if r == "string" then
r = mw.text.trim(apply)
if not r:match('^{%s*"') or not r:match('"%s*}$') then
r = JSONutil.Encoder.string(r)
end
else
r = string.format('{ "%s": "%s: %s" }', JSONutil.Encoder.scream, "Bad Lua type", r)
end
return r
end -- JSONutil.Encoder.Polyglott()
JSONutil.Encoder.string = function(apply)
-- Convert plain string to strict JSON string.
-- Parameter:
-- apply -- string, with plain string
-- Returns:
-- string, with quoted trimmed JSON string
return string.format(
'"%s"',
mw.text
.trim(apply)
:gsub("\\", "\\\\")
:gsub('"', '\\"')
:gsub(JSONutil.Encoder.sep, "\\n")
:gsub(JSONutil.Encoder.stab, "\\t")
)
end -- JSONutil.Encoder.string()
JSONutil.fair = function(apply)
-- Reduce enhanced JSON data to strict JSON.
-- Parameter:
-- apply -- string, with enhanced JSON
-- Returns:
-- 1 -- string|nil|false, with error keyword
-- 2 -- string, with JSON or context
local m = 0
local n = 0
local s = mw.text.trim(apply)
local i, j, last, r, scan, sep0, sep1, start, stub, suffix
local framework = function(a)
-- Syntax analysis outside strings.
local k = 1
local c
while k do
k = a:find("[{%[%]}]", k)
if k then
c = a:byte(k, k)
if c == 0x7B then -- {
m = m + 1
elseif c == 0x7D then -- }
m = m - 1
elseif c == 0x5B then -- [
n = n + 1
else -- ]
n = n - 1
end
k = k + 1
end
end -- while k
end -- framework()
local free = function(a, at, f)
-- Throws: error if /* is not matched by */
local s = a
local i = s:find("//", at, true)
local k = s:find("/*", at, true)
if i or k then
local m = s:find(sep0, at)
if i and (not m or i < m) then
k = s:find("\n", i + 2, true)
if k then
if i == 1 then
s = s:sub(k + 1)
else
s = s:sub(1, i - 1) .. s:sub(k + 1)
end
elseif i > 1 then
s = s:sub(1, i - 1)
else
s = ""
end
elseif k and (not m or k < m) then
i = s:find("*/", k + 2, true)
if i then
if k == 1 then
s = s:sub(i + 2)
else
s = s:sub(1, k - 1) .. s:sub(i + 2)
end
else
error(s:sub(k + 2), 0)
end
i = k
else
i = false
end
if i then
s = mw.text.trim(s)
if s:find("/", 1, true) then
s = f(s, i, f)
end
end
end
return s
end -- free()
if s:sub(1, 1) == "{" then
s = s:gsub(string.char(13, 10), JSONutil.Encoder.sep):gsub(string.char(13), JSONutil.Encoder.sep)
stub = s:gsub(JSONutil.Encoder.sep, ""):gsub(JSONutil.Encoder.stab, "")
scan = string.char(0x5B, 0x01, 0x2D, 0x1F, 0x5D) -- [ \-\ ]
j = stub:find(scan)
if j then
r = "ControlChar"
s = mw.text.trim(s:sub(j + 1))
s = mw.ustring.sub(s, 1, JSONutil.more)
else
i = true
j = 1
last = (stub:sub(-1) == "}")
sep0 = string.char(0x5B, 0x22, 0x27, 0x5D) -- [ " ' ]
sep1 = string.char(0x5B, 0x5C, 0x22, 0x5D) -- [ \ " ]
end
else
r = "Bracket0"
s = mw.ustring.sub(s, 1, JSONutil.more)
end
while i do
i, s = pcall(free, s, j, free)
if i then
i = s:find(sep0, j)
else
r = "CommentEnd"
s = mw.text.trim(s)
s = mw.ustring.sub(s, 1, JSONutil.more)
end
if i then
if j == 1 then
framework(s:sub(1, i - 1))
end
if s:sub(i, i) == '"' then
stub = s:sub(j, i - 1)
if stub:find('[^"]*,%s*[%]}]') then
r = "CommaEnd"
s = mw.text.trim(stub)
s = mw.ustring.sub(s, 1, JSONutil.more)
i = false
j = false
else
if j > 1 then
framework(stub)
end
i = i + 1
j = i
end
while j do
j = s:find(sep1, j)
if j then
if s:sub(j, j) == '"' then
start = s:sub(1, i - 1)
suffix = s:sub(j)
if j > i then
stub =
s:sub(i, j - 1):gsub(JSONutil.Encoder.sep, "\\n"):gsub(JSONutil.Encoder.stab, "\\t")
j = i + stub:len()
s = string.format("%s%s%s", start, stub, suffix)
else
s = start .. suffix
end
j = j + 1
break -- while j
else
j = j + 2
end
else
r = "QouteEnd"
s = mw.text.trim(s:sub(i))
s = mw.ustring.sub(s, 1, JSONutil.more)
i = false
end
end -- while j
else
r = "Qoute"
s = mw.text.trim(s:sub(i))
s = mw.ustring.sub(s, 1, JSONutil.more)
i = false
end
elseif not r then
stub = s:sub(j)
if stub:find('[^"]*,%s*[%]}]') then
r = "CommaEnd"
s = mw.text.trim(stub)
s = mw.ustring.sub(s, 1, JSONutil.more)
else
framework(stub)
end
end
end -- while i
if not r and (m ~= 0 or n ~= 0) then
if m ~= 0 then
s = "}"
if m > 0 then
r = "BracketCloseLack"
j = m
elseif m < 0 then
r = "BracketClosePlus"
j = -m
end
else
s = "]"
if n > 0 then
r = "BracketCloseLack"
j = n
else
r = "BracketClosePlus"
j = -n
end
end
if j > 1 then
s = string.format("%d %s", j, s)
end
elseif not (r or last) then
stub = suffix or apply or ""
j = stub:find("/", 1, true)
if j then
i, stub = pcall(free, stub, j, free)
else
i = true
end
stub = mw.text.trim(stub)
if i then
if stub:sub(-1) ~= "}" then
r = "Trailing"
s = stub:match("%}%s*(%S[^%}]*)$")
if s then
s = mw.ustring.sub(s, 1, JSONutil.more)
else
s = mw.ustring.sub(stub, -JSONutil.more)
end
end
else
r = "CommentEnd"
s = mw.ustring.sub(stub, 1, JSONutil.more)
end
end
if r and s then
s = s:gsub(JSONutil.Encoder.sep, " ")
s = mw.text.encode(s):gsub("|", "|")
end
return r, s
end -- JSONutil.fair()
JSONutil.fault = function(alert, add, adapt)
-- Retrieve formatted message.
-- Parameter:
-- alert -- string, with error keyword, or other text
-- add -- string|nil|false, with context
-- adapt -- function|string|table|nil|false, for I18N
-- Returns string, with HTML span.
local e = mw.html.create("span"):addClass("error")
local s = alert
if type(s) == "string" then
s = mw.text.trim(s)
if s == "" then
s = "EMPTY JSONutil.fault key"
end
if not s:find(" ", 1, true) then
local storage = string.format("I18n/Module:%s.tab", JSONutil.suite)
local lucky, t = pcall(mw.ext.data.get, storage, "_")
if type(t) == "table" then
t = t.data
if type(t) == "table" then
local e
s = "err_" .. s
for i = 1, #t do
e = t[i]
if type(e) == "table" then
if e[1] == s then
e = e[2]
if type(e) == "table" then
local q = type(adapt)
if q == "function" then
s = adapt(e, s)
t = false
elseif q == "string" then
t = mw.text.split(adapt, "%s+")
elseif q == "table" then
t = adapt
else
t = {}
end
if t then
table.insert(t, Fallback())
table.insert(t, "en")
for k = 1, #t do
q = e[t[k]]
if type(q) == "string" then
s = q
break -- for k
end
end -- for k
end
else
s = "JSONutil.fault I18N bad #" .. tostring(i)
end
break -- for i
end
else
break -- for i
end
end -- for i
else
s = "INVALID JSONutil.fault I18N corrupted"
end
else
s = "INVALID JSONutil.fault commons:Data: " .. type(t)
end
end
else
s = "INVALID JSONutil.fault key: " .. tostring(s)
end
if type(add) == "string" then
s = string.format("%s – %s", s, add)
end
e:wikitext(s)
return tostring(e)
end -- JSONutil.fault()
JSONutil.fetch = function(apply, always, adapt)
-- Retrieve JSON data or error message.
-- Parameter:
-- apply -- string, with presumable JSON text
-- always -- true, if apply is expected to need preprocessing
-- adapt -- function|string|table|nil|false, for I18N
-- Returns table with data or string, with error as HTML span.
local lucky, r
if not always then
lucky, r = pcall(mw.text.jsonDecode, apply)
end
if not lucky then
lucky, r = JSONutil.fair(apply)
if lucky then
r = JSONutil.fault(lucky, r, adapt)
else
lucky, r = pcall(mw.text.jsonDecode, r)
if not lucky then
r = JSONutil.fault(r, false, adapt)
end
end
end
return r
end -- JSONutil.fetch()
Failsafe.failsafe = function(atleast)
-- Retrieve versioning and check for compliance.
-- Precondition:
-- atleast -- string, with required version
-- or "wikidata" or "~" or "@" or false
-- Postcondition:
-- Returns string -- with queried version/item, also if problem
-- false -- if appropriate
-- 2020-08-17
local since = atleast
local last = (since == "~")
local linked = (since == "@")
local link = (since == "item")
local r
if last or link or linked or since == "wikidata" then
local item = Failsafe.item
since = false
if type(item) == "number" and item > 0 then
local suited = string.format("Q%d", item)
if link then
r = suited
else
local entity = mw.wikibase.getEntity(suited)
if type(entity) == "table" then
local seek = Failsafe.serialProperty or "P348"
local vsn = entity:formatPropertyValues(seek)
if type(vsn) == "table" and type(vsn.value) == "string" and vsn.value ~= "" then
if last and vsn.value == Failsafe.serial then
r = false
elseif linked then
if mw.title.getCurrentTitle().prefixedText == mw.wikibase.getSitelink(suited) then
r = false
else
r = suited
end
else
r = vsn.value
end
end
end
end
end
end
if type(r) == "nil" then
if not since or since <= Failsafe.serial then
r = Failsafe.serial
else
r = false
end
end
return r
end -- Failsafe.failsafe()
-- Export.
local p = {}
p.failsafe = function(frame)
-- Versioning interface.
local s = type(frame)
local since
if s == "table" then
since = frame.args[1]
elseif s == "string" then
since = frame
end
if since then
since = mw.text.trim(since)
if since == "" then
since = false
end
end
return Failsafe.failsafe(since) or ""
end -- p.failsafe
p.encodeArray = function(frame)
return JSONutil.Encoder.Array(frame:getParent().args, frame.args.type, frame.args.error == "1")
end -- p.encodeArray
p.encodeComponent = function(frame)
return JSONutil.Encoder.Component(
frame.args.sign,
frame.args.value,
frame.args.type,
flip(frame),
frame.args.error == "1"
)
end -- p.encodeComponent
p.encodeHash = function(frame)
return JSONutil.Encoder.Hash(frame:getParent().args, frame.args)
end -- p.encodeHash
p.encodeI18N = function(frame)
return JSONutil.Encoder.I18N(frame:getParent().args, flip(frame))
end -- p.encodeI18N
p.encodeObject = function(frame)
return JSONutil.Encoder.object(flat(frame.args[1]), flip(frame))
end -- p.encodeObject
p.encodePolyglott = function(frame)
return JSONutil.Encoder.Polyglott(flat(frame.args[1]), flip(frame))
end -- p.encodePolyglott
p.JSONutil = function()
-- Module interface.
return JSONutil
end
return p