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
mNo edit summary
mNo edit summary
(27 intermediate revisions by the same user not shown)
Line 2: Line 2:
 
local h = {}
 
local h = {}
   
  +
local cache = mw.ext.LuaCache
  +
  +
local Franchise = require("Module:Franchise")
 
local utilsArg = require("Module:UtilsArg")
 
local utilsArg = require("Module:UtilsArg")
 
local utilsCargo = require("Module:UtilsCargo")
 
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 utilsMarkup = require("Module:UtilsMarkup")
  +
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')
Line 12: Line 15:
 
local CARGO_TABLE = "Terminologies"
 
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.
-- Deprecated
 
  +
-- We still do the validation + caching layer here, though.
function p._Main(frame)
 
  +
function p.TermStore(frame)
local args = frame.args
 
  +
local args, err = utilsArg.parse(frame:getParent().args, p.Templates["Term/Store"])
args = utilsTable.mapValues(utilsString.nilIfEmpty)(args)
 
  +
local errCategories = err and err.categories or {}
return p.printTerm({game = args["game"], page = args["term"], link = args["link"], plural = args["plural"], display = args["display"], section = args["section"]})
 
  +
local result = args.singularTerm
end
 
  +
if args.plural and utilsString.isEmpty(args.pluralTerm) then
 
  +
table.insert(errCategories, "Pages with Invalid Arguments")
-- Deprecated
 
  +
utilsError.warn("<code>plural</code> option specified yet no plural term is defined. Using singular form.")
function p.Main(args)
 
args.page = args.term
+
elseif args.plural then
  +
result = args.pluralTerm
args = utilsTable.mapValues(mw.text.trim)(args)
 
  +
end
args = utilsTable.mapValues(utilsString.nilIfEmpty)(args)
 
return p.printTerm(args)
+
h.storeCache(args)
  +
return result .. utilsMarkup.categories(errCategories)
end
 
 
local function getArgs(frame)
 
local args = frame:getParent().args
 
args = utilsTable.mapValues(mw.text.trim)(args)
 
args = utilsTable.mapValues(utilsString.nilIfEmpty)(args)
 
return {
 
game = args[1],
 
page = args[2],
 
link = args[3],
 
plural = args[4],
 
display = args.display,
 
section = args.section,
 
}
 
 
end
 
end
   
 
function p.Singular(frame)
 
function p.Singular(frame)
local args = getArgs(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
return p.printTerm(args)
 
  +
local result = p.printTerm(args, false, printErrorCategories)
  +
if err and printErrorCategories then
  +
result = result .. utilsMarkup.categories(err.categories)
  +
end
  +
return result
 
end
 
end
   
 
function p.Plural(frame)
 
function p.Plural(frame)
local args = getArgs(frame)
+
local args, err = utilsArg.parse(frame:getParent().args, p.Templates.Plural)
  +
local printErrorCategories = not utilsPage.inNamespace({"User", "MediaWiki"})
return p.printTerm(args, true)
 
  +
local result = p.printTerm(args, true, printErrorCategories)
  +
if err and printErrorCategories then
  +
result = result .. utilsMarkup.categories(err.categories)
  +
end
  +
return result
 
end
 
end
   
 
function p._fetchTerm(frame)
 
function p._fetchTerm(frame)
 
local args = frame.args
 
local args = frame.args
args = utilsTable.mapValues(mw.text.trim)(args)
+
args = utilsTable.mapValues(args, utilsString.trim)
args = utilsTable.mapValues(utilsString.nilIfEmpty)(args)
+
args = utilsTable.mapValues(args, utilsString.nilIfEmpty)
 
return p.fetchTerm(args.term, args.game)
 
return p.fetchTerm(args.term, args.game)
 
end
 
end
   
function p.printTerm(args, plural)
+
function p.printTerm(args, plural, printErrorCategories)
local validationErrors = utilsArg.validator({
 
page = {
 
nonEmpty = true,
 
},
 
game = {
 
enum = utilsGame.getEnum()
 
},
 
link = {},
 
plural = {
 
deprecated = true
 
},
 
display = {},
 
section = {},
 
})(args)
 
 
 
local term, fetchErrors = p.fetchTerm(args.page, args.game, plural or args.plural)
 
local term, fetchErrors = p.fetchTerm(args.page, args.game, plural or args.plural)
local errors = utilsTable.concat(validationErrors or {}, fetchErrors or {})
 
 
 
  +
local errorCategories = ""
  +
if printErrorCategories then
  +
local errors = utilsTable.concat(validationErrors or {}, fetchErrors or {})
  +
errorCategories = utilsMarkup.categories(errors)
  +
end
 
local result = ""
 
local result = ""
if mw.title.getCurrentTitle().isContentPage then
 
result = utilsMarkup.categories(errors)
 
end
 
 
if not term then
 
if not term then
 
local errLink = utilsMarkup.sectionLink(args.page, args.section, args.display)
 
local errLink = utilsMarkup.sectionLink(args.page, args.section, args.display)
result = utilsMarkup.inline({
+
result = utilsMarkup.inline(errLink, {
 
class = "facelift-term-invalid",
 
class = "facelift-term-invalid",
 
tooltip = "Invalid or missing term",
 
tooltip = "Invalid or missing term",
  +
})
})(errLink) .. result
 
 
elseif args.link == "link" then
 
elseif args.link == "link" then
local gameSub = utilsGame.AbbToBaseGame(args.game, true)
+
local gameSub = args.game and Franchise.shortName(args.game)
if not args.section and gameSub ~= "Unknown" and args.game ~= "Series" then
+
if not args.section and gameSub and args.game ~= "Series" then
 
args.section = gameSub
 
args.section = gameSub
 
end
 
end
result = utilsMarkup.sectionLink(args.page, args.section, args.display or term) .. result
+
result = utilsMarkup.sectionLink(args.page, args.section, args.display or term)
 
else
 
else
result = utilsMarkup.class("term")(args.display or term) .. result
+
result = utilsMarkup.class("term", args.display or term)
 
end
 
end
return result
+
return result .. errorCategories
 
end
 
end
   
 
function p.fetchTerm(page, game, plural)
 
function p.fetchTerm(page, game, plural)
utilsArg.required(page, "page")
 
 
if not page then
 
if not page then
 
return nil
 
return nil
 
end
 
end
  +
if type(page) == "table" then --legacy
 
  +
-- Cargo queries don't allow # and it's impossible to have a page with # anyway because of section anchors.
game = page.game
 
  +
-- Ideally, users should input the name of the page where the term is stored (e.g. Swordsman Newsletter 4 instead of Swordsman Newsletter #4)
plural = page.plural
 
page = page.term
+
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 cacheKey = h.cacheKey(page, game, plural)
  +
local term = cache.get(cacheKey)
  +
if term then
  +
return term
 
end
 
end
 
 
local rows = utilsCargo.query(CARGO_TABLE, "term, games, plural", {
+
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({_pageName = page})
 
  +
where = utilsCargo.allOf(
  +
{ _pageName = page },
  +
utilsCargo.IN("termGames._value", {"Series", game})
  +
)
 
})
 
})
local term = h.findTerm(rows, game) or h.findTerm(rows, "Series")
+
local termsByGame = utilsTable.keyBy(rows, "game")
  +
local term = termsByGame[game or "Series"] or termsByGame["Series"]
 
  +
local subtitle = Franchise.shortName(game or "Series")
local errCategories = {
 
  +
local errCategories = {}
"Pages with Invalid or Missing Terms",
 
  +
if mw.title.getCurrentTitle().nsText ~= "User" then
string.format("%s Pages with Invalid or Missing Terms", utilsGame.AbbToGame(game or "Series")),
 
  +
table.insert(errCategories, "Pages with Invalid or Missing Terms")
}
 
  +
if subtitle then
  +
table.insert(errCategories, string.format("%s Pages with Invalid or Missing Terms", subtitle))
  +
end
  +
end
 
if not term then
 
if not term then
utilsError.warn(string.format("No term exists for game <code>%s</code> on page <code>%s</code>.", game or "Series", page))
 
 
return nil, errCategories
 
return nil, errCategories
 
elseif plural and utilsString.isEmpty(term.plural) then
 
elseif plural and utilsString.isEmpty(term.plural) then
Line 126: Line 126:
 
return term.term, errCategories
 
return term.term, errCategories
 
elseif plural then
 
elseif plural then
  +
cache.set(cacheKey, term.plural)
 
return term.plural
 
return term.plural
 
else
 
else
  +
cache.set(cacheKey, term.term)
 
return term.term
 
return term.term
 
end
 
end
 
end
 
end
   
function h.findTerm(rows, game)
+
function p.fetchSubjects(term, game)
  +
local rows = utilsCargo.query(CARGO_TABLE, "_pageName", {
for _, row in ipairs(rows) do
 
  +
where = utilsCargo.allOf(
local games = utilsString.split(row.games)
 
  +
{ term = term },
if not utilsString.isEmpty(utilsTable.keyOf(games, game)) then
 
  +
game and ("games HOLDS '%s'"):format(game)
return row
 
  +
)
  +
})
  +
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 plural = args.plural
  +
local page = mw.title.getCurrentTitle().text
  +
  +
local cacheTerms = {}
  +
for _, game in ipairs(games or {}) do
  +
if singularTerm then
  +
local key = h.cacheKey(page, game, false)
  +
cacheTerms[key] = singularTerm
  +
end
  +
if pluralTerm then
  +
local key = h.cacheKey(page, game, true)
  +
cacheTerms[key] = pluralTerm
 
end
 
end
 
end
 
end
  +
cache.setMulti(cacheTerms)
 
end
 
end
   
Line 150: Line 179:
 
game = {
 
game = {
 
type = "string",
 
type = "string",
default = "Series",
+
default = mw.dumpObject("Series"),
desc = "A game code. See [[Module:UtilsGame]].",
+
desc = "A game code. See [[Data:Franchise]].",
 
},
 
},
 
plural = {
 
plural = {
Line 157: Line 186:
 
desc = "If true, retrieves the plural form."
 
desc = "If true, retrieves the plural form."
 
},
 
},
  +
},
  +
fetchSubjects = {
  +
term = {
  +
type = "string",
  +
required = true,
  +
},
  +
game = {
  +
type = "string"
  +
}
 
}
 
}
 
}
 
}
   
 
p.Documentation = {
 
p.Documentation = {
  +
fetchTerm = {
{
 
name = "fetchTerm",
 
outputOnly = true,
 
 
params = {"page", "game", "plural"},
 
params = {"page", "game", "plural"},
 
returns = {
 
returns = {
Line 170: Line 206:
 
},
 
},
 
cases = {
 
cases = {
  +
outputOnly = true,
 
{
 
{
 
args = {"Dynalfos", "OoT"},
 
args = {"Dynalfos", "OoT"},
expected = { "Dinolfos", nil },
+
expect = { "Dinolfos", nil },
 
},
 
},
 
{
 
{
description = "Defaults to series term.",
+
desc = "Defaults to series term.",
 
args = {"Dinolfos"},
 
args = {"Dinolfos"},
expected = {"Dynalfos"},
+
expect = {"Dynalfos"},
 
},
 
},
 
{
 
{
description = "Defaults to series term when term does not exist for specified game.",
+
desc = "Defaults to series term when term does not exist for specified game.",
 
args = {"Dinolfos", "ALttP"},
 
args = {"Dinolfos", "ALttP"},
expected = {"Dynalfos"},
+
expect = {"Dynalfos"},
 
},
 
},
 
{
 
{
description = "Error when page does store any terms (game specified).",
+
desc = "Error when page does store any terms (game specified).",
 
args = {"Flippityfloppito", "SS"},
 
args = {"Flippityfloppito", "SS"},
expected = {nil, {"Pages with Invalid or Missing Terms", "Skyward Sword Pages with Invalid or Missing Terms"}}
+
expect = {nil, {"Pages with Invalid or Missing Terms", "Skyward Sword Pages with Invalid or Missing Terms"}}
 
},
 
},
 
{
 
{
description = "Error when page does store any terms (no game specified).",
+
desc = "Error when page does store any terms (no game specified).",
 
args = {"Flippityfloppityfloo"},
 
args = {"Flippityfloppityfloo"},
expected = {nil, {"Pages with Invalid or Missing Terms", "The Legend of Zelda Series Pages with Invalid or Missing Terms"}}
+
expect = {nil, {"Pages with Invalid or Missing Terms", "The Legend of Zelda Series Pages with Invalid or Missing Terms"}}
 
},
 
},
 
{
 
{
description = "Plural",
+
desc = "Plural",
 
args = {"Bubble", "Series", true},
 
args = {"Bubble", "Series", true},
expected = {"Bubbles", nil},
+
expect = {"Bubbles", nil},
 
},
 
},
 
{
 
{
description = "Returns singular when no plural form exists.",
+
desc = "Returns singular when no plural form exists.",
 
args = {"A Brother's Roast", "BotW", true},
 
args = {"A Brother's Roast", "BotW", true},
expected = { "A Brother's Roast", {
+
expect = { "A Brother's Roast", {
 
"Pages with Invalid or Missing Terms",
 
"Pages with Invalid or Missing Terms",
 
"Breath of the Wild Pages with Invalid or Missing Terms",
 
"Breath of the Wild Pages with Invalid or Missing Terms",
 
}}
 
}}
 
}
 
}
  +
},
  +
},
  +
fetchSubjects = {
  +
params = {"term", "game"},
  +
returns = "Returns the names of wiki pages that store the given term. If game is specified, the function will only return pages 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 {{TLoZ|Series}}. 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 <code>Series</code>.",
  +
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 <code>link</code> for standardization)",
  +
trim = true,
  +
nilIfEmpty = true,
  +
},
  +
[4] = {
  +
name = "plural",
  +
type = "string",
  +
deprecated = true,
  +
desc = "Deprecated.",
  +
trim = true,
  +
nilIfEmpty = true,
  +
},
  +
display = {
  +
type = "string",
  +
desc = "Alternative display text for term.",
  +
trim = true,
  +
nilIfEmpty = true,
  +
},
  +
section = {
  +
type = "string",
  +
desc = "Section of <code>page</code> to link to.",
  +
trim = true,
  +
nilIfEmpty = true,
  +
},
  +
}
  +
},
  +
["Term/Store"] = {
  +
purpose = "Used in article leads to store [[Guidelines:Terminology|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, <code>OoT, SS, BotW</code>.",
  +
split = true,
  +
trim = true,
  +
nilIfEmpty = true,
  +
},
  +
[4] = {
  +
name = "plural",
  +
type = "string",
  +
desc = "Entering <code>plural</code> here will make the template output the plural term instead of the singular.",
  +
canOmit = true,
  +
},
 
},
 
},
 
},
 
},
 
}
 
}
  +
p.Templates.Plural = p.Templates.Term
   
 
return p
 
return p

Revision as of 17:23, 15 November 2020

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

fetchTerm

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

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
1
fetchTerm("Dynalfos", "OoT")
"Dinolfos"
Green check
nil
Green check
Defaults to series term.
2
fetchTerm("Dinolfos")
"Dynalfos"
Green check
nil
Green check
Defaults to series term when term does not exist for specified game.
3
fetchTerm("Dinolfos", "ALttP")
"Dynalfos"
Green check
nil
Green check
Error when page does store any terms (game specified).
4
fetchTerm("Flippityfloppito", "SS")
nil
Green check
{
  "Pages with Invalid or Missing Terms",
  "Skyward Sword Pages with Invalid or Missing Terms",
}
Green check
Error when page does store any terms (no game specified).
5
fetchTerm("Flippityfloppityfloo")
nil
Green check
{
  "Pages with Invalid or Missing Terms",
  "The Legend of Zelda Series Pages with Invalid or Missing Terms",
}
Green check
Plural
6
fetchTerm("Bubble", "Series", true)
"Bubbles"
Green check
nil
Green check
Returns singular when no plural form exists.
7
fetchTerm("A Brother's Roast", "BotW", true)
"A Brother's Roast"
Green check
{
  "Pages with Invalid or Missing Terms",
  "Breath of the Wild Pages with Invalid or Missing Terms",
}
Green check

fetchSubjects

fetchSubjects(term, [game])

Parameters

Returns

  • Returns the names of wiki pages that store the given term. If game is specified, the function will only return pages that store the term for that game.

Examples

#InputOutputResult
8
fetchSubjects("Wood")
{"Wood", "Wood (Character)"}
Green check
9
fetchSubjects("Wood", "ST")
{"Wood (Character)"}
Green check
10
fetchSubjects("Link", "MM")
Expected
{"Link", "Link (Goron)", "Mr. No Fairy"}
Actual
{"Link (Goron)", "Mr. No Fairy"}
TFH Red Link desperate
11
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 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, "Pages 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", "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)
	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 {{PAGENAME}} 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 then
		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(
			{ _pageName = page },
			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, "Pages with Invalid or Missing Terms")
		if subtitle then
			table.insert(errCategories, string.format("%s Pages 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 <code>%s</code> 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 plural = args.plural
	local page = mw.title.getCurrentTitle().text
	
	local cacheTerms = {}
	for _, game in ipairs(games or {}) do
		if singularTerm then
			local key = h.cacheKey(page, game, false)
			cacheTerms[key] = singularTerm
		end
		if pluralTerm then
			local key = h.cacheKey(page, game, true)
			cacheTerms[key] = pluralTerm
		end
	end
	cache.setMulti(cacheTerms)
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, {"Pages with Invalid or Missing Terms", "Skyward Sword Pages with Invalid or Missing Terms"}}
			},
			{
				desc = "Error when page does store any terms (no game specified).",
				args = {"Flippityfloppityfloo"},
				expect = {nil, {"Pages with Invalid or Missing Terms", "The Legend of Zelda Series Pages 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", {
				  "Pages with Invalid or Missing Terms",
				  "Breath of the Wild Pages with Invalid or Missing Terms",
				}}
			}
		},
	},
	fetchSubjects = {
		params = {"term", "game"},
		returns = "Returns the names of wiki pages that store the given term. If game is specified, the function will only return pages 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 {{TLoZ|Series}}. 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 <code>Series</code>.",
				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 <code>link</code> for standardization)",
				trim = true,
				nilIfEmpty = true,
			},
			[4] = {
				name = "plural",
				type = "string",
				deprecated = true,
				desc = "Deprecated.",
				trim = true,
				nilIfEmpty = true,
			},
			display = {
				type = "string",
				desc = "Alternative display text for term.",
				trim = true,
				nilIfEmpty = true,
			},
			section = {
				type = "string",
				desc = "Section of <code>page</code> to link to.",
				trim = true,
				nilIfEmpty = true,
			},
		}
	},
	["Term/Store"] = {
		purpose = "Used in article leads to store [[Guidelines:Terminology|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, <code>OoT, SS, BotW</code>.",
				split = true,
				trim = true,
				nilIfEmpty = true,
			},
			[4] = {
				name = "plural",
				type = "string",
				desc = "Entering <code>plural</code> here will make the template output the plural term instead of the singular.",
				canOmit = true,
			},
		},
	},
}
p.Templates.Plural = p.Templates.Term

return p