Jump to content

Module:Navbox timeline: Difference between revisions

From Rolling Lounge Wiki
Trying out months change (AI)
Tag: Reverted
Replaced month text with numbers
 
(5 intermediate revisions by the same user not shown)
Line 5: Line 5:
local getArgs = require('Module:Arguments').getArgs
local getArgs = require('Module:Arguments').getArgs
local p = {}
local p = {}
-- Convert year-month to numeric value for comparison
local function dateToNumber(year, month)
return year * 12 + (month or 1) - 1
end
-- Convert numeric value back to year and month
local function numberToDate(num)
local year = math.floor(num / 12)
local month = (num % 12) + 1
return year, month
end
-- Get month abbreviation
local function getMonthNum(month)
return tostring(month)
end
-- Parse date string (supports YYYY, YYYY-MM, or YYYY-YYYY formats)
local function parseDate(dateStr)
if not dateStr then return nil, nil end
-- Match YYYY-MM format
local year, month = dateStr:match('^(%d%d%d%d)%-(%d%d?)$')
if year and month then
return tonumber(year), tonumber(month)
end
-- Match YYYY format
year = dateStr:match('^(%d%d%d%d)$')
if year then
return tonumber(year), 1
end
return nil, nil
end


-- Add blank table cells
-- Add blank table cells
local function addBlank(args, row, prev, current)
local function addBlank(args, row, prev, current)
if row and prev < current then
if row and prev < current then
if yesno(args.decades) == false then
local duration = current - prev
if yesno(args.months) ~= false then
if duration > 0 then
-- Divide the cell up by months when showing months
row:tag('td')
local date = prev
:addClass('timeline-blank')
:cssText(args.blankstyle)
while date < current do
:attr('colspan', duration)
local dur = math.min(1, current - date) -- 1 month duration
row:tag('td')
:addClass('timeline-blank')
:cssText(args.blankstyle)
:attr('colspan', dur)
date = date + dur
end
else
row:tag('td')
:addClass('timeline-blank')
:cssText(args.blankstyle)
:attr('colspan', current - prev)
end
else
-- Divide the cell up every decade if showing decades at the top
local year = prev
while year < current do
local dur = math.min(10 - year % 10, current - year)
row:tag('td')
:addClass('timeline-blank')
:cssText(args.blankstyle)
:attr('colspan', dur)
year = year + dur
end
end
end
end
end
Line 52: Line 60:
local rows = {
local rows = {
[1] = {},
[1] = {},
minYear = math.huge,
minDate = math.huge,
maxYear = -math.huge,
maxDate = -math.huge,
hasLabels = false
hasLabels = false,
useMonths = yesno(args.months) ~= false
}
}
Line 71: Line 80:
-- Data cell key value pairs
-- Data cell key value pairs
elseif key then
elseif key then
-- Data cell key value pairs
rows[#rows][cellIndex][key] = val
rows[#rows][cellIndex][key] = val
-- New row
-- New row
Line 81: Line 89:
elseif cellIndex > 0
elseif cellIndex > 0
and rows[#rows][cellIndex].item
and rows[#rows][cellIndex].item
and not rows[#rows][cellIndex].startYear
and not rows[#rows][cellIndex].startDate
then
then
local dates = mw.text.split(fullVal, '-', true)
local dates = mw.text.split(fullVal, '-', true)
local startYear = tonumber(dates[1])
local startYear, startMonth, endYear, endMonth
local endYear = tonumber(dates[2]) or tonumber(os.date('%Y')) + 1
if #dates == 2 then
-- Handle YYYY-YYYY format
startYear, startMonth = parseDate(dates[1])
endYear, endMonth = parseDate(dates[2])
elseif #dates == 3 then
-- Handle YYYY-MM-YYYY format 
startYear, startMonth = tonumber(dates[1]), tonumber(dates[2])
endYear, endMonth = parseDate(dates[3])
elseif #dates == 4 then
-- Handle YYYY-MM-YYYY-MM format
startYear, startMonth = tonumber(dates[1]), tonumber(dates[2])
endYear, endMonth = tonumber(dates[3]), tonumber(dates[4])
else
-- Single date
startYear, startMonth = parseDate(fullVal)
endYear, endMonth = startYear, (startMonth == 12) and 1 or (startMonth + 1)
if startMonth == 12 then endYear = endYear + 1 end
end
if startYear == nil then
if not startYear then
error('Argument ' .. num .. ' is an invalid time range', 0)
error('Argument ' .. num .. ' is an invalid time range', 0)
end
end
if endYear < startYear then
-- Set defaults
error('Argument ' .. num .. '\'s end year is less than the start year', 0)
startMonth = startMonth or 1
endMonth = endMonth or 1
endYear = endYear or (tonumber(os.date('%Y')) + 1)
local startDate = dateToNumber(startYear, startMonth)
local endDate = dateToNumber(endYear, endMonth)
if endDate <= startDate then
endDate = startDate + 1 -- Minimum duration of 1 month
end
end


rows[#rows][cellIndex].startDate = startDate
rows[#rows][cellIndex].endDate = endDate
rows[#rows][cellIndex].startYear = startYear
rows[#rows][cellIndex].startYear = startYear
rows[#rows][cellIndex].startMonth = startMonth
rows[#rows][cellIndex].endYear = endYear
rows[#rows][cellIndex].endYear = endYear
rows[#rows][cellIndex].endMonth = endMonth


if startYear < rows.minYear then
if startDate < rows.minDate then
rows.minYear = startYear
rows.minDate = startDate
end
end


if endYear > rows.maxYear then
if endDate > rows.maxDate then
rows.maxYear = endYear
rows.maxDate = endDate
end
end
-- New item
-- New item
Line 113: Line 151:
-- Add overrides from arguments
-- Add overrides from arguments
if args.startoffset then
if args.startoffset then
rows.minYear = rows.minYear - tonumber(args.startoffset)
rows.minDate = rows.minDate - tonumber(args.startoffset)
end
end
if args.startyear and tonumber(args.startyear) < rows.minYear then
if args.startyear then
rows.minYear = tonumber(args.startyear)
local startMonth = tonumber(args.startmonth) or 1
local customStart = dateToNumber(tonumber(args.startyear), startMonth)
if customStart < rows.minDate then
rows.minDate = customStart
end
end
end
if args.endoffset then
if args.endoffset then
rows.maxYear = rows.maxYear + tonumber(args.endoffset)
rows.maxDate = rows.maxDate + tonumber(args.endoffset)
end
end
if args.endyear and tonumber(args.endyear) > rows.maxYear then
if args.endyear then
rows.maxYear = tonumber(args.endyear)
local endMonth = tonumber(args.endmonth) or 12
local customEnd = dateToNumber(tonumber(args.endyear), endMonth)
if customEnd > rows.maxDate then
rows.maxDate = customEnd
end
end
end


return rows
return rows
end
-- Get month abbreviation
local function getMonthAbbr(month)
local months = {'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
                'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'}
return months[month] or ''
end
end


Line 141: Line 180:
local function renderDates(args, tbl, rows, invert)
local function renderDates(args, tbl, rows, invert)
local showDecades = yesno(args.decades)
local showDecades = yesno(args.decades)
local showMonths = yesno(args.months)
local useMonths = rows.useMonths
local yearRow = tbl:tag('tr')
local yearRow = tbl:tag('tr'):addClass('timeline-row')
:addClass('timeline-row')
-- Create label
-- Create label
if args.label or rows.hasLabels then
if args.label or rows.hasLabels then
local rowspan = 1
if showDecades ~= false then
rowspan = 2
elseif showDecades == false and showMonths ~= false then
rowspan = 2
end
local labelCell = mw.html.create('th')
local labelCell = mw.html.create('th')
:attr('scope', 'col')
:attr('scope', 'col')
:addClass('navbox-group timeline-label')
:addClass('navbox-group timeline-label')
:cssText(args.labelstyle)
:cssText(args.labelstyle)
:attr('rowspan', tostring(rowspan))
:attr('rowspan', (showDecades ~= false and useMonths) and '3' or
(showDecades ~= false or useMonths) and '2' or '1')
:wikitext(args.label or '')
:wikitext(args.label or '')
Line 166: Line 198:
-- Create decade row
-- Create decade row
if showDecades ~= false then
if showDecades ~= false then
local decadeRow = tbl:tag('tr')
local decadeRow = tbl:tag('tr'):addClass('timeline-row')
:addClass('timeline-row')
local currentDate = rows.minDate
local year = rows.minYear
-- Move decade row
if not invert then
if not invert then
decadeRow, yearRow = yearRow, decadeRow
decadeRow, yearRow = yearRow, decadeRow
end
end
while year < rows.maxYear do
while currentDate < rows.maxDate do
local dur = math.min(10 - year % 10, rows.maxYear - year)
local year, month = numberToDate(currentDate)
local nextDecade = math.ceil(year / 10) * 10
local decadeEnd = dateToNumber(nextDecade, 1)
local dur = math.min(decadeEnd - currentDate, rows.maxDate - currentDate)
decadeRow:tag('th')
decadeRow:tag('th')
Line 186: Line 219:
:wikitext(math.floor(year / 10) .. '0s')
:wikitext(math.floor(year / 10) .. '0s')
year = year + dur
currentDate = currentDate + dur
end
end
-- Create month row when decades are disabled but months are enabled
end
elseif showDecades == false and showMonths ~= false then
local monthRow = tbl:tag('tr')
-- Create month row if using months
:addClass('timeline-row')
local monthRow = nil
if useMonths then
-- Move month row
monthRow = tbl:tag('tr'):addClass('timeline-row')
if not invert then
if invert and showDecades ~= false then
monthRow, yearRow = yearRow, monthRow
-- Reorder for footer
monthRow, yearRow, decadeRow = decadeRow, monthRow, yearRow
end
end
end
-- Populate year row
local totalDuration = rows.maxDate - rows.minDate
local currentDate = rows.minDate
while currentDate < rows.maxDate do
local year, month = numberToDate(currentDate)
local nextYear = dateToNumber(year + 1, 1)
local yearDuration = math.min(nextYear - currentDate, rows.maxDate - currentDate)
local width = (yearDuration / totalDuration) * 100
-- Calculate total months in range
yearRow:tag('th')
local totalMonths = (rows.maxYear - rows.minYear) * 12
:attr('scope', 'col')
local monthWidth = 100 / totalMonths
:addClass('timeline-year')
:cssText(args.datestyle)
:cssText(args.yearstyle)
:cssText('width:' .. width .. '%')
:attr('colspan', yearDuration)
:wikitext(tostring(year))
for year = rows.minYear, rows.maxYear - 1 do
-- Add months for this year
for month = 1, 12 do
if useMonths and monthRow then
local yearStart = currentDate
while yearStart < nextYear and yearStart < rows.maxDate do
local monthYear, monthNum = numberToDate(yearStart)
local monthWidth = (1 / totalDuration) * 100
monthRow:tag('th')
monthRow:tag('th')
:attr('scope', 'col')
:attr('scope', 'col')
Line 210: Line 265:
:cssText(args.monthstyle)
:cssText(args.monthstyle)
:cssText('width:' .. monthWidth .. '%')
:cssText('width:' .. monthWidth .. '%')
:wikitext(getMonthAbbr(month))
:wikitext(getMonthNum(monthNum))
yearStart = yearStart + 1
end
end
end
end
end
-- Populate year row element
if showDecades == false and showMonths ~= false then
-- When showing months, each year spans 12 columns
for year = rows.minYear, rows.maxYear - 1 do
yearRow:tag('th')
:attr('scope', 'col')
:addClass('timeline-year')
:cssText(args.datestyle)
:cssText(args.yearstyle)
:attr('colspan', '12')
:wikitext(tostring(year))
end
else
-- Normal year display
local width = 100 / (rows.maxYear - rows.minYear)
for i = rows.minYear, rows.maxYear - 1 do
currentDate = nextYear
yearRow:tag('th')
:attr('scope', 'col')
:addClass('timeline-year')
:cssText(args.datestyle)
:cssText(args.yearstyle)
:cssText('width:' .. width .. '%')
:wikitext(showDecades == false and i or i % 10)
end
end
end
end
end
Line 245: Line 277:
-- Render the timeline itself
-- Render the timeline itself
local function renderTimeline(args, tbl, rows)
local function renderTimeline(args, tbl, rows)
local rowElement = nil
local rowSuffix = nil
local prev = rows.minYear
local prevItem = nil
local prevLabel = nil
local labelSpan = 0
for rowNum, row in ipairs(rows) do
for rowNum, row in ipairs(rows) do
local rowElement = tbl:tag('tr')
local rowElement = tbl:tag('tr'):addClass('timeline-row')
:addClass('timeline-row')
if labelSpan <= 0 and (rows.hasLabels or args.label) then
if rows.hasLabels or args.label then
labelSpan = tonumber(row.span) or 1
rowElement:tag('th')
prevLabel = rowElement:tag('th')
:attr('scope', 'row')
:attr('scope', 'row')
:attr('rowspan', labelSpan)
:addClass('navbox-group timeline-label')
:addClass('navbox-group timeline-label')
:cssText(args.labelstyle)
:cssText(args.labelstyle)
:cssText(row.labelstyle)
:cssText(row.labelstyle)
:wikitext(row.label)
:wikitext(row.label or '')
end
end
labelSpan = labelSpan - 1
local prevEndDate = rows.minDate
local prevEndYear = rows.minYear
local prevItem = nil
for cellNum, cell in ipairs(row) do
for cellNum, cell in ipairs(row) do
-- Shrink previous item so new item can start at the start year
if cell.startDate == nil then
if prevItem and prev > prevEndYear then
local colspan = prevItem:getAttr('colspan')
if yesno(args.decades) == false and yesno(args.months) ~= false then
-- Adjust for months
prevItem:attr('colspan', colspan - (prev - prevEndYear) * 12)
else
prevItem:attr('colspan', colspan - prev + prevEndYear)
end
end
 
if cell.startYear == nil then
error('Missing timerange for row ' .. rowNum .. ' cell ' .. cellNum, 0)
error('Missing timerange for row ' .. rowNum .. ' cell ' .. cellNum, 0)
end
end
-- Add blanks before the cell
-- Add blanks before the cell
addBlank(args, rowElement, prevEndYear, cell.startYear)
addBlank(args, rowElement, prevEndDate, cell.startDate)
local colspan = cell.endYear - cell.startYear
if yesno(args.decades) == false and yesno(args.months) ~= false then
-- Each year represents 12 months
colspan = colspan * 12
end
prevItem = rowElement:tag('td')
rowElement:tag('td')
:addClass('timeline-item')
:addClass('timeline-item')
:cssText(args.itemstyle)
:cssText(args.itemstyle)
:cssText(cell.style or '')
:cssText(cell.style or '')
:attr('colspan', colspan)
:attr('colspan', cell.endDate - cell.startDate)
:wikitext(cell.item)
:wikitext(cell.item)


prevEndYear = cell.endYear
prevEndDate = cell.endDate
end
end
-- Add blanks to the end of the row
-- Add blanks to the end of the row
addBlank(args, rowElement, prevEndYear, rows.maxYear)
addBlank(args, rowElement, prevEndDate, rows.maxDate)
end
-- Remove any extra rowspan from the label
if prevLabel and labelSpan > 0 then
prevLabel:attr('rowspan', prevLabel:getAttr('rowspan') - labelSpan);
end
end
end
end
Line 326: Line 322:
listpadding = '0'
listpadding = '0'
}
}
-- Arguments to passthrough to navbox
local passthrough = {
local passthrough = {
'name', 'title', 'above', 'below', 'state', 'navbar', 'border', 'image',
'name', 'title', 'above', 'below', 'state', 'navbar', 'border', 'image',
'imageleft', 'style', 'bodystyle', 'style', 'bodystyle', 'basestyle',
'imageleft', 'style', 'bodystyle', 'basestyle',
'titlestyle', 'abovestyle', 'belowstyle', 'imagestyle',
'titlestyle', 'abovestyle', 'belowstyle', 'imagestyle',
'imageleftstyle', 'titleclass', 'aboveclass', 'bodyclass', 'belowclass',
'imageleftstyle', 'titleclass', 'aboveclass', 'bodyclass', 'belowclass',
'imageclass'
'imageclass'
}
}
local rows = timelineInfo(args)
local rows = timelineInfo(args)
local wrapper = mw.html.create('table')
local wrapper = mw.html.create('table')
Line 356: Line 353:
targs.templatestyles = 'Module:Navbox timeline/styles.css'
targs.templatestyles = 'Module:Navbox timeline/styles.css'
targs.list1 = tostring(wrapper)
targs.list1 = tostring(wrapper)
targs.bodyclass = (targs.bodyclass or '') .. ' timeline-container' -- Add this line
return navbox(targs)
return navbox(targs)

Latest revision as of 17:55, 9 June 2025

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

require('strict')

local yesno = require('Module:Yesno')
local navbox = require('Module:Navbox')._navbox
local getArgs = require('Module:Arguments').getArgs
local p = {}

-- Convert year-month to numeric value for comparison
local function dateToNumber(year, month)
	return year * 12 + (month or 1) - 1
end

-- Convert numeric value back to year and month
local function numberToDate(num)
	local year = math.floor(num / 12)
	local month = (num % 12) + 1
	return year, month
end

-- Get month abbreviation
local function getMonthNum(month)
	return tostring(month)
end


-- Parse date string (supports YYYY, YYYY-MM, or YYYY-YYYY formats)
local function parseDate(dateStr)
	if not dateStr then return nil, nil end
	
	-- Match YYYY-MM format
	local year, month = dateStr:match('^(%d%d%d%d)%-(%d%d?)$')
	if year and month then
		return tonumber(year), tonumber(month)
	end
	
	-- Match YYYY format
	year = dateStr:match('^(%d%d%d%d)$')
	if year then
		return tonumber(year), 1
	end
	
	return nil, nil
end

-- Add blank table cells
local function addBlank(args, row, prev, current)
	if row and prev < current then
		local duration = current - prev
		if duration > 0 then
			row:tag('td')
				:addClass('timeline-blank')
				:cssText(args.blankstyle)
				:attr('colspan', duration)
		end
	end
end

-- Get timeline data
local function timelineInfo(args)
	local rows = {
		[1] = {},
		minDate = math.huge,
		maxDate = -math.huge,
		hasLabels = false,
		useMonths = yesno(args.months) ~= false
	}
	
	for num, fullVal in ipairs(args) do
		local key, val = fullVal:match('^([a-z]+): *(.*)$')
		local cellIndex = #rows[#rows]
		
		-- Row data key value pairs
		if cellIndex == 0 and key then
			rows[#rows][key] = val
			
			-- Record that there are labels
			if key == 'label' then
				rows.hasLabels = true
			end
		-- Data cell key value pairs
		elseif key then
			rows[#rows][cellIndex][key] = val
		-- New row
		elseif fullVal == '' then
			if next(rows[#rows]) then
				table.insert(rows, {})
			end
		-- Add date to cell with item already and no date
		elseif cellIndex > 0
			and rows[#rows][cellIndex].item
			and not rows[#rows][cellIndex].startDate
		then
			local dates = mw.text.split(fullVal, '-', true)
			local startYear, startMonth, endYear, endMonth
			
			if #dates == 2 then
				-- Handle YYYY-YYYY format
				startYear, startMonth = parseDate(dates[1])
				endYear, endMonth = parseDate(dates[2])
			elseif #dates == 3 then
				-- Handle YYYY-MM-YYYY format  
				startYear, startMonth = tonumber(dates[1]), tonumber(dates[2])
				endYear, endMonth = parseDate(dates[3])
			elseif #dates == 4 then
				-- Handle YYYY-MM-YYYY-MM format
				startYear, startMonth = tonumber(dates[1]), tonumber(dates[2])
				endYear, endMonth = tonumber(dates[3]), tonumber(dates[4])
			else
				-- Single date
				startYear, startMonth = parseDate(fullVal)
				endYear, endMonth = startYear, (startMonth == 12) and 1 or (startMonth + 1)
				if startMonth == 12 then endYear = endYear + 1 end
			end
			
			if not startYear then
				error('Argument ' .. num .. ' is an invalid time range', 0)
			end
			
			-- Set defaults
			startMonth = startMonth or 1
			endMonth = endMonth or 1
			endYear = endYear or (tonumber(os.date('%Y')) + 1)
			
			local startDate = dateToNumber(startYear, startMonth)
			local endDate = dateToNumber(endYear, endMonth)
			
			if endDate <= startDate then
				endDate = startDate + 1 -- Minimum duration of 1 month
			end

			rows[#rows][cellIndex].startDate = startDate
			rows[#rows][cellIndex].endDate = endDate
			rows[#rows][cellIndex].startYear = startYear
			rows[#rows][cellIndex].startMonth = startMonth
			rows[#rows][cellIndex].endYear = endYear
			rows[#rows][cellIndex].endMonth = endMonth

			if startDate < rows.minDate then
				rows.minDate = startDate
			end

			if endDate > rows.maxDate then
				rows.maxDate = endDate
			end
		-- New item
		else
			table.insert(rows[#rows], { item = fullVal })
		end
	end
	
	-- Add overrides from arguments
	if args.startoffset then
		rows.minDate = rows.minDate - tonumber(args.startoffset)
	end
	
	if args.startyear then
		local startMonth = tonumber(args.startmonth) or 1
		local customStart = dateToNumber(tonumber(args.startyear), startMonth)
		if customStart < rows.minDate then
			rows.minDate = customStart
		end
	end
	
	if args.endoffset then
		rows.maxDate = rows.maxDate + tonumber(args.endoffset)
	end
	
	if args.endyear then
		local endMonth = tonumber(args.endmonth) or 12
		local customEnd = dateToNumber(tonumber(args.endyear), endMonth)
		if customEnd > rows.maxDate then
			rows.maxDate = customEnd
		end
	end

	return rows
end

-- Render the date rows
local function renderDates(args, tbl, rows, invert)
	local showDecades = yesno(args.decades)
	local useMonths = rows.useMonths
	local yearRow = tbl:tag('tr'):addClass('timeline-row')
	
	-- Create label
	if args.label or rows.hasLabels then
		local labelCell = mw.html.create('th')
			:attr('scope', 'col')
			:addClass('navbox-group timeline-label')
			:cssText(args.labelstyle)
			:attr('rowspan', (showDecades ~= false and useMonths) and '3' or 
							 (showDecades ~= false or useMonths) and '2' or '1')
			:wikitext(args.label or '')
			
		yearRow:node(labelCell)
	end

	-- Create decade row
	if showDecades ~= false then
		local decadeRow = tbl:tag('tr'):addClass('timeline-row')
		local currentDate = rows.minDate
		
		if not invert then
			decadeRow, yearRow = yearRow, decadeRow
		end
		
		while currentDate < rows.maxDate do
			local year, month = numberToDate(currentDate)
			local nextDecade = math.ceil(year / 10) * 10
			local decadeEnd = dateToNumber(nextDecade, 1)
			local dur = math.min(decadeEnd - currentDate, rows.maxDate - currentDate)
			
			decadeRow:tag('th')
				:attr('scope', 'col')
				:addClass('timeline-decade')
				:cssText(args.datestyle)
				:cssText(args.decadestyle)
				:attr('colspan', dur)
				:wikitext(math.floor(year / 10) .. '0s')
			
			currentDate = currentDate + dur
		end
	end
	
	-- Create month row if using months
	local monthRow = nil
	if useMonths then
		monthRow = tbl:tag('tr'):addClass('timeline-row')
		if invert and showDecades ~= false then
			-- Reorder for footer
			monthRow, yearRow, decadeRow = decadeRow, monthRow, yearRow
		end
	end
	
	-- Populate year row
	local totalDuration = rows.maxDate - rows.minDate
	local currentDate = rows.minDate
	
	while currentDate < rows.maxDate do
		local year, month = numberToDate(currentDate)
		local nextYear = dateToNumber(year + 1, 1)
		local yearDuration = math.min(nextYear - currentDate, rows.maxDate - currentDate)
		local width = (yearDuration / totalDuration) * 100
		
		yearRow:tag('th')
			:attr('scope', 'col')
			:addClass('timeline-year')
			:cssText(args.datestyle)
			:cssText(args.yearstyle)
			:cssText('width:' .. width .. '%')
			:attr('colspan', yearDuration)
			:wikitext(tostring(year))
		
		-- Add months for this year
		if useMonths and monthRow then
			local yearStart = currentDate
			while yearStart < nextYear and yearStart < rows.maxDate do
				local monthYear, monthNum = numberToDate(yearStart)
				local monthWidth = (1 / totalDuration) * 100
				
				monthRow:tag('th')
					:attr('scope', 'col')
					:addClass('timeline-month')
					:cssText(args.datestyle)
					:cssText(args.monthstyle)
					:cssText('width:' .. monthWidth .. '%')
					:wikitext(getMonthNum(monthNum))
				
				yearStart = yearStart + 1
			end
		end
		
		currentDate = nextYear
	end
end

-- Render the timeline itself
local function renderTimeline(args, tbl, rows)
	for rowNum, row in ipairs(rows) do
		local rowElement = tbl:tag('tr'):addClass('timeline-row')
		
		if rows.hasLabels or args.label then
			rowElement:tag('th')
				:attr('scope', 'row')
				:addClass('navbox-group timeline-label')
				:cssText(args.labelstyle)
				:cssText(row.labelstyle)
				:wikitext(row.label or '')
		end
		
		local prevEndDate = rows.minDate
		
		for cellNum, cell in ipairs(row) do
			if cell.startDate == nil then
				error('Missing timerange for row ' .. rowNum .. ' cell ' .. cellNum, 0)
			end
			
			-- Add blanks before the cell
			addBlank(args, rowElement, prevEndDate, cell.startDate)
			
			rowElement:tag('td')
				:addClass('timeline-item')
				:cssText(args.itemstyle)
				:cssText(cell.style or '')
				:attr('colspan', cell.endDate - cell.startDate)
				:wikitext(cell.item)

			prevEndDate = cell.endDate
		end
		
		-- Add blanks to the end of the row
		addBlank(args, rowElement, prevEndDate, rows.maxDate)
	end
end

function p.main(frame)
	local args = getArgs(frame, {
		removeBlanks = false,
		wrappers = 'Template:Navbox timeline'
	})
	local targs = {
		listpadding = '0'
	}
	
	local passthrough = {
		'name', 'title', 'above', 'below', 'state', 'navbar', 'border', 'image',
		'imageleft', 'style', 'bodystyle', 'basestyle',
		'titlestyle', 'abovestyle', 'belowstyle', 'imagestyle',
		'imageleftstyle', 'titleclass', 'aboveclass', 'bodyclass', 'belowclass',
		'imageclass'
	}
	
	local rows = timelineInfo(args)
	local wrapper = mw.html.create('table')
		:addClass('timeline-wrapper notheme')
	local tbl = wrapper:tag('tr')
		:tag('td')
			:addClass('timeline-wrapper-cell')
			:tag('table')
				:addClass('timeline-table')
	
	renderDates(args, tbl, rows)
	renderTimeline(args, tbl, rows)
	
	if yesno(args.footer) then
		renderDates(args, tbl, rows, true)
	end
	
	for _, name in ipairs(passthrough) do 
		targs[name] = args[name]
	end

	targs.templatestyles = 'Module:Navbox timeline/styles.css'
	targs.list1 = tostring(wrapper)
	targs.bodyclass = (targs.bodyclass or '') .. ' timeline-container' -- Add this line
	
	return navbox(targs)
end

return p