Module:Navbox with columns

require('strict')
local p = {}
local parentCfg = mw.loadData('Module:Navbox/configuration')
local thisCfg = mw.loadData('Module:Navbox with columns/configuration')
local cfg = {}
for k, v in pairs(thisCfg) do
	if type(v) == 'table' then
		cfg[k] = {}
		if type(parentCfg[k]) == 'table' then
			for kk, vv in pairs(parentCfg[k]) do cfg[k][kk] = vv end
		end
		for kk, vv in pairs(v) do cfg[k][kk] = vv end
	end
end
local inArray = require("Module:TableTools").inArray
local getArgs -- lazily initialized

-- helper functions
local andnum = function(s, n) return string.format(cfg.arg[s..'_and_num'], n) end
	
local isblank = function(v) return (v or '') == '' end

local function concatstrings(s)
	local r = table.concat(s, '')
	if r:match('^%s*$') then return nil end
	return r
end
	
local function concatstyles(s)
	local r = ''
	for _, v in ipairs(s) do
		v = mw.text.trim(v, "%s;")
		if not isblank(v) then r = r .. v .. ';' end
	end
	if isblank(r) then return nil end
	return r
end
	
local function getSubgroup(args, listnum, listText, prefix)
	local subArgs = {
		[cfg.arg.border] = cfg.keyword.border_subgroup,
		[cfg.arg.navbar] = cfg.keyword.navbar_plain
	}
	local hasSubArgs = false
	local subgroups = prefix and {prefix} or cfg.keyword.subgroups
	for k, v in pairs(args) do
		k = tostring(k)
		for _, w in ipairs(subgroups) do
			w = w .. listnum .. "_"
			if (#k > #w) and (k:sub(1, #w) == w) then
				subArgs[k:sub(#w + 1)] = v
				hasSubArgs = true
			end
		end
	end
	return hasSubArgs and p._navbox(subArgs) or listText
end

-- Main functions
p._navbox = require('Module:Navbox')._navbox

function p._withColumns(pargs)
	-- table for args passed to navbox
	local targs = {}
	
	-- tables of column numbers
	local colheadernums = {}
	local colnums = {}
	local colfooternums = {}
	
	-- process args
	local passthrough = {
		[cfg.arg.above]=true,[cfg.arg.aboveclass]=true,[cfg.arg.abovestyle]=true,
		[cfg.arg.basestyle]=true,
		[cfg.arg.below]=true,[cfg.arg.belowclass]=true,[cfg.arg.belowstyle]=true,
		[cfg.arg.bodyclass]=true,
		[cfg.arg.evenstyle]=true,
		[cfg.arg.groupclass]=true,[cfg.arg.groupstyle]=true,
		[cfg.arg.image]=true,[cfg.arg.imageclass]=true,[cfg.arg.imagestyle]=true,
		[cfg.arg.imageleft]=true,[cfg.arg.imageleftstyle]=true,
		[cfg.arg.listclass]=true,[cfg.arg.liststyle]=true,
		[cfg.arg.name]=true,
		[cfg.arg.navbar]=true,
		[cfg.arg.oddstyle]=true,
		[cfg.arg.state]=true,
		[cfg.arg.title]=true,[cfg.arg.titleclass]=true,[cfg.arg.titlestyle]=true,
	}
	for k,v in pairs(pargs) do
		if passthrough[k] then
			targs[k] = v
		elseif type(k) == 'string' then
			if k:match(cfg.pattern.listnum) then
				local n = k:match(cfg.pattern.listnum)
				targs[andnum('liststyle', n + 2)] = pargs[andnum('liststyle', n)]
				targs[andnum('group', n + 2)] = pargs[andnum('group', n)]
				targs[andnum('groupstyle', n + 2)] = pargs[andnum('groupstyle', n)]
				if v and inArray(cfg.keyword.subgroups, v) then
					targs[andnum('list', n + 2)] = getSubgroup(pargs, n, v)
				else
					targs[andnum('list', n + 2)] = v
				end
			elseif (k:match(cfg.pattern.colheadernum) and v ~= '') then
				table.insert(colheadernums, tonumber(k:match(cfg.pattern.colheadernum)))
			elseif (k:match(cfg.pattern.colnum) and v ~= '') then
				table.insert(colnums,  tonumber(k:match(cfg.pattern.colnum)))
			elseif (k:match(cfg.pattern.colfooternum) and v ~= '') then
				table.insert(colfooternums,  tonumber(k:match(cfg.pattern.colfooternum)))
			end
		end
	end
	table.sort(colheadernums)
	table.sort(colnums)
	table.sort(colfooternums)
		
	-- HTML table for list1
	local coltable = mw.html.create( 'table' ):attr('class', 'navbox-columns-table')
	local row, col
	
	local tablestyle = ( (#colheadernums > 0) or (not isblank(pargs[cfg.arg.fullwidth])) )
		and 'width:100%'
		or 'width:auto; margin-left:auto; margin-right:auto'
	
	coltable:attr('style', concatstyles({
		'border-spacing: 0px; text-align:left',
		tablestyle,
		pargs[cfg.arg.coltablestyle] or ''
	}))
	
	--- Header row ---
	if (#colheadernums > 0) then
		row = coltable:tag('tr')
		for k, n in ipairs(colheadernums) do
			col = row:tag('td'):attr('class', 'navbox-abovebelow')
			col:attr('style', concatstyles({
				(k > 1) and 'border-left:2px solid #fdfdfd' or '',
				'font-weight:bold',
				pargs[cfg.arg.colheaderstyle] or '',
				pargs[andnum('colheaderstyle', n)] or ''
			}))
			if tonumber(pargs[andnum('colheadercolspan', n)]) then
				col:attr('colspan', pargs[andnum('colheadercolspan', n)])
			end
			col:wikitext(pargs[andnum('colheader', n)])
		end
	end
	
	--- Main columns ---
	row = coltable:tag('tr'):attr('style', 'vertical-align:top')
	for k, n in ipairs(colnums) do
		if k == 1 and isblank(pargs[andnum('colheader', 1)])
				and isblank(pargs[andnum('colfooter', 1)])
				and isblank(pargs[cfg.arg.fullwidth]) then
			local nopad = inArray(
				{'off', '0', '0em', '0px'},
				mw.ustring.gsub(pargs[cfg.arg.padding] or '', '[;%%]', ''))
			if not nopad then
				row:tag('td'):wikitext('   ')
					:attr('style', 'width:'..(pargs[cfg.arg.padding] or '5em')..';')
			end
		end
		col = row:tag('td'):attr('class', 'navbox-list')
		col:attr('style', concatstyles({
			(k > 1) and 'border-left:2px solid #fdfdfd' or '',
			'padding:0px',
			pargs[cfg.arg.colstyle] or '',
			((n%2 == 0) and pargs[cfg.arg.evencolstyle] or pargs[cfg.arg.oddcolstyle]) or '',
			pargs[andnum('colstyle', n)] or '',
			'width:' .. (pargs[andnum('colwidth', n)] or pargs[cfg.arg.colwidth] or '10em')
		}))
		local wt = pargs[andnum('col', n)]
		if wt and inArray(cfg.keyword.subgroups, wt) then
			local prefix = mw.ustring.gsub(cfg.arg.col_and_num,"%%d","")
			wt = getSubgroup(pargs, n, wt, prefix)
		end
		col:tag('div'):newline():wikitext(wt):newline()
	end
	
	--- Footer row ---
	if (#colfooternums > 0) then
		row = coltable:tag('tr')
		for k, n in ipairs(colfooternums) do
			col = row:tag('td'):attr('class', 'navbox-abovebelow')
			col:attr('style', concatstyles({
				(k > 1) and 'border-left:2px solid #fdfdfd' or '',
				'font-weight:bold',
				pargs[cfg.arg.colfooterstyle] or '',
				pargs[andnum('colfooterstyle', n)] or ''
			}))
			if tonumber(pargs[andnum('colfootercolspan', n)]) then
				col:attr('colspan', pargs[andnum('colfootercolspan', n)])
			end
			col:wikitext(pargs[andnum('colfooter', n)])
		end
	end
	
	-- assign table to list1
	targs[andnum('list', 1)] = tostring(coltable)
	if isblank(pargs[andnum('colheader', 1)]) 
			and isblank(pargs[andnum('col', 1)])
			and isblank(pargs[andnum('colfooter', 1)]) then
		targs[andnum('list', 1)] = targs[andnum('list', 1)] ..
			'[[' .. cfg.pattern.without_first_col .. ']]'
	end

	-- Other parameters
	targs[cfg.arg.border] = pargs[cfg.arg.border] or pargs[1]
	targs[cfg.arg.evenodd] = (not isblank(pargs[cfg.arg.evenodd])) and pargs[cfg.arg.evenodd] or nil
	targs[cfg.arg.list1padding] = '0px'
	targs[andnum('liststyle', 1)] = 'background:transparent;color:inherit;'
	targs[cfg.arg.style] = concatstyles({pargs[cfg.arg.style], pargs[cfg.arg.bodystyle]})
	targs[cfg.arg.tracking] = 'no'
	
	return p._navbox(targs)
end

-- Template entry points
function p.navbox (frame, boxtype)
	local function readArgs(args, prefix)
		-- Read the arguments in the order they'll be output in, to make references
		-- number in the right order.
		local _
		_ = args[prefix .. cfg.arg.title]
		_ = args[prefix .. cfg.arg.above]
		-- Limit this to 20 as covering 'most' cases (that's a SWAG) and because
		-- iterator approach won't work here
		for i = 1, 20 do
			_ = args[prefix .. andnum('group', i)]
			if inArray(cfg.keyword.subgroups, args[prefix .. andnum('list', i)]) then
				for _, v in ipairs(cfg.keyword.subgroups) do
					readArgs(args, prefix .. v .. i .. "_")
				end
			end
		end
		_ = args[prefix .. cfg.arg.below]
	end

	if not getArgs then
		getArgs = require('Module:Arguments').getArgs
	end
	local args = getArgs(frame, {wrappers = {cfg.pattern[boxtype or 'navbox']}})
	readArgs(args, "")
	return p['_'..(boxtype or 'navbox')](args)
end

p['with columns'] = function (frame)
	return p.navbox(frame, 'withColumns')
end

local q = {}
q._navbox = p._withColumns
q.navbox = p['with columns']
return q