Module:TemplateHelpers: Difference between revisions
// via Wikitext Extension for VSCode |
// via Wikitext Extension for VSCode |
||
| (105 intermediate revisions by the same user not shown) | |||
| Line 1: | Line 1: | ||
-- | --[[ | ||
* Name: TemplateHelpers | |||
* Author: Mark W. Datysgeld | |||
* Description: Common helper functions for template modules promoting code reuse and consistency across string processing, field handling, normalization, and block rendering | |||
* Notes: String processing functions for manipulating strings and template arguments; field processing functions for handling template fields and values; normalization wrappers for standardizing data formats; block generation helpers for rendering template blocks; category and semantic utilities (DEPRECATED wrappers for SemanticCategoryHelpers); configuration standardization for creating standard config structures; includes caching mechanism and multi-value string processing | |||
]] | |||
local p = {} | local p = {} | ||
| Line 7: | Line 10: | ||
-- Dependencies | -- Dependencies | ||
local linkParser = require('Module:LinkParser') | local linkParser = require('Module:LinkParser') | ||
local | local CountryData = require('Module:CountryData') | ||
local dateNormalization = require('Module: | local dateNormalization = require('Module:NormalizationDate') | ||
local CanonicalForms = require('Module:CanonicalForms') | local CanonicalForms = require('Module:CanonicalForms') | ||
local NormalizationText = require('Module:NormalizationText') | |||
local ListGeneration = require('Module:ListGeneration') | |||
-------------------------------------------------------------------------------- | |||
-- Caching Mechanism | |||
-------------------------------------------------------------------------------- | |||
-- Module-level unified cache | |||
local functionCache = {} | |||
-- Helper for generating cache keys from multiple arguments | |||
-- @param prefix String prefix for the cache key (usually the function name) | |||
-- @param ... Any number of arguments to include in the cache key | |||
-- @return A string cache key | |||
function p.generateCacheKey(prefix, ...) | |||
local args = {...} | |||
local parts = {prefix} | |||
for i, arg in ipairs(args) do | |||
if type(arg) == "table" then | |||
-- For tables, we can't reliably generate a cache key | |||
-- So we just use a placeholder with the table's memory address | |||
parts[i+1] = "table:" .. tostring(arg) | |||
elseif type(arg) == "nil" then | |||
parts[i+1] = "nil" | |||
else | |||
parts[i+1] = tostring(arg) | |||
end | |||
end | |||
return table.concat(parts, ":") | |||
end | |||
-- Generic caching wrapper | |||
-- @param cacheKey The cache key to use | |||
-- @param operation A function that returns the value to cache | |||
-- @return The cached result or the result of executing the operation | |||
function p.withCache(cacheKey, operation) | |||
-- Check if result is already cached | |||
if functionCache[cacheKey] ~= nil then | |||
return functionCache[cacheKey] | |||
end | |||
-- Execute operation and cache result | |||
local result = operation() | |||
functionCache[cacheKey] = result | |||
return result | |||
end | |||
-------------------------------------------------------------------------------- | |||
-- Constants | |||
-------------------------------------------------------------------------------- | |||
-- This constant defines how the "label" string are rendered in HTML; we further manipulate them with the "template-label-style" CSS class | |||
-- Added class to the entire row for alternating row colors and direct cell styling for better control | |||
local FIELD_FORMAT = "|- class=\"template-data-row\"\n| class=\"template-label-cell\" | <span class=\"template-label-style\">%s</span>\n| class=\"template-value-cell\" | %s" | |||
-- Format with tooltip support - includes tooltip data directly on the cell | |||
local FIELD_FORMAT_WITH_TOOLTIP = "|- class=\"template-data-row\"\n| class=\"template-label-cell%s\" %s | <span class=\"template-label-style\">%s</span>\n| class=\"template-value-cell\" | %s" | |||
-- Expose the field formats for use by other modules | |||
p.FIELD_FORMAT = FIELD_FORMAT | |||
p.FIELD_FORMAT_WITH_TOOLTIP = FIELD_FORMAT_WITH_TOOLTIP | |||
-- Registry for declarative list post-processors | |||
local listPostProcessors = { | |||
language = function(itemContent) | |||
local NormalizationLanguage = require('Module:NormalizationLanguage') | |||
local langName = NormalizationLanguage.normalize(itemContent) | |||
local nativeName = NormalizationLanguage.getNativeForm(langName) | |||
if nativeName and langName ~= "English" then | |||
return string.format('%s<br/><span style="display:inline-block; width:0.1em; visibility:hidden;">*</span><span style="font-size:75%%;">%s</span>', langName, nativeName) | |||
else | |||
return langName | |||
end | |||
end, | |||
website = function(itemContent) | |||
local linkUrl = itemContent | |||
if not linkUrl:match("^%a+://") then | |||
linkUrl = "https://" .. linkUrl | |||
end | |||
return string.format("[%s %s]", linkUrl, require('Module:LinkParser').strip(itemContent)) | |||
end, | |||
autoWikiLink = function(itemContent) | |||
-- Trim whitespace and check for existing wiki links | |||
local trimmedContent = p.trim(itemContent) | |||
if linkParser.processWikiLink(trimmedContent, "check") then | |||
-- Extract page name and display text | |||
local pageName, displayText = trimmedContent:match("^%[%[([^|]+)|?(.*)%]%]$") | |||
-- Normalize by trimming whitespace | |||
pageName = p.trim(pageName or "") | |||
displayText = p.trim(displayText or "") | |||
-- Reconstruct link, omitting display text if it's same as page name | |||
if displayText == "" or displayText == pageName then | |||
return string.format("[[%s]]", pageName) | |||
else | |||
return string.format("[[%s|%s]]", pageName, displayText) | |||
end | |||
else | |||
-- Not a wiki link, so just wrap it | |||
return string.format("[[%s]]", trimmedContent) | |||
end | |||
end | |||
} | |||
-------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- | ||
-- String Processing Functions | -- String Processing Functions | ||
-------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- | ||
-- Normalizes template arguments to be case-insensitive | |||
-- Returns a new table with lowercase keys while preserving original keys as well | |||
-- Also handles empty numeric parameters that can occur with {{Template|}} syntax | |||
function p.normalizeArgumentCase(args) | |||
local normalized = {} | |||
-- Process all keys | |||
for key, value in pairs(args) do | |||
-- Determine if this key is numeric (number type or numeric-string) | |||
local isNumericKey = type(key) == "number" or (type(key) == "string" and tonumber(key) ~= nil) | |||
-- Skip all numeric parameters entirely | |||
if not isNumericKey then | |||
-- Preserve original key and lowercase variant | |||
normalized[key] = value | |||
if type(key) == "string" then | |||
normalized[key:lower()] = value | |||
end | |||
end | |||
end | |||
return normalized | |||
end | |||
-- Trims leading and trailing whitespace from a string | -- Trims leading and trailing whitespace from a string | ||
function p.trim(s) | function p.trim(s) | ||
return (s | return NormalizationText.trim(s) | ||
end | |||
-- Joins a table of values with the specified delimiter | |||
function p.joinValues(values, delimiter) | |||
return NormalizationText.joinValues(values, delimiter) | |||
end | |||
-- Removes duplicate values from an array while preserving order | |||
-- @param t table The array to deduplicate | |||
-- @return table A new array with duplicates removed | |||
function p.removeDuplicates(t) | |||
-- Type checking | |||
if type(t) ~= 'table' then | |||
return {} | |||
end | |||
-- Helper function to check if a value is NaN | |||
local function isNan(v) | |||
return type(v) == 'number' and tostring(v) == '-nan' | |||
end | |||
-- Pre-allocate result table (maximum possible size is #t) | |||
local ret, exists = {}, {} | |||
-- Process each value, preserving order | |||
for i, v in ipairs(t) do | |||
if isNan(v) then | |||
-- NaNs can't be table keys, and they are also unique | |||
ret[#ret + 1] = v | |||
else | |||
if not exists[v] then | |||
ret[#ret + 1] = v | |||
exists[v] = true | |||
end | |||
end | |||
end | |||
return ret | |||
end | end | ||
-- | -- Get current page ID with caching | ||
-- | -- @return number|nil The current page ID or nil if not available | ||
function p. | 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 | end | ||
-- | -- Module-level cache for wiki link processing | ||
function p. | local wikiLinkCache = {} | ||
-- @deprecated See LinkParser.processWikiLink | |||
return | function p.processWikiLink(value, mode) | ||
return require('Module:LinkParser').processWikiLink(value, mode) | |||
end | |||
-- Module-level pattern categories for sanitizing user input | |||
-- These are exposed for potential extension by other modules | |||
p.SANITIZE_PATTERNS = { | |||
WIKI_LINKS = { | |||
{ | |||
pattern = "%[%[([^|%]]+)%]%]", | |||
replacement = function(match) | |||
return linkParser.processWikiLink("[[" .. match .. "]]", "strip") | |||
end | |||
}, | |||
{ | |||
pattern = "%[%[([^|%]]+)|([^%]]+)%]%]", | |||
replacement = function(match1, match2) | |||
return linkParser.processWikiLink("[[" .. match1 .. "|" .. match2 .. "]]", "strip") | |||
end | |||
} | |||
}, | |||
SINGLE_BRACES = { | |||
{ pattern = "{([^{}]+)}", replacement = "%1" } -- {text} -> text | |||
}, | |||
HTML_BASIC = { | |||
{ pattern = "</?[bi]>", replacement = "" }, -- Remove <b>, </b>, <i>, </i> | |||
{ pattern = "</?span[^>]*>", replacement = "" } -- Remove <span...>, </span> | |||
}, | |||
LOGO = { | |||
{ pattern = "^[Ff][Ii][Ll][Ee]%s*:", replacement = "" } -- Remove "File:" prefix | |||
}, | |||
IMAGE_FILES = { | |||
{ pattern = "%[%[([^|%]]+)%]%]", replacement = "%1" }, -- [[Image.jpg]] -> Image.jpg | |||
{ pattern = "%[%[([^|%]]+)|.+%]%]", replacement = "%1" }, -- [[Image.jpg|...]] -> Image.jpg | |||
{ pattern = "^[Ff][Ii][Ll][Ee]%s*:", replacement = "" }, -- Remove "File:" prefix | |||
{ pattern = "^[Ii][Mm][Aa][Gg][Ee]%s*:", replacement = "" } -- Remove "Image:" prefix too | |||
} | |||
} | |||
-- Sanitizes user input by removing or transforming unwanted patterns | |||
function p.sanitizeUserInput(value, patternCategories, customPatterns, options) | |||
return NormalizationText.sanitizeUserInput(value, patternCategories, customPatterns, options) | |||
end | end | ||
| Line 40: | Line 252: | ||
-------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- | ||
function p.getFieldValue(args, field) | function p.getFieldValue(args, field) | ||
if field.keys then | -- Cache lookup for performance | ||
local cacheKey = p.generateCacheKey("getFieldValue", field.key or table.concat(field.keys or {}, ","), args) | |||
local cached = p.withCache(cacheKey, function() | |||
return key, args[ | -- Case-insensitive lookup logic | ||
if field.keys then | |||
for _, key in ipairs(field.keys) do | |||
if args[key] and args[key] ~= "" then | |||
return { key = key, value = args[key] } | |||
end | |||
local lowerKey = key:lower() | |||
if args[lowerKey] and args[lowerKey] ~= "" and lowerKey ~= key then | |||
return { key = lowerKey, value = args[lowerKey] } | |||
end | |||
end | end | ||
return { key = nil, value = nil } | |||
end | end | ||
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 | end | ||
| Line 78: | Line 311: | ||
-------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- | ||
-- | -- Wrapper around CountryData for consistent country formatting | ||
function p.normalizeCountries(value) | |||
function p. | |||
if not value or value == "" then return "" end | 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) | |||
return | |||
end | |||
end | end | ||
-- Wrapper around | -- Wrapper around DateNormalization for consistent date formatting | ||
function p. | function p.normalizeDates(value) | ||
if not value or value == "" then return "" end | if not value or value == "" then return "" end | ||
return | |||
-- Create a cache key | |||
local cacheKey = p.generateCacheKey("normalizeDates", value) | |||
-- Use the caching wrapper | |||
return p.withCache(cacheKey, function() | |||
return tostring(dateNormalization.formatDate(value)) | |||
end) | |||
end | end | ||
-- | -- Formats a date range with configurable options | ||
function p. | -- @param startDate The start date string | ||
if not | -- @param endDate The end date string (optional) | ||
return | -- @param options Table of options for customizing the output: | ||
-- - dateLabel: Label to use for the date field (default: ConfigRepository.fieldLabels.date) | |||
-- - rangeDelimiter: String to use between dates (default: " – " [en dash]) | |||
-- - outputMode: Output format - "complete" (default), "text", or "html" | |||
-- - showSingleDate: Whether to show the start date when end date is missing (default: true) | |||
-- - consolidateIdenticalDates: Whether to show only one date when start=end (default: true) | |||
-- @return Based on outputMode: | |||
-- - "text": Returns the formatted date text as a string | |||
-- - "html": Returns the complete HTML for the date field as a string | |||
-- - "complete": Returns a table with text, html, and isCompleteHtml properties | |||
function p.formatDateRange(startDate, endDate, options) | |||
-- Default options | |||
options = options or {} | |||
local dateLabel = options.dateLabel or (require('Module:ConfigRepository').fieldLabels.date) | |||
local rangeDelimiter = options.rangeDelimiter or " – " -- en dash | |||
local outputMode = options.outputMode or "complete" -- "complete", "text", or "html" | |||
local showSingleDate = options.showSingleDate ~= false -- true by default | |||
local consolidateIdenticalDates = options.consolidateIdenticalDates ~= false -- true by default | |||
-- Global fallback: if only end date is present, treat end as start | |||
if (not startDate or startDate == "") and endDate and endDate ~= "" then | |||
startDate, endDate = endDate, nil | |||
end | |||
-- Handle empty input | |||
if not startDate or startDate == "" then | |||
if outputMode == "text" then return "" end | |||
if outputMode == "html" then return "" end | |||
return { text = "", html = "", isCompleteHtml = true } | |||
end | |||
-- Create a cache key | |||
-- For options, we only include the values that affect the output | |||
local optionsKey = string.format( | |||
"%s:%s:%s:%s", | |||
dateLabel, | |||
rangeDelimiter, | |||
outputMode, | |||
consolidateIdenticalDates and "consolidate" or "noconsolidate" | |||
) | |||
local cacheKey = p.generateCacheKey("formatDateRange", startDate, endDate or "nil", optionsKey) | |||
-- Use the caching wrapper | |||
return p.withCache(cacheKey, function() | |||
-- Normalize dates | |||
local startFormatted = p.normalizeDates(startDate) | |||
local endFormatted = endDate and endDate ~= "" and p.normalizeDates(endDate) or nil | |||
-- Format date text based on options | |||
local dateText | |||
if endFormatted and endFormatted ~= startFormatted then | |||
-- Different start and end dates | |||
dateText = startFormatted .. rangeDelimiter .. endFormatted | |||
elseif endFormatted and endFormatted == startFormatted and not consolidateIdenticalDates then | |||
-- Same start and end dates, but option to show both | |||
dateText = startFormatted .. rangeDelimiter .. endFormatted | |||
else | |||
-- Single date or consolidated identical dates | |||
dateText = startFormatted | |||
end | |||
-- Format HTML using the field format and label | |||
local dateHtml = string.format(FIELD_FORMAT, dateLabel, dateText) | |||
-- Return based on requested output mode | |||
if outputMode == "text" then return dateText end | |||
if outputMode == "html" then return dateHtml end | |||
-- Default: return both formats | |||
return { | |||
text = dateText, | |||
html = dateHtml, | |||
isCompleteHtml = true -- For compatibility with existing code | |||
} | |||
end) | |||
end | end | ||
| Line 125: | Line 422: | ||
-------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- | ||
-- | -- @deprecated See TemplateStructure.renderTitleBlock and AchievementSystem.renderTitleBlockWithAchievement | ||
function p.renderTitleBlock(args, titleClass, titleText, options) | function p.renderTitleBlock(args, titleClass, titleText, options) | ||
options = options or {} | options = options or {} | ||
-- | -- If achievement support is needed, use AchievementSystem | ||
if | if options.achievementSupport then | ||
return | return require('Module:AchievementSystem').renderTitleBlockWithAchievement( | ||
args, titleClass, titleText, | |||
options.achievementClass or "", | |||
options.achievementId or "", | |||
options.achievementName or "" | |||
) | ) | ||
else | else | ||
-- | -- Otherwise use the basic title block from TemplateStructure | ||
return | 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 | -- 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 164: | Line 461: | ||
local outIndex = 1 | local outIndex = 1 | ||
for _, field in ipairs( | 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 | ||
-- | local continue = false | ||
-- 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) | ||
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 | |||
-- Standard field rendering | end | ||
out[outIndex] = string.format( | 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 | ||
end | end | ||
end | end | ||
| Line 192: | Line 600: | ||
end | end | ||
-- | -- @deprecated See TemplateStructure.renderDividerBlock | ||
function p.renderDividerBlock(label) | function p.renderDividerBlock(label) | ||
return require('Module:TemplateStructure').renderDividerBlock(label) | |||
end | 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 | |||
function p. | |||
if not | |||
end | end | ||
-- | -- If the value already has wiki links, extract the name using LinkParser | ||
local LinkParser = require('Module:LinkParser') | |||
local | 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 | ||
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, ...) | ||
function p. | -- 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') | |||
local | |||
-- Create a wrapper function that passes the arguments to the operation | |||
local wrapper = function() | |||
return operation(unpack(args)) | |||
end | end | ||
return | -- Execute with error protection | ||
return ErrorHandling.protect( | |||
errorContext, | |||
functionName, | |||
wrapper, | |||
fallback | |||
) | |||
end | end | ||
---- | -- Renders a standard logo block with sanitized image path | ||
-- | -- @param args The template arguments | ||
------- | -- @param options Table of options for customizing the output: | ||
-- - cssClass: CSS class for the logo container (default: "template-logo") | |||
-- | -- - imageParams: Additional image parameters like size, alignment (default: "") | ||
-- | -- - errorContext: Optional error context for error handling | ||
function p. | -- @return The rendered logo block HTML or empty string if no logo | ||
function p.renderLogoBlock(args, options) | |||
-- Default options | |||
options = options or {} | options = options or {} | ||
local | local cssClass = options.cssClass or "template-logo" | ||
local imageParams = options.imageParams or "" | |||
-- | -- Define the logo rendering operation | ||
local | 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 | 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 | end | ||
return p. | -- Use the standardized error handling helper | ||
return p.withErrorHandling( | |||
" | options.errorContext, | ||
"renderLogoBlock", | |||
renderLogoOperation, | |||
"", -- Empty string fallback | |||
args, cssClass, imageParams | |||
) | ) | ||
end | end | ||
-- | -------------------------------------------------------------------------------- | ||
-- | -- Multi-Value String Processing | ||
-- | -------------------------------------------------------------------------------- | ||
-- | -- Default delimiters for splitMultiValueString | ||
-- | -- Defined once as an upvalue to avoid recreating on each function call | ||
function | 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) | |||
- | |||
function p. | |||
end | end | ||
| Line 560: | Line 736: | ||
-------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- | ||
-- | --[[ | ||
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 | |||
return | -- Use the standardized error handling helper | ||
return p.withErrorHandling( | |||
options.errorContext, | |||
"renderFieldTable", | |||
renderFieldTableOperation, | |||
"", -- Empty string fallback | |||
fields, options | |||
) | |||
end | end | ||
return p | return p | ||