Module:TemplateHelpers: Difference between revisions
// via Wikitext Extension for VSCode |
// via Wikitext Extension for VSCode |
||
| (15 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 = {} | ||
-- Dependencies | |||
local linkParser = require('Module:LinkParser') | |||
local CountryData = require('Module:CountryData') | |||
local dateNormalization = require('Module:NormalizationDate') | |||
local CanonicalForms = require('Module:CanonicalForms') | |||
local NormalizationText = require('Module:NormalizationText') | |||
local ListGeneration = require('Module:ListGeneration') | |||
-------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- | ||
| Line 74: | Line 77: | ||
p.FIELD_FORMAT_WITH_TOOLTIP = FIELD_FORMAT_WITH_TOOLTIP | p.FIELD_FORMAT_WITH_TOOLTIP = FIELD_FORMAT_WITH_TOOLTIP | ||
-- | -- Registry for declarative list post-processors | ||
local | local listPostProcessors = { | ||
local | language = function(itemContent) | ||
local | local NormalizationLanguage = require('Module:NormalizationLanguage') | ||
local langName = NormalizationLanguage.normalize(itemContent) | |||
local | local nativeName = NormalizationLanguage.getNativeForm(langName) | ||
if nativeName and langName ~= "English" then | |||
return string.format('%s<br/><span style="display:inline-block; width:0.1em; visibility:hidden;">*</span><span style="font-size:75%%;">%s</span>', langName, nativeName) | |||
else | |||
return langName | |||
end | |||
end, | |||
website = function(itemContent) | |||
local linkUrl = itemContent | |||
if not linkUrl:match("^%a+://") then | |||
linkUrl = "https://" .. linkUrl | |||
end | |||
return string.format("[%s %s]", linkUrl, require('Module:LinkParser').strip(itemContent)) | |||
end, | |||
autoWikiLink = function(itemContent) | |||
-- Trim whitespace and check for existing wiki links | |||
local trimmedContent = p.trim(itemContent) | |||
if linkParser.processWikiLink(trimmedContent, "check") then | |||
-- Extract page name and display text | |||
local pageName, displayText = trimmedContent:match("^%[%[([^|]+)|?(.*)%]%]$") | |||
-- Normalize by trimming whitespace | |||
pageName = p.trim(pageName or "") | |||
displayText = p.trim(displayText or "") | |||
-- Reconstruct link, omitting display text if it's same as page name | |||
if displayText == "" or displayText == pageName then | |||
return string.format("[[%s]]", pageName) | |||
else | |||
return string.format("[[%s|%s]]", pageName, displayText) | |||
end | |||
else | |||
-- Not a wiki link, so just wrap it | |||
return string.format("[[%s]]", trimmedContent) | |||
end | |||
end | |||
} | |||
-------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- | ||
| Line 116: | Line 155: | ||
function p.joinValues(values, delimiter) | function p.joinValues(values, delimiter) | ||
return NormalizationText.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 | ||
| Line 238: | Line 310: | ||
-- Normalization Wrappers | -- Normalization Wrappers | ||
-------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- | ||
-- Wrapper around CountryData for consistent country formatting | -- Wrapper around CountryData for consistent country formatting | ||
| Line 432: | Line 464: | ||
local key, value = p.getFieldValue(args, field) | local key, value = p.getFieldValue(args, field) | ||
if value then | if value then | ||
local continue = false | |||
local | |||
-- | -- 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 | ||
end | end | ||
if not continue 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 | ||
local | 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 | |||
-- Handle the case where a processor returns complete HTML | -- 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 | -- Apply wiki link handling | ||
finalValue = linkParser.applyWikiLinkHandling(finalValue, field) | |||
-- | -- Use format with tooltip | ||
out[outIndex] = string.format(FIELD_FORMAT_WITH_TOOLTIP, | out[outIndex] = string.format(FIELD_FORMAT_WITH_TOOLTIP, | ||
tooltipClass, tooltipAttr, field.label, | tooltipClass, tooltipAttr, field.label, finalValue) | ||
outIndex = outIndex + 1 | outIndex = outIndex + 1 | ||
end | end | ||
end | end | ||
end | end | ||