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) 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 = 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 ~= "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) cache.set(cacheKey, term.term) end if term and utilsString.notEmpty(term.plural) then local cacheKey = h.cacheKey(page, game, true) cache.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 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, 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) cache.delete(key) end

return p