Module:Term

local p = {} local h = {}

local Franchise = require("Module:Franchise") local utilsArg = require("Module:UtilsArg") local utilsCache = require("Module:UtilsCache") local utilsCargo = require("Module:UtilsCargo") local utilsError = require("Module:UtilsError") local utilsMarkup = require("Module:UtilsMarkup") local utilsPage = require("Module:UtilsPage") local utilsString = require("Module:UtilsString") local utilsTable = require("Module:UtilsTable") local utilsVar = require("Module:UtilsVar")

p.Templates = mw.loadData("Module:Term/TemplateData")

local CATEGORY_INVALID_ARGS = require("Module:Constants/category/invalidArgs") local CARGO_TABLE = "Terminologies"

-- In the past Cargo has been iffy with storage from modules, so the actual Cargo store is still done on the actual template. -- We still do the validation + caching layer here, though. function p.TermStore(frame) local args, err = utilsArg.parse(frame:getParent.args, p.Templates["Term/Store"]) local errCategories = err and err.categories or {} local result = args.singularTerm if args.plural and utilsString.isEmpty(args.pluralTerm) then table.insert(errCategories, CATEGORY_INVALID_ARGS) utilsError.warn(" option specified yet no plural term is defined. Using singular form.") elseif args.plural then result = args.pluralTerm end h.storeCache(args) return result .. utilsMarkup.categories(errCategories) end

function p.Singular(frame) local args, err = utilsArg.parse(frame:getParent.args, p.Templates.Term) local printErrorCategories = not utilsPage.inNamespace("User") local result = p.printTerm(args.page, args.game, {		plural = false,		link = args.link,		section = args.section,		display = args.display,		printErrorCategories = printErrorCategories,	}) if err and printErrorCategories then result = result .. utilsMarkup.categories(err.categories) end return result end

function p.Plural(frame) local args, err = utilsArg.parse(frame:getParent.args, p.Templates.Plural) local printErrorCategories = not utilsPage.inNamespace("User") local result = p.printTerm(args.page, args.game, {		plural = true,		link = args.link,		section = args.section,		display = args.display,		printErrorCategories = printErrorCategories,	}) if err and printErrorCategories then result = result .. utilsMarkup.categories(err.categories) end return result end

function p.FetchTerm(frame) local args = frame.args args = utilsTable.mapValues(args, utilsString.trim) args = utilsTable.mapValues(args, utilsString.nilIfEmpty) local term = p.fetchTerm(args.page, args.game, {		plural = args.plural	}) return term end

function p.link(page, game, options) options = utilsTable.merge({}, options or {}, {		link = true	}) return p.printTerm(page, game, options) end

function p.plural(page, game, options) options = utilsTable.merge({}, options or {}, {		plural = true,	}) return p.printTerm(page, game, options) end

function p.pluralLink(page, game, options) local options = utilsTable.merge({}, options or {}, {		link = true,		plural = true,	}) return p.printTerm(page, game, options) end

function p.printTerm(page, game, options) options = options or {} -- If page == nil, Template:Term would otherwise ouptut an empty string and the sentence it's in won't make sense. -- If page == "link", Template:Term would otherwise output "Link", which is almost certainly not what the editor intended -- This makes the sentence nonsensical at best and misinformative at worst. Better to display a bold red error. -- In the former case, it's usually that the editor accidentally added an extra pipe character after the game parameter, making the page argument empty -- e.g. BotW: -- In the latter case, it's usually that editor meant to link to a page but forgot to add either the page parameter or game parameter -- so the link parameter (param #3) took the place of the page parameter (param #2) -- e.g. Stalfos: if not page or page == "link" then error("page parameter cannot be empty") end local term, fetchErrors = p.fetchTerm(page, game, options) local errorCategories = "" if options.printErrorCategories ~= false then local errors = utilsTable.concat(validationErrors or {}, fetchErrors or {}) errorCategories = utilsMarkup.categories(errors) end local result = "" if not term then local errLink = utilsMarkup.sectionLink(page, options.section, options.display) result = utilsMarkup.inline(errLink, {			class = "facelift-term-invalid",			tooltip = "Invalid or missing term",		}) elseif options.link then local baseGame = game and Franchise.baseGame(game) local gameSub = baseGame and Franchise.shortName(baseGame) local section = options.section if not section and gameSub and game ~= "Series" then section = gameSub end result = utilsMarkup.sectionLink(page, section, options.display or term) else result = utilsMarkup.class("term", options.display or term) end -- escape commas for the benefit of templates that split list items by comma, e.g. Module:Infobox result = string.gsub(result, ",", "&#44;") return result .. errorCategories end

function p.fetchTerm(page, game, options) game = game or "Series" options = options or {} local plural = options.plural if not page then return nil end -- Cargo queries don't allow # and it's impossible to have a page with # anyway because of section anchors. -- Ideally, users should input the name of the page where the term is stored (e.g. Swordsman Newsletter 4 instead of Swordsman Newsletter #4) page = string.gsub(page, "#", "") -- Things like return HTML entities. These have to be removed as the "#" character cannot be used in Cargo queries. page = mw.text.decode(page) local term local cacheKey = h.cacheKey(page, game, plural) term = utilsCache.get(cacheKey) if term ~= nil and term ~= "" then -- The cache shouldn't store empty terms, but it used to. It's a good safeguard anyway. return term end -- If a term does not exist for the specified game, we fallback to earlier versions of the game or to the Series term if all else fails -- local baseGame = game and Franchise.baseGame(game) -- local remakes = baseGame and Franchise.remakes(baseGame) -- local games = utilsTable.reverse(remakes or {}) -- if baseGame then -- 	table.insert(games, #games + 1, baseGame) -- end -- table.insert(games, #games + 1, "Series") -- There's some uncertainty as to whether the above behaviour is desired. -- If that gets resolved, the next line can be deleted and the above lines uncommmented -- There's a test case in Module:Term/Documentation/Data that should be uncommented as well local games = game ~= "Series" and {game, "Series"} or {"Series"} local rows = utilsCargo.query("Terminologies=terms, Terminologies__games=termGames", "termGames._value=game, terms.term=term, terms.plural=plural", {		join = "terms._ID=termGames._rowID",		where = utilsCargo.allOf( { ["BINARY _pageName"] = page }, -- BINARY makes the search case-sensitive - we want to show a validation error when folks input the name with improper case utilsCargo.IN("termGames._value", games) )	})	local termsByGame = utilsTable.keyBy(rows, "game") for i, game in ipairs(games) do		term = termsByGame[game] if term then break end end local invalidPlural = term and plural and utilsString.isEmpty(term.plural) and not options.allowSingular if invalidPlural then utilsError.warn(string.format(" term for   has no plural form defined. Using singular form.", game, page)) end local categories = {} if not term or invalidPlural then table.insert(categories, "Articles with Invalid or Missing Terms") local subtitle = Franchise.shortName(game) if subtitle then table.insert(categories, string.format("%s Articles with Invalid or Missing Terms", subtitle)) end end if #categories == 0 or #categories > 0 and mw.title.getCurrentTitle.nsText == "User" then categories = nil end if term and utilsString.notEmpty(term.term) then local cacheKey = h.cacheKey(page, game, false) utilsCache.set(cacheKey, term.term) end if term and utilsString.notEmpty(term.plural) then local cacheKey = h.cacheKey(page, game, true) utilsCache.set(cacheKey, term.plural) end if not term then result = nil elseif plural and utilsString.notEmpty(term.plural) then result = term.plural else result = term.term end return result, categories end

function p.fetchSubjects(term, game) local rows = utilsCargo.query(CARGO_TABLE, "_pageName", {		where = utilsCargo.allOf( { term = term }, game and ("games HOLDS '%s'"):format(game) )	})	return utilsTable.map(rows, "_pageName") end

function h.cacheKey(page, game, plural) local key = string.format("%s.%s.%s", plural and "plural" or "term", game, page) return key end

function h.storeCache(args) local singularTerm = args.singularTerm local pluralTerm = args.pluralTerm local games = args.games local page = mw.title.getCurrentTitle.text h.clearCache(page) local cacheTerms = {} for _, game in ipairs(games or {}) do		if singularTerm and singularTerm ~= "" then local key = h.cacheKey(page, game, false) cacheTerms[key] = singularTerm end if pluralTerm and pluralTerm ~= "" then local key = h.cacheKey(page, game, true) cacheTerms[key] = pluralTerm end end utilsCache.setMulti(cacheTerms) end

-- When loading a page, we clear the cache of its terms once to remove potentially stale cache entries -- For example, say PH: is called when no term is stored for PH -- The Series term is returned as a fallback and that is cached as the term for PH -- If the Series term is changed, the cache entry is updated but the PH entry is not. function h.clearCache(page) local termCacheCleared = utilsVar.get("Module:Term/termCacheCleared") if termCacheCleared then return end for i, game in ipairs(Franchise.enum) do		local termCacheKey = h.cacheKey(page, game, false) local pluralCacheKey = h.cacheKey(page, game, true) p.deleteCacheEntry(termCacheKey) p.deleteCacheEntry(pluralCacheKey) end utilsVar.set("Module:Term/termCacheCleared", "true") end

-- Debug function to delete invalid entries that somehow make their way into the cache -- For example, maybe new validation was added that didn't exist before function p.deleteCacheEntry(key) utilsCache.delete(key) end

function p.Schemas return { printTerm = { page = { type = "string", required = true, desc = "The name of a wiki article from which to retrieve a term.", },			game = { type = "string", default = mw.dumpObject("Series"), desc = "A game code. See Data:Franchise.", },			options = { type = "record", properties = { {						name = "plural", type = "boolean", desc = "If true, the term's plural form is returned.", },					{						name = "allowSingular", type = "boolean", desc = "If true, no error is returned when  is true but only a singular term exists. See Module:ChallengesQuery for example usage.", },					{						name = "link", type = "boolean", desc = "If truthy, the output will link to the page on which the term is stored.", },					{						name = "section", type = "string", desc = "The section to link to when  is enabled. Defaults to the name of  's base game.", },					{						name = "display", type = "string", desc = "Text to display instead of the term when  is enabled.", },				},			}		},		fetchTerm = { page = { type = "string", required = true, desc = "The name of a wiki article from which to retrieve a term.", },			game = { type = "string", default = mw.dumpObject("Series"), desc = "A game code. See Data:Franchise.", },			options = { type = "record", properties = { {						name = "plural", type = "boolean", desc = "If true, the term's plural form is returned.", },					{						name = "allowSingular", type = "boolean", desc = "If true, no error is returned when  is true but only a singular term exists. See Module:ChallengesQuery for example usage.", }				},			}		},		fetchSubjects = { term = { type = "string", required = true, },			game = { type = "string" },		}	} end

function p.Documentation return { FetchTerm = { desc = "Used by Template:Translation/Store to get the raw term without the extra output from Template:Term.", frameParamsOrder = {"page", "game", "plural"}, frameParams = { page = { required = true, },				game = {}, plural = {}, },			cases = { {					args = { page = "2nd Potion", game = "Series", },				},				{					args = { page = "not a page", },				}			},		},		printTerm = { params = {"page", "game", "options"}, returns = "A term with formatting.", cases = { {					args = {"Dynalfos", "OoT"}, expect = ' Dinolfos ', },				{					args = {"Kara Kara Bazaar", "BotW", { link = true, }},					expect = "Kara Kara Bazaar", },				{					args = {"Kara Kara Bazaar", "BotW", { link = true, section = "Shaillu's General Store", }},					expect = "Kara Kara Bazaar", },				{					args = {"Kara Kara Bazaar", "BotW", { link = true, section = "Shaillu's General Store", display = "General Store", }},					expect = "General Store", },				{					args = {"invalid term"}, expect = ' invalid term  ', },			}		},		link = { params = {"page", "game", "options"}, desc = "Shorthand for ", returns = "A link to a term page.", cases = { {					args = {"Bubble"}, expect = "Bubble", },			},		},		plural = { params = {"page", "game", "options"}, desc = "Shorthand for ", returns = "A term in plural form.", cases = { {					args = {"Bubble"}, expect = ' Bubbles ', },			},		},		pluralLink = { params = {"page", "game", "options"}, desc = "Shorthand for ", returns = "A plural link to a term page.", cases = { {					args = {"Bubble"}, expect = "Bubbles", },			},		},		fetchTerm = { params = {"page", "game", "options"}, returns = { "The term for the given article and game, or nil if none found.", "An error category if no term was found.", },			cases = { outputOnly = true, {					args = {"Dynalfos", "OoT"}, expect = { "Dinolfos", nil }, },				{					desc = "Defaults to series term.", args = {"Dinolfos"}, expect = {"Dynalfos"}, },				-- It's still uncertain whether we want this behaviour yet. This test case can be re-enabled or deleted based on the decision. -- {				-- 	desc = "If the term does not exist for the specified remake, it defaults to the term from a previous game version.", -- 	args = {"Flying Tile", "OoT3D"}, -- 	expect = {"Crazy Floor Tile"}, -- },				{					desc = "Defaults to series term when term does not exist for specified game (nor its base game).", args = {"Flying Tile", "TPHD"}, expect = {"Flying Tile"}, },				{					desc = "Error when page does store any terms (game specified).", args = {"Flippityfloppito", "SS"}, expect = {nil, {"Articles with Invalid or Missing Terms", "Skyward Sword Articles with Invalid or Missing Terms"}} },				{					desc = "Error when page does store any terms (no game specified).", args = {"Flippityfloppityfloo"}, expect = {nil, {"Articles with Invalid or Missing Terms", "The Legend of Zelda Series Articles with Invalid or Missing Terms"}} },				{					desc = "Error when page has wrong casing", args = {"captain's hat"}, expect = {nil, {"Articles with Invalid or Missing Terms", "The Legend of Zelda Series Articles with Invalid or Missing Terms"}} },				{					desc = "Plural", args = {"Bubble", "Series", { plural = true }}, expect = {"Bubbles", nil}, },				{					desc = "Returns singular when no plural form exists.", args = {"A Brother's Roast", "BotW", { plural = true }}, expect = { "A Brother's Roast", { "Articles with Invalid or Missing Terms", "Breath of the Wild Articles with Invalid or Missing Terms", }}				},				{					desc = "Returns singular when no plural form exists.", args = {"Hestu", "HWAoC", { plural = true, allowSingular = true, }},					expect = {"Hestu", nil} }			},		},		fetchSubjects = { params = {"term", "game"}, desc = "See Module:Translation Page for usage.", returns = "Returns the names of wiki articles that store the given term. If game is specified, the function will only return articles that store the term for that game.", cases = { {					args = {"Wood"}, expect = {"Wood", "Wood (Character)"}, },				{					args = {"Wood", "ST"}, expect = {"Wood (Character)"}, },				{					args = {"Link", "MM"}, expect = {"Link", "Link (Goron)", "Mr. No Fairy"}, },				{					args = {"Fooloo Limpah"}, expect = {}, },			},		}	} end

return p