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 data = mw.loadData("Module:File/Data")
local CARGO_TABLE = "Files"
-- 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
-- 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 options.scale then
local redirectTarget = mw.title.new("File:"..filename).redirectTarget
redirectTarget = redirectTarget and redirectTarget.text
if redirectTarget then
filename = redirectTarget
end
end
if checkExists and not utilsPage.exists("File:" .. filename, true) then
return h.noimage(filename, sizeWidth, sizeHeight, options), false
end
if options.scale then
local file
if options.scaleUsingCargo then
results = utilsCargo.query(CARGO_TABLE, "width, height", {
where = utilsCargo.allOf({
["_pageName"] = "File:"..filename,
}),
limit = 1,
})
file = results[1]
end
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
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 options = utilsTable.merge({}, options, {
link = "File:" .. filename
})
-- 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=File: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=File: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