Module:Term

local p = {} local h = {}

local cache = mw.ext.LuaCache

local Franchise = require("Module:Franchise") local utilsArg = require("Module:UtilsArg") 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 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, "Articles with Invalid Arguments") 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", "MediaWiki"}) -- MediaWiki namespace is listed here to prevent a weird bug with MediaWiki:Gadget-EditToolbarButtons.js	local result = p.printTerm(args, false, 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", "MediaWiki"}) local result = p.printTerm(args, true, 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) return p.fetchTerm(args.term, args.game) end

function p.printTerm(args, plural, printErrorCategories) -- If args.page == nil, Template:Term would otherwise ouptut an empty string and the sentence it's in won't make sense. -- If args.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 args.page or args.page == "link" then error("page parameter cannot be empty") end -- args.plural has been deprecated for a while now and most likely could be removed -- but there's no harm in leaving it in just in case local term, fetchErrors = p.fetchTerm(args.page, args.game, plural or args.plural) local errorCategories = "" if printErrorCategories then local errors = utilsTable.concat(validationErrors or {}, fetchErrors or {}) errorCategories = utilsMarkup.categories(errors) end local result = "" if not term then local errLink = utilsMarkup.sectionLink(args.page, args.section, args.display) result = utilsMarkup.inline(errLink, {			class = "facelift-term-invalid",			tooltip = "Invalid or missing term",		}) elseif args.link == "link" then local gameSub = args.game and Franchise.shortName(args.game) if not args.section and gameSub and args.game ~= "Series" then args.section = gameSub end result = utilsMarkup.sectionLink(args.page, args.section, args.display or term) else result = utilsMarkup.class("term", args.display or term) end return result .. errorCategories end

function p.fetchTerm(page, game, 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 cacheKey = h.cacheKey(page, game, plural) local term = cache.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 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", {"Series", game}) )	})	local termsByGame = utilsTable.keyBy(rows, "game") local term = termsByGame[game or "Series"] or termsByGame["Series"] local subtitle = Franchise.shortName(game or "Series") local errCategories = {} if mw.title.getCurrentTitle.nsText ~= "User" then table.insert(errCategories, "Articles with Invalid or Missing Terms") if subtitle then table.insert(errCategories, string.format("%s Articles with Invalid or Missing Terms", subtitle)) end end if not term then return nil, errCategories elseif plural and utilsString.isEmpty(term.plural) then utilsError.warn(string.format("Term  has no plural form defined. Using singular form.", term.term)) return term.term, errCategories elseif plural then cache.set(cacheKey, term.plural) return term.plural else cache.set(cacheKey, term.term) return term.term end 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 or "Series", 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 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 cache.setMulti(cacheTerms) 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) cache.delete(key) end

p.Schemas = { 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.", },		plural = { type = "boolean", desc = "If true, retrieves the plural form." },	},	fetchSubjects = { term = { type = "string", required = true, },		game = { type = "string" }	} }

p.Documentation = { fetchTerm = { params = {"page", "game", "plural"}, 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"}, },			{				desc = "Defaults to series term when term does not exist for specified game.", args = {"Dinolfos", "ALttP"}, expect = {"Dynalfos"}, },			{				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", true}, expect = {"Bubbles", nil}, },			{				desc = "Returns singular when no plural form exists.", args = {"A Brother's Roast", "BotW", true}, expect = { "A Brother's Roast", { "Articles with Invalid or Missing Terms", "Breath of the Wild Articles with Invalid or Missing Terms", }}			}		},	},	fetchSubjects = { params = {"term", "game"}, 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 = {}, },		},	} }

p.Templates = { Term = { purpose = "Returns the proper singular form of a term for any given topic in . Terms are stored using Template:Term/Store.", format = "inline", params = { [1] = {				name = "game", type = "string", enum = Franchise.enum({ includeSeries = true }), desc = "The game from which to fetch the term of the given subject. Defaults to .", trim = true, nilIfEmpty = true, },			[2] = {				name = "page", type = "wiki-page-name", desc = "The name of the page for said subject.", trim = true, nilIfEmpty = true, },			[3] = {				name = "link", type = "string", desc = "Entering anything in this field will output the result as a link. (Enter  for standardization)", trim = true, nilIfEmpty = true, },			display = { type = "string", desc = "Alternative display text for term.", trim = true, nilIfEmpty = true, },			section = { type = "string", desc = "Section of  to link to.", trim = true, nilIfEmpty = true, },		}	},	["Term/Store"] = { purpose = "Used in article leads to store terms, for use by Template:Term and other modules.", format = "inline", paramOrder = {1, 2, 3, 4}, params = { [1] = {				name = "singularTerm", type = "string", required = true, desc = "The singular form of the term.", },			[2] = {				name = "pluralTerm", type = "string", required = true, desc = "The plural form of the term. Leave empty for characters or other terms where no plural form applies.", trim = true, },			[3] = {				name = "games", type = "string", required = true, enum = Franchise.enum({ includeSeries = true }), desc = "Comma-separated list of games to which the term applies. For example, .", split = true, trim = true, nilIfEmpty = true, },			[4] = {				name = "plural", type = "string", desc = "Entering  here will make the template output the plural term instead of the singular.", canOmit = true, },		},	}, } p.Templates.Plural = p.Templates.Term

return p