Module:TemplateHelpers: Difference between revisions

// via Wikitext Extension for VSCode
// via Wikitext Extension for VSCode
 
(108 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
 
-- 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 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)
        -- 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 124: 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 not options.achievementSupport then
        return string.format('|-\n! colspan="2" class="%s" | %s', titleClass, titleText)
    end
      
      
     -- With achievement support
     -- If achievement support is needed, use AchievementSystem
     local achievementClass = options.achievementClass or ""
     if options.achievementSupport then
    local achievementId = options.achievementId or ""
        return require('Module:AchievementSystem').renderTitleBlockWithAchievement(
    local achievementName = options.achievementName or ""
            args, titleClass, titleText,
   
            options.achievementClass or "",
    -- Only add achievement attributes if they exist
            options.achievementId or "",
    if achievementClass ~= "" and achievementId ~= "" then
            options.achievementName or ""
        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 163: 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 191: 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)
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
     else
         return '|-\n| colspan="2" class="template-divider" |'
        -- 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
     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
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


--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
-- Category Utilities
-- Multi-Value String Processing
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------


Line 211: Line 722:
     {pattern = ";%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
-- Generic function to split multi-value strings with various delimiters
-- Returns an array of individual values
function p.splitMultiValueString(value, delimiters)
function p.splitMultiValueString(value, delimiters)
    if not value or value == "" then return {} end
     return NormalizationText.splitMultiValueString(value, delimiters)
   
    -- Use provided delimiters or default ones
    delimiters = delimiters or defaultDelimiters
   
    -- Standardize all delimiters to semicolons
    local standardizedInput = value
    for _, delimiter in ipairs(delimiters) do
        standardizedInput = standardizedInput:gsub(delimiter.pattern, delimiter.replacement)
    end
   
    -- Pre-allocate table based on delimiter count
    -- Count semicolons to estimate the number of items
    local count = 0
    for _ in standardizedInput:gmatch(";") do
        count = count + 1
    end
   
    -- Pre-allocate table with estimated size (count+1 for the last item)
    local items = {}
   
    -- Split by semicolons and return the array
    local index = 1
    for item in standardizedInput:gmatch("[^;]+") do
        local trimmed = item:match("^%s*(.-)%s*$")
        if trimmed and trimmed ~= "" then
            items[index] = trimmed
            index = index + 1
        end
    end
   
    return items
end
 
-- Splits a region string that may contain "and" conjunctions
-- Returns an array of individual region names
-- This is now a wrapper around splitMultiValueString for backward compatibility
function p.splitRegionCategories(regionValue)
     return p.splitMultiValueString(regionValue)
end
 
-- Builds a category string from a table of category names
-- Pre-allocates the formatted table for better performance
function p.buildCategories(categories)
    if not categories or #categories == 0 then return "" end
   
    -- Pre-allocate formatted table based on input size
    local formatted = {}
    local index = 1
   
    for _, cat in ipairs(categories) do
        -- Check if the category already has the [[ ]] wrapper
        if not string.match(cat, "^%[%[Category:") then
            formatted[index] = string.format("[[Category:%s]]", cat)
        else
            formatted[index] = cat
        end
        index = index + 1
    end
    return table.concat(formatted, "\n")
end
 
-- Adds categories based on a canonical mapping
function p.addMappingCategories(value, mapping)
    if not value or value == "" or not mapping then return {} end
    local categories = {}
    local canonical = select(1, CanonicalForms.normalize(value, mapping))
   
    if canonical then
        for _, group in ipairs(mapping) do
            if group.canonical == canonical and group.category then
                table.insert(categories, group.category)
                break
            end
        end
    end
   
    return categories
end
end


--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
-- Semantic Property Helpers
-- Configuration Standardization
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------


-- Generic function to add multi-value semantic properties
--[[
-- This is a generalized helper that can be used for any multi-value property
    Renders a table of fields with labels and values using TemplateStructure.
function p.addMultiValueSemanticProperties(value, propertyName, processor, semanticOutput, options)
    if not value or value == "" then return semanticOutput end
      
      
     options = options or {}
     Parameters:
    local processedItems = {}
      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
      
      
     -- Get the values to process
     Returns:
    local items
      Wikitext markup for the field table
    if options.valueGetter and type(options.valueGetter) == "function" then
]]
        -- Use custom value getter if provided
function p.renderFieldTable(fields, options)
        items = options.valueGetter(value)
    -- Early return for empty fields
    else
    if not fields or #fields == 0 then
         -- Default to splitting the string
         return ""
        items = p.splitMultiValueString(value)
     end
     end
   
    -- For non-SMW case, collect property HTML fragments in a table for efficient concatenation
    local propertyHtml = {}
   
    -- Process each item and add as a semantic property
    for _, item in ipairs(items) do
        -- Apply processor if provided
        local processedItem = item
        if processor and type(processor) == "function" then
            processedItem = processor(item)
        end
       
        -- Only add if valid and not already processed
        if processedItem and processedItem ~= "" and not processedItems[processedItem] then
            processedItems[processedItem] = true
           
            -- Add as semantic property
            if mw.smw then
                mw.smw.set({[propertyName] = processedItem})
            else
                -- Collect HTML fragments instead of concatenating strings
                table.insert(propertyHtml, '<div style="display:none;">')
                table.insert(propertyHtml, '  {{#set: ' .. propertyName .. '=' .. processedItem .. ' }}')
                table.insert(propertyHtml, '</div>')
            end
        end
    end
   
    -- For non-SMW case, concatenate all property HTML fragments at once
    if not mw.smw and #propertyHtml > 0 then
        semanticOutput = semanticOutput .. "\n" .. table.concat(propertyHtml, "\n")
    end
   
    return semanticOutput
end
-- Generic function to add multi-value categories
-- This is a generalized helper that can be used for any multi-value category field
function p.addMultiValueCategories(value, processor, categories, options)
    if not value or value == "" then return categories end
      
      
     options = options or {}
     options = options or {}
      
      
     -- Get the values to process
     -- Define the field table rendering operation
     local items
     local function renderFieldTableOperation(fields, options)
    if options.valueGetter and type(options.valueGetter) == "function" then
         local TemplateStructure = require('Module:TemplateStructure')
        -- 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
         -- Create a config for the render function with optimized defaults
         if processedItem and processedItem ~= "" then
         local config = {
             categories[currentSize + 1] = processedItem
             tableClass = options.tableClass or "template-field-table",
             currentSize = currentSize + 1
             tableAttrs = options.tableAttrs or 'cellpadding="2"',
        end
             blocks = {}
    end
   
    return categories
end
 
-- Adds semantic properties for multiple countries
-- This is a wrapper around addMultiValueSemanticProperties for backward compatibility
-- For new code, prefer using addMultiValueSemanticProperties directly with appropriate options
function p.addMultiCountrySemanticProperties(countryValue, semanticOutput)
    local MultiCountryDisplay = require('Module:MultiCountryDisplay')
   
    return p.addMultiValueSemanticProperties(
        countryValue,
        "Has country",
        nil, -- No processor needed as we use a custom value getter
        semanticOutput,
        {
             valueGetter = function(value)
                return MultiCountryDisplay.getCountriesForCategories(value)
            end
         }
         }
    )
       
end
        -- Pre-allocate blocks array based on field count
 
        local blocks = {}
-- Adds semantic properties for multiple regions
       
-- This is a wrapper around addMultiValueSemanticProperties for backward compatibility
        -- Use the module's FIELD_FORMAT or a custom format if provided
-- For new code, prefer using addMultiValueSemanticProperties directly with appropriate options
        local fieldFormat = options.fieldFormat or FIELD_FORMAT
function p.addMultiRegionSemanticProperties(regionValue, semanticOutput)
          
    local RegionalMappingICANN = require('Module:RegionalMappingICANN')
         -- Create a block function for each field with direct index assignment
   
        for i = 1, #fields do
    -- First, replace "and" with semicolons to standardize the delimiter
            local field = fields[i]
    local standardizedInput = regionValue:gsub("%s+and%s+", ";")
            blocks[i] = function()
   
                -- Combine the field's class with the template-data-row class
    return p.addMultiValueSemanticProperties(
                local fieldClass = field.class and field.class or ""
         standardizedInput,
               
         "Has region",
                -- Create the row with proper classes
        RegionalMappingICANN.normalizeRegion,
                local row = '|- class="template-data-row' .. (fieldClass ~= "" and ' ' .. fieldClass or '') .. '"'
        semanticOutput
               
    )
                -- Format the cells using the field format but replace the row start
end
                local cellsFormat = fieldFormat:gsub("^[^|]+", "")
 
               
-- Adds semantic properties for multiple languages
                -- Return the complete row
-- This is a wrapper around addMultiValueSemanticProperties for backward compatibility
                return row .. string.format(cellsFormat, field.label, field.value)
-- For new code, prefer using addMultiValueSemanticProperties directly with appropriate options
function p.addMultiLanguageSemanticProperties(languagesValue, semanticOutput)
    local LanguageNormalization = require('Module:LanguageNormalization')
   
    return p.addMultiValueSemanticProperties(
        languagesValue,
        "Speaks language",
        LanguageNormalization.normalize,
        semanticOutput
    )
end
 
-- Helper function to process additional properties with multi-value support
-- This standardizes how additional properties are handled across templates
function p.processAdditionalProperties(args, semanticConfig, semanticOutput, skipProperties)
    if not semanticConfig or not semanticConfig.additionalProperties then
        return semanticOutput
    end
   
    skipProperties = skipProperties or {}
   
    -- For non-SMW case, collect property HTML fragments in a table for efficient concatenation
    local propertyHtml = {}
   
    for property, sourceFields in pairs(semanticConfig.additionalProperties) do
        -- Skip properties that are handled separately
        if not skipProperties[property] then
            for _, fieldName in ipairs(sourceFields) do
                if args[fieldName] and args[fieldName] ~= "" then
                    local value = args[fieldName]
                   
                    -- Apply transformation if available
                    if semanticConfig.transforms and semanticConfig.transforms[property] then
                        value = semanticConfig.transforms[property](value)
                    end
                   
                    -- Check if this is a multi-value field that needs to be split
                    if p.isMultiValueField(value) then
                        -- Use the generic multi-value function
                        semanticOutput = p.addMultiValueSemanticProperties(
                            value,
                            property,
                            semanticConfig.transforms and semanticConfig.transforms[property],
                            semanticOutput
                        )
                    else
                        -- Single value property
                        if mw.smw then
                            mw.smw.set({[property] = value})
                        else
                            -- Collect HTML fragments instead of concatenating strings
                            table.insert(propertyHtml, '<div style="display:none;">')
                            table.insert(propertyHtml, '  {{#set: ' .. property .. '=' .. value .. ' }}')
                            table.insert(propertyHtml, '</div>')
                        end
                    end
                end
             end
             end
         end
         end
       
        -- Assign blocks to config
        config.blocks = blocks
       
        -- Use TemplateStructure's render function
        return TemplateStructure.render({}, config, options.errorContext)
     end
     end
      
      
     -- For non-SMW case, concatenate all property HTML fragments at once
     -- Use the standardized error handling helper
     if not mw.smw and #propertyHtml > 0 then
     return p.withErrorHandling(
         semanticOutput = semanticOutput .. "\n" .. table.concat(propertyHtml, "\n")
         options.errorContext,
    end
        "renderFieldTable",
   
        renderFieldTableOperation,
    return semanticOutput
        "", -- Empty string fallback
end
        fields, options
 
-- Helper function to check if a field contains multiple values
function p.isMultiValueField(value)
    if not value or value == "" then return false end
   
    -- Check for common multi-value delimiters
    return value:match(";") or value:match("%s+and%s+")
end
 
-- Generates semantic properties based on configuration
-- @param args - Template parameters table
-- @param semanticConfig - Configuration table with properties, transforms, and additionalProperties
-- @param options - Options table with the following possible keys:
--  - transform: Table of transformation functions for properties
--  - skipProperties: Table of properties to skip in processAdditionalProperties
-- @return Wikitext string containing semantic annotations
function p.generateSemanticProperties(args, semanticConfig, options)
    if not args or not semanticConfig then return "" end
   
    local SemanticAnnotations = require('Module:SemanticAnnotations')
    options = options or {}
   
    -- Set options
    local semanticOptions = {
        transform = semanticConfig.transforms or options.transform
    }
   
    -- Set basic properties
    local semanticOutput = SemanticAnnotations.setSemanticProperties(
        args,
        semanticConfig.properties,
        semanticOptions
     )
     )
   
    -- Process additional properties with multi-value support
    local skipProperties = options.skipProperties or {}
    semanticOutput = p.processAdditionalProperties(args, semanticConfig, semanticOutput, skipProperties)
   
    return semanticOutput
end
--------------------------------------------------------------------------------
-- Configuration Standardization
--------------------------------------------------------------------------------
-- Creates a standardized configuration structure for template modules
-- 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