Module:Categories
Jump to navigation
Jump to search
This is the main module for the following templates:
local p = {}
local h = {}
local Data = mw.loadData("Module:Categories/Data")
local Franchise = require("Module:Franchise")
local utilsArg = require("Module:UtilsArg")
local utilsCargo = require("Module:UtilsCargo")
local utilsPage = require("Module:UtilsPage")
local utilsTable = require("Module:UtilsTable")
local MIN_PAGES_PER_NAV = 2
local CATEGORY_INVALID_ARGS = "[[Category:"..require("Module:Constants/category/invalidArgs").."]]"
local CATEGORY_NAVBOXES_ATTENTION = "Navigation templates needing attention"
local CATEGORY_NAVBOXES_OTHER = "Navboxes with other"
local CATEGORY_CAT_NAVBOX_TEMPLATES = "Category navbox templates"
local CATEGORY_TEMPLATES_DELETION = "[[Category:Templates needing deletion]]"
local title = mw.title.getCurrentTitle()
local isTemplatePage = title.nsText == "Template"
-- Template:Categories
function p.Main(frame)
local args, err = utilsArg.parse(frame:getParent().args, p.Templates.Categories)
local categories = err and err.categoryText or ""
h.storeCategories(args)
for i in ipairs(args.categories or {}) do
args.categories[i] = string.gsub(args.categories[i], "^Category:", "") -- strip namespace prefix in case it was added by mistake
end
local navboxes = ""
if not utilsPage.inNamespace("Category") then
navboxes = h.printNavboxes(args)
end
if not utilsPage.inNamespace("User") then
categories = h.printCategories(args.categories, args)..categories
end
return navboxes, categories
end
-- Template:Categories/Navbox
function p.Navbox(frame)
local categories = string.format("[[Category:%s|%s]]", CATEGORY_CAT_NAVBOX_TEMPLATES, title.subpageText)
local args, err = utilsArg.parse(frame:getParent().args, p.Templates["Categories/Navbox"])
if err then
categories = categories..err.categoryText
end
if err and isTemplatePage then
return "", categories
elseif err then
return ""
end
local groups = {}
for i, group in ipairs(args.groups) do
local groupCategories = {}
for j, category in ipairs(group.category) do
category = category and string.gsub(category, "^Category:", "") -- category may or may not have prefix - strip it in case it does
if category and not utilsPage.exists("Category:"..category) then
local utilsError = require("Module:UtilsError")
local err = utilsError.error(string.format("Category <code>%s</code> does not exist", category), true)
categories = categories..err..CATEGORY_INVALID_ARGS
else
table.insert(groupCategories, category)
end
end
if #groupCategories > 0 then
group.category = groupCategories
table.insert(groups, group)
end
end
local navbox, pageCount = h.printNavbox(args.navboxCategory, groups)
if pageCount > Data.maxPagesPerNav then
local utilsError = require("Module:UtilsError")
local err = utilsError.error(string.format("No navbox is shown for category <code>%s</code> as it has %d entries, which exceeds the %d-page maximum", args.navboxCategory, pageCount, Data.maxPagesPerNav), true)
categories = categories..err..CATEGORY_TEMPLATES_DELETION
end
if isTemplatePage then
return navbox or "", categories
else
return navbox or ""
end
end
function h.storeCategories(args)
for i, category in ipairs(Data.gameCategories) do
local games = args[category.parameter]
if games then
-- We only store the base game to avoid skewing appearance counts towards games with remakes
games = utilsTable.map(games, Franchise.baseGame)
games = utilsTable.unique(games)
local canon, nonCanon = utilsTable.partition(games, Franchise.isCanon)
mw.getCurrentFrame():expandTemplate({
title = "Categories/Store",
args = {
category = category.category,
canon = table.concat(canon, ", "),
nonCanon = table.concat(nonCanon, ", "),
}
})
end
end
for i, category in ipairs(args.categories or {}) do
mw.getCurrentFrame():expandTemplate({
title = "Categories/Store",
args = {
category = category,
}
})
end
end
function h.printCategories(plainCategories, perGameCategories)
local categories = plainCategories or {}
local gameCategoryMap = {} -- keeps track of which games are specified for which categories
for _, category in ipairs(Data.gameCategories) do
local gamesInCategory = perGameCategories[category.parameter]
if gamesInCategory then
table.insert(categories, category.category)
gameCategoryMap[category.parameter] = utilsTable.invert(gamesInCategory)
end
end
for _, game in ipairs(Franchise.enum()) do
for _, category in ipairs(Data.gameCategories) do
if gameCategoryMap[category.parameter] and gameCategoryMap[category.parameter][game] then
local categoryName = category.category .. " in " .. Franchise.shortName(game)
table.insert(categories, categoryName)
end
end
end
local categoryLinks = ""
for i, category in ipairs(categories) do
categoryLinks = categoryLinks..string.format("[[Category:%s]]", category)
end
return categoryLinks
end
function h.printNavboxes(args)
local html = mw.html.create("div")
:addClass("zw-categories")
:tag("div")
:addClass("zw-categories__navboxes")
local commonNavs = {}
for i, gameCategory in ipairs(Data.gameCategories) do
if args[gameCategory.parameter] and gameCategory.common then
local navbox = h.printCommonNav(gameCategory)
if navbox then
table.insert(commonNavs, navbox)
end
end
end
if #commonNavs > 0 then
html:tag("div")
:addClass("zw-categories__navboxes-common")
:wikitext(unpack(commonNavs))
end
if args.categories then
table.sort(args.categories)
local autoNavs = html:tag("div")
:addClass("zw-categories__navboxes-category-list")
for i, category in ipairs(args.categories) do
local templateName = "Template:Categories/Navbox/"..category
local navbox
if utilsPage.exists(templateName) then
navbox = mw.getCurrentFrame():expandTemplate({ title = templateName })
else
navbox = h.printNavbox(category)
end
autoNavs:wikitext(navbox)
end
end
return tostring(html:allDone())
end
function h.printCommonNav(gameCategory)
local category, tiers = gameCategory.category, gameCategory.common
-- Display nothing if the current subject does not appear in tiers[1] or more games
local results = utilsCargo.query("Categories, Categories__canon", "Categories._pageName=page", {
join = "Categories._ID = Categories__canon._rowID",
orderBy = "page",
groupBy = "page",
having = "COUNT(*) >= "..tiers[1],
where = utilsCargo.allOf({
_pageName = mw.title.getCurrentTitle().text,
category = category,
})
})
if #results == 0 then
return nil
else
return h._printCommonNav(category, tiers)
end
end
function h._printCommonNav(category, tiers)
local navboxArgs = {
id = "Common "..category,
title = "Recurring "..category
}
local numTiers = utilsTable.size(tiers)
for i in ipairs(tiers) do
local min = tiers[i]
local max = tiers[i+1]
local countRange = "COUNT(*) >= "..min
if max then
countRange = countRange.." AND COUNT(*) < "..max
end
local results = utilsCargo.query("Categories, Categories__canon", "Categories._pageName=page", {
join = "Categories._ID = Categories__canon._rowID",
orderBy = "page",
groupBy = "page",
having = countRange,
where = utilsCargo.allOf({
category = category
})
})
local pages = utilsTable.map(results, "page")
local index = numTiers - i + 1 -- we want to show the highest tiers first
navboxArgs["group"..index] = string.format("%d+ Appearances", min)
navboxArgs["links"..index] = h.rowContent(pages)
end
return mw.getCurrentFrame():expandTemplate({
title = "Navbox",
args = navboxArgs,
})
end
function h.printNavbox(category, groups)
local dplArgs = {
namespace = {"", "Community"},
includeSubpages = false,
skipthispage = "no",
orderMethod = "title",
category = category,
}
local navboxArgs = {
id = "category-"..category,
title = category,
footer = string.format("[[:Category:%s]]", category),
}
local pagesInCategory = utilsPage.dpl(dplArgs)
if #pagesInCategory < MIN_PAGES_PER_NAV or #pagesInCategory > Data.maxPagesPerNav then
return nil, #pagesInCategory
end
if not groups then
navboxArgs["links1"] = h.rowContent(pagesInCategory)
local navbox = mw.getCurrentFrame():expandTemplate({
title = "Navbox",
args = navboxArgs
})
return navbox, #pagesInCategory
end
local errCategories = ""
for i, group in ipairs(groups) do
local groupPages = {}
for j, groupCategory in ipairs(group.category) do
local rowDplArgs = utilsTable.merge({}, dplArgs, {
category = category.."&"..groupCategory
})
local pages = utilsPage.dpl(rowDplArgs)
groupPages = utilsTable.concat(groupPages, pages)
end
groupPages = utilsTable.unique(groupPages)
table.sort(groupPages)
if #groupPages > 0 then
navboxArgs["group"..i] = group.display or group.category[1]
navboxArgs["links"..i] = h.rowContent(groupPages)
navboxArgs["maxGroupSize"..i] = group.maxGroupSize
elseif isTemplatePage then
local utilsError = require("Module:UtilsError")
utilsError.warn(string.format("Group %d has 0 entries.", i))
errCategories = errCategories.."[[Category:"..CATEGORY_NAVBOXES_ATTENTION.."]]"
end
end
-- "Other" row
local categoriesToExclude = {}
for i, group in ipairs(groups) do
categoriesToExclude = utilsTable.concat(categoriesToExclude, group.category)
end
local otherDplArgs = utilsTable.merge({}, dplArgs, {
notcategory = categoriesToExclude
})
local results = utilsPage.dpl(otherDplArgs)
if #results > 0 then
local i = utilsTable.size(groups) + 1
navboxArgs["group"..i] = "Other"
navboxArgs["links"..i] = h.rowContent(results)
end
local navbox = mw.getCurrentFrame():expandTemplate({
title = "Navbox",
args = navboxArgs,
})
return navbox..errCategories, #pagesInCategory
end
function h.rowContent(pages)
local links = utilsTable.map(pages, function(page)
page = string.gsub(page, ",", ",") -- fixes bug where pages with commas in them are rendered incorrectly
local display = page
display = string.gsub(display, "^Community:", "")
return string.format("[[%s|%s]]", page, display)
end)
return table.concat(links, ", ")
end
function p.Data(frame)
local utilsLayout = require("Module:UtilsLayout")
local utilsMarkup = require("Module:UtilsMarkup")
local utilsString = require("Module:UtilsString")
local commonSubjectNavboxes = ""
for i, gameCategory in ipairs(Data.gameCategories) do
if gameCategory.common then
commonSubjectNavboxes = commonSubjectNavboxes .. h._printCommonNav(gameCategory.category, gameCategory.common)
end
end
local desc = 'The following navbox templates are automatically generated by [[Template:Categories]]. The commonality tiers are controlled by the <code>common</code> fields defined on this /Data page.'
commonSubjectNavboxes = desc..commonSubjectNavboxes
local subpages = utilsPage.getSubpages("Template:Categories/Navbox")
subpages = utilsTable.filter(subpages, function(subpage)
return not string.find(subpage, "/Documentation$") and not string.find(subpage, "/Preload$")
end)
local needingAttention = utilsPage.dpl({
category = CATEGORY_NAVBOXES_ATTENTION,
})
local withOther = utilsPage.dpl({
category = CATEGORY_NAVBOXES_OTHER,
})
needingAttention = utilsTable.invert(needingAttention)
withOther = utilsTable.invert(withOther)
local dataRows = utilsTable.map(subpages, function(subpage)
local display = string.match(subpage, "Template:Categories/Navbox/(.*)")
local navbox = string.format("[[%s|%s]]", subpage, display)
local needsAttention = needingAttention[subpage] and "yes" or ""
local hasOther = withOther[subpage] and "yes" or ""
return {navbox, needsAttention, hasOther}
end)
local customizedNavboxes = utilsLayout.table({
sortable = true,
headers = {
"[[:Category:"..CATEGORY_CAT_NAVBOX_TEMPLATES.."|Navbox]]",
"[[:Category:"..CATEGORY_NAVBOXES_ATTENTION.."|Needs Attention]]",
"[[:Category:"..CATEGORY_NAVBOXES_OTHER.."|Has Other]]",
},
rows = dataRows,
})
local desc = "These customized [[:Category:"..CATEGORY_CAT_NAVBOX_TEMPLATES.."|category navbox templates]] are added by [[Template:Categories]] instead of the ones it generates automatically (see previous tab)."
customizedNavboxes = desc..customizedNavboxes
local queryResults = utilsCargo.query("Categories, _pageData", "category, COUNT(*)=pageCount, Categories._pageName=sample, _isRedirect", {
join = "Categories._pageName=_pageData._pageName",
where = "canon HOLDS NOT LIKE '%' AND nonCanon HOLDS NOT LIKE '%' AND category IS NOT NULL AND _isRedirect = '0'",
groupBy = "category",
limit = 1000,
})
local autoNavCategories, fatCats = utilsTable.partition(queryResults, function(cat)
return tonumber(cat.pageCount) <= Data.maxPagesPerNav
end)
local autoNavCategories, tinyCats = utilsTable.partition(autoNavCategories, function(cat)
return tonumber(cat.pageCount) >= MIN_PAGES_PER_NAV
end)
local tableRows = {}
for i, cat in ipairs(autoNavCategories) do
local category = cat.category
local pageCount = tonumber(cat.pageCount)
local sample = cat.sample
local customNavboxTemplate = "Template:Categories/Navbox/"..category
if not utilsPage.exists(customNavboxTemplate) then
local categoryLink = "[[:Category:"..category.."]]"
local navboxId = "navbox-category-"..utilsString.kebabCase(category)
local sampleLink = string.format("[[%s#%s|%s]]", sample, navboxId, sample)
local customizeLink = mw.title.new(customNavboxTemplate):fullUrl({
action = "edit",
preload = "Template:Categories/Navbox/Preload",
["preloadparams[]"] = category,
})
customizeLink = string.format("[%s Customize %s]", customizeLink, category)
table.insert(tableRows, {pageCount, categoryLink, sampleLink, customizeLink})
end
end
-- Sort descending by number of pages
table.sort(tableRows, function(a, b)
return a[1] > b[1]
end)
local autoNavboxes = utilsLayout.table({
sortable = true,
headers = {"{{Exp|Number of pages in category|#}}", "Category", "Sample", "Customization"},
rows = tableRows,
})
local desc = ""
.."<p>The following categories have navboxes automatically generated by [[Template:Categories]]. As there are too many such navboxes to display them here, click the sample link to view the navbox on one of the pages that uses it.</p>"
.."<p>To customize these navboxes, click the associated link in the table below. See [[Template:Categories/Navbox]] (and the next tab) for more information.</p>"
.."<p>Navboxes with more than a dozen or so links should usually be partitioned using [[Template:Categories/Navbox]]. Navboxes with too many links in a single list may be less effective as navigation tools.</p>"
desc = frame:preprocess(desc)
autoNavboxes = desc .. autoNavboxes
local tableRows = {}
for i, cat in ipairs(fatCats) do
local catLink = string.format("[[:Category:%s]]", cat.category)
table.insert(tableRows, {catLink, cat.pageCount})
end
fatCats = utilsLayout.table({
sortable = true,
headers = {"Category", "{{Exp|Number of pages in category|#}}"},
rows = tableRows,
})
local desc = string.format("No navbox is generated for the following categories as they have more than %d pages.", Data.maxPagesPerNav)
fatCats = desc..fatCats
local tableRows = {}
for i, cat in ipairs(tinyCats) do
local catLink = string.format("[[:Category:%s]]", cat.category)
table.insert(tableRows, {catLink, cat.pageCount})
end
tinyCats = utilsLayout.table({
sortable = true,
headers = {"Category", "{{Exp|Number of pages in category|#}}"},
rows = tableRows,
})
local desc = string.format("<p>No navbox is generated for the following categories as they contain less than %d pages.</p>", MIN_PAGES_PER_NAV)
desc = desc.."<p>These categories should be added to more pages or removed entirely.</p>"
tinyCats = desc..tinyCats
local tabs = utilsLayout.tabs({
{
label = "Common Subject Navboxes",
content = commonSubjectNavboxes,
},
{
label = "Category Navboxes (Automatic)",
content = autoNavboxes,
},
{
label = "Category Navboxes (Customized)",
content = customizedNavboxes,
},
{
label = "Big Categories",
content = fatCats,
},
{
label = "Small Categories",
content = tinyCats
},
})
return tabs
end
local params = {}
local paramOrder = {}
for _, perGameCategory in ipairs(Data.gameCategories) do
params[perGameCategory.parameter] = {
type = "string",
enum = Franchise.enum(),
desc = string.format("Comma-separated list of [[Data:Franchise|codes]] representing the games or other titles in which the given article subject is one of the [[:Category:%s|%s]].", perGameCategory.category, perGameCategory.category),
split = true,
trim = true,
nilIfEmpty = true,
}
table.insert(paramOrder, perGameCategory.parameter)
end
function p.Schemas()
return {
Data = {
required = true,
type = "record",
properties = {
{
name = "maxPagesPerNav",
required = true,
type = "number",
desc = "Defines the maximum number of links in any given navbox. Categories with a number of pages exceeding this limit will not have navboxes generated for them.",
},
{
name = "gameCategories",
required = true,
type = "array",
desc = "Defines the categories which are subcategorized by [[Data:Franchise]] entry.",
items = {
type = "record",
properties = {
{
name = "parameter",
required = true,
type = "string",
desc = "The name of the parameter for [[Template:Categories]].",
},
{
name = "category",
required = true,
type = "string",
desc = "The parent category name.",
},
{
name = "common",
type = "array",
items = { type = "number" },
desc = "If present, [[Module:Categories]] will generate a navbox for subjects with more than N appearances in the given category.",
},
},
},
},
},
},
}
end
p.Templates = {
["Categories"] = {
purpose = "Adding categories to pages. For each category, a navbox is generated with links between the pages in the category.",
usesModuleData = true,
format = "block",
indent = 1,
paramOrder = utilsTable.concat({1}, paramOrder),
params = utilsTable.merge(params, {
[1] = {
name = "categories",
type = "string",
desc = "Comma separated list of categories which are not subcategorized by game. Examples of these include [[:Category:Animals|Animals]], [[:Category:Forests|Forests]], [[:Category:Fire-Related Enemies|Fire-Related Enemies]], and so on.",
split = true,
trim = true,
nilIfEmpty = true,
}
}),
examples = {
vertical = true,
{
desc = "[[Ice Keese]]",
args = {
[1] = "Keese, Ice-Related Enemies",
["enemies"] = "OoT, OoT3D, MM, MM3D, TP, TPHD, PH, ST, TFH, BotW",
["sub-bosses"] = "ST",
}
},
{
desc = "[[Yuga]]",
args = {
[1] = "Demons, Loruleans, Sorcerers",
["bosses"] = "TLoZ, ALttP, OoT, OoT3D, OoS, OoA, FSA, TP, TPHD, TFoE, TWoG, ZA, BSTLoZ, AST, HW, HWL, HWDE",
["characters"] = "ALBW, HW",
["playable"] = "HW",
},
},
{
desc = "[[Blue Fire]]",
args = {
[1] = "Ancient Technology",
["items"] = "OoT, OoT3D",
["objects"] = "BotW"
}
},
{
desc = "[[Desert Temple]]",
args = {
["dungeons"] = "OoT, OoT3D",
["levels"] = "TFH",
["locations"] = "OoT, OoT3D",
},
},
{
desc = "[[Eldin Caves]]",
args = {
["stages"] = "HW, HWL, HWDE",
},
},
{
desc = "[[Song of Healing]]",
args = {
["songs"] = "MM, MM3D, TP, TPHD, ST",
},
},
{
desc = string.format("Navboxes are not generated for categories with %s+ links.", Data.maxPagesPerNav),
args = {
[1] = "Hylians, Swords",
},
},
{
desc = "Invalid codes, duplicate codes, and improperly ordered codes are handled appropriately.",
args = {
characters = "TP, TP, fakeGame, OoT, OoT",
},
},
},
},
["Categories/Navbox"] = {
format = "block",
purpose = "For creating a [[:Category:Category navbox templates|category-based navbox]] to override the default navbox generated by [[Template:Categories]].",
boilerplate = {
hiddenParams = {"maxGroupSize"},
},
repeatedGroup = {
name = "groups",
params = {"category", "display", "maxGroupSize"},
counts = {2, 3, 4, 5},
},
params = {
[1] = {
name = "navboxCategory",
required = true,
inline = true,
type = "string",
desc = "The name of the category that this navbox is for.",
trim = true,
nilIfEmpty = true,
},
display = {
type = "string",
desc = "A label for the navbox partition. Corresponds to the <code>group</code> parameter of [[Template:Navbox]]. Defaults to <code>categoryN</code>.",
trim = true,
nilIfEmpty = true,
},
category = {
type = "string",
required = true,
desc = "<p>A category name, without the <code>Category:</code> prefix. The navbox group will contain all pages that are in both this category and the navbox category.</p><p>It is also possible to specify a comma-separated list of category names. In this case, the navbox group will contain all pages that are in the navbox category and ''any'' of the specified categories. The first category in the list is the default <code>display</code> name.</p>",
trim = true,
nilIfEmpty = true,
split = true,
},
maxGroupSize = {
type = "number",
desc = "Sets the <code>maxGroupSize</code> parameter of [[Template:Navbox]].",
},
},
examples = {
{
"Insects",
category1= "Bees",
category2= "Butterflies",
category3= "Dragonflies",
},
{
"Treasures",
category1= "Items in Phantom Hourglass",
display1= "{{PH|-}}",
category2= "Items in Spirit Tracks",
display2= "{{ST|-}}",
category3= "Items in Skyward Sword",
display3= "{{SS|-}}",
category4= "Items in A Link Between Worlds",
display4= "{{ALBW|-}}",
},
{
"Twilight Enemies",
category1= "Enemies",
category2= "Bosses, Sub-Bosses",
},
{
desc = "<code>navboxCategory</code> can refer to a category that doesn't exist yet, but the subgroup categories must exist.",
args = {
"New Category",
category1 = "Not a Category",
category2 = "",
},
},
{
"Hylians"
},
{
args = {""},
},
{
desc = "Non-existent grouping categories are ignored.",
args = {
"Medals",
category1= "Items in Skyward Sword",
category2= "Not a Category",
},
},
},
},
}
return p