Module:Infobox

From Feed The Beast Wiki
Jump to: navigation, search

This module implements all of the infoboxes. Individual infoboxes are defined on subpages of this page, and MUST be marked translatable, or you will get an error when trying to use them, as the module reads the translations exclusively.

Functions

{{#invoke:Infobox|infobox|<boxname>|...}}

Expands to the infobox defined by the subpage boxName. When creating the template wrappers, supply fromParent=1 instead of the parameters directly. This will cause the infobox to take its parameters from the ones supplied to the wrapper.

{{#invoke:Infobox|documentationPage|<boxname>}}

Expands to a documentation page for boxname.

{{#invoke:Infobox|parameterHelp|<boxname>}}

List the parameters available in a suitable form for template documentation.

{{#invoke:Infobox|emptyBox|<boxname>}}

Produce an infobox with all the parameter names shown.

Format of infobox definitions

Please note that any translatable text MUST use multiline string syntax with at least one = in it. That is, rather than "string", do [=[string]=]. This is because the translate extension inserts linebreaks, which are not compatible with quotes, and the [[string]] syntax breaks if it contains any wikitext links.

Fields listed as wikitext are strings, but ones in which it is reasonable to put wikitext. Templates should work in most of them.

Each definition is a Lua module, so it has to return a table. Typically this is done by:

local box = {}
-- box definition is here
return box

The contents of this table are as following:

KeyTypeDescription
docLeadWikitextLead section of the documentation (the hatnote and "This template is used to create an infobox..." text are inserted for you).
parameterLeadWikitextText immediately preceding the parameter list (the "an empty infobox..." text is also inserted automatically)
parameterTrailWikitextText immediately following the parameter list.
formatFormat tableDetails of the infobox (see the next section)
examplesTable of examplesExamples to place in the documentation. Each example is a table containing an optional description in the text key, and some wikitext to use as an example in the first indexed entry. For example:
{ text="Demonstration of hatnote", ":''This article is about the wiki concept. For sticking notes on actual hats, see [[Hat#Annotating]]''" }

Format of infobox details

This is also a table, containing a list of sections; a section is the part of the infobox that is headed by a blue header. Each section is a table, with the following contents. "Argument" denotes a field that can be either an argument table (see below), or wikitext.

KeyTypeDescription
nameStringName that appears as a header in the documentation. Consecutive sections with identical names will be merged into one in the documentation.
titleArgumentMandatory. The heading for a section (ie, the bit that appears in the blue bar).
descWikitextOptional. Explanatory text which will appear in the documentation for this section.
noDocBooleanOptional. If present, don't show this section in the documentation at all.
docOnlyBooleanOptional. If present, this section is not treated as part of the infobox, but documentation is still shown for it.
IndexedSubsection tableThe subsections to display. You need at least one of these.

Subsections can contain desc noDoc and docOnly keys, and contain a list of rows (again, you need at least one). Rows can also contain those three keys, and contain one or two Arguments as indexed entries. If one, then it will be centered across the width of the infobox, using the colspan parameter in the generated cell.

Format of Arguments

An argument which is not a string is a table referring to a template parameter supplied to an invocation of infobox, the value of this parameter being used as if it had been given in place of the table.

KeyTypeDescription
argString or table of stringsThe name of the template parameter. If you give a table here, its indexed parameters will be used as synonyms, with the first being the canonical name.
typeOne of:
"text"
"image"
"switch"
"link"
"templateCall"
How the argument will be interpreted. text is just wikitext. image will treat it as the name of an image. switch will look it up in the allowedValues parameter. switch will also add to if an invalid value is given for the parameter (but not none at all). link will treat it as the URL of an external link. templateCall will treat it as the lone argument to the template given in the template member.
styleStringCSS to insert in the table cell's style attribute.
maxSizeNumberMaximum size of an image in pixels. Omit the px, it will be added for you.
imageParametersStringAdditional parameters to add to the image. Use pipes to separate them, as you would in wikitext (but you don't need to start or end with one).
allowedValuesTable of stringsPermitted values for a switch. Mandatory for switches, pointless otherwise. The value given as a template parameter will be used as a key into this table, and the value will be displayed. A listing of keys and values will be entered into the documentation.
descWikitext (no templates)Description to displayed in the listing of parameters in the documentation. If omitted, then no entry will be displayed at all.
noDocBooleanIf present, the argument will not be mentioned in the documentation. Overrides the presence of desc.
linkTextStringIf the type is link use this as the link text.
prefixStringPrepend this text, followed by a space, to the argument's value.
unitsStringAppend a space and then this text, to the argument's value.
templateStringName of template to expand.

local p = {}

local util = require("Module:Utility_functions")

local ipairs, pairs, rawget, type = ipairs, pairs, rawget, type

Accumulator = {}
p.a = Accumulator

Accumulator.new = function()
    local nextidx = 1
    this = {}
    local n = {
        out = function(str)
            if not str then return end
            this[nextidx] = str
            nextidx = nextidx + 1
        end,
        
        outv = function(...)
            for i,t in ipairs({...}) do
                this[nextidx] = t
                nextidx = nextidx + 1
            end
        end,
        
        outt = function(tab)
            for i,t in ipairs(tab) do
                this[nextidx] = t
                nextidx = nextidx + 1
            end
        end
    }
    setmetatable(n, {__tostring = function()
        return table.concat(this)
    end})
    return n
end

-- returns (true, module) or (false, message)
function loadTranslatedModule(name, forceUntranslated)
    local moduleName = name
    if not forceUntranslated then
        moduleName = moduleName .. util.pageSuffix()
    end
    return pcall(function()
        return require(moduleName)
    end)
end

local stringsLoaded, strings = loadTranslatedModule("Module:Infobox/strings")
if not stringsLoaded then
    strings = {}
end
setmetatable(strings, {__index = function(tab, key)
    if (key == "_fellbackStrings") then
        tab._fellbackStrings = {}
        return tab._fellbackStrings
    end
    
    if (key == "_missingStrings") then
        tab._missingStrings = {}
        return tab._missingStrings
    end
    
    if key == "_ModuleStringTableProblem" then
        if rawget(tab,"_stringTableEntirelyMissing") then
            return "!!! [[Module:Infobox/strings]] is gone or not a valid Lua module !!!"
        end
        if (#tab._fellbackStrings > 0) or (#tab._missingStrings > 0) then
            return "!!! Problem with [[Module:Infobox/strings]] or [[Module:Infobox/strings".. util.pageSuffix() .."|a translation thereof]]. Fallbacks used for: {" .. table.concat(tab._fellbackStrings, ", ") .. "}. Missing strings: {" .. table.concat(tab._missingStrings, ", ") .. "}. Please inform a translation admin as the translation markings probably need updating. !!!"
        else
            return nil
        end
    end
    
    if not rawget(tab,"_fallback") then
        pcall(function()
            tab._fallback = require("Module:Infobox/strings")
        end)
        if not rawget(tab,"_fallback") then
            tab._stringTableEntirelyMissing = true
            tab._fallback = { }
        end
    end
    
    local val = tab._fallback[key]
    if val then
        tab._fellbackStrings[#(tab._fellbackStrings)+1] = key
    else
        tab._missingStrings[#(tab._missingStrings)+1] = key
        val = "noString!"..key
    end
    val = string.gsub(val, "</?translate>","")
    tab[key] = val -- inhibit repeats.
    return val
end})

local function makePreprocess(frame)
    return function(str)
        if str then
            return frame:preprocess(tostring(str)) or "wat"
        else
            return ""
        end
    end
end

-- load an infobox
local function loadInfobox(name, forceUntranslated)
    return loadTranslatedModule("Module:Infobox/" .. util.trim(name), forceUntranslated)
end

-- this is exactly as lua-users wiki defined it. Never mind the odd gsub argument.
    local function interp(s, tab)
        return (s:gsub('($%b{})', function(w) return tab[w:sub(3, -2)] or w end))
    end

-- return the canonical, ie first, name given in an argdata.
local function argCName(argdata)
    if type(argdata.arg) == "string" then
        return argdata.arg
    else
        return argdata.arg[1]
    end
end

-- wrap an argument value in the necessary surround
local function wrapArg(argdata, value, expansionFrame)
    if argdata.type == "image" then
        local sizeRequested = tonumber(argdata.maxSize or "0")
        if sizeRequested == 0 then
            sizeRequested = tonumber(strings.maxImageSize)
        end
        
        local parameters = (argdata.imageParameters or strings.imageParameters)
        if parameters == "" then
            parameters = nil
        end
        mw.log({'File:'.. value, sizeRequested .. 'px', parameters})
        
        -- force frameless for now, because that makes size do the right thing
        local stuff = util.compact({'File:'.. value, sizeRequested .. 'px', parameters , "frameless"})
        return '[['.. table.concat(stuff,"|")..']]'
    elseif argdata.type == "link" then
        local linktext = (argdata.linkText or strings.linkText)
        return table.concat({'[', value, ' ', linktext, ']'})
    elseif argdata.type == "templateCall" then
        if expansionFrame == "noTransclude" then
            return table.concat({'<nowiki>{{</nowiki>', argdata.template, '|', value, '<nowiki>}}</nowiki>'})
        else
            return expansionFrame:expandTemplate{title = argdata.template, args = { value }}
        end
    else
        return value
    end
end

-- resolve argument data for a given arg table
local function argtableResolver(argdata, frame, errordata)
    local argtable = frame.args
    if not argdata then
        return nil
    end
    
    if type(argdata) == "string" then
        return argdata
    end

    local text = nil
    if type(argdata.arg) == "string" then
        text = argtable[argdata.arg]
    elseif (type(argdata.arg) == "table") and (#(argdata.arg) > 0) then
        for i,arg in ipairs(argdata.arg) do
            local candidate = util.trim(argtable[arg] or "")
            if not (candidate == "") then
                text = candidate
            end
        end
    else
        error(strings.errArgMissingArgName .. " " .. interp(position.formatString, errordata))
    end
    
    if not text then
        return nil
    end
    
    if argdata.type == "switch" then
        if (not argdata.allowedValues) or (type(argdata.allowedValues) ~= "table") then
            error(strings.errArgMissingSwitchValues .. " " .. interp(position.formatString, errordata))
        elseif not argdata.allowedValues[text] then
            text = strings.unknownType .. ' [[Category:Articles with bad parameter values]]'
        else
            text = argdata.allowedValues[text]
        end
    end
    
    local wrappedText = wrapArg(argdata, text, frame)
    if argdata.prefix then
        wrappedText = argdata.prefix .. " " .. wrappedText
    end

    if argdata.units then
        wrappedText = wrappedText .. " " .. argdata.units
    end
    
    if argdata.style then
        return {style=argdata.style, wrappedText}
    else
        return wrappedText
    end
end

local function makeResolver(frame)
    sourceframe = frame
    if frame.args.fromParent then
        sourceframe = frame:getParent()
    end
    return function(argdata, errordata)
        return argtableResolver(argdata, sourceframe, errordata)
    end
end

-- resolve argument data for an empty infobox
-- parameters
--   argdata: argdata|string
-- returns
--   string|{style=cellstyle, string}
local function emptyBoxResolver(argdata)
    if not argdata then
        return "wat"
    end
    
    if type(argdata) == "string" then
        return argdata
    elseif type(argdata) == "table" then
        local text = "{{{"..argCName(argdata).."}}}"
        if argdata.style then
            return {style=argdata.style, wrapArg(argdata, text, "noTransclude")}
        else
            return wrapArg(argdata, text, "noTransclude")
        end
    end
end

-- resolve the arguments of an infobox definition according to the given resolver,
-- and return a structure with all the arguments resolved and any unnecessary sections
-- etc omitted (sections with an argument title are never omitted).
-- parameters
--   args.box: infobox definition
--   args.resolver: function(argdata|string) -> string.
local function calculateEffectiveInfobox(args)
    local effectiveBox = {type="box"}
    
    if (not args.box) or (type(args.box) ~= "table") then
        error(strings.errNoFormat)
    end
    
    local sectioncount = 0
    for i, section in ipairs(args.box) do
        if type(section) ~= "table" then
            error(interp(strings.errBadSection, {section = i}))
        end
    
        if not section.docOnly then
            sectioncount = sectioncount + 1
            
            if not section.title then
                error(interp(strings.errSectionNoTitle, {section = i}))
            end
            
            local newSection = { title = args.resolver(section.title, {formatString = strings.errSectionLocation, section = i}), type="section" }
            
            for j, subsection in ipairs(section) do
                if type(subsection) ~= "table" then
                    error(interp(strings.errBadSubsection, {section = i, subsection = j}))
                end
                if not subsection.docOnly then
                    local newSubsection = {type="subsection"}
                    
                    for k, row in ipairs(subsection) do
                        if type(row) ~= "table" then
                            error(interp(strings.errBadRow, {section = i, subsection = j, row = k}))
                        end
                        if not row.docOnly then
                            local newRow = { type="row"}
                            
                            for m, cell in ipairs(row) do
                                newRow[#newRow + 1] = args.resolver(cell, {formatString = strings.errCellLocation, section = i, subsection = j, row = k, cell = m})
                            end
                            
                            if (#row == #newRow) then
                                newSubsection[#newSubsection+1] = newRow
                            end
                        end
                    end
                    
                    if (#newSubsection > 0) then
                        newSection[#newSection + 1] = newSubsection
                    end
                end
            end
            
            if (#newSection > 0) or (type(section.title) == "table") then
                effectiveBox[#effectiveBox + 1] = newSection
            end
        end
    end
    
    if sectioncount == 0 then
        error(strings.errEmptyInfoboxFormat)
    end
    
    return effectiveBox
end

-- convert an infobox definition that has had all the arguments resolved, to a string.
-- box like
-- { class=tableclass, { title=sectionheader, { { cell, cell }, {cell, cell } } } }
-- cell like string or { style=cellstyle, content }
local function renderBox(box, wtprocessor)
    result = ""
    local out = function(thing)
        result = result .. thing
    end
    out('<table class="infobox')
    if box.class then
        out(' '..box.class)
    end
    out('">')
    
    local count = 0
    
    for i,section in ipairs(box) do
        if i == 1 then
            out('<tr><th class="infoboxFirstHeader" colspan="2">')
        else
            out('<tr class="infoboxSectionHeader"><th class="infoboxSectionHeader" colspan="2">')
        end
        local title = wtprocessor(section.title)
        out(title)
        out('</th></tr>')
        
        if title and (title ~= "") then
            count = count + 1
        end
        
        for j,subsection in ipairs(section) do
            if j > 1 then
                out('<tr class="infoboxSubsectionBreak"><td></td><td></td></tr>')
            end
            
            for k, row in ipairs(subsection) do
                out('<tr>')
                for m,cell in ipairs(row) do
                    if m < 3 then
                        out('<td')
                        if #row == 1 then
                            out(' colspan="2"')
                        end
                        if type(cell)=="table" and cell.style then
                            out(' style="'..cell.style..'"')
                        end
                        out('>')
                    else
                        if type(cell)=="table" and cell.style then
                            out('<span style="'..cell.style..'">')
                        end
                    end
                    if type(cell)=="string" then
                        out(wtprocessor(cell))
                    elseif type(cell)=="table" then
                        out(wtprocessor(cell[1]))
                    end
                    count = count + 1
                    if type(cell)=="table" and cell.style and m >= 3 then
                        out('</span>')
                    end
                    if m == 1 or (m == #row) then
                        out('</td>')
                    end
                end
                out('</tr>')
            end
        end
    end
    
    if count == 0 then
        out('<tr><td colspan="2">' .. strings.errNoOutput .. '</td></tr>')
    end
    
    out('</table>')
    
    return result
end

local function renderEmptyBox(frame, box)
    local presentedBox = calculateEffectiveInfobox{box=box.format, resolver = emptyBoxResolver}
    presentedBox.class="infoboxNoCollapse"
    
    return renderBox(presentedBox, function(str)
        return frame:preprocess(str or "")
    end)
end

--- returns list of { name = string, text = string, argdata... }
function p.calculateParameterListing(box, nondocmode)
    local parameters = {}
    for i,section in ipairs(box) do
        if (nondocmode and (not section.docOnly)) or (not section.noDoc) then
            local sectiondata = { name = section.name}
            if (not parameters[#parameters]) or (not (sectiondata.name == parameters[#parameters].name)) then
                parameters[#parameters+1] = sectiondata
            else
                sectiondata = parameters[#parameters]
            end
        
            if type(section.title) == "table" then
                sectiondata[#sectiondata+1] = section.title
            end
        
            if section.desc then
                sectiondata[#sectiondata+1] = { text = section.desc }
            end

            for j,subsection in ipairs(section) do
                if (nondocmode and (not subsection.docOnly)) or (not subsection.noDoc) then
                    if subsection.desc then
                        sectiondata[#sectiondata+1] = { text = subsection.desc }
                    end
                    for k,row in ipairs(subsection) do
                        if (nondocmode and (not row.docOnly)) or (not row.noDoc) then
                            if row.desc then
                                sectiondata[#sectiondata+1] = { text = row.desc }
                            end
                            for m,cell in ipairs(row) do
                                if (type(cell) == "table") and ((nondocmode and (not cell.docOnly)) or (cell.desc and (not cell.noDoc))) then
                                    sectiondata[#sectiondata+1] = cell
                                end
                            end
                        end
                    end
                end
            end
        end
    end
    return parameters
end

local function renderParameterListing(box, a)
    local parameters = p.calculateParameterListing(box)
    
    local collapseByValue = function(tab)
        local iv={}
        for k,v in pairs(tab) do
            if not iv[v] then
                iv[v]={k}
            else
                iv[v][#(iv[v]) + 1] = k
            end
         end
        local s={}
        for k,v in pairs(iv) do
            if not s[v] then
                s[v]=k
            end
         end
        return s
    end
    
    local out = a.out
    local outt = a.outt

    for i,section in ipairs(parameters) do
        if section.name then
            outt{"===",section.name,"===\n"}
        end
        for j,parameter in ipairs(section) do
            if parameter.text then
                outt{parameter.text,'\n'}
            elseif parameter.desc then
                local namelist = parameter.arg
                if type(parameter.arg) == "table" then
                    namelist = table.concat(parameter.arg, "''' '' " .. strings.nameorname .. " '' '''")
                end
                outt{"* '''",namelist,"''': ",parameter.desc}
                if parameter.type == "switch" then
                    outt{" ",strings.switchdoc,"\n"}
                    for key,value in util.orderedPairs(collapseByValue(parameter.allowedValues)) do
                        outt{"** ''",table.concat(key, " '' " .. strings.nameorname .. " '' "),"'': ",value,"\n"}
                    end
                elseif parameter.type == "image" then
                    outt{" ",strings.imagedoc ,"\n"}
                else
                    out("\n")
                end
            end
        end
    end
    return result
end

local function makeInvokeWithBox(func)
    return function(frame)
        local out = ""
        local success, box = loadInfobox(frame.args[1], frame.args.forceUntranslated)
        if not success then
            out =  "!!! " .. strings.loadFailure .. " [[Module:Infobox/" .. frame.args[1] .. util.pageSuffix() .. "]] " .. strings.informTranslationAdmin .. " \n\n"
            out = out .. box .. " !!!\n\n" 
        else
            local success = nil
            local result = nil
            if frame.args.stackTrace then
                success, result = xpcall(function()
                    func(frame, box)
                end, debug.traceback)
            else
                success, result = pcall(func, frame, box)
            end
            
            if not success then
                out = "!!! " .. result .. " " .. strings.informTranslationAdmin .. " !!!\n\n"
            else
                out = result
            end
        end
        
        if strings._ModuleStringTableProblem then
            out = strings._ModuleStringTableProblem .. "\n\n" .. out
        end
        
        return out
    end
end

-- emit an infobox.
-- parameters:
--   1: Infobox definition to use
--   everything else: whatever the infobox definition says

p.infobox = makeInvokeWithBox(function(frame, box)
    local presentedBox = calculateEffectiveInfobox{box=box.format, resolver = makeResolver(frame)}
    
    local out = renderBox(presentedBox, function(str)
        return frame:preprocess(str or "")
    end)
    
    local paramList = p.calculateParameterListing(box.format, true)
    local params = {}

    for i,section in ipairs(paramList) do
        for j, param in ipairs(section) do
            if type(param.arg) == "table" then
                for j,a in ipairs(param.arg) do
                    params[a] = true
                end
            elseif param.arg then
                params[param.arg] = true
            end
        end
    end

    local hasBadArgs = false
    for name, value in pairs((frame:getParent() or frame).args) do
        if not params[name] then
            hasBadArgs = true
            out = out .. "\n\n<div style=\"display:none;\">" .. strings.badParameterName .. " " .. name .. "</div>"
        end
    end
    if hasBadArgs then
        out = out .. "[[Category:Articles with bad template parameters]]"
    end
    
    return out
end)

-- emit an infobox with the parameters displayed
-- parameters:
--   1: Infobox definition to use
p.emptyBox = makeInvokeWithBox(function(frame, box)
    return renderEmptyBox(frame, box)
end)

-- emit the parameter list for documentation
-- parameters
--   1: Infobox definition to use
p.parameterHelp = makeInvokeWithBox(function(frame, box)
    return tostring(renderParameterListing(box.format, Accumulator.new()))
end)

-- emit the entire documentation page
-- parameters
--   1: infobox definition to use
-- in the infobox definition
--   intro is the text at the top
--   postParameterText is displayed after the parameter listing
--   examples is an array of examples: { { optional text = wikitext string, template-supporting wikitext}, ... }
p.documentationPage = makeInvokeWithBox(function(frame, box)
    local fp = makePreprocess(frame)
    
    local a = Accumulator.new()
    local out = a.out
    local outv = a.outv
    local outt = a.outt

    local function hatnote(str)
        return ":''" .. str .. "''"
    end
    
    outv("{{Doc/Start}}\n{{Lua{{L}}|Infobox|data=Infobox/", frame.args[1], "}}\n")

    --outv(hatnote(noteText), '\n') 
    -- ^^ noteText is never defined, leaving "Text?" at the start of template documentation
    outv(strings.docLead, ' ')
    outv(box.docLead, '\n\n')
    outv("==", strings.parameters, "==\n")
    outv(strings.parameterLead, " ")
    out(box.parameterLead)
    outv(renderEmptyBox(frame, box), '\n')
    renderParameterListing(box.format, a)
    outt{'\n', box.parameterTrail}
    
    out('<div style="clear:both;"></div>')
    
    outv("\n==", strings.examples, "==\n")
    if box.examples then
        for i, example in ipairs(box.examples) do
            outv(example.text or "", '\n')
            outv('<table class="wikitable">\n<tr><th>', strings.exampleCode, '</th><th>', strings.exampleResult, '</th></tr>\n')
            outv('<tr>\n<td><pre>\n', example[1], '\n</pre></td>\n')
            outv('<td>\n', example[1], '\n</td></tr>\n')
            out('</table>\n\n')
        end
    else
        out(hatnote(fp(strings.noExamples)))
    end

    out("{{Doc/End}}")
    return frame:preprocess(tostring(a))
end)

return p