Module:LayeredImage
Jump to navigation
Jump to search
Documentation for this module may be created at Module:LayeredImage/doc
local p = {}
local MAX_LAYERS = 32
local DEFAULT_SIZE = 200
local MAX_SIZE = 4096
local FILE_NAMESPACE = 6
local ALLOWED_NAMED_ARGS = {
alt = true,
class = true,
link = true,
link_layer = true,
link_mode = true,
preview = true,
size = true,
}
local function trim(value)
if value == nil then
return ''
end
return mw.text.trim(tostring(value))
end
local function errorMarkup(message)
return tostring(
mw.html.create('strong')
:addClass('error')
:wikitext('Layered image: ' .. mw.text.nowiki(message))
)
end
local function parseLayerKey(key)
local number = tonumber(key)
if number and number >= 1 and number == math.floor(number) then
return number
end
return nil
end
local function collectLayers(args)
local layers = {}
local highest = 0
for key, value in pairs(args) do
local index = parseLayerKey(key)
if index then
local image = trim(value)
if image ~= '' then
if index > MAX_LAYERS then
return nil, string.format(
'layer %d exceeds the supported maximum of %d',
index,
MAX_LAYERS
)
end
layers[index] = image
highest = math.max(highest, index)
end
elseif not ALLOWED_NAMED_ARGS[tostring(key)] then
return nil, string.format("unknown parameter '%s'", tostring(key))
end
end
if highest == 0 then
return nil, 'at least one numbered image layer is required'
end
for index = 1, highest do
if not layers[index] then
return nil, string.format('layer %d is missing', index)
end
end
return layers
end
local function normalizeFileName(value, index)
local title = mw.title.new(value, FILE_NAMESPACE)
if not title or title.namespace ~= FILE_NAMESPACE or title.interwiki ~= '' or title.fragment ~= '' then
return nil, string.format("layer %d is not a valid local file title: '%s'", index, value)
end
return title.text
end
local function normalizeSize(value)
local raw = trim(value)
if raw == '' then
return DEFAULT_SIZE .. 'px'
end
local number = tonumber(raw:match('^(%d+)px$') or raw:match('^(%d+)$'))
if not number or number < 1 or number > MAX_SIZE then
return nil, string.format('size must be between 1px and %dpx', MAX_SIZE)
end
return number .. 'px'
end
local function normalizeClasses(value)
local classes = {}
for class in trim(value):gmatch('%S+') do
if not class:match('^[%w_-]+$') then
return nil, string.format("invalid CSS class '%s'", class)
end
classes[#classes + 1] = class
end
return classes
end
local function normalizeLink(args, layerCount)
local link = trim(args.link)
local mode = trim(args.link_mode):lower()
if link ~= '' then
if mode ~= '' and mode ~= 'page' then
return nil, nil, nil, 'link_mode must be page when link is supplied'
end
local title = mw.title.new(link)
if not title or title.interwiki ~= '' then
return nil, nil, nil, string.format("link is not a valid local page title: '%s'", link)
end
mode = 'page'
link = title.fullText
elseif mode == '' or mode == 'none' then
mode = 'none'
link = nil
elseif mode == 'file' then
link = nil
elseif mode == 'page' then
return nil, nil, nil, 'link_mode=page requires link'
else
return nil, nil, nil, "link_mode must be 'none', 'file', or 'page'"
end
local rawLayer = trim(args.link_layer)
local linkLayer = rawLayer == '' and layerCount or tonumber(rawLayer)
if not linkLayer or linkLayer ~= math.floor(linkLayer) or linkLayer < 1 or linkLayer > layerCount then
return nil, nil, nil, string.format('link_layer must be between 1 and %d', layerCount)
end
return mode, link, linkLayer
end
local function previewEnabled(value)
local normalized = trim(value):lower()
if normalized == '' or normalized == '0' or normalized == 'false' or normalized == 'no' then
return false
end
if normalized == '1' or normalized == 'true' or normalized == 'yes' then
return true
end
return nil
end
function p.render(frame)
local parent = frame:getParent()
local args = parent and parent.args or frame.args
local layers, layerError = collectLayers(args)
if not layers then
return errorMarkup(layerError)
end
for index, value in ipairs(layers) do
local fileName, fileError = normalizeFileName(value, index)
if not fileName then
return errorMarkup(fileError)
end
layers[index] = fileName
end
local size, sizeError = normalizeSize(args.size)
if not size then
return errorMarkup(sizeError)
end
local classes, classError = normalizeClasses(args.class)
if not classes then
return errorMarkup(classError)
end
local linkMode, link, linkLayer, linkError = normalizeLink(args, #layers)
if not linkMode then
return errorMarkup(linkError)
end
local alt = trim(args.alt)
local wantsPreview = previewEnabled(args.preview)
if wantsPreview == nil then
return errorMarkup("preview must be '1', 'true', 'yes', '0', 'false', or 'no'")
end
return frame:callParserFunction{
name = '#layeredimage',
args = {
mw.text.jsonEncode{
layers = layers,
width = tonumber(size:match('^(%d+)px$')),
alt = alt,
linkMode = linkMode,
link = link,
linkLayer = linkLayer,
classes = classes,
preview = wantsPreview,
},
},
}
end
return p