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
(+checkExists)
(perf improvement attempt by using EPF count. not successful, will undo but keeping here for the record)
Line 11: Line 11:
 
local utilsString = require("Module:UtilsString")
 
local utilsString = require("Module:UtilsString")
 
local utilsTable = require("Module:UtilsTable")
 
local utilsTable = require("Module:UtilsTable")
  +
local utilsVar = require("Module:UtilsVar")
   
 
local data = mw.loadData("Module:File/Data")
 
local data = mw.loadData("Module:File/Data")
 
local CARGO_TABLE = "Files"
 
local CARGO_TABLE = "Files"
  +
local epfCounter = utilsVar.expensiveParserFunctionCounter()
   
 
-- Template:FileInfo
 
-- Template:FileInfo
Line 217: Line 219:
 
local sizeWidth, sizeHeight = p.dimensions(options.size)
 
local sizeWidth, sizeHeight = p.dimensions(options.size)
 
local checkExists = options.checkExists ~= false
 
local checkExists = options.checkExists ~= false
  +
local scaleUsingCargo = options.scale and epfCounter.value() >= epfCounter.limit
 
local filePage = mw.title.new("File:"..filename)
 
 
 
-- If the file is a redirect and we are doing an existence check or getting width/height from Cargo, then we need to get the redirectTarget.
 
-- If the file is a redirect and we are doing an existence check or getting width/height from Cargo, then we need to get the redirectTarget.
 
-- This is a somewhat expensive operation to do at scale so we only do it when needed
 
-- This is a somewhat expensive operation to do at scale so we only do it when needed
 
local originalFilename = filename
 
local originalFilename = filename
if checkExists or options.scale then
+
if checkExists or scaleUsingCargo then
local redirectTarget = mw.title.new("File:"..filename).redirectTarget
+
local redirectTarget = filePage.redirectTarget
 
redirectTarget = redirectTarget and redirectTarget.text
 
redirectTarget = redirectTarget and redirectTarget.text
 
if redirectTarget then
 
if redirectTarget then
Line 229: Line 233:
 
end
 
end
 
 
if checkExists and not utilsPage.exists("File:" .. filename, true) then
+
if checkExists and not h.fileExists(filename) then
 
return h.noimage(filename, sizeWidth, sizeHeight, options), false
 
return h.noimage(filename, sizeWidth, sizeHeight, options), false
 
end
 
end
Line 235: Line 239:
 
if options.scale then
 
if options.scale then
 
local file
 
local file
if options.scaleUsingCargo then
+
if scaleUsingCargo then
results = utilsCargo.query(CARGO_TABLE, "width, height", {
+
results = utilsCargo.query(CARGO_TABLE, "_pageName, width, height", {
 
where = utilsCargo.allOf({
 
where = utilsCargo.allOf({
 
["_pageName"] = "File:"..filename,
 
["_pageName"] = "File:"..filename,
Line 242: Line 246:
 
limit = 1,
 
limit = 1,
 
})
 
})
file = results[1]
+
file = {
  +
width = 10,
end
 
  +
height = 10,
if not file or utilsString.isEmpty(file.width) or utilsString.isEmpty(file.height) then -- if scaleUsingCargo = false or data does not return from Cargo query for some reason, then use the title object (an expensive parser function)
 
  +
}
file = mw.title.new("File:"..filename).file
 
  +
if utilsString.isEmpty(file.width) or utilsString.isEmpty(file.height) then
  +
error("width and height values empty for File:" .. filename)
 
end
  +
else
  +
epfCounter.increment()
  +
file = filePage.file
 
end
 
end
   

Revision as of 12:11, 27 October 2020

Lua error at line 17: attempt to call field 'expensiveParserFunctionCounter' (a nil value).


local p = {}
local h = {}

local Franchise = require("Module:Franchise")
local Term = require("Module:Term")
local utilsArg = require("Module:UtilsArg")  
local utilsCargo = require("Module:UtilsCargo")
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 utilsVar = require("Module:UtilsVar")

local data = mw.loadData("Module:File/Data")
local CARGO_TABLE = "Files" 
local epfCounter = utilsVar.expensiveParserFunctionCounter()

-- Template:FileInfo
function p.StoreWidth(frame)
	return mw.title.getCurrentTitle().file.width
end
function p.StoreHeight(frame)
	return mw.title.getCurrentTitle().file.height
end
function p.FileInfo(frame)
	local args, err = utilsArg.parse(frame:getParent().args, p.Templates.FileInfo)
	local result = p.printFileInfo(args)
	if err then
		return result .. utilsMarkup.categories(err.categories)
	else
		return result
	end
end
function p.printFileInfo(args)
	return h.printFileInfoTable(args) .. h.categories(args.type, args.game, args.subject)
end
function h.printFileInfoTable(args)
	local gameDisplay
	if args.game then
		local gameLogo = Franchise.logo(args.game)
		local gameImage = gameLogo and utilsPage.exists(gameLogo) and utilsMarkup.file(gameLogo, { size = "130x130px" })
		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
		else
			gameDisplay = ""
		end
	end
	
	local type = args.type and data.typesByKey[args.type]
	type = type and type.cat

	local license
	if args.licensing and utilsTable.includes(data.licenses, args.licensing) then
		license = mw.getCurrentFrame():expandTemplate({
			title = "FileInfo/" .. args.licensing,
			args = {
				trademark = args.trademark
			}
		})
	else
		license = mw.getCurrentFrame():expandTemplate({ title = "FileInfo/Unsure" })
	end

	local html = mw.html.create("table"):addClass("wikitable fileinfo")
	h.row(html, "Summary", args.summary)
	h.row(html, "Type", type)
	h.row(html, "Source", args.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 mw.getCurrentFrame():expandTemplate({ title = "FileInfo/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 gameName = game and Franchise.shortName(game)
	local typeCat = type and data.typesByKey[type] and data.typesByKey[type].cat
	
	local categories = {}
	if typeCat and not typeCat.nogame and gameName and game ~= "Series" then
		table.insert(categories, gameName .. " " .. typeCat)
	elseif typeCat then
		table.insert(categories, typeCat)
	elseif gameCat then
		table.insert(categories, gameName .. " Files")
	end
	if subjects then
		categories = utilsTable.concat(categories, h.subjectCategories(subjects, game))
	end
	if type == "sprite" and utilsString.endsWith(mw.title.getCurrentTitle().text, ".gif") then
		table.insert(categories, "GIF Sprites")
	end
	if not game then -- not necessarily invalid but worth tracking
		table.insert(categories, "Files without Game")
	end
	
	return utilsMarkup.categories(categories)
end
function h.subjectCategories(subjects, game)
	local categories = utilsTable.flatMap(subjects, function(subject)
		local term, err = Term.fetchTerm(subject, game)
		if not term then
			return err.categories -- add term-related maintenance categories, if any
		end
		term = string.gsub(term, "#", "") -- strip # from term because categories can't have them in their name
		local category = "Images of "..term
		 -- only add subject-based categories if they already exist, to avoid spamming Special:WantedCategories
		if utilsPage.exists("Category:" .. category) then
			return {category}
		else
			return {}
		end
	end)
	return categories
end

-- Module:File/Data
function p.Data(frame)
	local result = ""
	
	result = result .. utilsMarkup.heading(2, "Types")
	result = result .. utilsLayout.table({
		sortable = true,
		headers = {"Type", "Category"},
		rows = utilsTable.map(data.types, function(type)
			local key = utilsMarkup.code(type.key)
			local cat = "[[:Category:"..type.cat.."]]"
			return {key, cat}
		end)
	})
	
	result = result .. utilsMarkup.heading(2, "Licenses")
	result = result .. utilsLayout.table({
		sortable = true,
		headers = {"License", "Template", "Output"},
		rows = utilsTable.map(data.licenses, function(license)
			local template = "FileInfo/"..license
			local templateLink = "[[Template:"..template.."]]"
			local templateOutput = mw.getCurrentFrame():expandTemplate({title = template})
			return {utilsMarkup.code(license), templateLink, templateOutput} 
		end)
	})

	return result
end

-- Queries Cargo for the 100 most-used subjects, uses DPL to determine which ones don't exist as categories yet.
-- Repeat for the next 100 subjects, and so on until there are ~100 table rows or no more subjects to process.
function p.WantedSubjectCategories(frame)
	local BATCH_SIZE = 100
	local MAX_ROWS = 100
	local offset = 0
	local rows = {}
	local cargoResults
	repeat 
		cargoResults = utilsCargo.query(CARGO_TABLE.."__subject", "_value=subject, COUNT(*)=count", {
			groupBy = "_value",
			orderBy = "COUNT(*) DESC",
			limit = BATCH_SIZE,
			offset = offset
		})
		offset = offset + BATCH_SIZE
	
		local dplArgs = utilsTable.map(cargoResults, function(result)
			return {
				param = "titlematch",
				value = "Images of "..result.subject
			}
		end)
		dplArgs.namespace = "Category"
		local existingCats = utilsTable.invert(utilsPage.dpl(dplArgs))
		
		for _, result in ipairs(cargoResults) do
			local cat = "Category:Images of "..result.subject
			if not existingCats[cat] then
				table.insert(rows, { utilsMarkup.link(cat), result.count})
			end
		end
	until #rows >= MAX_ROWS or #cargoResults == 0
	return utilsLayout.table({
		sortable = true,
		headers = {"Category", "Count"},
		rows = rows
	})
end

-- Various templates
function p.Icon(frame)
	local args = frame.args
	local img = p.icon(args[1], args[2], {
		size = args.size
	})
	return img
end

-- Utilities
function p.image(filename, options)
	filename = utilsPage.stripNamespace(filename)
	options = options or {}
	local sizeWidth, sizeHeight = p.dimensions(options.size)
	local checkExists = options.checkExists ~= false
	local scaleUsingCargo = options.scale and epfCounter.value() >= epfCounter.limit
	local filePage = mw.title.new("File:"..filename)
	
	-- If the file is a redirect and we are doing an existence check or getting width/height from Cargo, then we need to get the redirectTarget.
	-- This is a somewhat expensive operation to do at scale so we only do it when needed
	local originalFilename = filename
	if checkExists or scaleUsingCargo then
		local redirectTarget = filePage.redirectTarget
		redirectTarget = redirectTarget and redirectTarget.text
		if redirectTarget then
			filename = redirectTarget
		end
	end
	
	if checkExists and not h.fileExists(filename) then
		return h.noimage(filename, sizeWidth, sizeHeight, options), false
	end
	
	if options.scale then
		local file
		if scaleUsingCargo then
			results = utilsCargo.query(CARGO_TABLE, "_pageName, width, height", {
				where = utilsCargo.allOf({
					["_pageName"] = "File:"..filename,
				}),
				limit = 1,
			})
			file = {
				width = 10,
				height = 10,
			}
			if utilsString.isEmpty(file.width) or utilsString.isEmpty(file.height) then
				error("width and height values empty for File:" .. filename)
			end
		else
			epfCounter.increment()
			file = filePage.file
		end

		local width = math.floor(tonumber(file.width) * options.scale)
		local height = math.floor(tonumber(file.height) * options.scale)
		if (sizeWidth and sizeWidth < width) or (sizeHeight and sizeHeight < height) then
			width = sizeWidth
			height = sizeHeight
		end
		size = ""
		if width then
			size = width
		end
		if height then
			size = size .. "x" .. height
		end
		size = size .. "px"
		options = utilsTable.merge({}, options, {
			size = size
		})
	end
	return utilsMarkup.file(originalFilename, options), checkExists and true or nil
end
function p.dimensions(size)
	if not size then
		return nil
	end
	local s, e
	s, e = size:find("^[0-9]+")
	local width = s and size:sub(s, e) or ""
	
	s, e = size:find("x[0-9]+")
	local height = s and size:sub(s+1, e) or ""
	
	return tonumber(width), tonumber(height)
end
function h.noimage(filename, sizeWidth, sizeHeight, options)
	local uploadUrl = mw.uri.fullUrl("Special:Upload")
	uploadUrl:extend({
		wpDestFile = filename
	})
	local options = utilsTable.merge({}, options, {
		link = tostring(uploadUrl)
	})
	-- Make sure thumbnail for 'no image' is no less than 100x100px
	if (sizeWidth and sizeWidth < 100) or (sizeHeight and sizeHeight < 100) then
		options.size = "100px"
	end
	return utilsMarkup.file("File:No Image Upload.png", options)
end

function p.gameImage(game, subject, type, options)
	local parts = utilsTable._filter(utilsString.notEmpty)({game, subject, type})
	local filename = table.concat(parts, " ") .. ".png"
	return p.image(filename, options)
end

function p.icon(game, subject, options)
	local type = "Icon"
	if Franchise.graphics(game) == "2D" then
		type = "Sprite"
	end
	return p.gameImage(game, subject, type, options)
end

function p.logo(code, options)
	local filename = Franchise.logo(code)
	return p.image(filename, options)
end

p.Templates = {
	FileInfo = {
		purpose = "Displays, categorizes, and stores file information. See [[Guidelines:Files]] for further guidance.",
		format = "block",
		paramOrder = {"summary", "subject", "type", "source", "game", "licensing", "trademark"},
		params = {
			summary = {
				--required = true,
				type = "content",
				desc = "A short description of the file.",
				trim = true,
				nilIfEmpty = true,
			},
			type = {
				required = "Category:Files Lacking Type",
				type = "string",
				desc = "The type of file, which determines how it is [[:Category:Files by Type|categorized]].",
				enum = data.typesEnum,
				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 = "A valid [[Data:Franchise|code]] for a game, book, comic, manga, or TV show (or <code>Series</code>).",
				enum = Franchise.enum({ 
					includeSeries = true,
					includeGroups = true,
				}),
				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 = data.licenses,
				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,
			}
		},
		examples = {
			vertical = true,
			{
				summary = "{{Term|LADX|Animal Village|link}}",
				subject = "Animal Village, Rabbit",
				type = "map",
				source = "{{Source|Original|MannedTooth}}",
				game = "LADX",
				licensing = "Copyright",
			},
			{
				summary = "The [[Timeline]]",
				source = "{{Cite Book|book= E |page= 10}}",
				type = "print",
				game = "Series",
				licensing = "Copyright"
			},
			{
				summary = "Nintendo's current logo.",
				type = "logo",
				source = "",
				licensing = "PD-Simple",
				trademark = "yes",
			},
			{
				summary = "File missing required info"
			},
		},
	}
}

local optionsSchema =  {
	type = "record",
	properties = {
		{
			name = "size",
			type = "string",
			desc = "Image size in pixels.",
		},
		{
			name = "scale",
			type = "number",
			desc = "Image scaling factor — the original image size is multitplied by <code>scale</code>. If both <code>scale</code> and <code>size</code> are present, the value which results in the smaller image will be used. <b>By default this uses an {{Mediawiki|Manual:$wgExpensiveParserFunctionLimit|expensive parser function}}</b>.",
		},
		{
			name = "scaleUsingCargo",
			type = "boolean",
			desc = "If set to true, then a Cargo query is used to determine the original image size for the <code>scale</code> option above. You can use this to avoid hitting expensive parser function limit. This option has an additional performance cost of roughly 2-5 milliseconds per image.",
		},
		{
			name = "link",
			type = "string",
			desc = "Name of a page on the wiki or an external URL for the image thumbnail to link to.",
		},
		{
			name = "caption",
			type = "string",
			desc = "[https://developer.mozilla.org/en-US/docs/Web/HTML/Element/img Alt text] for the image.",
		},
		{
			name = "checkExists",
			type = "boolean",
			default = "true",
			desc = "If set to <code>false</code> then the function skips the file existence check. A red link is returned instead of the 'please upload' placeholder.",
		},
	}
}
local franchiseCode = {
	required = true,
	type = "string",
	desc = "A [[Data:Franchise|franchise code]]."
}

p.Schemas = {
	image = {
		filename = {
			required = true,
			type = "string",
			desc = "Filename of the image, with or without the namespace prefix.",
		},
		options = optionsSchema,
	},
	gameImage = {
		game = franchiseCode,
		subject = {
			type = "string",
			required = true,
		},
		type = {
			type = "string",
			required = true,
			enum = {"", "Artwork", "Icon", "Model", "Render", "Screenshot", "Sprite", "Texture"},
		},
		options = optionsSchema,
	},
	icon = {
		game = franchiseCode,
		subject = {
			type = "string",
			required = true,
		},
		options = optionsSchema,
	},
	logo = {
		code = franchiseCode,
		optons = optionsSchema,
	},
}

p.Documentation = {
	image = {
		desc = "A higher-level version of [[Module:UtilsMarkup#file|utilsMarkup.file]] with awareness of whether the file exists or not.",
		params = {"filename", "options"},
		returns = {
			"Wikitext rendering an image thumbnail.",
			"A boolean — true if the image exists, false otherwise.",
		},
		cases = {
			{
				args = {"File:TWW Great Fairy Figurine Model.png", { 
					link = "Great Fairy", 
					size = "100px"
				}},
				expect = {"[[File:TWW Great Fairy Figurine Model.png|100px|link=Great Fairy|TWW Great Fairy Figurine Model.png]]", true}
			},
			{
				desc = "If file does not exist, show 'click to upload' thumbnail which links to [[Special:Upload]].",
				args = {"File:TWWHD Great Fairy Figurine Model.png", {
					link = "Great Fairy", 
					size = "150px",
				}},
				expect = {"[[File:No Image Upload.png|150px|link=https://zelda.gamepedia.com/Special:Upload?wpDestFile=TWWHD+Great+Fairy+Figurine+Model.png|File:No Image Upload.png]]", false}
			},
			{
				desc = "'No image' thumbnail has minimum 100px width, because it is illegible at smaller sizes.",
				args = {"File:TWWHD Great Fairy Figurine Model.png", {
					size = "64px",
				}},
				expect = {"[[File:No Image Upload.png|100px|link=https://zelda.gamepedia.com/Special:Upload?wpDestFile=TWWHD+Great+Fairy+Figurine+Model.png|File:No Image Upload.png]]", false},
			},
			{
				desc = "<code>checkExists = false</code> skips the existence check and simply render a red link",
				args = {"File:TWWHD Great Fairy Figurine Model.png", {
					link = "Great Fairy", 
					size = "100px",
					checkExists = false,
				}},
				expect = {"[[File:TWWHD Great Fairy Figurine Model.png|100px|link=Great Fairy|TWWHD Great Fairy Figurine Model.png]]", nil}
			},
			{
				desc = "Scaling factor.",
				args = {"File:TMC Vaati Sprite.png", { scale = 2 }},
				expect = {"[[File:TMC Vaati Sprite.png|48x56px|TMC Vaati Sprite.png]]", true}
			},
			{
				args = {"File:TMC Vaati Sprite.png", { scale = 2, scaleUsingCargo = true }},
				expect = {"[[File:TMC Vaati Sprite.png|48x56px|TMC Vaati Sprite.png]]", true},
			},
			{
				desc = "If both <code>scale</code> and <code>size</code> are specified, the one resulting in the smaller image is used.",
				args = {"File:TMC Vaati Sprite.png", { scale = 2, size = "80px" }},
				expect = {"[[File:TMC Vaati Sprite.png|48x56px|TMC Vaati Sprite.png]]", true},
			},
			{
				args = {"File:TMC Vaati Sprite.png", { scale = 10, size = "80px" }},
				expect = {"[[File:TMC Vaati Sprite.png|80px|TMC Vaati Sprite.png]]", true},
			},
		},
	},
	gameImage = {
		desc = "A specialized version of [[Module:File#image|image]] that infers the filename from game, subject, and type.",
		params = {"game", "subject", "type", "options"},
		returns = {
			"A <code>string</code> of wikitext that renders a thumbnail.",
			"A boolean — true if the image exists, false otherwise.",
		},
		cases = {
			{
				args = {"TWW", "Great Fairy Figurine", "Model", { 
					link = "Great Fairy", 
					size = "100px"
				}},
				expect = {"[[File:TWW Great Fairy Figurine Model.png|100px|link=Great Fairy|TWW Great Fairy Figurine Model.png]]", true}
			},
		}
	},
	icon = {
		params = {"game", "subject", "options"},
		returns = "An icon thumbnail for the subject in the given game.",
		cases = {
			{
				args = {"LANS", "Pineapple"},
				expect = "[[File:LANS Pineapple Icon.png|LANS Pineapple Icon.png]]"
			},
			{
				args = {"LADX", "Pineapple"},
				expect = "[[File:LADX Pineapple Sprite.png|LADX Pineapple Sprite.png]]"
			},
		}
	},
	logo = {
		params = {"code", "options"},
		returns = {
			"Given a valid [[Data:Franchise|franchise code]], returns a logo thumbnail.",
			"A boolean indicating whether a logo exists for the game yet.",
		},
		cases = {
			{
				args = {"LANS", { size = "200px" }},
				expect = {"[[File:LANS English Logo.png|200px|LANS English Logo.png]]", true}
			},
			{
				args = {"SSB4", { size = "200px" }},
				expect = {"[[File:SSB4 Logo.png|200px|SSB4 Logo.png]]", true},
			},
			{
				args = {"SS (Himekawa)", { size = "200px" }},
				expect = {"[[File:Viz Media Logo.svg|200px|Viz Media Logo.svg]]", true}
			},
			{
				args = {"TLoZ (Mishouzaki)", { size = "200px" }},
				expect = {"[[File:TLoZ (Mishouzaki) Manga Cover Artwork.png|200px|TLoZ (Mishouzaki) Manga Cover Artwork.png]]", true},
			},
			{
				args = {"TAoL (Mishouzaki)", { size = "200px" }},
				expect = {"[[File:No Image Upload.png|200px|link=https://zelda.gamepedia.com/Special:Upload?wpDestFile=TAoL+%28Mishouzaki%29+Manga+Cover+Artwork.png|File:No Image Upload.png]]", false},
			},
			{
				args = {"E", { size = "200px" }},
				expect = {"[[File:The Legend of Zelda Encyclopedia Cover.png|200px|The Legend of Zelda Encyclopedia Cover.png]]", true},
			},
			{
				args = {"TMoL", { size = "200px" }},
				expect = {"[[File:Misadventures Link logo2.png|200px|Misadventures Link logo2.png]]", true},
			},
		}
	}
}

return p