Module:Timeline: Difference between revisions
Jump to navigation
Jump to search
No edit summary Tag: Manual revert |
mNo edit summary |
||
| Line 6: | Line 6: | ||
local SPLIT_CHARACTERS = true | local SPLIT_CHARACTERS = true | ||
local LEGACY_MAX_EVENTS = 9 | local LEGACY_MAX_EVENTS = 9 | ||
local EMPTY_ROW_TEXT = "" | local EMPTY_ROW_TEXT = "No news" | ||
local CARGO_TABLE = "NewsPosts" | local CARGO_TABLE = "NewsPosts" | ||
| Line 205: | Line 205: | ||
end | end | ||
-- THIS IS THE ONLY FUNCTION THAT CHANGED | |||
local function renderSection(title, year, schedule) | local function renderSection(title, year, schedule) | ||
local out = {} | local out = {} | ||
-- Colors based on category | |||
local headerBg = "#3a506b" | |||
if title == "Character News" then headerBg = "#6e473b" end | |||
-- Table Wrapper | |||
out[#out + 1] = '<div style="overflow-x:auto; border: 1px solid #d1d5db; border-radius: 12px; margin-bottom: 2em; box-shadow: 0 4px 12px rgba(0,0,0,0.05);">' | |||
out[#out + 1] = '<table style="width:100%; border-collapse: collapse; background: #ffffff; margin: 0;">' | |||
-- Section Title | |||
out[#out + 1] = '<tr><th colspan="4" style="background:' .. headerBg .. '; color:#ffffff; ' | |||
.. 'font-family:\'Georgia\', serif; font-size:1.1em; letter-spacing:0.1em; text-align:center; padding:12px; ' | |||
out[#out + 1] = '<tr><th colspan="4" style="background | .. 'border-bottom:3px solid #e0a96d; text-transform:uppercase;">' | ||
.. 'text-align:center; | |||
.. title .. "</th></tr>" | .. title .. "</th></tr>" | ||
-- Column Headers | |||
local head = {} | local head = {} | ||
for _, col in ipairs(COLUMNS) do | for _, col in ipairs(COLUMNS) do | ||
head[#head + 1] = '<th style="width:' .. col[2] .. ';">' .. col[1] .. "</th>" | head[#head + 1] = '<th style="background:#f8fafc; color:#1e293b; font-family:\'Georgia\', serif; ' | ||
.. 'padding:10px; text-align:left; border-bottom:2px solid #e2e8f0; width:' .. col[2] .. ';">' .. col[1] .. "</th>" | |||
end | end | ||
out[#out + 1] = "<tr>" .. table.concat(head) .. "</tr>" | out[#out + 1] = "<tr>" .. table.concat(head) .. "</tr>" | ||
| Line 245: | Line 255: | ||
row[#row + 1] = cell( | row[#row + 1] = cell( | ||
DAY_LABELS[day] .. ", Year " .. year, | DAY_LABELS[day] .. ", Year " .. year, | ||
count > 1 and | (count > 1 and ' rowspan="' .. count .. '"' or "") .. ' style="padding:12px 10px; border-bottom:1px solid #e2e8f0; border-right:1px solid #e2e8f0; font-weight:bold; color:#1e293b; vertical-align:top;"' | ||
) | ) | ||
end | end | ||
row[#row + 1] = cell(ev.post) | row[#row + 1] = cell(ev.post, ' style="padding:12px 10px; border-bottom:1px solid #e2e8f0; border-right:1px solid #e2e8f0; vertical-align:top;"') | ||
row[#row + 1] = cell(ev.char) | row[#row + 1] = cell(ev.char, ' style="padding:12px 10px; border-bottom:1px solid #e2e8f0; border-right:1px solid #e2e8f0; vertical-align:top;"') | ||
if span[idx] then | if span[idx] then | ||
row[#row + 1] = cell( | row[#row + 1] = cell( | ||
dates[idx], | dates[idx], | ||
span[idx] > 1 and | (span[idx] > 1 and ' rowspan="' .. span[idx] .. '"' or "") .. ' style="padding:12px 10px; border-bottom:1px solid #e2e8f0; vertical-align:top; color:#64748b; font-size:0.9em; white-space:nowrap;"' | ||
) | ) | ||
end | end | ||
| Line 270: | Line 280: | ||
out[#out + 1] = "<tr>" | out[#out + 1] = "<tr>" | ||
.. cell(rangeLabel(first, last, year)) | .. cell(rangeLabel(first, last, year), ' style="padding:12px 10px; border-bottom:1px solid #e2e8f0; border-right:1px solid #e2e8f0; color:#64748b; font-style:italic; font-size:0.9em;"') | ||
.. cell(EMPTY_ROW_TEXT, ' colspan="2"') | .. cell(EMPTY_ROW_TEXT, ' colspan="2" style="padding:12px 10px; border-bottom:1px solid #e2e8f0; border-right:1px solid #e2e8f0; color:#94a3b8; font-style:italic;"') | ||
.. cell(dateText) | .. cell(dateText, ' style="padding:12px 10px; border-bottom:1px solid #e2e8f0; color:#64748b; font-size:0.9em; white-space:nowrap;"') | ||
.. "</tr>" | .. "</tr>" | ||
end | end | ||
Revision as of 10:43, 24 October 2026
Documentation for this module may be created at Module:Timeline/doc
local Calendar = require("Module:Calendar")
local p = {}
local SEPARATOR = ";"
local SPLIT_CHARACTERS = true
local LEGACY_MAX_EVENTS = 9
local EMPTY_ROW_TEXT = "No news"
local CARGO_TABLE = "NewsPosts"
local CARGO_FIELDS = "Page_Name,Title,Author,Publisher,RealDate,DayNum"
local CARGO_LIMIT = 1000
local DAY_LABELS = {}
for i, period in ipairs(Calendar.periods) do
DAY_LABELS[i] = Calendar.pretty(period)
end
local DAYS_PER_YEAR = Calendar.daysPerYear
local SECTIONS = {
Global = "Global News",
Domestic = "Domestic News",
Character = "Character News",
}
local TYPE_ALIASES = {
["global"] = "Global News",
["global news"] = "Global News",
["domestic"] = "Domestic News",
["domestic news"] = "Domestic News",
["character"] = "Character News",
["character news"] = "Character News",
}
local COLUMNS = {
{ "In-universe date", "24%" },
{ "Post", "34%" },
{ "Character / nation", "24%" },
{ "Real-world date", "18%" },
}
local SEP_PATTERN = (SEPARATOR:gsub("%p", "%%%0"))
local function trim(s)
return (tostring(s or ""):match("^%s*(.-)%s*$"))
end
local function errorText(message)
return '<strong class="error">Timeline error: ' .. message .. "</strong>"
end
local function linkify(text)
text = trim(text)
if text == "" then return "" end
if text:find("[[", 1, true) or text:find("^[%[<']") then
return text
end
return "[[" .. text .. "]]"
end
local function linkifyCharacters(text)
text = trim(text)
if text == "" then return "" end
if not SPLIT_CHARACTERS or text:find("[[", 1, true) or text:find("^[<']") then
return linkify(text)
end
local parts = {}
for part in (text .. ","):gmatch("(.-),") do
part = trim(part)
if part ~= "" then parts[#parts + 1] = linkify(part) end
end
return table.concat(parts, ", ")
end
local function normalizeTitle(name)
name = trim((tostring(name or ""):gsub("_", " ")))
return name:sub(1, 1):upper() .. name:sub(2)
end
local function linkTarget(wikitext)
local target = tostring(wikitext or ""):match("^%[%[%s*:?([^%]|]+)")
if target then
return normalizeTitle(target)
end
end
local function splitFields(line)
line = line:gsub("(&#?%w+);", "%1\1")
local fields = {}
for field in (line .. SEPARATOR):gmatch("(.-)" .. SEP_PATTERN) do
fields[#fields + 1] = trim((field:gsub("\1", ";")))
end
return fields
end
local function parseLine(line)
line = trim(line)
if line == "" then return nil end
local f = splitFields(line)
local post, char, date = f[1] or "", f[2] or "", f[3] or ""
if post == "" and char == "" and date == "" then return nil end
if post == "" then
post = '<span class="error">(missing post title)</span>'
else
post = linkify(post)
end
if #f > 3 then
post = post .. ' <span class="error">(too many “' .. SEPARATOR .. '” on this line)</span>'
end
return { post = post, char = linkifyCharacters(char), date = date }
end
local function collectManualEvents(getArg, day)
local events = {}
local text = getArg("Day" .. day)
if text and trim(text) ~= "" then
for line in (text .. "\n"):gmatch("(.-)\r?\n") do
local ev = parseLine(line)
if ev then events[#events + 1] = ev end
end
end
for n = 1, LEGACY_MAX_EVENTS do
local prefix = "Day" .. day .. "_" .. n .. "_"
local post = trim(getArg(prefix .. "Post"))
if post ~= "" then
events[#events + 1] = {
post = linkify(post),
char = linkifyCharacters(getArg(prefix .. "Char")),
date = trim(getArg(prefix .. "Date")),
}
end
end
return events
end
local function cargoEvents(typeName, year)
local byDay, names = {}, {}
for day = 1, DAYS_PER_YEAR do byDay[day] = {} end
if not (mw.ext and mw.ext.cargo) then
return byDay, names, "the Cargo extension is not available to Lua"
end
local ok, rows = pcall(mw.ext.cargo.query, CARGO_TABLE, CARGO_FIELDS, {
where = 'Type="' .. typeName .. '" AND YearNum=' .. string.format("%d", year),
orderBy = "RealDate,_pageID",
limit = CARGO_LIMIT,
})
if not ok then
return byDay, names, tostring(rows)
end
for _, row in ipairs(rows) do
local info = Calendar.info(row.RealDate)
local day = tonumber(row.DayNum) or (info and info.day)
local page = trim(row.Page_Name)
if page ~= "" and day and day >= 1 and day <= DAYS_PER_YEAR then
local title = trim(row.Title)
if title == "" then
title = page:match("[^/]+$") or page
end
local post
if title == page then
post = "[[" .. page .. "]]"
else
post = "[[" .. page .. "|" .. title .. "]]"
end
local char
if trim(row.Author) ~= "" then
char = linkify(row.Author)
else
char = linkifyCharacters(row.Publisher)
end
names[normalizeTitle(page)] = true
local list = byDay[day]
list[#list + 1] = {
post = post,
char = char,
date = info and info.iso or trim(row.RealDate),
}
end
end
return byDay, names
end
local function cell(content, attrs)
return "<td" .. (attrs or "") .. ">" .. content .. "</td>"
end
local function rangeLabel(first, last, year)
if first == last then
return DAY_LABELS[first] .. ", Year " .. year
end
return DAY_LABELS[first] .. " to " .. DAY_LABELS[last] .. ", Year " .. year
end
-- THIS IS THE ONLY FUNCTION THAT CHANGED
local function renderSection(title, year, schedule)
local out = {}
-- Colors based on category
local headerBg = "#3a506b"
if title == "Character News" then headerBg = "#6e473b" end
-- Table Wrapper
out[#out + 1] = '<div style="overflow-x:auto; border: 1px solid #d1d5db; border-radius: 12px; margin-bottom: 2em; box-shadow: 0 4px 12px rgba(0,0,0,0.05);">'
out[#out + 1] = '<table style="width:100%; border-collapse: collapse; background: #ffffff; margin: 0;">'
-- Section Title
out[#out + 1] = '<tr><th colspan="4" style="background:' .. headerBg .. '; color:#ffffff; '
.. 'font-family:\'Georgia\', serif; font-size:1.1em; letter-spacing:0.1em; text-align:center; padding:12px; '
.. 'border-bottom:3px solid #e0a96d; text-transform:uppercase;">'
.. title .. "</th></tr>"
-- Column Headers
local head = {}
for _, col in ipairs(COLUMNS) do
head[#head + 1] = '<th style="background:#f8fafc; color:#1e293b; font-family:\'Georgia\', serif; '
.. 'padding:10px; text-align:left; border-bottom:2px solid #e2e8f0; width:' .. col[2] .. ';">' .. col[1] .. "</th>"
end
out[#out + 1] = "<tr>" .. table.concat(head) .. "</tr>"
local day = 1
while day <= DAYS_PER_YEAR do
local events = schedule[day]
local count = #events
if count > 0 then
local default = Calendar.realDate(year, day)
local dates, span = {}, {}
for i, ev in ipairs(events) do
dates[i] = (ev.date ~= "") and ev.date or default
end
local i = 1
while i <= count do
local j = i
while j < count and dates[j + 1] == dates[i] do j = j + 1 end
span[i] = j - i + 1
i = j + 1
end
for idx, ev in ipairs(events) do
local row = {}
if idx == 1 then
row[#row + 1] = cell(
DAY_LABELS[day] .. ", Year " .. year,
(count > 1 and ' rowspan="' .. count .. '"' or "") .. ' style="padding:12px 10px; border-bottom:1px solid #e2e8f0; border-right:1px solid #e2e8f0; font-weight:bold; color:#1e293b; vertical-align:top;"'
)
end
row[#row + 1] = cell(ev.post, ' style="padding:12px 10px; border-bottom:1px solid #e2e8f0; border-right:1px solid #e2e8f0; vertical-align:top;"')
row[#row + 1] = cell(ev.char, ' style="padding:12px 10px; border-bottom:1px solid #e2e8f0; border-right:1px solid #e2e8f0; vertical-align:top;"')
if span[idx] then
row[#row + 1] = cell(
dates[idx],
(span[idx] > 1 and ' rowspan="' .. span[idx] .. '"' or "") .. ' style="padding:12px 10px; border-bottom:1px solid #e2e8f0; vertical-align:top; color:#64748b; font-size:0.9em; white-space:nowrap;"'
)
end
out[#out + 1] = "<tr>" .. table.concat(row) .. "</tr>"
end
day = day + 1
else
local first = day
while day <= DAYS_PER_YEAR and #schedule[day] == 0 do
day = day + 1
end
local last = day - 1
local startDate, endDate = Calendar.realDate(year, first), Calendar.realDate(year, last)
local dateText = (first == last) and startDate or (startDate .. " to " .. endDate)
out[#out + 1] = "<tr>"
.. cell(rangeLabel(first, last, year), ' style="padding:12px 10px; border-bottom:1px solid #e2e8f0; border-right:1px solid #e2e8f0; color:#64748b; font-style:italic; font-size:0.9em;"')
.. cell(EMPTY_ROW_TEXT, ' colspan="2" style="padding:12px 10px; border-bottom:1px solid #e2e8f0; border-right:1px solid #e2e8f0; color:#94a3b8; font-style:italic;"')
.. cell(dateText, ' style="padding:12px 10px; border-bottom:1px solid #e2e8f0; color:#64748b; font-size:0.9em; white-space:nowrap;"')
.. "</tr>"
end
end
out[#out + 1] = "</table>"
out[#out + 1] = "</div>"
return table.concat(out, "\n")
end
function p.newsType(frame)
local key = trim(frame.args[1]):lower()
return TYPE_ALIASES[key] or SECTIONS.Character
end
function p.render(frame)
local own = frame.args
local parent = frame:getParent()
local inherited = parent and parent.args or {}
local function getArg(name)
local v = inherited[name]
if v == nil or trim(v) == "" then v = own[name] end
return v
end
local key = trim(getArg("Category"))
local title = SECTIONS[key]
if not title then
return errorText('unknown Category "' .. key .. '" (use Global, Domestic or Character).')
end
local year = tonumber(trim(getArg("Year")))
if not year or year < 1 or year ~= math.floor(year) then
return errorText("set <code>Year</code> to a whole number, 1 or higher.")
end
local auto, names, cargoError = {}, {}, nil
if trim(getArg("Auto")):lower() ~= "no" then
auto, names, cargoError = cargoEvents(title, year)
end
local schedule = {}
for day = 1, DAYS_PER_YEAR do
local events = {}
for _, ev in ipairs(collectManualEvents(getArg, day)) do
local target = linkTarget(ev.post)
if not (target and names[target]) then
events[#events + 1] = ev
end
end
for _, ev in ipairs(auto[day] or {}) do
events[#events + 1] = ev
end
schedule[day] = events
end
local html = renderSection(title, year, schedule)
if cargoError then
html = html .. '\n<div class="error">Could not read the Cargo table “' .. CARGO_TABLE
.. "”: " .. cargoError .. "</div>"
end
return html
end
return p