Module:LuaTemplateBlueprint

Revision as of 00:30, 21 April 2025 by MarkWD (talk | contribs) (// via Wikitext Extension for VSCode)
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)

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

--[[
 * LuaTemplateBlueprint.lua
 * Provides a unified foundation for all ICANNWiki templates with feature toggling
 * 
 * This module standardizes template architecture, provides feature toggling,
 * centralizes common functionality, and improves maintainability.
 *
 * Integration with other modules:
 * - ErrorHandling: All operations are protected with centralized error handling
 * - ConfigRepository: Templates load configuration from this central repository
 * - TemplateHelpers: Common utilities for rendering and normalization
 * - TemplateStructure: Block-based rendering engine
 * - SemanticAnnotations: Semantic property generation
 *
 * Note on parameter handling:
 * - Template parameters are extracted and normalized by TemplateHelpers.extractArgs()
 * - This function handles case-insensitive parameter names for better user experience
 * - Parameters are accessible via args[paramName] regardless of case used in the template
]]

local p = {}

-- ========== Required modules ==========
local ErrorHandling = require('Module:ErrorHandling')
local ConfigRepository = require('Module:ConfigRepository')
local TemplateHelpers = require('Module:TemplateHelpers')
local TemplateStructure = require('Module:TemplateStructure')
local SemanticAnnotations = require('Module:SemanticAnnotations')
local mw = mw -- MediaWiki API

-- Module-level caches for expensive operations
local featureCache = {}
local processorCache = {}

-- ========== Template Registry ==========

-- Registry to store all registered templates
p.registry = {}

-- ========== Feature Management ==========

-- Default features for all templates
p.defaultFeatures = {
    -- Core rendering features
    title = true,
    logo = true,
    fields = true,
    socialMedia = true,
    
    -- Semantic features
    semanticProperties = true,
    categories = true,
    
    -- Error handling
    errorReporting = true
}

-- Create a cache key for features
-- @param featureOverrides table Optional table of feature overrides
-- @return string Cache key
local function createFeatureCacheKey(featureOverrides)
    if not featureOverrides or not next(featureOverrides) then
        return 'default'
    end
    
    -- Create stable key from overrides
    local parts = {}
    local keys = {}
    for k in pairs(featureOverrides) do
        table.insert(keys, k)
    end
    table.sort(keys)
    
    for _, k in ipairs(keys) do
        table.insert(parts, k .. '=' .. tostring(featureOverrides[k]))
    end
    
    return table.concat(parts, ',')
end

-- Initialize feature toggles for a template
-- @param featureOverrides table Optional table of feature overrides
-- @return table The initialized features
function p.initializeFeatures(featureOverrides)
    -- Fast path for common case
    if not featureOverrides then
        -- Return a copy of the default features
        local features = {}
        for k, v in pairs(p.defaultFeatures) do
            features[k] = v
        end
        return features
    end
    
    -- Check cache first
    local cacheKey = createFeatureCacheKey(featureOverrides)
    if featureCache[cacheKey] then
        -- Return a copy of the cached features
        local features = {}
        for k, v in pairs(featureCache[cacheKey]) do
            features[k] = v
        end
        return features
    end
    
    -- Create new feature set
    local features = {}
    
    -- First copy default features
    for featureId, enabled in pairs(p.defaultFeatures) do
        features[featureId] = enabled
    end
    
    -- Then apply overrides
    for featureId, enabled in pairs(featureOverrides) do
        features[featureId] = enabled
    end
    
    -- Cache the result
    featureCache[cacheKey] = {}
    for k, v in pairs(features) do
        featureCache[cacheKey][k] = v
    end
    
    return features
end

-- ========== Template Registration ==========

-- Register a new template
-- @param templateType string The template type (e.g., "Event", "Person", "TLD")
-- @param config table Configuration overrides for the template
-- @return table The registered template object
function p.registerTemplate(templateType, config)
    -- Create template instance with features
    local template = {
        type = templateType,
        config = config or {},
        features = p.initializeFeatures(config and config.features or nil)
    }
    
    -- Add template methods
    template.render = function(frame) 
        return p.renderTemplate(template, frame) 
    end
    
    -- Store in registry
    p.registry[templateType] = template
    
    return template
end

-- ========== Error Handling Integration ==========

-- Create an error context for a template
-- @param template table The template object
-- @return table The error context
function p.createErrorContext(template)
    local context = ErrorHandling.createContext(template.type .. "Template")
    template._errorContext = context
    return context
end

-- Execute a function with error protection
-- @param template table The template object
-- @param functionName string Name of the function being protected
-- @param func function The function to execute
-- @param fallback any The fallback value if an error occurs
-- @param ... any Arguments to pass to the function
-- @return any The result of the function or fallback
function p.protectedExecute(template, functionName, func, fallback, ...)
    if not template._errorContext then
        template._errorContext = p.createErrorContext(template)
    end
    
    return ErrorHandling.protect(
        template._errorContext,
        functionName,
        func,
        fallback,
        ...
    )
end

-- ========== Configuration Integration ==========

-- Standard configuration sections used by templates
p.configSections = {
    'meta',
    'categories',
    'patterns',
    'fields',
    'mappings',
    'constants',
    'semantics'
}

-- Initialize the standard configuration for a template
-- Combines base config from ConfigRepository with template overrides
-- @param template table The template object
-- @return table The complete configuration
function p.initializeConfig(template)
    local templateType = template.type
    local configOverrides = template.config or {}
    
    -- Get base configuration from repository
    local baseConfig = ConfigRepository.getStandardConfig(templateType)
    
    -- Apply overrides to each section
    local config = {}
    for _, section in ipairs(p.configSections) do
        config[section] = config[section] or {}
        
        -- Copy base config for this section if available
        if baseConfig[section] then
            for k, v in pairs(baseConfig[section]) do
                config[section][k] = v
            end
        end
        
        -- Apply overrides for this section if available
        if configOverrides[section] then
            for k, v in pairs(configOverrides[section]) do
                config[section][k] = v
            end
        end
    end
    
    -- Store complete config in template
    template.config = config
    
    return config
end

-- ========== Block Framework ==========

-- Standard sequence of blocks for template rendering
p.standardBlockSequence = {
    'title',
    'logo',
    'fields',
    'socialMedia',
    'semanticProperties',
    'categories',
    'errors'
}

-- Standard blocks available to all templates
p.standardBlocks = {
    -- Title block - renders the template title
    title = {
        feature = 'title',
        render = function(template, args)
            return p.protectedExecute(
                template,
                'StandardBlock_title',
                function()
                    return TemplateHelpers.renderTitleBlock(
                        args,
                        'template-title template-title-' .. string.lower(template.type),
                        template.type,
                        template.config.meta and template.config.meta.titleOptions or {}
                    )
                end,
                EMPTY_STRING,
                args
            )
        end
    },
    
    -- Logo block - renders the template logo/image
    logo = {
        feature = 'logo',
        render = function(template, args)
            return p.protectedExecute(
                template,
                'StandardBlock_logo',
                function()
                    local logoClass = 'template-logo template-logo-' .. string.lower(template.type)
                    local logoOptions = template.config.meta and template.config.meta.logoOptions or {}
                    
                    -- Use logoField from config or default to 'logo'
                    local logoField = logoOptions.logoField or 'logo'
                    local logoValue = args[logoField]
                    
                    if not logoValue or logoValue == '' then
                        return EMPTY_STRING
                    end
                    
                    return TemplateHelpers.renderLogoBlock(
                        logoValue,
                        logoClass,
                        logoOptions
                    )
                end,
                EMPTY_STRING,
                args
            )
        end
    },
    
    -- Fields block - renders all configured fields
    fields = {
        feature = 'fields',
        render = function(template, args)
            return p.protectedExecute(
                template,
                'StandardBlock_fields',
                function()
                    -- Get field definitions from config
                    local fieldDefs = template.config.fields or {}
                    local fields = {}
                    
                    -- Process field values using appropriate processors
                    for _, field in ipairs(fieldDefs) do
                        -- Skip hidden fields
                        if not field.hidden then
                            local fieldValue = p.processField(template, field, args)
                            
                            -- Only include fields with values
                            if fieldValue and fieldValue ~= '' then
                                table.insert(fields, {
                                    label = field.label or field.key,
                                    value = fieldValue,
                                    class = field.class
                                })
                            end
                        end
                    end
                    
                    -- Render the fields using TemplateStructure
                    return TemplateStructure.renderFieldTable(
                        fields,
                        template.config.meta and template.config.meta.fieldOptions or {}
                    )
                end,
                '',
                args
            )
        end
    },
    
    -- Social media block - renders social media links
    socialMedia = {
        feature = 'socialMedia',
        render = function(template, args)
            return p.protectedExecute(
                template,
                'StandardBlock_socialMedia',
                function()
                    return TemplateHelpers.renderSocialMediaBlock(
                        args,
                        template.config.meta and template.config.meta.socialMediaOptions or {}
                    )
                end,
                '',
                args
            )
        end
    },
    
    -- Semantic properties block - renders semantic properties
    semanticProperties = {
        feature = 'semanticProperties',
        render = function(template, args)
            return p.protectedExecute(
                template,
                'StandardBlock_semanticProperties',
                function()
                    return p.generateSemanticProperties(template, args)
                end,
                '',
                args
            )
        end
    },
    
    -- Categories block - renders category links
    categories = {
        feature = 'categories',
        render = function(template, args)
            return p.protectedExecute(
                template,
                'StandardBlock_categories',
                function()
                    return p.generateCategories(template, args)
                end,
                '',
                args
            )
        end
    },
    
    -- Errors block - renders error messages
    errors = {
        feature = 'errorReporting',
        render = function(template, args)
            return p.protectedExecute(
                template,
                'StandardBlock_errors',
                function()
                    if not template._errorContext then
                        return ''
                    end
                    
                    return ErrorHandling.formatOutput(template._errorContext)
                end,
                '',
                args
            )
        end
    }
}

-- Initialize blocks for a template
-- @param template table The template object
-- @return table The initialized blocks
function p.initializeBlocks(template)
    -- Get custom blocks from template config
    local customBlocks = template.config.blocks or {}
    
    -- Combine with standard blocks, preferring custom implementations
    local blocks = {}
    
    -- Use template's block sequence if provided, otherwise use standard
    local blockSequence = template.config.blockSequence or p.standardBlockSequence
    
    -- Initialize with blocks in sequence
    for _, blockId in ipairs(blockSequence) do
        -- Use custom block if available, otherwise use standard
        blocks[blockId] = customBlocks[blockId] or p.standardBlocks[blockId]
    end
    
    -- Store blocks in template
    template._blocks = blocks
    
    return blocks
end

-- Get block rendering function, respecting feature toggles
-- @param template table The template object
-- @param blockId string The ID of the block to get
-- @return function|nil The rendering function or nil if disabled
function p.getBlockRenderer(template, blockId)
    if not template._blocks then
        template._blocks = p.initializeBlocks(template)
    end
    
    local block = template._blocks[blockId]
    if not block then
        return nil
    end
    
    -- Check if feature is enabled
    if block.feature and not template.features[block.feature] then
        return nil -- Feature is disabled
    end
    
    return block.render
end

-- Build block rendering sequence for template
-- @param template table The template object
-- @return table Array of rendering functions
function p.buildRenderingSequence(template)
    local sequence = template.config.blockSequence or p.standardBlockSequence
    
    -- Pre-allocate with estimated size
    local renderingFunctions = {}
    local funcIndex = 1
    
    -- Cache template features for faster lookup in loop
    local features = template.features
    
    for _, blockId in ipairs(sequence) do
        local renderer = p.getBlockRenderer(template, blockId)
        if renderer then
            renderingFunctions[funcIndex] = function(args)
                return renderer(template, args)
            end
            funcIndex = funcIndex + 1
        end
    end
    
    -- Store sequence length for optimization
    renderingFunctions._length = funcIndex - 1
    
    return renderingFunctions
end

-- ========== Field Processing System ==========

-- Standard field processors
p.standardProcessors = {
    -- Identity processor (default, returns value unchanged)
    identity = function(value, args, template)
        return value
    end,
    
    -- Website processor
    website = function(value, args, template)
        return TemplateHelpers.normalizeWebsites(value)
    end,
    
    -- Country processor
    country = function(value, args, template)
        return TemplateHelpers.normalizeCountries(value)
    end,
    
    -- Date processor
    date = function(value, args, template)
        return TemplateHelpers.normalizeDate(value)
    end,
    
    -- Language processor
    language = function(value, args, template)
        return TemplateHelpers.normalizeLanguages(value)
    end,
    
    -- Text processor (wikifies text)
    text = function(value, args, template)
        return TemplateHelpers.wikifyText(value)
    end,
    
    -- Email processor
    email = function(value, args, template)
        return TemplateHelpers.normalizeEmail(value)
    end,
    
    -- Multi-value processor
    multivalue = function(value, args, template)
        return TemplateHelpers.normalizeMultiValue(value)
    end
}

-- Create a cache key for processors
-- @param template table The template object
-- @return string Cache key
local function createProcessorCacheKey(template)
    -- For simple case without custom processors
    if not template.config.processors or not next(template.config.processors) then
        return template.type .. '_default'
    end
    
    -- For case with custom processors, create stable cache key
    local parts = {template.type}
    local keys = {}
    for k in pairs(template.config.processors) do
        table.insert(keys, k)
    end
    table.sort(keys)
    
    for _, k in ipairs(keys) do
        table.insert(parts, k)
    end
    
    return table.concat(parts, '_')
end

-- Initialize processors for a template
-- @param template table The template object
-- @return table The initialized processors
function p.initializeProcessors(template)
    -- Check if already initialized
    if template._processors then
        return template._processors
    end
    
    -- Use cache if available
    local cacheKey = createProcessorCacheKey(template)
    if processorCache[cacheKey] then
        -- Create wrapper functions with template context
        local wrappedProcessors = {}
        
        for processorId, processor in pairs(processorCache[cacheKey]) do
            wrappedProcessors[processorId] = function(value, args)
                return processor(value, args, template)
            end
        end
        
        -- Store in template
        template._processors = wrappedProcessors
        
        return wrappedProcessors
    end
    
    -- Get custom processors from template config
    local customProcessors = template.config.processors or {}
    
    -- Combine with standard processors, preferring custom implementations
    local processors = {}
    
    -- Pre-estimate size for better performance
    local estimatedSize = 0
    for _ in pairs(p.standardProcessors) do estimatedSize = estimatedSize + 1 end
    for _ in pairs(customProcessors) do estimatedSize = estimatedSize + 1 end
    
    -- First, copy standard processors
    for processorId, processor in pairs(p.standardProcessors) do
        processors[processorId] = processor
    end
    
    -- Then apply custom processors, overriding standard ones if needed
    for processorId, processor in pairs(customProcessors) do
        processors[processorId] = processor
    end
    
    -- Store in cache
    processorCache[cacheKey] = processors
    
    -- Create wrapped processors that pass template context
    local wrappedProcessors = {}
    
    for processorId, processor in pairs(processors) do
        wrappedProcessors[processorId] = function(value, args)
            return processor(value, args, template)
        end
    end
    
    -- Store in template
    template._processors = wrappedProcessors
    
    return wrappedProcessors
end

-- Get processor for a field
-- @param template table The template object
-- @param field table The field definition
-- @return function The processor function
function p.getFieldProcessor(template, field)
    if not template._processors then
        template._processors = p.initializeProcessors(template)
    end
    
    -- Check if field has explicit processor
    if field.processor and template._processors[field.processor] then
        return template._processors[field.processor]
    end
    
    -- Check if there's a processor matching the field key
    local fieldKey = field.key or (field.keys and field.keys[1])
    if fieldKey and template._processors[fieldKey] then
        return template._processors[fieldKey]
    end
    
    -- Fall back to identity processor
    return template._processors.identity
end

-- Get field value from args
-- @param field table The field definition
-- @param args table The template arguments
-- @return string|nil The field value or nil if not found
function p.getFieldValue(field, args)
    -- If field has a single key, get that value
    if field.key then
        return args[field.key]
    end
    
    -- If field has multiple keys, try each in order
    if field.keys then
        for _, key in ipairs(field.keys) do
            local value = args[key]
            if value and value ~= '' then
                return value
            end
        end
    end
    
    return nil
end

-- Process a field using its processor
-- @param template table The template object
-- @param field table The field definition
-- @param args table The template arguments
-- @return string The processed field value
function p.processField(template, field, args)
    -- Get the raw field value
    local value = p.getFieldValue(field, args)
    
    -- If no value or empty, return empty string
    if not value or value == '' then
        return ''
    end
    
    -- Get the processor for this field
    local processor = p.getFieldProcessor(template, field)
    
    -- Process the value
    return p.protectedExecute(
        template,
        'FieldProcessor_' .. (field.key or 'unknown'),
        function()
            return processor(value, args)
        end,
        value, -- fallback to original value on error
        value,
        args
    )
end

-- ========== Preprocessing Pipeline ==========

-- Standard preprocessors
p.preprocessors = {
    -- Derive region from country values
    deriveRegionFromCountry = function(template, args)
        if (not args.region or args.region == "") and args.country then
            -- Load CountryData module
            local CountryData = require('Module:CountryData')
            
            -- Handle the country-to-region mapping
            local region = CountryData.getRegionForCountry(args.country)
            if region and region ~= "" then
                args.region = region
            end
        end
        
        return args
    end
}

-- Register a preprocessor with a template
-- @param template table The template object
-- @param preprocessor function|string The preprocessor function or name
-- @return table The template object (for chaining)
function p.addPreprocessor(template, preprocessor)
    -- Initialize preprocessors array if not exists
    template._preprocessors = template._preprocessors or {}
    
    -- Add preprocessor to array
    table.insert(template._preprocessors, preprocessor)
    
    return template
end

-- Run all preprocessors in sequence
-- @param template table The template object
-- @param args table The template arguments
-- @return table The processed arguments
function p.runPreprocessors(template, args)
    -- If no preprocessors, return args unchanged
    if not template._preprocessors then
        return args
    end
    
    -- Copy args to avoid modifying original
    local processedArgs = {}
    for k, v in pairs(args) do
        processedArgs[k] = v
    end
    
    -- Apply each preprocessor in sequence
    for _, preprocessor in ipairs(template._preprocessors) do
        if type(preprocessor) == "function" then
            processedArgs = preprocessor(template, processedArgs) or processedArgs
        elseif type(preprocessor) == "string" and p.preprocessors[preprocessor] then
            -- Look up named preprocessor
            processedArgs = p.preprocessors[preprocessor](template, processedArgs) or processedArgs
        end
    end
    
    return processedArgs
end

-- ========== Semantic and Category Integration ==========

-- Register semantic property provider function
-- @param template table The template object
-- @param provider function The provider function
-- @return table The template object (for chaining)
function p.registerPropertyProvider(template, provider)
    -- Initialize property providers array if not exists
    template._propertyProviders = template._propertyProviders or {}
    
    -- Add provider to array
    table.insert(template._propertyProviders, provider)
    
    return template
end

-- Process a value using a transform function
-- @param value string The value to transform
-- @param property string The property name (for error context)
-- @param transform function The transform function
-- @param args table The template arguments
-- @param template table The template object
-- @return string The transformed value
local function applyTransform(value, property, transform, args, template)
    if not value or value == '' then
        return ''
    end
    
    -- Skip transform if missing
    if not transform then
        return value
    end
    
    -- Apply transformation with error protection
    local transformedValue = p.protectedExecute(
        template,
        'Transform_' .. property,
        function() return transform(value, args, template) end,
        value, -- fallback to original on error
        value,
        args,
        template
    )
    
    return transformedValue or ''
end

-- ========== Constants as Upvalues ==========

-- Common empty string for fast returns
local EMPTY_STRING = ''

-- Common separator for concatenation
local NEWLINE = '\n'

-- Common semicolon separator for multi-values
local SEMICOLON = ';'

-- Common category prefix
local CATEGORY_PREFIX = '[[Category:'
local CATEGORY_SUFFIX = ']]'

-- Generate semantic properties for template
-- @param template table The template object
-- @param args table The template arguments
-- @return string The generated semantic properties HTML
function p.generateSemanticProperties(template, args)
    -- Early return if feature is disabled (fast path)
    if not template.features.semanticProperties then
        return EMPTY_STRING
    end
    
    -- Get semantic configuration
    local semanticConfig = template.config.semantics or {}
    local properties = semanticConfig.properties or {}
    local transforms = semanticConfig.transforms or {}
    local additionalProperties = semanticConfig.additionalProperties or {}
    
    -- Fast path for empty configuration
    if not next(properties) and not next(additionalProperties) and 
       (not template._propertyProviders or #template._propertyProviders == 0) then
        return EMPTY_STRING
    end
    
    -- Pre-allocate output table with estimated size
    local propertyCount = 0
    for _ in pairs(properties) do propertyCount = propertyCount + 1 end
    for _, fields in pairs(additionalProperties) do
        propertyCount = propertyCount + #fields
    end
    if template._propertyProviders then
        propertyCount = propertyCount + #template._propertyProviders * 2
    end
    
    -- Initialize output with estimated size
    local propertyHtml = {}
    
    -- Configure standard property mapping
    local propertyMapping = {}
    for property, field in pairs(properties) do
        local value = args[field]
        if value and value ~= '' then
            -- Apply transformation if available
            local transform = transforms[property]
            value = applyTransform(value, property, transform, args, template)
            
            if value and value ~= '' then
                propertyMapping[property] = value
            end
        end
    end
    
    -- Process additional properties (multiple fields mapping to one property)
    for property, fields in pairs(additionalProperties) do
        local transform = transforms[property]
        
        for _, field in ipairs(fields) do
            local value = args[field]
            if value and value ~= '' then
                -- Apply transformation
                value = applyTransform(value, property, transform, args, template)
                
                if value and value ~= '' then
                    -- Add to existing value if property already exists
                    local existingValue = propertyMapping[property]
                    if existingValue and existingValue ~= '' then
                        propertyMapping[property] = existingValue .. ';' .. value
                    else
                        propertyMapping[property] = value
                    end
                end
            end
        end
    end
    
    -- Set the properties (directly set index rather than table.insert for better performance)
    local htmlIndex = 1
    for property, value in pairs(propertyMapping) do
        propertyHtml[htmlIndex] = SemanticAnnotations.setSemanticProperty(property, value)
        htmlIndex = htmlIndex + 1
    end
    
    -- Call custom property providers
    if template._propertyProviders then
        for _, provider in ipairs(template._propertyProviders) do
            local providerResult = p.protectedExecute(
                template,
                'PropertyProvider',
                function() return provider(template, args) end,
                {},
                template,
                args
            )
            
            if providerResult then
                for property, value in pairs(providerResult) do
                    if value and value ~= '' then
                        propertyHtml[htmlIndex] = SemanticAnnotations.setSemanticProperty(property, value)
                        htmlIndex = htmlIndex + 1
                    end
                end
            end
        end
    end
    
    -- Return concatenated property HTML
    return table.concat(propertyHtml, '\n')
end

-- Register category provider function
-- @param template table The template object
-- @param provider function The provider function
-- @return table The template object (for chaining)
function p.registerCategoryProvider(template, provider)
    -- Initialize category providers array if not exists
    template._categoryProviders = template._categoryProviders or {}
    
    -- Add provider to array
    table.insert(template._categoryProviders, provider)
    
    return template
end

-- Generate categories for template
-- @param template table The template object
-- @param args table The template arguments
-- @return string The generated category HTML
function p.generateCategories(template, args)
    -- Early return if feature is disabled (fast path)
    if not template.features.categories then
        return EMPTY_STRING
    end
    
    -- Get configuration
    local configCategories = template.config.categories or {}
    
    -- Fast path for empty configuration
    if #configCategories == 0 and 
       (not template._categoryProviders or #template._categoryProviders == 0) then
        return EMPTY_STRING
    end
    
    -- Pre-allocate with estimated size
    local categoryEstimate = #configCategories
    if template._categoryProviders then
        categoryEstimate = categoryEstimate + #template._categoryProviders * 2
    end
    
    -- Initialize categories array with estimated size
    local categories = {}
    local categoryIndex = 1
    
    -- Add base categories from config (direct indexing for better performance)
    for _, category in ipairs(configCategories) do
        categories[categoryIndex] = category
        categoryIndex = categoryIndex + 1
    end
    
    -- Call custom category providers
    if template._categoryProviders then
        for _, provider in ipairs(template._categoryProviders) do
            local providerCategories = p.protectedExecute(
                template,
                'CategoryProvider',
                function() return provider(template, args) end,
                {},
                template,
                args
            )
            
            if providerCategories then
                for _, category in ipairs(providerCategories) do
                    categories[categoryIndex] = category
                    categoryIndex = categoryIndex + 1
                end
            end
        end
    end
    
    -- Generate category HTML with pre-allocated size
    local categoryHtml = {}
    for i = 1, categoryIndex - 1 do
        categoryHtml[i] = CATEGORY_PREFIX .. categories[i] .. CATEGORY_SUFFIX
    end
    
    -- Return concatenated category HTML
    return table.concat(categoryHtml, NEWLINE)
end

-- ========== Template Rendering ==========

-- Main rendering function for templates
-- @param template table The template object
-- @param frame Frame The MediaWiki frame object
-- @return string The rendered template HTML
function p.renderTemplate(template, frame)
    -- Create error context if not exists
    if not template._errorContext then
        template._errorContext = p.createErrorContext(template)
    end
    
    -- Initialize config if not already done
    if not template.config.meta then
        p.initializeConfig(template)
    end
    
    -- Extract and normalize arguments
    local args = TemplateHelpers.extractArgs(frame)
    
    -- Run preprocessors
    args = p.runPreprocessors(template, args)
    
    -- Build rendering sequence
    local renderingSequence = p.buildRenderingSequence(template)
    
    -- Fast path for empty sequence
    if renderingSequence._length == 0 then
        return EMPTY_STRING
    end
    
    -- Pre-allocate output array with estimated size
    local output = {}
    local outputIndex = 1
    
    -- Render each block (use numeric indexing for faster iteration)
    for i = 1, renderingSequence._length do
        local blockHtml = renderingSequence[i](args)
        if blockHtml and blockHtml ~= '' then
            output[outputIndex] = blockHtml
            outputIndex = outputIndex + 1
        end
    end
    
    -- Fast path for empty output
    if outputIndex == 1 then
        return EMPTY_STRING
    end
    
    -- Return concatenated output
    return table.concat(output, NEWLINE)
end

-- Return the module
return p