Module:Navbox: Difference between revisions
		
		
		
		Jump to navigation
		Jump to search
		
Content added Content deleted
m (1 revision imported)  | 
				 (fix striping)  | 
				||
| Line 1: | Line 1: | ||
require('strict')  | 
  require('strict')  | 
||
local p = {}  | 
  local p = {}  | 
||
| − | local navbar = require('Module:Navbar')._navbar  | 
  ||
local cfg = mw.loadData('Module:Navbox/configuration')  | 
  local cfg = mw.loadData('Module:Navbox/configuration')  | 
||
| + | local inArray = require("Module:TableTools").inArray  | 
||
local getArgs -- lazily initialized  | 
  local getArgs -- lazily initialized  | 
||
| − | local args  | 
  ||
local format = string.format  | 
  local format = string.format  | 
||
| − | + | function p._navbox(args)  | 
|
| + | 	local function striped(wikitext, border)  | 
||
| − | 	-- Return wikitext with markers replaced for odd/even striping.  | 
  ||
| + | 		-- Return wikitext with markers replaced for odd/even striping.  | 
||
| − | 	-- Child (subgroup) navboxes are flagged with a category that is removed  | 
  ||
| − | + | 		-- 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.  | 
  ||
| + | 		-- where a child navbox is not contained in a parent navbox.  | 
||
| − | 	local orphanCat = cfg.category.orphan  | 
  ||
| + | 		local orphanCat = cfg.category.orphan  | 
||
| − | 	if border == cfg.keyword.border_subgroup and args[cfg.arg.orphan] ~= cfg.keyword.orphan_yes then  | 
  ||
| + | 		if border == cfg.keyword.border_subgroup and args[cfg.arg.orphan] ~= cfg.keyword.orphan_yes then  | 
||
| − | 		-- No change; striping occurs in outermost navbox.  | 
  ||
| + | 			-- No change; striping occurs in outermost navbox.  | 
||
| − | 		return wikitext .. orphanCat  | 
  ||
| + | 			return wikitext .. orphanCat  | 
||
| − | 	end  | 
  ||
| + | 		end  | 
||
| − | 	local first, second = cfg.class.navbox_odd_part, cfg.class.navbox_even_part  | 
  ||
| + | 		local first, second = cfg.class.navbox_odd_part, cfg.class.navbox_even_part  | 
||
| − | 	if args[cfg.arg.evenodd] then  | 
  ||
| − | 		if args[cfg.arg.evenodd]  | 
  + | 		if args[cfg.arg.evenodd] then  | 
| + | 			if args[cfg.arg.evenodd] == cfg.keyword.evenodd_swap then  | 
||
| − | 			first, second = second, first  | 
  ||
| + | 				first, second = second, first  | 
||
| + | 			else  | 
||
| + | 				first = args[cfg.arg.evenodd]  | 
||
| + | 				second = first  | 
||
| + | 			end  | 
||
| + | 		end  | 
||
| + | 		local changer  | 
||
| + | 		if first == second then  | 
||
| + | 			changer = first  | 
||
		else  | 
  		else  | 
||
| + | 			local index = 0  | 
||
| − | 			first = args[cfg.arg.evenodd]  | 
  ||
| − | + | 			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  | 
  		end  | 
||
| + | 		local regex = orphanCat:gsub('([%[%]])', '%%%1')  | 
||
| + | 		return (wikitext:gsub(regex, ''):gsub(cfg.marker.regex, changer)) -- () omits gsub count  | 
||
	end  | 
  	end  | 
||
| + | |||
| − | 	local changer  | 
  ||
| + | 	local function processItem(item, nowrapitems)  | 
||
| − | 	if first == second then  | 
  ||
| + | 		if item:sub(1, 2) == '{|' then  | 
||
| − | 		changer = first  | 
  ||
| + | 			-- Applying nowrap to lines in a table does not make sense.  | 
||
| − | 	else  | 
  ||
| + | 			-- Add newlines to compensate for trim of x in |parm=x in a template.  | 
||
| − | 		local index = 0  | 
  ||
| + | 			return '\n' .. item ..'\n'  | 
||
| − | 		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  | 
||
| + | 		if nowrapitems == cfg.keyword.nowrapitems_yes then  | 
||
| − | 	end  | 
  ||
| + | 			local lines = {}  | 
||
| − | 	local regex = orphanCat:gsub('([%[%]])', '%%%1')  | 
  ||
| + | 			for line in (item .. '\n'):gmatch('([^\n]*)\n') do  | 
||
| − | 	return (wikitext:gsub(regex, ''):gsub(cfg.marker.regex, changer)) -- () omits gsub count  | 
  ||
| + | 				local prefix, content = line:match('^([*:;#]+)%s*(.*)')  | 
||
| − | end  | 
  ||
| + | 				if prefix and not content:match(cfg.pattern.nowrap) then  | 
||
| − | |||
| + | 					line = format(cfg.nowrap_item, prefix, content)  | 
||
| − | local function processItem(item, nowrapitems)  | 
  ||
| + | 				end  | 
||
| − | 	if item:sub(1, 2) == '{|' then  | 
  ||
| + | 				table.insert(lines, line)  | 
||
| − | 		-- 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 == cfg.keyword.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(cfg.pattern.nowrap) then  | 
  ||
| − | 				line = format(cfg.nowrap_item, prefix, content)  | 
  ||
			end  | 
  			end  | 
||
| − | 			table.  | 
  + | 			item = table.concat(lines, '\n')  | 
		end  | 
  		end  | 
||
| − | 		item  | 
  + | 		if item:match('^[*:;#]') then  | 
| + | 			return '\n' .. item ..'\n'  | 
||
| + | 		end  | 
||
| + | 		return item  | 
||
	end  | 
  	end  | 
||
| + | |||
| − | 	if item:match('^[*:;#]') then  | 
  ||
| + | 	local function has_navbar()  | 
||
| − | 		return '\n' .. item ..'\n'  | 
  ||
| + | 		return args[cfg.arg.navbar] ~= cfg.keyword.navbar_off  | 
||
| + | 			and args[cfg.arg.navbar] ~= cfg.keyword.navbar_plain  | 
||
| + | 			and (  | 
||
| + | 				args[cfg.arg.name]  | 
||
| + | 				or mw.getCurrentFrame():getParent():getTitle():gsub(cfg.pattern.sandbox, '')  | 
||
| + | 					~= cfg.pattern.navbox  | 
||
| + | 			)  | 
||
	end  | 
  	end  | 
||
| + | |||
| − | 	return item  | 
  ||
| + | 	-- extract text color from css, which is the only permitted inline CSS for the navbar  | 
||
| − | end  | 
  ||
| + | 	local function extract_color(css_str)  | 
||
| − | |||
| + | 		-- return nil because navbar takes its argument into mw.html which handles  | 
||
| − | local function has_navbar()  | 
  ||
| + | 		-- nil gracefully, removing the associated style attribute  | 
||
| − | 	return args[cfg.arg.navbar] ~= cfg.keyword.navbar_off  | 
  ||
| + | 		return mw.ustring.match(';' .. css_str .. ';', '.*;%s*([Cc][Oo][Ll][Oo][Rr]%s*:%s*.-)%s*;') or nil  | 
||
| − | 		and args[cfg.arg.navbar] ~= cfg.keyword.navbar_plain  | 
  ||
| − | 		and (  | 
  ||
| − | 			args[cfg.arg.name]  | 
  ||
| − | 			or mw.getCurrentFrame():getParent():getTitle():gsub(cfg.pattern.sandbox, '')  | 
  ||
| − | 				~= cfg.pattern.navbox  | 
  ||
| − | 		)  | 
  ||
| − | end  | 
  ||
| − | |||
| − | local function renderNavBar(titleCell)  | 
  ||
| − | 	if has_navbar() then  | 
  ||
| − | 		titleCell:wikitext(navbar{  | 
  ||
| − | 			[cfg.navbar.name] = args[cfg.arg.name],  | 
  ||
| − | 			[cfg.navbar.mini] = 1,  | 
  ||
| − | 			[cfg.navbar.fontstyle] = (args[cfg.arg.basestyle] or '') .. ';' ..  | 
  ||
| − | 				(args[cfg.arg.titlestyle] or '') ..  | 
  ||
| − | 				';background:none transparent;border:none;box-shadow:none;padding:0;'  | 
  ||
| − | 		})  | 
  ||
	end  | 
  	end  | 
||
| + | |||
| − | |||
| + | 	local function renderNavBar(titleCell)  | 
||
| − | end  | 
  ||
| + | 		if has_navbar() then  | 
||
| − | |||
| + | 			local navbar = require('Module:Navbar')._navbar  | 
||
| − | local function renderTitleRow(tbl)  | 
  ||
| + | 			titleCell:wikitext(navbar{  | 
||
| − | 	if not args[cfg.arg.title] then return end  | 
  ||
| + | 				[cfg.navbar.name] = args[cfg.arg.name],  | 
||
| − | |||
| + | 				[cfg.navbar.mini] = 1,  | 
||
| − | 	local titleRow = tbl:tag('tr')  | 
  ||
| + | 				[cfg.navbar.fontstyle] = extract_color(  | 
||
| − | |||
| + | 					(args[cfg.arg.basestyle] or '') .. ';' .. (args[cfg.arg.titlestyle] or '')  | 
||
| − | 	local titleCell = titleRow:tag('th'):attr('scope', 'col')  | 
  ||
| + | 				)  | 
||
| − | |||
| + | 			})  | 
||
| − | 	local titleColspan = 2  | 
  ||
| + | 		end  | 
||
| − | 	if args[cfg.arg.imageleft] then titleColspan = titleColspan + 1 end  | 
  ||
| + | |||
| − | 	if args[cfg.arg.image] then titleColspan = titleColspan + 1 end  | 
  ||
| + | 	end  | 
||
| − | |||
| + | |||
| − | 	titleCell  | 
  ||
| + | 	local function renderTitleRow(tbl)  | 
||
| − | 		:cssText(args[cfg.arg.basestyle])  | 
  ||
| − | + | 		if not args[cfg.arg.title] then return end  | 
|
| + | |||
| − | 		:addClass(cfg.class.navbox_title)  | 
  ||
| + | 		local titleRow = tbl:tag('tr')  | 
||
| − | 		:attr('colspan', titleColspan)  | 
  ||
| + | |||
| − | |||
| + | 		local titleCell = titleRow:tag('th'):attr('scope', 'col')  | 
||
| − | 	renderNavBar(titleCell)  | 
  ||
| + | |||
| − | |||
| + | 		local titleColspan = 2  | 
||
| − | 	titleCell  | 
  ||
| + | 		if args[cfg.arg.imageleft] then titleColspan = titleColspan + 1 end  | 
||
| − | 		:tag('div')  | 
  ||
| + | 		if args[cfg.arg.image] then titleColspan = titleColspan + 1 end  | 
||
| − | 			-- id for aria-labelledby attribute  | 
  ||
| + | |||
| − | 			:attr('id', mw.uri.anchorEncode(args[cfg.arg.title]))  | 
  ||
| + | 		titleCell  | 
||
| − | 			:addClass(args[cfg.arg.titleclass])  | 
  ||
| − | 			:css('font-size', '114%')  | 
  ||
| − | 			:css('margin', '0 4em')  | 
  ||
| − | 			:wikitext(processItem(args[cfg.arg.title]))  | 
  ||
| − | end  | 
  ||
| − | |||
| − | local function getAboveBelowColspan()  | 
  ||
| − | 	local ret = 2  | 
  ||
| − | 	if args[cfg.arg.imageleft] then ret = ret + 1 end  | 
  ||
| − | 	if args[cfg.arg.image] then ret = ret + 1 end  | 
  ||
| − | 	return ret  | 
  ||
| − | end  | 
  ||
| − | |||
| − | local function renderAboveRow(tbl)  | 
  ||
| − | 	if not args[cfg.arg.above] then return end  | 
  ||
| − | |||
| − | 	tbl:tag('tr')  | 
  ||
| − | 		:tag('td')  | 
  ||
| − | 			:addClass(cfg.class.navbox_abovebelow)  | 
  ||
| − | 			:addClass(args[cfg.arg.aboveclass])  | 
  ||
			:cssText(args[cfg.arg.basestyle])  | 
  			:cssText(args[cfg.arg.basestyle])  | 
||
| − | 			:cssText(args[cfg.arg.  | 
  + | 			:cssText(args[cfg.arg.titlestyle])  | 
| + | 			:addClass(cfg.class.navbox_title)  | 
||
| − | 			:attr('colspan', getAboveBelowColspan())  | 
  ||
| + | 			:attr('colspan', titleColspan)  | 
||
| + | |||
| + | 		renderNavBar(titleCell)  | 
||
| + | |||
| + | 		titleCell  | 
||
			:tag('div')  | 
  			:tag('div')  | 
||
| − | 				-- id for aria-labelledby attribute  | 
  + | 				-- id for aria-labelledby attribute  | 
| − | 				:attr('id',  | 
  + | 				:attr('id', mw.uri.anchorEncode(args[cfg.arg.title]))  | 
| − | 				:  | 
  + | 				:addClass(args[cfg.arg.titleclass])  | 
| + | 				:css('font-size', '114%')  | 
||
| − | end  | 
  ||
| + | 				:css('margin', '0 4em')  | 
||
| − | |||
| + | 				:wikitext(processItem(args[cfg.arg.title]))  | 
||
| − | local function renderBelowRow(tbl)  | 
  ||
| + | 	end  | 
||
| − | 	if not args[cfg.arg.below] then return end  | 
  ||
| + | |||
| − | |||
| + | 	local function getAboveBelowColspan()  | 
||
| − | 	tbl:tag('tr')  | 
  ||
| + | 		local ret = 2  | 
||
| − | 		:tag('td')  | 
  ||
| + | 		if args[cfg.arg.imageleft] then ret = ret + 1 end  | 
||
| − | 			:addClass(cfg.class.navbox_abovebelow)  | 
  ||
| − | + | 		if args[cfg.arg.image] then ret = ret + 1 end  | 
|
| + | 		return ret  | 
||
| − | 			:cssText(args[cfg.arg.basestyle])  | 
  ||
| + | 	end  | 
||
| − | 			:cssText(args[cfg.arg.belowstyle])  | 
  ||
| + | |||
| − | 			:attr('colspan', getAboveBelowColspan())  | 
  ||
| + | 	local function renderAboveRow(tbl)  | 
||
| − | 			:tag('div')  | 
  ||
| − | + | 		if not args[cfg.arg.above] then return end  | 
|
| + | |||
| − | end  | 
  ||
| + | 		tbl:tag('tr')  | 
||
| − | |||
| − | local function renderListRow(tbl, index, listnum, listnums_size)  | 
  ||
| − | 	local row = tbl:tag('tr')  | 
  ||
| − | |||
| − | 	if index == 1 and args[cfg.arg.imageleft] then  | 
  ||
| − | 		row  | 
  ||
			:tag('td')  | 
  			:tag('td')  | 
||
| − | 				:addClass(cfg.class.  | 
  + | 				:addClass(cfg.class.navbox_abovebelow)  | 
| − | 				:addClass(cfg.  | 
  + | 				:addClass(args[cfg.arg.aboveclass])  | 
| − | 				:  | 
  + | 				:cssText(args[cfg.arg.basestyle])  | 
| + | 				:cssText(args[cfg.arg.abovestyle])  | 
||
| − | 				:css('width', '1px')               -- Minimize width  | 
  ||
| − | 				:  | 
  + | 				:attr('colspan', getAboveBelowColspan())  | 
| − | 				:cssText(args[cfg.arg.imageleftstyle])  | 
  ||
| − | 				:attr('rowspan', listnums_size)  | 
  ||
				:tag('div')  | 
  				:tag('div')  | 
||
| + | 					-- id for aria-labelledby attribute, if no title  | 
||
| − | 					:wikitext(processItem(args[cfg.arg.imageleft]))  | 
  ||
| + | 					:attr('id', (not args[cfg.arg.title]) and mw.uri.anchorEncode(args[cfg.arg.above]) or nil)  | 
||
| + | 					:wikitext(processItem(args[cfg.arg.above], args[cfg.arg.nowrapitems]))  | 
||
	end  | 
  	end  | 
||
| + | |||
| − | |||
| + | 	local function renderBelowRow(tbl)  | 
||
| − | 	local group_and_num = format(cfg.arg.group_and_num, listnum)  | 
  ||
| + | 		if not args[cfg.arg.below] then return end  | 
||
| − | 	local groupstyle_and_num = format(cfg.arg.groupstyle_and_num, listnum)  | 
  ||
| + | |||
| − | 	if args[group_and_num] then  | 
  ||
| − | + | 		tbl:tag('tr')  | 
|
| − | |||
| − | 		-- id for aria-labelledby attribute, if lone group with no title or above  | 
  ||
| − | 		if listnum == 1 and not (args[cfg.arg.title] or args[cfg.arg.above] or args[cfg.arg.group2]) then  | 
  ||
| − | 			groupCell  | 
  ||
| − | 				:attr('id', mw.uri.anchorEncode(args[cfg.arg.group1]))  | 
  ||
| − | 		end  | 
  ||
| − | |||
| − | 		groupCell  | 
  ||
| − | 			:attr('scope', 'row')  | 
  ||
| − | 			:addClass(cfg.class.navbox_group)  | 
  ||
| − | 			:addClass(args[cfg.arg.groupclass])  | 
  ||
| − | 			:cssText(args[cfg.arg.basestyle])  | 
  ||
| − | 			-- If groupwidth not specified, minimize width  | 
  ||
| − | 			:css('width', args[cfg.arg.groupwidth] or '1%')  | 
  ||
| − | |||
| − | 		groupCell  | 
  ||
| − | 			:cssText(args[cfg.arg.groupstyle])  | 
  ||
| − | 			:cssText(args[groupstyle_and_num])  | 
  ||
| − | 			:wikitext(args[group_and_num])  | 
  ||
| − | 	end  | 
  ||
| − | |||
| − | 	local listCell = row:tag('td')  | 
  ||
| − | |||
| − | 	if args[group_and_num] then  | 
  ||
| − | 		listCell  | 
  ||
| − | 			:addClass(cfg.class.navbox_list_with_group)  | 
  ||
| − | 	else  | 
  ||
| − | 		listCell:attr('colspan', 2)  | 
  ||
| − | 	end  | 
  ||
| − | |||
| − | 	if not args[cfg.arg.groupwidth] then  | 
  ||
| − | 		listCell:css('width', '100%')  | 
  ||
| − | 	end  | 
  ||
| − | |||
| − | 	local rowstyle  -- usually nil so cssText(rowstyle) usually adds nothing  | 
  ||
| − | 	if index % 2 == 1 then  | 
  ||
| − | 		rowstyle = args[cfg.arg.oddstyle]  | 
  ||
| − | 	else  | 
  ||
| − | 		rowstyle = args[cfg.arg.evenstyle]  | 
  ||
| − | 	end  | 
  ||
| − | |||
| − | 	local list_and_num = format(cfg.arg.list_and_num, listnum)  | 
  ||
| − | 	local listText = args[list_and_num]  | 
  ||
| − | 	local oddEven = cfg.marker.oddeven  | 
  ||
| − | 	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(cfg.pattern.navbox_title) and cfg.marker.restart or cfg.class.navbox_odd_part  | 
  ||
| − | 	end  | 
  ||
| − | |||
| − | 	local liststyle_and_num = format(cfg.arg.liststyle_and_num, listnum)  | 
  ||
| − | 	local listclass_and_num = format(cfg.arg.listclass_and_num, listnum)  | 
  ||
| − | 	listCell  | 
  ||
| − | 		:css('padding', '0')  | 
  ||
| − | 		:cssText(args[cfg.arg.liststyle])  | 
  ||
| − | 		:cssText(rowstyle)  | 
  ||
| − | 		:cssText(args[liststyle_and_num])  | 
  ||
| − | 		:addClass(cfg.class.navbox_list)  | 
  ||
| − | 		:addClass(cfg.class.navbox_part .. oddEven)  | 
  ||
| − | 		:addClass(args[cfg.arg.listclass])  | 
  ||
| − | 		:addClass(args[listclass_and_num])  | 
  ||
| − | 		:tag('div')  | 
  ||
| − | 			:css('padding',  | 
  ||
| − | 				(index == 1 and args[cfg.arg.list1padding]) or args[cfg.arg.listpadding] or '0 0.25em'  | 
  ||
| − | 			)  | 
  ||
| − | 			:wikitext(processItem(listText, args[cfg.arg.nowrapitems]))  | 
  ||
| − | |||
| − | 	if index == 1 and args[cfg.arg.image] then  | 
  ||
| − | 		row  | 
  ||
			:tag('td')  | 
  			:tag('td')  | 
||
| − | 				:addClass(cfg.class.  | 
  + | 				:addClass(cfg.class.navbox_abovebelow)  | 
| − | 				:addClass(cfg.  | 
  + | 				:addClass(args[cfg.arg.belowclass])  | 
| − | 				:  | 
  + | 				:cssText(args[cfg.arg.basestyle])  | 
| + | 				:cssText(args[cfg.arg.belowstyle])  | 
||
| − | 				:css('width', '1px')               -- Minimize width  | 
  ||
| − | 				:  | 
  + | 				:attr('colspan', getAboveBelowColspan())  | 
| − | 				:cssText(args[cfg.arg.imagestyle])  | 
  ||
| − | 				:attr('rowspan', listnums_size)  | 
  ||
				:tag('div')  | 
  				:tag('div')  | 
||
| − | 					:wikitext(processItem(args[cfg.arg.  | 
  + | 					:wikitext(processItem(args[cfg.arg.below], args[cfg.arg.nowrapitems]))  | 
	end  | 
  	end  | 
||
| − | end  | 
  ||
| − | |||
| − | local function has_list_class(htmlclass)  | 
  ||
| − | 	local patterns = {  | 
  ||
| − | 		'^' .. htmlclass .. '$',  | 
  ||
| − | 		'%s' .. htmlclass .. '$',  | 
  ||
| − | 		'^' .. htmlclass .. '%s',  | 
  ||
| − | 		'%s' .. htmlclass .. '%s'  | 
  ||
| − | 	}  | 
  ||
| + | 	local function renderListRow(tbl, index, listnum, listnums_size)  | 
||
| − | 	for arg, _ in pairs(args) do  | 
  ||
| + | 		local row = tbl:tag('tr')  | 
||
| − | 		if type(arg) == 'string' and mw.ustring.find(arg, cfg.pattern.class) then  | 
  ||
| + | |||
| − | 			for _, pattern in ipairs(patterns) do  | 
  ||
| − | + | 		if index == 1 and args[cfg.arg.imageleft] then  | 
|
| + | 			row  | 
||
| − | 					return true  | 
  ||
| − | + | 				:tag('td')  | 
|
| + | 					:addClass(cfg.class.noviewer)  | 
||
| + | 					:addClass(cfg.class.navbox_image)  | 
||
| + | 					:addClass(args[cfg.arg.imageclass])  | 
||
| + | 					:css('width', '1px')               -- Minimize width  | 
||
| + | 					:css('padding', '0 2px 0 0')  | 
||
| + | 					:cssText(args[cfg.arg.imageleftstyle])  | 
||
| + | 					:attr('rowspan', listnums_size)  | 
||
| + | 					:tag('div')  | 
||
| + | 						:wikitext(processItem(args[cfg.arg.imageleft]))  | 
||
| + | 		end  | 
||
| + | |||
| + | 		local group_and_num = format(cfg.arg.group_and_num, listnum)  | 
||
| + | 		local groupstyle_and_num = format(cfg.arg.groupstyle_and_num, listnum)  | 
||
| + | 		if args[group_and_num] 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[cfg.arg.title] or args[cfg.arg.above] or args[cfg.arg.group2]) then  | 
||
| + | 				groupCell  | 
||
| + | 					:attr('id', mw.uri.anchorEncode(args[cfg.arg.group1]))  | 
||
			end  | 
  			end  | 
||
| + | |||
| + | 			groupCell  | 
||
| + | 				:attr('scope', 'row')  | 
||
| + | 				:addClass(cfg.class.navbox_group)  | 
||
| + | 				:addClass(args[cfg.arg.groupclass])  | 
||
| + | 				:cssText(args[cfg.arg.basestyle])  | 
||
| + | 				-- If groupwidth not specified, minimize width  | 
||
| + | 				:css('width', args[cfg.arg.groupwidth] or '1%')  | 
||
| + | |||
| + | 			groupCell  | 
||
| + | 				:cssText(args[cfg.arg.groupstyle])  | 
||
| + | 				:cssText(args[groupstyle_and_num])  | 
||
| + | 				:wikitext(args[group_and_num])  | 
||
		end  | 
  		end  | 
||
| − | + | ||
| + | 		local listCell = row:tag('td')  | 
||
| − | 	return false  | 
  ||
| + | |||
| − | end  | 
  ||
| + | 		if args[group_and_num] then  | 
||
| − | |||
| + | 			listCell  | 
||
| − | -- there are a lot of list classes in the wild, so we add their TemplateStyles  | 
  ||
| + | 				:addClass(cfg.class.navbox_list_with_group)  | 
||
| − | local function add_list_styles()  | 
  ||
| − | 	local frame = mw.getCurrentFrame()  | 
  ||
| − | 	local function add_list_templatestyles(htmlclass, templatestyles)  | 
  ||
| − | 		if has_list_class(htmlclass) then  | 
  ||
| − | 			return frame:extensionTag{  | 
  ||
| − | 				name = 'templatestyles', args = { src = templatestyles }  | 
  ||
| − | 			}  | 
  ||
		else  | 
  		else  | 
||
| + | 			listCell:attr('colspan', 2)  | 
||
| − | 			return ''  | 
  ||
		end  | 
  		end  | 
||
| − | 	end  | 
  ||
| + | 		if not args[cfg.arg.groupwidth] then  | 
||
| − | 	local hlist_styles = add_list_templatestyles('hlist', cfg.hlist_templatestyles)  | 
  ||
| + | 			listCell:css('width', '100%')  | 
||
| − | 	local plainlist_styles = add_list_templatestyles('plainlist', cfg.plainlist_templatestyles)  | 
  ||
| + | 		end  | 
||
| + | 		local rowstyle  -- usually nil so cssText(rowstyle) usually adds nothing  | 
||
| − | 	-- a second workaround for [[phab:T303378]]  | 
  ||
| + | 		if index % 2 == 1 then  | 
||
| − | 	-- when that issue is fixed, we can actually use has_navbar not to emit the  | 
  ||
| + | 			rowstyle = args[cfg.arg.oddstyle]  | 
||
| − | 	-- tag here if we want  | 
  ||
| + | 		else  | 
||
| − | 	if has_navbar() and hlist_styles == '' then  | 
  ||
| + | 			rowstyle = args[cfg.arg.evenstyle]  | 
||
| − | 		hlist_styles = frame:extensionTag{  | 
  ||
| + | 		end  | 
||
| − | 			name = 'templatestyles', args = { src = cfg.hlist_templatestyles }  | 
  ||
| + | |||
| − | 		}  | 
  ||
| + | 		local list_and_num = format(cfg.arg.list_and_num, listnum)  | 
||
| + | 		local listText = args[list_and_num]  | 
||
| + | |||
| + | 		if inArray(cfg.keyword.subgroups, listText) then  | 
||
| + | 			local childArgs = {  | 
||
| + | 				[cfg.arg.border] = cfg.keyword.border_subgroup,  | 
||
| + | 				[cfg.arg.navbar] = cfg.keyword.navbar_plain  | 
||
| + | 			}  | 
||
| + | 			local hasChildArgs = false  | 
||
| + | 			for k, v in pairs(args) do  | 
||
| + | 				k = tostring(k)  | 
||
| + | 				for _, w in ipairs(cfg.keyword.subgroups) do  | 
||
| + | 					w = w .. listnum .. "_"  | 
||
| + | 					if (#k > #w) and (k:sub(1, #w) == w) then  | 
||
| + | 						childArgs[k:sub(#w + 1)] = v  | 
||
| + | 						hasChildArgs = true  | 
||
| + | 					end  | 
||
| + | 				end  | 
||
| + | 			end  | 
||
| + | 			listText = hasChildArgs and p._navbox(childArgs) or listText  | 
||
| + | 		end  | 
||
| + | |||
| + | 		local oddEven = cfg.marker.oddeven  | 
||
| + | 		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(cfg.pattern.navbox_title) and cfg.marker.restart or cfg.class.navbox_odd_part  | 
||
| + | 		end  | 
||
| + | |||
| + | 		local liststyle_and_num = format(cfg.arg.liststyle_and_num, listnum)  | 
||
| + | 		local listclass_and_num = format(cfg.arg.listclass_and_num, listnum)  | 
||
| + | 		listCell  | 
||
| + | 			:css('padding', '0')  | 
||
| + | 			:cssText(args[cfg.arg.liststyle])  | 
||
| + | 			:cssText(rowstyle)  | 
||
| + | 			:cssText(args[liststyle_and_num])  | 
||
| + | 			:addClass(cfg.class.navbox_list)  | 
||
| + | 			:addClass(cfg.class.navbox_part .. oddEven)  | 
||
| + | 			:addClass(args[cfg.arg.listclass])  | 
||
| + | 			:addClass(args[listclass_and_num])  | 
||
| + | 			:tag('div')  | 
||
| + | 				:css('padding',  | 
||
| + | 					(index == 1 and args[cfg.arg.list1padding]) or args[cfg.arg.listpadding] or '0 0.25em'  | 
||
| + | 				)  | 
||
| + | 				:wikitext(processItem(listText, args[cfg.arg.nowrapitems]))  | 
||
| + | |||
| + | 		if index == 1 and args[cfg.arg.image] then  | 
||
| + | 			row  | 
||
| + | 				:tag('td')  | 
||
| + | 					:addClass(cfg.class.noviewer)  | 
||
| + | 					:addClass(cfg.class.navbox_image)  | 
||
| + | 					:addClass(args[cfg.arg.imageclass])  | 
||
| + | 					:css('width', '1px')               -- Minimize width  | 
||
| + | 					:css('padding', '0 0 0 2px')  | 
||
| + | 					:cssText(args[cfg.arg.imagestyle])  | 
||
| + | 					:attr('rowspan', listnums_size)  | 
||
| + | 					:tag('div')  | 
||
| + | 						:wikitext(processItem(args[cfg.arg.image]))  | 
||
| + | 		end  | 
||
	end  | 
  	end  | 
||
| + | 	local function has_list_class(htmlclass)  | 
||
| − | 	-- hlist -> plainlist is best-effort to preserve old Common.css ordering.  | 
  ||
| + | 		local patterns = {  | 
||
| − | 	-- this ordering is not a guarantee because most navboxes will emit only  | 
  ||
| + | 			'^' .. htmlclass .. '$',  | 
||
| − | 	-- one of these classes [hlist_note]  | 
  ||
| + | 			'%s' .. htmlclass .. '$',  | 
||
| − | 	return hlist_styles .. plainlist_styles  | 
  ||
| + | 			'^' .. htmlclass .. '%s',  | 
||
| − | end  | 
  ||
| + | 			'%s' .. htmlclass .. '%s'  | 
||
| − | |||
| + | 		}  | 
||
| − | local function needsHorizontalLists(border)  | 
  ||
| + | |||
| − | 	if border == cfg.keyword.border_subgroup or args[cfg.arg.tracking] == cfg.keyword.tracking_no then  | 
  ||
| + | 		for arg, _ in pairs(args) do  | 
||
| + | 			if type(arg) == 'string' and mw.ustring.find(arg, cfg.pattern.class) then  | 
||
| + | 				for _, pattern in ipairs(patterns) do  | 
||
| + | 					if mw.ustring.find(args[arg] or '', pattern) then  | 
||
| + | 						return true  | 
||
| + | 					end  | 
||
| + | 				end  | 
||
| + | 			end  | 
||
| + | 		end  | 
||
		return false  | 
  		return false  | 
||
	end  | 
  	end  | 
||
| + | |||
| − | 	return not has_list_class(cfg.pattern.hlist) and not has_list_class(cfg.pattern.plainlist)  | 
  ||
| + | 	-- there are a lot of list classes in the wild, so we add their TemplateStyles  | 
||
| − | end  | 
  ||
| + | 	local function add_list_styles()  | 
||
| − | |||
| − | local   | 
  + | 		local frame = mw.getCurrentFrame()  | 
| + | 		local function add_list_templatestyles(htmlclass, templatestyles)  | 
||
| − | 	for _, key in ipairs({cfg.arg.titlestyle, cfg.arg.groupstyle,  | 
  ||
| + | 			if has_list_class(htmlclass) then  | 
||
| − | 		cfg.arg.basestyle, cfg.arg.abovestyle, cfg.arg.belowstyle}) do  | 
  ||
| + | 				return frame:extensionTag{  | 
||
| − | 		if tostring(args[key]):find('background', 1, true) then  | 
  ||
| + | 					name = 'templatestyles', args = { src = templatestyles }  | 
||
| − | 			return true  | 
  ||
| + | 				}  | 
||
| + | 			else  | 
||
| + | 				return ''  | 
||
| + | 			end  | 
||
		end  | 
  		end  | 
||
| + | |||
| + | 		local hlist_styles = add_list_templatestyles('hlist', cfg.hlist_templatestyles)  | 
||
| + | 		local plainlist_styles = add_list_templatestyles('plainlist', cfg.plainlist_templatestyles)  | 
||
| + | |||
| + | 		-- a second workaround for [[phab:T303378]]  | 
||
| + | 		-- when that issue is fixed, we can actually use has_navbar not to emit the  | 
||
| + | 		-- tag here if we want  | 
||
| + | 		if has_navbar() and hlist_styles == '' then  | 
||
| + | 			hlist_styles = frame:extensionTag{  | 
||
| + | 				name = 'templatestyles', args = { src = cfg.hlist_templatestyles }  | 
||
| + | 			}  | 
||
| + | 		end  | 
||
| + | |||
| + | 		-- hlist -> plainlist is best-effort to preserve old Common.css ordering.  | 
||
| + | 		-- this ordering is not a guarantee because most navboxes will emit only  | 
||
| + | 		-- one of these classes [hlist_note]  | 
||
| + | 		return hlist_styles .. plainlist_styles  | 
||
	end  | 
  	end  | 
||
| + | |||
| − | 	return false  | 
  ||
| + | 	local function needsHorizontalLists(border)  | 
||
| − | end  | 
  ||
| + | 		if border == cfg.keyword.border_subgroup or args[cfg.arg.tracking] == cfg.keyword.tracking_no then  | 
||
| − | |||
| + | 			return false  | 
||
| − | local function hasBorders()  | 
  ||
| − | 	for _, key in ipairs({cfg.arg.groupstyle, cfg.arg.basestyle,  | 
  ||
| − | 		cfg.arg.abovestyle, cfg.arg.belowstyle}) do  | 
  ||
| − | 		if tostring(args[key]):find('border', 1, true) then  | 
  ||
| − | 			return true  | 
  ||
		end  | 
  		end  | 
||
| + | 		return not has_list_class(cfg.pattern.hlist) and not has_list_class(cfg.pattern.plainlist)  | 
||
	end  | 
  	end  | 
||
| + | |||
| − | 	return false  | 
  ||
| + | 	local function hasBackgroundColors()  | 
||
| − | end  | 
  ||
| + | 		for _, key in ipairs({cfg.arg.titlestyle, cfg.arg.groupstyle,  | 
||
| − | |||
| + | 			cfg.arg.basestyle, cfg.arg.abovestyle, cfg.arg.belowstyle}) do  | 
||
| − | local function isIllegible()  | 
  ||
| + | 			if tostring(args[key]):find('background', 1, true) then  | 
||
| − | 	local styleratio = require('Module:Color contrast')._styleratio  | 
  ||
| − | 	for key, style in pairs(args) do  | 
  ||
| − | 		if tostring(key):match(cfg.pattern.style) then  | 
  ||
| − | 			if styleratio{mw.text.unstripNoWiki(style)} < 4.5 then  | 
  ||
				return true  | 
  				return true  | 
||
			end  | 
  			end  | 
||
		end  | 
  		end  | 
||
| + | 		return false  | 
||
	end  | 
  	end  | 
||
| + | |||
| − | 	return false  | 
  ||
| + | 	local function hasBorders()  | 
||
| − | end  | 
  ||
| + | 		for _, key in ipairs({cfg.arg.groupstyle, cfg.arg.basestyle,  | 
||
| − | |||
| + | 			cfg.arg.abovestyle, cfg.arg.belowstyle}) do  | 
||
| − | local function getTrackingCategories(border)  | 
  ||
| + | 			if tostring(args[key]):find('border', 1, true) then  | 
||
| − | 	local cats = {}  | 
  ||
| + | 				return true  | 
||
| − | 	if needsHorizontalLists(border) then table.insert(cats, cfg.category.horizontal_lists) end  | 
  ||
| + | 			end  | 
||
| − | 	if hasBackgroundColors() then table.insert(cats, cfg.category.background_colors) end  | 
  ||
| + | 		end  | 
||
| − | 	if isIllegible() then table.insert(cats, cfg.category.illegible) end  | 
  ||
| + | 		return false  | 
||
| − | 	if hasBorders() then table.insert(cats, cfg.category.borders) end  | 
  ||
| − | 	return cats  | 
  ||
| − | end  | 
  ||
| − | |||
| − | local function renderTrackingCategories(builder, border)  | 
  ||
| − | 	local title = mw.title.getCurrentTitle()  | 
  ||
| − | 	if title.namespace ~= 10 then return end -- not in template space  | 
  ||
| − | 	local subpage = title.subpageText  | 
  ||
| − | 	if subpage == cfg.keyword.subpage_doc or subpage == cfg.keyword.subpage_sandbox  | 
  ||
| − | 		or subpage == cfg.keyword.subpage_testcases then return end  | 
  ||
| − | |||
| − | 	for _, cat in ipairs(getTrackingCategories(border)) do  | 
  ||
| − | 		builder:wikitext('[[Category:' .. cat .. ']]')  | 
  ||
	end  | 
  	end  | 
||
| + | |||
| − | end  | 
  ||
| + | 	local function isIllegible()  | 
||
| − | |||
| + | 		local styleratio = require('Module:Color contrast')._styleratio  | 
||
| − | local function renderMainTable(border, listnums)  | 
  ||
| + | 		for key, style in pairs(args) do  | 
||
| − | 	local tbl = mw.html.create('table')  | 
  ||
| + | 			if tostring(key):match(cfg.pattern.style) then  | 
||
| − | 		:addClass(cfg.class.nowraplinks)  | 
  ||
| + | 				if styleratio{mw.text.unstripNoWiki(style)} < 4.5 then  | 
||
| − | 		:addClass(args[cfg.arg.bodyclass])  | 
  ||
| + | 					return true  | 
||
| − | |||
| + | 				end  | 
||
| − | 	local state = args[cfg.arg.state]  | 
  ||
| + | 			end  | 
||
| − | 	if args[cfg.arg.title] and state ~= cfg.keyword.state_plain and state ~= cfg.keyword.state_off then  | 
  ||
| − | 		if state == cfg.keyword.state_collapsed then  | 
  ||
| − | 			state = cfg.class.collapsed  | 
  ||
		end  | 
  		end  | 
||
| + | 		return false  | 
||
| − | 		tbl  | 
  ||
| − | 			:addClass(cfg.class.collapsible)  | 
  ||
| − | 			:addClass(state or cfg.class.autocollapse)  | 
  ||
	end  | 
  	end  | 
||
| + | |||
| − | |||
| + | 	local function getTrackingCategories(border)  | 
||
| − | 	tbl:css('border-spacing', 0)  | 
  ||
| + | 		local cats = {}  | 
||
| − | 	if border == cfg.keyword.border_subgroup or border == cfg.keyword.border_none then  | 
  ||
| + | 		if needsHorizontalLists(border) then table.insert(cats, cfg.category.horizontal_lists) end  | 
||
| − | 		tbl  | 
  ||
| + | 		if hasBackgroundColors() then table.insert(cats, cfg.category.background_colors) end  | 
||
| − | 			:addClass(cfg.class.navbox_subgroup)  | 
  ||
| + | 		if isIllegible() then table.insert(cats, cfg.category.illegible) end  | 
||
| − | 			:cssText(args[cfg.arg.bodystyle])  | 
  ||
| + | 		if hasBorders() then table.insert(cats, cfg.category.borders) end  | 
||
| − | 			:cssText(args[cfg.arg.style])  | 
  ||
| + | 		return cats  | 
||
| − | 	else  -- regular navbox - bodystyle and style will be applied to the wrapper table  | 
  ||
| − | 		tbl  | 
  ||
| − | 			:addClass(cfg.class.navbox_inner)  | 
  ||
| − | 			:css('background', 'transparent')  | 
  ||
| − | 			:css('color', 'inherit')  | 
  ||
	end  | 
  	end  | 
||
| + | |||
| − | 	tbl:cssText(args[cfg.arg.innerstyle])  | 
  ||
| + | 	local function renderTrackingCategories(builder, border)  | 
||
| − | |||
| + | 		local title = mw.title.getCurrentTitle()  | 
||
| − | 	renderTitleRow(tbl)  | 
  ||
| + | 		if title.namespace ~= 10 then return end -- not in template space  | 
||
| − | 	renderAboveRow(tbl)  | 
  ||
| − | + | 		local subpage = title.subpageText  | 
|
| + | 		if subpage == cfg.keyword.subpage_doc or subpage == cfg.keyword.subpage_sandbox  | 
||
| − | 	for i, listnum in ipairs(listnums) do  | 
  ||
| + | 			or subpage == cfg.keyword.subpage_testcases then return end  | 
||
| − | 		renderListRow(tbl, i, listnum, listnums_size)  | 
  ||
| + | |||
| + | 		for _, cat in ipairs(getTrackingCategories(border)) do  | 
||
| + | 			builder:wikitext('[[Category:' .. cat .. ']]')  | 
||
| + | 		end  | 
||
	end  | 
  	end  | 
||
| + | |||
| − | 	renderBelowRow(tbl)  | 
  ||
| + | 	local function renderMainTable(border, listnums)  | 
||
| − | |||
| + | 		local tbl = mw.html.create('table')  | 
||
| − | 	return tbl  | 
  ||
| + | 			:addClass(cfg.class.nowraplinks)  | 
||
| − | end  | 
  ||
| + | 			:addClass(args[cfg.arg.bodyclass])  | 
||
| − | |||
| + | |||
| − | local function add_navbox_styles(hiding_templatestyles)  | 
  ||
| − | + | 		local state = args[cfg.arg.state]  | 
|
| + | 		if args[cfg.arg.title] and state ~= cfg.keyword.state_plain and state ~= cfg.keyword.state_off then  | 
||
| − | 	-- This is a lambda so that it doesn't need the frame as a parameter  | 
  ||
| + | 			if state == cfg.keyword.state_collapsed then  | 
||
| − | 	local function add_user_styles(templatestyles)  | 
  ||
| + | 				state = cfg.class.collapsed  | 
||
| − | 		if templatestyles and templatestyles ~= '' then  | 
  ||
| + | 			end  | 
||
| − | 			return frame:extensionTag{  | 
  ||
| + | 			tbl  | 
||
| − | 				name = 'templatestyles', args = { src = templatestyles }  | 
  ||
| + | 				:addClass(cfg.class.collapsible)  | 
||
| − | 			}  | 
  ||
| + | 				:addClass(state or cfg.class.autocollapse)  | 
||
		end  | 
  		end  | 
||
| + | |||
| − | 		return ''  | 
  ||
| + | 		tbl:css('border-spacing', 0)  | 
||
| + | 		if border == cfg.keyword.border_subgroup or border == cfg.keyword.border_none then  | 
||
| + | 			tbl  | 
||
| + | 				:addClass(cfg.class.navbox_subgroup)  | 
||
| + | 				:cssText(args[cfg.arg.bodystyle])  | 
||
| + | 				:cssText(args[cfg.arg.style])  | 
||
| + | 		else  -- regular navbox - bodystyle and style will be applied to the wrapper table  | 
||
| + | 			tbl  | 
||
| + | 				:addClass(cfg.class.navbox_inner)  | 
||
| + | 				:css('background', 'transparent')  | 
||
| + | 				:css('color', 'inherit')  | 
||
| + | 		end  | 
||
| + | 		tbl:cssText(args[cfg.arg.innerstyle])  | 
||
| + | |||
| + | 		renderTitleRow(tbl)  | 
||
| + | 		renderAboveRow(tbl)  | 
||
| + | 		local listnums_size = #listnums  | 
||
| + | 		for i, listnum in ipairs(listnums) do  | 
||
| + | 			renderListRow(tbl, i, listnum, listnums_size)  | 
||
| + | 		end  | 
||
| + | 		renderBelowRow(tbl)  | 
||
| + | |||
| + | 		return tbl  | 
||
	end  | 
  	end  | 
||
| + | |||
| − | |||
| + | 	local function add_navbox_styles(hiding_templatestyles)  | 
||
| − | 	-- get templatestyles. load base from config so that Lua only needs to do  | 
  ||
| + | 		local frame = mw.getCurrentFrame()  | 
||
| − | 	-- the work once of parser tag expansion  | 
  ||
| + | 		-- This is a lambda so that it doesn't need the frame as a parameter  | 
||
| − | 	local base_templatestyles = cfg.templatestyles  | 
  ||
| − | + | 		local function add_user_styles(templatestyles)  | 
|
| + | 			if templatestyles and templatestyles ~= '' then  | 
||
| − | 	local child_templatestyles = add_user_styles(args[cfg.arg.child_templatestyles])  | 
  ||
| + | 				return frame:extensionTag{  | 
||
| − | |||
| + | 					name = 'templatestyles', args = { src = templatestyles }  | 
||
| − | 	-- The 'navbox-styles' div exists to wrap the styles to work around T200206  | 
  ||
| + | 				}  | 
||
| − | 	-- more elegantly. Instead of combinatorial rules, this ends up being linear  | 
  ||
| + | 			end  | 
||
| − | 	-- number of CSS rules.  | 
  ||
| − | + | 			return ''  | 
|
| − | 		:addClass(cfg.class.navbox_styles)  | 
  ||
| − | 		:wikitext(  | 
  ||
| − | 			add_list_styles() .. -- see [hlist_note] applied to 'before base_templatestyles'  | 
  ||
| − | 			base_templatestyles ..  | 
  ||
| − | 			templatestyles ..  | 
  ||
| − | 			child_templatestyles ..  | 
  ||
| − | 			table.concat(hiding_templatestyles)  | 
  ||
| − | 		)  | 
  ||
| − | 		:done()  | 
  ||
| − | end  | 
  ||
| − | |||
| − | -- work around [[phab:T303378]]  | 
  ||
| − | -- for each arg: find all the templatestyles strip markers, insert them into a  | 
  ||
| − | -- table. then remove all templatestyles markers from the arg  | 
  ||
| − | local function move_hiding_templatestyles(args)  | 
  ||
| − | 	local gfind = string.gfind  | 
  ||
| − | 	local gsub = string.gsub  | 
  ||
| − | 	local templatestyles_markers = {}  | 
  ||
| − | 	local strip_marker_pattern = '(\127[^\127]*UNIQ%-%-templatestyles%-%x+%-QINU[^\127]*\127)'  | 
  ||
| − | 	for k, arg in pairs(args) do  | 
  ||
| − | 		for marker in gfind(arg, strip_marker_pattern) do  | 
  ||
| − | 			table.insert(templatestyles_markers, marker)  | 
  ||
		end  | 
  		end  | 
||
| + | |||
| − | 		args[k] = gsub(arg, strip_marker_pattern, '')  | 
  ||
| + | 		-- get templatestyles. load base from config so that Lua only needs to do  | 
||
| + | 		-- the work once of parser tag expansion  | 
||
| + | 		local base_templatestyles = cfg.templatestyles  | 
||
| + | 		local templatestyles = add_user_styles(args[cfg.arg.templatestyles])  | 
||
| + | 		local child_templatestyles = add_user_styles(args[cfg.arg.child_templatestyles])  | 
||
| + | |||
| + | 		-- The 'navbox-styles' div exists to wrap the styles to work around T200206  | 
||
| + | 		-- more elegantly. Instead of combinatorial rules, this ends up being linear  | 
||
| + | 		-- number of CSS rules.  | 
||
| + | 		return mw.html.create('div')  | 
||
| + | 			:addClass(cfg.class.navbox_styles)  | 
||
| + | 			:wikitext(  | 
||
| + | 				add_list_styles() .. -- see [hlist_note] applied to 'before base_templatestyles'  | 
||
| + | 				base_templatestyles ..  | 
||
| + | 				templatestyles ..  | 
||
| + | 				child_templatestyles ..  | 
||
| + | 				table.concat(hiding_templatestyles)  | 
||
| + | 			)  | 
||
| + | 			:done()  | 
||
	end  | 
  	end  | 
||
| + | |||
| − | 	return templatestyles_markers  | 
  ||
| + | 	-- work around [[phab:T303378]]  | 
||
| − | end  | 
  ||
| + | 	-- for each arg: find all the templatestyles strip markers, insert them into a  | 
||
| − | |||
| + | 	-- table. then remove all templatestyles markers from the arg  | 
||
| − | function p._navbox(navboxArgs)  | 
  ||
| + | 	local function move_hiding_templatestyles(args)  | 
||
| − | 	args = navboxArgs  | 
  ||
| + | 		local gfind = string.gfind  | 
||
| + | 		local gsub = string.gsub  | 
||
| + | 		local templatestyles_markers = {}  | 
||
| + | 		local strip_marker_pattern = '(\127[^\127]*UNIQ%-%-templatestyles%-%x+%-QINU[^\127]*\127)'  | 
||
| + | 		for k, arg in pairs(args) do  | 
||
| + | 			for marker in gfind(arg, strip_marker_pattern) do  | 
||
| + | 				table.insert(templatestyles_markers, marker)  | 
||
| + | 			end  | 
||
| + | 			args[k] = gsub(arg, strip_marker_pattern, '')  | 
||
| + | 		end  | 
||
| + | 		return templatestyles_markers  | 
||
| + | 	end  | 
||
| + | |||
	local hiding_templatestyles = move_hiding_templatestyles(args)  | 
  	local hiding_templatestyles = move_hiding_templatestyles(args)  | 
||
	local listnums = {}  | 
  	local listnums = {}  | 
||
| + | |||
| − | |||
	for k, _ in pairs(args) do  | 
  	for k, _ in pairs(args) do  | 
||
		if type(k) == 'string' then  | 
  		if type(k) == 'string' then  | 
||
| Line 542: | Line 568: | ||
function p.navbox(frame)  | 
  function p.navbox(frame)  | 
||
| + | 	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[cfg.arg.title]  | 
||
| + | 		_ = args[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 .. format(cfg.arg.group_and_num, i)]  | 
||
| + | 			if inArray(cfg.keyword.subgroups, args[prefix .. format(cfg.arg.list_and_num, i)]) then  | 
||
| + | 				for _, v in ipairs(cfg.keyword.subgroups) do  | 
||
| + | 					readArgs(args, prefix .. v .. i .. "_")  | 
||
| + | 				end  | 
||
| + | 			end  | 
||
| + | 		end  | 
||
| + | 		_ = args[cfg.arg.below]  | 
||
| + | 	end  | 
||
| + | |||
	if not getArgs then  | 
  	if not getArgs then  | 
||
		getArgs = require('Module:Arguments').getArgs  | 
  		getArgs = require('Module:Arguments').getArgs  | 
||
	end  | 
  	end  | 
||
| − | 	args = getArgs(frame, {wrappers = {cfg.pattern.navbox}})  | 
  + | 	local args = getArgs(frame, {wrappers = {cfg.pattern.navbox}})  | 
| + | 	readArgs(args, "")  | 
||
| − | |||
| − | 	-- Read the arguments in the order they'll be output in, to make references  | 
  ||
| − | 	-- number in the right order.  | 
  ||
| − | 	local _  | 
  ||
| − | 	_ = args[cfg.arg.title]  | 
  ||
| − | 	_ = args[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[format(cfg.arg.group_and_num, i)]  | 
  ||
| − | 		_ = args[format(cfg.arg.list_and_num, i)]  | 
  ||
| − | 	end  | 
  ||
| − | 	_ = args[cfg.arg.below]  | 
  ||
| − | |||
	return p._navbox(args)  | 
  	return p._navbox(args)  | 
||
end  | 
  end  | 
||
Revision as of 15:10, 16 August 2024
Documentation for this module may be created at Module:Navbox/doc
require('strict')
local p = {}
local cfg = mw.loadData('Module:Navbox/configuration')
local inArray = require("Module:TableTools").inArray
local getArgs -- lazily initialized
local format = string.format
function p._navbox(args)
	local function striped(wikitext, border)
		-- 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 = cfg.category.orphan
		if border == cfg.keyword.border_subgroup and args[cfg.arg.orphan] ~= cfg.keyword.orphan_yes then
			-- No change; striping occurs in outermost navbox.
			return wikitext .. orphanCat
		end
		local first, second = cfg.class.navbox_odd_part, cfg.class.navbox_even_part
		if args[cfg.arg.evenodd] then
			if args[cfg.arg.evenodd] == cfg.keyword.evenodd_swap then
				first, second = second, first
			else
				first = args[cfg.arg.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(cfg.marker.regex, 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 == cfg.keyword.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(cfg.pattern.nowrap) then
					line = format(cfg.nowrap_item, prefix, content)
				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 has_navbar()
		return args[cfg.arg.navbar] ~= cfg.keyword.navbar_off
			and args[cfg.arg.navbar] ~= cfg.keyword.navbar_plain
			and (
				args[cfg.arg.name]
				or mw.getCurrentFrame():getParent():getTitle():gsub(cfg.pattern.sandbox, '')
					~= cfg.pattern.navbox
			)
	end
	
	-- extract text color from css, which is the only permitted inline CSS for the navbar
	local function extract_color(css_str)
		-- return nil because navbar takes its argument into mw.html which handles
		-- nil gracefully, removing the associated style attribute
		return mw.ustring.match(';' .. css_str .. ';', '.*;%s*([Cc][Oo][Ll][Oo][Rr]%s*:%s*.-)%s*;') or nil
	end
	
	local function renderNavBar(titleCell)
		if has_navbar() then
			local navbar = require('Module:Navbar')._navbar
			titleCell:wikitext(navbar{
				[cfg.navbar.name] = args[cfg.arg.name],
				[cfg.navbar.mini] = 1,
				[cfg.navbar.fontstyle] = extract_color(
					(args[cfg.arg.basestyle] or '') .. ';' .. (args[cfg.arg.titlestyle] or '')
				)
			})
		end
	
	end
	
	local function renderTitleRow(tbl)
		if not args[cfg.arg.title] then return end
	
		local titleRow = tbl:tag('tr')
	
		local titleCell = titleRow:tag('th'):attr('scope', 'col')
	
		local titleColspan = 2
		if args[cfg.arg.imageleft] then titleColspan = titleColspan + 1 end
		if args[cfg.arg.image] then titleColspan = titleColspan + 1 end
	
		titleCell
			:cssText(args[cfg.arg.basestyle])
			:cssText(args[cfg.arg.titlestyle])
			:addClass(cfg.class.navbox_title)
			:attr('colspan', titleColspan)
	
		renderNavBar(titleCell)
	
		titleCell
			:tag('div')
				-- id for aria-labelledby attribute
				:attr('id', mw.uri.anchorEncode(args[cfg.arg.title]))
				:addClass(args[cfg.arg.titleclass])
				:css('font-size', '114%')
				:css('margin', '0 4em')
				:wikitext(processItem(args[cfg.arg.title]))
	end
	
	local function getAboveBelowColspan()
		local ret = 2
		if args[cfg.arg.imageleft] then ret = ret + 1 end
		if args[cfg.arg.image] then ret = ret + 1 end
		return ret
	end
	
	local function renderAboveRow(tbl)
		if not args[cfg.arg.above] then return end
	
		tbl:tag('tr')
			:tag('td')
				:addClass(cfg.class.navbox_abovebelow)
				:addClass(args[cfg.arg.aboveclass])
				:cssText(args[cfg.arg.basestyle])
				:cssText(args[cfg.arg.abovestyle])
				:attr('colspan', getAboveBelowColspan())
				:tag('div')
					-- id for aria-labelledby attribute, if no title
					:attr('id', (not args[cfg.arg.title]) and mw.uri.anchorEncode(args[cfg.arg.above]) or nil)
					:wikitext(processItem(args[cfg.arg.above], args[cfg.arg.nowrapitems]))
	end
	
	local function renderBelowRow(tbl)
		if not args[cfg.arg.below] then return end
	
		tbl:tag('tr')
			:tag('td')
				:addClass(cfg.class.navbox_abovebelow)
				:addClass(args[cfg.arg.belowclass])
				:cssText(args[cfg.arg.basestyle])
				:cssText(args[cfg.arg.belowstyle])
				:attr('colspan', getAboveBelowColspan())
				:tag('div')
					:wikitext(processItem(args[cfg.arg.below], args[cfg.arg.nowrapitems]))
	end
	
	local function renderListRow(tbl, index, listnum, listnums_size)
		local row = tbl:tag('tr')
	
		if index == 1 and args[cfg.arg.imageleft] then
			row
				:tag('td')
					:addClass(cfg.class.noviewer)
					:addClass(cfg.class.navbox_image)
					:addClass(args[cfg.arg.imageclass])
					:css('width', '1px')               -- Minimize width
					:css('padding', '0 2px 0 0')
					:cssText(args[cfg.arg.imageleftstyle])
					:attr('rowspan', listnums_size)
					:tag('div')
						:wikitext(processItem(args[cfg.arg.imageleft]))
		end
	
		local group_and_num = format(cfg.arg.group_and_num, listnum)
		local groupstyle_and_num = format(cfg.arg.groupstyle_and_num, listnum)
		if args[group_and_num] 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[cfg.arg.title] or args[cfg.arg.above] or args[cfg.arg.group2]) then
				groupCell
					:attr('id', mw.uri.anchorEncode(args[cfg.arg.group1]))
			end
	
			groupCell
				:attr('scope', 'row')
				:addClass(cfg.class.navbox_group)
				:addClass(args[cfg.arg.groupclass])
				:cssText(args[cfg.arg.basestyle])
				-- If groupwidth not specified, minimize width
				:css('width', args[cfg.arg.groupwidth] or '1%')
	
			groupCell
				:cssText(args[cfg.arg.groupstyle])
				:cssText(args[groupstyle_and_num])
				:wikitext(args[group_and_num])
		end
	
		local listCell = row:tag('td')
	
		if args[group_and_num] then
			listCell
				:addClass(cfg.class.navbox_list_with_group)
		else
			listCell:attr('colspan', 2)
		end
	
		if not args[cfg.arg.groupwidth] then
			listCell:css('width', '100%')
		end
	
		local rowstyle  -- usually nil so cssText(rowstyle) usually adds nothing
		if index % 2 == 1 then
			rowstyle = args[cfg.arg.oddstyle]
		else
			rowstyle = args[cfg.arg.evenstyle]
		end
	
		local list_and_num = format(cfg.arg.list_and_num, listnum)
		local listText = args[list_and_num]
		
		if inArray(cfg.keyword.subgroups, listText) then
			local childArgs = {
				[cfg.arg.border] = cfg.keyword.border_subgroup,
				[cfg.arg.navbar] = cfg.keyword.navbar_plain
			}
			local hasChildArgs = false
			for k, v in pairs(args) do
				k = tostring(k)
				for _, w in ipairs(cfg.keyword.subgroups) do
					w = w .. listnum .. "_"
					if (#k > #w) and (k:sub(1, #w) == w) then
						childArgs[k:sub(#w + 1)] = v
						hasChildArgs = true
					end
				end
			end
			listText = hasChildArgs and p._navbox(childArgs) or listText
		end
		
		local oddEven = cfg.marker.oddeven
		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(cfg.pattern.navbox_title) and cfg.marker.restart or cfg.class.navbox_odd_part
		end
		
		local liststyle_and_num = format(cfg.arg.liststyle_and_num, listnum)
		local listclass_and_num = format(cfg.arg.listclass_and_num, listnum)
		listCell
			:css('padding', '0')
			:cssText(args[cfg.arg.liststyle])
			:cssText(rowstyle)
			:cssText(args[liststyle_and_num])
			:addClass(cfg.class.navbox_list)
			:addClass(cfg.class.navbox_part .. oddEven)
			:addClass(args[cfg.arg.listclass])
			:addClass(args[listclass_and_num])
			:tag('div')
				:css('padding',
					(index == 1 and args[cfg.arg.list1padding]) or args[cfg.arg.listpadding] or '0 0.25em'
				)
				:wikitext(processItem(listText, args[cfg.arg.nowrapitems]))
	
		if index == 1 and args[cfg.arg.image] then
			row
				:tag('td')
					:addClass(cfg.class.noviewer)
					:addClass(cfg.class.navbox_image)
					:addClass(args[cfg.arg.imageclass])
					:css('width', '1px')               -- Minimize width
					:css('padding', '0 0 0 2px')
					:cssText(args[cfg.arg.imagestyle])
					:attr('rowspan', listnums_size)
					:tag('div')
						:wikitext(processItem(args[cfg.arg.image]))
		end
	end
	
	local function has_list_class(htmlclass)
		local patterns = {
			'^' .. htmlclass .. '$',
			'%s' .. htmlclass .. '$',
			'^' .. htmlclass .. '%s',
			'%s' .. htmlclass .. '%s'
		}
		
		for arg, _ in pairs(args) do
			if type(arg) == 'string' and mw.ustring.find(arg, cfg.pattern.class) then
				for _, pattern in ipairs(patterns) do
					if mw.ustring.find(args[arg] or '', pattern) then
						return true
					end
				end
			end
		end
		return false
	end
	
	-- there are a lot of list classes in the wild, so we add their TemplateStyles
	local function add_list_styles()
		local frame = mw.getCurrentFrame()
		local function add_list_templatestyles(htmlclass, templatestyles)
			if has_list_class(htmlclass) then
				return frame:extensionTag{
					name = 'templatestyles', args = { src = templatestyles }
				}
			else
				return ''
			end
		end
		
		local hlist_styles = add_list_templatestyles('hlist', cfg.hlist_templatestyles)
		local plainlist_styles = add_list_templatestyles('plainlist', cfg.plainlist_templatestyles)
		
		-- a second workaround for [[phab:T303378]]
		-- when that issue is fixed, we can actually use has_navbar not to emit the
		-- tag here if we want
		if has_navbar() and hlist_styles == '' then
			hlist_styles = frame:extensionTag{
				name = 'templatestyles', args = { src = cfg.hlist_templatestyles }
			}
		end
		
		-- hlist -> plainlist is best-effort to preserve old Common.css ordering.
		-- this ordering is not a guarantee because most navboxes will emit only
		-- one of these classes [hlist_note]
		return hlist_styles .. plainlist_styles
	end
	
	local function needsHorizontalLists(border)
		if border == cfg.keyword.border_subgroup or args[cfg.arg.tracking] == cfg.keyword.tracking_no then
			return false
		end
		return not has_list_class(cfg.pattern.hlist) and not has_list_class(cfg.pattern.plainlist)
	end
	
	local function hasBackgroundColors()
		for _, key in ipairs({cfg.arg.titlestyle, cfg.arg.groupstyle,
			cfg.arg.basestyle, cfg.arg.abovestyle, cfg.arg.belowstyle}) do
			if tostring(args[key]):find('background', 1, true) then
				return true
			end
		end
		return false
	end
	
	local function hasBorders()
		for _, key in ipairs({cfg.arg.groupstyle, cfg.arg.basestyle,
			cfg.arg.abovestyle, cfg.arg.belowstyle}) do
			if tostring(args[key]):find('border', 1, true) then
				return true
			end
		end
		return false
	end
	
	local function isIllegible()
		local styleratio = require('Module:Color contrast')._styleratio
		for key, style in pairs(args) do
			if tostring(key):match(cfg.pattern.style) then
				if styleratio{mw.text.unstripNoWiki(style)} < 4.5 then
					return true
				end
			end
		end
		return false
	end
	
	local function getTrackingCategories(border)
		local cats = {}
		if needsHorizontalLists(border) then table.insert(cats, cfg.category.horizontal_lists) end
		if hasBackgroundColors() then table.insert(cats, cfg.category.background_colors) end
		if isIllegible() then table.insert(cats, cfg.category.illegible) end
		if hasBorders() then table.insert(cats, cfg.category.borders) end
		return cats
	end
	
	local function renderTrackingCategories(builder, border)
		local title = mw.title.getCurrentTitle()
		if title.namespace ~= 10 then return end -- not in template space
		local subpage = title.subpageText
		if subpage == cfg.keyword.subpage_doc or subpage == cfg.keyword.subpage_sandbox
			or subpage == cfg.keyword.subpage_testcases then return end
	
		for _, cat in ipairs(getTrackingCategories(border)) do
			builder:wikitext('[[Category:' .. cat .. ']]')
		end
	end
	
	local function renderMainTable(border, listnums)
		local tbl = mw.html.create('table')
			:addClass(cfg.class.nowraplinks)
			:addClass(args[cfg.arg.bodyclass])
	
		local state = args[cfg.arg.state]
		if args[cfg.arg.title] and state ~= cfg.keyword.state_plain and state ~= cfg.keyword.state_off then
			if state == cfg.keyword.state_collapsed then
				state = cfg.class.collapsed
			end
			tbl
				:addClass(cfg.class.collapsible)
				:addClass(state or cfg.class.autocollapse)
		end
	
		tbl:css('border-spacing', 0)
		if border == cfg.keyword.border_subgroup or border == cfg.keyword.border_none then
			tbl
				:addClass(cfg.class.navbox_subgroup)
				:cssText(args[cfg.arg.bodystyle])
				:cssText(args[cfg.arg.style])
		else  -- regular navbox - bodystyle and style will be applied to the wrapper table
			tbl
				:addClass(cfg.class.navbox_inner)
				:css('background', 'transparent')
				:css('color', 'inherit')
		end
		tbl:cssText(args[cfg.arg.innerstyle])
	
		renderTitleRow(tbl)
		renderAboveRow(tbl)
		local listnums_size = #listnums
		for i, listnum in ipairs(listnums) do
			renderListRow(tbl, i, listnum, listnums_size)
		end
		renderBelowRow(tbl)
	
		return tbl
	end
	
	local function add_navbox_styles(hiding_templatestyles)
		local frame = mw.getCurrentFrame()
		-- This is a lambda so that it doesn't need the frame as a parameter
		local function add_user_styles(templatestyles)
			if templatestyles and templatestyles ~= '' then
				return frame:extensionTag{
					name = 'templatestyles', args = { src = templatestyles }
				}
			end
			return ''
		end
	
		-- get templatestyles. load base from config so that Lua only needs to do
		-- the work once of parser tag expansion
		local base_templatestyles = cfg.templatestyles
		local templatestyles = add_user_styles(args[cfg.arg.templatestyles])
		local child_templatestyles = add_user_styles(args[cfg.arg.child_templatestyles])
	
		-- The 'navbox-styles' div exists to wrap the styles to work around T200206
		-- more elegantly. Instead of combinatorial rules, this ends up being linear
		-- number of CSS rules.
		return mw.html.create('div')
			:addClass(cfg.class.navbox_styles)
			:wikitext(
				add_list_styles() .. -- see [hlist_note] applied to 'before base_templatestyles'
				base_templatestyles ..
				templatestyles ..
				child_templatestyles ..
				table.concat(hiding_templatestyles)
			)
			:done()
	end
	
	-- work around [[phab:T303378]]
	-- for each arg: find all the templatestyles strip markers, insert them into a
	-- table. then remove all templatestyles markers from the arg
	local function move_hiding_templatestyles(args)
		local gfind = string.gfind
		local gsub = string.gsub
		local templatestyles_markers = {}
		local strip_marker_pattern = '(\127[^\127]*UNIQ%-%-templatestyles%-%x+%-QINU[^\127]*\127)'
		for k, arg in pairs(args) do
			for marker in gfind(arg, strip_marker_pattern) do
				table.insert(templatestyles_markers, marker)
			end
			args[k] = gsub(arg, strip_marker_pattern, '')
		end
		return templatestyles_markers
	end
	
	local hiding_templatestyles = move_hiding_templatestyles(args)
	local listnums = {}
	
	for k, _ in pairs(args) do
		if type(k) == 'string' then
			local listnum = k:match(cfg.pattern.listnum)
			if listnum then table.insert(listnums, tonumber(listnum)) end
		end
	end
	table.sort(listnums)
	local border = mw.text.trim(args[cfg.arg.border] or args[1] or '')
	if border == cfg.keyword.border_child then
		border = cfg.keyword.border_subgroup
	end
	-- render the main body of the navbox
	local tbl = renderMainTable(border, listnums)
	local res = mw.html.create()
	-- render the appropriate wrapper for the navbox, based on the border param
	if border == cfg.keyword.border_none then
		res:node(add_navbox_styles(hiding_templatestyles))
		local nav = res:tag('div')
			:attr('role', 'navigation')
			:node(tbl)
		-- aria-labelledby title, otherwise above, otherwise lone group
		if args[cfg.arg.title] or args[cfg.arg.above] or (args[cfg.arg.group1]
			and not args[cfg.arg.group2]) then
			nav:attr(
				'aria-labelledby',
				mw.uri.anchorEncode(
					args[cfg.arg.title] or args[cfg.arg.above] or args[cfg.arg.group1]
				)
			)
		else
			nav:attr('aria-label', cfg.aria_label)
		end
	elseif border == cfg.keyword.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
		res:node(add_navbox_styles(hiding_templatestyles))
		local nav = res:tag('div')
			:attr('role', 'navigation')
			:addClass(cfg.class.navbox)
			:addClass(args[cfg.arg.navboxclass])
			:cssText(args[cfg.arg.bodystyle])
			:cssText(args[cfg.arg.style])
			:css('padding', '3px')
			:node(tbl)
		-- aria-labelledby title, otherwise above, otherwise lone group
		if args[cfg.arg.title] or args[cfg.arg.above]
			or (args[cfg.arg.group1] and not args[cfg.arg.group2]) then
			nav:attr(
				'aria-labelledby',
				mw.uri.anchorEncode(args[cfg.arg.title] or args[cfg.arg.above] or args[cfg.arg.group1])
			)
		else
			nav:attr('aria-label', cfg.aria_label)
		end
	end
	if (args[cfg.arg.nocat] or cfg.keyword.nocat_false):lower() == cfg.keyword.nocat_false then
		renderTrackingCategories(res, border)
	end
	return striped(tostring(res), border)
end
function p.navbox(frame)
	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[cfg.arg.title]
		_ = args[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 .. format(cfg.arg.group_and_num, i)]
			if inArray(cfg.keyword.subgroups, args[prefix .. format(cfg.arg.list_and_num, i)]) then
				for _, v in ipairs(cfg.keyword.subgroups) do
					readArgs(args, prefix .. v .. i .. "_")
				end
			end
		end
		_ = args[cfg.arg.below]
	end
	if not getArgs then
		getArgs = require('Module:Arguments').getArgs
	end
	local args = getArgs(frame, {wrappers = {cfg.pattern.navbox}})
	readArgs(args, "")
	return p._navbox(args)
end
return p