Module:T-CountryHub: Difference between revisions
// via Wikitext Extension for VSCode Tag: Reverted |
// via Wikitext Extension for VSCode Tag: Reverted |
||
| Line 1: | Line 1: | ||
-- Modules/T-CountryHub.lua | -- Modules/T-CountryHub.lua | ||
-- Blueprint-based full-page template for country hubs with | -- Blueprint-based full-page template for country hubs with a Lua-driven masonry layout engine. | ||
-- ================================================================================ | |||
-- #region REQUIRES AND HELPERS | |||
-- ================================================================================ | |||
-- Safe require helpers | -- Safe require helpers | ||
local function safeRequire(name) | local function safeRequire(name) | ||
local ok, mod = pcall(require, name) | local ok, mod = pcall(require, name) | ||
if ok and mod then | if ok and mod then return mod end | ||
return setmetatable({}, { __index = function() return function() return '' end end }) | |||
return setmetatable({}, { | |||
end | end | ||
| Line 25: | Line 23: | ||
local mw = mw | local mw = mw | ||
local html = mw.html | local html = mw.html | ||
local smw = (mw.smw and mw.smw.ask) and mw.smw or nil | local smw = (mw.smw and mw.smw.ask) and mw.smw or nil | ||
-- askCached: Performs a Semantic MediaWiki #ask query with caching | -- askCached: Performs a Semantic MediaWiki #ask query with caching. | ||
local _askCache = {} | local _askCache = {} | ||
local function askCached(key, params) | local function askCached(key, params) | ||
| Line 40: | Line 35: | ||
end | end | ||
-- safeField: Safely retrieves a field from a data row | -- safeField: Safely retrieves a field from a data row. | ||
local function safeField(row, key) | local function safeField(row, key) | ||
if not row then return '' end | if not row then return '' end | ||
if row[key] ~= nil and row[key] ~= '' then return row[key] end | if row[key] ~= nil and row[key] ~= '' then return row[key] end | ||
if row[1] | if row[1] ~= nil and row[1] ~= '' then return row[1] end | ||
return '' | return '' | ||
end | end | ||
-- | -- #endregion | ||
-- | -- ================================================================================ | ||
-- | -- #region CORE TEMPLATE AND BLUEPRINT CONFIGURATION | ||
-- | -- ================================================================================ | ||
local errorContext = ErrorHandling.createContext("T-CountryHub") | local errorContext = ErrorHandling.createContext("T-CountryHub") | ||
local template = Blueprint.registerTemplate('CountryHub', { | local template = Blueprint.registerTemplate('CountryHub', { | ||
features = { | features = { | ||
fullPage = true, | fullPage = true, | ||
countryWrapper = true, | countryWrapper = true, | ||
infoBox = true, | infoBox = true, | ||
semanticProperties = true, | semanticProperties = true, | ||
categories = true, | categories = true, | ||
| Line 137: | Line 65: | ||
}) | }) | ||
Blueprint.initializeConfig(template) | 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 = { | template.config.blockSequence = { | ||
'wrapperOpen', | 'wrapperOpen', | ||
'infoBox', | 'infoBox', | ||
' | 'layoutController', -- This new block will render the entire masonry layout | ||
'wrapperClose', | 'wrapperClose', | ||
'categories', | 'categories', | ||
| Line 161: | Line 78: | ||
} | } | ||
template.config.blocks = template.config.blocks or {} | |||
-- #endregion | |||
-- ================================================================================ | |||
-- #region STATIC BLOCKS (WRAPPERS AND INFOBOX) | |||
-- ================================================================================ | -- ================================================================================ | ||
template.config.blocks.wrapperOpen = { | template.config.blocks.wrapperOpen = { | ||
feature = 'countryWrapper', | feature = 'countryWrapper', | ||
render = function( | render = function() return '<div class="country-hub-wrapper">' end | ||
} | |||
template.config.blocks.wrapperClose = { | |||
feature = 'countryWrapper', | |||
render = function() return '</div>' end | |||
} | } | ||
template.config.blocks.infoBox = { | template.config.blocks.infoBox = { | ||
feature = 'infoBox', | feature = 'infoBox', | ||
| Line 205: | Line 112: | ||
-- Fetch ICANN region for the country | -- Fetch ICANN region for the country | ||
local regionParams = { | 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 regionData = askCached('infoBox:region:' .. args.has_country, regionParams) | ||
local regionText = regionData[1] and regionData[1]['Has ICANN region'] or '' | local regionText = regionData[1] and regionData[1]['Has ICANN region'] or '' | ||
| Line 216: | Line 118: | ||
-- Check for an ISOC chapter in the country using fuzzy matching | -- Check for an ISOC chapter in the country using fuzzy matching | ||
local ISOCParams = { | local ISOCParams = { '[[Category:Internet Society Chapter]]', limit = 200 } | ||
local ISOCData = askCached('infoBox:ISOC:all_chapters', ISOCParams) | local ISOCData = askCached('infoBox:ISOC:all_chapters', ISOCParams) | ||
local ISOCText | local ISOCText | ||
| Line 269: | Line 168: | ||
-- Check for a Youth IGF initiative in the country | -- Check for a Youth IGF initiative in the country | ||
local youthParams = { | local youthParams = { string.format('[[Category:Youth IGF %s]]', args.has_country), limit = 1 } | ||
local youthData = askCached('infoBox:youth:' .. args.has_country, youthParams) | local youthData = askCached('infoBox:youth:' .. args.has_country, youthParams) | ||
local youthText | 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 = "" | local flagImageWikitext = "" | ||
| Line 286: | Line 177: | ||
if isoCode and #isoCode == 2 then | if isoCode and #isoCode == 2 then | ||
local flagFile = "Flag-" .. string.lower(isoCode) .. ".svg" | local flagFile = "Flag-" .. string.lower(isoCode) .. ".svg" | ||
flagImageWikitext = string.format( | flagImageWikitext = string.format("[[File:%s|link=|class=country-infobox-bg-image]]", flagFile) | ||
end | end | ||
end | end | ||
-- Assemble the HTML for the infobox table | -- Assemble the HTML for the infobox table | ||
local infoBoxWrapper = html.create('div') | 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() | |||
local infoBox = infoBoxWrapper:tag('table') | 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() | |||
infoBox:tag('tr') | |||
infoBox:tag('tr') | |||
infoBox:tag('tr') | |||
infoBox:tag('tr') | |||
infoBox:tag('tr') | |||
infoBoxWrapper:wikitext(flagImageWikitext) | infoBoxWrapper:wikitext(flagImageWikitext) | ||
| Line 341: | Line 195: | ||
} | } | ||
-- | -- #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 | 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) | local data = askCached('organizations:' .. args.has_country, params) | ||
return | 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) | local data = askCached('people:' .. args.has_country, params) | ||
return | 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, | 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', | |||
local params = { | 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, | 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', | |||
local params = { | 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, | 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. | template.config.blocks.layoutController = { | ||
feature = ' | feature = 'fullPage', | ||
render = function(template, args) | render = function(template, args) | ||
local | local numColumns = 3 -- Define the number of columns | ||
-- 1. Collect all available content blocks | |||
local availableBlocks = {} | |||
return | 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 | |||
end | return tostring(root) | ||
end | |||
} | } | ||
-- | -- #endregion | ||
-- | -- ================================================================================ | ||
-- #region PREPROCESSORS AND PROVIDERS | |||
-- ================================================================================ | |||
Blueprint.addPreprocessor(template, function(_, args) | Blueprint.addPreprocessor(template, function(_, args) | ||
args.country = args.country or mw.title.getCurrentTitle().text or "" | args.country = args.country or mw.title.getCurrentTitle().text or "" | ||
| Line 507: | Line 378: | ||
end) | end) | ||
Blueprint.registerPropertyProvider(template, function(_, args) | Blueprint.registerPropertyProvider(template, function(_, args) | ||
local props = {} | local props = {} | ||
| Line 517: | Line 387: | ||
end) | end) | ||
Blueprint.registerCategoryProvider(template, function(_, args) | Blueprint.registerCategoryProvider(template, function(_, args) | ||
local cats = {"Country Hub"} | local cats = {"Country Hub"} | ||
| Line 527: | Line 396: | ||
end) | end) | ||
-- | -- #endregion | ||
-- ================================================================================ | |||
-- #region RENDER ENTRY POINT | |||
-- ================================================================================ | |||
function p.render(frame) | function p.render(frame) | ||
return ErrorHandling.protect( | return ErrorHandling.protect( | ||
| Line 538: | Line 412: | ||
return p | return p | ||
-- #endregion | |||