MannedTooth (talk | contribs) (converting to external links, effectively unclogging the whatlinkshere pages) |
m ((For some reason my edit never saved.) Standardizing: Places -> Locations) |
||
(46 intermediate revisions by 5 users not shown) | |||
Line 1: | Line 1: | ||
local p = {} |
local p = {} |
||
local h = {} |
local h = {} |
||
+ | |||
− | local cargo = mw.ext.cargo |
||
− | local |
+ | local Franchise = require("Module:Franchise") |
− | local |
+ | local utilsArg = require("Module:UtilsArg") |
− | local |
+ | local utilsLayout = require("Module:UtilsLayout") |
+ | local utilsMarkup = require("Module:UtilsMarkup") |
||
+ | local utilsPage = require("Module:UtilsPage") |
||
local utilsTable = require("Module:UtilsTable") |
local utilsTable = require("Module:UtilsTable") |
||
-- See the module for documentation about subcategories. |
-- See the module for documentation about subcategories. |
||
− | local |
+ | local data = mw.loadData("Module:Categories/Data") |
+ | local PER_GAME_CATEGORIES = { |
||
+ | { |
||
+ | parameter = "bosses", |
||
+ | category = "Bosses", |
||
+ | }, |
||
+ | { |
||
+ | parameter = "characters", |
||
+ | category = "Characters", |
||
+ | }, |
||
+ | { |
||
+ | parameter = "challenges", |
||
+ | category = "Challenges", |
||
+ | }, |
||
+ | { |
||
+ | parameter = "dungeons", |
||
+ | category = "Dungeons", |
||
+ | }, |
||
+ | { |
||
+ | parameter = "enemies", |
||
+ | category = "Enemies", |
||
+ | }, |
||
+ | { |
||
+ | parameter = "items", |
||
+ | category = "Items", |
||
+ | }, |
||
+ | { |
||
+ | parameter = "levels", |
||
+ | category = "Levels", |
||
+ | }, |
||
+ | { |
||
+ | parameter = "locations", |
||
+ | category = "Locations", |
||
+ | }, |
||
+ | { |
||
+ | parameter = "mini-games", |
||
+ | cargoField = "miniGames", |
||
+ | category = "Mini-Games", |
||
+ | }, |
||
+ | { |
||
+ | parameter = "objects", |
||
+ | category = "Objects", |
||
+ | }, |
||
+ | { |
||
+ | parameter = "playable", |
||
+ | category = "Playable Characters", |
||
+ | }, |
||
+ | { |
||
+ | parameter = "quests", |
||
+ | category = "Quests", |
||
+ | }, |
||
+ | { |
||
+ | parameter = "side-quests", |
||
+ | cargoField = "sideQuests", |
||
+ | category = "Side Quests", |
||
+ | }, |
||
+ | { |
||
+ | parameter = "songs", |
||
+ | category = "Songs", |
||
+ | }, |
||
+ | { |
||
+ | parameter = "stages", |
||
+ | category = "Stages", |
||
+ | }, |
||
+ | { |
||
+ | parameter = "sub-bosses", |
||
+ | cargoField = "subBosses", |
||
+ | category = "Sub-Bosses", |
||
+ | }, |
||
+ | } |
||
+ | local APPEARANCES_TABLE = "Appearances" |
||
− | function p. |
+ | function p.CargoDeclare(frame) |
− | local |
+ | local fields = {} |
+ | for _, category in ipairs(PER_GAME_CATEGORIES) do |
||
− | return p.CategorizeEntries(frame.args["plain"], args["bosses"], args["characters"], args["dungeons"], args["enemies"], args["items"], args["objects"], args["places"], args["songs"], args["stages"], args["sub-bosses"]) |
||
+ | fields[category.cargoField or category.parameter] = "Integer" |
||
+ | end |
||
+ | fields["mainAppearances"] = "Integer" |
||
+ | fields["totalAppearances"] = "Integer" |
||
+ | return frame:callParserFunction("#cargo_declare:_table=" .. APPEARANCES_TABLE, fields) |
||
end |
end |
||
+ | function p.Main(frame) |
||
− | function p.CategorizeEntries(plain, bosses, characters, dungeons, enemies, items, objects, places, songs, stages, subbosses) |
||
+ | local args, err = utilsArg.parse(frame:getParent().args, p.Templates.Categories) |
||
− | local result = "" |
||
+ | local appearances = p.appearances(args) |
||
− | result = result .. p.PlainToNavboxes(plain) |
||
+ | local result = p.printNavboxes(args.categories, args, appearances) |
||
− | result = result .. p.PlainToCategories(plain) |
||
+ | if utilsPage.inNamespace("") then |
||
− | result = result .. p.GamesToCategories(bosses, characters, dungeons, enemies, items, objects, places, songs, stages, subbosses) |
||
+ | 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 |
return result |
||
end |
end |
||
− | function p. |
+ | function p.appearances(perGameCategories) |
+ | local appearances = {} |
||
− | result = "" |
||
+ | local mainAppearances = {} |
||
− | local plaintable = mw.text.split(plain, '%s*,%s*') |
||
+ | local totalAppearances = {} |
||
− | |||
+ | for _, category in ipairs(PER_GAME_CATEGORIES) do |
||
− | local navboxTitle = "" |
||
+ | local games = perGameCategories[category.parameter] |
||
− | local rows = {} |
||
+ | if games then |
||
− | local notCategories = "" |
||
+ | local baseGames = utilsTable.map(games, Franchise.baseGame) |
||
− | local dplQuery = "" |
||
+ | local uniqueAppearances = utilsTable.unique(baseGames) |
||
− | local dplParameters = '|namespace=|includesubpages=false|skipthispage=no|mode=userformat|format=,%PAGE%,;,|ordermethod=titlewithoutnamespace|noresultsheader= }}' |
||
+ | local uniqueAppearancesByType = utilsTable.groupBy(uniqueAppearances, Franchise.type) |
||
− | |||
+ | local uniqueMainAppearances = uniqueAppearancesByType["main"] or {} |
||
− | -- For every category entered in the template |
||
+ | appearances[category.parameter] = uniqueMainAppearances |
||
− | for key, category in ipairs(plaintable) do |
||
+ | table.insert(mainAppearances, uniqueMainAppearances) |
||
− | rows = {} |
||
+ | table.insert(totalAppearances, uniqueAppearances) |
||
− | if not utilsCode.IsEmpty(category) then |
||
− | + | else |
|
+ | appearances[category.parameter] = {} |
||
− | -- Sets the title, which is "[[:Category:X|X]] in {{TLoZ|Series}}" |
||
− | navboxTitle = "[[:Category:" .. category .. "|" .. category .. "]] in " .. mw.getCurrentFrame():expandTemplate{ title = "TLoZ", args = { "Series" } } |
||
− | |||
− | -- If subcategories exist, handles that |
||
− | if not utilsCode.IsEmpty(SUBCATEGORIES[category]) then |
||
− | notCategories = "" |
||
− | for key2, subCategory in ipairs(SUBCATEGORIES[category]) do |
||
− | notCategories = notCategories .. "|notcategory=" .. subCategory |
||
− | dplQuery = h.dplToExternalLinks(mw.getCurrentFrame():preprocess("{{#dpl:|category=" .. category .. "|category=" .. subCategory .. dplParameters)) |
||
− | |||
− | if not (dplQuery == " ") then |
||
− | table.insert(rows, {title = subCategory, content = dplQuery}) |
||
− | end |
||
− | end |
||
− | |||
− | -- Other(s) row |
||
− | dplQuery = h.dplToExternalLinks(mw.getCurrentFrame():preprocess("{{#dpl:|category=" .. category .. notCategories .. dplParameters)) |
||
− | if not (dplQuery == " ") then |
||
− | table.insert(rows, {title = "Other(s)", content = dplQuery}) |
||
− | end |
||
− | result = result .. utilsNavbox.CreateRowNavbox(rows, navboxTitle) |
||
− | |||
− | -- else just outputs everything |
||
− | else |
||
− | dplQuery = h.dplToExternalLinks(mw.getCurrentFrame():preprocess("{{#dpl:|category=" .. category .. dplParameters)) |
||
− | if not (dplQuery == " ") then |
||
− | result = result .. utilsNavbox.CreateNavbox(dplQuery, navboxTitle) |
||
− | end |
||
− | end |
||
− | |||
end |
end |
||
end |
end |
||
+ | mainAppearances = utilsTable.union(mainAppearances) |
||
− | return tostring(result) |
||
+ | totalAppearances = utilsTable.union(totalAppearances) |
||
+ | appearances["mainAppearances"] = #mainAppearances |
||
+ | appearances["totalAppearances"] = #totalAppearances |
||
+ | return appearances |
||
end |
end |
||
− | function |
+ | function p.storeAppearances(frame, appearances) |
− | local |
+ | local fields = {} |
+ | for _, category in ipairs(PER_GAME_CATEGORIES) do |
||
− | local firstLink = true |
||
+ | fields[category.cargoField or category.parameter] = #appearances[category.parameter] |
||
− | dplResult = mw.text.split(dplResult, '%s*;%s*') |
||
− | -- Removing the last entry in the table since it's "empty" due to how DPL |
||
− | -- adds a ";" at the end of each entry instead of just in-between entries |
||
− | table.remove(dplResult) |
||
− | for key, link in ipairs(dplResult) do |
||
− | if firstLink == true then |
||
− | firstLink = false |
||
− | else |
||
− | result = result .. ' <b>·</b> ' |
||
− | end |
||
− | result = result .. '<span class="plainlinks">[https://zelda.gamepedia.com/' .. mw.getCurrentFrame():callParserFunction{name = 'urlencode', args = link} .. ' ' .. link .. ']</span>' |
||
end |
end |
||
+ | fields["mainAppearances"] = appearances["mainAppearances"] |
||
− | return result |
||
+ | fields["totalAppearances"] = appearances["totalAppearances"] |
||
+ | frame:callParserFunction("#cargo_store:_table=" .. APPEARANCES_TABLE, fields) |
||
end |
end |
||
− | function p. |
+ | function p.printCategories(plainCategories, perGameCategories) |
+ | local categories = plainCategories or {} |
||
− | result = "" |
||
+ | local gameCategoryMap = {} -- keeps track of which games are specified for which categories |
||
− | local plaintable = mw.text.split(plain, '%s*,%s*') |
||
+ | |||
− | for key, category in ipairs(plaintable) do |
||
+ | for _, category in ipairs(PER_GAME_CATEGORIES) do |
||
− | if not utilsCode.IsEmpty(category) then |
||
+ | local gamesInCategory = perGameCategories[category.parameter] |
||
− | result = result .. "[[Category:" .. category .. "]]" |
||
+ | if gamesInCategory then |
||
+ | table.insert(categories, category.category) |
||
+ | gameCategoryMap[category.parameter] = utilsTable.invert(gamesInCategory) |
||
end |
end |
||
end |
end |
||
+ | |||
− | return result |
||
+ | 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 |
end |
||
+ | function p.printNavboxes(categories, perGameCategories, appearances) |
||
− | function p.GamesToCategories(bosses, characters, dungeons, enemies, items, objects, places, songs, stages, subbosses) |
||
local result = "" |
local result = "" |
||
− | + | if categories then |
|
+ | local collapse = #categories > 1 |
||
− | local sortOrder = utilsGame.GetSortOrder("canon") |
||
+ | for _, category in ipairs(categories) do |
||
− | |||
+ | local navbox = p.printNavbox(category, collapse) |
||
− | if not (utilsCode.IsEmpty(bosses)) then |
||
+ | result = result .. navbox |
||
− | bosses = mw.text.split(bosses, '%s*,%s*') |
||
− | for _, value in ipairs(bosses) do |
||
− | table.insert(categories, {"Bosses", value}) |
||
end |
end |
||
end |
end |
||
+ | -- TODO: Print navs for per-game categories. |
||
− | if not (utilsCode.IsEmpty(characters)) then |
||
+ | -- TODO: Print navs for recurring entities (see "appearances" object) |
||
− | characters = mw.text.split(characters, '%s*,%s*') |
||
+ | return result |
||
− | for _, value in ipairs(characters) do |
||
+ | end |
||
− | table.insert(categories, {"Characters", value}) |
||
+ | |||
− | 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 |
end |
||
+ | |||
− | if not (utilsCode.IsEmpty(dungeons)) then |
||
+ | local categoryLink = string.format("[[:Category:%s|%s]]", category, category) |
||
− | dungeons = mw.text.split(dungeons, '%s*,%s*') |
||
+ | local seriesLink = Franchise.link("Series") |
||
− | for _, value in ipairs(dungeons) do |
||
+ | local navboxTitle = string.format("%s in %s", categoryLink, seriesLink) |
||
− | table.insert(categories, {"Dungeons", value}) |
||
+ | |||
− | end |
||
+ | 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 |
end |
||
+ | |||
− | if not utilsCode.IsEmpty(enemies) then |
||
+ | local rows = {} |
||
− | enemies = mw.text.split(enemies, '%s*,%s*') |
||
+ | local pagesInRows = 0 |
||
− | for _, value in ipairs(enemies) do |
||
+ | for _, subcategory in ipairs(subcategories) do |
||
− | table.insert(categories, {"Enemies", value}) |
||
+ | 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 |
end |
||
+ | local results = utilsPage.dpl(rowDplArgs) |
||
− | end |
||
− | + | if #results > 0 then |
|
+ | table.insert(rows, { |
||
− | items = mw.text.split(items, '%s*,%s*') |
||
+ | title = subcategory.display or subcategory.category, |
||
− | for _, value in ipairs(items) do |
||
+ | content = h.rowContent(results) |
||
− | table.insert(categories, {"Items", value}) |
||
+ | }) |
||
end |
end |
||
end |
end |
||
+ | |||
− | if not (utilsCode.IsEmpty(objects)) then |
||
+ | -- "Other" row |
||
− | objects = mw.text.split(objects, '%s*,%s*') |
||
− | + | for _, subcategory in ipairs(subcategories) do |
|
− | + | table.insert(dplArgs, { |
|
+ | param = "notcategory", |
||
− | end |
||
+ | value = subcategory.category |
||
+ | }) |
||
end |
end |
||
+ | local results = utilsPage.dpl(dplArgs) |
||
− | if not (utilsCode.IsEmpty(places)) then |
||
+ | if #results > 0 then |
||
− | places = mw.text.split(places, '%s*,%s*') |
||
+ | table.insert(rows, { |
||
− | for _, value in ipairs(places) do |
||
+ | title = "Other", |
||
− | table.insert(categories, {"Places", value}) |
||
+ | content = h.rowContent(results) |
||
− | end |
||
+ | }) |
||
end |
end |
||
+ | return utilsLayout.CreateRowNavbox(rows, navboxTitle, { |
||
− | if not (utilsCode.IsEmpty(songs)) then |
||
+ | collapsed = collapse |
||
− | songs = mw.text.split(songs, '%s*,%s*') |
||
+ | }), pagesInCategory |
||
− | for _, value in ipairs(songs) do |
||
+ | end |
||
− | table.insert(categories, {"Songs", value}) |
||
+ | 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 |
||
+ | end) |
||
+ | return table.concat(links, " <b>·</b> ") |
||
+ | 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 |
end |
||
+ | tableRows = utilsTable.sortBy(tableRows, 1) |
||
− | if not (utilsCode.IsEmpty(stages)) then |
||
+ | result = result .. utilsLayout.table({ |
||
− | stages = mw.text.split(stages, '%s*,%s*') |
||
+ | sortable = true, |
||
− | for _, value in ipairs(stages) do |
||
+ | headers = {"Category", "Navbox", "# Pages"}, |
||
− | table.insert(categories, {"Stages", value}) |
||
+ | rows = tableRows, |
||
− | end |
||
− | + | }) |
|
− | if not (utilsCode.IsEmpty(subbosses)) then |
||
− | subbosses = mw.text.split(subbosses, '%s*,%s*') |
||
− | for _, value in ipairs(subbosses) do |
||
− | table.insert(categories, {"Sub-Bosses", value}) |
||
− | end |
||
− | end |
||
− | |||
− | for _, game in ipairs(sortOrder) do |
||
− | for _2, category in ipairs(categories) do |
||
− | if game == category[2] then |
||
− | result = result .. "[[Category:" .. category[1] .. " in " .. utilsGame.AbbToGame(category[2], "true") .. "]]" |
||
− | end |
||
− | end |
||
− | end |
||
− | |||
− | |||
return result |
return result |
||
end |
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 [[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 |
||
+ | |||
+ | 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 [[: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", |
||
+ | ["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 |
return p |
Revision as of 02:29, 13 October 2021
This is the main module for the following templates:
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 = "challenges",
category = "Challenges",
},
{
parameter = "dungeons",
category = "Dungeons",
},
{
parameter = "enemies",
category = "Enemies",
},
{
parameter = "items",
category = "Items",
},
{
parameter = "levels",
category = "Levels",
},
{
parameter = "locations",
category = "Locations",
},
{
parameter = "mini-games",
cargoField = "miniGames",
category = "Mini-Games",
},
{
parameter = "objects",
category = "Objects",
},
{
parameter = "playable",
category = "Playable Characters",
},
{
parameter = "quests",
category = "Quests",
},
{
parameter = "side-quests",
cargoField = "sideQuests",
category = "Side Quests",
},
{
parameter = "songs",
category = "Songs",
},
{
parameter = "stages",
category = "Stages",
},
{
parameter = "sub-bosses",
cargoField = "subBosses",
category = "Sub-Bosses",
},
}
local APPEARANCES_TABLE = "Appearances"
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 = ""
if categories then
local collapse = #categories > 1
for _, category in ipairs(categories) do
local navbox = p.printNavbox(category, collapse)
result = result .. navbox
end
end
-- TODO: Print navs for per-game categories.
-- TODO: Print navs for recurring entities (see "appearances" object)
return result
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("[[:Category:%s|%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, " <b>·</b> ")
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 [[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
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 [[: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",
["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