Jump to content

Module:ElementNavigation

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

--[[
 * Module:ElementNavigation
 * Provides navigation detection and rendering for sequential content
 * 
 * This module detects previous/next pages based on naming patterns
 * and renders navigation links. It's designed to be used as a block
 * in the Blueprint template system.
 *
 * Features:
 * - Automatic detection of previous/next pages based on patterns
 * - Support for both series+number and series+year patterns
 * - Configurable field names and styling
]]

local p = {}

p.elementName = "navigation"
p.defaultConfig = {
    autoDetect = true,
    patterns = {
        seriesNumber = "([^%d]+)%s+(%d+)$",
        seriesYear   = "([^%d]+)%s+(%d%d%d%d)$"
    },
    prevField  = "has_previous",
    nextField  = "has_next",
    prevClass  = "element-navigation-prev",
    nextClass  = "element-navigation-next",
    prevLabel  = "← Previous",
    nextLabel  = "Next →",
    classPrefix = nil,
    rowHeight  = "40"
}

-- Cache for navigation detection results
local navigationCache = {}

-- ========== Navigation Detection ==========

-- Detect navigation links based on a list of patterns
-- @param pageName string The current page name
-- @param patterns table A list of patterns to try
-- @return table Navigation result with prev and next properties, or nil
function p.detectNavigation(pageName, patterns)
    local cacheKey = pageName
    if navigationCache[cacheKey] ~= nil then
        return navigationCache[cacheKey]
    end

    local result = nil
    for _, pattern in ipairs(patterns) do
        local series, numberStr = pageName:match(pattern)
        if series and numberStr then
            local number = tonumber(numberStr)
            if number then
                local prevName = (number > 1) and string.format("%s %d", series, number - 1) or nil
                local nextName = string.format("%s %d", series, number + 1)

                local prevPage = prevName and mw.title.new(prevName)
                local nextPage = mw.title.new(nextName)

                result = {
                    prev = (prevPage and prevPage.exists) and prevName or nil,
                    next = (nextPage and nextPage.exists) and nextName or nil,
                }
                break -- Stop after the first successful match
            end
        end
    end

    navigationCache[cacheKey] = result
    return result
end

-- ========== Blueprint Integration ==========

-- Helper function to merge two tables. The custom table's values override the base table's.
local function mergeConfigs(base, custom)
    local merged = {}
    for k, v in pairs(base) do
        merged[k] = v
    end
    for k, v in pairs(custom) do
        merged[k] = v
    end
    return merged
end

-- Create a navigation block for Blueprint
-- @return function Block rendering function for Blueprint
function p.createBlock()
    return function(template, args)
        -- Get error context from template if available
        local errorContext = template._errorContext
        
        -- Protected execution if error context is available
        local execute = function(func, ...)
            if errorContext and errorContext.protect then
                return errorContext.protect(
                    errorContext,
                    "NavigationBlock",
                    func,
                    "",
                    ...
                )
            else
                return func(...)
            end
        end
        
        return execute(function()
            -- Merge default and template-specific configurations
            local config = mergeConfigs(p.defaultConfig, template.config.navigation or {})

            -- Automatic navigation detection
            local autoNavigation
            if config.autoDetect then
                local pageName = mw.title.getCurrentTitle().text
                -- The patterns are now expected to be an indexed table for ipairs
                local patterns = {config.patterns.seriesNumber, config.patterns.seriesYear}
                autoNavigation = p.detectNavigation(pageName, patterns) or {}
            else
                autoNavigation = {}
            end

            -- Determine final previous and next page links
            local prevPage = args[config.prevField] or autoNavigation.prev
            local nextPage = args[config.nextField] or autoNavigation.next

            -- Exit if no navigation links are found
            if not prevPage and not nextPage then
                return ""
            end

            -- Helper to create a navigation link
            local function createNavLink(page, label, class)
                if not page then return '' end
                local linkLabel = label:find("%%s") and string.format(label, page) or label
                return string.format('<div class="%s">[[%s|%s]]</div>', class, page, linkLabel)
            end

            -- Build the navigation container
            local output = {
                '|-',
                string.format('| colspan="2" height="%s" valign="middle" |', config.rowHeight),
                '<div class="element-navigation-container">'
            }
            
            local prevLink = createNavLink(prevPage, config.prevLabel, config.prevClass)
            local nextLink = createNavLink(nextPage, config.nextLabel, config.nextClass)

            table.insert(output, prevLink)
            table.insert(output, nextLink)
            table.insert(output, '</div>')

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

return p