Module:Navbox: Difference between revisions

From CivWiki
Jump to navigation Jump to search
Content added Content deleted
(fix handling of the border param)
m (87 revisions imported from wikipedia:Module:Navbox: this is nice to have)
 
(85 intermediate revisions by 22 users not shown)
Line 1: Line 1:
--
--
-- This module will implement {{Navbox}}
-- This module implements {{Navbox}}
--
--


local p = {}
local p = {}


local navbar = require('Module:Navbar')._navbar
local gutterRow = '<tr style="height:2px;"><td></td></tr>'
local getArgs -- lazily initialized

local args
local border
local border
local listnums = {}
local listnums
local ret = {}
local ODD_EVEN_MARKER = '\127_ODDEVEN_\127'
local RESTART_MARKER = '\127_ODDEVEN0_\127'
local REGEX_MARKER = '\127_ODDEVEN(%d?)_\127'


function add(...)
local function striped(wikitext)
-- Return wikitext with markers replaced for odd/even striping.
local args = {...}
-- Child (subgroup) navboxes are flagged with a category that is removed
for i = 1, #args do
-- by parent navboxes. The result is that the category shows all pages
if args[i] then
-- where a child navbox is not contained in a parent navbox.
table.insert(ret, args[i])
local orphanCat = '[[Category:Navbox orphans]]'
end
if border == 'subgroup' and args.orphan ~= 'yes' then
end
-- No change; striping occurs in outermost navbox.
return wikitext .. orphanCat
end
local first, second = 'odd', 'even'
if args.evenodd then
if args.evenodd == 'swap' then
first, second = second, first
else
first = args.evenodd
second = first
end
end
local changer
if first == second then
changer = first
else
local index = 0
changer = function (code)
if code == '0' then
-- Current occurrence is for a group before a nested table.
-- Set it to first as a valid although pointless class.
-- The next occurrence will be the first row after a title
-- in a subgroup and will also be first.
index = 0
return first
end
index = index + 1
return index % 2 == 1 and first or second
end
end
local regex = orphanCat:gsub('([%[%]])', '%%%1')
return (wikitext:gsub(regex, ''):gsub(REGEX_MARKER, changer)) -- () omits gsub count
end
end


function trim(s)
local function processItem(item, nowrapitems)
if item:sub(1, 2) == '{|' then
return (mw.ustring.gsub(s, "^%s*(.-)%s*$", "%1"))
-- Applying nowrap to lines in a table does not make sense.
-- Add newlines to compensate for trim of x in |parm=x in a template.
return '\n' .. item ..'\n'
end
if nowrapitems == 'yes' then
local lines = {}
for line in (item .. '\n'):gmatch('([^\n]*)\n') do
local prefix, content = line:match('^([*:;#]+)%s*(.*)')
if prefix and not content:match('^<span class="nowrap">') then
line = prefix .. '<span class="nowrap">' .. content .. '</span>'
end
table.insert(lines, line)
end
item = table.concat(lines, '\n')
end
if item:match('^[*:;#]') then
return '\n' .. item ..'\n'
end
return item
end
end


function renderTitleRow(args)
local function renderNavBar(titleCell)
if not args.title then return end
add('<tr>')
if args.titlegroup then
add('<th scope="row" class="navbox-group ', args.titlegroupclass, '" style="', args.basestyle, ';', args.groupstyle, ';', args.titlegroupstyle, '">', args.titlegroup, '</th><th scope="col" style="border-left:2px solid #fdfdfd;width:100%;')
else
add('<th scope="col" style="')
end
local colspan = 2
if args.imageleft then colspan = colspan + 1 end
if args.image then colspan = colspan + 1 end
if args.titlegroup then colspan = colspan - 1 end
add(args.basestyle, ';', args.titlestyle, '" class="navbox-title" colspan=', colspan, '>')


if args.navbar ~= 'off' and args.navbar ~= 'plain' and not (not args.name and mw.getCurrentFrame():getParent():getTitle():gsub('/sandbox$', '') == 'Template:Navbox') then
local stateLinkPlaceholder = '<span style="float:right;width:6em;">&nbsp;</span>'
titleCell:wikitext(navbar{
if args.navbar == 'plain' or args.navbar == 'off' or (not args.name and (border == 'subgroup' or border == 'child' or border == 'none')) then
args.name,
if args.navbar == 'off' then
mini = 1,
if args.state == 'plain' then add(stateLinkPlaceholder) end
fontstyle = (args.basestyle or '') .. ';' .. (args.titlestyle or '') .. ';background:none transparent;border:none;box-shadow:none;padding:0;'
else
})
if args.state ~= 'plain' then add(stateLinkPlaceholder) end
end
end
else
if args.name then
add(mw.getCurrentFrame():expandTemplate{ title = 'navbar', args = {
args.name,
mini = 1,
fontstyle = (args.basestyle or '') .. ';' .. (args.titlestyle or '') .. ';background:none transparent;border:none;'
}})
else
add('<span class="error" style="float:left;white-space:nowrap;">Error: No name provided</span>')
if args.state == 'plain' then add(stateLinkPlaceholder) end
end
end


add('<div class="', args.titleclass, '" style="font-size:110%;">')
add(args.title)
add('</div></th></tr>')
end
end


--
function renderAboveRow(args)
-- Title row
if not args.above then return end
--
local function renderTitleRow(tbl)
if args.title then add(gutterRow) end
if not args.title then return end
add('<tr><td class="navbox-abovebelow ', args.aboveclass, '" style="', args.basestyle, ';', args.abovestyle, '" colspan="', getAboveBelowColspan(args), '">')

add('<div>\n', args.above, '</div></td></tr>')
local titleRow = tbl:tag('tr')

if args.titlegroup then
titleRow
:tag('th')
:attr('scope', 'row')
:addClass('navbox-group')
:addClass(args.titlegroupclass)
:cssText(args.basestyle)
:cssText(args.groupstyle)
:cssText(args.titlegroupstyle)
:wikitext(args.titlegroup)
end

local titleCell = titleRow:tag('th'):attr('scope', 'col')

if args.titlegroup then
titleCell
:css('border-left', '2px solid #fdfdfd')
:css('width', '100%')
end

local titleColspan = 2
if args.imageleft then titleColspan = titleColspan + 1 end
if args.image then titleColspan = titleColspan + 1 end
if args.titlegroup then titleColspan = titleColspan - 1 end

titleCell
:cssText(args.basestyle)
:cssText(args.titlestyle)
:addClass('navbox-title')
:attr('colspan', titleColspan)

renderNavBar(titleCell)

titleCell
:tag('div')
-- id for aria-labelledby attribute
:attr('id', mw.uri.anchorEncode(args.title))
:addClass(args.titleclass)
:css('font-size', '114%')
:css('margin', '0 4em')
:wikitext(processItem(args.title))
end
end

function renderBelowRow(args)
--
if args.below then
-- Above/Below rows
if args.title or args.above or #listnums > 0 then add(gutterRow) end
--
add('<tr><td class="navbox-abovebelow ', args.belowclass, '" style="', args.basestyle, ';', args.belowstyle, '" colspan="', getAboveBelowColspan(args), '">')

add('<div>\n', args.below, '</div></td></tr>')
local function getAboveBelowColspan()
end
local ret = 2
if args.imageleft then ret = ret + 1 end
if args.image then ret = ret + 1 end
return ret
end
end

function getAboveBelowColspan(args)
local function renderAboveRow(tbl)
local ret = 2
if args.imageleft then ret = ret + 1 end
if not args.above then return end

if args.image then ret = ret + 1 end
tbl:tag('tr')
return ret
:tag('td')
:addClass('navbox-abovebelow')
:addClass(args.aboveclass)
:cssText(args.basestyle)
:cssText(args.abovestyle)
:attr('colspan', getAboveBelowColspan())
:tag('div')
-- id for aria-labelledby attribute, if no title
:attr('id', args.title and nil or mw.uri.anchorEncode(args.above))
:wikitext(processItem(args.above, args.nowrapitems))
end
end


function renderFirstListRow(args)
local function renderBelowRow(tbl)
if not args.list1 then return end
if not args.below then return end
if args.title or args.above then add(gutterRow) end
add('<tr>')
if args.imageleft then
add('<td class="navbox-image ', args.imageclass, '" style="width:0%;padding:0px 2px 0px 0px;', args.imageleftstyle, '"')
add(' rowspan=', (2 * #listnums - 1), '><div>', args.imageleft, '</div></td>')
end


tbl:tag('tr')
if args.group1 then
:tag('td')
add('<th scope="row" class="navbox-group ', args.groupclass, '" style="', args.basestyle, ';')
:addClass('navbox-abovebelow')
if args.groupwidth then add('width:', args.groupwidth, ';') end
:addClass(args.belowclass)
add(args.groupstyle, ';', args.group1style, '">')
add(args.group1, '</th>')
:cssText(args.basestyle)
:cssText(args.belowstyle)
add('<td style="text-align:left;border-left-width:2px;border-left-style:solid;')
:attr('colspan', getAboveBelowColspan())
else
:tag('div')
add('<td colspan=2 style="')
:wikitext(processItem(args.below, args.nowrapitems))
end
end
if not args.groupwidth then add('width:100%;') end

add('padding:0px;', args.liststyle, ';', args.oddstyle, ';', args.list1style, '" class="navbox-list navbox-')
--
if args.evenodd == 'swap' then
-- List rows
add('even')
--
else
local function renderListRow(tbl, index, listnum)
add(args.evenodd or 'odd')
local row = tbl:tag('tr')
end

add(' ', args.listclass, '">')
if index == 1 and args.imageleft then
add('<div style="padding:', args.list1padding or args.listpadding or '0em 0.25em', '">\n')
row
add(args.list1)
add('</div></td>')
:tag('td')
:addClass('noviewer')
:addClass('navbox-image')
:addClass(args.imageclass)
:css('width', '1px') -- Minimize width
:css('padding', '0px 2px 0px 0px')
:cssText(args.imageleftstyle)
:attr('rowspan', #listnums)
:tag('div')
:wikitext(processItem(args.imageleft))
end

if args['group' .. listnum] then
local groupCell = row:tag('th')

-- id for aria-labelledby attribute, if lone group with no title or above
if listnum == 1 and not (args.title or args.above or args.group2) then
groupCell
:attr('id', mw.uri.anchorEncode(args.group1))
end

groupCell
:attr('scope', 'row')
:addClass('navbox-group')
:addClass(args.groupclass)
:cssText(args.basestyle)
:css('width', args.groupwidth or '1%') -- If groupwidth not specified, minimize width

groupCell
:cssText(args.groupstyle)
:cssText(args['group' .. listnum .. 'style'])
:wikitext(args['group' .. listnum])
end

local listCell = row:tag('td')

if args['group' .. listnum] then
listCell
:css('text-align', 'left')
:css('border-left-width', '2px')
:css('border-left-style', 'solid')
else
listCell:attr('colspan', 2)
end

if not args.groupwidth then
listCell:css('width', '100%')
end

local rowstyle -- usually nil so cssText(rowstyle) usually adds nothing
if index % 2 == 1 then
rowstyle = args.oddstyle
else
rowstyle = args.evenstyle
end

local listText = args['list' .. listnum]
local oddEven = ODD_EVEN_MARKER
if listText:sub(1, 12) == '</div><table' then
-- Assume list text is for a subgroup navbox so no automatic striping for this row.
oddEven = listText:find('<th[^>]*"navbox%-title"') and RESTART_MARKER or 'odd'
end
listCell
:css('padding', '0px')
:cssText(args.liststyle)
:cssText(rowstyle)
:cssText(args['list' .. listnum .. 'style'])
:addClass('navbox-list')
:addClass('navbox-' .. oddEven)
:addClass(args.listclass)
:addClass(args['list' .. listnum .. 'class'])
:tag('div')
:css('padding', (index == 1 and args.list1padding) or args.listpadding or '0em 0.25em')
:wikitext(processItem(listText, args.nowrapitems))


if args.image then
if index == 1 and args.image then
row
add('<td class="navbox-image ', args.imageclass, '" style="width:0%;padding:0px 0px 0px 2px;', args.imagestyle, '" ')
:tag('td')
add(' rowspan=', (2 * #listnums - 1), '>')
:addClass('noviewer')
add('<div>', args.image, '</div></td>')
:addClass('navbox-image')
end
:addClass(args.imageclass)
:css('width', '1px') -- Minimize width
add('</tr>')
:css('padding', '0px 0px 0px 2px')
:cssText(args.imagestyle)
:attr('rowspan', #listnums)
:tag('div')
:wikitext(processItem(args.image))
end
end
end



function renderNthListRow(args, listnum)
--
if args.title or args.above or args.list1 then
-- Tracking categories
add(gutterRow)
--
end

add('<tr>')
local function needsHorizontalLists()
if args['group' .. listnum] then
if border == 'subgroup' or args.tracking == 'no' then
add('<th scope="row" class="navbox-group ', args.groupclass, '" style="', args.basestyle, ';')
return false
if args.groupwidth then add('width:', args.groupwidth, ';') end
end
add(args.groupstyle, ';', args['group' .. listnum .. 'style'], '">')
local listClasses = {
add(args['group' .. listnum])
['plainlist'] = true, ['hlist'] = true, ['hlist hnum'] = true,
add('</th><td style="text-align:left;border-left-width:2px;border-left-style:solid;')
['hlist hwrap'] = true, ['hlist vcard'] = true, ['vcard hlist'] = true,
else
['hlist vevent'] = true,
add('<td colspan=2 style="')
}
end
return not (listClasses[args.listclass] or listClasses[args.bodyclass])
if not args.groupwidth then add('width:100%;') end
local isOdd = (listnum % 2) == 1
local rowstyle = args.evenstyle
if isOdd then rowstyle = args.oddstyle end
add('padding:0px;', args.liststyle, ';', rowstyle, ';', args['list' .. listnum .. 'style'], '" ')
add('class="navbox-list navbox-')
if args.evenodd == 'swap' then
if isOdd then add('even') else add('odd') end
else
if isOdd then add(args.evenodd or 'odd') else add(args.evenodd or 'even') end
end
add(' ', args.listclass, '">')
add('<div style="padding:', args.listpadding or '0em 0.25em', '">\n', args['list' .. listnum], '</div></td></tr>')
end
end


function p._navbox(args)
local function hasBackgroundColors()
for _, key in ipairs({'titlestyle', 'groupstyle', 'basestyle', 'abovestyle', 'belowstyle'}) do
for k, v in pairs(args) do
if tostring(args[key]):find('background', 1, true) then
local listnum = ('' .. k):match('^list(%d+)$')
return true
if listnum then table.insert(listnums, tonumber(listnum)) end
end
end
end
table.sort(listnums)
end
border = trim(args.border or args[1] or '')
if border == 'subgroup' or border == 'child' then
add('</div>')
elseif border ~= 'none' then
add('<table cellspacing="0" class="navbox" style="border-spacing:0;', args.bodystyle, ';', args.style, '"><tr><td style="padding:2px;">')
end
add('<table cellspacing="0" class="nowraplinks ', args.bodyclass, ' ')
if args.title and (args.state ~= 'plain' and args.state ~= 'off') then
add('collapsible ', args.state or 'autocollapse', ' ')
end
if border == 'subgroup' or border == 'child' or border == 'none' then
add('navbox-subgroup" style="border-spacing:0;', args.bodystyle, ';', args.style)
else
add('navbox-inner" style="border-spacing:0;background:transparent;color:inherit')
end
add(';', args.innerstyle, ';">')
renderTitleRow(args)
renderAboveRow(args)
renderFirstListRow(args)


local function hasBorders()
-- render lists 2 through N
for i, listnum in ipairs(listnums) do
for _, key in ipairs({'groupstyle', 'basestyle', 'abovestyle', 'belowstyle'}) do
if tostring(args[key]):find('border', 1, true) then
if listnum > 1 then
return true
renderNthListRow(args, listnum)
end
end
end
end
end


local function isIllegible()
renderBelowRow(args)
local styleratio = require('Module:Color contrast')._styleratio


for key, style in pairs(args) do
add('</table>')
if tostring(key):match("style$") then
if styleratio{mw.text.unstripNoWiki(style)} < 4.5 then
if border == 'subgroup' or border == 'child' then
return true
add('<div>')
end
elseif border ~= 'none' then
end
add('</td></tr></table>')
end
end
return false
end


local function getTrackingCategories()
-- TODO: add tracking categories
local cats = {}
if needsHorizontalLists() then table.insert(cats, 'Navigational boxes without horizontal lists') end
return table.concat(ret, '')
if hasBackgroundColors() then table.insert(cats, 'Navboxes using background colours') end
if isIllegible() then table.insert(cats, 'Potentially illegible navboxes') end
if hasBorders() then table.insert(cats, 'Navboxes using borders') end
return cats
end

local function renderTrackingCategories(builder)
local title = mw.title.getCurrentTitle()
if title.namespace ~= 10 then return end -- not in template space
local subpage = title.subpageText
if subpage == 'doc' or subpage == 'sandbox' or subpage == 'testcases' then return end

for _, cat in ipairs(getTrackingCategories()) do
builder:wikitext('[[Category:' .. cat .. ']]')
end
end

--
-- Main navbox tables
--
local function renderMainTable()
local tbl = mw.html.create('table')
:addClass('nowraplinks')
:addClass(args.bodyclass)

if args.title and (args.state ~= 'plain' and args.state ~= 'off') then
if args.state == 'collapsed' then args.state = 'mw-collapsed' end
tbl
:addClass('mw-collapsible')
:addClass(args.state or 'autocollapse')
end

tbl:css('border-spacing', 0)
if border == 'subgroup' or border == 'none' then
tbl
:addClass('navbox-subgroup')
:cssText(args.bodystyle)
:cssText(args.style)
else -- regular navbox - bodystyle and style will be applied to the wrapper table
tbl
:addClass('navbox-inner')
:css('background', 'transparent')
:css('color', 'inherit')
end
tbl:cssText(args.innerstyle)

renderTitleRow(tbl)
renderAboveRow(tbl)
for i, listnum in ipairs(listnums) do
renderListRow(tbl, i, listnum)
end
renderBelowRow(tbl)

return tbl
end

function p._navbox(navboxArgs)
args = navboxArgs
listnums = {}

for k, _ in pairs(args) do
if type(k) == 'string' then
local listnum = k:match('^list(%d+)$')
if listnum then table.insert(listnums, tonumber(listnum)) end
end
end
table.sort(listnums)

border = mw.text.trim(args.border or args[1] or '')
if border == 'child' then
border = 'subgroup'
end

-- render the main body of the navbox
local tbl = renderMainTable()

-- render the appropriate wrapper around the navbox, depending on the border param
local res = mw.html.create()
if border == 'none' then
local nav = res:tag('div')
:attr('role', 'navigation')
:node(tbl)
-- aria-labelledby title, otherwise above, otherwise lone group
if args.title or args.above or (args.group1 and not args.group2) then
nav:attr('aria-labelledby', mw.uri.anchorEncode(args.title or args.above or args.group1))
else
nav:attr('aria-label', 'Navbox')
end
elseif border == 'subgroup' then
-- We assume that this navbox is being rendered in a list cell of a parent navbox, and is
-- therefore inside a div with padding:0em 0.25em. We start with a </div> to avoid the
-- padding being applied, and at the end add a <div> to balance out the parent's </div>
res
:wikitext('</div>')
:node(tbl)
:wikitext('<div>')
else
local nav = res:tag('div')
:attr('role', 'navigation')
:addClass('navbox')
:addClass(args.navboxclass)
:cssText(args.bodystyle)
:cssText(args.style)
:css('padding', '3px')
:node(tbl)
-- aria-labelledby title, otherwise above, otherwise lone group
if args.title or args.above or (args.group1 and not args.group2) then
nav:attr('aria-labelledby', mw.uri.anchorEncode(args.title or args.above or args.group1))
else
nav:attr('aria-label', 'Navbox')
end
end

if (args.nocat or 'false'):lower() == 'false' then
renderTrackingCategories(res)
end
return striped(tostring(res))
end
end


function p.navbox(frame)
function p.navbox(frame)
if not getArgs then
return p._navbox(frame:getParent().args)
getArgs = require('Module:Arguments').getArgs
end
args = getArgs(frame, {wrappers = {'Template:Navbox'}})

-- Read the arguments in the order they'll be output in, to make references number in the right order.
local _
_ = args.title
_ = args.above
for i = 1, 20 do
_ = args["group" .. tostring(i)]
_ = args["list" .. tostring(i)]
end
_ = args.below

return p._navbox(args)
end
end



Latest revision as of 23:23, 15 February 2021

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

--
-- This module implements {{Navbox}}
--

local p = {}

local navbar = require('Module:Navbar')._navbar
local getArgs -- lazily initialized

local args
local border
local listnums
local ODD_EVEN_MARKER = '\127_ODDEVEN_\127'
local RESTART_MARKER = '\127_ODDEVEN0_\127'
local REGEX_MARKER = '\127_ODDEVEN(%d?)_\127'

local function striped(wikitext)
	-- Return wikitext with markers replaced for odd/even striping.
	-- Child (subgroup) navboxes are flagged with a category that is removed
	-- by parent navboxes. The result is that the category shows all pages
	-- where a child navbox is not contained in a parent navbox.
	local orphanCat = '[[Category:Navbox orphans]]'
	if border == 'subgroup' and args.orphan ~= 'yes' then
		-- No change; striping occurs in outermost navbox.
		return wikitext .. orphanCat
	end
	local first, second = 'odd', 'even'
	if args.evenodd then
		if args.evenodd == 'swap' then
			first, second = second, first
		else
			first = args.evenodd
			second = first
		end
	end
	local changer
	if first == second then
		changer = first
	else
		local index = 0
		changer = function (code)
			if code == '0' then
				-- Current occurrence is for a group before a nested table.
				-- Set it to first as a valid although pointless class.
				-- The next occurrence will be the first row after a title
				-- in a subgroup and will also be first.
				index = 0
				return first
			end
			index = index + 1
			return index % 2 == 1 and first or second
		end
	end
	local regex = orphanCat:gsub('([%[%]])', '%%%1')
	return (wikitext:gsub(regex, ''):gsub(REGEX_MARKER, changer))  -- () omits gsub count
end

local function processItem(item, nowrapitems)
	if item:sub(1, 2) == '{|' then
		-- Applying nowrap to lines in a table does not make sense.
		-- Add newlines to compensate for trim of x in |parm=x in a template.
		return '\n' .. item ..'\n'
	end
	if nowrapitems == 'yes' then
		local lines = {}
		for line in (item .. '\n'):gmatch('([^\n]*)\n') do
			local prefix, content = line:match('^([*:;#]+)%s*(.*)')
			if prefix and not content:match('^<span class="nowrap">') then
				line = prefix .. '<span class="nowrap">' .. content .. '</span>'
			end
			table.insert(lines, line)
		end
		item = table.concat(lines, '\n')
	end
	if item:match('^[*:;#]') then
		return '\n' .. item ..'\n'
	end
	return item
end

local function renderNavBar(titleCell)

	if args.navbar ~= 'off' and args.navbar ~= 'plain' and not (not args.name and mw.getCurrentFrame():getParent():getTitle():gsub('/sandbox$', '') == 'Template:Navbox') then
		titleCell:wikitext(navbar{
			args.name,
			mini = 1,
			fontstyle = (args.basestyle or '') .. ';' .. (args.titlestyle or '') .. ';background:none transparent;border:none;box-shadow:none;padding:0;'
		})
	end

end

--
--   Title row
--
local function renderTitleRow(tbl)
	if not args.title then return end

	local titleRow = tbl:tag('tr')

	if args.titlegroup then
		titleRow
			:tag('th')
				:attr('scope', 'row')
				:addClass('navbox-group')
				:addClass(args.titlegroupclass)
				:cssText(args.basestyle)
				:cssText(args.groupstyle)
				:cssText(args.titlegroupstyle)
				:wikitext(args.titlegroup)
	end

	local titleCell = titleRow:tag('th'):attr('scope', 'col')

	if args.titlegroup then
		titleCell
			:css('border-left', '2px solid #fdfdfd')
			:css('width', '100%')
	end

	local titleColspan = 2
	if args.imageleft then titleColspan = titleColspan + 1 end
	if args.image then titleColspan = titleColspan + 1 end
	if args.titlegroup then titleColspan = titleColspan - 1 end

	titleCell
		:cssText(args.basestyle)
		:cssText(args.titlestyle)
		:addClass('navbox-title')
		:attr('colspan', titleColspan)

	renderNavBar(titleCell)

	titleCell
		:tag('div')
			-- id for aria-labelledby attribute
			:attr('id', mw.uri.anchorEncode(args.title))
			:addClass(args.titleclass)
			:css('font-size', '114%')
			:css('margin', '0 4em')
			:wikitext(processItem(args.title))
end

--
--   Above/Below rows
--

local function getAboveBelowColspan()
	local ret = 2
	if args.imageleft then ret = ret + 1 end
	if args.image then ret = ret + 1 end
	return ret
end

local function renderAboveRow(tbl)
	if not args.above then return end

	tbl:tag('tr')
		:tag('td')
			:addClass('navbox-abovebelow')
			:addClass(args.aboveclass)
			:cssText(args.basestyle)
			:cssText(args.abovestyle)
			:attr('colspan', getAboveBelowColspan())
			:tag('div')
				-- id for aria-labelledby attribute, if no title
				:attr('id', args.title and nil or mw.uri.anchorEncode(args.above))
				:wikitext(processItem(args.above, args.nowrapitems))
end

local function renderBelowRow(tbl)
	if not args.below then return end

	tbl:tag('tr')
		:tag('td')
			:addClass('navbox-abovebelow')
			:addClass(args.belowclass)
			:cssText(args.basestyle)
			:cssText(args.belowstyle)
			:attr('colspan', getAboveBelowColspan())
			:tag('div')
				:wikitext(processItem(args.below, args.nowrapitems))
end

--
--   List rows
--
local function renderListRow(tbl, index, listnum)
	local row = tbl:tag('tr')

	if index == 1 and args.imageleft then
		row
			:tag('td')
				:addClass('noviewer')
				:addClass('navbox-image')
				:addClass(args.imageclass)
				:css('width', '1px')               -- Minimize width
				:css('padding', '0px 2px 0px 0px')
				:cssText(args.imageleftstyle)
				:attr('rowspan', #listnums)
				:tag('div')
					:wikitext(processItem(args.imageleft))
	end

	if args['group' .. listnum] then
		local groupCell = row:tag('th')

		-- id for aria-labelledby attribute, if lone group with no title or above
		if listnum == 1 and not (args.title or args.above or args.group2) then
			groupCell
				:attr('id', mw.uri.anchorEncode(args.group1))
		end

		groupCell
			:attr('scope', 'row')
			:addClass('navbox-group')
			:addClass(args.groupclass)
			:cssText(args.basestyle)
			:css('width', args.groupwidth or '1%') -- If groupwidth not specified, minimize width

		groupCell
			:cssText(args.groupstyle)
			:cssText(args['group' .. listnum .. 'style'])
			:wikitext(args['group' .. listnum])
	end

	local listCell = row:tag('td')

	if args['group' .. listnum] then
		listCell
			:css('text-align', 'left')
			:css('border-left-width', '2px')
			:css('border-left-style', 'solid')
	else
		listCell:attr('colspan', 2)
	end

	if not args.groupwidth then
		listCell:css('width', '100%')
	end

	local rowstyle  -- usually nil so cssText(rowstyle) usually adds nothing
	if index % 2 == 1 then
		rowstyle = args.oddstyle
	else
		rowstyle = args.evenstyle
	end

	local listText = args['list' .. listnum]
	local oddEven = ODD_EVEN_MARKER
	if listText:sub(1, 12) == '</div><table' then
		-- Assume list text is for a subgroup navbox so no automatic striping for this row.
		oddEven = listText:find('<th[^>]*"navbox%-title"') and RESTART_MARKER or 'odd'
	end
	listCell
		:css('padding', '0px')
		:cssText(args.liststyle)
		:cssText(rowstyle)
		:cssText(args['list' .. listnum .. 'style'])
		:addClass('navbox-list')
		:addClass('navbox-' .. oddEven)
		:addClass(args.listclass)
		:addClass(args['list' .. listnum .. 'class'])
		:tag('div')
			:css('padding', (index == 1 and args.list1padding) or args.listpadding or '0em 0.25em')
			:wikitext(processItem(listText, args.nowrapitems))

	if index == 1 and args.image then
		row
			:tag('td')
				:addClass('noviewer')
				:addClass('navbox-image')
				:addClass(args.imageclass)
				:css('width', '1px')               -- Minimize width
				:css('padding', '0px 0px 0px 2px')
				:cssText(args.imagestyle)
				:attr('rowspan', #listnums)
				:tag('div')
					:wikitext(processItem(args.image))
	end
end


--
--   Tracking categories
--

local function needsHorizontalLists()
	if border == 'subgroup' or args.tracking == 'no' then
		return false
	end
	local listClasses = {
		['plainlist'] = true, ['hlist'] = true, ['hlist hnum'] = true,
		['hlist hwrap'] = true, ['hlist vcard'] = true, ['vcard hlist'] = true,
		['hlist vevent'] = true,
	}
	return not (listClasses[args.listclass] or listClasses[args.bodyclass])
end

local function hasBackgroundColors()
	for _, key in ipairs({'titlestyle', 'groupstyle', 'basestyle', 'abovestyle', 'belowstyle'}) do
		if tostring(args[key]):find('background', 1, true) then
			return true
		end
	end
end

local function hasBorders()
	for _, key in ipairs({'groupstyle', 'basestyle', 'abovestyle', 'belowstyle'}) do
		if tostring(args[key]):find('border', 1, true) then
			return true
		end
	end
end

local function isIllegible()
	local styleratio = require('Module:Color contrast')._styleratio

	for key, style in pairs(args) do
		if tostring(key):match("style$") then
			if styleratio{mw.text.unstripNoWiki(style)} < 4.5 then
				return true
			end
		end
	end
	return false
end

local function getTrackingCategories()
	local cats = {}
	if needsHorizontalLists() then table.insert(cats, 'Navigational boxes without horizontal lists') end
	if hasBackgroundColors() then table.insert(cats, 'Navboxes using background colours') end
	if isIllegible() then table.insert(cats, 'Potentially illegible navboxes') end
	if hasBorders() then table.insert(cats, 'Navboxes using borders') end
	return cats
end

local function renderTrackingCategories(builder)
	local title = mw.title.getCurrentTitle()
	if title.namespace ~= 10 then return end -- not in template space
	local subpage = title.subpageText
	if subpage == 'doc' or subpage == 'sandbox' or subpage == 'testcases' then return end

	for _, cat in ipairs(getTrackingCategories()) do
		builder:wikitext('[[Category:' .. cat .. ']]')
	end
end

--
--   Main navbox tables
--
local function renderMainTable()
	local tbl = mw.html.create('table')
		:addClass('nowraplinks')
		:addClass(args.bodyclass)

	if args.title and (args.state ~= 'plain' and args.state ~= 'off') then
		if args.state == 'collapsed' then args.state = 'mw-collapsed' end
		tbl
			:addClass('mw-collapsible')
			:addClass(args.state or 'autocollapse')
	end

	tbl:css('border-spacing', 0)
	if border == 'subgroup' or border == 'none' then
		tbl
			:addClass('navbox-subgroup')
			:cssText(args.bodystyle)
			:cssText(args.style)
	else  -- regular navbox - bodystyle and style will be applied to the wrapper table
		tbl
			:addClass('navbox-inner')
			:css('background', 'transparent')
			:css('color', 'inherit')
	end
	tbl:cssText(args.innerstyle)

	renderTitleRow(tbl)
	renderAboveRow(tbl)
	for i, listnum in ipairs(listnums) do
		renderListRow(tbl, i, listnum)
	end
	renderBelowRow(tbl)

	return tbl
end

function p._navbox(navboxArgs)
	args = navboxArgs
	listnums = {}

	for k, _ in pairs(args) do
		if type(k) == 'string' then
			local listnum = k:match('^list(%d+)$')
			if listnum then table.insert(listnums, tonumber(listnum)) end
		end
	end
	table.sort(listnums)

	border = mw.text.trim(args.border or args[1] or '')
	if border == 'child' then
		border = 'subgroup'
	end

	-- render the main body of the navbox
	local tbl = renderMainTable()

	-- render the appropriate wrapper around the navbox, depending on the border param
	local res = mw.html.create()
	if border == 'none' then
		local nav = res:tag('div')
			:attr('role', 'navigation')
			:node(tbl)
		-- aria-labelledby title, otherwise above, otherwise lone group
		if args.title or args.above or (args.group1 and not args.group2) then
			nav:attr('aria-labelledby', mw.uri.anchorEncode(args.title or args.above or args.group1))
		else
			nav:attr('aria-label', 'Navbox')
		end
	elseif border == 'subgroup' then
		-- We assume that this navbox is being rendered in a list cell of a parent navbox, and is
		-- therefore inside a div with padding:0em 0.25em. We start with a </div> to avoid the
		-- padding being applied, and at the end add a <div> to balance out the parent's </div>
		res
			:wikitext('</div>')
			:node(tbl)
			:wikitext('<div>')
	else
		local nav = res:tag('div')
			:attr('role', 'navigation')
			:addClass('navbox')
			:addClass(args.navboxclass)
			:cssText(args.bodystyle)
			:cssText(args.style)
			:css('padding', '3px')
			:node(tbl)
		-- aria-labelledby title, otherwise above, otherwise lone group
		if args.title or args.above or (args.group1 and not args.group2) then
			nav:attr('aria-labelledby', mw.uri.anchorEncode(args.title or args.above or args.group1))
		else
			nav:attr('aria-label', 'Navbox')
		end
	end

	if (args.nocat or 'false'):lower() == 'false' then
		renderTrackingCategories(res)
	end
	return striped(tostring(res))
end

function p.navbox(frame)
	if not getArgs then
		getArgs = require('Module:Arguments').getArgs
	end
	args = getArgs(frame, {wrappers = {'Template:Navbox'}})

	-- Read the arguments in the order they'll be output in, to make references number in the right order.
	local _
	_ = args.title
	_ = args.above
	for i = 1, 20 do
		_ = args["group" .. tostring(i)]
		_ = args["list" .. tostring(i)]
	end
	_ = args.below

	return p._navbox(args)
end

return p