Module:FileLink
Details
The FileLink module offers a Lua class that makes it easier to work with links to files, as well as some functions to create file-linking templates. It uses Module:Size, Module:StringSet, and Module:Time internally to do this.
Unlike ordinary file links, this module will ignore key arguments that aren't used for files, making it useful in templates which create links to files but may be configured with other arguments. Although it will fail if there are more than two positional arguments (a file and a caption), the methods specifically for use in templates have some ways of getting around this.
This page will only document the functionality specific to templates, since those looking to understand how to use it in modules can just read the code directly.
Usage
fromFrame
The fromFrame
function simply creates a file link directly from the arguments passed to it via an {{#invoke:...}}
call. So, for example, {{#invoke:FileLink|fromFrame|MyFile|...}}
will be roughly equivalent to [[File:MyFile|...]]
, although unrecognized key arguments will be ignored as described earlier.
This function mostly exists to test out how the FileLink module works, and probably isn't very useful in practice. You can experiment with it for yourself on a user page.
fromParent
This is the intended way to use FileLink in templates, and it's designed to help create templates which have custom options added to file links by default. It has access to both the arguments passed directly to it via {{#invoke:...}}
and those passed to the parent template as well, making it quite powerful. Essentially, the invocation arguments are used to set default options for the file link while the parent's arguments are then used to construct the actual link. You can think of this as a way of converting a template into a special fromFrame
call with custom defaults, where those defaults are included in the template itself.
The original code for this module (before it was rewritten), for example, was to create a pixelated image template which always applied a pixelated
class to images that got passed to it. This template can be represented with the following code:
{{#invoke:FileLink|fromParent|class=pixelated}}
However, because you can use wikitext to control the arguments provided here, you can actually do more than just set options. For example, the old Pixelated module allowed an unpixelated=1
argument that would remove the pixelated class, so that you could opt out of pixelation instead of opting in by providing the class. You can accomplish this via:
{{#invoke:FileLink|fromParent|class={{#if:{{{unpixelated|}}}||pixelated}}}}
--- Module:FileLink
--- by User:LtDk, licensed CC-BY-SA 4.0
--- Class designed to make it easier to link files in custom templates.
local Size = require('Module:Size')
local StringSet = require('Module:StringSet')
local Time = require('Module:Time')
--- Format argument for file links. (frameless, framed, thumbnail)
--- @enum Format
local Format = {
frameless = 'frameless',
frame = 'frame',
framed = 'frame',
thumb = 'thumb',
thumbnail = 'thumb',
}
local Upright = {}
--- "Upright" sizing argument to file links.
--- @class Upright
--- @field [1] number?
local UprightProto = {}
local UprightMeta = { __index = UprightProto }
--- Checks if a value is an "upright" sizing.
--- @param value any
--- @return boolean
function Upright.isUpright(value)
return type(value) == 'table' and rawequal(getmetatable(value), UprightMeta)
end
function UprightMeta.__eq(lhs, rhs)
if not Upright.isUpright(rhs) then
return false
end
return lhs[1] == rhs[1]
end
function UprightMeta:__newindex(k, v)
if k ~= 1 then
error(string.format('invalid key %s', k))
end
if v ~= nil and (type(v) ~= 'number' or v <= 0) then
error(string.format('upright requires a positive number, got %q', tostring(v)))
end
rawset(self, k, v)
end
function UprightMeta:__tostring()
if self[1] ~= nil then
return string.format('upright %s', tostring(self[1]))
else
return 'upright'
end
end
local uplen = string.len('upright')
--- Parses an "upright" sizing from a string.
--- @param val string
--- @return Upright
function Upright.parse(val)
val = tostring(val)
local upright = Upright.tryParse(val)
if upright == nil then
error(string.format('not a valid upright sizing: %q', val))
end
return upright
end
--- Tries to parses an "upright" sizing from a string, returning nil instead of errors.
--- @param val string
--- @return Upright?
function Upright.tryParse(val)
val = mw.text.trim(tostring(val))
if string.lower(string.sub(val, 1, uplen)) == 'upright' then
val = mw.text.trim(string.sub(val, uplen + 1))
end
if string.sub(val, 1, 1) == '=' then
val = mw.text.trim(string.sub(val, 2))
end
if val == '' then
return Upright.new(nil)
end
local num = tonumber(val)
if num == nil or num <= 0 then
return nil
end
return Upright.new(num)
end
--- Creates a new "upright" sizing.
--- @param val number?
--- @return Upright
function Upright.new(val)
local up = { val }
if val ~= nil and (type(val) ~= 'number' or val <= 0) then
error(string.format('upright requires a positive number, got %q', tostring(val)))
end
setmetatable(up, UprightMeta)
return up
end
--- Horizontal alignment argument for file links.
--- @enum HAlign
local HAlign = {
left = 'left',
right = 'right',
center = 'center',
none = 'none',
}
--- Vertical alignment argument for file links.
--- @enum VAlign
local VAlign = {
baseline = 'baseline',
sub = 'sub',
super = 'super',
top = 'top',
['text-top'] = 'text-top',
middle = 'middle',
bottom = 'bottom',
['text-bottom'] = 'text-bottom',
}
local FileLinkOptions = { Upright = Upright }
--- Options passed to a file link. Includes everything but the file being linked.
--- @class FileLinkOptions
--- @field border boolean?
--- @field format Format?
--- @field sizing Size|Upright?
--- @field halign HAlign?
--- @field valign VAlign?
--- @field link string?
--- @field alt string?
--- @field page integer?
--- @field thumbtime Time?
--- @field start Time?
--- @field muted boolean?
--- @field loop boolean?
--- @field class StringSet?
--- @field caption string?
local FileLinkOptionsProto = {}
local FileLinkOptionsMeta = { __index = FileLinkOptionsProto }
local function booleanOnly(k, v)
if v ~= nil and type(v) ~= 'boolean' then
error(k .. ' must be boolean or nil')
end
return v
end
local function stringOnly(k, v)
if v ~= nil and type(v) ~= 'string' then
error(k .. ' must be string or nil')
end
return v
end
local function positiveIntegerOnly(k, v)
if v ~= nil and (type(v) ~= 'number' or math.floor(v) ~= v or v < 0) then
error(k .. ' must be positive integer or nil')
end
return v
end
local function enumOnly(k, v)
local enum = {
format = Format,
halign = HAlign,
valign = VAlign,
}
if v ~= nil then
local val = enum[k][v]
if val == nil then
error(k .. ' cannot have value ' .. tostring(v))
end
return val
end
end
local function titleOnly(k, v)
if v == '' and k ~= 'file' then
if k == 'link' then
return ''
else
return nil
end
end
local defaultNs = nil
if k == 'file' then
defaultNs = 'File'
if v == nil then
error('file cannot be nil')
end
end
if type(v) == 'table' and v.fullText then
return v.fullText
elseif type(v) == 'string' then
local title = mw.title.new(v, defaultNs)
if not title then
error(k .. ' cannot be ' .. tostring(v))
end
return title.fullText
elseif v ~= nil then
error(k .. ' cannot be ' .. tostring(v))
end
end
local function timeOnly(k, v)
if v ~= nil and not Time.isTime(v) then
error(k .. ' must be Time or nil')
end
return v
end
local function stringSetOnly(k, v)
if v ~= nil and not StringSet.isStringSet(v) then
error(k .. ' must be StringSet or nil')
end
return v
end
local function optionsOnly(k, v)
if v ~= nil and not FileLinkOptions.isFileLinkOptions(v) then
error(k .. ' must be FileLinkOptions or nil')
end
return v
end
local function sizingOnly(_, v)
if v ~= nil and not Size.isSize(v) and not Upright.isUpright(v) then
error('sizing must be Size, Upright, or nil')
end
return v
end
local validateOptions = {
border = booleanOnly,
format = enumOnly,
sizing = sizingOnly,
halign = enumOnly,
valign = enumOnly,
link = titleOnly,
alt = stringOnly,
page = positiveIntegerOnly,
thumbtime = timeOnly,
start = timeOnly,
muted = booleanOnly,
loop = booleanOnly,
class = stringSetOnly,
caption = stringOnly,
}
function FileLinkOptionsMeta:__newindex(k, v)
local f = validateOptions[k]
if f == nil then
error('invalid key ' .. k)
else
v = f(k, v)
rawset(self, k, v)
end
end
--- Options are always formatted with a leading pipe if they exist, to allow easily creating links.
function FileLinkOptionsMeta:__tostring()
local ret = ''
if self.border then
ret = ret .. '|border'
end
if self.format ~= nil then
ret = ret .. '|' .. self.format
end
if self.sizing ~= nil then
ret = ret .. '|' .. tostring(self.sizing)
end
if self.halign ~= nil then
ret = ret .. '|' .. self.halign
end
if self.valign ~= nil then
ret = ret .. '|' .. self.valign
end
if self.link ~= nil then
ret = ret .. '|link=' .. self.link
end
if self.alt ~= nil then
ret = ret .. '|alt=' .. self.alt
end
if self.page ~= nil then
ret = ret .. '|page=' .. tostring(self.page)
end
if self.thumbtime ~= nil then
ret = ret .. '|thumbtime=' .. tostring(self.thumbtime)
end
if self.start ~= nil then
ret = ret .. '|start=' .. tostring(self.start)
end
if self.muted then
ret = ret .. '|muted'
end
if self.loop then
ret = ret .. '|loop'
end
if self.class ~= nil then
ret = ret .. '|class='
local first = true
for _, key in ipairs(self.class:sortedKeys()) do
if first then
first = false
else
ret = ret .. ' '
end
ret = ret .. key
end
end
if self.caption ~= nil then
ret = ret .. '|' .. mw.text.nowiki('') .. self.caption
end
return ret
end
--- Checks if a value is FileLinkOptions.
--- @param value any
--- @return boolean
function FileLinkOptions.isFileLinkOptions(value)
return type(value) == 'table' and rawequal(getmetatable(value), FileLinkOptionsMeta)
end
--- Creates a new FileLinkOptions with its arguments defaulted to those in defaults.
--- @param self FileLinkOptions
--- @param defaults FileLinkOptions
--- @return FileLinkOptions
function FileLinkOptionsProto:withDefaults(defaults)
if not FileLinkOptions.isFileLinkOptions(defaults) then
error('cannot merge FileLinkOptions with non-FileLinkOptions')
end
-- copy over this link
local opts = {
border = self.border,
format = self.format,
sizing = self.sizing,
link = self.link,
alt = self.alt,
page = self.page,
thumbtime = self.thumbtime,
start = self.start,
muted = self.muted,
loop = self.loop,
class = self.class,
caption = self.caption,
}
-- boolean flags: override if nil
if self.border == nil then
opts.border = defaults.border
end
if self.muted == nil then
opts.muted = defaults.muted
end
if self.loop == nil then
opts.loop = defaults.loop
end
-- merge classes
if self.class == nil then
opts.class = defaults.class
else
self.class:addAll(defaults.class)
end
-- replace caption
if self.caption == nil then
opts.caption = defaults.caption
end
setmetatable(opts, FileLinkOptionsMeta)
return opts
end
--- Creates new file link options.
--- @return FileLinkOptions
function FileLinkOptions.new()
local opts = {}
setmetatable(opts, FileLinkOptionsMeta)
return opts
end
local valMatch = {}
for key, val in pairs(Format) do
valMatch[key] = function (opts)
opts.format = val
end
end
for key, val in pairs(HAlign) do
valMatch[key] = function (opts)
opts.halign = val
end
end
for key, val in pairs(VAlign) do
valMatch[key] = function (opts)
opts.valign = val
end
end
function valMatch.border(opts)
opts.border = true
end
function valMatch.muted(opts)
opts.muted = true
end
function valMatch.loop(opts)
opts.loop = true
end
local function stringOk(opts, key, val)
opts[key] = validateOptions[key](key, val)
end
local function numberFirst(opts, key, val)
local num = tonumber(val)
if num == nil then
error(key .. ' must be a number')
end
opts[key] = validateOptions[key](key, num)
end
local function timeFirst(opts, key, val)
local time = Time.parse(val)
opts[key] = time
end
local keyMatch = {}
function keyMatch.upright(opts, key, val)
opts.sizing = Upright.parse(val)
end
keyMatch.link = stringOk
keyMatch.alt = stringOk
keyMatch.page = numberFirst
keyMatch.thumbtime = timeFirst
keyMatch.start = timeFirst
function keyMatch.class(opts, key, val)
opts.class = StringSet.new(mw.text.split(val, '%s'))
end
--- Creates new file link options from a list of arguments.
--- @param args { [number|string]: string } arguments to file link; mirrors template arguments
--- @param skip number? number of positional arguments to skip
--- @return FileLinkOptions
function FileLinkOptions.fromArgs(args, skip)
local opts = FileLinkOptions.new()
if skip == nil then
skip = 0
end
for idx, arg in ipairs(args) do
if idx > skip then
arg = mw.text.trim(arg)
local match = valMatch[arg]
if match ~= nil then
match(opts)
else
local gotupright = false
if string.lower(string.sub(arg, 1, uplen)) == 'upright' then
local upright = Upright.tryParse(arg)
if upright ~= nil then
opts.sizing = upright
gotupright = true
end
end
if not gotupright then
local size = Size.tryParse(arg)
if size ~= nil then
opts.sizing = size
elseif opts.caption == nil then
opts.caption = arg
else
error(string.format('multiple captions in options: %q and %q', opts.caption,
arg))
end
end
end
end
end
for key, val in pairs(args) do
if tonumber(key) == nil then
local match = keyMatch[key]
if match ~= nil then
match(opts, key, val)
end
end
end
return opts
end
local FileLink = { Options = FileLinkOptions }
--- Link to a file.
--- @class FileLink
--- @field file string
--- @field options FileLinkOptions?
local FileLinkProto = {}
local FileLinkMeta = { __index = FileLinkProto }
local validateLink = {
file = titleOnly,
options = optionsOnly,
}
function FileLinkMeta:__newindex(k, v)
local f = validateLink[k]
if f == nil then
error('invalid key ' .. k)
else
v = f(k, v)
rawset(self, k, v)
end
end
--- File links are formatted as, real shocker, the wikitext for file links.
function FileLinkMeta:__tostring()
local opts = ''
if self.options ~= nil then
opts = tostring(self.options)
end
return string.format('[[%s%s]]', self.file, opts)
end
--- Creates new a file link with no options.
--- @param file string
--- @return FileLink
function FileLink.new(file)
local fl = { file = titleOnly('file', file) }
setmetatable(fl, FileLinkMeta)
return fl
end
--- Creates new file link from a list of arguments.
--- @param args { [number|string]: string } arguments to file link; mirrors template arguments
--- @param skip number? number of arguments to skip
--- @return FileLink
function FileLink.fromArgs(args, skip)
if skip == nil then
skip = 0
end
local file = titleOnly('file', args[skip + 1])
local opts = FileLinkOptions.fromArgs(args, skip + 1)
local fl = { file = file, options = opts }
setmetatable(fl, FileLinkMeta)
return fl
end
--- Creates a new file link from frame's arguments, stringifying the result.
--- @param frame mw.frame
--- @return string
function FileLink.fromFrame(frame)
local link = FileLink.fromArgs(frame.args)
return tostring(link)
end
--- Creates a new file link from the parent frame's arguments, using the defaults for this frame.
--- If the first argument to this frame is a number, uses that as the number of positional arguments
--- to skip on the parent before parsing file options.
--- @param frame mw.frame
--- @return string
function FileLink.fromParent(frame)
local skip = tonumber(frame.args[1])
local skipOpts = 0
if skip ~= nil then
skipOpts = 1
end
local defaults = FileLinkOptions.fromArgs(frame.args, skipOpts)
frame = frame:getParent()
local link = FileLink.fromArgs(frame.args, skip)
link.options = link.options:withDefaults(defaults)
return tostring(link)
end
return FileLink