Module:T-CountryHub

Revision as of 22:56, 1 June 2025 by MarkWD (talk | contribs) (// via Wikitext Extension for VSCode)
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)

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

local p = {}
local Blueprint     = safeRequire('Module:LuaTemplateBlueprint')
local ErrorHandling = safeRequire('Module:ErrorHandling')
local CountryData   = safeRequire('Module:CountryData')
local mw            = mw
local html          = mw.html
local smw           = (mw.smw and mw.smw.ask) and mw.smw or nil

-- Cache for Semantic queries
local _askCache = {}

local function askCached(key, params)
    if not smw then return {} end
    local serial = table.concat(params, '¦')
    if _askCache[key or serial] then return _askCache[key or serial] end
    local res = smw.ask(params) or {}
    _askCache[key or serial] = res
    return res
end

local function safeField(row, key)
    if not row then return '' end
    if row[key] ~= nil and row[key] ~= '' then return row[key] end
    if row[1]     ~= nil and row[1]     ~= '' then return row[1] end
    return ''
end

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

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

-- Register template with fullPage enabled
local template = Blueprint.registerTemplate('CountryHub', {
    features = {
        fullPage           = true,
        countryFlag        = true,
        countryWrapper     = true,
        infoBox            = true,
        intro              = true,
        overview           = true,
        organizations      = true,
        people             = true,
        laws               = true,
        documents          = true,
        geoTlds            = true,
        meetings           = true,
        nra                = true,
        relatedCategories  = true,
        externalResources  = true,
        semanticProperties = true,
        categories         = true,
        errorReporting     = true
    },
    constants = {
        tableClass = ""
    }
})

Blueprint.initializeConfig(template)
template.config.blocks = template.config.blocks or {}

-- Wrapper open block
template.config.blocks.wrapperOpen = {
    feature = 'countryWrapper',
    render  = function() return '<div class="country-hub-wrapper">' end
}

-- Flag block
template.config.blocks.countryFlag = {
    feature = 'countryFlag',
    render  = function(template, args)
        return Blueprint.protectedExecute(
            template,
            'CustomBlock_countryFlag',
            function()
                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
}

-- Info box block
template.config.blocks.infoBox = {
    feature = 'infoBox',
    render  = function(template, args)
        local country = args.country or mw.title.getCurrentTitle().text or ""
        country = country:gsub('_',' ')
        
        -- ccTLD query (one line changed, two lines added)
        local ccTLDParams = {
            string.format('[[Has TLD type::Country code top-level domain]] [[Has country::%s]]', country),
            mainlabel = 'ccTLD', -- give the title a predictable key
            limit = 1 -- you only need one hit
        }
local ccTLDData = askCached('infoBox:ccTLD:' .. country, ccTLDParams)
        local ccTLDText = ccTLDData[1] and ccTLDData[1]['ccTLD'] or
                        string.format('No ccTLD found for %s.', country)
        
        -- ICANN region query
        local regionParams = {
            string.format('[[Has country::%s]]', country),
            '?Has ICANN region',
            format = 'plain',
            limit = 1
        }
local regionData = askCached('infoBox:region:' .. country, regionParams)
        local regionText = regionData[1] and regionData[1]['Has ICANN region'] or ''
        
        -- ISOC chapter query
        local ISOCParams = {
            string.format('[[Category:ISOC Chapter]] [[Has country::%s]]', country),
            limit = 1
        }
local ISOCData = askCached('infoBox:ISOC:' .. country, ISOCParams)
        local ISOCText = ISOCData[1] and ISOCData[1]['result'] or ''
        
        -- Youth IGF query
        local youthParams = {
            string.format('[[Has country::%s]] [[Category:Youth IGF]]', country),
            '?Has website',
            mainlabel = 'Youth IGF',
            format = 'plain',
            sort = 'Has date',
            order = 'desc'
        }
local youthData = askCached('infoBox:youth:' .. country, youthParams)
        local youthText = youthData[1] and youthData[1]['result'] or string.format('No Youth IGF found for %s.', country)
        
        -- Build the infobox table
        local infoBox = html.create('table')
            :addClass('wikitable')
            :css('float', 'right')
            :css('margin-left', '1em')
            :css('width', '300px')
        
        -- Header row
        infoBox:tag('tr')
            :tag('th')
                :attr('colspan', '2')
                :css('text-align', 'center')
                :css('background-color', 'green')
                :wikitext(string.format('%s', country))
                :done()
            :done()
        
        -- ccTLD row
        infoBox:tag('tr')
            :tag('td'):wikitext('ccTLD'):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()
        
        -- ICANN region row
        infoBox:tag('tr')
            :tag('td'):wikitext('ICANN region'):done()
            :tag('td'):wikitext(regionText):done()
            :done()
        
        -- ISOC chapter row
        infoBox:tag('tr')
            :tag('td'):wikitext('ISOC chapter'):done()
            :tag('td'):wikitext(ISOCText):done()
            :done()
        
        -- Youth IGF row
        infoBox:tag('tr')
            :tag('td'):wikitext('Youth IGF'):done()
            :tag('td'):wikitext(youthText):done()
            :done()
        
        return tostring(infoBox)
    end
}

-- Intro paragraph block
template.config.blocks.intro = {
    feature = 'intro',
    render  = function()
        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>'
    end
}

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

-- Organizations section block
template.config.blocks.organizations = {
    feature = 'organizations',
    render = function(template, args)
        local country = args.country or mw.title.getCurrentTitle().text or ""
        country = country:gsub('_',' ')
        local params = {
            string.format('[[Has country::%s]]', country),
            '[[Category:Organization]]',
            mainlabel = 'Organization',
            limit     = 50
        }
local data = askCached('organizations:' .. country, params)
        local htmlTable = renderTable(data, {'Organization'})
        local browseLink = string.format(
            '[{{fullurl:Special:BrowseData/Organization|Has_country=%s}} Browse all organizations for %s →]',
            country, country
        )
        return '<div class="icannwiki-hubs-container"><div class="icannwiki-hub-column">\n'
             .. '=== Organizations ===\n'
             .. htmlTable .. '\n' .. browseLink .. '\n'
             .. '</div>'
    end
}

-- People section block
template.config.blocks.people = {
    feature = 'people',
    render = function(template, args)
        local country = args.country or mw.title.getCurrentTitle().text or ""
        country = country:gsub('_',' ')
        local params = {
            string.format('[[Has country::%s]]', country),
            '[[Category:Person]]',
            mainlabel = 'Person',
            limit     = 20
        }
local data = askCached('people:' .. country, params)
        local tableHtml = renderTable(data, {'Person'})
        local link = string.format(
            '[{{fullurl:Special:BrowseData/Person|Has_country=%s}} Browse all people for %s →]',
            country, country
        )
        local html = '<div class="icannwiki-hubs-container"><div class="icannwiki-hub-column">\n' ..
                     '=== People ===\n' ..
                     tableHtml .. '\n' ..
                     link .. '\n' ..
                     '</div></div>'
        return html
    end,
}

-- Laws and Regulations block
template.config.blocks.laws = {
    feature = 'laws',
    render = function(template, args)
        local country = args.country or mw.title.getCurrentTitle().text or ""
        country = country:gsub('_',' ')
        local params = {
            string.format('[[Has country::%s]]', country),
            '[[Category:Laws]]',
            '?Has title', '?Has date', '?Has description',
            mainlabel = 'Law', sort = 'Has date', order = 'desc',
            limit     = 50
        }
local data = askCached('laws:' .. country, params)
        local tableHtml = renderTable(data, {'Law','Has title','Has date','Has description'})
        local link = string.format(
            '[{{fullurl:Special:BrowseData/Laws|Has_country=%s}} Browse all laws and regulations for %s →]',
            country, country
        )
        return '=== Laws and Regulations ===\n' .. tableHtml .. '\n' .. link
    end,
}

-- Key Documents block
template.config.blocks.documents = {
    feature = 'documents',
    render = function(template, args)
        local country = args.country or mw.title.getCurrentTitle().text or ""
        country = country:gsub('_',' ')
        local params = {
            string.format('[[Has country::%s]]', country),
            '[[Category:Document]]',
            '?Has title', '?Has date', '?Has author',
            mainlabel = 'Document', sort = 'Has date', order = 'desc',
            limit     = 50
        }
local data = askCached('documents:' .. country, params)
        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,
}

-- GeoTLDs block
template.config.blocks.geoTlds = {
    feature = 'geoTlds',
    render = function(template, args)
        local country = args.country or mw.title.getCurrentTitle().text or ""
        country = country:gsub('_',' ')
        local params = {
            '[[Category:gTLD]]',
            string.format('[[Has geographic focus::%s]]', country),
            '?Has registry operator', '?Has registration date',
            mainlabel = 'GeoTLD',
            limit     = 50
        }
local data = askCached('geoTlds:' .. country, params)
        local tableHtml = renderTable(data, {'GeoTLD','Has registry operator','Has registration date'})
        local link = string.format(
            '[{{fullurl:Special:BrowseData/TLD|Has_country=%s}} Browse all TLDs for %s →]',
            country, country
        )
        return '=== GeoTLDs ===\n' .. tableHtml .. '\n' .. link
    end,
}

-- Meetings block
template.config.blocks.meetings = {
    feature = 'meetings',
    render = function(template, args)
        local country = args.country or mw.title.getCurrentTitle().text or ""
        country = country:gsub('_',' ')
        local params = {
            string.format('[[Has country::%s]]', country),
            '[[Category:Meeting]]',
            '?Has date', '?Has location', '?Has organizer',
            mainlabel = 'Meeting', sort = 'Has date', order = 'desc',
            limit     = 50
        }
local data = askCached('meetings:' .. country, params)
        local tableHtml = renderTable(data, {'Meeting','Has date','Has location','Has organizer'})
        local link = string.format(
            '[{{fullurl:Special:BrowseData/Meeting|Has_country=%s}} Browse all meetings for %s →]',
            country, country
        )
        return '=== Internet Governance Meetings ===\n' .. tableHtml .. '\n' .. link
    end,
}

-- National Regulatory Authority block
template.config.blocks.nra = {
    feature = 'nra',
    render = function(template, args)
        local country = args.country or mw.title.getCurrentTitle().text or ""
        country = country:gsub('_',' ')
        local params = {
            string.format('[[Has country::%s]]', country),
            '[[Category:Regulatory Authority]]',
            '?Has website', '?Has description',
            mainlabel = 'Authority',
            limit     = 50
        }
local data = askCached('nra:' .. country, params)
        local tableHtml = renderTable(data, {'Authority','Has website','Has description'})
        return '=== National Regulatory Authority ===\n' .. tableHtml
    end,
}

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

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

-- Define block sequence
template.config.blockSequence = {
    'countryFlag',
    'wrapperOpen',
    'infoBox',
    'intro',
    'overview',
    'organizations',
    'people',
    'laws',
    'documents',
    'geoTlds',
    'meetings',
    'nra',
    'relatedCategories',
    'externalResources',
    'wrapperClose',
    'categories',
    'errors'
}

-- Preprocessor: seed country/region
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
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
function p.render(frame)
    return ErrorHandling.protect(
        errorContext, "render",
        function() return template.render(frame) end,
        ErrorHandling.getMessage("TEMPLATE_RENDER_ERROR"),
        frame
    )
end

return p