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

-- 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 = h.printNavboxes(args) 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 title = mw.title.getCurrentTitle local isTemplatePage = string.find(title.fullText, "^Template:Categories/Navbox")

local categories = ""

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 isValid = true 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 isValid = false end end if isValid then 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.."" end if isTemplatePage then -- Makes the navbox look like it would when it is actually used local styles = frame:extensionTag({			name = "templatestyles",			args = { src = "Template:Categories/Styles.css" },		}) navbox = navbox and (styles..''..navbox..' ') return navbox or "", categories else return navbox 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("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 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) 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, groups) local pagesInCategory = tonumber(mw.getCurrentFrame:callParserFunction("PAGESINCATEGORY", category, "R", "pages")) if pagesInCategory == 0 or pagesInCategory > Data.maxPagesPerNav then return nil, pagesInCategory end local categoryLink = string.format("Category:%s", category) local dplArgs = { namespace = "", includeSubpages = false, skipthispage = "no", orderMethod = "title", category = category, }	groups = groups if not groups 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 errCategories = "" local navboxArgs = { id = "category-"..category, title = category, footer = categoryLink, }	local pagesInRows = 0 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 table.sort(groupPages) if #groupPages > 0 then navboxArgs["group"..i] = group.display or group.category[1] navboxArgs["links"..i] = h.rowContent(groupPages) else local utilsError = require("Module:UtilsError") utilsError.warn(string.format("Group %d has 0 entries.", i)) errCategories = errCategories.."" end end -- "Other" row for i, group in ipairs(groups) do		for j, category in ipairs(group.category) do			table.insert(dplArgs, {				param = "notcategory",				value = category			}) end end local results = utilsPage.dpl(dplArgs) 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 currentPage = string.gsub(mw.title.getCurrentTitle.fullText, ",", "&#44;")		if page == currentPage then			return string.format("%s", page)		else			return string.format("%s", page, page)		end	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 subpages = utilsPage.getSubpages("Template:Categories/Navbox") subpages = utilsTable.filter(subpages, function(subpage)		return not string.find(subpage, "/Documentation$")	end) subpages = utilsTable.map(subpages, function(subpage)		local display = string.match(subpage, "Template:Categories/Navbox/(.*)")		return string.format("%s", subpage, display)	end) subpages = utilsMarkup.bulletList(subpages) local desc = "The following navbox templates are automatically added by Template:Categories." local groupedNavboxes = desc..subpages

local queryResults = 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",		limit = 1000,	}) local autoNavCategories, fatCats = utilsTable.partition(queryResults, function(cat)		return tonumber(cat.pageCount) <= Data.maxPagesPerNav	end)

local tableRows = {} for i, cat in ipairs(autoNavCategories) do		local category = cat.category local pageCount = tonumber(cat.pageCount) local sample = cat.sample if not utilsPage.exists("Template:Categories/Navbox/"..category) then local categoryLink = "Category:"..category.."" local navboxId = "navbox-category-"..utilsString.kebabCase(category) local sampleLink = string.format("%s", sample, navboxId, sample) table.insert(tableRows, {pageCount, categoryLink, sampleLink}) end end -- Sort descending by number of pages table.sort(tableRows, function(a, b)		return a[1] > b[1]	end) local ungroupedNavboxes = utilsLayout.table({		sortable = true,		headers = {"", "Category", "Sample"},		rows = tableRows,	}) local desc = "" .." The following categories have navboxes automatically generated by Template:Categories as no corresponding grouped navbox template has been created for them. These navboxes are ungrouped–all category entries are shown in a single list. " .. " Navboxes with more than 7 links or so should have a grouped navbox template created for them with . Navboxes with too many links per group are less effective as navigation tools as the page lists exceed human . " .." 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. " desc = frame:preprocess(desc) ungroupedNavboxes = desc .. ungroupedNavboxes 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 tabs = utilsLayout.tabs({		{			label = "Common Subject Navboxes",			content = commonSubjectNavboxes,		},		{			label = "Grouped Navboxes",			content = groupedNavboxes,		},		{			label = "Ungrouped Navboxes",			content = ungroupedNavboxes,		},		{			label = "Big Categories",			content = fatCats,		}	})

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 navboxes with curated subgroups. These navboxes override the automatic single-group navboxes generated by Template:Categories.", repeatedGroup = { name = "groups", params = {"category", "display"}, 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 subgroup. Defaults to  prefix. The navbox group will contain all pages that are in both this category and the navbox category.", trim = true, nilIfEmpty = true, split = true, },		},		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= "", },			{				"Hylians" },			{				args = {""}, },			{				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 = "", },			},			{				desc = "Non-existent grouping categories are ignored.", args = { "Medals", category1= "Items in Skyward Sword", category2= "Not a Category", },			},		},	}, }

return p