Jump to content

Module:TemplateHelpers

From ICANNWiki

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')

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

-- 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

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

-- Retrieves a field value from args using either multiple possible keys or a single key
function p.getFieldValue(args, field)
    if field.keys then
        for _, key in ipairs(field.keys) do
            if args[key] and args[key] ~= "" then
                return key, args[key]
            end
        end
        return nil, nil
    end
    return field.key, (args[field.key] and args[field.key] ~= "") and args[field.key] or 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 Utilities
--------------------------------------------------------------------------------

-- Default delimiters for splitMultiValueString
-- Defined once as an upvalue to avoid recreating on each function call
local defaultDelimiters = {
    {pattern = "%s+and%s+", replacement = ";"},
    {pattern = ";%s*", replacement = ";"}
}

-- Generic function to split multi-value strings with various delimiters
-- Returns an array of individual values
function p.splitMultiValueString(value, delimiters)
    if not value or value == "" then return {} end
    
    -- Use provided delimiters or default ones
    delimiters = delimiters or defaultDelimiters
    
    -- Standardize all delimiters to semicolons
    local standardizedInput = value
    for _, delimiter in ipairs(delimiters) do
        standardizedInput = standardizedInput:gsub(delimiter.pattern, delimiter.replacement)
    end
    
    -- Pre-allocate table based on delimiter count
    -- Count semicolons to estimate the number of items
    local count = 0
    for _ in standardizedInput:gmatch(";") do 
        count = count + 1 
    end
    
    -- Pre-allocate table with estimated size (count+1 for the last item)
    local items = {}
    
    -- Split by semicolons and return the array
    local index = 1
    for item in standardizedInput:gmatch("[^;]+") do
        local trimmed = item:match("^%s*(.-)%s*$")
        if trimmed and trimmed ~= "" then
            items[index] = trimmed
            index = index + 1
        end
    end
    
    return items
end

-- Splits a region string that may contain "and" conjunctions
-- Returns an array of individual region names
-- This is now a wrapper around splitMultiValueString for backward compatibility
function p.splitRegionCategories(regionValue)
    return p.splitMultiValueString(regionValue)
end

-- Builds a category string from a table of category names
-- Pre-allocates the formatted table for better performance
function p.buildCategories(categories)
    if not categories or #categories == 0 then return "" end
    
    -- Pre-allocate formatted table based on input size
    local formatted = {}
    local index = 1
    
    for _, cat in ipairs(categories) do
        -- Check if the category already has the [[ ]] wrapper
        if not string.match(cat, "^%[%[Category:") then
            formatted[index] = string.format("[[Category:%s]]", cat)
        else
            formatted[index] = cat
        end
        index = index + 1
    end
    return table.concat(formatted, "\n")
end

-- Adds categories based on a canonical mapping
function p.addMappingCategories(value, mapping)
    if not value or value == "" or not mapping then return {} end
    local categories = {}
    local canonical = select(1, CanonicalForms.normalize(value, mapping))
    
    if canonical then
        for _, group in ipairs(mapping) do
            if group.canonical == canonical and group.category then
                table.insert(categories, group.category)
                break
            end
        end
    end
    
    return categories
end

--------------------------------------------------------------------------------
-- Semantic Property Helpers
--------------------------------------------------------------------------------

-- Generic function to add multi-value semantic properties
-- This is a generalized helper that can be used for any multi-value property
function p.addMultiValueSemanticProperties(value, propertyName, processor, semanticOutput, options)
    if not value or value == "" then return semanticOutput end
    
    options = options or {}
    local processedItems = {}
    
    -- Get the values to process
    local items
    if options.valueGetter and type(options.valueGetter) == "function" then
        -- Use custom value getter if provided
        items = options.valueGetter(value)
    else
        -- Default to splitting the string
        items = p.splitMultiValueString(value)
    end
    
    -- For non-SMW case, collect property HTML fragments in a table for efficient concatenation
    local propertyHtml = {}
    
    -- Process each item and add as a semantic property
    for _, item in ipairs(items) do
        -- Apply processor if provided
        local processedItem = item
        if processor and type(processor) == "function" then
            processedItem = processor(item)
        end
        
        -- Only add if valid and not already processed
        if processedItem and processedItem ~= "" and not processedItems[processedItem] then
            processedItems[processedItem] = true
            
            -- Add as semantic property
            if mw.smw then
                mw.smw.set({[propertyName] = processedItem})
            else
                -- Collect HTML fragments instead of concatenating strings
                table.insert(propertyHtml, '<div style="display:none;">')
                table.insert(propertyHtml, '  {{#set: ' .. propertyName .. '=' .. processedItem .. ' }}')
                table.insert(propertyHtml, '</div>')
            end
        end
    end
    
    -- For non-SMW case, concatenate all property HTML fragments at once
    if not mw.smw and #propertyHtml > 0 then
        semanticOutput = semanticOutput .. "\n" .. table.concat(propertyHtml, "\n")
    end
    
    return semanticOutput
end

-- Generic function to add multi-value categories
-- This is a generalized helper that can be used for any multi-value category field
function p.addMultiValueCategories(value, processor, categories, options)
    if not value or value == "" then return categories end
    
    options = options or {}
    
    -- Get the values to process
    local items
    if options.valueGetter and type(options.valueGetter) == "function" then
        -- Use custom value getter if provided
        items = options.valueGetter(value)
    else
        -- Default to splitting the string
        items = p.splitMultiValueString(value)
    end
    
    -- Pre-allocate space in the categories table
    -- Estimate the number of new categories to add
    local currentSize = #categories
    local estimatedNewSize = currentSize + #items
    
    -- Process each item and add as a category
    for _, item in ipairs(items) do
        -- Apply processor if provided
        local processedItem = item
        if processor and type(processor) == "function" then
            processedItem = processor(item)
        end
        
        -- Only add if valid
        if processedItem and processedItem ~= "" then
            categories[currentSize + 1] = processedItem
            currentSize = currentSize + 1
        end
    end
    
    return categories
end

-- Adds semantic properties for multiple countries
-- This is a wrapper around addMultiValueSemanticProperties for backward compatibility
-- For new code, prefer using addMultiValueSemanticProperties directly with appropriate options
function p.addMultiCountrySemanticProperties(countryValue, semanticOutput)
    local MultiCountryDisplay = require('Module:MultiCountryDisplay')
    
    return p.addMultiValueSemanticProperties(
        countryValue,
        "Has country",
        nil, -- No processor needed as we use a custom value getter
        semanticOutput,
        {
            valueGetter = function(value)
                return MultiCountryDisplay.getCountriesForCategories(value)
            end
        }
    )
end

-- Adds semantic properties for multiple regions
-- This is a wrapper around addMultiValueSemanticProperties for backward compatibility
-- For new code, prefer using addMultiValueSemanticProperties directly with appropriate options
function p.addMultiRegionSemanticProperties(regionValue, semanticOutput)
    local RegionalMappingICANN = require('Module:RegionalMappingICANN')
    
    -- First, replace "and" with semicolons to standardize the delimiter
    local standardizedInput = regionValue:gsub("%s+and%s+", ";")
    
    return p.addMultiValueSemanticProperties(
        standardizedInput,
        "Has region",
        RegionalMappingICANN.normalizeRegion,
        semanticOutput
    )
end

-- Adds semantic properties for multiple languages
-- This is a wrapper around addMultiValueSemanticProperties for backward compatibility
-- For new code, prefer using addMultiValueSemanticProperties directly with appropriate options
function p.addMultiLanguageSemanticProperties(languagesValue, semanticOutput)
    local LanguageNormalization = require('Module:LanguageNormalization')
    
    return p.addMultiValueSemanticProperties(
        languagesValue,
        "Speaks language",
        LanguageNormalization.normalize,
        semanticOutput
    )
end

-- Helper function to process additional properties with multi-value support
-- This standardizes how additional properties are handled across templates
function p.processAdditionalProperties(args, semanticConfig, semanticOutput, skipProperties)
    if not semanticConfig or not semanticConfig.additionalProperties then
        return semanticOutput
    end
    
    skipProperties = skipProperties or {}
    
    -- For non-SMW case, collect property HTML fragments in a table for efficient concatenation
    local propertyHtml = {}
    
    for property, sourceFields in pairs(semanticConfig.additionalProperties) do
        -- Skip properties that are handled separately
        if not skipProperties[property] then
            for _, fieldName in ipairs(sourceFields) do
                if args[fieldName] and args[fieldName] ~= "" then
                    local value = args[fieldName]
                    
                    -- Apply transformation if available
                    if semanticConfig.transforms and semanticConfig.transforms[property] then
                        value = semanticConfig.transforms[property](value)
                    end
                    
                    -- Check if this is a multi-value field that needs to be split
                    if p.isMultiValueField(value) then
                        -- Use the generic multi-value function
                        semanticOutput = p.addMultiValueSemanticProperties(
                            value,
                            property,
                            semanticConfig.transforms and semanticConfig.transforms[property],
                            semanticOutput
                        )
                    else
                        -- Single value property
                        if mw.smw then
                            mw.smw.set({[property] = value})
                        else
                            -- Collect HTML fragments instead of concatenating strings
                            table.insert(propertyHtml, '<div style="display:none;">')
                            table.insert(propertyHtml, '  {{#set: ' .. property .. '=' .. value .. ' }}')
                            table.insert(propertyHtml, '</div>')
                        end
                    end
                end
            end
        end
    end
    
    -- For non-SMW case, concatenate all property HTML fragments at once
    if not mw.smw and #propertyHtml > 0 then
        semanticOutput = semanticOutput .. "\n" .. table.concat(propertyHtml, "\n")
    end
    
    return semanticOutput
end

-- Helper function to check if a field contains multiple values
function p.isMultiValueField(value)
    if not value or value == "" then return false end
    
    -- Check for common multi-value delimiters
    return value:match(";") or value:match("%s+and%s+")
end

-- Generates semantic properties based on configuration
-- @param args - Template parameters
-- @param semanticConfig - Config with properties, transforms, additionalProperties
-- @param options - Options: transform (functions), skipProperties (to exclude)
-- @return Wikitext with semantic annotations
function p.generateSemanticProperties(args, semanticConfig, options)
    if not args or not semanticConfig then return "" end
    
    local SemanticAnnotations = require('Module:SemanticAnnotations')
    options = options or {}
    
    -- Set options
    local semanticOptions = {
        transform = semanticConfig.transforms or options.transform
    }
    
    -- Set basic properties
    local semanticOutput = SemanticAnnotations.setSemanticProperties(
        args, 
        semanticConfig.properties, 
        semanticOptions
    )
    
    -- Process additional properties with multi-value support
    local skipProperties = options.skipProperties or {}
    semanticOutput = p.processAdditionalProperties(args, semanticConfig, semanticOutput, skipProperties)
    
    return semanticOutput
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 {}
    }
    
    return standardConfig
end

return p