Module:Gallery List

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.

parseEntry

parseEntry(subject, game, fileType, [options])

Allows other modules such as Module:Data Table to use Template:Gallery List's filename generation syntax.

Parameters

Returns

  • An object containing a filename, a term link, a term string, and the subject received minus any Template:Gallery List syntax.

Examples

#InputOutputStatus
1
parseEntry("Candy (The Minish Cap)", "TMC", "Sprite")
{
  subject = "Candy (The Minish Cap)",
  file = "File:TMC Candy Sprite.png",
  term = "Candy",
  link = "[[Candy (The Minish Cap)#The Minish Cap|Candy]]",
  info = "",
}
Green check.svg
2
parseEntry(
  "Ore Chunk [Red] <span>(10)</span>",
  "OoS",
  "Sprite"
)
{
  subject = "Ore Chunk",
  file = "File:OoS Ore Chunk Red Sprite.png",
  link = "[[Ore Chunk#Oracle of Seasons|Ore Chunk]]",
  term = "Ore Chunk",
  variant = "Red",
  info = " <span>(10)</span>",
}
Green check.svg
3
parseEntry(
  "Stained Glass [File:TMC Stained Glass Artwork.png]",
  "TMC",
  ""
)
{
  info = "",
  file = "File:TMC Stained Glass Artwork.png",
  term = "Stained Glass",
  link = "[[Stained Glass#The Minish Cap|Stained Glass]]",
  subject = "Stained Glass",
}
Green check.svg
4
parseEntry("Hyper Pico Bloom [No Image]", "TMC", "")
{
  info = "",
  file = "File:No Image.png",
  term = "Hyper Pico Bloom",
  link = "[[Hyper Pico Bloom#The Minish Cap|Hyper Pico Bloom]]",
  subject = "Hyper Pico Bloom",
}
Green check.svg
5
parseEntry(
  "Deku Tree",
  "TWW",
  "Figurine Model",
  { useTerms = false }
)
{
  subject = "Deku Tree",
  file = "File:TWW Deku Tree Figurine Model.png",
  term = "Deku Tree",
  link = "[[Deku Tree]]",
  info = "",
}
Green check.svg
6
parseEntry("Bomb", "ALttP", "30 Sprite", { plural = true })
{
  subject = "Bomb",
  file = "File:ALttP Bombs 30 Sprite.png",
  term = "Bombs",
  link = "[[Bomb#A Link to the Past|Bombs]]",
  info = "",
}
Green check.svg

local p = {}
local h = {}
local Data = mw.loadData("Module:Gallery List/Data")

local Franchise = require("Module:Franchise")
local Term = require("Module:Term")
local TermList = require("Module:Term List")
local utilsArg = require("Module:UtilsArg")
local utilsLayout = require("Module:UtilsLayout")
local utilsMarkup = require("Module:UtilsMarkup")
local utilsPage = require("Module:UtilsPage")
local utilsString = require("Module:UtilsString")
local utilsTable = require("Module:UtilsTable")

local CATEGORY_INVALID_ARGS = "[[Category:"..require("Module:Constants/category/invalidArgs").."]]"

-- Generally the latest remake is the default tab but some remakes don't have enough high quality images uploaded yet
local DEFAULT_TAB_DENYLIST = {
	["MM3D"] = true,
	["TWWHD"] = true,
	["TPHD"] = true,
	["SSHD"] = true,
}

-- Enhanced ports may have the same assets as the original game
-- There's no need to show a separate tab in those cases
local ENHANCED_PORTS = {
	["OoTMQ"] = true,
	--["FSAE"] = true -- turns out FSAE does have its own sprites
}

-- Gallery List stores data for [[Template:Spawn Locations]] if the page has any of the following infoboxes.
-- See comments on function h.isLocationArticle for more information
local LOCATION_INFOBOXES = {
	"Template:Infobox Dungeon",
	"Template:Infobox Location",
	"Template:Infobox Minigame",
	"Template:Infobox Scenario",
}

function h.warn(msg, ...)
	local utilsError = require("Module:UtilsError")
	local warnMessage = string.format(msg, ...)
	utilsError.warn(warnMessage)
end

function p.Main(frame)
	local args, err = utilsArg.parse(frame:getParent().args, p.Templates["Gallery List"])
	local categories = err and err.categoryText or ""
	
	local storeOnly = frame:getParent():getTitle() == "Template:Store Spawns"
	local options = utilsTable.clone(args)
	local subjectType = args.subjectType
	local storeLocationData = storeOnly or h.isLocationArticle()

	local tabData = {}
	local defaultTab = 1
	local allEntries = {}
	for i, game in ipairs(Franchise.enumGames()) do
		local rawEntries = args[game] or {}
		rawEntries = utilsTable.filter(rawEntries, utilsString.notEmpty)

		local fileType = options.fileType or h.fileType(subjectType, game)
		local entries = utilsTable.map(rawEntries, function(rawEntry)
			return p.parseEntry(rawEntry, game, fileType)
		end)
		allEntries = utilsTable.concat(allEntries, entries)
		
		if #entries > 0 then
			if not storeOnly then
				local gallery = h.printGallery(entries, subjectType, game, options)
				table.insert(tabData, {
					label = Franchise.display(game),
					content = gallery
				})
				if not DEFAULT_TAB_DENYLIST[game] then
					defaultTab = #tabData
				end
			end
			if args.storeAs then
				local subjects = utilsTable.map(entries, "subject")
				subjects = utilsTable.unique(subjects)
				TermList.storeSequence(game, args.storeAs, subjects)
			end
			if storeLocationData then
				local baseGame = Franchise.baseGame(game)
				for i, entry in ipairs(entries) do
					categories = categories..frame:expandTemplate({
						title = "Location Features/Store",
						args = {
							feature = subjectType,
							baseGame = baseGame,
							game = game,
							subject = entry.subject,
							quantity= entry.quantity,
							sublocations = entry.sublocations and table.concat(entry.sublocations, ","),
						}
					})
				end
			end
			for j, remake in ipairs(Franchise.remakes(game)) do
				if not storeOnly and not args[remake] and not ENHANCED_PORTS[remake] then
					local fileType = options.fileType or h.fileType(subjectType, remake)
					local entries = utilsTable.map(rawEntries, function(rawEntry)
						return p.parseEntry(rawEntry, remake, fileType)
					end)
					local gallery = h.printGallery(entries, subjectType, remake, options)
					table.insert(tabData, {
						label = Franchise.display(remake),
						content = gallery,
					})
					if not DEFAULT_TAB_DENYLIST[remake] then
						defaultTab = #tabData
					end
				end
			end
		end
	end

	if storeOnly then
		return "", categories
	end

	local tabs = utilsLayout.tabs(tabData, {
		default = defaultTab,
		tabOptions = {
			collapse = true,
		},
	})
	local html = mw.html.create("div")
		:addClass("zw-gallery-list")
		:wikitext(tabs)

	local result = h.hatnote(allEntries)..tostring(html)
	
	return result, categories
end

function p.LocationArticleCriteria(frame)
	local links = utilsTable.map(LOCATION_INFOBOXES, utilsMarkup.link)
	local list = utilsMarkup.bulletList(links)
	return list
end

function p.parseEntry(rawEntry, game, fileType, options)
	local options = options or {}

	rawEntry = string.gsub(rawEntry, "&#44;", ",") 	-- Unescape commas created using {{,}}
	local subject, info = utilsMarkup.separateMarkup(rawEntry)
	local tags, info = h.parseTags(info)
	
	local link
	local term
	if options.useTerms ~= false then
		link = Term.link(subject, game, options)
		term = Term.fetchTerm(subject, game, options) or subject
	else
		link = string.format("[[%s]]", subject)
		term = subject
	end
	
	local entry = utilsTable.merge({}, tags, {
		link = link,
		info = info,
		term = term,
		subject = subject,
	})

	entry.file = tags.file or h.file(game, entry, fileType, options)

	return entry
end

-- Looks at the article's infobox to determine whether the Gallery List data should be stored for use by [[Template:Spawn Locations]]
-- Without this check, the Gallery List on [[Captain Construct]] causes that page to appear as a "habitat" for, say, [[Captain Construct I]]
--
-- Previous revisions checked the article's categories. However, there got to be so many categories in the query (Dungeons, Levels, Locations, Scenarios, Stages) that we exceeded the default DPL maximum 
-- (see maxCategoryCount at https://www.mediawiki.org/wiki/Extension:DynamicPageList3)
function h.isLocationArticle()
	local title = mw.title.getCurrentTitle()
	local locationInfoboxes = table.concat(LOCATION_INFOBOXES, "|")
	local dplResults = utilsPage.dpl({
		uses = locationInfoboxes,
		skipthispage= "no",
		title = title.prefixedText,
	})
	return #dplResults ~= 0
end

local TAG_REGEX = "^%s*%[([^%]%[]+)%]"
function h.parseTags(info)
	local tags = {}
	local errorCategories = ""
	local tagCount = 0
	local matches
	repeat
		info, matches = string.gsub(info, TAG_REGEX, function(tag)
			local tagParts = utilsString.split(tag, ":")
			local tagName = tagParts[1]
			local tagArgs = utilsTable.tail(tagParts)
			if tagName == "File" then
				tags.file = "File:"..tagArgs[1]
			elseif tagName == "Qty" then
				tags.quantity = tagArgs[1]
			elseif tagName == "Sublocation" then
				tags.sublocations = tagArgs
			elseif #tagParts == 1 and tagCount == 0 then
				if tagParts[1] == "No Image" then
					tags.file = "File:No Image.png"
				else
					tags.variant = tagParts[1]
				end
			else 
				h.warn("Invalid tag <code>%s</code>. See [[Template:Gallery List#Tags]] for supported tags.", tagName)
				errorCategories = CATEGORY_INVALID_ARGS
			end
			tagCount = tagCount + 1
			return ""
		end)
	until matches == 0
	return tags, info..errorCategories
end

function h.file(game, entry, fileType, options)
	local suffix = fileType == "" and "" or " "..fileType
	local variant = entry.variant
	if variant then
		if tonumber(variant) then
			suffix = suffix .. " " .. variant
		else
			-- numbers can be placed in quotes to escape the above functionality
			-- e.g. [[Items in Spirit Tracks]] uses Quiver ["50"] to produce `File:ST Quiver 50 Icon.png` instead of `File:ST Quiver Icon 50.png`
			variant = utilsString.trim(variant, [["]])
			suffix = " " .. variant .. suffix
		end
	end
	
	local file
	if variant == "No Image" then
		file = "File:No Image.png"
	elseif options.useTerms == false then
		file = string.format("File:%s %s%s.png", game, entry.subject, suffix)
	else
		local term = entry.term
		term = string.gsub(term, "#", "") -- filenames can't have # so we strip them from the terms
		term = string.gsub(term, "''", "") -- same goes for any formatting in the term, e.g. ''Cuccodex''
		file = string.format("File:%s %s%s.png", game, term, suffix)
	end
	return file
end

function h.printGallery(entries, subjectType, game, options)
	options = options or {}
	
	local galleryContent = ""
	for i, entry in ipairs(entries) do
		galleryContent = galleryContent..entry.file.."|"..h.printEntry(entry).."\n"
	end
	
	local size = Data.sizes[game] and Data.sizes[game][subjectType] or {}
	local gallery =  mw.getCurrentFrame():extensionTag({
		name = "gallery",
		content = galleryContent,
		args = {
			caption = options.caption,
			widths = options.widths or size.widths,
			heights = options.heights or size.heights,
			perrow = options.perrow,
		}
	})
	return gallery
end

function h.fileType(subjectType, game)
	local graphics = Franchise.graphics(game)
	if subjectType == "Locations" then
		return ""
	elseif graphics == "2D" then
		return "Sprite"
	elseif subjectType == "Items" then
		return "Icon"
	elseif subjectType == "Treasures" then
		return "Icon"
	elseif subjectType == "Zonai Devices" then
		return "Icon"
	else
		return "Model"
	end
end

function h.printEntry(entry)
	local result = entry.link
	if entry.quantity then
		result = result.." ×"..entry.quantity
	end
	if entry.info then
		result = result..entry.info
	end
	return result
end

function h.hatnote(entries)
	local entriesWithSublocations = utilsTable.filter(entries, "sublocations")
	if #entriesWithSublocations == 0 then
		return ""
	end
	local sublocations = utilsTable.map(entries, "sublocations")
	sublocations = utilsTable.compact(sublocations)
	sublocations = utilsTable.flatten(sublocations)
	sublocations = utilsTable.unique(sublocations)
	table.sort(sublocations)
	for i, sublocation in ipairs(sublocations) do
		sublocations[i] = Term.link(sublocation, game)
	end
	sublocations = mw.text.listToText(sublocations)
	local hatnote = mw.getCurrentFrame():expandTemplate({
		title = "Hatnote",
		args = {"Parts of this section are derived from the following location(s): "..sublocations}
	})
	return hatnote
end

function p.Schemas()
	return {
		parseEntry = {
			subject = {
				required = true,
				type = "string",
				desc = "A wiki article name referring to the subject for which a filename is being generated.",
			},
			game = {
				required = true,
				type = "string",
				desc = "A game code from [[Data:Franchise]].",
			},
			fileType = {
				required = true,
				type = "string",
				desc = "A file type such as <code>Sprite</code> or <code>Model</code>.",
			},
			options = {
				type = "record",
				desc = "The options object is passed along to [[Module:Term#fetchTerm]] and [[Module:Term#printTerm]], which is used to create the return object unless <code>useTerms</code> is false.",
				properties = {
					{
						name = "useTerms",
						type = "boolean",
					},
				},
			},
		},
		Data = {
			type = "record",
			required = true,
			properties = {
				{
					name = "sizes",
					desc = "Sets the size of gallery thumbnails based on the <code>game</code> and <code>subjectType</code> parameters of [[Template:Gallery List]].",
					required = true,
					type = "map",
					keyPlaceholder = "game",
					keys = { type = "string" },
					values = {
						type = "map",
						keyPlaceholder = "subjectType",
						keys = { type = "string" },
						values = {
							type = "record",
							properties = {
								{
									name = "widths",
									type = "string",
									required = true,
									desc = "A value in pixels corresponding to the <code>widths</code> {{MediaWiki|Help:Images#Optional_gallery_attributes|gallery attribute}}."
								},
								{
									name = "heights",
									type = "string",
									required = true,
									desc = "A value in pixels corresponding to the <code>heights</code> {{MediaWiki|Help:Images#Optional_gallery_attributes|gallery attribute}}."
								},
							},
						},
					},
				},
			},
		}
	}
end

function p.Documentation()
	return {
		parseEntry = {
			desc = "Allows other modules such as [[Module:Data Table]] to use [[Template:Gallery List]]'s [[Template:Gallery List#Variant Syntax|filename generation syntax]].",
			params = {"subject", "game", "fileType", "options"},
			returns = "An object containing a filename, a term link, a term string, and the <code>subject</code> received minus any [[Template:Gallery List]] syntax.",
			cases = {
				outputOnly = true,
				{
					args = {"Candy (The Minish Cap)", "TMC", "Sprite"},
					expect = {
						info = "",
						file = "File:TMC Candy Sprite.png",
						subject = "Candy (The Minish Cap)",
						term = "Candy",
						link = "[[Candy (The Minish Cap)#The Minish Cap|Candy]]",
					}
				},
				{
					args = {"Ore Chunk [Red] <span>(10)</span>", "OoS", "Sprite"},
					expect = {
						file = "File:OoS Ore Chunk Red Sprite.png",
						subject = "Ore Chunk",
						term = "Ore Chunk",
						link = "[[Ore Chunk#Oracle of Seasons|Ore Chunk]]",
						variant = "Red",
						info = " <span>(10)</span>",
					}
				},
				{
					args = {"Stained Glass [File:TMC Stained Glass Artwork.png]", "TMC", ""},
					expect = {
						file = "File:TMC Stained Glass Artwork.png",
						subject = "Stained Glass",
						term = "Stained Glass",
						link = "[[Stained Glass#The Minish Cap|Stained Glass]]",
						info = "",
					}
				},
				{
					args = {"Hyper Pico Bloom [No Image]", "TMC", ""},
					expect = {
						file = "File:No Image.png",
						term = "Hyper Pico Bloom",
						subject = "Hyper Pico Bloom",
						link = "[[Hyper Pico Bloom#The Minish Cap|Hyper Pico Bloom]]",
						info = "",
					}
				},
				{
					args = {"Deku Tree", "TWW", "Figurine Model", { useTerms = false }},
					expect = {
						file = "File:TWW Deku Tree Figurine Model.png",
						subject = "Deku Tree",
						term = "Deku Tree",
						link = "[[Deku Tree]]",
						info = "",
					},
				},
				{
					args = {"Bomb", "ALttP", "30 Sprite", { plural = true } },
					expect = {
						file = "File:ALttP Bombs 30 Sprite.png",
						subject = "Bomb",
						term = "Bombs",
						link = "[[Bomb#A Link to the Past|Bombs]]",
						info = "",
					}
				},
			},
		},
	}
end

function p.templateData()
	local paramOrder = {1, "fileType", "storeAs", "caption", "perrow", "widths", "heights"}
	local params = {
		[1] = {
			name = "subjectType",
			required = true,
			type = "string",
			enum = {"Animals", "Bosses", "Characters", "Creatures", "Enemies", "Equipment", "Locations", "Items", "Materials", "Objects", "Treasures", "Zonai Devices"},
			desc = "The type of subject being listed.",
			trim = true,
			nilIfEmpty = true,
		},
		fileType = {
			type = "string",
			desc = "Sets a custom filename suffix for gallery entries",
			trim = true,
		},
		storeAs = {
			type = "string",
			desc = "Stores the list in the [[Special:CargoTables/Sequences|Sequences]] Cargo table for use by [[Template:Sort Value]] and [[Module:Sequences]].",
			canOmit = true,
			trim = true,
			nilIfEmpty = true,
		},
		caption = {
			type = "string",
			desc = "Caption text for the gallery.",
			trim = true,
			nilIfEmpty = true,
			canOmit= true,
		},
		perrow = {
			type = "number",
			desc = "Maximum number of thumbnails to show per row in the gallery.",
			trim = true,
			nilIfEmpty = true,
			canOmit = true,
		},
		widths = {
			type = "string",
			desc = "A value in pixels. Sets the width of gallery entries, overriding any default set in [[Module:Gallery List/Data]]",
			trim = true,
			nilIfEmpty = true,
			canOmit = true,
		},
		heights = {
			type = "string",
			desc = "A value in pixels. Sets the heights of gallery entries, overriding any default set in [[Module:Gallery List/Data]]",
			trim = true,
			nilIfEmpty = true,
			canOmit = true,
		}
	}
	for i, game in ipairs(Franchise.enumGames()) do
		if Franchise.type(game) ~= "" then -- only games featured on the main page for now, to avoid adding too many parameters
			table.insert(paramOrder, game)
			params[game] = {
				type = "string",
				desc = "Comma-separated list of wiki page names referring to subjects in "..Franchise.display(game),
				trim = true,
				split = true,
			}
		end
	end
	return {
		format = "block",
		params = params,
		paramOrder = paramOrder,
	}
end

p.Templates = {
	["Gallery List"] = p.templateData(),
	["Store Spawns"] = {},
}

return p