Module:Navbox: Difference between revisions

From Zelda Wiki, the Zelda encyclopedia
Jump to navigation Jump to search
No edit summary
No edit summary
 
(36 intermediate revisions by 2 users not shown)
Line 3: Line 3:


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


local CATEGORY_INVALID_ARGS = "[[Category:"..require("Module:Constants/category/invalidArgs").."]]"
local CATEGORY_INVALID_ARGS = "[[Category:"..require("Module:Constants/category/invalidArgs").."]]"
local CLASS_PIXEL_ART = require("Module:Constants/class/pixelArt")
local CATEGORY_NAVBOXES_ATTENTION = "Navigation templates needing attention"
local CATEGORY_NAVBOXES_BIG_GROUPS = "[[Category:Navboxes with big groups]]"
local CATEGORY_NAVBOXES_OTHER = "[[Category:Navboxes with other]]"
local CATEGORY_NAVBOX_TEMPLATES = "Navbox templates"
 
local TITLE_IMG_SIZE = "28x28px"
local DEFAULT_IMG_SIZE = "150x150px"
local DEFAULT_IMG_SIZE = "150x150px"
local MAX_RECOMMENDED_GROUP_SIZE = 16
local MAX_RECOMMENDED_PARTITION_SIZE = require("Module:Constants/number/maxNavboxPartitionSize")
 
local title = mw.title.getCurrentTitle()
local isTemplate = title.nsText == "Template"
local isNavboxPage = isTemplate and title.rootText ~= "Navbox" and title.baseText ~= "Categories" and title.text ~= "Categories/Navbox/Documentation"
local isManualNavboxPage = isNavboxPage and title.baseText ~= "Categories/Navbox" -- to distinguish manually curated navboxes from automated ones


function p.Main(frame)
function p.Main(frame)
Line 19: Line 20:
if not args.title then
if not args.title then
if isManualNavboxPage then
if h.isManualNavboxPage() then
return "", errCategories.."[[Category:Navbox Templates]]"
return "", errCategories.."[[Category:"..CATEGORY_NAVBOX_TEMPLATES.."]]"
elseif isTemplate then
elseif h.isTemplate() then
return "", errCategories
return "", errCategories
else
else
Line 27: Line 28:
end
end
end
end
h.storeArgs(args)
local navbox, categories = h.printNavbox(args)
local navbox, categories = h.printNavbox(args)
categories = categories..errCategories
categories = categories..errCategories


if isManualNavboxPage then
if h.isManualNavboxPage() then
return navbox, categories.."[[Category:Navbox Templates]]", h.printReport()
return navbox, categories.."[[Category:"..CATEGORY_NAVBOX_TEMPLATES.."]]", h.printReport()
elseif isNavboxPage then
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()
return navbox, categories, h.printReport()
elseif isTemplate then
elseif h.isTemplate() then
return navbox, categories
return navbox, categories
else
else
Line 44: Line 57:
function h.printNavbox(args)
function h.printNavbox(args)
local categories = ""
local categories = ""
-- [[MediaWiki:Gadget-Site.js]] automatically removes the "mw-collapsed" when there is only one navbox on the page.
-- [[MediaWiki:Gadget-Navbox.js]] automatically removes the "mw-collapsed" when there is only one navbox on the page.
local navbox = mw.html.create("div")
local navbox = mw.html.create("div")
-- MediaWiki (Timeless?) automatically removes elements with class "navbox" on mobile
-- we _want_ to show navboxes on mobile since we've made them mobile friendly, so we use a different class name
:addClass("zw-navbox mw-collapsible mw-collapsed")
:addClass("zw-navbox mw-collapsible mw-collapsed")
local id = args.id and "navbox-"..string.gsub(string.lower(args.id), " ", "-") -- to kebab case which is the standard for IDs
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
if id then
navbox:attr("id", id)
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|%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|%s|link=]]", titleImages[2] or titleImages[1], TITLE_IMG_SIZE)
navboxTitle:tag("span")
:addClass("zw-navbox__title-image")
:wikitext(rightImage)
end
end
Line 64: Line 95:
:tag("span")
:tag("span")
:addClass("zw-navbox__title")
:addClass("zw-navbox__title")
:wikitext(args.title)
:node(navboxTitle)
:done()
:done()
:tag("span")
:tag("span")
Line 83: Line 114:
:addClass("zw-navbox__rows")
:addClass("zw-navbox__rows")
for i, row in ipairs(args.rows) do
for i, row in ipairs(args.rows) do
h.storeGroupSize(row.group or "Row "..i, #(row.links or {}))
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
if row.group then
rows:tag("div")
rows:tag("div")
:addClass("zw-navbox__row-header")
:addClass("zw-navbox__row-header")
:wikitext(row.group)
:tag("span")
:addClass("zw-navbox__row-header-text")
:wikitext(row.group)
:done()
:done()
:done()
elseif i ~= 1 or #args.rows > 1 then
local utilsError = require("Module:UtilsError")
utilsError.warn(string.format("<code>group%d</code> parameter is required when there is more than one group.", i))
categories = categories..CATEGORY_INVALID_ARGS
end
end
local links = {}
local links = {}
for j, link in ipairs(row.links or {}) do
for j, link in ipairs(row.links or {}) do
link = h.killBacklinks(link)
link = h.link(link)
table.insert(links, '<span class="zw-navbox__link">'..link..'</span>')
table.insert(links, '<span class="zw-navbox__link">'..link..'</span>')
end
end


local evenOdd = (i % 2 == 0) and "even" or "odd"
local evenOdd = (i % 2 == 0) and "even" or "odd"
local rowModifiers = " zw-navbox__row-links--"..evenOdd
if #args.rows == 1 and not args.rows[1].group then
rowModifiers = rowModifiers.." zw-navbox__row-links--nogroups"
end
local links = table.concat(links, "&nbsp;• ")
local links = table.concat(links, "&nbsp;• ")
rows:tag("div")
rows:tag("div")
:addClass("zw-navbox__row-links"..rowModifiers)
:addClass("zw-navbox__row-links zw-navbox__row-links--"..evenOdd)
:addClass(not row.group and "zw-navbox__row-links--nogroups" or nil)
:tag("div")
:tag("div")
:addClass("zw-navbox__row-links-content")
:addClass("zw-navbox__row-links-content")
:addClass(not row.group and "zw-navbox__row-links-content--nogroups" or nil)
:wikitext(links)
:wikitext(links)
:done()
:done()
Line 124: Line 159:
local thumbnail = string.format("[[%s|%s]]", filename, DEFAULT_IMG_SIZE)
local thumbnail = string.format("[[%s|%s]]", filename, DEFAULT_IMG_SIZE)
body:tag("div")
body:tag("div")
:addClass("zw-navbox__image "..CLASS_PIXEL_ART)
:addClass("zw-navbox__image")
:wikitext(thumbnail)
:wikitext(thumbnail)
end
end
Line 140: Line 175:
-- Prevents navbox entries from appearing in the Special:WhatLinksHere of every other navbox entry
-- 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
-- Copied from Module:UtilsMarkup/Link for job queue optimization, plus some modifications
function h.killBacklinks(links)
function h.link(links)
return string.gsub(links, "%[%[[^%]]+%]%]", h.killBacklink)
return string.gsub(links, "%[%[[^%]]+%]%]", h._link)
end
end
function h.killBacklink(link)
function h._link(link)
if string.find(link, "^%[%[Category:") or string.find(link, "^%[%[File:") then
return link
end
local linkParts = string.gsub(link, "^%[%[:?", "")
local linkParts = string.gsub(link, "^%[%[:?", "")
linkParts = string.gsub(linkParts, "%]%]$", "")
linkParts = string.gsub(linkParts, "%]%]$", "")
local pipe = string.find(linkParts, "|")
local pipe = string.find(linkParts, "|")
local page = string.sub(linkParts, 1, pipe and pipe - 1)
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)
h.storePage(page)
local display = pipe and string.sub(linkParts, pipe + 1)
 
local display = pipe and string.sub(linkParts, pipe + 1) or page
local url = mw.site.server.."/"..mw.uri.encode(page, "WIKI")
if page == mw.title.getCurrentTitle().fullText then
return string.format('<span class="plainlinks">[%s %s]</span>', url, display or page)
return "<b>"..display.."</b>"
else
local url = mw.site.server.."/wiki/"..mw.uri.encode(page, "WIKI")
return string.format('<span class="plainlinks">[%s %s]</span>', url, display)
end
end
end


-- Store data for the report function below
-- Store data for the report function below
local VAR_ARGS = "navbox_args"
local VAR_PAGES = "navbox_pages"
local VAR_PAGES = "navbox_pages"
local VAR_GROUP_SIZES = "navbox_group_size"
local VAR_GROUP_SIZES = "navbox_group_size"
function h.storePage(page)
function h.storePage(page)
page = string.gsub(page, "#.*", "")
if h.isNavboxPage() then
if isNavboxPage then
local utilsVar = require("Module:UtilsVar")
local utilsVar = require("Module:UtilsVar")
utilsVar.add(VAR_PAGES, page)
utilsVar.add(VAR_PAGES, page)
end
end
end
end
function h.storeGroupSize(group, size)
function h.storeGroupSize(group)
if isNavboxPage then
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")
local utilsVar = require("Module:UtilsVar")
utilsVar.add(VAR_GROUP_SIZES, { name = group, size = size})
utilsVar.set(VAR_ARGS, args)
end
end
end
end
Line 177: Line 229:
function h.printReport()
function h.printReport()
local utilsError = require("Module:UtilsError")
local utilsError = require("Module:UtilsError")
local utilsLayout = require("Module:UtilsLayout")
local utilsMarkup = require("Module:UtilsMarkup")
local utilsMarkup = require("Module:UtilsMarkup")
local utilsPage = require("Module:UtilsPage")
local utilsTable = require("Module:UtilsTable")
local utilsTable = require("Module:UtilsTable")
local utilsVar = require("Module:UtilsVar")
local utilsVar = require("Module:UtilsVar")
local args = utilsVar.get(VAR_ARGS) or {}
local pagesInNav = utilsVar.get(VAR_PAGES) or {}
local pagesInNav = utilsVar.get(VAR_PAGES) or {}
local groupSizes = utilsVar.get(VAR_GROUP_SIZES) or {}
local groupSizes = utilsVar.get(VAR_GROUP_SIZES) or {}
local templatePage = title.text
if string.find("/Documentation$", templatePage) then
templatePage = title.baseText
end
local pagesUsingNav = utilsPage.dpl({
local pagesUsingNav = utilsPage.dpl({
uses = "Template:"..templatePage,
uses = "Template:"..h.templatePage(),
notnamespace = "User"
notnamespace = {"User", "Category"},
})
nottitlematch = {
local usingCategoryNavbox = utilsPage.dpl({
"%/Documentation",  
uses = "Template:Categories/Navbox",
mw.title.getCurrentTitle().prefixedText ~= "Template:Guidelines Nav" and "Categories" or nil  -- To avoid counting examples at [[Template:Categories]] as uses (but we do want to count [[Template:Guidelines Nav]])
skipthispage = "no",
},
titlematch = templatePage,
})
})
local isUsingCategoryNavbox = #usingCategoryNavbox > 0


local missingPages = utilsTable.difference(pagesUsingNav, pagesInNav)
local missingLinks = utilsTable.difference(pagesUsingNav, pagesInNav)
local missingUses = utilsTable.difference(pagesInNav, pagesUsingNav)
local missingUses = utilsTable.difference(pagesInNav, pagesUsingNav)
local redirects = h.reportRedirects(pagesInNav)
missingLinks = utilsTable.difference(missingLinks, redirects)
missingUses = utilsTable.difference(missingUses, redirects)
local wantedPages = utilsTable.filter(pagesInNav, function(page)
local wantedPages = utilsTable.filter(pagesInNav, function(page)
return not utilsPage.exists(page)
return not utilsPage.exists(page)
end)
local redirects = utilsTable.filter(pagesInNav, function(page)
return mw.title.new(page).isRedirect
end)
end)
local bigGroups = utilsTable.filter(groupSizes, function(group)
local bigGroups = utilsTable.filter(groupSizes, function(group)
return group.size > MAX_RECOMMENDED_GROUP_SIZE
return group.size > group.maxGroupSize
end)
end)
local categoryName, missingPages, extraPages = h.reportCategoryMismatches(pagesInNav)
local issues = ""
local issues = ""
if categoryName and args.title and categoryName ~= args.title then
issues = issues
.."\n====Title Mismatch====\n"
..string.format("<p>The navbox title should be <code>%s</code> rather than <code>%s</code>.</p>", categoryName, args.title)
.."<p>For navboxes added by [[Template:Categories]], the navbox title must match the category name, as navboxes are listed alphabetically by category name.</p>"
end
if #wantedPages > 0 then
if #wantedPages > 0 then
issues = issues.."\n====Red Links====\n"
issues = issues.."\n====Red Links====\n"
Line 222: Line 277:
issues = issues.."\n====Redirect Links====\n"
issues = issues.."\n====Redirect Links====\n"
redirects = utilsTable.map(redirects, utilsMarkup.link)
redirects = utilsTable.map(redirects, utilsMarkup.link)
issues = issues.."The above navbox contains links to redirects. Please update these links to refer to the redirect targets:"
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)  
issues = issues..utilsMarkup.bulletList(redirects)  
end
end
if #missingPages > 0 then
if #missingLinks > 0 then
issues = issues.."\n====Missing Links====\n"
issues = issues.."\n====Missing Links====\n"
missingPages = utilsTable.map(missingPages, utilsMarkup.link)
missingLinks = utilsTable.map(missingLinks, utilsMarkup.link)
issues = issues.."<p>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.</p>"
issues = issues.."<p>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.</p>"
issues = issues..utilsMarkup.bulletList(missingPages)
issues = issues..utilsMarkup.bulletList(missingLinks)
end
end
if #missingUses > 0 and isManualNavboxPage then -- missing uses is impossible for navboxes added by [[Template:Category]]
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"
issues = issues.."\n====Missing Uses====\n"
missingUses = utilsTable.map(missingUses, utilsMarkup.link)
missingUses = utilsTable.map(missingUses, utilsMarkup.link)
issues = issues.."The above navbox is missing from the following articles. Please add <code><nowiki>{{"..title.baseText.."}}</nowiki></code> to these articles to ensure that the navigation does not have dead ends."
issues = issues.."The above navbox is missing from the following articles. Please add <code><nowiki>{{"..h.templatePage().."}}</nowiki></code> to these articles to ensure that the navigation does not have dead ends."
issues = issues..utilsMarkup.bulletList(missingUses)
issues = issues..utilsMarkup.bulletList(missingUses)
end
end
if #bigGroups > 0 then
if #bigGroups > 0 then
issues = issues.."\n====Big Groups====\n"
issues = issues.."\n====Big Groups====\n"
bigGroups = utilsTable.map(bigGroups, function(group)
local dataRows = utilsTable.map(bigGroups, function(group)
return string.format("<li>%s: %d pages</li>", group.name, group.size)
return {group.name, group.size, group.maxGroupSize}
end)
end)
bigGroups = "<ul>"..table.concat(bigGroups, "").."</ul>"
bigGroups = utilsLayout.table({
issues = issues..string.format("The above navbox has rows exceeding the recommended maximum size of %d entries. Please subdivide these rows if possible.", MAX_RECOMMENDED_GROUP_SIZE)
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..bigGroups
issues = issues.."If you are certain that a row should not be subdivided despite its size, you can increase the <code>maxGroupSize</code> of the row. See [[Template:Categories/Navbox]] for more information."
end
end
if not isManualNavboxPage and not isUsingCategoryNavbox then
if #extraPages > 0 or #missingPages > 0 then
issues = issues.."\n====[[Template:Categories/Navbox]] Required====\n"
issues = issues.."\n====Category Mismatch====\n"
issues = issues.."<p>This template does not appear to be using {{Template|Categories/Navbox}}. Please use this template to ensure that all pages in the category are represented in the navbox.</p><p>If the correct template is in fact being used, purge the cache to remove this from the report.</p>"
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 {{Template|Categories/Navbox}} should be used instead of {{Template|Navbox}} for category-based navboxes."
end
end
local report = "\n==Report==\n"
local report = "\n==Report==\n"
if issues ~= "" then
if issues ~= "" then
report = "<p>{{TOC}}</p>"..report -- fixes the lack of space between the navbox and the toc beneath it
report = report
report = report
.."\n===Issues===\n"
.."\n===Issues===\n"
..issues
..issues
.."[[Category:Navigation Templates Needing Attention]]"
-- Special subcategory for big groups as it is by far the most common issue but also the least urgent,
-- so it helps to keep it separate to avoid burying other more important issues
if #bigGroups > 0 then
report = report..CATEGORY_NAVBOXES_BIG_GROUPS
else
report = report.."[[Category:"..CATEGORY_NAVBOXES_ATTENTION.."]]"
end
else
else
report = report.."<p>[[File:TFH Green Link ok.png|32px|link=]] No issues have been detected in this navbox.</p>"
report = report.."<p>[[File:TFH Green Link ok.png|32px|link=]] No issues have been detected in this navbox.</p>"
Line 264: Line 341:
return mw.getCurrentFrame():preprocess(report)
return mw.getCurrentFrame():preprocess(report)
end
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 {{Guidelines Nav}}}
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 = {
p.Templates = {
["Navbox"] = {
["Navbox"] = {
format = "block",
format = "block",
purpose = "Creates [[:Category:Navbox Templates|navbox templates]].",
purpose = "Creates [[:Category:"..CATEGORY_NAVBOX_TEMPLATES.."|navbox templates]].",
categories = {"Metatemplates"},
categories = {"Metatemplates"},
boilerplate = {
paramOrder = {"id", "title", "titleImages", "image", "group", "links", "maxGroupSize", "footer"},
separateRequiredParams = false,
},
paramOrder = {"id", "title", "image", "group", "links", "footer"},
repeatedGroup = {
repeatedGroup = {
name = "rows",
name = "rows",
params = {"group", "links"},
params = {"group", "links", "maxGroupSize"},
counts = {2, 3, 4, 5, 6, 7},
counts = {2, 3, 4, 5, 6, 7},
},
boilerplate = {
separateRequiredParams = false,
hiddenParams = {"maxGroupSize", "type"},
},
},
params = {
params = {
id = {
id = {
type = "string",
type = "string",
desc = "A unique ID for the navbox. Sets the [https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/id id HTML attribute] so that the navbox can be linked to.",
desc = "A unique ID for the navbox. Sets the [https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/id id HTML attribute] so that the navbox can be linked to. Defaults to the navbox title.",
trim = true,
trim = true,
nilIfEmpty = true,
nilIfEmpty = true,
Line 292: Line 436:
trim = true,
trim = true,
nilIfEmpty = true,
nilIfEmpty = true,
},
titleImages = {
type = "string",
desc = "Two file names with the <code>File:</code> prefix, separated by a comma. Renders image to the left and right of the navbox title.",
trim = true,
nilIfEmpty = true,
split = true,
},
},
image = {
image = {
type = "wiki-file-name",
type = "wiki-file-name",
desc = "A file name, with the <code>File:</code> prefix.",
desc = "A file name, with the <code>File:</code> prefix. Renders an image in the navbox body.",
trim = true,
trim = true,
nilIfEmpty = true,
nilIfEmpty = true,
Line 312: Line 463:
nilIfEmpty = true,
nilIfEmpty = true,
split = 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 = {
footer = {
Line 318: Line 475:
trim = true,
trim = true,
nilIfEmpty = true,
nilIfEmpty = true,
},
}
},
},
},
},

Latest revision as of 17:20, 25 April 2024

This is the main module for the following templates:
local p = {}
local h = {}

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

local CATEGORY_INVALID_ARGS = "[[Category:"..require("Module:Constants/category/invalidArgs").."]]"
local CATEGORY_NAVBOXES_ATTENTION = "Navigation templates needing attention"
local CATEGORY_NAVBOXES_BIG_GROUPS = "[[Category:Navboxes with big groups]]"
local CATEGORY_NAVBOXES_OTHER = "[[Category:Navboxes with 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.."[[Category:"..CATEGORY_NAVBOX_TEMPLATES.."]]"
		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.."[[Category:"..CATEGORY_NAVBOX_TEMPLATES.."]]", 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|%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|%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, '<span class="zw-navbox__link">'..link..'</span>')
		end

		local evenOdd = (i % 2 == 0) and "even" or "odd"
		
		local links = table.concat(links, "&nbsp;• ")
		rows:tag("div")
				:addClass("zw-navbox__row-links zw-navbox__row-links--"..evenOdd)
				:addClass(not row.group and "zw-navbox__row-links--nogroups" or nil)
				:tag("div")
					:addClass("zw-navbox__row-links-content")
					:addClass(not row.group 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|%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 <span class="plainlinks">[https://zeldawiki.wiki/wiki/Link Link]</span>
-- 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 "<b>"..display.."</b>"
	else
		local url = mw.site.server.."/wiki/"..mw.uri.encode(page, "WIKI")
		return string.format('<span class="plainlinks">[%s %s]</span>', 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", 
			mw.title.getCurrentTitle().prefixedText ~= "Template:Guidelines Nav" and "Categories" or nil  -- To avoid counting examples at [[Template:Categories]] as uses (but we do want to count [[Template:Guidelines Nav]])
		},
	})

	local missingLinks = utilsTable.difference(pagesUsingNav, pagesInNav)
	local missingUses = utilsTable.difference(pagesInNav, pagesUsingNav)
	local redirects = h.reportRedirects(pagesInNav)
	missingLinks = utilsTable.difference(missingLinks, redirects)
	missingUses = utilsTable.difference(missingUses, redirects)
	
	local wantedPages = utilsTable.filter(pagesInNav, function(page)
		return not utilsPage.exists(page)
	end)
	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("<p>The navbox title should be <code>%s</code> rather than <code>%s</code>.</p>", categoryName, args.title)
			.."<p>For navboxes added by [[Template:Categories]], the navbox title must match the category name, as navboxes are listed alphabetically by category name.</p>"
	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.."<p>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.</p>"
		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 <code><nowiki>{{"..h.templatePage().."}}</nowiki></code> 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 <code>maxGroupSize</code> 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 {{Template|Categories/Navbox}} should be used instead of {{Template|Navbox}} for category-based navboxes."
	end
	
	local report = "\n==Report==\n"
	if issues ~= "" then
		report = "<p>{{TOC}}</p>"..report -- fixes the lack of space between the navbox and the toc beneath it
		report = report
		.."\n===Issues===\n"
		..issues
		-- Special subcategory for big groups as it is by far the most common issue but also the least urgent, 
		-- so it helps to keep it separate to avoid burying other more important issues
		if #bigGroups > 0 then
			report = report..CATEGORY_NAVBOXES_BIG_GROUPS
		else
			report = report.."[[Category:"..CATEGORY_NAVBOXES_ATTENTION.."]]"
		end
	else
		report = report.."<p>[[File:TFH Green Link ok.png|32px|link=]] No issues have been detected in this navbox.</p>"
	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 {{Guidelines Nav}}}
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 [[:Category:"..CATEGORY_NAVBOX_TEMPLATES.."|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 [https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/id 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 = "<p>The navbox title.</p><p>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.</p>",
				trim = true,
				nilIfEmpty = true,
			},
			titleImages = {
				type = "string",
				desc = "Two file names with the <code>File:</code> 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 <code>File:</code> 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