Jump to content

Module:TemplateHelpers

Revision as of 02:14, 12 April 2025 by MarkWD (talk | contribs) (// via Wikitext Extension for VSCode)

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

-- Module:TemplateHelpers
-- Common helper functions for template modules promoting code reuse and consistency.
-- Provides utilities for string processing, field handling, normalization, and block rendering.

local p = {}

-- Dependencies
local linkParser = require('Module:LinkParser')
local MultiCountryDisplay = require('Module:MultiCountryDisplay')
local dateNormalization = require('Module:DateNormalization')
local CanonicalForms = require('Module:CanonicalForms')
local SemanticCategoryHelpers = require('Module:SemanticCategoryHelpers')

--------------------------------------------------------------------------------
-- String Processing Functions
--------------------------------------------------------------------------------

-- Normalizes template arguments to be case-insensitive
-- Returns a new table with lowercase keys while preserving original keys as well
-- Also handles empty numeric parameters that can occur with {{Template|}} syntax
function p.normalizeArgumentCase(args)
    local normalized = {}
    
    -- Process all keys
    for key, value in pairs(args) do
        -- Skip empty numeric parameters (created by leading pipes after template name)
        if type(key) == "number" and (value == nil or value == "") then
            -- Do nothing with empty numeric parameters
        else
            -- For all other parameters, add lowercase version
            if type(key) == "string" then
                normalized[key:lower()] = value
            end
            -- Preserve original key as well
            normalized[key] = value
        end
    end
    
    return normalized
end

-- Trims leading and trailing whitespace from a string
function p.trim(s)
    return (s:gsub("^%s+", ""):gsub("%s+$", ""))
end

-- Splits a semicolon-delimited string into a table of trimmed non-empty values
-- This is now a wrapper around splitMultiValueString for backward compatibility
function p.splitSemicolonValues(value)
    -- For backward compatibility, maintain the same behavior
    return p.splitMultiValueString(value, {
        {pattern = ";%s*", replacement = ";"}
    })
end

-- Joins a table of values with the specified delimiter
function p.joinValues(values, delimiter)
    delimiter = delimiter or "; "
    if not values or #values == 0 then return "" end
    return table.concat(values, delimiter)
end

-- Module-level pattern categories for sanitizing user input
-- These are exposed for potential extension by other modules
p.SANITIZE_PATTERNS = {
    WIKI_LINKS = {
        { pattern = "%[%[([^|%]]+)%]%]", replacement = "%1" },           -- [[Link]] -> Link
        { pattern = "%[%[([^|%]]+)|([^%]]+)%]%]", replacement = "%2" }   -- [[Link|Text]] -> Text
    },
    SINGLE_BRACES = {
        { pattern = "{([^{}]+)}", replacement = "%1" }                   -- {text} -> text
    },
    HTML_BASIC = {
        { pattern = "</?[bi]>", replacement = "" },                      -- Remove <b>, </b>, <i>, </i>
        { pattern = "</?span[^>]*>", replacement = "" }                  -- Remove <span...>, </span>
    }
}

-- Sanitizes user input by removing or transforming unwanted patterns
-- @param value The input string to sanitize
-- @param patternCategories Optional table or string of pattern categories to apply
-- @param customPatterns Optional table of additional patterns to apply
-- @return The sanitized string
function p.sanitizeUserInput(value, patternCategories, customPatterns)
    -- Fast path for nil/empty values
    if not value or value == "" then return "" end
    
    -- Collect patterns to apply
    local patternsToApply = {}
    local patternCount = 0
    
    -- Process requested pattern categories
    if patternCategories then
        -- Handle single category string
        if type(patternCategories) == "string" then
            if p.SANITIZE_PATTERNS[patternCategories] then
                for _, pattern in ipairs(p.SANITIZE_PATTERNS[patternCategories]) do
                    patternCount = patternCount + 1
                    patternsToApply[patternCount] = pattern
                end
            end
        -- Handle table of categories
        elseif type(patternCategories) == "table" then
            for _, category in ipairs(patternCategories) do
                if p.SANITIZE_PATTERNS[category] then
                    for _, pattern in ipairs(p.SANITIZE_PATTERNS[category]) do
                        patternCount = patternCount + 1
                        patternsToApply[patternCount] = pattern
                    end
                end
            end
        end
    else
        -- Default to WIKI_LINKS and SINGLE_BRACES if no categories specified
        for _, pattern in ipairs(p.SANITIZE_PATTERNS.WIKI_LINKS) do
            patternCount = patternCount + 1
            patternsToApply[patternCount] = pattern
        end
        for _, pattern in ipairs(p.SANITIZE_PATTERNS.SINGLE_BRACES) do
            patternCount = patternCount + 1
            patternsToApply[patternCount] = pattern
        end
    end
    
    -- Add any custom patterns
    if customPatterns and type(customPatterns) == "table" then
        for _, pattern in ipairs(customPatterns) do
            patternCount = patternCount + 1
            patternsToApply[patternCount] = pattern
        end
    end
    
    -- Fast path if no patterns to apply
    if patternCount == 0 then
        return value
    end
    
    -- Apply each pattern sequentially
    local result = value
    for i = 1, patternCount do
        local patternInfo = patternsToApply[i]
        result = result:gsub(patternInfo.pattern, patternInfo.replacement)
    end
    
    return result
end

--------------------------------------------------------------------------------
-- Field Processing Functions
--------------------------------------------------------------------------------

-- Retrieves a field value from args using either multiple possible keys or a single key
-- Now supports case-insensitive lookup by default
function p.getFieldValue(args, field)
    if field.keys then
        for _, key in ipairs(field.keys) do
            -- First try exact match to maintain backward compatibility
            if args[key] and args[key] ~= "" then
                return key, args[key]
            end
            
            -- Then try lowercase version
            local lowerKey = key:lower()
            if args[lowerKey] and args[lowerKey] ~= "" and lowerKey ~= key then
                return lowerKey, args[lowerKey]
            end
        end
        return nil, nil
    end
    
    -- First try exact match
    if args[field.key] and args[field.key] ~= "" then
        return field.key, args[field.key]
    end
    
    -- Then try lowercase version
    local lowerKey = field.key:lower()
    if args[lowerKey] and args[lowerKey] ~= "" and lowerKey ~= field.key then
        return lowerKey, args[lowerKey]
    end
    
    return field.key, nil
end

-- Processes multiple values with a given processor function
-- Uses splitMultiValueString for more flexible delimiter handling
-- Pre-allocates result table for better performance
function p.processMultipleValues(values, processor)
    if not values or values == "" then return {} end
    local items = p.splitMultiValueString(values)
    
    -- Pre-allocate results table based on input size
    local results = {}
    local resultIndex = 1
    
    for _, item in ipairs(items) do
        local processed = processor(item)
        if processed and processed ~= "" then
            results[resultIndex] = processed
            resultIndex = resultIndex + 1
        end
    end
    return results
end

--------------------------------------------------------------------------------
-- Normalization Wrappers
--------------------------------------------------------------------------------

-- Formats website URLs as either a single link or an HTML unordered list of links
-- Uses splitMultiValueString for more flexible delimiter handling
-- Optimized to avoid unnecessary table creation for single websites
function p.normalizeWebsites(value)
    if not value or value == "" then return "" end
    
    -- Quick check for single website (no delimiters)
    if not value:match(";") and not value:match("%s+and%s+") then
        -- Single website case - avoid table creation entirely
        return string.format("[%s %s]", value, linkParser.strip(value))
    end
    
    -- Multiple websites case
    local websites = p.splitMultiValueString(value)
    if #websites > 1 then
        -- Pre-allocate listItems table based on number of websites
        local listItems = {}
        local index = 1
        
        for _, site in ipairs(websites) do
            local formattedLink = string.format("[%s %s]", site, linkParser.strip(site))
            listItems[index] = string.format("<li>%s</li>", formattedLink)
            index = index + 1
        end
        return string.format("<ul class=\"template-list template-list-website\" style=\"margin:0; padding-left:1em;\">%s</ul>", table.concat(listItems, ""))
    elseif #websites == 1 then
        return string.format("[%s %s]", websites[1], linkParser.strip(websites[1]))
    end
    return ""
end

-- Wrapper around MultiCountryDisplay for consistent country formatting
function p.normalizeCountries(value)
    if not value or value == "" then return "" end
    return MultiCountryDisplay.formatCountries(value)
end

-- Wrapper around DateNormalization for consistent date formatting
function p.normalizeDates(value)
    if not value or value == "" then return "" end
    return tostring(dateNormalization.formatDate(value))
end

--------------------------------------------------------------------------------
-- Block Generation Helpers
--------------------------------------------------------------------------------

-- Generates a standard title block with configurable class and text
-- Enhanced to support achievement integration with options
function p.renderTitleBlock(args, titleClass, titleText, options)
    options = options or {}
    titleClass = titleClass or "template-title"
    
    -- Basic title block without achievement integration
    if not options.achievementSupport then
        return string.format('|-\n! colspan="2" class="%s" | %s', titleClass, titleText)
    end
    
    -- With achievement support
    local achievementClass = options.achievementClass or ""
    local achievementId = options.achievementId or ""
    local achievementName = options.achievementName or ""
    
    -- Only add achievement attributes if they exist
    if achievementClass ~= "" and achievementId ~= "" then
        return string.format(
            '|-\n! colspan="2" class="%s %s" data-achievement-id="%s" data-achievement-name="%s" | %s',
            titleClass, achievementClass, achievementId, achievementName, titleText
        )
    else
        -- Clean row with no achievement data
        return string.format('|-\n! colspan="2" class="%s" | %s', titleClass, titleText)
    end
end

-- Renders a standard fields block based on field definitions and processors
-- Enhanced to support complete HTML blocks and custom field rendering
-- Pre-allocates output table for better performance
function p.renderFieldsBlock(args, fields, processors)
    processors = processors or {}
    
    -- Pre-allocate output table - estimate based on number of fields
    -- Not all fields may be present in args, but this gives us a reasonable upper bound
    local out = {}
    local outIndex = 1
    
    for _, field in ipairs(fields) do
        local key, value = p.getFieldValue(args, field)
        if value then
            -- Apply processor if available for this field
            if key and processors[key] and type(processors[key]) == "function" then
                local processedValue = processors[key](value, args)
                
                -- Handle the case where a processor returns complete HTML
                if type(processedValue) == "table" and processedValue.isCompleteHtml then
                    -- Add the complete HTML as is
                    out[outIndex] = processedValue.html
                    outIndex = outIndex + 1
                elseif processedValue ~= nil and processedValue ~= false then
                    -- Standard field rendering
                    out[outIndex] = string.format("|-\n| '''%s''':\n| %s", field.label, processedValue)
                    outIndex = outIndex + 1
                end
            else
                -- Standard field rendering without processor
                out[outIndex] = string.format("|-\n| '''%s''':\n| %s", field.label, value)
                outIndex = outIndex + 1
            end
        end
    end
    
    return table.concat(out, "\n")
end

-- Renders a standard divider block with optional label
function p.renderDividerBlock(label)
    if label and label ~= "" then
        return string.format('|-\n| colspan="2" class="template-divider" |\n|-\n| colspan="2" class="icannwiki-centered" | <span class="icannwiki-bold">%s</span>', label)
    else
        return '|-\n| colspan="2" class="template-divider" |'
    end
end


--------------------------------------------------------------------------------
-- Category and Semantic Utilities
--------------------------------------------------------------------------------

-- These functions have been moved to Module:SemanticCategoryHelpers
-- Wrapper functions are provided for backward compatibility

-- Generic function to split multi-value strings with various delimiters
function p.splitMultiValueString(...)
    return SemanticCategoryHelpers.splitMultiValueString(...)
end

-- Splits a region string that may contain "and" conjunctions
function p.splitRegionCategories(...)
    return SemanticCategoryHelpers.splitRegionCategories(...)
end

-- Ensures a category string is properly wrapped in MediaWiki syntax
function p.formatCategoryName(...)
    return SemanticCategoryHelpers.formatCategoryName(...)
end

-- Builds a category string from a table of category names
function p.buildCategories(...)
    return SemanticCategoryHelpers.buildCategories(...)
end

-- Adds categories based on a canonical mapping
function p.addMappingCategories(...)
    return SemanticCategoryHelpers.addMappingCategories(...)
end

-- Generic function to add multi-value semantic properties
function p.addMultiValueSemanticProperties(...)
    return SemanticCategoryHelpers.addMultiValueSemanticProperties(...)
end

-- Generic function to add multi-value categories
function p.addMultiValueCategories(...)
    return SemanticCategoryHelpers.addMultiValueCategories(...)
end

-- Adds semantic properties for multiple countries
function p.addMultiCountrySemanticProperties(...)
    return SemanticCategoryHelpers.addMultiCountrySemanticProperties(...)
end

-- Adds semantic properties for multiple regions
function p.addMultiRegionSemanticProperties(...)
    return SemanticCategoryHelpers.addMultiRegionSemanticProperties(...)
end

-- Adds semantic properties for multiple languages
function p.addMultiLanguageSemanticProperties(...)
    return SemanticCategoryHelpers.addMultiLanguageSemanticProperties(...)
end

-- Helper function to process additional properties with multi-value support
function p.processAdditionalProperties(...)
    return SemanticCategoryHelpers.processAdditionalProperties(...)
end

-- Helper function to check if a field contains multiple values
function p.isMultiValueField(...)
    return SemanticCategoryHelpers.isMultiValueField(...)
end

-- Generates semantic properties based on configuration
function p.generateSemanticProperties(...)
    return SemanticCategoryHelpers.generateSemanticProperties(...)
end

--------------------------------------------------------------------------------
-- Configuration Standardization
--------------------------------------------------------------------------------

-- Creates a standardized configuration structure for template modules
function p.createStandardConfig(config)
    config = config or {}
    
    -- Initialize with defaults
    local standardConfig = {
        meta = config.meta or {
            description = "Template module configuration"
        },
        mappings = config.mappings or {},
        fields = config.fields or {},
        semantics = config.semantics or {
            properties = {},
            transforms = {},
            additionalProperties = {}
        },
        constants = config.constants or {},
        patterns = config.patterns or {},
        categories = config.categories or {} -- Add categories field to preserve base categories
    }
    
    return standardConfig
end

return p