Module:TemplateBox

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( '' )

--! 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 = ' '

param = ' ' if item.aliases then alias = aliasTT .. table.concat(item.aliases, ' ' .. aliasTT) .. ' '               param = table.concat({param, ' ', alias, ' '}) 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 = ' ' .. msg('param-default-empty') .. ' '           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 = ' .. msg('param-status-required') .. '           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('' })   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, ' ' if colspan > 1 then oTd = '' end row = table.concat({ row, oTd, wikitext, ' ' }) 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 = ''

row = row .. th .. msg(field.col) .. ' '   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 ' ' 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