Module:Categories

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 = "" 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 = ""

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_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  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, pagesInCategory = h.printNavbox(args.navboxCategory, groups) if pagesInCategory > Data.maxPagesPerNav then local utilsError = require("Module:UtilsError") local err = utilsError.error(string.format("No navbox is shown for category  as it has %d entries, which exceeds the %d-page maximum", args.navboxCategory, pagesInCategory, 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) 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 pagesInCategory = tonumber(mw.getCurrentFrame:callParserFunction("PAGESINCATEGORY", category, "R", "pages")) if pagesInCategory < MIN_PAGES_PER_NAV or pagesInCategory > Data.maxPagesPerNav then return nil, pagesInCategory end local categoryLink = string.format("Category:%s", category) local dplArgs = { namespace = {"", "Community"}, includeSubpages = false, skipthispage = "no", orderMethod = "title", category = category, }	local navboxArgs = { id = "category-"..category, title = category, footer = categoryLink, }	if not groups then local results = utilsPage.dpl(dplArgs) navboxArgs["links1"] = h.rowContent(results) 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.."" 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, ",", "&#44;") -- fixes bug where pages with commas in them are rendered incorrectly		local display = page		display = string.gsub(display, "^Community:", "")		return string.format("%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  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", 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 = { "Navbox", "Needs Attention", "Has Other", },		rows = dataRows, })	local desc = "These customized 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", 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 = {"", "Category", "Sample", "Customization"},		rows = tableRows,	}) local desc = "" .." 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. " .." To customize these navboxes, click the associated link in the table below. See Template:Categories/Navbox (and the next tab) for more information. " .." 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. " 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", ""},		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", ""},		rows = tableRows,	}) local desc = string.format(" No navbox is generated for the following categories as they contain less than %d pages. ", MIN_PAGES_PER_NAV) desc = desc.." These categories should be added to more pages or removed entirely. " 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 codes representing the games or other titles in which the given article subject is one of the %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 Animals, Forests, 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-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  parameter of Template:Navbox. Defaults to  .", trim = true, nilIfEmpty = true, },			category = { type = "string", required = true, desc = " A category name, without the  prefix. The navbox group will contain all pages that are in both this category and the navbox category.  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   name. ", trim = true, nilIfEmpty = true, split = true, },			maxGroupSize = { type = "number", desc = "Sets the  parameter of Template:Navbox.", },		},		examples = { {				"Insects", category1= "Bees", category2= "Butterflies", category3= "Dragonflies", },			{				"Treasures", category1= "Items in Phantom Hourglass", display1= "", category2= "Items in Spirit Tracks", display2= "", category3= "Items in Skyward Sword", display3= "SS -", category4= "Items in A Link Between Worlds", display4= "", },			{				"Twilight Enemies", category1= "Enemies", category2= "Bosses, Sub-Bosses", },			{				desc = " 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