Module:Infobox: Difference between revisions

From Brighter Shores Wiki
Jump to navigation Jump to search
Content added Content deleted
(Fix tabs)
(Clean up, annotated. Finalized, hopefully not many bugs)
Line 6: Line 6:
-- Edit button for unknown params
-- Edit button for unknown params
local editbutton = require('Module:Edit button')
local editbutton = require('Module:Edit button')
local edit = editbutton("'''?''' (edit)")

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


local Infobox = {}
local Infobox = {}
Infobox.__index = Infobox
Infobox.__index = Infobox



--[[
--[[
Infobox class
Infobox class
-- config: table containing configuration parameters
-- infobox_name: the name of the infobox
-- args : parameters from frame to pass through
-- params : definitions for each used value
-- args : values passed from the Template
-- Special args: version, default_version
---- the following values are treated specially: default_version, version, version1, version2...
-- Sets a meta table and creates a <div> tag wrapper
-- other fields are initialized in other functions
--]]
--]]
function Infobox.new(config, params, args)
function Infobox.new(config, params, args)
Line 32: Line 29:
default_version = 1, -- default version to populate the infobox
default_version = 1, -- default version to populate the infobox
versions = nil, -- number of versions
versions = nil, -- number of versions
version_names = {'INFOBOX_ERROR'}, -- title of each version (for selection and SMW); version_name[0] should not be used (base index should be 1)
version_names = {}, -- title of each version (for selection and SMW)
rtable = nil, -- infobox table to return at the end
rtable = nil, -- infobox table to return at the end
switch_datatable = nil, -- datatable for javascript for switch infoboxes
switch_datatable = nil, -- datatable for javascript for switch infoboxes
categories = {}, -- set of categories
errors = {}, -- list of errors
errors = {}, -- list of errors
__smw_debug = {}, -- debug data for smw subobjects
},
},
Infobox)
Infobox)
-- Step 1, setup config vars and count the versions
obj:config(config)
obj:config(config)
obj:parse_versions()
obj:parse_versions()
-- Step 2, process the params
obj:create()
obj:define_params(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




--[[
function Infobox:config(...)
Refers to a param after being processed by the validating func
for k, v in pairs(...) do
Used in add_row(), define_params() and is_param_defined()
if k == 'infobox_name' then
--]]
self.infobox_name = mw.ustring.gsub(v, '%s', '_')
function Infobox.param(param_name)
elseif k == 'max_buttons' then
param = {
self.max_buttons = tonumber(v)
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)
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)
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
end
end
return 1 + defined - undefined
if self.infobox_name == nil then
end
table.insert(self.errors, 'infobox_name needs to be defined in Infobox.new()\'s config!')


--[[
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
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
end
return self
return self
end
end



--[[
--[[
Adds a blank row of padding spanning the given number of columns
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:pad(colspan, class)
local tr = self:tag('tr'):tag('td'):attr('colspan', colspan or 1):addClass('infobox-padding')
if class then
tr:addClass(class)
end
return self
end



function Infobox:define_params(...)
--[[
for _, v in ipairs(...) do
Adds a class to the table as a whole
-- For every parameter, store its corresponding function to self.params
--]]
if v.name then
function Infobox:addClass(arg)
local param = {}
self.rtable:addClass(arg)
-- Copy the function
return self
if type(v.func) == 'function' or type(v.func) == 'table' then
end
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
Setup config values
param.smw_property = v.smw_property
-- config: table containing configuration parameters
if type(v.smw_func) == 'function' or type(v.smw_func) == 'table' then
---- infobox_name = mandatory unique identifier for this infobox, used for css
param.smw_func = v.smw_func
---- max_buttuons = max number of switch buttons before using a dropdown list instead
else
--]]
param.smw_func = param.func
function Infobox:config(config)
end
for k, v in pairs(config) do
end
if k == 'infobox_name' then
self.params[v.name] = param
self.infobox_name = mw.ustring.gsub(v, '%s', '_')
table.insert(self.param_names, v.name)
elseif k == 'max_buttons' then
self.max_buttons = tonumber(v)
end
end
end
end
if self.infobox_name == nil then
self:parse_params()
table.insert(self.errors, 'infobox_name needs to be defined in Infobox.new()\'s config!')
--self:parse_categories()
end
return self
return self
end
end



--[[
--[[
Counts the number of versions in the infobox, and populates version_names
Counts the number of versions in the infobox
Populates Infobox.version_names
Sets Infobox.default_version
--]]
--]]
function Infobox:parse_versions()
function Infobox:parse_versions()
-- Count the versions
-- Count the versions and setup self.version_names
local i = 1
local i = 1
while self.args_raw['version'..i] do
while self.args_raw['version'..i] do
Line 107: Line 239:
end
end
self.versions = i - 1
self.versions = i - 1
-- Handle the no version case - might have a custom version_name
-- Handle the no version case - check for a custom version_name
if self.versions == 0 then
if self.versions == 0 then
table.insert(self.version_names, self.args_raw['version'] or 'DEFAULT')
table.insert(self.version_names, self.args_raw['version'] or 'DEFAULT')
end
end
-- Should either have 0 or 2+ versions
if self.versions == 1 then
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".')
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".')
self.versions = 0
self.versions = 0
end
end
-- Check for a default_version
if self.args_raw['default_version'] then
if self.args_raw['default_version'] then
self.default_version = tonumber(self.args_raw['default_version'])
self.default_version = tonumber(self.args_raw['default_version'])
Line 123: Line 257:
end
end


-- Value is not defined if "action=edit" exists in string, otherwise
-- value is defined if not nil and non-empty string
function Infobox.is_value_defined(value)
if value == nil then
return false
end
if type(value) ~= 'string' then
return true
end
if value:find('action=edit') then
return false
end
if value:find('%S') then
return true
end
return false
end


--[[
--[[
Define all used parameters (except for default_version/version)
Checks to see if a param is defined.
-- name : parameter name as used in the Template
Returns 0 if param is never defined. Returns 1 if param is defined for some versions. Returns 2 if param is defined for all versions.
-- func : function to validate and process the Template argument
-- param: a table generated from Infobox:param(), Infobox:raw_param() or Infobox:smw_param()
-- If func is a function, will call func(Infobox.raw_param(name))
]]
-- If func is a table, contains the following key-value pairs:
function Infobox:is_param_defined(param)
---- name : function
local undefined = 0
---- params : list of arguments to pass to the function (use Infobox.raw_param(),
local defined = 0
---- Infobox.param() and Infobox.smw_param() to use arguments)
for version = 1, self.versions do
-- empty (optional) : text to display in the infobox if func returns nil, defaults to "? (edit)"
local value = self:get_param(param, version)
-- category_never : category to add if func returns nil for all versions
if self.is_value_defined(value) then
-- category_partial : category to add if func returns nil for some versions, but a value for other versions
defined = 1
-- category_incomplete : category to add if func returns nil for at least 1 version (i.e. category_never and category_partial combined)
else
-- category_complete : category to add if func returns a value for all versions
undefined = 1
-- 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
end
end
return 1 + defined - undefined
return self
end
end



--[[
--[[
Fetches a param value. If the value is nil, will return the default value instead
Fetches a param value. If the value is nil, will return the default value instead
-- arg: a table generated from Infobox:param(), Infobox:raw_param() or Infobox:smw_param(), or else
-- arg: a non-table value (string, number, etc), or Infobox.param(name), Infobox.raw_param(name), Infobox.smw_param(name)
-- version: '' for default, or else a number
-- 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)
function Infobox:get_param(arg, version)
Line 169: Line 321:
version = ''
version = ''
end
end
-- Handle Infobox.param(), Infobox.raw_param(), Infobox.smw_param()
if type(arg) == 'table' then
if type(arg) == 'table' then
local value = self[arg.property][arg.param_name..version]
local value = self[arg.property][arg.param_name..version]
if value == nil then -- Try to get default value if it exists
-- If nil, grab default value (which could be nil as well)
if value == nil then
value = self[arg.property][arg.param_name]
value = self[arg.property][arg.param_name]
end
end
return value
return value
end
end
-- Everything else passes through unmodified (string, number, etc)
-- Other (int, string)
return arg
return arg
end
end



--[[
--[[
Calculates the parsed value of a param
Calculates the parsed value of a param
-- param_name : string, name of the param
-- param_name : param_name as a string
-- version : 0/'' for default, or else a number
-- version : 0/'' for default, or else a number
-- smw : boolean, whether to use the smw function or default function
-- smw : boolean, will use smw_func if true, or func is false
--]]
--]]
function Infobox:parse_param(param_name, version, smw)
function Infobox:parse_param(param_name, version, smw)
Line 211: Line 366:




--[[
Parses all the param values (args_parsed and args_smw)
--]]
function Infobox:parse_params()
function Infobox:parse_params()
-- Calculate the param value for all params and all versions
-- Calculate the param value for all params and all versions
Line 216: Line 374:
for version=0, self.versions do
for version=0, self.versions do
if version == 0 then
if version == 0 then
version = '' -- default version
version = ''
end
end
-- Only get the parsed value if the raw value is defined
-- get the parsed value
if self.args_raw[param_name..version] then
self.args_parsed[param_name..version] = self:parse_param(param_name, version, false)
-- Only get the smw value if smw_property is defined
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
if self.params[param_name].smw_property then
self.args_smw[param_name..version] = self:parse_param(param_name, version, true)
self.args_smw[param_name..version] = self:parse_param(param_name, version, true)
end
end
end
end
end
Line 231: Line 387:




--[[
function Infobox:store_smw()
Creates the table header
-- Abort if not in mainspace
--]]
if mw.title.getCurrentTitle().namespace ~= 0 then
function Infobox:table_header()
return false
-- Create infobox table
self.rtable = mw.html.create('table')
:addClass('plainlinks')
:addClass('infobox')
:addClass('infobox-'..self.infobox_name)
-- 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')
self.switch_datatable:tag('span'):wikitext('Versions: '..self.versions)
self.switch_datatable:tag('span'):wikitext('Default version: '..self.default_version)
end
end
return self
-- Store a subobject for each version
for version=1, self.versions do
-- Reminder - subobject name cannot have a . in the first 5 characters
local subobject_name = 'Infobox.'..pagename..'#'..self.version_names[version]
local subobject = {}
-- Store each param that has smw_property defined and has a defined value
for _, param_name in ipairs(self.param_names) do
local property = self.params[param_name].smw_property
if property then
value = self:get_param(self.smw_param(param_name), version)
if self.is_value_defined(value) then
subobject[property] = value
end
end
end
local result = mw.smw.subobject(subobject, subobject_name)
if result ~= true then
table.insert(self.errors, 'SMW error: '..result.error)
end
end
return true
end
end


function Infobox.param(param_name)
param = {
property = 'args_parsed',
param_name = param_name,
}
return param
end


--[[
function Infobox.raw_param(param_name)
If multiple versions exist, creates a set of buttons for switching
param = {
-- self.max_buttons : if the number of versions exceeds this value, a dropdown will be created instead
property = 'args_raw',
--]]
param_name = param_name,
}
return param
end

function Infobox.smw_param(param_name)
param = {
property = 'args_smw',
param_name = param_name,
}
return param
end

-----------
-- Table --
-----------
function Infobox:buttons()
function Infobox:buttons()
if self.versions < 2 then
if self.versions < 2 then
return
return
end
end
-- Button caption
local buttons = self.rtable:tag('caption')
local buttons = self.rtable:tag('caption')
:tag('div')
:tag('div')
:addClass('infobox-buttons')
:addClass('infobox-buttons')
:attr('data-default-version', self.default_version)
:attr('data-default-version', self.default_version)
-- Dropdown list instead of buttons
-- Dropdown list instead of buttons if too many versions
if self.versions > self.max_buttons then
if self.versions > self.max_buttons then
buttons:addClass('infobox-buttons-select')
buttons:addClass('infobox-buttons-select')
end
end
-- Create all the buttons
for version=1, self.versions do
for version=1, self.versions do
buttons:tag('span')
buttons:tag('span')
Line 305: Line 435:
:wikitext(self.version_names[version])
:wikitext(self.version_names[version])
end
end
end
end



function Infobox:create()
--[[
-- Create infobox table
For each version, stores a subobject for all params that have smw_property defined
self.rtable = mw.html.create('table')
--]]
:addClass('plainlinks')
function Infobox:store_smw()
:addClass('infobox')
for version=1, self.versions do
:addClass('infobox-'..self.infobox_name)
-- Generate a subobject name
-- Add necessary class if switch infobox
-- Reminder - subobject name cannot have a . in the first 5 characters
if self.versions > 1 then
local subobject_name = 'Infobox.'..mw.title.getCurrentTitle().fullText..'#'..self.version_names[version]
self.rtable:addClass('infobox-switch')
-- Store each param that has smw_property defined and is not nil
self.switch_datatable = mw.html.create('div')
local subobject = {}
:addClass('infobox-switch-resources')
for _, param_name in ipairs(self.param_names) do
:addClass('infobox-resources-'..self.infobox_name)
local property = self.params[param_name].smw_property
:addClass('hidden')
if property then
self.switch_datatable:tag('span'):wikitext('Versions: '..self.versions)
value = self:get_param(self.smw_param(param_name), version)
self.switch_datatable:tag('span'):wikitext('Default version: '..self.default_version)
if value ~= nil then
self:buttons()
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 = mw.smw.subobject(subobject, subobject_name)
if result ~= true then
table.insert(self.errors, 'SMW error: '..result.error)
end
end
end
end
return self
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)
function Infobox:add_switch_data(content)
-- Only check for params if there are multiple versions
if self.versions <= 1 then
if self.versions <= 1 then
return false
return false
end
end
-- Only needed for non-constant params
if type(content) ~= 'table' then
if type(content) ~= 'table' then
return false
return false
end
end
-- Only if the data varies between the different versions
-- Only needed if the param changes value between different versions
local first_value = self:get_param(cell_params.content, 1)
local first_value = self:get_param(content, 1)
local all_same = true
local all_same = true
for version=2, self.versions do
for version=2, self.versions do
if first_value ~= self:get_param(cell_params.content, version) then
if first_value ~= self:get_param(content, version) then
all_same = false
all_same = false
break
break
Line 350: Line 519:
end
end
-- Let's build the datatable
-- Let's build the datatable
-- Prepend raw__ or smw$__ if not a parsed argument
-- datum name: Prepend raw__ or smw__ if not a parsed argument
local name = content.param_name
local name = content.param_name
if content.property == 'args_raw' then
if content.property == 'args_raw' then
Line 363: Line 532:
text = self:get_param(content, version)
text = self:get_param(content, version)
if text == nil then
if text == nil then
text = edit
text = self.params[content.param_name].empty
end
end
data_param:tag('span'):attr('data-attr-index', version):wikitext(text)
data_param:tag('span'):attr('data-attr-index', version):wikitext(text)
Line 370: Line 539:
return name
return name
end
end



--[[
--[[
Override tostring
Add parameters functions
All parameters should be tables
The first parameter defines the type of cell to create
-- tag = 'th' or 'td'
The second parameter defines what is inside the tag
-- content = string or Infobox.param
Additional named parameters can be used to add any styling or attributes
-- attr : mw.html:attr({ arg1 = '1', ... })
-- css : mw.html:css({ arg1 = '1', ...)
-- class : mw.html:addClass('arg') (common class: infobox-subheader)
---- 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>

adding it to the infobox table of ipsobox

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 "addClass'
-- addClass : mw.html:addClass('arg')
-- this function currently only supports a single string
--]]
--]]
function Infobox:add_row(...)
function Infobox:__tostring()
-- New row to add
-- Create error messages
local args = ...
local error_text = ''
local _row = self.rtable:tag('tr')
for _, v in ipairs(self.errors) do
error_text = error_text..'<span class="mw-message-box-error">'..v..'</span>'
-- For each member of tags
for _, v in ipairs(args) do
local _cell = _row:tag(v.tag)

-- 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
_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 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 type(v.class) == 'string' then
_cell:addClass(v.class)
elseif type(v.class) == 'table' then
for _, w in ipairs(v.class) do
_cell:addClass(w)
end
end
end

local content = Infobox:get_param(v.content, self.default_version)
if content == nil then
content = edit
end
_cell:wikitext(content)

-- Add the switch data if multiple values exist
data_attr_param = self:add_switch_data(v.content)
if data_attr_param then
_cell:attr('data-attr-param', data_attr_param)
end
end
end
-- Create categories

local category_text = ''
-- allow classes to be defined on the whole row
for key, _ in pairs(self.categories) do
if args.addClass then
category_text = category_text..'[[Category:'..key..']]'
_row:addClass(args.addClass)
end
end
return tostring(self.rtable) .. tostring(self.switch_datatable) .. error_text .. category_text

return self
end
end




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

-- addClass
function Infobox:addClass(arg)
self.rtable:addClass(arg)
return self
end

-- Override tostring
function Infobox:tostring()
error_text = ''
if #self.errors > 0 then
for _, v in ipairs(self.errors) do
error_text = error_text..'<span class="mw-message-box-error">'..v..'</span>'
end
end
return tostring(self.rtable) .. tostring(self.switch_datatable) .. tostring(error_text)
end



function Infobox:dump()
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))
mw.log(mw.dumpObject(self))
setmetatable(self, Infobox)
mw.log(tostring(self))
mw.log(tostring(self))
end
end



return Infobox
return Infobox

Revision as of 15:48, 26 March 2024

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

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>
-- Edit button for unknown params
local editbutton = require('Module:Edit button')

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
				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 = nil, -- 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)
	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)
	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)
	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
		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: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_buttuons = 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)
		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()
	-- Count the versions and setup self.version_names
	local i = 1
	while self.args_raw['version'..i] do
		table.insert(self.version_names, self.args_raw['version'..i])
		i = i + 1
	end
	self.versions = i - 1
	-- Handle the no version case - check for a custom version_name
	if self.versions == 0 then
		table.insert(self.version_names, self.args_raw['version'] or 'DEFAULT')
	end
	-- 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".')
		self.versions = 0
	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 = {}
		for _, func_param in ipairs(func_params) do
			table.insert(func_fetched_params, self:get_param(func_param, version))
		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)
	-- 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')
		self.switch_datatable:tag('span'):wikitext('Versions: '..self.versions)
		self.switch_datatable:tag('span'):wikitext('Default version: '..self.default_version)
	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-version', 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
		buttons:tag('span')
			   :attr('data-switch-index', version)
			   :attr('data-switch-anchor', '#'..self.version_names[version])
			   :addClass('button')
			   :wikitext(self.version_names[version])
	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 = 'Infobox.'..mw.title.getCurrentTitle().fullText..'#'..self.version_names[version]
		-- Store each param that has smw_property defined and is not nil
		local subobject = {}
		for _, param_name in ipairs(self.param_names) do
			local property = self.params[param_name].smw_property
			if property then
				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 = mw.smw.subobject(subobject, subobject_name)
			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 <= 1 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
	data_param = self.switch_datatable:tag('span'):attr('data-attr-param', name)
	-- Add each version to the datatable
	for version=1, self.versions do
		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 = ''
	for key, _ in pairs(self.categories) do
		category_text = category_text..'[[Category:'..key..']]'
	end
	return tostring(self.rtable) .. tostring(self.switch_datatable) .. error_text .. category_text
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