Jump to content

Module:SocialMedia

Documentation for this module may be created at Module:SocialMedia/doc

-- Module:SocialMedia
-- Reusable social media component meant to be called from other modules

-- ###############################################################
-- Currently supported platforms are:
-- (UPDATE THE LIST BETWEEN THE HOOK EMOJIS AS PLATFORMS GET ADDED OR REMOVED)
-- đŸȘFacebook, Github, Instagram, LinkedIn, Telegram, Threads, TikTok, X/Twitter, YoutubeđŸȘ
-- ###############################################################

local sf = {}
local TemplateHelpers = require('Module:TemplateHelpers')

-- This section defines the social platforms; can be added or deleted without consequence
-- param = handle for the platform; for "x", we also accept "twitter" for legacy purposes
-- icon  = wiki file for the icon
-- prefix = URL prefix
-- label  = used for alt text/screen readers
local socialPlatforms = {
    {
        param  = "facebook",
        icon   = "File:SocialFacebookIcon.svg",
        prefix = "https://www.facebook.com/",
        label  = "Facebook"
    },
    {
        param  = "instagram",
        icon   = "File:SocialInstagramIcon.svg",
        prefix = "https://www.instagram.com/",
        label  = "Instagram"
    },
    {
        param  = "linkedin",
        icon   = "File:SocialLinkedInIcon.svg",
        prefix = "https://www.linkedin.com/in/",
        label  = "LinkedIn"
    },
    {
        param  = "telegram",
        icon   = "File:SocialTelegramIcon.svg",
        prefix = "https://t.me/",
        label  = "Telegram"
    },
    {
        param  = "threads",
        icon   = "File:SocialThreadsIcon.svg",
        prefix = "https://www.threads.net/@",
        label  = "Threads"
    },
    {
        param  = "tiktok",
        icon   = "File:SocialTikTokIcon.svg",
        prefix = "https://www.tiktok.com/@",
        label  = "TikTok"
    },
    {
        param  = { "x", "twitter" },
        icon   = "File:SocialXIcon.svg",
        prefix = "https://x.com/",
        label  = "X (Twitter)"
    },
    {
        param  = "youtube",
        icon   = "File:SocialYouTubeIcon.svg",
        prefix = "https://www.youtube.com/@",
        label  = "YouTube"
    },
    {
        param  = "github",
        icon   = "File:SocialGithub.svg",
        prefix = "https://github.com/",
        label  = "Github"
    },
}

-- Helper function to check if text is a full URL
local function isFullUrl(text)
    return text and text:match("^https?://") ~= nil
end

-- Platform-specific normalizers table
local normalizers = {
    linkedin = function(handle)
        -- If it's not a URL, return as is
        if not isFullUrl(handle) then return handle end
        
        -- Extract the path from the URL (case insensitive for linkedin.com)
        local path = handle:lower():match("linkedin%.com/(.+)")
        if not path then return handle end
        
        -- Handle /pub/ format
        if path:match("^pub/") then
            -- Extract the name part (first segment after /pub/)
            local name = path:match("^pub/([^/]+)")
            if name then return name end
        end
        
        -- Handle /in/ format - just extract the username
        if path:match("^in/") then
            return path:match("^in/([^/]+)")
        end
        
        -- If we can't parse it, return the original
        return handle
    end
    -- Add other platform normalizers as needed
}

-- Helper function to normalize handles based on platform
local function normalizeHandle(platform, handle)
    -- Skip normalization if handle is nil or empty
    if not handle or handle == "" then return handle end
    
    -- Get the platform param (use first one if it's a table)
    local platformParam = platform.param
    if type(platformParam) == "table" then
        platformParam = platformParam[1]
    end
    
    -- If platform has a specific normalizer, use it
    if normalizers[platformParam] then
        return normalizers[platformParam](handle)
    end
    
    -- Default behavior: if it's a full URL, try to extract the handle
    if isFullUrl(handle) then
        -- Try to extract the handle from the URL based on the platform's prefix
        local prefix = platform.prefix:gsub("([%%%-%.])", "%%%1") -- Escape special pattern chars
        return handle:match(prefix .. "(.+)") or handle
    end
    
    -- If not a URL, return as is
    return handle
end

-- (Non-generalizable) Helper to find the first non-empty user handle from a param or table of params
local function getUserHandle(args, param)
    local key, handle
    if type(param) == "string" then
        key, handle = TemplateHelpers.getFieldValue(args, { key = param })
    else
        key, handle = TemplateHelpers.getFieldValue(args, { keys = param })
    end
    if handle and handle ~= "" then
        return mw.text.trim(handle)
    end
    return nil
end

-- Build a clickable icon link (24px) with alt text for accessibility: [[File:Name.svg|24px|alt=ALT_TEXT|link=URL]]
local function buildIconLink(iconFile, altText, url)
    return string.format("[[%s|24px|alt=%s|link=%s]]", iconFile, altText, url)
end

-- Build a single table row with colspan=2, containing a flex container for icons
local function buildSocialRow(icons)
    local iconMarkup = table.concat({
        '<div class="social-icons external-social">',
            table.concat(icons, ""),
        '</div>'
    }, "\n")
    
    return table.concat({
        "|-",
        '| colspan="2" |',
        iconMarkup
    }, "\n")
end

-- Main render function, takes a table of arguments (handles), returns wiki markup with social icons
function sf.render(args)
    -- Early return if args is empty or nil
    if not args or next(args) == nil then
        return ""
    end
    
    -- Pre-allocate icons table based on maximum possible size (number of platforms)
    local icons = {}
    icons[#socialPlatforms] = nil  -- Pre-allocate table to avoid reallocation
    local iconCount = 0

    for _, platform in ipairs(socialPlatforms) do
        local handle = getUserHandle(args, platform.param)
        if handle and handle ~= "" then
            -- Normalize the handle based on platform
            local normalizedHandle = normalizeHandle(platform, handle)
            
            -- Check for special FULLURL: prefix from Python transformation
            local url
            if handle:match("^FULLURL:") then
                -- Extract the URL part after the FULLURL: prefix
                url = handle:gsub("^FULLURL:", "")
                print("Found special FULLURL prefix, using direct URL: " .. url)
            -- If the handle is already a full URL, use it directly
            elseif isFullUrl(handle) and not isFullUrl(normalizedHandle) then
                -- We have a full URL that was successfully normalized to a handle
                url = platform.prefix .. normalizedHandle
            elseif isFullUrl(handle) then
                -- We have a full URL that couldn't be normalized, use it directly
                url = handle
            else
                -- We have a regular handle, use the standard approach
                url = platform.prefix .. handle
            end
            
            local iconLink = buildIconLink(platform.icon, platform.label, url)
            iconCount = iconCount + 1
            icons[iconCount] = iconLink  -- Direct index assignment is faster than table.insert
        end
    end

    -- If no handles were provided, return nothing
    if #icons == 0 then
        return ""
    end

    -- Return a row with a flex container for icons
    return buildSocialRow(icons)
end

return sf