Modul:JSONutil
Die Dokumentation für dieses Modul kann unter Modul:JSONutil/Doku erstellt werden
local JSONutil = { suite = "JSONutil", serial = "2020-11-08", item = 63869449 } --[=[ preprocess or generate JSON data ]=] local Failsafe = JSONutil JSONutil.Encoder = { stab = string.char( 9 ), sep = string.char( 10 ), scream = "@error@JSONencoder@" } 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