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
 * - Caching of detection results for performance
 * - Configurable field names and styling
 * - Blueprint integration as a custom block
]]

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 →",
    rowHeight  = "40"
}

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

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

-- Detect navigation links based on patterns
-- @param pageName string The current page name
-- @param patterns table Table of patterns to use
-- @return table Navigation result with prev and next properties
function p.detectNavigation(pageName, patterns)
    -- Check cache first
    local cacheKey = pageName
    if navigationCache[cacheKey] ~= nil then
        return navigationCache[cacheKey]
    end
    
    local result = nil
    
    -- Try Series + Number pattern: "ICANN 76"
    local series, number = pageName:match(patterns.seriesNumber or "([^%d]+)%s+(%d+)$")
    if series and number then
        number = tonumber(number)
        local prev = (number > 1) and string.format("%s %d", series, number - 1) or nil
        local next = string.format("%s %d", series, number + 1)
        
        -- Check if pages exist
        local prevPage = prev and mw.title.new(prev) or nil
        local nextPage = mw.title.new(next)
        
        result = {
            prev = prevPage and prevPage.exists and prev or nil,
            next = nextPage and nextPage.exists and next or nil
        }
    end
    
    -- If no result yet, try Series + Year pattern: "IGF 2023"
    if not result then
        local series, year = pageName:match(patterns.seriesYear or "([^%d]+)%s+(%d%d%d%d)$")
        if series and year then
            year = tonumber(year)
            local prev = string.format("%s %d", series, year - 1)
            local next = string.format("%s %d", series, year + 1)
            
            -- Check if pages exist
            local prevPage = mw.title.new(prev)
            local nextPage = mw.title.new(next)
            
            result = {
                prev = prevPage and prevPage.exists and prev or nil,
                next = nextPage and nextPage.exists and next or nil
            }
        end
    end
    
    -- Store in cache (including nil results)
    navigationCache[cacheKey] = result
    return result
end

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

-- 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()
            -- Get configuration from template
            local config = template.config.navigation or {}
            
            -- Get current page name
            local pageName = mw.title.getCurrentTitle().text
            
            -- Detect navigation
            local autoNavigation = nil
            if config.autoDetect ~= false then
                autoNavigation = p.detectNavigation(pageName, config.patterns or {})
            end
            
            -- Get field names
            local prevField = config.prevField or "has_previous"
            local nextField = config.nextField or "has_next"
            
            -- Check for user-provided navigation values
            local hasPrev = args[prevField]
            local hasNext = args[nextField]
            
            -- If no navigation is provided at all, return empty string
            if (not hasPrev or hasPrev == "") and (not hasNext or hasNext == "") and not autoNavigation then
                return ""
            end
            
            -- Determine previous and next pages
            local prevPage = nil
            local nextPage = nil
            
            -- Process previous page
            if hasPrev and hasPrev ~= "" then
                if hasPrev ~= "yes" and hasPrev ~= "true" then
                    prevPage = hasPrev
                elseif autoNavigation and autoNavigation.prev then
                    prevPage = autoNavigation.prev
                end
            elseif autoNavigation and autoNavigation.prev then
                prevPage = autoNavigation.prev
            end
            
            -- Process next page
            if hasNext and hasNext ~= "" then
                if hasNext ~= "yes" and hasNext ~= "true" then
                    nextPage = hasNext
                elseif autoNavigation and autoNavigation.next then
                    nextPage = autoNavigation.next
                end
            elseif autoNavigation and autoNavigation.next then
                nextPage = autoNavigation.next
            end
            
            -- If no actual navigation links were found, return empty string
            if not prevPage and not nextPage then
                return ""
            end
            
            -- Get styling options
local prevClass = config.prevClass or "element-navigation-prev"
local nextClass = config.nextClass or "element-navigation-next"
local rawPrevLabel = config.prevLabel or "← Previous"
local rawNextLabel = config.nextLabel or "Next →"
local rowHeight = config.rowHeight or "40"
-- Compute dynamic labels based on page names
local prevLabel = ""
if prevPage then
    if rawPrevLabel:find("%%s") then
        prevLabel = string.format(rawPrevLabel, prevPage)
    else
        prevLabel = rawPrevLabel
    end
end
local nextLabel = ""
if nextPage then
    if rawNextLabel:find("%%s") then
        nextLabel = string.format(rawNextLabel, nextPage)
    else
        nextLabel = rawNextLabel
    end
end
            
            -- Create navigation row
            local output = {
                '|-',
                '| colspan="2" height="' .. rowHeight .. '" valign="middle" |'
            }
            
            -- Add previous link
            if prevPage then
                table.insert(output, string.format(
                    '<div class="element-navigation-prev">[[%s|%s]]</div>',
                    prevPage, prevLabel
                ))
            else
                table.insert(output, "&nbsp;")
            end
            
            -- Add next link cell
            -- Removed separate next cell; using single colspan cell
            
            if nextPage then
                table.insert(output, string.format(
                    '<div class="element-navigation-next">[[%s|%s]]</div>',
                    nextPage, nextLabel
                ))
            else
                table.insert(output, "&nbsp;")
            end
            
            return table.concat(output, "\n")
        end)
    end
end

return p