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 = ('&nbsp;'):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