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

--------------------------------------------------------------------------------
-- Module:Hatnote
-- Unified hatnote system for disambiguation, navigation, and cross-references.
--
-- INSTALLATION:
-- 1. Create page: Module:Hatnote
-- 2. Paste this entire file
-- 3. Create TemplateStyles page: Module:Hatnote/styles.css
--
-- USAGE:
-- {{#invoke:Hatnote|hatnote|Custom text here}}
-- {{#invoke:Hatnote|about|topic|other topic|Other article}}
-- {{#invoke:Hatnote|main|Article name}}
-- {{#invoke:Hatnote|seealso|Article 1|Article 2}}
-- {{#invoke:Hatnote|further|Article name}}
-- {{#invoke:Hatnote|redirect|Term|description|Article}}
-- {{#invoke:Hatnote|distinguish|Article 1|Article 2}}
--
-- @author Remilia Wiki
-- @license MIT
--------------------------------------------------------------------------------

local p = {}

--------------------------------------------------------------------------------
-- CONFIGURATION
--------------------------------------------------------------------------------

-- Maximum number of links to support in multi-link templates
local MAX_LINKS = 10

--------------------------------------------------------------------------------
-- UTILITY FUNCTIONS
--------------------------------------------------------------------------------

local function trim(s)
    if not s then return nil end
    return tostring(s):match("^%s*(.-)%s*$")
end

local function isEmpty(s)
    if s == nil then return true end
    if type(s) == 'string' then
        return trim(s) == ''
    end
    return false
end

local function hasValue(s)
    return not isEmpty(s)
end

local function getArg(args, name, default)
    local val = args[name]
    if isEmpty(val) then return default end
    return trim(val)
end

local function getFrameArgs(frame)
    local args = {}
    local parentArgs = frame:getParent() and frame:getParent().args or {}
    local directArgs = frame.args or {}
    for k, v in pairs(directArgs) do args[k] = v end
    for k, v in pairs(parentArgs) do args[k] = v end
    return args
end

--- Create a wikilink with optional display text
local function makeLink(target, display)
    if isEmpty(target) then return nil end
    target = trim(target)
    if hasValue(display) then
        return '[[' .. target .. '|' .. trim(display) .. ']]'
    else
        return '[[' .. target .. ']]'
    end
end

--- Build a comma-separated list from numbered args, using Oxford comma
local function buildLinkList(args, startIndex, maxCount)
    local links = {}
    local count = maxCount or MAX_LINKS

    for i = startIndex, startIndex + count - 1 do
        local arg = getArg(args, i)
        if hasValue(arg) then
            table.insert(links, makeLink(arg))
        else
            break
        end
    end

    local n = #links
    if n == 0 then
        return nil
    elseif n == 1 then
        return links[1]
    elseif n == 2 then
        return links[1] .. ' or ' .. links[2]
    else
        local list = table.concat(links, ', ', 1, n - 1)
        return list .. ', or ' .. links[n]
    end
end

--- Build a comma-separated list with "and" for final item
local function buildLinkListAnd(args, startIndex, maxCount)
    local links = {}
    local count = maxCount or MAX_LINKS

    for i = startIndex, startIndex + count - 1 do
        local arg = getArg(args, i)
        if hasValue(arg) then
            table.insert(links, makeLink(arg))
        else
            break
        end
    end

    local n = #links
    if n == 0 then
        return nil
    elseif n == 1 then
        return links[1]
    elseif n == 2 then
        return links[1] .. ' and ' .. links[2]
    else
        local list = table.concat(links, ', ', 1, n - 1)
        return list .. ', and ' .. links[n]
    end
end

--------------------------------------------------------------------------------
-- CORE HATNOTE RENDERING
--------------------------------------------------------------------------------

--- Render a basic hatnote div
local function renderHatnote(text, extraClass)
    if isEmpty(text) then
        return ''
    end

    local div = mw.html.create('div')
        :addClass('hatnote')

    if hasValue(extraClass) then
        div:addClass(extraClass)
    end

    div:wikitext(text)

    return tostring(div)
end

--------------------------------------------------------------------------------
-- PUBLIC API
--------------------------------------------------------------------------------

--- Generic hatnote with custom text
-- Usage: {{#invoke:Hatnote|hatnote|Custom text here}}
function p.hatnote(frame)
    local args = getFrameArgs(frame)
    local text = getArg(args, 1) or getArg(args, 'text')
    local extraClass = getArg(args, 'class')

    if isEmpty(text) then
        return '<span class="error">Error: Hatnote text required</span>'
    end

    return renderHatnote(text, extraClass)
end

--- About template - disambiguation at article top
-- Usage: {{#invoke:Hatnote|about|this topic|other topic|Other article|...}}
function p.about(frame)
    local args = getFrameArgs(frame)

    local thisTopic = getArg(args, 1)
    if isEmpty(thisTopic) then
        return '<span class="error">Error: Topic description required</span>'
    end

    local text = 'This article is about ' .. thisTopic .. '.'

    -- Build "For X, see Y" pairs
    local i = 2
    while hasValue(getArg(args, i)) and hasValue(getArg(args, i + 1)) do
        local forTopic = getArg(args, i)
        local seeArticle = getArg(args, i + 1)
        text = text .. ' For ' .. forTopic .. ', see ' .. makeLink(seeArticle) .. '.'
        i = i + 2
    end

    return renderHatnote(text)
end

--- Main article template - summary sections
-- Usage: {{#invoke:Hatnote|main|Article 1|Article 2|...}}
function p.main(frame)
    local args = getFrameArgs(frame)

    local links = buildLinkListAnd(args, 1, 5)
    if not links then
        return '<span class="error">Error: Article name required</span>'
    end

    -- Count links to determine singular/plural
    local count = 0
    for i = 1, 5 do
        if hasValue(getArg(args, i)) then count = count + 1 end
    end

    local prefix = count == 1 and 'Main article: ' or 'Main articles: '
    return renderHatnote(prefix .. links, 'hatnote-main')
end

--- See also template - related articles
-- Usage: {{#invoke:Hatnote|seealso|Article 1|Article 2|...}}
function p.seealso(frame)
    local args = getFrameArgs(frame)

    local links = buildLinkListAnd(args, 1, 10)
    if not links then
        return '<span class="error">Error: Article name required</span>'
    end

    return renderHatnote('See also: ' .. links, 'hatnote-seealso')
end

--- Further information template
-- Usage: {{#invoke:Hatnote|further|Article 1|Article 2|...}}
function p.further(frame)
    local args = getFrameArgs(frame)

    local links = buildLinkListAnd(args, 1, 5)
    if not links then
        return '<span class="error">Error: Article name required</span>'
    end

    return renderHatnote('Further information: ' .. links, 'hatnote-further')
end

--- Redirect template - explains redirects
-- Usage: {{#invoke:Hatnote|redirect|Term|desc|Article|desc2|Article2|disambiguation}}
function p.redirect(frame)
    local args = getFrameArgs(frame)

    local term = getArg(args, 1)
    if isEmpty(term) then
        return '<span class="error">Error: Redirect term required</span>'
    end

    local text = '"' .. term .. '" redirects here.'

    -- First alternative
    local desc1 = getArg(args, 2)
    local art1 = getArg(args, 3)
    if hasValue(desc1) and hasValue(art1) then
        text = text .. ' For ' .. desc1 .. ', see ' .. makeLink(art1) .. '.'
    end

    -- Second alternative
    local desc2 = getArg(args, 4)
    local art2 = getArg(args, 5)
    if hasValue(desc2) and hasValue(art2) then
        text = text .. ' For ' .. desc2 .. ', see ' .. makeLink(art2) .. '.'
    end

    -- Disambiguation link
    local disambig = getArg(args, 6)
    if hasValue(disambig) and disambig:lower() == 'disambiguation' then
        text = text .. ' For other uses, see ' .. makeLink(term .. ' (disambiguation)') .. '.'
    end

    return renderHatnote(text, 'hatnote-redirect')
end

--- Distinguish template - "Not to be confused with"
-- Usage: {{#invoke:Hatnote|distinguish|Article 1|Article 2|...}}
function p.distinguish(frame)
    local args = getFrameArgs(frame)

    -- Check for custom text
    local customText = getArg(args, 'text')
    if hasValue(customText) then
        return renderHatnote(customText, 'hatnote-distinguish')
    end

    local links = buildLinkList(args, 1, 10)
    if not links then
        return '<span class="error">Error: Article name required</span>'
    end

    return renderHatnote('Not to be confused with ' .. links .. '.', 'hatnote-distinguish')
end

--- For other uses template
-- Usage: {{#invoke:Hatnote|otheruses|Disambiguation page}}
function p.otheruses(frame)
    local args = getFrameArgs(frame)

    local disambig = getArg(args, 1)
    local text

    if hasValue(disambig) then
        text = 'For other uses, see ' .. makeLink(disambig) .. '.'
    else
        -- Use current page title with (disambiguation)
        local title = mw.title.getCurrentTitle()
        text = 'For other uses, see ' .. makeLink(title.text .. ' (disambiguation)') .. '.'
    end

    return renderHatnote(text, 'hatnote-otheruses')
end

--- Short description alias (returns empty, handled elsewhere)
function p.shortdesc(frame)
    local args = getFrameArgs(frame)
    local desc = getArg(args, 1) or getArg(args, 'desc')

    if isEmpty(desc) then
        return ''
    end

    -- Just output the magic word
    return '{{SHORTDESC:' .. desc .. '}}'
end

return p