コンテンツにスキップ

モジュール:Navbox

提供:カノウィキ

このモジュールについての説明文ページを モジュール:Navbox/doc に作成できます

  1 require('strict')
  2 local p = {}
  3 local cfg = mw.loadData('Module:Navbox/configuration')
  4 local inArray = require("Module:TableTools").inArray
  5 local getArgs -- lazily initialized
  6 local hiding_templatestyles = {} 
  7 
  8 -- global passthrough variables
  9 local passthrough = {
 10 	[cfg.arg.above]=true,[cfg.arg.aboveclass]=true,[cfg.arg.abovestyle]=true,
 11 	[cfg.arg.basestyle]=true,
 12 	[cfg.arg.below]=true,[cfg.arg.belowclass]=true,[cfg.arg.belowstyle]=true,
 13 	[cfg.arg.bodyclass]=true,
 14 	[cfg.arg.groupclass]=true,
 15 	[cfg.arg.image]=true,[cfg.arg.imageclass]=true,[cfg.arg.imagestyle]=true,
 16 	[cfg.arg.imageleft]=true,[cfg.arg.imageleftstyle]=true,
 17 	[cfg.arg.listclass]=true,
 18 	[cfg.arg.name]=true,
 19 	[cfg.arg.navbar]=true,
 20 	[cfg.arg.state]=true,
 21 	[cfg.arg.title]=true,[cfg.arg.titleclass]=true,[cfg.arg.titlestyle]=true,
 22 	argHash=true
 23 }
 24 
 25 -- helper functions
 26 local andnum = function(s, n) return string.format(cfg.arg[s .. '_and_num'], n) end
 27 local isblank = function(v) return (v or '') == '' end
 28 
 29 local function concatstrings(s)
 30 	local r = table.concat(s, '')
 31 	if r:match('^%s*$') then return nil end
 32 	return r
 33 end
 34 
 35 local function concatstyles(s)
 36 	local r = ''
 37 	for _, v in ipairs(s) do
 38 		v = mw.text.trim(v, "%s;")
 39 		if not isblank(v) then r = r .. v .. ';' end
 40 	end
 41 	if isblank(r) then return nil end
 42 	return r
 43 end
 44 
 45 local function getSubgroup(args, listnum, listText, prefix)
 46 	local subArgs = {
 47 		[cfg.arg.border] = cfg.keyword.border_subgroup,
 48 		[cfg.arg.navbar] = cfg.keyword.navbar_plain,
 49 		argHash = 0
 50 	}
 51 	local hasSubArgs = false
 52 	local subgroups_and_num = prefix and {prefix} or cfg.arg.subgroups_and_num
 53 	for k, v in pairs(args) do
 54 		k = tostring(k)
 55 		for _, w in ipairs(subgroups_and_num) do
 56 			w = string.format(w, listnum) .. "_"
 57 			if (#k > #w) and (k:sub(1, #w) == w) then
 58 				subArgs[k:sub(#w + 1)] = v
 59 				hasSubArgs = true
 60 				subArgs.argHash = subArgs.argHash + (v and #v or 0)
 61 			end
 62 		end
 63 	end
 64 	return hasSubArgs and p._navbox(subArgs) or listText
 65 end
 66 
 67 -- Main functions
 68 function p._navbox(args)
 69 	if args.type == cfg.keyword.with_collapsible_groups then
 70 		return p._withCollapsibleGroups(args)
 71 	elseif args.type == cfg.keyword.with_columns then
 72 		return p._withColumns(args)
 73 	end
 74 
 75 	local function striped(wikitext, border)
 76 		-- Return wikitext with markers replaced for odd/even striping.
 77 		-- Child (subgroup) navboxes are flagged with a category that is removed
 78 		-- by parent navboxes. The result is that the category shows all pages
 79 		-- where a child navbox is not contained in a parent navbox.
 80 		local orphanCat = cfg.category.orphan
 81 		if border == cfg.keyword.border_subgroup and args[cfg.arg.orphan] ~= cfg.keyword.orphan_yes then
 82 			-- No change; striping occurs in outermost navbox.
 83 			return wikitext .. orphanCat
 84 		end
 85 		local first, second = cfg.class.navbox_odd_part, cfg.class.navbox_even_part
 86 		if args[cfg.arg.evenodd] then
 87 			if args[cfg.arg.evenodd] == cfg.keyword.evenodd_swap then
 88 				first, second = second, first
 89 			else
 90 				first = args[cfg.arg.evenodd]
 91 				second = first
 92 			end
 93 		end
 94 		local changer
 95 		if first == second then
 96 			changer = first
 97 		else
 98 			local index = 0
 99 			changer = function (code)
100 				if code == '0' then
101 					-- Current occurrence is for a group before a nested table.
102 					-- Set it to first as a valid although pointless class.
103 					-- The next occurrence will be the first row after a title
104 					-- in a subgroup and will also be first.
105 					index = 0
106 					return first
107 				end
108 				index = index + 1
109 				return index % 2 == 1 and first or second
110 			end
111 		end
112 		local regex = orphanCat:gsub('([%[%]])', '%%%1')
113 		return (wikitext:gsub(regex, ''):gsub(cfg.marker.regex, changer)) -- () omits gsub count
114 	end
115 
116 	local function processItem(item, nowrapitems)
117 		if item:sub(1, 2) == '{|' then
118 			-- Applying nowrap to lines in a table does not make sense.
119 			-- Add newlines to compensate for trim of x in |parm=x in a template.
120 			return '\n' .. item .. '\n'
121 		end
122 		if nowrapitems == cfg.keyword.nowrapitems_yes then
123 			local lines = {}
124 			for line in (item .. '\n'):gmatch('([^\n]*)\n') do
125 				local prefix, content = line:match('^([*:;#]+)%s*(.*)')
126 				if prefix and not content:match(cfg.pattern.nowrap) then
127 					line = string.format(cfg.nowrap_item, prefix, content)
128 				end
129 				table.insert(lines, line)
130 			end
131 			item = table.concat(lines, '\n')
132 		end
133 		if item:match('^[*:;#]') then
134 			return '\n' .. item .. '\n'
135 		end
136 		return item
137 	end
138 
139 	local function has_navbar()
140 		return args[cfg.arg.navbar] ~= cfg.keyword.navbar_off
141 			and args[cfg.arg.navbar] ~= cfg.keyword.navbar_plain
142 			and (
143 				args[cfg.arg.name]
144 				or mw.getCurrentFrame():getParent():getTitle():gsub(cfg.pattern.sandbox, '')
145 					~= cfg.pattern.navbox
146 			)
147 	end
148 
149 	-- extract text color from css, which is the only permitted inline CSS for the navbar
150 	local function extract_color(css_str)
151 		-- return nil because navbar takes its argument into mw.html which handles
152 		-- nil gracefully, removing the associated style attribute
153 		return mw.ustring.match(';' .. css_str .. ';', '.*;%s*([Cc][Oo][Ll][Oo][Rr]%s*:%s*.-)%s*;') or nil
154 	end
155 
156 	local function renderNavBar(titleCell)
157 		if has_navbar() then
158 			local navbar = require('Module:Navbar')._navbar
159 			titleCell:wikitext(navbar{
160 				[cfg.navbar.name] = args[cfg.arg.name],
161 				[cfg.navbar.mini] = 1,
162 				[cfg.navbar.fontstyle] = extract_color(
163 					(args[cfg.arg.basestyle] or '') .. ';' .. (args[cfg.arg.titlestyle] or '')
164 				)
165 			})
166 		end
167 
168 	end
169 
170 	local function renderTitleRow(tbl)
171 		if not args[cfg.arg.title] then return end
172 
173 		local titleRow = tbl:tag('tr')
174 
175 		local titleCell = titleRow:tag('th'):attr('scope', 'col')
176 
177 		local titleColspan = 2
178 		if args[cfg.arg.imageleft] then titleColspan = titleColspan + 1 end
179 		if args[cfg.arg.image] then titleColspan = titleColspan + 1 end
180 
181 		titleCell
182 			:cssText(args[cfg.arg.basestyle])
183 			:cssText(args[cfg.arg.titlestyle])
184 			:addClass(cfg.class.navbox_title)
185 			:attr('colspan', titleColspan)
186 
187 		renderNavBar(titleCell)
188 
189 		titleCell
190 			:tag('div')
191 				-- id for aria-labelledby attribute
192 				:attr('id', mw.uri.anchorEncode(args[cfg.arg.title]) .. args.argHash)
193 				:addClass(args[cfg.arg.titleclass])
194 				:css('font-size', '114%')
195 				:css('margin', '0 4em')
196 				:wikitext(processItem(args[cfg.arg.title]))
197 	end
198 
199 	local function getAboveBelowColspan()
200 		local ret = 2
201 		if args[cfg.arg.imageleft] then ret = ret + 1 end
202 		if args[cfg.arg.image] then ret = ret + 1 end
203 		return ret
204 	end
205 
206 	local function renderAboveRow(tbl)
207 		if not args[cfg.arg.above] then return end
208 
209 		tbl:tag('tr')
210 			:tag('td')
211 				:addClass(cfg.class.navbox_abovebelow)
212 				:addClass(args[cfg.arg.aboveclass])
213 				:cssText(args[cfg.arg.basestyle])
214 				:cssText(args[cfg.arg.abovestyle])
215 				:attr('colspan', getAboveBelowColspan())
216 				:tag('div')
217 					-- id for aria-labelledby attribute, if no title
218 					:attr('id', (not args[cfg.arg.title]) and 
219 						(mw.uri.anchorEncode(args[cfg.arg.above]) .. args.argHash)
220 						or nil)
221 					:wikitext(processItem(args[cfg.arg.above], args[cfg.arg.nowrapitems]))
222 	end
223 
224 	local function renderBelowRow(tbl)
225 		if not args[cfg.arg.below] then return end
226 
227 		tbl:tag('tr')
228 			:tag('td')
229 				:addClass(cfg.class.navbox_abovebelow)
230 				:addClass(args[cfg.arg.belowclass])
231 				:cssText(args[cfg.arg.basestyle])
232 				:cssText(args[cfg.arg.belowstyle])
233 				:attr('colspan', getAboveBelowColspan())
234 				:tag('div')
235 					:wikitext(processItem(args[cfg.arg.below], args[cfg.arg.nowrapitems]))
236 	end
237 
238 	local function renderListRow(tbl, index, listnum, listnums_size)
239 		local row = tbl:tag('tr')
240 
241 		if index == 1 and args[cfg.arg.imageleft] then
242 			row
243 				:tag('td')
244 					:addClass(cfg.class.noviewer)
245 					:addClass(cfg.class.navbox_image)
246 					:addClass(args[cfg.arg.imageclass])
247 					:css('width', '1px')               -- Minimize width
248 					:css('padding', '0 2px 0 0')
249 					:cssText(args[cfg.arg.imageleftstyle])
250 					:attr('rowspan', listnums_size)
251 					:tag('div')
252 						:wikitext(processItem(args[cfg.arg.imageleft]))
253 		end
254 
255 		local group_and_num = andnum('group', listnum)
256 		local groupstyle_and_num = andnum('groupstyle', listnum)
257 		if args[group_and_num] then
258 			local groupCell = row:tag('th')
259 
260 			-- id for aria-labelledby attribute, if lone group with no title or above
261 			if listnum == 1 and not (args[cfg.arg.title] or args[cfg.arg.above] or args[cfg.arg.group2]) then
262 				groupCell
263 					:attr('id', mw.uri.anchorEncode(args[cfg.arg.group1]) .. args.argHash)
264 			end
265 
266 			groupCell
267 				:attr('scope', 'row')
268 				:addClass(cfg.class.navbox_group)
269 				:addClass(args[cfg.arg.groupclass])
270 				:cssText(args[cfg.arg.basestyle])
271 				-- If groupwidth not specified, minimize width
272 				:css('width', args[cfg.arg.groupwidth] or '1%')
273 
274 			groupCell
275 				:cssText(args[cfg.arg.groupstyle])
276 				:cssText(args[groupstyle_and_num])
277 				:wikitext(args[group_and_num])
278 		end
279 
280 		local listCell = row:tag('td')
281 
282 		if args[group_and_num] then
283 			listCell
284 				:addClass(cfg.class.navbox_list_with_group)
285 		else
286 			listCell:attr('colspan', 2)
287 		end
288 
289 		if not args[cfg.arg.groupwidth] then
290 			listCell:css('width', '100%')
291 		end
292 
293 		local rowstyle  -- usually nil so cssText(rowstyle) usually adds nothing
294 		if index % 2 == 1 then
295 			rowstyle = args[cfg.arg.oddstyle]
296 		else
297 			rowstyle = args[cfg.arg.evenstyle]
298 		end
299 
300 		local list_and_num = andnum('list', listnum)
301 		local listText = inArray(cfg.keyword.subgroups, args[list_and_num])
302 			and getSubgroup(args, listnum, args[list_and_num]) or args[list_and_num]
303 
304 		local oddEven = cfg.marker.oddeven
305 		if listText:sub(1, 12) == '</div><table' then
306 			-- Assume list text is for a subgroup navbox so no automatic striping for this row.
307 			oddEven = listText:find(cfg.pattern.navbox_title) and cfg.marker.restart or cfg.class.navbox_odd_part
308 		end
309 
310 		local liststyle_and_num = andnum('liststyle', listnum)
311 		local listclass_and_num = andnum('listclass', listnum)
312 		listCell
313 			:css('padding', '0')
314 			:cssText(args[cfg.arg.liststyle])
315 			:cssText(rowstyle)
316 			:cssText(args[liststyle_and_num])
317 			:addClass(cfg.class.navbox_list)
318 			:addClass(cfg.class.navbox_part .. oddEven)
319 			:addClass(args[cfg.arg.listclass])
320 			:addClass(args[listclass_and_num])
321 			:tag('div')
322 				:css('padding',
323 					(index == 1 and args[cfg.arg.list1padding]) or args[cfg.arg.listpadding] or '0 0.25em'
324 				)
325 				:wikitext(processItem(listText, args[cfg.arg.nowrapitems]))
326 
327 		if index == 1 and args[cfg.arg.image] then
328 			row
329 				:tag('td')
330 					:addClass(cfg.class.noviewer)
331 					:addClass(cfg.class.navbox_image)
332 					:addClass(args[cfg.arg.imageclass])
333 					:css('width', '1px')               -- Minimize width
334 					:css('padding', '0 0 0 2px')
335 					:cssText(args[cfg.arg.imagestyle])
336 					:attr('rowspan', listnums_size)
337 					:tag('div')
338 						:wikitext(processItem(args[cfg.arg.image]))
339 		end
340 	end
341 
342 	local function has_list_class(htmlclass)
343 		local patterns = {
344 			'^' .. htmlclass .. '$',
345 			'%s' .. htmlclass .. '$',
346 			'^' .. htmlclass .. '%s',
347 			'%s' .. htmlclass .. '%s'
348 		}
349 
350 		for arg, _ in pairs(args) do
351 			if type(arg) == 'string' and mw.ustring.find(arg, cfg.pattern.class) then
352 				for _, pattern in ipairs(patterns) do
353 					if mw.ustring.find(args[arg] or '', pattern) then
354 						return true
355 					end
356 				end
357 			end
358 		end
359 		return false
360 	end
361 
362 	-- there are a lot of list classes in the wild, so we add their TemplateStyles
363 	local function add_list_styles()
364 		local frame = mw.getCurrentFrame()
365 		local function add_list_templatestyles(htmlclass, templatestyles)
366 			if has_list_class(htmlclass) then
367 				return frame:extensionTag{
368 					name = 'templatestyles', args = { src = templatestyles }
369 				}
370 			else
371 				return ''
372 			end
373 		end
374 
375 		local hlist_styles = add_list_templatestyles('hlist', cfg.hlist_templatestyles)
376 		local plainlist_styles = add_list_templatestyles('plainlist', cfg.plainlist_templatestyles)
377 
378 		-- a second workaround for [[phab:T303378]]
379 		-- when that issue is fixed, we can actually use has_navbar not to emit the
380 		-- tag here if we want
381 		if has_navbar() and hlist_styles == '' then
382 			hlist_styles = frame:extensionTag{
383 				name = 'templatestyles', args = { src = cfg.hlist_templatestyles }
384 			}
385 		end
386 
387 		-- hlist -> plainlist is best-effort to preserve old Common.css ordering.
388 		-- this ordering is not a guarantee because most navboxes will emit only
389 		-- one of these classes [hlist_note]
390 		return hlist_styles .. plainlist_styles
391 	end
392 
393 	local function needsHorizontalLists(border)
394 		if border == cfg.keyword.border_subgroup or args[cfg.arg.tracking] == cfg.keyword.tracking_no then
395 			return false
396 		end
397 		return not has_list_class(cfg.pattern.hlist) and not has_list_class(cfg.pattern.plainlist)
398 	end
399 
400 	local function hasBackgroundColors()
401 		for _, key in ipairs({cfg.arg.titlestyle, cfg.arg.groupstyle,
402 			cfg.arg.basestyle, cfg.arg.abovestyle, cfg.arg.belowstyle}) do
403 			if tostring(args[key]):find('background', 1, true) then
404 				return true
405 			end
406 		end
407 		return false
408 	end
409 
410 	local function hasBorders()
411 		for _, key in ipairs({cfg.arg.groupstyle, cfg.arg.basestyle,
412 			cfg.arg.abovestyle, cfg.arg.belowstyle}) do
413 			if tostring(args[key]):find('border', 1, true) then
414 				return true
415 			end
416 		end
417 		return false
418 	end
419 
420 	local function isIllegible()
421 		local styleratio = require('Module:Color contrast')._styleratio
422 		for key, style in pairs(args) do
423 			if tostring(key):match(cfg.pattern.style) then
424 				if styleratio{mw.text.unstripNoWiki(style)} < 4.5 then
425 					return true
426 				end
427 			end
428 		end
429 		return false
430 	end
431 
432 	local function getTrackingCategories(border)
433 		local cats = {}
434 		if needsHorizontalLists(border) then table.insert(cats, cfg.category.horizontal_lists) end
435 		if hasBackgroundColors() then table.insert(cats, cfg.category.background_colors) end
436 		if isIllegible() then table.insert(cats, cfg.category.illegible) end
437 		if hasBorders() then table.insert(cats, cfg.category.borders) end
438 		return cats
439 	end
440 
441 	local function renderTrackingCategories(builder, border)
442 		local title = mw.title.getCurrentTitle()
443 		if title.namespace ~= 10 then return end -- not in template space
444 		local subpage = title.subpageText
445 		if subpage == cfg.keyword.subpage_doc or subpage == cfg.keyword.subpage_sandbox
446 			or subpage == cfg.keyword.subpage_testcases then return end
447 
448 		for _, cat in ipairs(getTrackingCategories(border)) do
449 			builder:wikitext('[[Category:' .. cat .. ']]')
450 		end
451 	end
452 
453 	local function renderMainTable(border, listnums)
454 		local tbl = mw.html.create('table')
455 			:addClass(cfg.class.nowraplinks)
456 			:addClass(args[cfg.arg.bodyclass])
457 
458 		local state = args[cfg.arg.state]
459 		if args[cfg.arg.title] and state ~= cfg.keyword.state_plain and state ~= cfg.keyword.state_off then
460 			if state == cfg.keyword.state_collapsed then
461 				state = cfg.class.collapsed
462 			end
463 			tbl
464 				:addClass(cfg.class.collapsible)
465 				:addClass(state or cfg.class.autocollapse)
466 		end
467 
468 		tbl:css('border-spacing', 0)
469 		if border == cfg.keyword.border_subgroup or border == cfg.keyword.border_none then
470 			tbl
471 				:addClass(cfg.class.navbox_subgroup)
472 				:cssText(args[cfg.arg.bodystyle])
473 				:cssText(args[cfg.arg.style])
474 		else  -- regular navbox - bodystyle and style will be applied to the wrapper table
475 			tbl
476 				:addClass(cfg.class.navbox_inner)
477 				:css('background', 'transparent')
478 				:css('color', 'inherit')
479 		end
480 		tbl:cssText(args[cfg.arg.innerstyle])
481 
482 		renderTitleRow(tbl)
483 		renderAboveRow(tbl)
484 		local listnums_size = #listnums
485 		for i, listnum in ipairs(listnums) do
486 			renderListRow(tbl, i, listnum, listnums_size)
487 		end
488 		renderBelowRow(tbl)
489 
490 		return tbl
491 	end
492 
493 	local function add_navbox_styles(hiding_templatestyles)
494 		local frame = mw.getCurrentFrame()
495 		-- This is a lambda so that it doesn't need the frame as a parameter
496 		local function add_user_styles(templatestyles)
497 			if not isblank(templatestyles) then
498 				return frame:extensionTag{
499 					name = 'templatestyles', args = { src = templatestyles }
500 				}
501 			end
502 			return ''
503 		end
504 
505 		-- get templatestyles. load base from config so that Lua only needs to do
506 		-- the work once of parser tag expansion
507 		local base_templatestyles = cfg.templatestyles
508 		local templatestyles = add_user_styles(args[cfg.arg.templatestyles])
509 		local child_templatestyles = add_user_styles(args[cfg.arg.child_templatestyles])
510 
511 		-- The 'navbox-styles' div exists to wrap the styles to work around T200206
512 		-- more elegantly. Instead of combinatorial rules, this ends up being linear
513 		-- number of CSS rules.
514 		return mw.html.create('div')
515 			:addClass(cfg.class.navbox_styles)
516 			:wikitext(
517 				add_list_styles() .. -- see [hlist_note] applied to 'before base_templatestyles'
518 				base_templatestyles ..
519 				templatestyles ..
520 				child_templatestyles ..
521 				table.concat(hiding_templatestyles)
522 			)
523 			:done()
524 	end
525 
526 	-- work around [[phab:T303378]]
527 	-- for each arg: find all the templatestyles strip markers, insert them into a
528 	-- table. then remove all templatestyles markers from the arg
529 	local strip_marker_pattern = '(\127[^\127]*UNIQ%-%-templatestyles%-%x+%-QINU[^\127]*\127)'
530 	local argHash = 0
531 	for k, arg in pairs(args) do
532 		if type(arg) == 'string' then
533 			for marker in string.gfind(arg, strip_marker_pattern) do
534 				table.insert(hiding_templatestyles, marker)
535 			end
536 			argHash = argHash + #arg
537 			args[k] = string.gsub(arg, strip_marker_pattern, '')
538 		end
539 	end
540 	
541 	if not args.argHash then args.argHash = argHash end
542 
543 	local listnums = {}
544 
545 	for k, _ in pairs(args) do
546 		if type(k) == 'string' then
547 			local listnum = k:match(cfg.pattern.listnum)
548 			if listnum and args[andnum('list', tonumber(listnum))] then
549 				table.insert(listnums, tonumber(listnum))
550 			end
551 		end
552 	end
553 	table.sort(listnums)
554 
555 	local border = mw.text.trim(args[cfg.arg.border] or args[1] or '')
556 	if border == cfg.keyword.border_child then
557 		border = cfg.keyword.border_subgroup
558 	end
559 
560 	-- render the main body of the navbox
561 	local tbl = renderMainTable(border, listnums)
562 
563 	local res = mw.html.create()
564 	-- render the appropriate wrapper for the navbox, based on the border param
565 
566 	if border == cfg.keyword.border_none then
567 		res:node(add_navbox_styles(hiding_templatestyles))
568 		local nav = res:tag('div')
569 			:attr('role', 'navigation')
570 			:node(tbl)
571 		-- aria-labelledby title, otherwise above, otherwise lone group
572 		if args[cfg.arg.title] or args[cfg.arg.above] or (args[cfg.arg.group1]
573 			and not args[cfg.arg.group2]) then
574 			nav:attr(
575 				'aria-labelledby',
576 				mw.uri.anchorEncode(
577 					args[cfg.arg.title] or args[cfg.arg.above] or args[cfg.arg.group1]
578 				) .. args.argHash
579 			)
580 		else
581 			nav:attr('aria-label', cfg.aria_label)
582 		end
583 	elseif border == cfg.keyword.border_subgroup then
584 		-- We assume that this navbox is being rendered in a list cell of a
585 		-- parent navbox, and is therefore inside a div with padding:0em 0.25em.
586 		-- We start with a </div> to avoid the padding being applied, and at the
587 		-- end add a <div> to balance out the parent's </div>
588 		res
589 			:wikitext('</div>')
590 			:node(tbl)
591 			:wikitext('<div>')
592 	else
593 		res:node(add_navbox_styles(hiding_templatestyles))
594 		local nav = res:tag('div')
595 			:attr('role', 'navigation')
596 			:addClass(cfg.class.navbox)
597 			:addClass(args[cfg.arg.navboxclass])
598 			:cssText(args[cfg.arg.bodystyle])
599 			:cssText(args[cfg.arg.style])
600 			:css('padding', '3px')
601 			:node(tbl)
602 		-- aria-labelledby title, otherwise above, otherwise lone group
603 		if args[cfg.arg.title] or args[cfg.arg.above]
604 			or (args[cfg.arg.group1] and not args[cfg.arg.group2]) then
605 			nav:attr(
606 				'aria-labelledby',
607 				mw.uri.anchorEncode(
608 					args[cfg.arg.title] or args[cfg.arg.above] or args[cfg.arg.group1]
609 				) .. args.argHash
610 			)
611 		else
612 			nav:attr('aria-label', cfg.aria_label .. args.argHash)
613 		end
614 	end
615 
616 	if (args[cfg.arg.nocat] or cfg.keyword.nocat_false):lower() == cfg.keyword.nocat_false then
617 		renderTrackingCategories(res, border)
618 	end
619 	return striped(tostring(res), border)
620 end --p._navbox
621 
622 function p._withCollapsibleGroups(pargs)
623 	-- table for args passed to navbox
624 	local targs = {}
625 
626 	-- process args
627 	local passthroughLocal = {
628 		[cfg.arg.bodystyle] = true,
629 		[cfg.arg.border] = true,
630 		[cfg.arg.style] = true,
631 	}
632 	for k,v in pairs(pargs) do
633 		if k and type(k) == 'string' then
634 			if passthrough[k] or passthroughLocal[k] then
635 				targs[k] = v
636 			elseif (k:match(cfg.pattern.num)) then
637 				local n = k:match(cfg.pattern.num)
638 				local list_and_num = andnum('list', n)
639 				if ((k:match(cfg.pattern.listnum) or k:match(cfg.pattern.contentnum))
640 						and targs[list_and_num] == nil
641 						and pargs[andnum('group', n)] == nil
642 						and pargs[andnum('sect', n)] == nil
643 						and pargs[andnum('section', n)] == nil) then
644 					targs[list_and_num] = concatstrings({
645 						pargs[list_and_num] or '',
646 						pargs[andnum('content', n)] or ''
647 					})
648 					if (targs[list_and_num] and inArray(cfg.keyword.subgroups, targs[list_and_num])) then
649 						targs[list_and_num] = getSubgroup(pargs, n, targs[list_and_num])
650 					end
651 				elseif ((k:match(cfg.pattern.groupnum) or k:match(cfg.pattern.sectnum) or k:match(cfg.pattern.sectionnum))
652 						and targs[list_and_num] == nil) then
653 					local titlestyle = concatstyles({
654 						pargs[cfg.arg.groupstyle] or '',
655 						pargs[cfg.arg.secttitlestyle] or '', 
656 						pargs[andnum('groupstyle', n)] or '', 
657 						pargs[andnum('sectiontitlestyle', n)] or ''
658 					})
659 					local liststyle = concatstyles({
660 						pargs[cfg.arg.liststyle] or '',
661 						pargs[cfg.arg.contentstyle] or '', 
662 						pargs[andnum('liststyle', n)] or '', 
663 						pargs[andnum('contentstyle', n)] or ''
664 					})
665 					local title = concatstrings({
666 						pargs[andnum('group', n)] or '',
667 						pargs[andnum('sect', n)] or '',
668 						pargs[andnum('section', n)] or ''
669 					})
670 					local list = concatstrings({
671 						pargs[list_and_num] or '', 
672 						pargs[andnum('content', n)] or ''
673 					})
674 					if list and inArray(cfg.keyword.subgroups, list) then
675 						list = getSubgroup(pargs, n, list)
676 					end
677 					local abbr_and_num = andnum('abbr', n)
678 					local state = (pargs[abbr_and_num] and pargs[abbr_and_num] == pargs[cfg.arg.selected]) 
679 						and cfg.keyword.state_uncollapsed
680 						or (pargs[andnum('state', n)] or cfg.keyword.state_collapsed)
681 					
682 					targs[list_and_num] =p._navbox({
683 						cfg.keyword.border_child,
684 						[cfg.arg.navbar] = cfg.keyword.navbar_plain,
685 						[cfg.arg.state] = state,
686 						[cfg.arg.basestyle] = pargs[cfg.arg.basestyle],
687 						[cfg.arg.title] = title,
688 						[cfg.arg.titlestyle] = titlestyle,
689 						[andnum('list', 1)] = list,
690 						[cfg.arg.liststyle] = liststyle,
691 						[cfg.arg.listclass] = pargs[andnum('listclass', n)],
692 						[cfg.arg.image] = pargs[andnum('image', n)],
693 						[cfg.arg.imageleft] = pargs[andnum('imageleft', n)],
694 						[cfg.arg.listpadding] = pargs[cfg.arg.listpadding],
695 						argHash = pargs.argHash
696 					})
697 				end
698 			end
699 		end
700 	end
701 	-- ordering of style and bodystyle
702 	targs[cfg.arg.style] = concatstyles({targs[cfg.arg.style] or '', targs[cfg.arg.bodystyle] or ''})
703 	targs[cfg.arg.bodystyle] = nil
704 
705 	-- child or subgroup
706 	if targs[cfg.arg.border] == nil then targs[cfg.arg.border] = pargs[1] end
707 
708 	return p._navbox(targs)
709 end --p._withCollapsibleGroups
710 
711 function p._withColumns(pargs)
712 	-- table for args passed to navbox
713 	local targs = {}
714 
715 	-- tables of column numbers
716 	local colheadernums = {}
717 	local colnums = {}
718 	local colfooternums = {}
719 
720 	-- process args
721 	local passthroughLocal = {
722 		[cfg.arg.evenstyle]=true,
723 		[cfg.arg.groupstyle]=true,
724 		[cfg.arg.liststyle]=true,
725 		[cfg.arg.oddstyle]=true,
726 		[cfg.arg.state]=true,
727 	}
728 	for k,v in pairs(pargs) do
729 		if passthrough[k] or passthroughLocal[k] then
730 			targs[k] = v
731 		elseif type(k) == 'string' then
732 			if k:match(cfg.pattern.listnum) then
733 				local n = k:match(cfg.pattern.listnum)
734 				targs[andnum('liststyle', n + 2)] = pargs[andnum('liststyle', n)]
735 				targs[andnum('group', n + 2)] = pargs[andnum('group', n)]
736 				targs[andnum('groupstyle', n + 2)] = pargs[andnum('groupstyle', n)]
737 				if v and inArray(cfg.keyword.subgroups, v) then
738 					targs[andnum('list', n + 2)] = getSubgroup(pargs, n, v)
739 				else
740 					targs[andnum('list', n + 2)] = v
741 				end
742 			elseif (k:match(cfg.pattern.colheadernum) and v ~= '') then
743 				table.insert(colheadernums, tonumber(k:match(cfg.pattern.colheadernum)))
744 			elseif (k:match(cfg.pattern.colnum) and v ~= '') then
745 				table.insert(colnums, tonumber(k:match(cfg.pattern.colnum)))
746 			elseif (k:match(cfg.pattern.colfooternum) and v ~= '') then
747 				table.insert(colfooternums, tonumber(k:match(cfg.pattern.colfooternum)))
748 			end
749 		end
750 	end
751 	table.sort(colheadernums)
752 	table.sort(colnums)
753 	table.sort(colfooternums)
754 
755 	-- HTML table for list1
756 	local coltable = mw.html.create( 'table' ):addClass('navbox-columns-table')
757 	local row, col
758 
759 	local tablestyle = ( (#colheadernums > 0) or (not isblank(pargs[cfg.arg.fullwidth])) )
760 		and 'width:100%'
761 		or 'width:auto; margin-left:auto; margin-right:auto'
762 
763 	coltable:cssText(concatstyles({
764 		'border-spacing: 0px; text-align:left',
765 		tablestyle,
766 		pargs[cfg.arg.coltablestyle] or ''
767 	}))
768 
769 	--- Header row ---
770 	if (#colheadernums > 0) then
771 		row = coltable:tag('tr')
772 		for k, n in ipairs(colheadernums) do
773 			col = row:tag('td'):addClass('navbox-abovebelow')
774 			col:cssText(concatstyles({
775 				(k > 1) and 'border-left:2px solid #fdfdfd' or '',
776 				'font-weight:bold',
777 				pargs[cfg.arg.colheaderstyle] or '',
778 				pargs[andnum('colheaderstyle', n)] or ''
779 			}))
780 			col:attr('colspan', tonumber(pargs[andnum('colheadercolspan', n)]))
781 			col:wikitext(pargs[andnum('colheader', n)])
782 		end
783 	end
784 
785 	--- Main columns ---
786 	row = coltable:tag('tr'):css('vertical-align', 'top')
787 	for k, n in ipairs(colnums) do
788 		if k == 1 and isblank(pargs[andnum('colheader', 1)])
789 				and isblank(pargs[andnum('colfooter', 1)])
790 				and isblank(pargs[cfg.arg.fullwidth]) then
791 			local nopad = inArray(
792 				{'off', '0', '0em', '0px'},
793 				mw.ustring.gsub(pargs[cfg.arg.padding] or '', '[;%%]', ''))
794 			if not nopad then
795 				row:tag('td'):wikitext('&nbsp;&nbsp;&nbsp;')
796 					:css('width', (pargs[cfg.arg.padding] or '5em'))
797 			end
798 		end
799 		col = row:tag('td'):addClass('navbox-list')
800 		col:cssText(concatstyles({
801 			(k > 1) and 'border-left:2px solid #fdfdfd' or '',
802 			'padding:0px',
803 			pargs[cfg.arg.colstyle] or '',
804 			((n%2 == 0) and pargs[cfg.arg.evencolstyle] or pargs[cfg.arg.oddcolstyle]) or '',
805 			pargs[andnum('colstyle', n)] or '',
806 			'width:' .. (pargs[andnum('colwidth', n)] or pargs[cfg.arg.colwidth] or '10em')
807 		}))
808 		local wt = pargs[andnum('col', n)]
809 		if wt and inArray(cfg.keyword.subgroups, wt) then
810 			wt = getSubgroup(pargs, n, wt, cfg.arg.col_and_num)
811 		end
812 		col:tag('div'):newline():wikitext(wt):newline()
813 	end
814 
815 	--- Footer row ---
816 	if (#colfooternums > 0) then
817 		row = coltable:tag('tr')
818 		for k, n in ipairs(colfooternums) do
819 			col = row:tag('td'):addClass('navbox-abovebelow')
820 			col:cssText(concatstyles({
821 				(k > 1) and 'border-left:2px solid #fdfdfd' or '',
822 				'font-weight:bold',
823 				pargs[cfg.arg.colfooterstyle] or '',
824 				pargs[andnum('colfooterstyle', n)] or ''
825 			}))
826 			col:attr('colspan', tonumber(pargs[andnum('colfootercolspan', n)]))
827 			col:wikitext(pargs[andnum('colfooter', n)])
828 		end
829 	end
830 
831 	-- assign table to list1
832 	targs[andnum('list', 1)] = tostring(coltable)
833 	if isblank(pargs[andnum('colheader', 1)]) 
834 			and isblank(pargs[andnum('col', 1)])
835 			and isblank(pargs[andnum('colfooter', 1)]) then
836 		targs[andnum('list', 1)] = targs[andnum('list', 1)] ..
837 			cfg.category.without_first_col
838 	end
839 
840 	-- Other parameters
841 	targs[cfg.arg.border] = pargs[cfg.arg.border] or pargs[1]
842 	targs[cfg.arg.evenodd] = (not isblank(pargs[cfg.arg.evenodd])) and pargs[cfg.arg.evenodd] or nil
843 	targs[cfg.arg.list1padding] = '0px'
844 	targs[andnum('liststyle', 1)] = 'background:transparent;color:inherit;'
845 	targs[cfg.arg.style] = concatstyles({pargs[cfg.arg.style], pargs[cfg.arg.bodystyle]})
846 	targs[cfg.arg.tracking] = 'no'
847 	
848 	return p._navbox(targs)
849 end --p._withColumns
850 
851 -- Template entry points
852 function p.navbox (frame, boxtype)
853 	local function readArgs(args, prefix)
854 		-- Read the arguments in the order they'll be output in, to make references
855 		-- number in the right order.
856 		local _ = 0
857 		_ = _ + (args[prefix .. cfg.arg.title] and #args[prefix .. cfg.arg.title] or 0)
858 		_ = _ + (args[prefix .. cfg.arg.above] and #args[prefix .. cfg.arg.above] or 0)
859 		-- Limit this to 20 as covering 'most' cases (that's a SWAG) and because
860 		-- iterator approach won't work here
861 		for i = 1, 20 do
862 			_ = _ + (args[prefix .. andnum('group', i)] and #args[prefix .. andnum('group', i)] or 0)
863 			if inArray(cfg.keyword.subgroups, args[prefix .. andnum('list', i)]) then
864 				for _, v in ipairs(cfg.arg.subgroups_and_num) do
865 					readArgs(args, prefix .. string.format(v, i) .. "_")
866 				end
867 				readArgs(args, prefix .. andnum('col', i) .. "_")
868 			end
869 		end
870 		_ = _ + (args[prefix .. cfg.arg.below] and #args[prefix .. cfg.arg.below] or 0)
871 		return _
872 	end
873 
874 	if not getArgs then
875 		getArgs = require('Module:Arguments').getArgs
876 	end
877 	local args = getArgs(frame, {wrappers = {cfg.pattern[boxtype or 'navbox']}})
878 	args.argHash = readArgs(args, "")
879 	args.type = args.type or cfg.keyword[boxtype]
880 	return p['_navbox'](args)
881 end
882 
883 p[cfg.keyword.with_collapsible_groups] = function (frame)
884 	return p.navbox(frame, 'with_collapsible_groups')
885 end
886 
887 p[cfg.keyword.with_columns] = function (frame)
888 	return p.navbox(frame, 'with_columns')
889 end
890 
891 return p