Module:T-CountryHub: Difference between revisions

// via Wikitext Extension for VSCode
 
Maintenance update // via Wikitext Extension for VSCode
 
(119 intermediate revisions by the same user not shown)
Line 2: Line 2:
-- Blueprint-based full-page template for country hubs with discrete content blocks
-- Blueprint-based full-page template for country hubs with discrete content blocks


-- 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)
Line 15: Line 15:
end
end


-- Requires
local p = {}
local p = {}
local Blueprint    = safeRequire('Module:LuaTemplateBlueprint')
local Blueprint    = safeRequire('Module:LuaTemplateBlueprint')
local ErrorHandling = safeRequire('Module:ErrorHandling')
local ErrorHandling = safeRequire('Module:ErrorHandling')
local CountryData  = safeRequire('Module:CountryData')
local CountryData  = safeRequire('Module:CountryData')
local TemplateHelpers = safeRequire('Module:TemplateHelpers')
local LinkParser = safeRequire('Module:LinkParser')
local NormalizationText = safeRequire('Module:NormalizationText')
local NormalizationDiacritic = safeRequire('Module:NormalizationDiacritic')
local MasonryLayout = safeRequire('Module:MasonryLayout')
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


-- Cache for Semantic queries
-- 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)
     if not smw then return {} end
     if not smw then return {} end
     local serial = table.concat(params, '¦')
     local cacheKey = TemplateHelpers.generateCacheKey('CountryHub:ask', key)
     if _askCache[key or serial] then return _askCache[key or serial] end
     return TemplateHelpers.withCache(cacheKey, function()
    local res = smw.ask(params) or {}
        return smw.ask(params) or {}
     _askCache[key or serial] = res
     end)
    return res
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)
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


-- renderTable: Generates HTML for a standard MediaWiki table
local function renderTable(result, columns)
local function renderTable(result, columns)
     local t = html.create('table'):addClass('wikitable')
     local t = html.create('table'):addClass('wikitable')
Line 57: Line 69:
     end
     end
     return tostring(t)
     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
end


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


-- Register template with fullPage enabled
-- ================================================================================
 
-- 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
         countryFlag        = true,
         countryWrapper = true,
         countryWrapper    = true,
         countryFlag = true,
         infoBox           = true,
         infoBox = true,
         intro             = true,
         intro = true,
         overview           = true,
         overview = false,
         organizations     = true,
         organizations = true,
         people             = true,
         people = true,
         laws               = true,
         laws = true,
         documents         = true,
         documents = false,
         geoTlds           = true,
         geoTlds = true,
         meetings           = true,
         meetings = true,
         nra               = true,
         nra = true,
         relatedCategories  = true,
         resources = false,
        externalResources  = true,
         semanticProperties = true,
         semanticProperties = true,
         categories         = true,
         categories = true,
         errorReporting     = true
         errorReporting = true
     },
     },
     constants = {
     constants = {
Line 88: Line 115:
})
})


-- Blueprint default: Initialize standard configuration
Blueprint.initializeConfig(template)
Blueprint.initializeConfig(template)
-- CONTROL THE VISUAL ORDER THAT EACH ASPECT IS RENDERED IN
template.config.blockSequence = {
    'wrapperOpen',
    'featureBanner',
    'overview',
    'intelligentMasonry',
    'wrapperClose',
    'categories',
    'errors'
}
-- MASONRY LAYOUT CONFIGURATION
local cardDefinitions = {
    {blockId = 'intro', feature = 'intro', title = 'Welcome'},
    {blockId = 'organizations', feature = 'organizations', title = 'Organizations'},
    {blockId = 'people', feature = 'people', title = 'People'},
    {blockId = 'geoTlds', feature = 'geoTlds', title = 'GeoTLDs'},
    {blockId = 'meetings', feature = 'meetings', title = 'Internet Governance Events'},
    {blockId = 'nra', feature = 'nra', title = 'National Authorities'},
    {blockId = 'laws', feature = 'laws', title = 'Laws and Regulations'},
    {blockId = 'documents', feature = 'documents', title = 'Key Documents'},
    {blockId = 'resources', feature = 'resources', title = 'Resources'},
    {blockId = 'infoBox', feature = 'infoBox', title = 'Country Info'}
}
local masonryOptions = {
    columns = 3,
    mobileColumns = 1,
    containerClass = 'country-hub-masonry-container',
    columnClass = 'country-hub-masonry-column',
    cardClass = 'country-hub-masonry-card',
    -- Note: We cannot detect mobile server-side in MediaWiki
    -- The MasonryLayout will output both desktop and mobile HTML
    -- CSS media queries will handle the actual display
    mobileMode = false -- Always use desktop mode in Lua, CSS handles responsive
}
-- ================================================================================
template.config.blocks = template.config.blocks or {}
template.config.blocks = template.config.blocks or {}


-- Wrapper open block
-- Wrapper open block: Defines the opening div for the main content wrapper and includes the flag image
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 = ""
 
        -- 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
}
}


-- Flag block
-- Feature Preview Banner
template.config.blocks.countryFlag = {
template.config.blocks.featureBanner = {
     feature = 'countryFlag',
     feature = 'fullPage',
     render = function(template, args)
     render = function(template, args)
         return Blueprint.protectedExecute(
         return '<div class="country-hub-feature-banner">' ..
            template,
              '<strong>Country Hubs</strong> have been enabled as a feature preview and are still under testing.' ..
            'CustomBlock_countryFlag',
              ' Contribute more knowledge to our database so that they can keep growing!' ..
            function()
              '</div>'
                local country = args.country or mw.title.getCurrentTitle().text or ""
                country = country:gsub('_', ' ')
                local flagFile = CountryData.getFlagFileName(country)
                if not flagFile then return "" end
                local ok, fileUrl = pcall(function()
                    return mw.title.new('File:' .. flagFile):fullUrl()
                end)
                if not ok or not fileUrl then fileUrl = "" end
                return string.format(
                    '<div class="country-hub-flag" style="background-image:url(\'%s\');"></div>',
                    fileUrl
                )
            end, '', args
        )
     end
     end
}
}


-- Info box block
-- ANCHOR: INFOBOX
template.config.blocks.infoBox = {
template.config.blocks.infoBox = {
     feature = 'infoBox',
     feature = 'infoBox',
     render  = function(template, args)
     render  = function(template, args)
         local country = args.country or mw.title.getCurrentTitle().text or ""
         local displayCountry = (args.country or mw.title.getCurrentTitle().text or ""):gsub('_',' ')
        country = country:gsub('_',' ')
          
          
         -- ccTLD query (one line changed, two lines added)
         -- Derive ccTLD from ISO code via CountryData module
         local ccTLDParams = {
         local isoCode = CountryData.getCountryCodeByName(args.has_country)
            string.format('[[Has TLD type::Country code top-level domain]] [[Has country::%s]]', country),
        local ccTLDText
            mainlabel = 'ccTLD', -- give the title a predictable key
        if isoCode and #isoCode == 2 then
             limit = 1 -- you only need one hit
             local cctldValue = "." .. string.lower(isoCode)
        }
            ccTLDText = "[[" .. cctldValue .. "]]" -- Make it a wiki link
local ccTLDData = askCached('infoBox:ccTLD:' .. country, ccTLDParams)
        else
        local ccTLDText = ccTLDData[1] and ccTLDData[1]['ccTLD'] or
            ccTLDText = string.format('ccTLD data unavailable for %s.', displayCountry)
                        string.format('No ccTLD found for %s.', country)
        end
          
          
         -- ICANN region query
         -- Fetch ICANN region for the country
         local regionParams = {
         local regionParams = {
             string.format('[[Has country::%s]]', country),
             string.format('[[Has country::%s]]', args.has_country),
             '?Has ICANN region',
             '?Has ICANN region',
             format = 'plain',
             format = 'plain',
             limit = 1
             limit = 1
         }
         }
local regionData = askCached('infoBox:region:' .. 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 ''
        regionText = LinkParser.processWikiLink(regionText, 'strip')
          
          
         -- ISOC chapter query
         -- Check for an ISOC chapter in the country using fuzzy matching
         local ISOCParams = {
         local ISOCParams = {
             string.format('[[Category:ISOC Chapter]] [[Has country::%s]]', country),
             '[[Category:Internet Society Chapter]]',
             limit = 1
             limit = 200 -- Fetch all chapters to perform fuzzy matching
         }
         }
local ISOCData = askCached('infoBox:ISOC:' .. country, ISOCParams)
        local ISOCData = askCached('infoBox:ISOC:all_chapters', ISOCParams)
         local ISOCText = ISOCData[1] and ISOCData[1]['result'] or ''
         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
          
          
         -- Youth IGF query
         local flagImageWikitext = ""
         local youthParams = {
         if args.has_country and args.has_country ~= "" then
             string.format('[[Has country::%s]] [[Category:Youth IGF]]', country),
            local isoCode = CountryData.getCountryCodeByName(args.has_country)
            '?Has website',
             if isoCode and #isoCode == 2 then
            mainlabel = 'Youth IGF',
                local flagFile = "Flag-" .. string.lower(isoCode) .. ".svg"
             format = 'plain',
                flagImageWikitext = string.format(
            sort = 'Has date',
                    "[[File:%s|link=|class=country-infobox-bg-image]]",  
            order = 'desc'
                    flagFile
         }
                )
local youthData = askCached('infoBox:youth:' .. country, youthParams)
             end
        local youthText = youthData[1] and youthData[1]['result'] or string.format('No Youth IGF found for %s.', country)
        end
 
         -- Assemble the HTML for the infobox table
        local infoBoxWrapper = html.create('div')
            :addClass('country-hub-infobox-wrapper')
          
          
        -- Build the infobox table
         local infoBox = infoBoxWrapper:tag('table')
         local infoBox = html.create('table')
             :addClass('country-hub-infobox icannwiki-automatic-text')
             :addClass('wikitable')
            :css('float', 'right')
            :css('margin-left', '1em')
            :css('width', '300px')
          
          
         -- Header row
         -- Header row
Line 180: Line 296:
             :tag('th')
             :tag('th')
                 :attr('colspan', '2')
                 :attr('colspan', '2')
                 :css('text-align', 'center')
                 :addClass('country-hub-infobox-header icannwiki-automatic-text')
                :css('background-color', 'green')
                 :wikitext(string.format('%s', displayCountry))
                 :wikitext(string.format('%s', country))
                 :done()
                 :done()
             :done()
             :done()
Line 188: Line 303:
         -- ccTLD row
         -- ccTLD row
         infoBox:tag('tr')
         infoBox:tag('tr')
             :tag('td'):wikitext('ccTLD'):done()
             :tag('th'):wikitext('ccTLD'):done()
             :tag('td'):wikitext(ccTLDText):done()
             :tag('td'):wikitext(ccTLDText):done()
            :done()
       
        -- ccNSO query
        local ccNSOParams = {
            string.format('([[Category:ccNSO]] OR [[Has membership::ccNSO]]) [[Has country::%s]]', country),
            format = 'plain'
        }
local ccNSOData = askCached('infoBox:ccNSO:' .. country, ccNSOParams)
        local ccNSOText = ccNSOData[1] and ccNSOData[1]['result'] or string.format('No ccNSO membership for %s.', country)
       
        -- ccNSO row
        infoBox:tag('tr')
            :tag('td'):wikitext('ccNSO'):done()
            :tag('td'):wikitext(ccNSOText):done()
             :done()
             :done()
          
          
         -- ICANN region row
         -- ICANN region row
         infoBox:tag('tr')
         infoBox:tag('tr')
             :tag('td'):wikitext('ICANN region'):done()
             :tag('th'):wikitext('ICANN region'):done()
             :tag('td'):wikitext(regionText):done()
             :tag('td'):wikitext(regionText):done()
             :done()
             :done()
          
          
        -- REVIEW: Check for ccNSO membership or affiliation in the country / https://ccnso.icann.org/en/about/members.htm
         -- ISOC chapter row
         -- ISOC chapter row
         infoBox:tag('tr')
         infoBox:tag('tr')
             :tag('td'):wikitext('ISOC chapter'):done()
             :tag('th'):wikitext('ISOC chapter'):done()
             :tag('td'):wikitext(ISOCText):done()
             :tag('td'):wikitext(ISOCText):done()
             :done()
             :done()
          
          
         -- Youth IGF row
         infoBoxWrapper:wikitext(flagImageWikitext)
        infoBox:tag('tr')
            :tag('td'):wikitext('Youth IGF'):done()
            :tag('td'):wikitext(youthText):done()
            :done()
          
          
         return tostring(infoBox)
         return tostring(infoBoxWrapper)
     end
     end
}
}


-- Intro paragraph block
-- ANCHOR: INTRO
template.config.blocks.intro = {
template.config.blocks.intro = {
     feature = 'intro',
     feature = 'intro',
     render  = function()
     render  = function(template, args)
         return '<p>Welcome to ICANNWiki\'s hub for {{PAGENAME}}. With the use of semantics, this page aggregates all Internet Governance content for this territory that is currently indexed in our database.</p>'
         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
     end
}
}


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


-- Organizations section block
-- ANCHOR: ORGANIZATIONS
template.config.blocks.organizations = {
template.config.blocks.organizations = {
     feature = 'organizations',
     feature = 'organizations',
     render = function(template, args)
     render = function(template, args)
        local country = args.country or mw.title.getCurrentTitle().text or ""
        country = country:gsub('_',' ')
         local params = {
         local params = {
             string.format('[[Has country::%s]]', country),
             string.format('[[Has country::%s]] [[Has entity type::Organization]]', args.has_country),
            '[[Category:Organization]]',
             limit    = 20
            mainlabel = 'Organization',
             limit    = 50
         }
         }
local data = askCached('organizations:' .. country, params)
        local data = askCached('organizations:' .. args.has_country, params)
         local htmlTable = renderTable(data, {'Organization'})
         -- Store the raw count for masonry layout
         local browseLink = string.format(
         template._rawDataCounts = template._rawDataCounts or {}
            '[{{fullurl:Special:BrowseData/Organization|Has_country=%s}} Browse all organizations for %s →]',
         template._rawDataCounts.organizations = #data
            country, country
         -- Only render if we have data
         )
        if #data == 0 then
         return '<div class="icannwiki-hubs-container"><div class="icannwiki-hub-column">\n'
            return ''
            .. '=== Organizations ===\n'
        end
            .. htmlTable .. '\n' .. browseLink .. '\n'
        return renderTable(data, {'Organizations'})
            .. '</div>'
     end
     end
}
}


-- People section block
-- ANCHOR: PEOPLE
template.config.blocks.people = {
template.config.blocks.people = {
     feature = 'people',
     feature = 'people',
     render = function(template, args)
     render = function(template, args)
        local country = args.country or mw.title.getCurrentTitle().text or ""
        country = country:gsub('_',' ')
         local params = {
         local params = {
             string.format('[[Has country::%s]]', country),
             string.format('[[Has country::%s]] [[Has entity type::Person]]',  
            '[[Category:Person]]',
             args.has_country),
             mainlabel = 'Person',
             limit    = 20
             limit    = 20
         }
         }
local data = askCached('people:' .. country, params)
        local data = askCached('people:' .. args.has_country, params)
         local tableHtml = renderTable(data, {'Person'})
         -- Store the raw count for masonry layout
         local link = string.format(
         template._rawDataCounts = template._rawDataCounts or {}
            '[{{fullurl:Special:BrowseData/Person|Has_country=%s}} Browse all people for %s →]',
         template._rawDataCounts.people = #data
            country, country
         -- Only render if we have data
         )
        if #data == 0 then
         local html = '<div class="icannwiki-hubs-container"><div class="icannwiki-hub-column">\n' ..
            return ''
                    '=== People ===\n' ..
        end
                    tableHtml .. '\n' ..
        return renderTable(data, {'People'})
                    link .. '\n' ..
                    '</div></div>'
        return html
     end,
     end,
}
}


-- Laws and Regulations block
-- ANCHOR: REGULATIONS
template.config.blocks.laws = {
template.config.blocks.laws = {
     feature = 'laws',
     feature = 'laws',
     render = function(template, args)
     render = function(template, args)
        local country = args.country or mw.title.getCurrentTitle().text or ""
        country = country:gsub('_',' ')
         local params = {
         local params = {
             string.format('[[Has country::%s]]', country),
             string.format('[[Has country::%s]] [[Has entity type::Norm]]', args.has_country),
            '[[Category:Laws]]',
             limit    = 20
            '?Has title', '?Has date', '?Has description',
            mainlabel = 'Law', sort = 'Has date', order = 'desc',
             limit    = 50
         }
         }
local data = askCached('laws:' .. country, params)
        local data = askCached('laws:' .. args.has_country, params)
         local tableHtml = renderTable(data, {'Law','Has title','Has date','Has description'})
         -- Store the raw count for masonry layout
         local link = string.format(
        template._rawDataCounts = template._rawDataCounts or {}
             '[{{fullurl:Special:BrowseData/Laws|Has_country=%s}} Browse all laws and regulations for %s →]',
         template._rawDataCounts.laws = #data
            country, country
        -- Only render if we have data
         )
        if #data == 0 then
         return '=== Laws and Regulations ===\n' .. tableHtml .. '\n' .. link
             return ''
         end
         return renderTable(data, {'Laws and Regulations'})
     end,
     end,
}
}


-- Key Documents block
-- ANCHOR: DOCUMENTS
template.config.blocks.documents = {
template.config.blocks.documents = {
     feature = 'documents',
     feature = 'documents',
     render = function(template, args)
     render = function(template, args)
        local country = args.country or mw.title.getCurrentTitle().text or ""
        country = country:gsub('_',' ')
         local params = {
         local params = {
             string.format('[[Has country::%s]]', country),
             string.format('[[Has country::%s]]', args.has_country),
             '[[Category:Document]]',
             '[[Category:Document]]',
            '?Has title', '?Has date', '?Has author',
             mainlabel = 'Document', sort = 'Has date', order = 'desc',
             mainlabel = 'Document', sort = 'Has date', order = 'desc',
             limit    = 50
             limit    = 20
         }
         }
local data = askCached('documents:' .. country, params)
        return renderSection('documents:' .. args.has_country, params, {'Key Documents'})
        local tableHtml = renderTable(data, {'Document','Has title','Has date','Has author'})
        local link = string.format(
            '[{{fullurl:Special:BrowseData/Document|Has_country=%s}} Browse all documents for %s →]',
            country, country
        )
        return '=== Key Documents ===\n' .. tableHtml .. '\n' .. link
     end,
     end,
}
}


-- GeoTLDs block
-- ANCHOR: GEOTLDS
template.config.blocks.geoTlds = {
template.config.blocks.geoTlds = {
     feature = 'geoTlds',
     feature = 'geoTlds',
     render = function(template, args)
     render = function(template, args)
        local country = args.country or mw.title.getCurrentTitle().text or ""
        country = country:gsub('_',' ')
         local params = {
         local params = {
            '[[Category:gTLD]]',
             string.format('[[Has country::%s]] [[Has entity type::TLD]] [[Has TLD subtype::geoTLD]]', args.has_country),
             string.format('[[Has geographic focus::%s]]', country),
             limit    = 20
            '?Has registry operator', '?Has registration date',
            mainlabel = 'GeoTLD',
             limit    = 50
         }
         }
local data = askCached('geoTlds:' .. country, params)
        local data = askCached('geoTlds:' .. args.has_country, params)
         local tableHtml = renderTable(data, {'GeoTLD','Has registry operator','Has registration date'})
         -- Store the raw count for masonry layout
         local link = string.format(
        template._rawDataCounts = template._rawDataCounts or {}
             '[{{fullurl:Special:BrowseData/TLD|Has_country=%s}} Browse all TLDs for %s →]',
         template._rawDataCounts.geoTlds = #data
            country, country
        -- Only render if we have data
         )
        if #data == 0 then
         return '=== GeoTLDs ===\n' .. tableHtml .. '\n' .. link
             return ''
         end
         return renderTable(data, {'GeoTLDs'})
     end,
     end,
}
}


-- Meetings block
-- ANCHOR: EVENTS
template.config.blocks.meetings = {
template.config.blocks.meetings = {
     feature = 'meetings',
     feature = 'meetings',
     render = function(template, args)
     render = function(template, args)
        local country = args.country or mw.title.getCurrentTitle().text or ""
        country = country:gsub('_',' ')
         local params = {
         local params = {
             string.format('[[Has country::%s]]', country),
             string.format('[[Has country::%s]]', args.has_country),  
             '[[Category:Meeting]]',
             '[[Has entity type::Event]]',  
            '?Has date', '?Has location', '?Has organizer',
             limit    = 20
            mainlabel = 'Meeting', sort = 'Has date', order = 'desc',
             limit    = 50
         }
         }
local data = askCached('meetings:' .. country, params)
        local data = askCached('events:' .. args.has_country, params)
         local tableHtml = renderTable(data, {'Meeting','Has date','Has location','Has organizer'})
         -- Store the raw count for masonry layout
         local link = string.format(
        template._rawDataCounts = template._rawDataCounts or {}
             '[{{fullurl:Special:BrowseData/Meeting|Has_country=%s}} Browse all meetings for %s →]',
         template._rawDataCounts.meetings = #data
            country, country
        -- Only render if we have data
         )
        if #data == 0 then
         return '=== Internet Governance Meetings ===\n' .. tableHtml .. '\n' .. link
             return ''
         end
         return renderTable(data, {'Internet Governance Events'})
     end,
     end,
}
}


-- National Regulatory Authority block
-- ANCHOR: AUTHORITIES
template.config.blocks.nra = {
template.config.blocks.nra = {
     feature = 'nra',
     feature = 'nra',
     render = function(template, args)
     render = function(template, args)
        local country = args.country or mw.title.getCurrentTitle().text or ""
        country = country:gsub('_',' ')
         local params = {
         local params = {
             string.format('[[Has country::%s]]', country),
             string.format('[[Has country::%s]] [[Has entity type::Organization]] [[Has organization type::Government agency]]', args.has_country),
            '[[Category:Regulatory Authority]]',
             limit    = 20
            '?Has website', '?Has description',
            mainlabel = 'Authority',
             limit    = 50
         }
         }
local data = askCached('nra:' .. country, params)
        local data = askCached('nra:' .. args.has_country, params)
         local tableHtml = renderTable(data, {'Authority','Has website','Has description'})
         -- Store the raw count for masonry layout
        return '=== National Regulatory Authority ===\n' .. tableHtml
        template._rawDataCounts = template._rawDataCounts or {}
        template._rawDataCounts.nra = #data
        -- Only render if we have data
        if #data == 0 then
            return ''
        end
        return renderTable(data, {'National Authorities'})
     end,
     end,
}
}


-- Related Categories block
-- -- REVIEW: CONNECTED COUNTRIES (COLONIES)
template.config.blocks.relatedCategories = {
    feature = 'relatedCategories',
    render = function(template, args)
        local country = args.country or mw.title.getCurrentTitle().text or ""
        country = country:gsub('_',' ')
        return '== Related Categories ==\n' ..
              '* [[:Category:' .. country .. '|All pages in Category:' .. country .. ']]\n' ..
              '* {{#ask: [[Has country::' .. country .. ']] | format=category | limit=10 | sort=Has date | order=desc }}'
    end,
}


-- External Resources block
-- ANCHOR: RESOURCES
template.config.blocks.externalResources = {
template.config.blocks.resources = {
     feature = 'externalResources',
     feature = 'resources',
     render = function(template, args)
     render = function(template, args)
        local country = args.country or mw.title.getCurrentTitle().text or ""
        country = country:gsub('_',' ')
         local params = {
         local params = {
             string.format('[[Has country::%s]]', country),
             string.format('[[Has country::%s]]', args.has_country),
             '[[Category:External Resource]]',
             '[[Category:Resource]]',
            '?Has URL', '?Has description',
             mainlabel = 'Resource',
             mainlabel = 'Resource',
             limit    = 50
             limit    = 20
         }
         }
local data = askCached('externalResources:' .. country, params)
        return renderSection('resources:' .. args.has_country, params, {'Resources'})
        local tableHtml = renderTable(data, {'Resource','Has URL','Has description'})
        return '== External Resources ==\n' .. tableHtml
     end,
     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
}
}


Line 458: Line 529:
}
}


-- Define block sequence
-- INTELLIGENT MASONRY LAYOUT INTEGRATION
template.config.blockSequence = {
-- Single block that handles all masonry logic at render-time (Blueprint pattern)
     'countryFlag',
template.config.blocks.intelligentMasonry = {
     'wrapperOpen',
     feature = 'fullPage',
    'infoBox',
     render = function(template, args)
    'intro',
        return MasonryLayout.renderIntelligentLayout(template, args, {
    'overview',
            cardDefinitions = cardDefinitions,
    'organizations',
            options = masonryOptions,
    'people',
            blockRenderers = {
    'laws',
                intro = template.config.blocks.intro,
    'documents',
                organizations = template.config.blocks.organizations,
    'geoTlds',
                people = template.config.blocks.people,
    'meetings',
                geoTlds = template.config.blocks.geoTlds,
    'nra',
                meetings = template.config.blocks.meetings,
    'relatedCategories',
                nra = template.config.blocks.nra,
    'externalResources',
                laws = template.config.blocks.laws,
    'wrapperClose',
                documents = template.config.blocks.documents,
    'categories',
                resources = template.config.blocks.resources,
     'errors'
                infoBox = template.config.blocks.infoBox
            }
        })
     end
}
}


-- Preprocessor: seed country/region
-- 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)
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 487: Line 561:
end)
end)


-- Semantic property provider
-- 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 507: Line 581:
end)
end)


-- Render entry point
-- Render entry point; wraps the Blueprint rendering process with error protection
function p.render(frame)
function p.render(frame)
     return ErrorHandling.protect(
     return ErrorHandling.protect(