Module:LuaTemplateBlueprint
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
* - SemanticAnnotations: Semantic property generation
*
* Note on parameter handling:
* - Template parameters are extracted and normalized by TemplateHelpers.extractArgs()
* - This function handles case-insensitive parameter names for better user experience
* - Parameters are accessible via args[paramName] regardless of case used in the template
]]
local p = {}
-- ========== Required modules ==========
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 mw = mw -- MediaWiki API
-- Module-level caches for expensive operations
local featureCache = {}
local processorCache = {}
-- ========== Template Registry ==========
-- Registry to store all registered templates
p.registry = {}
-- ========== Feature Management ==========
-- Default features for all templates
p.defaultFeatures = {
-- Core rendering features
title = true,
logo = true,
fields = true,
socialMedia = true,
-- Semantic features
semanticProperties = true,
categories = true,
-- Error handling
errorReporting = true
}
-- Create a cache key for features
-- @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
-- Create stable key from overrides
local parts = {}
local keys = {}
for k in pairs(featureOverrides) do
table.insert(keys, k)
end
table.sort(keys)
for _, k in ipairs(keys) do
table.insert(parts, k .. '=' .. tostring(featureOverrides[k]))
end
return table.concat(parts, ',')
end
-- Initialize feature toggles for a template
-- @param featureOverrides table Optional table of feature overrides
-- @return table The initialized features
function p.initializeFeatures(featureOverrides)
-- Fast path for common case
if not featureOverrides then
-- Return a copy of the default features
local features = {}
for k, v in pairs(p.defaultFeatures) do
features[k] = v
end
return features
end
-- Check cache first
local cacheKey = createFeatureCacheKey(featureOverrides)
if featureCache[cacheKey] then
-- Return a copy of the cached features
local features = {}
for k, v in pairs(featureCache[cacheKey]) do
features[k] = v
end
return features
end
-- Create new feature set
local features = {}
-- First copy default features
for featureId, enabled in pairs(p.defaultFeatures) do
features[featureId] = enabled
end
-- Then apply overrides
for featureId, enabled in pairs(featureOverrides) do
features[featureId] = enabled
end
-- Cache the result
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)
-- Create template instance with features
local template = {
type = templateType,
config = config or {},
features = p.initializeFeatures(config and config.features or nil)
}
-- Add template methods
template.render = function(frame)
return p.renderTemplate(template, frame)
end
-- Store in registry
p.registry[templateType] = template
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
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()
return TemplateHelpers.renderTitleBlock(
args,
'template-title template-title-' .. string.lower(template.type),
template.type,
template.config.meta and template.config.meta.titleOptions or {}
)
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 template-logo-' .. 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
return TemplateHelpers.renderLogoBlock(
logoValue,
logoClass,
logoOptions
)
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 = {}
-- Process field values using appropriate processors
for _, field in ipairs(fieldDefs) do
-- Skip hidden fields
if not field.hidden then
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
-- Render the fields using TemplateStructure
return TemplateStructure.renderFieldTable(
fields,
template.config.meta and template.config.meta.fieldOptions or {}
)
end,
'',
args
)
end
},
-- Social media block - renders social media links
socialMedia = {
feature = 'socialMedia',
render = function(template, args)
return p.protectedExecute(
template,
'StandardBlock_socialMedia',
function()
return TemplateHelpers.renderSocialMediaBlock(
args,
template.config.meta and template.config.meta.socialMediaOptions or {}
)
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)
-- Get custom blocks from template config
local customBlocks = template.config.blocks or {}
-- Combine with standard blocks, preferring custom implementations
local blocks = {}
-- Use template's block sequence if provided, otherwise use standard
local blockSequence = template.config.blockSequence or p.standardBlockSequence
-- Initialize with blocks in sequence
for _, blockId in ipairs(blockSequence) do
-- Use custom block if available, otherwise use standard
blocks[blockId] = customBlocks[blockId] or p.standardBlocks[blockId]
end
-- Store blocks in template
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
-- Check if feature is enabled
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
-- Pre-allocate with estimated size
local renderingFunctions = {}
local funcIndex = 1
-- Cache template features for faster lookup in loop
local features = template.features
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
-- Store sequence length for optimization
renderingFunctions._length = funcIndex - 1
return renderingFunctions
end
-- ========== Field Processing System ==========
-- Standard field processors
p.standardProcessors = {
-- Identity processor (default, returns value unchanged)
identity = function(value, args, template)
return value
end,
-- Website processor
website = function(value, args, template)
return TemplateHelpers.normalizeWebsites(value)
end,
-- Country processor
country = function(value, args, template)
return TemplateHelpers.normalizeCountries(value)
end,
-- Date processor
date = function(value, args, template)
return TemplateHelpers.normalizeDate(value)
end,
-- Language processor
language = function(value, args, template)
return TemplateHelpers.normalizeLanguages(value)
end,
-- Text processor (wikifies text)
text = function(value, args, template)
return TemplateHelpers.wikifyText(value)
end,
-- Email processor
email = function(value, args, template)
return TemplateHelpers.normalizeEmail(value)
end,
-- Multi-value processor
multivalue = function(value, args, template)
return TemplateHelpers.normalizeMultiValue(value)
end
}
-- Create a cache key for processors
-- @param template table The template object
-- @return string Cache key
local function createProcessorCacheKey(template)
-- For simple case without custom processors
if not template.config.processors or not next(template.config.processors) then
return template.type .. '_default'
end
-- For case with custom processors, create stable cache key
local parts = {template.type}
local keys = {}
for k in pairs(template.config.processors) do
table.insert(keys, k)
end
table.sort(keys)
for _, k in ipairs(keys) do
table.insert(parts, k)
end
return table.concat(parts, '_')
end
-- Initialize processors for a template
-- @param template table The template object
-- @return table The initialized processors
function p.initializeProcessors(template)
-- Check if already initialized
if template._processors then
return template._processors
end
-- Use cache if available
local cacheKey = createProcessorCacheKey(template)
if processorCache[cacheKey] then
-- Create wrapper functions with template context
local wrappedProcessors = {}
for processorId, processor in pairs(processorCache[cacheKey]) do
wrappedProcessors[processorId] = function(value, args)
return processor(value, args, template)
end
end
-- Store in template
template._processors = wrappedProcessors
return wrappedProcessors
end
-- Get custom processors from template config
local customProcessors = template.config.processors or {}
-- Combine with standard processors, preferring custom implementations
local processors = {}
-- Pre-estimate size for better performance
local estimatedSize = 0
for _ in pairs(p.standardProcessors) do estimatedSize = estimatedSize + 1 end
for _ in pairs(customProcessors) do estimatedSize = estimatedSize + 1 end
-- First, copy standard processors
for processorId, processor in pairs(p.standardProcessors) do
processors[processorId] = processor
end
-- Then apply custom processors, overriding standard ones if needed
for processorId, processor in pairs(customProcessors) do
processors[processorId] = processor
end
-- Store in cache
processorCache[cacheKey] = processors
-- Create wrapped processors that pass template context
local wrappedProcessors = {}
for processorId, processor in pairs(processors) do
wrappedProcessors[processorId] = function(value, args)
return processor(value, args, template)
end
end
-- Store in template
template._processors = wrappedProcessors
return wrappedProcessors
end
-- Get processor for a field
-- @param template table The template object
-- @param field table The field definition
-- @return function The processor function
function p.getFieldProcessor(template, field)
if not template._processors then
template._processors = p.initializeProcessors(template)
end
-- Check if field has explicit processor
if field.processor and template._processors[field.processor] then
return template._processors[field.processor]
end
-- Check if there's a processor matching the field key
local fieldKey = field.key or (field.keys and field.keys[1])
if fieldKey and template._processors[fieldKey] then
return template._processors[fieldKey]
end
-- Fall back to identity processor
return template._processors.identity
end
-- Get field value from args
-- @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)
-- If field has a single key, get that value
if field.key then
return args[field.key]
end
-- If field has multiple keys, try each in order
if field.keys then
for _, key in ipairs(field.keys) do
local value = args[key]
if value and value ~= '' then
return value
end
end
end
return nil
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)
-- Get the raw field value
local value = p.getFieldValue(field, args)
-- If no value or empty, return empty string
if not value or value == '' then
return ''
end
-- Get the processor for this field
local processor = p.getFieldProcessor(template, field)
-- Process the value
return p.protectedExecute(
template,
'FieldProcessor_' .. (field.key or 'unknown'),
function()
return processor(value, args)
end,
value, -- fallback to original value on error
value,
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
-- Load CountryData module
local CountryData = require('Module:CountryData')
-- Handle the country-to-region mapping
local region = CountryData.getRegionForCountry(args.country)
if region and region ~= "" then
args.region = region
end
end
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 no preprocessors, return args unchanged
if not template._preprocessors then
return args
end
-- Copy args to avoid modifying original
local processedArgs = {}
for k, v in pairs(args) do
processedArgs[k] = v
end
-- Apply each preprocessor in sequence
for _, preprocessor in ipairs(template._preprocessors) do
if type(preprocessor) == "function" then
processedArgs = preprocessor(template, processedArgs) or processedArgs
elseif type(preprocessor) == "string" and p.preprocessors[preprocessor] then
-- Look up named preprocessor
processedArgs = p.preprocessors[preprocessor](template, processedArgs) or processedArgs
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
-- Process a value using a transform function
-- @param value string The value to transform
-- @param property string The property name (for error context)
-- @param transform function The transform function
-- @param args table The template arguments
-- @param template table The template object
-- @return string The transformed value
local function applyTransform(value, property, transform, args, template)
if not value or value == '' then
return ''
end
-- Skip transform if missing
if not transform then
return value
end
-- Apply transformation with error protection
local transformedValue = p.protectedExecute(
template,
'Transform_' .. property,
function() return transform(value, args, template) end,
value, -- fallback to original on error
value,
args,
template
)
return transformedValue or ''
end
-- ========== 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
local CATEGORY_PREFIX = '[[Category:'
local CATEGORY_SUFFIX = ']]'
-- 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)
-- Early return if feature is disabled (fast path)
if not template.features.semanticProperties then
return EMPTY_STRING
end
-- Get semantic configuration
local semanticConfig = template.config.semantics or {}
local properties = semanticConfig.properties or {}
local transforms = semanticConfig.transforms or {}
local additionalProperties = semanticConfig.additionalProperties or {}
-- Fast path for empty configuration
if not next(properties) and not next(additionalProperties) and
(not template._propertyProviders or #template._propertyProviders == 0) then
return EMPTY_STRING
end
-- Pre-allocate output table with estimated size
local propertyCount = 0
for _ in pairs(properties) do propertyCount = propertyCount + 1 end
for _, fields in pairs(additionalProperties) do
propertyCount = propertyCount + #fields
end
if template._propertyProviders then
propertyCount = propertyCount + #template._propertyProviders * 2
end
-- Initialize output with estimated size
local propertyHtml = {}
-- Configure standard property mapping
local propertyMapping = {}
for property, field in pairs(properties) do
local value = args[field]
if value and value ~= '' then
-- Apply transformation if available
local transform = transforms[property]
value = applyTransform(value, property, transform, args, template)
if value and value ~= '' then
propertyMapping[property] = value
end
end
end
-- Process additional properties (multiple fields mapping to one property)
for property, fields in pairs(additionalProperties) do
local transform = transforms[property]
for _, field in ipairs(fields) do
local value = args[field]
if value and value ~= '' then
-- Apply transformation
value = applyTransform(value, property, transform, args, template)
if value and value ~= '' then
-- Add to existing value if property already exists
local existingValue = propertyMapping[property]
if existingValue and existingValue ~= '' then
propertyMapping[property] = existingValue .. ';' .. value
else
propertyMapping[property] = value
end
end
end
end
end
-- Set the properties (directly set index rather than table.insert for better performance)
local htmlIndex = 1
for property, value in pairs(propertyMapping) do
propertyHtml[htmlIndex] = SemanticAnnotations.setSemanticProperty(property, value)
htmlIndex = htmlIndex + 1
end
-- Call custom property providers
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 then
for property, value in pairs(providerResult) do
if value and value ~= '' then
propertyHtml[htmlIndex] = SemanticAnnotations.setSemanticProperty(property, value)
htmlIndex = htmlIndex + 1
end
end
end
end
end
-- Return concatenated property HTML
return table.concat(propertyHtml, '\n')
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)
-- Early return if feature is disabled (fast path)
if not template.features.categories then
return EMPTY_STRING
end
-- Get configuration
local configCategories = template.config.categories or {}
-- Fast path for empty configuration
if #configCategories == 0 and
(not template._categoryProviders or #template._categoryProviders == 0) then
return EMPTY_STRING
end
-- Pre-allocate with estimated size
local categoryEstimate = #configCategories
if template._categoryProviders then
categoryEstimate = categoryEstimate + #template._categoryProviders * 2
end
-- Initialize categories array with estimated size
local categories = {}
local categoryIndex = 1
-- Add base categories from config (direct indexing for better performance)
for _, category in ipairs(configCategories) do
categories[categoryIndex] = category
categoryIndex = categoryIndex + 1
end
-- Call custom category providers
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
categories[categoryIndex] = category
categoryIndex = categoryIndex + 1
end
end
end
end
-- Generate category HTML with pre-allocated size
local categoryHtml = {}
for i = 1, categoryIndex - 1 do
categoryHtml[i] = CATEGORY_PREFIX .. categories[i] .. CATEGORY_SUFFIX
end
-- Return concatenated category HTML
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)
-- Create error context if not exists
if not template._errorContext then
template._errorContext = p.createErrorContext(template)
end
-- Initialize config if not already done
if not template.config.meta then
p.initializeConfig(template)
end
-- Extract and normalize arguments
local args = TemplateHelpers.extractArgs(frame)
-- Run preprocessors
args = p.runPreprocessors(template, args)
-- Build rendering sequence
local renderingSequence = p.buildRenderingSequence(template)
-- Fast path for empty sequence
if renderingSequence._length == 0 then
return EMPTY_STRING
end
-- Pre-allocate output array with estimated size
local output = {}
local outputIndex = 1
-- Render each block (use numeric indexing for faster iteration)
for i = 1, renderingSequence._length do
local blockHtml = renderingSequence[i](args)
if blockHtml and blockHtml ~= '' then
output[outputIndex] = blockHtml
outputIndex = outputIndex + 1
end
end
-- Fast path for empty output
if outputIndex == 1 then
return EMPTY_STRING
end
-- Return concatenated output
return table.concat(output, NEWLINE)
end
-- Return the module
return p