×
Create a new article
Write your page title here:
We currently have 179 articles on NBITTRPG Wiki. Type your article name above or click on one of the titles below and start writing!



NBITTRPG Wiki
Revision as of 12:16, 3 March 2025 by CStatic (talk | contribs)
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)

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

Recent changes

  • Zaptrap • Friday at 22:20
  • Zaptrap • Friday at 22:18
  • Zaptrap • Friday at 22:15
  • Ember • Friday at 22:03
  • AndrewFBR • Friday at 00:24
  • AndrewFBR • Friday at 00:24
  • AndrewFBR • Friday at 00:24
  • AndrewFBR • Friday at 00:24