Module:Term

From Zelda Wiki, the Zelda encyclopedia
Jump to navigation Jump to search
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.svg

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.svg

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.svg

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.svg
7
printTerm("Kara Kara Bazaar", "BotW", { link = true })
"[[Kara Kara Bazaar#Breath of the Wild|Kara Kara Bazaar]]"
Kara Kara Bazaar
Green check.svg
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.svg
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.svg
10
printTerm("invalid term")
'<span class="term--invalid"><span title="Invalid or missing term" class="tooltip">[[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.svg
Checks for redundant display arguments.
11
printTerm("Link", "Series", { display = "Link" })
'<span class="term">Link</span>[[Category:Terms with redundant display arguments]]'
Link
Green check.svg

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

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
21
fetchSubjects("Wood")
{"Wood", "Wood (Character)"}
Green check.svg
22
fetchSubjects("Wood", "ST")
{"Wood (Character)"}
Green check.svg
23
fetchSubjects("Link", "MM")
Expected
{"Link", "Link (Goron)", "Mr. No Fairy"}
Actual
{
  "Link",
  "Link (Goron)",
  "Mr. No Fairy",
  "Professor Link",
}
TFH Red Link desperate.png
24
fetchSubjects("Fooloo Limpah")
{}
Green check.svg

local p = {}
local h = {}

local Franchise = require("Module:Franchise")
local utilsArg = require("Module:UtilsArg")
local utilsCache = require("Module:UtilsCache")
local utilsCargo = require("Module:UtilsCargo")
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 CATEGORY_INVALID_ARGS = require("Module:Constants/category/invalidArgs")
local CATEGORY_REDUNDANT_DISPLAY = "Terms with redundant display arguments"
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, CATEGORY_INVALID_ARGS)
		h.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.ClearCache(frame)
	local page = frame.args[1]
	h.clearCache(page)
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 = "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
	if term and options.display == term then
		errorCategories = errorCategories.."[[Category:"..CATEGORY_REDUNDANT_DISPLAY.."]]"
		h.warn(string.format("Redundant display argument <code>%s</code> is the same as the term value.", options.display))
	end
	
	-- escape commas for the benefit of templates that split list items by comma, e.g. Module:Infobox
	result = string.gsub(result, ",", "&#44;")
	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 = utilsCache.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
		h.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)
		utilsCache.set(cacheKey, term.term)
	end
	if term and utilsString.notEmpty(term.plural) then
		local cacheKey = h.cacheKey(page, game, true)
		utilsCache.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
	utilsCache.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)
	utilsCache.delete(key)
end

function h.warn(msg)
	local utilsError = require("Module:UtilsError")
	return utilsError.warn(msg)
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:Wares]] 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:Wares]] 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="term--invalid"><span title="Invalid or missing term" class="tooltip">[[invalid term]]</span></span>[[Category:Articles with invalid or missing terms]][[Category:The Legend of Zelda Series articles with invalid or missing terms]]',
				},
				{
					desc = "Checks for redundant <code>display</code> arguments.",
					args = {"Link", "Series", { display = "Link" } },
					expect = '<span class="term">Link</span>[[Category:'..CATEGORY_REDUNDANT_DISPLAY..']]',
				},
			}
		},
		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