Module:Tabs

From Pathfinder Wiki

Documentation for this module may be created at Module:Tabs/doc

local p = {}
local args = {}
local origArgs = {}

local function deleteDuplicates(array)
	local hash = {}
	local res = {}
	for _,v in ipairs(array) do
	   if (not hash[v]) then
	       res[#res+1] = v -- you could print here instead of saving to result table if you wanted
	       hash[v] = true
	   end
	end
	return res
end

-- Returns a table containing the numbers of the arguments that exist
-- for the specified prefix. For example, if the prefix was 'data', and
-- 'data1', 'data2', and 'data5' exist, it would return {1, 2, 5}.
local function getArgNums(prefix)
	local nums = {}
	for k, v in pairs(args) do
		local num = tostring(k):match('^' .. prefix .. '([1-9]%d*)$')
		if num then table.insert(nums, tonumber(num)) end
	end
	table.sort(nums)
	return nums
end

local function getTableArgNums(prefix)
	local nums = {}
	for k, v in pairs(args) do
		local num = tostring(k):match('^' .. prefix .. '([1-9]%d*)')
		if num then table.insert(nums, tonumber(num)) end
	end
	table.sort(nums)
	return deleteDuplicates(nums)
end

local function renderTab( wikiLink )
	local HTMLCode = mw.html.create( 'li' )
	HTMLCode
		:addClass( 'navtabs-item' )
		:wikitext( wikiLink )
	return tostring( HTMLCode )
end

local function renderDropdownSeperator()
	HTMLSeperator = mw.html.create( 'div' )
	HTMLSeperator
		:addClass('dropdown-divider')
	return HTMLSeperator
end

local function renderDropdownTab(tabNum)
	local prefix			= 'dropdown'..tabNum..''
	local linkNums			= getArgNums(prefix..'_link')
	local linkSeperatorNums	= getArgNums(prefix..'_seperatorLink')
	local retHTMLCode		= ''
	
	if #linkNums == 0 then
		error('Invalid dropdown configuration. No links defined!')
	elseif #linkNums == 1 then
		retHTMLCode = renderTab(args[prefix..'_link'..linkNums[1]])
	else
		local HTMLTabParent = mw.html.create( 'li' )
		local buttonArgs = {}
		buttonArgs['tagname'] = 'button'
		buttonArgs['class'] = 'dropdown-toggle'
		-- do the selflink with JS Module
		buttonArgs['type'] = 'button'
		if args[prefix..'_CSSID'] then
			buttonArgs['id'] = 'dropdown'..args[prefix..'_CSSID']..'Keys'
		end
		buttonArgs['data-toggle'] = 'dropdown'
		buttonArgs['aria-haspopup'] = 'true'
		buttonArgs['aria-expanded'] = 'false'
		buttonText = args[prefix..'_btnText']
		local frame = mw.getCurrentFrame()
		local dropdownButton = frame:extensionTag('htmltag', buttonText, buttonArgs)
		HTMLTabParent
			:addClass( 'navtabs-item' )
			:addClass( 'dropdown' )
			:wikitext( dropdownButton )
		
		HTMLVariantsContainer = mw.html.create( 'div' )
		HTMLVariantsContainer:addClass( 'dropdown-menu' )
		if args[prefix..'_CSSID'] then
			HTMLVariantsContainer:attr( 'aria-labelledby', 'dropdown'..args[prefix..'_CSSID']..'Keys')
		end
		-- add link's to the dropdown menu
		for key, num in pairs(linkNums) do
			if (args[prefix..'_seperatorLink'..num] == 'before') then
				HTMLVariantsContainer:node( renderDropdownSeperator() )
			end
			HTMLVariantLink = mw.html.create( 'span' )
			HTMLVariantLink
				:addClass( 'dropdown-item' )
				:wikitext( args[prefix..'_link'..num] )
				:done()
			HTMLVariantsContainer:node( HTMLVariantLink )
			if (args[prefix..'_seperatorLink'..num] == 'after') then
				HTMLVariantsContainer:node( renderDropdownSeperator() )
			end
		end
		HTMLVariantsContainer:done()
		HTMLTabParent:node( HTMLVariantsContainer )
		retHTMLCode = tostring( HTMLTabParent )
	end
	return retHTMLCode
end

local function renderTabs()
	-- get configured tab's
	local tabSimpleNum = getArgNums('simple')
	local tabDropNum = getTableArgNums('dropdown')
	-- check which tab's should be shown
	local displayTabs = {}
	for simpleI, simpleNum in ipairs(tabSimpleNum) do
		for dropI, dropNum in ipairs(tabDropNum) do
			if simpleNum == dropNum and args['dropdown'..tostring(dropNum)..'_btnText'] and
				args['dropdown'..tostring(dropNum)..'_CSSID'] then
				displayTabs[simpleI] = {
					tabType = 'dropdown',
					tabNum = dropNum
				}
				mw.log('Tabs.renderTabs: Simple tab replaced by Dropdown with number: '..dropNum )
			end
		end
		if not displayTabs[simpleI] then
			displayTabs[simpleI] = {
				tabType = 'simple',
				tabNum = simpleNum
			}
		end
	end
	local strTabs = ''
	for k, dispTab in ipairs(displayTabs) do
		if (dispTab.tabType == 'simple') then
			strTabs = strTabs .. tostring(renderTab(args['simple' .. tostring(dispTab.tabNum)]))
		elseif (dispTab.tabType == 'dropdown') then
			strTabs = strTabs .. tostring(renderDropdownTab(dispTab.tabNum))
		end
	end
	return strTabs
end

local function _tabbar()
	local tabContainer = mw.html.create( 'ul' )
	tabContainer
		:attr( 'id', 'navtabs' )
		:attr( 'class', 'noprint')
		:wikitext( renderTabs() )
	return tostring( tabContainer )
end

-- If the argument exists and isn't blank, add it to the argument table.
-- Blank arguments are treated as nil to match the behaviour of ParserFunctions.
local function preprocessSingleArg(argName)
	mw.log('Tabs:preprocessSingleArg: '..argName)
	if origArgs[argName] and origArgs[argName] ~= '' then
		args[argName] = origArgs[argName]
	end
end

-- Assign the parameters with the given prefixes to the args table, in order, in
-- batches of the step size specified. This is to prevent references etc. from
-- appearing in the wrong order. The prefixTable should be an array containing
-- tables, each of which has two possible fields, a "prefix" string and a
-- "depend" table. The function always parses parameters containing the "prefix"
-- string, but only parses parameters in the "depend" table if the prefix
-- parameter is present and non-blank.
local function preprocessArgs(prefixTable, step)
	if type(prefixTable) ~= 'table' then
		error("Non-table value detected for the prefix table", 2)
	end
	if type(step) ~= 'number' then
		error("Invalid step value detected", 2)
	end

	-- Get arguments without a number suffix, and check for bad input.
	for i,v in ipairs(prefixTable) do
		if type(v) ~= 'table' or type(v.prefix) ~= "string" or
			(v.depend and type(v.depend) ~= 'table') then
			error('Invalid input detected to preprocessArgs prefix table', 2)
		end
		preprocessSingleArg(v.prefix)
		-- Only parse the depend parameter if the prefix parameter is present
		-- and not blank.
		if args[v.prefix] and v.depend then
			for j, dependValue in ipairs(v.depend) do
				if type(dependValue) ~= 'string' then
					error('Invalid "depend" parameter value detected in preprocessArgs')
				end
				preprocessSingleArg(dependValue)
			end
		end
	end

	-- Get arguments with number suffixes.
	local a = 1 -- Counter variable.
	local moreArgumentsExist = true
	while moreArgumentsExist == true do
		moreArgumentsExist = false
		for i = a, a + step - 1 do
			for j,v in ipairs(prefixTable) do
				local prefixArgName = v.prefix .. tostring(i)
				if origArgs[prefixArgName] then
					-- Do another loop if any arguments are found, even blank ones.
					moreArgumentsExist = true
					preprocessSingleArg(prefixArgName)
				end
				-- Process the depend table if the prefix argument is present
				-- and not blank, or we are processing "prefix1" and "prefix" is
				-- present and not blank, and if the depend table is present.
				if v.depend and (args[prefixArgName] or (i == 1 and args[v.prefix])) then
					for j,dependValue in ipairs(v.depend) do
						local dependArgName = dependValue .. tostring(i)
						preprocessSingleArg(dependArgName)
					end
				end
			end
		end
		a = a + step
	end
end

local function preprocessTableArgs(prefixTable, step)
	if type(prefixTable) ~= 'table' then
		error("Non-table value detected for the prefix table", 2)
	end
	if type(step) ~= 'number' then
		error("Invalid step value detected", 2)
	end
	if (not(prefixTable.tablePrefix) and (type(prefixTable.tablePrefix) ~= 'string')) then
		error("Invalid tablePrefix value detected", 2)
	end
	
	-- search for all arguement's with tablePrefix
	local a = 1 -- Counter variable.
	local moreArgumentsExist = true
	while moreArgumentsExist == true do
		moreArgumentsExist = false
		for i = a, a + step - 1 do
			local prefixTableName = prefixTable.tablePrefix .. tostring(i)
			local tablePrefixExists = false
			for key, args in pairs(origArgs) do
				if string.find(key, prefixTableName) then
					-- there is atleast one argument with the table index
					moreArgumentsExist = true
					tablePrefixExists = true
					break
				end
			end
			if tablePrefixExists then
				if (prefixTable.tableSingleArgs and type(prefixTable.tableSingleArgs) == 'table') then
					for key, single in ipairs(prefixTable.tableSingleArgs) do
						if (not single.prefix or type(single.prefix) ~= 'string') then
							error("Invalid tableSingleArg value detected!")
						end
						preprocessSingleArg(prefixTableName..'_'..single.prefix)
					end
				else
					error('Invalid type of tableSingleArgs!')
				end
				if (prefixTable.tableArgs and type(prefixTable.tableArgs) == 'table') then
					for key, mArgs in ipairs(prefixTable.tableArgs) do
						if (mArgs and type(mArgs) == 'table' and
							mArgs.prefix and type(mArgs.prefix) == 'string') then
							local locArgs = {}
							if (not(mArgs.prefix) or type(mArgs.prefix) ~= 'string') then
								error('Invalid type of mArgs.prefix!')
							end
							locArgs.prefix = prefixTableName..'_'..mArgs.prefix
							locArgs.depend = {}
							if mArgs.depend and type(mArgs.depend) == 'table' then
								for depIndex, depValue in ipairs(mArgs.depend) do
									table.insert(locArgs.depend, prefixTableName..'_'..depValue)
								end
							end
							preprocessArgs({locArgs}, step)
						else
							error('Invalid mArgs Argument!')
						end
					end
				else
					error('Invalid type of tableArgs!')
				end
			end
		end
		a = a + step
	end
end

-- Parse the data parameters in the same order that the old {{tabbar}} did, so
-- that references etc. will display in the expected places. Parameters that
-- depend on another parameter are only processed if that parameter is present,
-- to avoid phantom references appearing in article reference lists.
local function parseDataParameters()
	-- preprocessSingleArg('')
	preprocessArgs({
		{prefix = 'simple'}
	}, 10)
	preprocessTableArgs({
		tablePrefix = 'dropdown',
		tableSingleArgs = {
			{prefix = 'btnText'},
			{prefix = 'CSSID'}
		},
		tableArgs = {
			{prefix = 'link', depend = {'seperatorLink'}}
		}
	}, 10)
end

-- If called via #invoke, use the args passed into the invoking template.
-- Otherwise, for testing purposes, assume args are being passed directly in.
function p.tabbar(frame)
	mw.log('Tabs.tabbar: Start!')
	if frame == mw.getCurrentFrame() then
		origArgs = frame:getParent().args
	else
		origArgs = frame
	end
	
	parseDataParameters()
	
	return _tabbar()
end

-- For calling via #invoke within a template
function p.tabbarTemplate(frame)
	mw.log('Tabs.tabbarTemplate: Start!')
	origArgs = {}
	for k,v in pairs(frame.args) do origArgs[k] = mw.text.trim(v) end
	
	parseDataParameters()
	
	return _tabbar()
end
return p