Jump to content

Module:T-CountryHub: Difference between revisions

// via Wikitext Extension for VSCode
Tag: Reverted
// via Wikitext Extension for VSCode
Tag: Manual revert
Line 1: Line 1:
-- Modules/T-CountryHub.lua
-- Modules/T-CountryHub.lua
-- Blueprint-based full-page template for country hubs with a Lua-driven masonry layout engine.
-- Blueprint-based full-page template for country hubs with discrete content blocks
 
-- ================================================================================
-- #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 return mod end
     if ok and mod then
     return setmetatable({}, { __index = function() return function() return '' end end })
        return mod
    end
     return setmetatable({}, {
        __index = function()
            return function() return '' end
        end
    })
end
end


Line 23: Line 25:
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 -- Handle for Semantic MediaWiki's #ask functionality, if available


-- askCached: Performs a Semantic MediaWiki #ask query with caching.
-- askCached: Performs a Semantic MediaWiki #ask query with caching; uses TemplateHelpers.generateCacheKey to create a unique key for the query and TemplateHelpers.withCache to manage the caching logic.
-- @param key string: A unique identifier part for the query, used for generating the cache key.
-- @param params table: Parameters to be passed to the smw.ask function.
-- @return table: The result of the SMW query, or an empty table if SMW is unavailable or the query fails.
local _askCache = {}
local _askCache = {}
local function askCached(key, params)
local function askCached(key, params)
Line 35: Line 40:
end
end


-- safeField: Safely retrieves a field from a data row.
-- safeField: Safely retrieves a field from a data row; it first tries to access the field by its string 'key'. If not found or empty, it falls back to the first element of the row (index 1).
-- @param row table: The data row (a table).
-- @param key string/number: The key or index of the field to retrieve.
-- @return string: The field's value, or an empty string if not found or if the row is nil.
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]   ~= nil and row[1]   ~= '' then return row[1] end
     if row[1]     ~= nil and row[1]     ~= '' then return row[1] end -- Fallback for SMW results where data might be in the first unnamed field
     return ''
     return ''
end
end


-- #endregion
-- renderTable: Generates HTML for a standard MediaWiki table
local function renderTable(result, columns)
    local t = html.create('table'):addClass('wikitable')
    -- Header row
    local header = t:tag('tr')
    for _, col in ipairs(columns) do
        header:tag('th'):wikitext(col):done()
    end
    -- Data rows
    for _, row in ipairs(result) do
        local tr = t:tag('tr')
        for _, col in ipairs(columns) do
tr:tag('td'):wikitext(safeField(row, col)):done()
        end
    end
    return tostring(t)
end
 
-- renderSection: A wrapper function that performs a cached SMW query and renders a table only if results are found.
-- @param key string: The base cache key for the query.
-- @param params table: The parameters for the smw.ask query.
-- @param columns table: A list of column headers for the table.
-- @return string: An HTML table if data is found, otherwise an empty string.
local function renderSection(key, params, columns)
    local data = askCached(key, params)
    if not data or #data == 0 then
        return ''
    end
    return renderTable(data, columns)
end
 
-- -- generateProcessedBrowseLink: Constructs and preprocesses a MediaWiki link for browsing data, with {{fullurl:...}} correctly expanded by MediaWiki before the link is rendered.
-- local function generateProcessedBrowseLink(template, browseType, browseQueryParam, linkTextPattern, country)
--    -- Explicitly URL-encode the country name for use in the URL query parameter
--    local encodedCountry = mw.uri.encode(country, 'QUERY') -- 'QUERY' mode is for query string values
 
--    local browseLinkString = string.format(
--        '[{{fullurl:Special:BrowseData/%s|%s=%s}} %s →]',
--        browseType,
--        browseQueryParam,
--        encodedCountry,  -- Use the encoded country name for the URL part
--        string.format(linkTextPattern, country) -- Use the original country name for the display text
--    )
   
--    if template.current_frame and template.current_frame.preprocess then
--        local ok, result = pcall(function() return template.current_frame:preprocess(browseLinkString) end)
--        if ok then
--            return result
--        else
--            return browseLinkString -- Fallback on preprocess error
--        end
--    end
--    return browseLinkString -- Fallback if no frame or preprocess
-- end
 
local errorContext = ErrorHandling.createContext("T-CountryHub")


-- ================================================================================
-- #region CORE TEMPLATE AND BLUEPRINT CONFIGURATION
-- ================================================================================
-- ================================================================================


local errorContext = ErrorHandling.createContext("T-CountryHub")
-- CONTROL OF TEMPLATE FEATURES: THIS LIST SPECIFIES IN AN EXPLICIT MANNER WHAT FEATURES ARE TO BE CALLED/RENDERED BY THE TEMPLATE.


local template = Blueprint.registerTemplate('CountryHub', {
local template = Blueprint.registerTemplate('CountryHub', {
     features = {
     features = {
         fullPage = true,
         fullPage = true, -- does not render as a template box, but rather occupies the entire page
         countryWrapper = true,
         countryWrapper = true,
        countryFlag = true,
        infoBox = true,
        intro = true,
        overview = false,
        organizations = true,
        people = true,
        laws = false,
        documents = false,
        geoTlds = true,
        meetings = true,
        nra = true,
        resources = false,
         semanticProperties = true,
         semanticProperties = true,
         categories = true,
         categories = true,
Line 64: Line 137:
})
})


-- Blueprint default: Initialize standard configuration
Blueprint.initializeConfig(template)
Blueprint.initializeConfig(template)


-- The sequence now only controls the main wrapper and the layout engine.
-- CONTROL THE VISUAL ORDER THAT EACH ASPECT IS RENDERED IN
template.config.blockSequence = {
template.config.blockSequence = {
     'wrapperOpen',
     'wrapperOpen',
     'layoutController', -- This block now renders ALL content, including the infobox.
     'infoBox',
    'intro',
    'overview',
    'dataWrapperOpen',
    'organizations',
    'people',
    'laws',
    'documents',
    'geoTlds',
    'meetings',
    'nra',
    'resources',
    'dataWrapperClose',
     'wrapperClose',
     'wrapperClose',
     'categories',
     'categories',
     'errors'
     'errors'
}
}
-- ================================================================================


template.config.blocks = template.config.blocks or {}
template.config.blocks = template.config.blocks or {}


-- #endregion
-- Wrapper open block: Defines the opening div for the main content wrapper and includes the flag image
 
-- ================================================================================
-- #region STATIC WRAPPER BLOCKS
-- ================================================================================
 
template.config.blocks.wrapperOpen = {
template.config.blocks.wrapperOpen = {
     feature = 'countryWrapper',
     feature = 'countryWrapper',
     render  = function() return '<div class="country-hub-wrapper">' end
     render  = function(template, args) -- Added template, args
}
        -- local normalizedCountryName = args.has_country -- From preprocessor
        -- local flagImageWikitext = ""


template.config.blocks.wrapperClose = {
        -- if normalizedCountryName and normalizedCountryName ~= "" then
    feature = 'countryWrapper',
        --    local isoCode = CountryData.getCountryCodeByName(normalizedCountryName)
    render  = function() return '</div>' end
        --    if isoCode and #isoCode == 2 then
        --        local flagFile = "Flag-" .. string.lower(isoCode) .. ".svg"
        --        -- Using a new class 'country-wrapper-bg-image' for the image
        --        flagImageWikitext = string.format(
        --            "[[File:%s|link=|class=country-wrapper-bg-image]]",
        --            flagFile
        --        )
        --    end
        -- end
       
        return '<div class="country-hub-wrapper">' -- .. flagImageWikitext -- Appended flag
    end
}
}


-- #endregion
-- ANCHOR: INFOBOX
 
template.config.blocks.infoBox = {
-- ================================================================================
     feature = 'infoBox',
-- #region LAYOUT ENGINE AND CONTENT BLOCK DEFINITIONS
    render  = function(template, args)
-- ================================================================================
 
-- 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 = {
     infoBox = function(template, args)
         local displayCountry = (args.country or mw.title.getCurrentTitle().text or ""):gsub('_',' ')
         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 isoCode = CountryData.getCountryCodeByName(args.has_country)
         local ccTLDText = (isoCode and #isoCode == 2) and ("[[" .. "." .. string.lower(isoCode) .. "]]") or string.format('ccTLD data unavailable for %s.', displayCountry)
         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
          
          
         local regionParams = { string.format('[[Has country::%s]]', args.has_country), '?Has ICANN region', format = 'plain', limit = 1 }
        -- 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 regionData = askCached('infoBox:region:' .. args.has_country, regionParams)
         local regionText = NormalizationText.processWikiLink((regionData[1] and regionData[1]['Has ICANN region'] or ''), 'strip')
         local regionText = regionData[1] and regionData[1]['Has ICANN region'] or ''
        regionText = NormalizationText.processWikiLink(regionText, 'strip')
          
          
         local ISOCParams = { '[[Category:Internet Society Chapter]]', limit = 200 }
        -- Check for an ISOC chapter in the country using fuzzy matching
         local ISOCParams = {
            '[[Category:Internet Society Chapter]]',
            limit = 200 -- Fetch all chapters to perform fuzzy matching
        }
         local ISOCData = askCached('infoBox:ISOC:all_chapters', ISOCParams)
         local ISOCData = askCached('infoBox:ISOC:all_chapters', ISOCParams)
         local ISOCText
         local ISOCText
Line 119: Line 226:
             if not str then return '' end
             if not str then return '' end
             local pageName = string.match(str, '^%[%[([^%]]+)%]%]$') or str
             local pageName = string.match(str, '^%[%[([^%]]+)%]%]$') or str
             return NormalizationText.trim(string.gsub(string.gsub(string.lower(NormalizationDiacritic.removeDiacritics(pageName)), 'internet society', ''), 'chapter', ''))
             pageName = NormalizationDiacritic.removeDiacritics(pageName)
            pageName = string.lower(pageName)
            pageName = string.gsub(pageName, 'internet society', '')
            pageName = string.gsub(pageName, 'chapter', '')
            return NormalizationText.trim(pageName)
         end
         end


Line 126: Line 237:
         if countryData then
         if countryData then
             table.insert(countryNamesToMatch, normalizeForMatching(countryData.name))
             table.insert(countryNamesToMatch, normalizeForMatching(countryData.name))
             for _, variation in pairs(countryData.variations) do
             if countryData.variations then
                table.insert(countryNamesToMatch, normalizeForMatching(variation))
                for _, variation in pairs(countryData.variations) do
                    table.insert(countryNamesToMatch, normalizeForMatching(variation))
                end
             end
             end
         else
         else
Line 150: Line 263:
             end
             end
         end
         end
         if not foundMatch then ISOCText = string.format('[[Internet Society %s Chapter]]', args.has_country) end
 
         if not foundMatch then
            ISOCText = string.format('[[Internet Society %s Chapter]]', args.has_country)
        end
          
          
         local youthParams = { string.format('[[Category:Youth IGF %s]]', args.has_country), limit = 1 }
        -- 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 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 youthText
        if youthData[1] and youthData[1]['result'] and youthData[1]['result'] ~= '' then
            youthText = youthData[1]['result']
        else
            youthText = string.format('[[Youth IGF %s]]', args.has_country)
        end
          
          
         local flagImageWikitext = ""
         local flagImageWikitext = ""
         if isoCode and #isoCode == 2 then
         if args.has_country and args.has_country ~= "" then
            flagImageWikitext = string.format("[[File:%s|link=|class=country-infobox-bg-image]]", "Flag-" .. string.lower(isoCode) .. ".svg")
            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')
       
        -- Header row
        infoBox:tag('tr')
            :tag('th')
                :attr('colspan', '2')
                :addClass('country-hub-infobox-header icannwiki-automatic-text')
                :wikitext(string.format('%s', displayCountry))
                :done()
            :done()
       
        -- ccTLD row
        infoBox:tag('tr')
            :tag('th'):wikitext('ccTLD'):done()
            :tag('td'):wikitext(ccTLDText):done()
            :done()
       
        -- ICANN region row
        infoBox:tag('tr')
            :tag('th'):wikitext('ICANN region'):done()
            :tag('td'):wikitext(regionText):done()
            :done()
       
        -- REVIEW: Check for ccNSO membership or affiliation in the country / https://ccnso.icann.org/en/about/members.htm
 
        -- ISOC chapter row
        infoBox:tag('tr')
            :tag('th'):wikitext('ISOC chapter'):done()
            :tag('td'):wikitext(ISOCText):done()
            :done()
       
        -- Youth IGF row
        infoBox:tag('tr')
            :tag('th'):wikitext('Youth IGF'):done()
            :tag('td'):wikitext(youthText):done()
            :done()
       
        infoBoxWrapper:wikitext(flagImageWikitext)
       
        return tostring(infoBoxWrapper)
    end
}
 
-- ANCHOR: INTRO
template.config.blocks.intro = {
    feature = 'intro',
    render  = 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>'
        if template.current_frame and template.current_frame.preprocess then
            return template.current_frame:preprocess(rawIntroText)
         end
         end
        return rawIntroText
    end
}


        return {
-- ANCHOR: OVERVIEW
            id = 'infoBox',
template.config.blocks.overview = {
            height = 6, -- Fixed height proxy for the infobox
    feature = 'overview',
            renderFunc = function()
    render = function(template, args)
                local infoBoxWrapper = html.create('div'):addClass('country-hub-infobox-wrapper')
        local displayCountry = (args.country or mw.title.getCurrentTitle().text or ""):gsub('_',' ')
                local infoBox = infoBoxWrapper:tag('table'):addClass('country-hub-infobox icannwiki-automatic-text')
        local queryCountryName = args.has_country
                infoBox:tag('tr'):tag('th'):attr('colspan', '2'):addClass('country-hub-infobox-header icannwiki-automatic-text'):wikitext(displayCountry):done():done()
        local params = {
                infoBox:tag('tr'):tag('th'):wikitext('ccTLD'):done():tag('td'):wikitext(ccTLDText):done():done()
            string.format('[[Has country::%s]]', queryCountryName),
                infoBox:tag('tr'):tag('th'):wikitext('ICANN region'):done():tag('td'):wikitext(regionText):done():done()
            '[[Category:Country]]',
                infoBox:tag('tr'):tag('th'):wikitext('ISOC chapter'):done():tag('td'):wikitext(ISOCText):done():done()
            '?Has description',
                infoBox:tag('tr'):tag('th'):wikitext('Youth IGF'):done():tag('td'):wikitext(youthText):done():done()
            format = 'plain',
                infoBoxWrapper:wikitext(flagImageWikitext)
             limit  = 1
                return tostring(infoBoxWrapper)
             end
         }
         }
    end,
        local data = askCached('overview:' .. queryCountryName, params)
    intro = function(template, args)
        local desc = (data[1] and data[1]['Has description']) or ''
         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>'
         if desc == '' then
         local processedText = template.current_frame:preprocess(rawIntroText)
            desc = 'No overview description found for ' .. displayCountry .. '.'
         return {
         end
             id = 'intro',
        return '== Overview ==\n' .. desc
            height = 2, -- Assign a fixed height proxy for the intro text
    end
             renderFunc = function() return processedText end
}
 
-- ANCHOR: ORGANIZATIONS
template.config.blocks.organizations = {
    feature = 'organizations',
    render = function(template, args)
         local params = {
             string.format('[[Has country::%s]] [[Has entity type::Organization]]', args.has_country),
             limit    = 50
         }
         }
    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)
         if #data == 0 then return nil end
         return renderTable(data, {'Organizations'})
        return {
    end
            id = 'organizations',
}
            content = data,
 
            height = #data + 1, -- +1 for the header
-- ANCHOR: PEOPLE
            renderFunc = function(d)
template.config.blocks.people = {
                local t = html.create('table'):addClass('wikitable')
    feature = 'people',
                t:tag('tr'):tag('th'):wikitext('Organizations'):done():done()
    render = function(template, args)
                for _, row in ipairs(d) do
        local params = {
                    t:tag('tr'):tag('td'):wikitext(safeField(row, 'result')):done():done()
            string.format('[[Has country::%s]] [[Has entity type::Person]]',  
                end
            args.has_country),
                return tostring(t)
             limit    = 20
             end
         }
         }
        local data = askCached('people:' .. args.has_country, params)
        return renderTable(data, {'People'})
     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)
-- ANCHOR: REGULATIONS
        if #data == 0 then return nil end
template.config.blocks.laws = {
        return {
    feature = 'laws',
             id = 'people',
     render = function(template, args)
            content = data,
         local params = {
            height = #data + 1,
            string.format('[[Has country::%s]]', args.has_country),
            renderFunc = function(d)
            '[[Category:Laws]]',
                local t = html.create('table'):addClass('wikitable')
             mainlabel = 'Law', sort = 'Has date', order = 'desc',
                t:tag('tr'):tag('th'):wikitext('People'):done():done()
             limit    = 50
                for _, row in ipairs(d) do
                    t:tag('tr'):tag('td'):wikitext(safeField(row, 'result')):done():done()
                end
                return tostring(t)
             end
         }
         }
        return renderSection('laws:' .. args.has_country, params, {'Laws and Regulations'})
     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)
-- ANCHOR: DOCUMENTS
        if #data == 0 then return nil end
template.config.blocks.documents = {
        return {
    feature = 'documents',
             id = 'geoTlds',
     render = function(template, args)
            content = data,
         local params = {
            height = #data + 1,
            string.format('[[Has country::%s]]', args.has_country),
            renderFunc = function(d)
            '[[Category:Document]]',
                local t = html.create('table'):addClass('wikitable')
             mainlabel = 'Document', sort = 'Has date', order = 'desc',
                t:tag('tr'):tag('th'):wikitext('GeoTLDs'):done():done()
             limit    = 50
                for _, row in ipairs(d) do
                    t:tag('tr'):tag('td'):wikitext(safeField(row, 'result')):done():done()
                end
                return tostring(t)
             end
         }
         }
        return renderSection('documents:' .. args.has_country, params, {'Key Documents'})
     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)
-- ANCHOR: GEOTLDS
        if #data == 0 then return nil end
template.config.blocks.geoTlds = {
        return {
    feature = 'geoTlds',
            id = 'meetings',
     render = function(template, args)
            content = data,
         local params = {
            height = #data + 1,
            string.format('[[Has country::%s]] [[Has entity type::TLD]] [[Has TLD subtype::geoTLD]]', args.has_country),
            renderFunc = function(d)
            limit     = 50
                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
         }
         }
        return renderSection('geoTlds:' .. args.has_country, params, {'GeoTLDs'})
     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)
-- ANCHOR: EVENTS
        if #data == 0 then return nil end
template.config.blocks.meetings = {
        return {
    feature = 'meetings',
             id = 'nra',
     render = function(template, args)
            content = data,
         local params = {
            height = #data + 1,
            string.format('[[Has country::%s]]', args.has_country),  
            renderFunc = function(d)
             '[[Has entity type::Event]]',  
                local t = html.create('table'):addClass('wikitable')
             limit    = 50
                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
        return renderSection('events:' .. args.has_country, params, {'Internet Governance Events'})
     end,
}
}


-- The main controller block that drives the masonry layout.
-- ANCHOR: AUTHORITIES
template.config.blocks.layoutController = {
template.config.blocks.nra = {
     feature = 'fullPage',
     feature = 'nra',
     render = function(template, args)
     render = function(template, args)
         local numColumns = 3 -- Define the number of columns
         local params = {
       
            string.format('[[Has country::%s]] [[Has entity type::Organization]] [[Has organization type::Government agency]]', args.has_country),
        -- 1. Collect all available content blocks
             limit    = 10
        local availableBlocks = {}
        }
        local blockSequenceForLayout = {'infoBox', 'intro', 'organizations', 'people', 'geoTlds', 'meetings', 'nra'}
        return renderSection('nra:' .. args.has_country, params, {'National Authorities'})
        for _, blockId in ipairs(blockSequenceForLayout) do
    end,
             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
-- -- REVIEW: CONNECTED COUNTRIES


        -- 2. Sort blocks by height, descending. This is key for the greedy algorithm.
-- ANCHOR: RESOURCES
         table.sort(availableBlocks, function(a, b)
template.config.blocks.resources = {
             return a.height > b.height
    feature = 'resources',
        end)
    render = function(template, args)
         local params = {
            string.format('[[Has country::%s]]', args.has_country),
            '[[Category:Resource]]',
            mainlabel = 'Resource',
             limit    = 10
        }
        return renderSection('resources:' .. args.has_country, params, {'Resources'})
    end,
}


        -- 3. Distribute blocks into columns using a greedy algorithm
-- Data wrapper blocks
        local columns = {}
template.config.blocks.dataWrapperOpen = {
        for i = 1, numColumns do
    feature = 'fullPage',
            columns[i] = { height = 0, blocks = {} }
    render  = function() return '<div class="country-hub-data-container">' end
        end
}
template.config.blocks.dataWrapperClose = {
    feature = 'fullPage',
    render  = function() return '</div>' end
}


        for _, block in ipairs(availableBlocks) do
-- Wrapper close block
            -- Find the shortest column
template.config.blocks.wrapperClose = {
            local shortestColumnIndex = 1
    feature = 'countryWrapper',
            for i = 2, numColumns do
    render  = function() return '</div>' end
                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
-- Preprocessor: Seeds 'country', 'has_country' (normalized), and 'region' arguments; ensures these key values are available and standardized early in the rendering process
 
-- ================================================================================
-- #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 351: Line 507:
end)
end)


-- Semantic property provider: Sets 'Has country' and 'Has ICANN region' semantic properties
Blueprint.registerPropertyProvider(template, function(_, args)
Blueprint.registerPropertyProvider(template, function(_, args)
     local props = {}
     local props = {}
Line 360: Line 517:
end)
end)


-- Category provider
Blueprint.registerCategoryProvider(template, function(_, args)
Blueprint.registerCategoryProvider(template, function(_, args)
     local cats = {"Country Hub"}
     local cats = {"Country Hub"}
Line 369: Line 527:
end)
end)


-- #endregion
-- Render entry point; wraps the Blueprint rendering process with error protection
 
-- ================================================================================
-- #region RENDER ENTRY POINT
-- ================================================================================
 
function p.render(frame)
function p.render(frame)
     return ErrorHandling.protect(
     return ErrorHandling.protect(
Line 385: Line 538:


return p
return p
-- #endregion

Revision as of 15:50, 6 June 2025

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 discrete content blocks

-- 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 -- Handle for Semantic MediaWiki's #ask functionality, if available

-- askCached: Performs a Semantic MediaWiki #ask query with caching; uses TemplateHelpers.generateCacheKey to create a unique key for the query and TemplateHelpers.withCache to manage the caching logic.
-- @param key string: A unique identifier part for the query, used for generating the cache key.
-- @param params table: Parameters to be passed to the smw.ask function.
-- @return table: The result of the SMW query, or an empty table if SMW is unavailable or the query fails.
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; it first tries to access the field by its string 'key'. If not found or empty, it falls back to the first element of the row (index 1).
-- @param row table: The data row (a table).
-- @param key string/number: The key or index of the field to retrieve.
-- @return string: The field's value, or an empty string if not found or if the row is nil.
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 -- Fallback for SMW results where data might be in the first unnamed field
    return ''
end

-- renderTable: Generates HTML for a standard MediaWiki table
local function renderTable(result, columns)
    local t = html.create('table'):addClass('wikitable')
    -- Header row
    local header = t:tag('tr')
    for _, col in ipairs(columns) do
        header:tag('th'):wikitext(col):done()
    end
    -- Data rows
    for _, row in ipairs(result) do
        local tr = t:tag('tr')
        for _, col in ipairs(columns) do
tr:tag('td'):wikitext(safeField(row, col)):done()
        end
    end
    return tostring(t)
end

-- renderSection: A wrapper function that performs a cached SMW query and renders a table only if results are found.
-- @param key string: The base cache key for the query.
-- @param params table: The parameters for the smw.ask query.
-- @param columns table: A list of column headers for the table.
-- @return string: An HTML table if data is found, otherwise an empty string.
local function renderSection(key, params, columns)
    local data = askCached(key, params)
    if not data or #data == 0 then
        return ''
    end
    return renderTable(data, columns)
end

-- -- generateProcessedBrowseLink: Constructs and preprocesses a MediaWiki link for browsing data, with {{fullurl:...}} correctly expanded by MediaWiki before the link is rendered.
-- local function generateProcessedBrowseLink(template, browseType, browseQueryParam, linkTextPattern, country)
--     -- Explicitly URL-encode the country name for use in the URL query parameter
--     local encodedCountry = mw.uri.encode(country, 'QUERY') -- 'QUERY' mode is for query string values

--     local browseLinkString = string.format(
--         '[{{fullurl:Special:BrowseData/%s|%s=%s}} %s →]',
--         browseType,
--         browseQueryParam, 
--         encodedCountry,  -- Use the encoded country name for the URL part
--         string.format(linkTextPattern, country) -- Use the original country name for the display text
--     )
    
--     if template.current_frame and template.current_frame.preprocess then
--         local ok, result = pcall(function() return template.current_frame:preprocess(browseLinkString) end)
--         if ok then
--             return result
--         else
--             return browseLinkString -- Fallback on preprocess error
--         end
--     end
--     return browseLinkString -- Fallback if no frame or preprocess
-- end

local errorContext = ErrorHandling.createContext("T-CountryHub")

-- ================================================================================

-- CONTROL OF TEMPLATE FEATURES: THIS LIST SPECIFIES IN AN EXPLICIT MANNER WHAT FEATURES ARE TO BE CALLED/RENDERED BY THE TEMPLATE.

local template = Blueprint.registerTemplate('CountryHub', {
    features = {
        fullPage = true, -- does not render as a template box, but rather occupies the entire page
        countryWrapper = true,
        countryFlag = true,
        infoBox = true,
        intro = true,
        overview = false,
        organizations = true,
        people = true,
        laws = false,
        documents = false,
        geoTlds = true,
        meetings = true,
        nra = true,
        resources = false,
        semanticProperties = true,
        categories = true,
        errorReporting = true
    },
    constants = {
        tableClass = ""
    }
})

-- Blueprint default: Initialize standard configuration
Blueprint.initializeConfig(template)

-- CONTROL THE VISUAL ORDER THAT EACH ASPECT IS RENDERED IN
template.config.blockSequence = {
    'wrapperOpen',
    'infoBox',
    'intro',
    'overview',
    'dataWrapperOpen',
    'organizations',
    'people',
    'laws',
    'documents',
    'geoTlds',
    'meetings',
    'nra',
    'resources',
    'dataWrapperClose',
    'wrapperClose',
    'categories',
    'errors'
}

-- ================================================================================

template.config.blocks = template.config.blocks or {}

-- Wrapper open block: Defines the opening div for the main content wrapper and includes the flag image
template.config.blocks.wrapperOpen = {
    feature = 'countryWrapper',
    render  = function(template, args) -- Added template, args
        -- local normalizedCountryName = args.has_country -- From preprocessor
        -- local flagImageWikitext = ""

        -- if normalizedCountryName and normalizedCountryName ~= "" then
        --     local isoCode = CountryData.getCountryCodeByName(normalizedCountryName)
        --     if isoCode and #isoCode == 2 then
        --         local flagFile = "Flag-" .. string.lower(isoCode) .. ".svg"
        --         -- Using a new class 'country-wrapper-bg-image' for the image
        --         flagImageWikitext = string.format(
        --             "[[File:%s|link=|class=country-wrapper-bg-image]]", 
        --             flagFile
        --         )
        --     end
        -- end
        
        return '<div class="country-hub-wrapper">' -- .. flagImageWikitext -- Appended flag
    end
}

-- ANCHOR: INFOBOX
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 -- Fetch all chapters to perform fuzzy matching
        }
        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
        if youthData[1] and youthData[1]['result'] and youthData[1]['result'] ~= '' then
            youthText = youthData[1]['result']
        else
            youthText = string.format('[[Youth IGF %s]]', args.has_country)
        end
        
        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')
        
        -- Header row
        infoBox:tag('tr')
            :tag('th')
                :attr('colspan', '2')
                :addClass('country-hub-infobox-header icannwiki-automatic-text')
                :wikitext(string.format('%s', displayCountry))
                :done()
            :done()
        
        -- ccTLD row
        infoBox:tag('tr')
            :tag('th'):wikitext('ccTLD'):done()
            :tag('td'):wikitext(ccTLDText):done()
            :done()
        
        -- ICANN region row
        infoBox:tag('tr')
            :tag('th'):wikitext('ICANN region'):done()
            :tag('td'):wikitext(regionText):done()
            :done()
        
        -- REVIEW: Check for ccNSO membership or affiliation in the country / https://ccnso.icann.org/en/about/members.htm

        -- ISOC chapter row
        infoBox:tag('tr')
            :tag('th'):wikitext('ISOC chapter'):done()
            :tag('td'):wikitext(ISOCText):done()
            :done()
        
        -- Youth IGF row
        infoBox:tag('tr')
            :tag('th'):wikitext('Youth IGF'):done()
            :tag('td'):wikitext(youthText):done()
            :done()
        
        infoBoxWrapper:wikitext(flagImageWikitext)
        
        return tostring(infoBoxWrapper)
    end
}

-- ANCHOR: INTRO
template.config.blocks.intro = {
    feature = 'intro',
    render  = 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>'
        if template.current_frame and template.current_frame.preprocess then
            return template.current_frame:preprocess(rawIntroText)
        end
        return rawIntroText
    end
}

-- ANCHOR: OVERVIEW
template.config.blocks.overview = {
    feature = 'overview',
    render = function(template, args)
        local displayCountry = (args.country or mw.title.getCurrentTitle().text or ""):gsub('_',' ')
        local queryCountryName = args.has_country
        local params = {
            string.format('[[Has country::%s]]', queryCountryName),
            '[[Category:Country]]',
            '?Has description',
            format = 'plain',
            limit  = 1
        }
        local data = askCached('overview:' .. queryCountryName, params)
        local desc = (data[1] and data[1]['Has description']) or ''
        if desc == '' then
            desc = 'No overview description found for ' .. displayCountry .. '.'
        end
        return '== Overview ==\n' .. desc
    end
}

-- ANCHOR: ORGANIZATIONS
template.config.blocks.organizations = {
    feature = 'organizations',
    render = 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)
        return renderTable(data, {'Organizations'})
    end
}

-- ANCHOR: PEOPLE
template.config.blocks.people = {
    feature = 'people',
    render = function(template, args)
        local params = {
            string.format('[[Has country::%s]] [[Has entity type::Person]]', 
            args.has_country),
            limit     = 20
        }
        local data = askCached('people:' .. args.has_country, params)
        return renderTable(data, {'People'})
    end,
}

-- ANCHOR: REGULATIONS
template.config.blocks.laws = {
    feature = 'laws',
    render = function(template, args)
        local params = {
            string.format('[[Has country::%s]]', args.has_country),
            '[[Category:Laws]]',
            mainlabel = 'Law', sort = 'Has date', order = 'desc',
            limit     = 50
        }
        return renderSection('laws:' .. args.has_country, params, {'Laws and Regulations'})
    end,
}

-- ANCHOR: DOCUMENTS
template.config.blocks.documents = {
    feature = 'documents',
    render = function(template, args)
        local params = {
            string.format('[[Has country::%s]]', args.has_country),
            '[[Category:Document]]',
            mainlabel = 'Document', sort = 'Has date', order = 'desc',
            limit     = 50
        }
        return renderSection('documents:' .. args.has_country, params, {'Key Documents'})
    end,
}

-- ANCHOR: GEOTLDS
template.config.blocks.geoTlds = {
    feature = 'geoTlds',
    render = function(template, args)
        local params = {
            string.format('[[Has country::%s]] [[Has entity type::TLD]] [[Has TLD subtype::geoTLD]]', args.has_country),
            limit     = 50
        }
        return renderSection('geoTlds:' .. args.has_country, params, {'GeoTLDs'})
    end,
}

-- ANCHOR: EVENTS
template.config.blocks.meetings = {
    feature = 'meetings',
    render = function(template, args)
        local params = {
            string.format('[[Has country::%s]]', args.has_country), 
            '[[Has entity type::Event]]', 
            limit     = 50
        }
        return renderSection('events:' .. args.has_country, params, {'Internet Governance Events'})
    end,
}

-- ANCHOR: AUTHORITIES
template.config.blocks.nra = {
    feature = 'nra',
    render = function(template, args)
        local params = {
            string.format('[[Has country::%s]] [[Has entity type::Organization]] [[Has organization type::Government agency]]', args.has_country),
            limit     = 10
        }
        return renderSection('nra:' .. args.has_country, params, {'National Authorities'})
    end,
}

-- -- REVIEW: CONNECTED COUNTRIES

-- ANCHOR: RESOURCES
template.config.blocks.resources = {
    feature = 'resources',
    render = function(template, args)
        local params = {
            string.format('[[Has country::%s]]', args.has_country),
            '[[Category:Resource]]',
            mainlabel = 'Resource',
            limit     = 10
        }
        return renderSection('resources:' .. args.has_country, params, {'Resources'})
    end,
}

-- Data wrapper blocks
template.config.blocks.dataWrapperOpen = {
    feature = 'fullPage',
    render  = function() return '<div class="country-hub-data-container">' end
}
template.config.blocks.dataWrapperClose = {
    feature = 'fullPage',
    render  = function() return '</div>' end
}

-- Wrapper close block
template.config.blocks.wrapperClose = {
    feature = 'countryWrapper',
    render  = function() return '</div>' end
}

-- Preprocessor: Seeds 'country', 'has_country' (normalized), and 'region' arguments; ensures these key values are available and standardized early in the rendering process
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)

-- Semantic property provider: Sets 'Has country' and 'Has ICANN region' semantic properties
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)

-- Category provider
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)

-- Render entry point; wraps the Blueprint rendering process with error protection
function p.render(frame)
    return ErrorHandling.protect(
        errorContext, "render",
        function() return template.render(frame) end,
        ErrorHandling.getMessage("TEMPLATE_RENDER_ERROR"),
        frame
    )
end

return p