Jump to content

Module:LuaTemplateBlueprint

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
 * - TemplateFieldProcessor: Field processing and value retrieval
 * - CountryData: Country normalization and region derivation
 * - SocialMedia: Social media icon rendering
 *
 * Feature Configuration:
 * Templates must explicitly specify which features they want to use when registering:
 *
 * local template = Blueprint.registerTemplate('TemplateName', {
 *     features = {
 *         title = true,
 *         fields = true,
 *         semanticProperties = true,
 *         categories = true,
 *         errorReporting = true
 *         -- logo and socialMedia intentionally disabled
 *     }
 * })
 *
 * Alternatively, templates can use predefined feature sets:
 *
 * local template = Blueprint.registerTemplate('TemplateName', {
 *     features = Blueprint.featureSets.minimal
 * })
 *
 * Available feature sets:
 * - minimal: Just title, fields, and error reporting
 * - standard: All features enabled
 * - content: Standard without social media
 * - data: Just fields, semantics, categories, and error reporting
 *
 * Note on parameter handling:
 * Template parameters are extracted using frame:getParent().args and normalized
 * with TemplateHelpers.normalizeArgumentCase() for case-insensitive access.
]]

local p = {}

-- ========== 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/suffix
local CATEGORY_PREFIX = '[[Category:'
local CATEGORY_SUFFIX = ']]'
-- Template class prefixes
local TEMPLATE_TITLE_CLASS_PREFIX = 'template-title template-title-'
local TEMPLATE_LOGO_CLASS_PREFIX = 'template-logo template-logo-'
-- Default table class
local DEFAULT_TABLE_CLASS = 'template-table'
-- Wiki link pattern for regex matching
local WIKI_LINK_PATTERN = '%[%[.-%]%]'
-- Semantic property HTML templates
local PROPERTY_DIV_PREFIX = '<div style="display:none;">\n  {{#set: '
local PROPERTY_DIV_SUFFIX = ' }}\n</div>'
-- Empty table for returning when needed
local EMPTY_OBJECT = {}

-- ========== Required modules ==========
-- Core modules that are always needed
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 LinkParser = require('Module:LinkParser')
local TemplateFieldProcessor = require('Module:TemplateFieldProcessor')
local mw = mw -- MediaWiki API

-- Module-level caches for expensive operations
local featureCache = {}
local moduleCache = {}  -- Cache for lazy-loaded modules

-- Lazy module loader
local function lazyRequire(moduleName)
    return function()
        if not moduleCache[moduleName] then
            moduleCache[moduleName] = require(moduleName)
        end
        return moduleCache[moduleName]
    end
end

-- Lazily loaded modules
local getSocialMedia = lazyRequire('Module:SocialMedia')
local getNormalizationDate = lazyRequire('Module:NormalizationDate')
local getCountryData = lazyRequire('Module:CountryData')

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

-- Element registry for custom Blueprint elements
p.elementRegistry = {}

-- Register and lookup functions for Blueprint elements
function p.registerElement(name, module)
    if not name or not module then return end
    p.elementRegistry[name] = module
    return module
end

-- Get element from registry by name
-- This function is used internally by createElementBlock; it may appear unused in T-* templates, but is essential for the Element system
function p.getElement(name)
    return p.elementRegistry[name]
end

-- Create a Blueprint block for a registered element
function p.createElementBlock(name)
    local element = p.getElement(name)
    if not element or not element.createBlock then return end
    return {
        feature = name,
        render = function(template, args)
            return p.protectedExecute(
                template,
                "ElementBlock_" .. name,
                function()
                    return element.createBlock()(template, args)
                end,
                "",
                args
            )
        end
    }
end

-- Add a registered element's block to a template at a given position
function p.addElementToTemplate(template, name, _)
    local block = p.createElementBlock(name)
    if not block then return false end
    template.config.blocks = template.config.blocks or {}
    template.config.blocks[name] = block
    template.features[name] = true
    -- Rebuild blockSequence, only append if placeholder not present
    local baseSeq = template.config.blockSequence or p.standardBlockSequence
    local seq = {}
    local found = false
    for _, b in ipairs(baseSeq) do
        table.insert(seq, b)
        if b == name then found = true end
    end
    if not found then
        table.insert(seq, name)
    end
    template.config.blockSequence = seq
    return true
end

-- Automatically initialize and register built-in elements
function p.initializeElements()
    local context = ErrorHandling.createContext("LuaTemplateBlueprint")
    local mod = ErrorHandling.safeRequire(context, 'Module:ElementNavigation', false)
    if mod and mod.elementName then
        p.registerElement(mod.elementName, mod)
    end
end

-- ========== Feature Management ==========
-- Templates must explicitly include the features they want to use

-- Create a cache key for features
-- Internal helper function: Used only by initializeFeatures to generate cache keys for feature sets
-- @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
    
    local keyCount = 0
    for _ in pairs(featureOverrides) do keyCount = keyCount + 1 end
    
    local keys = {}
    local keyIndex = 1
    
    for k in pairs(featureOverrides) do
        keys[keyIndex] = k
        keyIndex = keyIndex + 1
    end
    table.sort(keys)
    
    local parts = {}
    for i = 1, keyCount do
        local k = keys[i]
        parts[i] = k .. '=' .. tostring(featureOverrides[k])
    end
    
    return table.concat(parts, ',')
end

-- Initialize feature toggles for a template
-- @param featureOverrides table Table of feature configurations
-- @return table The initialized features
function p.initializeFeatures(featureOverrides)
    -- If no features are specified, return an empty features table
    -- This ensures templates must explicitly define their features
    if not featureOverrides then
        return {}
    end
    
    -- Check if we have a cached version of this feature set
    local cacheKey = createFeatureCacheKey(featureOverrides)
    if featureCache[cacheKey] then
        -- Return a copy of the cached features to prevent modification
        local features = {}
        for k, v in pairs(featureCache[cacheKey]) do
            features[k] = v
        end
        return features
    end
    
    -- Create a new features table with only the specified features
    local features = {}
    for featureId, enabled in pairs(featureOverrides) do
        features[featureId] = enabled
    end
    
    -- Cache the features for future use
    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)
    local template = {
        type = templateType,
        config = config or {},
        features = p.initializeFeatures(config and config.features or nil)
    }
    
    template.render = function(frame) 
        return p.renderTemplate(template, frame) 
    end
    
    p.registry[templateType] = template

    -- Default provider for country/region categories
    p.registerCategoryProvider(template, function(template, args)
        local cats = {}
        local SCH = require('Module:SemanticCategoryHelpers')
        local CD = require('Module:CountryData')
        if args.country and args.country ~= '' then
            local TH = require('Module:TemplateHelpers')
            cats = SCH.addMultiValueCategories(
                args.country,
                CD.normalizeCountryName,
                cats,
                { valueGetter = function(v)
                    return TH.splitMultiValueString(v, TH.SEMICOLON_PATTERN)
                  end }
            )
        end
        if args.region and args.region ~= '' then
            cats = SCH.addMultiValueCategories(args.region, nil, cats)
        end
        return cats
    end)

    -- Shared provider for conditional and mapping categories
    -- Iterates 'config.categories.conditional' to add categories when template args are non-empty
    -- Iterates 'config.mappings' to compute mapping categories via SemanticCategoryHelpers
    -- Uses addMappingCategories to normalize values and generate category links
    -- Centralizes category logic for all templates in a single provider function
    p.registerCategoryProvider(template, function(template, args)
        local cats = {}

        -- Conditional categories
        for param, category in pairs(template.config.categories.conditional or {}) do
            if args[param] and args[param] ~= "" then
                table.insert(cats, category)
            end
        end

        -- Mapping categories
        local SCH = require('Module:SemanticCategoryHelpers')
        for key, mappingDef in pairs(template.config.mappings or {}) do
            local value = args[key]
            for _, cat in ipairs(SCH.addMappingCategories(value, mappingDef) or {}) do
                table.insert(cats, cat)
            end
        end

        return cats
    end)
    
    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

    -- Auto-normalize arguments for all mappings
    for key, mappingDef in pairs(config.mappings or {}) do
        p.addPreprocessor(template, function(template, args)
            local cf = require('Module:CanonicalForms')
            local val = args[key] or ''
            local canonical = select(1, cf.normalize(val, mappingDef))
            if canonical then args[key] = canonical end
            return args
        end)
    end
    
    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()
                -- Get title from config if available, otherwise use template type
                local titleText = template.type
                if template.config.constants and template.config.constants.title then
                    titleText = template.config.constants.title
                end
                
                -- Get template ID from config or use the template name as fallback
                local templateId = template.type
                if template.config.meta and template.config.meta.templateId then
                    templateId = template.config.meta.templateId
                end
                
                return require('Module:TemplateStructure').renderTitleBlock(
                    args,
                    TEMPLATE_TITLE_CLASS_PREFIX .. string.lower(templateId),
                    titleText
                )
                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_CLASS_PREFIX .. 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
                    
                    -- Create options object for renderLogoBlock
                    local options = {
                        cssClass = logoClass,
                        errorContext = template._errorContext
                    }
                    
                    -- Add any additional options from logoOptions
                    if logoOptions then
                        for k, v in pairs(logoOptions) do
                            options[k] = v
                        end
                    end
                    
                    -- Create args object with logo value
                    local logoArgs = {
                        logo = logoValue
                    }
                    
                    return TemplateHelpers.renderLogoBlock(logoArgs, options)
                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 = {}
                    
                    -- Get property mappings from semantics config
                    local propertyMappings = template.config.semantics and 
                                            template.config.semantics.properties or {}
                    
                    -- Add special processor for logo field when logo feature is enabled
                    if not template._processors then
                        template._processors = p.initializeProcessors(template)
                    end
                    
                    -- If logo feature is enabled, add a processor to skip the logo field
                    if template.features.logo then
                        template._processors.logo = function() return false end
                    end
                    
                    -- Process field values using appropriate processors
                    for _, field in ipairs(fieldDefs) do
                        -- Skip hidden fields
                        if not field.hidden then
                            local fieldKey = field.key or (field.keys and field.keys[1] or "unknown")
                            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
                    
                    -- Create a processors table for renderFieldsBlock
                    local processors = {}
                    for _, field in ipairs(fields) do
                        local fieldKey = field.label or field.key or (field.keys and field.keys[1] or "unknown")
                        processors[fieldKey] = function(value, args)
                            return field.value
                        end
                    end
                    
                    -- Use renderFieldsBlock which generates only rows, not a complete table
                    -- Pass property mappings for tooltip generation
                    local result = TemplateHelpers.renderFieldsBlock(
                        args,
                        fieldDefs,
                        template._processors,
                        propertyMappings
                    )
                    
                    return result
                end,
                '',
                args
            )
        end
    },
    
    -- Social media block - renders social media links
    socialMedia = {
        feature = 'socialMedia',
        render = function(template, args)
            return p.protectedExecute(
                template,
                'StandardBlock_socialMedia',
                function()
                    -- Use lazily loaded SocialMedia module
                    return getSocialMedia().render(args)
                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)
    local customBlocks = template.config.blocks or {}
    local blocks = {}
    local blockSequence = template.config.blockSequence or p.standardBlockSequence
    
    for _, blockId in ipairs(blockSequence) do
        blocks[blockId] = customBlocks[blockId] or p.standardBlocks[blockId]
    end
    
    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
    
    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
    local renderingFunctions = {}
    local funcIndex = 1
    
    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
    
    renderingFunctions._length = funcIndex - 1
    return renderingFunctions
end

-- ========== Field Processing System ==========
-- Initialize processors for a template
-- @param template table The template object
-- @return table The initialized processors
function p.initializeProcessors(template)
    if template._processors then
        return template._processors
    end
    
    template._processors = TemplateFieldProcessor.initializeProcessors(template)
    return template._processors
end

-- Get field value from args (delegated to TemplateHelpers)
-- @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, template)
    local _, value = TemplateHelpers.getFieldValue(args, field)
    return value
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)
    if not field then
        return EMPTY_STRING
    end
    
    -- Initialize processors if needed
    if not template._processors then
        template._processors = p.initializeProcessors(template)
    end
    
    -- Use the TemplateFieldProcessor module with error context
    return p.protectedExecute(
        template,
        'processField',
        function()
            return TemplateFieldProcessor.processField(template, field, args, template._errorContext)
        end,
        EMPTY_STRING,
        template,
        field,
        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
            -- Split multi-value country string into individual countries
            local regions = {}
            local seen = {}
            for country in string.gmatch(args.country, "[^;]+") do
                local trimmed = country:match("^%s*(.-)%s*$")
                local region = getCountryData().getRegionByCountry(trimmed)
                if region and region ~= "(Unrecognized)" and not seen[region] then
                    table.insert(regions, region)
                    seen[region] = true
                end
            end
            if #regions > 0 then
                args.region = table.concat(regions, "; ")
            end
        end

        return args
    end,
    
    -- Set the ID field to the current page ID
    setPageIdField = function(template, args)
        -- Get the current page ID and set it in the args table
        local pageId = TemplateHelpers.getCurrentPageId()
        args.ID = tostring(pageId or "")
        args.id = args.ID

        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 not template._preprocessors or #template._preprocessors == 0 then
        return args
    end
    
    local processedArgs = {}
    for k, v in pairs(args) do
        processedArgs[k] = v
    end
    
    local preprocessorCount = #template._preprocessors
    
    for i = 1, preprocessorCount do
        local preprocessor = template._preprocessors[i]
        local preprocessorType = type(preprocessor)
        
        if preprocessorType == "function" then
            local result = preprocessor(template, processedArgs)
            if result then
                processedArgs = result
            end
        elseif preprocessorType == "string" then
            local namedPreprocessor = p.preprocessors[preprocessor]
            if namedPreprocessor then
                local result = namedPreprocessor(template, processedArgs)
                if result then
                    processedArgs = result
                end
            end
        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

-- Validate property value to prevent SMW parser issues
-- @param value string The value to validate
-- @return string The validated value
local function validatePropertyValue(value)
    if not value or value == '' then
        return ''
    end
    
    -- Convert to string if needed
    value = tostring(value)
    
    -- Remove potentially problematic wiki markup
    value = value:gsub('{{.-}}', '')  -- Remove template calls
    value = value:gsub('%[%[Category:.-]]', '')  -- Remove categories
    
    -- Escape pipe characters that might break SMW
    value = value:gsub('|', '{{!}}')
    
    return value
end

-- 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)
    if not template.features.semanticProperties then
        return EMPTY_STRING
    end
    
    local semanticConfig = template.config.semantics or {}
    local properties = semanticConfig.properties or {}
    local transforms = semanticConfig.transforms or {}
    local additionalProperties = semanticConfig.additionalProperties or {}
    local skipProperties = semanticConfig.skipProperties or {}
    
    if not next(properties) and not next(additionalProperties) and 
       (not template._propertyProviders or #template._propertyProviders == 0) then
        return EMPTY_STRING
    end
    
    -- Set time budget for semantic property processing (450ms)
    local startTime = os.clock()
    local timeLimit = 0.45  -- seconds
    local checkInterval = 10  -- check every N properties
    local propertyCounter = 0
    
    local function checkTimeLimit()
        propertyCounter = propertyCounter + 1
        if propertyCounter % checkInterval == 0 then
            if os.clock() - startTime > timeLimit then
                return true  -- time exceeded
            end
        end
        return false
    end
    
    -- Set options for SemanticAnnotations
    local semanticOptions = {
        transform = transforms
    }
    
    -- Build initial property mapping (like original code)
    local allProperties = {}
    
    -- Process basic properties - just map property names to field names
    for property, param in pairs(properties) do
        if not skipProperties[property] then
            -- Just map the property to the field name
            -- SemanticAnnotations will handle value extraction and transforms
            allProperties[property] = param
        end
    end
    
    -- Create collector for deduplication of additional properties and providers
    local collector = {
        seen = {},        -- Track property:value signatures
        properties = {},  -- Final deduplicated properties
        count = 0        -- Track total property count
    }
    
    -- Process additional properties with early deduplication and multi-value handling
    for property, fields in pairs(additionalProperties) do
        -- Skip properties that are explicitly marked to skip
        if not skipProperties[property] then
            local transform = transforms[property]
            for _, fieldName in ipairs(fields) do
                local rawValue = args[fieldName]
                if rawValue and rawValue ~= '' then
                    -- Handle multi-value fields by splitting first
                    local values
                    if rawValue:find(';') then
                        values = TemplateHelpers.splitMultiValueString(rawValue)
                    else
                        values = {rawValue}
                    end
                    
                    -- Process each value individually
                    for _, singleValue in ipairs(values) do
                        local trimmedValue = singleValue:match("^%s*(.-)%s*$")
                        if trimmedValue and trimmedValue ~= '' then
                            -- Apply transform if available
                            local finalValue = trimmedValue
                            if transform then
                                finalValue = p.protectedExecute(
                                    template,
                                    'Transform_' .. property,
                                    function() return transform(trimmedValue, args, template) end,
                                    trimmedValue,
                                    trimmedValue,
                                    args,
                                    template
                                )
                            end
                            
                            -- Validate and add to collector
                            finalValue = validatePropertyValue(finalValue)
                            if finalValue and finalValue ~= '' then
                                if not collector.properties[property] then
                                    collector.properties[property] = {}
                                end
                                table.insert(collector.properties[property], finalValue)
                                collector.count = collector.count + 1
                            end
                        end
                    end
                end
            end
        end
    end
    
    
    -- Process property providers with early deduplication
    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 and next(providerResult) then
                -- Process provider properties through deduplication
                for property, value in pairs(providerResult) do
                    -- Skip properties marked to skip
                    if not skipProperties[property] then
                        if type(value) == "table" then
                            -- Provider returned an array of values
                            for _, v in ipairs(value) do
                                local validated = validatePropertyValue(v)
                                if validated and validated ~= '' then
                                    if not collector.properties[property] then
                                        collector.properties[property] = {}
                                    end
                                    table.insert(collector.properties[property], validated)
                                    collector.count = collector.count + 1
                                end
                            end
                        else
                            -- Provider returned a single value
                            local validated = validatePropertyValue(value)
                            if validated and validated ~= '' then
                                if not collector.properties[property] then
                                    collector.properties[property] = {}
                                end
                                table.insert(collector.properties[property], validated)
                                collector.count = collector.count + 1
                            end
                        end
                    end
                end
            end
        end
    end
    
    -- Process all collected properties in one batch
    --[[
        After collecting all mapped and provider properties, override the country and region
        entries with normalized values from CountryData.getSemanticCountryRegionProperties.
        This final step replaces any literal user input with canonical names,
        ensures a single batched SMW call emits deduplicated properties,
        and centralizes normalization logic for clarity and consistency.
    ]]
    -- Override raw country/region with normalized names if country field exists
    if args.country and args.country ~= '' then
        local cr = require('Module:ConfigRepository')
        local cd = require('Module:CountryData')
        local norm = p.protectedExecute(
            template,
            'CountryData_Override',
            function() return cd.getSemanticCountryRegionProperties(args.country) end,
            {},
            args.country
        )
        if norm then
            local countryKey = cr.semanticProperties.country
            local regionKey = cr.semanticProperties.region
            if norm[countryKey] then
                collector.properties[countryKey] = norm[countryKey]
            end
            if norm[regionKey] then
                collector.properties[regionKey] = norm[regionKey]
            end
        end
    end
    
    -- Merge basic properties mapping with deduplicated additional properties
    -- Basic properties (allProperties) contains field mappings
    -- Additional properties (collector.properties) contains actual values
    local finalProperties = {}
    
    -- Copy basic property mappings
    for property, fieldName in pairs(allProperties) do
        finalProperties[property] = fieldName
    end
    
    -- Add deduplicated additional properties (these have actual values)
    for property, value in pairs(collector.properties) do
        finalProperties[property] = value -- This might be an array of values or a single value
    end

    -- Process fixed properties
    if semanticConfig.fixedProperties and type(semanticConfig.fixedProperties) == 'table' then
        for propName, propValue in pairs(semanticConfig.fixedProperties) do
            if not skipProperties[propName] then -- Check skipProperties as well
                local validatedValue = validatePropertyValue(propValue) 
                if validatedValue and validatedValue ~= '' then
                    -- Pass as a single-item array to be processed by the
                    -- 'Direct array of values' path in SemanticAnnotations.lua
                    finalProperties[propName] = {validatedValue} 
                end
            end
        end
    end
    
    -- Add debug info as HTML comment (Phase 1 monitoring)
    local basicCount = 0
    for _ in pairs(allProperties) do basicCount = basicCount + 1 end
    
    local debugInfo = string.format(
        "<!-- SMW Debug: basic_props=%d, additional_props=%d, unique_signatures=%d -->",
        basicCount,
        collector.count or 0,
        table.maxn(collector.seen or {})
    )
    
    -- Send all properties to SemanticAnnotations in one batch
    local semanticOutput = SemanticAnnotations.setSemanticProperties(
        args,
        finalProperties,
        semanticOptions
    )
    
    -- Append debug info to output
    if semanticOutput and semanticOutput ~= '' then
        return semanticOutput .. '\n' .. debugInfo
    else
        return debugInfo
    end
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)
    if not template.features.categories then
        return EMPTY_STRING
    end
    
    local configCategories = {}
    
    if template.config.categories and template.config.categories.base and 
       type(template.config.categories.base) == "table" then
        configCategories = template.config.categories.base
    elseif template.config.categories and type(template.config.categories) == "table" then
        if #template.config.categories > 0 then
            configCategories = template.config.categories
        end
    end
    
    if #configCategories == 0 and 
       (not template._categoryProviders or #template._categoryProviders == 0) then
        return EMPTY_STRING
    end
    
    -- Use a seen table for deduplication
    local seen = {}
    local uniqueCategories = {}
    local categoryCount = 0
    
    -- Add config categories with deduplication
    for i = 1, #configCategories do
        local category = configCategories[i]
        if category and category ~= "" and not seen[category] then
            seen[category] = true
            categoryCount = categoryCount + 1
            uniqueCategories[categoryCount] = category
        end
    end
    
    -- Process provider categories with deduplication
    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
                    if category and category ~= "" and not seen[category] then
                        seen[category] = true
                        categoryCount = categoryCount + 1
                        uniqueCategories[categoryCount] = category
                    end
                end
            end
        end
    end
    
    -- Generate HTML for unique categories
    local categoryHtml = {}
    for i = 1, categoryCount do
        categoryHtml[i] = CATEGORY_PREFIX .. uniqueCategories[i] .. CATEGORY_SUFFIX
    end
    
    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)
    template.current_frame = frame -- Store frame on template instance

    -- Check recursion depth to prevent infinite loops
    local depth = 0
    if frame.args and frame.args._recursion_depth then
        depth = tonumber(frame.args._recursion_depth) or 0
    elseif frame:getParent() and frame:getParent().args and frame:getParent().args._recursion_depth then
        depth = tonumber(frame:getParent().args._recursion_depth) or 0
    end
    
    if depth > 3 then
        return '<span class="error">Template recursion depth exceeded (limit: 3)</span>'
    end
    
    if not template._errorContext then
        template._errorContext = p.createErrorContext(template)
    end
    
    if not template.config.meta then
        p.initializeConfig(template)
    end
    
    local args = frame:getParent().args or {}
    args = TemplateHelpers.normalizeArgumentCase(args)
    
    -- Increment recursion depth for any child template calls
    args._recursion_depth = tostring(depth + 1)
    
    args = p.runPreprocessors(template, args)
    
    local tableClass = DEFAULT_TABLE_CLASS
    if template.config.constants and template.config.constants.tableClass then
        tableClass = template.config.constants.tableClass
    end
    
    local structureConfig = {
        tableClass = tableClass,
        blocks = {},
        containerTag = template.features.fullPage and "div" or "table"
    }
    
    local renderingSequence = p.buildRenderingSequence(template)
    
    if renderingSequence._length == 0 then
        return EMPTY_STRING
    end
    
    for i = 1, renderingSequence._length do
        table.insert(structureConfig.blocks, function(a)
            return renderingSequence[i](a)
        end)
    end
    
    local result = TemplateStructure.render(args, structureConfig, template._errorContext)
    
    template.current_frame = nil -- Clear frame from template instance
    
    return result
end

-- Return the module
-- Initialize elements after module load
p.initializeElements()

return p