Module:LuaTemplateBlueprint: Difference between revisions
Appearance
// via Wikitext Extension for VSCode |
// via Wikitext Extension for VSCode |
||
| (167 intermediate revisions by the same user not shown) | |||
| Line 11: | Line 11: | ||
* - TemplateHelpers: Common utilities for rendering and normalization | * - TemplateHelpers: Common utilities for rendering and normalization | ||
* - TemplateStructure: Block-based rendering engine | * - TemplateStructure: Block-based rendering engine | ||
* - | * - TemplateFieldProcessor: Field processing and value retrieval | ||
* - CountryData: Country normalization and region derivation | |||
* - SocialMedia: Social media icon rendering | |||
* | |||
* Feature Configuration: | |||
* Templates must explicitly specify which features they want to use when registering: | |||
* | |||
* local template = Blueprint.registerTemplate('TemplateName', { | |||
* features = { | |||
* title = true, | |||
* fields = true, | |||
* semanticProperties = true, | |||
* categories = true, | |||
* errorReporting = true | |||
* -- logo and socialMedia intentionally disabled | |||
* } | |||
* }) | |||
* | |||
* Alternatively, templates can use predefined feature sets: | |||
* | |||
* local template = Blueprint.registerTemplate('TemplateName', { | |||
* features = Blueprint.featureSets.minimal | |||
* }) | |||
* | |||
* Available feature sets: | |||
* - minimal: Just title, fields, and error reporting | |||
* - standard: All features enabled | |||
* - content: Standard without social media | |||
* - data: Just fields, semantics, categories, and error reporting | |||
* | * | ||
* Note on parameter handling: | * Note on parameter handling: | ||
* | * Template parameters are extracted using frame:getParent().args and normalized | ||
* with TemplateHelpers.normalizeArgumentCase() for case-insensitive access. | |||
]] | ]] | ||
local p = {} | local p = {} | ||
-- ========== Constants as Upvalues ========== | |||
-- Common empty string for fast returns | |||
local EMPTY_STRING = '' | |||
-- Common separator for concatenation | |||
local NEWLINE = '\n' | |||
-- Common semicolon separator for multi-values | |||
local SEMICOLON = ';' | |||
-- Common category prefix/suffix | |||
local CATEGORY_PREFIX = '[[Category:' | |||
local CATEGORY_SUFFIX = ']]' | |||
-- Template class prefixes | |||
local TEMPLATE_TITLE_CLASS_PREFIX = 'template-title template-title-' | |||
local TEMPLATE_LOGO_CLASS_PREFIX = 'template-logo template-logo-' | |||
-- Default table class | |||
local DEFAULT_TABLE_CLASS = 'template-table' | |||
-- Wiki link pattern for regex matching | |||
local WIKI_LINK_PATTERN = '%[%[.-%]%]' | |||
-- Empty table for returning when needed | |||
local EMPTY_OBJECT = {} | |||
-- ========== Required modules ========== | -- ========== Required modules ========== | ||
-- Core modules that are always needed | |||
local ErrorHandling = require('Module:ErrorHandling') | local ErrorHandling = require('Module:ErrorHandling') | ||
local ConfigRepository = require('Module:ConfigRepository') | local ConfigRepository = require('Module:ConfigRepository') | ||
local ConfigHelpers = require('Module:ConfigHelpers') | |||
local TemplateHelpers = require('Module:TemplateHelpers') | local TemplateHelpers = require('Module:TemplateHelpers') | ||
local TemplateStructure = require('Module:TemplateStructure') | local TemplateStructure = require('Module:TemplateStructure') | ||
local SemanticAnnotations = require('Module:SemanticAnnotations') | local SemanticAnnotations = require('Module:SemanticAnnotations') | ||
local LinkParser = require('Module:LinkParser') | |||
local TemplateFieldProcessor = require('Module:TemplateFieldProcessor') | |||
local mw = mw -- MediaWiki API | local mw = mw -- MediaWiki API | ||
-- Module-level caches for expensive operations | -- Module-level caches for expensive operations | ||
local featureCache = {} | local featureCache = {} | ||
local | local moduleCache = {} -- Cache for lazy-loaded modules | ||
-- Lazy module loader | |||
local function lazyRequire(moduleName) | |||
return function() | |||
if not moduleCache[moduleName] then | |||
moduleCache[moduleName] = require(moduleName) | |||
end | |||
return moduleCache[moduleName] | |||
end | |||
end | |||
-- Lazily loaded modules | |||
local getSocialMedia = lazyRequire('Module:SocialMedia') | |||
local getNormalizationDate = lazyRequire('Module:NormalizationDate') | |||
local getCountryData = lazyRequire('Module:CountryData') | |||
-- ========== Template Registry ========== | -- ========== Template Registry ========== | ||
-- Registry to store all registered templates | -- Registry to store all registered templates | ||
p.registry = {} | p.registry = {} | ||
-- Element registry for custom Blueprint elements | |||
p.elementRegistry = {} | |||
-- Register and lookup functions for Blueprint elements | |||
function p.registerElement(name, module) | |||
if not name or not module then return end | |||
p.elementRegistry[name] = module | |||
return module | |||
end | |||
-- Get element from registry by name | |||
-- This function is used internally by createElementBlock; it may appear unused in T-* templates, but is essential for the Element system | |||
function p.getElement(name) | |||
return p.elementRegistry[name] | |||
end | |||
-- Create a Blueprint block for a registered element | |||
function p.createElementBlock(name) | |||
local element = p.getElement(name) | |||
if not element or not element.createBlock then return end | |||
return { | |||
feature = name, | |||
render = function(template, args) | |||
return p.protectedExecute( | |||
template, | |||
"ElementBlock_" .. name, | |||
function() | |||
return element.createBlock()(template, args) | |||
end, | |||
"", | |||
args | |||
) | |||
end | |||
} | |||
end | |||
-- Add a registered element's block to a template at a given position | |||
function p.addElementToTemplate(template, name, _) | |||
local block = p.createElementBlock(name) | |||
if not block then return false end | |||
template.config.blocks = template.config.blocks or {} | |||
template.config.blocks[name] = block | |||
template.features[name] = true | |||
-- Rebuild blockSequence, only append if placeholder not present | |||
local baseSeq = template.config.blockSequence or p.standardBlockSequence | |||
local seq = {} | |||
local found = false | |||
for _, b in ipairs(baseSeq) do | |||
table.insert(seq, b) | |||
if b == name then found = true end | |||
end | |||
if not found then | |||
table.insert(seq, name) | |||
end | |||
template.config.blockSequence = seq | |||
return true | |||
end | |||
-- Automatically initialize and register built-in elements | |||
function p.initializeElements() | |||
local context = ErrorHandling.createContext("LuaTemplateBlueprint") | |||
local mod = ErrorHandling.safeRequire(context, 'Module:ElementNavigation', false) | |||
if mod and mod.elementName then | |||
p.registerElement(mod.elementName, mod) | |||
end | |||
end | |||
-- ========== Feature Management ========== | -- ========== Feature Management ========== | ||
-- Templates must explicitly include the features they want to use | |||
-- | |||
-- Create a cache key for features | -- Create a cache key for features | ||
-- Internal helper function: Used only by initializeFeatures to generate cache keys for feature sets | |||
-- @param featureOverrides table Optional table of feature overrides | -- @param featureOverrides table Optional table of feature overrides | ||
-- @return string Cache key | -- @return string Cache key | ||
| Line 64: | Line 182: | ||
end | end | ||
local keyCount = 0 | |||
for _ in pairs(featureOverrides) do keyCount = keyCount + 1 end | |||
local keys = {} | local keys = {} | ||
local keyIndex = 1 | |||
for k in pairs(featureOverrides) do | for k in pairs(featureOverrides) do | ||
keys[keyIndex] = k | |||
keyIndex = keyIndex + 1 | |||
end | end | ||
table.sort(keys) | table.sort(keys) | ||
for | local parts = {} | ||
for i = 1, keyCount do | |||
local k = keys[i] | |||
parts[i] = k .. '=' .. tostring(featureOverrides[k]) | |||
end | end | ||
| Line 80: | Line 204: | ||
-- Initialize feature toggles for a template | -- Initialize feature toggles for a template | ||
-- @param featureOverrides table | -- @param featureOverrides table Table of feature configurations | ||
-- @return table The initialized features | -- @return table The initialized features | ||
function p.initializeFeatures(featureOverrides) | function p.initializeFeatures(featureOverrides) | ||
-- | -- If no features are specified, return an empty features table | ||
-- This ensures templates must explicitly define their features | |||
if not featureOverrides then | if not featureOverrides then | ||
return {} | |||
end | end | ||
-- Check | -- Check if we have a cached version of this feature set | ||
local cacheKey = createFeatureCacheKey(featureOverrides) | local cacheKey = createFeatureCacheKey(featureOverrides) | ||
if featureCache[cacheKey] then | if featureCache[cacheKey] then | ||
-- Return a copy of the cached features | -- Return a copy of the cached features to prevent modification | ||
local features = {} | local features = {} | ||
for k, v in pairs(featureCache[cacheKey]) do | for k, v in pairs(featureCache[cacheKey]) do | ||
| Line 104: | Line 224: | ||
end | end | ||
-- Create new | -- Create a new features table with only the specified features | ||
local features = {} | local features = {} | ||
for featureId, enabled in pairs(featureOverrides) do | for featureId, enabled in pairs(featureOverrides) do | ||
features[featureId] = enabled | features[featureId] = enabled | ||
end | end | ||
-- Cache the | -- Cache the features for future use | ||
featureCache[cacheKey] = {} | featureCache[cacheKey] = {} | ||
for k, v in pairs(features) do | for k, v in pairs(features) do | ||
| Line 127: | Line 240: | ||
-- ========== Template Registration ========== | -- ========== Template Registration ========== | ||
-- Register a new template | -- Register a new template | ||
-- @param templateType string The template type (e.g., "Event", "Person", "TLD") | -- @param templateType string The template type (e.g., "Event", "Person", "TLD") | ||
| Line 133: | Line 245: | ||
-- @return table The registered template object | -- @return table The registered template object | ||
function p.registerTemplate(templateType, config) | function p.registerTemplate(templateType, config) | ||
local template = { | local template = { | ||
type = templateType, | type = templateType, | ||
| Line 140: | Line 251: | ||
} | } | ||
template.render = function(frame) | template.render = function(frame) | ||
return p.renderTemplate(template, frame) | return p.renderTemplate(template, frame) | ||
end | end | ||
p.registry[templateType] = template | p.registry[templateType] = template | ||
-- Default provider for country/region categories | |||
p.registerCategoryProvider(template, function(template, args) | |||
local cats = {} | |||
local SCH = require('Module:SemanticCategoryHelpers') | |||
local CD = require('Module:CountryData') | |||
if args.country and args.country ~= '' then | |||
local TH = require('Module:TemplateHelpers') | |||
cats = SCH.addMultiValueCategories( | |||
args.country, | |||
CD.normalizeCountryName, | |||
cats, | |||
{ valueGetter = function(v) | |||
return TH.splitMultiValueString(v, TH.SEMICOLON_PATTERN) | |||
end } | |||
) | |||
end | |||
if args.region and args.region ~= '' then | |||
cats = SCH.addMultiValueCategories(args.region, nil, cats) | |||
end | |||
return cats | |||
end) | |||
-- Shared provider for conditional and mapping categories | |||
-- Iterates 'config.categories.conditional' to add categories when template args are non-empty | |||
-- Iterates 'config.mappings' to compute mapping categories via SemanticCategoryHelpers | |||
-- Uses addMappingCategories to normalize values and generate category links | |||
-- Centralizes category logic for all templates in a single provider function | |||
p.registerCategoryProvider(template, function(template, args) | |||
local cats = {} | |||
-- Conditional categories | |||
for param, category in pairs(template.config.categories.conditional or {}) do | |||
if args[param] and args[param] ~= "" then | |||
table.insert(cats, category) | |||
end | |||
end | |||
-- Mapping categories | |||
local SCH = require('Module:SemanticCategoryHelpers') | |||
for key, mappingDef in pairs(template.config.mappings or {}) do | |||
local value = args[key] | |||
for _, cat in ipairs(SCH.addMappingCategories(value, mappingDef) or {}) do | |||
table.insert(cats, cat) | |||
end | |||
end | |||
return cats | |||
end) | |||
return template | return template | ||
| Line 152: | Line 310: | ||
-- ========== Error Handling Integration ========== | -- ========== Error Handling Integration ========== | ||
-- Create an error context for a template | -- Create an error context for a template | ||
-- @param template table The template object | -- @param template table The template object | ||
| Line 184: | Line 341: | ||
-- ========== Configuration Integration ========== | -- ========== Configuration Integration ========== | ||
-- Initialize the standard configuration for a template | -- Initialize the standard configuration for a template | ||
-- Combines base config from ConfigRepository with template overrides | -- Combines base config from ConfigRepository with template overrides | ||
| Line 203: | Line 348: | ||
local templateType = template.type | local templateType = template.type | ||
local configOverrides = template.config or {} | local configOverrides = template.config or {} | ||
-- Get base configuration from repository | -- Get base configuration from repository | ||
local baseConfig = ConfigRepository.getStandardConfig(templateType) | local baseConfig = ConfigRepository.getStandardConfig(templateType) | ||
-- | -- Use ConfigHelpers to deep merge configurations | ||
local config = | local config = ConfigHelpers.deepMerge(baseConfig, configOverrides) | ||
-- Store complete config in template | -- Store complete config in template | ||
template.config = config | template.config = config | ||
-- Auto-normalize arguments for all mappings | |||
for key, mappingDef in pairs(config.mappings or {}) do | |||
p.addPreprocessor(template, function(template, args) | |||
local cf = require('Module:CanonicalForms') | |||
local val = args[key] or '' | |||
local canonical = select(1, cf.normalize(val, mappingDef)) | |||
if canonical then args[key] = canonical end | |||
return args | |||
end) | |||
end | |||
return config | return config | ||
end | end | ||
-- ========== Block Framework ========== | -- ========== Block Framework ========== | ||
-- Standard sequence of blocks for template rendering | -- Standard sequence of blocks for template rendering | ||
p.standardBlockSequence = { | p.standardBlockSequence = { | ||
| Line 256: | Line 394: | ||
'StandardBlock_title', | 'StandardBlock_title', | ||
function() | function() | ||
-- Get title from config if available, otherwise use template type | |||
local titleText = template.type | |||
if template.config.constants and template.config.constants.title then | |||
titleText = template.config.constants.title | |||
end | |||
) | |||
-- Get template ID from config or use the template name as fallback | |||
local templateId = template.type | |||
if template.config.meta and template.config.meta.templateId then | |||
templateId = template.config.meta.templateId | |||
end | |||
return require('Module:TemplateStructure').renderTitleBlock( | |||
args, | |||
TEMPLATE_TITLE_CLASS_PREFIX .. string.lower(templateId), | |||
titleText, | |||
template.titleId | |||
) | |||
end, | end, | ||
EMPTY_STRING, | EMPTY_STRING, | ||
| Line 277: | Line 427: | ||
'StandardBlock_logo', | 'StandardBlock_logo', | ||
function() | function() | ||
local logoClass = | local logoClass = TEMPLATE_LOGO_CLASS_PREFIX .. string.lower(template.type) | ||
local logoOptions = template.config.meta and template.config.meta.logoOptions or {} | local logoOptions = template.config.meta and template.config.meta.logoOptions or {} | ||
| Line 288: | Line 438: | ||
end | end | ||
-- Create options object for renderLogoBlock | |||
local options = { | |||
cssClass = logoClass, | |||
errorContext = template._errorContext | |||
) | } | ||
-- Add any additional options from logoOptions | |||
if logoOptions then | |||
for k, v in pairs(logoOptions) do | |||
options[k] = v | |||
end | |||
end | |||
-- Create args object with logo value | |||
local logoArgs = { | |||
logo = logoValue | |||
} | |||
return TemplateHelpers.renderLogoBlock(logoArgs, options) | |||
end, | end, | ||
EMPTY_STRING, | EMPTY_STRING, | ||
| Line 311: | Line 475: | ||
local fieldDefs = template.config.fields or {} | local fieldDefs = template.config.fields or {} | ||
local fields = {} | local fields = {} | ||
-- Get property mappings from semantics config | |||
local propertyMappings = template.config.semantics and | |||
template.config.semantics.properties or {} | |||
-- Add special processor for logo field when logo feature is enabled | |||
if not template._processors then | |||
template._processors = p.initializeProcessors(template) | |||
end | |||
-- If logo feature is enabled, add a processor to skip the logo field | |||
if template.features.logo then | |||
template._processors.logo = function() return false end | |||
end | |||
-- Process field values using appropriate processors | -- Process field values using appropriate processors | ||
| Line 316: | Line 494: | ||
-- Skip hidden fields | -- Skip hidden fields | ||
if not field.hidden then | if not field.hidden then | ||
local fieldKey = field.key or (field.keys and field.keys[1] or "unknown") | |||
local fieldValue = p.processField(template, field, args) | local fieldValue = p.processField(template, field, args) | ||
| Line 329: | Line 508: | ||
end | end | ||
-- | -- Create a processors table for renderFieldsBlock | ||
local processors = {} | |||
for _, field in ipairs(fields) do | |||
template. | local fieldKey = field.label or field.key or (field.keys and field.keys[1] or "unknown") | ||
processors[fieldKey] = function(value, args) | |||
return field.value | |||
end | |||
end | |||
-- Use renderFieldsBlock which generates only rows, not a complete table | |||
-- Pass property mappings for tooltip generation | |||
local result = TemplateHelpers.renderFieldsBlock( | |||
args, | |||
fieldDefs, | |||
template._processors, | |||
propertyMappings | |||
) | ) | ||
return result | |||
end, | end, | ||
'', | '', | ||
| Line 349: | Line 542: | ||
'StandardBlock_socialMedia', | 'StandardBlock_socialMedia', | ||
function() | function() | ||
return | -- Use lazily loaded SocialMedia module | ||
return getSocialMedia().render(args) | |||
end, | end, | ||
'', | '', | ||
| Line 417: | Line 608: | ||
-- @return table The initialized blocks | -- @return table The initialized blocks | ||
function p.initializeBlocks(template) | function p.initializeBlocks(template) | ||
local customBlocks = template.config.blocks or {} | local customBlocks = template.config.blocks or {} | ||
local blocks = {} | local blocks = {} | ||
local blockSequence = template.config.blockSequence or p.standardBlockSequence | local blockSequence = template.config.blockSequence or p.standardBlockSequence | ||
for _, blockId in ipairs(blockSequence) do | for _, blockId in ipairs(blockSequence) do | ||
blocks[blockId] = customBlocks[blockId] or p.standardBlocks[blockId] | blocks[blockId] = customBlocks[blockId] or p.standardBlocks[blockId] | ||
end | end | ||
template._blocks = blocks | template._blocks = blocks | ||
return blocks | return blocks | ||
end | end | ||
| Line 452: | Line 634: | ||
end | end | ||
if block.feature and not template.features[block.feature] then | if block.feature and not template.features[block.feature] then | ||
return nil -- Feature is disabled | return nil -- Feature is disabled | ||
| Line 465: | Line 646: | ||
function p.buildRenderingSequence(template) | function p.buildRenderingSequence(template) | ||
local sequence = template.config.blockSequence or p.standardBlockSequence | local sequence = template.config.blockSequence or p.standardBlockSequence | ||
local renderingFunctions = {} | local renderingFunctions = {} | ||
local funcIndex = 1 | local funcIndex = 1 | ||
for _, blockId in ipairs(sequence) do | for _, blockId in ipairs(sequence) do | ||
| Line 483: | Line 659: | ||
end | end | ||
renderingFunctions._length = funcIndex - 1 | renderingFunctions._length = funcIndex - 1 | ||
return renderingFunctions | return renderingFunctions | ||
end | end | ||
-- ========== Field Processing System ========== | -- ========== Field Processing System ========== | ||
-- Initialize processors for a template | -- Initialize processors for a template | ||
-- @param template table The template object | -- @param template table The template object | ||
-- @return table The initialized processors | -- @return table The initialized processors | ||
function p.initializeProcessors(template) | function p.initializeProcessors(template) | ||
if template._processors then | if template._processors then | ||
return template._processors | return template._processors | ||
end | end | ||
template._processors = TemplateFieldProcessor.initializeProcessors(template) | |||
return template._processors | |||
template._processors = | |||
return template._processors | |||
end | end | ||
| Line 677: | Line 682: | ||
-- @return string The processed field value | -- @return string The processed field value | ||
function p.processField(template, field, args) | function p.processField(template, field, args) | ||
if not field then | |||
return EMPTY_STRING | |||
end | |||
-- | -- Initialize processors if needed | ||
if not | if not template._processors then | ||
template._processors = p.initializeProcessors(template) | |||
end | end | ||
-- | -- Use the TemplateFieldProcessor module with error context | ||
return p.protectedExecute( | return p.protectedExecute( | ||
template, | template, | ||
' | 'processField', | ||
function() | function() | ||
return | return TemplateFieldProcessor.processField(template, field, args, template._errorContext) | ||
end, | end, | ||
EMPTY_STRING, | |||
template, | |||
field, | |||
args | args | ||
) | ) | ||
| Line 702: | Line 706: | ||
-- ========== Preprocessing Pipeline ========== | -- ========== Preprocessing Pipeline ========== | ||
-- Standard preprocessors | -- Standard preprocessors | ||
p.preprocessors = { | p.preprocessors = { | ||
| Line 708: | Line 711: | ||
deriveRegionFromCountry = function(template, args) | deriveRegionFromCountry = function(template, args) | ||
if (not args.region or args.region == "") and args.country then | if (not args.region or args.region == "") and args.country then | ||
-- | -- Split multi-value country string into individual countries | ||
local | local regions = {} | ||
local seen = {} | |||
for country in string.gmatch(args.country, "[^;]+") do | |||
local trimmed = country:match("^%s*(.-)%s*$") | |||
local region = getCountryData().getRegionByCountry(trimmed) | |||
args.region = | if region and region ~= "(Unrecognized)" and not seen[region] then | ||
table.insert(regions, region) | |||
seen[region] = true | |||
end | |||
end | |||
if #regions > 0 then | |||
args.region = table.concat(regions, "; ") | |||
end | end | ||
end | end | ||
return args | |||
end, | |||
-- Set the ID field to the current page ID | |||
setPageIdField = function(template, args) | |||
-- Get the current page ID and set it in the args table | |||
local pageId = TemplateHelpers.getCurrentPageId() | |||
args.ID = tostring(pageId or "") | |||
args.id = args.ID | |||
return args | return args | ||
end | end | ||
| Line 741: | Line 760: | ||
-- @return table The processed arguments | -- @return table The processed arguments | ||
function p.runPreprocessors(template, args) | function p.runPreprocessors(template, args) | ||
if not template._preprocessors then | if not template._preprocessors or #template._preprocessors == 0 then | ||
return args | return args | ||
end | end | ||
local processedArgs = {} | local processedArgs = {} | ||
for k, v in pairs(args) do | for k, v in pairs(args) do | ||
| Line 752: | Line 770: | ||
end | end | ||
local preprocessorCount = #template._preprocessors | |||
for | |||
for i = 1, preprocessorCount do | |||
local preprocessor = template._preprocessors[i] | |||
elseif | local preprocessorType = type(preprocessor) | ||
if preprocessorType == "function" then | |||
local result = preprocessor(template, processedArgs) | |||
if result then | |||
processedArgs = result | |||
end | |||
elseif preprocessorType == "string" then | |||
local namedPreprocessor = p.preprocessors[preprocessor] | |||
if namedPreprocessor then | |||
local result = namedPreprocessor(template, processedArgs) | |||
if result then | |||
processedArgs = result | |||
end | |||
end | |||
end | end | ||
end | end | ||
| Line 766: | Line 796: | ||
-- ========== Semantic and Category Integration ========== | -- ========== Semantic and Category Integration ========== | ||
-- Register semantic property provider function | -- Register semantic property provider function | ||
-- @param template table The template object | -- @param template table The template object | ||
| Line 781: | Line 810: | ||
end | end | ||
-- | -- Validate property value to prevent SMW parser issues | ||
-- @param value string The value to | -- @param value string The value to validate | ||
-- @return string The validated value | |||
local function validatePropertyValue(value) | |||
-- @return string The | |||
local function | |||
if not value or value == '' then | if not value or value == '' then | ||
return '' | return '' | ||
end | end | ||
-- | -- Convert to string if needed | ||
value = tostring(value) | |||
-- Remove potentially problematic wiki markup | |||
value = value:gsub('{{.-}}', '') -- Remove template calls | |||
value = value:gsub('%[%[Category:.-]]', '') -- Remove categories | |||
-- | -- Escape pipe characters that might break SMW | ||
value = value:gsub('|', '{{!}}') | |||
return | return value | ||
end | end | ||
-- Generate semantic properties for template | -- Generate semantic properties for template | ||
| Line 832: | Line 836: | ||
-- @return string The generated semantic properties HTML | -- @return string The generated semantic properties HTML | ||
function p.generateSemanticProperties(template, args) | function p.generateSemanticProperties(template, args) | ||
if not template.features.semanticProperties then | if not template.features.semanticProperties then | ||
return EMPTY_STRING | return EMPTY_STRING | ||
end | end | ||
local semanticConfig = template.config.semantics or {} | local semanticConfig = template.config.semantics or {} | ||
local properties = semanticConfig.properties or {} | local properties = semanticConfig.properties or {} | ||
local transforms = semanticConfig.transforms or {} | local transforms = semanticConfig.transforms or {} | ||
local additionalProperties = semanticConfig.additionalProperties or {} | local additionalProperties = semanticConfig.additionalProperties or {} | ||
local skipProperties = semanticConfig.skipProperties or {} | |||
if not next(properties) and not next(additionalProperties) and | if not next(properties) and not next(additionalProperties) and | ||
(not template._propertyProviders or #template._propertyProviders == 0) then | (not template._propertyProviders or #template._propertyProviders == 0) then | ||
| Line 849: | Line 851: | ||
end | end | ||
-- | -- Set time budget for semantic property processing (450ms) | ||
local | local startTime = os.clock() | ||
local timeLimit = 0.45 -- seconds | |||
local checkInterval = 10 -- check every N properties | |||
local propertyCounter = 0 | |||
local function checkTimeLimit() | |||
propertyCounter = propertyCounter + 1 | |||
if propertyCounter % checkInterval == 0 then | |||
if os.clock() - startTime > timeLimit then | |||
return true -- time exceeded | |||
end | |||
end | |||
return false | |||
end | end | ||
-- | -- Set options for SemanticAnnotations | ||
local | local semanticOptions = { | ||
transform = transforms | |||
} | |||
-- Build initial property mapping (like original code) | |||
local allProperties = {} | |||
-- | -- Process basic properties - just map property names to field names | ||
for property, param in pairs(properties) do | |||
for property, | if not skipProperties[property] then | ||
-- Just map the property to the field name | |||
-- SemanticAnnotations will handle value extraction and transforms | |||
-- | allProperties[property] = param | ||
value | |||
end | end | ||
end | end | ||
-- Process additional properties | -- Create collector for deduplication of additional properties and providers | ||
local collector = { | |||
seen = {}, -- Track property:value signatures | |||
properties = {}, -- Final deduplicated properties | |||
count = 0 -- Track total property count | |||
} | |||
-- Process additional properties with early deduplication and multi-value handling | |||
for property, fields in pairs(additionalProperties) do | for property, fields in pairs(additionalProperties) do | ||
local transform = transforms[property] | -- Skip properties that are explicitly marked to skip | ||
if not skipProperties[property] then | |||
local transform = transforms[property] | |||
for _, fieldName in ipairs(fields) do | |||
local rawValue = args[fieldName] | |||
if rawValue and rawValue ~= '' then | |||
-- Handle multi-value fields by splitting first | |||
local values | |||
if | if rawValue:find(';') then | ||
-- | values = TemplateHelpers.splitMultiValueString(rawValue) | ||
local | |||
if | |||
else | else | ||
values = {rawValue} | |||
end | |||
-- Process each value individually | |||
for _, singleValue in ipairs(values) do | |||
local trimmedValue = singleValue:match("^%s*(.-)%s*$") | |||
if trimmedValue and trimmedValue ~= '' then | |||
-- Apply transform if available | |||
local finalValue = trimmedValue | |||
if transform then | |||
finalValue = p.protectedExecute( | |||
template, | |||
'Transform_' .. property, | |||
function() return transform(trimmedValue, args, template) end, | |||
trimmedValue, | |||
trimmedValue, | |||
args, | |||
template | |||
) | |||
end | |||
-- Validate and add to collector | |||
finalValue = validatePropertyValue(finalValue) | |||
if finalValue and finalValue ~= '' then | |||
if not collector.properties[property] then | |||
collector.properties[property] = {} | |||
end | |||
table.insert(collector.properties[property], finalValue) | |||
collector.count = collector.count + 1 | |||
end | |||
end | |||
end | end | ||
end | end | ||
| Line 900: | Line 941: | ||
end | end | ||
-- | -- Process property providers with early deduplication | ||
if template._propertyProviders then | if template._propertyProviders then | ||
for _, provider in ipairs(template._propertyProviders) do | for _, provider in ipairs(template._propertyProviders) do | ||
| Line 919: | Line 954: | ||
) | ) | ||
if providerResult then | if providerResult and next(providerResult) then | ||
-- Process provider properties through deduplication | |||
for property, value in pairs(providerResult) do | for property, value in pairs(providerResult) do | ||
if value and value ~= '' then | -- Skip properties marked to skip | ||
if not skipProperties[property] then | |||
if type(value) == "table" then | |||
-- Provider returned an array of values | |||
for _, v in ipairs(value) do | |||
local validated = validatePropertyValue(v) | |||
if validated and validated ~= '' then | |||
if not collector.properties[property] then | |||
collector.properties[property] = {} | |||
end | |||
table.insert(collector.properties[property], validated) | |||
collector.count = collector.count + 1 | |||
end | |||
end | |||
else | |||
-- Provider returned a single value | |||
local validated = validatePropertyValue(value) | |||
if validated and validated ~= '' then | |||
if not collector.properties[property] then | |||
collector.properties[property] = {} | |||
end | |||
table.insert(collector.properties[property], validated) | |||
collector.count = collector.count + 1 | |||
end | |||
end | |||
end | end | ||
end | end | ||
| Line 930: | Line 988: | ||
end | end | ||
-- | -- Process all collected properties in one batch | ||
--[[ | |||
After collecting all mapped and provider properties, override the country and region | |||
entries with normalized values from CountryData.getSemanticCountryRegionProperties. | |||
This final step replaces any literal user input with canonical names, | |||
ensures a single batched SMW call emits deduplicated properties, | |||
and centralizes normalization logic for clarity and consistency. | |||
]] | |||
-- Override raw country/region with normalized names if country field exists | |||
if args.country and args.country ~= '' then | |||
local cr = require('Module:ConfigRepository') | |||
local cd = require('Module:CountryData') | |||
local norm = p.protectedExecute( | |||
template, | |||
'CountryData_Override', | |||
function() return cd.getSemanticCountryRegionProperties(args.country) end, | |||
{}, | |||
args.country | |||
) | |||
if norm then | |||
local countryKey = cr.semanticProperties.country | |||
local regionKey = cr.semanticProperties.region | |||
if norm[countryKey] then | |||
collector.properties[countryKey] = norm[countryKey] | |||
end | |||
if norm[regionKey] then | |||
collector.properties[regionKey] = norm[regionKey] | |||
end | |||
end | |||
end | |||
-- Merge basic properties mapping with deduplicated additional properties | |||
-- Basic properties (allProperties) contains field mappings | |||
-- Additional properties (collector.properties) contains actual values | |||
local finalProperties = {} | |||
-- Copy basic property mappings | |||
for property, fieldName in pairs(allProperties) do | |||
finalProperties[property] = fieldName | |||
end | |||
-- Add deduplicated additional properties (these have actual values) | |||
for property, value in pairs(collector.properties) do | |||
finalProperties[property] = value -- This might be an array of values or a single value | |||
end | |||
-- Process fixed properties | |||
if semanticConfig.fixedProperties and type(semanticConfig.fixedProperties) == 'table' then | |||
for propName, propValue in pairs(semanticConfig.fixedProperties) do | |||
if not skipProperties[propName] then -- Check skipProperties as well | |||
local validatedValue = validatePropertyValue(propValue) | |||
if validatedValue and validatedValue ~= '' then | |||
-- Pass as a single-item array to be processed by the | |||
-- 'Direct array of values' path in SemanticAnnotations.lua | |||
finalProperties[propName] = {validatedValue} | |||
end | |||
end | |||
end | |||
end | |||
-- Add debug info as HTML comment (Phase 1 monitoring) | |||
local basicCount = 0 | |||
for _ in pairs(allProperties) do basicCount = basicCount + 1 end | |||
local debugInfo = string.format( | |||
"<!-- SMW Debug: basic_props=%d, additional_props=%d, unique_signatures=%d -->", | |||
basicCount, | |||
collector.count or 0, | |||
table.maxn(collector.seen or {}) | |||
) | |||
-- Send all properties to SemanticAnnotations in one batch | |||
local semanticOutput = SemanticAnnotations.setSemanticProperties( | |||
args, | |||
finalProperties, | |||
semanticOptions | |||
) | |||
-- Append debug info to output | |||
if semanticOutput and semanticOutput ~= '' then | |||
return semanticOutput .. '\n' .. debugInfo | |||
else | |||
return debugInfo | |||
end | |||
end | end | ||
| Line 953: | Line 1,093: | ||
-- @return string The generated category HTML | -- @return string The generated category HTML | ||
function p.generateCategories(template, args) | function p.generateCategories(template, args) | ||
if not template.features.categories then | if not template.features.categories then | ||
return EMPTY_STRING | return EMPTY_STRING | ||
end | end | ||
local configCategories = {} | |||
if template.config.categories and template.config.categories.base and | |||
type(template.config.categories.base) == "table" then | |||
configCategories = template.config.categories.base | |||
elseif template.config.categories and type(template.config.categories) == "table" then | |||
if #template.config.categories > 0 then | |||
configCategories = template.config.categories | |||
end | |||
end | |||
if #configCategories == 0 and | if #configCategories == 0 and | ||
(not template._categoryProviders or #template._categoryProviders == 0) then | (not template._categoryProviders or #template._categoryProviders == 0) then | ||
| Line 967: | Line 1,113: | ||
end | end | ||
-- | -- Use a seen table for deduplication | ||
local | local seen = {} | ||
local uniqueCategories = {} | |||
local categoryCount = 0 | |||
-- | -- Add config categories with deduplication | ||
for i = 1, #configCategories do | |||
local category = configCategories[i] | |||
if category and category ~= "" and not seen[category] then | |||
seen[category] = true | |||
categoryCount = categoryCount + 1 | |||
uniqueCategories[categoryCount] = category | |||
end | |||
end | end | ||
-- | -- Process provider categories with deduplication | ||
if template._categoryProviders then | if template._categoryProviders then | ||
for _, provider in ipairs(template._categoryProviders) do | for _, provider in ipairs(template._categoryProviders) do | ||
| Line 997: | Line 1,142: | ||
if providerCategories then | if providerCategories then | ||
for _, category in ipairs(providerCategories) do | for _, category in ipairs(providerCategories) do | ||
if category and category ~= "" and not seen[category] then | |||
seen[category] = true | |||
categoryCount = categoryCount + 1 | |||
uniqueCategories[categoryCount] = category | |||
end | |||
end | end | ||
end | end | ||
| Line 1,004: | Line 1,152: | ||
end | end | ||
-- Generate | -- Generate HTML for unique categories | ||
local categoryHtml = {} | local categoryHtml = {} | ||
for i = 1, | for i = 1, categoryCount do | ||
categoryHtml[i] = CATEGORY_PREFIX .. | categoryHtml[i] = CATEGORY_PREFIX .. uniqueCategories[i] .. CATEGORY_SUFFIX | ||
end | end | ||
return table.concat(categoryHtml, NEWLINE) | return table.concat(categoryHtml, NEWLINE) | ||
end | end | ||
-- ========== Template Rendering ========== | -- ========== Template Rendering ========== | ||
-- Main rendering function for templates | -- Main rendering function for templates | ||
-- @param template table The template object | -- @param template table The template object | ||
| Line 1,021: | Line 1,167: | ||
-- @return string The rendered template HTML | -- @return string The rendered template HTML | ||
function p.renderTemplate(template, frame) | function p.renderTemplate(template, frame) | ||
-- | template.current_frame = frame -- Store frame on template instance | ||
-- Generate a unique ID for the title element for ARIA | |||
local pageId = TemplateHelpers.getCurrentPageId() or '0' | |||
template.titleId = 'template-title-' .. template.type .. '-' .. pageId | |||
-- Check recursion depth to prevent infinite loops | |||
local depth = 0 | |||
if frame.args and frame.args._recursion_depth then | |||
depth = tonumber(frame.args._recursion_depth) or 0 | |||
elseif frame:getParent() and frame:getParent().args and frame:getParent().args._recursion_depth then | |||
depth = tonumber(frame:getParent().args._recursion_depth) or 0 | |||
end | |||
if depth > 3 then | |||
return '<span class="error">Template recursion depth exceeded (limit: 3)</span>' | |||
end | |||
if not template._errorContext then | if not template._errorContext then | ||
template._errorContext = p.createErrorContext(template) | template._errorContext = p.createErrorContext(template) | ||
end | end | ||
ErrorHandling.addStatus(template._errorContext, "LuaTemplateBlueprint", "Now rendering " .. template.type) | |||
if not template.config.meta then | if not template.config.meta then | ||
p.initializeConfig(template) | p.initializeConfig(template) | ||
end | end | ||
-- | local args = frame:getParent().args or {} | ||
args = TemplateHelpers.normalizeArgumentCase(args) | |||
-- Increment recursion depth for any child template calls | |||
args._recursion_depth = tostring(depth + 1) | |||
args = p.runPreprocessors(template, args) | args = p.runPreprocessors(template, args) | ||
local tableClass = DEFAULT_TABLE_CLASS | |||
if template.config.constants and template.config.constants.tableClass then | |||
tableClass = template.config.constants.tableClass | |||
end | |||
local structureConfig = { | |||
tableClass = tableClass, | |||
blocks = {}, | |||
containerTag = template.features.fullPage and "div" or "table", | |||
ariaLabelledBy = template.titleId | |||
} | |||
local renderingSequence = p.buildRenderingSequence(template) | local renderingSequence = p.buildRenderingSequence(template) | ||
if renderingSequence._length == 0 then | if renderingSequence._length == 0 then | ||
return EMPTY_STRING | return EMPTY_STRING | ||
end | end | ||
for i = 1, renderingSequence._length do | for i = 1, renderingSequence._length do | ||
table.insert(structureConfig.blocks, function(a) | |||
return renderingSequence[i](a) | |||
end) | |||
end | |||
end | end | ||
-- | local result = TemplateStructure.render(args, structureConfig, template._errorContext) | ||
-- Append status and error divs to the final output | |||
result = result .. ErrorHandling.formatCombinedOutput(template._errorContext) | |||
template.current_frame = nil -- Clear frame from template instance | |||
return result | |||
return | |||
end | end | ||
-- Return the module | -- Return the module | ||
-- Initialize elements after module load | |||
p.initializeElements() | |||
return p | return p | ||
Latest revision as of 19:52, 1 August 2025
Documentation for this module may be created at Module:LuaTemplateBlueprint/doc
--[[
* LuaTemplateBlueprint.lua
* Provides a unified foundation for all ICANNWiki templates with feature toggling
*
* This module standardizes template architecture, provides feature toggling,
* centralizes common functionality, and improves maintainability.
*
* Integration with other modules:
* - ErrorHandling: All operations are protected with centralized error handling
* - ConfigRepository: Templates load configuration from this central repository
* - TemplateHelpers: Common utilities for rendering and normalization
* - TemplateStructure: Block-based rendering engine
* - TemplateFieldProcessor: Field processing and value retrieval
* - CountryData: Country normalization and region derivation
* - SocialMedia: Social media icon rendering
*
* Feature Configuration:
* Templates must explicitly specify which features they want to use when registering:
*
* local template = Blueprint.registerTemplate('TemplateName', {
* features = {
* title = true,
* fields = true,
* semanticProperties = true,
* categories = true,
* errorReporting = true
* -- logo and socialMedia intentionally disabled
* }
* })
*
* Alternatively, templates can use predefined feature sets:
*
* local template = Blueprint.registerTemplate('TemplateName', {
* features = Blueprint.featureSets.minimal
* })
*
* Available feature sets:
* - minimal: Just title, fields, and error reporting
* - standard: All features enabled
* - content: Standard without social media
* - data: Just fields, semantics, categories, and error reporting
*
* Note on parameter handling:
* Template parameters are extracted using frame:getParent().args and normalized
* with TemplateHelpers.normalizeArgumentCase() for case-insensitive access.
]]
local p = {}
-- ========== Constants as Upvalues ==========
-- Common empty string for fast returns
local EMPTY_STRING = ''
-- Common separator for concatenation
local NEWLINE = '\n'
-- Common semicolon separator for multi-values
local SEMICOLON = ';'
-- Common category prefix/suffix
local CATEGORY_PREFIX = '[[Category:'
local CATEGORY_SUFFIX = ']]'
-- Template class prefixes
local TEMPLATE_TITLE_CLASS_PREFIX = 'template-title template-title-'
local TEMPLATE_LOGO_CLASS_PREFIX = 'template-logo template-logo-'
-- Default table class
local DEFAULT_TABLE_CLASS = 'template-table'
-- Wiki link pattern for regex matching
local WIKI_LINK_PATTERN = '%[%[.-%]%]'
-- Empty table for returning when needed
local EMPTY_OBJECT = {}
-- ========== Required modules ==========
-- Core modules that are always needed
local ErrorHandling = require('Module:ErrorHandling')
local ConfigRepository = require('Module:ConfigRepository')
local ConfigHelpers = require('Module:ConfigHelpers')
local TemplateHelpers = require('Module:TemplateHelpers')
local TemplateStructure = require('Module:TemplateStructure')
local SemanticAnnotations = require('Module:SemanticAnnotations')
local LinkParser = require('Module:LinkParser')
local TemplateFieldProcessor = require('Module:TemplateFieldProcessor')
local mw = mw -- MediaWiki API
-- Module-level caches for expensive operations
local featureCache = {}
local moduleCache = {} -- Cache for lazy-loaded modules
-- Lazy module loader
local function lazyRequire(moduleName)
return function()
if not moduleCache[moduleName] then
moduleCache[moduleName] = require(moduleName)
end
return moduleCache[moduleName]
end
end
-- Lazily loaded modules
local getSocialMedia = lazyRequire('Module:SocialMedia')
local getNormalizationDate = lazyRequire('Module:NormalizationDate')
local getCountryData = lazyRequire('Module:CountryData')
-- ========== Template Registry ==========
-- Registry to store all registered templates
p.registry = {}
-- Element registry for custom Blueprint elements
p.elementRegistry = {}
-- Register and lookup functions for Blueprint elements
function p.registerElement(name, module)
if not name or not module then return end
p.elementRegistry[name] = module
return module
end
-- Get element from registry by name
-- This function is used internally by createElementBlock; it may appear unused in T-* templates, but is essential for the Element system
function p.getElement(name)
return p.elementRegistry[name]
end
-- Create a Blueprint block for a registered element
function p.createElementBlock(name)
local element = p.getElement(name)
if not element or not element.createBlock then return end
return {
feature = name,
render = function(template, args)
return p.protectedExecute(
template,
"ElementBlock_" .. name,
function()
return element.createBlock()(template, args)
end,
"",
args
)
end
}
end
-- Add a registered element's block to a template at a given position
function p.addElementToTemplate(template, name, _)
local block = p.createElementBlock(name)
if not block then return false end
template.config.blocks = template.config.blocks or {}
template.config.blocks[name] = block
template.features[name] = true
-- Rebuild blockSequence, only append if placeholder not present
local baseSeq = template.config.blockSequence or p.standardBlockSequence
local seq = {}
local found = false
for _, b in ipairs(baseSeq) do
table.insert(seq, b)
if b == name then found = true end
end
if not found then
table.insert(seq, name)
end
template.config.blockSequence = seq
return true
end
-- Automatically initialize and register built-in elements
function p.initializeElements()
local context = ErrorHandling.createContext("LuaTemplateBlueprint")
local mod = ErrorHandling.safeRequire(context, 'Module:ElementNavigation', false)
if mod and mod.elementName then
p.registerElement(mod.elementName, mod)
end
end
-- ========== Feature Management ==========
-- Templates must explicitly include the features they want to use
-- Create a cache key for features
-- Internal helper function: Used only by initializeFeatures to generate cache keys for feature sets
-- @param featureOverrides table Optional table of feature overrides
-- @return string Cache key
local function createFeatureCacheKey(featureOverrides)
if not featureOverrides or not next(featureOverrides) then
return 'default'
end
local keyCount = 0
for _ in pairs(featureOverrides) do keyCount = keyCount + 1 end
local keys = {}
local keyIndex = 1
for k in pairs(featureOverrides) do
keys[keyIndex] = k
keyIndex = keyIndex + 1
end
table.sort(keys)
local parts = {}
for i = 1, keyCount do
local k = keys[i]
parts[i] = k .. '=' .. tostring(featureOverrides[k])
end
return table.concat(parts, ',')
end
-- Initialize feature toggles for a template
-- @param featureOverrides table Table of feature configurations
-- @return table The initialized features
function p.initializeFeatures(featureOverrides)
-- If no features are specified, return an empty features table
-- This ensures templates must explicitly define their features
if not featureOverrides then
return {}
end
-- Check if we have a cached version of this feature set
local cacheKey = createFeatureCacheKey(featureOverrides)
if featureCache[cacheKey] then
-- Return a copy of the cached features to prevent modification
local features = {}
for k, v in pairs(featureCache[cacheKey]) do
features[k] = v
end
return features
end
-- Create a new features table with only the specified features
local features = {}
for featureId, enabled in pairs(featureOverrides) do
features[featureId] = enabled
end
-- Cache the features for future use
featureCache[cacheKey] = {}
for k, v in pairs(features) do
featureCache[cacheKey][k] = v
end
return features
end
-- ========== Template Registration ==========
-- Register a new template
-- @param templateType string The template type (e.g., "Event", "Person", "TLD")
-- @param config table Configuration overrides for the template
-- @return table The registered template object
function p.registerTemplate(templateType, config)
local template = {
type = templateType,
config = config or {},
features = p.initializeFeatures(config and config.features or nil)
}
template.render = function(frame)
return p.renderTemplate(template, frame)
end
p.registry[templateType] = template
-- Default provider for country/region categories
p.registerCategoryProvider(template, function(template, args)
local cats = {}
local SCH = require('Module:SemanticCategoryHelpers')
local CD = require('Module:CountryData')
if args.country and args.country ~= '' then
local TH = require('Module:TemplateHelpers')
cats = SCH.addMultiValueCategories(
args.country,
CD.normalizeCountryName,
cats,
{ valueGetter = function(v)
return TH.splitMultiValueString(v, TH.SEMICOLON_PATTERN)
end }
)
end
if args.region and args.region ~= '' then
cats = SCH.addMultiValueCategories(args.region, nil, cats)
end
return cats
end)
-- Shared provider for conditional and mapping categories
-- Iterates 'config.categories.conditional' to add categories when template args are non-empty
-- Iterates 'config.mappings' to compute mapping categories via SemanticCategoryHelpers
-- Uses addMappingCategories to normalize values and generate category links
-- Centralizes category logic for all templates in a single provider function
p.registerCategoryProvider(template, function(template, args)
local cats = {}
-- Conditional categories
for param, category in pairs(template.config.categories.conditional or {}) do
if args[param] and args[param] ~= "" then
table.insert(cats, category)
end
end
-- Mapping categories
local SCH = require('Module:SemanticCategoryHelpers')
for key, mappingDef in pairs(template.config.mappings or {}) do
local value = args[key]
for _, cat in ipairs(SCH.addMappingCategories(value, mappingDef) or {}) do
table.insert(cats, cat)
end
end
return cats
end)
return template
end
-- ========== Error Handling Integration ==========
-- Create an error context for a template
-- @param template table The template object
-- @return table The error context
function p.createErrorContext(template)
local context = ErrorHandling.createContext(template.type .. "Template")
template._errorContext = context
return context
end
-- Execute a function with error protection
-- @param template table The template object
-- @param functionName string Name of the function being protected
-- @param func function The function to execute
-- @param fallback any The fallback value if an error occurs
-- @param ... any Arguments to pass to the function
-- @return any The result of the function or fallback
function p.protectedExecute(template, functionName, func, fallback, ...)
if not template._errorContext then
template._errorContext = p.createErrorContext(template)
end
return ErrorHandling.protect(
template._errorContext,
functionName,
func,
fallback,
...
)
end
-- ========== Configuration Integration ==========
-- Initialize the standard configuration for a template
-- Combines base config from ConfigRepository with template overrides
-- @param template table The template object
-- @return table The complete configuration
function p.initializeConfig(template)
local templateType = template.type
local configOverrides = template.config or {}
-- Get base configuration from repository
local baseConfig = ConfigRepository.getStandardConfig(templateType)
-- Use ConfigHelpers to deep merge configurations
local config = ConfigHelpers.deepMerge(baseConfig, configOverrides)
-- Store complete config in template
template.config = config
-- Auto-normalize arguments for all mappings
for key, mappingDef in pairs(config.mappings or {}) do
p.addPreprocessor(template, function(template, args)
local cf = require('Module:CanonicalForms')
local val = args[key] or ''
local canonical = select(1, cf.normalize(val, mappingDef))
if canonical then args[key] = canonical end
return args
end)
end
return config
end
-- ========== Block Framework ==========
-- Standard sequence of blocks for template rendering
p.standardBlockSequence = {
'title',
'logo',
'fields',
'socialMedia',
'semanticProperties',
'categories',
'errors'
}
-- Standard blocks available to all templates
p.standardBlocks = {
-- Title block - renders the template title
title = {
feature = 'title',
render = function(template, args)
return p.protectedExecute(
template,
'StandardBlock_title',
function()
-- Get title from config if available, otherwise use template type
local titleText = template.type
if template.config.constants and template.config.constants.title then
titleText = template.config.constants.title
end
-- Get template ID from config or use the template name as fallback
local templateId = template.type
if template.config.meta and template.config.meta.templateId then
templateId = template.config.meta.templateId
end
return require('Module:TemplateStructure').renderTitleBlock(
args,
TEMPLATE_TITLE_CLASS_PREFIX .. string.lower(templateId),
titleText,
template.titleId
)
end,
EMPTY_STRING,
args
)
end
},
-- Logo block - renders the template logo/image
logo = {
feature = 'logo',
render = function(template, args)
return p.protectedExecute(
template,
'StandardBlock_logo',
function()
local logoClass = TEMPLATE_LOGO_CLASS_PREFIX .. string.lower(template.type)
local logoOptions = template.config.meta and template.config.meta.logoOptions or {}
-- Use logoField from config or default to 'logo'
local logoField = logoOptions.logoField or 'logo'
local logoValue = args[logoField]
if not logoValue or logoValue == '' then
return EMPTY_STRING
end
-- Create options object for renderLogoBlock
local options = {
cssClass = logoClass,
errorContext = template._errorContext
}
-- Add any additional options from logoOptions
if logoOptions then
for k, v in pairs(logoOptions) do
options[k] = v
end
end
-- Create args object with logo value
local logoArgs = {
logo = logoValue
}
return TemplateHelpers.renderLogoBlock(logoArgs, options)
end,
EMPTY_STRING,
args
)
end
},
-- Fields block - renders all configured fields
fields = {
feature = 'fields',
render = function(template, args)
return p.protectedExecute(
template,
'StandardBlock_fields',
function()
-- Get field definitions from config
local fieldDefs = template.config.fields or {}
local fields = {}
-- Get property mappings from semantics config
local propertyMappings = template.config.semantics and
template.config.semantics.properties or {}
-- Add special processor for logo field when logo feature is enabled
if not template._processors then
template._processors = p.initializeProcessors(template)
end
-- If logo feature is enabled, add a processor to skip the logo field
if template.features.logo then
template._processors.logo = function() return false end
end
-- Process field values using appropriate processors
for _, field in ipairs(fieldDefs) do
-- Skip hidden fields
if not field.hidden then
local fieldKey = field.key or (field.keys and field.keys[1] or "unknown")
local fieldValue = p.processField(template, field, args)
-- Only include fields with values
if fieldValue and fieldValue ~= '' then
table.insert(fields, {
label = field.label or field.key,
value = fieldValue,
class = field.class
})
end
end
end
-- Create a processors table for renderFieldsBlock
local processors = {}
for _, field in ipairs(fields) do
local fieldKey = field.label or field.key or (field.keys and field.keys[1] or "unknown")
processors[fieldKey] = function(value, args)
return field.value
end
end
-- Use renderFieldsBlock which generates only rows, not a complete table
-- Pass property mappings for tooltip generation
local result = TemplateHelpers.renderFieldsBlock(
args,
fieldDefs,
template._processors,
propertyMappings
)
return result
end,
'',
args
)
end
},
-- Social media block - renders social media links
socialMedia = {
feature = 'socialMedia',
render = function(template, args)
return p.protectedExecute(
template,
'StandardBlock_socialMedia',
function()
-- Use lazily loaded SocialMedia module
return getSocialMedia().render(args)
end,
'',
args
)
end
},
-- Semantic properties block - renders semantic properties
semanticProperties = {
feature = 'semanticProperties',
render = function(template, args)
return p.protectedExecute(
template,
'StandardBlock_semanticProperties',
function()
return p.generateSemanticProperties(template, args)
end,
'',
args
)
end
},
-- Categories block - renders category links
categories = {
feature = 'categories',
render = function(template, args)
return p.protectedExecute(
template,
'StandardBlock_categories',
function()
return p.generateCategories(template, args)
end,
'',
args
)
end
},
-- Errors block - renders error messages
errors = {
feature = 'errorReporting',
render = function(template, args)
return p.protectedExecute(
template,
'StandardBlock_errors',
function()
if not template._errorContext then
return ''
end
return ErrorHandling.formatOutput(template._errorContext)
end,
'',
args
)
end
}
}
-- Initialize blocks for a template
-- @param template table The template object
-- @return table The initialized blocks
function p.initializeBlocks(template)
local customBlocks = template.config.blocks or {}
local blocks = {}
local blockSequence = template.config.blockSequence or p.standardBlockSequence
for _, blockId in ipairs(blockSequence) do
blocks[blockId] = customBlocks[blockId] or p.standardBlocks[blockId]
end
template._blocks = blocks
return blocks
end
-- Get block rendering function, respecting feature toggles
-- @param template table The template object
-- @param blockId string The ID of the block to get
-- @return function|nil The rendering function or nil if disabled
function p.getBlockRenderer(template, blockId)
if not template._blocks then
template._blocks = p.initializeBlocks(template)
end
local block = template._blocks[blockId]
if not block then
return nil
end
if block.feature and not template.features[block.feature] then
return nil -- Feature is disabled
end
return block.render
end
-- Build block rendering sequence for template
-- @param template table The template object
-- @return table Array of rendering functions
function p.buildRenderingSequence(template)
local sequence = template.config.blockSequence or p.standardBlockSequence
local renderingFunctions = {}
local funcIndex = 1
for _, blockId in ipairs(sequence) do
local renderer = p.getBlockRenderer(template, blockId)
if renderer then
renderingFunctions[funcIndex] = function(args)
return renderer(template, args)
end
funcIndex = funcIndex + 1
end
end
renderingFunctions._length = funcIndex - 1
return renderingFunctions
end
-- ========== Field Processing System ==========
-- Initialize processors for a template
-- @param template table The template object
-- @return table The initialized processors
function p.initializeProcessors(template)
if template._processors then
return template._processors
end
template._processors = TemplateFieldProcessor.initializeProcessors(template)
return template._processors
end
-- Process a field using its processor
-- @param template table The template object
-- @param field table The field definition
-- @param args table The template arguments
-- @return string The processed field value
function p.processField(template, field, args)
if not field then
return EMPTY_STRING
end
-- Initialize processors if needed
if not template._processors then
template._processors = p.initializeProcessors(template)
end
-- Use the TemplateFieldProcessor module with error context
return p.protectedExecute(
template,
'processField',
function()
return TemplateFieldProcessor.processField(template, field, args, template._errorContext)
end,
EMPTY_STRING,
template,
field,
args
)
end
-- ========== Preprocessing Pipeline ==========
-- Standard preprocessors
p.preprocessors = {
-- Derive region from country values
deriveRegionFromCountry = function(template, args)
if (not args.region or args.region == "") and args.country then
-- Split multi-value country string into individual countries
local regions = {}
local seen = {}
for country in string.gmatch(args.country, "[^;]+") do
local trimmed = country:match("^%s*(.-)%s*$")
local region = getCountryData().getRegionByCountry(trimmed)
if region and region ~= "(Unrecognized)" and not seen[region] then
table.insert(regions, region)
seen[region] = true
end
end
if #regions > 0 then
args.region = table.concat(regions, "; ")
end
end
return args
end,
-- Set the ID field to the current page ID
setPageIdField = function(template, args)
-- Get the current page ID and set it in the args table
local pageId = TemplateHelpers.getCurrentPageId()
args.ID = tostring(pageId or "")
args.id = args.ID
return args
end
}
-- Register a preprocessor with a template
-- @param template table The template object
-- @param preprocessor function|string The preprocessor function or name
-- @return table The template object (for chaining)
function p.addPreprocessor(template, preprocessor)
-- Initialize preprocessors array if not exists
template._preprocessors = template._preprocessors or {}
-- Add preprocessor to array
table.insert(template._preprocessors, preprocessor)
return template
end
-- Run all preprocessors in sequence
-- @param template table The template object
-- @param args table The template arguments
-- @return table The processed arguments
function p.runPreprocessors(template, args)
if not template._preprocessors or #template._preprocessors == 0 then
return args
end
local processedArgs = {}
for k, v in pairs(args) do
processedArgs[k] = v
end
local preprocessorCount = #template._preprocessors
for i = 1, preprocessorCount do
local preprocessor = template._preprocessors[i]
local preprocessorType = type(preprocessor)
if preprocessorType == "function" then
local result = preprocessor(template, processedArgs)
if result then
processedArgs = result
end
elseif preprocessorType == "string" then
local namedPreprocessor = p.preprocessors[preprocessor]
if namedPreprocessor then
local result = namedPreprocessor(template, processedArgs)
if result then
processedArgs = result
end
end
end
end
return processedArgs
end
-- ========== Semantic and Category Integration ==========
-- Register semantic property provider function
-- @param template table The template object
-- @param provider function The provider function
-- @return table The template object (for chaining)
function p.registerPropertyProvider(template, provider)
-- Initialize property providers array if not exists
template._propertyProviders = template._propertyProviders or {}
-- Add provider to array
table.insert(template._propertyProviders, provider)
return template
end
-- Validate property value to prevent SMW parser issues
-- @param value string The value to validate
-- @return string The validated value
local function validatePropertyValue(value)
if not value or value == '' then
return ''
end
-- Convert to string if needed
value = tostring(value)
-- Remove potentially problematic wiki markup
value = value:gsub('{{.-}}', '') -- Remove template calls
value = value:gsub('%[%[Category:.-]]', '') -- Remove categories
-- Escape pipe characters that might break SMW
value = value:gsub('|', '{{!}}')
return value
end
-- Generate semantic properties for template
-- @param template table The template object
-- @param args table The template arguments
-- @return string The generated semantic properties HTML
function p.generateSemanticProperties(template, args)
if not template.features.semanticProperties then
return EMPTY_STRING
end
local semanticConfig = template.config.semantics or {}
local properties = semanticConfig.properties or {}
local transforms = semanticConfig.transforms or {}
local additionalProperties = semanticConfig.additionalProperties or {}
local skipProperties = semanticConfig.skipProperties or {}
if not next(properties) and not next(additionalProperties) and
(not template._propertyProviders or #template._propertyProviders == 0) then
return EMPTY_STRING
end
-- Set time budget for semantic property processing (450ms)
local startTime = os.clock()
local timeLimit = 0.45 -- seconds
local checkInterval = 10 -- check every N properties
local propertyCounter = 0
local function checkTimeLimit()
propertyCounter = propertyCounter + 1
if propertyCounter % checkInterval == 0 then
if os.clock() - startTime > timeLimit then
return true -- time exceeded
end
end
return false
end
-- Set options for SemanticAnnotations
local semanticOptions = {
transform = transforms
}
-- Build initial property mapping (like original code)
local allProperties = {}
-- Process basic properties - just map property names to field names
for property, param in pairs(properties) do
if not skipProperties[property] then
-- Just map the property to the field name
-- SemanticAnnotations will handle value extraction and transforms
allProperties[property] = param
end
end
-- Create collector for deduplication of additional properties and providers
local collector = {
seen = {}, -- Track property:value signatures
properties = {}, -- Final deduplicated properties
count = 0 -- Track total property count
}
-- Process additional properties with early deduplication and multi-value handling
for property, fields in pairs(additionalProperties) do
-- Skip properties that are explicitly marked to skip
if not skipProperties[property] then
local transform = transforms[property]
for _, fieldName in ipairs(fields) do
local rawValue = args[fieldName]
if rawValue and rawValue ~= '' then
-- Handle multi-value fields by splitting first
local values
if rawValue:find(';') then
values = TemplateHelpers.splitMultiValueString(rawValue)
else
values = {rawValue}
end
-- Process each value individually
for _, singleValue in ipairs(values) do
local trimmedValue = singleValue:match("^%s*(.-)%s*$")
if trimmedValue and trimmedValue ~= '' then
-- Apply transform if available
local finalValue = trimmedValue
if transform then
finalValue = p.protectedExecute(
template,
'Transform_' .. property,
function() return transform(trimmedValue, args, template) end,
trimmedValue,
trimmedValue,
args,
template
)
end
-- Validate and add to collector
finalValue = validatePropertyValue(finalValue)
if finalValue and finalValue ~= '' then
if not collector.properties[property] then
collector.properties[property] = {}
end
table.insert(collector.properties[property], finalValue)
collector.count = collector.count + 1
end
end
end
end
end
end
end
-- Process property providers with early deduplication
if template._propertyProviders then
for _, provider in ipairs(template._propertyProviders) do
local providerResult = p.protectedExecute(
template,
'PropertyProvider',
function() return provider(template, args) end,
{},
template,
args
)
if providerResult and next(providerResult) then
-- Process provider properties through deduplication
for property, value in pairs(providerResult) do
-- Skip properties marked to skip
if not skipProperties[property] then
if type(value) == "table" then
-- Provider returned an array of values
for _, v in ipairs(value) do
local validated = validatePropertyValue(v)
if validated and validated ~= '' then
if not collector.properties[property] then
collector.properties[property] = {}
end
table.insert(collector.properties[property], validated)
collector.count = collector.count + 1
end
end
else
-- Provider returned a single value
local validated = validatePropertyValue(value)
if validated and validated ~= '' then
if not collector.properties[property] then
collector.properties[property] = {}
end
table.insert(collector.properties[property], validated)
collector.count = collector.count + 1
end
end
end
end
end
end
end
-- Process all collected properties in one batch
--[[
After collecting all mapped and provider properties, override the country and region
entries with normalized values from CountryData.getSemanticCountryRegionProperties.
This final step replaces any literal user input with canonical names,
ensures a single batched SMW call emits deduplicated properties,
and centralizes normalization logic for clarity and consistency.
]]
-- Override raw country/region with normalized names if country field exists
if args.country and args.country ~= '' then
local cr = require('Module:ConfigRepository')
local cd = require('Module:CountryData')
local norm = p.protectedExecute(
template,
'CountryData_Override',
function() return cd.getSemanticCountryRegionProperties(args.country) end,
{},
args.country
)
if norm then
local countryKey = cr.semanticProperties.country
local regionKey = cr.semanticProperties.region
if norm[countryKey] then
collector.properties[countryKey] = norm[countryKey]
end
if norm[regionKey] then
collector.properties[regionKey] = norm[regionKey]
end
end
end
-- Merge basic properties mapping with deduplicated additional properties
-- Basic properties (allProperties) contains field mappings
-- Additional properties (collector.properties) contains actual values
local finalProperties = {}
-- Copy basic property mappings
for property, fieldName in pairs(allProperties) do
finalProperties[property] = fieldName
end
-- Add deduplicated additional properties (these have actual values)
for property, value in pairs(collector.properties) do
finalProperties[property] = value -- This might be an array of values or a single value
end
-- Process fixed properties
if semanticConfig.fixedProperties and type(semanticConfig.fixedProperties) == 'table' then
for propName, propValue in pairs(semanticConfig.fixedProperties) do
if not skipProperties[propName] then -- Check skipProperties as well
local validatedValue = validatePropertyValue(propValue)
if validatedValue and validatedValue ~= '' then
-- Pass as a single-item array to be processed by the
-- 'Direct array of values' path in SemanticAnnotations.lua
finalProperties[propName] = {validatedValue}
end
end
end
end
-- Add debug info as HTML comment (Phase 1 monitoring)
local basicCount = 0
for _ in pairs(allProperties) do basicCount = basicCount + 1 end
local debugInfo = string.format(
"<!-- SMW Debug: basic_props=%d, additional_props=%d, unique_signatures=%d -->",
basicCount,
collector.count or 0,
table.maxn(collector.seen or {})
)
-- Send all properties to SemanticAnnotations in one batch
local semanticOutput = SemanticAnnotations.setSemanticProperties(
args,
finalProperties,
semanticOptions
)
-- Append debug info to output
if semanticOutput and semanticOutput ~= '' then
return semanticOutput .. '\n' .. debugInfo
else
return debugInfo
end
end
-- Register category provider function
-- @param template table The template object
-- @param provider function The provider function
-- @return table The template object (for chaining)
function p.registerCategoryProvider(template, provider)
-- Initialize category providers array if not exists
template._categoryProviders = template._categoryProviders or {}
-- Add provider to array
table.insert(template._categoryProviders, provider)
return template
end
-- Generate categories for template
-- @param template table The template object
-- @param args table The template arguments
-- @return string The generated category HTML
function p.generateCategories(template, args)
if not template.features.categories then
return EMPTY_STRING
end
local configCategories = {}
if template.config.categories and template.config.categories.base and
type(template.config.categories.base) == "table" then
configCategories = template.config.categories.base
elseif template.config.categories and type(template.config.categories) == "table" then
if #template.config.categories > 0 then
configCategories = template.config.categories
end
end
if #configCategories == 0 and
(not template._categoryProviders or #template._categoryProviders == 0) then
return EMPTY_STRING
end
-- Use a seen table for deduplication
local seen = {}
local uniqueCategories = {}
local categoryCount = 0
-- Add config categories with deduplication
for i = 1, #configCategories do
local category = configCategories[i]
if category and category ~= "" and not seen[category] then
seen[category] = true
categoryCount = categoryCount + 1
uniqueCategories[categoryCount] = category
end
end
-- Process provider categories with deduplication
if template._categoryProviders then
for _, provider in ipairs(template._categoryProviders) do
local providerCategories = p.protectedExecute(
template,
'CategoryProvider',
function() return provider(template, args) end,
{},
template,
args
)
if providerCategories then
for _, category in ipairs(providerCategories) do
if category and category ~= "" and not seen[category] then
seen[category] = true
categoryCount = categoryCount + 1
uniqueCategories[categoryCount] = category
end
end
end
end
end
-- Generate HTML for unique categories
local categoryHtml = {}
for i = 1, categoryCount do
categoryHtml[i] = CATEGORY_PREFIX .. uniqueCategories[i] .. CATEGORY_SUFFIX
end
return table.concat(categoryHtml, NEWLINE)
end
-- ========== Template Rendering ==========
-- Main rendering function for templates
-- @param template table The template object
-- @param frame Frame The MediaWiki frame object
-- @return string The rendered template HTML
function p.renderTemplate(template, frame)
template.current_frame = frame -- Store frame on template instance
-- Generate a unique ID for the title element for ARIA
local pageId = TemplateHelpers.getCurrentPageId() or '0'
template.titleId = 'template-title-' .. template.type .. '-' .. pageId
-- Check recursion depth to prevent infinite loops
local depth = 0
if frame.args and frame.args._recursion_depth then
depth = tonumber(frame.args._recursion_depth) or 0
elseif frame:getParent() and frame:getParent().args and frame:getParent().args._recursion_depth then
depth = tonumber(frame:getParent().args._recursion_depth) or 0
end
if depth > 3 then
return '<span class="error">Template recursion depth exceeded (limit: 3)</span>'
end
if not template._errorContext then
template._errorContext = p.createErrorContext(template)
end
ErrorHandling.addStatus(template._errorContext, "LuaTemplateBlueprint", "Now rendering " .. template.type)
if not template.config.meta then
p.initializeConfig(template)
end
local args = frame:getParent().args or {}
args = TemplateHelpers.normalizeArgumentCase(args)
-- Increment recursion depth for any child template calls
args._recursion_depth = tostring(depth + 1)
args = p.runPreprocessors(template, args)
local tableClass = DEFAULT_TABLE_CLASS
if template.config.constants and template.config.constants.tableClass then
tableClass = template.config.constants.tableClass
end
local structureConfig = {
tableClass = tableClass,
blocks = {},
containerTag = template.features.fullPage and "div" or "table",
ariaLabelledBy = template.titleId
}
local renderingSequence = p.buildRenderingSequence(template)
if renderingSequence._length == 0 then
return EMPTY_STRING
end
for i = 1, renderingSequence._length do
table.insert(structureConfig.blocks, function(a)
return renderingSequence[i](a)
end)
end
local result = TemplateStructure.render(args, structureConfig, template._errorContext)
-- Append status and error divs to the final output
result = result .. ErrorHandling.formatCombinedOutput(template._errorContext)
template.current_frame = nil -- Clear frame from template instance
return result
end
-- Return the module
-- Initialize elements after module load
p.initializeElements()
return p