Module:Gallery List

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

-- 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 . See Template:Gallery List 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  or  .", },			options = { type = "record", desc = "The options object is passed along to Module:Term and Module:Term, which is used to create the return object unless  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  and   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  ." },								{									name = "heights", type = "string", required = true, desc = "A value in pixels corresponding to the  ." },							},						},					},				},			},		}	} end

function p.Documentation return { parseEntry = { desc = "Allows other modules such as Module:Data Table to use Template:Gallery List's filename generation syntax.", params = {"subject", "game", "fileType", "options"}, returns = "An object containing a filename, a term link, a term string, and the  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", }				},				{					args = {"Ore Chunk [Red] (10) ", "OoS", "Sprite"}, expect = { file = "File:OoS Ore Chunk Red Sprite.png", subject = "Ore Chunk", term = "Ore Chunk", link = "Ore Chunk", variant = "Red", info = " (10) ", }				},				{					args = {"Stained Glass ", "TMC", ""}, expect = { file = "File:TMC Stained Glass Artwork.png", subject = "Stained Glass", term = "Stained Glass", link = "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", 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 = "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 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