Module:TemplateHelpers: Difference between revisions

// via Wikitext Extension for VSCode
// via Wikitext Extension for VSCode
 
(90 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
-- REVIEW: Still uses wrappers for Module:SemanticCategoryHelpers
* Description: Common helper functions for template modules promoting code reuse and consistency across string processing, field handling, normalization, and block rendering
-- REVIEW: Bring description of helpers up here
* 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 = {}
-- Dependencies
local linkParser = require('Module:LinkParser')
local CountryData = require('Module:CountryData')
local dateNormalization = require('Module:NormalizationDate')
local CanonicalForms = require('Module:CanonicalForms')
local NormalizationText = require('Module:NormalizationText')
local ListGeneration = require('Module:ListGeneration')
--------------------------------------------------------------------------------
-- Caching Mechanism
--------------------------------------------------------------------------------
-- Module-level unified cache
local functionCache = {}
-- Helper for generating cache keys from multiple arguments
-- @param prefix String prefix for the cache key (usually the function name)
-- @param ... Any number of arguments to include in the cache key
-- @return A string cache key
function p.generateCacheKey(prefix, ...)
    local args = {...}
    local parts = {prefix}
   
    for i, arg in ipairs(args) do
        if type(arg) == "table" then
            -- For tables, we can't reliably generate a cache key
            -- So we just use a placeholder with the table's memory address
            parts[i+1] = "table:" .. tostring(arg)
        elseif type(arg) == "nil" then
            parts[i+1] = "nil"
        else
            parts[i+1] = tostring(arg)
        end
    end
   
    return table.concat(parts, ":")
end
-- Generic caching wrapper
-- @param cacheKey The cache key to use
-- @param operation A function that returns the value to cache
-- @return The cached result or the result of executing the operation
function p.withCache(cacheKey, operation)
    -- Check if result is already cached
    if functionCache[cacheKey] ~= nil then
        return functionCache[cacheKey]
    end
   
    -- Execute operation and cache result
    local result = operation()
    functionCache[cacheKey] = result
    return result
end


--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
Line 12: Line 67:


-- This constant defines how the "label" string are rendered in HTML; we further manipulate them with the "template-label-style" CSS class
-- This constant defines how the "label" string are rendered in HTML; we further manipulate them with the "template-label-style" CSS class
local FIELD_FORMAT = "|-\n| <span class=\"template-label-style\">%s</span>\n| %s"
-- 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"


-- Expose the field format for use by other modules
-- 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 = FIELD_FORMAT
p.FIELD_FORMAT_WITH_TOOLTIP = FIELD_FORMAT_WITH_TOOLTIP


-- Dependencies
-- Registry for declarative list post-processors
local linkParser = require('Module:LinkParser')
local listPostProcessors = {
local CountryData = require('Module:CountryData')
    language = function(itemContent)
local dateNormalization = require('Module:NormalizationDate')
        local NormalizationLanguage = require('Module:NormalizationLanguage')
local CanonicalForms = require('Module:CanonicalForms')
        local langName = NormalizationLanguage.normalize(itemContent)
local SemanticCategoryHelpers = require('Module:SemanticCategoryHelpers')
        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
}


--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
Line 36: Line 132:
     -- Process all keys
     -- Process all keys
     for key, value in pairs(args) do
     for key, value in pairs(args) do
         -- Skip empty numeric parameters (created by leading pipes after template name)
         -- Determine if this key is numeric (number type or numeric-string)
         if type(key) == "number" and (value == nil or value == "") then
         local isNumericKey = type(key) == "number" or (type(key) == "string" and tonumber(key) ~= nil)
            -- Do nothing with empty numeric parameters
        -- Skip all numeric parameters entirely
         else
         if not isNumericKey then
             -- For all other parameters, add lowercase version
             -- Preserve original key and lowercase variant
            normalized[key] = value
             if type(key) == "string" then
             if type(key) == "string" then
                 normalized[key:lower()] = value
                 normalized[key:lower()] = value
             end
             end
            -- Preserve original key as well
            normalized[key] = value
         end
         end
     end
     end
Line 54: Line 149:
-- Trims leading and trailing whitespace from a string
-- Trims leading and trailing whitespace from a string
function p.trim(s)
function p.trim(s)
     return (s:gsub("^%s+", ""):gsub("%s+$", ""))
     return NormalizationText.trim(s)
end
end


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


-- Joins a table of values with the specified delimiter
-- Removes duplicate values from an array while preserving order
function p.joinValues(values, delimiter)
-- @param t table The array to deduplicate
     delimiter = delimiter or "; "
-- @return table A new array with duplicates removed
     if not values or #values == 0 then return "" end
function p.removeDuplicates(t)
     return table.concat(values, delimiter)
    -- Type checking
    if type(t) ~= 'table' then
        return {}
    end
   
    -- Helper function to check if a value is NaN
    local function isNan(v)
        return type(v) == 'number' and tostring(v) == '-nan'
    end
   
    -- Pre-allocate result table (maximum possible size is #t)
     local ret, exists = {}, {}
   
    -- Process each value, preserving order
     for i, v in ipairs(t) do
        if isNan(v) then
            -- NaNs can't be table keys, and they are also unique
            ret[#ret + 1] = v
        else
            if not exists[v] then
                ret[#ret + 1] = v
                exists[v] = true
            end
        end
    end
   
    return ret
end
 
-- Get current page ID with caching
-- @return number|nil The current page ID or nil if not available
function p.getCurrentPageId()
    -- Use mw.title API to get the current page title object
    local title = mw.title.getCurrentTitle()
    -- Return the ID property or nil if not available
    return title and title.id or nil
end
 
-- Module-level cache for wiki link processing
local wikiLinkCache = {}
 
-- @deprecated See LinkParser.processWikiLink
function p.processWikiLink(value, mode)
     return require('Module:LinkParser').processWikiLink(value, mode)
end
end


-- Module-level pattern categories for sanitizing user input
-- Module-level pattern categories for sanitizing user input
Line 77: Line 212:
p.SANITIZE_PATTERNS = {
p.SANITIZE_PATTERNS = {
     WIKI_LINKS = {
     WIKI_LINKS = {
         { pattern = "%[%[([^|%]]+)%]%]", replacement = "%1" },          -- [[Link]] -> Link
         {  
         { pattern = "%[%[([^|%]]+)|([^%]]+)%]%]", replacement = "%2" }  -- [[Link|Text]] -> Text
            pattern = "%[%[([^|%]]+)%]%]",  
            replacement = function(match)
                return linkParser.processWikiLink("[[" .. match .. "]]", "strip")
            end
        },
         {  
            pattern = "%[%[([^|%]]+)|([^%]]+)%]%]",  
            replacement = function(match1, match2)
                return linkParser.processWikiLink("[[" .. match1 .. "|" .. match2 .. "]]", "strip")
            end
        }
     },
     },
     SINGLE_BRACES = {
     SINGLE_BRACES = {
         { pattern = "{([^{}]+)}", replacement = "%1" }                   -- {text} -> text
         { pattern = "{([^{}]+)}", replacement = "%1" } -- {text} -> text
     },
     },
     HTML_BASIC = {
     HTML_BASIC = {
         { pattern = "</?[bi]>", replacement = "" },                     -- Remove <b>, </b>, <i>, </i>
         { pattern = "</?[bi]>", replacement = "" },     -- Remove <b>, </b>, <i>, </i>
         { pattern = "</?span[^>]*>", replacement = "" }                 -- Remove <span...>, </span>
         { 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
-- Sanitizes user input by removing or transforming unwanted patterns
-- @param value The input string to sanitize
function p.sanitizeUserInput(value, patternCategories, customPatterns, options)
-- @param patternCategories Optional table or string of pattern categories to apply
     return NormalizationText.sanitizeUserInput(value, patternCategories, customPatterns, options)
-- @param customPatterns Optional table of additional patterns to apply
-- @return The sanitized string
function p.sanitizeUserInput(value, patternCategories, customPatterns)
     -- Fast path for nil/empty values
    if not value or value == "" then return "" end
   
    -- Collect patterns to apply
    local patternsToApply = {}
    local patternCount = 0
   
    -- Process requested pattern categories
    if patternCategories then
        -- Handle single category string
        if type(patternCategories) == "string" then
            if p.SANITIZE_PATTERNS[patternCategories] then
                for _, pattern in ipairs(p.SANITIZE_PATTERNS[patternCategories]) do
                    patternCount = patternCount + 1
                    patternsToApply[patternCount] = pattern
                end
            end
        -- Handle table of categories
        elseif type(patternCategories) == "table" then
            for _, category in ipairs(patternCategories) do
                if p.SANITIZE_PATTERNS[category] then
                    for _, pattern in ipairs(p.SANITIZE_PATTERNS[category]) do
                        patternCount = patternCount + 1
                        patternsToApply[patternCount] = pattern
                    end
                end
            end
        end
    else
        -- Default to WIKI_LINKS and SINGLE_BRACES if no categories specified
        for _, pattern in ipairs(p.SANITIZE_PATTERNS.WIKI_LINKS) do
            patternCount = patternCount + 1
            patternsToApply[patternCount] = pattern
        end
        for _, pattern in ipairs(p.SANITIZE_PATTERNS.SINGLE_BRACES) do
            patternCount = patternCount + 1
            patternsToApply[patternCount] = pattern
        end
    end
   
    -- Add any custom patterns
    if customPatterns and type(customPatterns) == "table" then
        for _, pattern in ipairs(customPatterns) do
            patternCount = patternCount + 1
            patternsToApply[patternCount] = pattern
        end
    end
   
    -- Fast path if no patterns to apply
    if patternCount == 0 then
        return value
    end
   
    -- Apply each pattern sequentially
    local result = value
    for i = 1, patternCount do
        local patternInfo = patternsToApply[i]
        result = result:gsub(patternInfo.pattern, patternInfo.replacement)
    end
   
    return result
end
end


Line 162: Line 252:
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------


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


Line 220: Line 311:
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------


-- Formats website URLs as an HTML unordered list of links, ensuring consistent emoji display
-- Wrapper around CountryData for consistent country formatting
-- Uses splitMultiValueString for more flexible delimiter handling
function p.normalizeCountries(value)
function p.normalizeWebsites(value)
     if not value or value == "" then return "" end
     if not value or value == "" then return "" end
      
      
     -- Get websites as a table, handling both single and multiple cases
     -- Create a cache key
     local websites
     local cacheKey = p.generateCacheKey("normalizeCountries", value)
      
      
     -- Quick check for single website (no delimiters)
     -- Use the caching wrapper
     if not value:match(";") and not value:match("%s+and%s+") then
     return p.withCache(cacheKey, function()
         -- Single website case - create a single-item table
         return CountryData.formatCountries(value)
        websites = {value}
     end)
     else
end
        -- Multiple websites case
 
        websites = p.splitMultiValueString(value)
-- Wrapper around DateNormalization for consistent date formatting
     end
function p.normalizeDates(value)
     if not value or value == "" then return "" end
      
      
     -- Handle all websites consistently using the list format
     -- Create a cache key
     if #websites > 0 then
     local cacheKey = p.generateCacheKey("normalizeDates", value)
        -- Pre-allocate listItems table based on number of websites
        local listItems = {}
        local index = 1
       
        for _, site in ipairs(websites) do
            local formattedLink = string.format("[%s %s]", site, linkParser.strip(site))
            listItems[index] = string.format("<li>%s</li>", formattedLink)
            index = index + 1
        end
        return string.format("<ul class=\"template-list template-list-website\">%s</ul>", table.concat(listItems, ""))
    end
      
      
     return ""
    -- Use the caching wrapper
     return p.withCache(cacheKey, function()
        return tostring(dateNormalization.formatDate(value))
    end)
end
end


-- Wrapper around CountryData for consistent country formatting
-- Formats a date range with configurable options
function p.normalizeCountries(value)
-- @param startDate The start date string
     if not value or value == "" then return "" end
-- @param endDate The end date string (optional)
     return CountryData.formatCountries(value)
-- @param options Table of options for customizing the output:
end
--  - 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


-- Wrapper around DateNormalization for consistent date formatting
    -- Global fallback: if only end date is present, treat end as start
function p.normalizeDates(value)
    if (not startDate or startDate == "") and endDate and endDate ~= "" then
     if not value or value == "" then return "" end
        startDate, endDate = endDate, nil
     return tostring(dateNormalization.formatDate(value))
    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 270: 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 309: 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 with processor
                    end
                     out[outIndex] = string.format(FIELD_FORMAT, 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(FIELD_FORMAT, field.label, value)
                outIndex = outIndex + 1
             end
             end
         end
         end
Line 337: 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
end
end


 
-- Standardized error handling helper
--------------------------------------------------------------------------------
-- Executes a function with error protection if an error context is provided
-- Category and Semantic Utilities
-- @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
-- These functions have been moved to Module:SemanticCategoryHelpers
-- @param fallback The fallback value to return if an error occurs
-- Wrapper functions are provided for backward compatibility
-- @param ... Additional arguments to pass to the operation function
 
-- @return The result of the operation or the fallback value if an error occurs
-- Generic function to split multi-value strings with various delimiters
function p.withErrorHandling(errorContext, functionName, operation, fallback, ...)
function p.splitMultiValueString(...)
    -- Capture varargs in a local table to avoid using ... multiple times
     return SemanticCategoryHelpers.splitMultiValueString(...)
    local args = {...}
   
    -- If no error context is provided, execute the operation directly
    if not errorContext or type(errorContext) ~= "table" then
        return operation(unpack(args))
    end
   
    -- Use ErrorHandling module for protected execution
    local ErrorHandling = require('Module:ErrorHandling')
   
    -- Create a wrapper function that passes the arguments to the operation
    local wrapper = function()
        return operation(unpack(args))
    end
   
    -- Execute with error protection
     return ErrorHandling.protect(
        errorContext,
        functionName,
        wrapper,
        fallback
    )
end
end


-- Splits a region string that may contain "and" conjunctions
-- Renders a standard logo block with sanitized image path
function p.splitRegionCategories(...)
-- @param args The template arguments
     return SemanticCategoryHelpers.splitRegionCategories(...)
-- @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
end


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


-- Builds a category string from a table of category names
-- Default delimiters for splitMultiValueString
function p.buildCategories(...)
-- Defined once as an upvalue to avoid recreating on each function call
     return SemanticCategoryHelpers.buildCategories(...)
local defaultDelimiters = {
end
    {pattern = "%s+and%s+", replacement = ";"},
     {pattern = ";%s*", replacement = ";"}
}


-- Adds categories based on a canonical mapping
-- Semicolon-only pattern for backward compatibility with splitSemicolonValues
function p.addMappingCategories(...)
-- Exposed as a module-level constant for use by other modules
    return SemanticCategoryHelpers.addMappingCategories(...)
p.SEMICOLON_PATTERN = {{pattern = ";%s*", replacement = ";"}}
end


-- Generic function to add multi-value semantic properties
-- Generic function to split multi-value strings with various delimiters
function p.addMultiValueSemanticProperties(...)
function p.splitMultiValueString(value, delimiters)
    return SemanticCategoryHelpers.addMultiValueSemanticProperties(...)
     return NormalizationText.splitMultiValueString(value, delimiters)
end
 
-- Generic function to add multi-value categories
function p.addMultiValueCategories(...)
    return SemanticCategoryHelpers.addMultiValueCategories(...)
end
 
-- Adds semantic properties for multiple countries
function p.addMultiCountrySemanticProperties(...)
    return SemanticCategoryHelpers.addMultiCountrySemanticProperties(...)
end
 
-- Adds semantic properties for multiple regions
function p.addMultiRegionSemanticProperties(...)
    return SemanticCategoryHelpers.addMultiRegionSemanticProperties(...)
end
 
-- Adds semantic properties for multiple languages
function p.addMultiLanguageSemanticProperties(...)
     return SemanticCategoryHelpers.addMultiLanguageSemanticProperties(...)
end
 
-- Helper function to process additional properties with multi-value support
function p.processAdditionalProperties(...)
    return SemanticCategoryHelpers.processAdditionalProperties(...)
end
 
-- Helper function to check if a field contains multiple values
function p.isMultiValueField(...)
    return SemanticCategoryHelpers.isMultiValueField(...)
end
 
-- Generates semantic properties based on configuration
function p.generateSemanticProperties(...)
    return SemanticCategoryHelpers.generateSemanticProperties(...)
end
end


Line 423: 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 {}
   
    Parameters:
      fields      - Array of field objects, each with:
                    - label: The field label to display
                    - value: The field value to display
                    - class: Optional CSS class for the row
      options      - Optional configuration:
                    - tableClass: CSS class for the table (default: "template-field-table")
                    - tableAttrs: Additional table attributes
                    - fieldFormat: Format string for field rows (default: uses FIELD_FORMAT)
                    - errorContext: Optional error context for error handling
   
    Returns:
      Wikitext markup for the field table
]]
function p.renderFieldTable(fields, options)
     -- Early return for empty fields
    if not fields or #fields == 0 then
        return ""
    end
      
      
     -- Initialize with defaults
     options = options or {}
    local standardConfig = {
   
        meta = config.meta or {
    -- Define the field table rendering operation
             description = "Template module configuration"
    local function renderFieldTableOperation(fields, options)
         },
        local TemplateStructure = require('Module:TemplateStructure')
         mappings = config.mappings or {},
       
         fields = config.fields or {},
        -- Create a config for the render function with optimized defaults
         semantics = config.semantics or {
        local config = {
            properties = {},
            tableClass = options.tableClass or "template-field-table",
             transforms = {},
             tableAttrs = options.tableAttrs or 'cellpadding="2"',
             additionalProperties = {}
            blocks = {}
        },
         }
        constants = config.constants or {},
          
        patterns = config.patterns or {},
        -- Pre-allocate blocks array based on field count
         categories = config.categories or {} -- Add categories field to preserve base categories
        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