Module:TemplateHelpers
Appearance
Documentation for this module may be created at Module:TemplateHelpers/doc
-- 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.
local p = {}
-- Dependencies
local linkParser = require('Module:LinkParser')
local MultiCountryDisplay = require('Module:MultiCountryDisplay')
local dateNormalization = require('Module:DateNormalization')
local CanonicalForms = require('Module:CanonicalForms')
--------------------------------------------------------------------------------
-- String Processing Functions
--------------------------------------------------------------------------------
-- Trims leading and trailing whitespace from a string
function p.trim(s)
return (s:gsub("^%s+", ""):gsub("%s+$", ""))
end
-- Splits a semicolon-delimited string into a table of trimmed non-empty values
-- This is now a wrapper around splitMultiValueString for backward compatibility
function p.splitSemicolonValues(value)
-- For backward compatibility, maintain the same behavior
return p.splitMultiValueString(value, {
{pattern = ";%s*", replacement = ";"}
})
end
-- Joins a table of values with the specified delimiter
function p.joinValues(values, delimiter)
delimiter = delimiter or "; "
if not values or #values == 0 then return "" end
return table.concat(values, delimiter)
end
--------------------------------------------------------------------------------
-- Field Processing Functions
--------------------------------------------------------------------------------
-- Retrieves a field value from args using either multiple possible keys or a single key
function p.getFieldValue(args, field)
if field.keys then
for _, key in ipairs(field.keys) do
if args[key] and args[key] ~= "" then
return key, args[key]
end
end
return nil, nil
end
return field.key, (args[field.key] and args[field.key] ~= "") and args[field.key] or nil
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 either a single link or an HTML unordered list of links
-- Uses splitMultiValueString for more flexible delimiter handling
-- Optimized to avoid unnecessary table creation for single websites
function p.normalizeWebsites(value)
if not value or value == "" then return "" end
-- Quick check for single website (no delimiters)
if not value:match(";") and not value:match("%s+and%s+") then
-- Single website case - avoid table creation entirely
return string.format("[%s %s]", value, linkParser.strip(value))
end
-- Multiple websites case
local websites = p.splitMultiValueString(value)
if #websites > 1 then
-- 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\" 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
-- Wrapper around MultiCountryDisplay for consistent country formatting
function p.normalizeCountries(value)
if not value or value == "" then return "" end
return MultiCountryDisplay.formatCountries(value)
end
-- Wrapper around DateNormalization for consistent date formatting
function p.normalizeDates(value)
if not value or value == "" then return "" end
return tostring(dateNormalization.formatDate(value))
end
--------------------------------------------------------------------------------
-- Block Generation Helpers
--------------------------------------------------------------------------------
-- Generates a standard title block with configurable class and text
-- Enhanced to support achievement integration with options
function p.renderTitleBlock(args, titleClass, titleText, options)
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
local achievementClass = options.achievementClass or ""
local achievementId = options.achievementId or ""
local achievementName = options.achievementName or ""
-- Only add achievement attributes if they exist
if achievementClass ~= "" and achievementId ~= "" then
return string.format(
'|-\n! colspan="2" class="%s %s" data-achievement-id="%s" data-achievement-name="%s" | %s',
titleClass, achievementClass, achievementId, achievementName, titleText
)
else
-- Clean row with no achievement data
return string.format('|-\n! colspan="2" class="%s" | %s', titleClass, titleText)
end
end
-- Renders a standard fields block based on field definitions and processors
-- Enhanced to support complete HTML blocks and custom field rendering
-- Pre-allocates output table for better performance
function p.renderFieldsBlock(args, fields, processors)
processors = processors or {}
-- 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(fields) do
local key, value = p.getFieldValue(args, field)
if value then
-- Apply processor if available for this field
if key and processors[key] and type(processors[key]) == "function" then
local processedValue = processors[key](value, args)
-- 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
-- Standard field rendering
out[outIndex] = string.format("|-\n| '''%s''':\n| %s", field.label, processedValue)
outIndex = outIndex + 1
end
else
-- Standard field rendering without processor
out[outIndex] = string.format("|-\n| '''%s''':\n| %s", field.label, value)
outIndex = outIndex + 1
end
end
end
return table.concat(out, "\n")
end
-- Renders a standard divider block with optional label
function p.renderDividerBlock(label)
if label and label ~= "" then
return string.format('|-\n| colspan="2" class="template-divider" |\n|-\n| colspan="2" class="icannwiki-centered" | <span class="icannwiki-bold">%s</span>', label)
else
return '|-\n| colspan="2" class="template-divider" |'
end
end
--------------------------------------------------------------------------------
-- Category Utilities
--------------------------------------------------------------------------------
-- 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 = ";"}
}
-- Generic function to split multi-value strings with various delimiters
-- Returns an array of individual values
function p.splitMultiValueString(value, delimiters)
if not value or value == "" then return {} end
-- Use provided delimiters or default ones
delimiters = delimiters or defaultDelimiters
-- Standardize all delimiters to semicolons
local standardizedInput = value
for _, delimiter in ipairs(delimiters) do
standardizedInput = standardizedInput:gsub(delimiter.pattern, delimiter.replacement)
end
-- 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
--------------------------------------------------------------------------------
-- Semantic Property Helpers
--------------------------------------------------------------------------------
-- Generic function to add multi-value semantic properties
-- This is a generalized helper that can be used for any multi-value property
function p.addMultiValueSemanticProperties(value, propertyName, processor, semanticOutput, options)
if not value or value == "" then return semanticOutput end
options = options or {}
local processedItems = {}
-- Get the values to process
local items
if options.valueGetter and type(options.valueGetter) == "function" then
-- Use custom value getter if provided
items = options.valueGetter(value)
else
-- Default to splitting the string
items = p.splitMultiValueString(value)
end
-- 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 {}
-- Get the values to process
local items
if options.valueGetter and type(options.valueGetter) == "function" then
-- Use custom value getter if provided
items = options.valueGetter(value)
else
-- Default to splitting the string
items = p.splitMultiValueString(value)
end
-- Pre-allocate space in the categories table
-- Estimate the number of new categories to add
local currentSize = #categories
local estimatedNewSize = currentSize + #items
-- Process each item and add as a category
for _, item in ipairs(items) do
-- Apply processor if provided
local processedItem = item
if processor and type(processor) == "function" then
processedItem = processor(item)
end
-- Only add if valid
if processedItem and processedItem ~= "" then
categories[currentSize + 1] = processedItem
currentSize = currentSize + 1
end
end
return categories
end
-- Adds semantic properties for multiple countries
-- This is a wrapper around addMultiValueSemanticProperties for backward compatibility
-- For new code, prefer using addMultiValueSemanticProperties directly with appropriate options
function p.addMultiCountrySemanticProperties(countryValue, semanticOutput)
local 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
-- Adds semantic properties for multiple regions
-- This is a wrapper around addMultiValueSemanticProperties for backward compatibility
-- For new code, prefer using addMultiValueSemanticProperties directly with appropriate options
function p.addMultiRegionSemanticProperties(regionValue, semanticOutput)
local RegionalMappingICANN = require('Module:RegionalMappingICANN')
-- First, replace "and" with semicolons to standardize the delimiter
local standardizedInput = regionValue:gsub("%s+and%s+", ";")
return p.addMultiValueSemanticProperties(
standardizedInput,
"Has region",
RegionalMappingICANN.normalizeRegion,
semanticOutput
)
end
-- Adds semantic properties for multiple languages
-- This is a wrapper around addMultiValueSemanticProperties for backward compatibility
-- 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
-- For non-SMW case, concatenate all property HTML fragments at once
if not mw.smw and #propertyHtml > 0 then
semanticOutput = semanticOutput .. "\n" .. table.concat(propertyHtml, "\n")
end
return semanticOutput
end
-- Helper function to check if a field contains multiple values
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
return p