Module:Infobox: Difference between revisions

From Brighter Shores Wiki
Jump to navigation Jump to search
Content added Content deleted
(Simplify the table_class param)
(When a parameter is nil, make sure the function is called with it being set to nil (previously, all other parameters would be shifted left one))
 
(44 intermediate revisions by 6 users not shown)
Line 4: Line 4:


-- <nowiki>
-- <nowiki>
require('strict')
local editbutton = require('Module:Edit button')
local smwutils = require('Module:SMW Utils')

local Infobox = {}
local Infobox = {}
Infobox.__index = Infobox
Infobox.__index = Infobox
Infobox.__tostring = Infobox.tostring

-- Edit button for unknown params
local editbutton = require('Module:Edit button')
local edit = editbutton("'''?''' (edit)")

-- Page title
local pagename = mw.title.getCurrentTitle().fullText

-- map of flags to html tags used by Infobox.addRow()
-- let's only define it once, since :addRow() is used multiple times per module
local tagmap = {
tr = 'tr',
th = 'th',
td = 'td',
argh = 'th',
argd = 'td'
}

--[=[
-- Standardized functions
-- called as string with defineParams
--]=]

-- Standardized "has content" function
function hasContent(arg, default)
-- Return arg if any non-whitespace character is found
return string.match(arg or '','%S') and arg or default
end


-- Standardized "name" function
function subjectName(arg)
return string.match(arg or '','%S') and arg or nil
end

-- Create a standardized release function, since so many pages use it
-- Turns release and update into a single parameter
function releaseUpdate(release, update)
if not Infobox.isDefined(release) then
return nil
end
if string.lower(release) == 'no' then
return 'N/A'
end
if not Infobox.isDefined(update) then
return string.format('%s (Update unknown)',release)
end
if string.lower(update) == 'no' then
return release
end
return string.format('%s ([[Update:%s|Update]])', release, update)
end

-- Standardized image function
function image(img)
if img and img:find('%S') then
return img
else
return nil
end
end

-- Standardized numbers
function numbers(num)
num = string.gsub(num or '',',','')
return tonumber(num)
end

-- map of names to pre-defined functions, used by Infobox:defineParams
local func_map = {
name = subjectName,
release = { name = releaseUpdate, params = { 'release', 'update' }, flag = 'p' },
removal = { name = releaseUpdate, params = { 'removal', 'removalupdate' }, flag = 'p' },
has_content = hasContent,
image = image,
numbers = numbers,
}

-- used to fill nil params in switching sections
-- this message isn't kidding
-- If you see this message anywhere outside of this code
-- (including inside switchfo box data)
-- report it
local nil_param = 'INFOBOX MODULE ERROR, PLEASE REPORT TO WIKI ADMIN'

-- In case the nil_param is needed outside of this module
-- give it an easy way to be accessed
function Infobox.nilParam()
return nil_param
end

-- switch infobox globals
local LINE_WIDTH = 300
local MAX_LINES = 2
local DEFAULT_MAX_BUTTONS = 6
-- calculate with width of a switch infobox button
-- potential @TODO: rework to use actual character widths
function button_width(label)
local PX_PER_CHAR = 6
local PX_PAD_MAR = 24
return string.len(label) * PX_PER_CHAR + PX_PAD_MAR
end

Infobox.splitpoint = '&&SPLITPOINT&&'

-- quick test to see if a value is considered nil
function Infobox.isDefined(arg)
if arg == nil then
return false
end

if type(arg) == 'string' then
if arg == nil_param then
return false
elseif arg:find('%S') then
if arg:find('action=edit') then
return false
else
return true
end
else
return false
end
end

return true
end



--[[
--[[
Infobox class
Infobox class
-- config: table containing configuration parameters
-- args : parameters from frame to pass through
-- params : definitions for each used value
-- Sets a meta table and creates a <div> tag wrapper
-- args : values passed from the Template
-- other fields are initialized in other functions
---- the following values are treated specially: default_version, version, version1, version2...
--]]
--]]
function Infobox.new(args)
function Infobox.new(config, params, args)
local obj = setmetatable({
local obj = setmetatable({
args = args, -- parameters (uncleaned)
args_raw = args, -- parameters (uncleaned)
rargs = {}, -- parameters (cleaned)
args_parsed = {}, -- parsed parameters
params = {}, -- parameters mapped to functions
args_smw = {}, -- parameters parsed for smw
paramnames = {}, -- parameter names
infobox_name = nil, -- template name
dupeable = {}, -- parameters that are allowed to have duplicated switch data
classes = {}, -- list of classes to add, using the default_version
table_class = 'infobox', -- The base class of the infobox
param_names = {}, -- ordered param list
switchfo = false, -- switch infobox? or not?
params = {}, -- param definitions
max_buttons = 6, -- If there are more buttons than the max, the buttons will become a dropdown menu
switchfoattr = {}, -- switch data class changes
default_version = 1, -- default version to populate the infobox
maxbuttons = DEFAULT_MAX_BUTTONS, -- maximum number of buttons before switching becomes a menu
switch_tag = '', -- switchfo data
versions = nil, -- number of versions
version_names = {}, -- title of each version (for selection and SMW)
switch_buttons_tag = '', -- switchfo buttons
rtable = nil, -- infobox table to return at the end
custom_buttons = false,
switch_datatable = '', -- datatable for javascript for switch infoboxes
smw_error_tag = '',
rtable = nil, -- returned infobox table
categories = {}, -- set of categories
labels = nil, -- returned labels
errors = {}, -- list of errors
_smw = {}, -- semantic mediawiki data
__smw_debug = {}, -- debug data for smw subobjects
_smwOne = {}, -- semantic mediawiki data part 2
_smwSubobject = {}, -- semantic mediawiki data part 3
_smwSubobjectAppliesTo = nil, -- semantic mediawiki data part 3.5
_smwElement = {}, -- semantic mediawiki data part 4
setSMWElement = true,
versions = -1, -- number of switch versions (-1 is uncalculated)
infoboxname = nil, -- template name
appendStrs = {},
bottomlinks = { -- template bottom links
links = {
{ 'Template talk:%s', 'talk' },
{ 'Template:%s', 'view' }
},
colspan = 2
},
catdata = {}, -- meta category data
catlist = {}, -- defined table of category names (strings)
__finished = false, -- infobox status
},
},
Infobox)
Infobox)
-- Step 1, setup config vars and count the versions
obj:config(config)
obj:parse_versions()
-- Step 2, process the params
obj:define_params(params)
obj:parse_params()
-- Table
obj:table_header()
obj:buttons()
-- Misc
obj:store_smw()
obj:parse_categories()
return obj
return obj
end
end



--[[
--[[
Refers to a param after being processed by the validating func
Changes the class of the Infobox from 'infobox' to something else
Used in add_row(), define_params() and is_param_defined()
use before :create()
--]]
--]]
function Infobox:setClass(new_class)
function Infobox.param(param_name)
local param = {
if type(arg) == 'string' then
property = 'args_parsed',
self.table_class = new_class
param_name = param_name,
end
}
return param
end
end



--[[
--[[
Refers to a param in raw form as passed from the Template
Creates an infobox
Used in add_row(), define_params() and is_param_defined()
-- If Infobox:maxVersions() has not been run, it will be run here
-- If the infobox should be a switch infobox, all labels will be added
-- Creates a wikitable that will be the infobox
THIS SHOULD BE DONE AFTER ADDING AND CLEANING PARAMETERS
--]]
--]]
function Infobox:create()
function Infobox.raw_param(param_name)
local param = {
-- Run to find if this is a switch infobox and if so, how many boxes
property = 'args_raw',
if self.versions == -1 then
param_name = param_name,
self:maxVersion()
end
}
return param
-- Run if switch infobox
end
if self.switchfo then
-- Buttons wrapper
-- Hidden by default, unhidden by javascript
self.switch_buttons_tag = mw.html.create('div')
:addClass('infobox-buttons')


-- default version to immediately switch to via js
local defv = tonumber(self.args.defver)
if defv and defv <= self.versions then -- you troll, don't try to show something that isn't there
self.switch_buttons_tag:attr('data-default-version',defv)
end

local numlines = 1
local width_working = 0
local total_width = 0
local buttons = {}
-- Add individual buttons to the wrapper
for i=1,self.versions do
local wid = button_width(self.labels[i] or i)
width_working = width_working + wid
total_width = total_width + wid
if width_working > LINE_WIDTH then
numlines = numlines + 1
width_working = wid
end
local b = mw.html.create('span')
:attr('data-switch-index',tostring(i))
-- space to underscore
:attr('data-switch-anchor','#'..string.gsub(self.labels[i] or i,' ','_'))
:addClass('button')
:wikitext(self.labels[i] or i)
table.insert(buttons, {b, wid})
end
local best = {-1, 100000}
if (numlines > 1) and (numlines <= MAX_LINES) then
-- attempt to balance line widths
local w_s, w_e = 0,total_width
for i = 1,#buttons-1 do
w_s = w_s + buttons[i][2]
w_e = w_e - buttons[i][2]
if w_s > LINE_WIDTH then
-- w_s only increases, so we're done once it exceeds the width
break
end
if w_e <= LINE_WIDTH then
-- w_e only decreases, so just continue if it exceeds line
local diff = math.abs(w_s - w_e)
if diff < best[2] then
best = { i, diff }
end
end
end
if best[1] == -1 then
best = { math.floor(#buttons/2), 100000 }
end
end
for i,v in ipairs(buttons) do
self.switch_buttons_tag:node(v[1])
if i == best[1] then
self.switch_buttons_tag:tag('br')
end
end
-- Used by JavaScript to turn the buttons into a menu list if too many variants
if self.versions > self.maxbuttons or numlines > MAX_LINES then
self.switch_buttons_tag:addClass('infobox-buttons-select')
end
self.switch_buttons_tag:done()
end

-- Create infobox table
self.rtable = mw.html.create('table')
:addClass('plainlinks')
:addClass(table_class)
-- Add necessary class if switch infobox
if self.switchfo then
self.rtable:addClass('infobox-switch')
end


--[[
Refers to a param after being processed by the validating smw_func
Used in add_row(), define_params() and is_param_defined()
--]]
function Infobox.smw_param(param_name)
local param = {
property = 'args_smw',
param_name = param_name,
}
return param
end
end


-- Defines an infobox name ({{Template:arg}})
-- Used to create a link at the bottom of pages and for css
function Infobox:defineName(arg)
self.infoboxname = arg
end


--[[
-- Defines the bottom links of the infobox
Checks to see if a param is defined.
-- pass a table whose elements are tables that define a link and a label
-- param: Infobox.param(name), Infobox.raw_param(name) or Infobox.smw_param(name)
-- {
Returns 0 if param is never defined
-- { 'link', 'label },
1 if param is defined for a fraction of the versions
-- ...
2 if param is defined for all versions
-- }
]]
-- The template name can be substituted into the tables using '%s'
function Infobox:is_param_defined(param)
-- If we wanted Template:InFooBar to link to it's /doc page with a "doc" label:
local undefined = 0
-- { ...
local defined = 0
-- { 'Template:%s/doc', 'doc' },
for version = 1, self.versions do
-- ... }
local value = self:get_param(param, version)
-- The template's name can only be called 5 times
if value ~= nil then
function Infobox:defineLinks(arg)
defined = 1
if type(arg) == 'table' then
else
if arg.colspan then
undefined = 1
self.bottomlinks.colspan = arg.colspan
end
if arg.links then
if type(arg.links) == 'table' then
self.bottomlinks.links = arg.links
end
end
if arg.hide then
self.bottomlinks.hide = arg.hide
end
end
end
end
return 1 + defined - undefined
end
end


-- Change max number of buttons before switching to menu
-- defaults to 5
-- MUST BE RUN BEFORE :create()
function Infobox:setMaxButtons(arg)
-- if not a number, just go back to default
self.maxbuttons = tonumber(arg) or DEFAULT_MAX_BUTTONS
end


--[[
--[[
Adds a row to the infobox table
Add parameters functions
Parameter should be a table of cells, where each cell is a table with the following arguments:
All parameters should be tables
-- tag : 'td' or 'th'
The first parameter defines the type of cell to create
-- content : a string or Infobox.param(name), Infobox.raw_param(name), Infobox.smw_param(name)
-- th : <th>
-- attr (optional) : mw.html:attr({ arg1 = '1', ... })
-- td : <td>
-- css (optional) : mw.html:css({ arg1 = '1', ...)
-- argh : <th>
-- class (optional) : mw.html:addClass('arg')
-- argd : <td>
---- class also supports a table of values, even though mw.html:addClass() does not
The second parameter defines what is inside the tag
---- common classes: infobox-subheader
-- th | th : text passed
-- rowspan (optional) : mw.html:attr('rowspan',arg)
-- argh | argd : parameter with the name passed
-- colspan (optional) : mw.html:attr('colspan',arg)
Additional named parameters can be used to add any styling or attributes
-- attr : mw.html:attr({ arg1 = '1', ... })
-- title (optional) : mw.html:attr('title',arg)
-- css : mw.html:css({ arg1 = '1', ...)
-- class : mw.html:addClass('arg')
---- class also supports a table of values, even though mw.html:addClass() does not
-- rowspan : mw.html:attr('rowspan',arg)
-- colspan : mw.html:attr('colspan',arg)
-- title : mw.html:attr('title',arg)
Example:
ipsobox:addRow( { 'th' , 'Header', title = 'Title' },
{ 'argh', 'arg1', class = 'parameter' } })
produces:
<tr><th title="Title">Header</th><th class="parameter">args.arg1</th></tr>


The row itself may be assigned a single class by setting the value of the key addClass
adding it to the infobox table of ipsobox
-- addClass : mw.html:addClass('arg')

Cells defined as 'argh' and 'argd' will automatically have data-attr-param="" added, and defined as the passed argument if the infobox in creation is defined as a switch infobox

The row itself may be modified with metadata using the named index at "meta"
-- meta.addClass : mw.html:addClass('arg')
-- this function currently only supports a single string
-- this function currently only supports a single string
--]]
--]]
function Infobox.addRow(box, ...)
function Infobox:add_row(...)
-- New row to add
local args = ...
local args = ...
local _row = box.rtable:tag('tr')
local _row = self.rtable:tag('tr')
-- For each member of tags
-- For each cell
for i, v in ipairs(args) do
for _, v in ipairs(args) do
local _cell = _row:tag(v.tag)
-- map tag name to appropriate tag, default to <td>
-- Optional parameters
local _cell = _row:tag(tagmap[v.tag] or 'td')

-- mw.html:attr() and mw.html:css() both accept table input
-- colspan, rowspan, title will be quick ways to access attr
-- these functions also do all the necessary work
if v.attr then
if v.attr then
_cell:attr(v.attr)
_cell:attr(v.attr)
Line 391: Line 155:
_cell:css(v.css)
_cell:css(v.css)
end
end

-- if class is a string, it can be added directly
-- if a table, add every value
-- mw.html:addClass() doesn't function with tables
-- so iterate over the class names here and add them individually
if v.class then
if v.class then
if type(v.class) == 'string' then
if type(v.class) == 'string' then
_cell:addClass(v.class)
_cell:addClass(v.class)
-- mw.html:addClass() doesn't function with tables, add in a loop
elseif type(v.class) == 'table' then
elseif type(v.class) == 'table' then
for _, w in ipairs(v.class) do
for _, w in ipairs(v.class) do
Line 405: Line 165:
end
end
end
end
-- Populate the cell contents

local content = self:get_param(v.content, self.default_version)
-- if the cell is a normal th or td, add the exact argument passed
if v.tag == 'th' or v.tag == 'td' then
if content == nil then
_cell:wikitext(v.content)
content = self.params[v.content.param_name].empty
end
-- if defined with "arg", add the argument with name passed
_cell:wikitext(content)
elseif v.tag == 'argh' or v.tag == 'argd' then
-- Add the switch data if multiple values exist
local content = box.rargs[v.content]
local data_attr_param = self:add_switch_data(v.content)
-- if the requested parameter doesn't exist whatsoever, just return a blank string
if not content then
if data_attr_param then
_cell:attr('data-attr-param', data_attr_param)
content = ''
-- If switches exist, first attempt to use the version1 values
elseif content.switches then
if content.switches[1] ~= nil_param then
content = content.switches[1] or ''
else
content = content.d or ''
end
-- fallback to default value
else
content = content.d or ''
end

_cell:wikitext(content)

-- add necessary attribute for switch infoboxes
if box.switchfo then
_cell:attr('data-attr-param',v.content)
end
end
end
end
end

-- not that meta
-- allow classes to be defined on the whole row
-- allow classes to be defined on the whole row
if args.addClass then
-- okay, sort of meta
_row:addClass(args.addClass)
if args.meta then
if args.meta.addClass then
_row:addClass(args.meta.addClass)
end
end
end
return self

return box
end
end


function Infobox.customButtonPlacement(box,arg)
box.custom_buttons = arg
return box
end

function Infobox.addButtonsRow(box, args)
if box.switchfo then
box.custom_buttons = true
local _row = box.rtable:tag('tr')
:addClass('rsw-infobox-switch-buttons-row')
:tag('td')
:addClass('rsw-infobox-switch-buttons')
:attr('colspan', args.colspan)
:node(box.switch_buttons_tag)
end
return box
end
function Infobox.addButtonsCaption(box)
if box.switchfo then
box.custom_buttons = true
local _row = box.rtable:tag('caption')
:addClass('rsw-infobox-switch-buttons-caption')
:node(box.switch_buttons_tag)
end
return box
end


--[[
--[[
-- adds a blank row of padding spanning the given number of columns
Adds a blank row of padding spanning the given number of columns
--]]
--]]
function Infobox.pad(box, colspan, class)
function Infobox:pad(colspan, class)
local tr = box:tag('tr')
local tr = self.rtable:tag('tr'):tag('td'):attr('colspan', colspan or 1):addClass('infobox-padding')
:tag('td'):attr('colspan', colspan or 1):addClass('infobox-padding')
:done()
if class then
if class then
tr:addClass(class)
tr:addClass(class)
end
end
return self
tr:done()
return box
end
end


--[[
--[[
Adds a class to the table as a whole
-- functions the same as mw.html:wikitext() on the wrapper
-- Should only be used for categories really
--]]
--]]
function Infobox.wikitext(box, arg)
function Infobox:addClass(arg)
box.rtable:wikitext(arg)
self.rtable:addClass(arg)
return box
return self
end
end


--[[
-- Adds the specified item(s) to the end of the infobox, outside of the table
-- items are concatenated together with an empty space
--]]
function Infobox.append(box, ...)
for i,v in ipairs({...}) do
table.insert(box.appendStrs, v)
end
return box
end


--[[
--[[
Setup config values
-- Adds a caption to the infobox
-- config: table containing configuration parameters
-- defaults to the pagename
---- infobox_name = mandatory unique identifier for this infobox, used for css
-- or the default argument if defined
---- max_buttons = max number of switch buttons before using a dropdown list instead
--]]
--]]
function Infobox.caption(box)
function Infobox:config(config)
for k, v in pairs(config) do
-- default to the article's name
if k == 'infobox_name' then
local name = pagename
self.infobox_name = mw.ustring.gsub(v, '%s', '_')
-- first see if the name parameter exists
elseif k == 'max_buttons' then
if box.rargs.name then
self.max_buttons = tonumber(v)
-- then try the default
if box.rargs.name.d then
elseif k == 'class' then
if type(v) == 'string' then
name = box.rargs.name.d
self.classes = {v}
-- then look for swithes
elseif box.rargs.name.switches then
elseif type(v) == 'table' then
self.classes = v
-- then look at version 1
if box.rargs.name.switches[1] ~= nil_param then
name = box.rargs.name.switches[1]
end
end
end
end
end
end
if self.infobox_name == nil then

table.insert(self.errors, 'infobox_name needs to be defined in Infobox.new()\'s config!')
local caption = box.rtable:tag('caption')
:wikitext(name)

-- add necessary attribute for switch infoboxes
if box.switchfo then
caption:attr('data-attr-param','name')
end
end
return self

return box
end
end



--[[
--[[
-- Functions for styling the infobox
Counts the number of versions in the infobox
Populates Infobox.version_names
-- works the same as the respective mw.html functions
Sets Infobox.default_version
--]]
--]]
function Infobox:parse_versions()
-- attr
function Infobox.attr(box, arg)
local function insert_version_name(version_name)
table.insert(self.version_names, version_name)
box.rtable:attr(arg)
if smwutils.valid_subobject_name(version_name) == false then
return box
table.insert(self.errors, 'Illegal version value: must not be "0" nor contain a "." in the first five characters')
end
end

end
-- css
function Infobox.float(box,float)
-- Count the versions and setup self.version_names
box.rtable:css('float',float)
local i = 1
return box
while self.args_raw['version'..i] do
end
insert_version_name(self.args_raw['version'..i])

i = i + 1
function Infobox.css(box, ...)
end
box.rtable:css(...)
self.versions = i - 1
return box
-- Should either have 0 or 2+ versions
end
if self.versions == 1 then

table.insert(self.errors, 'There should be multiple versions or no versions. If defining a custom version name for a single entry, use "version=Name" instead of "version1=Name".')
-- addClass
end
function Infobox.addClass(box, arg)
-- Handle the no version case - check for a custom version_name
box.rtable:addClass(arg)
if self.versions == 0 then
return box
insert_version_name(self.args_raw['version'] or 'DEFAULT')
end
self.versions = 1

end
-- Much like Infobox.addClass, but adds multiple classes
-- Check for a default_version
function Infobox.addClasses(box, ...)
if self.args_raw['default_version'] then
for _, v in ipairs(...) do
self.default_version = tonumber(self.args_raw['default_version'])
box.rtable:addClass(box)
if self.default_version > self.versions then -- Make sure the default version exists
self.default_version = 1
end
end
end
return box
end
end


--[[
Add tags directly to the infobox table
Use sparingly
Returns the tag created rather than the entire box
Which is an mw.html object
Further uses of :tag() will be mw.html.tag, rather than Infobox.tag
As such, Infobox:addRow() cannot be used afterwards without restating the infobox as the object
--]]
function Infobox.tag(box, arg)
return box.rtable:tag(arg)
end


--[[
--[[
Define all used parameters (except for default_version/version)
Allows the infobox to use Semantic Media Wiki and give parameters properties
-- name : parameter name as used in the Template
Pass a table to this function to map parameter names to properties
-- func : function to validate and process the Template argument

-- If func is a function, will call func(Infobox.raw_param(name))
Calling syntax:
-- If func is a table, contains the following key-value pairs:
-- {{#show:page|?property}}:
---- name : function
-- "<property>" - unqualified and without a number will display the default value
---- params : list of arguments to pass to the function (use Infobox.raw_param(),
-- "<property#>" - with a number will show the switch data from that index
---- Infobox.param() and Infobox.smw_param() to use arguments)
-- "all <property>" - adding all will display every unique value in a comma separated list
-- empty (optional) : text to display in the infobox if func returns nil, defaults to "? (edit)"

-- category_never : category to add if func returns nil for all versions
Properties initiated in Infobox:finish()
-- category_partial : category to add if func returns nil for some versions, but a value for other versions
-- category_incomplete : category to add if func returns nil for at least 1 version (i.e. category_never and category_partial combined)
-- category_complete : category to add if func returns a value for all versions
-- smw_property (optional) : if this string is defined, the parameter will be saved into smw
-- smw_func (optional) : function to validate and process the Template argument to save into smw
-- func is used by default if smw_func is not defined
--]]
--]]
function Infobox:useSMW(arg)
function Infobox:define_params(...)
-- For every parameter, store its corresponding function to self.params
if type(arg) == 'table' then
for w, v in pairs(arg) do
for _, v in ipairs(...) do
if v.name then
self._smw[w] = v
local param = {}
-- Copy the function
if type(v.func) == 'function' or type(v.func) == 'table' then
param.func = v.func
end
-- If smw_property is defined, then use smw_func, or default to func if it is not defined
if v.smw_property then
param.smw_property = v.smw_property
if type(v.smw_func) == 'function' or type(v.smw_func) == 'table' then
param.smw_func = v.smw_func
else
param.smw_func = param.func
end
end
-- If empty is not defined, default message is "? (edit)"
param.empty = v.empty or editbutton("'''?''' (edit)")
-- Get the category names
param.category_never = v.category_never
param.category_partial = v.category_partial
param.category_incomplete = v.category_incomplete
param.category_complete = v.category_complete
-- Store the param
self.params[v.name] = param
table.insert(self.param_names, v.name)
end
end
end
end
return self
end
end


--[[
--[[
Fetches a param value. If the value is nil, will return the default value instead
As above, but only assigns to "<property>", which will act like "all <property>" - "<property>#" not present
-- arg: a non-table value (string, number, etc), or Infobox.param(name), Infobox.raw_param(name), Infobox.smw_param(name)

-- version: 0/'' for default, or else a number
Properties initiated in Infobox:finish()
Returns arg if a constant
param value (or default value) if Infobox.param(name), Infobox.raw_param(name), Infobox.smw_param(name)
--]]
--]]
function Infobox:useSMWOne(arg)
function Infobox:get_param(arg, version)
if type(arg) == 'table' then
if version == 0 then
version = ''
for w, v in pairs(arg) do
self._smwOne[w] = v
end
end
end
-- Handle Infobox.param(), Infobox.raw_param(), Infobox.smw_param()
end
--[[
Set up the infobox to set properties in a SMW subobject. This will create a subobject for each version
- if there is only one version, it will put the properties directly on to the page, like useSMWOne

Properties initiated in Infobox:finish()
--]]
function Infobox:useSMWSubobject(arg)
if type(arg) == 'table' then
if type(arg) == 'table' then
local value = self[arg.property][arg.param_name..version]
for w, v in pairs(arg) do
-- If nil, grab default value (which could be nil as well)
self._smwSubobject[w] = v
if value == nil then
value = self[arg.property][arg.param_name]
end
end
return value
end
end
-- Everything else passes through unmodified (string, number, etc)
return arg
end
end



function Infobox:useSMWElement(arg)
if type(arg) == 'table' then
for w, v in pairs(arg) do
self._smwElement[w] = v
end
self.setSMWElement = true
end
end
--[[
--[[
Calculates the parsed value of a param
Finishing function
-- param_name : param_name as a string
-- Finishes the return, adding necessary final tags
-- version : 0/'' for default, or else a number
-- smw : boolean, will use smw_func if true, or func is false
--]]
--]]
function Infobox:finish()
function Infobox:parse_param(param_name, version, smw)
if version == 0 then
local onmain = mw.title.getCurrentTitle().namespace == 0
version = ''
-- Don't finish twice
if self.__finished then
return
end
end
-- use func or smw_func depending on smw argument
local param = self.params[param_name]
-- Add switch infobox resources
local func = smw and param.smw_func or param.func
--if self.switchfo then
-- call functions by passing the param_name
self.rtable:attr('data-resource-class', '.infobox-resources-'..string.gsub(tostring(self.infoboxname), ' ', '_'))
if type(func) == 'function' then
-- Wrapper tag, hidden
return func(self:get_param(self.raw_param(param_name), version))
self.switch_tag = mw.html.create('div')
-- call tables by grabbing the function and reading the param arguments
:addClass('infobox-switch-resources')
elseif type(func) == 'table' then
:addClass('infobox-resources-'..string.gsub(tostring(self.infoboxname), ' ', '_'))
local func_name = func.name
:addClass('hidden')
local func_params = func.params

local func_fetched_params = {}
for _, v in ipairs(self.paramnames) do
local param = self.rargs[v]
local i = 1
for _, func_param in ipairs(func_params) do
local default_value = param.d or edit
func_fetched_params[i] = self:get_param(func_param, version)
-- Parameters may not have any switches data, those are ignored
i = i + 1
local switchattr = self.switchfoattr[v]
-- Parameter data wrapper
local res_span = self.switch_tag:tag('span')
:attr('data-attr-param',v)
-- Child for default value
local def = res_span:tag('span')
:attr('data-attr-index',0)
:wikitext(tostring(default_value))

-- Switch classes
if switchattr then
def:attr('data-addclass',switchattr.d)
end

def:done()

if param.switches then
-- Add all switches, ignore those defined as nil
for i, w in ipairs(param.switches) do
if w ~= nil_param and w ~= nil and w ~= default_value then
local _w = res_span:tag('span')
:attr('data-attr-index',i)
:wikitext(tostring(w))
-- Switch classes
if switchattr then
_w:attr('data-addclass',switchattr.switches[i])
end

_w:done()
end
end
res_span:done()
end
end
end
return func_name(unpack(func_fetched_params))

else
-- Add a tracking category for mainspace pages that have more than 1 version
table.insert(self.errors, 'Invalid param definition for '..param_name)
if onmain then
if self.versions > 1 then
-- version count data
self.switch_tag:tag('span')
:wikitext(string.format('Versions: [[Version count::%s]]',self.versions))
:done()
self.switch_tag:wikitext('[[Category:Pages that contain switch infobox data]]')
end
end

self.switch_tag:done()
--end

-- smw data
if onmain then
-- members smw display, yes --> true; no --> false; other --> unknown
local function smwMembers(smw_arg)
local smw_argv = string.lower(smw_arg or '')
if smw_argv == 'yes' then
return 'true'
elseif smw_argv == 'no' then
return 'false'
else
return 'unknown'
end
end

-- release date smw display
local function smwRelease(smw_arg)
local _d,_m,_y = string.match(smw_arg or '', '%[%[(%d%d?) (%a+)%]%] %[%[(%d%d%d%d)%]%]')
if _d == nil then
return nil
end
return table.concat({_d,_m,_y},' ')
end

-- default, just return the text
local function smwDefault(smw_arg)
if smw_arg ~= nil_param and smw_arg ~= edit then
return smw_arg
else
return 'unknown'
end
end

local smw_to_func = {
members = smwMembers,
release = smwRelease,
removal = smwRelease,
default = smwDefault
}
local smw_data_arr = {}

-- custom properties
for w, v in pairs(self._smw) do
-- only needed to give special formatting to release
-- and to make members true/false
local smwfunc = smw_to_func[w] or smw_to_func.default

local curarg = self.rargs[w]
if curarg then
local _arg = curarg.d
local argdefault = _arg
if _arg == edit then
argdefault = 'unknown'
else
local _x = mw.text.split(tostring(_arg), Infobox.splitpoint, true)
if not smw_data_arr[v] then
smw_data_arr[v] = {}
end
if not smw_data_arr['All '..v] then
smw_data_arr['All '..v] = {}
end
for _,y in ipairs(_x) do
local temp_smw_data = smwfunc(y)
table.insert(smw_data_arr[v], temp_smw_data)
table.insert(smw_data_arr['All '..v], temp_smw_data)
end
end

if curarg.switches then
local _args = {}

for _, x in ipairs(curarg.switches) do
if x ~= nil_param then
table.insert(_args,x)
else
table.insert(_args,argdefault or nil_param)
end
end

for i, x in ipairs(_args) do
local _x = mw.text.split(tostring(x), Infobox.splitpoint, true)
if not smw_data_arr[v..i] then
smw_data_arr[v..i] = {}
end
if not smw_data_arr['All '..v] then
smw_data_arr['All '..v] = {}
end
for _,y in ipairs(_x) do
local temp_smw_data = smwfunc(y)
table.insert(smw_data_arr[v..i], temp_smw_data)
table.insert(smw_data_arr['All '..v], temp_smw_data)
end
end
end
end
end
-- if one version, put smwSubobject into smwOne and just do that
if self.versions < 2 and not self._smwSubobjectAppliesTo then
for w,v in pairs(self._smwSubobject) do
if not self._smwOne[w] then
self._smwOne[w] = v
elseif type(self._smwOne[w]) == 'table' then
table.insert(self._smwOne[w], v)
else
self._smwOne[w] = { self._smwOne[w], v }
end
end
end
for w, _v in pairs(self._smwOne) do
-- only needed to give special formatting to release
-- and to make members true/false
local smwfunc = smw_to_func[w] or smw_to_func.default

local curarg = self.rargs[w]
if curarg then
local _arg = curarg.d
local argdefault = _arg
if _arg == edit then
argdefault = 'unknown'
end
if curarg.switches then
local _args = {}

for _, x in ipairs(curarg.switches) do
if x ~= nil_param then
table.insert(_args,x)
else
table.insert(_args,argdefault or nil_param)
end
end

for i, x in ipairs(_args) do
local _x = mw.text.split(tostring(x), Infobox.splitpoint, true)
for _,y in ipairs(_x) do
local temp_smw_data = smwfunc(y)
for _,v in ipairs(type(_v) == 'table' and _v or {_v}) do
if not smw_data_arr[v] then
smw_data_arr[v] = {}
end
table.insert(smw_data_arr[v], temp_smw_data)
end
end
end
else
if Infobox.isDefined(_arg) then
local _targ = mw.text.split(tostring(_arg), Infobox.splitpoint, true)
for _,y in ipairs(_targ) do
local temp_smw_data = smwfunc(y)
for _,v in ipairs(type(_v) == 'table' and _v or {_v}) do
if not smw_data_arr[v] then
smw_data_arr[v] = {}
end
table.insert(smw_data_arr[v], temp_smw_data)
end
end
end
end
end
end
local smw_data_arr_elem = {}
for w, v in pairs(self._smwElement) do
-- only needed to give special formatting to release
-- and to make members true/false
local smwfunc = smw_to_func[w] or smw_to_func.default

local curarg = self.rargs[w]
if curarg then
local _arg = curarg.d
local argdefault = _arg
if _arg == edit then
argdefault = 'unknown'
end
if curarg.switches then
local _args = {}

for _, x in ipairs(curarg.switches) do
if x ~= nil_param then
table.insert(_args,x)
else
table.insert(_args,argdefault or nil_param)
end
end

for i, x in ipairs(_args) do
if Infobox.isDefined(x) then
local _x = mw.text.split(tostring(x), Infobox.splitpoint, true)
for _,y in ipairs(_x) do
local temp_smw_data = smwfunc(y)
if not smw_data_arr_elem[v] then
smw_data_arr_elem[v] = {}
end
table.insert(smw_data_arr_elem[v], temp_smw_data)
end
end
end
else
if Infobox.isDefined(_arg) then
local _targ = mw.text.split(tostring(_arg), Infobox.splitpoint, true)
for _,y in ipairs(_targ) do
local temp_smw_data = smwfunc(y)
if not smw_data_arr_elem[v] then
smw_data_arr_elem[v] = {}
end
table.insert(smw_data_arr_elem[v], temp_smw_data)
end
end
end
end
end
-- if is a switchfo, setup for subobjects
local smw_data_arr_subobj = {}
if self._smwSubobjectAppliesTo then
for i,k in ipairs(self._smwSubobjectAppliesTo) do
local subobj_data = {
['Is variant of'] = {pagename},
}
for w,v in pairs(self._smwSubobject) do
local smwfunc = smw_to_func[w] or smw_to_func.default
local curarg = self.rargs[w]
if curarg then
local argval = curarg.d
if curarg.switches then
argval = curarg.switches[i]
if not Infobox.isDefined(argval) then
argval = curarg.d
end
end
if Infobox.isDefined(argval) then
local _x = mw.text.split(tostring(argval), Infobox.splitpoint, true)
for _, _x1 in ipairs(_x) do
_x1 = smwfunc(_x1)
if _x1 ~= 'unknown' then
if not subobj_data[v] then
subobj_data[v] = {}
end
table.insert(subobj_data[v], _x1)
end
end
end
end
end
for w,v in ipairs(k) do
local subobj_name = v
-- can't have a . in the first 5 characters of a subobject identifier
if mw.ustring.find(mw.ustring.sub(subobj_name,0,5), '%.') then
self:wikitext('[[Category:Pages with an invalid subobject name]]')
subobj_name = mw.ustring.gsub(subobj_name, '%.', '')
end
if subobj_name == '0' or subobj_name == '' then
self:wikitext('[[Category:Pages with an invalid subobject name]]')
subobj_name = 'v_'..subobj_name
end
smw_data_arr_subobj[subobj_name] = subobj_data
end
end
end
local res = mw.smw.set(smw_data_arr)
local res_subobj = true
for w,v in pairs(smw_data_arr_subobj) do
res_subobj = mw.smw.subobject(v, w)
if not res_subobj == true then
break
end
end
if not (res == true and res_subobj == true) then
self.smw_error_tag = mw.html.create('div'):css('display','none'):addClass('infobox-smw-data')
if not res == true then
self.smw_error_tag:tag('div'):wikitext('Error setting SMW properties: '+res.error):done()
end
if not res_subobj == true then
self.smw_error_tag:tag('div'):wikitext('Error setting SMW subobject properties: '+res_subobj.error):done()
end
end
if self.setSMWElement then
if self.smw_error_tag == '' then
self.smw_error_tag = mw.html.create('div'):css('display','none'):addClass('infobox-smw-data')
end
for i,v in pairs(smw_data_arr_elem) do
for j,k in ipairs(v) do
if k ~= 'unknown' then
self.smw_error_tag:tag('span'):wikitext(mw.ustring.format('%s: [[%s::%s]]', i,i,k ))
end
end
end
end
if self._smwSubobjectAppliesTo then
if self.smw_error_tag == '' then
self.smw_error_tag = mw.html.create('div'):css('display','none'):addClass('infobox-smw-data')
end
for w,v in pairs(smw_data_arr_subobj) do
local subobjdiv = self.smw_error_tag:tag('div')
subobjdiv:tag('span'):wikitext('SMW Subobject for '..w)
for j,k in pairs(v) do
subobjdiv:tag('span'):wikitext(mw.ustring.format('%s: %s', j, table.concat(k, ', ')))
end
end
end
end

-- Add view and talk links to infobox
-- Only done if a name is defined
if self.infoboxname and not self.bottomlinks.hide then
local bottom_links = {}
for _, v in ipairs(self.bottomlinks.links) do
table.insert(bottom_links,
string.format(
table.concat({'[[',
v[1],
'|',
v[2],
']]'}),
self.infoboxname,
self.infoboxname,
self.infoboxname,
self.infoboxname,
self.infoboxname)
)
end

bottom_links = table.concat(bottom_links,' &bull; ')
self.rtable:tag('tr'):tag('td')
:addClass('infobox-template-links')
:attr('colspan', self.bottomlinks.colspan)
:wikitext(bottom_links)
:done()
end
end
-- Define as finished
self.__finished = true
end
end



--[[
--[[
Parses all the param values (args_parsed and args_smw)
Function for defining parameters
-- name : parameter name
-- func : function to define param, defaults to looking at blanks
DO NOT DEFINE VERSION HERE
USE :maxVersion()
Can be used any number of times for efficient definition
--]]
--]]
function Infobox:defineParams(...)
function Infobox:parse_params()
-- Calculate the param value for all params and all versions
for _, v in ipairs(...) do
for _, param_name in ipairs(self.param_names) do
-- For every parameter, store its corresponding function to self.params
for version=0, self.versions do
if v.name then
if version == 0 then
-- If the value is a function or a table (which should define a function)
version = ''
if type(v.func) == 'function' or type(v.func) == 'table' then
self.params[v.name] = v.func
-- If the value is a string, use the predefined Infobox function of that name
elseif type(v.func) == 'string' then
self.params[v.name] = func_map[v.func] or hasContent
-- Everything else just looks for blanks
else
self.params[v.name] = hasContent()
end
end
-- Create a list of all param names
-- get the parsed value
self.args_parsed[param_name..version] = self:parse_param(param_name, version, false)
table.insert(self.paramnames,v.name)
-- Only get the smw value if smw_property is defined
-- function to allow duplicated values
if v.dupes then
if self.params[param_name].smw_property then
self.dupeable[v.name] = true
self.args_smw[param_name..version] = self:parse_param(param_name, version, true)
end
end
end
end
end
end
end
end



--[[
--[[
Creates the table header
-- Forces an infobox to only use 1 variant
-- Mainly used by lite infoboxes
-- This should be run before creation
--]]
--]]
function Infobox:noSwitch()
function Infobox:table_header()
-- Create infobox table
self.versions = 1
self.switchfo = false
self.rtable = mw.html.create('table')
:addClass('plainlinks')
:addClass('infobox')
:addClass('infobox-'..self.infobox_name)
for _, class in ipairs(self.classes) do
local class_name = self:get_param(class, self.default_version)
if type(class_name) == 'string' then
self.rtable:addClass('infobox-'..mw.ustring.gsub(class_name, '%s', '_'))
end
end
-- Create the switch datatable if multiple versions
if self.versions > 1 then
self.rtable:addClass('infobox-switch')
self.switch_datatable = mw.html.create('div')
:addClass('infobox-switch-resources')
:addClass('infobox-resources-'..self.infobox_name)
:addClass('hidden')
end
return self
end
end



--[[
--[[
If multiple versions exist, creates a set of buttons for switching
-- Calculates the max version
-- self.max_buttons : if the number of versions exceeds this value, a dropdown will be created instead
-- Adds labels
-- Sees if this needs to be a switch infobox
-- Returns extra version count (even if already run)
-- Sets the smw subobject names from version or smwname
--]]
--]]
function Infobox.maxVersion(box)
function Infobox:buttons()
if self.versions < 2 then
-- Only allowed to run once
return
if box.versions ~= -1 then
return box.versions
end
end
-- Button caption

local buttons = self.rtable:tag('caption')
box.labels = {}
:tag('div')
box.versions = 0
:addClass('infobox-buttons')
local smwappliesto = {}
:attr('data-default-index', self.default_version)

-- Dropdown list instead of buttons if too many versions
-- Look for up to 125 variants, defined in order
if self.versions > self.max_buttons then
for i=1, 125 do
buttons:addClass('infobox-buttons-select')
-- If variant# exists
if box.args['version'..i] then
-- Increase version count
box.versions = box.versions + 1

-- Add its label
local label = box.args['version'..i] or ('Version '..i)
table.insert(box.labels,label)
-- add to appliesto
if box.args['smwname'..i] then
table.insert(smwappliesto, mw.text.split(box.args['smwname'..i], '¦'))
else
table.insert(smwappliesto, {label})
end
else
-- Stop if it doesn't exist
break
end
end
end
-- Create all the buttons

for version=1, self.versions do
-- Define self as a switch infobox if at least 1 other version is found
local button = buttons:tag('span')
if box.versions > 0 then
:attr('data-switch-index', version)
box.switchfo = true
:attr('data-switch-anchor', '#'..self.version_names[version])
box._smwSubobjectAppliesTo = smwappliesto
:addClass('button')
else
:wikitext(self.version_names[version])
-- single version, check for appliesto
-- In case of dropdown list, hide the buttons as the switch gadget will convert them to a dropdown list - we can't directly create the dropdown list here as the select/option tags are rejected by wikitext
if box.args['smwname'] then
if self.versions > self.max_buttons then
box._smwSubobjectAppliesTo = {mw.text.split(box.args['smwname'], '¦')}
button:addClass('hidden')
end
end
end
end

-- versions calculated
return box.versions
end
end



--[[
--[[
For each version, stores a subobject for all params that have smw_property defined
-- Cleans parameters as defined by the above function
-- SHOULD NOT BE RUN UNTIL ALL THINGS ARE DEFINED
-- Handles switches as well
-- adds table _add to rargs, a cleaned up version of arguments
-- d : default value
-- switches : table of switches (in numerical order)
-- Functions can be defined with tables
---- name : name of function
---- params : table of args to pass to functions
---- flag : flags for input
d | #default : use the cleaned parameter first, otherwise passed
p : use the passed value of parameters
r | l : use raw (literal) text, rather than values
-- Defining a single flag will use that flag on all parameters
-- Defining a table of flags will use the respective flag by position
--]]
--]]
function Infobox:cleanParams()
function Infobox:store_smw()
for version=1, self.versions do
-- map of flags to functionality
-- Generate a subobject name
local flagmap = {
-- Reminder - subobject name cannot have a . in the first 5 characters
r = 'r',
local subobject_name = self.version_names[version]
l = 'r',
-- Store each param that has smw_property defined and is not nil
d = 'd',
local subobject = {Infobox = self.infobox_name} -- Also store the current Infobox name
p = 'p'
for _, param_name in ipairs(self.param_names) do
}
local property = self.params[param_name].smw_property
-- For all parameters named
if property then
for _, v in ipairs(self.paramnames) do
local value = self:get_param(self.smw_param(param_name), version)
-- Parameter to add
local _add = {}
if value ~= nil then
subobject[property] = value
local catdata = { all_defined = true, one_defined = false }
-- If the value of params is a function
if type(self.params[v]) == 'function' then
-- Perform that function with the parameter
_add.d = self.params[v](self.args[v])
-- If it's a table, parse it into a function
elseif type(self.params[v]) == 'table' then
-- Find the functions name
local func = self.params[v].name

if type(func) == 'string' then
func = func_map[func]
end

-- catch all
if type(func) ~= 'function' then
func = has_content
end

-- Recreate table of args and flags
local func_args = {}
local flag = {}
-- If the flags are NOT a table, turn them into a table
-- Same size as the parameter table
-- Every flag will be the same
if type(self.params[v].flag) ~= 'table' then
-- Map flags, if unmapped, use default
local _flag = flagmap[self.params[v].flag] or 'd'
-- recreate table
for x=1,#self.params[v].params do
table.insert(flag,_flag)
end
-- If flags are already a table, recreate them in new table
elseif type(self.params[v].flag) == 'table' then
local _flag = self.params[v].flag
-- recreate table
for x=1,#self.params[v].params do
-- Map flags, if unmapped, use default
table.insert(flag,flagmap[_flag[x]] or 'd')
end
end
-- Recreate param table, parsing flags as instructions
for x, w in ipairs(self.params[v].params) do
local xarg
-- By default or defined as 'd'
-- looks for the cleaned value of the named parameter first
-- if it doesn't exist, look at the passed value next
-- if that doesn't exist, use blank
if flag[x] == 'd' then
xarg = self.rargs[w] and self.rargs[w].d
-- compare to nil explicitly because false is a valid value
if xarg == nil then
xarg = self.args[w] or ''
end
-- Look only at the passed value of the named parameter
-- if that doesn't exist, use blank
elseif flag[x] == 'p' then
xarg = self.args[w] or ''
-- Don't interpret value as a parameter name, and paste the as is
elseif flag[x] == 'r' then
xarg = w
end
end
-- Add parsed argument to table
table.insert(func_args,xarg)
end
end
-- Run function
_add.d = func(unpack(func_args))
end
end
-- Keep a copy for debugging

self.__smw_debug[subobject_name] = subobject
if _add.d == nil or _add.d == nil_param then
-- Save subobjects if not in mainspace
-- have to do pagename defaults here to prevent weird behaviour with switch values
if mw.title.getCurrentTitle():inNamespace(0) then
if v == 'name' then
_add.d = pagename
local result = true
if self.versions == 1 then
result = mw.smw.set(subobject)
else
else
result = mw.smw.subobject(subobject, subobject_name)
_add.d = edit
end
if result ~= true then
table.insert(self.errors, 'SMW error: '..result.error)
end
end
catdata.all_defined = false
else
--_add.d is not nil
catdata.one_defined = true
end
end
end
end
if self.switchfo then
-- Table of switches values and count of them
local _add_switch = {}
local switches = 0
-- Look for up to the maximum number
for i=1, self.versions do
local _addarg
-- see if this param is allowed to have switch
if v ~= 'image' and v ~= 'examine' and string.find(self.args[v..i] or '','%$%d') then
local refi = string.match(self.args[v..i],'%$(%d+)')
_addarg = _add_switch[tonumber(refi)] or nil_param
else
-- If the value of params is a function
if type(self.params[v]) == 'function' then
-- Perform that function with the parameter at that index
_addarg = self.params[v](self.args[v..i])
-- If it's a table, parse it into a function
elseif type(self.params[v]) == 'table' then
-- Find the functions name
local func = self.params[v].name




--[[
if type(func) == 'string' then
Automatically add categories for each param as defined in define_params()
func = func_map[func]
--]]
end
function Infobox:parse_categories()

for _, param_name in ipairs(self.param_names) do
-- catch all
local param = self.params[param_name]
if type(func) ~= 'function' then
local defined = self:is_param_defined(Infobox.param(param_name))
func = has_content
if defined == 0 and param.category_never then
end
self.categories[param.category_never] = 1

-- Recreate table of args and flags
local func_args = {}
local flag = {}
-- If the flags are NOT a table, turn them into a table
-- Same size as the parameter table
-- Every flag will be the same
if type(self.params[v].flag) ~= 'table' then
-- Map flags, if unmapped, use default
local _flag = flagmap[self.params[v].flag] or 'd'
-- recreate table
for x=1,#self.params[v].params do
table.insert(flag,_flag)
end
-- If flags are already a table, recreate them in new table
elseif type(self.params[v].flag) == 'table' then
local _flag = self.params[v].flag
-- recreate table
for x=1,#self.params[v].params do
-- Map flags, if unmapped, use default
table.insert(flag,flagmap[_flag[x]] or 'd')
end
end
-- Recreate param table, parsing flags as instructions
for x, w in ipairs(self.params[v].params) do
local xarg
-- By default or defined as 'd'
-- looks for the cleaned value of the named parameter first
-- if it doesn't exist, look at the passed value next
-- if that doesn't exist, look at the default
-- if that doesn't exist, use blank
if flag[x] == 'd' then
if self.rargs[w] then
if self.rargs[w].switches then
xarg = self.rargs[w].switches[i]
else
xarg = self.args[w..i]
end
if xarg == nil or xarg == nil_param then
xarg = self.rargs[w].d
end
end
-- multiple catches in a row just to cover everything
if xarg == nil or xarg == nil_param then
xarg = self.args[w..i]
end
if xarg == nil or xarg == edit or xarg == nil_param then
xarg = self.args[w]
end
if xarg == nil or xarg == edit or xarg == nil_param then
xarg = ''
end
-- Look only at the passed value of the named parameter
-- if that doesn't exist, use unnumbered parameter
-- if that doesn't exist, use blank
elseif flag[x] == 'p' then
xarg = self.args[w..i] or self.args[w] or ''
-- Don't interpret value as a parameter name, and paste the as is
elseif flag[x] == 'r' then
xarg = w
end
-- Add parsed argument to table
table.insert(func_args,xarg)
end
-- Run function
_addarg = func(unpack(func_args))
end
end
-- If not defined, add the nil_param value
-- An actual nil would cause errors in placement
-- So it needs to be filled with an actual value
-- "nil_param" is understood as nil in other functions
-- Include table in case parameter isn't defined by template

if _addarg == nil or _addarg == nil_param then
table.insert(_add_switch, nil_param)
else
switches = switches + 1
table.insert(_add_switch, _addarg)
catdata.one_defined = true
end
end
-- If there are actually other values to switch to
-- Define a switches subtable, otherwise ignore it
if switches > 0 then
_add.switches = _add_switch
end
end
end
if defined == 1 and param.category_partial then

self.categories[param.category_partial] = 1
-- Quick fix for names (which defaults to pagename)
if v == 'name' then
if _add.d == pagename then
if _add.switches and _add.switches[1] ~= pagename and _add.switches[1] ~= nil_param then
_add.d = _add.switches[1]
end
end
end
end
if defined < 2 and param.category_incomplete then

self.categories[param.category_incomplete] = 1
-- Parameter cleaning finished, add to table of cleaned args
self.rargs[v] = _add

-- Category metadata
-- If every param except default is defined, all_defined = true
if catdata.all_defined == false then
if _add.d == edit then
if _add.switches then
catdata.all_defined = true
for _, v in ipairs(_add.switches) do
if v == nil_param then
catdata.all_defined = false
break
end
end
end
end
end
end
if defined == 2 and param.category_complete then

self.catdata[v] = catdata
self.categories[param.category_complete] = 1
end

-- mass dupe removal
-- this needs to be done at the end to keep dependent parameters working
-- also removes incompatible data types
for _, v in ipairs(self.paramnames) do
-- not removed from dupe enabled params parameters
if not self.dupeable[v] and self.rargs[v] and self.rargs[v].switches then
-- tells us whether or not we'll need to remove the switch data
-- switched to false if a switch values does not match the default
local rmvswitch = true
for q, z in ipairs(self.rargs[v].switches) do
-- remove types that don't turn into strings properly
if type(z) == 'table' or type(z) == 'function' then
self.rargs[v].switches[q] = nil_param

-- if it isn't nil or an edit button
-- change variable to keep the switch data
elseif z ~= nil_param and z ~= edit then
rmvswitch = false
end
end
-- remove switch data if everything was a dupe
if rmvswitch then
self.rargs[v].switches = nil
end
end
end
end
end

-- Title parentheses (has to be done here, sadly)
local _name
if self.rargs.name then
_name = self.rargs.name.d
-- replace html entities to their actual character
_name = mw.text.decode(_name)

-- if the page name matches the item name, don't add parentheses
if _name == mw.title.getCurrentTitle().fullText then
self.rtable:addClass('no-parenthesis-style')
end
end
end
end


--[[
Function to link internal use parameters with JS class attribution
-- self:linkParams{ { arg1, arg2 }, ... { arg1a, arg2a } }
-- arg1: parameter name being linked
-- arg2: parameter name that holds the classes
-- THIS FUNCTION SHOULD BE RUN AFTER :cleanParams()
-- THIS FUNCTION SHOULD BE RUN BEFORE :finish()
-- The second argument's data should always contain a value (a CSS class name) at every index
-- This function is cancelled for non switch boxes
--]]
function Infobox:linkParams(...)
if not self.switchfo then
return
end
for _, v in ipairs(...) do
self.switchfoattr[v[1]] = self.rargs[v[2]]
end
end


--[==========================================[
-- Functions for accessing parameters easily
--]==========================================]
--[[
--[[
When a parameter is added into the table, add the alternative versions' data into the switch datatable
Access the param
-- content : a non-table value (string, number, etc), or Infobox.param(name), Infobox.raw_param(name), Infobox.smw_param(name)
-- arg : param name
Returns the name of the parameter in the tag 'data-attr-param'
-- retp : return type
d | #default : self.rargs[arg].d -- Default value
f | full : self.rargs[arg] -- Entire table
s | switches : self.rargs[arg].switches -- Entire switch table
s# : self.rargs[arg].switches[#] -- Single switch value at index #
r : switches[1] or d
--]]
--]]
function Infobox:param(arg, retp)
function Infobox:add_switch_data(content)
if self.versions < 2 then
-- All parameters
return false
if arg == 'all' then
return self.rargs
end
end
-- Only needed for non-constant params
-- case-insensitive flagging
if type(content) ~= 'table' then
retp = tostring(retp):lower()
return false
local fmap = {
d = 'd', default = 'd', s0 = 'd', -- let s-naught count as default (since that's what it is)
f = 'f', full = 'f',
s = 's', switch = 's', switches = 's',
r = 'r'
}
local ret_func
-- quickly see if the parameter is a value greater than 0
if retp:match('s[1-9]') then
ret_func = 's2'
else
-- Otherwise map it to the correct flag, or the default
ret_func = fmap[retp] or fmap.d
end
end
-- Only needed if the param changes value between different versions

local first_value = self:get_param(content, 1)
-- Fetch parameter
local param = self.rargs[arg]
local all_same = true
for version=2, self.versions do
-- Return nil if no table found
if first_value ~= self:get_param(content, version) then
if not param then return nil end
all_same = false

break
-- Return default
end
if ret_func == 'd' then
return param.d
end
end
if all_same then

return false
-- Return full table
if ret_func == 'f' then
return param
end
end
-- Let's build the datatable

-- datum name: Prepend raw__ or smw__ if not a parsed argument
-- Return switch table
local name = content.param_name
if ret_func == 's' then
if content.property == 'args_raw' then
return param.switches
name = 'raw__' + name
end
end
if content.property == 'args_smw' then

name = 'smw__' + name
-- Return the first switch, otherwise the default
if ret_func == 'r' then
if not param.switches then
return param.d
elseif param.switches[1] == nil_param then
return param.d
else
return param.switches[1]
end
end
end
local data_param = self.switch_datatable:tag('span'):attr('data-attr-param', name)

-- If s2, reread the param
-- Add each version to the datatable
for version=1, self.versions do
if ret_func == 's2' then
local text = self:get_param(content, version)
-- no switches
if not param.switches then
if text == nil then
text = self.params[content.param_name].empty
return nil
end
-- Parse index by removing the s
local index = retp:match('s(%d+)')
-- nil_param
if param.switches[index] == nil_param then
return nil
else
return param.switches[index]
end
end
data_param:tag('span'):attr('data-attr-index', version):wikitext(text)
end
end
-- return the 'data-attr-param' name
return name
end
end


--[[
Checks if a parameter is defined and not blank
-- arg : parameter to look at
-- index : index of switches to look at (defaults to default param)
-- defining 'all' will look at every possible value for the parameter
--]]
function Infobox:paramDefined(arg,index)
-- Can use cleaned params for switches
-- but need the passed to identify blanks in the template
local param = self.rargs[arg]
local _arg = self.args[arg]
if string.find(_arg or '','%?action=edit') then
_arg = ''
end
index = index or 0
local ret
-- create a long strong of every value to test for things if 'all'
if string.lower(index) == 'all' then
return self.catdata[arg] and (self.catdata[arg].one_defined or self.catdata[arg].all_defined)
-- index to number otherwise
else
index = tonumber(index) or 0
if index == 0 then
if param.switches then
if Infobox.isDefined(param.switches[1]) then
ret = param.switches[1]
else
ret = _arg
end
else
ret = _arg
end
else
if not param.switches then
return nil
end
if param.switches[index] == nil_param then
return nil
end
ret = param.switches[index]
end
end
return tostring(ret or ''):find('%S')
end


--[[
--[[
Override tostring
Function to perform a search on all parameters of a defined name
-- param: param name
-- val: a value or function
-- functions passed must return either true or false
-- with true being counted as a match
--]]
--]]
function Infobox:paramGrep(param,val)
function Infobox:__tostring()
-- Create error messages
local arg = self.rargs[param]
local error_text = ''
-- if no parameters, return nil
for _, v in ipairs(self.errors) do
if not arg then
error_text = error_text..'<span class="mw-message-box-error">'..v..'</span>'
return nil
end
end
-- Create categories

local ret
local category_text = ''
if mw.title.getCurrentTitle():inNamespace(0) then
local valtype = type(val)
for key, _ in pairs(self.categories) do
-- start with the default value
category_text = category_text..'[[Category:'..key..']]'
-- if it's a function, run it
if valtype == 'function' then
ret = val(arg.d)

-- true means it matched
if ret == true then
return ret
end

-- switches up here for functions
if arg.switches then
for _, v in ipairs(arg.switches) do
ret = val(v)
if ret == true then
return true
end
end
end

-- if it's just a value, compare the two
-- only run if types match to avoid errors
-- compare switches later
elseif valtype == type(arg.d) then
-- if a string, make case insensitive
if valtype == 'string' then
if string.lower(val) == string.lower(arg.d) then
return true
end
-- everything else just test directly
elseif val == arg.d then
return true
end
end
end
end
local dump = ''

if self.args_raw.__dump then
-- switch cases
setmetatable(self, nil)
-- more organized putting them here
dump = '<nowiki>'..mw.dumpObject(self)..'</nowiki>'
if arg.switches then
setmetatable(self, Infobox)
for _, v in ipairs(arg.switches) do
if valtype == type(v) then
if valtype == 'string' then
if val:lower() == v:lower() then
return true
end
elseif val == v then
return true
end
end
end
end
end
return tostring(self.rtable) .. tostring(self.switch_datatable) .. error_text .. category_text .. dump

-- return false in every other case
return false
end
end
------


function Infobox.paramRead(arg,val)
-- if no parameters, return nil
if not arg then
return nil
end


--[[
local ret
Debug function
local valtype = type(val)
--]]
-- start with the default value
-- if it's a function, run it
function Infobox:dump()
-- Temporarily detach the metatable so we don't override tostring anymore, allowing us to use dumpObject
if valtype == 'function' then
setmetatable(self, nil)
ret = val(arg.d)
mw.log(mw.dumpObject(self))

setmetatable(self, Infobox)
-- true means it matched
mw.log(tostring(self))
if ret == true then
return ret
end

-- switches up here for functions
if arg.switches then
for _, v in ipairs(arg.switches) do
ret = val(v)
if ret == true then
return true
end
end
end

-- if it's just a value, compare the two
-- only run if types match to avoid errors
-- compare switches later
elseif valtype == type(arg.d) then
-- if a string, make case insensitive
if valtype == 'string' then
if string.lower(val) == string.lower(arg.d) then
return true
end
-- everything else just test directly
elseif val == arg.d then
return true
end
end

-- switch cases
-- more organized putting them here
if arg.switches then
for _, v in ipairs(arg.switches) do
if valtype == type(v) then
if valtype == 'string' then
if val:lower() == v:lower() then
return true
end
elseif val == v then
return true
end
end
end
end

-- return false in every other case
return false
end
end


----

-- Return collected category data
function Infobox:categoryData()
return self.catdata
end

-- Override tostring
function Infobox.tostring(box)
-- If not finished, finish
if not box.__finished then
box:finish()
end

-- Make entire html wrapper a string and return it
local btns = box.switch_buttons_tag
if box.custom_buttons then
btns = ''
end
if box.args.__dump__ then
return '<' .. 'pre>'..mw.dumpObject(box) .. '</' .. 'pre>[[Category:Dumping infoboxes]]'
end
return tostring(btns) .. tostring(box.rtable) .. table.concat(box.appendStrs, '') .. tostring(box.switch_tag) .. tostring(box.smw_error_tag)
end


return Infobox
return Infobox
-- </nowiki>

Latest revision as of 12:28, 26 November 2024

Module documentation
This documentation is transcluded from Module:Infobox/doc. [edit] [history] [purge]
Module:Infobox requires Module:Edit button.
Module:Infobox requires Module:SMW Utils.
Module:Infobox requires strict.

Creating a template step-by-step

Import Module:Infobox and Module:Param Parse

local Infobox = require('Module:Infobox')
local parse = require('Module:Param Parse')

Unpack the frame arguments from the Template

local p = {}

function p.main(frame)
	local args = frame:getParent().args
	...

Setup the Module config settings

	local config = {
		infobox_name = 'Scenery',
		class = {Infobox.smw_param('episode')}, -- Add css class with episode name to colorize Infobox
	}

Map your arguments to parsing functions

Use the params in Module:Param Parse to validate and format the data - feel free to create your own new params in Module:Param Parse.

	local params = {
    	parse.name,
        parse.image,
		{name = 'description', func = parse.has_content, smw_property = 'Description'}, -- Custom param
        parse.episode,
 		...
	}

Define an Infobox object

	local infobox = Infobox.new(config, params, args)

Create your table

	infobox
		:add_row{
			{tag='th', content=Infobox.param('name'), class='infobox-header', colspan='20'},
		}
		:add_row{
			{tag='td', content=Infobox.param('image'), class='infobox-image', colspan='20'},
		}
		:pad("20")
		:add_row{
			{tag='td', content='Info', class='infobox-subheader', colspan='20'},
		}
		:pad("20")
		:add_row{
			{tag='th', content='Description', colspan="6"},
			{tag='td', content=Infobox.param('description'), colspan="14"},
		}
		:add_row{
			{tag='th', content='[[Episode]]', colspan="6"},
			{tag='td', content=Infobox.param('episode'), colspan="14"},
		}
		...


You're done!

	return infobox
end

return p

Functions

Special params

You don't need to do anything about these special parameters, but they may be used as parameters within the Template:

param explanation
version1, version2, version3 Button label and SMW name for each switch version of the infobox
default_version The default version to display when the page is loaded
version If there is only a single version, you can use version to set the SMW name (default SMW name is "DEFAULT")

Referring to params

Each parameter can have a different value for each version. In addition, there are 3 different representations of each value. Therefore, a parameter must be accessed via one of the 3 helper functions:

helper function example explanation
Infobox.raw_param(name) "1000" Raw value as passed by the Template
Infobox.param(name) "1,000" Value formatted for display in the Infobox
Infobox.smw_param(name) 1000 Value formatted to be saved as an SMW property

Infobox.new(config, params, args)

Creates a new infobox. Automatically parses the arguments, creates SMW subobjects and adds categories.

	local infobox = Infobox.new(config, params, args)

config

There are only 3 parameters for config

	local config = {
    	infobox_name = 'Scenery', -- mandatory unique identifier for css
        class = {'CustomClass', Infobox.smw_param('episode')} -- optional, defaults to {}. Adds css classes to infobox table: {'infobox-CustomClass', 'infobox-[default version parameter's value]'}
    	max_buttons = 6, -- optional, defaults to 6, max number of switch buttons before using a dropdown list instead
	}

params

A list of parameters to be processed by the Infobox module

	local params = {
		{ name = <param>, func = <func>, ... },
		...
	}


key value
name parameter name as used in the Template
func A function in Module:Param Parse to validate and process the Template argument. You can also use a local function, but this is not recommended.

If func is a function, will call func(Infobox.raw_param(name)):

	{name = <param>, func = <func>, ... },

If func is a table, it takes the following parameters:

	{name = <param>, func = { name = <func>, params = <func_params>}, ... },
name function in Module:Param Parse
params a list of parameters to pass the the function, in the form of constants, or Infobox.raw_param(name), Infobox.param(name), Infobox.smw_param(name)
empty (optional) text to display in the infobox if func returns nil; defaults to "? (edit)"
category_never (optional) category to add if func returns nil for all versions
category_partial (optional) category to add if func returns nil for some versions, but a value for other versions
category_incomplete (optional) category to add if func returns nil for at least 1 version (i.e. category_never and category_partial combined)
category_complete (optional) category to add if func returns a value for all versions
smw_property (optional) if this string is defined, the parameter will be saved into SMW
smw_func (optional) function to validate and process the Template argument to save into SMW. func is used by default if smw_func is not defined

args

Arguments passed via the Template

	local args = frame:getParent().args

infobox:is_param_defined(param)

Used to conditionally display a line in the infobox

	infobox:add_row{
		{tag='th', content=Infobox.param('name'), class='infobox.subheader', colspan='2'},
	}
	if infobox:is_param_defined(Infobox.param('owner')) > 0 then
		infobox:add_row{
			{tag='td', content='[[Owner]]'},
			{tag='td', content=Infobox.param('owner')},
		}
	end
param a parameter referenced via Infobox.raw_param(name), Infobox.param(name) or Infobox.smw_param(name)
Returns 0 if param is never defined

1 if param is defined for a fraction of the versions

2 if param is defined for all versions

Infobox:add_row(...)

Adds a row to the infobox table. Parameter should be a table of cells:

	infobox:add_row{
		{tag='td', content='[[Cell1]]', ...},
		{tag='td', content=Infobox.param('param'), ...},
		{tag='td', content='[[Cell3]]', ...},
        ...
        addClass = 'row-class'
	}

Each cell should have a set of key-values, of which only the tag and content are mandatory:

key value
tag 'td' or 'th'
content a string or Infobox.param(name), Infobox.raw_param(name), Infobox.smw_param(name)
attr (optional) a table of attributes to add to the cell

mw.html:attr({ arg1 = '1', ... })

css (optional) a table of css to add to the cell

mw.html:css({ arg1 = '1', ... })

class (optional) a string with a class to add, or a table of classes to add. infobox-header, infobox-subheader and infobox-image are commonly used.

mw.html:addClass(...)

rowspan (optional) Set the cell rowspan

mw.html:attr('rowspan',arg)

rowspan (optional) Set the cell colspan

mw.html:attr('colspan',arg)

title (optional) Set the cell title

mw.html:attr('title',arg)


Infobox:pad(colspan, class)

Adds a blank row of padding spanning the given number of columns. Will always add the class infobox-padding, but can optionally add another class:

	infobox:pad("10", class=<class>)


Infobox:addClass(class)

Adds a class to the entire table

	infobox:addClass(class)


Infobox:dump()

Logs all the values into the Debug console for debugging purposes. You can also dump all the values in an Infobox template by setting a template parameter "__dump = Yes".


--[=[
-- For documentation, see [[Module:Infobox/doc]]
--]=]

-- <nowiki>
require('strict')
local editbutton = require('Module:Edit button')
local smwutils = require('Module:SMW Utils')

local Infobox = {}
Infobox.__index = Infobox

--[[
	Infobox class
	-- config: table containing configuration parameters
	-- params : definitions for each used value
	-- args : values passed from the Template
	---- the following values are treated specially: default_version, version, version1, version2...
--]]
function Infobox.new(config, params, args)
	local obj = setmetatable({
				args_raw = args, -- parameters (uncleaned)
				args_parsed = {}, -- parsed parameters
				args_smw = {}, -- parameters parsed for smw
				infobox_name = nil, -- template name
				classes = {}, -- list of classes to add, using the default_version
				param_names = {}, -- ordered param list
				params = {}, -- param definitions
				max_buttons = 6, -- If there are more buttons than the max, the buttons will become a dropdown menu
				default_version = 1, -- default version to populate the infobox
				versions = nil, -- number of versions
				version_names = {}, -- title of each version (for selection and SMW)
				rtable = nil, -- infobox table to return at the end
				switch_datatable = '', -- datatable for javascript for switch infoboxes
				categories = {}, -- set of categories
				errors = {}, -- list of errors
				__smw_debug = {}, -- debug data for smw subobjects
				},
			Infobox)
	-- Step 1, setup config vars and count the versions
	obj:config(config)
	obj:parse_versions()
	-- Step 2, process the params
	obj:define_params(params)
	obj:parse_params()
	-- Table
	obj:table_header()
	obj:buttons()
	-- Misc
	obj:store_smw()
	obj:parse_categories()
	return obj
end


--[[
	Refers to a param after being processed by the validating func
	Used in add_row(), define_params() and is_param_defined()
--]]
function Infobox.param(param_name)
	local param = {
		property = 'args_parsed',
		param_name = param_name,
	}
	return param
end


--[[
	Refers to a param in raw form as passed from the Template
	Used in add_row(), define_params() and is_param_defined()
--]]
function Infobox.raw_param(param_name)
	local param = {
		property = 'args_raw',
		param_name = param_name,
	}
	return param
end


--[[
	Refers to a param after being processed by the validating smw_func
	Used in add_row(), define_params() and is_param_defined()
--]]
function Infobox.smw_param(param_name)
	local param = {
		property = 'args_smw',
		param_name = param_name,
	}
	return param
end


--[[
	Checks to see if a param is defined.
	-- param: Infobox.param(name), Infobox.raw_param(name) or Infobox.smw_param(name)
	Returns 0 if param is never defined
	        1 if param is defined for a fraction of the versions
			2 if param is defined for all versions
]]
function Infobox:is_param_defined(param)
	local undefined = 0
	local defined = 0
	for version = 1, self.versions do
		local value = self:get_param(param, version)
		if value ~= nil then
			defined = 1
		else
			undefined = 1
		end
	end
	return 1 + defined - undefined
end


--[[
	Adds a row to the infobox table
	Parameter should be a table of cells, where each cell is a table with the following arguments:
	-- tag : 'td' or 'th'
	-- content : a string or Infobox.param(name), Infobox.raw_param(name), Infobox.smw_param(name)
	-- attr (optional) : mw.html:attr({ arg1 = '1', ... })
	-- css (optional) : mw.html:css({ arg1 = '1', ...)
	-- class (optional) : mw.html:addClass('arg')
	---- class also supports a table of values, even though mw.html:addClass() does not
	---- common classes: infobox-subheader
	-- rowspan (optional) : mw.html:attr('rowspan',arg)
	-- colspan (optional) : mw.html:attr('colspan',arg)
	-- title (optional) : mw.html:attr('title',arg)

	The row itself may be assigned a single class by setting the value of the key addClass
		-- addClass : mw.html:addClass('arg')
		-- this function currently only supports a single string
--]]
function Infobox:add_row(...)
	local args = ...
	local _row = self.rtable:tag('tr')
	-- For each cell
	for _, v in ipairs(args) do
		local _cell = _row:tag(v.tag)
		-- Optional parameters
		if v.attr then
			_cell:attr(v.attr)
		end
		if v.colspan then
			_cell:attr('colspan',v.colspan)
		end
		if v.rowspan then
			_cell:attr('rowspan',v.rowspan)
		end
		if v.title then
			_cell:attr('title',v.title)
		end
		if v.css then
			_cell:css(v.css)
		end
		if v.class then
			if type(v.class) == 'string' then
				_cell:addClass(v.class)
			-- mw.html:addClass() doesn't function with tables, add in a loop
			elseif type(v.class) == 'table' then
				for _, w in ipairs(v.class) do
					_cell:addClass(w)
				end
			end
		end
		-- Populate the cell contents
		local content = self:get_param(v.content, self.default_version)
		if content == nil then
			content = self.params[v.content.param_name].empty
		end
		_cell:wikitext(content)
		-- Add the switch data if multiple values exist
		local data_attr_param = self:add_switch_data(v.content)
		if data_attr_param then
			_cell:attr('data-attr-param', data_attr_param)
		end
	end
	-- allow classes to be defined on the whole row
	if args.addClass then
		_row:addClass(args.addClass)
	end
	return self
end


--[[
	Adds a blank row of padding spanning the given number of columns
--]]
function Infobox:pad(colspan, class)
	local tr = self.rtable:tag('tr'):tag('td'):attr('colspan', colspan or 1):addClass('infobox-padding')
	if class then
		tr:addClass(class)	
	end
	return self
end

--[[
	Adds a class to the table as a whole
--]]
function Infobox:addClass(arg)
	self.rtable:addClass(arg)
	return self
end


--[[
	Setup config values
	-- config: table containing configuration parameters
	---- infobox_name = mandatory unique identifier for this infobox, used for css
	---- max_buttons = max number of switch buttons before using a dropdown list instead
--]]
function Infobox:config(config)
	for k, v in pairs(config) do
		if k == 'infobox_name' then
			self.infobox_name = mw.ustring.gsub(v, '%s', '_')
		elseif k == 'max_buttons' then
			self.max_buttons = tonumber(v)
		elseif k == 'class' then
			if type(v) == 'string' then
				self.classes = {v}
			elseif type(v) == 'table' then
				self.classes = v
			end
		end
	end
	if self.infobox_name == nil then
		table.insert(self.errors, 'infobox_name needs to be defined in Infobox.new()\'s config!')
	end
	return self
end


--[[
	Counts the number of versions in the infobox
	Populates Infobox.version_names
	Sets Infobox.default_version
--]]
function Infobox:parse_versions()
	local function insert_version_name(version_name)
		table.insert(self.version_names, version_name)
		if smwutils.valid_subobject_name(version_name) == false then
			table.insert(self.errors, 'Illegal version value: must not be "0" nor contain a "." in the first five characters')
		end
	end
		
	-- Count the versions and setup self.version_names
	local i = 1
	while self.args_raw['version'..i] do
		insert_version_name(self.args_raw['version'..i])
		i = i + 1
	end
	self.versions = i - 1
	-- Should either have 0 or 2+ versions
	if self.versions == 1 then
		table.insert(self.errors, 'There should be multiple versions or no versions. If defining a custom version name for a single entry, use "version=Name" instead of "version1=Name".')
	end
	-- Handle the no version case - check for a custom version_name
	if self.versions == 0 then
		insert_version_name(self.args_raw['version'] or 'DEFAULT')
		self.versions = 1
	end
	-- Check for a default_version
	if self.args_raw['default_version'] then
		self.default_version = tonumber(self.args_raw['default_version'])
		if self.default_version > self.versions then -- Make sure the default version exists
			self.default_version = 1
		end
	end
end


--[[
	Define all used parameters (except for default_version/version)
	-- name : parameter name as used in the Template
	-- func : function to validate and process the Template argument
	--        If func is a function, will call func(Infobox.raw_param(name))
	--        If func is a table, contains the following key-value pairs:
	----          name : function
	----          params : list of arguments to pass to the function (use Infobox.raw_param(),
	----                   Infobox.param() and Infobox.smw_param() to use arguments)
	-- empty (optional) : text to display in the infobox if func returns nil, defaults to "? (edit)"
	-- category_never : category to add if func returns nil for all versions
	-- category_partial : category to add if func returns nil for some versions, but a value for other versions
	-- category_incomplete : category to add if func returns nil for at least 1 version (i.e. category_never and category_partial combined)
	-- category_complete : category to add if func returns a value for all versions
	-- smw_property (optional) : if this string is defined, the parameter will be saved into smw
	-- smw_func (optional) : function to validate and process the Template argument to save into smw
	--                      func is used by default if smw_func is not defined
--]]
function Infobox:define_params(...)
	-- For every parameter, store its corresponding function to self.params
	for _, v in ipairs(...) do
		if v.name then
			local param = {}
			-- Copy the function
			if type(v.func) == 'function' or type(v.func) == 'table' then
				param.func = v.func
			end
			-- If smw_property is defined, then use smw_func, or default to func if it is not defined
			if v.smw_property then
				param.smw_property = v.smw_property
				if type(v.smw_func) == 'function' or type(v.smw_func) == 'table' then
					param.smw_func = v.smw_func
				else
					param.smw_func = param.func
				end
			end
			-- If empty is not defined, default message is "? (edit)"
			param.empty = v.empty or editbutton("'''?''' (edit)")
			-- Get the category names
			param.category_never = v.category_never
			param.category_partial = v.category_partial
			param.category_incomplete = v.category_incomplete
			param.category_complete = v.category_complete
			-- Store the param
			self.params[v.name] = param
			table.insert(self.param_names, v.name)
		end
	end
	return self
end


--[[
	Fetches a param value. If the value is nil, will return the default value instead
	-- arg: a non-table value (string, number, etc), or Infobox.param(name), Infobox.raw_param(name), Infobox.smw_param(name)
	-- version: 0/'' for default, or else a number
	Returns arg if a constant
	        param value (or default value) if Infobox.param(name), Infobox.raw_param(name), Infobox.smw_param(name)
--]]
function Infobox:get_param(arg, version)
	if version == 0 then
		version = ''
	end
	-- Handle Infobox.param(), Infobox.raw_param(), Infobox.smw_param()
	if type(arg) == 'table' then
		local value = self[arg.property][arg.param_name..version]
		-- If nil, grab default value (which could be nil as well)
		if value == nil then
			value = self[arg.property][arg.param_name]
		end
		return value
	end
	-- Everything else passes through unmodified (string, number, etc)
	return arg
end


--[[
	Calculates the parsed value of a param
	-- param_name : param_name as a string
	-- version : 0/'' for default, or else a number
	-- smw : boolean, will use smw_func if true, or func is false
--]]
function Infobox:parse_param(param_name, version, smw)
	if version == 0 then
		version = ''
	end
	-- use func or smw_func depending on smw argument
	local param = self.params[param_name]
	local func = smw and param.smw_func or param.func
	-- call functions by passing the param_name
	if type(func) == 'function' then
		return func(self:get_param(self.raw_param(param_name), version))
	-- call tables by grabbing the function and reading the param arguments
	elseif type(func) == 'table' then
		local func_name = func.name
		local func_params = func.params
		local func_fetched_params = {}
		local i = 1
		for _, func_param in ipairs(func_params) do
			func_fetched_params[i] = self:get_param(func_param, version)
			i = i + 1
		end
		return func_name(unpack(func_fetched_params))
	else
		table.insert(self.errors, 'Invalid param definition for '..param_name)
	end
end


--[[
	Parses all the param values (args_parsed and args_smw)
--]]
function Infobox:parse_params()
	-- Calculate the param value for all params and all versions
	for _, param_name in ipairs(self.param_names) do
		for version=0, self.versions do
			if version == 0 then
				version = ''
			end
			-- get the parsed value
			self.args_parsed[param_name..version] = self:parse_param(param_name, version, false)
			-- Only get the smw value if smw_property is defined
			if self.params[param_name].smw_property then
				self.args_smw[param_name..version] = self:parse_param(param_name, version, true)
			end
		end
	end
end


--[[
	Creates the table header
--]]
function Infobox:table_header()
	-- Create infobox table
	self.rtable = mw.html.create('table')
					:addClass('plainlinks')
					:addClass('infobox')
					:addClass('infobox-'..self.infobox_name)
	for _, class in ipairs(self.classes) do
		local class_name = self:get_param(class, self.default_version)
		if type(class_name) == 'string' then
			self.rtable:addClass('infobox-'..mw.ustring.gsub(class_name, '%s', '_'))
		end
	end
	-- Create the switch datatable if multiple versions
	if self.versions > 1 then
		self.rtable:addClass('infobox-switch')
		self.switch_datatable = mw.html.create('div')
									:addClass('infobox-switch-resources')
									:addClass('infobox-resources-'..self.infobox_name)
									:addClass('hidden')
	end
	return self
end


--[[
	If multiple versions exist, creates a set of buttons for switching
	-- self.max_buttons : if the number of versions exceeds this value, a dropdown will be created instead
--]]
function Infobox:buttons()
	if self.versions < 2 then
		return
	end
	-- Button caption
	local buttons = self.rtable:tag('caption')
								:tag('div')
								:addClass('infobox-buttons')
								:attr('data-default-index', self.default_version)
	-- Dropdown list instead of buttons if too many versions
	if self.versions > self.max_buttons then
		buttons:addClass('infobox-buttons-select')
	end
	-- Create all the buttons
	for version=1, self.versions do
		local button = buttons:tag('span')
			   :attr('data-switch-index', version)
			   :attr('data-switch-anchor', '#'..self.version_names[version])
			   :addClass('button')
			   :wikitext(self.version_names[version])
	    -- In case of dropdown list, hide the buttons as the switch gadget will convert them to a dropdown list - we can't directly create the dropdown list here as the select/option tags are rejected by wikitext
		if self.versions > self.max_buttons then
			button:addClass('hidden')
		end
	end
end


--[[
	For each version, stores a subobject for all params that have smw_property defined
--]]
function Infobox:store_smw()
	for version=1, self.versions do
		-- Generate a subobject name
		-- Reminder - subobject name cannot have a . in the first 5 characters
		local subobject_name = self.version_names[version]
		-- Store each param that has smw_property defined and is not nil
		local subobject = {Infobox = self.infobox_name} -- Also store the current Infobox name
		for _, param_name in ipairs(self.param_names) do
			local property = self.params[param_name].smw_property
			if property then
				local value = self:get_param(self.smw_param(param_name), version)
				if value ~= nil then
					subobject[property] = value
				end
			end
		end
		-- Keep a copy for debugging
		self.__smw_debug[subobject_name] = subobject
		-- Save subobjects if not in mainspace
		if mw.title.getCurrentTitle():inNamespace(0) then
			local result = true
			if self.versions == 1 then
				result = mw.smw.set(subobject)
			else
				result = mw.smw.subobject(subobject, subobject_name)
			end
			if result ~= true then
				table.insert(self.errors, 'SMW error: '..result.error)
			end
		end
	end
end


--[[
	Automatically add categories for each param as defined in define_params()
--]]
function Infobox:parse_categories()
	for _, param_name in ipairs(self.param_names) do
		local param = self.params[param_name]
		local defined = self:is_param_defined(Infobox.param(param_name))
		if defined == 0 and param.category_never then
			self.categories[param.category_never] = 1
		end
		if defined == 1 and param.category_partial then
			self.categories[param.category_partial] = 1
		end
		if defined < 2 and param.category_incomplete then
			self.categories[param.category_incomplete] = 1
		end
		if defined == 2 and param.category_complete then
			self.categories[param.category_complete] = 1
		end
	end
end


--[[
	When a parameter is added into the table, add the alternative versions' data into the switch datatable
	-- content : a non-table value (string, number, etc), or Infobox.param(name), Infobox.raw_param(name), Infobox.smw_param(name)
	Returns the name of the parameter in the tag 'data-attr-param'
--]]
function Infobox:add_switch_data(content)
	if self.versions < 2 then
		return false
	end
	-- Only needed for non-constant params
	if type(content) ~= 'table' then
		return false
	end
	-- Only needed if the param changes value between different versions
	local first_value = self:get_param(content, 1)
	local all_same = true
	for version=2, self.versions do
		if first_value ~= self:get_param(content, version) then
			all_same = false
			break
		end
	end
	if all_same then
		return false
	end
	-- Let's build the datatable
	-- datum name: Prepend raw__ or smw__ if not a parsed argument
	local name = content.param_name
	if content.property == 'args_raw' then
		name = 'raw__' + name
	end
	if content.property == 'args_smw' then
		name = 'smw__' + name
	end
	local data_param = self.switch_datatable:tag('span'):attr('data-attr-param', name)
	-- Add each version to the datatable
	for version=1, self.versions do
		local text = self:get_param(content, version)
		if text == nil then
			text = self.params[content.param_name].empty
		end
		data_param:tag('span'):attr('data-attr-index', version):wikitext(text)
	end
	-- return the 'data-attr-param' name
	return name
end


--[[
	Override tostring
--]]
function Infobox:__tostring()
	-- Create error messages
	local error_text = ''
	for _, v in ipairs(self.errors) do
		error_text = error_text..'<span class="mw-message-box-error">'..v..'</span>'
	end
	-- Create categories
	local category_text = ''
	if mw.title.getCurrentTitle():inNamespace(0) then
		for key, _ in pairs(self.categories) do
			category_text = category_text..'[[Category:'..key..']]'
		end
	end
	local dump = ''
	if self.args_raw.__dump then
		setmetatable(self, nil)
		dump = '<nowiki>'..mw.dumpObject(self)..'</nowiki>'
		setmetatable(self, Infobox)
	end
	return tostring(self.rtable) .. tostring(self.switch_datatable) .. error_text .. category_text .. dump
end


--[[
	Debug function
--]]
function Infobox:dump()
	-- Temporarily detach the metatable so we don't override tostring anymore, allowing us to use dumpObject
	setmetatable(self, nil)
	mw.log(mw.dumpObject(self))
	setmetatable(self, Infobox)
	mw.log(tostring(self))
end


return Infobox