Module:FileInfo

From Zelda Wiki, the Zelda encyclopedia
Jump to navigation Jump to search
This is the main module for the following templates:
local p = {}
local h = {}

local Data = mw.loadData("Module:FileInfo/Data")

-- It's important to minimize the number of imports because this module is linked on every file page
-- Any change to these modules will trigger a large MediWiki job queue 
local cargo = mw.ext.cargo
local Franchise = require("Module:Franchise")
local utilsArg = require("Module:UtilsArg")

local MAX_IMAGE_AREA = require("Module:Constants/number/maxImageArea")

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

function p.StoreWidth(frame)
	return mw.title.getCurrentTitle().file.width
end
function p.StoreHeight(frame)
	return mw.title.getCurrentTitle().file.height
end

function p.Main(frame)
	local args = frame:getParent().args
	local args = h.preformat(args)
	local args, err = utilsArg.parse(args, p.Templates.FileInfo)
	local categories = err and err.categoryText or ""
	
	local result = h.printFileInfoTable(frame, args)
	categories = categories..h.categories(args.type, args.game, args.subject)
	
	-- Avoid categories from being added to [[MediaWiki:Upload-default-description]] and [[MediaWiki:Msu-comment]]
	if mw.title.getCurrentTitle().nsText == "MediaWiki" then
		categories = ""
	end

	return categories, result -- categories first in case there's notices attached to them - see h.maintenanceCategories()
end
function h.printFileInfoTable(frame, args)
	local gameDisplay
	if args.game then
		local gameLogo = Franchise.logo(args.game)
		local gameImage = gameLogo and gameLogo ~= "" and string.format("[[%s|130x130px]]", gameLogo)
		local gameLink = Franchise.link(args.game)
		local gameText = gameLink and string.format("This is a file pertaining to %s.", gameLink)
		if gameImage and gameText then
			gameDisplay = gameImage .. " " .. gameText
		elseif gameText then
			gameDisplay = gameText
		end
	end
	
	local type = args.type and Data.types[args.type]
	local typeLink = type and type.category and string.format("[[:Category:%s|%s]]", type.category, args.type)

	local license
	if args.licensing and h.licenseExists(args.licensing) then
		license = frame:expandTemplate({
			title = "FileInfo/License/" .. args.licensing,
			args = {
				trademark = args.trademark
			}
		})
	else
		license = frame:expandTemplate({ title = "FileInfo/License/Unsure" })
	end

	local html = mw.html.create("table"):addClass("wikitable fileinfo")
	h.row(html, "Summary", args.summary)
	h.row(html, "Type", typeLink)
	h.row(html, "Source", args.source or frame:expandTemplate({ title = "No Source" }))
	h.row(html, "Game", gameDisplay)
	h.row(html, "Licensing", license, {
		rowspan = args.trademark and "2" or "1" 
	})
	h.row(html, "Trademark", args.trademark and frame:expandTemplate({ title = "FileInfo/License/Trademark" }))
	
	return tostring(html)
end
function h.row(html, field, value, attributes)
	if value then
		return html
			:tag("tr")
			:tag("th")
				:wikitext(field)
				:done()
			:tag("td")
				:wikitext(value)
				:done()
			:done()
	end
end

function h.categories(type, game, subjects)
	local categories = ""
	categories = categories..h.gameTypeCategories(game, type)
	categories = categories..h.subjectCategories(subjects)
	categories = categories..h.maintenanceCategories(type)
	return categories
end
function h.gameTypeCategories(game, type)
	local categories = ""
	local gameName = game and Franchise.shortName(game)
	local typeCat = type and Data.types[type] and Data.types[type].category
	local gameRequired = type and Data.types[type] and Data.types[type].gameRequired
	if typeCat and gameName and game ~= "Series" then
		categories = categories..string.format("[[Category:%s %s]]", gameName, typeCat)
	elseif typeCat then
		categories = categories..string.format("[[Category:%s]]", typeCat)
	elseif gameCat then
		categories = categories..string.format("[[Category:%s Files]]", gameName)
	end
	if game == "N/A" then
		categories = categories.."[[Category:Files with inapplicable game]]"
	end
	if type and gameRequired ~= false and not game then
		local errMsg = string.format("<code>game</code> parameter is required for file type <code>%s</code>.", type)
			.. " For a list of accepted game values, see [[Data:Franchise]]."
			.. " If none of these values apply, set the <code>game</code> parameter to <code>N/A</code>."
		h.warn(errMsg)
		categories = categories.."[[Category:Files lacking game info]]"
	end
	return categories
end
function h.subjectCategories(subjects)
	if not subjects then
		return ""
	end
	local Term = require("Module:Term")
	
	local categories = ""
	for i, subject in ipairs(subjects) do
		local term, errCategories = Term.fetchTerm(subject, "Series")
		if not term then
			local utilsError = require("Module:UtilsError")
			local utilsMarkup = require("Module:UtilsMarkup")
			utilsError.warn(string.format("subject <code>%s</code> is not a valid [[Template:Term|term]]", subject))
			categories = categories..utilsMarkup.categories(errCategories)
		else
			term = string.gsub(term, "#", "") -- strip # from term because categories can't have them in their name
			 -- only add subject-based categories if they already exist, to avoid spamming Special:WantedCategories
			if h.pageExists("Category:Images of "..term) then
				categories = categories.."[[Category:Images of "..term.."]]"
			end
		end
	end
	return categories
end
function h.maintenanceCategories(type)
	local categories = ""
	local frame = mw.getCurrentFrame()
	local title = mw.title.getCurrentTitle()
	local mimeType = title.file and title.file.mimeType
	
	if type == "Sprite" and mimeType == "image/gif" then
		categories = categories..frame:expandTemplate({ title = "FileInfo/Notices/GIF Sprite" })
	end
	
	local exceedsMaxImageArea = title.file and ((title.file.width or 0) * (title.file.height or 0)) > MAX_IMAGE_AREA
	if mimeType ~= "image/jpeg" and exceedsMaxImageArea then
		categories = categories..frame:expandTemplate({ title = "FileInfo/Notices/Oversized" })
	end
	
	-- See category page for why we do this ourselves instead of using Special:UnusedFiles
	if title.nsText == "File" and h.isUnused(title.text) then
		categories = categories..frame:expandTemplate({ title = "FileInfo/Notices/Unused" })
	end
	
	return categories
end
function h.isUnused(filename)
	local separator = "$separator$"
	local dplQuery = string.format("{{#dpl:|imageused=%s|count=2}}", filename, separator)
	local dplResult = mw.getCurrentFrame():preprocess(dplQuery)
	return dplResult == ""
end

-- For supporting types written in lower case or Title Case
-- Previously only the former was supported but editors had a tendency to use the latter
-- Which is understandable because the type category is in title case, Template:Media is in title case, 
-- and all other fields in FileInfo are in title case or sentence case
-- We may eventually decide to support only Title Case but that would mean 
-- running a text-replace on the thousands of file pages using lowercase
function h.preformat(args)
	local _args = {}
	for k, v in pairs(args) do
		if k == "type" then
			_args[k] = string.gsub(" "..v, "%W%l", string.upper):sub(2)
		else
			_args[k] = v
		end
	end
	return _args
end

function h.licenseExists(license)
	for i, definedLicense in ipairs(Data.licenses) do
		if license == definedLicense then
			return true
		end		
	end
	return false
end

-- Copied from [[Module:UtilsPage]] to reduce the number of imports
function h.pageExists(fullPageName, noRedirect)
	local anchorStart = string.find(fullPageName, "#")
	if anchorStart then
		fullPageName = string.sub(fullPageName, 1, anchorStart - 1)
	end
	fullPageName = string.gsub(fullPageName, "'", "\\'") -- escape apostrophes
	local queryResults = cargo.query("_pageData", "_pageName, _isRedirect", {
		where = string.format("_pageName = '%s'", fullPageName)
	})
	return #queryResults > 0 and (not noRedirect or queryResults[1]._isRedirect == "0")
end

function p.Schemas()
	return {
		Data = {
			type = "record",
			required = true,
			properties = {
				{
					name = "types",
					required = true,
					type = "map",
					desc = "Defines file types, associating a type name to a wiki category.",
					keys = { type = "string"},
					keyPlaceholder = "type name",
					values = {
						type = "record",
						properties = {
							{
								name = "category",
								required = true,
								type = "string",
								desc = "The name of a subcategory of [[:Category:Files by Type]].",
							},
							{
								name = "gameRequired",
								type = "boolean",
								default = true,
								desc = "Set this to <code>false</code> to indicate that images of this type may not apply to a particular [[Data:Franchise|entry]] in the ''Zelda'' franchise. This renders the FileInfo <code>game</code> parameter optional for the given type. Otherwise, files missing this parameter are added to [[:Category:Files lacking game info]].",
							},
						},
					},
				},
				{
					name = "licenses",
					required = true,
					type = "array",
					items = { type = "string" },
					desc = "A list of licenses supported by [[Template:FileInfo]]. Each license must have a subpage under [[Template:FileInfo/License]].",
				},
			},
		}
	}
end

function p.Data(frame)
	local result = ""
	result = result .. "\n==Types==\n"
	result = result .. h.typeTable()
	result = result .. "\n==Licenses==\n"
	result = result .. h.licenseTable()
	return result
end
function h.typeTable()
	local utilsLayout = require("Module:UtilsLayout")
	local utilsString = require("Module:UtilsString")
	local utilsTable = require("Module:UtilsTable")
	
	local gameTypes = {} 
	local conceptTypes = {}
	local otherTypes = {}
	for typeName, type in pairs(Data.types) do
		type = utilsTable.merge({}, type, {
			name = typeName
		})
		if utilsString.startsWith(type.name, "Concept") then
			table.insert(conceptTypes, type)
		elseif type.gameRequired == false then
			table.insert(otherTypes, type)
		else
			table.insert(gameTypes, type)
		end
	end
	gameTypes = utilsTable.sortBy(gameTypes, "name")
	conceptTypes = utilsTable.sortBy(conceptTypes, "name")
	otherTypes = utilsTable.sortBy(otherTypes, "name")
	local types = utilsTable.concat(gameTypes, conceptTypes, otherTypes)

	local dataRows = utilsTable.map(types, function(type)
		local typeCode = "<code>"..type.name.."</code>"
		local typeCategory = "[[:Category:"..type.category.."]]"
		local gameRequiredOrOptional = type.gameRequired == true and "Required" or "Optional"
		return {typeCode, typeCategory, gameRequiredOrOptional}
	end)
	
	table.insert(dataRows, 1, {
		{
			header = true,
			colspan = 3,
			content = "Game Types"
		}
	})
	table.insert(dataRows, #gameTypes + 2, {
		{
			header = true,
			colspan = 3,
			content = "Concept Types",
		}
	})
	table.insert(dataRows, #gameTypes + #conceptTypes + 3, {
		{
			header = true,
			colspan = 3,
			content = "Other Types",
		}
	})
	
	return utilsLayout.table({
		headers = {"Type", "Category", "<code>game</code> Required?"},
		rows = dataRows,
	})
end
function h.licenseTable()
	local utilsLayout = require("Module:UtilsLayout")
	local utilsTable = require("Module:UtilsTable")

	return utilsLayout.table({
		sortable = {1},
		headers = {"License", "Template", "Output"},
		rows = utilsTable.map(Data.licenses, function(license)
			local licenseCode = "<code>"..license.."</code>"
			local template = "FileInfo/License/"..license
			local templateLink = "[[Template:"..template.."]]"
			local templateOutput = mw.getCurrentFrame():expandTemplate({title = template})
			return {licenseCode, templateLink, templateOutput} 
		end)
	})
end

function p.enumTypes()
	local enum = {}
	for k in pairs(Data.types) do
		table.insert(enum, k)
	end
	enum.reference = "[[Module:FileInfo/Data]]"
	return enum
end
function p.enumLicenses()
	local enum = {}
	for i, license in ipairs(Data.licenses) do
		enum[i] = license
	end
	enum.reference = "[[Module:FileInfo/Data]]"
	return enum
end
function p.enumGames()
	local enum = {"N/A"}
	local franchiseEnum = Franchise.enum({ 
		includeSeries = true,
		includeNonfiction = true,
		includeGroups = true,
	})
	for i, entry in ipairs(franchiseEnum) do
		table.insert(enum, entry)
	end
	enum.reference = "[[Data:Franchise]]"
	return enum
end

p.Templates = {
	FileInfo = {
		purpose = "Displays, categorizes, and stores file information. See [[Guidelines:Files]] for further guidance.",
		format = "block",
		paramOrder = {"summary", "type", "source", "game", "licensing", "subject", "trademark"},
		boilerplate = {
			tabs = {
				{
					label = "Zelda-Related Files",
					params = {"summary", "type", "source", "game", "licensing", "subject"},
				},
				{
					label = "Other Files",
					params = {"summary", "type", "source", "licensing"},
				},
				{
					label = "All Parameters",
					params = {"summary", "type", "source", "game", "licensing", "subject", "trademark"},
				},
			},
		},
		params = {
			summary = {
				--required = true,
				type = "content",
				desc = "A short description of the file.",
				trim = true,
				nilIfEmpty = true,
			},
			type = {
				required = "Category:Files lacking type info",
				type = "string",
				desc = "The type of file, which determines how it is [[:Category:Files by Type|categorized]].",
				enum = p.enumTypes(),
				trim = true,
				nilIfEmpty = true,
			},
			source = {
				required = "Category:Files Lacking Sources",
				type = "string",
				desc = "The original source of the file. It may be in the form of a URL or author recognition. [[Template:Source]] exists for this purpose.",
				trim = true,
				nilIfEmpty = true,
			},
			subject = {
				type = "string",
				desc = "Wiki article names of all the subjects depicted in the file. A comma-separated list.",
				split = true,
				trim = true,
				nilIfEmpty = true,
			},
			game = {
				--required = true,
				type = "string",
				desc = "<p>A valid [[Data:Franchise]] code for a game, book, comic, manga, or TV show (or <code>Series</code>).</p>"
					.."<p>Required for game-based types such as <code>Artwork</code>, <code>Screenshot</code>, <code>Sprite</code>, etc. See [[Module:FileInfo/Data]] for full list.</p>"
					.."<p><code>game</code> can be set to <code>N/A</code> when a game is required for the type but no Data:Franchise code applies, namely for images of [[The Legend of Zelda in Popular Culture|unlicensed media]].",
				enum = p.enumGames(),
				trim = true,
				nilIfEmpty = true,
			},
			licensing = {
				required = "Category:Unlicensed Files",
				type = "string",
				desc = "The copyright licensing for the file. For the vast majority of files, <code>Copyright</code> is the correct value here.",
				enum = p.enumLicenses(),
				trim = true,
				nilIfEmpty = true,
			},
			trademark = {
				type = "boolean",
				desc = "Enter any text to add a trademark notice to the licensing. Use on all [[:Category:Trademarks|trademarks]] (usually denoted by an ® or ™ symbol).",
				trim = true,
				nilIfEmpty = true,
			}
		}
	}
}

return p