Module:Categories

local p = {} local h = {}

local Franchise = require("Module:Franchise") local utilsArg = require("Module:UtilsArg") local utilsLayout = require("Module:UtilsLayout") local utilsMarkup = require("Module:UtilsMarkup") local utilsPage = require("Module:UtilsPage") local utilsTable = require("Module:UtilsTable")

-- See the module for documentation about subcategories. local data = mw.loadData("Module:Categories/Data") local PER_GAME_CATEGORIES = { {		parameter = "bosses", category = "Bosses", },	{		parameter = "characters", category = "Characters", },	{		parameter = "dungeons", category = "Dungeons", },	{		parameter = "enemies", category = "Enemies", },	{		parameter = "items", category = "Items", },	{		parameter = "levels", category = "Levels", },	{		parameter = "objects", category = "Objects", },	{		parameter = "places", category = "Places", },	{		parameter = "playable", category = "Playable Characters", },	{		parameter = "songs", category = "Songs", },	{		parameter = "stages", category = "Stages", },	{		parameter = "sub-bosses", cargoField = "subBosses", category = "Sub-Bosses", }, } local APPEARANCES_TABLE = "Appearances" local RECURRENCE_TIERS = {5, 10, 15}

function p.CargoDeclare(frame) local fields = {} for _, category in ipairs(PER_GAME_CATEGORIES) do		fields[category.cargoField or category.parameter] = "Integer" end fields["mainAppearances"] = "Integer" fields["totalAppearances"] = "Integer" return frame:callParserFunction("#cargo_declare:_table=" .. APPEARANCES_TABLE, fields) end

function p.Main(frame) local args, err = utilsArg.parse(frame:getParent.args, p.Templates.Categories) local appearances = p.appearances(args) local result = p.printNavboxes(args.categories, args, appearances) if utilsPage.inNamespace("") then p.storeAppearances(frame, appearances) end if not utilsPage.inNamespace("User") then result = result .. p.printCategories(args.categories, args) end if err then result = result .. utilsMarkup.categories(err.categories) end return result end

function p.appearances(perGameCategories) local appearances = {} local mainAppearances = {} local totalAppearances = {} for _, category in ipairs(PER_GAME_CATEGORIES) do		local games = perGameCategories[category.parameter] if games then local baseGames = utilsTable.map(games, Franchise.baseGame) local uniqueAppearances = utilsTable.unique(baseGames) local uniqueAppearancesByType = utilsTable.groupBy(uniqueAppearances, Franchise.type) local uniqueMainAppearances = uniqueAppearancesByType["main"] or {} appearances[category.parameter] = uniqueMainAppearances table.insert(mainAppearances, uniqueMainAppearances) table.insert(totalAppearances, uniqueAppearances) else appearances[category.parameter] = {} end end mainAppearances = utilsTable.union(mainAppearances) totalAppearances = utilsTable.union(totalAppearances) appearances["mainAppearances"] = #mainAppearances appearances["totalAppearances"] = #totalAppearances return appearances end

function p.storeAppearances(frame, appearances) local fields = {} for _, category in ipairs(PER_GAME_CATEGORIES) do		fields[category.cargoField or category.parameter] = #appearances[category.parameter] end fields["mainAppearances"] = appearances["mainAppearances"] fields["totalAppearances"] = appearances["totalAppearances"] frame:callParserFunction("#cargo_store:_table=" .. APPEARANCES_TABLE, fields) end

function p.printCategories(plainCategories, perGameCategories) local categories = plainCategories or {} local gameCategoryMap = {} -- keeps track of which games are specified for which categories for _, category in ipairs(PER_GAME_CATEGORIES) 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(PER_GAME_CATEGORIES) 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 p.printNavboxes(categories, perGameCategories, appearances) local result = "" for _, category in ipairs(PER_GAME_CATEGORIES) do		local games = perGameCategories[category.category] if #appearances[category.parameter] > RECURRENCE_TIERS[1] then result = result .. h.printRecurringNavbox(category) elseif games then -- Should we add navs for spinoffs and other games too? p.appearances would need to be changed. local mainGames = utilsTable.filter(games, function(game) 				return Franchise.type(game) == "main" or Franchise.type(game) == "remake"			end) -- Item navs are slow to load so we only load them if there's less than RECURRENCE_TIERS[1] of them. result = result .. h.printGameNavs(category.category, mainGames) end end if categories then local collapse = #categories > 1 for _, category in ipairs(categories) do			local navbox = p.printNavbox(category, collapse) result = result .. navbox end end return result end

function h.printRecurringNavbox(category) return "" -- TODO end

function h.printGameNavs(category, games) return "" -- TODO end

function p.printNavbox(category, collapse) 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("%s", category, category) local seriesLink = Franchise.link("Series") local navboxTitle = string.format("%s in %s", categoryLink, seriesLink) 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 rows = { { title = "All", content = h.rowContent(results) } }		return utilsLayout.CreateRowNavbox(rows, navboxTitle), pagesInCategory end local rows = {} local pagesInRows = 0 for _, 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 table.insert(rows, { 				title = subcategory.display or subcategory.category, 				content = 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 table.insert(rows, {			title = "Other",			content = h.rowContent(results)		}) end return utilsLayout.CreateRowNavbox(rows, navboxTitle, {		collapsed = collapse	}), 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)		if page == mw.title.getCurrentTitle.fullText then			return utilsMarkup.bold(page)		else			return utilsMarkup.link(page, page, true)		end	end) return table.concat(links, " · ") end

function p.Data(frame) local result = "" result = result .. "Max navbox size: " .. data.maxPagesPerNav .." items\n" local tableRows = {} for k in pairs(data.subcategories) do table.insert(tableRows, {utilsMarkup.link("Category:" .. k), p.printNavbox(k)}) end tableRows = utilsTable.sortBy(tableRows, 1) result = result .. utilsLayout.table({		sortable = true,		headers = {"Category", "Navbox", "# Pages"},		rows = tableRows,	}) return result end

local params = {} local paramOrder = {} for _, perGameCategory in ipairs(PER_GAME_CATEGORIES) 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

p.Schemas = { 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 = "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.', keys = { type = "string" }, values = { type = "array", items = { type = "record", properties = { {								name = "category", required = true, type = "string", desc = 'Name of a category that intersects with the "main" 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.", },						},					}				},			},		},	}, }

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", ["places"] = "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