Module:TemplateHelpers: Difference between revisions

// via Wikitext Extension for VSCode
// via Wikitext Extension for VSCode
 
(105 intermediate revisions by the same user not shown)
Line 1: Line 1:
-- Module:TemplateHelpers
--[[
-- Common helper functions for template modules promoting code reuse and consistency.
* Name: TemplateHelpers
-- Provides utilities for string processing, field handling, normalization, and block rendering.
* 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 7: 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
 
-- 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
end


-- Splits a semicolon-delimited string into a table of trimmed non-empty values
-- Get current page ID with caching
-- This is now a wrapper around splitMultiValueString for backward compatibility
-- @return number|nil The current page ID or nil if not available
function p.splitSemicolonValues(value)
function p.getCurrentPageId()
     -- For backward compatibility, maintain the same behavior
     -- Use mw.title API to get the current page title object
     return p.splitMultiValueString(value, {
     local title = mw.title.getCurrentTitle()
        {pattern = ";%s*", replacement = ";"}
    -- Return the ID property or nil if not available
     })
     return title and title.id or nil
end
end


-- Joins a table of values with the specified delimiter
-- Module-level cache for wiki link processing
function p.joinValues(values, delimiter)
local wikiLinkCache = {}
     delimiter = delimiter or "; "
 
    if not values or #values == 0 then return "" end
-- @deprecated See LinkParser.processWikiLink
     return table.concat(values, delimiter)
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 40: 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 78: 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)
        -- Pre-allocate listItems table based on number of websites
     end)
        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
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 125: 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
-- Pre-allocates output table for better performance
-- Pre-allocates output table for better performance
function p.renderFieldsBlock(args, fields, processors)
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
     -- Pre-allocate output table - estimate based on number of fields
Line 164: Line 461:
     local outIndex = 1
     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
                }
                  
                  
                 -- Handle the case where a processor returns complete HTML
                 -- Get property name for this field if available (case-insensitive)
                if type(processedValue) == "table" and processedValue.isCompleteHtml then
                local propertyName = nil
                    -- Add the complete HTML as is
                for propName, fieldName in pairs(propertyMappings) do
                    out[outIndex] = processedValue.html
                    if key and (fieldName == key or tostring(fieldName):lower() == tostring(key):lower()) then
                    outIndex = outIndex + 1
                        propertyName = propName
                elseif processedValue ~= nil and processedValue ~= false then
                        break
                     -- Standard field rendering
                    end
                     out[outIndex] = string.format("|-\n| '''%s''':\n| %s", field.label, processedValue)
                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
                     outIndex = outIndex + 1
                 end
                 end
            else
                -- Standard field rendering without processor
                out[outIndex] = string.format("|-\n| '''%s''':\n| %s", field.label, value)
                outIndex = outIndex + 1
             end
             end
         end
         end
Line 192: 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
-- Pre-allocates the formatted table for better performance
function p.withErrorHandling(errorContext, functionName, operation, fallback, ...)
function p.buildCategories(categories)
     -- Capture varargs in a local table to avoid using ... multiple times
     if not categories or #categories == 0 then return "" end
    local args = {...}
      
      
     -- Pre-allocate formatted table based on input size
     -- If no error context is provided, execute the operation directly
     local formatted = {}
     if not errorContext or type(errorContext) ~= "table" then
     local index = 1
        return operation(unpack(args))
     end
      
      
     for _, cat in ipairs(categories) do
     -- Use ErrorHandling module for protected execution
        -- Check if the category already has the [[ ]] wrapper
     local ErrorHandling = require('Module:ErrorHandling')
        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
     -- Create a wrapper function that passes the arguments to the operation
        for _, group in ipairs(mapping) do
    local wrapper = function()
            if group.canonical == canonical and group.category then
        return operation(unpack(args))
                table.insert(categories, group.category)
                break
            end
        end
     end
     end
      
      
     return categories
    -- Execute with error protection
     return ErrorHandling.protect(
        errorContext,
        functionName,
        wrapper,
        fallback
    )
end
end


--------------------------------------------------------------------------------
-- Renders a standard logo block with sanitized image path
-- Semantic Property Helpers
-- @param args The template arguments
--------------------------------------------------------------------------------
-- @param options Table of options for customizing the output:
 
--   - cssClass: CSS class for the logo container (default: "template-logo")
-- Generic function to add multi-value semantic properties
--   - imageParams: Additional image parameters like size, alignment (default: "")
-- This is a generalized helper that can be used for any multi-value property
--   - errorContext: Optional error context for error handling
function p.addMultiValueSemanticProperties(value, propertyName, processor, semanticOutput, options)
-- @return The rendered logo block HTML or empty string if no logo
     if not value or value == "" then return semanticOutput end
function p.renderLogoBlock(args, options)
   
     -- Default options
     options = options or {}
     options = options or {}
     local processedItems = {}
     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
   
    -- 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
         end
          
          
         -- Only add if valid and not already processed
         -- Sanitize logo path - extract filename and remove prefixes
        if processedItem and processedItem ~= "" and not processedItems[processedItem] then
         logo = p.sanitizeUserInput(logo, "IMAGE_FILES")
            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
         -- Format image parameters if provided
         if processedItem and processedItem ~= "" then
         local imgParams = imageParams ~= "" and "|" .. imageParams or ""
            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 CountryData = require('Module:CountryData')
   
    -- Create a processor function that uses CountryData for normalization
    local function countryProcessor(country)
        local normalized = CountryData.normalizeCountryName(country)
        -- Skip unrecognized countries
        if normalized == "(Unrecognized)" then
            return nil
        end
        return normalized
    end
   
    return p.addMultiValueSemanticProperties(
        countryValue,
        "Has country",
        countryProcessor,
        semanticOutput
    )
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)
    -- Use CountryData for region information
    local CountryData = require('Module:CountryData')
   
    -- First, replace "and" with semicolons to standardize the delimiter
    local standardizedInput = regionValue:gsub("%s+and%s+", ";")
   
    -- Define a processor that works directly with the data in CountryData
    local function regionProcessor(region)
        -- Skip unrecognized regions
        if region == "(Unrecognized)" then
            return nil
        end
          
          
         -- Trim the region and return it - CountryData will handle normalization
         -- Render the logo image
        local trimmed = region:match("^%s*(.-)%s*$")
        return string.format(
         return trimmed
            '|-\n| colspan="2" class="%s" | [[Image:%s%s]]',
            cssClass, logo, imgParams
         )
     end
     end
      
      
     return p.addMultiValueSemanticProperties(
    -- Use the standardized error handling helper
         standardizedInput,
     return p.withErrorHandling(
         "Has ICANN region",
         options.errorContext,
         regionProcessor,
         "renderLogoBlock",
         semanticOutput
         renderLogoOperation,
         "", -- Empty string fallback
        args, cssClass, imageParams
     )
     )
end
end


-- Adds semantic properties for multiple languages
--------------------------------------------------------------------------------
-- 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.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
-- Default delimiters for splitMultiValueString
-- This standardizes how additional properties are handled across templates
-- Defined once as an upvalue to avoid recreating on each function call
function p.processAdditionalProperties(args, semanticConfig, semanticOutput, skipProperties)
local defaultDelimiters = {
    if not semanticConfig or not semanticConfig.additionalProperties then
     {pattern = "%s+and%s+", replacement = ";"},
        return semanticOutput
     {pattern = ";%s*", replacement = ";"}
    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
-- Semicolon-only pattern for backward compatibility with splitSemicolonValues
function p.isMultiValueField(value)
-- Exposed as a module-level constant for use by other modules
    if not value or value == "" then return false end
p.SEMICOLON_PATTERN = {{pattern = ";%s*", replacement = ";"}}
   
    -- Check for common multi-value delimiters
    return value:match(";") or value:match("%s+and%s+")
end


-- Generates semantic properties based on configuration
-- Generic function to split multi-value strings with various delimiters
-- @param args - Template parameters
function p.splitMultiValueString(value, delimiters)
-- @param semanticConfig - Config with properties, transforms, additionalProperties
     return NormalizationText.splitMultiValueString(value, delimiters)
-- @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
end


Line 560: Line 736:
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------


-- Creates a standardized configuration structure for template modules
--[[
function p.createStandardConfig(config)
    Renders a table of fields with labels and values using TemplateStructure.
    config = config or {}
      
      
     -- Initialize with defaults
    Parameters:
    local standardConfig = {
      fields      - Array of field objects, each with:
        meta = config.meta or {
                    - label: The field label to display
             description = "Template module configuration"
                    - value: The field value to display
         },
                    - class: Optional CSS class for the row
         mappings = config.mappings or {},
      options      - Optional configuration:
         fields = config.fields or {},
                    - tableClass: CSS class for the table (default: "template-field-table")
         semantics = config.semantics or {
                    - tableAttrs: Additional table attributes
            properties = {},
                    - fieldFormat: Format string for field rows (default: uses FIELD_FORMAT)
             transforms = {},
                    - errorContext: Optional error context for error handling
             additionalProperties = {}
   
        },
    Returns:
        constants = config.constants or {},
      Wikitext markup for the field table
         patterns = config.patterns or {}
]]
     }
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
      
      
     return standardConfig
    -- Use the standardized error handling helper
     return p.withErrorHandling(
        options.errorContext,
        "renderFieldTable",
        renderFieldTableOperation,
        "", -- Empty string fallback
        fields, options
    )
end
end


return p
return p