Documentation for this module may be created at Module:Navbox/doc
--------------------------------------------------------------------
--<pre> Navbox Module
--
-- * Fully CSS styled (inline styles possible but not default)
-- * Supports unlimited rows
-- * Automatic striping alternation, including for child navbox subsections
--
-- Original module by User:Tjcool007 from layton.fandom.com
--------------------------------------------------------------------
local p = {}
local i18n = require('Module:I18n').loadMessages('Navbox')
local showText, hideText = i18n:msg('show'), i18n:msg('hide')
local cfg = {
marker = {
oddeven = '\127_ODDEVEN_\127',
contains_subgroup = '\127_ODDEVEN0_\127',
restart_header = '\127_ODDEVEN1_\127',
regex_changer = '\127_ODDEVEN([01]?)_\127',
odd = '\127_ODD_\127',
even = '\127_EVEN_\127',
oddeven_sep = '\127_ODDEVEN2_\127',
oddeven_end = '\127_ODDEVEN3_\127',
regex_oddeven_sep = '\127_ODDEVEN[23]_\127',
regex_odd = '(\127_ODD_\127[^\127]*)\127_ODDEVEN2_\127([^\127]*)\127_ODDEVEN2_\127[^\127]*\127_ODDEVEN3_\127',
regex_even = '(\127_EVEN_\127[^\127]*)\127_ODDEVEN2_\127[^\127]*\127_ODDEVEN2_\127([^\127]*)\127_ODDEVEN3_\127'
},
pattern = {
hlist = 'hlist',
plainlist = 'plainlist',
},
category = {
error = '[[Category:Navboxes with internal errors]]',
orphan = '[[Category:Navbox orphans]]'
}
}
------------------------------------------------
-- ARGUMENTS PREPROCESSOR
-- * Extracts arguments from frame and stores them in args table
-- * At the same time, checks for valid row numbers
------------------------------------------------
--- Preprocessor for the arguments.
-- Will fill up the args table with the parameters from the frame grouped by their type.
--
-- @param frame The frame passed to the Module.
local function preProcessArgs(frame)
local tmp, args, rownums, skiprows = {}, {}, {}, {}
if frame == mw.getCurrentFrame() then
tmp = frame:getParent().args
else
tmp = frame
end
-- Storage tables
local nums, subnums = {}, {}
-- Loop over all the args
for k, v in pairs(tmp) do
-- Skip empty args, which are useless
if v ~= '' then
local str = tostring(k)
local cat, num, subnum = str:match('^(%a+)([1-9]%d*)%.([1-9][%d%.]*)$')
if subnum then
nums[num] = true
if type(subnums[num]) ~= "table" then
subnums[num] = {}
end
subnums[num][cat..subnum] = v
else
cat, num = str:match('^(%a+)([1-9]%d*)$')
if cat == 'header' or cat == 'list' then
nums[num] = true
end
args[k] = v -- Simple copy
end
end
end
-- Generate child navboxes
for k, v in pairs(subnums) do
for _, arg in ipairs({'showall', 'alternaterows', 'titlestyle', 'titleclass', 'abovestyle', 'aboveclass', 'rowclass', 'altrowclass', 'groupstyle', 'groupclass', 'altgroupstyle', 'altgroupclass', 'liststyle', 'listclass', 'altliststyle', 'altlistclass', 'belowstyle', 'belowclass', 'defaultstate', 'defaultcollapsetext', 'defaultexpandtext'}) do
v[arg] = args[arg]
end
v.border = 'child'
args['list'..k] = p.main(v)
end
for k, v in pairs(nums) do
rownums[#rownums+1] = tonumber(k)
end
table.sort(rownums)
-- Calculate skip rows
local cSection, cSkip
local showall = args.showall
for i=1,#rownums do
local num = rownums[i]
if args['header'..num] then
cSection = true
cSkip = false
local showme = args['show'..num]
if showme == 'no' then
cSkip = true
elseif showme == 'auto' or (showme ~= 'yes' and showall ~= 'yes') then
if not args['list'..num] then
local nextNum = rownums[i+1]
cSkip = not nextNum or args['header'..nextNum] -- If next has a header -> skip
end
end
end
if cSection and cSkip then
skiprows[num] = true
end
end
-- Insert markers into class and style parameters where needed
for i, v in ipairs({'groupclass', 'groupstyle', 'listclass', 'liststyle'}) do
if args['alt' .. v] then
args[v] = cfg.marker.oddeven_sep .. (args[v] or '') .. cfg.marker.oddeven_sep .. args['alt' .. v] .. cfg.marker.oddeven_end
end
end
return args, rownums, skiprows
end
------------------------------------------------
-- MAIN FUNCTIONS
------------------------------------------------
--- Processes the arguments to create the navbox.
--
-- @return A string with HTML that is the navbox.
local function _navbox(args, rownums, skiprows)
local navbox -- Actual navbox
local hasrows, hasData, hasTitle, hasChild = false, false, false, false
local activeSection, sections, cimage, cimageleft
local border = args.border or mw.text.trim(args[1] or '') or ''
if border == 'child' then border = 'subgroup' end
local isChild = (border == 'subgroup')
local colspan = args.image and 3 or 2
if args.imageleft then colspan = colspan + 1 end
local rowspan = 0
--- Wraps text in newlines if it begins with a list or table
local function processItem(item)
if item:sub(1, 2) == '{|' then
return '\n' .. item ..'\n'
end
if item:match('^[*:;#]') then
return '\n' .. item ..'\n'
end
return item
end
--- Returns navbox contents with markers replaced for odd/even striping
local function striped(wikitext, border)
-- 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 == 'subgroup' and args.orphan ~= 'yes' then
-- No change; striping occurs in outermost navbox.
return wikitext .. orphanCat
end
local altrowclass = args.altrowclass or 'alt'
altrowclass = ' ' .. altrowclass
local first, second = '', altrowclass
if args.alternaterows then
if args.alternaterows == 'swap' then
first, second = second, first
elseif args.alternaterows == 'no' or args.alternaterows == 'odd' then
second = ''
elseif args.alternaterows == 'even' then
first = altrowclass
else
first = args.alternaterows
second = first
end
end
local changer
if first == second then
changer = cfg.marker.odd
else
local index = 0
changer = function (code)
if code == '0' then
-- Subgroup encountered.
-- Return a marker without incrementing the index.
return index % 2 == 0 and cfg.marker.odd or cfg.marker.even
elseif code == '1' then
-- Title or header encountered.
-- Restart the index and remove the marker.
-- The next occurrence will use the initial class.
index = 0
return ''
end
index = index + 1
return index % 2 == 1 and cfg.marker.odd or cfg.marker.even
end
end
local regex = orphanCat:gsub('([%[%]])', '%%%1')
wikitext = wikitext:gsub(regex, ''):gsub(cfg.marker.regex_changer, changer)
local prevWikitext, hasNoMoreOddevenMarkers
repeat
prevWikitext = wikitext
wikitext = wikitext:gsub(cfg.marker.regex_odd, '%1%2'):gsub(cfg.marker.regex_even, '%1%2')
hasNoMoreOddevenMarkers = wikitext:match(cfg.marker.regex_oddeven_sep) == nil
until hasNoMoreOddevenMarkers or wikitext == prevWikitext
wikitext = wikitext:gsub(cfg.marker.odd, first):gsub(cfg.marker.even, second)
if not hasNoMoreOddevenMarkers then
-- Something went wrong. Flag for investigation.
-- See <https://discord.com/channels/246075715714416641/1266361437484486778> for details.
wikitext = wikitext .. cfg.category.error
end
return wikitext
end
------------------------------------------------
-- Title
------------------------------------------------
--- Processes the VDE links in the title
--
-- @param titlecell The table cell of the title
local function processVde( titlecell )
if not args.template then return end
titlecell:wikitext('<span class="navbox-vde">'
.. mw.getCurrentFrame():expandTemplate({
title = 'vdelinks',
args = { args.template, ['type'] = 'navbox' }
}) .. '</span>')
end
--- Processes the main title row
local function processTitle()
if not hasTitle then return end
local titlerow = mw.html.create('tr'):addClass('navbox-title'..cfg.marker.restart_header)
local titlecell = mw.html.create('th'):attr('colspan',colspan):attr('scope','col')
local titlediv = mw.html.create('div')
if not pcall( processVde, titlecell ) then
titlediv:wikitext( '<b class="navbox-vde error" title="Missing Template:Vdelinks">!!!</b>' )
end
titlediv:wikitext( args.title or '{{{title}}}' )
-- Padding
local hasTemplate = args.template ~= nil
local hasState = not args.state or args.state ~= 'plain'
if hasTemplate or hasState then
-- remove redundant classes in future if it won't break the display of wikis' navboxes
titlediv:addClass('navbox-title-pad navbox-title-padleft navbox-title-padright')
end
if args.titleclass then titlerow:addClass( args.titleclass ) end
if args.titlestyle then titlecell:cssText( args.titlestyle ) end
titlecell:node(titlediv)
titlerow:node(titlecell)
navbox:node(titlerow)
end
local function _addGutter( parent, incRowspan )
parent:tag('tr'):addClass('navbox-gutter'):tag('td'):attr('colspan',2)
if incRowspan then
rowspan = rowspan + 1
end
end
------------------------------------------------
-- Above/Below
------------------------------------------------
--- Processes the above and below rows
--
-- @param rowtype Either 'above' or 'below'
local function processAboveBelow( rowtype )
if not args[rowtype] then return end
local abrow = mw.html.create('tr'):addClass('navbox-'..rowtype)
local abcell = mw.html.create('td'):attr('colspan',colspan):wikitext( processItem(args[rowtype]) )
if args[rowtype .. 'class'] then abrow:addClass( args[rowtype .. 'class'] ) end
if args[rowtype .. 'style'] then abcell:cssText( args[rowtype .. 'style'] ) end
abrow:node( abcell )
if hasTitle or rowtype == 'below' then
_addGutter( navbox )
end
navbox:node( abrow )
end
------------------------------------------------
-- Main Rows
------------------------------------------------
--- Processes the images
local function _processImage(row, imgtype)
if not args[imgtype] then return end
local iclass = imgtype == 'image' and 'navbox-image-right' or 'navbox-image-left'
local imagecell = mw.html.create('td'):addClass('navbox-image'):addClass(iclass)
local image = args[imgtype]
if image:sub(1,1) ~= '[' then
local width = args[imgtype .. 'width'] or '100px'
imagecell:css('width',width):wikitext('['..'[' .. image .. '|' .. width .. '|link=' .. (args[imgtype .. 'link'] or '') .. ']]')
else
imagecell:css('width','0%'):wikitext(image)
end
if args[imgtype .. 'class'] then imagecell:addClass( args[imgtype .. 'class'] ) end
if args[imgtype .. 'style'] then imagecell:cssText( args[imgtype .. 'style'] ) end
row:node( imagecell )
if imgtype == 'image' then
cimage = imagecell
else
cimageleft = imagecell
end
end
--- Closes the currently active section (if any)
local function _closeCurrentSection()
if not activeSection then return end
local row = mw.html.create('tr'):addClass('navbox-section-row')
local cell = mw.html.create('td'):attr('colspan',2)
if not hasrows then
_processImage(row,'imageleft')
end
cell:node(sections[activeSection])
row:node(cell)
local firstRow = false
if not hasrows then
firstRow = true
hasrows = true
_processImage(row,'image')
end
if not isChild or not firstRow or hasTitle or args.above then
_addGutter(navbox,not firstRow)
end
navbox:node(row)
rowspan = rowspan + 1
activeSection = false
hasData = false
end
--- Process a single Header "row"
--
-- @param num Number of the row to be processed
local function processHeader(num)
if not args['header'..num] then return end
_closeCurrentSection()
local subtable = mw.html.create('table'):addClass('navbox-section')
local headerrow = mw.html.create('tr')
local header = mw.html.create('th'):addClass('navbox-header'..cfg.marker.restart_header):attr('colspan',2):attr('scope','col')
local headerdiv = mw.html.create('div'):wikitext( args['header'..num] )
local collapseme = args['state'..num] or false
local state = false
if collapseme then
-- Look at this one
if collapseme ~= 'plain' then
state = collapseme == 'expanded' and 'expanded' or 'collapsed'
end
else
-- Look at default
local collapseall = args.defaultstate or false
if collapseall then
state = collapseall == 'expanded' and 'expanded' or 'collapsed'
end
end
if state then
subtable:addClass('mw-collapsible'):attr('data-expandtext',args['expandtext'..num] or args['defaultexpandtext'] or showText):attr('data-collapsetext',args['collapsetext'..num] or args['defaultcollapsetext'] or hideText)
if state == 'collapsed' then
subtable:addClass('mw-collapsed')
end
-- remove redundant classes in future if it won't break the display of wikis' navboxes
headerdiv:addClass('navbox-header-pad navbox-title-padleft navbox-title-padright')
end
if args.headerclass then headerrow:addClass( args.headerclass ) end
if args.headerstyle then header:cssText( args.headerstyle ) end
header:node(headerdiv)
headerrow:node(header)
subtable:node(headerrow)
sections[num] = subtable
activeSection = num
end
-- Check if a string contains a particular CSS class
local function hasListClass(htmlclass, arg)
local patterns = {
'^' .. htmlclass .. '$',
'%s' .. htmlclass .. '$',
'^' .. htmlclass .. '%s',
'%s' .. htmlclass .. '%s'
}
for _, pattern in ipairs(patterns) do
if mw.ustring.find(args[arg] or '', pattern) then
return true
end
end
return false
end
--- Processes a single list row
--
-- @param num Number of the row to be processed
local function processList(num)
if not args['list'..num] then return end
local oddEven = cfg.marker.oddeven
local data = args['list'..num]
hasChild = false
if data:sub(1, 12) == '</div><table' then
hasChild = true
oddEven = cfg.marker.contains_subgroup
end
local row = mw.html.create('tr'):addClass('navbox-row'..oddEven)
if not hasrows and not activeSection then
_processImage(row, 'imageleft')
end
local listcell = mw.html.create('td'):addClass('navbox-list')
local hlistcell = listcell:tag('div')
-- Only add hlist class if listclass parameter does not contain hlist or plainlist
if not (args.listclass and (hasListClass(cfg.pattern.hlist, 'listclass') or hasListClass(cfg.pattern.plainlist, 'listclass'))) then
hlistcell:addClass('hlist')
end
hlistcell:wikitext( processItem(data) )
if args.rowclass then row:addClass( args.rowclass ) end
if args.listclass then listcell:addClass( args.listclass ) end
if args.liststyle then listcell:cssText( args.liststyle ) end
if hasChild then
listcell:cssText('background: none')
end
if args['group'..num] then
local groupcell = mw.html.create('th'):addClass('navbox-group'):attr('scope','row'):wikitext( args['group'..num] )
if args.groupclass then groupcell:addClass( args.groupclass ) end
if args.groupstyle then groupcell:cssText( args.groupstyle ) end
row:node( groupcell )
else
listcell:attr('colspan',2)
if not hasChild then
listcell:addClass('no-group')
end
end
row:node( listcell )
local firstRow = false
if not hasrows and not activeSection then
firstRow = true
hasrows = true
_processImage(row, 'image')
end
if activeSection then
local parent = sections[activeSection]
if not isChild or not firstRow then
_addGutter(parent)
end
parent:node(row)
hasData = true
else
if not isChild or not firstRow or hasTitle or args.above then
_addGutter(navbox,not firstRow)
end
navbox:node( row )
rowspan = rowspan + 1
end
end
--- Processes all rows
local function processRows()
sections = {}
for i=1,#rownums do
local num = rownums[i]
if not skiprows[num] then
processHeader(num)
processList(num)
end
end
_closeCurrentSection()
if cimageleft then
cimageleft:attr('rowspan',rowspan)
end
if cimage then
cimage:attr('rowspan',rowspan)
end
end
-- Create the root HTML element
if isChild then
navbox = mw.html.create('table'):addClass('navbox-subgroup')
else
navbox = mw.html.create('table'):addClass('navbox')
end
if not isChild or args.title then
hasTitle = true
end
if hasTitle and isChild then
navbox:addClass('navbox-subgroup-with-title')
end
if hasTitle and args.state ~= 'plain' then
navbox:addClass('mw-collapsible'):attr('data-expandtext',args['expandtext'] or args['defaultexpandtext'] or showText):attr('data-collapsetext',args['collapsetext'] or args['defaultcollapsetext'] or hideText)
if args.state == 'collapsed' then
navbox:addClass('mw-collapsed')
end
end
if args.bodyclass then navbox:addClass(args.bodyclass) end
if args.bodystyle then navbox:cssText(args.bodystyle) end
-- Process...
processTitle()
processAboveBelow('above')
processRows()
processAboveBelow('below')
if not isChild then
return striped(tostring(navbox), border)
else
local wrapper = mw.html.create('')
wrapper:wikitext('</div>')
wrapper:node(navbox)
wrapper:wikitext('<div class="hlist">')
return striped(tostring(wrapper), border)
end
end
--- Main module entry point.
-- To be called with {{#invoke:navbox|main}} or directly from another module.
--
-- @param frame The frame passed to the module via the #invoke. If called from another
-- module directly, this should be a table with the parameter definition.
function p.main(frame)
return _navbox(preProcessArgs(frame))
end
return p
