Module:LuaTemplatePerson
Appearance
Documentation for this module may be created at Module:LuaTemplatePerson/doc
-- Module:LuaTemplatePerson
-- Renders profiles of people with a carousel for multiple images, supporting various normalizations (countries, links, etc.) and integrating social media. The module dynamically assigns categories based on community and country.
local p = {}
-- Dependencies
local CanonicalForms = require('Module:CanonicalForms')
local CountryData = require('Module:CountryData')
local linkParser = require('Module:LinkParser')
local socialFooter = require('Module:SocialMedia')
local TemplateStructure = require('Module:TemplateStructure')
local TemplateHelpers = require('Module:TemplateHelpers')
local LanguageNormalization = require('Module:NormalizationLanguage')
local SemanticAnnotations = require('Module:SemanticAnnotations')
local ConfigRepository = require('Module:ConfigRepository')
local SemanticCategoryHelpers = require('Module:SemanticCategoryHelpers')
-- Load the Achievement system with error handling
-- Achievement styling is defined in CSS/Templates.css, not in JSON data
local Achievements
local achievementsLoaded = pcall(function()
Achievements = require('Module:AchievementSystem')
end)
if not achievementsLoaded or not Achievements then
Achievements = {
-- Minimal fallback to avoid errors if achievements are unavailable
getTitleClass = function() return '', '' end,
getTitleAchievement = function() return '', '', '' end,
renderAchievementBox = function() return '' end,
hasAchievements = function() return false end,
trackPage = function() return true end
}
end
--------------------------------------------------------------------------------
-- Configuration and Constants
--------------------------------------------------------------------------------
local Config = ConfigRepository.getStandardConfig('Person')
--------------------------------------------------------------------------------
-- Helper Functions
--------------------------------------------------------------------------------
-- Use TemplateHelpers for common functions
local splitMultiValueString = SemanticCategoryHelpers.splitMultiValueString
local getFieldValue = TemplateHelpers.getFieldValue
local normalizeWebsites = TemplateHelpers.normalizeWebsites
--------------------------------------------------------------------------------
-- Block Rendering
--------------------------------------------------------------------------------
-- Title block function returns only the Person row; Achievement header is handled by a separate function for proper row separation
local function renderTitleBlock(args, frame)
local currentTitle = mw.title.getCurrentTitle()
local pageId = currentTitle.id
-- Attempt to get the user's top-tier achievement class + name
local className, achievementName = Achievements.getTitleClass(pageId, frame)
-- Regular title element
local baseClass = "template-title template-title-person person-template"
-- Extract achievement ID if present
local achievementId = ""
if className and className ~= "" then
achievementId = className:gsub("^achievement%-", "") -- e.g. "dev-role" instead of "achievement-dev-role"
end
-- Use the enhanced function with achievement support
return TemplateHelpers.renderTitleBlock(args, baseClass, "Person", {
achievementSupport = true,
achievementClass = className or "",
achievementId = achievementId,
achievementName = achievementName or ""
})
end
-- Separate function to render the achievement header as its own row, only if a title achievement exists, otherwise returns an empty string
local function renderAchievementHeaderBlock(args, frame)
local currentTitle = mw.title.getCurrentTitle()
local pageId = currentTitle.id
-- Use getTitleAchievement function that specifically checks for type="title"
local cssClass, displayName, achievementId = Achievements.getTitleAchievement(pageId, frame)
-- If title achievement found, create a populated row
if cssClass ~= "" and displayName ~= "" and achievementId ~= "" then
-- Create a dedicated achievement row with the title achievement name
return string.format(
'|-\n! colspan="2" class="achievement-header %s" data-achievement-id="%s" data-achievement-name="%s" | %s',
achievementId, achievementId, displayName, displayName
)
else
-- Return empty string to prevent the empty row from taking up space
return ''
end
end
-- Render the carousel
-- Renders portrait images as either a single image or interactive carousel
local function renderPortraitCarousel(args)
-- Return empty string if no portrait specified
if not args.portrait or args.portrait == "" then
return ""
end
-- Split semicolon-separated list of image filenames
local imageFiles = splitMultiValueString(args.portrait, SemanticCategoryHelpers.SEMICOLON_PATTERN)
if #imageFiles == 0 then
return ""
end
-- For single image, render simple centered portrait
if #imageFiles == 1 then
return string.format("|-\n| colspan=\"2\" class=\"person-portrait\" | [[Image:%s|220px|center]]", imageFiles[1])
end
-- Start building carousel container for multiple images
local output = "|-\n| colspan=\"2\" class=\"person-portrait-carousel\" |"
output = output .. '<div class="carousel-container">'
-- Add left navigation arrow for multiple images
if #imageFiles > 1 then
output = output .. '<div class="carousel-nav carousel-prev">◀</div>'
end
-- Container for all carousel images
output = output .. '<div class="carousel-images">'
for i, imageFile in ipairs(imageFiles) do
-- First image visible, others hidden initially
local visibility = (i == 1) and "carousel-visible" or "carousel-hidden"
local position = ""
-- Special positioning for 2-image carousel (orbital layout)
if #imageFiles == 2 then
position = (i == 1) and "carousel-orbital-1" or "carousel-orbital-2"
else
-- For 3+ images: position second image right, last image left
if i == 2 then position = "carousel-right" end
if i == #imageFiles and #imageFiles > 2 then position = "carousel-left" end
end
-- Create image div with appropriate classes and data attributes
output = output .. string.format(
'<div class="carousel-item %s %s" data-index="%d">[[Image:%s|220px|center]]</div>',
visibility, position, i, imageFile
)
end
output = output .. '</div>'
-- Add right navigation arrow for multiple images
if #imageFiles > 1 then
output = output .. '<div class="carousel-nav carousel-next">▶</div>'
end
output = output .. '</div>'
return output
end
-- Renders all achievements for the current user in badge style; always creates a minimal container
local function renderAchievementButtons(args, frame)
local pageId = mw.title.getCurrentTitle().id
if not pageId or pageId == 0 then
-- Create minimal empty container
local html = {}
table.insert(html, '|-\n| colspan="2" |')
table.insert(html, '<div class="achievement-badges"></div>')
return table.concat(html)
end
if not achievementsLoaded or not Achievements then
-- Create minimal empty container
local html = {}
table.insert(html, '|-\n| colspan="2" |')
table.insert(html, '<div class="achievement-badges"></div>')
return table.concat(html)
end
local data = Achievements.loadData()
if not data or not data.user_achievements then
-- Create minimal empty container
local html = {}
table.insert(html, '|-\n| colspan="2" |')
table.insert(html, '<div class="achievement-badges"></div>')
return table.concat(html)
end
-- Get user achievements
local userAchievements = Achievements.getUserAchievements(pageId)
-- Get achievement types from the new loadTypes function
local types = Achievements.loadTypes(frame)
-- Build a table of achievement definitions for quick lookup
local achievementDefinitions = {}
for _, typeData in ipairs(types) do
achievementDefinitions[typeData.id] = typeData
end
-- Find badge achievements only
local badgeAchievements = {}
if #userAchievements > 0 then
for _, achievement in ipairs(userAchievements) do
local achType = achievement.type
if achType then
local typeData = achievementDefinitions[achType]
if typeData and typeData.type == "badge" then
table.insert(badgeAchievements, {
id = achType,
name = typeData.name or achType
})
end
end
end
end
-- Create the container (minimal or populated)
local html = {}
table.insert(html, '|-\n| colspan="2" |')
-- If badge achievements found, create a populated container
if #badgeAchievements > 0 then
table.insert(html, '<div class="achievement-badges">')
-- For each badge achievement, create a badge element
for _, badge in ipairs(badgeAchievements) do
table.insert(html, string.format(
'<div class="achievement-badge %s" data-achievement-id="%s" data-achievement-name="%s" title="%s"></div>',
badge.id, badge.id, badge.name, badge.name
))
end
table.insert(html, '</div>') -- Close achievement-badges
else
-- Create minimal empty container
table.insert(html, '<div class="achievement-badges"></div>')
end
return table.concat(html)
end
-- Dedicated block function for achievement badges to ensure complete decoupling
local function renderAchievementBadgesBlock(args, frame)
-- Simply call the existing renderAchievementButtons function
-- This creates a dedicated block for achievements independent of any other fields
return renderAchievementButtons(args, frame)
end
local function renderFieldsBlock(args, frame)
-- Define field processors with special handling for certain fields
local processors = {
community = function(value)
return select(1, CanonicalForms.normalize(value, Config.mappings.community)) or value
end,
languages = function(value)
return LanguageNormalization.formatLanguages(value)
end,
country = TemplateHelpers.normalizeCountries,
website = normalizeWebsites,
soi = function(value, args)
-- Format SOI link
local formattedValue = string.format("[%s Here]", value)
-- Create a complete HTML object that will be inserted as-is
local html = {}
table.insert(html, string.format(TemplateHelpers.FIELD_FORMAT, "SOI", formattedValue))
-- Return as a complete HTML object to prevent escaping
return {
isCompleteHtml = true,
html = table.concat(html, "\n")
}
end,
userbox = function(value, args, frame)
-- Attempt to show achievements in the legacy "userbox" area
local pageId = mw.title.getCurrentTitle().id
if achievementsLoaded and Achievements and pageId then
local success, achievementBox = pcall(function()
return Achievements.renderAchievementBox(pageId, frame)
end)
if success and achievementBox and achievementBox ~= "" then
return achievementBox
end
end
return value
end
}
-- Use TemplateHelpers.renderFieldsBlock for consistent field rendering
return TemplateHelpers.renderFieldsBlock(args, Config.fields, processors)
end
--------------------------------------------------------------------------------
-- Categories and Semantics
--------------------------------------------------------------------------------
-- Generate semantic properties for the person
local function generateSemanticProperties(args)
-- Set basic properties using the transformation functions from Config
local semanticOutput = SemanticAnnotations.setSemanticProperties(
args,
Config.semantics.properties,
{transform = Config.semantics.transforms}
)
-- For non-SMW case, collect property HTML fragments in a table for efficient concatenation
local propertyHtml = {}
-- Handle special case for multiple countries using the unified function
local countryValue = args.country
semanticOutput = SemanticCategoryHelpers.addSemanticProperties("country", countryValue, semanticOutput)
-- Handle special case for multiple regions using the unified function
semanticOutput = SemanticCategoryHelpers.addSemanticProperties("region", args.region, semanticOutput)
-- Handle special case for multiple languages using the unified function
semanticOutput = SemanticCategoryHelpers.addSemanticProperties("language", args.languages, semanticOutput)
-- Process additional properties with multi-value support
-- Skip properties that are handled separately above
semanticOutput = SemanticCategoryHelpers.processAdditionalProperties(
args,
Config.semantics,
semanticOutput,
Config.semantics.skipProperties
)
-- 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
local function computeCategories(args)
local cats = {}
-- Base Person 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
-- Add community categories
if args.community and args.community ~= "" then
local communityCategories = SemanticCategoryHelpers.addMappingCategories(args.community, Config.mappings.community)
for _, cat in ipairs(communityCategories) do
table.insert(cats, cat)
end
end
-- Add country categories using the generic function
if args.country and args.country ~= "" then
cats = SemanticCategoryHelpers.addMultiValueCategories(
args.country,
nil, -- No processor needed as we use a custom value getter
cats,
{
valueGetter = function(value)
return CountryData.getCountriesForCategories(value)
end
}
)
end
-- Add region categories using the generic function
if args.region and args.region ~= "" then
cats = SemanticCategoryHelpers.addMultiValueCategories(
args.region,
function(region) return region:match("^%s*(.-)%s*$") end, -- Trim whitespace
cats
)
end
-- Add language speaker categories using the generic function
if args.languages and args.languages ~= "" then
cats = SemanticCategoryHelpers.addMultiValueCategories(
args.languages,
function(langCode)
local canonicalLang = LanguageNormalization.normalize(langCode)
if canonicalLang and canonicalLang ~= "" then
return canonicalLang .. " speaker"
end
return nil
end,
cats
)
end
return SemanticCategoryHelpers.buildCategories(cats)
end
--------------------------------------------------------------------------------
-- Main render
--------------------------------------------------------------------------------
function p.render(frame)
local args = frame:getParent().args or {}
-- Normalize arguments for case-insensitivity
args = TemplateHelpers.normalizeArgumentCase(args)
-- Derive region from country if empty
if args.country and (not args.region or args.region == "") then
-- Parse the semicolon-separated country list
local countries = {}
for country in string.gmatch(args.country, "[^;]+") do
local trimmedCountry = country:match("^%s*(.-)%s*$")
if trimmedCountry and trimmedCountry ~= "" then
local region = CountryData.getRegionByCountry(trimmedCountry)
if region and region ~= "(Unrecognized)" then
table.insert(countries, region)
end
end
end
-- Remove duplicates from regions list
local unique = {}
local regions = {}
for _, region in ipairs(countries) do
if not unique[region] then
unique[region] = true
table.insert(regions, region)
end
end
if #regions > 0 then
args.region = table.concat(regions, " and ")
end
end
-- Load achievement data and types with frame for proper JSON loading
if achievementsLoaded and Achievements then
pcall(function()
-- Pre-load achievement data and types with frame
Achievements.loadData(frame)
Achievements.loadTypes(frame)
end)
end
local config = {
blocks = {
function(a) return renderTitleBlock(a, frame) end,
function(a) return renderAchievementHeaderBlock(a, frame) end,
function(a) return renderPortraitCarousel(a) end,
function(a) return renderFieldsBlock(a, frame) end,
function(a) return renderAchievementBadgesBlock(a, frame) end, -- New dedicated block for achievements
function(a) return socialFooter.render(a) or "" end
}
}
-- Track for cache purging
if achievementsLoaded and Achievements then
pcall(function()
local pageId = mw.title.getCurrentTitle().id
local pageName = mw.title.getCurrentTitle().fullText
if pageId then
Achievements.trackPage(pageId, pageName)
end
end)
end
-- Generate template output with categories
local output = TemplateStructure.render(args, config)
-- Add categories
local categories = computeCategories(args)
if categories and categories ~= "" then
output = output .. "\n" .. categories
end
-- Add semantic properties
local semantics = generateSemanticProperties(args)
if semantics and semantics ~= "" then
output = output .. "\n" .. semantics
end
return output
end
return p