Module:T-CountryHub
Documentation for this module may be created at Module:T-CountryHub/doc
-- Modules/T-CountryHub.lua
-- Blueprint-based full-page template for country hubs with a Lua-driven masonry layout engine.
-- ================================================================================
-- #region REQUIRES AND HELPERS
-- ================================================================================
-- Safe require helpers
local function safeRequire(name)
local ok, mod = pcall(require, name)
if ok and mod then return mod end
return setmetatable({}, { __index = function() return function() return '' end end })
end
-- Requires
local p = {}
local Blueprint = safeRequire('Module:LuaTemplateBlueprint')
local ErrorHandling = safeRequire('Module:ErrorHandling')
local CountryData = safeRequire('Module:CountryData')
local TemplateHelpers = safeRequire('Module:TemplateHelpers')
local NormalizationText = safeRequire('Module:NormalizationText')
local NormalizationDiacritic = safeRequire('Module:NormalizationDiacritic')
local mw = mw
local html = mw.html
local smw = (mw.smw and mw.smw.ask) and mw.smw or nil
-- askCached: Performs a Semantic MediaWiki #ask query with caching.
local _askCache = {}
local function askCached(key, params)
if not smw then return {} end
local cacheKey = TemplateHelpers.generateCacheKey('CountryHub:ask', key)
return TemplateHelpers.withCache(cacheKey, function()
return smw.ask(params) or {}
end)
end
-- safeField: Safely retrieves a field from a data row.
local function safeField(row, key)
if not row then return '' end
if row[key] ~= nil and row[key] ~= '' then return row[key] end
if row[1] ~= nil and row[1] ~= '' then return row[1] end
return ''
end
-- #endregion
-- ================================================================================
-- #region CORE TEMPLATE AND BLUEPRINT CONFIGURATION
-- ================================================================================
local errorContext = ErrorHandling.createContext("T-CountryHub")
local template = Blueprint.registerTemplate('CountryHub', {
features = {
fullPage = true,
countryWrapper = true,
infoBox = true,
semanticProperties = true,
categories = true,
errorReporting = true
},
constants = {
tableClass = ""
}
})
Blueprint.initializeConfig(template)
-- The sequence now only controls the non-masonry parts of the page.
-- The masonry content is handled by the new layout engine.
template.config.blockSequence = {
'wrapperOpen',
'infoBox',
'layoutController', -- This new block will render the entire masonry layout
'wrapperClose',
'categories',
'errors'
}
template.config.blocks = template.config.blocks or {}
-- #endregion
-- ================================================================================
-- #region STATIC BLOCKS (WRAPPERS AND INFOBOX)
-- ================================================================================
template.config.blocks.wrapperOpen = {
feature = 'countryWrapper',
render = function() return '<div class="country-hub-wrapper">' end
}
template.config.blocks.wrapperClose = {
feature = 'countryWrapper',
render = function() return '</div>' end
}
template.config.blocks.infoBox = {
feature = 'infoBox',
render = function(template, args)
local displayCountry = (args.country or mw.title.getCurrentTitle().text or ""):gsub('_',' ')
-- Derive ccTLD from ISO code via CountryData module
local isoCode = CountryData.getCountryCodeByName(args.has_country)
local ccTLDText
if isoCode and #isoCode == 2 then
local cctldValue = "." .. string.lower(isoCode)
ccTLDText = "[[" .. cctldValue .. "]]" -- Make it a wiki link
else
ccTLDText = string.format('ccTLD data unavailable for %s.', displayCountry)
end
-- Fetch ICANN region for the country
local regionParams = { string.format('[[Has country::%s]]', args.has_country), '?Has ICANN region', format = 'plain', limit = 1 }
local regionData = askCached('infoBox:region:' .. args.has_country, regionParams)
local regionText = regionData[1] and regionData[1]['Has ICANN region'] or ''
regionText = NormalizationText.processWikiLink(regionText, 'strip')
-- Check for an ISOC chapter in the country using fuzzy matching
local ISOCParams = { '[[Category:Internet Society Chapter]]', limit = 200 }
local ISOCData = askCached('infoBox:ISOC:all_chapters', ISOCParams)
local ISOCText
local function normalizeForMatching(str)
if not str then return '' end
local pageName = string.match(str, '^%[%[([^%]]+)%]%]$') or str
pageName = NormalizationDiacritic.removeDiacritics(pageName)
pageName = string.lower(pageName)
pageName = string.gsub(pageName, 'internet society', '')
pageName = string.gsub(pageName, 'chapter', '')
return NormalizationText.trim(pageName)
end
local countryData = CountryData.getCountryByName(args.has_country)
local countryNamesToMatch = {}
if countryData then
table.insert(countryNamesToMatch, normalizeForMatching(countryData.name))
if countryData.variations then
for _, variation in pairs(countryData.variations) do
table.insert(countryNamesToMatch, normalizeForMatching(variation))
end
end
else
table.insert(countryNamesToMatch, normalizeForMatching(args.has_country))
end
local foundMatch = false
if ISOCData and #ISOCData > 0 then
for _, chapter in ipairs(ISOCData) do
local chapterName = chapter.result
if chapterName and chapterName ~= '' then
local normalizedChapterName = normalizeForMatching(chapterName)
for _, countryNameToMatch in ipairs(countryNamesToMatch) do
if string.find(normalizedChapterName, countryNameToMatch, 1, true) then
ISOCText = chapterName
foundMatch = true
break
end
end
end
if foundMatch then break end
end
end
if not foundMatch then
ISOCText = string.format('[[Internet Society %s Chapter]]', args.has_country)
end
-- Check for a Youth IGF initiative in the country
local youthParams = { string.format('[[Category:Youth IGF %s]]', args.has_country), limit = 1 }
local youthData = askCached('infoBox:youth:' .. args.has_country, youthParams)
local youthText = (youthData[1] and youthData[1]['result'] and youthData[1]['result'] ~= '') and youthData[1]['result'] or string.format('[[Youth IGF %s]]', args.has_country)
local flagImageWikitext = ""
if args.has_country and args.has_country ~= "" then
local isoCode = CountryData.getCountryCodeByName(args.has_country)
if isoCode and #isoCode == 2 then
local flagFile = "Flag-" .. string.lower(isoCode) .. ".svg"
flagImageWikitext = string.format("[[File:%s|link=|class=country-infobox-bg-image]]", flagFile)
end
end
-- Assemble the HTML for the infobox table
local infoBoxWrapper = html.create('div'):addClass('country-hub-infobox-wrapper')
local infoBox = infoBoxWrapper:tag('table'):addClass('country-hub-infobox icannwiki-automatic-text')
infoBox:tag('tr'):tag('th'):attr('colspan', '2'):addClass('country-hub-infobox-header icannwiki-automatic-text'):wikitext(string.format('%s', displayCountry)):done():done()
infoBox:tag('tr'):tag('th'):wikitext('ccTLD'):done():tag('td'):wikitext(ccTLDText):done():done()
infoBox:tag('tr'):tag('th'):wikitext('ICANN region'):done():tag('td'):wikitext(regionText):done():done()
infoBox:tag('tr'):tag('th'):wikitext('ISOC chapter'):done():tag('td'):wikitext(ISOCText):done():done()
infoBox:tag('tr'):tag('th'):wikitext('Youth IGF'):done():tag('td'):wikitext(youthText):done():done()
infoBoxWrapper:wikitext(flagImageWikitext)
return tostring(infoBoxWrapper)
end
}
-- #endregion
-- ================================================================================
-- #region LAYOUT ENGINE AND CONTENT BLOCK DEFINITIONS
-- ================================================================================
-- This table defines all the content blocks that will be part of the masonry layout.
-- Each function fetches data and returns a block object for the layout engine.
local contentBlocks = {
intro = function(template, args)
local rawIntroText = '<p>Welcome to ICANNWiki\'s hub for <b>{{PAGENAME}}</b>. With the use of semantics, this page aggregates all Internet Governance content for this territory that is currently indexed in our database.</p>'
local processedText = template.current_frame:preprocess(rawIntroText)
return {
id = 'intro',
height = 2, -- Assign a fixed height proxy for the intro text
renderFunc = function() return processedText end
}
end,
organizations = function(template, args)
local params = { string.format('[[Has country::%s]] [[Has entity type::Organization]]', args.has_country), limit = 50 }
local data = askCached('organizations:' .. args.has_country, params)
if #data == 0 then return nil end
return {
id = 'organizations',
content = data,
height = #data + 1, -- +1 for the header
renderFunc = function(d)
local t = html.create('table'):addClass('wikitable')
t:tag('tr'):tag('th'):wikitext('Organizations'):done():done()
for _, row in ipairs(d) do
t:tag('tr'):tag('td'):wikitext(safeField(row, 'result')):done():done()
end
return tostring(t)
end
}
end,
people = function(template, args)
local params = { string.format('[[Has country::%s]] [[Has entity type::Person]]', args.has_country), limit = 50 }
local data = askCached('people:' .. args.has_country, params)
if #data == 0 then return nil end
return {
id = 'people',
content = data,
height = #data + 1,
renderFunc = function(d)
local t = html.create('table'):addClass('wikitable')
t:tag('tr'):tag('th'):wikitext('People'):done():done()
for _, row in ipairs(d) do
t:tag('tr'):tag('td'):wikitext(safeField(row, 'result')):done():done()
end
return tostring(t)
end
}
end,
geoTlds = function(template, args)
local params = { string.format('[[Has country::%s]] [[Has entity type::TLD]] [[Has TLD subtype::geoTLD]]', args.has_country), limit = 50 }
local data = askCached('geoTlds:' .. args.has_country, params)
if #data == 0 then return nil end
return {
id = 'geoTlds',
content = data,
height = #data + 1,
renderFunc = function(d)
local t = html.create('table'):addClass('wikitable')
t:tag('tr'):tag('th'):wikitext('GeoTLDs'):done():done()
for _, row in ipairs(d) do
t:tag('tr'):tag('td'):wikitext(safeField(row, 'result')):done():done()
end
return tostring(t)
end
}
end,
meetings = function(template, args)
local params = { string.format('[[Has country::%s]] [[Has entity type::Event]]', args.has_country), limit = 50 }
local data = askCached('events:' .. args.has_country, params)
if #data == 0 then return nil end
return {
id = 'meetings',
content = data,
height = #data + 1,
renderFunc = function(d)
local t = html.create('table'):addClass('wikitable')
t:tag('tr'):tag('th'):wikitext('Internet Governance Events'):done():done()
for _, row in ipairs(d) do
t:tag('tr'):tag('td'):wikitext(safeField(row, 'result')):done():done()
end
return tostring(t)
end
}
end,
nra = function(template, args)
local params = { string.format('[[Has country::%s]] [[Has entity type::Organization]] [[Has organization type::Government agency]]', args.has_country), limit = 10 }
local data = askCached('nra:' .. args.has_country, params)
if #data == 0 then return nil end
return {
id = 'nra',
content = data,
height = #data + 1,
renderFunc = function(d)
local t = html.create('table'):addClass('wikitable')
t:tag('tr'):tag('th'):wikitext('National Authorities'):done():done()
for _, row in ipairs(d) do
t:tag('tr'):tag('td'):wikitext(safeField(row, 'result')):done():done()
end
return tostring(t)
end
}
end
}
-- The main controller block that drives the masonry layout.
template.config.blocks.layoutController = {
feature = 'fullPage',
render = function(template, args)
local numColumns = 3 -- Define the number of columns
-- 1. Collect all available content blocks
local availableBlocks = {}
local blockSequenceForLayout = {'intro', 'organizations', 'people', 'geoTlds', 'meetings', 'nra'}
for _, blockId in ipairs(blockSequenceForLayout) do
local blockFunc = contentBlocks[blockId]
if blockFunc then
local blockData = blockFunc(template, args)
if blockData then
table.insert(availableBlocks, blockData)
end
end
end
if #availableBlocks == 0 then return '' end
-- 2. Sort blocks by height, descending. This is key for the greedy algorithm.
table.sort(availableBlocks, function(a, b)
return a.height > b.height
end)
-- 3. Distribute blocks into columns using a greedy algorithm
local columns = {}
for i = 1, numColumns do
columns[i] = { height = 0, blocks = {} }
end
for _, block in ipairs(availableBlocks) do
-- Find the shortest column
local shortestColumnIndex = 1
for i = 2, numColumns do
if columns[i].height < columns[shortestColumnIndex].height then
shortestColumnIndex = i
end
end
-- Add the block to the shortest column
table.insert(columns[shortestColumnIndex].blocks, block)
columns[shortestColumnIndex].height = columns[shortestColumnIndex].height + block.height
end
-- 4. Render the final HTML
local root = html.create('div'):addClass('country-hub-data-container')
for _, col in ipairs(columns) do
if #col.blocks > 0 then
local columnDiv = root:tag('div'):addClass('country-hub-column')
for _, block in ipairs(col.blocks) do
columnDiv:wikitext(block.renderFunc(block.content))
end
end
end
return tostring(root)
end
}
-- #endregion
-- ================================================================================
-- #region PREPROCESSORS AND PROVIDERS
-- ================================================================================
Blueprint.addPreprocessor(template, function(_, args)
args.country = args.country or mw.title.getCurrentTitle().text or ""
args.has_country = CountryData.normalizeCountryName(args.country)
args.region = CountryData.getRegionByCountry(args.country)
return args
end)
Blueprint.registerPropertyProvider(template, function(_, args)
local props = {}
if args.has_country and args.has_country ~= "(Unrecognized)" then
props["Has country"] = args.has_country
props["Has ICANN region"] = args.region
end
return props
end)
Blueprint.registerCategoryProvider(template, function(_, args)
local cats = {"Country Hub"}
if args.has_country and args.has_country ~= "(Unrecognized)" then
table.insert(cats, args.has_country)
table.insert(cats, args.region)
end
return cats
end)
-- #endregion
-- ================================================================================
-- #region RENDER ENTRY POINT
-- ================================================================================
function p.render(frame)
return ErrorHandling.protect(
errorContext, "render",
function() return template.render(frame) end,
ErrorHandling.getMessage("TEMPLATE_RENDER_ERROR"),
frame
)
end
return p
-- #endregion