Toggle menu
Toggle preferences menu
Toggle personal menu
Not logged in
Your IP address will be publicly visible if you make any edits.

Engine that renders specific card types defined as wrapper modules. Each wrapper supplies a configuration to Card.main, which returns a rendered module bound to that configuration.


local Card = {}

local BASE_STYLES = 'Template:Card/styles.css'
local BASE_BLOCK = 'card'

local function nilIfBlank(value)
    local trimmed = value and mw.text.trim(value)
    return trimmed ~= '' and trimmed or nil
end

Card.slots = {}

Card.slots.media = function(opts)
    opts = opts or {}
    local iconSize  = opts.iconSize  or 128
    local imageSize = opts.imageSize or 200

    return {
        args = {'image', 'icon'},
        render = function(parent, args, config)
            if args.icon or args.image then
                local div = parent:tag('div')
                    :addClass(BASE_BLOCK .. '__media')
                    :addClass(config.block .. '__media')
                if args.icon then
                    div:addClass(BASE_BLOCK .. '__media--icon')
                       :addClass(config.block .. '__media--icon')
                       :wikitext(string.format('[[File:%s|%dpx|link=]]',
                           args.icon, iconSize))
                else
                    div:wikitext(string.format('[[File:%s|%dpx|link=]]',
                        args.image, imageSize))
                end
            end
        end,
    }
end

Card.slots.title = function()
    return {
        args = {'title'},
        render = function(parent, args, config)
            if not args.title then return end
            parent:tag('div')
                :addClass(BASE_BLOCK .. '__title')
                :addClass(config.block .. '__title')
                :wikitext(args.title)
        end,
    }
end

Card.slots.description = function()
    return {
        args = {'description'},
        render = function(parent, args, config)
            if not args.description then return end
            parent:tag('div')
                :addClass(BASE_BLOCK .. '__description')
                :addClass(config.block .. '__description')
                :wikitext(args.description)
        end,
    }
end

Card.slots.interaction = function()
    return {
        args = {'link', 'title'},
        render = function(parent, args, config)
            local linkTarget = args.link or args.title
            if not linkTarget then return end
            parent:wikitext(string.format('[[%s|<span></span>]]', linkTarget))
        end,
    }
end

Card.modifiers = {}

Card.modifiers.titleOnly = function()
    return {
        name = 'title-only',
        when = function(args) return args.title and not args.description end,
    }
end

local function validateConfig(rawConfig)
    for _, name in ipairs({'block', 'styles'}) do
        if rawConfig[name] == nil then
            error(string.format('Module:Card: missing required field "%s"', name), 2)
        end
    end

    for i, slot in ipairs(rawConfig.slots or {}) do
        if type(slot) ~= 'table' or type(slot.render) ~= 'function' then
            error(string.format('Module:Card: slot %d must be a table with a render function', i), 2)
        end
    end
end

local function buildConfig(rawConfig)
    local argNames = {}
    local seen = {}
    for _, slot in ipairs(rawConfig.slots or {}) do
        for _, name in ipairs(slot.args or {}) do
            if not seen[name] then
                table.insert(argNames, name)
                seen[name] = true
            end
        end
    end

    return {
        block     = rawConfig.block,
        styles    = rawConfig.styles,
        slots     = rawConfig.slots or {},
        modifiers = rawConfig.modifiers or {},
        argNames  = argNames,
    }
end

local function normalizeArgs(rawArgs, config)
    local args = {}
    for _, name in ipairs(config.argNames) do
        args[name] = nilIfBlank(rawArgs[name])
    end
    return args
end

local function loadStyles(frame, config)
    local baseStyles = frame:extensionTag{
        name = 'templatestyles',
        args = {src = BASE_STYLES}
    }
    local typeStyles = frame:extensionTag{
        name = 'templatestyles',
        args = {src = config.styles}
    }
    return baseStyles .. typeStyles
end

local function buildCard(args, config)
    local block = config.block
    local html = mw.html.create('div')
        :addClass(BASE_BLOCK)
        :addClass(block)

    for _, modifier in ipairs(config.modifiers) do
        if modifier.when(args) then
            html:addClass(BASE_BLOCK .. '--' .. modifier.name)
            html:addClass(block .. '--' .. modifier.name)
        end
    end

    for _, slot in ipairs(config.slots) do
        slot.render(html, args, config)
    end

    return html
end

function Card.main(rawConfig)
    validateConfig(rawConfig)
    local config = buildConfig(rawConfig)

    return {
        render = function(frame)
            local args = normalizeArgs(frame:getParent().args, config)
            local html = buildCard(args, config)
            return loadStyles(frame, config) .. tostring(html)
        end
    }
end

return Card