Module:LuaTemplateBlueprint
Appearance
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 = '%[%[.-%]%]'
-- Semantic property HTML templates
local PROPERTY_DIV_PREFIX = '<div style="display:none;">\n {{#set: '
local PROPERTY_DIV_SUFFIX = ' }}\n</div>'
-- 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 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 ==========
-- Standard configuration sections used by templates
p.configSections = {
'meta',
'categories',
'patterns',
'fields',
'mappings',
'constants',
'semantics'
}
-- 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)
-- Apply overrides to each section
local config = {}
for _, section in ipairs(p.configSections) do
config[section] = config[section] or {}
-- Copy base config for this section if available
if baseConfig[section] then
for k, v in pairs(baseConfig[section]) do
config[section][k] = v
end
end
-- Apply overrides for this section if available
if configOverrides[section] then
for k, v in pairs(configOverrides[section]) do
config[section][k] = v
end
end
end
-- 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
)
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
-- Get field value from args (delegated to TemplateHelpers)
-- @param field table The field definition
-- @param args table The template arguments
-- @return string|nil The field value or nil if not found
function p.getFieldValue(field, args, template)
local _, value = TemplateHelpers.getFieldValue(args, field)
return value
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
-- 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
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"
}
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)
template.current_frame = nil -- Clear frame from template instance
return result
end
-- Return the module
-- Initialize elements after module load
p.initializeElements()
return p