Module:Infobox: Difference between revisions
		
		
		
		Jump to navigation
		Jump to search
		
|  (Blanked the page) Tags: Blanking Reverted | No edit summary Tag: Manual revert | ||
| Line 1: | Line 1: | ||
| --[=[ | |||
| -- For documentation, see [[Module:Infobox/doc]] | |||
| --]=] | |||
| -- <nowiki> | |||
| local Infobox = {} | |||
| Infobox.__index = Infobox | |||
| Infobox.__tostring = Infobox.tostring | |||
| -- Edit button for unknown params | |||
| local editbutton = require('Module:Edit button') | |||
| local var = mw.ext.VariablesLua | |||
| 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' | |||
| } | |||
| --[=[ | |||
| -- Standardised functions | |||
| -- called as string with defineParams | |||
| --]=] | |||
| -- Standardised "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 | |||
| -- Standardised "name" function | |||
| function subjectName(arg) | |||
| 	return string.match(arg or '','%S') and arg or nil | |||
| end | |||
| -- Create a standardised 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 | |||
| -- Standardised image function | |||
| function image(img) | |||
| 	if img and img:find('%S') then | |||
| 		return img | |||
| 	else | |||
| 		return nil | |||
| 	end | |||
| end | |||
| -- Standardised numbers | |||
| function numbers(num) | |||
| 	num = string.gsub(num or '',',','') | |||
| 	return tonumber(num) | |||
| end | |||
| -- Wrap content with line breaks if it contains list-like wiki syntax | |||
| function wrapContent(content) | |||
| 	if type(content) == "string" then | |||
| 		local firstListItem = math.min(content:find('^[*#]') or math.huge, content:find('\n[*#]') or math.huge) | |||
| 		if firstListItem ~= math.huge then | |||
| 			local suffix = content:find('\n[*#][^\n]+$') and '\n' or '' | |||
| 			content = content:sub(1, firstListItem - 1) .. '\n' .. content:sub(firstListItem) .. suffix | |||
| 		end | |||
| 	end | |||
| 	return content | |||
| 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, | |||
| 	hasContent = 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 = 'UH OH YOU SHOULDN\'T SEE THIS!' | |||
| -- 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 | |||
| 	-- args : parameters from frame to pass through | |||
| 	-- Sets a meta table and creates a <div> tag wrapper | |||
| 	-- other fields are initialised in other functions | |||
| --]] | |||
| function Infobox.new(args) | |||
| 	local obj = setmetatable({ | |||
| 				args = args, -- parameters (uncleaned) | |||
| 				rargs = {}, -- parameters (cleaned) | |||
| 				params = {}, -- parameters mapped to functions | |||
| 				paramnames = {}, -- parameter names | |||
| 				dupeable = {}, -- parameters that are allowed to have duplicated switch data | |||
| 				addrswibclass = true, | |||
| 				switchfo = false, -- switch infobox? or not? | |||
| 				switchfoattr = {}, -- switch data class changes | |||
| 				maxbuttons = DEFAULT_MAX_BUTTONS, -- maximum number of buttons before switching becomes a menu | |||
| 				switch_tag = '', -- switchfo data | |||
| 				switch_buttons_tag = '', -- switchfo buttons | |||
| 				custom_buttons = false, | |||
| 				smw_error_tag = '', | |||
| 				rtable = nil, -- returned infobox table | |||
| 				labels = nil, -- returned labels | |||
| 				_smw = {}, -- semantic mediawiki data | |||
| 				_smwOne = {}, -- semantic mediawiki data part 2 | |||
| 				_smwSubobject = {}, -- semantic mediawiki data part 3 | |||
| 				_smwSubobjectName = nil, -- semantic mediawiki data part 3.5 | |||
| 				_smwElement = {}, -- semantic mediawiki data part 4 | |||
| 				set_default_version_smw = false, -- whether to set [[Property:Default version]] | |||
| 				setSMWElement = true, | |||
| 				suppressAllSMW = false, | |||
| 				suppressVersionSMW = {}, | |||
| 				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) | |||
| 	return obj | |||
| end | |||
| --[[ | |||
| Toggles the addition of infobox class | |||
| use before :create() | |||
| noop if not a boolean | |||
| --]] | |||
| function Infobox:setAddRSWInfoboxClass(bool) | |||
| 	if type(bool) == 'boolean' then | |||
| 		self.addrswibclass = bool | |||
| 	end | |||
| end | |||
| --[[ | |||
| 	Creates an infobox | |||
| 	-- 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() | |||
| 	-- Run to find if this is a switch infobox and if so, how many boxes | |||
| 	if self.versions == -1 then | |||
| 		self:maxVersion() | |||
| 	end | |||
| 	-- Run if switch infobox | |||
| 	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('span'):addClass('line-break') | |||
| 			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') | |||
| 	if self.addrswibclass then | |||
| 		self.rtable:addClass('infobox') | |||
| 	end | |||
| 	-- Add necessary class if switch infobox | |||
| 	if self.switchfo then | |||
| 		self.rtable:addClass('infobox-switch') | |||
| 	end | |||
| end | |||
| -- Defines an infobox name ({{Template:arg}}) | |||
| -- Used to create a link at the bottom of pages | |||
| function Infobox:defineName(arg) | |||
| 	self.infoboxname = arg | |||
| end | |||
| -- Defines the bottom links of the infobox | |||
| -- pass a table whose elements are tables that define a link and a label | |||
| -- { | |||
| --	{ 'link', 'label }, | |||
| --	... | |||
| -- } | |||
| -- The template name can be substituted into the tables using '%s' | |||
| -- If we wanted Template:InFooBar to link to it's /doc page with a "doc" label: | |||
| -- { ... | |||
| --	{ 'Template:%s/doc', 'doc' }, | |||
| -- ... } | |||
| -- The template's name can only be called 5 times | |||
| function Infobox:defineLinks(arg) | |||
| 	if type(arg) == 'table' then | |||
| 		if arg.colspan then | |||
| 			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 | |||
| -- 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 | |||
| --[[ | |||
| 	Add parameters functions | |||
| 	All parameters should be tables | |||
| 	The first parameter defines the type of cell to create | |||
| 		-- th : <th> | |||
| 		-- td : <td> | |||
| 		-- argh : <th> | |||
| 		-- argd : <td> | |||
| 	The second parameter defines what is inside the tag | |||
| 		-- th | th : text passed | |||
| 		-- argh | argd : parameter with the name passed | |||
| 	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') | |||
| 		---- 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 at "meta" | |||
| 		-- meta.addClass : mw.html:addClass('arg') | |||
| 		-- this function currently only supports a single string | |||
| --]] | |||
| function Infobox.addRow(box, ...) | |||
| 	-- New row to add | |||
| 	local args = ... | |||
| 	local _row = box.rtable:tag('tr') | |||
| 	-- For each member of tags | |||
| 	for i, v in ipairs(args) do | |||
| 		-- map tag name to appropriate tag, default to <td> | |||
| 		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 | |||
| 			_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 | |||
| 		-- if the cell is a normal th or td, add the exact argument passed | |||
| 		if v.tag == 'th' or v.tag == 'td' then | |||
| 			_cell:wikitext(wrapContent(v.content)) | |||
| 		-- if defined with "arg", add the argument with name passed | |||
| 		elseif v.tag == 'argh' or v.tag == 'argd' then | |||
| 			local content = box.rargs[v.content] | |||
| 			-- if the requested parameter doesn't exist whatsoever, just return a blank string | |||
| 			if not content then | |||
| 				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(wrapContent(content)) | |||
| 			-- add necessary attribute for switch infoboxes | |||
| 			if box.switchfo then | |||
| 				_cell:attr('data-attr-param',v.content) | |||
| 			end | |||
| 		end | |||
| 	end | |||
| 	-- not that meta | |||
| 	-- allow classes to be defined on the whole row | |||
| 	-- okay, sort of meta | |||
| 	if args.meta then | |||
| 		if args.meta.addClass then | |||
| 			_row:addClass(args.meta.addClass) | |||
| 		end | |||
| 	end | |||
| 	return box | |||
| end | |||
| function Infobox.customButtonPlacement(box,arg) | |||
| 	box.custom_buttons = arg | |||
| 	return box | |||
| end | |||
| -- Choose whether to set [[Property:Default version]] | |||
| -- Defaults to false. | |||
| function Infobox.setDefaultVersionSMW(box, arg) | |||
| 	box.set_default_version_smw = arg | |||
| 	return box | |||
| end | |||
| function Infobox.addButtonsRow(box, args) | |||
| 	if box.switchfo then | |||
| 		box.custom_buttons = true | |||
| 		local _row = box.rtable:tag('tr') | |||
| 						:addClass('infobox-switch-buttons-row') | |||
| 						:tag('td') | |||
| 							:addClass('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('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 | |||
| --]] | |||
| function Infobox.pad(box, colspan, class) | |||
| 	local tr = box:tag('tr') | |||
| 			:tag('td'):attr('colspan', colspan or 1):addClass('infobox-padding') | |||
| 			:done() | |||
| 	if class then | |||
| 		tr:addClass(class)	 | |||
| 	end | |||
| 	tr:done() | |||
| 	return box | |||
| end | |||
| --[[ | |||
| 	-- functions the same as mw.html:wikitext() on the wrapper | |||
| 	-- Should only be used for categories really | |||
| --]] | |||
| function Infobox.wikitext(box, arg) | |||
| 	box.rtable:wikitext(arg) | |||
| 	return box | |||
| 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 | |||
| --[[ | |||
| 	-- Adds a caption to the infobox | |||
| 	-- defaults to the pagename | |||
| 	-- or the default argument if defined | |||
| --]] | |||
| function Infobox.caption(box) | |||
| 	-- default to the article's name | |||
| 	local name = pagename | |||
| 	-- first see if the name parameter exists | |||
| 	if box.rargs.name then | |||
| 		-- then try the default | |||
| 		if box.rargs.name.d then | |||
| 			name = box.rargs.name.d | |||
| 		-- then look for swithes | |||
| 		elseif box.rargs.name.switches then | |||
| 			-- then look at version 1 | |||
| 			if box.rargs.name.switches[1] ~= nil_param then | |||
| 				name = box.rargs.name.switches[1] | |||
| 			end | |||
| 		end | |||
| 	end | |||
| 	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 | |||
| 	return box | |||
| end | |||
| --[[ | |||
| 	-- Functions for styling the infobox | |||
| 	-- works the same as the respective mw.html functions | |||
| --]] | |||
| -- attr | |||
| function Infobox.attr(box, arg) | |||
| 	box.rtable:attr(arg) | |||
| 	return box | |||
| end | |||
| -- css | |||
| function Infobox.float(box,float) | |||
| 	box.rtable:css('float',float) | |||
| 	return box | |||
| end | |||
| function Infobox.css(box, ...) | |||
| 	box.rtable:css(...) | |||
| 	return box | |||
| end | |||
| -- addClass | |||
| function Infobox.addClass(box, arg) | |||
| 	box.rtable:addClass(arg) | |||
| 	return box | |||
| end | |||
| -- Much like Infobox.addClass, but adds multiple classes | |||
| function Infobox.addClasses(box, ...) | |||
| 	for _, v in ipairs(...) do | |||
| 		box.rtable:addClass(box) | |||
| 	end | |||
| 	return box | |||
| 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 | |||
| --[[ | |||
| 	Allows the infobox to use Semantic Media Wiki and give parameters properties | |||
| 	Pass a table to this function to map parameter names to properties | |||
| 	Calling syntax: | |||
| 	-- {{#show:page|?property}}: | |||
| 	-- "<property>" - unqualified and without a number will display the default value | |||
| 	-- "<property#>" - with a number will show the switch data from that index | |||
| 	-- "all <property>" - adding all will display every unique value in a comma separated list | |||
| 	Properties initiated in Infobox:finish() | |||
| --]] | |||
| function Infobox:useSMW(arg) | |||
| 	if type(arg) == 'table' then | |||
| 		for w, v in pairs(arg) do | |||
| 			self._smw[w] = v | |||
| 		end | |||
| 	end | |||
| end | |||
| --[[ | |||
| 	As above, but only assigns to "<property>", which will act like "all <property>" - "<property>#" not present | |||
| 	Properties initiated in Infobox:finish() | |||
| --]] | |||
| function Infobox:useSMWOne(arg) | |||
| 	if type(arg) == 'table' then | |||
| 		for w, v in pairs(arg) do | |||
| 			self._smwOne[w] = v | |||
| 		end | |||
| 	end | |||
| 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 | |||
| 		for w, v in pairs(arg) do | |||
| 			self._smwSubobject[w] = v | |||
| 		end | |||
| 	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 | |||
| --[[ | |||
| 	Finishing function | |||
| 	-- Finishes the return, adding necessary final tags | |||
| --]] | |||
| function Infobox:finish() | |||
| 	local currentNamespace = mw.title.getCurrentTitle().namespace | |||
| 	-- 0 = mainspace, 4 = RuneScape | |||
| 	local onContentNamespace = currentNamespace == 0 or currentNamespace == 4 | |||
| 	-- Don't finish twice | |||
| 	if self.__finished then | |||
| 		return | |||
| 	end | |||
| 	-- Add switch infobox resources | |||
| 	if self.switchfo then | |||
| 		self.rtable:attr('data-resource-class', '.infobox-resources-'..string.gsub(tostring(self.infoboxname), ' ', '_')) | |||
| 		-- Wrapper tag, hidden | |||
| 		self.switch_tag = mw.html.create('div') | |||
| 									:addClass('infobox-switch-resources') | |||
| 									:addClass('infobox-resources-'..string.gsub(tostring(self.infoboxname), ' ', '_')) | |||
| 									:addClass('hidden') | |||
| 		for _, v in ipairs(self.paramnames) do | |||
| 			local param = self.rargs[v] | |||
| 			local default_value = param.d or edit | |||
| 			-- Parameters may not have any switches data, those are ignored | |||
| 				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 and switchattr.switches then | |||
| 							_w:attr('data-addclass',switchattr.switches[i]) | |||
| 						elseif switchattr then | |||
| 							mw.logObject({string.format("Expected switches for `%s` but got none:", v), switchattr}) | |||
| 						end | |||
| 						_w:done() | |||
| 					end | |||
| 				end | |||
| 				res_span:done() | |||
| 			end | |||
| 		end | |||
| 		-- Add a tracking category for pages that have more than 1 version | |||
| 		if onContentNamespace and self.versions > 1 then | |||
| 			-- version count data | |||
| 			self.switch_tag:wikitext('[[Category:Pages that contain switch infobox data]]') | |||
| 			if not self.suppressAllSMW then | |||
| 				self.switch_tag:tag('span') | |||
| 					:wikitext(string.format('Versions: [[Version count::%s]]',self.versions)) | |||
| 				:done() | |||
| 				-- set default version smw | |||
| 				local defver = tonumber(self.args.defver) or 1 | |||
| 				local default_subobject_value = self.args['smwname'..defver] or self.args['version'..defver] | |||
| 				if default_subobject_value and self.set_default_version_smw then | |||
| 					-- Only take first subobject if multiple are defined using "¦" | |||
| 					local default_subobject_name = default_subobject_value:match("[^¦]+") | |||
| 					self.switch_tag:tag('span') | |||
| 						:wikitext(string.format('Default version: [[Default version::%s]]',default_subobject_name)) | |||
| 				end | |||
| 			end | |||
| 		end | |||
| 		self.switch_tag:done() | |||
| 	end | |||
| 	-- smw data | |||
| 	if onContentNamespace and not self.suppressAllSMW 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_i, x in ipairs(curarg.switches) do | |||
| 						if not self.suppressVersionSMW[x_i] then | |||
| 							if x ~= nil_param then | |||
| 								table.insert(_args,x) | |||
| 							else | |||
| 								table.insert(_args,argdefault or nil_param) | |||
| 							end | |||
| 						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._smwSubobjectName 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_i, x in ipairs(curarg.switches) do | |||
| 						if not self.suppressVersionSMW[x_i] then | |||
| 							if x ~= nil_param then | |||
| 								table.insert(_args,x) | |||
| 							else | |||
| 								table.insert(_args,argdefault or nil_param) | |||
| 							end | |||
| 						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_i, x in ipairs(curarg.switches) do | |||
| 						if not self.suppressVersionSMW[x_i] then | |||
| 							if x ~= nil_param then | |||
| 								table.insert(_args,x) | |||
| 							else | |||
| 								table.insert(_args,argdefault or nil_param) | |||
| 							end | |||
| 						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._smwSubobjectName then | |||
| 			for i,k in ipairs(self._smwSubobjectName) do | |||
| 				if not self.suppressVersionSMW[i] then | |||
| 					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 | |||
| 		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._smwSubobjectName 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,' • ') | |||
| 		self.rtable:tag('tr'):tag('td') | |||
| 				:addClass('infobox-template-links') | |||
| 				:attr('colspan', self.bottomlinks.colspan) | |||
| 				:wikitext(bottom_links) | |||
| 			:done() | |||
| 	end | |||
| 	-- Define as finished | |||
| 	self.__finished = true | |||
| end | |||
| --[[ | |||
| 	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(...) | |||
| 	for _, v in ipairs(...) do | |||
| 		-- For every parameter, store its corresponding function to self.params | |||
| 		if v.name then | |||
| 			-- If the value is a function or a table (which should define a function) | |||
| 			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 | |||
| 			-- Create a list of all param names | |||
| 			table.insert(self.paramnames,v.name) | |||
| 			-- function to allow duplicated values | |||
| 			if v.dupes then | |||
| 				self.dupeable[v.name] = true | |||
| 			end | |||
| 		end | |||
| 	end | |||
| end | |||
| --[[ | |||
| 	-- Forces an infobox to only use 1 variant | |||
| 	-- Mainly used by lite infoboxes | |||
| 	-- This should be run before creation | |||
| --]] | |||
| function Infobox:noSwitch() | |||
| 	self.versions = 1 | |||
| 	self.switchfo = false | |||
| end | |||
| --[[ | |||
| 	-- Calculates the max version | |||
| 	-- Adds labels | |||
| 	-- Sees if this needs to be a switch infobox | |||
| 	-- Returns extra version count (even if already run) | |||
| --]] | |||
| function Infobox.maxVersion(box) | |||
| 	-- Only allowed to run once | |||
| 	if box.versions ~= -1 then | |||
| 		return box.versions | |||
| 	end | |||
| 	box.labels = {} | |||
| 	box.versions = 0 | |||
| 	local smwname = {} | |||
| 	if string.lower(box.args['smw'] or '') == 'no' then | |||
| 		box.suppressAllSMW = true | |||
| 	end | |||
| 	-- Look for up to 125 variants, defined in order | |||
| 	for i=1, 125 do | |||
| 		-- 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 smwname | |||
| 			if box.args['smwname'..i] then | |||
| 				table.insert(smwname, mw.text.split(box.args['smwname'..i], '¦')) | |||
| 			else | |||
| 				table.insert(smwname, {label}) | |||
| 			end | |||
| 			box.suppressVersionSMW[i] = (box.args['smw'..i] and string.lower(box.args['smw'..i] or '') == 'no') | |||
| 		else | |||
| 			-- Stop if it doesn't exist | |||
| 			break | |||
| 		end | |||
| 	end | |||
| 	-- Define self as a switch infobox if at least 1 other version is found | |||
| 	if box.versions > 0 then | |||
| 		box.switchfo = true | |||
| 		box._smwSubobjectName = smwname | |||
| 	else | |||
| 		-- single version, check for smwname | |||
| 		if box.args['smwname'] then | |||
| 			box._smwSubobjectName = {mw.text.split(box.args['smwname'], '¦')} | |||
| 		end | |||
| 	end | |||
| 	-- versions calculated | |||
| 	return box.versions | |||
| end | |||
| --[[ | |||
| 	-- 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() | |||
| 	-- map of flags to functionality | |||
| 	local flagmap = { | |||
| 		r = 'r', | |||
| 		l = 'r', | |||
| 		d = 'd', | |||
| 		p = 'p' | |||
| 	} | |||
| 	-- For all parameters named | |||
| 	for _, v in ipairs(self.paramnames) do | |||
| 		-- Parameter to add | |||
| 		local _add = {} | |||
| 		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 | |||
| 				-- Add parsed argument to table | |||
| 				table.insert(func_args,xarg) | |||
| 			end | |||
| 			-- Run function | |||
| 			_add.d = func(unpack(func_args)) | |||
| 		end | |||
| 		if _add.d == nil or _add.d == nil_param then | |||
| 			-- have to do pagename defaults here to prevent weird behaviour with switch values | |||
| 			if v == 'name' then | |||
| 				_add.d = pagename | |||
| 			else | |||
| 				_add.d = edit | |||
| 			end | |||
| 			catdata.all_defined = false | |||
| 		else | |||
| 			--_add.d is not nil | |||
| 			catdata.one_defined = true | |||
| 		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 | |||
| 							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, 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 | |||
| 		-- 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 | |||
| 		-- 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 | |||
| 		self.catdata[v] = catdata | |||
| 	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 | |||
| 	-- 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 | |||
| --[[ | |||
| 	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 | |||
| --]==========================================] | |||
| --[[ | |||
| 	Access the param | |||
| 	-- arg : param name | |||
| 	-- 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) | |||
| 	-- All parameters | |||
| 	if arg == 'all' then | |||
| 		return self.rargs | |||
| 	end | |||
| 	-- case-insensitive flagging | |||
| 	retp = tostring(retp):lower() | |||
| 	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 | |||
| 	-- Fetch parameter | |||
| 	local param = self.rargs[arg] | |||
| 	-- Return nil if no table found | |||
| 	if not param then return nil end | |||
| 	-- Return default | |||
| 	if ret_func == 'd' then | |||
| 		return param.d | |||
| 	end | |||
| 	-- Return full table | |||
| 	if ret_func == 'f' then | |||
| 		return param | |||
| 	end | |||
| 	-- Return switch table | |||
| 	if ret_func == 's' then | |||
| 		return param.switches | |||
| 	end | |||
| 	-- 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 | |||
| 	-- If s2, reread the param | |||
| 	if ret_func == 's2' then | |||
| 		-- no switches | |||
| 		if not param.switches then | |||
| 			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 | |||
| 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 | |||
| --[[ | |||
| 	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) | |||
| 	local arg = self.rargs[param] | |||
| 	-- if no parameters, return nil | |||
| 	if not arg then | |||
| 		return nil | |||
| 	end | |||
| 	local ret | |||
| 	local valtype = type(val) | |||
| 	-- start with the default value | |||
| 	-- 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 | |||
| 	-- switch cases | |||
| 	-- more organised 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 | |||
| ------ | |||
| function Infobox.paramRead(arg,val) | |||
| 	-- if no parameters, return nil | |||
| 	if not arg then | |||
| 		return nil | |||
| 	end | |||
| 	local ret | |||
| 	local valtype = type(val) | |||
| 	-- start with the default value | |||
| 	-- 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 | |||
| 	-- switch cases | |||
| 	-- more organised 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 | |||
| ---- | |||
| -- Return collected category data | |||
| function Infobox:categoryData() | |||
| 	return self.catdata | |||
| end | |||
| -- Infobox:addDropLevelVars("thieving", "skilllvl1") | |||
| function Infobox:addDropLevelVars(key, paramName) | |||
| 	local levelParams = self:param(paramName, 'f') | |||
| 	local dropParams = self:param('dropversion', 'f') | |||
| 	if levelParams == nil then | |||
| 		return | |||
| 	end | |||
| 	if dropParams == nil or not self:paramDefined("dropversion", "all") then | |||
| 		dropParams = {d = 'DEFAULT'} | |||
| 	end | |||
| 	if dropParams.switches == nil then | |||
| 		dropParams.switches = {} | |||
| 	end | |||
| 	local levels = levelParams.switches or {levelParams.d} | |||
| 	local dropVersions = {} | |||
| 	for i=1,#levels do | |||
| 		local dropVersionFromInfobox = dropParams.switches[i] or dropParams.d | |||
| 		if dropVersionFromInfobox == nil_param then | |||
| 			dropVersionFromInfobox = 'DEFAULT' | |||
| 		else | |||
| 			dropVersionFromInfobox = dropVersionFromInfobox .. ",DEFAULT" | |||
| 		end | |||
| 		for dropVersion in string.gmatch(dropVersionFromInfobox, ' *([^,]+) *') do | |||
| 			if dropVersions[dropVersion] == nil then | |||
| 				dropVersions[dropVersion] = {} | |||
| 			end | |||
| 			dropVersions[dropVersion][levels[i]] = true | |||
| 		end | |||
| 	end | |||
| 	-- This part is to append levels from previous Infobox invocations | |||
| 	for dropVersion, dropLevels in pairs(dropVersions) do | |||
| 		-- set dummy property on versioned SMW subobject, otherwise it can't be part of an #ask | |||
| 		mw.smw.subobject({["Dummy property"] = true}, dropVersion) | |||
| 		-- example variable: DropLevel_combat_High_level | |||
| 		local var_name = string.format("DropLevel_%s_%s", key, dropVersion) | |||
| 		local previousVar = var.var(var_name) | |||
| 		if previousVar ~= "" then | |||
| 			for v in string.gmatch(previousVar, ' *([^,]+) *') do | |||
| 				dropVersions[dropVersion][v] = true | |||
| 			end | |||
| 		end | |||
| 		local ordered = {} | |||
| 		for k, v in pairs(dropVersions[dropVersion]) do | |||
| 			local n = tonumber(k) | |||
| 			if n ~= nil then | |||
| 				table.insert(ordered, n) | |||
| 			end | |||
| 		end | |||
| 		table.sort(ordered) | |||
| 		var.vardefine(var_name, table.concat(ordered, ',')) | |||
| 	end | |||
| 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 | |||
| -- </nowiki> | |||
Revision as of 15:27, 30 March 2024
Documentation for this module may be created at Module:Infobox/doc
--[=[
-- For documentation, see [[Module:Infobox/doc]]
--]=]
-- <nowiki>
local Infobox = {}
Infobox.__index = Infobox
Infobox.__tostring = Infobox.tostring
-- Edit button for unknown params
local editbutton = require('Module:Edit button')
local var = mw.ext.VariablesLua
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'
}
--[=[
-- Standardised functions
-- called as string with defineParams
--]=]
-- Standardised "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
-- Standardised "name" function
function subjectName(arg)
	return string.match(arg or '','%S') and arg or nil
end
-- Create a standardised 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
-- Standardised image function
function image(img)
	if img and img:find('%S') then
		return img
	else
		return nil
	end
end
-- Standardised numbers
function numbers(num)
	num = string.gsub(num or '',',','')
	return tonumber(num)
end
-- Wrap content with line breaks if it contains list-like wiki syntax
function wrapContent(content)
	if type(content) == "string" then
		local firstListItem = math.min(content:find('^[*#]') or math.huge, content:find('\n[*#]') or math.huge)
		if firstListItem ~= math.huge then
			local suffix = content:find('\n[*#][^\n]+$') and '\n' or ''
			content = content:sub(1, firstListItem - 1) .. '\n' .. content:sub(firstListItem) .. suffix
		end
	end
	
	return content
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,
	hasContent = 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 = 'UH OH YOU SHOULDN\'T SEE THIS!'
-- 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
	-- args : parameters from frame to pass through
	-- Sets a meta table and creates a <div> tag wrapper
	-- other fields are initialised in other functions
--]]
function Infobox.new(args)
	local obj = setmetatable({
				args = args, -- parameters (uncleaned)
				rargs = {}, -- parameters (cleaned)
				params = {}, -- parameters mapped to functions
				paramnames = {}, -- parameter names
				dupeable = {}, -- parameters that are allowed to have duplicated switch data
				addrswibclass = true,
				switchfo = false, -- switch infobox? or not?
				switchfoattr = {}, -- switch data class changes
				maxbuttons = DEFAULT_MAX_BUTTONS, -- maximum number of buttons before switching becomes a menu
				switch_tag = '', -- switchfo data
				switch_buttons_tag = '', -- switchfo buttons
				custom_buttons = false,
				smw_error_tag = '',
				rtable = nil, -- returned infobox table
				labels = nil, -- returned labels
				_smw = {}, -- semantic mediawiki data
				_smwOne = {}, -- semantic mediawiki data part 2
				_smwSubobject = {}, -- semantic mediawiki data part 3
				_smwSubobjectName = nil, -- semantic mediawiki data part 3.5
				_smwElement = {}, -- semantic mediawiki data part 4
				set_default_version_smw = false, -- whether to set [[Property:Default version]]
				setSMWElement = true,
				suppressAllSMW = false,
				suppressVersionSMW = {},
				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)
	return obj
end
--[[
Toggles the addition of infobox class
use before :create()
noop if not a boolean
--]]
function Infobox:setAddRSWInfoboxClass(bool)
	if type(bool) == 'boolean' then
		self.addrswibclass = bool
	end
end
--[[
	Creates an infobox
	-- 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()
	-- Run to find if this is a switch infobox and if so, how many boxes
	if self.versions == -1 then
		self:maxVersion()
	end
	
	-- Run if switch infobox
	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('span'):addClass('line-break')
			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')
	if self.addrswibclass then
		self.rtable:addClass('infobox')
	end
	-- Add necessary class if switch infobox
	if self.switchfo then
		self.rtable:addClass('infobox-switch')
	end
end
-- Defines an infobox name ({{Template:arg}})
-- Used to create a link at the bottom of pages
function Infobox:defineName(arg)
	self.infoboxname = arg
end
-- Defines the bottom links of the infobox
-- pass a table whose elements are tables that define a link and a label
-- {
--	{ 'link', 'label },
--	...
-- }
-- The template name can be substituted into the tables using '%s'
-- If we wanted Template:InFooBar to link to it's /doc page with a "doc" label:
-- { ...
--	{ 'Template:%s/doc', 'doc' },
-- ... }
-- The template's name can only be called 5 times
function Infobox:defineLinks(arg)
	if type(arg) == 'table' then
		if arg.colspan then
			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
-- 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
--[[
	Add parameters functions
	All parameters should be tables
	The first parameter defines the type of cell to create
		-- th : <th>
		-- td : <td>
		-- argh : <th>
		-- argd : <td>
	The second parameter defines what is inside the tag
		-- th | th : text passed
		-- argh | argd : parameter with the name passed
	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')
		---- 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 at "meta"
		-- meta.addClass : mw.html:addClass('arg')
		-- this function currently only supports a single string
--]]
function Infobox.addRow(box, ...)
	-- New row to add
	local args = ...
	local _row = box.rtable:tag('tr')
	-- For each member of tags
	for i, v in ipairs(args) do
		-- map tag name to appropriate tag, default to <td>
		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
			_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
		-- if the cell is a normal th or td, add the exact argument passed
		if v.tag == 'th' or v.tag == 'td' then
			_cell:wikitext(wrapContent(v.content))
		-- if defined with "arg", add the argument with name passed
		elseif v.tag == 'argh' or v.tag == 'argd' then
			local content = box.rargs[v.content]
			-- if the requested parameter doesn't exist whatsoever, just return a blank string
			if not content then
				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(wrapContent(content))
			-- add necessary attribute for switch infoboxes
			if box.switchfo then
				_cell:attr('data-attr-param',v.content)
			end
		end
	end
	-- not that meta
	-- allow classes to be defined on the whole row
	-- okay, sort of meta
	if args.meta then
		if args.meta.addClass then
			_row:addClass(args.meta.addClass)
		end
	end
	return box
end
function Infobox.customButtonPlacement(box,arg)
	box.custom_buttons = arg
	return box
end
-- Choose whether to set [[Property:Default version]]
-- Defaults to false.
function Infobox.setDefaultVersionSMW(box, arg)
	box.set_default_version_smw = arg
	return box
end
function Infobox.addButtonsRow(box, args)
	if box.switchfo then
		box.custom_buttons = true
		local _row = box.rtable:tag('tr')
						:addClass('infobox-switch-buttons-row')
						:tag('td')
							:addClass('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('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
--]]
function Infobox.pad(box, colspan, class)
	local tr = box:tag('tr')
			:tag('td'):attr('colspan', colspan or 1):addClass('infobox-padding')
			:done()
	if class then
		tr:addClass(class)	
	end
	tr:done()
	return box
end
--[[
	-- functions the same as mw.html:wikitext() on the wrapper
	-- Should only be used for categories really
--]]
function Infobox.wikitext(box, arg)
	box.rtable:wikitext(arg)
	return box
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
--[[
	-- Adds a caption to the infobox
	-- defaults to the pagename
	-- or the default argument if defined
--]]
function Infobox.caption(box)
	-- default to the article's name
	local name = pagename
	-- first see if the name parameter exists
	if box.rargs.name then
		-- then try the default
		if box.rargs.name.d then
			name = box.rargs.name.d
		-- then look for swithes
		elseif box.rargs.name.switches then
			-- then look at version 1
			if box.rargs.name.switches[1] ~= nil_param then
				name = box.rargs.name.switches[1]
			end
		end
	end
	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
	return box
end
--[[
	-- Functions for styling the infobox
	-- works the same as the respective mw.html functions
--]]
-- attr
function Infobox.attr(box, arg)
	box.rtable:attr(arg)
	return box
end
-- css
function Infobox.float(box,float)
	box.rtable:css('float',float)
	return box
end
function Infobox.css(box, ...)
	box.rtable:css(...)
	return box
end
-- addClass
function Infobox.addClass(box, arg)
	box.rtable:addClass(arg)
	return box
end
-- Much like Infobox.addClass, but adds multiple classes
function Infobox.addClasses(box, ...)
	for _, v in ipairs(...) do
		box.rtable:addClass(box)
	end
	return box
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
--[[
	Allows the infobox to use Semantic Media Wiki and give parameters properties
	Pass a table to this function to map parameter names to properties
	Calling syntax:
	-- {{#show:page|?property}}:
	-- "<property>" - unqualified and without a number will display the default value
	-- "<property#>" - with a number will show the switch data from that index
	-- "all <property>" - adding all will display every unique value in a comma separated list
	Properties initiated in Infobox:finish()
--]]
function Infobox:useSMW(arg)
	if type(arg) == 'table' then
		for w, v in pairs(arg) do
			self._smw[w] = v
		end
	end
end
--[[
	As above, but only assigns to "<property>", which will act like "all <property>" - "<property>#" not present
	Properties initiated in Infobox:finish()
--]]
function Infobox:useSMWOne(arg)
	if type(arg) == 'table' then
		for w, v in pairs(arg) do
			self._smwOne[w] = v
		end
	end
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
		for w, v in pairs(arg) do
			self._smwSubobject[w] = v
		end
	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
--[[
	Finishing function
	-- Finishes the return, adding necessary final tags
--]]
function Infobox:finish()
	local currentNamespace = mw.title.getCurrentTitle().namespace
	-- 0 = mainspace, 4 = RuneScape
	local onContentNamespace = currentNamespace == 0 or currentNamespace == 4
	-- Don't finish twice
	if self.__finished then
		return
	end
	
	-- Add switch infobox resources
	if self.switchfo then
		self.rtable:attr('data-resource-class', '.infobox-resources-'..string.gsub(tostring(self.infoboxname), ' ', '_'))
		-- Wrapper tag, hidden
		self.switch_tag = mw.html.create('div')
									:addClass('infobox-switch-resources')
									:addClass('infobox-resources-'..string.gsub(tostring(self.infoboxname), ' ', '_'))
									:addClass('hidden')
		for _, v in ipairs(self.paramnames) do
			local param = self.rargs[v]
			local default_value = param.d or edit
			-- Parameters may not have any switches data, those are ignored
				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 and switchattr.switches then
							_w:attr('data-addclass',switchattr.switches[i])
						elseif switchattr then
							mw.logObject({string.format("Expected switches for `%s` but got none:", v), switchattr})
						end
						_w:done()
					end
				end
				res_span:done()
			end
		end
		-- Add a tracking category for pages that have more than 1 version
		if onContentNamespace and self.versions > 1 then
			-- version count data
			self.switch_tag:wikitext('[[Category:Pages that contain switch infobox data]]')
			if not self.suppressAllSMW then
				self.switch_tag:tag('span')
					:wikitext(string.format('Versions: [[Version count::%s]]',self.versions))
				:done()
				-- set default version smw
				local defver = tonumber(self.args.defver) or 1
				local default_subobject_value = self.args['smwname'..defver] or self.args['version'..defver]
				if default_subobject_value and self.set_default_version_smw then
					-- Only take first subobject if multiple are defined using "¦"
					local default_subobject_name = default_subobject_value:match("[^¦]+")
					self.switch_tag:tag('span')
						:wikitext(string.format('Default version: [[Default version::%s]]',default_subobject_name))
				end
			end
		end
		self.switch_tag:done()
	end
	
	-- smw data
	if onContentNamespace and not self.suppressAllSMW 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_i, x in ipairs(curarg.switches) do
						if not self.suppressVersionSMW[x_i] then
							if x ~= nil_param then
								table.insert(_args,x)
							else
								table.insert(_args,argdefault or nil_param)
							end
						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._smwSubobjectName 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_i, x in ipairs(curarg.switches) do
						if not self.suppressVersionSMW[x_i] then
							if x ~= nil_param then
								table.insert(_args,x)
							else
								table.insert(_args,argdefault or nil_param)
							end
						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_i, x in ipairs(curarg.switches) do
						if not self.suppressVersionSMW[x_i] then
							if x ~= nil_param then
								table.insert(_args,x)
							else
								table.insert(_args,argdefault or nil_param)
							end
						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._smwSubobjectName then
			for i,k in ipairs(self._smwSubobjectName) do
				if not self.suppressVersionSMW[i] then
					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
		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._smwSubobjectName 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,' • ')
		self.rtable:tag('tr'):tag('td')
				:addClass('infobox-template-links')
				:attr('colspan', self.bottomlinks.colspan)
				:wikitext(bottom_links)
			:done()
	end
	-- Define as finished
	self.__finished = true
end
--[[
	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(...)
	for _, v in ipairs(...) do
		-- For every parameter, store its corresponding function to self.params
		if v.name then
			-- If the value is a function or a table (which should define a function)
			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
			-- Create a list of all param names
			table.insert(self.paramnames,v.name)
			-- function to allow duplicated values
			if v.dupes then
				self.dupeable[v.name] = true
			end
		end
	end
end
--[[
	-- Forces an infobox to only use 1 variant
	-- Mainly used by lite infoboxes
	-- This should be run before creation
--]]
function Infobox:noSwitch()
	self.versions = 1
	self.switchfo = false
end
--[[
	-- Calculates the max version
	-- Adds labels
	-- Sees if this needs to be a switch infobox
	-- Returns extra version count (even if already run)
--]]
function Infobox.maxVersion(box)
	-- Only allowed to run once
	if box.versions ~= -1 then
		return box.versions
	end
	box.labels = {}
	box.versions = 0
	local smwname = {}
	
	if string.lower(box.args['smw'] or '') == 'no' then
		box.suppressAllSMW = true
	end
	-- Look for up to 125 variants, defined in order
	for i=1, 125 do
		-- 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 smwname
			if box.args['smwname'..i] then
				table.insert(smwname, mw.text.split(box.args['smwname'..i], '¦'))
			else
				table.insert(smwname, {label})
			end
			
			box.suppressVersionSMW[i] = (box.args['smw'..i] and string.lower(box.args['smw'..i] or '') == 'no')
		else
			-- Stop if it doesn't exist
			break
		end
	end
	-- Define self as a switch infobox if at least 1 other version is found
	if box.versions > 0 then
		box.switchfo = true
		box._smwSubobjectName = smwname
	else
		-- single version, check for smwname
		if box.args['smwname'] then
			box._smwSubobjectName = {mw.text.split(box.args['smwname'], '¦')}
		end
	end
	-- versions calculated
	return box.versions
end
--[[
	-- 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()
	-- map of flags to functionality
	local flagmap = {
		r = 'r',
		l = 'r',
		d = 'd',
		p = 'p'
	}
	-- For all parameters named
	for _, v in ipairs(self.paramnames) do
		-- Parameter to add
		local _add = {}
		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
				-- Add parsed argument to table
				table.insert(func_args,xarg)
			end
			-- Run function
			_add.d = func(unpack(func_args))
		end
		if _add.d == nil or _add.d == nil_param then
			-- have to do pagename defaults here to prevent weird behaviour with switch values
			if v == 'name' then
				_add.d = pagename
			else
				_add.d = edit
			end
			catdata.all_defined = false
		else
			--_add.d is not nil
			catdata.one_defined = true
		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
							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, 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
		-- 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
		-- 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
		self.catdata[v] = catdata
	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
	-- 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
--[[
	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
--]==========================================]
--[[
	Access the param
	-- arg : param name
	-- 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)
	-- All parameters
	if arg == 'all' then
		return self.rargs
	end
	-- case-insensitive flagging
	retp = tostring(retp):lower()
	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
	-- Fetch parameter
	local param = self.rargs[arg]
	-- Return nil if no table found
	if not param then return nil end
	-- Return default
	if ret_func == 'd' then
		return param.d
	end
	-- Return full table
	if ret_func == 'f' then
		return param
	end
	-- Return switch table
	if ret_func == 's' then
		return param.switches
	end
	-- 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
	-- If s2, reread the param
	if ret_func == 's2' then
		-- no switches
		if not param.switches then
			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
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
--[[
	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)
	local arg = self.rargs[param]
	-- if no parameters, return nil
	if not arg then
		return nil
	end
	local ret
	local valtype = type(val)
	-- start with the default value
	-- 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
	-- switch cases
	-- more organised 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
------
function Infobox.paramRead(arg,val)
	-- if no parameters, return nil
	if not arg then
		return nil
	end
	local ret
	local valtype = type(val)
	-- start with the default value
	-- 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
	-- switch cases
	-- more organised 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
----
-- Return collected category data
function Infobox:categoryData()
	return self.catdata
end
-- Infobox:addDropLevelVars("thieving", "skilllvl1")
function Infobox:addDropLevelVars(key, paramName)
	local levelParams = self:param(paramName, 'f')
	local dropParams = self:param('dropversion', 'f')
	if levelParams == nil then
		return
	end
	if dropParams == nil or not self:paramDefined("dropversion", "all") then
		dropParams = {d = 'DEFAULT'}
	end
	if dropParams.switches == nil then
		dropParams.switches = {}
	end
	local levels = levelParams.switches or {levelParams.d}
	local dropVersions = {}
	for i=1,#levels do
		local dropVersionFromInfobox = dropParams.switches[i] or dropParams.d
		if dropVersionFromInfobox == nil_param then
			dropVersionFromInfobox = 'DEFAULT'
		else
			dropVersionFromInfobox = dropVersionFromInfobox .. ",DEFAULT"
		end
		for dropVersion in string.gmatch(dropVersionFromInfobox, ' *([^,]+) *') do
			if dropVersions[dropVersion] == nil then
				dropVersions[dropVersion] = {}
			end
			dropVersions[dropVersion][levels[i]] = true
		end
	end
	-- This part is to append levels from previous Infobox invocations
	for dropVersion, dropLevels in pairs(dropVersions) do
		-- set dummy property on versioned SMW subobject, otherwise it can't be part of an #ask
		mw.smw.subobject({["Dummy property"] = true}, dropVersion)
		-- example variable: DropLevel_combat_High_level
		local var_name = string.format("DropLevel_%s_%s", key, dropVersion)
		local previousVar = var.var(var_name)
		if previousVar ~= "" then
			for v in string.gmatch(previousVar, ' *([^,]+) *') do
				dropVersions[dropVersion][v] = true
			end
		end
		local ordered = {}
		for k, v in pairs(dropVersions[dropVersion]) do
			local n = tonumber(k)
			if n ~= nil then
				table.insert(ordered, n)
			end
		end
		table.sort(ordered)
		var.vardefine(var_name, table.concat(ordered, ','))
	end
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
-- </nowiki>