Jump to content

Module:T-CountryHub

Documentation for this module may be created at Module:T-CountryHub/doc

-- Modules/T-CountryHub.lua
-- Blueprint-based full-page template for country hubs with 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 MasonryLayout = safeRequire('Module:MasonryLayout')
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 = true,
        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',
    '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 {}

-- 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
}

-- Feature Preview Banner
template.config.blocks.featureBanner = {
    feature = 'fullPage',
    render = function(template, args)
        return '<div class="country-hub-feature-banner">' ..
               '<strong>Country Hubs</strong> have been enabled as a feature preview for ICANN 83, and are under <strong>HEAVY TESTING</strong>. ' ..
               'Help us improve them and, especially, contribute more knowledge to our database so that they can keep growing!' ..
               '</div>'
    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     = 20
        }
        local data = askCached('organizations:' .. args.has_country, params)
        -- Store the raw count for masonry layout
        template._rawDataCounts = template._rawDataCounts or {}
        template._rawDataCounts.organizations = #data
        -- Only render if we have data
        if #data == 0 then
            return ''
        end
        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)
        -- Store the raw count for masonry layout
        template._rawDataCounts = template._rawDataCounts or {}
        template._rawDataCounts.people = #data
        -- Only render if we have data
        if #data == 0 then
            return ''
        end
        return renderTable(data, {'People'})
    end,
}

-- ANCHOR: REGULATIONS
template.config.blocks.laws = {
    feature = 'laws',
    render = function(template, args)
        local params = {
            string.format('[[Has country::%s]] [[Has entity type::Norm]]', args.has_country),
            limit     = 20
        }
        local data = askCached('laws:' .. args.has_country, params)
        -- Store the raw count for masonry layout
        template._rawDataCounts = template._rawDataCounts or {}
        template._rawDataCounts.laws = #data
        -- Only render if we have data
        if #data == 0 then
            return ''
        end
        return renderTable(data, {'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     = 20
        }
        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     = 20
        }
        local data = askCached('geoTlds:' .. args.has_country, params)
        -- Store the raw count for masonry layout
        template._rawDataCounts = template._rawDataCounts or {}
        template._rawDataCounts.geoTlds = #data
        -- Only render if we have data
        if #data == 0 then
            return ''
        end
        return renderTable(data, {'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     = 20
        }
        local data = askCached('events:' .. args.has_country, params)
        -- Store the raw count for masonry layout
        template._rawDataCounts = template._rawDataCounts or {}
        template._rawDataCounts.meetings = #data
        -- Only render if we have data
        if #data == 0 then
            return ''
        end
        return renderTable(data, {'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     = 20
        }
        local data = askCached('nra:' .. args.has_country, params)
        -- Store the raw count for masonry layout
        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,
}

-- -- REVIEW: CONNECTED COUNTRIES (COLONIES)

-- 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     = 20
        }
        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
}

-- INTELLIGENT MASONRY LAYOUT INTEGRATION
-- Single block that handles all masonry logic at render-time (Blueprint pattern)
template.config.blocks.intelligentMasonry = {
    feature = 'fullPage',
    render = function(template, args)
        return MasonryLayout.renderIntelligentLayout(template, args, {
            cardDefinitions = cardDefinitions,
            options = masonryOptions,
            blockRenderers = {
                intro = template.config.blocks.intro,
                organizations = template.config.blocks.organizations,
                people = template.config.blocks.people,
                geoTlds = template.config.blocks.geoTlds,
                meetings = template.config.blocks.meetings,
                nra = template.config.blocks.nra,
                laws = template.config.blocks.laws,
                documents = template.config.blocks.documents,
                resources = template.config.blocks.resources,
                infoBox = template.config.blocks.infoBox
            }
        })
    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