Module:Cronos/Sandbox
Module documentation
[create]
---
-- This is the module that auto-generates the [[Meta:Cronos]] monthly calendar
--
-- Thanks to this module we do not need a bot to keep the calendar
-- updated. Note that this module needs to request some MediaWiki titles.
-- This increase the "Expensive parser function count" by ~40. Anyway, the
-- global limit actually is 500.
--
-- Happy hacking!
--
-- @author [[User:Valerio Bozzolan]]
-- @creation 2019-04-13
---
local DEFAULT_CONFIG = {
-- Namespace name for the event pages
-- e.g. 'Meta' for [[Meta:Cronos/Events/2000-12-31]
['event_ns'] = 'Meta',
-- Expected prefix for every Cronos event page
-- e.g. 'Cronos/Event/' for [[Meta:Cronos/Events/2000-12-31]
['event_prefix'] = 'Cronos/Events/',
-- Timezone
-- '0': use local wiki timezone
-- '1': use UTC timezone
['utc'] = '0',
-- Week
-- '1': week start from Monday
-- '0': week start from Sunday
['start_from_monday'] = '1',
-- How much days to be displayed in the calendar
['days'] = '28',
-- How much months to shift in the past or in the future
-- '0': Display the current month
-- '1': Display the next month
-- '-1': Display the previous month
-- etc.
['month_shift'] = '0',
-- Maximum length of an Event title in bytes before truncating
['max_title_len' ] = '20',
}
-- list of all 'MediaWiki:Sunday' etc.
local MW_WEEK_NAMES = {
'Sunday',
'Monday',
'Thursday',
'Wednesday',
'Thursday',
'Friday',
'Saturday',
}
-- list of all 'MediaWiki:January' etc.
local MW_MONTH_NAMES = {
'January',
'February',
'March',
'April',
'May',
'June',
'July',
'August',
'September',
'October',
'November',
'December',
}
local p = {}
-- constants
local SECONDS_IN_DAY = 86400;
local SECONDS_IN_MONTH = SECONDS_IN_DAY * 28 -- Not really :^) it's a lazy hack
local HEADING_TEMPLATE = 'Cronos month/Head'
-- this Lua pattern distinguish single events, and the known template arguments
local EVENT_PATTERN = '{{ *[Cc]ronos event'
local EVENT_ARG_TITLE = 'title'
local EVENT_ARG_WHEN = 'when'
--- Parse a single line of a template argument
-- @param string s Wikitext
-- @param string|nil arg Argument name
local function parse_arg( s, arg )
local pattern = '|%s*' .. arg .. '%s*=(.-)\n'
local capture = mw.ustring.match( s, pattern )
if capture ~= nil then
capture = mw.text.trim( capture )
end
return capture
end
--- Parse a single title
--
-- @param string s Wikitext
local function parse_title( s )
local pattern = '(.+)=%s*\n'
local capture = mw.ustring.match( s, pattern )
if capture ~= nil then
capture = mw.text.trim( capture, " =" )
end
return capture
end
---
-- Simple class describing a single CronosEvent
---
local CronosEvent = {}
CronosEvent.__index = CronosEvent
---
-- Create a CronosEvent
--
-- @param string title
-- @param string when
function CronosEvent:create( day, title, when )
local event = {}
setmetatable( event, CronosEvent )
event.day = day
event.title = title
event.when = when
return event
end
--- Create a CronosEvent parsing a block of wikitext
--
-- @param table day The day this event belongs to
-- @param string wikitext Wikitext that contains part of the arguments
-- @param string section_title Title of the current section
-- @return table|nil
function CronosEvent:createParsing( day, wikitext, section_title )
local title = parse_arg( wikitext, EVENT_ARG_TITLE ) or section_title
if title ~= nil then
local when = parse_arg( wikitext, EVENT_ARG_WHEN )
if when ~= nil then
return CronosEvent:create( day, title, when )
end
end
return nil
end
---
-- Generate a brief abstract of this specific event
--
function CronosEvent:renderBrief( frame )
frame = frame or mw.getCurrentFrame()
-- eventually truncate (but put the complete title inside a tooltip
local title = self.title
local title_is_pure_text = mw.ustring.find( title, '[', 1, true ) == nil
local maxlen = tonumber( self.day.config.max_title_len )
if mw.ustring.len( title ) > maxlen and title_is_pure_text then
local truncated = mw.ustring.sub( title, 1, maxlen ) .. '…'
title = tostring( mw.html.create( 'span' )
:attr( 'title', title )
:wikitext( truncated ) )
end
return frame:expandTemplate{
title = 'Cronos event brief',
args = {
self.day.yyyy,
self.day.mm,
self.day.dd,
title,
self.when,
}
}
end
---
-- Simple class collecting events
--
local CronosDay = {}
CronosDay.__index = CronosDay
---
-- Create a CronosDay table
--
-- @param string date Full date formatted in 'yyyy-mm-dd'
-- @param string y Year
-- @param string m Month 0-12
-- @param string d Day 0-31
-- @param table config Configuration
function CronosDay:create( date, config )
local day = {}
setmetatable( day, CronosDay )
local dmy = mw.text.split( date, '-', true )
day.yyyy = dmy[ 1 ]
day.mm = dmy[ 2 ]
day.dd = dmy[ 3 ]
day.date = date
day.m = tonumber( day.mm )
local timestamp = os.time{
year = day.yyyy,
month = day.m,
day = day.dd,
}
day.week = os.date( '%w', timestamp ) -- sunday is 0
day.config = config
day.title = config.event_prefix .. date
day.events = nil
return day
end
---
-- Get the wikitext of this event page
--
-- @return string|nil
function CronosDay:wikitext()
return mw.title.makeTitle( self.config.event_ns, self.title )
:getContent()
end
---
-- Parse this day page looking for events
--
function CronosDay:parse()
self.events = {}
local wikitext = self:wikitext()
local ok = wikitext ~= nil
if ok then
local sections = mw.text.split( wikitext, "\n=" )
for _, section in pairs( sections ) do
local section_title = parse_title( section )
local blocks = mw.text.split( section, EVENT_PATTERN )
for i, block in pairs( blocks ) do
local event = CronosEvent:createParsing( self, block, section_title )
if event ~= nil then
self.events[ #self.events + 1 ] = event
end
end
end
end
return ok
end
---
-- Get the CronosEvent(s) related to this day (if any)
--
-- @return table
--
function CronosDay:getEvents()
if self.events == nil then
self:parse()
end
return self.events
end
---
-- Generate a brief header of this day
--
function CronosDay:renderBrief( frame )
frame = frame or mw.getCurrentFrame()
return frame:expandTemplate{
title = 'Cronos day brief',
args = { self.yyyy, self.mm, self.dd }
}
end
---
-- Generate a list of all the events in this day
--
function CronosDay:renderEvents( frame )
local s = ''
frame = frame or mw.getCurrentFrame()
for _, event in pairs( self:getEvents() ) do
s = s .. "\n" .. event:renderBrief( frame )
end
return s
end
---
-- Generate a list of all the events in this day
--
function CronosDay:render( frame )
frame = frame or mw.getCurrentFrame()
return self:renderBrief( frame )
.. self:renderEvents( frame )
end
---
-- Get a complete configuration inheriting default options
--
-- @param table|nil config
--
function getConfig( config )
config = config or {}
for k, default in pairs( DEFAULT_CONFIG ) do
local v = config[ k ]
if v ~= nil then
v = mw.text.trim( tostring( config[ k ] ) )
if v == '' then
v = nil
end
end
config[ k ] = v or default
end
return config
end
---
-- Generate the monthly calendar (Lua API)
--
-- @param object config
--
function p._main( config )
-- localized week names
local weekNames = {}
for k, v in pairs( MW_WEEK_NAMES ) do
weekNames[ k ] = mw.message.new( v ):plain()
end
function fill_empty_week_days( week )
s = ''
while week < 7 do
s = s .. '\n|'
week = week + 1
end
return s
end
local frame = mw.getCurrentFrame()
-- inherit the default configs
config = getConfig( config )
local days = tonumber( config.days )
local start_from_monday = config.start_from_monday == '1'
-- today in unix time
local time = os.time()
-- shift the current time month forward or backward by some months
local month_shift = tonumber( config.month_shift )
time = time + month_shift * SECONDS_IN_MONTH
-- see the documentation of os.date() about UTC or local time
local format_prefix = ''
if config.utc == '1' then
format_prefix = '!'
end
-- os.date() formats to obtain a date table or a date in Y-m-d
local format_date = format_prefix .. '*t'
local format_ymd = format_prefix .. '%F'
-- today's date
local date = os.date( format_date, time )
-- the week day starts from sunday = 1
local week_day = date.wday
-- how much days since the start of this week
local days_since_start_week = week_day - 1
if start_from_monday then
days_since_start_week = days_since_start_week - 1
if days_since_start_week == 0 then
days_since_start_week = 6
end
end
-- calculate the date when this week started
local seconds_since_start_week = days_since_start_week * SECONDS_IN_DAY
local time_start_of_week = time - seconds_since_start_week
local date_start_of_week = os.date( format_date, time_start_of_week )
-- generate a list of days for this calendar
local daylist = {}
local i = 0
while i < days do
local event_time = time_start_of_week + i * SECONDS_IN_DAY
local event_ymd = os.date( format_ymd, event_time )
i = i + 1
daylist[ i ] = CronosDay:create( event_ymd, config )
end
-- heading
local s = frame:expandTemplate{ title = HEADING_TEMPLATE }
-- prepare week indexes
local week_start = 0
local week_end = 7
if start_from_monday then
week_end = week_end + 1
week_start = week_start + 1
end
-- start of the table
s = s .. '\n{| class="wikitable cronos-month-table"'
-- generate the table body
local i = 0
local lastMonth = -1
while i < days do
-- print a newline?
local newline = false
local week = i % 7
local day = daylist[ i + 1 ]
-- print week labels
if day.m ~= lastMonth then
lastMonth = day.m
local pre_days = 7 - week
if pre_days > 0 then
s = s .. fill_empty_week_days( 7 - week )
s = s .. "\n|-\n"
end
-- add row with month name
local monthName = mw.message.new( MW_MONTH_NAMES[ lastMonth ] ):plain()
s = s .. "\n|-\n"
s = s .. '\n!colspan="7"|' .. monthName
s = s .. "\n|-\n"
-- add row with week names
for k, v in pairs( weekNames ) do
s = s .. "\n!" .. v
end
if week > 0 then
s = s .. fill_empty_week_days( week )
s = s .. "\n|-"
end
end
-- new row
if week == 0 then
s = s .. "\n|-"
end
-- render day box
s = s .. "\n|" .. day:render( frame )
-- next day
i = i + 1
end
return s .. "\n|}"
end
---
-- Generate the monthly calendar (Wikitext API)
--
function p.main( frame )
local args = frame:getParent().args or frame.args
return p._main( args )
end
---
-- Get a single CronosDay object (Lua API)
--
-- You can use this in your Lua console e.g.:
-- =mw.logObject( p._test( '2019-03-25' ) )
--
-- @param string date Date formatted as 'yyyy-mm-dd'
-- @param table config Configuration
--
function p._day( date, config )
local config = getConfig( config )
local day = CronosDay:create( date, config )
day:parse()
return day
end
---
-- Print the events of a single CronosDay (Wikitext API)
--
-- You can use this in your Lua console to do tests e.g.:
-- =mw.logObject( p._day( '2019-03-25' ):getEvents() )
--
-- @param object config
--
function p.day( frame )
local args = frame:getParent().args or frame.args
local date = frame.args[ 1 ]
local day = p._day( date, frame.args )
return day:renderEvents( frame )
end
return p