Module:Navbox

From Remilia Wiki
Jump to navigation Jump to search

Documentation for this module may be created at Module:Navbox/doc

--------------------------------------------------------------------------------
-- Module:Navbox
-- Navigation box system for linking related articles.
-- Wikipedia-compatible with enhanced features.
--
-- INSTALLATION:
-- 1. Create page: Module:Navbox
-- 2. Paste this entire file
-- 3. Create TemplateStyles page: Module:Navbox/styles.css
--
-- USAGE:
-- {{#invoke:Navbox|navbox
--   |name = Navbox Name
--   |title = [[Topic]] Navigation
--   |group1 = Category | list1 = [[Link1]] · [[Link2]]
-- }}
--
-- @author Remilia Wiki
-- @license MIT
--------------------------------------------------------------------------------

local p = {}

--------------------------------------------------------------------------------
-- CONFIGURATION
--------------------------------------------------------------------------------

local MAX_GROUPS = 20  -- Maximum number of groups supported

--------------------------------------------------------------------------------
-- UTILITY FUNCTIONS
--------------------------------------------------------------------------------

local function trim(s)
    if not s then return nil end
    return tostring(s):match("^%s*(.-)%s*$")
end

local function isEmpty(s)
    if s == nil then return true end
    if type(s) == 'string' then
        return trim(s) == ''
    end
    return false
end

local function hasValue(s)
    return not isEmpty(s)
end

local function getArg(args, name, default)
    local val = args[name]
    if isEmpty(val) then return default end
    return trim(val)
end

local function getFrameArgs(frame)
    local args = {}
    local parentArgs = frame:getParent() and frame:getParent().args or {}
    local directArgs = frame.args or {}
    for k, v in pairs(directArgs) do args[k] = v end
    for k, v in pairs(parentArgs) do args[k] = v end
    return args
end

-- Count the number of content rows (for image rowspan)
local function countRows(args)
    local count = 0
    -- above row
    if hasValue(getArg(args, 'above')) then count = count + 1 end
    -- group/list rows
    for i = 1, MAX_GROUPS do
        local group = getArg(args, 'group' .. i)
        local list = getArg(args, 'list' .. i)
        if hasValue(list) or hasValue(group) then
            count = count + 1
        end
    end
    -- below row
    if hasValue(getArg(args, 'below')) then count = count + 1 end
    return count
end

--------------------------------------------------------------------------------
-- NAVBOX
--------------------------------------------------------------------------------

function p.navbox(frame)
    local args = getFrameArgs(frame)

    local name = getArg(args, 'name')
    local title = getArg(args, 'title')
    local state = getArg(args, 'state', 'autocollapse')
    local above = getArg(args, 'above')
    local below = getArg(args, 'below')
    local image = getArg(args, 'image')
    local imageleft = getArg(args, 'imageleft')
    local border = getArg(args, 'border')
    local bodyclass = getArg(args, 'bodyclass')
    local titleclass = getArg(args, 'titleclass')
    local listclass = getArg(args, 'listclass')
    local groupclass = getArg(args, 'groupclass')
    local groupwidth = getArg(args, 'groupwidth')
    local evenodd = getArg(args, 'evenodd')

    if isEmpty(title) then
        return '<span class="error">Error: Navbox title required</span>'
    end

    -- Calculate number of columns based on images
    local baseColspan = 2
    if hasValue(image) then baseColspan = baseColspan + 1 end
    if hasValue(imageleft) then baseColspan = baseColspan + 1 end

    -- Count content rows for image rowspan
    local contentRows = countRows(args)

    -- Build the navbox table
    local tbl = mw.html.create('table')
        :addClass('navbox')

    -- Handle child/nested navbox
    if border == 'child' or border == 'none' then
        tbl:addClass('navbox-child')
    end

    if hasValue(bodyclass) then
        tbl:addClass(bodyclass)
    end

    -- Handle collapse state
    if state == 'collapsed' then
        tbl:addClass('mw-collapsed')
        tbl:addClass('mw-collapsible')
    elseif state == 'expanded' then
        tbl:addClass('navbox-expanded')
    else
        tbl:addClass('mw-collapsible')
    end

    tbl:attr('cellspacing', '0')

    -- Title row
    local titleRow = tbl:tag('tr')
    local titleCell = titleRow:tag('th')
        :addClass('navbox-title')
        :attr('colspan', tostring(baseColspan))

    if hasValue(titleclass) then
        titleCell:addClass(titleclass)
    end

    -- V-T-E links
    if hasValue(name) and border ~= 'child' and border ~= 'none' then
        local vte = titleCell:tag('div')
            :addClass('navbox-vte')

        -- Build edit URL
        local editUrl = tostring(mw.uri.fullUrl('Template:' .. name, { action = 'edit' }))

        vte:wikitext('[')
        vte:tag('abbr')
            :attr('title', 'View this template')
            :wikitext('[[Template:' .. name .. '|v]]')
        vte:wikitext(' · ')
        vte:tag('abbr')
            :attr('title', 'Discuss this template')
            :wikitext('[[Template talk:' .. name .. '|t]]')
        vte:wikitext(' · ')
        vte:tag('abbr')
            :attr('title', 'Edit this template')
            :wikitext('[' .. editUrl .. ' e]')
        vte:wikitext(']')
    end

    -- Title text
    titleCell:tag('span')
        :addClass('navbox-title-text')
        :wikitext(title)

    -- Track row number for evenodd
    local rowNum = 0
    local imageRowsAdded = false

    -- Helper to add image cells
    local function addImageCells(row, isFirstContentRow)
        if isFirstContentRow then
            if hasValue(imageleft) then
                row:tag('td')
                    :addClass('navbox-image')
                    :attr('rowspan', tostring(contentRows))
                    :wikitext(imageleft)
            end
        end
    end

    local function addRightImageCell(row, isFirstContentRow)
        if isFirstContentRow and hasValue(image) then
            row:tag('td')
                :addClass('navbox-image')
                :attr('rowspan', tostring(contentRows))
                :wikitext(image)
        end
    end

    -- Get row class based on evenodd setting
    local function getRowClass(num)
        if evenodd == 'swap' then
            return (num % 2 == 0) and 'navbox-odd' or 'navbox-even'
        elseif evenodd == 'even' then
            return 'navbox-even'
        elseif evenodd == 'odd' then
            return 'navbox-odd'
        else
            return (num % 2 == 0) and 'navbox-even' or 'navbox-odd'
        end
    end

    -- Above row
    if hasValue(above) then
        rowNum = rowNum + 1
        local aboveRow = tbl:tag('tr')

        addImageCells(aboveRow, not imageRowsAdded)

        local aboveColspan = 2
        local aboveCell = aboveRow:tag('td')
            :addClass('navbox-above')
            :attr('colspan', tostring(aboveColspan))
            :wikitext(above)

        addRightImageCell(aboveRow, not imageRowsAdded)
        imageRowsAdded = true
    end

    -- Group rows
    for i = 1, MAX_GROUPS do
        local group = getArg(args, 'group' .. i)
        local list = getArg(args, 'list' .. i)

        if hasValue(list) then
            rowNum = rowNum + 1
            local row = tbl:tag('tr')

            addImageCells(row, not imageRowsAdded)

            if hasValue(group) then
                local groupCell = row:tag('th')
                    :addClass('navbox-group')
                if hasValue(groupclass) then
                    groupCell:addClass(groupclass)
                end
                if hasValue(groupwidth) then
                    groupCell:cssText('width: ' .. groupwidth)
                end
                groupCell:wikitext(group)

                local listCell = row:tag('td')
                    :addClass('navbox-list')
                    :addClass(getRowClass(rowNum))
                if hasValue(listclass) then
                    listCell:addClass(listclass)
                end
                listCell:wikitext(list)
            else
                -- List without group header spans full width
                local listCell = row:tag('td')
                    :addClass('navbox-list')
                    :addClass('navbox-list-full')
                    :addClass(getRowClass(rowNum))
                    :attr('colspan', '2')
                if hasValue(listclass) then
                    listCell:addClass(listclass)
                end
                listCell:wikitext(list)
            end

            addRightImageCell(row, not imageRowsAdded)
            imageRowsAdded = true

        elseif hasValue(group) then
            -- Group without list (subheader row)
            rowNum = rowNum + 1
            local row = tbl:tag('tr')

            addImageCells(row, not imageRowsAdded)

            row:tag('th')
                :addClass('navbox-subheader')
                :attr('colspan', '2')
                :wikitext(group)

            addRightImageCell(row, not imageRowsAdded)
            imageRowsAdded = true
        end
    end

    -- Below row
    if hasValue(below) then
        rowNum = rowNum + 1
        local belowRow = tbl:tag('tr')

        addImageCells(belowRow, not imageRowsAdded)

        belowRow:tag('td')
            :addClass('navbox-below')
            :attr('colspan', '2')
            :wikitext(below)

        addRightImageCell(belowRow, not imageRowsAdded)
    end

    return tostring(tbl)
end

--------------------------------------------------------------------------------
-- NAVBAR (V-T-E links only)
--------------------------------------------------------------------------------

function p.navbar(frame)
    local args = getFrameArgs(frame)

    local name = getArg(args, 1) or getArg(args, 'name')
    if isEmpty(name) then
        return '<span class="error">Error: Template name required</span>'
    end

    local mini = getArg(args, 'mini') == 'yes' or getArg(args, 'mini') == '1'

    local span = mw.html.create('span')
        :addClass('navbar')

    if mini then
        span:addClass('navbar-mini')
        span:wikitext('[')
        span:tag('abbr')
            :attr('title', 'View this template')
            :wikitext('[[Template:' .. name .. '|v]]')
        span:wikitext(' · ')
        span:tag('abbr')
            :attr('title', 'Discuss this template')
            :wikitext('[[Template talk:' .. name .. '|t]]')
        span:wikitext(' · ')
        span:tag('abbr')
            :attr('title', 'Edit this template')
            :wikitext('[{{fullurl:Template:' .. name .. '|action=edit}} e]')
        span:wikitext(']')
    else
        span:wikitext('[[Template:' .. name .. '|view]] · ')
        span:wikitext('[[Template talk:' .. name .. '|talk]] · ')
        span:wikitext('[{{fullurl:Template:' .. name .. '|action=edit}} edit]')
    end

    return tostring(span)
end

--------------------------------------------------------------------------------
-- HLIST (Horizontal list)
--------------------------------------------------------------------------------

function p.hlist(frame)
    local args = getFrameArgs(frame)

    local separator = getArg(args, 'separator', ' · ')
    local items = {}

    for i = 1, 20 do
        local item = getArg(args, i)
        if hasValue(item) then
            table.insert(items, item)
        end
    end

    if #items == 0 then
        return ''
    end

    local span = mw.html.create('span')
        :addClass('hlist')
        :wikitext(table.concat(items, separator))

    return tostring(span)
end

--------------------------------------------------------------------------------
-- FLATLIST (Comma-separated list)
--------------------------------------------------------------------------------

function p.flatlist(frame)
    local args = getFrameArgs(frame)

    local separator = getArg(args, 'separator', ', ')
    local items = {}

    for i = 1, 20 do
        local item = getArg(args, i)
        if hasValue(item) then
            table.insert(items, item)
        end
    end

    if #items == 0 then
        return ''
    end

    local span = mw.html.create('span')
        :addClass('flatlist')
        :wikitext(table.concat(items, separator))

    return tostring(span)
end

return p