Module:TemplateBox/sandbox

From Wikimedia Commons, the free media repository
Jump to navigation Jump to search
Lua

CodeDiscussionEditHistoryLinksLink count Subpages:DocumentationTestsResultsSandboxLive code All modules

warning Warning:This page is shared between multiple wikis.
All changes to this page will be automatically copied to all wikis listed in the left side bar.
To avoid unnecessary page regeneration and server load, changes should be tested on the page's sandbox.

Logic for powering Template:TemplateBox.

Module Quality

[edit]

Code

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 and multiline then
        	space = ('&#32;'):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 = { 'pre', 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