Module:PortalBase

From Soyjak Wiki, the free ensoyclopedia
Jump to navigationJump to search

This module implements {{PortalBase}}. See the template documentation for more usage info.


local getArgs = require('Module:Arguments').getArgs
local p = {}
local stylesheet = 'Module:PortalBase/styles.css'

local function cleanArgument(key, value)
    if type(value) == 'string' and mw.text.trim(value) == '' then
        return nil
    end

    return value
end

local function collectRowNumbers(args)
    local numbers = {}
    local seen = {}

    for key in pairs(args) do
        if type(key) == 'string' then
            local digits = key:match('^row([1-9]%d*)$')
                or key:match('^row([1-9]%d*)_')

            if digits then
                local number = tonumber(digits)

                if not seen[number] then
                    seen[number] = true
                    numbers[#numbers + 1] = number
                end
            end
        end
    end

    table.sort(numbers)

    return numbers
end

local function alignmentClass(value, classPrefix)
    if type(value) ~= 'string' then
        return nil
    end

    local alignment = mw.text.trim(value):lower()

    if alignment == 'left'
        or alignment == 'center'
        or alignment == 'right'
    then
        return (classPrefix or 'portal-align-') .. alignment
    end

    return nil
end

local function renderHeaderImage(header, content, position)
    if not content then
        return
    end

    header:tag('div')
        :addClass('portal-header-image')
        :addClass('portal-header-image-' .. position)
        :newline()
        :wikitext(content)
        :newline()
end

local function renderHeader(args)
    if not (
        args.title
        or args.subtitle
        or args.header_image1
        or args.header_image2
    ) then
        return nil
    end

    local header = mw.html.create('div')
        :addClass('portal-header')
        :addClass(alignmentClass(args.header_align))

    renderHeaderImage(header, args.header_image1, 'start')

    if args.title or args.subtitle then
        local text = header:tag('div')
            :addClass('portal-header-text')

        if args.title then
            text:tag('div')
                :addClass('portal-title')
                :wikitext(args.title)
        end

        if args.subtitle then
            text:tag('div')
                :addClass('portal-subtitle')
                :wikitext(args.subtitle)
        end
    end

    renderHeaderImage(header, args.header_image2, 'end')

    return header
end

local function renderSection(section)
    if not section.content then
        return nil
    end

    local card = mw.html.create('div')
        :addClass('portal-card')
        :addClass(section.class)
        :addClass(alignmentClass(section.align))

    if section.heading or section.action then
        local heading = card:tag('div')
            :addClass('portal-card-heading')
            :addClass(alignmentClass(section.headerAlign))

        if section.heading then
            heading:tag('div')
                :addClass('portal-card-heading-text')
                :wikitext(section.heading)
        end

        if section.action then
            heading:tag('span')
                :addClass('portal-card-heading-action')
                :wikitext(section.action)
        end
    end

    card:tag('div')
        :addClass('portal-card-body')
        :newline()
        :wikitext(section.content)
        :newline()

    if section.footer then
        card:tag('div')
            :addClass('portal-card-footer')
            :newline()
            :wikitext(section.footer)
            :newline()
    end

    return card
end

local function renderFullRow(args, number)
    local prefix = 'row' .. number

    local section = renderSection({
        heading = args[prefix .. '_header'],
        action = args[prefix .. '_action'],
        content = args[prefix],
        footer = args[prefix .. '_footer'],
        align = args[prefix .. '_align'],
        headerAlign = args[prefix .. '_header_align'],
    })

    if not section then
        return nil
    end

    local row = mw.html.create('div')
        :addClass('portal-row')
        :addClass('portal-row-full')
        :addClass('portal-row-' .. number)

    row:node(section)

    return row
end

local function renderRawRow(args, number)
    local prefix = 'row' .. number
    local content = args[prefix]

    if not content then
        return nil
    end

    local row = mw.html.create('div')
        :addClass('portal-row')
        :addClass('portal-row-raw')
        :addClass('portal-row-' .. number)
        :addClass(alignmentClass(args[prefix .. '_align']))

    row:newline()
        :wikitext(content)
        :newline()

    return row
end

local function collectSectionNumbers(args, prefix)
    local numbers = {}
    local seen = {}
    local pattern = '^' .. prefix .. '([1-9]%d*)'

    for key in pairs(args) do
        if type(key) == 'string' then
            local digits = key:match(pattern .. '$')
                or key:match(pattern .. '_')

            if digits then
                local number = tonumber(digits)

                if not seen[number] then
                    seen[number] = true
                    numbers[#numbers + 1] = number
                end
            end
        end
    end

    table.sort(numbers)

    return numbers
end

local function renderColumn(args, rowPrefix, side)
    local sidePrefix = rowPrefix .. '_' .. side
    local cards = {}

    local function addSection(prefix, number)
        local class = 'portal-card-' .. side

        if number then
            class = class .. ' portal-card-' .. side .. '-' .. number
        end

        local card = renderSection({
            heading = args[prefix .. '_header'],
            action = args[prefix .. '_action'],
            content = args[prefix],
            footer = args[prefix .. '_footer'],
            align = args[prefix .. '_align'] or args[sidePrefix .. '_align'],
            headerAlign = args[prefix .. '_header_align'],
            class = class
        })

        if card then
            cards[#cards + 1] = tostring(card)
        end
    end

    addSection(sidePrefix)

    for _, number in ipairs(collectSectionNumbers(args, sidePrefix)) do
        addSection(sidePrefix .. number, number)
    end

    if #cards == 0 then
        return nil
    end

    return table.concat(cards, '\n')
end

local function normalizeFlex(value)
    local number = tonumber(value)

    if number and number > 0 and number < math.huge then
        return tostring(number)
    end

    return nil
end

local function renderColumnsRow(frame, args, number)
    local prefix = 'row' .. number
    local left = renderColumn(args, prefix, 'left')
    local right = renderColumn(args, prefix, 'right')

    if not left and not right then
        return nil
    end

    local row = mw.html.create('div')
        :addClass('portal-row')
        :addClass('portal-row-' .. number)
        :addClass(alignmentClass(args[prefix .. '_align']))

    if left and right then
        row:addClass('portal-row-columns')
            :newline()
            :wikitext(frame:expandTemplate({
                title = 'Flex columns',
                args = {
                    [1] = left,
                    [2] = right,
                    flex1 = normalizeFlex(args[prefix .. '_flex1']),
                    flex2 = normalizeFlex(args[prefix .. '_flex2'])
                }
            }))
            :newline()
    else
        row:addClass('portal-row-full')
            :newline()
            :wikitext(left or right)
            :newline()
    end

    return row
end

local function renderTilesRow(frame, args, number)
    local prefix = 'row' .. number
    local itemPrefix = prefix .. '_item'
    local items = {}

    for _, itemNumber in ipairs(collectSectionNumbers(args, itemPrefix)) do
        local key = itemPrefix .. itemNumber
        local content = args[key]

        if content then
            local tile = mw.html.create('div')
                :addClass('portal-tile')
                :addClass('portal-tile-' .. itemNumber)
                :addClass(alignmentClass(args[key .. '_align']))

            tile:newline()
                :wikitext(content)
                :newline()

            items[#items + 1] = tostring(tile)
        end
    end

    if #items == 0 then
        return nil
    end

    local group = mw.html.create('div')
        :addClass('portal-tile-group')

    group:newline()
        :wikitext(frame:expandTemplate({
            title = 'Flexbox',
            args = items
        }))
        :newline()

    local section = renderSection({
        heading = args[prefix .. '_header'],
        action = args[prefix .. '_action'],
        content = tostring(group),
        footer = args[prefix .. '_footer'],
        align = args[prefix .. '_align'],
        headerAlign = args[prefix .. '_header_align'],
    })

    local row = mw.html.create('div')
        :addClass('portal-row')
        :addClass('portal-row-tiles')
        :addClass('portal-row-' .. number)

    row:node(section)

    return row
end

local function renderRow(frame, args, number)
    local prefix = 'row' .. number
    local layout = args[prefix .. '_layout']

    if type(layout) == 'string' then
        layout = mw.text.trim(layout):lower()
    end

    if layout == 'raw' then
        return renderRawRow(args, number)
    end

    if layout == 'tiles' then
        return renderTilesRow(frame, args, number)
    end

    if layout == 'columns' then
        return renderColumnsRow(frame, args, number)
    end

    if layout == 'full' or args[prefix] then
        return renderFullRow(args, number)
    end

    if not layout then
        return renderColumnsRow(frame, args, number)
    end

    return renderFullRow(args, number)
end

local function renderFooter(args)
    local section = renderSection({
        heading = args.footer_header,
        action = args.footer_action,
        content = args.footer,
        align = args.footer_align,
        headerAlign = args.footer_header_align,
    })

    if not section then
        return nil
    end

    local footer = mw.html.create('div')
        :addClass('portal-footer')

    footer:node(section)

    return footer
end

function p.main(frame)
    local args = getArgs(frame, {
        valueFunc = cleanArgument
    })

    local portal = mw.html.create('div')
        :addClass('portal-template')
        :addClass(args.class)
        :addClass(alignmentClass(args.align))
        :addClass(alignmentClass(args.header_align, 'portal-heading-align-'))

    portal:node(renderHeader(args))

    local content = mw.html.create('div')
        :addClass('portal-content')

    local manualContent = args.content or args[1]
    local hasContent = false

    if manualContent then
        content:addClass('portal-content-manual')
            :newline()
            :wikitext(manualContent)
            :newline()

        hasContent = true
    else
        local rows = collectRowNumbers(args)

        for _, number in ipairs(rows) do
            local row = renderRow(frame, args, number)

            if row then
                content:newline()
                    :node(row)
                    :newline()

                hasContent = true
            end
        end
    end

    if hasContent then
        portal:node(content)
    end

    portal:node(renderFooter(args))

    local styles = frame:extensionTag({
        name = 'templatestyles',
        args = {
            src = stylesheet
        }
    })

    return styles .. tostring(portal)
end

return p