Модуль:TemplateBox

    Матеріал з Релігія в огні

    Документацію для цього модуля можна створити у Модуль: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( '{{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