Module:LuaTemplatePerson
Appearance
Documentation for this module may be created at Module:LuaTemplatePerson/doc
-- Module:LuaTemplatePerson
-- module for rendering person profiles 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 countryNormalization = require('Module:CountryNormalization')
local RegionalMappingICANN = require('Module:RegionalMappingICANN')
local linkParser = require('Module:LinkParser')
local MultiCountryDisplay = require('Module:MultiCountryDisplay')
local socialFooter = require('Module:SocialMedia')
local TemplateStructure = require('Module:TemplateStructure')
local LanguageNormalization = require('Module:LanguageNormalization')
-- Debug logging
local DEBUG_MODE = false
local function debugLog(message)
if not DEBUG_MODE then return end
pcall(function()
mw.logObject({
system = "person_template",
message = message,
timestamp = os.date('%H:%M:%S')
})
end)
mw.log("PERSON-DEBUG: " .. message)
end
-- Load the Achievement system with error handling
-- Achievement styling is defined in CSS/Templates.css, not in the 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 is unavailable
getTitleClass = function() return '', '' end,
renderAchievementBox = function() return '' end,
hasAchievements = function() return false end,
trackPage = function() return true end
}
debugLog('WARNING: Module:AchievementSystem not available, achievement features will be disabled')
end
--------------------------------------------------------------------------------
-- Configuration and Constants
--------------------------------------------------------------------------------
local Config = {
communityMapping = {
{canonical = "ICANN Community",
synonyms = {"icann community", "icann", "community"},
category = "[[Category:ICANN Community]]"},
{canonical = "IG Community",
synonyms = {"ig community", "ig", "internet governance"},
category = "[[Category:IG Community]]"},
{canonical = "ICANN Staff",
synonyms = {"icann staff", "staff"},
category = "[[Category:ICANN Staff]]"},
{canonical = "Former ICANN Staff",
synonyms = {"former icann staff", "former staff", "ex-staff"},
category = "[[Category:Former ICANN Staff]]"}
},
fields = {
{key="community", label="Community"},
{key="affiliation", label="ICANN group"},
{key="organization",label="Organization"},
{key="region", label="Region"},
{key="country", label="Country"},
{key="languages", label="Languages"},
{key="website", label="Website"},
{key="soi", label="SOI"},
{key="userbox", label="Achievements"}
},
patterns = {
itemDelimiter = ";%s*",
websitePattern = "^https?://[^%s]+"
}
}
--------------------------------------------------------------------------------
-- Helper Functions
--------------------------------------------------------------------------------
local function splitSemicolonValues(value)
if not value or value == "" then return {} end
local items = {}
for item in string.gmatch(value, "[^;]+") do
local trimmed = item:match("^%s*(.-)%s*$")
if trimmed and trimmed ~= "" then
table.insert(items, trimmed)
end
end
return items
end
local function getFieldValue(args, field)
if field.keys then
for _, key in ipairs(field.keys) do
if args[key] and args[key] ~= "" then
return key, args[key]
end
end
return nil, nil
end
return field.key, (args[field.key] and args[field.key] ~= "") and args[field.key] or nil
end
local function normalizeWebsites(value)
if not value or value == "" then return "" end
local websites = splitSemicolonValues(value)
if #websites > 1 then
local listItems = {}
for _, site in ipairs(websites) do
local formattedLink = string.format("[%s %s]", site, linkParser.strip(site))
table.insert(listItems, string.format("<li>%s</li>", formattedLink))
end
return string.format("<ul class=\"template-list template-list-website\" style=\"margin:0; padding-left:1em;\">%s</ul>", table.concat(listItems, ""))
elseif #websites == 1 then
return string.format("[%s %s]", websites[1], linkParser.strip(websites[1]))
end
return ""
end
local function renderVisibleDebug(data)
if not DEBUG_MODE then return "" end
local jsonData = ""
pcall(function()
if type(data) == "table" then
jsonData = mw.text.jsonEncode(data)
else
jsonData = tostring(data)
end
end)
return string.format(
'<div class="person-debug-data" style="display:none" data-debug="person_template">%s</div>',
jsonData
)
end
--------------------------------------------------------------------------------
-- Block Rendering
--------------------------------------------------------------------------------
-- Title block that retrieves the highest-tier achievement from JSON
local function renderTitleBlock(args)
local currentTitle = mw.title.getCurrentTitle()
local pageId = currentTitle.id
local pageName = currentTitle.fullText
local namespace = currentTitle.namespace
mw.log("TITLE-DEBUG: Page ID: " .. tostring(pageId) ..
", Page Name: " .. tostring(pageName) ..
", Namespace: " .. tostring(namespace))
-- Attempt to get the user’s top-tier achievement class + name
local className, achievementName = Achievements.getTitleClass(pageId)
local baseClass = "template-title template-title-person person-template"
if className and className ~= "" then
baseClass = baseClass .. " " .. className
end
-- If no achievements, just return the "Person" row
if not className or className == "" then
return string.format('! colspan="2" class="%s" | %s', baseClass, "Person")
end
-- If we do have an achievement, embed it in data attributes
local achievementId = className:gsub("^achievement%-", "") -- e.g. "padawan" instead of "achievement-padawan"
return string.format(
'! colspan="2" class="%s" data-achievement-id="%s" data-achievement-name="%s" | %s',
baseClass, achievementId, achievementName, "Person"
)
end
local function renderPortraitCarousel(args)
if not args.portrait or args.portrait == "" then
return ""
end
local imageFiles = splitSemicolonValues(args.portrait)
if #imageFiles == 0 then
return ""
end
if #imageFiles == 1 then
return string.format("|-\n| colspan=\"2\" class=\"person-portrait\" | [[Image:%s|220px|center]]", imageFiles[1])
end
local output = "|-\n| colspan=\"2\" class=\"person-portrait-carousel\" |"
output = output .. '<div class="carousel-container">'
if #imageFiles > 1 then
output = output .. '<div class="carousel-nav carousel-prev">◀</div>'
end
output = output .. '<div class="carousel-images">'
for i, imageFile in ipairs(imageFiles) do
local visibility = (i == 1) and "carousel-visible" or "carousel-hidden"
local position = ""
if #imageFiles == 2 then
position = (i == 1) and "carousel-orbital-1" or "carousel-orbital-2"
else
if i == 2 then position = "carousel-right" end
if i == #imageFiles and #imageFiles > 2 then position = "carousel-left" end
end
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>'
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
-- No dev-role or test-page checks: purely from JSON.
local function renderAchievementButtons(args)
local pageId = mw.title.getCurrentTitle().id
if not pageId or pageId == 0 then
return ""
end
if not achievementsLoaded or not Achievements then
return ""
end
local data = Achievements.loadData()
if not data or not data.user_achievements then
return ""
end
local userAchievements = data.user_achievements[tostring(pageId)]
if not userAchievements or #userAchievements == 0 then
-- No achievements in JSON for this user => return ""
return ""
end
local html = {}
table.insert(html, '|-\n| colspan="2" |')
table.insert(html, '<div style="display: flex; gap: 10px; margin-top: 8px; margin-bottom: 8px; flex-wrap: wrap;">')
-- For each achievement in JSON, build a badge
for _, achievement in ipairs(userAchievements) do
local achType = achievement.type
if achType and achType ~= "" then
local achName = Achievements.getAchievementName(achType) or ""
-- Derive a short label for the badge
local label = "?"
-- If we have e.g. "ach1", just use "1" as label
local achNum = achType:match("^ach(%d+)$")
if achNum then
label = achNum
elseif #achName > 0 then
-- Maybe use first letter if you don't want the full name
label = achName:sub(1,1)
else
label = achType:sub(1,1):upper()
end
table.insert(html, string.format(
'<div class="achievement-button achievement-%s" title="%s">%s</div>',
achType, achName, label
))
end
end
table.insert(html, '</div>')
return table.concat(html)
end
local function renderFieldsBlock(args)
local out = {}
for _, field in ipairs(Config.fields) do
local key, value = getFieldValue(args, field)
if value then
local skipInsertion = false
if key == "community" then
value = select(1, CanonicalForms.normalize(value, Config.communityMapping)) or value
elseif key == "languages" then
value = LanguageNormalization.formatLanguages(value)
elseif key == "country" then
value = MultiCountryDisplay.formatCountries(value)
elseif key == "website" then
value = normalizeWebsites(value)
elseif key == "soi" then
-- Insert the row for SOI
value = string.format("[%s Here]", value)
table.insert(out, string.format("|-\n| '''%s''':\n| %s", field.label, value))
-- Then immediately insert the achievement buttons row
local buttonsHTML = renderAchievementButtons(args)
if buttonsHTML and buttonsHTML ~= "" then
table.insert(out, buttonsHTML)
end
skipInsertion = true
elseif key == "userbox" then
-- Attempt to show achievements in the userbox area
local pageId = mw.title.getCurrentTitle().id
if achievementsLoaded and Achievements and pageId then
local success, achievementBox = pcall(function()
return Achievements.renderAchievementBox(pageId)
end)
if success and achievementBox and achievementBox ~= "" then
value = renderVisibleDebug({
section = "achievements_userbox",
pageId = pageId,
contentLength = #achievementBox,
timestamp = os.date()
}) .. achievementBox
end
end
end
if not skipInsertion then
table.insert(out, string.format("|-\n| '''%s''':\n| %s", field.label, value))
end
end
end
return table.concat(out, "\n")
end
--------------------------------------------------------------------------------
-- Categories
--------------------------------------------------------------------------------
local function computeCategories(args)
local cats = {}
if args.community and args.community ~= "" then
local community = select(1, CanonicalForms.normalize(args.community, Config.communityMapping))
if community then
for _, group in ipairs(Config.communityMapping) do
if group.canonical == community and group.category then
table.insert(cats, group.category)
break
end
end
end
end
if args.country and args.country ~= "" then
local normalizedCountries = MultiCountryDisplay.getCountriesForCategories(args.country)
for _, countryName in ipairs(normalizedCountries) do
table.insert(cats, string.format("[[Category:%s]]", countryName))
end
end
if args.region and args.region ~= "" then
local regions = splitSemicolonValues(args.region)
for _, region in ipairs(regions) do
local trimmedRegion = region:match("^%s*(.-)%s*$")
if trimmedRegion and trimmedRegion ~= "" then
table.insert(cats, string.format("[[Category:%s]]", trimmedRegion))
end
end
end
return table.concat(cats, "\n")
end
--------------------------------------------------------------------------------
-- Main render
--------------------------------------------------------------------------------
function p.render(frame)
local args = frame:getParent().args or {}
-- Derive region from country if empty
if args.country and (not args.region or args.region == "") then
local regions = RegionalMappingICANN.getRegionsForCountries(args.country)
if #regions > 0 then
args.region = table.concat(regions, " and ")
end
elseif args.region and args.region ~= "" then
local regions = RegionalMappingICANN.normalizeRegions(args.region)
if #regions > 0 then
args.region = table.concat(regions, " and ")
end
end
local config = {
blocks = {
function(a) return renderTitleBlock(a) end,
function(a) return renderPortraitCarousel(a) end,
function(a) return renderFieldsBlock(a) end,
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
debugLog("Tracking page ID " .. tostring(pageId) .. " for cache purging")
Achievements.trackPage(pageId, pageName)
end
end)
end
return TemplateStructure.render(args, config) .. "\n" .. computeCategories(args)
end
return p