Module:TemplateHelpers
Appearance
Documentation for this module may be created at Module:TemplateHelpers/doc
-- Module:TemplateHelpers
-- Common helper functions for template modules promoting code reuse and consistency.
-- Provides utilities for string processing, field handling, normalization, and block rendering.
--
-- This module contains the following sections:
-- * 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
local p = {}
--------------------------------------------------------------------------------
-- 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
-- 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')
--------------------------------------------------------------------------------
-- 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
-- Skip empty numeric parameters (created by leading pipes after template name)
if type(key) == "number" and (value == nil or value == "") then
-- Do nothing with empty numeric parameters
else
-- For all other parameters, add lowercase version
if type(key) == "string" then
normalized[key:lower()] = value
end
-- Preserve original key as well
normalized[key] = value
end
end
return normalized
end
-- Trims leading and trailing whitespace from a string
function p.trim(s)
return NormalizationText.trim(s)
end
-- Joins a table of values with the specified delimiter
function p.joinValues(values, delimiter)
return NormalizationText.joinValues(values, delimiter)
end
-- Get current page ID with caching
-- @return number|nil The current page ID or nil if not available
function p.getCurrentPageId()
-- Use mw.title API to get the current page title object
local title = mw.title.getCurrentTitle()
-- Return the ID property or nil if not available
return title and title.id or nil
end
-- Module-level cache for wiki link processing
local wikiLinkCache = {}
-- @deprecated See LinkParser.processWikiLink
function p.processWikiLink(value, mode)
return require('Module:LinkParser').processWikiLink(value, mode)
end
-- Module-level pattern categories for sanitizing user input
-- These are exposed for potential extension by other modules
p.SANITIZE_PATTERNS = {
WIKI_LINKS = {
{
pattern = "%[%[([^|%]]+)%]%]",
replacement = function(match)
return linkParser.processWikiLink("[[" .. match .. "]]", "strip")
end
},
{
pattern = "%[%[([^|%]]+)|([^%]]+)%]%]",
replacement = function(match1, match2)
return linkParser.processWikiLink("[[" .. match1 .. "|" .. match2 .. "]]", "strip")
end
}
},
SINGLE_BRACES = {
{ pattern = "{([^{}]+)}", replacement = "%1" } -- {text} -> text
},
HTML_BASIC = {
{ pattern = "</?[bi]>", replacement = "" }, -- Remove <b>, </b>, <i>, </i>
{ pattern = "</?span[^>]*>", replacement = "" } -- Remove <span...>, </span>
},
LOGO = {
{ pattern = "^[Ff][Ii][Ll][Ee]%s*:", replacement = "" } -- Remove "File:" prefix
},
IMAGE_FILES = {
{ pattern = "%[%[([^|%]]+)%]%]", replacement = "%1" }, -- [[Image.jpg]] -> Image.jpg
{ pattern = "%[%[([^|%]]+)|.+%]%]", replacement = "%1" }, -- [[Image.jpg|...]] -> Image.jpg
{ pattern = "^[Ff][Ii][Ll][Ee]%s*:", replacement = "" }, -- Remove "File:" prefix
{ pattern = "^[Ii][Mm][Aa][Gg][Ee]%s*:", replacement = "" } -- Remove "Image:" prefix too
}
}
-- Sanitizes user input by removing or transforming unwanted patterns
function p.sanitizeUserInput(value, patternCategories, customPatterns, options)
return NormalizationText.sanitizeUserInput(value, patternCategories, customPatterns, options)
end
--------------------------------------------------------------------------------
-- Field Processing Functions
--------------------------------------------------------------------------------
function p.getFieldValue(args, field)
-- Cache lookup for performance
local cacheKey = p.generateCacheKey("getFieldValue", field.key or table.concat(field.keys or {}, ","), args)
local cached = p.withCache(cacheKey, function()
-- Case-insensitive lookup logic
if field.keys then
for _, key in ipairs(field.keys) do
if args[key] and args[key] ~= "" then
return { key = key, value = args[key] }
end
local lowerKey = key:lower()
if args[lowerKey] and args[lowerKey] ~= "" and lowerKey ~= key then
return { key = lowerKey, value = args[lowerKey] }
end
end
return { key = nil, value = nil }
end
if field.key then
if args[field.key] and args[field.key] ~= "" then
return { key = field.key, value = args[field.key] }
end
local lowerKey = field.key:lower()
if args[lowerKey] and args[lowerKey] ~= "" and lowerKey ~= field.key then
return { key = lowerKey, value = args[lowerKey] }
end
return { key = field.key, value = nil }
end
return { key = nil, value = nil }
end)
return cached.key, cached.value
end
-- Processes multiple values with a given processor function
-- Uses splitMultiValueString for more flexible delimiter handling
-- Pre-allocates result table for better performance
function p.processMultipleValues(values, processor)
if not values or values == "" then return {} end
local items = p.splitMultiValueString(values)
-- Pre-allocate results table based on input size
local results = {}
local resultIndex = 1
for _, item in ipairs(items) do
local processed = processor(item)
if processed and processed ~= "" then
results[resultIndex] = processed
resultIndex = resultIndex + 1
end
end
return results
end
--------------------------------------------------------------------------------
-- Normalization Wrappers
--------------------------------------------------------------------------------
-- Formats website URLs as an HTML unordered list of links, ensuring consistent emoji display
-- Uses splitMultiValueString for more flexible delimiter handling
function p.normalizeWebsites(value)
if not value or value == "" then return "" end
-- Get websites as a table, handling both single and multiple cases
local websites
-- Quick check for single website (no delimiters)
if not value:match(";") and not value:match("%s+and%s+") then
-- Single website case - create a single-item table
websites = {value}
else
-- Multiple websites case
websites = p.splitMultiValueString(value)
end
-- Handle all websites consistently using the list format
if #websites > 0 then
-- Pre-allocate listItems table based on number of websites
local listItems = {}
local index = 1
for _, site in ipairs(websites) do
-- Ensure the site has a protocol prefix for proper linking
local linkUrl = site
if not linkUrl:match("^%a+://") then
linkUrl = "https://" .. linkUrl
end
local formattedLink = string.format("[%s %s]", linkUrl, 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 ""
end
-- Wrapper around CountryData for consistent country formatting
function p.normalizeCountries(value)
if not value or value == "" then return "" end
-- Create a cache key
local cacheKey = p.generateCacheKey("normalizeCountries", value)
-- Use the caching wrapper
return p.withCache(cacheKey, function()
return CountryData.formatCountries(value)
end)
end
-- Wrapper around DateNormalization for consistent date formatting
function p.normalizeDates(value)
if not value or value == "" then return "" end
-- Create a cache key
local cacheKey = p.generateCacheKey("normalizeDates", value)
-- Use the caching wrapper
return p.withCache(cacheKey, function()
return tostring(dateNormalization.formatDate(value))
end)
end
-- Formats a date range with configurable options
-- @param startDate The start date string
-- @param endDate The end date string (optional)
-- @param options Table of options for customizing the output:
-- - dateLabel: Label to use for the date field (default: ConfigRepository.fieldLabels.date)
-- - rangeDelimiter: String to use between dates (default: " – " [en dash])
-- - outputMode: Output format - "complete" (default), "text", or "html"
-- - showSingleDate: Whether to show the start date when end date is missing (default: true)
-- - consolidateIdenticalDates: Whether to show only one date when start=end (default: true)
-- @return Based on outputMode:
-- - "text": Returns the formatted date text as a string
-- - "html": Returns the complete HTML for the date field as a string
-- - "complete": Returns a table with text, html, and isCompleteHtml properties
function p.formatDateRange(startDate, endDate, options)
-- Default options
options = options or {}
local dateLabel = options.dateLabel or (require('Module:ConfigRepository').fieldLabels.date)
local rangeDelimiter = options.rangeDelimiter or " – " -- en dash
local outputMode = options.outputMode or "complete" -- "complete", "text", or "html"
local showSingleDate = options.showSingleDate ~= false -- true by default
local consolidateIdenticalDates = options.consolidateIdenticalDates ~= false -- true by default
-- Global fallback: if only end date is present, treat end as start
if (not startDate or startDate == "") and endDate and endDate ~= "" then
startDate, endDate = endDate, nil
end
-- Handle empty input
if not startDate or startDate == "" then
if outputMode == "text" then return "" end
if outputMode == "html" then return "" end
return { text = "", html = "", isCompleteHtml = true }
end
-- Create a cache key
-- For options, we only include the values that affect the output
local optionsKey = string.format(
"%s:%s:%s:%s",
dateLabel,
rangeDelimiter,
outputMode,
consolidateIdenticalDates and "consolidate" or "noconsolidate"
)
local cacheKey = p.generateCacheKey("formatDateRange", startDate, endDate or "nil", optionsKey)
-- Use the caching wrapper
return p.withCache(cacheKey, function()
-- Normalize dates
local startFormatted = p.normalizeDates(startDate)
local endFormatted = endDate and endDate ~= "" and p.normalizeDates(endDate) or nil
-- Format date text based on options
local dateText
if endFormatted and endFormatted ~= startFormatted then
-- Different start and end dates
dateText = startFormatted .. rangeDelimiter .. endFormatted
elseif endFormatted and endFormatted == startFormatted and not consolidateIdenticalDates then
-- Same start and end dates, but option to show both
dateText = startFormatted .. rangeDelimiter .. endFormatted
else
-- Single date or consolidated identical dates
dateText = startFormatted
end
-- Format HTML using the field format and label
local dateHtml = string.format(FIELD_FORMAT, dateLabel, dateText)
-- Return based on requested output mode
if outputMode == "text" then return dateText end
if outputMode == "html" then return dateHtml end
-- Default: return both formats
return {
text = dateText,
html = dateHtml,
isCompleteHtml = true -- For compatibility with existing code
}
end)
end
--------------------------------------------------------------------------------
-- Block Generation Helpers
--------------------------------------------------------------------------------
-- @deprecated See TemplateStructure.renderTitleBlock and AchievementSystem.renderTitleBlockWithAchievement
function p.renderTitleBlock(args, titleClass, titleText, options)
options = options or {}
-- If achievement support is needed, use AchievementSystem
if options.achievementSupport then
return require('Module:AchievementSystem').renderTitleBlockWithAchievement(
args, titleClass, titleText,
options.achievementClass or "",
options.achievementId or "",
options.achievementName or ""
)
else
-- Otherwise use the basic title block from TemplateStructure
return require('Module:TemplateStructure').renderTitleBlock(args, titleClass, titleText)
end
end
-- Renders a standard fields block based on field definitions and processors
-- Enhanced to support complete HTML blocks, custom field rendering, and tooltips
-- Pre-allocates output table for better performance
function p.renderFieldsBlock(args, fields, processors, propertyMappings)
processors = processors or {}
propertyMappings = propertyMappings or {}
-- filter out hidden fields
local filteredFields = {}
for _, f in ipairs(fields) do
if not f.hidden then
table.insert(filteredFields, f)
end
end
-- Pre-allocate output table - estimate based on number of fields
-- Not all fields may be present in args, but this gives us a reasonable upper bound
local out = {}
local outIndex = 1
for _, field in ipairs(filteredFields) do
local key, value = p.getFieldValue(args, field)
if value then
-- Create sanitization options
local sanitizeOptions = {
preserveWikiLinks = field.autoWikiLink or field.preserveWikiLinks
}
-- Get property name for this field if available (case-insensitive)
local propertyName = nil
for propName, fieldName in pairs(propertyMappings) do
if key and (fieldName == key or tostring(fieldName):lower() == tostring(key):lower()) then
propertyName = propName
break
end
end
-- Get tooltip text if property exists
local tooltipText = ""
if propertyName then
tooltipText = require('Module:SemanticCategoryHelpers').getPropertyDescription(propertyName) or ""
end
-- Prepare tooltip attributes if tooltip text exists
local tooltipClass = ""
local tooltipAttr = ""
if tooltipText and tooltipText ~= "" then
-- Escape quotes in tooltip text to prevent HTML attribute issues
local escapedTooltip = tooltipText:gsub('"', '"')
tooltipClass = " has-tooltip"
tooltipAttr = string.format('data-tooltip="%s"', escapedTooltip)
end
-- Apply processor if available for this field
if key and processors[key] and type(processors[key]) == "function" then
local processedValue = processors[key](value, args)
-- Preserve wiki links if needed
processedValue = linkParser.preserveWikiLinks(
value,
processedValue,
sanitizeOptions.preserveWikiLinks
)
-- Handle the case where a processor returns complete HTML
if type(processedValue) == "table" and processedValue.isCompleteHtml then
-- Add the complete HTML as is
out[outIndex] = processedValue.html
outIndex = outIndex + 1
elseif processedValue ~= nil and processedValue ~= false then
-- Apply wiki link handling
processedValue = linkParser.applyWikiLinkHandling(processedValue, field)
-- Standard field rendering with tooltip
out[outIndex] = string.format(FIELD_FORMAT_WITH_TOOLTIP,
tooltipClass, tooltipAttr, field.label, processedValue)
outIndex = outIndex + 1
end
else
-- Standard field rendering without processor
-- Apply sanitization with preserveWikiLinks option if needed
local finalValue
if sanitizeOptions.preserveWikiLinks then
finalValue = value
else
finalValue = p.sanitizeUserInput(value, nil, nil, sanitizeOptions)
end
-- Apply wiki link handling
finalValue = linkParser.applyWikiLinkHandling(finalValue, field)
-- Use format with tooltip
out[outIndex] = string.format(FIELD_FORMAT_WITH_TOOLTIP,
tooltipClass, tooltipAttr, field.label, finalValue)
outIndex = outIndex + 1
end
end
end
return table.concat(out, "\n")
end
-- @deprecated See TemplateStructure.renderDividerBlock
function p.renderDividerBlock(label)
return require('Module:TemplateStructure').renderDividerBlock(label)
end
-- Extracts semantic value from a field, handling wiki links appropriately
-- @param fieldValue The value to extract semantic data from
-- @param fieldName The name of the field (for error reporting)
-- @param errorContext Optional error context for error handling
-- @return The extracted semantic value or nil if the input is empty
function p.extractSemanticValue(fieldValue, fieldName, errorContext)
if not fieldValue or fieldValue == "" then
return nil
end
-- If the value already has wiki links, extract the name using LinkParser
local LinkParser = require('Module:LinkParser')
if LinkParser.processWikiLink(fieldValue, "check") then
-- Use the standardized error handling helper
return p.withErrorHandling(
errorContext,
"extractFromWikiLink_" .. fieldName,
LinkParser.extractFromWikiLink,
fieldValue, -- fallback to original value on error
fieldValue
)
else
-- Otherwise, use the plain text value
return fieldValue
end
end
-- Standardized error handling helper
-- Executes a function with error protection if an error context is provided
-- @param errorContext The error context for error handling (optional)
-- @param functionName The name of the function being protected (for error reporting)
-- @param operation The function to execute
-- @param fallback The fallback value to return if an error occurs
-- @param ... Additional arguments to pass to the operation function
-- @return The result of the operation or the fallback value if an error occurs
function p.withErrorHandling(errorContext, functionName, operation, fallback, ...)
-- Capture varargs in a local table to avoid using ... multiple times
local args = {...}
-- If no error context is provided, execute the operation directly
if not errorContext or type(errorContext) ~= "table" then
return operation(unpack(args))
end
-- Use ErrorHandling module for protected execution
local ErrorHandling = require('Module:ErrorHandling')
-- Create a wrapper function that passes the arguments to the operation
local wrapper = function()
return operation(unpack(args))
end
-- Execute with error protection
return ErrorHandling.protect(
errorContext,
functionName,
wrapper,
fallback
)
end
-- Renders a standard logo block with sanitized image path
-- @param args The template arguments
-- @param options Table of options for customizing the output:
-- - cssClass: CSS class for the logo container (default: "template-logo")
-- - imageParams: Additional image parameters like size, alignment (default: "")
-- - errorContext: Optional error context for error handling
-- @return The rendered logo block HTML or empty string if no logo
function p.renderLogoBlock(args, options)
-- Default options
options = options or {}
local cssClass = options.cssClass or "template-logo"
local imageParams = options.imageParams or ""
-- Define the logo rendering operation
local function renderLogoOperation(args, cssClass, imageParams)
-- Get logo parameter
local logo = args["logo"]
-- If no logo or empty, return empty string
if not logo or logo == "" then
return ""
end
-- Sanitize logo path - extract filename and remove prefixes
logo = p.sanitizeUserInput(logo, "IMAGE_FILES")
-- Format image parameters if provided
local imgParams = imageParams ~= "" and "|" .. imageParams or ""
-- Render the logo image
return string.format(
'|-\n| colspan="2" class="%s" | [[Image:%s%s]]',
cssClass, logo, imgParams
)
end
-- Use the standardized error handling helper
return p.withErrorHandling(
options.errorContext,
"renderLogoBlock",
renderLogoOperation,
"", -- Empty string fallback
args, cssClass, imageParams
)
end
--------------------------------------------------------------------------------
-- Multi-Value String Processing
--------------------------------------------------------------------------------
-- Default delimiters for splitMultiValueString
-- Defined once as an upvalue to avoid recreating on each function call
local defaultDelimiters = {
{pattern = "%s+and%s+", replacement = ";"},
{pattern = ";%s*", replacement = ";"}
}
-- Semicolon-only pattern for backward compatibility with splitSemicolonValues
-- Exposed as a module-level constant for use by other modules
p.SEMICOLON_PATTERN = {{pattern = ";%s*", replacement = ";"}}
-- Generic function to split multi-value strings with various delimiters
function p.splitMultiValueString(value, delimiters)
return NormalizationText.splitMultiValueString(value, delimiters)
end
--------------------------------------------------------------------------------
-- Configuration Standardization
--------------------------------------------------------------------------------
--[[
Renders a table of fields with labels and values using TemplateStructure.
Parameters:
fields - Array of field objects, each with:
- label: The field label to display
- value: The field value to display
- class: Optional CSS class for the row
options - Optional configuration:
- tableClass: CSS class for the table (default: "template-field-table")
- tableAttrs: Additional table attributes
- fieldFormat: Format string for field rows (default: uses FIELD_FORMAT)
- errorContext: Optional error context for error handling
Returns:
Wikitext markup for the field table
]]
function p.renderFieldTable(fields, options)
-- Early return for empty fields
if not fields or #fields == 0 then
return ""
end
options = options or {}
-- Define the field table rendering operation
local function renderFieldTableOperation(fields, options)
local TemplateStructure = require('Module:TemplateStructure')
-- Create a config for the render function with optimized defaults
local config = {
tableClass = options.tableClass or "template-field-table",
tableAttrs = options.tableAttrs or 'cellpadding="2"',
blocks = {}
}
-- Pre-allocate blocks array based on field count
local blocks = {}
-- Use the module's FIELD_FORMAT or a custom format if provided
local fieldFormat = options.fieldFormat or FIELD_FORMAT
-- Create a block function for each field with direct index assignment
for i = 1, #fields do
local field = fields[i]
blocks[i] = function()
-- Combine the field's class with the template-data-row class
local fieldClass = field.class and field.class or ""
-- Create the row with proper classes
local row = '|- class="template-data-row' .. (fieldClass ~= "" and ' ' .. fieldClass or '') .. '"'
-- Format the cells using the field format but replace the row start
local cellsFormat = fieldFormat:gsub("^[^|]+", "")
-- Return the complete row
return row .. string.format(cellsFormat, field.label, field.value)
end
end
-- Assign blocks to config
config.blocks = blocks
-- Use TemplateStructure's render function
return TemplateStructure.render({}, config, options.errorContext)
end
-- Use the standardized error handling helper
return p.withErrorHandling(
options.errorContext,
"renderFieldTable",
renderFieldTableOperation,
"", -- Empty string fallback
fields, options
)
end
return p