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 utilsVar = require("Module:UtilsVar")

local DocData = mw.loadData("Module:Term/Documentation/Data")

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, DocData.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, DocData.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, DocData.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 return result .. errorCategories end

function p.fetchTerm(page, game, options) 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 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 -- 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 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") -- We also default to "Series" game == nil but no term exists for the game. This is subtly different from defaulting to "Series" if game is nil in the first place. local term = termsByGame[game or "Series"] local termGame = game or "Series" for i, fallback in ipairs(games) do		term = termsByGame[fallback] if term then termGame = fallback ~= "Series" and fallback or nil break end end local errCategories = {} if mw.title.getCurrentTitle.nsText ~= "User" then table.insert(errCategories, "Articles with Invalid or Missing Terms") local subtitle = termGame and Franchise.shortName(termGame) if termGame and 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) and not options.allowSingular then utilsError.warn(string.format("Term  has no plural form defined. Using singular form.", term.term)) return term.term, errCategories elseif plural and utilsString.isEmpty(term.plural) then return term.term 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 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 cache.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, false) 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) cache.delete(key) end

return p