Module:TemplateBox
From lisamarieyoung.ca
Documentation for this module may be created at Module:TemplateBox/doc
require('strict') --[[ @exports usagesample( frame ) argcount( frame ) args2table( args, onGetKey, forCustom ) paramtable( frame ) description( frame ) templatedata( frame ) ]] local p = {} -- Helper function, not exposed local function tobool(st) if type( st ) == 'string' then return st == 'true' else return not not st end end -- Required to determine in which languages the interface texts without langcode are local contentLangcode = mw.language.getContentLanguage():getCode() -- Forward declaration local msg, langIsInit, userLang local messagePrefix = "templatedata-doc-" local i18n = {} i18n['params'] = "Template parameters" i18n['param-name'] = "Parameter" i18n['param-desc'] = "Description" i18n['param-type'] = "Type" i18n['param-default'] = "Default" i18n['param-status'] = "Status" i18n['param-status-optional'] = "optional" i18n['param-status-required'] = "required" i18n['param-status-suggested'] = "suggested" i18n['param-status-deprecated'] = "deprecated" i18n['param-default-empty'] = "empty" local function initLangModule(frame) if langIsInit then return end userLang = frame:preprocess( '{{int:lang}}' ) --! From [[:de:Modul:Expr]]; by [[:de:User:PerfektesChaos]]; --! Derivative work: Rillke msg = function( key ) -- Retrieve localized message string in content language -- Precondition: -- key -- string; message ID -- Postcondition: -- Return some message string -- Uses: -- > messagePrefix -- > i18n -- > userLang -- mw.message.new() local m = mw.message.new( messagePrefix .. key ) local r = false if m:isBlank() then r = i18n[ key ] else m:inLanguage( userLang ) r = m:plain() end if not r then r = '((('.. key .. ')))' end return r end -- msg() langIsInit = true end -- A "hash" / table of everything TemplateData takes -- to ease maintenance. -- The type is automatically determined if t is omitted. -- If the type does not match or can't be converted, an error will be thrown! -- Available types (LUA-Types with exceptions): -- InterfaceText, boolean, number, selection, table, string -- selection*: - requires a selection-string of pipe-separated possibilities to be supplied -- InterfaceText*: A free-form string (no wikitext) in the content-language of the wiki, or, -- an object containing those strings keyed by language code. local paraminfoTemplate = { description = { default = '', t = 'InterfaceText', alias = 'desc' }, format = { default = 'inline', t = 'selection', selection = 'inline|block', alias = 'print', extract = function(pargs, number, paramVal) local m = { multi = 'block', one = 'inline', infobox = 'block' } return m[paramVal] or 'inline' end } } local paraminfoTLParams = { label = { default = '', t = 'InterfaceText' }, required = { default = false, extract = function(pargs, number, paramVal) local req = (pargs[number .. 'stat'] == 'required') return tobool( paramVal or req ) end }, suggested = { default = false, extract = function(pargs, number, paramVal) local sugg = (pargs[number .. 'stat'] == 'suggested') return tobool( paramVal or sugg ) end }, description = { default = '', t = 'InterfaceText', alias = 'd' }, deprecated = { default = false, extract = function(pargs, number, paramVal) local depr = (pargs[number .. 'stat'] == 'deprecated') return tobool( paramVal or depr ) end }, aliases = { default = '', t = 'table', extract = function(pargs, number, paramVal) local key = number .. 'aliases' local tdkey = key .. '-td' local aliases = pargs[tdkey] or pargs[key] if aliases and mw.text.trim( aliases ) ~= '' then local cleaned = {} for m in mw.text.gsplit( aliases, '/', true ) do cleaned[#cleaned+1] = mw.text.trim(m) end return cleaned else return nil end end }, default = { default = '', t = 'string', alias = 'def' }, type = { default = 'unknown', t = 'selection', selection = 'unknown|number|string|string/wiki-user-name|string/wiki-page-name|string/line|line|wiki-page-name|wiki-file-name|wiki-user-name|wiki-template-name|content|unbalanced-wikitext|date|url|boolean' }, inherits = { default = nil, t = 'string' }, autovalue = { default = '', t = 'string', alias = 'av', }, suggestedvalues = { default = '', t = 'table', alias = 'sv', extract = function(pargs, number, paramVal) if paramVal == nil then return nil end local cleaned = {} for m in mw.text.gsplit( paramVal, '/', true ) do cleaned[#cleaned+1] = mw.text.trim(m) end return cleaned end, }, -- sets will be treated differently because we can only have a plain structure in wikitext } local tableLayout = { { col = 'param-name', width = '15%', extract = function(item, renderCell, monolingual) local alias, param = '', item.key local aliasTT = '<span style="font-family: monospace; color:#777; border:1px solid #6A6A6A">' param = '<code>' .. param .. '</code>' if item.aliases then alias = aliasTT .. table.concat(item.aliases, '</span><br />' .. aliasTT) .. '</span>' param = table.concat({param, '<br /><div>', alias, '</div>'}) end renderCell(param) end }, { col = 'param-desc', cols = 2, width = '65%', extract = function(item, renderCell, monolingual) local label = item.label or '' label = monolingual(label) local labelLen = #label local colspan = 2 - labelLen if labelLen > 0 then renderCell(label) end renderCell(monolingual(item.description), colspan) end }, { col = 'param-default', width = '10%', extract = function(item, renderCell, monolingual) local def = monolingual(item.default) or '' if #def == 0 then def = '<span class="mw-templatedata-doc-muted" style="color:#777; font-variant:small-caps">' .. msg('param-default-empty') .. '</span>' end renderCell(def) end }, { col = 'param-status', width = '10%', extract = function(item, renderCell, monolingual) local stat = msg('param-status-optional') if item.required then stat = '<b>' .. msg('param-status-required') .. '</b>' elseif item.deprecated then stat = msg('param-status-deprecated') elseif item.suggested then stat = msg('param-status-suggested') end renderCell(stat) end } } -- Initialize param info -- Avoids having to add redundant information to the preceding tables local function init( which ) local setDefault = function(v) if v.t == nil and v.default ~= nil then v.t = type( v.default ) end if v.selection then local selection = mw.text.split(v.selection, '|', true) v.selection = {} for _, sel in ipairs(selection) do v.selection[sel] = true end end end for a, v in pairs( which ) do setDefault(v) end end local function initParamTables() init( paraminfoTemplate ) init( paraminfoTLParams ) end ------------------------------------------------------ -------------------- USAGE PART ---------------------- ------------------------------------------------------ function p.argcount( frame ) local pargs = ( frame:getParent() or {} ).args or {} local ac = 0 for i, arg in pairs( pargs ) do if ('number' == type(i)) then ac = ac + 1 end end return ac end function p.usagesample( frame ) local pargs = ( frame:getParent() or {} ).args or {} local multiline = (pargs.lines == 'multi' or pargs.print == 'multi' or pargs.print == 'infobox') local align = pargs.print == 'infobox' if not pargs.lines and not pargs.print and pargs.type == 'infobox' then multiline = true align = true end local sepStart = ' |' local sepEnd = multiline and '\n' or '' local sep = sepEnd local subst = #(pargs.mustbesubst or '') > 0 and 'subst:' or '' local beforeEqual = multiline and ' ' or '' local equal = beforeEqual .. '= ' local templateTitle = pargs.name or '' local args, argName, result = {} local maxArgLen, eachArg = 0 sep = sep .. sepStart local sparseIpairs = require('Module:TableTools').sparseIpairs local comapareLegacyVal = function(val) return val == 'optional-' or val == 'deprecated' end local shouldShow = function(i) if comapareLegacyVal(pargs[i .. 'stat']) or comapareLegacyVal(pargs[i .. 'stat-td']) or pargs[i .. 'deprecated'] == true then return false end return true end eachArg = function(cb) for i, arg in sparseIpairs( pargs ) do if ('number' == type(i)) then argName = mw.text.trim( arg or '' ) if #argName == 0 then argName = tostring(i) end if shouldShow(i) then cb(argName) end end end end if align then eachArg(function( arg ) local argL = #arg maxArgLen = argL > maxArgLen and argL or maxArgLen end) end eachArg(function( arg ) local space = '' if align then space = (' '):rep(maxArgLen - #arg) end table.insert( args, argName .. space .. equal ) end) if #args == 0 then sep = '' sepEnd = '' sepStart = '' end if #templateTitle == 0 then templateTitle = mw.title.getCurrentTitle().text end result = table.concat( args, sep ) result = table.concat({ mw.text.nowiki('{{'), subst, templateTitle, sep, result, sepEnd, '}}' }) if multiline then -- Preserve whitespace in front of new lines result = frame:callParserFunction{ name = '#tag', args = { 'poem', result } } end return result end ------------------------------------------------------ ------------------- GENERAL PART --------------------- ------------------------------------------------------ function p.args2table(args, onGetKey, consumer) initParamTables() local sets, asParamArray, laxtype, processParams, processDesc, unstrip if 'paramtable' == consumer then asParamArray = true processParams = true laxtype = true elseif 'templatedata' == consumer then sets = true processParams = true processDesc = true unstrip = true elseif 'description' == consumer then processDesc = true laxtype = true end -- All kind of strange stuff with the arguments is done, so play safe and make a copy local pargs = mw.clone( args ) -- Array-like table containing all parameter-numbers that were passed local templateArgs = {} -- Arguments that are localized (i.e. the user passed 1desc-en=English description of parameter one) local i18nTemplateArgs = {} -- Ensure that tables end up as array/object (esp. when they are empty) local tdata = {description="", params={}, sets={}} local isObject = { __tostring = function() return "JSON object" end } isObject.__index = isObject local isArray = { __tostring = function() return "JSON array" end } isArray.__index = isArray setmetatable(tdata.params, isObject) setmetatable(tdata.sets, isArray) onGetKey = onGetKey or function( prefix, alias, param ) local key, key2, tdkey, tdkey2 key = prefix .. (alias or param) key2 = prefix .. param tdkey = key .. '-td' tdkey2 = key2 .. '-td' return tdkey, tdkey2, key, key2 end local extractData = function( pi, number ) local prefix = number or '' local ppv, paramVal local key1, key2, key3, key4 local paramKey, paramTable, processKey if number then paramKey = mw.text.trim( pargs[number] ) if '' == paramKey then paramKey = tostring( number ) end paramTable = {} if asParamArray then paramTable.key = paramKey table.insert(tdata.params, paramTable) else tdata.params[paramKey] = paramTable end end for p, info in pairs( pi ) do key1, key2, key3, key4 = onGetKey(prefix, info.alias, p) paramVal = nil processKey = function(key) if paramVal ~= nil then return end local plain, multilingual = pargs[key], i18nTemplateArgs[key] paramVal = multilingual or plain end processKey( key1 ) processKey( key2 ) processKey( key3 ) processKey( key4 ) -- Ensure presence of entry in content language ppv = pargs[key1] or pargs[key2] or pargs[key3] or pargs[key4] or info.default if 'table' == type( paramVal ) then if (nil == paramVal[contentLangcode]) then paramVal[contentLangcode] = ppv end else paramVal = ppv end if 'function' == type( info.extract ) then if 'string' == type( paramVal ) then paramVal = mw.text.trim( paramVal ) if '' == paramVal then paramVal = nil end end paramVal = info.extract( pargs, number, paramVal ) end local insertValue = function() if number then paramTable[p] = paramVal else tdata[p] = paramVal end end if info.selection then if info.selection[paramVal] then insertValue() end elseif 'InterfaceText' == info.t then if ({ table=1, string=1 })[type( paramVal )] then insertValue() end else local paramType = type( paramVal ) if 'string' == info.t and 'string' == paramType then paramVal = mw.text.trim( paramVal ) if '' ~= paramVal then insertValue() end elseif 'boolean' == info.t then paramVal = tobool(paramVal) insertValue() elseif 'number' == info.t then paramVal = tonumber(paramVal) insertValue() elseif paramType == info.t then insertValue() elseif paramType == 'nil' then -- Do nothing elseif not laxtype and 'string' == info.t and 'table' == paramType then -- Convert multilingual object into content language string paramVal = paramVal[contentLangcode] insertValue() else if laxtype then insertValue() else error( p .. ': Is of type ' .. paramType .. ' but should be of type ' .. (info.t or 'unknown'), 1 ) end end end end -- Now, treat sets if sets then key1 = prefix .. 'set-td' key2 = prefix .. 'set' paramVal = pargs[key1] or pargs[key2] if paramVal then local found = false for i, s in ipairs( tdata.sets ) do if s.label == paramVal then table.insert( s.params, p ) found = true end end if not found then table.insert( tdata.sets, { label = paramVal, params = { p } } ) end end end end -- First, analyse the structure of the provided arguments for a, v in pairs( pargs ) do if unstrip then v = mw.text.unstrip( v ) pargs[a] = v end if type( a ) == 'number' then table.insert( templateArgs, a ) else local argSplit = mw.text.split( a, '-', true ) local argUnitl = {} local argAfter = {} local isTDArg = false local containsTD = a:find( '-td', 1, true ) for i, part in ipairs( argSplit ) do if isTDArg or (containsTD == nil and i > 1) then -- This is likely a language version table.insert( argAfter, part ) else table.insert( argUnitl, part ) end if part == 'td' then isTDArg = true end end if #argAfter > 0 then argUnitl = table.concat( argUnitl, '-' ) argAfter = table.concat( argAfter, '-' ) i18nTemplateArgs[argUnitl] = i18nTemplateArgs[argUnitl] or {} i18nTemplateArgs[argUnitl][argAfter] = v end end end -- Then, start building the actual template if processDesc then extractData( paraminfoTemplate ) end if processParams then -- Ensure that `templateArgs` contains indicies in ascending order table.sort( templateArgs ) for i, number in pairs( templateArgs ) do extractData( paraminfoTLParams, number ) end end return tdata, #templateArgs end ------------------------------------------------------ ------------ CUSTOM PARAMETER TABLE PART ------------- ------------------------------------------------------ -- A custom key-pref-function local customOnGetKey = function( prefix, alias, param ) local key, key2, tdkey, tdkey2 key = prefix .. (alias or param) key2 = prefix .. param tdkey = key .. '-td' tdkey2 = key2 .. '-td' return key2, key, tdkey2, tdkey end local toUserLanguage = function(input) if type(input) == 'table' then input = require( 'Module:LangSwitch' )._langSwitch( input, userLang ) or '' end return input end function p.description(frame) local pargs = ( frame:getParent() or {} ).args or {} -- Initialize the language-related stuff initLangModule(frame) local tdata, paramLen tdata, paramLen = p.args2table(pargs, customOnGetKey, 'description') return toUserLanguage(tdata.description) end function p.paramtable(frame) local pargs = ( frame:getParent() or {} ).args or {} local tdata, paramLen if 'only' == pargs.useTemplateData then return 'param table - output suppressed' end -- Initialize the language-related stuff initLangModule(frame) tdata, paramLen = p.args2table(pargs, customOnGetKey, 'paramtable') if 0 == paramLen then return '' end local row, rows = '', {} local renderCell = function(wikitext, colspan) local colspan, oTd = colspan or 1, '<td>' if colspan > 1 then oTd = '<td colspan="' .. colspan .. '">' end row = table.concat({ row, oTd, wikitext, '</td>' }) end -- Create the header for i, field in ipairs( tableLayout ) do local style = ' style="width:' .. field.width .. '"' local colspan = '' if field.cols then colspan = ' colspan="' .. field.cols .. '"' end local th = '<th' .. style .. colspan .. '>' row = row .. th .. msg(field.col) .. '</th>' end table.insert(rows, row) -- Now transform the Lua-table into an HTML-table for i, item in ipairs( tdata.params ) do row = '' for i2, field in ipairs( tableLayout ) do field.extract(item, renderCell, toUserLanguage) end table.insert(rows, row) end return '<table class="wikitable templatebox-table"><tr>' .. table.concat(rows, '</tr><tr>') .. '</tr></table>' end ------------------------------------------------------ ----------------- TEMPLATEDATA PART ------------------ ------------------------------------------------------ -- A real parser/transformer would look differently but it would likely be much more complex -- The TemplateData-portion for [[Template:TemplateBox]] function p.templatedata(frame) local tdata local args = frame.args or {} local formatting = args.formatting local pargs = ( frame:getParent() or {} ).args or {} local useTemplateData = pargs.useTemplateData if (formatting == 'pretty' and useTemplateData ~= 'export') or (not useTemplateData) or (useTemplateData == 'export' and formatting ~= 'pretty') then local warning = "Warning: Module:TemplateBox - templatedata invoked but not requested by user (setting useTemplateData=1)." mw.log(warning) tdata = '{"description":"' .. warning .. '","params":{},"sets":[]}' return tdata end -- Load the JSON-Module which will convert LUA tables into valid JSON local JSON = require('Module:JSON') JSON.strictTypes = true -- Obtain the object containing info tdata = p.args2table(pargs, nil, 'templatedata') -- And finally return the result if formatting == 'pretty' then return JSON:encode_pretty(tdata) else return JSON:encode(tdata) end end return p