Jump to content

Module:TemplateHelpers: Difference between revisions

// via Wikitext Extension for VSCode
// via Wikitext Extension for VSCode
 
(111 intermediate revisions by the same user not shown)
Line 1: Line 1:
-- Module:TemplateHelpers
--[[
-- Common helper functions for template modules to promote code reuse and consistency. This module provides utilities for string processing, field handling, normalization, and standardized block rendering to be used across multiple template types.
* Name: TemplateHelpers
* Author: Mark W. Datysgeld
* Description: Common helper functions for template modules promoting code reuse and consistency across string processing, field handling, normalization, and block rendering
* Notes: String processing functions for manipulating strings and template arguments; field processing functions for handling template fields and values; normalization wrappers for standardizing data formats; block generation helpers for rendering template blocks; category and semantic utilities (DEPRECATED wrappers for SemanticCategoryHelpers); configuration standardization for creating standard config structures; includes caching mechanism and multi-value string processing
]]


local p = {}
local p = {}
Line 6: Line 10:
-- Dependencies
-- Dependencies
local linkParser = require('Module:LinkParser')
local linkParser = require('Module:LinkParser')
local MultiCountryDisplay = require('Module:MultiCountryDisplay')
local CountryData = require('Module:CountryData')
local dateNormalization = require('Module:DateNormalization')
local dateNormalization = require('Module:NormalizationDate')
local CanonicalForms = require('Module:CanonicalForms')
local CanonicalForms = require('Module:CanonicalForms')
local NormalizationText = require('Module:NormalizationText')
local ListGeneration = require('Module:ListGeneration')
--------------------------------------------------------------------------------
-- Caching Mechanism
--------------------------------------------------------------------------------
-- Module-level unified cache
local functionCache = {}
-- Helper for generating cache keys from multiple arguments
-- @param prefix String prefix for the cache key (usually the function name)
-- @param ... Any number of arguments to include in the cache key
-- @return A string cache key
function p.generateCacheKey(prefix, ...)
    local args = {...}
    local parts = {prefix}
   
    for i, arg in ipairs(args) do
        if type(arg) == "table" then
            -- For tables, we can't reliably generate a cache key
            -- So we just use a placeholder with the table's memory address
            parts[i+1] = "table:" .. tostring(arg)
        elseif type(arg) == "nil" then
            parts[i+1] = "nil"
        else
            parts[i+1] = tostring(arg)
        end
    end
   
    return table.concat(parts, ":")
end
-- Generic caching wrapper
-- @param cacheKey The cache key to use
-- @param operation A function that returns the value to cache
-- @return The cached result or the result of executing the operation
function p.withCache(cacheKey, operation)
    -- Check if result is already cached
    if functionCache[cacheKey] ~= nil then
        return functionCache[cacheKey]
    end
   
    -- Execute operation and cache result
    local result = operation()
    functionCache[cacheKey] = result
    return result
end
--------------------------------------------------------------------------------
-- Constants
--------------------------------------------------------------------------------
-- This constant defines how the "label" string are rendered in HTML; we further manipulate them with the "template-label-style" CSS class
-- Added class to the entire row for alternating row colors and direct cell styling for better control
local FIELD_FORMAT = "|- class=\"template-data-row\"\n| class=\"template-label-cell\" | <span class=\"template-label-style\">%s</span>\n| class=\"template-value-cell\" | %s"
-- Format with tooltip support - includes tooltip data directly on the cell
local FIELD_FORMAT_WITH_TOOLTIP = "|- class=\"template-data-row\"\n| class=\"template-label-cell%s\" %s | <span class=\"template-label-style\">%s</span>\n| class=\"template-value-cell\" | %s"
-- Expose the field formats for use by other modules
p.FIELD_FORMAT = FIELD_FORMAT
p.FIELD_FORMAT_WITH_TOOLTIP = FIELD_FORMAT_WITH_TOOLTIP
-- Registry for declarative list post-processors
local listPostProcessors = {
    language = function(itemContent)
        local NormalizationLanguage = require('Module:NormalizationLanguage')
        local langName = NormalizationLanguage.normalize(itemContent)
        local nativeName = NormalizationLanguage.getNativeForm(langName)
        if nativeName and langName ~= "English" then
            return string.format('%s<br/><span style="display:inline-block; width:0.1em; visibility:hidden;">*</span><span style="font-size:75%%;">%s</span>', langName, nativeName)
        else
            return langName
        end
    end,
    website = function(itemContent)
        local linkUrl = itemContent
        if not linkUrl:match("^%a+://") then
            linkUrl = "https://" .. linkUrl
        end
        return string.format("[%s %s]", linkUrl, require('Module:LinkParser').strip(itemContent))
    end,
    autoWikiLink = function(itemContent)
        -- Trim whitespace and check for existing wiki links
        local trimmedContent = p.trim(itemContent)
        if linkParser.processWikiLink(trimmedContent, "check") then
            -- Extract page name and display text
            local pageName, displayText = trimmedContent:match("^%[%[([^|]+)|?(.*)%]%]$")
           
            -- Normalize by trimming whitespace
            pageName = p.trim(pageName or "")
            displayText = p.trim(displayText or "")
           
            -- Reconstruct link, omitting display text if it's same as page name
            if displayText == "" or displayText == pageName then
                return string.format("[[%s]]", pageName)
            else
                return string.format("[[%s|%s]]", pageName, displayText)
            end
        else
            -- Not a wiki link, so just wrap it
            return string.format("[[%s]]", trimmedContent)
        end
    end
}


--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
-- String Processing Functions
-- 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
        -- Determine if this key is numeric (number type or numeric-string)
        local isNumericKey = type(key) == "number" or (type(key) == "string" and tonumber(key) ~= nil)
        -- Skip all numeric parameters entirely
        if not isNumericKey then
            -- Preserve original key and lowercase variant
            normalized[key] = value
            if type(key) == "string" then
                normalized[key:lower()] = value
            end
        end
    end
   
    return normalized
end


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


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


-- Joins a table of values with the specified delimiter
-- Removes duplicate values from an array while preserving order
function p.joinValues(values, delimiter)
-- @param t table The array to deduplicate
     delimiter = delimiter or "; "
-- @return table A new array with duplicates removed
    if not values or #values == 0 then return "" end
function p.removeDuplicates(t)
     return table.concat(values, delimiter)
    -- Type checking
    if type(t) ~= 'table' then
        return {}
    end
   
    -- Helper function to check if a value is NaN
    local function isNan(v)
        return type(v) == 'number' and tostring(v) == '-nan'
    end
   
    -- Pre-allocate result table (maximum possible size is #t)
    local ret, exists = {}, {}
   
    -- Process each value, preserving order
    for i, v in ipairs(t) do
        if isNan(v) then
            -- NaNs can't be table keys, and they are also unique
            ret[#ret + 1] = v
        else
            if not exists[v] then
                ret[#ret + 1] = v
                exists[v] = true
            end
        end
    end
   
    return ret
end
 
-- Get current page ID with caching
-- @return number|nil The current page ID or nil if not available
function p.getCurrentPageId()
    -- Use mw.title API to get the current page title object
    local title = mw.title.getCurrentTitle()
    -- Return the ID property or nil if not available
    return title and title.id or nil
end
 
-- Module-level cache for wiki link processing
local wikiLinkCache = {}
 
-- @deprecated See LinkParser.processWikiLink
function p.processWikiLink(value, mode)
    return require('Module:LinkParser').processWikiLink(value, mode)
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 = function(match)
                return linkParser.processWikiLink("[[" .. match .. "]]", "strip")
            end
        },
        {
            pattern = "%[%[([^|%]]+)|([^%]]+)%]%]",
            replacement = function(match1, match2)
                return linkParser.processWikiLink("[[" .. match1 .. "|" .. match2 .. "]]", "strip")
            end  
        }
    },
    SINGLE_BRACES = {
        { pattern = "{([^{}]+)}", replacement = "%1" }  -- {text} -> text
    },
    HTML_BASIC = {
        { pattern = "</?[bi]>", replacement = "" },    -- Remove <b>, </b>, <i>, </i>
        { pattern = "</?span[^>]*>", replacement = "" } -- Remove <span...>, </span>
    },
    LOGO = {
        { pattern = "^[Ff][Ii][Ll][Ee]%s*:", replacement = "" } -- Remove "File:" prefix
    },
    IMAGE_FILES = {
        { pattern = "%[%[([^|%]]+)%]%]", replacement = "%1" },    -- [[Image.jpg]] -> Image.jpg
        { pattern = "%[%[([^|%]]+)|.+%]%]", replacement = "%1" },  -- [[Image.jpg|...]] -> Image.jpg
        { pattern = "^[Ff][Ii][Ll][Ee]%s*:", replacement = "" },  -- Remove "File:" prefix
        { pattern = "^[Ii][Mm][Aa][Gg][Ee]%s*:", replacement = "" } -- Remove "Image:" prefix too
    }
}
 
-- Sanitizes user input by removing or transforming unwanted patterns
function p.sanitizeUserInput(value, patternCategories, customPatterns, options)
     return NormalizationText.sanitizeUserInput(value, patternCategories, customPatterns, options)
end
end


Line 39: Line 252:
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------


-- Retrieves a field value from args using either multiple possible keys or a single key
function p.getFieldValue(args, field)
function p.getFieldValue(args, field)
     if field.keys then
     -- Cache lookup for performance
        for _, key in ipairs(field.keys) do
    local cacheKey = p.generateCacheKey("getFieldValue", field.key or table.concat(field.keys or {}, ","), args)
            if args[key] and args[key] ~= "" then
    local cached = p.withCache(cacheKey, function()
                 return key, args[key]
        -- Case-insensitive lookup logic
        if field.keys then
            for _, key in ipairs(field.keys) do
                if args[key] and args[key] ~= "" then
                    return { key = key, value = args[key] }
                end
                local lowerKey = key:lower()
                 if args[lowerKey] and args[lowerKey] ~= "" and lowerKey ~= key then
                    return { key = lowerKey, value = args[lowerKey] }
                end
             end
             end
            return { key = nil, value = nil }
         end
         end
         return nil, nil
 
    end
         if field.key then
    return field.key, (args[field.key] and args[field.key] ~= "") and args[field.key] or nil
            if args[field.key] and args[field.key] ~= "" then
                return { key = field.key, value = args[field.key] }
            end
            local lowerKey = field.key:lower()
            if args[lowerKey] and args[lowerKey] ~= "" and lowerKey ~= field.key then
                return { key = lowerKey, value = args[lowerKey] }
            end
            return { key = field.key, value = nil }
        end
 
        return { key = nil, value = nil }
    end)
    return cached.key, cached.value
end
end


Line 77: Line 311:
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------


-- Formats website URLs as either a single link or an HTML unordered list of links
-- Wrapper around CountryData for consistent country formatting
-- Uses splitMultiValueString for more flexible delimiter handling
function p.normalizeCountries(value)
-- Optimized to avoid unnecessary table creation for single websites
function p.normalizeWebsites(value)
     if not value or value == "" then return "" end
     if not value or value == "" then return "" end
      
      
     -- Quick check for single website (no delimiters)
     -- Create a cache key
     if not value:match(";") and not value:match("%s+and%s+") then
     local cacheKey = p.generateCacheKey("normalizeCountries", value)
        -- Single website case - avoid table creation entirely
        return string.format("[%s %s]", value, linkParser.strip(value))
    end
      
      
     -- Multiple websites case
     -- Use the caching wrapper
     local websites = p.splitMultiValueString(value)
     return p.withCache(cacheKey, function()
    if #websites > 1 then
         return CountryData.formatCountries(value)
        local listItems = {}
     end)
        for _, site in ipairs(websites) do
            local formattedLink = string.format("[%s %s]", site, linkParser.strip(site))
            table.insert(listItems, string.format("<li>%s</li>", formattedLink))
        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
end


-- Wrapper around MultiCountryDisplay for consistent country formatting
-- Wrapper around DateNormalization for consistent date formatting
function p.normalizeCountries(value)
function p.normalizeDates(value)
     if not value or value == "" then return "" end
     if not value or value == "" then return "" end
     return MultiCountryDisplay.formatCountries(value)
      
    -- Create a cache key
    local cacheKey = p.generateCacheKey("normalizeDates", value)
   
    -- Use the caching wrapper
    return p.withCache(cacheKey, function()
        return tostring(dateNormalization.formatDate(value))
    end)
end
end


-- Wrapper around DateNormalization for consistent date formatting
-- Formats a date range with configurable options
function p.normalizeDates(value)
-- @param startDate The start date string
     if not value or value == "" then return "" end
-- @param endDate The end date string (optional)
     return tostring(dateNormalization.formatDate(value))
-- @param options Table of options for customizing the output:
--  - dateLabel: Label to use for the date field (default: ConfigRepository.fieldLabels.date)
--  - rangeDelimiter: String to use between dates (default: " – " [en dash])
--  - outputMode: Output format - "complete" (default), "text", or "html"
--  - showSingleDate: Whether to show the start date when end date is missing (default: true)
--  - consolidateIdenticalDates: Whether to show only one date when start=end (default: true)
-- @return Based on outputMode:
--  - "text": Returns the formatted date text as a string
--  - "html": Returns the complete HTML for the date field as a string
--  - "complete": Returns a table with text, html, and isCompleteHtml properties
function p.formatDateRange(startDate, endDate, options)
    -- Default options
    options = options or {}
    local dateLabel = options.dateLabel or (require('Module:ConfigRepository').fieldLabels.date)
    local rangeDelimiter = options.rangeDelimiter or " – " -- en dash
    local outputMode = options.outputMode or "complete" -- "complete", "text", or "html"
    local showSingleDate = options.showSingleDate ~= false -- true by default
    local consolidateIdenticalDates = options.consolidateIdenticalDates ~= false -- true by default
 
    -- Global fallback: if only end date is present, treat end as start
    if (not startDate or startDate == "") and endDate and endDate ~= "" then
        startDate, endDate = endDate, nil
    end
   
    -- Handle empty input
     if not startDate or startDate == "" then
        if outputMode == "text" then return "" end
        if outputMode == "html" then return "" end
        return { text = "", html = "", isCompleteHtml = true }
    end
   
    -- Create a cache key
    -- For options, we only include the values that affect the output
    local optionsKey = string.format(
        "%s:%s:%s:%s",
        dateLabel,
        rangeDelimiter,
        outputMode,
        consolidateIdenticalDates and "consolidate" or "noconsolidate"
    )
   
    local cacheKey = p.generateCacheKey("formatDateRange", startDate, endDate or "nil", optionsKey)
   
    -- Use the caching wrapper
     return p.withCache(cacheKey, function()
        -- Normalize dates
        local startFormatted = p.normalizeDates(startDate)
        local endFormatted = endDate and endDate ~= "" and p.normalizeDates(endDate) or nil
       
        -- Format date text based on options
        local dateText
        if endFormatted and endFormatted ~= startFormatted then
            -- Different start and end dates
            dateText = startFormatted .. rangeDelimiter .. endFormatted
        elseif endFormatted and endFormatted == startFormatted and not consolidateIdenticalDates then
            -- Same start and end dates, but option to show both
            dateText = startFormatted .. rangeDelimiter .. endFormatted
        else
            -- Single date or consolidated identical dates
            dateText = startFormatted
        end
       
        -- Format HTML using the field format and label
        local dateHtml = string.format(FIELD_FORMAT, dateLabel, dateText)
       
        -- Return based on requested output mode
        if outputMode == "text" then return dateText end
        if outputMode == "html" then return dateHtml end
       
        -- Default: return both formats
        return {
            text = dateText,
            html = dateHtml,
            isCompleteHtml = true -- For compatibility with existing code
        }
    end)
end
end


Line 120: Line 422:
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------


-- Generates a standard title block with configurable class and text
-- @deprecated See TemplateStructure.renderTitleBlock and AchievementSystem.renderTitleBlockWithAchievement
-- Enhanced to support achievement integration with options
function p.renderTitleBlock(args, titleClass, titleText, options)
function p.renderTitleBlock(args, titleClass, titleText, options)
     options = options or {}
     options = options or {}
    titleClass = titleClass or "template-title"
      
      
     -- Basic title block without achievement integration
     -- If achievement support is needed, use AchievementSystem
     if not options.achievementSupport then
     if options.achievementSupport then
         return string.format('|-\n! colspan="2" class="%s" | %s', titleClass, titleText)
         return require('Module:AchievementSystem').renderTitleBlockWithAchievement(
    end
            args, titleClass, titleText,
   
            options.achievementClass or "",
    -- With achievement support
            options.achievementId or "",
    local achievementClass = options.achievementClass or ""
            options.achievementName 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
     else
         -- Clean row with no achievement data
         -- Otherwise use the basic title block from TemplateStructure
         return string.format('|-\n! colspan="2" class="%s" | %s', titleClass, titleText)
         return require('Module:TemplateStructure').renderTitleBlock(args, titleClass, titleText)
     end
     end
end
end


-- Renders a standard fields block based on field definitions and processors
-- Renders a standard fields block based on field definitions and processors
-- Enhanced to support complete HTML blocks and custom field rendering
-- Enhanced to support complete HTML blocks, custom field rendering, and tooltips
function p.renderFieldsBlock(args, fields, processors)
-- Pre-allocates output table for better performance
function p.renderFieldsBlock(args, fields, processors, propertyMappings)
     processors = processors or {}
     processors = processors or {}
    propertyMappings = propertyMappings or {}
    -- filter out hidden fields
    local filteredFields = {}
    for _, f in ipairs(fields) do
        if not f.hidden then
            table.insert(filteredFields, f)
        end
    end
   
    -- 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 out = {}
    local outIndex = 1
      
      
     for _, field in ipairs(fields) do
     for _, field in ipairs(filteredFields) do
         local key, value = p.getFieldValue(args, field)
         local key, value = p.getFieldValue(args, field)
         if value then
         if value then
             -- Apply processor if available for this field
            local continue = false
            if key and processors[key] and type(processors[key]) == "function" then
           
                 local processedValue = processors[key](value, args)
             -- Handle declarative lists first
            if field.list then
                -- Use a safe, semicolon-only pattern to avoid breaking up valid names.
                local semicolonOnlyPattern = {{pattern = ";%s*", replacement = ";"}}
                local items = NormalizationText.splitMultiValueString(value, semicolonOnlyPattern)
                if #items > 0 then
                    local options = {}
                    if type(field.list) == 'string' then
                        options.mode = field.list
                    elseif type(field.list) == 'table' then
                        for k, v in pairs(field.list) do
                            options[k] = v
                        end
                    end
 
                    -- Chain post-processors declaratively
                    local processorsToApply = {}
                    if field.autoWikiLink then
                        table.insert(processorsToApply, "autoWikiLink")
                    end
                    if options.postProcess then
                        table.insert(processorsToApply, options.postProcess)
                    end
 
                    for _, processorName in ipairs(processorsToApply) do
                        local processorFunc = listPostProcessors[processorName]
                        if processorFunc then
                            local originalHook = options.itemHook
                            options.itemHook = function(item)
                                if type(originalHook) == 'function' then
                                    item = originalHook(item)
                                end
                               
                                local itemContent = type(item) == 'table' and item.content or item
                                local processedContent = processorFunc(itemContent)
 
                                if type(item) == 'table' then
                                    item.content = processedContent
                                    return item
                                else
                                    return processedContent
                                end
                            end
                        end
                    end
                   
                    local listOutput = ListGeneration.createList(items, options)
                    out[outIndex] = string.format(FIELD_FORMAT, field.label, listOutput)
                    outIndex = outIndex + 1
                    continue = true
                end
            end
 
            if not continue then
                -- Create sanitization options
                 local sanitizeOptions = {
                    preserveWikiLinks = field.autoWikiLink or field.preserveWikiLinks
                }
               
                -- Get property name for this field if available (case-insensitive)
                local propertyName = nil
                for propName, fieldName in pairs(propertyMappings) do
                    if key and (fieldName == key or tostring(fieldName):lower() == tostring(key):lower()) then
                        propertyName = propName
                        break
                    end
                end
                  
                  
                 -- Handle the case where a processor returns complete HTML
                 -- Get tooltip text if property exists
                if type(processedValue) == "table" and processedValue.isCompleteHtml then
                local tooltipText = ""
                    -- Add the complete HTML as is
                if propertyName then
                    table.insert(out, processedValue.html)
                    tooltipText = require('Module:SemanticCategoryHelpers').getPropertyDescription(propertyName) or ""
                elseif processedValue ~= nil and processedValue ~= false then
                end
                     -- Standard field rendering
               
                     table.insert(out, string.format("|-\n| '''%s''':\n| %s", field.label, processedValue))
                -- Prepare tooltip attributes if tooltip text exists
                local tooltipClass = ""
                local tooltipAttr = ""
                if tooltipText and tooltipText ~= "" then
                    -- Escape quotes in tooltip text to prevent HTML attribute issues
                    local escapedTooltip = tooltipText:gsub('"', '"')
                    tooltipClass = " has-tooltip"
                    tooltipAttr = string.format('data-tooltip="%s"', escapedTooltip)
                end
               
                -- Apply processor if available for this field
                if key and processors[key] and type(processors[key]) == "function" then
                    local processedValue = processors[key](value, args)
                   
                    -- Preserve wiki links if needed
                    processedValue = linkParser.preserveWikiLinks(
                        value,
                        processedValue,
                        sanitizeOptions.preserveWikiLinks
                    )
                   
                    -- 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
                        -- Apply wiki link handling
                        processedValue = linkParser.applyWikiLinkHandling(processedValue, field)
                       
                        -- Standard field rendering with tooltip
                        out[outIndex] = string.format(FIELD_FORMAT_WITH_TOOLTIP,
                            tooltipClass, tooltipAttr, field.label, processedValue)
                        outIndex = outIndex + 1
                    end
                else
                     -- Standard field rendering without processor
                    -- Apply sanitization with preserveWikiLinks option if needed
                    local finalValue
                    if sanitizeOptions.preserveWikiLinks then
                        finalValue = value
                     else
                        finalValue = p.sanitizeUserInput(value, nil, nil, sanitizeOptions)
                    end
                   
                    -- Apply wiki link handling
                    finalValue = linkParser.applyWikiLinkHandling(finalValue, field)
                   
                    -- Use format with tooltip
                    out[outIndex] = string.format(FIELD_FORMAT_WITH_TOOLTIP,
                        tooltipClass, tooltipAttr, field.label, finalValue)
                    outIndex = outIndex + 1
                 end
                 end
            else
                -- Standard field rendering without processor
                table.insert(out, string.format("|-\n| '''%s''':\n| %s", field.label, value))
             end
             end
         end
         end
Line 179: Line 600:
end
end


-- Renders a standard divider block with optional label
-- @deprecated See TemplateStructure.renderDividerBlock
function p.renderDividerBlock(label)
function p.renderDividerBlock(label)
     if label and label ~= "" then
     return require('Module:TemplateStructure').renderDividerBlock(label)
        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
end


 
-- Extracts semantic value from a field, handling wiki links appropriately
--------------------------------------------------------------------------------
-- @param fieldValue The value to extract semantic data from
-- Category Utilities
-- @param fieldName The name of the field (for error reporting)
--------------------------------------------------------------------------------
-- @param errorContext Optional error context for error handling
 
-- @return The extracted semantic value or nil if the input is empty
-- Default delimiters for splitMultiValueString
function p.extractSemanticValue(fieldValue, fieldName, errorContext)
-- Defined once as an upvalue to avoid recreating on each function call
     if not fieldValue or fieldValue == "" then
local defaultDelimiters = {
         return nil
    {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
     end
      
      
     -- Pre-allocate table based on delimiter count
     -- If the value already has wiki links, extract the name using LinkParser
    -- Count semicolons to estimate the number of items
     local LinkParser = require('Module:LinkParser')
     local count = 0
     if LinkParser.processWikiLink(fieldValue, "check") then
     for _ in standardizedInput:gmatch(";") do
        -- Use the standardized error handling helper
         count = count + 1
        return p.withErrorHandling(
            errorContext,
            "extractFromWikiLink_" .. fieldName,
            LinkParser.extractFromWikiLink,
            fieldValue,  -- fallback to original value on error
            fieldValue
        )
    else
        -- Otherwise, use the plain text value
         return fieldValue
     end
     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
end


-- Splits a region string that may contain "and" conjunctions
-- Standardized error handling helper
-- Returns an array of individual region names
-- Executes a function with error protection if an error context is provided
-- This is now a wrapper around splitMultiValueString for backward compatibility
-- @param errorContext The error context for error handling (optional)
function p.splitRegionCategories(regionValue)
-- @param functionName The name of the function being protected (for error reporting)
    return p.splitMultiValueString(regionValue)
-- @param operation The function to execute
end
-- @param fallback The fallback value to return if an error occurs
 
-- @param ... Additional arguments to pass to the operation function
-- Builds a category string from a table of category names
-- @return The result of the operation or the fallback value if an error occurs
function p.buildCategories(categories)
function p.withErrorHandling(errorContext, functionName, operation, fallback, ...)
    if not categories or #categories == 0 then return "" end
     -- Capture varargs in a local table to avoid using ... multiple times
    local formatted = {}
     local args = {...}
    for _, cat in ipairs(categories) do
        -- Check if the category already has the [[ ]] wrapper
        if not string.match(cat, "^%[%[Category:") then
            table.insert(formatted, string.format("[[Category:%s]]", cat))
        else
            table.insert(formatted, cat)
        end
     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
    -- If no error context is provided, execute the operation directly
        for _, group in ipairs(mapping) do
     if not errorContext or type(errorContext) ~= "table" then
            if group.canonical == canonical and group.category then
        return operation(unpack(args))
                table.insert(categories, group.category)
                break
            end
        end
     end
     end
      
      
     return categories
     -- Use ErrorHandling module for protected execution
end
    local ErrorHandling = require('Module:ErrorHandling')
 
--------------------------------------------------------------------------------
-- 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 {}
     -- Create a wrapper function that passes the arguments to the operation
    local processedItems = {}
     local wrapper = function()
   
         return operation(unpack(args))
     -- 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
     end
      
      
     -- For non-SMW case, collect property HTML fragments in a table for efficient concatenation
     -- Execute with error protection
     local propertyHtml = {}
     return ErrorHandling.protect(
   
         errorContext,
    -- Process each item and add as a semantic property
         functionName,
    for _, item in ipairs(items) do
         wrapper,
         -- Apply processor if provided
         fallback
         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
end


-- Generic function to add multi-value categories
-- Renders a standard logo block with sanitized image path
-- This is a generalized helper that can be used for any multi-value category field
-- @param args The template arguments
function p.addMultiValueCategories(value, processor, categories, options)
-- @param options Table of options for customizing the output:
     if not value or value == "" then return categories end
--  - cssClass: CSS class for the logo container (default: "template-logo")
   
--  - imageParams: Additional image parameters like size, alignment (default: "")
--   - errorContext: Optional error context for error handling
-- @return The rendered logo block HTML or empty string if no logo
function p.renderLogoBlock(args, options)
     -- Default options
     options = options or {}
     options = options or {}
    local cssClass = options.cssClass or "template-logo"
    local imageParams = options.imageParams or ""
      
      
     -- Get the values to process
     -- Define the logo rendering operation
     local items
     local function renderLogoOperation(args, cssClass, imageParams)
    if options.valueGetter and type(options.valueGetter) == "function" then
         -- Get logo parameter
         -- Use custom value getter if provided
         local logo = args["logo"]
         items = options.valueGetter(value)
          
    else
         -- If no logo or empty, return empty string
         -- Default to splitting the string
         if not logo or logo == "" then
         items = p.splitMultiValueString(value)
             return ""
    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
         end
          
          
         -- Only add if valid
         -- Sanitize logo path - extract filename and remove prefixes
         if processedItem and processedItem ~= "" then
        logo = p.sanitizeUserInput(logo, "IMAGE_FILES")
             categories[currentSize + 1] = processedItem
       
             currentSize = currentSize + 1
        -- Format image parameters if provided
         end
         local imgParams = imageParams ~= "" and "|" .. imageParams or ""
       
        -- Render the logo image
        return string.format(
             '|-\n| colspan="2" class="%s" | [[Image:%s%s]]',
             cssClass, logo, imgParams
         )
     end
     end
      
      
     return categories
    -- Use the standardized error handling helper
     return p.withErrorHandling(
        options.errorContext,
        "renderLogoBlock",
        renderLogoOperation,
        "", -- Empty string fallback
        args, cssClass, imageParams
    )
end
end


-- Adds semantic properties for multiple countries
--------------------------------------------------------------------------------
-- This is a wrapper around addMultiValueSemanticProperties for backward compatibility
-- Multi-Value String Processing
-- For new code, prefer using addMultiValueSemanticProperties directly with appropriate options
--------------------------------------------------------------------------------
function p.addMultiCountrySemanticProperties(countryValue, semanticOutput)
 
    local MultiCountryDisplay = require('Module:MultiCountryDisplay')
-- Default delimiters for splitMultiValueString
      
-- Defined once as an upvalue to avoid recreating on each function call
     return p.addMultiValueSemanticProperties(
local defaultDelimiters = {
        countryValue,
     {pattern = "%s+and%s+", replacement = ";"},
        "Has country",
     {pattern = ";%s*", replacement = ";"}
        nil, -- No processor needed as we use a custom value getter
}
        semanticOutput,
 
        {
-- Semicolon-only pattern for backward compatibility with splitSemicolonValues
            valueGetter = function(value)
-- Exposed as a module-level constant for use by other modules
                return MultiCountryDisplay.getCountriesForCategories(value)
p.SEMICOLON_PATTERN = {{pattern = ";%s*", replacement = ";"}}
            end
        }
    )
end


-- Adds semantic properties for multiple regions
-- Generic function to split multi-value strings with various delimiters
-- This is a wrapper around addMultiValueSemanticProperties for backward compatibility
function p.splitMultiValueString(value, delimiters)
-- For new code, prefer using addMultiValueSemanticProperties directly with appropriate options
     return NormalizationText.splitMultiValueString(value, delimiters)
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
end


-- Adds semantic properties for multiple languages
--------------------------------------------------------------------------------
-- This is a wrapper around addMultiValueSemanticProperties for backward compatibility
-- Configuration Standardization
-- 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
     Renders a table of fields with labels and values using TemplateStructure.
function p.processAdditionalProperties(args, semanticConfig, semanticOutput, skipProperties)
     if not semanticConfig or not semanticConfig.additionalProperties then
        return semanticOutput
    end
      
      
     skipProperties = skipProperties or {}
     Parameters:
      fields      - Array of field objects, each with:
                    - label: The field label to display
                    - value: The field value to display
                    - class: Optional CSS class for the row
      options      - Optional configuration:
                    - tableClass: CSS class for the table (default: "template-field-table")
                    - tableAttrs: Additional table attributes
                    - fieldFormat: Format string for field rows (default: uses FIELD_FORMAT)
                    - errorContext: Optional error context for error handling
      
      
     -- For non-SMW case, collect property HTML fragments in a table for efficient concatenation
     Returns:
    local propertyHtml = {}
      Wikitext markup for the field table
   
]]
    for property, sourceFields in pairs(semanticConfig.additionalProperties) do
function p.renderFieldTable(fields, options)
        -- Skip properties that are handled separately
     -- Early return for empty fields
        if not skipProperties[property] then
     if not fields or #fields == 0 then
            for _, fieldName in ipairs(sourceFields) do
         return ""
                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
     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
-- Enhanced to handle common patterns while allowing template-specific customization
function p.generateSemanticProperties(args, semanticConfig, options)
    if not args or not semanticConfig then return "" end
   
    local SemanticAnnotations = require('Module:SemanticAnnotations')
     options = options or {}
     options = options or {}
      
      
     -- Set options
     -- Define the field table rendering operation
     local semanticOptions = {
     local function renderFieldTableOperation(fields, options)
         transform = semanticConfig.transforms or options.transform
         local TemplateStructure = require('Module:TemplateStructure')
    }
       
   
        -- Create a config for the render function with optimized defaults
    -- Set basic properties
        local config = {
    local semanticOutput = SemanticAnnotations.setSemanticProperties(
            tableClass = options.tableClass or "template-field-table",
        args,
            tableAttrs = options.tableAttrs or 'cellpadding="2"',
        semanticConfig.properties,
            blocks = {}
        semanticOptions
        }
    )
   
    -- Handle boolean flags
    if options.booleanFlags then
        -- For non-SMW case, collect property HTML fragments in a table for efficient concatenation
        local propertyHtml = {}
          
          
         for property, flagName in pairs(options.booleanFlags) do
         -- Pre-allocate blocks array based on field count
            if args[flagName] then
        local blocks = {}
                if mw.smw then
                    mw.smw.set({[property] = "true"})
                else
                    -- Collect HTML fragments instead of concatenating strings
                    table.insert(propertyHtml, '<div style="display:none;">')
                    table.insert(propertyHtml, '  {{#set: ' .. property .. '=true }}')
                    table.insert(propertyHtml, '</div>')
                end
            end
        end
          
          
         -- For non-SMW case, concatenate all property HTML fragments at once
         -- Use the module's FIELD_FORMAT or a custom format if provided
        if not mw.smw and #propertyHtml > 0 then
         local fieldFormat = options.fieldFormat or FIELD_FORMAT
            semanticOutput = semanticOutput .. "\n" .. table.concat(propertyHtml, "\n")
         end
    end
   
    -- Process additional properties with multi-value support
    local skipProperties = options.skipProperties or {}
    semanticOutput = p.processAdditionalProperties(args, semanticConfig, semanticOutput, skipProperties)
   
    -- Handle multi-value fields that need to be split
    if options.multiValueFields then
        -- For non-SMW case, collect property HTML fragments in a table for efficient concatenation
        local propertyHtml = {}
          
          
         for property, config in pairs(options.multiValueFields) do
        -- Create a block function for each field with direct index assignment
             local fieldName = config.field
         for i = 1, #fields do
             local splitter = config.splitter or p.splitMultiValueString
             local field = fields[i]
            local processor = config.processor
             blocks[i] = function()
           
                -- Combine the field's class with the template-data-row class
            if args[fieldName] and args[fieldName] ~= "" then
                local fieldClass = field.class and field.class or ""
                 local values = splitter(args[fieldName])
                  
                 for _, value in ipairs(values) do
                 -- Create the row with proper classes
                    -- Apply processor if available
                local row = '|- class="template-data-row' .. (fieldClass ~= "" and ' ' .. fieldClass or '') .. '"'
                    if processor and type(processor) == "function" then
               
                        value = processor(value)
                -- Format the cells using the field format but replace the row start
                    end
                local cellsFormat = fieldFormat:gsub("^[^|]+", "")
                   
               
                    if value and value ~= "" then
                -- Return the complete row
                        if mw.smw then
                return row .. string.format(cellsFormat, field.label, field.value)
                            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
         end
          
          
         -- For non-SMW case, concatenate all property HTML fragments at once
         -- Assign blocks to config
         if not mw.smw and #propertyHtml > 0 then
         config.blocks = blocks
            semanticOutput = semanticOutput .. "\n" .. table.concat(propertyHtml, "\n")
       
        end
        -- Use TemplateStructure's render function
        return TemplateStructure.render({}, config, options.errorContext)
     end
     end
      
      
     -- Handle custom special cases
     -- Use the standardized error handling helper
     if options.specialCaseHandler and type(options.specialCaseHandler) == "function" then
     return p.withErrorHandling(
         local specialCaseOutput = options.specialCaseHandler(args, semanticOutput, semanticConfig)
         options.errorContext,
        if specialCaseOutput then
         "renderFieldTable",
            semanticOutput = specialCaseOutput
         renderFieldTableOperation,
         end
         "", -- Empty string fallback
    end
         fields, options
   
     )
    return semanticOutput
end
 
--------------------------------------------------------------------------------
-- Configuration Standardization
--------------------------------------------------------------------------------
 
-- Creates a standardized configuration structure for template modules
-- This ensures all templates have a consistent configuration format
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
end


return p
return p

Latest revision as of 03:13, 25 August 2025

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

--[[
* Name: TemplateHelpers
* Author: Mark W. Datysgeld
* Description: Common helper functions for template modules promoting code reuse and consistency across string processing, field handling, normalization, and block rendering
* Notes: String processing functions for manipulating strings and template arguments; field processing functions for handling template fields and values; normalization wrappers for standardizing data formats; block generation helpers for rendering template blocks; category and semantic utilities (DEPRECATED wrappers for SemanticCategoryHelpers); configuration standardization for creating standard config structures; includes caching mechanism and multi-value string processing
]]

local p = {}

-- Dependencies
local linkParser = require('Module:LinkParser')
local CountryData = require('Module:CountryData')
local dateNormalization = require('Module:NormalizationDate')
local CanonicalForms = require('Module:CanonicalForms')
local NormalizationText = require('Module:NormalizationText')
local ListGeneration = require('Module:ListGeneration')

--------------------------------------------------------------------------------
-- Caching Mechanism
--------------------------------------------------------------------------------

-- Module-level unified cache
local functionCache = {}

-- Helper for generating cache keys from multiple arguments
-- @param prefix String prefix for the cache key (usually the function name)
-- @param ... Any number of arguments to include in the cache key
-- @return A string cache key
function p.generateCacheKey(prefix, ...)
    local args = {...}
    local parts = {prefix}
    
    for i, arg in ipairs(args) do
        if type(arg) == "table" then
            -- For tables, we can't reliably generate a cache key
            -- So we just use a placeholder with the table's memory address
            parts[i+1] = "table:" .. tostring(arg)
        elseif type(arg) == "nil" then
            parts[i+1] = "nil"
        else
            parts[i+1] = tostring(arg)
        end
    end
    
    return table.concat(parts, ":")
end

-- Generic caching wrapper
-- @param cacheKey The cache key to use
-- @param operation A function that returns the value to cache
-- @return The cached result or the result of executing the operation
function p.withCache(cacheKey, operation)
    -- Check if result is already cached
    if functionCache[cacheKey] ~= nil then
        return functionCache[cacheKey]
    end
    
    -- Execute operation and cache result
    local result = operation()
    functionCache[cacheKey] = result
    return result
end

--------------------------------------------------------------------------------
-- Constants
--------------------------------------------------------------------------------

-- This constant defines how the "label" string are rendered in HTML; we further manipulate them with the "template-label-style" CSS class
-- Added class to the entire row for alternating row colors and direct cell styling for better control
local FIELD_FORMAT = "|- class=\"template-data-row\"\n| class=\"template-label-cell\" | <span class=\"template-label-style\">%s</span>\n| class=\"template-value-cell\" | %s"

-- Format with tooltip support - includes tooltip data directly on the cell
local FIELD_FORMAT_WITH_TOOLTIP = "|- class=\"template-data-row\"\n| class=\"template-label-cell%s\" %s | <span class=\"template-label-style\">%s</span>\n| class=\"template-value-cell\" | %s"

-- Expose the field formats for use by other modules
p.FIELD_FORMAT = FIELD_FORMAT
p.FIELD_FORMAT_WITH_TOOLTIP = FIELD_FORMAT_WITH_TOOLTIP

-- Registry for declarative list post-processors
local listPostProcessors = {
    language = function(itemContent)
        local NormalizationLanguage = require('Module:NormalizationLanguage')
        local langName = NormalizationLanguage.normalize(itemContent)
        local nativeName = NormalizationLanguage.getNativeForm(langName)
        if nativeName and langName ~= "English" then
            return string.format('%s<br/><span style="display:inline-block; width:0.1em; visibility:hidden;">*</span><span style="font-size:75%%;">%s</span>', langName, nativeName)
        else
            return langName
        end
    end,
    website = function(itemContent)
        local linkUrl = itemContent
        if not linkUrl:match("^%a+://") then
            linkUrl = "https://" .. linkUrl
        end
        return string.format("[%s %s]", linkUrl, require('Module:LinkParser').strip(itemContent))
    end,
    autoWikiLink = function(itemContent)
        -- Trim whitespace and check for existing wiki links
        local trimmedContent = p.trim(itemContent)
        if linkParser.processWikiLink(trimmedContent, "check") then
            -- Extract page name and display text
            local pageName, displayText = trimmedContent:match("^%[%[([^|]+)|?(.*)%]%]$")
            
            -- Normalize by trimming whitespace
            pageName = p.trim(pageName or "")
            displayText = p.trim(displayText or "")
            
            -- Reconstruct link, omitting display text if it's same as page name
            if displayText == "" or displayText == pageName then
                return string.format("[[%s]]", pageName)
            else
                return string.format("[[%s|%s]]", pageName, displayText)
            end
        else
            -- Not a wiki link, so just wrap it
            return string.format("[[%s]]", trimmedContent)
        end
    end
}

--------------------------------------------------------------------------------
-- 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
        -- Determine if this key is numeric (number type or numeric-string)
        local isNumericKey = type(key) == "number" or (type(key) == "string" and tonumber(key) ~= nil)
        -- Skip all numeric parameters entirely
        if not isNumericKey then
            -- Preserve original key and lowercase variant
            normalized[key] = value
            if type(key) == "string" then
                normalized[key:lower()] = value
            end
        end
    end
    
    return normalized
end

-- Trims leading and trailing whitespace from a string
function p.trim(s)
    return NormalizationText.trim(s)
end

-- Joins a table of values with the specified delimiter
function p.joinValues(values, delimiter)
    return NormalizationText.joinValues(values, delimiter)
end

-- Removes duplicate values from an array while preserving order
-- @param t table The array to deduplicate
-- @return table A new array with duplicates removed
function p.removeDuplicates(t)
    -- Type checking
    if type(t) ~= 'table' then
        return {}
    end
    
    -- Helper function to check if a value is NaN
    local function isNan(v)
        return type(v) == 'number' and tostring(v) == '-nan'
    end
    
    -- Pre-allocate result table (maximum possible size is #t)
    local ret, exists = {}, {}
    
    -- Process each value, preserving order
    for i, v in ipairs(t) do
        if isNan(v) then
            -- NaNs can't be table keys, and they are also unique
            ret[#ret + 1] = v
        else
            if not exists[v] then
                ret[#ret + 1] = v
                exists[v] = true
            end
        end
    end
    
    return ret
end

-- Get current page ID with caching
-- @return number|nil The current page ID or nil if not available
function p.getCurrentPageId()
    -- Use mw.title API to get the current page title object
    local title = mw.title.getCurrentTitle()
    -- Return the ID property or nil if not available
    return title and title.id or nil
end

-- Module-level cache for wiki link processing
local wikiLinkCache = {}

-- @deprecated See LinkParser.processWikiLink
function p.processWikiLink(value, mode)
    return require('Module:LinkParser').processWikiLink(value, mode)
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 = function(match) 
                return linkParser.processWikiLink("[[" .. match .. "]]", "strip") 
            end 
        },
        { 
            pattern = "%[%[([^|%]]+)|([^%]]+)%]%]", 
            replacement = function(match1, match2) 
                return linkParser.processWikiLink("[[" .. match1 .. "|" .. match2 .. "]]", "strip") 
            end 
        }
    },
    SINGLE_BRACES = {
        { pattern = "{([^{}]+)}", replacement = "%1" }  -- {text} -> text
    },
    HTML_BASIC = {
        { pattern = "</?[bi]>", replacement = "" },     -- Remove <b>, </b>, <i>, </i>
        { pattern = "</?span[^>]*>", replacement = "" } -- Remove <span...>, </span>
    },
    LOGO = {
        { pattern = "^[Ff][Ii][Ll][Ee]%s*:", replacement = "" } -- Remove "File:" prefix
    },
    IMAGE_FILES = {
        { pattern = "%[%[([^|%]]+)%]%]", replacement = "%1" },     -- [[Image.jpg]] -> Image.jpg
        { pattern = "%[%[([^|%]]+)|.+%]%]", replacement = "%1" },  -- [[Image.jpg|...]] -> Image.jpg
        { pattern = "^[Ff][Ii][Ll][Ee]%s*:", replacement = "" },   -- Remove "File:" prefix
        { pattern = "^[Ii][Mm][Aa][Gg][Ee]%s*:", replacement = "" } -- Remove "Image:" prefix too
    }
}

-- Sanitizes user input by removing or transforming unwanted patterns
function p.sanitizeUserInput(value, patternCategories, customPatterns, options)
    return NormalizationText.sanitizeUserInput(value, patternCategories, customPatterns, options)
end

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

function p.getFieldValue(args, field)
    -- Cache lookup for performance
    local cacheKey = p.generateCacheKey("getFieldValue", field.key or table.concat(field.keys or {}, ","), args)
    local cached = p.withCache(cacheKey, function()
        -- Case-insensitive lookup logic
        if field.keys then
            for _, key in ipairs(field.keys) do
                if args[key] and args[key] ~= "" then
                    return { key = key, value = args[key] }
                end
                local lowerKey = key:lower()
                if args[lowerKey] and args[lowerKey] ~= "" and lowerKey ~= key then
                    return { key = lowerKey, value = args[lowerKey] }
                end
            end
            return { key = nil, value = nil }
        end

        if field.key then
            if args[field.key] and args[field.key] ~= "" then
                return { key = field.key, value = args[field.key] }
            end
            local lowerKey = field.key:lower()
            if args[lowerKey] and args[lowerKey] ~= "" and lowerKey ~= field.key then
                return { key = lowerKey, value = args[lowerKey] }
            end
            return { key = field.key, value = nil }
        end

        return { key = nil, value = nil }
    end)
    return cached.key, cached.value
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
--------------------------------------------------------------------------------

-- Wrapper around CountryData for consistent country formatting
function p.normalizeCountries(value)
    if not value or value == "" then return "" end
    
    -- Create a cache key
    local cacheKey = p.generateCacheKey("normalizeCountries", value)
    
    -- Use the caching wrapper
    return p.withCache(cacheKey, function()
        return CountryData.formatCountries(value)
    end)
end

-- Wrapper around DateNormalization for consistent date formatting
function p.normalizeDates(value)
    if not value or value == "" then return "" end
    
    -- Create a cache key
    local cacheKey = p.generateCacheKey("normalizeDates", value)
    
    -- Use the caching wrapper
    return p.withCache(cacheKey, function()
        return tostring(dateNormalization.formatDate(value))
    end)
end

-- Formats a date range with configurable options
-- @param startDate The start date string
-- @param endDate The end date string (optional)
-- @param options Table of options for customizing the output:
--   - dateLabel: Label to use for the date field (default: ConfigRepository.fieldLabels.date)
--   - rangeDelimiter: String to use between dates (default: " – " [en dash])
--   - outputMode: Output format - "complete" (default), "text", or "html"
--   - showSingleDate: Whether to show the start date when end date is missing (default: true)
--   - consolidateIdenticalDates: Whether to show only one date when start=end (default: true)
-- @return Based on outputMode:
--   - "text": Returns the formatted date text as a string
--   - "html": Returns the complete HTML for the date field as a string
--   - "complete": Returns a table with text, html, and isCompleteHtml properties
function p.formatDateRange(startDate, endDate, options)
    -- Default options
    options = options or {}
    local dateLabel = options.dateLabel or (require('Module:ConfigRepository').fieldLabels.date)
    local rangeDelimiter = options.rangeDelimiter or " – " -- en dash
    local outputMode = options.outputMode or "complete" -- "complete", "text", or "html"
    local showSingleDate = options.showSingleDate ~= false -- true by default
    local consolidateIdenticalDates = options.consolidateIdenticalDates ~= false -- true by default

    -- Global fallback: if only end date is present, treat end as start
    if (not startDate or startDate == "") and endDate and endDate ~= "" then
        startDate, endDate = endDate, nil
    end
    
    -- Handle empty input
    if not startDate or startDate == "" then
        if outputMode == "text" then return "" end
        if outputMode == "html" then return "" end
        return { text = "", html = "", isCompleteHtml = true }
    end
    
    -- Create a cache key
    -- For options, we only include the values that affect the output
    local optionsKey = string.format(
        "%s:%s:%s:%s",
        dateLabel,
        rangeDelimiter,
        outputMode,
        consolidateIdenticalDates and "consolidate" or "noconsolidate"
    )
    
    local cacheKey = p.generateCacheKey("formatDateRange", startDate, endDate or "nil", optionsKey)
    
    -- Use the caching wrapper
    return p.withCache(cacheKey, function()
        -- Normalize dates
        local startFormatted = p.normalizeDates(startDate)
        local endFormatted = endDate and endDate ~= "" and p.normalizeDates(endDate) or nil
        
        -- Format date text based on options
        local dateText
        if endFormatted and endFormatted ~= startFormatted then
            -- Different start and end dates
            dateText = startFormatted .. rangeDelimiter .. endFormatted
        elseif endFormatted and endFormatted == startFormatted and not consolidateIdenticalDates then
            -- Same start and end dates, but option to show both
            dateText = startFormatted .. rangeDelimiter .. endFormatted
        else
            -- Single date or consolidated identical dates
            dateText = startFormatted
        end
        
        -- Format HTML using the field format and label
        local dateHtml = string.format(FIELD_FORMAT, dateLabel, dateText)
        
        -- Return based on requested output mode
        if outputMode == "text" then return dateText end
        if outputMode == "html" then return dateHtml end
        
        -- Default: return both formats
        return {
            text = dateText,
            html = dateHtml,
            isCompleteHtml = true -- For compatibility with existing code
        }
    end)
end

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

-- @deprecated See TemplateStructure.renderTitleBlock and AchievementSystem.renderTitleBlockWithAchievement
function p.renderTitleBlock(args, titleClass, titleText, options)
    options = options or {}
    
    -- If achievement support is needed, use AchievementSystem
    if options.achievementSupport then
        return require('Module:AchievementSystem').renderTitleBlockWithAchievement(
            args, titleClass, titleText, 
            options.achievementClass or "", 
            options.achievementId or "", 
            options.achievementName or ""
        )
    else
        -- Otherwise use the basic title block from TemplateStructure
        return require('Module:TemplateStructure').renderTitleBlock(args, titleClass, titleText)
    end
end


-- Renders a standard fields block based on field definitions and processors
-- Enhanced to support complete HTML blocks, custom field rendering, and tooltips
-- Pre-allocates output table for better performance
function p.renderFieldsBlock(args, fields, processors, propertyMappings)
    processors = processors or {}
    propertyMappings = propertyMappings or {}

    -- filter out hidden fields
    local filteredFields = {}
    for _, f in ipairs(fields) do
        if not f.hidden then
            table.insert(filteredFields, f)
        end
    end
    
    -- 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(filteredFields) do
        local key, value = p.getFieldValue(args, field)
        if value then
            local continue = false
            
            -- Handle declarative lists first
            if field.list then
                -- Use a safe, semicolon-only pattern to avoid breaking up valid names.
                local semicolonOnlyPattern = {{pattern = ";%s*", replacement = ";"}}
                local items = NormalizationText.splitMultiValueString(value, semicolonOnlyPattern)
                if #items > 0 then
                    local options = {}
                    if type(field.list) == 'string' then
                        options.mode = field.list
                    elseif type(field.list) == 'table' then
                        for k, v in pairs(field.list) do
                            options[k] = v
                        end
                    end

                    -- Chain post-processors declaratively
                    local processorsToApply = {}
                    if field.autoWikiLink then
                        table.insert(processorsToApply, "autoWikiLink")
                    end
                    if options.postProcess then
                        table.insert(processorsToApply, options.postProcess)
                    end

                    for _, processorName in ipairs(processorsToApply) do
                        local processorFunc = listPostProcessors[processorName]
                        if processorFunc then
                            local originalHook = options.itemHook
                            options.itemHook = function(item)
                                if type(originalHook) == 'function' then
                                    item = originalHook(item)
                                end
                                
                                local itemContent = type(item) == 'table' and item.content or item
                                local processedContent = processorFunc(itemContent)

                                if type(item) == 'table' then
                                    item.content = processedContent
                                    return item
                                else
                                    return processedContent
                                end
                            end
                        end
                    end
                    
                    local listOutput = ListGeneration.createList(items, options)
                    out[outIndex] = string.format(FIELD_FORMAT, field.label, listOutput)
                    outIndex = outIndex + 1
                    continue = true
                end
            end

            if not continue then
                -- Create sanitization options
                local sanitizeOptions = {
                    preserveWikiLinks = field.autoWikiLink or field.preserveWikiLinks
                }
                
                -- Get property name for this field if available (case-insensitive)
                local propertyName = nil
                for propName, fieldName in pairs(propertyMappings) do
                    if key and (fieldName == key or tostring(fieldName):lower() == tostring(key):lower()) then
                        propertyName = propName
                        break
                    end
                end
                
                -- Get tooltip text if property exists
                local tooltipText = ""
                if propertyName then
                    tooltipText = require('Module:SemanticCategoryHelpers').getPropertyDescription(propertyName) or ""
                end
                
                -- Prepare tooltip attributes if tooltip text exists
                local tooltipClass = ""
                local tooltipAttr = ""
                if tooltipText and tooltipText ~= "" then
                    -- Escape quotes in tooltip text to prevent HTML attribute issues
                    local escapedTooltip = tooltipText:gsub('"', '"')
                    tooltipClass = " has-tooltip"
                    tooltipAttr = string.format('data-tooltip="%s"', escapedTooltip)
                end
                
                -- Apply processor if available for this field
                if key and processors[key] and type(processors[key]) == "function" then
                    local processedValue = processors[key](value, args)
                    
                    -- Preserve wiki links if needed
                    processedValue = linkParser.preserveWikiLinks(
                        value, 
                        processedValue, 
                        sanitizeOptions.preserveWikiLinks
                    )
                    
                    -- 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
                        -- Apply wiki link handling
                        processedValue = linkParser.applyWikiLinkHandling(processedValue, field)
                        
                        -- Standard field rendering with tooltip
                        out[outIndex] = string.format(FIELD_FORMAT_WITH_TOOLTIP, 
                            tooltipClass, tooltipAttr, field.label, processedValue)
                        outIndex = outIndex + 1
                    end
                else
                    -- Standard field rendering without processor
                    -- Apply sanitization with preserveWikiLinks option if needed
                    local finalValue
                    if sanitizeOptions.preserveWikiLinks then
                        finalValue = value
                    else
                        finalValue = p.sanitizeUserInput(value, nil, nil, sanitizeOptions)
                    end
                    
                    -- Apply wiki link handling
                    finalValue = linkParser.applyWikiLinkHandling(finalValue, field)
                    
                    -- Use format with tooltip
                    out[outIndex] = string.format(FIELD_FORMAT_WITH_TOOLTIP, 
                        tooltipClass, tooltipAttr, field.label, finalValue)
                    outIndex = outIndex + 1
                end
            end
        end
    end
    
    return table.concat(out, "\n")
end

-- @deprecated See TemplateStructure.renderDividerBlock
function p.renderDividerBlock(label)
    return require('Module:TemplateStructure').renderDividerBlock(label)
end

-- Extracts semantic value from a field, handling wiki links appropriately
-- @param fieldValue The value to extract semantic data from
-- @param fieldName The name of the field (for error reporting)
-- @param errorContext Optional error context for error handling
-- @return The extracted semantic value or nil if the input is empty
function p.extractSemanticValue(fieldValue, fieldName, errorContext)
    if not fieldValue or fieldValue == "" then
        return nil
    end
    
    -- If the value already has wiki links, extract the name using LinkParser
    local LinkParser = require('Module:LinkParser')
    if LinkParser.processWikiLink(fieldValue, "check") then
        -- Use the standardized error handling helper
        return p.withErrorHandling(
            errorContext,
            "extractFromWikiLink_" .. fieldName,
            LinkParser.extractFromWikiLink,
            fieldValue,  -- fallback to original value on error
            fieldValue
        )
    else
        -- Otherwise, use the plain text value
        return fieldValue
    end
end

-- Standardized error handling helper
-- Executes a function with error protection if an error context is provided
-- @param errorContext The error context for error handling (optional)
-- @param functionName The name of the function being protected (for error reporting)
-- @param operation The function to execute
-- @param fallback The fallback value to return if an error occurs
-- @param ... Additional arguments to pass to the operation function
-- @return The result of the operation or the fallback value if an error occurs
function p.withErrorHandling(errorContext, functionName, operation, fallback, ...)
    -- Capture varargs in a local table to avoid using ... multiple times
    local args = {...}
    
    -- If no error context is provided, execute the operation directly
    if not errorContext or type(errorContext) ~= "table" then
        return operation(unpack(args))
    end
    
    -- Use ErrorHandling module for protected execution
    local ErrorHandling = require('Module:ErrorHandling')
    
    -- Create a wrapper function that passes the arguments to the operation
    local wrapper = function()
        return operation(unpack(args))
    end
    
    -- Execute with error protection
    return ErrorHandling.protect(
        errorContext,
        functionName,
        wrapper,
        fallback
    )
end

-- Renders a standard logo block with sanitized image path
-- @param args The template arguments
-- @param options Table of options for customizing the output:
--   - cssClass: CSS class for the logo container (default: "template-logo")
--   - imageParams: Additional image parameters like size, alignment (default: "")
--   - errorContext: Optional error context for error handling
-- @return The rendered logo block HTML or empty string if no logo
function p.renderLogoBlock(args, options)
    -- Default options
    options = options or {}
    local cssClass = options.cssClass or "template-logo"
    local imageParams = options.imageParams or ""
    
    -- Define the logo rendering operation
    local function renderLogoOperation(args, cssClass, imageParams)
        -- Get logo parameter
        local logo = args["logo"]
        
        -- If no logo or empty, return empty string
        if not logo or logo == "" then
            return ""
        end
        
        -- Sanitize logo path - extract filename and remove prefixes
        logo = p.sanitizeUserInput(logo, "IMAGE_FILES")
        
        -- Format image parameters if provided
        local imgParams = imageParams ~= "" and "|" .. imageParams or ""
        
        -- Render the logo image
        return string.format(
            '|-\n| colspan="2" class="%s" | [[Image:%s%s]]', 
            cssClass, logo, imgParams
        )
    end
    
    -- Use the standardized error handling helper
    return p.withErrorHandling(
        options.errorContext,
        "renderLogoBlock",
        renderLogoOperation,
        "", -- Empty string fallback
        args, cssClass, imageParams
    )
end

--------------------------------------------------------------------------------
-- Multi-Value String Processing
--------------------------------------------------------------------------------

-- 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 = ";"}
}

-- Semicolon-only pattern for backward compatibility with splitSemicolonValues
-- Exposed as a module-level constant for use by other modules
p.SEMICOLON_PATTERN = {{pattern = ";%s*", replacement = ";"}}

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

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

--[[
    Renders a table of fields with labels and values using TemplateStructure.
    
    Parameters:
      fields       - Array of field objects, each with:
                     - label: The field label to display
                     - value: The field value to display
                     - class: Optional CSS class for the row
      options      - Optional configuration:
                     - tableClass: CSS class for the table (default: "template-field-table")
                     - tableAttrs: Additional table attributes
                     - fieldFormat: Format string for field rows (default: uses FIELD_FORMAT)
                     - errorContext: Optional error context for error handling
    
    Returns:
      Wikitext markup for the field table
]]
function p.renderFieldTable(fields, options)
    -- Early return for empty fields
    if not fields or #fields == 0 then
        return ""
    end
    
    options = options or {}
    
    -- Define the field table rendering operation
    local function renderFieldTableOperation(fields, options)
        local TemplateStructure = require('Module:TemplateStructure')
        
        -- Create a config for the render function with optimized defaults
        local config = {
            tableClass = options.tableClass or "template-field-table",
            tableAttrs = options.tableAttrs or 'cellpadding="2"',
            blocks = {}
        }
        
        -- Pre-allocate blocks array based on field count
        local blocks = {}
        
        -- Use the module's FIELD_FORMAT or a custom format if provided
        local fieldFormat = options.fieldFormat or FIELD_FORMAT
        
        -- Create a block function for each field with direct index assignment
        for i = 1, #fields do
            local field = fields[i]
            blocks[i] = function()
                -- Combine the field's class with the template-data-row class
                local fieldClass = field.class and field.class or ""
                
                -- Create the row with proper classes
                local row = '|- class="template-data-row' .. (fieldClass ~= "" and ' ' .. fieldClass or '') .. '"'
                
                -- Format the cells using the field format but replace the row start
                local cellsFormat = fieldFormat:gsub("^[^|]+", "")
                
                -- Return the complete row
                return row .. string.format(cellsFormat, field.label, field.value)
            end
        end
        
        -- Assign blocks to config
        config.blocks = blocks
        
        -- Use TemplateStructure's render function
        return TemplateStructure.render({}, config, options.errorContext)
    end
    
    -- Use the standardized error handling helper
    return p.withErrorHandling(
        options.errorContext,
        "renderFieldTable",
        renderFieldTableOperation,
        "", -- Empty string fallback
        fields, options
    )
end

return p