Module:Timeline

From Halopedia, the Halo wiki

Documentation for this module may be created at ModuleDoc:Timeline

local p = {}

-- Cache for page existence checks
local existenceCache = {}

-- This checks if a wiki page for a given year exists.
local function pageExists(year)
    local key = tostring(year)
    if existenceCache[key] ~= nil then
        return existenceCache[key]
    end
    local title = mw.title.new(key)
    local exists = title and title.exists
    existenceCache[key] = exists
    return exists
end

function p.main(frame)
    local args = frame:getParent().args
    local title = mw.title.getCurrentTitle().text
    
    -- Detect year and era (Example: 2552, 480 BCE, so on)
    local yearStr = args.year or title
    local yearNum, era
    
    -- This checks if the year is BCE or CE
    if yearStr:match("%d+%s*[BCE]+$") then
        yearNum = tonumber(yearStr:match("%d+"))
        era = "BCE"
    else
        yearNum = tonumber(yearStr:match("%d+")) or 0
        era = "CE"
    end
    
    -- This stops 0 from being used. (Kinda a null thing, but hey)
    if yearNum == 0 then
        return '<!-- Invalid year -->'
    end

    -- Parse ignore list (allows us to hide specific years from the smart nav and decade grid)
    local ignoreSet = {}
    if args.ignore and args.ignore ~= '' then
        for y in mw.text.gsplit(args.ignore, ",", true) do
            y = mw.text.trim(y)
            local num = tonumber(y:match("%d+"))
            if num then ignoreSet[num] = true end
        end
    end

    -- Use this to style the template.
    local html = mw.html.create('div')
        :addClass('infobox')
        :css({
            float = 'right',
            width = '300px',
            margin = '0 0 1em 1em',
            padding = '8px',
            border = '1px solid #aaa',
            background = '#f9f9f9',
            ['border-radius'] = '4px'
        })

    -- Timeline Logo Header (Update this if a new image is created)
    local logoClass = ''
    if args.image and args.image ~= '' then
        logoClass = 'notpageimage'
    end
    html:tag('div')
        :css('text-align', 'center')
        :css('margin-bottom', '10px')
        :wikitext('[[File:HP-Timeline.png|300px|link=Timeline|alt=Timeline|class=' .. logoClass .. ']]')

    -- Year Header
    local displayYear = (era == "BCE") and (yearNum .. " BCE") or tostring(yearNum)
    html:tag('div')
        :addClass('infobox-header')
        :css('text-align', 'center')
        :css('font-weight', 'bold')
        :css('font-size', '200%')
        :wikitext(displayYear)

    -- Year image
    if args.image and args.image ~= '' then
        html:tag('div')
            :css('text-align', 'center')
            :css('margin', '10px 0 6px 0')
            :wikitext('[[' .. args.image .. '|300px]]')
        
        -- Image caption
        if args.caption and args.caption ~= '' then
            html:tag('div')
                :css('text-align', 'center')
                :css('font-size', 'larger')
                :css('margin-top', '4px')
                :wikitext(args.caption)
        end
    end

    -- Other calendars
    if args.other and args.other ~= '' then
        html:tag('div')
            :css('margin-top', '10px')
            :css('text-align', 'center')
            :wikitext(args.other)
    end

    local function isUsable(y)
        if not y or ignoreSet[y] then return false end
        if y < 1 and era == "CE" then return false end
        local page = (era == "BCE") and (y .. " BCE") or tostring(y)
        return pageExists(page)
    end

    -- Override code - CIA note: This is needed for years so stuff doesnt break with timespans that go beyond the capabilities of Mediawiki.
    local manual = {
        previous1  = args.previous1  or args.manualprevious1,
        previous2 = args.previous2 or args.manualprevious2,
        next1 = args.next1 or args.manualnext1,
        next2  = args.next2  or args.manualnext2
    }

    -- Optimized find functions (with cache + limit)
    local function findNearestPrev(year, currentEra)
        local maxSearch = 500
        if currentEra == "BCE" then
            local y = year + 1
            for _ = 1, maxSearch do
                if y > 3000 then break end
                if not ignoreSet[y] and pageExists(y .. " BCE") then return y, "BCE" end
                y = y + 1
            end
        else
            local y = year - 1
            for _ = 1, maxSearch do
                if y < 1 then break end
                if not ignoreSet[y] and pageExists(tostring(y)) then return y, "CE" end
                y = y - 1
            end
            local y = 3000
            for _ = 1, maxSearch do
                if y < 1 then break end
                if not ignoreSet[y] and pageExists(y .. " BCE") then return y, "BCE" end
                y = y - 1
            end
        end
        return nil
    end

    local function findNearestNext(year, currentEra)
        local maxSearch = 500
        if currentEra == "BCE" then
            local y = year - 1
            for _ = 1, maxSearch do
                if y < 1 then break end
                if not ignoreSet[y] and pageExists(y .. " BCE") then return y, "BCE" end
                y = y - 1
            end
            local y = 1
            for _ = 1, maxSearch do
                if y > 3000 then break end
                if not ignoreSet[y] and pageExists(tostring(y)) then return y, "CE" end
                y = y + 1
            end
        else
            local y = year + 1
            for _ = 1, maxSearch do
                if y > 3000 then break end
                if not ignoreSet[y] and pageExists(tostring(y)) then return y, "CE" end
                y = y + 1
            end
        end
        return nil
    end

    local function makeLink(y, text, isBold, targetEra)
        if not y then
            return '<span style="color:#888">' .. (text or '?') .. '</span>'
        end
        local page = (targetEra == "BCE") and (y .. " BCE") or tostring(y)
        local display = (targetEra == "BCE") and (y .. " BCE") or tostring(y)
        if text then display = text end
        local link = '[[' .. page .. '|' .. display .. ']]'
        return isBold and "'''" .. link .. "'''" or link
    end

    -- === SMART NAVIGATION (THIS SECTION SUCKS OH MY GOONDESS) ===
    local nav = html:tag('div')
        :css('margin-top', '12px')
        :css('text-align', 'center')
        :css('font-size', 'larger')

    if era == "BCE" then
        local farPrev   = manual.previous1  or findNearestPrev( findNearestPrev(yearNum, "BCE") or yearNum, "BCE" )
        local nearPrev  = manual.previous2 or findNearestPrev(yearNum, "BCE")
        local nearNext  = manual.next1 or findNearestNext(yearNum, "BCE")
        local farNext   = manual.next2  or findNearestNext(nearNext or yearNum, nearNext and "BCE" or "CE")

        local parts = {
            farPrev  and makeLink(farPrev,  nil, false, "BCE") or "«",
            nearPrev and makeLink(nearPrev, nil, false, "BCE") or "",
            makeLink(yearNum, nil, true, "BCE"),
            nearNext and makeLink(nearNext, nil, false, "BCE") or "",
            farNext  and makeLink(farNext,  nil, false, "CE") or "»"
        }
        nav:wikitext(table.concat(parts, " • "))
    else
        local farPrev   = manual.previous1  or findNearestPrev( findNearestPrev(yearNum, "CE") or yearNum, "CE" )
        local nearPrev  = manual.previous2 or findNearestPrev(yearNum, "CE")
        local nearNext  = manual.next1 or findNearestNext(yearNum, "CE")
        local farNext   = manual.next2  or findNearestNext(nearNext or yearNum, "CE")

        local parts = {
            farPrev  and makeLink(farPrev,  nil, false, "CE") or "«",
            nearPrev and makeLink(nearPrev, nil, false, "CE") or "",
            makeLink(yearNum, nil, true, "CE"),
            nearNext and makeLink(nearNext, nil, false, "CE") or "",
            farNext  and makeLink(farNext,  nil, false, "CE") or "»"
        }
        nav:wikitext(table.concat(parts, " • "))
    end

    -- Decade Grid with 0s exception
    local decadeStart = math.floor(yearNum / 10) * 10
    local decadeLabel = tostring(decadeStart) .. "s"
    if era == "BCE" then decadeLabel = decadeLabel .. " BCE" end

    local decadeDiv = html:tag('div')
        :css('margin-top', '14px')
        :css('text-align', 'center')
        :css('font-size', 'larger')
        :css('line-height', '1.8')

    decadeDiv:wikitext("'''Years in the " .. decadeLabel .. "'''<br>")

    local firstRowStart = (decadeStart == 0 and era == "CE") and 1 or 0
    for i = firstRowStart, 4 do
        local y = decadeStart + i
        local text = tostring(y) .. (era == "BCE" and " BCE" or "")
        local link = isUsable(y) and makeLink(y, text, y == yearNum, era) or '<span style="color:#888">' .. text .. '</span>'
        decadeDiv:wikitext(link)
        if i < 4 then decadeDiv:wikitext(' • ') end
    end
    decadeDiv:wikitext('<br>')

    for i = 5, 9 do
        local y = decadeStart + i
        local text = tostring(y) .. (era == "BCE" and " BCE" or "")
        local link = isUsable(y) and makeLink(y, text, y == yearNum, era) or '<span style="color:#888">' .. text .. '</span>'
        decadeDiv:wikitext(link)
        if i < 9 then decadeDiv:wikitext(' • ') end
    end

    -- Footer
    html:tag('div')
        :css('margin-top', '12px')
        :css('text-align', 'center')
        :css('font-size', '85%')
        :wikitext("For a complete list, see the [[:Category:Timeline|timeline category]].")

    return html
end

return p