Module:Navbox

local p = {} local h = {}

local utilsArg = require("Module:UtilsArg") local utilsPage = require("Module:UtilsPage") local utilsTable = require("Module:UtilsTable")

local CATEGORY_INVALID_ARGS = "" local CATEGORY_NAVBOXES_ATTENTION = "Navigation templates needing attention" local CATEGORY_NAVBOXES_OTHER = "" local CATEGORY_NAVBOX_TEMPLATES = "Navbox templates"

local TITLE_IMG_SIZE = "28x28px" local DEFAULT_IMG_SIZE = "150x150px" local MAX_RECOMMENDED_PARTITION_SIZE = require("Module:Constants/number/maxNavboxPartitionSize")

function p.Main(frame) local args, err = utilsArg.parse(frame:getParent.args, p.Templates.Navbox) local errCategories = err and err.categoryText or "" if not args.title then if h.isManualNavboxPage then return "", errCategories.."" elseif h.isTemplate then return "", errCategories else return "" end end h.storeArgs(args) local navbox, categories = h.printNavbox(args) categories = categories..errCategories

if h.isManualNavboxPage then return navbox, categories.."", h.printReport elseif h.isNavboxPage then -- apply the styles that would be applied when this template is actually used navbox = tostring(mw.html.create("div")			:addClass("zw-categories__navboxes-category-list")			:wikitext(navbox)			:done		) local styles = frame:extensionTag({ 			name = "templatestyles", 			args = { src = "Template:Categories/Styles.css" }		}) navbox = styles..navbox return navbox, categories, h.printReport elseif h.isTemplate then return navbox, categories else return navbox end end

function h.printNavbox(args) local categories = "" -- MediaWiki:Gadget-Navbox.js automatically removes the "mw-collapsed" when there is only one navbox on the page. local navbox = mw.html.create("div") :addClass("zw-navbox mw-collapsible mw-collapsed") local id = args.id or args.title id = id and "navbox-"..string.gsub(string.lower(id), " ", "-") -- to kebab case which is the standard for IDs if id then navbox:attr("id", id) end local titleImages = args.titleImages or {} local navboxTitle = mw.html.create("div") :addClass("zw-navbox__title") if #titleImages > 0 then local leftImage = string.format("%s|link=", titleImages[1], TITLE_IMG_SIZE) navboxTitle:tag("span") :addClass("zw-navbox__title-image") :wikitext(leftImage) end navboxTitle:tag("span") :addClass("zw-navbox__title-text") :wikitext(args.title) if #titleImages > 0 then local rightImage = string.format("%s|link=", titleImages[2] or titleImages[1], TITLE_IMG_SIZE) navboxTitle:tag("span") :addClass("zw-navbox__title-image") :wikitext(rightImage) end local navboxContent = navbox :tag("div") :addClass("zw-navbox__header mw-collapsible-toggle") :tag("span") -- Used to center the heading - see Template:Navbox/Styles.css :addClass("zw-navbox__toggle-button-counterbalance") :done :tag("span") :addClass("zw-navbox__title") :node(navboxTitle) :done :tag("span") :addClass("zw-navbox__toggle-button") :tag("span") :addClass("zw-navbox__toggle-button-text mw-collapsible-text") :wikitext("hide ▲") :done :done :done :done :tag("div") :addClass("zw-navbox__content mw-collapsible-content") local body = navboxContent:tag("div") :addClass("zw-navbox__body") local rows = body:tag("div") :addClass("zw-navbox__rows") for i, row in ipairs(args.rows) do		if row.group == "Other" or row.group == "Miscellaneous" then categories = categories..CATEGORY_NAVBOXES_OTHER end h.storeGroupSize({			name = row.group or "Row "..i, 			size = #(row.links or {}),			maxGroupSize = row.maxGroupSize or MAX_RECOMMENDED_PARTITION_SIZE		}) if row.group then rows:tag("div") :addClass("zw-navbox__row-header") :tag("span") :addClass("zw-navbox__row-header-text") :wikitext(row.group) :done :done end local links = {} for j, link in ipairs(row.links or {}) do			link = h.link(link) table.insert(links, ''..link..' ') end

local evenOdd = (i % 2 == 0) and "even" or "odd" local groups = utilsTable.filter(args.rows, "group") local links = table.concat(links, " • ") rows:tag("div") :addClass("zw-navbox__row-links zw-navbox__row-links--"..evenOdd) :addClass(#groups == 0 and "zw-navbox__row-links--nogroups" or nil) :tag("div") :addClass("zw-navbox__row-links-content") :addClass(#groups == 0 and "zw-navbox__row-links-content--nogroups" or nil) :wikitext(links) :done :done end if args.image then local filename = args.image if not string.find(filename, "^File:") then filename = "File:"..filename end local thumbnail = string.format("%s", filename, DEFAULT_IMG_SIZE) body:tag("div") :addClass("zw-navbox__image") :wikitext(thumbnail) end if args.footer then navboxContent:tag("div") :addClass("zw-navbox__footer") :wikitext(args.footer) end local result = tostring(navboxContent:allDone) return result, categories end

-- Turns links like Link into Link -- Prevents navbox entries from appearing in the Special:WhatLinksHere of every other navbox entry -- Copied from Module:UtilsMarkup/Link for job queue optimization, plus some modifications function h.link(links) return string.gsub(links, "%[%^%+%]%]", h._link) end function h._link(link) if string.find(link, "^%[%[Category:") or string.find(link, "^%[%[File:") then return link end local linkParts = string.gsub(link, "^%[%[:?", "") linkParts = string.gsub(linkParts, "%]%]$", "") local pipe = string.find(linkParts, "|") local page = string.sub(linkParts, 1, pipe and pipe - 1) page = string.gsub(page, "&#44;", ",") -- unescape any commas that were escaped in input due to splitting by , page = string.gsub(page, "#.*", "") page = string.gsub(page, "%s*$", "") -- trim trailing whitespace h.storePage(page)

local display = pipe and string.sub(linkParts, pipe + 1) or page if page == mw.title.getCurrentTitle.fullText then return ""..display.."" else local url = mw.site.server.."/"..mw.uri.encode(page, "WIKI") return string.format(' [%s %s] ', url, display) end end

-- Store data for the report function below local VAR_ARGS = "navbox_args" local VAR_PAGES = "navbox_pages" local VAR_GROUP_SIZES = "navbox_group_size" function h.storePage(page) if h.isNavboxPage then local utilsVar = require("Module:UtilsVar") utilsVar.add(VAR_PAGES, page) end end function h.storeGroupSize(group) if h.isNavboxPage then local utilsVar = require("Module:UtilsVar") utilsVar.add(VAR_GROUP_SIZES, group) end end function h.storeArgs(args) if h.isNavboxPage then local utilsVar = require("Module:UtilsVar") utilsVar.set(VAR_ARGS, args) end end

-- Ensures that every page linked in the navbox uses that navbox -- and that every page that uses the navbox is linked in the navbox -- so that the navigation is bidirectional and doesn't have "dead ends" function h.printReport local utilsError = require("Module:UtilsError") local utilsLayout = require("Module:UtilsLayout") local utilsMarkup = require("Module:UtilsMarkup") local utilsTable = require("Module:UtilsTable") local utilsVar = require("Module:UtilsVar") local args = utilsVar.get(VAR_ARGS) or {} local pagesInNav = utilsVar.get(VAR_PAGES) or {} local groupSizes = utilsVar.get(VAR_GROUP_SIZES) or {} local pagesUsingNav = utilsPage.dpl({		uses = "Template:"..h.templatePage,		notnamespace = {"User", "Category"},		nottitlematch = "%/Documentation",		redirects = "include",	})

local missingLinks = utilsTable.difference(pagesUsingNav, pagesInNav) local missingUses = utilsTable.difference(pagesInNav, pagesUsingNav) local wantedPages = utilsTable.filter(pagesInNav, function(page)		return not utilsPage.exists(page)	end) local redirects = h.reportRedirects(pagesInNav) local bigGroups = utilsTable.filter(groupSizes, function(group)		return group.size > group.maxGroupSize	end) local categoryName, missingPages, extraPages = h.reportCategoryMismatches(pagesInNav) local issues = "" if categoryName and args.title and categoryName ~= args.title then issues = issues .."\n====Title Mismatch====\n" ..string.format(" The navbox title should be  rather than  . ", categoryName, args.title) .." For navboxes added by Template:Categories, the navbox title must match the category name, as navboxes are listed alphabetically by category name. " end if #wantedPages > 0 then issues = issues.."\n====Red Links====\n" wantedPages = utilsTable.map(wantedPages, utilsMarkup.link) issues = issues.."The above navbox contains links to pages which do not exist. Please create these pages or remove them from the navbox:" issues = issues..utilsMarkup.bulletList(wantedPages) end if #redirects > 0 then issues = issues.."\n====Redirect Links====\n" redirects = utilsTable.map(redirects, utilsMarkup.link) issues = issues.."The above navbox contains links to redirects. Please create an article to replace the redirect, or update the link to refer to the redirect target." issues = issues..utilsMarkup.bulletList(redirects) end if #missingLinks > 0 then issues = issues.."\n====Missing Links====\n" missingLinks = utilsTable.map(missingLinks, utilsMarkup.link) issues = issues.." The above navbox is missing the following links to articles that use it. Please add these articles to the navbox to ensure that the navigation does not have dead ends. " issues = issues..utilsMarkup.bulletList(missingLinks) end if #missingUses > 0 and h.isManualNavboxPage then -- missing uses is impossible if it's a category-based navbox and Template:Category is used everywhere issues = issues.."\n====Missing Uses====\n" missingUses = utilsTable.map(missingUses, utilsMarkup.link) issues = issues.."The above navbox is missing from the following articles. Please add  to these articles to ensure that the navigation does not have dead ends." issues = issues..utilsMarkup.bulletList(missingUses) end if #bigGroups > 0 then issues = issues.."\n====Big Groups====\n" local dataRows = utilsTable.map(bigGroups, function(group)			return {group.name, group.size, group.maxGroupSize}		end) bigGroups = utilsLayout.table({			headers = {"Row", "Size", "Max Size"},			rows = dataRows,		}) issues = issues.."The following navbox rows exceed their recommended maximum size. Please subdivide these rows if possible." issues = issues..bigGroups issues = issues.."If you are certain that a row should not be subdivided despite its size, you can increase the  of the row. See Template:Categories/Navbox for more information." end if #extraPages > 0 or #missingPages > 0 then issues = issues.."\n====Category Mismatch====\n" if #missingPages > 0 then missingPages = utilsTable.map(missingPages, utilsMarkup.link) issues = issues .."This navbox is missing entries from Category:"..categoryName..":" ..utilsMarkup.bulletList(missingPages) end if #extraPages > 0 then extraPages = utilsTable.map(extraPages, utilsMarkup.link) issues = issues .."This navbox contains links to pages that are not in Category:"..categoryName..":" ..utilsMarkup.bulletList(extraPages) end issues = issues.."Usually should be used instead of  for category-based navboxes." end local report = "\n==Report==\n" if issues ~= "" then report = " "..report -- fixes the lack of space between the navbox and the toc beneath it		report = report .."\n===Issues===\n" ..issues ..""	else report = report.." No issues have been detected in this navbox. " end return mw.getCurrentFrame:preprocess(report) end function h.reportRedirects(pagesInNav) local utilsTable = require("Module:UtilsTable")

return utilsTable.filter(pagesInNav, function(page)		-- mw.title is more reliable than utilsPage for checking redirects		-- but mw.title uses an expensive parser function while utilsPage does not		-- we cannot "afford" the expensive parser function when there are too many pages in the nav		if #pagesInNav > 90 then			return utilsPage.isRedirect(page)		else			return mw.title.new(page).isRedirect		end	end) end function h.reportCategoryMismatches(pagesInNav) local utilsTable = require("Module:UtilsTable")

if h.isManualNavboxPage then return nil, {}, {} end local usingCategoryNavbox = utilsPage.dpl({		uses = "Template:Categories/Navbox",		skipthispage = "no",		titlematch = h.templatePage,	}) if #usingCategoryNavbox > 0 then return nil, {}, {} end local title = mw.title.getCurrentTitle local category = title.subpageText if title.subpageText == "Documentation" then category = title.basePageTitle.subpageText end local pagesInCategory = utilsPage.dpl({ category = category, namespace = "" }) local missingPages = utilsTable.difference(pagesInCategory, pagesInNav) local extraPages = utilsTable.difference(pagesInNav, pagesInCategory) return category, missingPages, extraPages end

local title = mw.title.getCurrentTitle function h.isTemplate return title.nsText == "Template" end function h.isNavboxPage return h.isTemplate and title.rootText ~= "Navbox" and title.baseText ~= "Categories" and title.text ~= "Categories/Navbox/Documentation" and not h.isCurrentPageInNavbox -- to prevent false positives on template pages that use navboxes, e.g. Template:Term for } end function h.isCurrentPageInNavbox local utilsTable = require("Module:UtilsTable") local utilsVar = require("Module:UtilsVar") local pagesInNav = utilsVar.get(VAR_PAGES) or {} return utilsTable.includes(pagesInNav, title.basePageTitle.fullText) end function h.isManualNavboxPage -- to distinguish manually curated navboxes from automated ones return h.isNavboxPage and title.baseText ~= "Categories/Navbox" end function h.templatePage local templatePage = title.text if string.find("/Documentation$", templatePage) then templatePage = title.baseText end return templatePage end

p.Templates = { ["Navbox"] = { format = "block", purpose = "Creates navbox templates.", categories = {"Metatemplates"}, paramOrder = {"id", "title", "titleImages", "image", "group", "links", "maxGroupSize", "footer"}, repeatedGroup = { name = "rows", params = {"group", "links", "maxGroupSize"}, counts = {2, 3, 4, 5, 6, 7}, },		boilerplate = { separateRequiredParams = false, hiddenParams = {"maxGroupSize", "type"}, },		params = { id = { type = "string", desc = "A unique ID for the navbox. Sets the id HTML attribute so that the navbox can be linked to. Defaults to the navbox title.", trim = true, nilIfEmpty = true, },			title = { required = true, type = "content", desc = " The navbox title. It is recommended not to place links in the title as this can create confusion between the clickable navbox header and the link within it. Category links should be placed in the footer. ", trim = true, nilIfEmpty = true, },			titleImages = { type = "string", desc = "Two file names with the  prefix, separated by a comma. Renders image to the left and right of the navbox title.", trim = true, nilIfEmpty = true, split = true, },			image = { type = "wiki-file-name", desc = "A file name, with the  prefix. Renders an image in the navbox body.", trim = true, nilIfEmpty = true, },			group = { type = "string", desc = "A header for the given row in the navbox. Required if there is more than one row.", trim = true, nilIfEmpty = true, },			links = { type = "content", required = true, desc = "A comma-separated list of links for the given row.", trim = true, nilIfEmpty = true, split = true, },			maxGroupSize = { type = "number", nilIfEmpty = true, default = MAX_RECOMMENDED_PARTITION_SIZE, desc = "Adds the template to Category:"..CATEGORY_NAVBOXES_ATTENTION.." if the number of links in the group exceeds this number. In most cases this value should not be set higher than its default; navboxes with too many links per group can be difficult to read.", },			footer = { type = "content", desc = "The navbox footer. Usually contains links to relevant categories.", trim = true, nilIfEmpty = true, }		},	}, }

return p