Module:LuaTemplateEvent
Appearance
Documentation for this module may be created at Module:LuaTemplateEvent/doc
-- Module:LuaTemplateEvent
-- Renders Event templates with automatic event navigation and normalized data. Features:
-- * Auto-detects previous/next events based on series name and numbering
-- * Normalizes dates, countries, and website links
-- * Auto-derives regions from countries using CountryData
-- * Assigns categories based on country, region, and subject
-- * Integrates social media components
-- * Includes error handling and graceful failure recovery using ErrorHandling module
local p = {}
--------------------------------------------------------------------------------
-- Error Handling System
--------------------------------------------------------------------------------
-- Using the centralized ErrorHandling module for error tracking, reporting, and
-- graceful failure handling.
local ErrorHandling = require('Module:ErrorHandling')
-- Error context for this template
local ERROR_CONTEXT = nil
-- Initialize the error context at the start of rendering
local function initErrorContext()
-- Create a new error context
ERROR_CONTEXT = ErrorHandling.createContext("EventTemplate")
return ERROR_CONTEXT
end
-- Add an error to the context (wrapper for ErrorHandling.addError)
-- @param source The source of the error (function name)
-- @param message The error message
-- @param details Optional additional details about the error
-- @param isCritical Whether this is a critical error (default: false)
local function addError(source, message, details, isCritical)
return ErrorHandling.addError(ERROR_CONTEXT, source, message, details, isCritical)
end
-- Format the error context for debugging output (wrapper for ErrorHandling.formatOutput)
local function formatErrorOutput()
return ErrorHandling.formatOutput(ERROR_CONTEXT)
end
-- Create emergency display for catastrophic failures (wrapper for ErrorHandling.createEmergencyDisplay)
-- @param args The template arguments
-- @param errorSource The source of the error
-- @param errorMessage The error message
local function createEmergencyDisplay(args, errorSource, errorMessage)
return ErrorHandling.createEmergencyDisplay(args, errorSource, errorMessage, "Event")
end
-- Protected require function to gracefully handle missing dependencies
local function safeRequire(moduleName)
return ErrorHandling.safeRequire(ERROR_CONTEXT, moduleName, true)
end
-- Dependencies
local CountryData = safeRequire('Module:CountryData')
local dateNormalization = safeRequire('Module:NormalizationDate')
local linkParser = safeRequire('Module:LinkParser')
local socialFooter = safeRequire('Module:SocialMedia')
local TemplateStructure = safeRequire('Module:TemplateStructure')
local TemplateHelpers = safeRequire('Module:TemplateHelpers')
local SemanticAnnotations = safeRequire('Module:SemanticAnnotations')
local ConfigRepository = safeRequire('Module:ConfigRepository')
local SemanticCategoryHelpers = safeRequire('Module:SemanticCategoryHelpers')
--------------------------------------------------------------------------------
-- Configuration and Constants
--------------------------------------------------------------------------------
-- Safely load configuration with fallback
local Config = {}
do
-- Attempt to load standard config
local success, config = pcall(function()
return ConfigRepository.getStandardConfig('Event')
end)
if success then
Config = config
else
-- Record error
addError(
"ConfigLoading",
"Failed to load Event configuration",
success or "Unknown error",
true
)
-- Set minimal fallback configuration
Config = {
fields = {},
semantics = {
properties = {},
transforms = {},
additionalProperties = {}
},
patterns = {
seriesNumber = "([^%d]+)%s+(%d+)$",
seriesYear = "([^%d]+)%s+(%d%d%d%d)$"
}
}
end
end
--------------------------------------------------------------------------------
-- Helper Functions
--------------------------------------------------------------------------------
-- Use TemplateHelpers for common functions
local getFieldValue = TemplateHelpers.getFieldValue
local splitMultiValueString = SemanticCategoryHelpers.splitMultiValueString
local normalizeWebsites = TemplateHelpers.normalizeWebsites
local normalizeDates = TemplateHelpers.normalizeDates
-- Wrapper for country normalization
local normalizeCountries = TemplateHelpers.normalizeCountries
-- Cache for event navigation detection results
local navigationCache = {}
-- Event navigation detection function with caching
local function detectEventNavigation(pageName)
-- Check cache first
if navigationCache[pageName] ~= nil then
return navigationCache[pageName]
end
local result = nil
-- Try Series + Number pattern: "ICANN 76"
local series, number = pageName:match(Config.patterns.seriesNumber)
if series and number then
number = tonumber(number)
local prev = (number > 1) and string.format("%s %d", series, number - 1) or nil
local next = string.format("%s %d", series, number + 1)
-- Create page objects to check if they exist
local prevPage = prev and mw.title.new(prev) or nil
local nextPage = mw.title.new(next)
result = {
prev = prevPage and prevPage.exists and prev or nil,
next = nextPage and nextPage.exists and next or nil
}
end
-- If no result yet, try Series + Year pattern: "IGF 2023"
if not result then
local series, year = pageName:match(Config.patterns.seriesYear)
if series and year then
year = tonumber(year)
local prev = string.format("%s %d", series, year - 1)
local next = string.format("%s %d", series, year + 1)
-- Create page objects to check if they exist
local prevPage = mw.title.new(prev)
local nextPage = mw.title.new(next)
result = {
prev = prevPage and prevPage.exists and prev or nil,
next = nextPage and nextPage.exists and next or nil
}
end
end
-- Store in cache (including nil results)
navigationCache[pageName] = result
return result
end
--------------------------------------------------------------------------------
-- Block Rendering Functions
--------------------------------------------------------------------------------
local function renderTitleBlock(args)
return TemplateHelpers.renderTitleBlock(args, "template-title template-title-event", "Event", {
achievementSupport = false
})
end
local function renderLogoBlock(args)
return TemplateHelpers.renderLogoBlock(args, {
cssClass = "event-logo",
imageParams = "220px|center",
errorContext = ERROR_CONTEXT
})
end
local function renderFieldsBlock(args)
-- Define field processors
local processors = {
logo = function() return false end, -- Skip logo field as it's handled by renderLogoBlock
website = normalizeWebsites,
url = normalizeWebsites, -- Add URL field processor to use the LinkParser
-- Date fields
start = function(value)
-- Sanitize the date input
value = TemplateHelpers.sanitizeUserInput(value)
-- Get the end date if available
local endValue = args["end"]
if endValue and endValue ~= "" then
endValue = TemplateHelpers.sanitizeUserInput(endValue)
end
-- Use the new formatDateRange function
return TemplateHelpers.formatDateRange(value, endValue, {
outputMode = "complete"
})
end,
["end"] = function()
-- Always skip the end date field since it's incorporated into the start date field
return false
end,
-- Location fields
country = function(value)
-- Sanitize country input
value = TemplateHelpers.sanitizeUserInput(value)
return normalizeCountries(value)
end,
territory = function(value)
-- Sanitize territory input
value = TemplateHelpers.sanitizeUserInput(value)
return normalizeCountries(value)
end,
city = function(value)
-- Sanitize city input
return TemplateHelpers.sanitizeUserInput(value)
end,
venue = function(value)
-- Sanitize venue input
return TemplateHelpers.sanitizeUserInput(value)
end,
-- Organization fields
organizer = function(value)
-- Sanitize organizer input
return TemplateHelpers.sanitizeUserInput(value)
end,
-- Category/subject fields
subject = function(value)
-- Sanitize subject input
return TemplateHelpers.sanitizeUserInput(value)
end,
category = function(value)
-- Sanitize category input
return TemplateHelpers.sanitizeUserInput(value)
end,
process = function(value)
-- Sanitize process input
return TemplateHelpers.sanitizeUserInput(value)
end
}
return TemplateHelpers.renderFieldsBlock(args, Config.fields, processors)
end
local function renderNavigationBlock(args, autoNavigation)
-- Check for user-provided navigation values
local hasPrev = args["has_previous_event"]
local hasNext = args["has_next_event"]
-- If no navigation is provided at all, return empty string
if (not hasPrev or hasPrev == "") and (not hasNext or hasNext == "") and not autoNavigation then
return ""
end
-- Handle specific page names vs. yes/no values
local prevPage = nil
local nextPage = nil
-- Determine previous event page
if hasPrev and hasPrev ~= "" then
if hasPrev ~= "yes" and hasPrev ~= "true" then
-- If it's a specific page name, use it directly
prevPage = hasPrev
elseif autoNavigation and autoNavigation.prev then
-- If it's "yes" and auto-detection found something, use that
prevPage = autoNavigation.prev
end
elseif autoNavigation and autoNavigation.prev then
-- Fall back to auto-detection if no manual value is set
prevPage = autoNavigation.prev
end
-- Determine next event page
if hasNext and hasNext ~= "" then
if hasNext ~= "yes" and hasNext ~= "true" then
-- If it's a specific page name, use it directly
nextPage = hasNext
elseif autoNavigation and autoNavigation.next then
-- If it's "yes" and auto-detection found something, use that
nextPage = autoNavigation.next
end
elseif autoNavigation and autoNavigation.next then
-- Fall back to auto-detection if no manual value is set
nextPage = autoNavigation.next
end
-- If no actual navigation links were found, return empty string
-- This prevents creating empty space when there are no links to display
if not prevPage and not nextPage then
return ""
end
-- Create a navigation row with fixed height to ensure proper vertical centering
-- Pre-allocate output table with known initial size
local output = {
'|-',
'| class="element-navigation-prev" height="40" valign="middle" |'
}
-- Add previous event link (left-aligned and centered vertically)
if prevPage then
table.insert(output, string.format(
'<div class="element-navigation-prev">[[%s|← Previous]]</div>',
prevPage
))
else
-- Empty cell if no previous event
table.insert(output, " ")
end
-- Add next event link cell (right-aligned and centered vertically)
table.insert(output, '| class="element-navigation-next" height="40" valign="middle" |')
if nextPage then
table.insert(output, string.format(
'<div class="element-navigation-next">[[%s|Next →]]</div>',
nextPage
))
else
-- Empty cell if no next event
table.insert(output, " ")
end
-- Use table.concat for efficient string building
return table.concat(output, "\n")
end
--------------------------------------------------------------------------------
-- Semantic Properties
--------------------------------------------------------------------------------
-- Generate semantic properties for the Event with error handling
local function generateSemanticProperties(args)
-- Use ErrorHandling.protect instead of pcall directly
return ErrorHandling.protect(
ERROR_CONTEXT,
"SemanticGeneration",
function()
-- Create a modified args table without subject/category
-- This is temporary until rules for handling multiple subjects are determined
-- Also sanitize all values to remove wiki markup and braces
local modifiedArgs = {}
for k, v in pairs(args) do
-- Skip subject and category fields
if k ~= "subject" and k ~= "category" then
-- Sanitize the value before adding it to modifiedArgs
if type(v) == "string" and v ~= "" then
modifiedArgs[k] = TemplateHelpers.sanitizeUserInput(v)
else
modifiedArgs[k] = v
end
end
end
-- Set basic properties using the transformation functions from Config
local semanticOutput
local propertiesSuccess, propertiesResult = pcall(function()
return SemanticAnnotations.setSemanticProperties(
modifiedArgs,
Config.semantics.properties,
{transform = Config.semantics.transforms}
)
end)
if propertiesSuccess then
semanticOutput = propertiesResult
else
addError(
"SemanticProperties",
"Failed to set basic semantic properties",
propertiesResult,
false -- Not critical for rendering
)
semanticOutput = "" -- Empty fallback
end
-- For non-SMW case, collect property HTML fragments in a table for efficient concatenation
local propertyHtml = {}
-- Handle special case for multiple countries using the helper function
local countryValue = args["country"] or args["territory"]
if countryValue and countryValue ~= "" then
local countrySuccess, countryResult = pcall(function()
return CountryData.addCountrySemanticProperties(countryValue, semanticOutput)
end)
if countrySuccess then
semanticOutput = countryResult
else
addError(
"CountryProperties",
"Failed to add country semantic properties",
countryResult,
false -- Not critical for rendering
)
end
end
-- Handle special case for multiple regions using the unified function (if applicable)
if args.region and args.region ~= "" then
local regionSuccess, regionResult = pcall(function()
return SemanticCategoryHelpers.addSemanticProperties("region", args.region, semanticOutput)
end)
if regionSuccess then
semanticOutput = regionResult
else
addError(
"RegionProperties",
"Failed to add region semantic properties",
regionResult,
false -- Not critical for rendering
)
end
end
-- Process additional properties with multi-value support
-- Skip properties that are handled separately above
local additionalSuccess, additionalResult = pcall(function()
return SemanticCategoryHelpers.processAdditionalProperties(
args,
Config.semantics,
semanticOutput,
Config.semantics.skipProperties
)
end)
if additionalSuccess then
semanticOutput = additionalResult
else
addError(
"AdditionalProperties",
"Failed to process additional semantic properties",
additionalResult,
false -- Not critical for rendering
)
end
-- For non-SMW case, concatenate all property HTML fragments at once
if not mw.smw and #propertyHtml > 0 then
semanticOutput = semanticOutput .. "\n" .. table.concat(propertyHtml, "\n")
end
return semanticOutput
end,
"" -- Return empty string as fallback
)
end
--------------------------------------------------------------------------------
-- Category Assignment
--------------------------------------------------------------------------------
local function computeCategories(args)
-- Use ErrorHandling.protect instead of pcall directly
return ErrorHandling.protect(
ERROR_CONTEXT,
"CategoryComputation",
function()
local cats = {}
-- Base Event category from config
if Config.categories and Config.categories.base then
for _, category in ipairs(Config.categories.base) do
table.insert(cats, category)
end
end
-- Process country categories from either country or territory key using CountryData
local countryValue = args["country"] or args["territory"]
if countryValue and countryValue ~= "" then
local countrySuccess, countryCats = pcall(function()
return SemanticCategoryHelpers.addMultiValueCategories(
countryValue,
function(value)
-- Use the CountryData module directly
return CountryData.normalizeCountryName(value)
end,
cats
)
end)
if countrySuccess then
cats = countryCats
else
addError(
"CountryCategories",
"Failed to add country categories",
countryCats,
false -- Not critical for rendering
)
end
end
-- Process region categories using the generic function
if args["region"] and args["region"] ~= "" then
local regionSuccess, regionCats = pcall(function()
return SemanticCategoryHelpers.addMultiValueCategories(
args["region"],
function(region) return region:match("^%s*(.-)%s*$") end, -- Trim whitespace
cats
)
end)
if regionSuccess then
cats = regionCats
else
addError(
"RegionCategories",
"Failed to add region categories",
regionCats,
false -- Not critical for rendering
)
end
end
-- Add process category if provided
if args["process"] and args["process"] ~= "" then
-- Trim whitespace and add as a category
local processCategory = args["process"]:match("^%s*(.-)%s*$")
if processCategory and processCategory ~= "" then
table.insert(cats, processCategory)
end
end
-- NOTE: Subject/category categories are temporarily disabled
-- as per request due to users adding too many comma-separated values
-- Build categories safely
local buildSuccess, categories = pcall(function()
return SemanticCategoryHelpers.buildCategories(cats)
end)
if buildSuccess then
return categories
else
addError(
"BuildCategories",
"Failed to build categories",
categories,
false -- Not critical for rendering
)
return ""
end
end,
"" -- Return empty string as fallback
)
end
--------------------------------------------------------------------------------
-- Main Render Function
--------------------------------------------------------------------------------
-- Execute a function with protection and fallback using ErrorHandling.protect
local function safeExecute(functionName, func, ...)
return ErrorHandling.protect(ERROR_CONTEXT, functionName, func, nil, ...)
end
function p.render(frame)
-- Initialize error tracking
initErrorContext()
-- Outer pcall to ensure we always return something, even in catastrophic failure
local success, result = pcall(function()
local args = frame:getParent().args or {}
-- Normalize arguments for case-insensitivity
args = TemplateHelpers.normalizeArgumentCase(args)
-- Get template name from frame if available
local title = mw.title.getCurrentTitle()
if title then
-- Extract template name from page title (usually in Module:LuaTemplateX format)
local templateName = title.text:match("LuaTemplate([^/]+)") or "Event"
ERROR_CONTEXT.TEMPLATE_NAME = templateName .. "Template"
end
-- Safely derive region from country, overriding any user-provided value
if args.country then
local success, _ = pcall(function()
-- Use splitMultiValueString for consistent multi-value handling
local countryList = splitMultiValueString(args.country, SemanticCategoryHelpers.SEMICOLON_PATTERN)
-- Get regions for each country
local regions = {}
local unique = {}
for _, country in ipairs(countryList) do
local region = CountryData.getRegionByCountry(country)
if region and region ~= "(Unrecognized)" and not unique[region] then
unique[region] = true
table.insert(regions, region)
end
end
-- Use table.concat for efficient string building
if #regions > 0 then
-- Override any user-provided region value
args.region = table.concat(regions, " and ")
end
end)
if not success then
addError(
"RegionDerivation",
"Failed to derive region from country",
args.country,
false -- Not critical
)
end
end
local titleObj = mw.title.getCurrentTitle()
local pageName = titleObj and titleObj.text or ""
-- Detect navigation links for previous/next events
local autoNavigation = detectEventNavigation(pageName)
-- Only set auto-detected values if user hasn't provided specific values
if autoNavigation then
-- Check for previous event - only set if not already provided by user
if autoNavigation.prev and (not args["has_previous_event"] or args["has_previous_event"] == "") then
args["has_previous_event"] = "yes"
end
-- Check for next event - only set if not already provided by user
if autoNavigation.next and (not args["has_next_event"] or args["has_next_event"] == "") then
args["has_next_event"] = "yes"
end
end
-- Protected block rendering - this is the most critical part of the template
local outputComponents = {}
-- Safely execute each block rendering function
local function executeBlock(name, func, ...)
local blockContent = safeExecute(name, func, ...)
return blockContent or "" -- Return empty string as fallback
end
-- Configure block rendering
local config = {
blocks = {
function(a) return executeBlock("TitleBlock", renderTitleBlock, a) end,
function(a) return executeBlock("LogoBlock", renderLogoBlock, a) end,
function(a) return executeBlock("FieldsBlock", renderFieldsBlock, a) end,
function(a) return executeBlock("NavigationBlock", renderNavigationBlock, a, autoNavigation) end,
function(a) return executeBlock("SocialFooter", socialFooter.render, a) or "" end
}
}
-- Generate template output with protected rendering
-- Pass the ERROR_CONTEXT to TemplateStructure.render
local success, output = pcall(function()
return TemplateStructure.render(args, config, ERROR_CONTEXT)
end)
if not success then
-- If TemplateStructure.render fails, use emergency rendering
addError(
"TemplateStructure",
"Failed to render template structure",
output,
true -- This is a critical error
)
-- Create a minimal table structure as fallback
output = '{| class="template-table" cellpadding="2"\n'
-- Add the title at minimum
local titleSuccess, titleOutput = pcall(renderTitleBlock, args)
output = output .. (titleSuccess and titleOutput or "|-\n| class=\"template-title template-title-event\"><span>" .. (args.name or "Unnamed Event") .. "</span>")
-- Close the table
output = output .. "\n|}"
end
-- Start with main output
table.insert(outputComponents, output)
-- Safely add categories
local categories = computeCategories(args)
if categories and categories ~= "" then
table.insert(outputComponents, categories)
end
-- Safely add semantic properties
local semantics = generateSemanticProperties(args)
if semantics and semantics ~= "" then
table.insert(outputComponents, semantics)
end
-- Add error debug information if any errors occurred
if ERROR_CONTEXT.ERROR_COUNT > 0 then
-- Add data attribute div with error information
table.insert(outputComponents, formatErrorOutput())
-- Add minimal visual indicator for errors
table.insert(outputComponents, string.format(
"\n|-\n|colspan=\"2\" style=\"text-align:right; font-size:9px; color:#999;\"" ..
"|<span title=\"%d error(s)\">⚠</span>",
ERROR_CONTEXT.ERROR_COUNT
))
end
-- Use table.concat for efficient string building
return table.concat(outputComponents, "\n")
end)
-- Final fallback for catastrophic failures
if not success then
-- Log the catastrophic error
ERROR_CONTEXT.HAS_CRITICAL_ERROR = true
addError(
"CatastrophicFailure",
"Template rendering failed completely",
result,
true
)
-- Create emergency display with minimal information
local emergencyOutput = createEmergencyDisplay(
{name = "Event"}, -- Minimal args
"render",
tostring(result)
)
-- Add error debug information
local errorOutput = formatErrorOutput()
-- Return emergency output with debug information
return emergencyOutput .. "\n" .. errorOutput
end
return result
end
return p