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 utilsMarkup = require("Module:UtilsMarkup") local utilsPage = require("Module:UtilsPage") local utilsTable = require("Module:UtilsTable")

function p.Main(frame) local args, err = utilsArg.parse(frame:getParent.args, p.Templates.Categories)

h.storeCategories(args)

local result = h.printNavboxes(args) if not utilsPage.inNamespace("User") then result = result .. h.printCategories(args.categories, args) end if err then result = result .. utilsMarkup.categories(err.categories) end return result 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 return utilsMarkup.categories(categories) end

function h.printNavboxes(args) local html = mw.html.create("div") :addClass("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("categories__navboxes-common") :wikitext(unpack(commonNavs)) end if args.categories then local autoNavs = html:tag("div") :addClass("categories__navboxes-auto") for i, category in ipairs(args.categories) do			local navbox = h.printNavbox(category) autoNavs:wikitext(navbox) end end return tostring(html) 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 = "Common "..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) local pagesInCategory = tonumber(mw.getCurrentFrame:callParserFunction("PAGESINCATEGORY", category, "R", "pages")) if pagesInCategory == 0 or pagesInCategory > Data.maxPagesPerNav then return "", pagesInCategory end local categoryLink = string.format("Category:%s", category) local dplArgs = { namespace = "", includeSubpages = false, skipthispage = "no", orderMethod = "title", category = category, }	local subcategories = Data.subcategories[category] if not subcategories then local results = utilsPage.dpl(dplArgs) local links = h.rowContent(results) local navbox = mw.getCurrentFrame:expandTemplate({			title = "Navbox",			args = {				id = "category-"..category,				title = category,				links1 = links,				footer = categoryLink,			}		}) return navbox, pagesInCategory end local navboxArgs = { id = "category-"..category, title = category, footer = categoryLink, }	local pagesInRows = 0 for i, subcategory in ipairs(subcategories) do		local rowDplArgs = utilsTable.merge({}, dplArgs, {			category = category .. "&" .. subcategory.category		}) for _, parentCategory in ipairs(subcategory.parents or {}) do			table.insert(rowDplArgs, {				param = "notcategory",				value = parentCategory			}) end local results = utilsPage.dpl(rowDplArgs) if #results > 0 then navboxArgs["group"..i] = subcategory.display or subcategory.category navboxArgs["links"..i] = h.rowContent(results) end end -- "Other" row for _, subcategory in ipairs(subcategories) do		table.insert(dplArgs, {			param = "notcategory",			value = subcategory.category		}) end local results = utilsPage.dpl(dplArgs) if #results > 0 then navboxArgs["group"..(#subcategories+1)] = "Other" navboxArgs["links"..(#subcategories+1)] = h.rowContent(results) end local navbox = mw.getCurrentFrame:expandTemplate({		title = "Navbox",		args = navboxArgs,	}) return navbox, pagesInCategory end

function h.rowContent(pages) -- creates link to the pages that do not register on Special:WhatLinksHere, to avoid spamming it	local links = utilsTable.map(pages, function(page)		page = string.gsub(page, ",", "&#44;") -- fixes bug where pages with commas in them are rendered incorrectly		local currentPage = string.gsub(mw.title.getCurrentTitle.fullText, ",", "&#44;")		if page == currentPage then			return utilsMarkup.bold(page)		else			return utilsMarkup.link(page, page, true)		end	end) return table.concat(links, ", ") end

function p.Data(frame) local utilsLayout = require("Module:UtilsLayout") local utilsString = require("Module:UtilsString")

local maxNavboxSize = "Max navbox size: " .. Data.maxPagesPerNav .." items\n" 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 tableRows = {} for k in pairs(Data.subcategories) do		local navbox, pageCount = h.printNavbox(k) local categoryLink = "Category:"..k.."" table.insert(tableRows, {categoryLink, pageCount, navbox}) end tableRows = utilsTable.sortBy(tableRows, 1) local customizedNavboxes = utilsLayout.table({		sortable = true,		headers = {"Category", "", "Navbox"},		rows = tableRows,	})

local plainCategories = utilsCargo.query("Categories", "category, COUNT(*)=pageCount, _pageName=sample", {		where = "canon HOLDS NOT LIKE '%' AND nonCanon HOLDS NOT LIKE '%' AND category IS NOT NULL",		groupBy = "category",		sortBy = "category",		limit = 1000,	}) local tableRows = {} for i, plainCategory in ipairs(plainCategories) do		local category = plainCategory.category local pageCount = tonumber(plainCategory.pageCount) local sample = plainCategory.sample if not Data.subcategories[category] then local categoryLink = "Category:"..category.."" local navboxId = "navbox-category-"..utilsString.kebabCase(category) local sampleLink = string.format("%s", sample, navboxId, sample) table.insert(tableRows, {categoryLink, pageCount, sampleLink}) end end local uncustomizedNavboxes = utilsLayout.table({		sortable = true,		headers = {"Category", "", "Sample"},		rows = tableRows,	}) local desc = " Navboxes are automatically generated for the following categories. These navboxes contain no subgroups as none have been defined on this page. As there are too many such navboxes to display them all here, click the sample link to view the navbox on one of the pages that uses it. " uncustomizedNavboxes = desc .. uncustomizedNavboxes local tabs = utilsLayout.tabs({		{			label = "Common Subject Navboxes",			content = commonSubjectNavboxes,		},		{			label = "Customized Navboxes",			content = customizedNavboxes,		},		{			label = "Uncustomized Navboxes",			content = uncustomizedNavboxes,		},	}) local result = maxNavboxSize..tabs

return result 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.", },						},					},				},				{					name = "subcategories", required = true, type = "map", desc = 'Maps a category to a list of "subcategories". Allows for sub-categorization in the navbox. For each "subcategory", a row is created in the navbox listing the members that exist in both the "subcategory" and the "main" category. An "Other" row is created for any remaining pages that are in the "main" category but none of the "subcategories" listed.', keyPlaceholder = "navboxCategory", keys = { type = "string" }, values = { type = "array", items = { type = "record", properties = { {									name = "category", required = true, type = "string", desc = 'Name of a subcategory that intersects with the navbox category.', },								{									name = "parents", type = "array", items = { type = "string" }, desc = "Used to exclude members of one or more parent categories. Not needed for most cases." },								{									name = "display", type = "string", default = "category", desc = "Text displayed for the navbox row. Defaults to the category name, without the namespace prefix.", },							},						}					},				},			},		},	} 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 cods, and improperly ordered codes are handled appropriately.", args = { characters = "TP, TP, fakeGame, OoT, OoT", },			},		},	}, }

return p