Jump to content

Module:TemplateFieldProcessor

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

--[[
 * TemplateFieldProcessor.lua
 * Provides field processing functionality for ICANNWiki templates
 * 
 * This module handles the processing of template fields, including:
 * - Standard field processors for common field types
 * - Processor initialization and caching
 * - Field value retrieval
 * - Field processing with error handling
]]

local p = {}

-- ========== Constants as Upvalues ==========
-- Common empty string for fast returns
local EMPTY_STRING = ''
-- Wiki link pattern for regex matching
local WIKI_LINK_PATTERN = '%[%[.-%]%]'

-- ========== Required modules ==========
local ErrorHandling = require('Module:ErrorHandling')
local TemplateHelpers = require('Module:TemplateHelpers')
local LinkParser = require('Module:LinkParser')
local NormalizationText = require('Module:NormalizationText')

-- Module-level caches for expensive operations
local processorCache = {}
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 getNormalizationDate = lazyRequire('Module:NormalizationDate')

-- ========== Cache Management ==========

-- Create a cache key for processors
-- @param template table The template object
-- @return string Cache key
function p.createProcessorCacheKey(template)
    if not template.config.processors or not next(template.config.processors) then
        return template.type .. '_default'
    end
    
    local processorCount = 0
    for _ in pairs(template.config.processors) do processorCount = processorCount + 1 end
    
    local keys = {}
    local keyIndex = 1
    
    for k in pairs(template.config.processors) do
        keys[keyIndex] = k
        keyIndex = keyIndex + 1
    end
    table.sort(keys)
    
    local parts = {template.type}
    for i = 1, processorCount do
        parts[i+1] = keys[i]
    end
    
    return table.concat(parts, '_')
end

-- ========== Field Value Management ==========

-- 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)
    if not field then
        return nil
    end
    local _, value = TemplateHelpers.getFieldValue(args, field)
    return value
end

-- ========== Processor Management ==========

-- Initialize processors for a template
-- @param template table The template object
-- @return table The initialized processors
function p.initializeProcessors(template)
    local cacheKey = p.createProcessorCacheKey(template)
    if processorCache[cacheKey] then
        local wrappedProcessors = {}
        
        for processorId, processor in pairs(processorCache[cacheKey]) do
            wrappedProcessors[processorId] = function(value, args)
                return processor(value, args, template)
            end
        end
        
        return wrappedProcessors
    end
    
    local customProcessors = template.config.processors or {}
    local processors = {}
    
    for processorId, processor in pairs(p.standardProcessors) do
        processors[processorId] = processor
    end
    
    for processorId, processor in pairs(customProcessors) do
        processors[processorId] = processor
    end
    
    processorCache[cacheKey] = processors
    
    local wrappedProcessors = {}
    for processorId, processor in pairs(processors) do
        wrappedProcessors[processorId] = function(value, args)
            return processor(value, args, template)
        end
    end
    
    return wrappedProcessors
end

-- Get processor for a field
-- Internal helper function: determines which processor to use for a field; called by processField to select the appropriate processor based on field configuration
-- @param processors table The processors table
-- @param field table The field definition
-- @return function The processor function
function p.getFieldProcessor(processors, field)
    -- Check if there's a processor matching the field key (most common case)
    local fieldKey = field.key or (field.keys and field.keys[1])
    if fieldKey and processors[fieldKey] then
        return processors[fieldKey]
    end
    
    -- Check if field has explicit processor
    if field.processor and processors[field.processor] then
        return processors[field.processor]
    end
    
    -- Fall back to identity processor
    return processors.identity
end

-- ========== Standard Field Processors ==========

-- 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 (joins start and ending dates for Event templates)
    date = function(value, args, template)
        -- For Event templates, if an ending date exists, render a range
        if template and template.type == 'Event' then
            local endValue = args['ending'] or args['end']
            if endValue and endValue ~= '' then
                return TemplateHelpers.formatDateRange(value, endValue, {outputMode="complete"})
            end
        end
        -- Default single-date formatting
        return getNormalizationDate().formatDate(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
}

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

-- Process a field using its processor
-- @param template table The template object
-- @param field table The field definition
-- @param args table The template arguments
-- @param errorContext table Optional error context for error handling
-- @return string The processed field value
function p.processField(template, field, args, errorContext)
    if not field then
        return EMPTY_STRING
    end
    
    local fieldKey = field.key or (field.keys and field.keys[1] or "unknown")
    local value = p.getFieldValue(field, args)
    
    if not value or value == '' then
        return EMPTY_STRING
    end
    
    -- Wiki Link Handling Strategy: preserveWikiLinks prevents stripping of wiki links
    local sanitizeOptions = {
        preserveWikiLinks = field.autoWikiLink or field.preserveWikiLinks
    }
    
    -- Get processors - this assumes template._processors exists
    local processors = template._processors
    if not processors then
        processors = p.initializeProcessors(template)
    end
    
    local processor = p.getFieldProcessor(processors, field)
    
    if processor == processors.identity then
        if sanitizeOptions.preserveWikiLinks then
            return value
        else
            return NormalizationText.sanitizeUserInput(value, nil, nil, sanitizeOptions)
        end
    end
    
    -- Use error context if provided
    local protectFunc = function(func, fallback, ...)
        if errorContext then
            return ErrorHandling.protect(
                errorContext,
                'FieldProcessor_' .. fieldKey,
                func,
                fallback,
                ...
            )
        else
            -- Simple pcall if no error context
            local success, result = pcall(func, ...)
            return success and result or fallback
        end
    end
    
    local result = protectFunc(
        function()
            local processed = processor(value, args)
            
            if sanitizeOptions.preserveWikiLinks and processed and processed ~= '' then
                if value:match(WIKI_LINK_PATTERN) and not processed:match(WIKI_LINK_PATTERN) then
                    return value
                end
            end
            
            return processed
        end,
        value,
        value,
        args
    )
    
    return result or EMPTY_STRING
end

return p