Zelda Wiki

Want to contribute to this wiki?
Sign up for an account, and get started!

Come join the Zelda Wiki community Discord server!

READ MORE

Zelda Wiki
(+ some error handling)
No edit summary
 
(61 intermediate revisions by 3 users not shown)
Line 1: Line 1:
 
local p = {}
 
local p = {}
local cargo = mw.ext.cargo
+
local h = {}
  +
local utilsCode = require("Module:UtilsCode")
 
  +
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 utilsError = require("Module:UtilsError")
local utilsGame = require("Module:UtilsGame")
+
local utilsMarkup = require("Module:UtilsMarkup")
local utilsLocalization = require('Module:UtilsLocalization')
+
local utilsPage = require("Module:UtilsPage")
local utilsString = require('Module:UtilsString')
+
local utilsString = require("Module:UtilsString")
local utilsTable = require('Module:UtilsTable')
+
local utilsTable = require("Module:UtilsTable")
  +
local utilsVar = require("Module:UtilsVar")
   
  +
p.Templates = mw.loadData("Module:Term/TemplateData")
function p._Main (frame)
 
  +
local args = frame.args
 
  +
local CARGO_TABLE = "Terminologies"
return p.Main({game = args["game"], term = args["term"], link = args["link"], plural = args["plural"], display = args["display"], section = args["section"], sort = args["sort"]})
 
  +
  +
-- 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("<code>plural</code> 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
 
end
   
function p.Main(args)
+
function p.Singular(frame)
  +
local args, err = utilsArg.parse(frame:getParent().args, p.Templates.Term)
local returnedValue = ""
 
  +
local printErrorCategories = not utilsPage.inNamespace("User")
 
local fetchedTerm = p.fetchRow({game = args["game"], term = args["term"]})
+
local result = p.printTerm(args.page, args.game, {
  +
plural = false,
if utilsCode.IsEmpty(fetchedTerm) then
 
  +
link = args.link,
 
local displayedTerm = args["term"]
+
section = args.section,
if not utilsCode.IsEmpty(args["display"]) then
+
display = args.display,
  +
printErrorCategories = printErrorCategories,
displayedTerm = args["display"]
 
  +
})
end
 
  +
if err and printErrorCategories then
 
  +
result = result .. utilsMarkup.categories(err.categories)
local linkedTerm = args["term"]
 
if not utilsCode.IsEmpty(args["section"]) then
 
linkedTerm = linkedTerm .. "#" .. args["section"]
 
end
 
return "<span class='explain facelift-term-invalid' title='Invalid or missing term'>[[" .. args["term"] .. "|" .. displayedTerm .. "]]</span>[[Category:Pages with Invalid or Missing Terms]][[Category:" .. utilsGame.AbbToGame(args["game"]) .. " Pages with Invalid or Missing Terms]]"
 
else
 
if args["link"] == "link" then
 
local gameSub = utilsGame.AbbToBaseGame(args["game"], true)
 
returnedValue = returnedValue .. "[[" .. args["term"]
 
if not utilsCode.IsEmpty(args["section"]) then
 
returnedValue = returnedValue .. "#" .. args["section"]
 
elseif (gameSub ~= "Unknown" and args["game"] ~= "Series") then
 
returnedValue = returnedValue .. "#" .. gameSub
 
end
 
returnedValue = returnedValue .. "|"
 
else
 
returnedValue = returnedValue .. "<span class='term'>"
 
end
 
 
if not utilsCode.IsEmpty(args["display"]) then
 
returnedValue = returnedValue .. args["display"]
 
elseif args["plural"] == "plural" then
 
returnedValue = returnedValue .. fetchedTerm["plural"]
 
else
 
returnedValue = returnedValue .. fetchedTerm["term"]
 
end
 
 
if args["link"] == "link" then
 
returnedValue = returnedValue .. "]]"
 
else
 
returnedValue = returnedValue .. "</span>"
 
end
 
 
end
 
end
return returnedValue
+
return result
 
end
 
end
   
  +
function p.Plural(frame)
-- Returns raw term, or nil if none found
 
  +
local args, err = utilsArg.parse(frame:getParent().args, p.Templates.Plural)
function p.fetchTerm(args)
 
  +
local printErrorCategories = not utilsPage.inNamespace("User")
local row = p.fetchRow({game = args["game"], term = args["term"]})
 
  +
local result = p.printTerm(args.page, args.game, {
if row then
 
  +
plural = true,
return row.term
 
  +
link = args.link,
else
 
  +
section = args.section,
utilsError.warn("Invalid or missing term %s for game %s", args.term, args.game)
 
  +
display = args.display,
  +
printErrorCategories = printErrorCategories,
  +
})
  +
if err and printErrorCategories then
  +
result = result .. utilsMarkup.categories(err.categories)
 
end
 
end
  +
return result
 
end
 
end
   
function p._fetchTerm(frame)
+
function p.FetchTerm(frame)
 
local args = frame.args
 
local args = frame.args
  +
args = utilsTable.mapValues(args, utilsString.trim)
return p.fetchTerm({game = args["game"], term = args["term"]})
 
  +
args = utilsTable.mapValues(args, utilsString.nilIfEmpty)
  +
local term = p.fetchTerm(args.page, args.game, {
  +
plural = args.plural
  +
})
  +
return term
 
end
 
end
   
  +
function p.link(page, game, options)
-- Returns raw plural
 
  +
options = utilsTable.merge({}, options or {}, {
function p.fetchPlural(args)
 
  +
link = true
return p.fetchRow({game = args["game"], term = args["term"]})["plural"]
 
  +
})
  +
return p.printTerm(page, game, options)
 
end
 
end
   
function p._fetchPlural(frame)
+
function p.plural(page, game, options)
  +
options = utilsTable.merge({}, options or {}, {
local args = frame.args
 
  +
plural = true,
return p.fetchPlural({game = args["game"], term = args["term"]})
 
  +
})
  +
return p.printTerm(page, game, options)
 
end
 
end
   
  +
function p.pluralLink(page, game, options)
-- Returns the table row of the specified game for the specified page.
 
  +
local options = utilsTable.merge({}, options or {}, {
-- Returns the entry for "Series" if not found.
 
  +
link = true,
function p.fetchRow(args)
 
  +
plural = true,
local tables = 'Terminologies'
 
  +
})
local fields = 'games, term, plural'
 
  +
return p.printTerm(page, game, options)
local queryArgs = {
 
  +
end
where = "_pageName = '" .. string.gsub(string.gsub(args["term"], "&#39;", "''"), "'", "''") .. "' AND lang = '" .. utilsLocalization.GetPageLanguage() .. "'"
 
  +
}
 
  +
function p.printTerm(page, game, options)
local result = cargo.query( tables, fields, queryArgs )
 
  +
options = options or {}
 
  +
-- If page == nil, Template:Term would otherwise ouptut an empty string and the sentence it's in won't make sense.
--Looks for the game
 
  +
-- If page == "link", Template:Term would otherwise output "Link", which is almost certainly not what the editor intended
for _, row in pairs(result) do
 
  +
-- This makes the sentence nonsensical at best and misinformative at worst. Better to display a bold red error.
local games = utilsString.split(row.games)
 
  +
-- 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
if utilsTable.keyOf(games, args["game"]) then
 
  +
-- e.g. {{Term|BotW||Shield|link}}
return row
 
  +
-- 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. {{Term|Stalfos|link}}
  +
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
 
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
 
end
 
 
  +
-- Cargo queries don't allow # and it's impossible to have a page with # anyway because of section anchors.
--Else, looks for "Series"
 
  +
-- Ideally, users should input the name of the page where the term is stored (e.g. Swordsman Newsletter 4 instead of Swordsman Newsletter #4)
for _, row in pairs(result) do
 
  +
page = string.gsub(page, "#", "")
local games = utilsString.split(row.games)
 
  +
if utilsTable.keyOf(games, "Series") then
 
  +
-- Things like {{PAGENAME}} return HTML entities. These have to be removed as the "#" character cannot be used in Cargo queries.
return row
 
  +
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
 
end
 
end
 
 
  +
local invalidPlural = term and plural and utilsString.isEmpty(term.plural) and not options.allowSingular
return nil
 
  +
if invalidPlural then
  +
utilsError.warn(string.format("<code>%s</code> term for <code>%s</code> 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 {{Term|PH|Links}} 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
  +
  +
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 <code>plural</code> 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 <code>link</code> is enabled. Defaults to the name of <code>game</code>'s [[Module:Franchise#baseGame|base game]].",
  +
},
  +
{
  +
name = "display",
  +
type = "string",
  +
desc = "Text to display instead of the term when <code>link</code> 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 <code>plural</code> 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 = '<span class="term">Dinolfos</span>',
  +
},
  +
{
  +
args = {"Kara Kara Bazaar", "BotW", {
  +
link = true,
  +
}},
  +
expect = "[[Kara Kara Bazaar#Breath of the Wild|Kara Kara Bazaar]]",
  +
},
  +
{
  +
args = {"Kara Kara Bazaar", "BotW", {
  +
link = true,
  +
section = "Shaillu's General Store",
  +
}},
  +
expect = "[[Kara Kara Bazaar#Shaillu's General Store|Kara Kara Bazaar]]",
  +
},
  +
{
  +
args = {"Kara Kara Bazaar", "BotW", {
  +
link = true,
  +
section = "Shaillu's General Store",
  +
display = "General Store",
  +
}},
  +
expect = "[[Kara Kara Bazaar#Shaillu's General Store|General Store]]",
  +
},
  +
{
  +
args = {"invalid term"},
  +
expect = '<span class="facelift-term-invalid"><span title="Invalid or missing term" class="explain">[[invalid term]]</span></span>[[Category:Articles with Invalid or Missing Terms]][[Category:The Legend of Zelda Series Articles with Invalid or Missing Terms]]',
  +
},
  +
}
  +
},
  +
link = {
  +
params = {"page", "game", "options"},
  +
desc = "Shorthand for <code>printTerm(page, game, { link = true })</code>",
  +
returns = "A link to a term page.",
  +
cases = {
  +
{
  +
args = {"Bubble"},
  +
expect = "[[Bubble|Bubble]]",
  +
},
  +
},
  +
},
  +
plural = {
  +
params = {"page", "game", "options"},
  +
desc = "Shorthand for <code>printTerm(page, game, { plural = true })</code>",
  +
returns = "A term in plural form.",
  +
cases = {
  +
{
  +
args = {"Bubble"},
  +
expect = '<span class="term">Bubbles</span>',
  +
},
  +
},
  +
},
  +
pluralLink = {
  +
params = {"page", "game", "options"},
  +
desc = "Shorthand for <code>printTerm(page, game, { plural = true, link = true })</code>",
  +
returns = "A plural link to a term page.",
  +
cases = {
  +
{
  +
args = {"Bubble"},
  +
expect = "[[Bubble|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
 
end
   

Latest revision as of 02:57, 17 October 2022

This is the main module for the following templates: In addition, this module exports the following functions.

FetchTerm

{{#invoke:Term|FetchTerm|page=|game=|plural=}}

Used by Template:Translation/Store to get the raw term without the extra output from Template:Term.

Parameters

ParameterStatus
pagerequired
gameoptional
pluraloptional

Examples

#InputOutput
1
{{#invoke:Term|FetchTerm|page= 2nd Potion|game= Series}}
Red Water of Life
2
{{#invoke:Term|FetchTerm|page= not a page}}

link

link(page, game, options)

Shorthand for printTerm(page, game, { link = true })

Returns

  • A link to a term page.

Examples

#InputOutputResultStatus
3
link("Bubble", nil, nil)
"[[Bubble|Bubble]]"
Bubble
Green check

plural

plural(page, game, options)

Shorthand for printTerm(page, game, { plural = true })

Returns

  • A term in plural form.

Examples

#InputOutputResultStatus
4
plural("Bubble", nil, nil)
'<span class="term">Bubbles</span>'
Bubbles
Green check

pluralLink

pluralLink(page, game, options)

Shorthand for printTerm(page, game, { plural = true, link = true })

Returns

  • A plural link to a term page.

Examples

#InputOutputResultStatus
5
pluralLink("Bubble", nil, nil)
"[[Bubble|Bubbles]]"
Bubbles
Green check

printTerm

printTerm(page, [game], [options])

Parameters

Returns

  • A term with formatting.

Examples

#InputOutputResultStatus
6
printTerm("Dynalfos", "OoT")
'<span class="term">Dinolfos</span>'
Dinolfos
Green check
7
printTerm("Kara Kara Bazaar", "BotW", { link = true })
Expected
"[[Kara Kara Bazaar#Breath of the Wild|Kara Kara Bazaar]]"
Actual
"[[Kara Kara Bazaar|Kara Kara Bazaar]]"
Kara Kara Bazaar
TFH Red Link desperate
8
printTerm(
  "Kara Kara Bazaar",
  "BotW",
  {
    section = "Shaillu's General Store",
    link = true,
  }
)
"[[Kara Kara Bazaar#Shaillu's General Store|Kara Kara Bazaar]]"
Kara Kara Bazaar
Green check
9
printTerm(
  "Kara Kara Bazaar",
  "BotW",
  {
    display = "General Store",
    section = "Shaillu's General Store",
    link = true,
  }
)
"[[Kara Kara Bazaar#Shaillu's General Store|General Store]]"
General Store
Green check
10
printTerm("invalid term")
'<span class="facelift-term-invalid"><span title="Invalid or missing term" class="explain">[[invalid term]]</span></span>[[Category:Articles with Invalid or Missing Terms]][[Category:The Legend of Zelda Series Articles with Invalid or Missing Terms]]'
invalid term
Green check

fetchTerm

fetchTerm(page, [game], [options])

Parameters

Returns

  • The term for the given article and game, or nil if none found.
  • An error category if no term was found.

Examples

#InputOutputStatus
11
fetchTerm("Dynalfos", "OoT")
"Dinolfos"
Green check
nil
Green check
Defaults to series term.
12
fetchTerm("Dinolfos")
"Dynalfos"
Green check
nil
Green check
Defaults to series term when term does not exist for specified game (nor its base game).
13
fetchTerm("Flying Tile", "TPHD")
"Flying Tile"
Green check
nil
Green check
Error when page does store any terms (game specified).
14
fetchTerm("Flippityfloppito", "SS")
nil
Green check
{
  "Articles with Invalid or Missing Terms",
  "Skyward Sword Articles with Invalid or Missing Terms",
}
Green check
Error when page does store any terms (no game specified).
15
fetchTerm("Flippityfloppityfloo")
nil
Green check
{
  "Articles with Invalid or Missing Terms",
  "The Legend of Zelda Series Articles with Invalid or Missing Terms",
}
Green check
Error when page has wrong casing
16
fetchTerm("captain's hat")
nil
Green check
{
  "Articles with Invalid or Missing Terms",
  "The Legend of Zelda Series Articles with Invalid or Missing Terms",
}
Green check
Plural
17
fetchTerm("Bubble", "Series", { plural = true })
"Bubbles"
Green check
nil
Green check
Returns singular when no plural form exists.
18
fetchTerm("A Brother's Roast", "BotW", { plural = true })
"A Brother's Roast"
Green check
{
  "Articles with Invalid or Missing Terms",
  "Breath of the Wild Articles with Invalid or Missing Terms",
}
Green check
Returns singular when no plural form exists.
19
fetchTerm(
  "Hestu",
  "HWAoC",
  {
    plural = true,
    allowSingular = true,
  }
)
"Hestu"
Green check
nil
Green check

fetchSubjects

fetchSubjects(term, [game])

See Module:Translation Page for usage.

Parameters

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.

Examples

#InputOutputResult
20
fetchSubjects("Wood")
{"Wood", "Wood (Character)"}
Green check
21
fetchSubjects("Wood", "ST")
{"Wood (Character)"}
Green check
22
fetchSubjects("Link", "MM")
Expected
{"Link", "Link (Goron)", "Mr. No Fairy"}
Actual
{"Link (Goron)", "Mr. No Fairy"}
TFH Red Link desperate
23
fetchSubjects("Fooloo Limpah")
{}
Green check

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")

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

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("<code>plural</code> 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. {{Term|BotW||Shield|link}}
	-- 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. {{Term|Stalfos|link}}
	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 {{PAGENAME}} 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("<code>%s</code> term for <code>%s</code> 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 {{Term|PH|Links}} 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

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 <code>plural</code> 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 <code>link</code> is enabled. Defaults to the name of <code>game</code>'s [[Module:Franchise#baseGame|base game]].",
					},
					{
						name = "display",
						type = "string",
						desc = "Text to display instead of the term when <code>link</code> 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 <code>plural</code> 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 = '<span class="term">Dinolfos</span>',
				},
				{
					args = {"Kara Kara Bazaar", "BotW", {
						link = true,
					}},
					expect = "[[Kara Kara Bazaar#Breath of the Wild|Kara Kara Bazaar]]",
				},
				{
					args = {"Kara Kara Bazaar", "BotW", {
						link = true,
						section = "Shaillu's General Store",
					}},
					expect = "[[Kara Kara Bazaar#Shaillu's General Store|Kara Kara Bazaar]]",
				},
				{
					args = {"Kara Kara Bazaar", "BotW", {
						link = true,
						section = "Shaillu's General Store",
						display = "General Store",
					}},
					expect = "[[Kara Kara Bazaar#Shaillu's General Store|General Store]]",
				},
				{
					args = {"invalid term"},
					expect = '<span class="facelift-term-invalid"><span title="Invalid or missing term" class="explain">[[invalid term]]</span></span>[[Category:Articles with Invalid or Missing Terms]][[Category:The Legend of Zelda Series Articles with Invalid or Missing Terms]]',
				},
			}
		},
		link = {
			params = {"page", "game", "options"},
			desc = "Shorthand for <code>printTerm(page, game, { link = true })</code>",
			returns = "A link to a term page.",
			cases = {
				{
					args = {"Bubble"},
					expect = "[[Bubble|Bubble]]",
				},
			},
		},
		plural = {
			params = {"page", "game", "options"},
			desc = "Shorthand for <code>printTerm(page, game, { plural = true })</code>",
			returns = "A term in plural form.",
			cases = {
				{
					args = {"Bubble"},
					expect = '<span class="term">Bubbles</span>',
				},
			},
		},
		pluralLink = {
			params = {"page", "game", "options"},
			desc = "Shorthand for <code>printTerm(page, game, { plural = true, link = true })</code>",
			returns = "A plural link to a term page.",
			cases = {
				{
					args = {"Bubble"},
					expect = "[[Bubble|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