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 isValidYearPage(key)
	local title = mw.title.new(key)
	if not title or not title.exists then
		return false
	end
	return true
end

local function pageExists(year)
	local key = tostring(year)
	if existenceCache[key] ~= nil then
		return existenceCache[key]
	end
	local exists = isValidYearPage(key)
	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.
	if yearNum == 0 then
		return '<!-- Invalid year -->'
	end

	-- Parse ignore list
	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
	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 + caption
	if args.image and args.image ~= '' then
		html:tag('div')
			:css('text-align', 'center')
			:css('margin', '10px 0 6px 0')
			:wikitext('[[' .. args.image .. '|300px]]')

		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

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

	-- Helper to check if we're near the BCE/CE boundary (only affects ~199 BCE and ~199 CE)
	local function isNearEraTransition(year)
		return year <= 200
	end

	local function findNearestPrev(year, currentEra)
		local maxSearch = 200
		if currentEra == "BCE" then
			local y = year + 1
			for _ = 1, maxSearch do
				if y > 3000 then break end
				if y ~= year and 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 y ~= year and not ignoreSet[y] and pageExists(tostring(y)) then
					return y, "CE"
				end
				y = y - 1
			end
		end

		-- Only cross to BCE when near the boundary AND we are in CE
		if currentEra == "CE" and isNearEraTransition(year) then
			local y = 1
			for _ = 1, maxSearch do
				if y > 3000 then break end
				if y ~= year and 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 = 200
		if currentEra == "BCE" then
			local y = year - 1
			for _ = 1, maxSearch do
				if y < 1 then break end
				if y ~= year and 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 > 3000 then break end
				if y ~= year and not ignoreSet[y] and pageExists(tostring(y)) then
					return y, "CE"
				end
				y = y + 1
			end
		end

		-- Only cross to CE when near the boundary AND we are in BCE
		if currentEra == "BCE" and isNearEraTransition(year) then
			local y = 1
			for _ = 1, maxSearch do
			if y > 3000 then break end
				if y ~= year and 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 nil 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', '110%')

	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 nearNextYear, nearNextEra = findNearestNext(yearNum, "BCE")
		local farNextYear,  farNextEra  = nil, nil
		if nearNextYear then
			farNextYear, farNextEra = findNearestNext(nearNextYear, nearNextEra)
		end

		local navTable = nav:tag('table')
			:css('width', '100%')
			:css('border-collapse', 'collapse')

		local row = navTable:tag('tr')

		-- Left side (previous years)
		local leftCell = row:tag('td')
			:css('text-align', 'right')
			:css('width', '45%')
			:css('padding-right', '8px')

		local leftParts = {}
		if farPrev then table.insert(leftParts, makeLink(farPrev, nil, false, "BCE")) end
		if nearPrev then table.insert(leftParts, makeLink(nearPrev, nil, false, "BCE")) end
		leftCell:wikitext(table.concat(leftParts, " • "))

		-- Center (current year - bold, no link)
		local centerCell = row:tag('td')
			:css('text-align', 'center')
  			:css('font-weight', 'bold')
			:css('width', '10%')
			:css('white-space', 'nowrap')

		-- Right side (next years)
		local rightCell = row:tag('td')
			:css('text-align', 'left')
			:css('width', '45%')
			:css('padding-left', '8px')

		local rightParts = {}
		if nearNextYear then table.insert(rightParts, makeLink(nearNextYear, nil, false, nearNextEra)) end
		if farNextYear then table.insert(rightParts, makeLink(farNextYear, nil, false, farNextEra)) end
		rightCell:wikitext(table.concat(rightParts, " • "))

		local centerText = displayYear
		if #leftParts > 0 then
			centerText = " • " .. centerText
		end
		if #rightParts > 0 then
			centerText = centerText .. " • "
		end
		centerCell:wikitext(centerText)

	else
		local nearPrevYear, nearPrevEra = findNearestPrev(yearNum, "CE")
		local farPrevYear,  farPrevEra  = nil, nil
		if nearPrevYear then
			farPrevYear, farPrevEra = findNearestPrev(nearPrevYear, nearPrevEra)
		end

		local nearNext = manual.next1 or findNearestNext(yearNum, "CE")
		local farNext  = manual.next2 or findNearestNext(nearNext or yearNum, "CE")

		local navTable = nav:tag('table')
			:css('width', '100%')
			:css('border-collapse', 'collapse')

		local row = navTable:tag('tr')

		-- Left side
		local leftCell = row:tag('td')
			:css('text-align', 'right')
			:css('width', '45%')
			:css('padding-right', '8px')
        
		local leftParts = {}
		if farPrevYear then table.insert(leftParts, makeLink(farPrevYear, nil, false, farPrevEra)) end
		if nearPrevYear then table.insert(leftParts, makeLink(nearPrevYear, nil, false, nearPrevEra)) end
		leftCell:wikitext(table.concat(leftParts, " • "))

		-- Center (current year)
		local centerCell = row:tag('td')
			:css('text-align', 'center')
			:css('font-weight', 'bold')
			:css('width', '10%')
			:css('white-space', 'nowrap')

		-- Right side
		local rightCell = row:tag('td')
			:css('text-align', 'left')
			:css('width', '45%')
			:css('padding-left', '8px')

		local rightParts = {}
		if nearNext then table.insert(rightParts, makeLink(nearNext, nil, false, "CE")) end
		if farNext then table.insert(rightParts, makeLink(farNext, nil, false, "CE")) end
		rightCell:wikitext(table.concat(rightParts, " • "))
		
		local centerText = displayYear
		if #leftParts > 0 then
			centerText = " • " .. centerText
		end
		if #rightParts > 0 then
			centerText = centerText .. " • "
		end
		centerCell:wikitext(centerText)
	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', '110%')
		: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
		if y == yearNum then
			link = "'''" .. text .. "'''"
		elseif pageExists((era == "BCE") and (y .. " BCE") or tostring(y)) then
			link = makeLink(y, text, false, era)
		else
			link = '<span style="color:#888">' .. text .. '</span>'
		end
		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
		if y == yearNum then
			link = "'''" .. text .. "'''"
		elseif pageExists((era == "BCE") and (y .. " BCE") or tostring(y)) then
			link = makeLink(y, text, false, era)
		else
			link = '<span style="color:#888">' .. text .. '</span>'
		end
		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