Module:Navbox: Difference between revisions

From The Sextant of Worlds
Jump to navigationJump to search
m 1 revision imported
No edit summary
 
Line 1: Line 1:
require('strict')
--
-- This module implements {{Navbox}}
--
 
local p = {}
local p = {}
local cfg = mw.loadData('Module:Navbox/configuration')
 
local inArray = require("Module:TableTools").inArray
local navbar = require('Module:Navbar')._navbar
local getArgs -- lazily initialized
local getArgs -- lazily initialized
local hiding_templatestyles = {}


-- global passthrough variables
local args
local passthrough = {
local border
[cfg.arg.above]=true,[cfg.arg.aboveclass]=true,[cfg.arg.abovestyle]=true,
local listnums
[cfg.arg.basestyle]=true,
local ODD_EVEN_MARKER = '\127_ODDEVEN_\127'
[cfg.arg.below]=true,[cfg.arg.belowclass]=true,[cfg.arg.belowstyle]=true,
local RESTART_MARKER = '\127_ODDEVEN0_\127'
[cfg.arg.bodyclass]=true,
local REGEX_MARKER = '\127_ODDEVEN(%d?)_\127'
[cfg.arg.groupclass]=true,
[cfg.arg.image]=true,[cfg.arg.imageclass]=true,[cfg.arg.imagestyle]=true,
[cfg.arg.imageleft]=true,[cfg.arg.imageleftstyle]=true,
[cfg.arg.listclass]=true,
[cfg.arg.name]=true,
[cfg.arg.navbar]=true,
[cfg.arg.state]=true,
[cfg.arg.title]=true,[cfg.arg.titleclass]=true,[cfg.arg.titlestyle]=true,
argHash=true
}


-- helper functions
local function striped(wikitext)
local andnum = function(s, n) return string.format(cfg.arg[s .. '_and_num'], n) end
-- Return wikitext with markers replaced for odd/even striping.
local isblank = function(v) return (v or '') == '' end
-- Child (subgroup) navboxes are flagged with a category that is removed
 
-- by parent navboxes. The result is that the category shows all pages
local function concatstrings(s)
-- where a child navbox is not contained in a parent navbox.
local r = table.concat(s, '')
local orphanCat = '[[Category:Navbox orphans]]'
if r:match('^%s*$') then return nil end
if border == 'subgroup' and args.orphan ~= 'yes' then
return r
-- No change; striping occurs in outermost navbox.
end
return wikitext .. orphanCat
 
end
local function concatstyles(s)
local first, second = 'odd', 'even'
local r = ''
if args.evenodd then
for _, v in ipairs(s) do
if args.evenodd == 'swap' then
v = mw.text.trim(v, "%s;")
first, second = second, first
if not isblank(v) then r = r .. v .. ';' end
else
first = args.evenodd
second = first
end
end
end
if isblank(r) then return nil end
local changer
return r
if first == second then
end
changer = first
 
else
local function getSubgroup(args, listnum, listText, prefix)
local index = 0
local subArgs = {
changer = function (code)
[cfg.arg.border] = cfg.keyword.border_subgroup,
if code == '0' then
[cfg.arg.navbar] = cfg.keyword.navbar_plain,
-- Current occurrence is for a group before a nested table.
argHash = 0
-- Set it to first as a valid although pointless class.
}
-- The next occurrence will be the first row after a title
local hasSubArgs = false
-- in a subgroup and will also be first.
local subgroups_and_num = prefix and {prefix} or cfg.arg.subgroups_and_num
index = 0
for k, v in pairs(args) do
return first
k = tostring(k)
for _, w in ipairs(subgroups_and_num) do
w = string.format(w, listnum) .. "_"
if (#k > #w) and (k:sub(1, #w) == w) then
subArgs[k:sub(#w + 1)] = v
hasSubArgs = true
subArgs.argHash = subArgs.argHash + (v and #v or 0)
end
end
index = index + 1
return index % 2 == 1 and first or second
end
end
end
end
return hasSubArgs and p._navbox(subArgs) or listText
local regex = orphanCat:gsub('([%[%]])', '%%%1')
return (wikitext:gsub(regex, ''):gsub(REGEX_MARKER, changer))  -- () omits gsub count
end
end


-- Main functions
local function processItem(item, nowrapitems)
function p._navbox(args)
if item:sub(1, 2) == '{|' then
if args.type == cfg.keyword.with_collapsible_groups then
-- Applying nowrap to lines in a table does not make sense.
return p._withCollapsibleGroups(args)
-- Add newlines to compensate for trim of x in |parm=x in a template.
elseif args.type == cfg.keyword.with_columns then
return '\n' .. item ..'\n'
return p._withColumns(args)
end
end
 
if nowrapitems == 'yes' then
local function striped(wikitext, border)
local lines = {}
-- Return wikitext with markers replaced for odd/even striping.
for line in (item .. '\n'):gmatch('([^\n]*)\n') do
-- Child (subgroup) navboxes are flagged with a category that is removed
local prefix, content = line:match('^([*:;#]+)%s*(.*)')
-- by parent navboxes. The result is that the category shows all pages
if prefix and not content:match('^<span class="nowrap">') then
-- where a child navbox is not contained in a parent navbox.
line = prefix .. '<span class="nowrap">' .. content .. '</span>'
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
table.insert(lines, line)
end
end
local changer
item = table.concat(lines, '\n')
if first == second then
end
changer = first
if item:match('^[*:;#]') then
else
return '\n' .. item ..'\n'
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
end
return item
end
-- Separate function so that we can evaluate properly whether hlist should
-- be added by the module
local function has_navbar()
return args.navbar ~= 'off' and args.navbar ~= 'plain' and not
(not args.name and mw.getCurrentFrame():getParent():getTitle():gsub('/sandbox$', '') == 'Template:Navbox')
end
local function renderNavBar(titleCell)


local function processItem(item, nowrapitems)
if has_navbar() then
if item:sub(1, 2) == '{|' then
titleCell:wikitext(navbar{
-- Applying nowrap to lines in a table does not make sense.
args.name,
-- Add newlines to compensate for trim of x in |parm=x in a template.
-- we depend on this being mini = 1 when the navbox module decides
return '\n' .. item .. '\n'
-- to add hlist templatestyles. we also depend on navbar outputting
end
-- a copy of the hlist templatestyles.
if nowrapitems == cfg.keyword.nowrapitems_yes then
mini = 1,
local lines = {}
fontstyle = (args.basestyle or '') .. ';' .. (args.titlestyle or '') .. ';background:none transparent;border:none;box-shadow:none; padding:0;'
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 = string.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
end


local function has_navbar()
end
return args[cfg.arg.navbar] ~= cfg.keyword.navbar_off
 
and args[cfg.arg.navbar] ~= cfg.keyword.navbar_plain
--
and (
--  Title row
args[cfg.arg.name]
--
or mw.getCurrentFrame():getParent():getTitle():gsub(cfg.pattern.sandbox, '')
local function renderTitleRow(tbl)
~= cfg.pattern.navbox
if not args.title then return end
)
 
end
local titleRow = tbl:tag('tr')


-- extract text color from css, which is the only permitted inline CSS for the navbar
if args.titlegroup then
local function extract_color(css_str)
titleRow
-- return nil because navbar takes its argument into mw.html which handles
:tag('th')
-- nil gracefully, removing the associated style attribute
:attr('scope', 'row')
return mw.ustring.match(';' .. css_str .. ';', '.*;%s*([Cc][Oo][Ll][Oo][Rr]%s*:%s*.-)%s*;') or nil
:addClass('navbox-group')
:addClass(args.titlegroupclass)
:cssText(args.basestyle)
:cssText(args.groupstyle)
:cssText(args.titlegroupstyle)
:wikitext(args.titlegroup)
end
end


local function renderNavBar(titleCell)
local titleCell = titleRow:tag('th'):attr('scope', 'col')
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


if args.titlegroup then
titleCell
:addClass('navbox-title1')
end
end


local function renderTitleRow(tbl)
local titleColspan = 2
if not args[cfg.arg.title] then return end
if args.imageleft then titleColspan = titleColspan + 1 end
if args.image then titleColspan = titleColspan + 1 end
if args.titlegroup then titleColspan = titleColspan - 1 end


local titleRow = tbl:tag('tr')
titleCell
:cssText(args.basestyle)
:cssText(args.titlestyle)
:addClass('navbox-title')
: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.title))
:addClass(args.titleclass)
:css('font-size', '114%')
:css('margin', '0 4em')
:wikitext(processItem(args.title))
end


titleCell
--
:cssText(args[cfg.arg.basestyle])
--  Above/Below rows
:cssText(args[cfg.arg.titlestyle])
--
:addClass(cfg.class.navbox_title)
 
:attr('colspan', titleColspan)
local function getAboveBelowColspan()
local ret = 2
if args.imageleft then ret = ret + 1 end
if args.image then ret = ret + 1 end
return ret
end


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


titleCell
tbl:tag('tr')
:tag('td')
:addClass('navbox-abovebelow')
:addClass(args.aboveclass)
:cssText(args.basestyle)
:cssText(args.abovestyle)
:attr('colspan', getAboveBelowColspan())
:tag('div')
:tag('div')
-- id for aria-labelledby attribute
-- id for aria-labelledby attribute, if no title
:attr('id', mw.uri.anchorEncode(args[cfg.arg.title]) .. args.argHash)
:attr('id', args.title and nil or mw.uri.anchorEncode(args.above))
:addClass(args[cfg.arg.titleclass])
:wikitext(processItem(args.above, args.nowrapitems))
:css('font-size', '114%')
end
:css('margin', '0 4em')
:wikitext(processItem(args[cfg.arg.title]))
end


local function getAboveBelowColspan()
local function renderBelowRow(tbl)
local ret = 2
if not args.below then return end
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)
tbl:tag('tr')
if not args[cfg.arg.above] then return end
:tag('td')
 
:addClass('navbox-abovebelow')
tbl:tag('tr')
:addClass(args.belowclass)
:tag('td')
:cssText(args.basestyle)
:addClass(cfg.class.navbox_abovebelow)
:cssText(args.belowstyle)
:addClass(args[cfg.arg.aboveclass])
:attr('colspan', getAboveBelowColspan())
:cssText(args[cfg.arg.basestyle])
:tag('div')
:cssText(args[cfg.arg.abovestyle])
:wikitext(processItem(args.below, args.nowrapitems))
:attr('colspan', getAboveBelowColspan())
end
: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]) .. args.argHash)
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
--  List rows
--
local function renderListRow(tbl, index, listnum)
local row = tbl:tag('tr')


tbl:tag('tr')
if index == 1 and args.imageleft then
row
:tag('td')
:tag('td')
:addClass(cfg.class.navbox_abovebelow)
:addClass('navbox-image')
:addClass(args[cfg.arg.belowclass])
:addClass(args.imageclass)
:cssText(args[cfg.arg.basestyle])
:css('width', '1px')               -- Minimize width
:cssText(args[cfg.arg.belowstyle])
:css('padding', '0px 2px 0px 0px')
:attr('colspan', getAboveBelowColspan())
:cssText(args.imageleftstyle)
:attr('rowspan', #listnums)
:tag('div')
:tag('div')
:wikitext(processItem(args[cfg.arg.below], args[cfg.arg.nowrapitems]))
:wikitext(processItem(args.imageleft))
end
end


local function renderListRow(tbl, index, listnum, listnums_size)
if args['group' .. listnum] then
local row = tbl:tag('tr')
local groupCell = row:tag('th')


if index == 1 and args[cfg.arg.imageleft] then
-- id for aria-labelledby attribute, if lone group with no title or above
row
if listnum == 1 and not (args.title or args.above or args.group2) then
:tag('td')
groupCell
:addClass(cfg.class.noviewer)
:attr('id', mw.uri.anchorEncode(args.group1))
: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
end


local group_and_num = andnum('group', listnum)
groupCell
local groupstyle_and_num = andnum('groupstyle', listnum)
:attr('scope', 'row')
if args[group_and_num] then
:addClass('navbox-group')
local groupCell = row:tag('th')
:addClass(args.groupclass)
:cssText(args.basestyle)
:css('width', args.groupwidth or '1%') -- If groupwidth not specified, minimize width


-- id for aria-labelledby attribute, if lone group with no title or above
groupCell
if listnum == 1 and not (args[cfg.arg.title] or args[cfg.arg.above] or args[cfg.arg.group2]) then
:cssText(args.groupstyle)
groupCell
:cssText(args['group' .. listnum .. 'style'])
:attr('id', mw.uri.anchorEncode(args[cfg.arg.group1]) .. args.argHash)
:wikitext(args['group' .. listnum])
end
end


groupCell
local listCell = row:tag('td')
: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
if args['group' .. listnum] then
:cssText(args[cfg.arg.groupstyle])
listCell
:cssText(args[groupstyle_and_num])
:addClass('navbox-list1')
:wikitext(args[group_and_num])
else
end
listCell:attr('colspan', 2)
end


local listCell = row:tag('td')
if not args.groupwidth then
listCell:css('width', '100%')
end


if args[group_and_num] then
local rowstyle  -- usually nil so cssText(rowstyle) usually adds nothing
listCell
if index % 2 == 1 then
:addClass(cfg.class.navbox_list_with_group)
rowstyle = args.oddstyle
else
else
listCell:attr('colspan', 2)
rowstyle = args.evenstyle
end
end


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


local rowstyle  -- usually nil so cssText(rowstyle) usually adds nothing
if index == 1 and args.image then
if index % 2 == 1 then
row
rowstyle = args[cfg.arg.oddstyle]
:tag('td')
else
:addClass('navbox-image')
rowstyle = args[cfg.arg.evenstyle]
:addClass(args.imageclass)
end
:css('width', '1px')              -- Minimize width
:css('padding', '0px 0px 0px 2px')
:cssText(args.imagestyle)
:attr('rowspan', #listnums)
:tag('div')
:wikitext(processItem(args.image))
end
end


local list_and_num = andnum('list', listnum)
local listText = inArray(cfg.keyword.subgroups, args[list_and_num])
and getSubgroup(args, listnum, args[list_and_num]) or args[list_and_num]


local oddEven = cfg.marker.oddeven
--
if listText:sub(1, 12) == '</div><table' then
--  Tracking categories
-- 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 = andnum('liststyle', listnum)
local function needsHorizontalLists()
local listclass_and_num = andnum('listclass', listnum)
if border == 'subgroup' or args.tracking == 'no' then
listCell
return false
: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 listClasses = {
['plainlist'] = true, ['hlist'] = true, ['hlist hnum'] = true,
['hlist hwrap'] = true, ['hlist vcard'] = true, ['vcard hlist'] = true,
['hlist vevent'] = true,
}
return not (listClasses[args.listclass] or listClasses[args.bodyclass])
end


local function has_list_class(htmlclass)
-- there are a lot of list classes in the wild, so we have a function to find
-- them and add their TemplateStyles
local function addListStyles()
local frame = mw.getCurrentFrame()
-- TODO?: Should maybe take a table of classes for e.g. hnum, hwrap as above
-- I'm going to do the stupid thing first though
-- Also not sure hnum and hwrap are going to live in the same TemplateStyles
-- as hlist
local function _addListStyles(htmlclass, templatestyles)
local class_args = { -- rough order of probability of use
'bodyclass', 'listclass', 'aboveclass', 'belowclass', 'titleclass',
'navboxclass', 'groupclass', 'titlegroupclass', 'imageclass'
}
local patterns = {
local patterns = {
'^' .. htmlclass .. '$',
'^' .. htmlclass .. '$',
Line 347: Line 322:
'%s' .. htmlclass .. '%s'
'%s' .. htmlclass .. '%s'
}
}
 
for arg, _ in pairs(args) do
local found = false
if type(arg) == 'string' and mw.ustring.find(arg, cfg.pattern.class) then
for _, arg in ipairs(class_args) do
for _, pattern in ipairs(patterns) do
for _, pattern in ipairs(patterns) do
if mw.ustring.find(args[arg] or '', pattern) then
if mw.ustring.find(args[arg] or '', pattern) then
return true
found = true
end
break
end
end
end
end
if found then break end
end
end
return false
if found then
end
return frame:extensionTag{
 
name = 'templatestyles', args = { src = templatestyles }
-- 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 }
}
}
else
return ''
end
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
 
local function needsHorizontalLists(border)
local hlist_styles = ''
if border == cfg.keyword.border_subgroup or args[cfg.arg.tracking] == cfg.keyword.tracking_no then
-- navbar always has mini = 1, so here (on this wiki) we can assume that
return false
-- we don't need to output hlist styles in navbox again.
end
if not has_navbar() then
return not has_list_class(cfg.pattern.hlist) and not has_list_class(cfg.pattern.plainlist)
hlist_styles = _addListStyles('hlist', 'Flatlist/styles.css')
end
end
local plainlist_styles = _addListStyles('plainlist', 'Plainlist/styles.css')
return hlist_styles .. plainlist_styles
end


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


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


local function isIllegible()
local function isIllegible()
local styleratio = require('Module:Color contrast')._styleratio
-- require('Module:Color contrast') absent on mediawiki.org
for key, style in pairs(args) do
return false
if tostring(key):match(cfg.pattern.style) then
end
if styleratio{mw.text.unstripNoWiki(style)} < 4.5 then
return true
end
end
end
return false
end


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


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


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


local function renderMainTable(border, listnums)
--
local tbl = mw.html.create('table')
--  Main navbox tables
:addClass(cfg.class.nowraplinks)
--
:addClass(args[cfg.arg.bodyclass])
local function renderMainTable()
local tbl = mw.html.create('table')
:addClass('nowraplinks')
:addClass(args.bodyclass)


local state = args[cfg.arg.state]
if args.title and (args.state ~= 'plain' and args.state ~= 'off') then
if args[cfg.arg.title] and state ~= cfg.keyword.state_plain and state ~= cfg.keyword.state_off then
if args.state == 'collapsed' then args.state = 'mw-collapsed' end
if state == cfg.keyword.state_collapsed then
tbl
state = cfg.class.collapsed
:addClass('mw-collapsible')
end
:addClass(args.state or 'autocollapse')
tbl
end
: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
tbl:css('border-spacing', 0)
if border == 'subgroup' or border == 'none' then
tbl
:addClass('navbox-subgroup')
:cssText(args.bodystyle)
:cssText(args.style)
else  -- regular navbox - bodystyle and style will be applied to the wrapper table
tbl
:addClass('navbox-inner')
:css('background', 'transparent')
:css('color', 'inherit')
end
end
tbl:cssText(args.innerstyle)


local function add_navbox_styles(hiding_templatestyles)
renderTitleRow(tbl)
local frame = mw.getCurrentFrame()
renderAboveRow(tbl)
-- This is a lambda so that it doesn't need the frame as a parameter
for i, listnum in ipairs(listnums) do
local function add_user_styles(templatestyles)
renderListRow(tbl, i, listnum)
if not isblank(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
end
renderBelowRow(tbl)


-- work around [[phab:T303378]]
return tbl
-- for each arg: find all the templatestyles strip markers, insert them into a
end
-- table. then remove all templatestyles markers from the arg
local strip_marker_pattern = '(\127[^\127]*UNIQ%-%-templatestyles%-%x+%-QINU[^\127]*\127)'
local argHash = 0
for k, arg in pairs(args) do
if type(arg) == 'string' then
for marker in string.gfind(arg, strip_marker_pattern) do
table.insert(hiding_templatestyles, marker)
end
argHash = argHash + #arg
args[k] = string.gsub(arg, strip_marker_pattern, '')
end
end
if not args.argHash then args.argHash = argHash end


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


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


local border = mw.text.trim(args[cfg.arg.border] or args[1] or '')
border = mw.text.trim(args.border or args[1] or '')
if border == cfg.keyword.border_child then
if border == 'child' then
border = cfg.keyword.border_subgroup
border = 'subgroup'
end
end


-- render the main body of the navbox
-- render the main body of the navbox
local tbl = renderMainTable(border, listnums)
local tbl = renderMainTable()
 
-- get templatestyles
local frame = mw.getCurrentFrame()
local base_templatestyles = frame:extensionTag{
name = 'templatestyles', args = { src = 'Module:Navbox/styles.css' }
}
local templatestyles = ''
if args.templatestyles and args.templatestyles ~= '' then
templatestyles = frame:extensionTag{
name = 'templatestyles', args = { src = args.templatestyles }
}
end
local res = mw.html.create()
local res = mw.html.create()
-- render the appropriate wrapper for the navbox, based on the border param
-- 'navbox-styles' exists for two reasons:
 
--  1. To wrap the styles to work around phab: T200206 more elegantly. Instead
if border == cfg.keyword.border_none then
--   of combinatorial rules, this ends up being linear number of CSS rules.
res:node(add_navbox_styles(hiding_templatestyles))
--  2. To allow MobileFrontend to rip the styles out with 'nomobile' such that
--    they are not dumped into the mobile view.
res:tag('div')
:addClass('navbox-styles')
:addClass('nomobile')
:wikitext(base_templatestyles .. templatestyles)
:done()
-- render the appropriate wrapper around the navbox, depending on the border param
if border == 'none' then
local nav = res:tag('div')
local nav = res:tag('div')
:attr('role', 'navigation')
:attr('role', 'navigation')
:wikitext(addListStyles())
:node(tbl)
:node(tbl)
-- aria-labelledby title, otherwise above, otherwise lone group
-- aria-labelledby title, otherwise above, otherwise lone group
if args[cfg.arg.title] or args[cfg.arg.above] or (args[cfg.arg.group1]
if args.title or args.above or (args.group1 and not args.group2) then
and not args[cfg.arg.group2]) then
nav:attr('aria-labelledby', mw.uri.anchorEncode(args.title or args.above or args.group1))
nav:attr(
'aria-labelledby',
mw.uri.anchorEncode(
args[cfg.arg.title] or args[cfg.arg.above] or args[cfg.arg.group1]
) .. args.argHash
)
else
else
nav:attr('aria-label', cfg.aria_label)
nav:attr('aria-label', 'Navbox')
end
end
elseif border == cfg.keyword.border_subgroup then
elseif border == 'subgroup' then
-- We assume that this navbox is being rendered in a list cell of a
-- 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.
-- parent navbox, and is therefore inside a div with padding:0em 0.25em.
Line 588: Line 496:
res
res
:wikitext('</div>')
:wikitext('</div>')
:wikitext(addListStyles())
:node(tbl)
:node(tbl)
:wikitext('<div>')
:wikitext('<div>')
else
else
res:node(add_navbox_styles(hiding_templatestyles))
local nav = res:tag('div')
local nav = res:tag('div')
:attr('role', 'navigation')
:attr('role', 'navigation')
:addClass(cfg.class.navbox)
:addClass('navbox')
:addClass(args[cfg.arg.navboxclass])
:addClass(args.navboxclass)
:cssText(args[cfg.arg.bodystyle])
:cssText(args.bodystyle)
:cssText(args[cfg.arg.style])
:cssText(args.style)
:css('padding', '3px')
:css('padding', '3px')
:wikitext(addListStyles())
:node(tbl)
:node(tbl)
-- aria-labelledby title, otherwise above, otherwise lone group
-- aria-labelledby title, otherwise above, otherwise lone group
if args[cfg.arg.title] or args[cfg.arg.above]
if args.title or args.above or (args.group1 and not args.group2) then
or (args[cfg.arg.group1] and not args[cfg.arg.group2]) then
nav:attr('aria-labelledby', mw.uri.anchorEncode(args.title or args.above or args.group1))
nav:attr(
'aria-labelledby',
mw.uri.anchorEncode(
args[cfg.arg.title] or args[cfg.arg.above] or args[cfg.arg.group1]
) .. args.argHash
)
else
else
nav:attr('aria-label', cfg.aria_label .. args.argHash)
nav:attr('aria-label', 'Navbox')
end
end
end
end


if (args[cfg.arg.nocat] or cfg.keyword.nocat_false):lower() == cfg.keyword.nocat_false then
if (args.nocat or 'false'):lower() == 'false' then
renderTrackingCategories(res, border)
renderTrackingCategories(res)
end
end
return striped(tostring(res), border)
end --p._navbox


function p._withCollapsibleGroups(pargs)
return striped(tostring(res))
-- table for args passed to navbox
end
local targs = {}


-- process args
function p.navbox(frame)
local passthroughLocal = {
if not getArgs then
[cfg.arg.bodystyle] = true,
getArgs = require('Module:Arguments').getArgs
[cfg.arg.border] = true,
[cfg.arg.style] = true,
}
for k,v in pairs(pargs) do
if k and type(k) == 'string' then
if passthrough[k] or passthroughLocal[k] then
targs[k] = v
elseif (k:match(cfg.pattern.num)) then
local n = k:match(cfg.pattern.num)
local list_and_num = andnum('list', n)
if ((k:match(cfg.pattern.listnum) or k:match(cfg.pattern.contentnum))
and targs[list_and_num] == nil
and pargs[andnum('group', n)] == nil
and pargs[andnum('sect', n)] == nil
and pargs[andnum('section', n)] == nil) then
targs[list_and_num] = concatstrings({
pargs[list_and_num] or '',
pargs[andnum('content', n)] or ''
})
if (targs[list_and_num] and inArray(cfg.keyword.subgroups, targs[list_and_num])) then
targs[list_and_num] = getSubgroup(pargs, n, targs[list_and_num])
end
elseif ((k:match(cfg.pattern.groupnum) or k:match(cfg.pattern.sectnum) or k:match(cfg.pattern.sectionnum))
and targs[list_and_num] == nil) then
local titlestyle = concatstyles({
pargs[cfg.arg.groupstyle] or '',
pargs[cfg.arg.secttitlestyle] or '',
pargs[andnum('groupstyle', n)] or '',
pargs[andnum('sectiontitlestyle', n)] or ''
})
local liststyle = concatstyles({
pargs[cfg.arg.liststyle] or '',
pargs[cfg.arg.contentstyle] or '',
pargs[andnum('liststyle', n)] or '',
pargs[andnum('contentstyle', n)] or ''
})
local title = concatstrings({
pargs[andnum('group', n)] or '',
pargs[andnum('sect', n)] or '',
pargs[andnum('section', n)] or ''
})
local list = concatstrings({
pargs[list_and_num] or '',
pargs[andnum('content', n)] or ''
})
if list and inArray(cfg.keyword.subgroups, list) then
list = getSubgroup(pargs, n, list)
end
local abbr_and_num = andnum('abbr', n)
local state = (pargs[abbr_and_num] and pargs[abbr_and_num] == pargs[cfg.arg.selected])
and cfg.keyword.state_uncollapsed
or (pargs[andnum('state', n)] or cfg.keyword.state_collapsed)
targs[list_and_num] =p._navbox({
cfg.keyword.border_child,
[cfg.arg.navbar] = cfg.keyword.navbar_plain,
[cfg.arg.state] = state,
[cfg.arg.basestyle] = pargs[cfg.arg.basestyle],
[cfg.arg.title] = title,
[cfg.arg.titlestyle] = titlestyle,
[andnum('list', 1)] = list,
[cfg.arg.liststyle] = liststyle,
[cfg.arg.listclass] = pargs[andnum('listclass', n)],
[cfg.arg.image] = pargs[andnum('image', n)],
[cfg.arg.imageleft] = pargs[andnum('imageleft', n)],
[cfg.arg.listpadding] = pargs[cfg.arg.listpadding],
argHash = pargs.argHash
})
end
end
end
end
end
-- ordering of style and bodystyle
args = getArgs(frame, {wrappers = {'Template:Navbox', 'Template:Navbox subgroup'}})
targs[cfg.arg.style] = concatstyles({targs[cfg.arg.style] or '', targs[cfg.arg.bodystyle] or ''})
if frame.args.border then
targs[cfg.arg.bodystyle] = nil
-- This allows Template:Navbox_subgroup to use {{#invoke:Navbox|navbox|border=...}}.
 
args.border = frame.args.border
-- child or subgroup
if targs[cfg.arg.border] == nil then targs[cfg.arg.border] = pargs[1] end
 
return p._navbox(targs)
end --p._withCollapsibleGroups
 
function p._withColumns(pargs)
-- table for args passed to navbox
local targs = {}
 
-- tables of column numbers
local colheadernums = {}
local colnums = {}
local colfooternums = {}
 
-- process args
local passthroughLocal = {
[cfg.arg.evenstyle]=true,
[cfg.arg.groupstyle]=true,
[cfg.arg.liststyle]=true,
[cfg.arg.oddstyle]=true,
[cfg.arg.state]=true,
}
for k,v in pairs(pargs) do
if passthrough[k] or passthroughLocal[k] then
targs[k] = v
elseif type(k) == 'string' then
if k:match(cfg.pattern.listnum) then
local n = k:match(cfg.pattern.listnum)
targs[andnum('liststyle', n + 2)] = pargs[andnum('liststyle', n)]
targs[andnum('group', n + 2)] = pargs[andnum('group', n)]
targs[andnum('groupstyle', n + 2)] = pargs[andnum('groupstyle', n)]
if v and inArray(cfg.keyword.subgroups, v) then
targs[andnum('list', n + 2)] = getSubgroup(pargs, n, v)
else
targs[andnum('list', n + 2)] = v
end
elseif (k:match(cfg.pattern.colheadernum) and v ~= '') then
table.insert(colheadernums, tonumber(k:match(cfg.pattern.colheadernum)))
elseif (k:match(cfg.pattern.colnum) and v ~= '') then
table.insert(colnums, tonumber(k:match(cfg.pattern.colnum)))
elseif (k:match(cfg.pattern.colfooternum) and v ~= '') then
table.insert(colfooternums, tonumber(k:match(cfg.pattern.colfooternum)))
end
end
end
end
table.sort(colheadernums)
table.sort(colnums)
table.sort(colfooternums)


-- HTML table for list1
-- Read the arguments in the order they'll be output in, to make references number in the right order.
local coltable = mw.html.create( 'table' ):addClass('navbox-columns-table')
local _
local row, col
_ = args.title
 
_ = args.above
local tablestyle = ( (#colheadernums > 0) or (not isblank(pargs[cfg.arg.fullwidth])) )
for i = 1, 20 do
and 'width:100%'
_ = args["group" .. tostring(i)]
or 'width:auto; margin-left:auto; margin-right:auto'
_ = args["list" .. tostring(i)]
 
coltable:cssText(concatstyles({
'border-spacing: 0px; text-align:left',
tablestyle,
pargs[cfg.arg.coltablestyle] or ''
}))
 
--- Header row ---
if (#colheadernums > 0) then
row = coltable:tag('tr')
for k, n in ipairs(colheadernums) do
col = row:tag('td'):addClass('navbox-abovebelow')
col:cssText(concatstyles({
(k > 1) and 'border-left:2px solid #fdfdfd' or '',
'font-weight:bold',
pargs[cfg.arg.colheaderstyle] or '',
pargs[andnum('colheaderstyle', n)] or ''
}))
col:attr('colspan', tonumber(pargs[andnum('colheadercolspan', n)]))
col:wikitext(pargs[andnum('colheader', n)])
end
end
end
_ = args.below


--- Main columns ---
return p._navbox(args)
row = coltable:tag('tr'):css('vertical-align', 'top')
for k, n in ipairs(colnums) do
if k == 1 and isblank(pargs[andnum('colheader', 1)])
and isblank(pargs[andnum('colfooter', 1)])
and isblank(pargs[cfg.arg.fullwidth]) then
local nopad = inArray(
{'off', '0', '0em', '0px'},
mw.ustring.gsub(pargs[cfg.arg.padding] or '', '[;%%]', ''))
if not nopad then
row:tag('td'):wikitext('&nbsp;&nbsp;&nbsp;')
:css('width', (pargs[cfg.arg.padding] or '5em'))
end
end
col = row:tag('td'):addClass('navbox-list')
col:cssText(concatstyles({
(k > 1) and 'border-left:2px solid #fdfdfd' or '',
'padding:0px',
pargs[cfg.arg.colstyle] or '',
((n%2 == 0) and pargs[cfg.arg.evencolstyle] or pargs[cfg.arg.oddcolstyle]) or '',
pargs[andnum('colstyle', n)] or '',
'width:' .. (pargs[andnum('colwidth', n)] or pargs[cfg.arg.colwidth] or '10em')
}))
local wt = pargs[andnum('col', n)]
if wt and inArray(cfg.keyword.subgroups, wt) then
wt = getSubgroup(pargs, n, wt, cfg.arg.col_and_num)
end
col:tag('div'):newline():wikitext(wt):newline()
end
 
--- Footer row ---
if (#colfooternums > 0) then
row = coltable:tag('tr')
for k, n in ipairs(colfooternums) do
col = row:tag('td'):addClass('navbox-abovebelow')
col:cssText(concatstyles({
(k > 1) and 'border-left:2px solid #fdfdfd' or '',
'font-weight:bold',
pargs[cfg.arg.colfooterstyle] or '',
pargs[andnum('colfooterstyle', n)] or ''
}))
col:attr('colspan', tonumber(pargs[andnum('colfootercolspan', n)]))
col:wikitext(pargs[andnum('colfooter', n)])
end
end
 
-- assign table to list1
targs[andnum('list', 1)] = tostring(coltable)
if isblank(pargs[andnum('colheader', 1)])
and isblank(pargs[andnum('col', 1)])
and isblank(pargs[andnum('colfooter', 1)]) then
targs[andnum('list', 1)] = targs[andnum('list', 1)] ..
cfg.category.without_first_col
end
 
-- Other parameters
targs[cfg.arg.border] = pargs[cfg.arg.border] or pargs[1]
targs[cfg.arg.evenodd] = (not isblank(pargs[cfg.arg.evenodd])) and pargs[cfg.arg.evenodd] or nil
targs[cfg.arg.list1padding] = '0px'
targs[andnum('liststyle', 1)] = 'background:transparent;color:inherit;'
targs[cfg.arg.style] = concatstyles({pargs[cfg.arg.style], pargs[cfg.arg.bodystyle]})
targs[cfg.arg.tracking] = 'no'
return p._navbox(targs)
end --p._withColumns
 
-- Template entry points
function p.navbox (frame, boxtype)
local function readArgs(args, prefix)
-- Read the arguments in the order they'll be output in, to make references
-- number in the right order.
local _ = 0
_ = _ + (args[prefix .. cfg.arg.title] and #args[prefix .. cfg.arg.title] or 0)
_ = _ + (args[prefix .. cfg.arg.above] and #args[prefix .. cfg.arg.above] or 0)
-- Limit this to 20 as covering 'most' cases (that's a SWAG) and because
-- iterator approach won't work here
for i = 1, 20 do
_ = _ + (args[prefix .. andnum('group', i)] and #args[prefix .. andnum('group', i)] or 0)
if inArray(cfg.keyword.subgroups, args[prefix .. andnum('list', i)]) then
for _, v in ipairs(cfg.arg.subgroups_and_num) do
readArgs(args, prefix .. string.format(v, i) .. "_")
end
readArgs(args, prefix .. andnum('col', i) .. "_")
end
end
_ = _ + (args[prefix .. cfg.arg.below] and #args[prefix .. cfg.arg.below] or 0)
return _
end
 
if not getArgs then
getArgs = require('Module:Arguments').getArgs
end
local args = getArgs(frame, {wrappers = {cfg.pattern[boxtype or 'navbox']}})
args.argHash = readArgs(args, "")
args.type = args.type or cfg.keyword[boxtype]
return p['_navbox'](args)
end
 
p[cfg.keyword.with_collapsible_groups] = function (frame)
return p.navbox(frame, 'with_collapsible_groups')
end
 
p[cfg.keyword.with_columns] = function (frame)
return p.navbox(frame, 'with_columns')
end
end


return p
return p

Latest revision as of 16:57, 3 May 2025

Lua error in Module:Lua_banner at line 113: attempt to index field 'edit' (a nil value). Lua error in Module:TNT at line 159: Missing JsonConfig extension; Cannot load https://commons.wikimedia.org/wiki/Data:I18n/Uses TemplateStyles.tab.

This module implements the {{Navbox}} template.

Usage

{{#invoke:Navbox|navbox}}

Please see the template page for usage instructions.

Tracking/maintenance categories

See also


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

local p = {}

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

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

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

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

-- Separate function so that we can evaluate properly whether hlist should
-- be added by the module
local function has_navbar()
	return args.navbar ~= 'off' and args.navbar ~= 'plain' and not
		(not args.name and mw.getCurrentFrame():getParent():getTitle():gsub('/sandbox$', '') == 'Template:Navbox')
end

local function renderNavBar(titleCell)

	if has_navbar() then
		titleCell:wikitext(navbar{
			args.name,
			-- we depend on this being mini = 1 when the navbox module decides
			-- to add hlist templatestyles. we also depend on navbar outputting
			-- a copy of the hlist templatestyles.
			mini = 1,
			fontstyle = (args.basestyle or '') .. ';' .. (args.titlestyle or '') .. ';background:none transparent;border:none;box-shadow:none; padding:0;'
		})
	end

end

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

	local titleRow = tbl:tag('tr')

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

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

	if args.titlegroup then
		titleCell
			:addClass('navbox-title1')
	end

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

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

	renderNavBar(titleCell)

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

--
--   Above/Below rows
--

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

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

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

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

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

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

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

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

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

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

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

	local listCell = row:tag('td')

	if args['group' .. listnum] then
		listCell
			:addClass('navbox-list1')
	else
		listCell:attr('colspan', 2)
	end

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

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

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

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


--
--   Tracking categories
--

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

-- there are a lot of list classes in the wild, so we have a function to find
-- them and add their TemplateStyles
local function addListStyles()
	local frame = mw.getCurrentFrame()
	-- TODO?: Should maybe take a table of classes for e.g. hnum, hwrap as above
	-- I'm going to do the stupid thing first though
	-- Also not sure hnum and hwrap are going to live in the same TemplateStyles
	-- as hlist
	local function _addListStyles(htmlclass, templatestyles)
		local class_args = { -- rough order of probability of use
			'bodyclass', 'listclass', 'aboveclass', 'belowclass', 'titleclass',
			'navboxclass', 'groupclass', 'titlegroupclass', 'imageclass'
		}
		local patterns = {
			'^' .. htmlclass .. '$',
			'%s' .. htmlclass .. '$',
			'^' .. htmlclass .. '%s',
			'%s' .. htmlclass .. '%s'
		}
		
		local found = false
		for _, arg in ipairs(class_args) do
			for _, pattern in ipairs(patterns) do
				if mw.ustring.find(args[arg] or '', pattern) then
					found = true
					break
				end
			end
			if found then break end
		end
		if found then
			return frame:extensionTag{
				name = 'templatestyles', args = { src = templatestyles }
			}
		else
			return ''
		end
	end
	
	local hlist_styles = ''
	-- navbar always has mini = 1, so here (on this wiki) we can assume that
	-- we don't need to output hlist styles in navbox again.
	if not has_navbar() then
		hlist_styles = _addListStyles('hlist', 'Flatlist/styles.css')
	end
	local plainlist_styles = _addListStyles('plainlist', 'Plainlist/styles.css')
	
	return hlist_styles .. plainlist_styles
end

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

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

local function isIllegible()
	-- require('Module:Color contrast') absent on mediawiki.org
	return false
end

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

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

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

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

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

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

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

	return tbl
end

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

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

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

	-- render the main body of the navbox
	local tbl = renderMainTable()
	
	-- get templatestyles
	local frame = mw.getCurrentFrame()
	local base_templatestyles = frame:extensionTag{
		name = 'templatestyles', args = { src = 'Module:Navbox/styles.css' }
	}
	local templatestyles = ''
	if args.templatestyles and args.templatestyles ~= '' then
		templatestyles = frame:extensionTag{
			name = 'templatestyles', args = { src = args.templatestyles }
		}
	end
	
	local res = mw.html.create()
	-- 'navbox-styles' exists for two reasons:
	--  1. To wrap the styles to work around phab: T200206 more elegantly. Instead
	--	   of combinatorial rules, this ends up being linear number of CSS rules.
	--  2. To allow MobileFrontend to rip the styles out with 'nomobile' such that
	--     they are not dumped into the mobile view.
	res:tag('div')
		:addClass('navbox-styles')
		:addClass('nomobile')
		:wikitext(base_templatestyles .. templatestyles)
		:done()
	
	-- render the appropriate wrapper around the navbox, depending on the border param
	if border == 'none' then
		local nav = res:tag('div')
			:attr('role', 'navigation')
			:wikitext(addListStyles())
			:node(tbl)
		-- aria-labelledby title, otherwise above, otherwise lone group
		if args.title or args.above or (args.group1 and not args.group2) then
			nav:attr('aria-labelledby', mw.uri.anchorEncode(args.title or args.above or args.group1))
		else
			nav:attr('aria-label', 'Navbox')
		end
	elseif border == 'subgroup' then
		-- We assume that this navbox is being rendered in a list cell of a
		-- parent navbox, and is therefore inside a div with padding:0em 0.25em.
		-- We start with a </div> to avoid the padding being applied, and at the
		-- end add a <div> to balance out the parent's </div>
		res
			:wikitext('</div>')
			:wikitext(addListStyles())
			:node(tbl)
			:wikitext('<div>')
	else
		local nav = res:tag('div')
			:attr('role', 'navigation')
			:addClass('navbox')
			:addClass(args.navboxclass)
			:cssText(args.bodystyle)
			:cssText(args.style)
			:css('padding', '3px')
			:wikitext(addListStyles())
			:node(tbl)
		-- aria-labelledby title, otherwise above, otherwise lone group
		if args.title or args.above or (args.group1 and not args.group2) then
			nav:attr('aria-labelledby', mw.uri.anchorEncode(args.title or args.above or args.group1))
		else
			nav:attr('aria-label', 'Navbox')
		end
	end

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

	return striped(tostring(res))
end

function p.navbox(frame)
	if not getArgs then
		getArgs = require('Module:Arguments').getArgs
	end
	args = getArgs(frame, {wrappers = {'Template:Navbox', 'Template:Navbox subgroup'}})
	if frame.args.border then
		-- This allows Template:Navbox_subgroup to use {{#invoke:Navbox|navbox|border=...}}.
		args.border = frame.args.border
	end

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

	return p._navbox(args)
end

return p