56,117
edits
PhantomCaleb (talk | contribs) No edit summary |
PhantomCaleb (talk | contribs) No edit summary |
||
(46 intermediate revisions by 2 users not shown) | |||
Line 1: | Line 1: | ||
local p = {} | local p = {} | ||
local h = {} | |||
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 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_PARTITION_SIZE = require("Module:Constants/number/maxNavboxPartitionSize") | |||
function p.Main(frame) | function p.Main(frame) | ||
local args, err = utilsArg.parse(frame:getParent().args, p.Templates.Navbox) | local args, err = utilsArg.parse(frame:getParent().args, p.Templates.Navbox) | ||
local | local errCategories = err and err.categoryText or "" | ||
if not args.title then | if not args.title then | ||
return "", | if h.isManualNavboxPage() then | ||
return "", errCategories.."[[Category:"..CATEGORY_NAVBOX_TEMPLATES.."]]" | |||
elseif h.isTemplate() then | |||
return "", errCategories | |||
else | |||
return "" | |||
end | |||
end | end | ||
h.storeArgs(args) | |||
-- [[MediaWiki:Gadget- | local navbox, categories = h.printNavbox(args) | ||
local | 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") | :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") | :tag("div") | ||
:addClass("zw-navbox__header mw-collapsible-toggle") | :addClass("zw-navbox__header mw-collapsible-toggle") | ||
Line 27: | Line 95: | ||
:tag("span") | :tag("span") | ||
:addClass("zw-navbox__title") | :addClass("zw-navbox__title") | ||
: | :node(navboxTitle) | ||
:done() | :done() | ||
:tag("span") | :tag("span") | ||
Line 46: | 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 | ||
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() | ||
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.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 links = table.concat(links, " • ") | local links = table.concat(links, " • ") | ||
rows:tag("div") | rows:tag("div") | ||
:addClass("zw-navbox__row-links".. | :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 97: | Line 171: | ||
return result, categories | return result, categories | ||
end | 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, ",", ",") -- 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 = { | p.Templates = { | ||
["Navbox"] = { | ["Navbox"] = { | ||
format = "block", | format = "block", | ||
purpose = "Creates [[:Category: | purpose = "Creates [[:Category:"..CATEGORY_NAVBOX_TEMPLATES.."|navbox templates]].", | ||
categories = {"Metatemplates"}, | categories = {"Metatemplates"}, | ||
paramOrder = {"id", "title", "titleImages", "image", "group", "links", "maxGroupSize", "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 = { | |||
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 = { | title = { | ||
required = true, | required = true, | ||
Line 119: | 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 139: | 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 145: | Line 475: | ||
trim = true, | trim = true, | ||
nilIfEmpty = true, | nilIfEmpty = true, | ||
} | } | ||
}, | }, | ||
}, | }, |