Module:TemplateHelpers: Difference between revisions
Appearance
// via Wikitext Extension for VSCode |
// via Wikitext Extension for VSCode |
||
| Line 59: | Line 59: | ||
if not values or #values == 0 then return "" end | if not values or #values == 0 then return "" end | ||
return table.concat(values, delimiter) | return table.concat(values, delimiter) | ||
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 = "%1" }, -- [[Link]] -> Link | |||
{ pattern = "%[%[([^|%]]+)|([^%]]+)%]%]", replacement = "%2" } -- [[Link|Text]] -> Text | |||
}, | |||
SINGLE_BRACES = { | |||
{ pattern = "{([^{}]+)}", replacement = "%1" } -- {text} -> text | |||
}, | |||
HTML_BASIC = { | |||
{ pattern = "</?[bi]>", replacement = "" }, -- Remove <b>, </b>, <i>, </i> | |||
{ pattern = "</?span[^>]*>", replacement = "" } -- Remove <span...>, </span> | |||
} | |||
} | |||
-- Sanitizes user input by removing or transforming unwanted patterns | |||
-- @param value The input string to sanitize | |||
-- @param patternCategories Optional table or string of pattern categories to apply | |||
-- @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 | ||
Revision as of 19:04, 11 April 2025
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.
local p = {}
-- Dependencies
local linkParser = require('Module:LinkParser')
local MultiCountryDisplay = require('Module:MultiCountryDisplay')
local dateNormalization = require('Module:DateNormalization')
local CanonicalForms = require('Module:CanonicalForms')
local SemanticCategoryHelpers = require('Module:SemanticCategoryHelpers')
--------------------------------------------------------------------------------
-- 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 (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
-- Module-level pattern categories for sanitizing user input
-- These are exposed for potential extension by other modules
p.SANITIZE_PATTERNS = {
WIKI_LINKS = {
{ pattern = "%[%[([^|%]]+)%]%]", replacement = "%1" }, -- [[Link]] -> Link
{ pattern = "%[%[([^|%]]+)|([^%]]+)%]%]", replacement = "%2" } -- [[Link|Text]] -> Text
},
SINGLE_BRACES = {
{ pattern = "{([^{}]+)}", replacement = "%1" } -- {text} -> text
},
HTML_BASIC = {
{ pattern = "</?[bi]>", replacement = "" }, -- Remove <b>, </b>, <i>, </i>
{ pattern = "</?span[^>]*>", replacement = "" } -- Remove <span...>, </span>
}
}
-- Sanitizes user input by removing or transforming unwanted patterns
-- @param value The input string to sanitize
-- @param patternCategories Optional table or string of pattern categories to apply
-- @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
--------------------------------------------------------------------------------
-- Field Processing Functions
--------------------------------------------------------------------------------
-- 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)
if field.keys then
for _, key in ipairs(field.keys) do
-- First try exact match to maintain backward compatibility
if args[key] and args[key] ~= "" then
return key, args[key]
end
-- Then try lowercase version
local lowerKey = key:lower()
if args[lowerKey] and args[lowerKey] ~= "" and lowerKey ~= key then
return lowerKey, args[lowerKey]
end
end
return nil, nil
end
-- First try exact match
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
-- 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 and Semantic Utilities
--------------------------------------------------------------------------------
-- These functions have been moved to Module:SemanticCategoryHelpers
-- Wrapper functions are provided for backward compatibility
-- Generic function to split multi-value strings with various delimiters
function p.splitMultiValueString(...)
return SemanticCategoryHelpers.splitMultiValueString(...)
end
-- Splits a region string that may contain "and" conjunctions
function p.splitRegionCategories(...)
return SemanticCategoryHelpers.splitRegionCategories(...)
end
-- Ensures a category string is properly wrapped in MediaWiki syntax
function p.formatCategoryName(...)
return SemanticCategoryHelpers.formatCategoryName(...)
end
-- Builds a category string from a table of category names
function p.buildCategories(...)
return SemanticCategoryHelpers.buildCategories(...)
end
-- Adds categories based on a canonical mapping
function p.addMappingCategories(...)
return SemanticCategoryHelpers.addMappingCategories(...)
end
-- Generic function to add multi-value semantic properties
function p.addMultiValueSemanticProperties(...)
return SemanticCategoryHelpers.addMultiValueSemanticProperties(...)
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
--------------------------------------------------------------------------------
-- Configuration Standardization
--------------------------------------------------------------------------------
-- Creates a standardized configuration structure for template modules
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