Documentation for this module may be created at Module:Timeline/doc
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
-- 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', '85%')
: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 = tonumber(args.previous1 or args.manualprevious1),
previous2 = tonumber(args.previous2 or args.manualprevious2),
next1 = tonumber(args.next1 or args.manualnext1),
next2 = tonumber(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%')
local function getChain(directionFunc, startYear, startEra, count)
local results = {}
local currentYear = startYear
local currentEra = startEra
for _ = 1, count do
local y, e = directionFunc(currentYear, currentEra)
if not y then
break
end
table.insert(results, {
year = y,
era = e
})
currentYear = y
currentEra = e
end
return results
end
local prevYears = getChain(findNearestPrev, yearNum, era, 2)
local nextYears = getChain(findNearestNext, yearNum, era, 2)
-- Manual overrides
if manual.previous1 then
prevYears[2] = {
year = manual.previous1,
era = era
}
end
if manual.previous2 then
prevYears[1] = {
year = manual.previous2,
era = era
}
end
if manual.next1 then
nextYears[1] = {
year = manual.next1,
era = era
}
end
if manual.next2 then
nextYears[2] = {
year = manual.next2,
era = era
}
end
local navTable = nav:tag('table')
:css('width', '100%')
:css('border-collapse', 'collapse')
:css('table-layout', 'fixed')
local row = navTable:tag('tr')
-- Left side (previous years)
local leftCell = row:tag('td')
:css('text-align', 'right')
:css('vertical-align', 'middle')
:css('width', '40%')
:css('padding-right', '10px')
local leftParts = {}
for i = #prevYears, 1, -1 do
local item = prevYears[i]
table.insert(
leftParts,
makeLink(item.year, nil, false, item.era)
)
end
leftCell:wikitext(table.concat(leftParts, ' <span style="color:#777;">•</span> '))
-- Center (current year - bold, no link)
local centerCell = row:tag('td')
:css('text-align', 'center')
:css('font-weight', 'bold')
:css('vertical-align', 'middle')
:css('width', '20%')
:css('white-space', 'nowrap')
:css('font-size', '115%')
centerCell:wikitext(displayYear)
-- Right side (next years)
local rightCell = row:tag('td')
:css('text-align', 'left')
:css('vertical-align', 'middle')
:css('width', '40%')
:css('padding-left', '10px')
local rightParts = {}
for _, item in ipairs(nextYears) do
table.insert(
rightParts,
makeLink(item.year, nil, false, item.era)
)
end
rightCell:wikitext(table.concat(rightParts, ' <span style="color:#777;">•</span> '))
-- 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 tostring(html)
end
return p